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
10 changes: 10 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,13 @@ jobs:

- name: Build AppImage
run: nix develop -c zig build -Dappimage -Doptimize=ReleaseSafe

- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: spacecap-linux-${{ github.event.pull_request.number }}
path: |
zig-out/linux/spacecap
zig-out/linux/spacecap-linux-x86_64.AppImage
retention-days: 7
if-no-files-found: error
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .spacecap,
.version = "0.4.0",
.version = "0.5.0",
.fingerprint = 0xdc2511c12abe6e80,
.minimum_zig_version = "0.16.0",
.dependencies = .{
Expand Down
2 changes: 1 addition & 1 deletion build/build_app_image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ rm -f zig-out/linux/spacecap-linux-x86_64.AppImage
LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" linuxdeploy \
--appdir AppDir \
--executable zig-out/linux/spacecap \
--desktop-file packaging/linux/spacecap.desktop \
--desktop-file packaging/linux/org.spacecap.spacecap.desktop \
--icon-file packaging/spacecap.svg \
--exclude-library libvulkan.so.1 \
--exclude-library libxkbcommon.so.0 \
Expand Down
10 changes: 9 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@
system: let
pkgs = import nixpkgs {
inherit system;
overlays = [];
overlays = [
(_final: prev: {
# NOTE: This can be removed with: https://github.com/mgerb/spacecap/issues/144
# GTK's optional file-search integration pulls TinySPARQL into the
# AppImage, which then tries to link sqlite3. This can be avoided
# by disabling it here.
gtk3 = prev.gtk3.override {trackerSupport = false;};
})
];
};
zigpkgs = zig.packages.${system};
in {
Expand Down
14 changes: 10 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
set -eu

APP_NAME="spacecap"
APP_ID="org.spacecap.spacecap"
ARTIFACT_NAME="spacecap-linux-x86_64.AppImage"
CHECKSUMS_NAME="SHA256SUMS.txt"
RELEASES_URL="https://github.com/mgerb/spacecap/releases"
RAW_URL="https://raw.githubusercontent.com/mgerb/spacecap/main"
DESKTOP_URL="$RAW_URL/packaging/linux/spacecap.desktop"
DESKTOP_URL="$RAW_URL/packaging/linux/$APP_ID.desktop"
ICON_URL="$RAW_URL/packaging/spacecap.svg"
CHANNEL="nightly"
MODE="install"
Expand Down Expand Up @@ -72,7 +73,9 @@ INSTALL_DIR="$HOME/.local/bin"
INSTALL_PATH="$INSTALL_DIR/$APP_NAME"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}"
DESKTOP_DIR="$DATA_DIR/applications"
DESKTOP_PATH="$DESKTOP_DIR/$APP_NAME.desktop"
DESKTOP_PATH="$DESKTOP_DIR/$APP_ID.desktop"
# NOTE: Remove this eventually.
LEGACY_DESKTOP_PATH="$DESKTOP_DIR/$APP_NAME.desktop"
ICON_DIR="$DATA_DIR/icons/hicolor/scalable/apps"
ICON_PATH="$ICON_DIR/$APP_NAME.svg"
WAS_INSTALLED=0
Expand All @@ -82,7 +85,7 @@ fi

if [ "$MODE" = "uninstall" ]; then
REMOVED=0
for path in "$INSTALL_PATH" "$DESKTOP_PATH" "$ICON_PATH"; do
for path in "$INSTALL_PATH" "$DESKTOP_PATH" "$LEGACY_DESKTOP_PATH" "$ICON_PATH"; do
if [ -e "$path" ] || [ -L "$path" ]; then
rm -f "$path"
echo "Removed $path."
Expand Down Expand Up @@ -121,7 +124,7 @@ TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
APPIMAGE_TMP="$TMP_DIR/$ARTIFACT_NAME"
CHECKSUMS_TMP="$TMP_DIR/$CHECKSUMS_NAME"
DESKTOP_TMP="$TMP_DIR/$APP_NAME.desktop"
DESKTOP_TMP="$TMP_DIR/$APP_ID.desktop"
ICON_TMP="$TMP_DIR/$APP_NAME.svg"

echo "Downloading $APP_NAME ($CHANNEL) from $DOWNLOAD_URL."
Expand All @@ -143,6 +146,9 @@ mkdir -p "$INSTALL_DIR" "$DESKTOP_DIR" "$ICON_DIR"
mv "$APPIMAGE_TMP" "$INSTALL_PATH"
mv "$DESKTOP_TMP" "$DESKTOP_PATH"
mv "$ICON_TMP" "$ICON_PATH"
if [ "$LEGACY_DESKTOP_PATH" != "$DESKTOP_PATH" ]; then
rm -f "$LEGACY_DESKTOP_PATH"
fi

VERSION="$("$INSTALL_PATH" --version)"
if [ "$WAS_INSTALLED" -eq 1 ]; then
Expand Down
88 changes: 88 additions & 0 deletions src/app_registration/linux/xdg_desktop_portal_app_registration.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const std = @import("std");
const app_identity = @import("../../common/linux/app_identity.zig");
const XdgDesktopPortal = @import("../../common/linux/xdg_desktop_portal.zig");
const c = @import("libportal");

const log = std.log.scoped(.xdg_desktop_portal_app_registration);

fn variant_type(comptime signature: [:0]const u8) *const c.GVariantType {
return @ptrCast(signature.ptr);
}

fn is_registry_unavailable(err: *c.GError) bool {
if (err.domain != c.g_dbus_error_quark()) return false;

return err.code == c.G_DBUS_ERROR_UNKNOWN_METHOD or
err.code == c.G_DBUS_ERROR_UNKNOWN_INTERFACE or
err.code == c.G_DBUS_ERROR_UNKNOWN_OBJECT or
err.code == c.G_DBUS_ERROR_SERVICE_UNKNOWN or
err.code == c.G_DBUS_ERROR_NAME_HAS_NO_OWNER;
}

/// Register the app ID before any other dbus connections start up.
/// The dbus connection remains open until the app closes.
pub const XdgDesktopPortalAppRegistration = struct {
const Self = @This();

dbus: *c.GDBusConnection,

pub fn init() !Self {
var g_error: ?*c.GError = null;
defer if (g_error) |err| c.g_error_free(err);

const dbus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_error) orelse {
if (g_error) |err| {
log.err("[init] failed to get session bus: {s}", .{err.message.?});
}
return error.Dbus;
};
errdefer c.g_object_unref(dbus);

register(dbus);

return .{ .dbus = dbus };
}

pub fn deinit(self: *Self) void {
c.g_object_unref(self.dbus);
}

fn register(dbus: *c.GDBusConnection) void {
var options: c.GVariantBuilder = undefined;
c.g_variant_builder_init(&options, variant_type("a{sv}"));
const payload = c.g_variant_new(
"(s@a{sv})",
app_identity.APP_ID.ptr,
c.g_variant_builder_end(&options),
).?;

var g_error: ?*c.GError = null;
defer if (g_error) |err| c.g_error_free(err);

const result = c.g_dbus_connection_call_sync(
dbus,
XdgDesktopPortal.DBUS_DESTINATION.ptr,
XdgDesktopPortal.DBUS_OBJECT_PATH.ptr,
XdgDesktopPortal.REGISTRY_INTERFACE.ptr,
XdgDesktopPortal.REGISTER_METHOD.ptr,
payload,
variant_type("()"),
c.G_DBUS_CALL_FLAGS_NONE,
-1,
null,
&g_error,
);
defer if (result) |value| c.g_variant_unref(value);

if (g_error) |err| {
if (is_registry_unavailable(err)) {
log.info("[register] host portal registry is unavailable: {s}", .{err.message.?});
} else {
log.err("[register] failed to register app id '{s}': {s}", .{ app_identity.APP_ID, err.message.? });
}
return;
}

log.info("[register] registered app id '{s}'", .{app_identity.APP_ID});
}
};
14 changes: 14 additions & 0 deletions src/app_registration/platform_app_registration.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const Util = @import("../util.zig");

