packages feed

helic 0.2.0.0 → 0.3.0.0

raw patch · 24 files changed

+488/−253 lines, 24 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Helic.Net.Api: receiveEvent :: Members [Events resource Event, Reader InstanceName] r => Event -> Sem r ()
+ Helic.Data.LoadConfig: instance Data.Default.Class.Default Helic.Data.LoadConfig.LoadConfig
+ Helic.Data.LoadConfig: instance GHC.Classes.Eq Helic.Data.LoadConfig.LoadConfig
+ Helic.Data.LoadConfig: instance GHC.Generics.Generic Helic.Data.LoadConfig.LoadConfig
+ Helic.Data.LoadConfig: instance GHC.Show.Show Helic.Data.LoadConfig.LoadConfig
+ Helic.Effect.Client: [Load] :: Int -> Client m (Either Text Event)
+ Helic.Effect.Client: load :: forall r. Member Client r => Int -> Sem r (Either Text Event)
+ Helic.Effect.History: [Get] :: History m [Event]
+ Helic.Effect.History: [Load] :: Int -> History m (Maybe Event)
+ Helic.Effect.History: [Receive] :: Event -> History m ()
+ Helic.Effect.History: data History :: Effect
+ Helic.Effect.History: get :: forall r. Member History r => Sem r [Event]
+ Helic.Effect.History: load :: forall r. Member History r => Int -> Sem r (Maybe Event)
+ Helic.Effect.History: receive :: forall r. Member History r => Event -> Sem r ()
+ Helic.Interpreter.Agent: interpretAgent :: forall id r. (Event -> Sem r ()) -> InterpreterFor (Tagged id Agent) r
+ Helic.Interpreter.History: appendIfValid :: Time -> Event -> Seq Event -> Maybe (Seq Event)
+ Helic.Interpreter.History: broadcast :: Members Agents r => Member Log r => Event -> Sem r ()
+ Helic.Interpreter.History: inRecent :: Time -> Event -> Seq Event -> Bool
+ Helic.Interpreter.History: insertEvent :: Members [AtomicState (Seq Event), ChronosTime] r => Event -> Sem r Bool
+ Helic.Interpreter.History: interpretHistory :: Members Agents r => Members [Reader InstanceName, AtomicState (Seq Event), ChronosTime, Log] r => Maybe Int -> InterpreterFor History r
+ Helic.Interpreter.History: isNetworkCycle :: Member (Reader InstanceName) r => Event -> Sem r Bool
+ Helic.Interpreter.History: loadEvent :: Members [AtomicState (Seq Event), ChronosTime, Log] r => Int -> Sem r (Maybe Event)
+ Helic.Interpreter.History: receiveEvent :: Members Agents r => Members [AtomicState (Seq Event), ChronosTime, Log] r => Maybe Int -> Event -> Sem r ()
+ Helic.Interpreter.History: runAgent :: forall (tag :: AgentTag) r. AgentName tag => Member (Tagged tag Agent) r => Event -> Sem r ()
+ Helic.Interpreter.History: truncateLog :: Member (AtomicState (Seq Event)) r => Int -> Sem r (Maybe Int)
+ Helic.Listen: Listening :: Listening
+ Helic.Listen: data Listening
+ Helic.Listen: instance GHC.Classes.Eq Helic.Listen.Listening
+ Helic.Listen: instance GHC.Show.Show Helic.Listen.Listening
+ Helic.Listen: withListen :: Members [EventConsumer token Event, History, Resource, Race, Async, Embed IO] r => Sem r a -> Sem r a
- Helic: interpretAgentNet :: Members [Manager, Events resource Event, Reader InstanceName, Reader NetConfig] r => Members [AtomicState (Seq Event), Log, Interrupt, Race, Resource, Async, Embed IO, Final IO] r => InterpreterFor (Tagged AgentNet Agent) r
+ Helic: interpretAgentNet :: Members [Manager, Reader NetConfig] r => Members [Log, Interrupt, Race, Resource, Async, Embed IO, Final IO] r => InterpreterFor (Tagged AgentNet Agent) r
- Helic: listen :: Members Agents r => Members [EventConsumer token Event, AtomicState (Seq Event), ChronosTime, Log] r => Maybe Int -> Sem r ()
+ Helic: listen :: Members [EventConsumer token Event, History, Sync Listening] r => Sem r ()
- Helic: serve :: Members [Events resource Event, Reader NetConfig] r => Members [AtomicState (Seq Event), Reader InstanceName, Sync ServerReady, Log, Interrupt, Final IO] r => Sem r ()
+ Helic: serve :: Members [History, Reader NetConfig, Sync ServerReady, Log, Interrupt, Final IO] r => Sem r ()
- Helic: type Api = "event" :> (Get '[JSON] (Seq Event) :<|> ReqBody '[JSON] Event :> PostCreated '[JSON] ())
+ Helic: type Api = "event" :> (Get '[JSON] [Event] :<|> ReqBody '[JSON] Event :> PostCreated '[JSON] NoContent :<|> ReqBody '[JSON] Int :> Post '[JSON] (Maybe Event))
- Helic.Interpreter.AgentNet: interpretAgentNet :: Members [Manager, Events resource Event, Reader InstanceName, Reader NetConfig] r => Members [AtomicState (Seq Event), Log, Interrupt, Race, Resource, Async, Embed IO, Final IO] r => InterpreterFor (Tagged AgentNet Agent) r
+ Helic.Interpreter.AgentNet: interpretAgentNet :: Members [Manager, Reader NetConfig] r => Members [Log, Interrupt, Race, Resource, Async, Embed IO, Final IO] r => InterpreterFor (Tagged AgentNet Agent) r
- Helic.Listen: listen :: Members Agents r => Members [EventConsumer token Event, AtomicState (Seq Event), ChronosTime, Log] r => Maybe Int -> Sem r ()
+ Helic.Listen: listen :: Members [EventConsumer token Event, History, Sync Listening] r => Sem r ()
- Helic.Net.Api: serve :: Members [Events resource Event, Reader NetConfig] r => Members [AtomicState (Seq Event), Reader InstanceName, Sync ServerReady, Log, Interrupt, Final IO] r => Sem r ()
+ Helic.Net.Api: serve :: Members [History, Reader NetConfig, Sync ServerReady, Log, Interrupt, Final IO] r => Sem r ()
- Helic.Net.Api: server :: Members [Events resource Event, AtomicState (Seq Event), Reader InstanceName] r => ServerT Api (Sem r)
+ Helic.Net.Api: server :: Member History r => ServerT Api (Sem r)
- Helic.Net.Api: type Api = "event" :> (Get '[JSON] (Seq Event) :<|> ReqBody '[JSON] Event :> PostCreated '[JSON] ())
+ Helic.Net.Api: type Api = "event" :> (Get '[JSON] [Event] :<|> ReqBody '[JSON] Event :> PostCreated '[JSON] NoContent :<|> ReqBody '[JSON] Int :> Post '[JSON] (Maybe Event))

Files

changelog.md view
@@ -1,5 +1,9 @@ # Unreleased +# 0.3.0.0++* Add the `load` command that sets the clipboard to an event from the history.+ # 0.2.0.0  * Add the `list` command that prints the current history.
helic.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           helic-version:        0.2.0.0+version:        0.3.0.0 synopsis:       Clipboard Manager description:    See <https://hackage.haskell.org/package/helic/docs/Helic.html> category:       Clipboard@@ -38,6 +38,7 @@       Helic.Data.Host       Helic.Data.InstanceName       Helic.Data.ListConfig+      Helic.Data.LoadConfig       Helic.Data.NetConfig       Helic.Data.Selection       Helic.Data.TmuxConfig@@ -45,12 +46,15 @@       Helic.Data.YankConfig       Helic.Effect.Agent       Helic.Effect.Client+      Helic.Effect.History       Helic.Effect.XClipboard       Helic.Gtk+      Helic.Interpreter.Agent       Helic.Interpreter.AgentNet       Helic.Interpreter.AgentTmux       Helic.Interpreter.AgentX       Helic.Interpreter.Client+      Helic.Interpreter.History       Helic.Interpreter.InstanceName       Helic.Interpreter.XClipboard       Helic.List@@ -253,9 +257,11 @@   main-is: Main.hs   other-modules:       Helic.Test.ConfigFileTest+      Helic.Test.Fixtures       Helic.Test.InsertEventTest       Helic.Test.ListenTest       Helic.Test.ListTest+      Helic.Test.LoadTest       Paths_helic   hs-source-dirs:       test
lib/Helic/Cli.hs view
@@ -14,12 +14,14 @@   interpretEventsChan,   interpretInterrupt,   interpretRace,+  interpretSync,+  withAsync_,   ) import Polysemy.Error (errorToIOFinal)+import Polysemy.Http (Manager) import Polysemy.Http.Interpreter.Manager (interpretManager) import qualified Polysemy.Log as Log import Polysemy.Log (Log, Severity (Info, Trace), interpretLogStdoutLevelConc)-import Polysemy.Reader (runReader) import Polysemy.Time (MilliSeconds (MilliSeconds)) import System.IO (hLookAhead) @@ -28,16 +30,22 @@ import Helic.Data.Config (Config (Config)) import Helic.Data.Event (Event) import Helic.Data.ListConfig (ListConfig)+import Helic.Data.LoadConfig (LoadConfig (LoadConfig))+import Helic.Data.NetConfig (NetConfig) import Helic.Data.XClipboardEvent (XClipboardEvent) import Helic.Data.YankConfig (YankConfig (YankConfig))+import qualified Helic.Effect.Client as Client+import Helic.Effect.Client (Client)+import qualified Helic.Effect.History as History import Helic.Interpreter.AgentNet (interpretAgentNet) import Helic.Interpreter.AgentTmux (interpretAgentTmux) import Helic.Interpreter.AgentX (interpretAgentX) import Helic.Interpreter.Client (interpretClientNet)+import Helic.Interpreter.History (interpretHistory) import Helic.Interpreter.InstanceName (interpretInstanceName) import Helic.Interpreter.XClipboard (interpretXClipboardGtk, listenXClipboard) import Helic.List (list)-import Helic.Listen (listen)+import Helic.Net.Api (serve) import Helic.Yank (yank)  logError ::@@ -95,7 +103,10 @@   interpretAgentX $   interpretAgentNet $   interpretAgentTmux $-  listen maxHistory+  interpretHistory maxHistory $+  interpretSync $+  withAsync_ serve $+  Conc.subscribeLoop History.receive  yankApp ::   Config ->@@ -108,16 +119,31 @@   interpretClientNet $   yank yankConfig +runClient ::+  Members [Log, Error Text, Race, Embed IO] r =>+  Maybe NetConfig ->+  InterpretersFor [Client, Reader NetConfig, Manager] r+runClient net =+  interpretManager .+  runReader (fromMaybe def net) .+  interpretClientNet+ listApp ::   Config ->   ListConfig ->   Sem IOStack () listApp (Config _ _ net _) listConfig =   runReader listConfig $-  interpretManager $-  runReader (fromMaybe def net) $-  interpretClientNet $+  runClient net $   list++loadApp ::+  Config ->+  LoadConfig ->+  Sem IOStack ()+loadApp (Config _ _ net _) (LoadConfig event) =+  runClient net $+  (void . fromEither =<< Client.load event)  runCommand :: Config -> Command -> Sem IOStack () runCommand config = \case
+ lib/Helic/Data/LoadConfig.hs view
@@ -0,0 +1,11 @@+{-# options_haddock prune #-}++-- |LoadConfig Data Type, Internal+module Helic.Data.LoadConfig where++data LoadConfig =+  LoadConfig {+    event :: Int+  }+  deriving stock (Eq, Show, Generic)+  deriving anyclass (Default)
lib/Helic/Effect/Client.hs view
@@ -1,11 +1,37 @@--- Client Effect, Internal+-- |Client Effect, Internal module Helic.Effect.Client where +import Polysemy (makeSem_)+ import Helic.Data.Event (Event) --- An abstraction of the API, used by the @list@ command.+-- |An abstraction of the API, used by the @list@ command. data Client :: Effect where+  -- |Return all events currently in memory.   Get :: Client m (Either Text [Event])+  -- |Add a new event.   Yank :: Event -> Client m (Either Text ())+  -- |Broadcast an older event.+  Load :: Int -> Client m (Either Text Event) -makeSem ''Client+makeSem_ ''Client++-- |Return all events currently in memory.+get ::+  ∀ r .+  Member Client r =>+  Sem r (Either Text [Event])++-- |Add a new event.+yank ::+  ∀ r .+  Member Client r =>+  Event ->+  Sem r (Either Text ())++-- |Broadcast an older event.+load ::+  ∀ r .+  Member Client r =>+  Int ->+  Sem r (Either Text Event)
+ lib/Helic/Effect/History.hs view
@@ -0,0 +1,12 @@+-- |History Effect, Internal+module Helic.Effect.History where++import Helic.Data.Event (Event)++-- |The core actions of the 'Event' history.+data History :: Effect where+  Get :: History m [Event]+  Receive :: Event -> History m ()+  Load :: Int -> History m (Maybe Event)++makeSem ''History
+ lib/Helic/Interpreter/Agent.hs view
@@ -0,0 +1,15 @@+module Helic.Interpreter.Agent where++import Polysemy.Tagged (Tagged, untag)++import Helic.Data.Event (Event)+import Helic.Effect.Agent (Agent (Update))++interpretAgent ::+  ∀ id r .+  (Event -> Sem r ()) ->+  InterpreterFor (Tagged id Agent) r+interpretAgent handle sem =+  interpreting (untag sem) \case+    Update e ->+      handle e
lib/Helic/Interpreter/AgentNet.hs view
@@ -1,30 +1,26 @@ -- |Agent Interpreter for Network, Internal module Helic.Interpreter.AgentNet where -import Polysemy.Conc (Events, Interrupt, interpretSync, withAsync_)+import Polysemy.Conc (Interrupt) import Polysemy.Http (Manager) import qualified Polysemy.Log as Log import Polysemy.Log (Log) import Polysemy.Tagged (Tagged, untag)  import Helic.Data.Event (Event (source))-import Helic.Data.InstanceName (InstanceName) import Helic.Data.NetConfig (NetConfig (NetConfig)) import Helic.Effect.Agent (Agent (Update), AgentNet, agentIdNet)-import Helic.Net.Api (serve) import Helic.Net.Client (sendTo)  -- |Interpret 'Agent' using remote hosts as targets. -- This also starts the HTTP server that listens to network events, which are used both for remote hosts and CLI -- events. interpretAgentNet ::-  Members [Manager, Events resource Event, Reader InstanceName, Reader NetConfig] r =>-  Members [AtomicState (Seq Event), Log, Interrupt, Race, Resource, Async, Embed IO, Final IO] r =>+  Members [Manager, Reader NetConfig] r =>+  Members [Log, Interrupt, Race, Resource, Async, Embed IO, Final IO] r =>   InterpreterFor (Tagged AgentNet Agent) r interpretAgentNet sem =-  interpretSync $-  withAsync_ serve $-  interpreting (raiseUnder (untag sem)) \case+  interpreting (untag sem) \case     Update e -> do       NetConfig _ timeout hosts <- ask       for_ (fold hosts) \ host ->
lib/Helic/Interpreter/Client.hs view
@@ -1,3 +1,4 @@+-- |Client Interpreter, Internal module Helic.Interpreter.Client where  import Polysemy.Http (Manager)@@ -8,10 +9,11 @@ import Helic.Data.Event (Event) import qualified Helic.Data.NetConfig as NetConfig import Helic.Data.NetConfig (NetConfig)-import Helic.Effect.Client (Client (Get, Yank))+import Helic.Effect.Client (Client (Get, Load, Yank)) import qualified Helic.Net.Client as Api import Helic.Net.Client (localhost, localhostUrl, sendTo) +-- |Interpret 'Client' via HTTP. interpretClientNet ::   Members [Manager, Reader NetConfig, Log, Error Text, Race, Embed IO] r =>   InterpreterFor Client r@@ -24,7 +26,12 @@       host <- localhost       timeout <- asks NetConfig.timeout       runError (sendTo timeout host event)+    Load event -> do+      env <- mkClientEnv <$> Manager.get <*> localhostUrl+      result <- mapLeft show <$> embed (runClientM (Api.load event) env)+      pure (result >>= maybeToRight "There is no event for that index") +-- |Interpret 'Client' with a constant list of 'Event's and no capability to yank. interpretClientConst ::   [Event] ->   InterpreterFor Client r@@ -32,3 +39,4 @@   interpret \case     Get -> pure (Right evs)     Yank _ -> pure (Left "const client cannot yank")+    Load _ -> pure (Left "const client cannot load")
+ lib/Helic/Interpreter/History.hs view
@@ -0,0 +1,168 @@+{-# options_haddock prune #-}++-- |Core daemon logic, Internal+module Helic.Interpreter.History where++import qualified Chronos+import qualified Data.Sequence as Seq+import Data.Sequence (Seq ((:|>)), (!?), (|>))+import Polysemy.AtomicState (atomicState')+import Polysemy.Chronos (ChronosTime)+import qualified Polysemy.Log as Log+import Polysemy.Log (Log)+import Polysemy.Tagged (Tagged, tag)+import qualified Polysemy.Time as Time+import Polysemy.Time (Seconds (Seconds), convert)+import Polysemy.Time.Diff (diff)++import Helic.Data.AgentId (AgentId (AgentId))+import qualified Helic.Data.Event as Event+import Helic.Data.Event (Event (Event, time))+import Helic.Data.InstanceName (InstanceName)+import qualified Helic.Effect.Agent as Agent+import Helic.Effect.Agent (Agent, AgentName, AgentNet, AgentTag, AgentTmux, AgentX, Agents, agentIdNet, agentName)+import qualified Helic.Effect.History as History+import Helic.Effect.History (History)++-- |Send an event to an agent unless it was published by that agent.+runAgent ::+  ∀ (tag :: AgentTag) r .+  AgentName tag =>+  Member (Tagged tag Agent) r =>+  Event ->+  Sem r ()+runAgent (Event _ (AgentId eId) _ _) | eId == agentName @tag =+  unit+runAgent e =+  tag (Agent.update e)++-- |Send an event to all agents.+broadcast ::+  Members Agents r =>+  Member Log r =>+  Event ->+  Sem r ()+broadcast event@(Event _ (AgentId ag) _ text) = do+  Log.debug [exon|broadcasting from #{ag}: #{show text}|]+  runAgent @AgentTmux event+  runAgent @AgentNet event+  runAgent @AgentX event++-- |Whether there was an event within the last second that contained the same text as the current event.+inRecent ::+  Chronos.Time ->+  Event ->+  Seq Event ->+  Bool+inRecent now (Event _ _ _ c) =+  any ((c ==) . Event.content) . Seq.takeWhileR newer+  where+    newer (Event _ _ t _) =+      diff now t <= convert (Seconds 1)++-- |Append an event to the history unless the newest event contains the same text or there was an event within the last+-- second that contained the same text, to avoid clobbering due to cycles induced by external programs.+appendIfValid ::+  Chronos.Time ->+  Event ->+  Seq Event ->+  Maybe (Seq Event)+appendIfValid now e = \case+  Seq.Empty ->+    Just (Seq.singleton e)+  _ :|> Event _ _ _ newest | newest == Event.content e ->+    Nothing+  hist | inRecent now e hist ->+    Nothing+  hist ->+    Just (hist |> e)++-- |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+  now <- Time.now+  atomicState' \ s -> result s (appendIfValid now e s)+  where+    result s = \case+      Just new -> (new, True)+      Nothing -> (s, False)++-- |Remove excess entries from the front of the 'Seq', given a maximum number of entries.+-- Return the number of dropped entries.+truncateLog ::+  Member (AtomicState (Seq Event)) r =>+  Int ->+  Sem r (Maybe Int)+truncateLog maxHistory =+  atomicState' \ evs -> do+    let dropped = length evs - maxHistory+    if dropped > 0+    then (Seq.drop dropped evs, Just dropped)+    else (evs, Nothing)++logTruncation ::+  Member Log r =>+  Int ->+  Sem r ()+logTruncation num =+  Log.info [exon|removed #{show num} #{noun} from the history.|]+  where+    noun =+      if num == 1 then "entry" else "entries"++-- |Process an event received from outside.+receiveEvent ::+  Members Agents r =>+  Members [AtomicState (Seq Event), ChronosTime, Log] r =>+  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)++-- |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.+loadEvent ::+  Members [AtomicState (Seq Event), ChronosTime, Log] r =>+  Int ->+  Sem r (Maybe Event)+loadEvent index = do+  now <- Time.now+  atomicState' \ s ->+    case (s !? (length s - index - 1)) of+      Just event ->+        (Seq.deleteAt index s |> event { time = now }, Just event)+      Nothing ->+        (s, Nothing)++-- |In the unlikely case of a remote host sending an event back to this instance and not updating the sender, this will+-- be 'True'.+isNetworkCycle ::+  Member (Reader InstanceName) r =>+  Event ->+  Sem r Bool+isNetworkCycle Event {..} = do+  name <- ask+  pure (name == sender && source == agentIdNet)++-- |Interpret 'History' as 'AtomicState', broadcasting to agents.+interpretHistory ::+  Members Agents r =>+  Members [Reader InstanceName, AtomicState (Seq Event), ChronosTime, Log] r =>+  Maybe Int ->+  InterpreterFor History r+interpretHistory maxHistory =+  interpret \case+    History.Get ->+      toList <$> atomicGet+    History.Receive event ->+      unlessM (isNetworkCycle event) do+        receiveEvent maxHistory event+    History.Load index ->+      loadEvent index
lib/Helic/Interpreter/InstanceName.hs view
@@ -3,7 +3,6 @@  import Network.HostName (getHostName) import qualified Polysemy.Error as Polysemy-import Polysemy.Reader (runReader)  import Helic.Data.InstanceName (InstanceName (InstanceName)) 
lib/Helic/Interpreter/XClipboard.hs view
@@ -7,7 +7,6 @@ import Polysemy.Conc (Events, withAsync_) import qualified Polysemy.Log as Log import Polysemy.Log (Log)-import Polysemy.Reader (runReader) import Polysemy.Resource (bracket)  import qualified Helic.Data.GtkState as GtkState
lib/Helic/List.hs view
@@ -4,6 +4,7 @@ module Helic.List where  import Chronos (Datetime (Datetime), SubsecondPrecision (SubsecondPrecisionFixed), builder_HMS, timeToDatetime)+import qualified Data.Text as Text import Data.Text.Lazy.Builder (toLazyText) import qualified System.Console.Terminal.Size as TerminalSize import Text.Layout.Table (center, column, expandUntil, fixedCol, left, right, rowG, tableString, titlesH, unicodeRoundS)@@ -16,6 +17,28 @@ import qualified Helic.Effect.Client as Client import Helic.Effect.Client (Client) +truncateLines :: Int -> Text -> Text+truncateLines maxWidth a =+  case Text.lines a of+    [] ->+      a+    [firstLine] ->+      firstLine+    firstLine : (length -> count) ->+      let+        lineIndicator =+          [exon| [#{show (count + 1)} lines]|]+        maxlen =+          maxWidth - Text.length lineIndicator+      in [exon|#{Text.take maxlen firstLine}#{lineIndicator}|]++eventColumns :: Int -> Int -> Event -> [Text]+eventColumns maxWidth i Event {..} =+  [show i, coerce sender, coerce source, toStrict (formatTime (timeToDatetime time)), truncateLines maxWidth content]+  where+    formatTime (Datetime _ tod) =+      toLazyText (builder_HMS (SubsecondPrecisionFixed 0) (Just ':') tod)+ format :: Int -> [Event] -> String format width events =   tableString cols unicodeRoundS titles (row <$> zip [lastIndex,lastIndex-1..0] events)@@ -28,12 +51,10 @@       column (expandUntil w) al def def     titles =       titlesH ["#", "Instance", "Agent", "Time", "Content"]-    row (i, Event {..}) =-      rowG (toString <$> [show i, coerce sender, coerce source, toStrict (formatTime (timeToDatetime time)), content])-    formatTime (Datetime _ tod) =-      toLazyText (builder_HMS (SubsecondPrecisionFixed 0) (Just ':') tod)+    row (i, event) =+      rowG (toString <$> eventColumns contentWidth i event)     contentWidth =-      max 20 (width - 40)+      min 100 (max 20 (width - 40))  -- |Fetch all events from the server, limit them to the configured number and format them in a nice table. buildList ::@@ -43,8 +64,10 @@   history <- fromEither =<< Client.get   limit <- asks ListConfig.limit   let+    dropper l =+      drop (length history - l)     events =-      maybe id take limit (reverse (toList history))+      maybe id dropper limit (toList history)   width <- fromMaybe 80 . fmap TerminalSize.width <$> embed TerminalSize.size   pure (format width events) 
lib/Helic/Listen.hs view
@@ -1,125 +1,38 @@ {-# options_haddock prune #-} --- |Daemon Logic, Internal+-- |Listen, Internal module Helic.Listen where -import qualified Chronos-import qualified Data.Sequence as Seq-import Data.Sequence (Seq ((:|>)), (|>))-import Polysemy.AtomicState (atomicState')-import Polysemy.Chronos (ChronosTime) import qualified Polysemy.Conc as Conc-import Polysemy.Conc (EventConsumer)-import qualified Polysemy.Log as Log-import Polysemy.Log (Log)-import Polysemy.Tagged (Tagged, tag)-import qualified Polysemy.Time as Time-import Polysemy.Time (Seconds (Seconds), convert)-import Polysemy.Time.Diff (diff)--import Helic.Data.AgentId (AgentId (AgentId))-import qualified Helic.Data.Event as Event-import Helic.Data.Event (Event (Event))-import qualified Helic.Effect.Agent as Agent-import Helic.Effect.Agent (Agent, AgentName, AgentNet, AgentTag, AgentTmux, AgentX, Agents, agentName)--runAgent ::-  ∀ (tag :: AgentTag) r .-  AgentName tag =>-  Member (Tagged tag Agent) r =>-  Event ->-  Sem r ()-runAgent (Event _ (AgentId eId) _ _) | eId == agentName @tag =-  unit-runAgent e =-  tag (Agent.update e)--broadcast ::-  Members Agents r =>-  Member Log r =>-  Event ->-  Sem r ()-broadcast event@(Event _ (AgentId ag) _ text) = do-  Log.debug [exon|broadcasting from #{ag}: #{show text}|]-  runAgent @AgentTmux event-  runAgent @AgentNet event-  runAgent @AgentX event--inRecent ::-  Chronos.Time ->-  Event ->-  Seq Event ->-  Bool-inRecent now (Event _ _ _ c) =-  any ((c ==) . Event.content) . Seq.takeWhileR newer-  where-    newer (Event _ _ t _) =-      diff now t <= convert (Seconds 1)--appendIfValid ::-  Chronos.Time ->-  Event ->-  Seq Event ->-  Maybe (Seq Event)-appendIfValid now e = \case-  Seq.Empty ->-    Just (Seq.singleton e)-  _ :|> Event _ _ _ newest | newest == Event.content e ->-    Nothing-  hist | inRecent now e hist ->-    Nothing-  hist ->-    Just (hist |> e)--insertEvent ::-  Members [AtomicState (Seq Event), ChronosTime] r =>-  Event ->-  Sem r Bool-insertEvent e = do-  now <- Time.now-  atomicState' \ s -> result s (appendIfValid now e s)-  where-    result s = \case-      Just new -> (new, True)-      Nothing -> (s, False)--truncateLog ::-  Member (AtomicState (Seq Event)) r =>-  Int ->-  Sem r (Maybe Int)-truncateLog maxHistory =-  atomicState' \ evs ->-    if length evs > maxHistory-    then (Seq.drop 1 evs, Just (length evs - maxHistory))-    else (evs, Nothing)+import Polysemy.Conc (EventConsumer, Sync, interpretSync, withAsync_)+import qualified Polysemy.Conc.Sync as Sync -logTruncation ::-  Member Log r =>-  Int ->-  Sem r ()-logTruncation num =-  Log.info [exon|removed #{show num} #{noun} from the history.|]-  where-    noun =-      if num == 1 then "entry" else "entries"+import Helic.Data.Event (Event)+import qualified Helic.Effect.History as History+import Helic.Effect.History (History) -handleEvent ::-  Members Agents r =>-  Members [AtomicState (Seq Event), ChronosTime, Log] r =>-  Maybe Int ->-  Event ->-  Sem r ()-handleEvent maxHistory e = do-  Log.debug [exon|listen: #{show e}|]-  whenM (insertEvent e) do-    broadcast e-    traverse_ logTruncation =<< truncateLog (fromMaybe 100 maxHistory)+-- |Signal type that indicates that the subscriber of 'listen' is running.+data Listening =+  Listening+  deriving stock (Eq, Show)  -- |Listen for 'Event' via 'Polysemy.Conc.Events', broadcasting them to agents. listen ::-  Members Agents r =>-  Members [EventConsumer token Event, AtomicState (Seq Event), ChronosTime, Log] r =>-  Maybe Int ->+  Members [EventConsumer token Event, History, Sync Listening] r =>   Sem r ()-listen maxHistory =-  Conc.subscribeLoop (handleEvent maxHistory)+listen =+  Conc.subscribe do+    Sync.putBlock Listening+    (forever (History.receive =<< Conc.consume))++-- |Run an action with 'listen' in a thread, waiting for the event subscriber to be up and running before executing the+-- action.+withListen ::+  Members [EventConsumer token Event, History, Resource, Race, Async, Embed IO] r =>+  Sem r a ->+  Sem r a+withListen ma =+  interpretSync $+  withAsync_ listen do+    Listening <- Sync.takeBlock+    raise ma
lib/Helic/Net/Api.hs view
@@ -1,45 +1,38 @@ -- |HTTP API of the Daemon, Internal module Helic.Net.Api where -import qualified Polysemy.Conc as Conc-import Polysemy.Conc (Events, Interrupt, Sync)+import Polysemy.Conc (Interrupt, Sync) import Polysemy.Log (Log)-import Servant (Get, JSON, PostCreated, ReqBody, type (:<|>) ((:<|>)), type (:>))+import Servant (Get, JSON, NoContent (NoContent), Post, PostCreated, ReqBody, type (:<|>) ((:<|>)), type (:>)) import Servant.Server (Context (EmptyContext), ServerT) -import Helic.Data.Event (Event (Event, sender, source))-import Helic.Data.InstanceName (InstanceName)+import Helic.Data.Event (Event) import qualified Helic.Data.NetConfig as NetConfig import Helic.Data.NetConfig (NetConfig)-import Helic.Effect.Agent (agentIdNet)+import qualified Helic.Effect.History as History+import Helic.Effect.History (History) import Helic.Net.Server (ServerReady, runServerWithContext)  -- |The Servant API of the daemon, providing endpoints for getting all events and creating one. type Api =   "event" :> (-    Get '[JSON] (Seq Event)+    Get '[JSON] [Event]     :<|>-    ReqBody '[JSON] Event :> PostCreated '[JSON] ()+    ReqBody '[JSON] Event :> PostCreated '[JSON] NoContent+    :<|>+    ReqBody '[JSON] Int :> Post '[JSON] (Maybe Event)   ) --- |Publish a received event unless it was sent by the network agent of this instance.-receiveEvent ::-  Members [Events resource Event, Reader InstanceName] r =>-  Event ->-  Sem r ()-receiveEvent e@Event {sender, source} = do-  name <- ask-  unless (name == sender && source == agentIdNet) do-    Conc.publish e- -- |The server implementation. server ::-  Members [Events resource Event, AtomicState (Seq Event), Reader InstanceName] r =>+  Member History r =>   ServerT Api (Sem r) server =-  atomicGet+  History.get   :<|>-  receiveEvent+  as NoContent . History.receive+  :<|>+  History.load  -- |The default port, 9500. defaultPort :: Int@@ -48,8 +41,7 @@  -- |Run the daemon API. serve ::-  Members [Events resource Event, Reader NetConfig] r =>-  Members [AtomicState (Seq Event), Reader InstanceName, Sync ServerReady, Log, Interrupt, Final IO] r =>+  Members [History, Reader NetConfig, Sync ServerReady, Log, Interrupt, Final IO] r =>   Sem r () serve = do   port <- asks NetConfig.port
lib/Helic/Net/Client.hs view
@@ -9,7 +9,7 @@ import qualified Polysemy.Log as Log import Polysemy.Log (Log) import Polysemy.Time (MilliSeconds (MilliSeconds))-import Servant (type (:<|>) ((:<|>)))+import Servant (NoContent, type (:<|>) ((:<|>))) import Servant.Client (BaseUrl, ClientM, client, mkClientEnv, parseBaseUrl, runClientM)  import Helic.Data.Event (Event)@@ -18,9 +18,10 @@ import Helic.Data.NetConfig (NetConfig, Timeout) import Helic.Net.Api (Api, defaultPort) -get :: ClientM (Seq Event)-yank :: Event -> ClientM ()-get :<|> yank = client (Proxy @Api)+get :: ClientM [Event]+yank :: Event -> ClientM NoContent+load :: Int -> ClientM (Maybe Event)+get :<|> yank :<|> load = client (Proxy @Api)  sendTo ::   Members [Manager, Log, Race, Error Text, Embed IO] r =>@@ -39,7 +40,7 @@       mkClientEnv mgr url     req =       fmap (mapLeft show) <$> tryAny (runClientM (yank event) env)-  fromEither =<< fromEither =<< Conc.timeoutAs_ (Left "timed out") timeout req+  void . fromEither =<< fromEither =<< Conc.timeoutAs_ (Left "timed out") timeout req  localhost ::   Member (Reader NetConfig) r =>
lib/Helic/Prelude.hs view
@@ -73,7 +73,7 @@ import Polysemy.Internal.CustomErrors (FirstOrder) import Polysemy.Internal.Kind (Append) import Polysemy.Internal.Tactics (liftT)-import Polysemy.Reader (Reader, ask, asks)+import Polysemy.Reader (Reader, ask, asks, runReader) import Polysemy.Resource (Resource, resourceToIOFinal, runResource) import Polysemy.State (State, evalState, get, gets, modify, modify', put, runState) import Relude hiding (@@ -178,3 +178,12 @@ interpreting s h =   interpret h s {-# inline interpreting #-}++as ::+  Functor m =>+  a ->+  m b ->+  m a+as =+  (<$)+{-# inline as #-}
readme.md view
@@ -15,13 +15,14 @@  Several yank events are stored in memory in order to detect duplicates and cycles. -The CLI understands three different commands:+The CLI understands four different commands:  |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 list`|Print the event history.|+|`hel load`|Load an older event to the clipboard, given its index into the history.|  # Installing and Running Helic @@ -34,6 +35,7 @@ $ nix run github:tek/helic -- listen $ echo 'yank me' | nix run github:tek/helic -- yank --agent cli $ nix run github:tek/helic -- list 100+$ nix run github:tek/helic -- load 5 ```  ## NixOS@@ -90,6 +92,7 @@ |Global|`--config-file FILE`|Use the specified file path instead of the default locations.| |`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.|  # Configuring Helic 
+ test/Helic/Test/Fixtures.hs view
@@ -0,0 +1,10 @@+module Helic.Test.Fixtures where++import qualified Chronos+import Chronos (datetimeToTime)+import Polysemy.Chronos ()+import Polysemy.Time (mkDatetime)++testTime :: Chronos.Time+testTime =+  datetimeToTime (mkDatetime 2030 1 1 12 0 0)
test/Helic/Test/InsertEventTest.hs view
@@ -10,7 +10,7 @@ import Torsor (add)  import Helic.Data.Event (Event (Event))-import Helic.Listen (appendIfValid)+import Helic.Interpreter.History (appendIfValid)  old :: Chronos.Time old =
test/Helic/Test/ListTest.hs view
@@ -1,62 +1,47 @@ module Helic.Test.ListTest where -import qualified Chronos-import Chronos (datetimeToTime) import Polysemy.Chronos (interpretTimeChronosConstant)-import Polysemy.Conc (-  Events,-  Queue,-  )-import qualified Polysemy.Conc.Queue as Queue import Polysemy.Error (errorToIOFinal)-import Polysemy.Reader (runReader)-import Polysemy.Tagged (Tagged, untag) import Polysemy.Test (UnitTest, assertRight, runTestAuto)-import Polysemy.Time (mkDatetime)  import Helic.Data.AgentId (AgentId (AgentId)) import qualified Helic.Data.Event as Event-import Helic.Data.Event (Event (Event, content))-import Helic.Data.InstanceName (InstanceName) import Helic.Data.ListConfig (ListConfig (ListConfig))-import Helic.Effect.Agent (Agent (Update)) import Helic.Interpreter.Client (interpretClientConst) import Helic.List (buildList)-import Helic.Net.Api (receiveEvent)--testTime :: Chronos.Time-testTime =-  datetimeToTime (mkDatetime 2030 1 1 12 0 0)--interpretAgent ::-  ∀ id r .-  (Event -> Sem r ()) ->-  InterpreterFor (Tagged id Agent) r-interpretAgent handle sem =-  interpreting (untag sem) \case-    Update e ->-      handle e+import Helic.Test.Fixtures (testTime) -handleNet ::-  Members [Events resource Event, Reader InstanceName] r =>-  Event ->-  Sem r ()-handleNet (Event {..}) =-  receiveEvent (Event "test" (AgentId "net") testTime content)+eventContents :: [Text]+eventContents =+  [+    "extra",+    "extra",+    "extra",+    "extra",+    "extra",+    "single line",+    "single line with newline\n",+    "three lines 1\nthree lines 2\nthree lines 3"+  ] -handleLog ::-  Member (Queue Text) r =>-  Event ->-  Sem r ()-handleLog Event {content} =-  Queue.write content+target :: String+target =+  [exon|╭───┬──────────┬───────┬──────────┬──────────────────────────╮+│ # │ Instance │ Agent │   Time   │         Content          │+╞═══╪══════════╪═══════╪══════════╪══════════════════════════╡+│ 2 │   test   │ nvim  │ 12:00:00 │ single line              │+├───┼──────────┼───────┼──────────┼──────────────────────────┤+│ 1 │   test   │ nvim  │ 12:00:00 │ single line with newline │+├───┼──────────┼───────┼──────────┼──────────────────────────┤+│ 0 │   test   │ nvim  │ 12:00:00 │ three lines 1 [3 lines]  │+╰───┴──────────┴───────┴──────────┴──────────────────────────╯|] -test_listen :: UnitTest-test_listen =+test_list :: UnitTest+test_list =   runTestAuto $   interpretTimeChronosConstant testTime $   runReader "test" $-  runReader (ListConfig (Just 8)) do-    events <- traverse (Event.now (AgentId "nvim") . show) ([1..10] :: [Int])+  runReader (ListConfig (Just 3)) do+    events <- traverse (Event.now (AgentId "nvim")) eventContents     interpretClientConst events do-      assertRight 873 . fmap length =<< errorToIOFinal buildList+      assertRight target =<< errorToIOFinal buildList
test/Helic/Test/ListenTest.hs view
@@ -1,7 +1,6 @@ module Helic.Test.ListenTest where -import qualified Chronos-import Chronos (datetimeToTime)+import qualified Data.Set as Set import Polysemy.Chronos (interpretTimeChronosConstant) import qualified Polysemy.Conc as Conc import Polysemy.Conc (@@ -11,43 +10,28 @@   interpretEventsChan,   interpretQueueTBM,   interpretRace,+  interpretSync,   resultToMaybe,-  withAsync_,   ) import qualified Polysemy.Conc.Queue as Queue import Polysemy.Log (interpretLogNull)-import Polysemy.Reader (runReader)-import Polysemy.Tagged (Tagged, untag)-import Polysemy.Test (UnitTest, assertJust, runTestAuto)-import Polysemy.Time (mkDatetime)+import Polysemy.Test (UnitTest, assertEq, assertJust, runTestAuto)  import Helic.Data.AgentId (AgentId (AgentId)) import qualified Helic.Data.Event as Event import Helic.Data.Event (Event (Event, content))-import Helic.Data.InstanceName (InstanceName)-import Helic.Effect.Agent (Agent (Update), AgentNet, AgentTmux, AgentX)-import Helic.Listen (listen)-import Helic.Net.Api (receiveEvent)--testTime :: Chronos.Time-testTime =-  datetimeToTime (mkDatetime 2030 1 1 12 0 0)--interpretAgent ::-  ∀ id r .-  (Event -> Sem r ()) ->-  InterpreterFor (Tagged id Agent) r-interpretAgent handle sem =-  interpreting (untag sem) \case-    Update e ->-      handle e+import Helic.Effect.Agent (AgentNet, AgentTmux, AgentX)+import Helic.Interpreter.Agent (interpretAgent)+import Helic.Interpreter.History (interpretHistory)+import Helic.Listen (withListen)+import Helic.Test.Fixtures (testTime)  handleNet ::-  Members [Events resource Event, Reader InstanceName] r =>+  Member (Events res Event) r =>   Event ->   Sem r () handleNet (Event {..}) =-  receiveEvent (Event "test" (AgentId "net") testTime content)+  Conc.publish (Event "test" (AgentId "net") testTime content)  handleLog ::   Member (Queue Text) r =>@@ -66,15 +50,17 @@   interpretEventsChan $   interpretAtomic def $   interpretQueueTBM 64 $+  interpretSync $   runReader "test" $   interpretAgent @AgentNet handleNet $   interpretAgent @AgentTmux handleLog $-  interpretAgent @AgentX (const unit) do-    result <- withAsync_ (listen (Just 5)) do-      Conc.subscribe do-        let-          pub n =-            Conc.publish =<< Event.now (AgentId "nvim") (show n)-        traverse_ pub ([1..10] :: [Int])+  interpretAgent @AgentX (const unit) $+  interpretHistory (Just 5) do+    result <- withListen do+      let+        pub n =+          Conc.publish =<< Event.now (AgentId "nvim") (show n)+      traverse_ pub ([1..10] :: [Int])       traverse resultToMaybe <$> replicateM 10 Queue.read-    assertJust (show <$> ([1..10] :: [Int])) result+    assertEq Nothing . resultToMaybe =<< Queue.tryRead+    assertJust (Set.map show ([1..10] :: Set Int)) (Set.fromList <$> result)
+ test/Helic/Test/LoadTest.hs view
@@ -0,0 +1,39 @@+module Helic.Test.LoadTest where++import Polysemy.Chronos (ChronosTime, interpretTimeChronosConstant)+import Polysemy.Conc (interpretAtomic)+import Polysemy.Log (interpretLogNull)+import Polysemy.Test (UnitTest, assertEq, assertJust, runTestAuto)++import Helic.Data.AgentId (AgentId (AgentId))+import qualified Helic.Data.Event as Event+import Helic.Data.Event (Event)+import Helic.Data.InstanceName (InstanceName)+import Helic.Effect.Agent (AgentNet, AgentTmux, AgentX)+import qualified Helic.Effect.History as History+import Helic.Interpreter.Agent (interpretAgent)+import Helic.Interpreter.History (interpretHistory)+import Helic.Test.Fixtures (testTime)++event ::+  Members [ChronosTime, Reader InstanceName] r =>+  Int ->+  Sem r Event+event n =+  Event.now (AgentId "test") (show n)++test_load :: UnitTest+test_load =+  runTestAuto $+  interpretTimeChronosConstant testTime $+  interpretLogNull $+  runReader "test" $+  interpretAtomic def $+  interpretAgent @AgentNet (const unit) $+  interpretAgent @AgentTmux (const unit) $+  interpretAgent @AgentX (const unit) $+  interpretHistory Nothing do+    atomicPut =<< traverse event [1..10]+    ev5 <- event 6+    assertJust ev5 =<< History.load 4+    assertEq Nothing =<< History.load 11
test/Main.hs view
@@ -1,17 +1,21 @@ module Main where +import Helic.Test.ConfigFileTest (test_readConfigFile) import Helic.Test.InsertEventTest (test_insertEvent)+import Helic.Test.ListTest (test_list)+import Helic.Test.ListenTest (test_listen)+import Helic.Test.LoadTest (test_load) import Polysemy.Test (unitTest) import Test.Tasty (TestTree, defaultMain, testGroup)-import Helic.Test.ConfigFileTest (test_readConfigFile)-import Helic.Test.ListenTest (test_listen)  tests :: TestTree tests =   testGroup "all" [     unitTest "insert an event" test_insertEvent,     unitTest "parse a config file" test_readConfigFile,-    unitTest "listen for events, filter duplicates from network feedback" test_listen+    unitTest "listen for events, filter duplicates from network feedback" test_listen,+    unitTest "print the history" test_list,+    unitTest "load an old event to the clipboard" test_load   ]  main :: IO ()