helic 0.6.0.0 → 0.6.1.0
raw patch · 13 files changed
+93/−57 lines, 13 filesdep +randomPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: random
API changes (from Hackage documentation)
+ Helic.Data.AgentId: instance GHC.Generics.Generic Helic.Data.AgentId.AgentId
+ Helic.Data.Event: describe :: Event -> Text
- Helic: yank :: Members [Reader InstanceName, Client, ChronosTime, Error Text, Embed IO] r => YankConfig -> Sem r ()
+ Helic: yank :: Members [Reader InstanceName, Client, ChronosTime, Log, Error Text, Embed IO] r => YankConfig -> Sem r ()
- Helic.Effect.Agent: update :: forall r_a1jaO. Member Agent r_a1jaO => Event -> Sem r_a1jaO ()
+ Helic.Effect.Agent: update :: forall r_a1jII. Member Agent r_a1jII => Event -> Sem r_a1jII ()
- Helic.Effect.Gtk: main :: forall s_a1jCH r_a1jDO. Member (Gtk s_a1jCH) r_a1jDO => Sem r_a1jDO ()
+ Helic.Effect.Gtk: main :: forall s_a1kaB r_a1kbI. Member (Gtk s_a1kaB) r_a1kbI => Sem r_a1kbI ()
- Helic.Effect.Gtk: resource :: forall s_a1jCJ r_a1jDP. Member (Gtk s_a1jCJ) r_a1jDP => Sem r_a1jDP s_a1jCJ
+ Helic.Effect.Gtk: resource :: forall s_a1kaD r_a1kbJ. Member (Gtk s_a1kaD) r_a1kbJ => Sem r_a1kbJ s_a1kaD
- Helic.Effect.GtkClipboard: events :: forall r_a1jKf. Member GtkClipboard r_a1jKf => (Selection -> Text -> Sem r_a1jKf ()) -> Sem r_a1jKf ()
+ Helic.Effect.GtkClipboard: events :: forall r_a1ki9. Member GtkClipboard r_a1ki9 => (Selection -> Text -> Sem r_a1ki9 ()) -> Sem r_a1ki9 ()
- Helic.Effect.GtkClipboard: read :: forall r_a1jKa. Member GtkClipboard r_a1jKa => Selection -> Sem r_a1jKa (Maybe Text)
+ Helic.Effect.GtkClipboard: read :: forall r_a1ki4. Member GtkClipboard r_a1ki4 => Selection -> Sem r_a1ki4 (Maybe Text)
- Helic.Effect.GtkClipboard: write :: forall r_a1jKc. Member GtkClipboard r_a1jKc => Selection -> Text -> Sem r_a1jKc ()
+ Helic.Effect.GtkClipboard: write :: forall r_a1ki6. Member GtkClipboard r_a1ki6 => Selection -> Text -> Sem r_a1ki6 ()
- Helic.Effect.GtkMain: access :: forall s_a1jUc r_a1jWN. Member (GtkMain s_a1jUc) r_a1jWN => Sem r_a1jWN s_a1jUc -> Sem r_a1jWN s_a1jUc
+ Helic.Effect.GtkMain: access :: forall s_a1ks6 r_a1kuH. Member (GtkMain s_a1ks6) r_a1kuH => Sem r_a1kuH s_a1ks6 -> Sem r_a1kuH s_a1ks6
- Helic.Effect.GtkMain: request :: forall s_a1jUe r_a1jWP. Member (GtkMain s_a1jUe) r_a1jWP => Sem r_a1jWP s_a1jUe -> Sem r_a1jWP s_a1jUe
+ Helic.Effect.GtkMain: request :: forall s_a1ks8 r_a1kuJ. Member (GtkMain s_a1ks8) r_a1kuJ => Sem r_a1kuJ s_a1ks8 -> Sem r_a1kuJ s_a1ks8
- Helic.Effect.GtkMain: run :: forall s_a1jUh r_a1jWR a_a1jUg. Member (GtkMain s_a1jUh) r_a1jWR => Sem r_a1jWR a_a1jUg -> Sem r_a1jWR a_a1jUg
+ Helic.Effect.GtkMain: run :: forall s_a1ksb r_a1kuL a_a1ksa. Member (GtkMain s_a1ksb) r_a1kuL => Sem r_a1kuL a_a1ksa -> Sem r_a1kuL a_a1ksa
- Helic.Effect.GtkMain: running :: forall s_a1jUi r_a1jWT. Member (GtkMain s_a1jUi) r_a1jWT => s_a1jUi -> Sem r_a1jWT ()
+ Helic.Effect.GtkMain: running :: forall s_a1ksc r_a1kuN. Member (GtkMain s_a1ksc) r_a1kuN => s_a1ksc -> Sem r_a1kuN ()
- Helic.Effect.XClipboard: current :: forall r_a1kkS. Member XClipboard r_a1kkS => Sem r_a1kkS (Maybe Text)
+ Helic.Effect.XClipboard: current :: forall r_a1kSM. Member XClipboard r_a1kSM => Sem r_a1kSM (Maybe Text)
- Helic.Effect.XClipboard: set :: forall r_a1kkT. Member XClipboard r_a1kkT => Text -> Sem r_a1kkT ()
+ Helic.Effect.XClipboard: set :: forall r_a1kSN. Member XClipboard r_a1kSN => Text -> Sem r_a1kSN ()
- Helic.Effect.XClipboard: sync :: forall r_a1kkV. Member XClipboard r_a1kkV => Text -> Selection -> Sem r_a1kkV ()
+ Helic.Effect.XClipboard: sync :: forall r_a1kSP. Member XClipboard r_a1kSP => Text -> Selection -> Sem r_a1kSP ()
- Helic.Yank: yank :: Members [Reader InstanceName, Client, ChronosTime, Error Text, Embed IO] r => YankConfig -> Sem r ()
+ Helic.Yank: yank :: Members [Reader InstanceName, Client, ChronosTime, Log, Error Text, Embed IO] r => YankConfig -> Sem r ()
Files
- changelog.md +4/−0
- helic.cabal +2/−3
- lib/Helic/App.hs +9/−11
- lib/Helic/Cli.hs +1/−1
- lib/Helic/Cli/Options.hs +5/−2
- lib/Helic/Data/AgentId.hs +1/−1
- lib/Helic/Data/Event.hs +5/−0
- lib/Helic/Data/YankConfig.hs +3/−2
- lib/Helic/Interpreter/History.hs +23/−20
- lib/Helic/Net/Api.hs +1/−2
- lib/Helic/Yank.hs +9/−6
- readme.md +10/−6
- test/Helic/Dev.hs +20/−3
changelog.md view
@@ -1,5 +1,9 @@ # Unreleased +# 0.6.1.0++* Add cli option `--text` for the `yank` command as an alternative to stdin.+ # 0.6.0.0 * Allow all agents to be disabled in the config file.
helic.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: helic-version: 0.6.0.0+version: 0.6.1.0 synopsis: Clipboard Manager description: See https://hackage.haskell.org/package/helic/docs/Helic.html category: Clipboard@@ -260,12 +260,11 @@ , path ==0.9.* , polysemy ==1.9.* , polysemy-chronos ==0.6.*- , polysemy-conc ==0.12.* , polysemy-log ==0.9.* , polysemy-plugin ==0.4.* , polysemy-test ==0.7.*- , polysemy-time ==0.6.* , prelate ==0.5.*+ , random , tasty ==1.4.* , torsor ==0.1.* , zeugma ==0.7.*
lib/Helic/App.hs view
@@ -47,17 +47,6 @@ withAsync_ serve $ Conc.subscribeLoop History.receive -yankApp ::- Config ->- YankConfig ->- Sem AppStack ()-yankApp (Config name _ net _ _ _) yankConfig =- interpretManager $- interpretInstanceName name $- runReader (fromMaybe def net) $- interpretClientNet $- yank yankConfig- runClient :: Members [Log, Error Text, Race, Embed IO] r => Maybe NetConfig ->@@ -66,6 +55,15 @@ interpretManager . runReader (fromMaybe def net) . interpretClientNet++yankApp ::+ Config ->+ YankConfig ->+ Sem AppStack ()+yankApp (Config name _ net _ _ _) yankConfig =+ interpretInstanceName name $+ runClient net $+ yank yankConfig listApp :: Config ->
lib/Helic/Cli.hs view
@@ -30,7 +30,7 @@ defaultCommand :: Sem AppStack Command defaultCommand = do Conc.timeout_ (pure Nothing) (MilliSeconds 100) (Just <$> tryAny (hLookAhead stdin)) <&> \case- Just (Right _) -> Yank (YankConfig (Just "cli"))+ Just (Right _) -> Yank (YankConfig (Just "cli") Nothing) _ -> Listen withCliOptions :: Conf -> Maybe Command -> IO ()
lib/Helic/Cli/Options.hs view
@@ -28,6 +28,7 @@ import Helic.Data.ListConfig (ListConfig (ListConfig)) import Helic.Data.LoadConfig (LoadConfig (LoadConfig))+import qualified Helic.Data.YankConfig import Helic.Data.YankConfig (YankConfig (YankConfig)) data Conf =@@ -63,8 +64,10 @@ command "listen" (info (pure Listen) (progDesc "Run the daemon")) yankParser :: Parser YankConfig-yankParser =- YankConfig <$> optional (strOption (long "agent" <> help "Source of the yank"))+yankParser = do+ agent <- optional (strOption (long "agent" <> help "Source of the yank"))+ text <- optional (strOption (long "text" <> help "Yank text, uses stdin if not specified"))+ pure YankConfig {..} yankCommand :: Mod CommandFields Command yankCommand =
lib/Helic/Data/AgentId.hs view
@@ -5,7 +5,7 @@ newtype AgentId = AgentId { unAgentId :: Text }- deriving stock (Eq, Show)+ deriving stock (Eq, Show, Generic) deriving newtype (IsString) json ''AgentId
lib/Helic/Data/Event.hs view
@@ -7,6 +7,7 @@ import Helic.Data.AgentId (AgentId) import Helic.Data.InstanceName (InstanceName)+import Exon (exon) -- |The central data type representing a clipboard event. data Event =@@ -34,3 +35,7 @@ sender <- ask time <- Time.now pure Event {..}++describe :: Event -> Text+describe Event {..} =+ [exon|##{sender}:##{source}|]
lib/Helic/Data/YankConfig.hs view
@@ -1,11 +1,12 @@ {-# options_haddock prune #-}--- |YankConfig Data Type, Internal +-- | YankConfig Data Type, Internal module Helic.Data.YankConfig where data YankConfig = YankConfig {- agent :: Maybe Text+ agent :: Maybe Text,+ text :: Maybe Text } deriving stock (Eq, Show, Generic) deriving anyclass (Default)
lib/Helic/Interpreter/History.hs view
@@ -31,8 +31,8 @@ Sem r () runAgent (Event _ (AgentId eId) _ _) | eId == agentName @tag = unit-runAgent e =- tag (Agent.update e)+runAgent event =+ tag (Agent.update event) -- |Send an event to all agents. broadcast ::@@ -63,8 +63,8 @@ Text.replace "\r" "\n" . Text.replace "\r\n" "\n" sanitize :: Event -> Event-sanitize e@Event {content} =- e { content = sanitizeNewlines content }+sanitize event@Event {content} =+ event { content = sanitizeNewlines content } -- |Append an event to the history unless the latest event contains the same text, or there was an event within the last -- second that contained the same text, or the new event has an earlier time stamp than the latest event, to avoid@@ -74,24 +74,24 @@ Event -> Seq Event -> Maybe (Seq Event)-appendIfValid now (sanitize -> e@Event {content, time}) = \case+appendIfValid now (sanitize -> event@Event {content, time}) = \case Seq.Empty ->- Just (Seq.singleton e)+ Just (Seq.singleton event) _ :|> Event _ _ latestTime latest | latest == content || time < latestTime -> Nothing- hist | inRecent now e hist ->+ hist | inRecent now event hist -> Nothing hist ->- Just (hist |> e)+ Just (hist |> event) -- |Add an event to the history unless it is a duplicate. insertEvent :: Members [AtomicState (Seq Event), ChronosTime] r => Event -> Sem r Bool-insertEvent e = do+insertEvent event = do now <- Time.now- atomicState' \ s -> result s (appendIfValid now e s)+ atomicState' \ s -> result s (appendIfValid now event s) where result s = \case Just new -> (new, True)@@ -127,11 +127,13 @@ Maybe Int -> Event -> Sem r ()-receiveEvent maxHistory e = do- Log.debug [exon|listen: #{show e}|]- whenM (insertEvent e) do- broadcast e- traverse_ logTruncation =<< truncateLog (fromMaybe 100 maxHistory)+receiveEvent maxHistory event = do+ Log.debug [exon|listen: #{show event}|]+ ifM (insertEvent event)+ do+ broadcast event+ traverse_ logTruncation =<< truncateLog (fromMaybe 100 maxHistory)+ do Log.debug [exon|Ignoring duplicate event: #{Event.describe event}|] -- |Re-broadcast an older event from the history at the given index (ordered by increasing age) and move it to the end -- of the history.@@ -159,7 +161,7 @@ name <- ask pure (name == sender && source == agentIdNet) --- |Interpret 'History' as 'AtomicState', broadcasting to agents.+-- |Interpret 'History' using 'AtomicState', broadcasting to agents. interpretHistory :: Members Agents r => Members [Reader InstanceName, AtomicState (Seq Event), ChronosTime, Log] r =>@@ -170,8 +172,9 @@ History.Get -> toList <$> atomicGet History.Receive event ->- unlessM (isNetworkCycle event) do- receiveEvent maxHistory event+ ifM (isNetworkCycle event)+ do Log.debug [exon|Ignoring network cycle event: #{Event.describe event}|]+ do receiveEvent maxHistory event History.Load index -> do- e <- loadEvent index- e <$ traverse_ broadcast e+ event <- loadEvent index+ event <$ traverse_ broadcast event
lib/Helic/Net/Api.hs view
@@ -34,8 +34,7 @@ -- |The default port, 9500. defaultPort :: Int-defaultPort =- 9500+defaultPort = 9500 -- |Run the daemon API. serve ::
lib/Helic/Yank.hs view
@@ -4,22 +4,25 @@ module Helic.Yank where import qualified Data.Text.IO as Text+import Exon (exon)+import qualified Log import Polysemy.Chronos (ChronosTime) import System.IO (stdin) import Helic.Data.AgentId (AgentId (AgentId)) import qualified Helic.Data.Event as Event import Helic.Data.InstanceName (InstanceName)-import Helic.Data.YankConfig (YankConfig (YankConfig))+import qualified Helic.Data.YankConfig+import Helic.Data.YankConfig (YankConfig) import qualified Helic.Effect.Client as Client import Helic.Effect.Client (Client) -- |Send an event to the server. yank ::- Members [Reader InstanceName, Client, ChronosTime, Error Text, Embed IO] r =>+ Members [Reader InstanceName, Client, ChronosTime, Log, Error Text, Embed IO] r => YankConfig -> Sem r ()-yank (YankConfig agent) = do- text <- embed (Text.hGetContents stdin)- event <- Event.now (AgentId (fromMaybe "cli" agent)) text- fromEither =<< Client.yank event+yank conf = do+ text <- fromMaybeA (embed (Text.hGetContents stdin)) conf.text+ event <- Event.now (AgentId (fromMaybe "cli" conf.agent)) text+ Client.yank event >>= leftA \ err -> Log.debug [exon|Http client error: #{err}|]
readme.md view
@@ -20,7 +20,7 @@ |Command|Meaning| |---|---| |`hel listen`|Start the daemon. This is best done from a *systemd* user service.|-|`hel yank`|Send standard input to the daemon as a manual yank event.|+|`hel yank`|Send standard input or argument to the daemon as a manual yank event.| |`hel list`|Print the event history.| |`hel load`|Load an older event to the clipboard, given its index into the history.| @@ -45,7 +45,7 @@ ## Nix The project uses a [Nix] [flake] to configure its build, and it is recommended to install or run it using *Nix* as well.-If *Nix* is installed and configured for use with *flakes*, the app can be run like this:+If *Nix* is installed and configured for use with *flakes*, the app can be run without installation like this: ```shell $ nix run github:tek/helic -- listen@@ -56,24 +56,25 @@ ## NixOS -The flake provides a *NixOS* module that can be used by adding it to `/etc/nixos/configuration.nix`:+The flake provides a *NixOS* module that can be used by adding it to `/etc/nixos/flake.nix`: ```nix { inputs.helic.url = "github:tek/helic"; outputs = { nixpkgs, helic, ... }: { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {- modules = [helic.nixosModule];+ modules = [helic.nixosModules.default]; services.helic.enable = true; }; }; } ``` -With this, a *systemd* user service will be started on login and the client will be in `$PATH`:+With this, a `systemd` user service will be started on login and the client will be in `$PATH`: ```shell $ echo 'yank me' | hel yank+$ hel yank --agent custom-name --text 'yank me' ``` After a rebuild, the service may not be started right away, so this command must be executed:@@ -82,7 +83,7 @@ $ systemctl --user start helic ``` -Globally enabled SystemD user services are started for all users on login.+Globally enabled `systemd` user services are started for all users on login. To prevent that, you can set the module option `services.helic.user = "myuser"`. ## Without Nix@@ -112,6 +113,8 @@ |`listen`|`--agent NAME`|Used to avoid sending yanks back to the application that sent them.| |`list`|positional (`hel list 5`)|Limit the number of printed events.| |`load`|positional (`hel load 5`)|Choose the index of the event to be loaded.|+|`yank`|`--agent NAME`|Custom name used in the `list` output and to avoid cycles.|+|`yank`|`--text TEXT`|Yank text, uses stdin if not specified.| # Configuring Helic @@ -151,6 +154,7 @@ name = "myhost"; maxHistory = 1000; verbose = true;+ user = "myuser"; net = { enable = true; port = 10001;
test/Helic/Dev.hs view
@@ -1,22 +1,39 @@ module Helic.Dev where import Conc (withAsync_)+import Exon (exon) import Log (Severity (Trace)) import System.Environment (setEnv)+import System.Random (randomIO) import qualified Time import Time (Seconds (Seconds)) -import Helic.App (listenApp)+import Helic.App (listenApp, yankApp)+import Helic.Config.File (findFileConfig) import Helic.Data.Config (Config (Config)) import Helic.Data.NetConfig (NetConfig (NetConfig))+import Helic.Data.YankConfig (YankConfig (YankConfig)) conf :: Config conf = Config (Just "dev") (Just def) (Just (NetConfig (Just True) (Just 11111) Nothing Nothing)) Nothing Nothing (Just True) -main :: IO ()-main =+listen :: IO ()+listen = runAppLevel Trace (withAsync_ setenv (listenApp conf)) where setenv = Time.sleep (Seconds 12) *> embed (setEnv "DISPLAY" ":0")++listenSystem :: IO ()+listenSystem =+ runAppLevel Trace do+ config <- findFileConfig Nothing+ listenApp config++yank :: IO ()+yank =+ runAppLevel Trace do+ config <- findFileConfig Nothing+ num :: Int64 <- embed randomIO+ yankApp config (YankConfig Nothing (Just [exon|yanky #{show num}|]))