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
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <unpacked package>`. 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.
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup Label="UI">
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Wayland" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Controls.ColorPicker" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Headless" Version="$(AvaloniaVersion)" />
Expand Down
1 change: 1 addition & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/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 |
Expand Down
24 changes: 17 additions & 7 deletions installer/linux/INSTALL.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,16 +36,26 @@ 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
The qdbus tool is qdbus6, qdbus-qt6 or plain qdbus depending on the
distribution; use whichever one you have.
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 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)$

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
Expand Down
10 changes: 10 additions & 0 deletions src/Filee.App/Filee.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
<PackageReference Include="SharpHook" />
</ItemGroup>

<PropertyGroup>
<FileeNativeWayland Condition="$(RuntimeIdentifier.StartsWith('linux')) Or ('$(RuntimeIdentifier)' == '' And $([MSBuild]::IsOSPlatform('Linux')))">true</FileeNativeWayland>
<DefineConstants Condition="'$(FileeNativeWayland)' == 'true'">$(DefineConstants);FILEE_NATIVE_WAYLAND</DefineConstants>
</PropertyGroup>

<!-- Linux-only Wayland packages to keep Windows and macOS binaries small -->
<ItemGroup Condition="'$(FileeNativeWayland)' == 'true'">
<PackageReference Include="Avalonia.Wayland" />
</ItemGroup>

<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<!-- Shown on the About page. -->
Expand Down
11 changes: 10 additions & 1 deletion src/Filee.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,19 +274,28 @@ private static List<Process> OtherProcessesOfThisExe(string exe)
/// <summary>Also used by the XAML previewer.</summary>
public static AppBuilder BuildAvaloniaApp()
{
var builder = AppBuilder.Configure<App>().UsePlatformDetect();
var builder = AppBuilder.Configure<App>();
#if FILEE_NATIVE_WAYLAND
// 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 = builder.UsePlatformDetect();
#endif

if (OperatingSystem.IsMacOS())
builder = builder.With(new MacOSPlatformOptions { ShowInDock = PreloadedStore?.Settings.ShowInDock ?? true });
if (OperatingSystem.IsWindows())
builder = builder.With(WindowsRendering.Options(
WindowsRendering.Normalize(Environment.GetEnvironmentVariable(WindowsRendering.Variable)),
adapters => Log.LogInformation("Graphics adapters: {Adapters}", adapters)));

builder = builder
.ConfigureFonts(fonts => fonts.AddFontCollection(new FileeFontCollection()))
#if DEBUG
.WithDeveloperTools()
#endif
.LogToTrace();

AvaloniaLogForwarder.Install(Log);
return builder;
}
Expand Down
11 changes: 11 additions & 0 deletions src/Filee.App/Services/RadialController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,17 @@ private RadialWindow EnsureWindow()
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 (Filee.Platform.Linux.WaylandSupport.NativeWaylandRequested)
{
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 =>
Expand Down
11 changes: 7 additions & 4 deletions src/Filee.Platform.Linux/LinuxPlatformServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ 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 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"
&& (Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP") ?? "").Split(':').Contains("KDE", StringComparer.OrdinalIgnoreCase)
&& WaylandSupport.IsSupportedCompositor(
Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP"),
Environment.GetEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"))
&& CanReadInputDevices();

private static bool CanReadInputDevices()
Expand Down
32 changes: 32 additions & 0 deletions src/Filee.Platform.Linux/WaylandSupport.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Whether the native Wayland backend (Avalonia.Wayland) is opted into: on Linux, with <c>FILEE_USE_WAYLAND=1</c>
/// and a Wayland display to connect to. Otherwise Filee runs through X11 or XWayland.
/// </summary>
public static bool IsNativeWaylandRequested(bool isLinux, string? fileeUseWayland, string? waylandDisplay) =>
isLinux
&& string.Equals(fileeUseWayland, "1", StringComparison.Ordinal)
&& !string.IsNullOrEmpty(waylandDisplay);

/// <summary><see cref="IsNativeWaylandRequested(bool, string?, string?)"/> for this process.</summary>
public static bool NativeWaylandRequested => IsNativeWaylandRequested(OperatingSystem.IsLinux(),
Environment.GetEnvironmentVariable("FILEE_USE_WAYLAND"), Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));

/// <summary>
/// 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 <c>XDG_CURRENT_DESKTOP</c> or <c>HYPRLAND_INSTANCE_SIGNATURE</c>.
/// </summary>
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);
}
}
46 changes: 46 additions & 0 deletions tests/Filee.Platform.Tests/WaylandDecisionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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;

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 KDE_and_Hyprland_can_place_the_donut(
string? xdgCurrentDesktop, string? hyprlandSignature, bool expected)
{
var result = WaylandSupport.IsSupportedCompositor(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 The_native_backend_needs_Linux_the_switch_and_a_display(
bool isLinux, string? fileeUseWayland, string? waylandDisplay, bool expected)
{
var result = WaylandSupport.IsNativeWaylandRequested(isLinux, fileeUseWayland, waylandDisplay);
Assert.Equal(expected, result);
}
}
Loading