Merge pull request 'clan-app: Now displays runtime icon correctly in process overview' (#5019) from Qubasa/clan-core:fix_runtime_icon into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5019
This commit is contained in:
Luis Hebendanz
2025-09-01 15:37:51 +00:00
7 changed files with 198 additions and 25 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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))

View File

@@ -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'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Clan App</string>
<key>CFBundleExecutable</key>
<string>Clan App</string>
<key>CFBundleIconFile</key>
<string>clan-app.icns</string>
<key>CFBundleIdentifier</key>
<string>org.clan.app</string>
<key>CFBundleName</key>
<string>Clan App</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Clan Protocol</string>
<key>CFBundleURLSchemes</key>
<array>
<string>clan</string>
</array>
</dict>
</array>
</dict>
</plist>
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 ];
}

View File

@@ -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

9
pkgs/clan-app/macos-remote.sh Executable file
View File

@@ -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"

View File

@@ -24,19 +24,10 @@ clangStdenv.mkDerivation {
domain = "git.clan.lol";
owner = "clan";
repo = "webview";
rev = "ef481aca8e531f6677258ca911c61aaaf71d2214";
hash = "sha256-KF9ESpo40z6VXyYsZCLWJAIh0RFe1Zy/Qw4k7cTpoYU=";
rev = "c27041cb50f79c197080a3f4fa2bad4557ef3234";
hash = "sha256-xNkX7O+GFMbv3YnXPrtO6vw+BUqCbVeFd8FjgPKfEG0=";
};
# @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"