/// Any type of platform specific app init logic should go in here.
/// e.g. On Linux, register the app name over dbus.
pub const PlatformAppRegistration = if (Util.is_linux())
@import("./linux/xdg_desktop_portal_app_registration.zig").XdgDesktopPortalAppRegistration
else
struct {
pub fn init() !@This() {
return .{};
}

pub fn deinit(_: *@This()) void {}
};
2 changes: 2 additions & 0 deletions src/common/linux/app_identity.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub const APP_NAME: [:0]const u8 = "Spacecap";
pub const APP_ID: [:0]const u8 = "org.spacecap.spacecap";
3 changes: 2 additions & 1 deletion src/common/linux/token_manager.zig
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const std = @import("std");
const XdgDesktopPortal = @import("./xdg_desktop_portal.zig");

/// Caller owns the memory
pub fn get_request_path(allocator: std.mem.Allocator, unique_name: [:0]const u8, token: [:0]const u8) std.mem.Allocator.Error![:0]const u8 {
// Generate the path
const path: [:0]u8 = try std.fmt.allocPrintSentinel(
allocator,
"/org/freedesktop/portal/desktop/request/{s}/{s}",
XdgDesktopPortal.DBUS_OBJECT_PATH ++ "/request/{s}/{s}",
.{ unique_name, token },
0,
);
Expand Down
23 changes: 23 additions & 0 deletions src/common/linux/xdg_desktop_portal.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! dbus constants

pub const DBUS_DESTINATION: [:0]const u8 = "org.freedesktop.portal.Desktop";
pub const DBUS_OBJECT_PATH: [:0]const u8 = "/org/freedesktop/portal/desktop";

// Interface
pub const FILE_CHOOSER_INTERFACE: [:0]const u8 = "org.freedesktop.portal.FileChooser";
pub const GLOBAL_SHORTCUTS_INTERFACE: [:0]const u8 = "org.freedesktop.portal.GlobalShortcuts";
pub const REGISTRY_INTERFACE: [:0]const u8 = "org.freedesktop.host.portal.Registry";
pub const REQUEST_INTERFACE: [:0]const u8 = "org.freedesktop.portal.Request";
pub const SESSION_INTERFACE: [:0]const u8 = "org.freedesktop.portal.Session";

// Method
pub const BIND_SHORTCUTS_METHOD: [:0]const u8 = "BindShortcuts";
pub const CLOSE_METHOD: [:0]const u8 = "Close";
pub const CONFIGURE_SHORTCUTS_METHOD: [:0]const u8 = "ConfigureShortcuts";
pub const CREATE_SESSION_METHOD: [:0]const u8 = "CreateSession";
pub const OPEN_FILE_METHOD: [:0]const u8 = "OpenFile";
pub const REGISTER_METHOD: [:0]const u8 = "Register";

// Signal
pub const ACTIVATED_SIGNAL: [:0]const u8 = "Activated";
pub const RESPONSE_SIGNAL: [:0]const u8 = "Response";
24 changes: 9 additions & 15 deletions src/file_picker/linux/xdg_desktop_portal_file_picker.zig
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const TokenManager = @import("../../common/linux/token_manager.zig");
const XdgDesktopPortal = @import("../../common/linux/xdg_desktop_portal.zig");
const FilePicker = @import("../file_picker.zig").FilePicker;
const FilePickerError = @import("../file_picker.zig").FilePickerError;

const c = @import("libportal");

const log = std.log.scoped(.xdg_desktop_portal_file_picker);

const DBUS_DESTINATION: [:0]const u8 = "org.freedesktop.portal.Desktop";
const DBUS_OBJECT_PATH: [:0]const u8 = "/org/freedesktop/portal/desktop";
const FILE_CHOOSER_INTERFACE: [:0]const u8 = "org.freedesktop.portal.FileChooser";
const REQUEST_INTERFACE: [:0]const u8 = "org.freedesktop.portal.Request";
const OPEN_FILE_METHOD: [:0]const u8 = "OpenFile";
const RESPONSE_SIGNAL: [:0]const u8 = "Response";

fn variant_type(comptime signature: [:0]const u8) *const c.GVariantType {
return @ptrCast(signature.ptr);
}
Expand Down Expand Up @@ -145,8 +139,8 @@ pub const XdgDesktopPortalFilePicker = struct {
var subscription_id = c.g_dbus_connection_signal_subscribe(
self.dbus,
null,
REQUEST_INTERFACE.ptr,
RESPONSE_SIGNAL.ptr,
XdgDesktopPortal.REQUEST_INTERFACE.ptr,
XdgDesktopPortal.RESPONSE_SIGNAL.ptr,
request_path.ptr,
null,
c.G_DBUS_SIGNAL_FLAGS_NONE,
Expand All @@ -164,10 +158,10 @@ pub const XdgDesktopPortalFilePicker = struct {
var err: ?*c.GError = null;
const request_handle = c.g_dbus_connection_call_sync(
self.dbus,
DBUS_DESTINATION.ptr,
DBUS_OBJECT_PATH.ptr,
FILE_CHOOSER_INTERFACE.ptr,
OPEN_FILE_METHOD.ptr,
XdgDesktopPortal.DBUS_DESTINATION.ptr,
XdgDesktopPortal.DBUS_OBJECT_PATH.ptr,
XdgDesktopPortal.FILE_CHOOSER_INTERFACE.ptr,
XdgDesktopPortal.OPEN_FILE_METHOD.ptr,
payload,
null,
c.G_DBUS_CALL_FLAGS_NONE,
Expand Down Expand Up @@ -205,8 +199,8 @@ pub const XdgDesktopPortalFilePicker = struct {
subscription_id = c.g_dbus_connection_signal_subscribe(
self.dbus,
null,
REQUEST_INTERFACE.ptr,
RESPONSE_SIGNAL.ptr,
XdgDesktopPortal.REQUEST_INTERFACE.ptr,
XdgDesktopPortal.RESPONSE_SIGNAL.ptr,
actual_request_path,
null,
c.G_DBUS_SIGNAL_FLAGS_NONE,
Expand Down
Loading
Loading