packages feed

glirc 2.10 → 2.11

raw patch · 27 files changed

+891/−333 lines, 27 filesdep +processdep ~irc-coredep ~vty

Dependencies added: process

Dependency ranges changed: irc-core, vty

Files

ChangeLog.md view
@@ -1,5 +1,17 @@ # Revision history for glirc2 +## 2.11++* Add `M-S` to jump to previously focused window+* Add `extra-highlights` section+* Tab complete servernames in `/connect`+* Add `/windows` command for listing active windows+* Add `glirc_clear_window` C API procedure+* Allow `process_message` callback to drop messages+* Add optional network and channel arguments to `/clear` (intended to assist macros)+* Automatically reconnect on ping timeout+* Many commands will report message to client window on error+ ## 2.10  * Fixes for multiline editing
README.md view
@@ -101,6 +101,8 @@     commands:       * "extension Lua some-parameter $network $channel" +extra-highlights: ["glirc", "lens"]+ palette:   time:     fg: [10,10,10] -- RGB values for color for timestamps@@ -125,6 +127,7 @@ * `palette` - Client color overrides * `window-names` - text - Names of windows (typically overridden on non QWERTY layouts) * `nick-padding` - nonnegative integer - Nicks are padded until they have the specified length+* `extra-highlights` - list of text - Extra words/nicks to highlight * `extensions` - list of text - Filenames of extension to load  Settings@@ -205,7 +208,9 @@ * `/reconnect` - Reconnect to the current server * `/reload` - Reload the previous configuration file (not retroactive!) * `/reload <path>` - Load a new configuration file+* `/windows` - List all open windows * `/extension <extension name> <params>` - Send the given params to the named extension+* `/exec [-n network] [-c channel] <command> <arguments>` - Execute a command, If no network or channel are provided send output to client window, if network and channel are provided send output as messages, if network is provided send output as raw IRC messages.  Connection commands @@ -216,7 +221,7 @@  * `/focus <server>` - Change focus to server window * `/focus <server> <channel>` - Change focus to channel window-* `/clear` - Clear contents of current window+* `/clear [network] [channel]` - Clear contents of current or specified window * `/ignore <nick>` - Toggle ignore of a user * `/channel <channel>` - Change focus to channel on current network (alias: `/c`) @@ -282,6 +287,7 @@ * `^P` previous channel * `M-#` jump to window - `1234567890qwertyuiop!@#$%^&*()QWERTYUIOP` * `M-A` jump to activity+* `M-S` jump to previous window * `^A` beginning of line * `^E` end of line * `^K` delete to end
exported_symbols.txt view
@@ -6,4 +6,6 @@ glirc_list_channels; glirc_list_channel_users; glirc_my_nick;+glirc_mark_seen;+glirc_clear_window; };
glirc.cabal view
@@ -1,5 +1,5 @@ name:                glirc-version:             2.10+version:             2.11 synopsis:            Console IRC client description:         Console IRC client license:             ISC@@ -27,11 +27,12 @@ executable glirc2   main-is:             Main.hs   other-modules:       Client.CommandArguments-                       Client.Commands-                       Client.Commands.Interpolation                        Client.CApi                        Client.CApi.Exports                        Client.CApi.Types+                       Client.Commands+                       Client.Commands.Exec+                       Client.Commands.Interpolation                        Client.Commands.WordCompletion                        Client.Configuration                        Client.Configuration.Colors@@ -48,6 +49,7 @@                        Client.Image.Palette                        Client.Image.StatusLine                        Client.Image.UserList+                       Client.Image.Windows                        Client.Message                        Client.Network.Async                        Client.Network.Connect@@ -66,7 +68,7 @@   build-tools:         hsc2hs    build-depends:       base                 >=4.9    && <4.10,-                       async                >=2.1    && < 2.2,+                       async                >=2.1    && <2.2,                        attoparsec           >=0.13   && <0.14,                        bytestring           >=0.10.8 && <0.11,                        config-value         >=0.5    && <0.6,@@ -78,14 +80,15 @@                        filepath             >=1.4.1  && <1.5,                        gitrev               >=1.2    && <1.3,                        hashable             >=1.2.4  && <1.3,-                       irc-core             >=2.1    && <2.2,+                       irc-core             >=2.1.1  && <2.2,                        lens                 >=4.14   && <4.15,                        network              >=2.6.2  && <2.7,+                       process              >=1.4.2  && <1.5,+                       regex-tdfa           >=1.2    && <1.3,+                       regex-tdfa-text      >=1.0    && <1.1,                        split                >=0.2    && <0.3,                        stm                  >=2.4    && <2.5,                        text                 >=1.2.2  && <1.3,-                       regex-tdfa           >=1.2    && <1.3,-                       regex-tdfa-text      >=1.0    && <1.1,                        time                 >=1.6    && <1.7,                        tls                  >=1.3.8  && <1.4,                        transformers         >=0.5.2  && <0.6,
src/Client/CApi.hs view
@@ -18,23 +18,18 @@   , deactivateExtension   , notifyExtensions   , commandExtension-  , withStableMVar   ) where  import           Client.CApi.Types-import           Control.Concurrent.MVar-import           Control.Exception import           Control.Monad import           Control.Monad.Trans.Class import           Control.Monad.Trans.Cont-import           Data.Foldable import           Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Foreign as Text import           Foreign.C import           Foreign.Marshal import           Foreign.Ptr-import           Foreign.StablePtr import           Foreign.Storable import           Irc.Identifier import           Irc.RawIrcMsg@@ -95,33 +90,38 @@ deactivateExtension stab ae =   do let f = fgnStop (aeFgn ae)      unless (nullFunPtr == f) $-       (runStopExtension f stab (aeSession ae))+       runStopExtension f stab (aeSession ae)      dlclose (aeDL ae)  -- | Call all of the process message callbacks in the list of extensions. -- This operation marshals the IRC message once and shares that across -- all of the callbacks. notifyExtensions ::-  Ptr () {- ^ clientstate stable pointer -} ->+  Ptr ()            {- ^ clientstate stable pointer -} ->   Text              {- ^ network              -} ->   RawIrcMsg         {- ^ current message      -} ->   [ActiveExtension] ->-  IO ()+  IO Bool {- ^ Return 'True' to pass message -} notifyExtensions stab network msg aes-  | null aes' = return ()-  | otherwise = evalContT doNotifications+  | null aes' = return True+  | otherwise = doNotifications   where     aes' = [ (f,s) | ae <- aes                   , let f = fgnMessage (aeFgn ae)                         s = aeSession ae                   , f /= nullFunPtr ] -    doNotifications :: ContT () IO ()     doNotifications =-      do msgPtr <- withRawIrcMsg network msg-         (f,s)  <- ContT $ for_ aes'-         lift $ runProcessMessage f stab s msgPtr+      runContT (withRawIrcMsg network msg) (go aes') +    -- run handlers until one of them drops the message+    go [] _ = return True+    go ((f,s):rest) msgPtr =+       do res <- runProcessMessage f stab s msgPtr+          if res == passMessage+            then go rest msgPtr+            else return False+ commandExtension ::   Ptr ()          {- ^ client state stableptr -} ->   [Text]          {- ^ parameters             -} ->@@ -132,15 +132,6 @@      let f = fgnCommand (aeFgn ae)      lift $ unless (f == nullFunPtr)           $ runProcessCommand f stab (aeSession ae) cmd---- | Create a 'StablePtr' around a 'MVar' which will be valid for the remainder--- of the computation.-withStableMVar :: a -> (Ptr () -> IO b) -> IO (a,b)-withStableMVar x k =-  do mvar <- newMVar x-     res <- bracket (newStablePtr mvar) freeStablePtr (k . castStablePtrToPtr)-     x' <- takeMVar mvar-     return (x', res)  -- | Marshal a 'RawIrcMsg' into a 'FgnMsg' which will be valid for -- the remainder of the computation.
src/Client/CApi/Exports.hs view
@@ -18,13 +18,17 @@  , Glirc_list_channel_users  , Glirc_my_nick  , Glirc_identifier_cmp+ , Glirc_mark_seen+ , Glirc_clear_window  ) where  import           Client.CApi.Types import           Client.Message import           Client.State import           Client.State.Channel+import           Client.State.Focus import           Client.State.Network+import           Client.State.Window import           Control.Concurrent.MVar import           Control.Exception import           Control.Lens@@ -41,6 +45,7 @@ import           Irc.Identifier import           Irc.RawIrcMsg import           Irc.UserInfo+import           LensUtils  ------------------------------------------------------------------------ @@ -89,10 +94,10 @@   Ptr FgnMsg {- ^ pointer to message -} ->   IO CInt    {- ^ 0 on success       -} -foreign export ccall "glirc_send_message" capiSendMessage :: Glirc_send_message+foreign export ccall "glirc_send_message" glirc_send_message :: Glirc_send_message -capiSendMessage :: Glirc_send_message-capiSendMessage token msgPtr =+glirc_send_message :: Glirc_send_message+glirc_send_message token msgPtr =   do mvar    <- derefToken token      fgn     <- peek msgPtr      msg     <- peekFgnMsg fgn@@ -108,7 +113,7 @@  type Glirc_print =   Ptr ()  {- ^ api token         -} ->-  CInt    {- ^ enum message_code -} ->+  MessageCode {- ^ enum message_code -} ->   CString {- ^ message           -} ->   CSize   {- ^ message length    -} ->   IO CInt {- ^ 0 on success      -}@@ -118,11 +123,11 @@ glirc_print :: Glirc_print glirc_print stab code msgPtr msgLen =   do mvar <- derefToken stab-     txt  <- Text.peekCStringLen (msgPtr, fromIntegral msgLen)+     txt  <- peekFgnStringLen (FgnStringLen msgPtr msgLen)      now  <- getZonedTime -     let con | code == normalMessageCode = NormalBody-             | otherwise                 = ErrorBody+     let con | code == normalMessage = NormalBody+             | otherwise             = ErrorBody          msg = ClientMessage                  { _msgBody    = con txt                  , _msgTime    = now@@ -164,8 +169,8 @@  glirc_identifier_cmp :: Glirc_identifier_cmp glirc_identifier_cmp p1 n1 p2 n2 =-  do txt1 <- Text.peekCStringLen (p1, fromIntegral n1)-     txt2 <- Text.peekCStringLen (p2, fromIntegral n2)+  do txt1 <- peekFgnStringLen (FgnStringLen p1 n1)+     txt2 <- peekFgnStringLen (FgnStringLen p2 n2)      return $! case compare (mkId txt1) (mkId txt2) of                  LT -> -1                  EQ ->  0@@ -187,7 +192,7 @@ glirc_list_channels stab networkPtr networkLen =   do mvar <- derefToken stab      st   <- readMVar mvar-     network <- Text.peekCStringLen (networkPtr, fromIntegral networkLen)+     network <- peekFgnStringLen (FgnStringLen networkPtr networkLen)      case preview (clientConnection network . csChannels) st of         Nothing -> return nullPtr         Just m  ->@@ -212,8 +217,8 @@ glirc_list_channel_users stab networkPtr networkLen channelPtr channelLen =   do mvar <- derefToken stab      st   <- readMVar mvar-     network <- Text.peekCStringLen (networkPtr, fromIntegral networkLen)-     channel <- Text.peekCStringLen (channelPtr, fromIntegral channelLen)+     network <- peekFgnStringLen (FgnStringLen networkPtr networkLen)+     channel <- peekFgnStringLen (FgnStringLen channelPtr channelLen)      let mb = preview ( clientConnection network                       . csChannels . ix (mkId channel)                       . chanUsers@@ -240,8 +245,66 @@ glirc_my_nick stab networkPtr networkLen =   do mvar <- derefToken stab      st   <- readMVar mvar-     network <- Text.peekCStringLen (networkPtr, fromIntegral networkLen)+     network <- peekFgnStringLen (FgnStringLen networkPtr networkLen)      let mb = preview (clientConnection network . csNick) st      case mb of        Nothing -> return nullPtr        Just me -> newCString (Text.unpack (idText me))++------------------------------------------------------------------------++-- | Mark a window as being seen clearing the new message counter.+-- To clear the client window send an empty network name.+-- To clear a network window send an empty channel name.+type Glirc_mark_seen =+  Ptr ()  {- ^ api token           -} ->+  CString {- ^ network name        -} ->+  CSize   {- ^ network name length -} ->+  CString {- ^ channel name        -} ->+  CSize   {- ^ channel name length -} ->+  IO ()++foreign export ccall glirc_mark_seen :: Glirc_mark_seen++glirc_mark_seen :: Glirc_mark_seen+glirc_mark_seen stab networkPtr networkLen channelPtr channelLen =+  do network <- peekFgnStringLen (FgnStringLen networkPtr networkLen)+     channel <- peekFgnStringLen (FgnStringLen channelPtr channelLen)++     let focus+           | Text.null network = Unfocused+           | Text.null channel = NetworkFocus network+           | otherwise         = ChannelFocus network (mkId channel)++     mvar <- derefToken stab+     modifyMVar_ mvar $ \st ->+       return $! overStrict (clientWindows . ix focus) windowSeen st++------------------------------------------------------------------------++-- | Mark a window as being seen clearing the new message counter.+-- To clear the client window send an empty network name.+-- To clear a network window send an empty channel name.+type Glirc_clear_window =+  Ptr ()  {- ^ api token           -} ->+  CString {- ^ network name        -} ->+  CSize   {- ^ network name length -} ->+  CString {- ^ channel name        -} ->+  CSize   {- ^ channel name length -} ->+  IO ()++foreign export ccall glirc_clear_window :: Glirc_clear_window++glirc_clear_window :: Glirc_clear_window+glirc_clear_window stab networkPtr networkLen channelPtr channelLen =+  do network <- peekFgnStringLen (FgnStringLen networkPtr networkLen)+     channel <- peekFgnStringLen (FgnStringLen channelPtr channelLen)++     let focus+           | Text.null network = Unfocused+           | Text.null channel = NetworkFocus network+           | otherwise         = ChannelFocus network (mkId channel)++     mvar <- derefToken stab+     modifyMVar_ mvar $ \st ->+       return $! set (clientWindows . ix focus) emptyWindow st
src/Client/CApi/Types.hsc view
@@ -38,19 +38,21 @@   , runProcessCommand    -- * report message codes-  , normalMessageCode-  , errorMessageCode+  , MessageCode(..), normalMessage, errorMessage++  -- * process message results+  , MessageResult(..), passMessage, dropMessage   ) where  import Foreign.C import Foreign.Ptr import Foreign.Storable -normalMessageCode :: CInt-normalMessageCode = #const NORMAL_MESSAGE+newtype MessageCode = MessageCode CInt deriving Eq+#enum MessageCode, MessageCode, NORMAL_MESSAGE, ERROR_MESSAGE -errorMessageCode :: CInt-errorMessageCode = #const ERROR_MESSAGE+newtype MessageResult = MessageResult CInt deriving Eq+#enum MessageResult, MessageResult, PASS_MESSAGE, DROP_MESSAGE  type StartExtension =   Ptr ()      {- ^ api token                   -} ->@@ -66,7 +68,7 @@   Ptr ()     {- ^ api token       -} ->   Ptr ()     {- ^ extention state -} ->   Ptr FgnMsg {- ^ message to send -} ->-  IO ()+  IO MessageResult  type ProcessCommand =   Ptr ()     {- ^ api token       -} ->
src/Client/Commands.hs view
@@ -19,6 +19,7 @@   ) where  import           Client.CApi+import           Client.Commands.Exec import           Client.Commands.Interpolation import           Client.Commands.WordCompletion import           Client.Configuration@@ -67,7 +68,6 @@  -- | Type of commands that require an active network to be focused type NetworkCommand =-  Text            {- ^ focused network          -} ->   NetworkState    {- ^ focused connection state -} ->   ClientState                                      ->   String          {- ^ command arguments        -} ->@@ -75,7 +75,6 @@  -- | Type of commands that require an active channel to be focused type ChannelCommand =-  Text            {- ^ focused network          -} ->   NetworkState    {- ^ focused connection state -} ->   Identifier      {- ^ focused channel          -} ->   ClientState                                      ->@@ -99,6 +98,12 @@ commandFailure :: Monad m => ClientState -> m CommandResult commandFailure = return . CommandFailure +-- | Command failure with an error message printed to client window+commandFailureMsg :: Text -> ClientState -> IO CommandResult+commandFailureMsg e st =+  do now <- getZonedTime+     return $! CommandFailure $! recordError now st e+ -- | Interpret the given chat message or command. Leading @/@ indicates a -- command. Otherwise if a channel or user query is focused a chat message -- will be sent.@@ -120,7 +125,7 @@     Nothing -> executeCommand Nothing command st     Just cmdExs ->       case traverse (resolveExpansions expandVar expandInt) cmdExs of-        Nothing   -> commandFailure st+        Nothing   -> commandFailureMsg "Macro expansions failed" st         Just cmds -> process cmds st   where     args = Text.words (Text.pack command)@@ -171,7 +176,7 @@              sendMsg cs ircMsg              commandSuccess $ recordChannelMessage network channel entry st -    _ -> commandFailure st+    _ -> commandFailureMsg "This command requires an active channel" st  splitWord :: String -> (String, String) splitWord str = (w, drop 1 rest)@@ -197,6 +202,11 @@       cmdTxt      = Text.toLower (Text.pack cmd) in   case HashMap.lookup cmdTxt commands of +    Nothing ->+      case tabCompleteReversed of+        Nothing         -> commandFailureMsg "Unknown command" st+        Just isReversed -> commandSuccess (nickTabCompletion isReversed st)+     Just (ClientCommand exec tab) ->           maybe exec tab tabCompleteReversed             st rest@@ -205,24 +215,23 @@       | Just network <- views clientFocus focusNetwork st       , Just cs      <- preview (clientConnection network) st ->           maybe exec tab tabCompleteReversed-            network cs st rest+            cs st rest+      | otherwise -> commandFailureMsg "This command requires an active network" st      Just (ChannelCommand exec tab)       | ChannelFocus network channelId <- view clientFocus st       , Just cs <- preview (clientConnection network) st       , isChannelIdentifier cs channelId ->           maybe exec tab tabCompleteReversed-            network cs channelId st rest+            cs channelId st rest+      | otherwise -> commandFailureMsg "This command requires an active channel" st      Just (ChatCommand exec tab)       | ChannelFocus network channelId <- view clientFocus st       , Just cs <- preview (clientConnection network) st ->           maybe exec tab tabCompleteReversed-            network cs channelId st rest--    _ -> case tabCompleteReversed of-           Nothing         -> commandFailure st-           Just isReversed -> commandSuccess (nickTabCompletion isReversed st)+            cs channelId st rest+      | otherwise -> commandFailureMsg "This command requires an active chat window" st  -- Expands each alias to have its own copy of the command callbacks expandAliases :: [([a],b)] -> [(a,b)]@@ -231,7 +240,7 @@ commands :: HashMap Text Command commands = HashMap.fromList          $ expandAliases-  [ (["connect"   ], ClientCommand cmdConnect noClientTab)+  [ (["connect"   ], ClientCommand cmdConnect tabConnect)   , (["exit"      ], ClientCommand cmdExit    noClientTab)   , (["focus"     ], ClientCommand cmdFocus   tabFocus)   , (["clear"     ], ClientCommand cmdClear   noClientTab)@@ -239,6 +248,8 @@   , (["ignore"    ], ClientCommand cmdIgnore simpleClientTab)   , (["reload"    ], ClientCommand cmdReload  tabReload)   , (["extension" ], ClientCommand cmdExtension simpleClientTab)+  , (["windows"   ], ClientCommand cmdWindows noClientTab)+  , (["exec"      ], ClientCommand cmdExec simpleClientTab)    , (["quote"     ], NetworkCommand cmdQuote  simpleNetworkTab)   , (["j","join"  ], NetworkCommand cmdJoin   simpleNetworkTab)@@ -281,21 +292,21 @@ noClientTab _ st _ = commandFailure st  noNetworkTab :: Bool -> NetworkCommand-noNetworkTab _ _ _ st _ = commandFailure st+noNetworkTab _ _ st _ = commandFailure st  noChannelTab :: Bool -> ChannelCommand-noChannelTab _ _ _ _ st _ = commandFailure st+noChannelTab _ _ _ st _ = commandFailure st  simpleClientTab :: Bool -> ClientCommand simpleClientTab isReversed st _ =   commandSuccess (nickTabCompletion isReversed st)  simpleNetworkTab :: Bool -> NetworkCommand-simpleNetworkTab isReversed _ _ st _ =+simpleNetworkTab isReversed _ st _ =   commandSuccess (nickTabCompletion isReversed st)  simpleChannelTab :: Bool -> ChannelCommand-simpleChannelTab isReversed _ _ _ st _ =+simpleChannelTab isReversed _ _ st _ =   commandSuccess (nickTabCompletion isReversed st)  cmdExit :: ClientCommand@@ -306,41 +317,48 @@ -- preserve the window. When used on a window that the -- user is not joined to this command will delete the window. cmdClear :: ClientCommand-cmdClear st _ = commandSuccess (windowEffect st)+cmdClear st rest =+  case Text.pack <$> words rest of+    []                -> clearFocus (view clientFocus st)+    ["*"]             -> clearFocus Unfocused+    [network]         -> clearFocus (NetworkFocus network)+    [network,channel] -> clearFocus (ChannelFocus network (mkId channel))+    _                 -> commandFailureMsg "Usage: /clear [network] [channel]" st   where-    windowEffect-      | isActive  = clearWindow-      | otherwise = deleteWindow+    clearFocus focus = commandSuccess (windowEffect st)+      where+        windowEffect+          | isActive  = clearWindow+          | otherwise = deleteWindow -    deleteWindow = advanceFocus . setWindow Nothing-    clearWindow  =                setWindow (Just emptyWindow)+        deleteWindow = advanceFocus . setWindow Nothing+        clearWindow  =                setWindow (Just emptyWindow) -    setWindow = set (clientWindows . at (view clientFocus st))+        setWindow = set (clientWindows . at (view clientFocus st)) -    isActive =-      case view clientFocus st of-        Unfocused -> False-        NetworkFocus network ->-            has (clientConnection network) st-        ChannelFocus network channel ->-            has ( clientConnection network-                . csChannels . ix channel) st+        isActive =+          case focus of+            Unfocused                    -> False+            NetworkFocus network         -> has (clientConnection network) st+            ChannelFocus network channel -> has (clientConnection network+                                                .csChannels . ix channel) st   cmdQuote :: NetworkCommand-cmdQuote _ cs st rest =+cmdQuote cs st rest =   case parseRawIrcMsg (Text.pack rest) of-    Nothing  -> commandFailure st+    Nothing  -> commandFailureMsg "Failed to parse IRC command" st     Just raw ->       do sendMsg cs raw          commandSuccess st  -- | Implementation of @/me@ cmdMe :: ChannelCommand-cmdMe network cs channelId st rest =+cmdMe cs channelId st rest =   do now <- getZonedTime      let actionTxt = Text.pack ("\^AACTION " ++ rest ++ "\^A")          !myNick = UserInfo (view csNick cs) "" ""+         network = view csNetwork cs          entry = ClientMessage                     { _msgTime = now                     , _msgNetwork = network@@ -352,9 +370,9 @@  -- | Implementation of @/ctcp@ cmdCtcp :: NetworkCommand-cmdCtcp network cs st rest =+cmdCtcp cs st rest =   case parse of-    Nothing -> commandFailure st+    Nothing -> commandFailureMsg "Usage: /ctcp TARGET COMMAND ARGS" st     Just (target, cmd, args) ->       do let cmdTxt = Text.toUpper (Text.pack cmd)              argTxt = Text.pack args@@ -363,8 +381,7 @@          sendMsg cs (ircPrivmsg (mkId tgtTxt) ("\^A" <> cmdTxt <> " " <> argTxt <> "\^A"))          chatCommand             (\src tgt -> Ctcp src tgt cmdTxt argTxt)-            tgtTxt-            network cs st+            tgtTxt cs st   where     parse =       do (target, rest1) <- nextWord rest@@ -373,47 +390,56 @@  -- | Implementation of @/notice@ cmdNotice :: NetworkCommand-cmdNotice network cs st rest =+cmdNotice cs st rest =   case nextWord rest of-    Nothing -> commandFailure st-    Just (target, rest1) ->+    Just (target, rest1) | not (null rest1) ->       do let restTxt = Text.pack rest1              tgtTxt = Text.pack target           sendMsg cs (ircNotice (mkId tgtTxt) restTxt)          chatCommand             (\src tgt -> Notice src tgt restTxt)-            tgtTxt-            network cs st+            tgtTxt cs st +    _ -> commandFailureMsg "Usage: /notice TARGET MESSAGE" st+ -- | Implementation of @/msg@ cmdMsg :: NetworkCommand-cmdMsg network cs st rest =+cmdMsg cs st rest =   case nextWord rest of-    Nothing -> commandFailure st-    Just (target, rest1) ->+    Just (target, rest1) | not (null rest1) ->       do let restTxt = Text.pack rest1              tgtTxt = Text.pack target           sendMsg cs (ircPrivmsg (mkId tgtTxt) restTxt)          chatCommand             (\src tgt -> Privmsg src tgt restTxt)-            tgtTxt-            network cs st+            tgtTxt cs st+    _ -> commandFailureMsg "Usage: /msg TARGET MESSAGE" st  -- | Common logic for @/msg@ and @/notice@ chatCommand ::   (UserInfo -> Identifier -> IrcMsg) ->   Text {- ^ target  -} ->-  Text {- ^ network -} ->   NetworkState         ->   ClientState          ->   IO CommandResult-chatCommand con targetsTxt network cs st =+chatCommand mkmsg target cs st =+  commandSuccess =<< chatCommand' mkmsg target cs st++-- | Common logic for @/msg@ and @/notice@ returning the client state+chatCommand' ::+  (UserInfo -> Identifier -> IrcMsg) ->+  Text {- ^ target  -} ->+  NetworkState         ->+  ClientState          ->+  IO ClientState+chatCommand' con targetsTxt cs st =   do now <- getZonedTime      let targetTxts = Text.split (==',') targetsTxt          targetIds  = mkId <$> targetTxts          !myNick = UserInfo (view csNick cs) "" ""+         network = view csNetwork cs          entries = [ (targetId,                           ClientMessage                           { _msgTime = now@@ -422,12 +448,11 @@                           })                        | targetId <- targetIds ] -         st' = foldl' (\acc (targetId, entry) ->-                             recordChannelMessage network targetId entry acc)-                          st-                          entries+     return $! foldl' (\acc (targetId, entry) ->+                         recordChannelMessage network targetId entry acc)+                      st+                      entries -     commandSuccess st'  cmdConnect :: ClientCommand cmdConnect st rest =@@ -439,11 +464,14 @@          commandSuccess            $ changeFocus (NetworkFocus network) st' -    _ -> commandFailure st+    _ -> commandFailureMsg "Usage: /connect NETWORK" st  cmdFocus :: ClientCommand cmdFocus st rest =   case words rest of+    ["*"] ->+      commandSuccess (changeFocus Unfocused st)+     [network] ->       let focus = NetworkFocus (Text.pack network) in       commandSuccess (changeFocus focus st)@@ -453,8 +481,23 @@       commandSuccess         $ changeFocus focus st -    _ -> commandFailure st+    _ -> commandFailureMsg "Focus requires a network and an optional channel" st +-- | Implementation of @/windows@ command. Set subfocus to Windows.+cmdWindows :: ClientCommand+cmdWindows st _rest = commandSuccess (changeSubfocus FocusWindows st)+++-- | @/connect@ tab completes known server names+tabConnect :: Bool -> ClientCommand+tabConnect isReversed st _+  = commandSuccess+  $ fromMaybe st+  $ clientTextBox (wordComplete id isReversed [] networks) st+  where+    networks = HashMap.keys $ view clientNetworkMap st++ -- | When tab completing the first parameter of the focus command -- the current networks are used. tabFocus :: Bool -> ClientCommand@@ -471,58 +514,58 @@       | otherwise          = currentCompletionList st  cmdWhois :: NetworkCommand-cmdWhois _ cs st rest =+cmdWhois cs st rest =   do sendMsg cs (ircWhois (Text.pack <$> words rest))      commandSuccess st  cmdWho :: NetworkCommand-cmdWho _ cs st rest =+cmdWho cs st rest =   do sendMsg cs (ircWho (Text.pack <$> words rest))      commandSuccess st  cmdWhowas :: NetworkCommand-cmdWhowas _ cs st rest =+cmdWhowas cs st rest =   do sendMsg cs (ircWhowas (Text.pack <$> words rest))      commandSuccess st  cmdIson :: NetworkCommand-cmdIson _ cs st rest =+cmdIson cs st rest =   do sendMsg cs (ircIson (Text.pack <$> words rest))      commandSuccess st  cmdUserhost :: NetworkCommand-cmdUserhost _ cs st rest =+cmdUserhost cs st rest =   do sendMsg cs (ircUserhost (Text.pack <$> words rest))      commandSuccess st  cmdStats :: NetworkCommand-cmdStats _ cs st rest =+cmdStats cs st rest =   do sendMsg cs (ircStats (Text.pack <$> words rest))      commandSuccess st  cmdAway :: NetworkCommand-cmdAway _ cs st rest =-  do sendMsg cs (ircAway (Text.pack (dropWhile (==' ') rest)))+cmdAway cs st rest =+  do sendMsg cs (ircAway (Text.pack rest))      commandSuccess st  cmdLinks :: NetworkCommand-cmdLinks _ cs st rest =+cmdLinks cs st rest =   do sendMsg cs (ircLinks (Text.pack <$> words rest))      commandSuccess st  cmdTime :: NetworkCommand-cmdTime _ cs st rest =+cmdTime cs st rest =   do sendMsg cs (ircTime (Text.pack <$> words rest))      commandSuccess st  cmdZnc :: NetworkCommand-cmdZnc _ cs st rest =+cmdZnc cs st rest =   do sendMsg cs (ircZnc (Text.words (Text.pack rest)))      commandSuccess st  -- TODO: support time ranges cmdZncPlayback :: NetworkCommand-cmdZncPlayback _ cs st rest =+cmdZncPlayback cs st rest =   case words rest of      -- request everything@@ -546,7 +589,7 @@                    { localTimeOfDay = tod                    , localDay       = day } } -    _ -> commandFailure st+    _ -> commandFailureMsg "Unable to parse date/time arguments" st    where     -- %k doesn't require a leading 0 for times before 10AM@@ -562,29 +605,29 @@          commandSuccess st  cmdMode :: NetworkCommand-cmdMode _ cs st rest = modeCommand (Text.pack <$> words rest) cs st+cmdMode cs st rest = modeCommand (Text.pack <$> words rest) cs st  cmdNick :: NetworkCommand-cmdNick _ cs st rest =+cmdNick cs st rest =   case words rest of     [nick] ->       do sendMsg cs (ircNick (mkId (Text.pack nick)))          commandSuccess st-    _ -> commandFailure st+    _ -> commandFailureMsg "Usage: /nick NICK" st  cmdPart :: ChannelCommand-cmdPart _ cs channelId st rest =-  do let msg = dropWhile isSpace rest+cmdPart cs channelId st rest =+  do let msg = rest      sendMsg cs (ircPart channelId (Text.pack msg))      commandSuccess st  -- | This command is equivalent to chatting without a command. The primary use -- at the moment is to be able to send a leading @/@ to chat easily. cmdSay :: ChannelCommand-cmdSay _network _cs _channelId st rest = executeChat rest st+cmdSay _cs _channelId st rest = executeChat rest st  cmdInvite :: ChannelCommand-cmdInvite _ cs channelId st rest =+cmdInvite cs channelId st rest =   case words rest of     [nick] ->       do let freeTarget = has (csChannels . ix channelId . chanModes . ix 'g') cs@@ -594,7 +637,7 @@                   else sendModeration channelId [cmd] cs          commandSuccessUpdateCS cs' st -    _ -> commandFailure st+    _ -> commandFailureMsg "Usage: /invite NICK" st  commandSuccessUpdateCS :: NetworkState    -> ClientState -> IO CommandResult commandSuccessUpdateCS cs st =@@ -603,12 +646,12 @@     $ setStrict (clientConnections . ix networkId) cs st  cmdTopic :: ChannelCommand-cmdTopic _ cs channelId st rest =+cmdTopic cs channelId st rest =   do let cmd =-           case dropWhile isSpace rest of+           case rest of              ""    -> ircTopic channelId ""              topic | useChanServ channelId cs ->-                        ircPrivmsg (mkId "ChanServ")+                        ircPrivmsg "ChanServ"                           ("TOPIC " <> idText channelId <> Text.pack (' ' : topic))                    | otherwise -> ircTopic channelId (Text.pack topic)      sendMsg cs cmd@@ -617,7 +660,7 @@ tabTopic ::   Bool {- ^ reversed -} ->   ChannelCommand-tabTopic _ _ cs channelId st rest+tabTopic _ cs channelId st rest    | all isSpace rest   , Just topic <- preview (csChannels . ix channelId . chanTopic) cs =@@ -628,35 +671,35 @@   cmdUsers :: ChannelCommand-cmdUsers _ _ _ st _ = commandSuccess (changeSubfocus FocusUsers st)+cmdUsers _ _ st _ = commandSuccess (changeSubfocus FocusUsers st)  cmdChannelInfo :: ChannelCommand-cmdChannelInfo _ _ _ st _ = commandSuccess (changeSubfocus FocusInfo st)+cmdChannelInfo _ _ st _ = commandSuccess (changeSubfocus FocusInfo st)  cmdMasks :: ChannelCommand-cmdMasks _ cs _ st rest =+cmdMasks cs _ st rest =   case words rest of     [[mode]] | mode `elem` view (csModeTypes . modesLists) cs ->         commandSuccess (changeSubfocus (FocusMasks mode) st)-    _ -> commandFailure st+    _ -> commandFailureMsg "Unknown mask mode" st  cmdKick :: ChannelCommand-cmdKick _ cs channelId st rest =+cmdKick cs channelId st rest =   case nextWord rest of-    Nothing -> commandFailure st+    Nothing -> commandFailureMsg "Usage: /kick NICK [MESSAGE]" st     Just (who,reason) ->-      do let msg = Text.pack (dropWhile isSpace reason)+      do let msg = Text.pack reason              cmd = ircKick channelId (Text.pack who) msg          cs' <- sendModeration channelId [cmd] cs          commandSuccessUpdateCS cs' st   cmdKickBan :: ChannelCommand-cmdKickBan _ cs channelId st rest =+cmdKickBan cs channelId st rest =   case nextWord rest of-    Nothing -> commandFailure st+    Nothing -> commandFailureMsg "Usage: /kickban NICK [MESSAGE]" st     Just (whoStr,reason) ->-      do let msg = Text.pack (dropWhile isSpace reason)+      do let msg = Text.pack reason               whoTxt     = Text.pack whoStr @@ -670,22 +713,23 @@ computeBanUserInfo :: Identifier -> NetworkState    -> UserInfo computeBanUserInfo who cs =   case view (csUser who) cs of-    Nothing                   -> UserInfo who        "*" "*"-    Just (UserAndHost _ host) -> UserInfo (mkId "*") "*" host+    Nothing                   -> UserInfo who "*" "*"+    Just (UserAndHost _ host) -> UserInfo "*" "*" host  cmdRemove :: ChannelCommand-cmdRemove _ cs channelId st rest =+cmdRemove cs channelId st rest =   case nextWord rest of-    Nothing -> commandFailure st+    Nothing -> commandFailureMsg "Usage: /remove NICK [MESSAGE]" st     Just (who,reason) ->-      do let msg = Text.pack (dropWhile isSpace reason)+      do let msg = Text.pack reason              cmd = ircRemove channelId (Text.pack who) msg          cs' <- sendModeration channelId [cmd] cs          commandSuccessUpdateCS cs' st  cmdJoin :: NetworkCommand-cmdJoin network cs st rest =+cmdJoin cs st rest =   let ws = words rest+      network = view csNetwork cs       doJoin channelStr keyStr =         do let channelId = mkId (Text.pack (takeWhile (/=',') channelStr))            sendMsg cs (ircJoin (Text.pack channelStr) (Text.pack <$> keyStr))@@ -694,29 +738,29 @@   in case ws of        [channel]     -> doJoin channel Nothing        [channel,key] -> doJoin channel (Just key)-       _             -> commandFailure st+       _             -> commandFailureMsg "Usage: /join CHANNELS [KEYS]" st   -- | @/channel@ command. Takes the name of a channel and switches -- focus to that channel on the current network. cmdChannel :: NetworkCommand-cmdChannel network _ st rest =+cmdChannel cs st rest =   case mkId . Text.pack <$> words rest of     [ channelId ] ->        commandSuccess-         $ changeFocus (ChannelFocus network channelId) st-    _ -> commandFailure st+         $ changeFocus (ChannelFocus (view csNetwork cs) channelId) st+    _ -> commandFailureMsg "Usage: /channel CHANNEL" st   cmdQuit :: NetworkCommand-cmdQuit _ cs st rest =-  do let msg = Text.pack (dropWhile isSpace rest)+cmdQuit cs st rest =+  do let msg = Text.pack rest      sendMsg cs (ircQuit msg)      commandSuccess st  cmdDisconnect :: NetworkCommand-cmdDisconnect network _ st _ =-  do st' <- abortNetwork network st+cmdDisconnect cs st _ =+  do st' <- abortNetwork (view csNetwork cs) st      commandSuccess st'  -- | Reconnect to the currently focused network. It's possible@@ -730,7 +774,7 @@          commandSuccess            $ changeFocus (NetworkFocus network) st' -  | otherwise = commandFailure st+  | otherwise = commandFailureMsg "/reconnect requires focused network" st  cmdIgnore :: ClientCommand cmdIgnore st rest =@@ -752,11 +796,19 @@               | otherwise = Just rest      res <- loadConfiguration path      case res of-       Left{} -> commandFailure st+       Left e    -> commandFailureMsg (describeProblem e) st        Right cfg ->          do st1 <- clientStartExtensions (set clientConfig cfg st)             commandSuccess st1 +  where+    describeProblem err =+      Text.pack $+      case err of+       ConfigurationReadFailed e  -> "Failed to open configuration:" ++ e+       ConfigurationParseFailed e -> "Failed to parse configuration:" ++ e+       ConfigurationMalformed e   -> "Configuration malformed: " ++ e+ -- | Support file name tab completion when providing an alternative -- configuration file. --@@ -781,7 +833,7 @@         [] -> success False [[]]         flags:params ->           case splitModes (view csModeTypes cs) flags params of-            Nothing -> commandFailure st+            Nothing -> commandFailureMsg "Failed to parse modes" st             Just parsedModes ->               success needOp (unsplitModes <$> chunksOf (view csModeCount cs) parsedModes')               where@@ -804,7 +856,7 @@     _ -> commandFailure st  tabMode :: Bool -> NetworkCommand-tabMode isReversed _ cs st rest =+tabMode isReversed cs st rest =   case view clientFocus st of      ChannelFocus _ channel@@ -885,7 +937,7 @@     n = length leadingPart     (cursorPos, line) = clientLine st     leadingPart = takeWhile (not . isSpace) line-    possibilities = mkId . Text.cons '/' <$> commandNames+    possibilities = Text.cons '/' <$> commandNames     commandNames = HashMap.keys commands                 ++ HashMap.keys (view (clientConfig . configMacros) st) @@ -910,7 +962,7 @@   IO NetworkState sendModeration channel cmds cs   | useChanServ channel cs =-      do sendMsg cs (ircPrivmsg (mkId "ChanServ") ("OP " <> idText channel))+      do sendMsg cs (ircPrivmsg "ChanServ" ("OP " <> idText channel))          return $ csChannels . ix channel . chanQueuedModeration <>~ cmds $ cs   | otherwise = cs <$ traverse_ (sendMsg cs) cmds @@ -922,10 +974,89 @@ cmdExtension :: ClientCommand cmdExtension st rest =   case Text.words (Text.pack rest) of-    name:params-      | Just ae <- find (\ae -> aeName ae == name) (view clientExtensions st) ->-         do (st',_) <- withStableMVar st $ \stab ->-                         commandExtension stab params ae-            commandSuccess st'-    _ -> commandFailure st+    name:params ->+      case find (\ae -> aeName ae == name)+                (view (clientExtensions . esActive) st) of+        Nothing -> commandFailureMsg "Unknown extension" st+        Just ae ->+          do (st',_) <- clientPark st $ \ptr ->+                          commandExtension ptr params ae+             commandSuccess st'+    _ -> commandFailureMsg "Usage: /extension EXTENSION ARGUMENTS" st +-- | Implementation of @/exec@ command.+cmdExec :: ClientCommand+cmdExec st rest =+  do now <- getZonedTime+     case parseExecCmd rest of+       Left es -> failure now es+       Right ec ->+         case buildTransmitter now ec of+           Left es -> failure now es+           Right tx ->+             do res <- runExecCmd ec+                case res of+                  Left es -> failure now es+                  Right msgs -> tx (map Text.pack msgs)++  where+    buildTransmitter now ec =+      case (Text.pack <$> view execOutputNetwork ec,+            Text.pack <$> view execOutputChannel ec) of+        (Nothing, Nothing) -> Right (sendToClient now)+        (Just network, Nothing) ->+          case preview (clientConnection network) st of+            Nothing -> Left ["Unknown network"]+            Just cs -> Right (sendToNetwork now cs)+        (Nothing , Just channel) ->+          case currentNetworkState of+            Nothing -> Left ["No current network"]+            Just cs -> Right (sendToChannel cs channel)+        (Just network, Just channel) ->+          case preview (clientConnection network) st of+            Nothing -> Left ["Unknown network"]+            Just cs -> Right (sendToChannel cs channel)++    sendToClient now msgs = commandSuccess $! foldl' (recordSuccess now) st msgs++    sendToNetwork now cs msgs =+      commandSuccess =<<+      foldM (\st1 msg ->+           case parseRawIrcMsg msg of+             Nothing ->+               return $! recordError now st1 ("Bad raw message: " <> msg)+             Just raw ->+               do sendMsg cs raw+                  return st1) st msgs++    sendToChannel cs channel msgs =+      commandSuccess =<<+      foldM (\st1 msg ->+        do sendMsg cs (ircPrivmsg (mkId channel) msg)+           chatCommand'+              (\src tgt -> Privmsg src tgt msg)+              channel+              cs st1) st (filter (not . Text.null) msgs)++    currentNetworkState =+      do network <- views clientFocus focusNetwork st+         preview (clientConnection network) st++    failure now es =+      commandFailure $! foldl' (recordError now) st (map Text.pack es)++recordError :: ZonedTime -> ClientState -> Text -> ClientState+recordError now ste e =+  recordNetworkMessage ClientMessage+    { _msgTime    = now+    , _msgBody    = ErrorBody e+    , _msgNetwork = ""+    } ste++recordSuccess :: ZonedTime -> ClientState -> Text -> ClientState+recordSuccess now ste m =+  recordNetworkMessage ClientMessage+    { _msgTime    = now+    , _msgBody    = NormalBody m+    , _msgNetwork = ""+    } ste
+ src/Client/Commands/Exec.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE TemplateHaskell, BangPatterns, OverloadedStrings #-}++{-|+Module      : Client.Commands+Description : Implementation of slash commands+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module renders the lines used in the channel mask list. A mask list+can show channel bans, quiets, invites, and exceptions.+-}++module Client.Commands.Exec where++import           Control.Exception+import           Control.Lens+import           System.Console.GetOpt+import           System.Process++data ExecCmd = ExecCmd+  { _execOutputNetwork :: Maybe String+  , _execOutputChannel :: Maybe String+  , _execCommand       :: String+  , _execStdIn         :: String+  , _execArguments     :: [String]+  }+  deriving (Read,Show)++makeLenses ''ExecCmd++emptyExecCmd :: ExecCmd+emptyExecCmd = ExecCmd+  { _execOutputNetwork = Nothing+  , _execOutputChannel = Nothing+  , _execCommand       = error "no default command"+  , _execStdIn         = ""+  , _execArguments     = []+  }++options :: [OptDescr (ExecCmd -> ExecCmd)]+options =+  [ Option "n" ["network"]+        (ReqArg (set execOutputNetwork . Just) "NETWORK")+        "Set network target"+  , Option "c" ["channel"]+        (ReqArg (set execOutputChannel . Just) "CHANNEL")+        "Set channel target"+  , Option "i" ["input"]+        (ReqArg (set execStdIn) "INPUT")+        "Use string as stdin"+  ]++-- TODO: support quoted strings+parseExecCmd :: String -> Either [String] ExecCmd+parseExecCmd str =+  case getOpt RequireOrder options (words str) of+    (_, [] , errs) -> Left ("No command specified":errs)+    (fs, cmd:args, []) -> Right+                        $ foldl (\x f -> f x) ?? fs+                        $ set execCommand cmd+                        $ set execArguments args+                        $ emptyExecCmd+    (_,_, errs) -> Left errs++runExecCmd :: ExecCmd -> IO (Either [String] [String])+runExecCmd e =+  do res <- try (readProcess (view execCommand e)+                             (view execArguments e)+                             (view execStdIn e))+     return $ case res of+       Left er -> Left [show (er :: IOError)]+       Right x -> Right (lines x)
src/Client/Commands/WordCompletion.hs view
@@ -19,6 +19,7 @@ import           Data.Char import           Data.List import qualified Data.Set as Set+import           Data.String (IsString(..)) import qualified Data.Text as Text import           Data.Text (Text) import           Irc.Identifier@@ -34,29 +35,30 @@ -- will be considered in order before resorting to the set of possible -- completions. wordComplete ::+  Prefix a =>   (String -> String) {- ^ leading update operation -} ->   Bool               {- ^ reversed -} ->-  [Identifier]       {- ^ priority completions -} ->-  [Identifier]       {- ^ possible completions -} ->+  [a]       {- ^ priority completions -} ->+  [a]       {- ^ possible completions -} ->   Edit.EditBox -> Maybe Edit.EditBox wordComplete leadingCase isReversed hint vals box =   do let current = currentWord box      guard (not (null current))-     let cur = mkId (Text.pack current)+     let cur = fromString current      case view Edit.lastOperation box of        Edit.TabOperation patternStr-         | idPrefix pat cur ->+         | isPrefix pat cur ->           do next <- tabSearch isReversed pat cur vals-            Just $ replaceWith leadingCase (idString next) box+            Just $ replaceWith leadingCase (toString next) box          where-           pat = mkId (Text.pack patternStr)+           pat = fromString patternStr         _ ->-         do next <- find (idPrefix cur) hint <|>+         do next <- find (isPrefix cur) hint <|>                     tabSearch isReversed cur cur vals             Just $ set Edit.lastOperation (Edit.TabOperation current)-                 $ replaceWith leadingCase (idString next) box+                 $ replaceWith leadingCase (toString next) box  replaceWith :: (String -> String) -> String -> Edit.EditBox -> Edit.EditBox replaceWith leadingCase str box =@@ -65,9 +67,6 @@              | otherwise               = str     in over Edit.content (Edit.insertString str1) box1 -idString :: Identifier -> String-idString = Text.unpack . idText- currentWord :: Edit.EditBox -> String currentWord box   = reverse@@ -77,12 +76,19 @@   $ take n txt  where Edit.Line n txt = view Edit.line box -class            Prefix a          where isPrefix :: a -> a -> Bool-instance         Prefix Identifier where isPrefix = idPrefix-instance         Prefix Text       where isPrefix = Text.isPrefixOf-instance Eq a => Prefix [a]        where isPrefix = isPrefixOf+class (IsString a, Ord a) => Prefix a where+  isPrefix :: a -> a -> Bool+  toString :: a -> String -tabSearch :: (Ord a, Prefix a) => Bool -> a -> a -> [a] -> Maybe a+instance Prefix Identifier where+  isPrefix = idPrefix+  toString = Text.unpack . idText++instance Prefix Text where+  isPrefix = Text.isPrefixOf+  toString = Text.unpack++tabSearch :: Prefix a => Bool -> a -> a -> [a] -> Maybe a tabSearch isReversed pat cur vals   | Just next <- advanceFun cur valSet   , isPrefix pat next
src/Client/Configuration.hs view
@@ -26,6 +26,7 @@   , configConfigPath   , configMacros   , configExtensions+  , configExtraHighlights    -- * Loading configuration   , loadConfiguration@@ -46,6 +47,8 @@ import           Data.Foldable (for_) import           Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap+import           Data.HashSet (HashSet)+import qualified Data.HashSet as HashSet import           Data.Maybe import           Data.Text (Text) import qualified Data.Text as Text@@ -60,15 +63,16 @@ -- server configuration from '_configServers' is used where possible, -- otherwise '_configDefaults' is used. data Configuration = Configuration-  { _configDefaults :: ServerSettings -- ^ Default connection settings-  , _configServers  :: (HashMap Text ServerSettings) -- ^ Host-specific settings-  , _configPalette  :: Palette-  , _configWindowNames :: Text -- ^ Names of windows, used when alt-jumping)-  , _configNickPadding :: Maybe Integer -- ^ Padding of nicks-  , _configConfigPath :: Maybe FilePath+  { _configDefaults         :: ServerSettings -- ^ Default connection settings+  , _configServers          :: (HashMap Text ServerSettings) -- ^ Host-specific settings+  , _configPalette          :: Palette+  , _configWindowNames      :: Text -- ^ Names of windows, used when alt-jumping)+  , _configExtraHighlights  :: HashSet Identifier -- ^ Extra highlight nicks/terms+  , _configNickPadding      :: Maybe Integer -- ^ Padding of nicks+  , _configConfigPath       :: Maybe FilePath         -- ^ manually specified configuration path, used for reloading-  , _configMacros :: HashMap Text [[ExpansionChunk]] -- ^ command macros-  , _configExtensions :: [FilePath] -- ^ paths to shared library+  , _configMacros           :: HashMap Text [[ExpansionChunk]] -- ^ command macros+  , _configExtensions       :: [FilePath] -- ^ paths to shared library   }   deriving Show @@ -180,6 +184,9 @@       _configExtensions <- fromMaybe []                     <$> sectionOptWith (parseList parseString) "extensions"++     _configExtraHighlights <- maybe HashSet.empty HashSet.fromList+                    <$> sectionOptWith (parseList parseIdentifier) "extra-highlights"       _configNickPadding <- sectionOpt "nick-padding"      for_ _configNickPadding (\padding ->
src/Client/EventLoop.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, NondecreasingIndentation #-}  {-| Module      : Client.EventLoop@@ -43,6 +43,7 @@ import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding.Error as Text import           Data.Time+import           GHC.IO.Exception (IOErrorType(ResourceVanished), ioe_type) import           Graphics.Vty import           Irc.Codes import           Irc.Message@@ -138,15 +139,25 @@   SomeException {- ^ termination reason -} ->   ClientState -> IO () doNetworkError networkId time ex st =-  let (cs,st') = removeNetwork networkId st-      msg = ClientMessage-              { _msgTime    = time-              , _msgNetwork = view csNetwork cs-              , _msgBody    = ErrorBody (Text.pack (show ex))-              }-  in eventLoop $ recordNetworkMessage msg st'+  do let (cs,st1) = removeNetwork networkId st+         msg = ClientMessage+                 { _msgTime    = time+                 , _msgNetwork = view csNetwork cs+                 , _msgBody    = ErrorBody (Text.pack (displayException ex))+                 } +         shouldReconnect+           | Just PingTimeout      <-              fromException ex = True+           | Just ResourceVanished <- ioe_type <$> fromException ex = True+           | otherwise                                              = False +         nextAction+           | shouldReconnect = addConnection (view csNetwork cs)+           | otherwise       = return++     eventLoop =<< nextAction (recordNetworkMessage msg st1)++ -- | Respond to an IRC protocol line. This will parse the message, updated the -- relevant connection state and update the UI buffers. doNetworkLine ::@@ -170,8 +181,13 @@              eventLoop (recordNetworkMessage msg st)          Just raw ->-          do (st1,_) <- withStableMVar st $ \ptr ->-                          notifyExtensions ptr network raw (view clientExtensions st)+          do (st1,passed) <- clientPark st $ \ptr ->+                               notifyExtensions ptr network raw+                                 (view (clientExtensions . esActive) st)+++             if not passed then eventLoop st1 else do+              let time' = computeEffectiveTime time (view msgTags raw)                   (stateHook, viewHook)@@ -206,7 +222,7 @@ -- | Client-level responses to specific IRC messages. -- This is in contrast to the connection state tracking logic in -- "Client.NetworkState   "-clientResponse :: ZonedTime -> IrcMsg -> NetworkState    -> ClientState -> IO ClientState+clientResponse :: ZonedTime -> IrcMsg -> NetworkState -> ClientState -> IO ClientState clientResponse now irc cs st =   case irc of     Reply RPL_WELCOME _ ->@@ -277,7 +293,7 @@                    $ set clientHeight h st     EvPaste utf8 ->        let str = Text.unpack (Text.decodeUtf8With Text.lenientDecode utf8)-           st' = over (clientTextBox . Edit.content) (Edit.insertString str) st+           st' = over clientTextBox (Edit.insertPaste str) st        in eventLoop st'     _ -> eventLoop st @@ -298,7 +314,7 @@         KChar 'e' -> changeEditor Edit.end         KChar 'u' -> changeEditor Edit.killHome         KChar 'k' -> changeEditor Edit.killEnd-        KChar 'y' -> changeEditor Edit.paste+        KChar 'y' -> changeEditor Edit.yank         KChar 'w' -> changeEditor (Edit.killWordBackward True)         KChar 'b' -> changeEditor (Edit.insert '\^B')         KChar 'c' -> changeEditor (Edit.insert '\^C')@@ -319,6 +335,7 @@         KChar 'b' -> changeContent Edit.leftWord         KChar 'f' -> changeContent Edit.rightWord         KChar 'a' -> eventLoop (jumpToActivity st)+        KChar 's' -> eventLoop (returnFocus st)         KChar c   | let names = view (clientConfig . configWindowNames) st                   , Just i <- Text.findIndex (==c) names ->                             eventLoop (jumpFocus i st)
src/Client/Hook/Znc/Buffextras.hs view
@@ -37,11 +37,11 @@   Bool {- ^ enable debugging -} ->   IrcMsg -> MessageResult remap debug (Privmsg user chan msg)-  | userNick user == mkId "*buffextras"+  | userNick user == "*buffextras"   , Right newMsg <- parseOnly (prefixedParser chan) msg   = RemapMessage newMsg -  | userNick user == mkId "*buffextras"+  | userNick user == "*buffextras"   , not debug   = OmitMessage 
src/Client/Image.hs view
@@ -19,6 +19,7 @@ import           Client.Image.Palette import           Client.Image.StatusLine import           Client.Image.UserList+import           Client.Image.Windows import           Client.Message import           Client.State import qualified Client.State.EditBox as Edit@@ -68,8 +69,8 @@       | otherwise                -> userListImages network channel st     (ChannelFocus network channel, FocusMasks mode) ->       maskListImages mode network channel st+    (_, FocusWindows) -> windowsImages st -    -- subfocuses only make sense for channels     _ -> chatMessageImages st  chatMessageImages :: ClientState -> [Image]@@ -205,7 +206,7 @@    lineImage = beginning <|> content <|> ending -  leftOfCurWidth = safeWcswidth txt+  leftOfCurWidth = myWcswidth txt    croppedImage     | leftOfCurWidth < width = lineImage@@ -244,5 +245,16 @@       | z > w = acc + w -- didn't fit, will be filled in       | otherwise = go (acc+1) (w-z) xs       where-        z | isControl x = 1-          | otherwise   = safeWcwidth x+        z = myWcwidth x++-- | Version of 'safeWcwidth' that accounts for how control characters are+-- rendered+myWcwidth :: Char -> Int+myWcwidth x+  | isControl x = 1+  | otherwise   = safeWcwidth x++-- | Version of 'safeWcswidth' that accounts for how control characters are+-- rendered+myWcswidth :: String -> Int+myWcswidth = sum . map myWcwidth
src/Client/Image/ChannelInfo.hs view
@@ -24,6 +24,7 @@ import           Client.State.Channel import           Client.State.Network import           Control.Lens+import           Data.HashSet (HashSet) import           Data.Text (Text) import           Data.Time import           Graphics.Vty.Image@@ -38,14 +39,14 @@    | Just cs      <- preview (clientConnection network) st   , Just channel <- preview (csChannels . ix channelId) cs-  = channelInfoImages' pal channel cs+  = channelInfoImages' pal (clientHighlights cs st) channel    | otherwise = [text' (view palError pal) "No channel information"]   where     pal = view (clientConfig . configPalette) st -channelInfoImages' :: Palette -> ChannelState -> NetworkState -> [Image]-channelInfoImages' pal !channel !cs+channelInfoImages' :: Palette -> HashSet Identifier -> ChannelState -> [Image]+channelInfoImages' pal myNicks !channel     = topicLine     : provenanceLines    ++ creationLines@@ -56,7 +57,6 @@      topicLine = label "Topic: " <|> parseIrcText (view chanTopic channel) -    myNick = view csNick cs      utcTimeImage = string defAttr . formatTime defaultTimeLocale "%F %T" @@ -65,7 +65,7 @@           Nothing -> []           Just !prov ->             [ label "Topic set by: " <|>-                coloredUserInfo pal DetailedRender [myNick] (view topicAuthor prov)+                coloredUserInfo pal DetailedRender myNicks (view topicAuthor prov)             , label "Topic set on: " <|> utcTimeImage (view topicTime prov)             ] 
src/Client/Image/Message.hs view
@@ -28,6 +28,7 @@ import           Control.Lens import           Data.Char import           Data.Hashable (hash)+import           Data.HashSet (HashSet) import qualified Data.HashSet as HashSet import           Data.List import           Data.Maybe@@ -46,8 +47,8 @@ data MessageRendererParams = MessageRendererParams   { rendStatusMsg  :: [Char] -- ^ restricted message sigils   , rendUserSigils :: [Char] -- ^ sender sigils-  , rendNicks      :: [Identifier] -- ^ nicknames to highlight-  , rendMyNicks    :: [Identifier] -- ^ nicknames to highlight in red+  , rendNicks      :: HashSet Identifier -- ^ nicknames to highlight+  , rendMyNicks    :: HashSet Identifier -- ^ nicknames to highlight in red   , rendPalette    :: Palette -- ^ nick color palette   , rendNickPadding :: Maybe Integer -- ^ nick padding   }@@ -55,11 +56,11 @@ -- | Default 'MessageRenderParams' with no sigils or nicknames specified defaultRenderParams :: MessageRendererParams defaultRenderParams = MessageRendererParams-  { rendStatusMsg = ""-  , rendUserSigils = ""-  , rendNicks = []-  , rendMyNicks = []-  , rendPalette = defaultPalette+  { rendStatusMsg   = ""+  , rendUserSigils  = ""+  , rendNicks       = HashSet.empty+  , rendMyNicks     = HashSet.empty+  , rendPalette     = defaultPalette   , rendNickPadding = Nothing   } @@ -349,16 +350,16 @@  -- | Render a nickname in its hash-based color. coloredIdentifier ::-  Palette ->-  IdentifierColorMode ->-  [Identifier] {- ^ my nicknames -} ->-  Identifier ->+  Palette             {- ^ color palette      -} ->+  IdentifierColorMode {- ^ draw mode          -} ->+  HashSet Identifier  {- ^ my nicknames       -} ->+  Identifier          {- ^ identifier to draw -} ->   Image coloredIdentifier palette icm myNicks ident =   text' color (idText ident)   where     color-      | ident `elem` myNicks =+      | ident `HashSet.member` myNicks =           case icm of             PrivmsgIdentifier -> fromMaybe                                    (view palSelf palette)@@ -374,10 +375,11 @@ -- If detailed mode the full user info including the username and hostname parts -- will be rendered. The nickname will be colored. coloredUserInfo ::-  Palette ->-  RenderMode ->-  [Identifier] {- ^ my nicks -} ->-  UserInfo -> Image+  Palette            {- ^ color palette   -} ->+  RenderMode         {- ^ mode            -} ->+  HashSet Identifier {- ^ my nicks        -} ->+  UserInfo           {- ^ userinfo to draw-} ->+  Image coloredUserInfo palette NormalRender myNicks ui =   coloredIdentifier palette NormalIdentifier myNicks (userNick ui) coloredUserInfo palette DetailedRender myNicks !ui =@@ -403,8 +405,8 @@ -- highlighted. parseIrcTextWithNicks ::   Palette ->-  [Identifier] {- ^ my nicks -} ->-  [Identifier] {- ^ other nicks -} ->+  HashSet Identifier {- ^ my nicks    -} ->+  HashSet Identifier {- ^ other nicks -} ->   Text -> Image parseIrcTextWithNicks palette myNicks nicks txt   | Text.any isControl txt = parseIrcText txt@@ -414,16 +416,16 @@ -- an image where all of the occurrences of those nicknames are colored. highlightNicks ::   Palette ->-  [Identifier] {- ^ my nicks -} ->-  [Identifier] {- ^ other nicks -} ->+  HashSet Identifier {- ^ my nicks    -} ->+  HashSet Identifier {- ^ other nicks -} ->   Text -> Image highlightNicks palette myNicks nicks txt = horizCat (highlight1 <$> txtParts)   where-    nickSet = HashSet.fromList nicks     txtParts = nickSplit txt+    allNicks = HashSet.union myNicks nicks     highlight1 part-      | HashSet.member partId nickSet = coloredIdentifier palette PrivmsgIdentifier myNicks partId-      | otherwise                     = text' defAttr part+      | HashSet.member partId allNicks = coloredIdentifier palette PrivmsgIdentifier myNicks partId+      | otherwise                      = text' defAttr part       where         partId = mkId part 
src/Client/Image/StatusLine.hs view
@@ -147,6 +147,7 @@     subfocusName =       case view clientSubfocus st of         FocusMessages -> Nothing+        FocusWindows  -> Just $ string (view palLabel pal) "windows"         FocusInfo     -> Just $ string (view palLabel pal) "info"         FocusUsers    -> Just $ string (view palLabel pal) "users"         FocusMasks m  -> Just $ horizCat
src/Client/Image/UserList.hs view
@@ -49,7 +49,7 @@      matcher = clientMatcher st -    myNicks = toListOf csNick cs+    myNicks = clientHighlights cs st      renderUser (ident, sigils) =       string (view palSigil pal) sigils <|>@@ -95,7 +95,7 @@   where     matcher = clientMatcher st -    myNicks = toListOf csNick cs+    myNicks = clientHighlights cs st      pal = view (clientConfig . configPalette) st 
+ src/Client/Image/Windows.hs view
@@ -0,0 +1,72 @@+{-|+Module      : Client.Image.Windows+Description : View of the list of open windows+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module implements the rendering of the client window list.++-}+module Client.Image.Windows (windowsImages) where++import           Client.Configuration+import           Client.Image.Palette+import           Client.State+import           Client.State.Focus+import           Client.State.Window+import           Control.Lens+import           Data.List+import qualified Data.Map as Map+import qualified Data.Text as Text+import           Graphics.Vty.Image+import           Irc.Identifier++windowsImages :: ClientState -> [Image]+windowsImages st+  = reverse+  $ createColumns+  $ zipWith (renderWindowColumns pal) names windows+  where+    cfg     = view clientConfig st+    windows = views clientWindows Map.toList st++    pal     = view configPalette cfg+    names   = views configWindowNames Text.unpack cfg ++ repeat '?'++renderWindowColumns :: Palette -> Char -> (Focus, Window) -> [Image]+renderWindowColumns pal name (focus, win) =+  [ char (view palWindowName pal) name+  , renderedFocus pal focus+  , renderedWindowInfo pal win+  ]++createColumns :: [[Image]] -> [Image]+createColumns xs = map makeRow xs+  where+    columnWidths = maximum . map imageWidth <$> transpose xs+    makeRow = horizCat+            . intersperse (char defAttr ' ')+            . zipWith resizeWidth columnWidths++renderedFocus :: Palette -> Focus -> Image+renderedFocus pal focus =+  case focus of+    Unfocused ->+      char (view palError pal) '*'+    NetworkFocus network ->+      text' (view palLabel pal) network+    ChannelFocus network channel ->+      text' (view palLabel pal) network <|>+      char defAttr ':' <|>+      text' (view palLabel pal) (idText channel)++renderedWindowInfo :: Palette -> Window -> Image+renderedWindowInfo pal win =+  string (view newMsgAttrLens pal) (views winUnread show win) <|>+  char defAttr '/' <|>+  string (view palActivity pal) (views winTotal show win)+  where+    newMsgAttrLens+      | view winMention win = palMention+      | otherwise           = palActivity
src/Client/Network/Async.hs view
@@ -27,8 +27,11 @@   , NetworkId   , NetworkEvent(..)   , createConnection-  , abortConnection   , send++  -- * Abort connections+  , abortConnection+  , TerminationReason(..)   ) where  import           Client.Configuration.ServerSettings@@ -70,6 +73,16 @@   showsPrec p _ = showParen (p > 10)                 $ showString "NetworkConnection _" +-- | Exceptions used to kill connections manually.+data TerminationReason+  = PingTimeout      -- ^ sent when ping timer expires+  | ForcedDisconnect -- ^ sent when client commands force disconnect+  deriving Show++instance Exception TerminationReason where+  displayException PingTimeout      = "connection killed due to ping timeout"+  displayException ForcedDisconnect = "connection killed by client command"+ -- | Schedule a message to be transmitted on the network connection. -- These messages are sent unmodified. The message should contain a -- newline terminator.@@ -77,8 +90,8 @@ send c msg = atomically (writeTQueue (connOutQueue c) msg)  -- | Force the given connection to terminate.-abortConnection :: NetworkConnection -> IO ()-abortConnection = cancel . connAsync+abortConnection :: TerminationReason -> NetworkConnection -> IO ()+abortConnection reason c = cancelWith (connAsync c) reason  -- | Initiate a new network connection according to the given 'ServerSettings'. -- All events on this connection will be added to the given queue. The resulting
src/Client/State.hs view
@@ -33,9 +33,10 @@   , clientConnection   , clientBell   , clientExtensions-  , initialClientState+  , withClientState   , clientShutdown   , clientStartExtensions+  , clientPark    -- * Client operations   , clientMatcher@@ -49,6 +50,7 @@   , removeNetwork   , clientTick   , applyMessageToClientState+  , clientHighlights    -- * Add messages to buffers   , recordChannelMessage@@ -58,6 +60,7 @@   -- * Focus manipulation   , changeFocus   , changeSubfocus+  , returnFocus   , advanceFocus   , retreatFocus   , jumpToActivity@@ -67,6 +70,10 @@   , pageUp   , pageDown +  -- * Extensions+  , ExtensionState+  , esActive+   ) where  import           Client.CApi@@ -80,11 +87,13 @@ import           Client.State.Focus import           Client.State.Network import           Client.State.Window+import           Control.Concurrent.MVar import           Control.Concurrent.STM import           Control.DeepSeq import           Control.Exception import           Control.Lens import           Control.Monad+import           Data.Default.Class import           Data.Foldable import           Data.Either import           Data.HashMap.Strict (HashMap)@@ -98,6 +107,8 @@ import           Data.Text (Text) import qualified Data.Text as Text import           Data.Time+import           Foreign.Ptr+import           Foreign.StablePtr import           Graphics.Vty import           Irc.Codes import           Irc.Identifier@@ -113,6 +124,7 @@ -- | All state information for the IRC client data ClientState = ClientState   { _clientWindows           :: !(Map Focus Window) -- ^ client message buffers+  , _clientPrevFocus         :: !Focus              -- ^ previously focused buffer   , _clientFocus             :: !Focus              -- ^ currently focused buffer   , _clientSubfocus          :: !Subfocus           -- ^ sec @@ -133,11 +145,27 @@    , _clientIgnores           :: !(HashSet Identifier)     -- ^ ignored nicknames -  , _clientExtensions        :: [ActiveExtension]       -- ^ Active extensions+  , _clientExtensions        :: !ExtensionState   } +data ExtensionState = ExtensionState+  { _esActive    :: [ActiveExtension]+  , _esMVar      :: MVar ClientState+  , _esStablePtr :: StablePtr (MVar ClientState)+  }+ makeLenses ''ClientState+makeLenses ''ExtensionState +clientPark :: ClientState -> (Ptr () -> IO a) -> IO (ClientState, a)+clientPark st k =+  do let mvar = view (clientExtensions . esMVar) st+     putMVar mvar st+     let token = views (clientExtensions . esStablePtr) castStablePtrToPtr st+     res <- k token+     st' <- takeMVar mvar+     return (st', res)+ -- | 'Traversal' for finding the 'NetworkState' associated with a given network -- if that connection is currently active. clientConnection ::@@ -158,12 +186,16 @@ clientLine = views (clientTextBox . Edit.line) (\(Edit.Line n t) -> (n, t))  -- | Construct an initial 'ClientState' using default values.-initialClientState :: Configuration -> Vty -> IO ClientState-initialClientState cfg vty =+withClientState :: Configuration -> (ClientState -> IO a) -> IO a+withClientState cfg k =++  withVty            $ \vty ->+  withExtensionState $ \exts ->+   do (width,height) <- displayBounds (outputIface vty)      cxt            <- initConnectionContext      events         <- atomically newTQueue-     return ClientState+     k ClientState         { _clientWindows           = _Empty # ()         , _clientNetworkMap        = _Empty # ()         , _clientIgnores           = _Empty # ()@@ -173,6 +205,7 @@         , _clientHeight            = height         , _clientVty               = vty         , _clientEvents            = events+        , _clientPrevFocus         = Unfocused         , _clientFocus             = Unfocused         , _clientSubfocus          = FocusMessages         , _clientConnectionContext = cxt@@ -181,9 +214,24 @@         , _clientDetailView        = False         , _clientNextConnectionId  = 0         , _clientBell              = False-        , _clientExtensions        = []+        , _clientExtensions        = exts         } +-- | Initialize a 'Vty' value and run a continuation. Shutdown the 'Vty'+-- once the continuation finishes.+withVty :: (Vty -> IO a) -> IO a+withVty = bracket (mkVty def{bracketedPasteMode = Just True}) shutdown++withExtensionState :: (ExtensionState -> IO a) -> IO a+withExtensionState k =+  do mvar <- newEmptyMVar+     bracket (newStablePtr mvar) freeStablePtr $ \stab ->+       k ExtensionState+         { _esActive    = []+         , _esMVar      = mvar+         , _esStablePtr = stab+         }+ -- | Forcefully terminate the connection currently associated -- with a given network name. abortNetwork ::@@ -192,7 +240,7 @@ abortNetwork network st =   case preview (clientConnection network) st of     Nothing -> return st-    Just cs -> do abortConnection (view csSocket cs)+    Just cs -> do abortConnection ForcedDisconnect (view csSocket cs)                   return $ set (clientNetworkMap . at network) Nothing st  -- | Add a message to the window associated with a given channel@@ -210,8 +258,8 @@     rendParams = MessageRendererParams       { rendStatusMsg   = statusModes       , rendUserSigils  = computeMsgLineSigils network channel' msg st-      , rendNicks       = channelUserList network channel' st-      , rendMyNicks     = toListOf (clientConnection network . csNick) st+      , rendNicks       = HashSet.fromList (channelUserList network channel' st)+      , rendMyNicks     = highlights       , rendPalette     = view (clientConfig . configPalette) st       , rendNickPadding = view (clientConfig . configNickPadding) st       }@@ -220,6 +268,7 @@     possibleStatusModes     = view (clientConnection network . csStatusMsg) st     (statusModes, channel') = splitStatusMsgModes possibleStatusModes channel     importance              = msgImportance msg st+    highlights              = clientHighlightsNetwork network st   -- | Extract the status mode sigils from a message target.@@ -238,10 +287,12 @@ msgImportance msg st =   let network = view msgNetwork msg       me      = preview (clientConnection network . csNick) st+      highlights = clientHighlightsNetwork network st       isMe x  = Just x == me-      checkTxt txt = case me of-                       Just me' | me' `elem` (mkId <$> nickSplit txt) -> WLImportant-                       _ -> WLNormal+      checkTxt txt+        | any (\x -> HashSet.member (mkId x) highlights)+              (nickSplit txt) = WLImportant+        | otherwise           = WLNormal   in   case view msgBody msg of     NormalBody{} -> WLImportant@@ -380,6 +431,7 @@   toWindowLine defaultRenderParams     { rendPalette     = view configPalette     config     , rendNickPadding = view configNickPadding config+    , rendMyNicks     = view configExtraHighlights config     }  @@ -392,7 +444,8 @@ markSeen :: ClientState -> ClientState markSeen st =   case view clientSubfocus st of-    FocusMessages -> overStrict (clientWindows . ix (view clientFocus st)) windowSeen st+    FocusMessages ->+       overStrict (clientWindows . ix (view clientFocus st)) windowSeen st     _             -> st  -- | Add the textbox input to the edit history and clear the textbox.@@ -444,13 +497,13 @@   case (clientConnections . at networkId <<.~ Nothing) st of     (Nothing, _  ) -> error "removeNetwork: network not found"     (Just cs, st1) ->-      -- Only remove the network mapping if it hasn't already been replace+      -- Only remove the network mapping if it hasn't already been replaced       -- with a new one. This can happen during reconnect in particular.       let network = view csNetwork cs in-      case view (clientNetworkMap . at network) st of-        Just i | i == networkId ->-          (cs, set (clientNetworkMap . at network) Nothing st1)-        _ -> (cs,st1)+      forOf (clientNetworkMap . at network) st1 $ \mb ->+        case mb of+          Just i | i == networkId -> (cs,Nothing)+          _                       -> (cs,mb)  addConnection :: Text -> ClientState -> IO ClientState addConnection network st =@@ -525,8 +578,8 @@  clientStopExtensions :: ClientState -> IO ClientState clientStopExtensions st =-  do let (aes,st1) = (clientExtensions <<.~ []) st-     (st2,_) <- withStableMVar st1 $ \ptr ->+  do let (aes,st1) = (clientExtensions . esActive <<.~ []) st+     (st2,_) <- clientPark st1 $ \ptr ->                   traverse_ (deactivateExtension ptr) aes      return st2 @@ -535,13 +588,13 @@ clientStartExtensions st =   do let cfg = view clientConfig st      st1        <- clientStopExtensions st-     (st2, res) <- withStableMVar st1 $ \ptr ->+     (st2, res) <- clientPark st1 $ \ptr ->             traverse (try . activateExtension ptr <=< resolveConfigurationPath)                      (view configExtensions cfg)       let (errors, exts) = partitionEithers res      st3 <- recordErrors errors st2-     return $! set clientExtensions exts st3+     return $! set (clientExtensions . esActive) exts st3   where     recordErrors [] ste = return ste     recordErrors es ste =@@ -601,16 +654,27 @@     (focus,_) = Map.elemAt i windows  changeFocus :: Focus -> ClientState -> ClientState-changeFocus focus+changeFocus focus st   = set clientScroll 0+  . updatePrevious   . set clientFocus focus   . set clientSubfocus FocusMessages+  $ st+  where+    oldFocus = view clientFocus st+    updatePrevious+      | focus == oldFocus = id+      | otherwise         = set clientPrevFocus oldFocus  changeSubfocus :: Subfocus -> ClientState -> ClientState changeSubfocus focus   = set clientScroll 0   . set clientSubfocus focus +-- | Return to previously focused window.+returnFocus :: ClientState -> ClientState+returnFocus st = changeFocus (view clientPrevFocus st) st+ -- | Step focus to the next window when on message view. Otherwise -- switch to message view. advanceFocus :: ClientState -> ClientState@@ -637,3 +701,15 @@   where     isForward = not isReversed     (l,r)     = Map.split (view clientFocus st) (view clientWindows st)++clientHighlights :: NetworkState -> ClientState -> HashSet Identifier+clientHighlights cs st =+  HashSet.insert+    (view csNick cs)+    (view (clientConfig . configExtraHighlights) st)++clientHighlightsNetwork :: Text -> ClientState -> HashSet Identifier+clientHighlightsNetwork network st =+  case preview (clientConnection network) st of+    Just cs -> clientHighlights cs st+    Nothing -> view (clientConfig . configExtraHighlights) st
src/Client/State/EditBox.hs view
@@ -42,12 +42,13 @@   , killEnd   , killWordBackward   , killWordForward-  , paste+  , yank   , left   , right   , leftWord   , rightWord   , insert+  , insertPaste   , insertString   , earlier   , later@@ -165,7 +166,7 @@   = case view (content . below) e of       []   -> e       b:bs -> set (content . below) bs-            $ updateYankBuffer KillForward b e -- add newline?+            $ updateYankBuffer KillForward ('\n':b) e   | otherwise   = set line (endLine keep)   $ updateYankBuffer KillForward kill e@@ -178,10 +179,10 @@ killHome :: EditBox -> EditBox killHome e   | null kill-  = case view (content.above) e of+  = case view (content . above) e of       []   -> e-      a:as -> set (content.above) as-            $ updateYankBuffer KillBackward a e+      a:as -> set (content . above) as+            $ updateYankBuffer KillBackward (a++"\n") e    | otherwise   = set line (Line 0 keep)@@ -191,15 +192,15 @@   (kill,keep) = splitAt n txt  -- | Insert the yank buffer at the cursor.-paste :: EditBox -> EditBox-paste e+yank :: EditBox -> EditBox+yank e   = over content (insertString (view yankBuffer e))   $ set lastOperation OtherOperation e  -- | Kill the content from the cursor back to the previous word boundary. -- When @yank@ is set the yank buffer will be updated. killWordBackward :: Bool {- ^ yank -} -> EditBox -> EditBox-killWordBackward yank e+killWordBackward saveKill e   = sometimesUpdateYank   $ set line (Line (length l') (l'++r))   $ e@@ -212,13 +213,13 @@   yanked = reverse (sp++wd)    sometimesUpdateYank-    | yank      = updateYankBuffer KillBackward yanked+    | saveKill  = updateYankBuffer KillBackward yanked     | otherwise = id -- don't update operation  -- | Kill the content from the curser forward to the next word boundary. -- When @yank@ is set the yank buffer will be updated killWordForward :: Bool {- ^ yank -} -> EditBox -> EditBox-killWordForward yank e+killWordForward saveKill e   = sometimesUpdateYank   $ set line (Line (length l) (l++r2))   $ e@@ -230,11 +231,16 @@   yanked = sp++wd    sometimesUpdateYank-    | yank      = updateYankBuffer KillForward yanked+    | saveKill  = updateYankBuffer KillForward yanked     | otherwise = id -- don't update operation  -- | Insert a character at the cursor and advance the cursor. insert :: Char -> EditBox -> EditBox insert c   = set lastOperation OtherOperation-  . over content (insertString [c])+  . over content (insertChar c)++insertPaste :: String -> EditBox -> EditBox+insertPaste paste+  = over content (insertPastedString paste)+  . set lastOperation OtherOperation
src/Client/State/EditBox/Content.hs view
@@ -6,11 +6,47 @@ License     : ISC Maintainer  : emertens@gmail.com +This module manages simple text navigation and manipulation,+but leaves more complicated operations like yank/kill and+history management to "Client.State.EditBox"+ -}-module Client.State.EditBox.Content where+module Client.State.EditBox.Content+  (+  -- * Multiple lines+    Content+  , above+  , below+  , singleLine+  , noContent+  , shift +  -- * Focused line+  , Line(..)+  , HasLine(..)+  , endLine++  -- * Movements+  , left+  , right++  , leftWord+  , rightWord++  , jumpLeft+  , jumpRight++  -- * Edits+  , delete+  , backspace+  , insertPastedString+  , insertString+  , insertChar+  ) where+ import           Control.Lens hiding (below)-import           Data.Char+import           Data.Char (isAlphaNum)+import           Data.List (find)  data Line = Line   { _pos  :: !Int@@ -58,6 +94,8 @@ shift (Content a@(_:_) l b) = (last a, Content (init a) l b) shift (Content [] l (b:bs)) = (view text l, Content [] (beginLine b) bs) +-- | When at beginning of line, jump to beginning of previous line.+-- Otherwise jump to beginning of current line. jumpLeft :: Content -> Content jumpLeft c   | view pos c == 0 = maybe c begin1 (backwardLine c)@@ -65,17 +103,18 @@   where     begin1 = set pos 0 +-- | When at end of line, jump to end of next line.+-- Otherwise jump to end of current line. jumpRight :: Content -> Content jumpRight c   | view pos c == len = maybe c end1 (forwardLine c)   | otherwise         = set pos len c-   where     len    = views text length c     end1 l = set pos (views text length l) l  --- Move the cursor left, across lines if necessary.+-- | Move the cursor left, across lines if necessary. left :: Content -> Content left c =   case compare (view pos c) 0 of@@ -83,7 +122,7 @@     EQ | Just c' <- backwardLine c -> c'     _                              -> c --- Move the cursor right, across lines if necessary.+-- | Move the cursor right, across lines if necessary. right :: Content -> Content right c =   let Line n s = view line c in@@ -95,38 +134,30 @@ -- | Move the cursor left to the previous word boundary. leftWord :: Content -> Content leftWord c-  | n == 0-  = maybe c leftWord (backwardLine c)-  | otherwise-  = case search of-      []      -> set pos 0     c-      (i,_):_ -> set pos (i+1) c+  | n == 0    = maybe c leftWord (backwardLine c)+  | otherwise = set pos search c   where-  Line n txt = view line c-  search = dropWhile (isAlphaNum . snd)-         $ dropWhile (not . isAlphaNum . snd)-         $ reverse-         $ take n-         $ zip [0..]-         $ txt+    Line n txt = view line c+    search = maybe 0 fst+           $ find      (not . isAlphaNum . snd)+           $ dropWhile (not . isAlphaNum . snd)+           $ reverse+           $ take n+           $ zip [1..] txt  -- | Move the cursor right to the next word boundary. rightWord :: Content -> Content rightWord c-  | n == length txt-  = case forwardLine c of-      Nothing -> c-      Just c' -> rightWord c'-  | otherwise-  = case search of-      []      -> set pos (length txt) c-      (i,_):_ -> set pos i c+  | n == txtLen = maybe c rightWord (forwardLine c)+  | otherwise   = set pos search c   where-  Line n txt = view line c-  search = dropWhile (isAlphaNum . snd)-         $ dropWhile (not . isAlphaNum . snd)-         $ drop n-         $ zip [0..] txt+    Line n txt = view line c+    txtLen = length txt+    search = maybe txtLen fst+           $ find      (not . isAlphaNum . snd)+           $ dropWhile (not . isAlphaNum . snd)+           $ drop n+           $ zip [0..] txt  -- | Delete the character before the cursor. backspace :: Content -> Content@@ -155,6 +186,39 @@                                . set text (s ++ b)                                $ c +-- | Insert character at cursor, cursor is advanced.+insertChar :: Char -> Content -> Content+insertChar '\n' c+  = over above (view text c :)+  $ set line emptyLine c++insertChar ins c = over line aux c+  where+    aux (Line n txt) =+      case splitAt n txt of+        (preS, postS) -> Line (n+1) (preS ++ ins : postS)++-- | Smarter version of 'insertString' that removes spurious newlines.+insertPastedString :: String -> Content -> Content+insertPastedString paste c = insertString (foldr scrub "" paste) c+  where+    cursorAtEnd = null (view below c)+               && length (view text c) == view pos c++    -- ignore formfeeds+    scrub '\r' xs = xs++    -- avoid adding empty lines+    scrub '\n' xs@('\n':_) = xs++    -- avoid adding trailing newline at end of textbox+    scrub '\n' "" | cursorAtEnd = ""++    -- pass-through everything else+    scrub x xs = x : xs++-- | Insert string at cursor, cursor is advanced to the+-- end of the inserted string. insertString :: String -> Content -> Content insertString ins c =   case push (view above c) (preS ++ l) ls of@@ -168,6 +232,7 @@     push stk x []     = (stk, Line (length x) (x ++ postS))     push stk x (y:ys) = push (x:stk) y ys +-- | Advance to the beginning of the next line forwardLine :: Content -> Maybe Content forwardLine c =   case view below c of@@ -177,6 +242,7 @@           $ set below bs           $ set line (beginLine b) c +-- | Retreat to the end of the previous line backwardLine :: Content -> Maybe Content backwardLine c =   case view above c of
src/Client/State/Focus.hs view
@@ -30,6 +30,7 @@   , _FocusInfo   , _FocusUsers   , _FocusMasks+  , _FocusWindows   ) where  import           Control.Lens@@ -46,12 +47,13 @@  makePrisms ''Focus --- | Subfocus for a channel view+-- | Subfocus view data Subfocus-  = FocusMessages    -- ^ Show chat messages+  = FocusMessages    -- ^ Show messages   | FocusInfo        -- ^ Show channel metadata-  | FocusUsers       -- ^ Show user list-  | FocusMasks !Char -- ^ Show mask list for given mode+  | FocusUsers       -- ^ Show channel user list+  | FocusMasks !Char -- ^ Show channel mask list for given mode+  | FocusWindows     -- ^ Show client windows   deriving (Eq,Show)  makePrisms ''Subfocus
src/Client/State/Network.hs view
@@ -407,7 +407,7 @@             . doMode msgWhen who chanId modes params             . set (csChannels . ix chanId . chanModes) Map.empty             where chanId = mkId chan-                  !who = UserInfo (mkId "*") "" ""+                  !who = UserInfo "*" "" ""         _ -> id     _ -> id @@ -600,8 +600,8 @@ initialMessages cs    = [ ircCapLs ]   ++ [ ircPass pass | Just pass <- [view ssPassword ss]]-  ++ [ ircUser (view ssUser ss) False True (view ssReal ss)-     , ircNick (view csNick cs)+  ++ [ ircNick (view csNick cs)+     , ircUser (view ssUser ss) False True (view ssReal ss)      ]   where     ss = view csSettings cs@@ -779,7 +779,7 @@ applyTimedAction action cs =   case action of     TimedDisconnect ->-      do abortConnection (view csSocket cs)+      do abortConnection PingTimeout (view csSocket cs)          return $! set csNextPingTime Nothing cs      TimedSendPing ->
src/Client/State/Window.hs view
@@ -17,6 +17,7 @@     Window(..)   , winMessages   , winUnread+  , winTotal   , winMention    -- * Window lines@@ -53,6 +54,7 @@ data Window = Window   { _winMessages :: ![WindowLine] -- ^ Messages to display, newest first   , _winUnread   :: !Int          -- ^ Messages added since buffer was visible+  , _winTotal    :: !Int          -- ^ Messages in buffer   , _winMention  :: !Bool         -- ^ Indicates an important event is unread   } @@ -71,6 +73,7 @@ emptyWindow = Window   { _winMessages = []   , _winUnread   = 0+  , _winTotal    = 0   , _winMention  = False   } @@ -79,6 +82,7 @@ addToWindow :: WindowLineImportance -> WindowLine -> Window -> Window addToWindow importance !msg !win = Window     { _winMessages = msg : _winMessages win+    , _winTotal    = _winTotal win + 1     , _winUnread   = _winUnread win                    + (if importance == WLBoring then 0 else 1)     , _winMention  = _winMention win
src/Main.hs view
@@ -11,12 +11,9 @@ module Main where  import Control.Concurrent-import Control.Exception import Control.Lens import Control.Monad-import Data.Default.Class import Data.Text (Text)-import Graphics.Vty import System.Exit import System.IO @@ -27,22 +24,16 @@ import Client.State import Client.State.Focus --- | Initialize a 'Vty' value and run a continuation. Shutdown the 'Vty'--- once the continuation finishes.-withVty :: (Vty -> IO a) -> IO a-withVty = bracket (mkVty def{bracketedPasteMode = Just True}) shutdown- -- | Main action for IRC client main :: IO () main =   do args <- getCommandArguments      cfg  <- loadConfiguration' (view cmdArgConfigFile args)-     withVty $ \vty ->-       runInUnboundThread $-         initialClientState cfg vty >>=-         clientStartExtensions      >>=-         addInitialNetworks (view cmdArgInitialNetworks args) >>=-         eventLoop+     runInUnboundThread $+       withClientState cfg $+       clientStartExtensions >=>+       addInitialNetworks (view cmdArgInitialNetworks args) >=>+       eventLoop  -- | Load configuration and handle errors along the way. loadConfiguration' :: Maybe FilePath -> IO Configuration