From e6efd5e731396b135200c9a78fe7e71435a7b0e3 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Thu, 28 Aug 2025 18:38:24 +0200 Subject: [PATCH 1/2] clan-app: display runtime icon correctly in process overview --- pkgs/clan-app/clan_app/app.py | 1 + .../clan_app/deps/webview/_webview_ffi.py | 13 ++++++ .../clan-app/clan_app/deps/webview/webview.py | 46 ++++++++++++++++++- pkgs/clan-app/webview-lib/default.nix | 13 +----- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/pkgs/clan-app/clan_app/app.py b/pkgs/clan-app/clan_app/app.py index d4a2d8bc9..1da360cbf 100644 --- a/pkgs/clan-app/clan_app/app.py +++ b/pkgs/clan-app/clan_app/app.py @@ -109,6 +109,7 @@ def app_run(app_opts: ClanAppOptions) -> int: title="Clan App", size=Size(1280, 1024, SizeHint.NONE), shared_threads=shared_threads, + app_id="org.clan.app", ) API.overwrite_fn(get_system_file) diff --git a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py index 1c303d447..14578d16a 100644 --- a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py +++ b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py @@ -5,6 +5,11 @@ import platform from ctypes import CFUNCTYPE, c_char_p, c_int, c_void_p from pathlib import Path +# Native handle kinds +WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW = 0 +WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET = 1 +WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER = 2 + def _encode_c_string(s: str) -> bytes: return s.encode("utf-8") @@ -72,6 +77,10 @@ class _WebviewLibrary: self.webview_create.argtypes = [c_int, c_void_p] self.webview_create.restype = c_void_p + self.webview_create_with_app_id = self.lib.webview_create_with_app_id + self.webview_create_with_app_id.argtypes = [c_int, c_void_p, c_char_p] + self.webview_create_with_app_id.restype = c_void_p + self.webview_destroy = self.lib.webview_destroy self.webview_destroy.argtypes = [c_void_p] @@ -105,6 +114,10 @@ class _WebviewLibrary: self.webview_return = self.lib.webview_return self.webview_return.argtypes = [c_void_p, c_char_p, c_int, c_char_p] + self.webview_get_native_handle = self.lib.webview_get_native_handle + self.webview_get_native_handle.argtypes = [c_void_p, c_int] + self.webview_get_native_handle.restype = c_void_p + self.binding_callback_t = CFUNCTYPE(None, c_char_p, c_char_p, c_void_p) self.CFUNCTYPE = CFUNCTYPE diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index 6d648b1ae..96f27215c 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -1,6 +1,7 @@ import functools import json import logging +import platform import threading from collections.abc import Callable from dataclasses import dataclass, field @@ -11,7 +12,10 @@ from typing import TYPE_CHECKING, Any from clan_lib.api import MethodRegistry, message_queue from clan_lib.api.tasks import WebThread -from ._webview_ffi import _encode_c_string, _webview_lib +from ._webview_ffi import ( + _encode_c_string, + _webview_lib, +) from .webview_bridge import WebviewBridge if TYPE_CHECKING: @@ -32,6 +36,21 @@ class FuncStatus(IntEnum): FAILURE = 1 +class NativeHandleKind(IntEnum): + # Top-level window. @c GtkWindow pointer (GTK), @c NSWindow pointer (Cocoa) + # or @c HWND (Win32) + UI_WINDOW = 0 + + # Browser widget. @c GtkWidget pointer (GTK), @c NSView pointer (Cocoa) or + # @c HWND (Win32). + UI_WIDGET = 1 + + # Browser controller. @c WebKitWebView pointer (WebKitGTK), @c WKWebView + # pointer (Cocoa/WebKit) or @c ICoreWebView2Controller pointer + # (Win32/WebView2). + BROWSER_CONTROLLER = 2 + + @dataclass(frozen=True) class Size: width: int @@ -46,6 +65,7 @@ class Webview: size: Size | None = None window: int | None = None shared_threads: dict[str, WebThread] | None = None + app_id: str | None = None # initialized later _bridge: WebviewBridge | None = None @@ -56,7 +76,14 @@ class Webview: def _create_handle(self) -> None: # Initialize the webview handle with_debugger = True - handle = _webview_lib.webview_create(int(with_debugger), self.window) + + # Use webview_create_with_app_id only on Linux if app_id is provided + if self.app_id and platform.system() == "Linux": + handle = _webview_lib.webview_create_with_app_id( + int(with_debugger), self.window, _encode_c_string(self.app_id) + ) + else: + handle = _webview_lib.webview_create(int(with_debugger), self.window) callbacks: dict[str, Callable[..., Any]] = {} # Since we can't use object.__setattr__, we'll initialize differently @@ -217,6 +244,21 @@ class Webview: self._callbacks[name] = c_callback _webview_lib.webview_bind(self.handle, _encode_c_string(name), c_callback, None) + def get_native_handle( + self, kind: NativeHandleKind = NativeHandleKind.UI_WINDOW + ) -> int | None: + """Get the native handle (platform-dependent). + + Args: + kind: Handle kind - NativeHandleKind enum value + + Returns: + Native handle as integer, or None if failed + + """ + handle = _webview_lib.webview_get_native_handle(self.handle, kind.value) + return handle if handle else None + def unbind(self, name: str) -> None: if name in self._callbacks: _webview_lib.webview_unbind(self.handle, _encode_c_string(name)) diff --git a/pkgs/clan-app/webview-lib/default.nix b/pkgs/clan-app/webview-lib/default.nix index 812a70d22..8220770f4 100644 --- a/pkgs/clan-app/webview-lib/default.nix +++ b/pkgs/clan-app/webview-lib/default.nix @@ -24,19 +24,10 @@ clangStdenv.mkDerivation { domain = "git.clan.lol"; owner = "clan"; repo = "webview"; - rev = "ef481aca8e531f6677258ca911c61aaaf71d2214"; - hash = "sha256-KF9ESpo40z6VXyYsZCLWJAIh0RFe1Zy/Qw4k7cTpoYU="; + rev = "0ba936b247106219c363a855763ef06b2535363e"; + hash = "sha256-pyH5v7+ytkLOlXpW5WrvN0bqPCOnKnna8+1C0DANoDQ="; }; - # @Mic92: Where is this revision coming from? I can't see it in any of the branches. - # I removed the icon python code for now - # src = pkgs.fetchFromGitHub { - # owner = "clan-lol"; - # repo = "webview"; - # rev = "7d24f0192765b7e08f2d712fae90c046d08f318e"; - # hash = "sha256-yokVI9tFiEEU5M/S2xAeJOghqqiCvTelLo8WLKQZsSY="; - # }; - outputs = [ "out" "dev" From 8ae43ff9a01118afe93df9d07763bca653778fd2 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 1 Sep 2025 16:16:52 +0200 Subject: [PATCH 2/2] clan-app: display runtime icon on macOS too --- pkgs/clan-app/default.nix | 114 +++++++++++++++++++++++++- pkgs/clan-app/install-desktop.sh | 27 ++++-- pkgs/clan-app/macos-remote.sh | 9 ++ pkgs/clan-app/webview-lib/default.nix | 4 +- 4 files changed, 140 insertions(+), 14 deletions(-) create mode 100755 pkgs/clan-app/macos-remote.sh diff --git a/pkgs/clan-app/default.nix b/pkgs/clan-app/default.nix index 069fe0ad2..54fcb089d 100644 --- a/pkgs/clan-app/default.nix +++ b/pkgs/clan-app/default.nix @@ -11,6 +11,11 @@ gobject-introspection, gtk4, lib, + stdenv, + # macOS-specific dependencies + imagemagick, + makeWrapper, + libicns, }: let source = @@ -91,7 +96,12 @@ pythonRuntime.pkgs.buildPythonApplication { # gtk4 deps wrapGAppsHook4 ] - ++ runtimeDependencies; + ++ runtimeDependencies + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + imagemagick + makeWrapper + libicns + ]; # The necessity of setting buildInputs and propagatedBuildInputs to the # same values for your Python package within Nix largely stems from ensuring @@ -148,16 +158,113 @@ pythonRuntime.pkgs.buildPythonApplication { postInstall = '' mkdir -p $out/${pythonRuntime.sitePackages}/clan_app/.webui cp -r ${clan-app-ui}/lib/node_modules/@clan/ui/dist/* $out/${pythonRuntime.sitePackages}/clan_app/.webui - mkdir -p $out/share/icons/hicolor - cp -r ./clan_app/assets/white-favicons/* $out/share/icons/hicolor + + ${lib.optionalString (!stdenv.hostPlatform.isDarwin) '' + mkdir -p $out/share/icons/hicolor + cp -r ./clan_app/assets/white-favicons/* $out/share/icons/hicolor + ''} + + ${lib.optionalString stdenv.hostPlatform.isDarwin '' + set -eu pipefail + # Create macOS app bundle structure + mkdir -p "$out/Applications/Clan App.app/Contents/"{MacOS,Resources} + + # Create Info.plist + cat > "$out/Applications/Clan App.app/Contents/Info.plist" << 'EOF' + + + + + CFBundleDisplayName + Clan App + CFBundleExecutable + Clan App + CFBundleIconFile + clan-app.icns + CFBundleIdentifier + org.clan.app + CFBundleName + Clan App + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + CFBundleInfoDictionaryVersion + 6.0 + CFBundleURLTypes + + + CFBundleURLName + Clan Protocol + CFBundleURLSchemes + + clan + + + + + + EOF + + # Create app icon (convert PNG to ICNS using minimal approach to avoid duplicates) + # Create a temporary iconset directory structure + mkdir clan-app.iconset + + # Create a minimal iconset with only essential, non-duplicate sizes + # Each PNG file should map to a unique ICNS type + cp ./clan_app/assets/white-favicons/16x16/apps/clan-app.png clan-app.iconset/icon_16x16.png + cp ./clan_app/assets/white-favicons/128x128/apps/clan-app.png clan-app.iconset/icon_128x128.png + + # Use libicns png2icns tool to create proper ICNS file with minimal set + png2icns "$out/Applications/Clan App.app/Contents/Resources/clan-app.icns" \ + clan-app.iconset/icon_16x16.png \ + clan-app.iconset/icon_128x128.png + + # Create PkgInfo file (standard requirement for macOS apps) + echo -n "APPL????" > "$out/Applications/Clan App.app/Contents/PkgInfo" + + # Create the main executable script with proper process name + cat > "$out/Applications/Clan App.app/Contents/MacOS/Clan App" << EOF + #!/bin/bash + # Execute with the correct process name for app icon to appear + exec -a "\$0" "$out/bin/.clan-app-orig" "\$@" + EOF + + chmod +x "$out/Applications/Clan App.app/Contents/MacOS/Clan App" + set +eu pipefail + ''} ''; + # TODO: If we start clan-app over the cli the process name is "python" and icons don't show up correctly on macOS + # I looked in how blender does it, but couldn't figure it out yet. + # They do an exec -a in their wrapper script, but that doesn't seem to work here. + # Don't leak python packages into a devshell. # It can be very confusing if you `nix run` than load the cli from the devshell instead. postFixup = '' rm $out/nix-support/propagated-build-inputs + '' + + lib.optionalString stdenv.hostPlatform.isDarwin '' + set -eu pipefail + mv $out/bin/clan-app $out/bin/.clan-app-orig + + + # Create command line wrapper that executes the app bundle + cat > $out/bin/clan-app << EOF + #!/bin/bash + exec "$out/Applications/Clan App.app/Contents/MacOS/Clan App" "\$@" + EOF + chmod +x $out/bin/clan-app + set +eu pipefail ''; checkPhase = '' + set -eu pipefail export FONTCONFIG_FILE=${fontconfig.out}/etc/fonts/fonts.conf export FONTCONFIG_PATH=${fontconfig.out}/etc/fonts @@ -171,6 +278,7 @@ pythonRuntime.pkgs.buildPythonApplication { fc-list PYTHONPATH= $out/bin/clan-app --help + set +eu pipefail ''; desktopItems = [ desktop-file ]; } diff --git a/pkgs/clan-app/install-desktop.sh b/pkgs/clan-app/install-desktop.sh index 5c2fd2727..8423f684b 100755 --- a/pkgs/clan-app/install-desktop.sh +++ b/pkgs/clan-app/install-desktop.sh @@ -1,10 +1,5 @@ #!/usr/bin/env bash - -if ! command -v xdg-mime &> /dev/null; then - echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed." -fi - ALREADY_INSTALLED=$(nix profile list --json | jq 'has("elements") and (.elements | has("clan-app"))') if [ "$ALREADY_INSTALLED" = "true" ]; then @@ -14,9 +9,23 @@ else nix profile install .#clan-app fi +# Check OS type +if [[ "$OSTYPE" == "linux-gnu"* ]]; then -# install desktop file -set -eou pipefail -DESKTOP_FILE_NAME=org.clan.app.desktop + if ! command -v xdg-mime &> /dev/null; then + echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed." + fi -xdg-mime default "$DESKTOP_FILE_NAME" x-scheme-handler/clan + # install desktop file on Linux + set -eou pipefail + DESKTOP_FILE_NAME=org.clan.app.desktop + xdg-mime default "$DESKTOP_FILE_NAME" x-scheme-handler/clan + +elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "macOS detected." + mkdir -p ~/Applications + ln -sf ~/.nix-profile/Applications/Clan\ App.app ~/Applications + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f ~/Applications/Clan\ App.app +else + echo "Unsupported OS: $OSTYPE" +fi diff --git a/pkgs/clan-app/macos-remote.sh b/pkgs/clan-app/macos-remote.sh new file mode 100755 index 000000000..fb8bb2b7e --- /dev/null +++ b/pkgs/clan-app/macos-remote.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -eou pipefail + +rsync --exclude result --exclude .direnv --exclude node_modules --delete -r ~/Projects/clan-core/pkgs/clan-app mac-mini-dev:~/clan-core/pkgs + +ssh mac-mini-dev "cd \$HOME/clan-core/pkgs/clan-app && nix build .#clan-app -Lv --show-trace" +ssh mac-mini-dev "cd \$HOME/clan-core/pkgs/clan-app && ./install-desktop.sh" + diff --git a/pkgs/clan-app/webview-lib/default.nix b/pkgs/clan-app/webview-lib/default.nix index 8220770f4..fe0dc59a8 100644 --- a/pkgs/clan-app/webview-lib/default.nix +++ b/pkgs/clan-app/webview-lib/default.nix @@ -24,8 +24,8 @@ clangStdenv.mkDerivation { domain = "git.clan.lol"; owner = "clan"; repo = "webview"; - rev = "0ba936b247106219c363a855763ef06b2535363e"; - hash = "sha256-pyH5v7+ytkLOlXpW5WrvN0bqPCOnKnna8+1C0DANoDQ="; + rev = "c27041cb50f79c197080a3f4fa2bad4557ef3234"; + hash = "sha256-xNkX7O+GFMbv3YnXPrtO6vw+BUqCbVeFd8FjgPKfEG0="; }; outputs = [