Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# RSR-template-repo - Editor Configuration
# gossamer - Editor Configuration
# https://editorconfig.org

root = true
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/abi-typecheck.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .github/workflows/e2e.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .machine_readable/ai/AI.a2ml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .machine_readable/bot_directives/methodology.a2ml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .well-known/groove/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"groove_version": "1",
"service_id": "gossamer",
"service_version": "0.1.0",
"service_version": "0.3.1",
"capabilities": {
"panel-ui": {
"type": "panel-ui",
Expand Down
28 changes: 28 additions & 0 deletions src/interface/ffi/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,34 @@ pub fn build(b: *std.Build) void {
const display_test_step = b.step("test-display", "Run Gossamer display integration tests (requires X11/Wayland/Xvfb)");
display_test_step.dependOn(&run_display_tests.step);

// --- FFI/ABI integration tests (headless; no display server) ---
// test/integration_test.zig exercises the exported C API surface (result
// codes, capability lifecycle, IPC, groove, filesystem, ssg, plugin) against
// the Idris2 ABI. It imports the `gossamer` module (main.zig re-exports the
// submodules) rather than ../src/*.zig, which Zig 0.15 forbids from a
// test/-rooted module. GTK/WebKit dev libs are needed to compile main.zig,
// but no display server is required to run.
// zig build test-integration
const integration_test_module = b.createModule(.{
.root_source_file = b.path("test/integration_test.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "gossamer", .module = gossamer_module },
},
});

linkPlatformLibs(integration_test_module, os, abi);

const integration_tests = b.addTest(.{
.root_module = integration_test_module,
});

const run_integration_tests = b.addRunArtifact(integration_tests);
const integration_test_step = b.step("test-integration", "Run Gossamer FFI/ABI integration tests (headless)");
integration_test_step.dependOn(&run_integration_tests.step);

// --- Android component host logic tests (host-runnable; pure Zig, no NDK) ---
// The JNI binding and the Service/Receiver/Widget hosts are pure Zig, so
// their registry/dispatch/JSON/directive logic runs on the host via
Expand Down
4 changes: 2 additions & 2 deletions src/interface/ffi/src/clipboard.zig
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const backend = switch (builtin.os.tag) {
/// Null-safety: returns -1 (invalid_param) if buf is null or buf_len is 0.
///
/// Matches ABI: Gossamer.ABI.Types.ResourceKind.Clipboard (kind = 3)
export fn gossamer_clipboard_read(buf: ?[*]u8, buf_len: usize) callconv(.c) c_int {
pub export fn gossamer_clipboard_read(buf: ?[*]u8, buf_len: usize) callconv(.c) c_int {
if (buf == null or buf_len == 0) {
setError("Clipboard read: null buffer or zero length");
return -1;
Expand All @@ -167,7 +167,7 @@ export fn gossamer_clipboard_read(buf: ?[*]u8, buf_len: usize) callconv(.c) c_in
/// Null-safety: returns invalid_param if text is null.
///
/// Matches ABI: Gossamer.ABI.Types.ResourceKind.Clipboard (kind = 3)
export fn gossamer_clipboard_write(text: ?[*:0]const u8) callconv(.c) c_int {
pub export fn gossamer_clipboard_write(text: ?[*:0]const u8) callconv(.c) c_int {
if (text == null) {
setError("Clipboard write: null text pointer");
return @intFromEnum(Result.invalid_param);
Expand Down
6 changes: 3 additions & 3 deletions src/interface/ffi/src/csp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const c = @cImport({
///
/// Thread safety: if called from a non-GTK thread, uses g_idle_add to marshal
/// the JS evaluation onto the GTK main thread.
export fn gossamer_set_csp(handle_ptr: u64, csp: [*:0]const u8) main.Result {
pub export fn gossamer_set_csp(handle_ptr: u64, csp: [*:0]const u8) main.Result {
main.clearError();
const handle = main.ptrFromU64(handle_ptr) orelse {
main.setError("Null webview handle");
Expand Down Expand Up @@ -149,7 +149,7 @@ const EmitContext = struct {
/// window.__gossamer_on("file_changed", function(data) {
/// console.log("Changed:", data.path);
/// });
export fn gossamer_emit(
pub export fn gossamer_emit(
handle_ptr: u64,
event_name: [*:0]const u8,
payload_json: [*:0]const u8,
Expand Down Expand Up @@ -233,7 +233,7 @@ export fn gossamer_emit(
/// Thread safety: uses g_idle_add exactly like gossamer_emit. Safe from any thread.
///
/// Matches: Gossamer.ABI.Foreign.prim__emitBinary
export fn gossamer_emit_binary(
pub export fn gossamer_emit_binary(
handle_ptr: u64,
event_name: [*:0]const u8,
data: [*]const u8,
Expand Down
10 changes: 5 additions & 5 deletions src/interface/ffi/src/dialog.zig
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ fn runFileChooserDialog(
/// gossamer_dialog_free_path), or 0 if the user cancelled.
///
/// Matches: Gossamer.ABI.Foreign.prim__dialogOpen
export fn gossamer_dialog_open(title: [*:0]const u8, filters: [*:0]const u8) u64 {
pub export fn gossamer_dialog_open(title: [*:0]const u8, filters: [*:0]const u8) u64 {
clearError();
return runFileChooserDialog(
title,
Expand All @@ -232,7 +232,7 @@ export fn gossamer_dialog_open(title: [*:0]const u8, filters: [*:0]const u8) u64
/// gossamer_dialog_free_path), or 0 if the user cancelled.
///
/// Matches: Gossamer.ABI.Foreign.prim__dialogSave
export fn gossamer_dialog_save(title: [*:0]const u8, filters: [*:0]const u8) u64 {
pub export fn gossamer_dialog_save(title: [*:0]const u8, filters: [*:0]const u8) u64 {
clearError();
return runFileChooserDialog(
title,
Expand All @@ -252,7 +252,7 @@ export fn gossamer_dialog_save(title: [*:0]const u8, filters: [*:0]const u8) u64
/// gossamer_dialog_free_path), or 0 if the user cancelled.
///
/// Matches: Gossamer.ABI.Foreign.prim__dialogOpenDirectory
export fn gossamer_dialog_open_directory(title: [*:0]const u8) u64 {
pub export fn gossamer_dialog_open_directory(title: [*:0]const u8) u64 {
clearError();
return runFileChooserDialog(
title,
Expand All @@ -274,7 +274,7 @@ export fn gossamer_dialog_open_directory(title: [*:0]const u8) u64 {
/// Returns 0 if the user cancelled or an error occurred.
///
/// Matches: Gossamer.ABI.Foreign.prim__dialogOpenMultiple
export fn gossamer_dialog_open_multiple(title: [*:0]const u8, filters: [*:0]const u8) u64 {
pub export fn gossamer_dialog_open_multiple(title: [*:0]const u8, filters: [*:0]const u8) u64 {
clearError();
if (!ensureGtkInit()) {
setError("GTK not initialised — cannot show file dialog");
Expand Down Expand Up @@ -376,7 +376,7 @@ export fn gossamer_dialog_open_multiple(title: [*:0]const u8, filters: [*:0]cons
/// to match — using the wrong free will corrupt the heap.
///
/// Matches: Gossamer.ABI.Foreign.prim__dialogFreePath
export fn gossamer_dialog_free_path(path_ptr: u64) void {
pub export fn gossamer_dialog_free_path(path_ptr: u64) void {
if (path_ptr == 0) return;
std.c.free(@ptrFromInt(@as(usize, @intCast(path_ptr))));
}
Expand Down
14 changes: 7 additions & 7 deletions src/interface/ffi/src/filesystem.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const main = @import("main.zig");
/// Validates the capability token is active and of type FileSystem (kind=0).
///
/// Returns null on error (check gossamer_last_error).
export fn gossamer_fs_read_text(
pub export fn gossamer_fs_read_text(
path: [*:0]const u8,
cap_token: u64,
) ?[*:0]u8 {
Expand Down Expand Up @@ -68,7 +68,7 @@ export fn gossamer_fs_read_text(
/// Validates the capability token is active and of type FileSystem (kind=0).
///
/// Returns Result (0=ok, 1=error, 10=capability_denied).
export fn gossamer_fs_write_text(
pub export fn gossamer_fs_write_text(
path: [*:0]const u8,
contents: [*:0]const u8,
cap_token: u64,
Expand Down Expand Up @@ -106,7 +106,7 @@ export fn gossamer_fs_write_text(
/// Validates the capability token is active and of type FileSystem (kind=0).
///
/// Returns 1 (exists), 0 (does not exist), or 0xFFFFFFFF on error.
export fn gossamer_fs_exists(
pub export fn gossamer_fs_exists(
path: [*:0]const u8,
cap_token: u64,
) u32 {
Expand All @@ -130,7 +130,7 @@ export fn gossamer_fs_exists(
/// Returns a null-terminated C string (caller frees), or null on error.
///
/// Validates the capability token is active and of type FileSystem (kind=0).
export fn gossamer_fs_list_dir(
pub export fn gossamer_fs_list_dir(
path: [*:0]const u8,
cap_token: u64,
) ?[*:0]u8 {
Expand Down Expand Up @@ -182,7 +182,7 @@ export fn gossamer_fs_list_dir(
/// Remove a file. Directories are not removed (use gossamer_fs_remove_dir).
///
/// Validates the capability token is active and of type FileSystem (kind=0).
export fn gossamer_fs_remove(
pub export fn gossamer_fs_remove(
path: [*:0]const u8,
cap_token: u64,
) main.Result {
Expand All @@ -207,7 +207,7 @@ export fn gossamer_fs_remove(
/// Succeeds when the directory already exists.
///
/// Validates the capability token is active and of type FileSystem (kind=0).
export fn gossamer_fs_mkdir_p(
pub export fn gossamer_fs_mkdir_p(
path: [*:0]const u8,
cap_token: u64,
) main.Result {
Expand Down Expand Up @@ -244,7 +244,7 @@ export fn gossamer_fs_mkdir_p(
/// gossamer_fs_mkdir_p first if needed.
///
/// Validates the capability token is active and of type FileSystem (kind=0).
export fn gossamer_fs_copy_file(
pub export fn gossamer_fs_copy_file(
src: [*:0]const u8,
dst: [*:0]const u8,
cap_token: u64,
Expand Down
18 changes: 9 additions & 9 deletions src/interface/ffi/src/groove.zig
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ fn hasCapability(idx: usize, name: []const u8) bool {

/// Discover all groove targets by probing well-known ports.
/// Returns the number of successfully connected grooves.
export fn gossamer_groove_discover() callconv(.c) u32 {
pub export fn gossamer_groove_discover() callconv(.c) u32 {
var count: u32 = 0;
for (0..TARGET_COUNT) |i| {
probeTarget(i);
Expand All @@ -316,15 +316,15 @@ export fn gossamer_groove_discover() callconv(.c) u32 {
}

/// Get the status of a specific groove target.
export fn gossamer_groove_status(target_id: u32) callconv(.c) u32 {
pub export fn gossamer_groove_status(target_id: u32) callconv(.c) u32 {
if (target_id >= TARGET_COUNT) return 0;
return @intFromEnum(grooves[target_id].status);
}

/// Get the manifest JSON for a connected groove target.
/// Returns a pointer to a null-terminated string (thread-local buffer).
/// Returns pointer to empty string if not connected.
export fn gossamer_groove_manifest(target_id: u32) callconv(.c) [*:0]const u8 {
pub export fn gossamer_groove_manifest(target_id: u32) callconv(.c) [*:0]const u8 {
if (target_id >= TARGET_COUNT) return "";
const state = &grooves[target_id];
if (state.status == .not_found or state.manifest_len == 0) return "";
Expand All @@ -337,7 +337,7 @@ export fn gossamer_groove_manifest(target_id: u32) callconv(.c) [*:0]const u8 {

/// Find which groove target provides a given capability.
/// Returns the target index, or 0xFFFFFFFF if none.
export fn gossamer_groove_find_capability(cap_name: [*:0]const u8) callconv(.c) u32 {
pub export fn gossamer_groove_find_capability(cap_name: [*:0]const u8) callconv(.c) u32 {
// Determine string length.
var len: usize = 0;
while (cap_name[len] != 0 and len < MAX_CAP_NAME) : (len += 1) {}
Expand All @@ -357,7 +357,7 @@ export fn gossamer_groove_find_capability(cap_name: [*:0]const u8) callconv(.c)
/// Check if two groove targets are compatible for composition.
/// Two targets are compatible if each offers what the other consumes.
/// Returns 1 if compatible, 0 if not.
export fn gossamer_groove_check_compat(target_a: u32, target_b: u32) callconv(.c) u32 {
pub export fn gossamer_groove_check_compat(target_a: u32, target_b: u32) callconv(.c) u32 {
if (target_a >= TARGET_COUNT or target_b >= TARGET_COUNT) return 0;
const a = &grooves[target_a];
const b = &grooves[target_b];
Expand Down Expand Up @@ -424,7 +424,7 @@ pub export fn gossamer_groove_send(target_id: u32, msg_ptr: [*:0]const u8) callc
/// Receive a pending message from a grooved service.
/// Uses HTTP GET to /.well-known/groove/recv on the target port.
/// Returns a pointer to the response body (thread-local buffer).
export fn gossamer_groove_recv(target_id: u32) callconv(.c) [*:0]const u8 {
pub export fn gossamer_groove_recv(target_id: u32) callconv(.c) [*:0]const u8 {
if (target_id >= TARGET_COUNT) return "";
if (grooves[target_id].status == .not_found) return "";

Expand Down Expand Up @@ -459,7 +459,7 @@ export fn gossamer_groove_recv(target_id: u32) callconv(.c) [*:0]const u8 {

/// Get a JSON summary of all groove connections.
/// Format: [{"id":0,"service":"burble","status":2,"port":6473,"caps":3}, ...]
export fn gossamer_groove_summary() callconv(.c) [*:0]const u8 {
pub export fn gossamer_groove_summary() callconv(.c) [*:0]const u8 {
var pos: usize = 0;
out_buf[pos] = '[';
pos += 1;
Expand Down Expand Up @@ -490,13 +490,13 @@ export fn gossamer_groove_summary() callconv(.c) [*:0]const u8 {
}

/// Disconnect a specific groove target.
export fn gossamer_groove_disconnect(target_id: u32) callconv(.c) void {
pub export fn gossamer_groove_disconnect(target_id: u32) callconv(.c) void {
if (target_id >= TARGET_COUNT) return;
grooves[target_id] = .{};
}

/// Disconnect all groove targets.
export fn gossamer_groove_disconnect_all() callconv(.c) void {
pub export fn gossamer_groove_disconnect_all() callconv(.c) void {
for (&grooves) |*g| {
g.* = .{};
}
Expand Down
Loading
Loading