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 +12/−0
- README.md +7/−1
- exported_symbols.txt +2/−0
- glirc.cabal +10/−7
- src/Client/CApi.hs +14/−23
- src/Client/CApi/Exports.hs +76/−13
- src/Client/CApi/Types.hsc +9/−7
- src/Client/Commands.hs +249/−118
- src/Client/Commands/Exec.hs +73/−0
- src/Client/Commands/WordCompletion.hs +22/−16
- src/Client/Configuration.hs +15/−8
- src/Client/EventLoop.hs +30/−13
- src/Client/Hook/Znc/Buffextras.hs +2/−2
- src/Client/Image.hs +16/−4
- src/Client/Image/ChannelInfo.hs +5/−5
- src/Client/Image/Message.hs +25/−23
- src/Client/Image/StatusLine.hs +1/−0
- src/Client/Image/UserList.hs +2/−2
- src/Client/Image/Windows.hs +72/−0
- src/Client/Network/Async.hs +16/−3
- src/Client/State.hs +99/−23
- src/Client/State/EditBox.hs +18/−12
- src/Client/State/EditBox/Content.hs +97/−31
- src/Client/State/Focus.hs +6/−4
- src/Client/State/Network.hs +4/−4
- src/Client/State/Window.hs +4/−0
- src/Main.hs +5/−14
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