diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index e0f097d5a0a6..0357e6fa5b66 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -142,6 +142,8 @@ - [nvme-rs](https://github.com/liberodark/nvme-rs), NVMe monitoring [services.nvme-rs](#opt-services.nvme-rs.enable). +- [ringboard](https://github.com/SUPERCILEX/clipboard-history), a fast, efficient, and composable clipboard manager for Linux. Available for x11 as [services.ringboard](#opt-services.ringboard.x11.enable) and for wayland as [services.ringboard](#opt-services.ringboard.wayland.enable). + ## Backward Incompatibilities {#sec-release-25.11-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 3171684a30e6..c9ac96aa6706 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -915,6 +915,7 @@ ./services/misc/redlib.nix ./services/misc/redmine.nix ./services/misc/renovate.nix + ./services/misc/ringboard.nix ./services/misc/rkvm.nix ./services/misc/rmfakecloud.nix ./services/misc/rshim.nix diff --git a/nixos/modules/services/misc/ringboard.nix b/nixos/modules/services/misc/ringboard.nix new file mode 100644 index 000000000000..2e2847da853e --- /dev/null +++ b/nixos/modules/services/misc/ringboard.nix @@ -0,0 +1,78 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.ringboard; +in +{ + options.services.ringboard = { + x11.enable = lib.mkEnableOption "X11 support for Ringboard"; + wayland.enable = lib.mkEnableOption "Wayland support for Ringboard"; + x11.package = lib.mkPackageOption pkgs "ringboard" { }; + wayland.package = lib.mkPackageOption pkgs "ringboard-wayland" { }; + }; + + config = lib.mkIf (cfg.x11.enable || cfg.wayland.enable) { + systemd.user.services.ringboard-server = { + description = "Ringboard server"; + documentation = [ "https://github.com/SUPERCILEX/clipboard-history" ]; + after = [ "multi-user.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "notify"; + Environment = "RUST_LOG=trace"; + ExecStart = "${ + if cfg.x11.enable then cfg.x11.package else cfg.wayland.package + }/bin/ringboard-server"; + Restart = "on-failure"; + Slice = "session-ringboard.slice"; + }; + }; + + systemd.user.services.ringboard-listener = { + description = "Ringboard clipboard listener"; + documentation = [ "https://github.com/SUPERCILEX/clipboard-history" ]; + requires = [ "ringboard-server.service" ]; + after = [ + "ringboard-server.service" + "graphical-session.target" + ]; + bindsTo = [ "graphical-session.target" ]; + wantedBy = [ "graphical-session.target" ]; + script = + if cfg.x11.enable && cfg.wayland.enable then + '' + if [ "$XDG_SESSION_TYPE" = "wayland" ]; then + exec '${cfg.wayland.package}'/bin/ringboard-wayland + else + exec '${cfg.x11.package}'/bin/ringboard-x11 + fi + '' + else if cfg.wayland.enable then + '' + exec '${cfg.wayland.package}'/bin/ringboard-wayland + '' + else + '' + exec '${cfg.x11.package}'/bin/ringboard-x11 + ''; + serviceConfig = { + Type = "exec"; + Restart = "on-failure"; + Slice = "session-ringboard.slice"; + }; + environment.RUST_LOG = "trace"; + }; + + systemd.user.slices.session-ringboard = { + description = "Ringboard clipboard services"; + }; + + environment.systemPackages = + lib.optionals cfg.x11.enable [ cfg.x11.package ] + ++ lib.optionals cfg.wayland.enable [ cfg.wayland.package ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0216ea9d9bd5..98cb57a02c3a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1300,6 +1300,7 @@ in restic = runTest ./restic.nix; restic-rest-server = runTest ./restic-rest-server.nix; retroarch = runTest ./retroarch.nix; + ringboard = runTest ./ringboard.nix; rke2 = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./rke2 { }; rkvm = handleTest ./rkvm { }; rmfakecloud = runTest ./rmfakecloud.nix; diff --git a/nixos/tests/ringboard.nix b/nixos/tests/ringboard.nix new file mode 100644 index 000000000000..a0bd26c25c51 --- /dev/null +++ b/nixos/tests/ringboard.nix @@ -0,0 +1,49 @@ +{ + config, + pkgs, + lib, + ... +}: +{ + name = "ringboard"; + meta = { inherit (pkgs.ringboard.meta) maintainers; }; + + nodes.machine = { + imports = [ + ./common/user-account.nix + ./common/x11.nix + ]; + + test-support.displayManager.auto.user = "alice"; + + services.xserver.displayManager.sessionCommands = '' + '${lib.getExe pkgs.gedit}' & + ''; + + services.ringboard.x11.enable = true; + }; + + testScript = + { nodes, ... }: + let + inherit (nodes.machine.test-support.displayManager.auto) user; + in + '' + @polling_condition + def gedit_running(): + machine.succeed("pgrep gedit") + + with subtest("Wait for service startup"): + machine.wait_for_unit("graphical.target") + machine.wait_for_unit("ringboard-server.service", "${user}") + machine.wait_for_unit("ringboard-listener.service", "${user}") + + with subtest("Ensure clipboard is monitored"): + with gedit_running: # type: ignore[union-attr] + machine.send_chars("Hello world!") + machine.send_key("ctrl-a") + machine.send_key("ctrl-c") + machine.wait_for_console_text("Small selection transfer complete") + machine.succeed("su - '${user}' -c 'ringboard search Hello | grep world!'") + ''; +} diff --git a/pkgs/by-name/ri/ringboard/package.nix b/pkgs/by-name/ri/ringboard/package.nix new file mode 100644 index 000000000000..af04fa4b014f --- /dev/null +++ b/pkgs/by-name/ri/ringboard/package.nix @@ -0,0 +1,109 @@ +{ + stdenv, + rustPlatform, + fetchFromGitHub, + lib, + libxkbcommon, + libGL, + wayland, + xorg, + makeWrapper, + displayServer ? "x11", + nixosTests, +}: + +assert lib.assertOneOf "displayServer" displayServer [ + "x11" + "wayland" +]; + +rustPlatform.buildRustPackage (finalAttrs: { + pname = "ringboard" + lib.optionalString (displayServer == "wayland") "-wayland"; + + # release version needs nightly, so we use a custom tree, see: + # https://github.com/SUPERCILEX/clipboard-history/issues/22#issuecomment-3322075172 + version = "0.12.2-unstable-2025-09-23"; + + src = fetchFromGitHub { + owner = "SUPERCILEX"; + repo = "clipboard-history"; + rev = "228a39dd8a9aece0bb06f68ad44906b297270628"; + hash = "sha256-qA7wwvWnnZHN9edkmubEo37F+peU0LQGo/Zl8FpywuE="; + }; + + cargoHash = "sha256-MFfuUu/hpb6Uaqe21bvXNKRyJazAL5m+Vw/vAeeDYEk="; + + nativeBuildInputs = [ + makeWrapper + ]; + + buildInputs = [ + libxkbcommon + libGL + ] + ++ lib.optionals (displayServer == "x11") [ + xorg.libXcursor + xorg.libXrandr + xorg.libXi + xorg.libX11 + ] + ++ lib.optionals (displayServer == "wayland") [ + wayland + ]; + + buildPhase = '' + runHook preBuild + + local flagsArray=("-j $NIX_BUILD_CORES --target ${stdenv.hostPlatform.rust.rustcTarget} --offline --release"); + concatTo flagsArray cargoBuildFlags; + + echo "Building package: clipboard-history-server" + cargo build $flagsArray --package clipboard-history-server --no-default-features --features systemd + ${lib.optionalString (displayServer == "x11") '' + echo "Building package: clipboard-history-x11" + cargo build $flagsArray --package clipboard-history-x11 --no-default-features + ''} + ${lib.optionalString (displayServer == "wayland") '' + echo "Building package: clipboard-history-wayland" + cargo build $flagsArray --package clipboard-history-wayland --no-default-features + ''} + echo "Building package: clipboard-history" + cargo build $flagsArray --package clipboard-history + echo "Building package: clipboard-history-tui" + cargo build $flagsArray --package clipboard-history-tui + echo "Building package: clipboard-history-egui" + cargo build $flagsArray --package clipboard-history-egui --no-default-features --features ${displayServer} + + runHook postBuild + ''; + + # check needs nightly, see: + # https://github.com/SUPERCILEX/clipboard-history/issues/22#issuecomment-3322330559 + doCheck = false; + + postInstall = '' + # Wrap the program in a script that sets the LD_LIBRARY_PATH environment variable + # so that it can find the shared libraries it depends on. This is currently a + # requirement for running Rust programs that depend on `egui` within a Nix environment. + # https://github.com/emilk/egui/issues/2486 + wrapProgram $out/bin/ringboard-egui --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath finalAttrs.buildInputs}" + install -m 444 -D egui/ringboard-egui.desktop $out/share/applications/ringboard-egui.desktop + install -Dm644 logo.jpeg $out/share/icons/hicolor/1024x1024/ringboard.jpeg + # Initializing a GUI can be quite slow, so GUI clients make their windows invisible when closed rather than completely quitting. + # To reopen the window, a special file can be deleted which wakes the GUI via inotify. + # If, instead, a new instance of the GUI is opened, this special file is used to first check for a previously running instance of the GUI and kill it if it exists. + # https://alexsaveau.dev/blog/projects/performance/clipboard/ringboard/ringboard#gui-startup-latency-and-long-lived-client-windows + sed -i "s|Exec=ringboard-egui|Exec=$(echo /bin/sh -c \"ps -p \`cat /tmp/.ringboard/\$USER.egui-sleep 2\> /dev/null\` \> /dev/null 2\>\\\&1 \\\&\\\& exec rm -f /tmp/.ringboard/\$USER.egui-sleep \\\|\\\| exec $out/bin/ringboard-egui\")|g" $out/share/applications/ringboard-egui.desktop + sed -i "s|Icon=ringboard|Icon=$out/share/icons/hicolor/1024x1024/ringboard.jpeg|g" $out/share/applications/ringboard-egui.desktop + ''; + + passthru.tests.nixos = nixosTests.ringboard; + + meta = { + description = "Fast, efficient, and composable clipboard manager for Linux"; + homepage = "https://github.com/SUPERCILEX/clipboard-history"; + license = lib.licenses.asl20; + platforms = lib.platforms.linux; + maintainers = [ lib.maintainers.magnetophon ]; + }; +}) diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index c8e36b72bd8a..8f7965364e78 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -12612,6 +12612,8 @@ with pkgs; rgp = libsForQt5.callPackage ../development/tools/rgp { }; + ringboard-wayland = callPackage ../by-name/ri/ringboard/package.nix { displayServer = "wayland"; }; + ripcord = if stdenv.hostPlatform.isLinux then qt5.callPackage ../applications/networking/instant-messengers/ripcord { }