From 56ff0b588ff71dc267c5ebc748dfa51c7b77c40b Mon Sep 17 00:00:00 2001 From: Georgi Lyubenov Date: Fri, 30 Jan 2026 15:07:05 +0200 Subject: [PATCH 1/2] Allow specifying a set of subscribed selections as a config option --- ops/module.nix | 10 +++++++++- packages/helic/lib/Helic/Data/Selection.hs | 2 ++ packages/helic/lib/Helic/Data/X11Config.hs | 5 ++++- packages/helic/test/Helic/Test/ConfigFileTest.hs | 4 +++- packages/helic/test/fixtures/config.yaml | 1 + readme.md | 4 ++++ 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ops/module.nix b/ops/module.nix index 641b9ff..0cf4428 100644 --- a/ops/module.nix +++ b/ops/module.nix @@ -2,6 +2,7 @@ self: { config, lib, pkgs, ... }: with lib; let cfg = config.services.helic; + nixStringListToJsonArray = xs: "[${concatMapStringsSep ", " (h: "'${h}'") xs}]"; in { options.services.helic = { enable = mkEnableOption "Clipboard Manager"; @@ -67,6 +68,12 @@ in { default = ":0"; description = "The X11 display to connect to if there is no active display in the environment."; }; + subscribedSelections = mkOption { + type = types.listOf types.str; + default = ["Clipboard" "Primary" "Selection"]; + description = "A list of unique X11 selections from which to listen to events for."; + example = literalExpression ["Clipboard"]; + }; }; }; config = mkIf cfg.enable { @@ -81,11 +88,12 @@ in { net: enable: ${if cfg.net.enable then "true" else "false"} port: ${toString cfg.net.port} - hosts: [${concatMapStringsSep ", " (h: "'${h}'") cfg.net.hosts}] + hosts: ${nixStringListToJsonArray cfg.net.hosts} ${if cfg.net.timeout == null then "" else "timeout: ${toString cfg.net.timeout}"} x11: enable: ${if cfg.x11.enable then "true" else "false"} display: ${cfg.x11.display} + subscribedSelections: ${nixStringListToJsonArray cfg.x11.subscribedSelections} ''; systemd.user.services.helic = { description = "Clipboard Manager"; diff --git a/packages/helic/lib/Helic/Data/Selection.hs b/packages/helic/lib/Helic/Data/Selection.hs index 932967d..6ddf1fc 100644 --- a/packages/helic/lib/Helic/Data/Selection.hs +++ b/packages/helic/lib/Helic/Data/Selection.hs @@ -14,6 +14,8 @@ data Selection = Secondary deriving stock (Eq, Show, Ord, Enum, Bounded) +json ''Selection + -- |Convert a 'Selection' into the string that X11 uses to identify it. toXString :: Selection -> Text toXString = \case diff --git a/packages/helic/lib/Helic/Data/X11Config.hs b/packages/helic/lib/Helic/Data/X11Config.hs index 8997cf6..2a6e8f5 100644 --- a/packages/helic/lib/Helic/Data/X11Config.hs +++ b/packages/helic/lib/Helic/Data/X11Config.hs @@ -2,6 +2,7 @@ -- |X11Config Data Type, Internal module Helic.Data.X11Config where +import Helic.Data.Selection (Selection) newtype DisplayId = DisplayId { unDisplayId :: Text } @@ -13,7 +14,9 @@ json ''DisplayId data X11Config = X11Config { enable :: Maybe Bool, - display :: Maybe DisplayId + display :: Maybe DisplayId, + subscribedSelections :: Maybe (Set Selection) + -- ^ if not specified, we default to subscribing to all selections } deriving stock (Eq, Show, Generic) deriving anyclass (Default) diff --git a/packages/helic/test/Helic/Test/ConfigFileTest.hs b/packages/helic/test/Helic/Test/ConfigFileTest.hs index 0a75ceb..40779d2 100644 --- a/packages/helic/test/Helic/Test/ConfigFileTest.hs +++ b/packages/helic/test/Helic/Test/ConfigFileTest.hs @@ -10,6 +10,8 @@ import Helic.Data.Config (Config (Config)) import Helic.Data.NetConfig (NetConfig (NetConfig)) import Helic.Data.TmuxConfig (TmuxConfig (TmuxConfig)) import Helic.Data.X11Config (X11Config (X11Config)) +import qualified Helic.Data.Selection as Selection +import qualified Data.Set as Set target :: Config target = @@ -20,7 +22,7 @@ target = net = NetConfig (Just True) (Just 10001) (Just 5) (Just ["remote:1000"]) x = - X11Config (Just True) (Just ":1") + X11Config (Just True) (Just ":1") (Just (Set.fromList [Selection.Clipboard, Selection.Primary, Selection.Secondary])) test_readConfigFile :: UnitTest test_readConfigFile = do diff --git a/packages/helic/test/fixtures/config.yaml b/packages/helic/test/fixtures/config.yaml index 77d0e32..d62a974 100644 --- a/packages/helic/test/fixtures/config.yaml +++ b/packages/helic/test/fixtures/config.yaml @@ -13,3 +13,4 @@ net: x11: enable: true display: ":1" + subscribedSelections: ['Clipboard', 'Primary', 'Secondary'] diff --git a/readme.md b/readme.md index 27df363..4f89cdd 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,7 @@ Selections are used when text is selected with the mouse, while the clipboard is When some text is copied or selected in *X11*, the daemon receives an event that it proceeds to broadcast to the configured targets. If the source was a selection, the *X11* clipboard is updated as well. +The choice of which *X11* selections to listen to is configurable via `subscribedSelections`. The CLI program `hel` can be used to manually send text to the daemon, for example from *tmux* or *Neovim*. If remote hosts are configured, each yank event is sent over the network to update their clipboards. @@ -170,6 +171,8 @@ For *NixOS*, the file `/etc/helic.yaml` is generated from module options: x11 = { enable = true; display = ":0"; + subscribedSelections = ["Clipboard" "Primary" "Selection"]; + }; }; } @@ -193,6 +196,7 @@ The meaning of these options is: |`tmux.exe`|`tmux`|Only for YAML file: The path to the *tmux* executable| |`x11.enable`|`true`|Whether to synchronize the X11 clipboard.| |`x11.display`|`:0`|The display identifier used when connecting to the default display via GTK fails.| +|`x11.subscribedSelections`|`["Clipboard" "Primary" "Selection"]`|Which X11 selections to listen to.| # Neovim From d4e50985c1339e0fb0c0ddc4084692c7d805f5f7 Mon Sep 17 00:00:00 2001 From: Georgi Lyubenov Date: Fri, 30 Jan 2026 15:13:43 +0200 Subject: [PATCH 2/2] Respect subscribedSelections config option when subscribing to gtk's clipboard --- .../helic/lib/Helic/Interpreter/GtkClipboard.hs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/helic/lib/Helic/Interpreter/GtkClipboard.hs b/packages/helic/lib/Helic/Interpreter/GtkClipboard.hs index d37af3b..3e77353 100644 --- a/packages/helic/lib/Helic/Interpreter/GtkClipboard.hs +++ b/packages/helic/lib/Helic/Interpreter/GtkClipboard.hs @@ -8,6 +8,9 @@ import Helic.Effect.GtkClipboard (GtkClipboard) import Helic.Effect.GtkMain (GtkMain) import Helic.Gtk (clipboardText, setClipboardText, subscribeToClipboard) import Helic.Interpreter.GtkMain (interpretWithGtk) +import Helic.Data.X11Config (X11Config(..)) +import Helic.Data.Selection (Selection) +import qualified Data.Set as Set -- |Specialization of 'scoped_' to 'GtkClipboard' for syntactic sugar. withGtkClipboard :: @@ -21,7 +24,7 @@ withGtkClipboard = -- The effect then needs to be scoped using 'withGtkClipboard'. -- The default implementation for this purpose is 'interpretWithGtk'. handleGtkClipboard :: - Members [Log, Embed IO, Final IO] r => + Members [Reader X11Config, Log, Embed IO, Final IO] r => Display -> GtkClipboard (Sem r0) a -> Tactical effect (Sem r0) (Stop Text : r) a @@ -33,12 +36,16 @@ handleGtkClipboard display = \case GtkClipboard.Events f -> do let f' s t = void (raise (runTSimple (f s t))) runReader display do - for_ @[] [minBound..maxBound] (subscribeToClipboard f') + x11Config <- ask @X11Config + let + targetSelections :: Set Selection + targetSelections = fromMaybe (Set.fromList [minBound..maxBound]) x11Config.subscribedSelections + for_ @Set targetSelections (subscribeToClipboard f') pureT () -- |Native interpreter for 'GtkClipboard' that requires the effect to be used within a 'withGtkClipboard' region. interpretGtkClipboard :: - Members [GtkMain Display, Log, Embed IO, Final IO] r => + Members [Reader X11Config, GtkMain Display, Log, Embed IO, Final IO] r => InterpreterFor (Scoped_ GtkClipboard !! Text) r interpretGtkClipboard = interpretWithGtk handleGtkClipboard