From 580aafed5ed0b70282eacef51e19f1d7428fb5ad Mon Sep 17 00:00:00 2001 From: mahaveergurjar Date: Wed, 21 Oct 2026 22:04:02 +0530 Subject: [PATCH 1/4] feat(linux): add native Wayland support and generalize compositor gestures - Add Avalonia.Wayland, Avalonia.Skia, and Avalonia.HarfBuzz packages - Initialize native Wayland platform backend in Program.cs when WAYLAND_DISPLAY is set - Re-create RadialWindow if closed to avoid xdg_surface destruction crash on Wayland - Generalize WaylandGestures check in LinuxPlatformServices.cs beyond KDE Plasma - Improve initial drag ring visibility in RadialViewModel.cs - Update Linux installer script for native Wayland deployment --- Directory.Packages.props | 3 ++ installer/linux/install.sh | 28 ++++++++----------- src/Filee.App/Filee.App.csproj | 3 ++ src/Filee.App/Program.cs | 16 +++++++++-- src/Filee.App/Services/RadialController.cs | 9 +++++- src/Filee.App/ViewModels/RadialViewModel.cs | 4 +-- .../LinuxPlatformServices.cs | 8 +++--- 7 files changed, 44 insertions(+), 27 deletions(-) mode change 100644 => 100755 installer/linux/install.sh diff --git a/Directory.Packages.props b/Directory.Packages.props index 18aba06ca..3675f0927 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,6 +11,9 @@ + + + diff --git a/installer/linux/install.sh b/installer/linux/install.sh old mode 100644 new mode 100755 index 2a3cec01c..945350e65 --- a/installer/linux/install.sh +++ b/installer/linux/install.sh @@ -6,25 +6,17 @@ data_dir=${XDG_DATA_HOME:-"$HOME/.local/share"} install_dir="$data_dir/filee/application" bin_dir="$HOME/.local/bin" desktop_file="$data_dir/applications/com.filee.app.desktop" -if [ -e "$install_dir" ] || [ -L "$install_dir" ]; then - printf '%s\n' "An installation already exists at $install_dir. Run its uninstall.sh first." - exit 1 -fi -if [ -e "$bin_dir/filee" ] || [ -L "$bin_dir/filee" ]; then - printf '%s\n' "$bin_dir/filee already exists. Remove or rename it before installing Filee." - exit 1 -fi -if [ -e "$desktop_file" ] || [ -L "$desktop_file" ]; then - printf '%s\n' "$desktop_file already exists. Remove or rename it before installing Filee." - exit 1 -fi -command -v python3 >/dev/null -test -x "$source_dir/Filee" -test -x "$source_dir/filee-cli" + +# Close running Filee instances +pkill -f "Filee" 2>/dev/null || true + mkdir -p "$data_dir/filee" "$bin_dir" "$data_dir/applications" +rm -rf "$install_dir" cp -R "$source_dir" "$install_dir" printf '%s\n' 'com.filee.app' > "$install_dir/.filee-package" +rm -f "$bin_dir/filee" ln -s "$install_dir/filee-cli" "$bin_dir/filee" + python3 - "$install_dir" "$desktop_file" <<'PY' import pathlib import sys @@ -41,12 +33,14 @@ def desktop_value(value): desktop.write_text( '[Desktop Entry]\nType=Application\nName=Filee\n' - 'Comment=Convert files with Filee\n' + 'Comment=Convert files with Filee (Native Wayland)\n' f'Exec={quoted_exec(str(app / "Filee"))} --convert %F\n' f'Icon={desktop_value(app / "filee.png")}\n' 'Terminal=false\nCategories=Utility;\nX-Filee-Managed=true\n', encoding='utf-8') PY + if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database "$data_dir/applications" fi -printf '%s\n' "Installed Filee at $install_dir" "CLI: $bin_dir/filee (add $bin_dir to PATH if needed)." + +printf '%s\n' "Installed Filee (Native Wayland) at $install_dir" diff --git a/src/Filee.App/Filee.App.csproj b/src/Filee.App/Filee.App.csproj index b08ead4f7..4abd5ee48 100644 --- a/src/Filee.App/Filee.App.csproj +++ b/src/Filee.App/Filee.App.csproj @@ -23,6 +23,9 @@ + + + None diff --git a/src/Filee.App/Program.cs b/src/Filee.App/Program.cs index 385811bf4..3586128fc 100644 --- a/src/Filee.App/Program.cs +++ b/src/Filee.App/Program.cs @@ -263,14 +263,24 @@ private static List OtherProcessesOfThisExe(string exe) } /// Also used by the XAML previewer. - public static AppBuilder BuildAvaloniaApp() => - AppBuilder.Configure() - .UsePlatformDetect() + public static AppBuilder BuildAvaloniaApp() + { + var builder = AppBuilder.Configure(); + if (OperatingSystem.IsLinux() && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) + { + builder.UseWayland().UseSkia().UseHarfBuzz(); + } + else + { + builder.UsePlatformDetect(); + } + return builder .ConfigureFonts(fonts => fonts.AddFontCollection(new FileeFontCollection())) #if DEBUG .WithDeveloperTools() #endif .LogToTrace(); + } } /// Embedded Noto fonts: fonts:Filee#Noto Sans, #Noto Sans KR, #Noto Sans SC, diff --git a/src/Filee.App/Services/RadialController.cs b/src/Filee.App/Services/RadialController.cs index cc7dcb9c4..b330d41ef 100644 --- a/src/Filee.App/Services/RadialController.cs +++ b/src/Filee.App/Services/RadialController.cs @@ -193,8 +193,15 @@ private void OfferFormatRequest() private RadialWindow EnsureWindow() { + if (_window is { IsLoaded: true } w) + return w; + + // On native Wayland, hiding a window can destroy its xdg_surface. Re-create if closed. if (_window is not null) - return _window; + { + try { _window.Close(); } catch { } + _window = null; + } var window = new RadialWindow { DataContext = viewModel }; var donut = window.DonutControl; diff --git a/src/Filee.App/ViewModels/RadialViewModel.cs b/src/Filee.App/ViewModels/RadialViewModel.cs index 37f25c5b9..95b43f7fa 100644 --- a/src/Filee.App/ViewModels/RadialViewModel.cs +++ b/src/Filee.App/ViewModels/RadialViewModel.cs @@ -87,8 +87,8 @@ public void ResetForDrag() Items = []; CenterTitle = loc["donut.center_drag"]; CenterSubtitle = null; - DonutOpacity = 0; - HitBackground = "#01000000"; + DonutOpacity = 1; + HitBackground = "Transparent"; } /// Fills the donut for the given files. diff --git a/src/Filee.Platform.Linux/LinuxPlatformServices.cs b/src/Filee.Platform.Linux/LinuxPlatformServices.cs index b09f22c2b..ab5387516 100644 --- a/src/Filee.Platform.Linux/LinuxPlatformServices.cs +++ b/src/Filee.Platform.Linux/LinuxPlatformServices.cs @@ -21,12 +21,12 @@ public sealed class LinuxPlatformServices : IPlatformServices string.Equals(Environment.GetEnvironmentVariable("XDG_SESSION_TYPE"), "wayland", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")); - // Wayland gestures (experimental) are opt-in: FILEE_WAYLAND_GESTURES=1 on KDE Plasma. libuiohook's libinput backend - // reads /dev/input, so /dev/input is only probed once the switch is set, and it works once the user is in the - // `input` group. Presses carry no position and a mouse only moves relatively there; a KWin script places the donut. + // Wayland gestures (experimental) are opt-in: FILEE_WAYLAND_GESTURES=1. + // Originally KDE Plasma only; patched to support any Wayland compositor (e.g. Hyprland). + // libuiohook's libinput backend reads /dev/input, so the user must be in the `input` group. + // On Hyprland, windowrulev2 places the donut at the cursor instead of a KWin script. private static readonly bool WaylandGestures = IsWaylandSession && Environment.GetEnvironmentVariable("FILEE_WAYLAND_GESTURES") == "1" - && (Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP") ?? "").Split(':').Contains("KDE", StringComparer.OrdinalIgnoreCase) && CanReadInputDevices(); private static bool CanReadInputDevices() From 6b8276740f6d40c03ac03799494d3e26d0ead308 Mon Sep 17 00:00:00 2001 From: mahaveergurjar Date: Thu, 22 Oct 2026 20:54:27 +0530 Subject: [PATCH 2/4] address review feedback: opt-in native Wayland, Closed event tracking, and preserve existing installer/drag behaviors --- CONTRIBUTING.md | 9 +++--- installer/linux/INSTALL.txt | 23 ++++++++++----- installer/linux/install.sh | 28 +++++++++++-------- src/Filee.App/Filee.App.csproj | 10 +++++-- src/Filee.App/Program.cs | 4 ++- src/Filee.App/Services/RadialController.cs | 20 +++++++------ src/Filee.App/ViewModels/RadialViewModel.cs | 4 +-- .../LinuxPlatformServices.cs | 20 ++++++++++--- 8 files changed, 78 insertions(+), 40 deletions(-) mode change 100755 => 100644 installer/linux/install.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d21cccad..7d99b79d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,14 +45,15 @@ Linux release packages must run on Ubuntu 22.04 (glibc 2.35). CI builds rhwp fro machine that built it. Check a package with `python3 build/check-elf-glibc.py --max 2.35 `. See [docs/ENGINES.md](docs/ENGINES.md#linux-builds-for-older-glibc). -- **Linux:** drag gestures require X11. Install the Avalonia desktop dependencies (fontconfig, FreeType, X11, +- **Linux:** drag gestures require X11 by default. Install the Avalonia desktop dependencies (fontconfig, FreeType, X11, Xrandr, Xi, Xtst, Xt, XkbCommon, ICE and SM) and CJK fonts for document comparisons. File actions are installed for Thunar, Nautilus, Nemo and Dolphin. The first enabled keyboard shortcut uses Thunar's native custom action; close Thunar before changing or pausing that shortcut and reopen it afterwards. Global selected-file queries and hold gestures are not supported. Wayland can use file actions, the drop zone, watch folders and CLI. - On KDE Plasma 6 the drag gesture can also run there, experimentally and only with `FILEE_WAYLAND_GESTURES=1`. It needs - the user in the `input` group (libuiohook's libinput back end) and the KWin script in `installer/linux/kwin`, which - moves the donut window (titled "Filee Donut") to the cursor. See `installer/linux/INSTALL.txt`. + On KDE Plasma 6 and Hyprland, the drag gesture can also run there, experimentally with `FILEE_WAYLAND_GESTURES=1`. It needs + the user in the `input` group (libuiohook's libinput back end) and a compositor positioning mechanism (the KWin script in + `installer/linux/kwin` or a Hyprland floating window rule). Native Wayland support (`Avalonia.Wayland`) is also available + as an opt-in via `FILEE_USE_WAYLAND=1`. See `installer/linux/INSTALL.txt`. - **macOS:** grant Filee Accessibility permission for gestures and Finder Automation permission for selection. Keep the development `.app` at a stable path while testing these permissions. Finder integration is a Quick Action under Services; auto-start uses a per-user LaunchAgent. diff --git a/installer/linux/INSTALL.txt b/installer/linux/INSTALL.txt index 2852fddb1..9e32b506f 100644 --- a/installer/linux/INSTALL.txt +++ b/installer/linux/INSTALL.txt @@ -24,10 +24,10 @@ X11 supports the global gesture toolbar; it needs libXtst and libXt (libxtst6, libxt6). A native Wayland session has additional input restrictions; use the drop window and file-manager actions, or the experimental setup below. -Wayland gesture (experimental, KDE Plasma 6 only, off by default): Filee reads -the mouse and keyboard from /dev/input through libinput, and a KWin script moves -the donut to the cursor because Wayland apps cannot place windows. It needs -libinput, libudev and libwayland-client (libinput10, libudev1 and +Wayland gesture (experimental, KDE Plasma 6 and Hyprland, off by default): Filee reads +the mouse and keyboard from /dev/input through libinput, and a KWin script or Hyprland +window rule places the donut at the cursor because Wayland apps cannot place windows. +It needs libinput, libudev and libwayland-client (libinput10, libudev1 and libwayland-client0 on Ubuntu). To turn it on: 1. Start Filee with FILEE_WAYLAND_GESTURES=1 in its environment, for example with the line FILEE_WAYLAND_GESTURES=1 in ~/.config/environment.d/filee.conf @@ -36,12 +36,21 @@ libwayland-client0 on Ubuntu). To turn it on: can read all keyboard and mouse input from any program they run, so enable this only if you accept that. You may already be in it for other tools. sudo usermod -aG input "$USER" -3. Install the KWin script and enable it, so it loads at every login (you can - also enable "Filee donut at the cursor" in System Settings > Window - Management > KWin Scripts): +3. For KDE Plasma: Install the KWin script and enable it, so it loads at every login: kpackagetool6 --type=KWin/Script --install "${XDG_DATA_HOME:-$HOME/.local/share}/filee/application/kwin/filee-donut" kwriteconfig6 --file kwinrc --group Plugins --key filee-donutEnabled true qdbus6 org.kde.KWin /KWin reconfigure + For Hyprland: Add the floating window rule in ~/.config/hypr/hyprland.conf (or hyprland.lua): + windowrulev2 = float, title:^(Filee Donut)$ + windowrulev2 = noborder, title:^(Filee Donut)$ + windowrulev2 = noshadow, title:^(Filee Donut)$ + windowrulev2 = noblur, title:^(Filee Donut)$ + +Native Wayland Backend (experimental, off by default): +By default, Filee runs through XWayland on Wayland desktops for maximum compatibility. +To opt into the native Wayland backend (Avalonia.Wayland): + Start Filee with FILEE_USE_WAYLAND=1 in its environment. + (e.g., export FILEE_USE_WAYLAND=1 or set in ~/.config/environment.d/filee.conf). Limits: Wayland cannot say which window is under the cursor, so a Ctrl+drag in any app opens an invisible donut window. It only shows when files are dragged onto it, but it rejects drops that are not files, so Ctrl+drag to copy text in an diff --git a/installer/linux/install.sh b/installer/linux/install.sh old mode 100755 new mode 100644 index 945350e65..2a3cec01c --- a/installer/linux/install.sh +++ b/installer/linux/install.sh @@ -6,17 +6,25 @@ data_dir=${XDG_DATA_HOME:-"$HOME/.local/share"} install_dir="$data_dir/filee/application" bin_dir="$HOME/.local/bin" desktop_file="$data_dir/applications/com.filee.app.desktop" - -# Close running Filee instances -pkill -f "Filee" 2>/dev/null || true - +if [ -e "$install_dir" ] || [ -L "$install_dir" ]; then + printf '%s\n' "An installation already exists at $install_dir. Run its uninstall.sh first." + exit 1 +fi +if [ -e "$bin_dir/filee" ] || [ -L "$bin_dir/filee" ]; then + printf '%s\n' "$bin_dir/filee already exists. Remove or rename it before installing Filee." + exit 1 +fi +if [ -e "$desktop_file" ] || [ -L "$desktop_file" ]; then + printf '%s\n' "$desktop_file already exists. Remove or rename it before installing Filee." + exit 1 +fi +command -v python3 >/dev/null +test -x "$source_dir/Filee" +test -x "$source_dir/filee-cli" mkdir -p "$data_dir/filee" "$bin_dir" "$data_dir/applications" -rm -rf "$install_dir" cp -R "$source_dir" "$install_dir" printf '%s\n' 'com.filee.app' > "$install_dir/.filee-package" -rm -f "$bin_dir/filee" ln -s "$install_dir/filee-cli" "$bin_dir/filee" - python3 - "$install_dir" "$desktop_file" <<'PY' import pathlib import sys @@ -33,14 +41,12 @@ def desktop_value(value): desktop.write_text( '[Desktop Entry]\nType=Application\nName=Filee\n' - 'Comment=Convert files with Filee (Native Wayland)\n' + 'Comment=Convert files with Filee\n' f'Exec={quoted_exec(str(app / "Filee"))} --convert %F\n' f'Icon={desktop_value(app / "filee.png")}\n' 'Terminal=false\nCategories=Utility;\nX-Filee-Managed=true\n', encoding='utf-8') PY - if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database "$data_dir/applications" fi - -printf '%s\n' "Installed Filee (Native Wayland) at $install_dir" +printf '%s\n' "Installed Filee at $install_dir" "CLI: $bin_dir/filee (add $bin_dir to PATH if needed)." diff --git a/src/Filee.App/Filee.App.csproj b/src/Filee.App/Filee.App.csproj index 3355c555b..68a5d6a4d 100644 --- a/src/Filee.App/Filee.App.csproj +++ b/src/Filee.App/Filee.App.csproj @@ -23,9 +23,6 @@ - - - @@ -38,6 +35,13 @@ + + + + + + + diff --git a/src/Filee.App/Program.cs b/src/Filee.App/Program.cs index f9419de4a..6b9caa8f5 100644 --- a/src/Filee.App/Program.cs +++ b/src/Filee.App/Program.cs @@ -275,7 +275,9 @@ private static List OtherProcessesOfThisExe(string exe) public static AppBuilder BuildAvaloniaApp() { var builder = AppBuilder.Configure(); - if (OperatingSystem.IsLinux() && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) + if (OperatingSystem.IsLinux() + && string.Equals(Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), "1", StringComparison.Ordinal) + && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) { builder.UseWayland().UseSkia().UseHarfBuzz(); } diff --git a/src/Filee.App/Services/RadialController.cs b/src/Filee.App/Services/RadialController.cs index a837c5fef..9fe6c33e0 100644 --- a/src/Filee.App/Services/RadialController.cs +++ b/src/Filee.App/Services/RadialController.cs @@ -214,19 +214,23 @@ private void OfferFormatRequest() private RadialWindow EnsureWindow() { - if (_window is { IsLoaded: true } w) - return w; - - // On native Wayland, hiding a window can destroy its xdg_surface. Re-create if closed. if (_window is not null) - { - try { _window.Close(); } catch { } - _window = null; - } + return _window; var window = new RadialWindow { DataContext = viewModel }; var donut = window.DonutControl; + // On native Wayland, hiding an xdg_toplevel surface causes the compositor to destroy its surface, + // and re-showing raises an InvalidOperationException. Track Closed to re-create on the next gesture. + if (OperatingSystem.IsLinux() && string.Equals(Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), "1", StringComparison.Ordinal)) + { + window.Closed += (_, _) => + { + if (ReferenceEquals(_window, window)) + _window = null; + }; + } + window.DragEntered += () => _dragReachedDonut = true; // Tells "the drag reached the donut but brought no files" apart, with what it brought (file promises, say). window.DragWithoutFiles += formats => diff --git a/src/Filee.App/ViewModels/RadialViewModel.cs b/src/Filee.App/ViewModels/RadialViewModel.cs index 8e3c7b8fc..9e6705cc6 100644 --- a/src/Filee.App/ViewModels/RadialViewModel.cs +++ b/src/Filee.App/ViewModels/RadialViewModel.cs @@ -135,8 +135,8 @@ public void ResetForDrag() ShowFace(back: false); CenterTitle = loc["donut.center_drag"]; CenterSubtitle = null; - DonutOpacity = 1; - HitBackground = "Transparent"; + DonutOpacity = 0; + HitBackground = "#01000000"; } /// diff --git a/src/Filee.Platform.Linux/LinuxPlatformServices.cs b/src/Filee.Platform.Linux/LinuxPlatformServices.cs index d6d5bc574..ff2f2a2da 100644 --- a/src/Filee.Platform.Linux/LinuxPlatformServices.cs +++ b/src/Filee.Platform.Linux/LinuxPlatformServices.cs @@ -21,14 +21,26 @@ public sealed class LinuxPlatformServices : IPlatformServices string.Equals(Environment.GetEnvironmentVariable("XDG_SESSION_TYPE"), "wayland", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")); - // Wayland gestures (experimental) are opt-in: FILEE_WAYLAND_GESTURES=1. - // Originally KDE Plasma only; patched to support any Wayland compositor (e.g. Hyprland). - // libuiohook's libinput backend reads /dev/input, so the user must be in the `input` group. - // On Hyprland, windowrulev2 places the donut at the cursor instead of a KWin script. + // Wayland gestures (experimental) are opt-in: FILEE_WAYLAND_GESTURES=1 on KDE Plasma and Hyprland. + // libuiohook's libinput backend reads /dev/input, so /dev/input is only probed once the switch is set, + // and it works once the user is in the `input` group. Presses carry no position and a mouse only moves + // relatively there; a KWin script or Hyprland window rule places the donut at the cursor. private static readonly bool WaylandGestures = IsWaylandSession && Environment.GetEnvironmentVariable("FILEE_WAYLAND_GESTURES") == "1" + && IsSupportedWaylandCompositor() && CanReadInputDevices(); + private static bool IsSupportedWaylandCompositor() + { + var desktop = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP") ?? ""; + if (desktop.Split(':').Contains("KDE", StringComparer.OrdinalIgnoreCase)) + return true; + if (desktop.Split(':').Contains("Hyprland", StringComparer.OrdinalIgnoreCase) + || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"))) + return true; + return false; + } + private static bool CanReadInputDevices() { try From f6a34b120460f62bfdf237ee9f98bed096046a94 Mon Sep 17 00:00:00 2001 From: mahaveergurjar Date: Fri, 23 Oct 2026 15:00:40 +0530 Subject: [PATCH 3/4] feat(linux): fix cross-platform build, add Wayland unit tests, and update notices and placement docs --- Directory.Packages.props | 2 - THIRD-PARTY-NOTICES.md | 1 + installer/linux/INSTALL.txt | 3 +- src/Filee.App/Filee.App.csproj | 9 ++-- src/Filee.App/Program.cs | 13 ++++-- src/Filee.App/Services/RadialController.cs | 5 ++- .../LinuxPlatformServices.cs | 17 +++++-- .../WaylandDecisionTests.cs | 45 +++++++++++++++++++ 8 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 tests/Filee.Platform.Tests/WaylandDecisionTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ec8addbe4..5ec34bcdc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,8 +12,6 @@ - - diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 2e63361cb..512f75c0a 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -8,6 +8,7 @@ Each component remains under its own license. | Component | License | Project | |---|---|---| | Avalonia UI | MIT | https://github.com/AvaloniaUI/Avalonia | +| NWayland (used by Avalonia.Wayland) | MIT | https://github.com/Glavo/NWayland | | CommunityToolkit.Mvvm | MIT | https://github.com/CommunityToolkit/dotnet | | SharpHook (libuiohook) | MIT (libuiohook: GPL-3.0 with linking exception / LGPL-3.0) | https://github.com/TolikPylypchuk/SharpHook | | Magick.NET / ImageMagick | Apache-2.0 / ImageMagick License | https://github.com/dlemstra/Magick.NET | diff --git a/installer/linux/INSTALL.txt b/installer/linux/INSTALL.txt index 9c34e8dd4..32f6b9fa4 100644 --- a/installer/linux/INSTALL.txt +++ b/installer/linux/INSTALL.txt @@ -42,8 +42,9 @@ libwayland-client0 on Ubuntu). To turn it on: qdbus6 org.kde.KWin /KWin reconfigure After updating Filee, upgrade the installed script the same way, with --upgrade instead of --install, then run the reconfigure line again. - For Hyprland: Add the floating window rule in ~/.config/hypr/hyprland.conf (or hyprland.lua): + For Hyprland: Add the floating and placement window rules in ~/.config/hypr/hyprland.conf (or hyprland.lua): windowrulev2 = float, title:^(Filee Donut)$ + windowrulev2 = move cursor -50% -50%, title:^(Filee Donut)$ windowrulev2 = noborder, title:^(Filee Donut)$ windowrulev2 = noshadow, title:^(Filee Donut)$ windowrulev2 = noblur, title:^(Filee Donut)$ diff --git a/src/Filee.App/Filee.App.csproj b/src/Filee.App/Filee.App.csproj index 68a5d6a4d..fee2ef10a 100644 --- a/src/Filee.App/Filee.App.csproj +++ b/src/Filee.App/Filee.App.csproj @@ -35,11 +35,14 @@ + + true + $(DefineConstants);FILEE_NATIVE_WAYLAND + + - + - - diff --git a/src/Filee.App/Program.cs b/src/Filee.App/Program.cs index 6b9caa8f5..dd93cbb74 100644 --- a/src/Filee.App/Program.cs +++ b/src/Filee.App/Program.cs @@ -275,16 +275,21 @@ private static List OtherProcessesOfThisExe(string exe) public static AppBuilder BuildAvaloniaApp() { var builder = AppBuilder.Configure(); - if (OperatingSystem.IsLinux() - && string.Equals(Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), "1", StringComparison.Ordinal) - && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) +#if FILEE_NATIVE_WAYLAND + if (Filee.Platform.Linux.LinuxPlatformServices.IsNativeWaylandRequested( + OperatingSystem.IsLinux(), + Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), + Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) { - builder.UseWayland().UseSkia().UseHarfBuzz(); + builder.UseWayland(); } else { builder.UsePlatformDetect(); } +#else + builder.UsePlatformDetect(); +#endif if (OperatingSystem.IsMacOS()) builder = builder.With(new MacOSPlatformOptions { ShowInDock = PreloadedStore?.Settings.ShowInDock ?? true }); diff --git a/src/Filee.App/Services/RadialController.cs b/src/Filee.App/Services/RadialController.cs index 9640b8362..129f376ef 100644 --- a/src/Filee.App/Services/RadialController.cs +++ b/src/Filee.App/Services/RadialController.cs @@ -222,7 +222,10 @@ private RadialWindow EnsureWindow() // On native Wayland, hiding an xdg_toplevel surface causes the compositor to destroy its surface, // and re-showing raises an InvalidOperationException. Track Closed to re-create on the next gesture. - if (OperatingSystem.IsLinux() && string.Equals(Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), "1", StringComparison.Ordinal)) + if (Filee.Platform.Linux.LinuxPlatformServices.IsNativeWaylandRequested( + OperatingSystem.IsLinux(), + Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), + Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) { window.Closed += (_, _) => { diff --git a/src/Filee.Platform.Linux/LinuxPlatformServices.cs b/src/Filee.Platform.Linux/LinuxPlatformServices.cs index ff2f2a2da..a34cc1bdb 100644 --- a/src/Filee.Platform.Linux/LinuxPlatformServices.cs +++ b/src/Filee.Platform.Linux/LinuxPlatformServices.cs @@ -27,20 +27,29 @@ public sealed class LinuxPlatformServices : IPlatformServices // relatively there; a KWin script or Hyprland window rule places the donut at the cursor. private static readonly bool WaylandGestures = IsWaylandSession && Environment.GetEnvironmentVariable("FILEE_WAYLAND_GESTURES") == "1" - && IsSupportedWaylandCompositor() + && IsSupportedWaylandCompositor( + Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP"), + Environment.GetEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE")) && CanReadInputDevices(); - private static bool IsSupportedWaylandCompositor() + /// Decides whether the Wayland compositor supports positioning the donut at the cursor. + public static bool IsSupportedWaylandCompositor(string? xdgCurrentDesktop, string? hyprlandSignature) { - var desktop = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP") ?? ""; + var desktop = xdgCurrentDesktop ?? ""; if (desktop.Split(':').Contains("KDE", StringComparer.OrdinalIgnoreCase)) return true; if (desktop.Split(':').Contains("Hyprland", StringComparer.OrdinalIgnoreCase) - || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"))) + || !string.IsNullOrEmpty(hyprlandSignature)) return true; return false; } + /// Decides whether the native Wayland backend (Avalonia.Wayland) is opted into. + public static bool IsNativeWaylandRequested(bool isLinux, string? fileeUseWayland, string? waylandDisplay) => + isLinux + && string.Equals(fileeUseWayland, "1", StringComparison.Ordinal) + && !string.IsNullOrEmpty(waylandDisplay); + private static bool CanReadInputDevices() { try diff --git a/tests/Filee.Platform.Tests/WaylandDecisionTests.cs b/tests/Filee.Platform.Tests/WaylandDecisionTests.cs new file mode 100644 index 000000000..244200c5e --- /dev/null +++ b/tests/Filee.Platform.Tests/WaylandDecisionTests.cs @@ -0,0 +1,45 @@ +using System.Runtime.Versioning; +using Filee.Platform.Linux; + +namespace Filee.Platform.Tests; + +[SupportedOSPlatform("linux")] +public sealed class WaylandDecisionTests +{ + [Theory] + [InlineData("KDE", null, true)] + [InlineData("kde", null, true)] + [InlineData("KDE:GNOME", null, true)] + [InlineData("Hyprland", null, true)] + [InlineData("hyprland", null, true)] + [InlineData(null, "instance_12345", true)] + [InlineData("", "instance_12345", true)] + [InlineData("GNOME", null, false)] + [InlineData("sway", null, false)] + [InlineData("XFCE", null, false)] + [InlineData(null, null, false)] + [InlineData("", "", false)] + public void IsSupportedWaylandCompositor_Recognizes_Supported_Desktops( + string? xdgCurrentDesktop, string? hyprlandSignature, bool expected) + { + var result = LinuxPlatformServices.IsSupportedWaylandCompositor(xdgCurrentDesktop, hyprlandSignature); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(true, "1", "wayland-0", true)] + [InlineData(true, "1", "wayland-1", true)] + [InlineData(false, "1", "wayland-0", false)] // Not Linux + [InlineData(true, null, "wayland-0", false)] // Switch unset + [InlineData(true, "", "wayland-0", false)] // Switch empty + [InlineData(true, "0", "wayland-0", false)] // Switch 0 + [InlineData(true, "true", "wayland-0", false)] // Switch not "1" + [InlineData(true, "1", null, false)] // No WAYLAND_DISPLAY + [InlineData(true, "1", "", false)] // Empty WAYLAND_DISPLAY + public void IsNativeWaylandRequested_Requires_Linux_And_Switch_And_Display( + bool isLinux, string? fileeUseWayland, string? waylandDisplay, bool expected) + { + var result = LinuxPlatformServices.IsNativeWaylandRequested(isLinux, fileeUseWayland, waylandDisplay); + Assert.Equal(expected, result); + } +} From 8b1874757bc7c037f484984b17b90a0d6f1d3b4e Mon Sep 17 00:00:00 2001 From: KnifeLemon Date: Fri, 23 Oct 2026 20:07:41 +0900 Subject: [PATCH 4/4] Wayland: decisions in a platform-neutral helper, NWayland link - The native-backend and compositor checks move to WaylandSupport, which has no OS attribute: RadialController calls it on every platform, and the Linux-only LinuxPlatformServices raised CA1416 there. Program.cs and RadialController use the same NativeWaylandRequested property. - THIRD-PARTY-NOTICES: NWayland's project is kekekeks/NWayland (the package's repository). - Test names in the repo's sentence style. Co-Authored-By: Claude Opus 5.5 --- THIRD-PARTY-NOTICES.md | 2 +- src/Filee.App/Program.cs | 15 ++------- src/Filee.App/Services/RadialController.cs | 5 +-- .../LinuxPlatformServices.cs | 20 +----------- src/Filee.Platform.Linux/WaylandSupport.cs | 32 +++++++++++++++++++ .../WaylandDecisionTests.cs | 13 ++++---- 6 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 src/Filee.Platform.Linux/WaylandSupport.cs diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 33c3d6eda..95b3db47b 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -8,7 +8,7 @@ Each component remains under its own license. | Component | License | Project | |---|---|---| | Avalonia UI | MIT | https://github.com/AvaloniaUI/Avalonia | -| NWayland (used by Avalonia.Wayland) | MIT | https://github.com/Glavo/NWayland | +| NWayland (used by Avalonia.Wayland) | MIT | https://github.com/kekekeks/NWayland | | CommunityToolkit.Mvvm | MIT | https://github.com/CommunityToolkit/dotnet | | SharpHook (libuiohook) | MIT (libuiohook: GPL-3.0 with linking exception / LGPL-3.0) | https://github.com/TolikPylypchuk/SharpHook | | Magick.NET / ImageMagick | Apache-2.0 / ImageMagick License | https://github.com/dlemstra/Magick.NET | diff --git a/src/Filee.App/Program.cs b/src/Filee.App/Program.cs index c1cc14717..7aa04f93a 100644 --- a/src/Filee.App/Program.cs +++ b/src/Filee.App/Program.cs @@ -276,19 +276,10 @@ public static AppBuilder BuildAvaloniaApp() { var builder = AppBuilder.Configure(); #if FILEE_NATIVE_WAYLAND - if (Filee.Platform.Linux.LinuxPlatformServices.IsNativeWaylandRequested( - OperatingSystem.IsLinux(), - Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), - Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) - { - builder.UseWayland(); - } - else - { - builder.UsePlatformDetect(); - } + // The native backend is only compiled into Linux builds and only used when asked for (FILEE_USE_WAYLAND=1). + builder = Filee.Platform.Linux.WaylandSupport.NativeWaylandRequested ? builder.UseWayland() : builder.UsePlatformDetect(); #else - builder.UsePlatformDetect(); + builder = builder.UsePlatformDetect(); #endif if (OperatingSystem.IsMacOS()) diff --git a/src/Filee.App/Services/RadialController.cs b/src/Filee.App/Services/RadialController.cs index 129f376ef..659223aa5 100644 --- a/src/Filee.App/Services/RadialController.cs +++ b/src/Filee.App/Services/RadialController.cs @@ -222,10 +222,7 @@ private RadialWindow EnsureWindow() // On native Wayland, hiding an xdg_toplevel surface causes the compositor to destroy its surface, // and re-showing raises an InvalidOperationException. Track Closed to re-create on the next gesture. - if (Filee.Platform.Linux.LinuxPlatformServices.IsNativeWaylandRequested( - OperatingSystem.IsLinux(), - Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), - Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) + if (Filee.Platform.Linux.WaylandSupport.NativeWaylandRequested) { window.Closed += (_, _) => { diff --git a/src/Filee.Platform.Linux/LinuxPlatformServices.cs b/src/Filee.Platform.Linux/LinuxPlatformServices.cs index a34cc1bdb..151353961 100644 --- a/src/Filee.Platform.Linux/LinuxPlatformServices.cs +++ b/src/Filee.Platform.Linux/LinuxPlatformServices.cs @@ -27,29 +27,11 @@ public sealed class LinuxPlatformServices : IPlatformServices // relatively there; a KWin script or Hyprland window rule places the donut at the cursor. private static readonly bool WaylandGestures = IsWaylandSession && Environment.GetEnvironmentVariable("FILEE_WAYLAND_GESTURES") == "1" - && IsSupportedWaylandCompositor( + && WaylandSupport.IsSupportedCompositor( Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP"), Environment.GetEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE")) && CanReadInputDevices(); - /// Decides whether the Wayland compositor supports positioning the donut at the cursor. - public static bool IsSupportedWaylandCompositor(string? xdgCurrentDesktop, string? hyprlandSignature) - { - var desktop = xdgCurrentDesktop ?? ""; - if (desktop.Split(':').Contains("KDE", StringComparer.OrdinalIgnoreCase)) - return true; - if (desktop.Split(':').Contains("Hyprland", StringComparer.OrdinalIgnoreCase) - || !string.IsNullOrEmpty(hyprlandSignature)) - return true; - return false; - } - - /// Decides whether the native Wayland backend (Avalonia.Wayland) is opted into. - public static bool IsNativeWaylandRequested(bool isLinux, string? fileeUseWayland, string? waylandDisplay) => - isLinux - && string.Equals(fileeUseWayland, "1", StringComparison.Ordinal) - && !string.IsNullOrEmpty(waylandDisplay); - private static bool CanReadInputDevices() { try diff --git a/src/Filee.Platform.Linux/WaylandSupport.cs b/src/Filee.Platform.Linux/WaylandSupport.cs new file mode 100644 index 000000000..f8504ce82 --- /dev/null +++ b/src/Filee.Platform.Linux/WaylandSupport.cs @@ -0,0 +1,32 @@ +// Decisions about Wayland that only depend on environment values, kept apart from the Linux services so the app can ask +// them on every platform and tests can feed them values. + +namespace Filee.Platform.Linux; + +public static class WaylandSupport +{ + /// + /// Whether the native Wayland backend (Avalonia.Wayland) is opted into: on Linux, with FILEE_USE_WAYLAND=1 + /// and a Wayland display to connect to. Otherwise Filee runs through X11 or XWayland. + /// + public static bool IsNativeWaylandRequested(bool isLinux, string? fileeUseWayland, string? waylandDisplay) => + isLinux + && string.Equals(fileeUseWayland, "1", StringComparison.Ordinal) + && !string.IsNullOrEmpty(waylandDisplay); + + /// for this process. + public static bool NativeWaylandRequested => IsNativeWaylandRequested(OperatingSystem.IsLinux(), + Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")); + + /// + /// Whether the compositor has a way to put the donut at the cursor: KDE Plasma (the KWin script) or Hyprland (a + /// window rule), told by XDG_CURRENT_DESKTOP or HYPRLAND_INSTANCE_SIGNATURE. + /// + public static bool IsSupportedCompositor(string? xdgCurrentDesktop, string? hyprlandSignature) + { + var desktops = (xdgCurrentDesktop ?? "").Split(':'); + return desktops.Contains("KDE", StringComparer.OrdinalIgnoreCase) + || desktops.Contains("Hyprland", StringComparer.OrdinalIgnoreCase) + || !string.IsNullOrEmpty(hyprlandSignature); + } +} diff --git a/tests/Filee.Platform.Tests/WaylandDecisionTests.cs b/tests/Filee.Platform.Tests/WaylandDecisionTests.cs index 244200c5e..131ccb79f 100644 --- a/tests/Filee.Platform.Tests/WaylandDecisionTests.cs +++ b/tests/Filee.Platform.Tests/WaylandDecisionTests.cs @@ -1,9 +1,10 @@ -using System.Runtime.Versioning; +// The Wayland decisions that only depend on environment values (#367): which compositors can place the donut, and +// when the native backend is used. + using Filee.Platform.Linux; namespace Filee.Platform.Tests; -[SupportedOSPlatform("linux")] public sealed class WaylandDecisionTests { [Theory] @@ -19,10 +20,10 @@ public sealed class WaylandDecisionTests [InlineData("XFCE", null, false)] [InlineData(null, null, false)] [InlineData("", "", false)] - public void IsSupportedWaylandCompositor_Recognizes_Supported_Desktops( + public void KDE_and_Hyprland_can_place_the_donut( string? xdgCurrentDesktop, string? hyprlandSignature, bool expected) { - var result = LinuxPlatformServices.IsSupportedWaylandCompositor(xdgCurrentDesktop, hyprlandSignature); + var result = WaylandSupport.IsSupportedCompositor(xdgCurrentDesktop, hyprlandSignature); Assert.Equal(expected, result); } @@ -36,10 +37,10 @@ public void IsSupportedWaylandCompositor_Recognizes_Supported_Desktops( [InlineData(true, "true", "wayland-0", false)] // Switch not "1" [InlineData(true, "1", null, false)] // No WAYLAND_DISPLAY [InlineData(true, "1", "", false)] // Empty WAYLAND_DISPLAY - public void IsNativeWaylandRequested_Requires_Linux_And_Switch_And_Display( + public void The_native_backend_needs_Linux_the_switch_and_a_display( bool isLinux, string? fileeUseWayland, string? waylandDisplay, bool expected) { - var result = LinuxPlatformServices.IsNativeWaylandRequested(isLinux, fileeUseWayland, waylandDisplay); + var result = WaylandSupport.IsNativeWaylandRequested(isLinux, fileeUseWayland, waylandDisplay); Assert.Equal(expected, result); } }