packages feed

glirc 2.35 → 2.36

raw patch · 39 files changed

+3271/−2737 lines, 39 filesdep +mwc-randomdep ~basedep ~base64-bytestringdep ~hookupnew-component:exe:glircPVP ok

version bump matches the API change (PVP)

Dependencies added: mwc-random

Dependency ranges changed: base, base64-bytestring, hookup, irc-core, template-haskell, time, vty

API changes (from Hackage documentation)

- Client.Authentication.Ecdsa: encodeUsername :: Text -> AuthenticatePayload
- Client.Configuration.ServerSettings: [_ssProtocolFamily] :: ServerSettings -> Maybe Family
- Client.Configuration.ServerSettings: [_ssSaslEcdsaFile] :: ServerSettings -> !Maybe FilePath
- Client.Configuration.ServerSettings: [_ssSaslPassword] :: ServerSettings -> !Maybe Text
- Client.Configuration.ServerSettings: [_ssSaslUsername] :: ServerSettings -> !Maybe Text
- Client.Configuration.ServerSettings: loadDefaultServerSettings :: IO ServerSettings
- Client.Configuration.ServerSettings: ssProtocolFamily :: Lens' ServerSettings (Maybe Family)
- Client.Configuration.ServerSettings: ssSaslEcdsaFile :: Lens' ServerSettings (Maybe FilePath)
- Client.Configuration.ServerSettings: ssSaslPassword :: Lens' ServerSettings (Maybe Text)
- Client.Configuration.ServerSettings: ssSaslUsername :: Lens' ServerSettings (Maybe Text)
- Client.State: Matcher :: !Int -> !Int -> (Text -> Bool) -> Matcher
- Client.State: [matcherAfter] :: Matcher -> !Int
- Client.State: [matcherBefore] :: Matcher -> !Int
- Client.State: [matcherPred] :: Matcher -> Text -> Bool
- Client.State: clientMatcher :: ClientState -> Maybe Matcher
- Client.State: data Matcher
- Client.State.Network: CapTransaction :: Transaction
+ Client.Authentication.Ecdsa: encodeAuthentication :: Text -> Text -> AuthenticatePayload
+ Client.Commands.Channel: channelCommands :: CommandSection
+ Client.Commands.Chat: chatCommand' :: (UserInfo -> Identifier -> IrcMsg) -> Text -> NetworkState -> ClientState -> IO ClientState
+ Client.Commands.Chat: chatCommands :: CommandSection
+ Client.Commands.Chat: cmdCtcp :: NetworkCommand (String, String, String)
+ Client.Commands.Chat: executeChat :: String -> ClientState -> IO CommandResult
+ Client.Commands.Connection: connectionCommands :: CommandSection
+ Client.Commands.DCC: dccCommands :: CommandSection
+ Client.Commands.Operator: operatorCommands :: CommandSection
+ Client.Commands.Queries: queryCommands :: CommandSection
+ Client.Commands.TabCompletion: activeNicks :: ClientState -> [Identifier]
+ Client.Commands.TabCompletion: nickTabCompletion :: Bool -> ClientState -> IO CommandResult
+ Client.Commands.TabCompletion: noChannelTab :: Bool -> ChannelCommand String
+ Client.Commands.TabCompletion: noClientTab :: Bool -> ClientCommand String
+ Client.Commands.TabCompletion: noNetworkTab :: Bool -> NetworkCommand String
+ Client.Commands.TabCompletion: simpleChannelTab :: Bool -> ChannelCommand String
+ Client.Commands.TabCompletion: simpleClientTab :: Bool -> ClientCommand String
+ Client.Commands.TabCompletion: simpleNetworkTab :: Bool -> NetworkCommand String
+ Client.Commands.TabCompletion: simpleTabCompletion :: Prefix a => WordCompletionMode -> [a] -> [a] -> Bool -> ClientState -> IO CommandResult
+ Client.Commands.Toggles: togglesCommands :: CommandSection
+ Client.Commands.Types: ChannelCommand :: ChannelCommand a -> (Bool -> ChannelCommand String) -> CommandImpl a
+ Client.Commands.Types: ChatCommand :: ChannelCommand a -> (Bool -> ChannelCommand String) -> CommandImpl a
+ Client.Commands.Types: ClientCommand :: ClientCommand a -> (Bool -> ClientCommand String) -> CommandImpl a
+ Client.Commands.Types: Command :: NonEmpty Text -> Args ClientState a -> Text -> CommandImpl a -> Command
+ Client.Commands.Types: CommandFailure :: ClientState -> CommandResult
+ Client.Commands.Types: CommandQuit :: ClientState -> CommandResult
+ Client.Commands.Types: CommandSection :: Text -> [Command] -> CommandSection
+ Client.Commands.Types: CommandSuccess :: ClientState -> CommandResult
+ Client.Commands.Types: NetworkCommand :: NetworkCommand a -> (Bool -> NetworkCommand String) -> CommandImpl a
+ Client.Commands.Types: [cmdArgumentSpec] :: Command -> Args ClientState a
+ Client.Commands.Types: [cmdDocumentation] :: Command -> Text
+ Client.Commands.Types: [cmdImplementation] :: Command -> CommandImpl a
+ Client.Commands.Types: [cmdNames] :: Command -> NonEmpty Text
+ Client.Commands.Types: [cmdSectionCmds] :: CommandSection -> [Command]
+ Client.Commands.Types: [cmdSectionName] :: CommandSection -> Text
+ Client.Commands.Types: commandFailure :: Monad m => ClientState -> m CommandResult
+ Client.Commands.Types: commandFailureMsg :: Text -> ClientState -> IO CommandResult
+ Client.Commands.Types: commandSuccess :: Monad m => ClientState -> m CommandResult
+ Client.Commands.Types: data Command
+ Client.Commands.Types: data CommandImpl a
+ Client.Commands.Types: data CommandResult
+ Client.Commands.Types: data CommandSection
+ Client.Commands.Types: type ChannelCommand a = Identifier " focused channel " -> NetworkCommand a
+ Client.Commands.Types: type ClientCommand a = ClientState -> a " arguments " -> IO CommandResult
+ Client.Commands.Types: type NetworkCommand a = NetworkState " current network " -> ClientCommand a
+ Client.Commands.Window: parseFocus :: Maybe Text -> String -> Maybe Focus
+ Client.Commands.Window: windowCommands :: CommandSection
+ Client.Commands.ZNC: zncCommands :: CommandSection
+ Client.Configuration.ServerSettings: SaslEcdsa :: Maybe Text -> Text -> FilePath -> SaslMechanism
+ Client.Configuration.ServerSettings: SaslExternal :: Maybe Text -> SaslMechanism
+ Client.Configuration.ServerSettings: SaslPlain :: Maybe Text -> Text -> Secret -> SaslMechanism
+ Client.Configuration.ServerSettings: SecretCommand :: NonEmpty Text -> Secret
+ Client.Configuration.ServerSettings: SecretException :: String -> String -> SecretException
+ Client.Configuration.ServerSettings: SecretText :: Text -> Secret
+ Client.Configuration.ServerSettings: [_ssBindHostName] :: ServerSettings -> Maybe HostName
+ Client.Configuration.ServerSettings: [_ssSaslMechanism] :: ServerSettings -> !Maybe SaslMechanism
+ Client.Configuration.ServerSettings: [_ssTlsClientKeyPassword] :: ServerSettings -> !Maybe Secret
+ Client.Configuration.ServerSettings: _SaslEcdsa :: Prism' SaslMechanism (Maybe Text, Text, FilePath)
+ Client.Configuration.ServerSettings: _SaslExternal :: Prism' SaslMechanism (Maybe Text)
+ Client.Configuration.ServerSettings: _SaslPlain :: Prism' SaslMechanism (Maybe Text, Text, Secret)
+ Client.Configuration.ServerSettings: data SaslMechanism
+ Client.Configuration.ServerSettings: data Secret
+ Client.Configuration.ServerSettings: data SecretException
+ Client.Configuration.ServerSettings: defaultServerSettings :: ServerSettings
+ Client.Configuration.ServerSettings: instance Config.Schema.Spec.HasSpec Client.Configuration.ServerSettings.Secret
+ Client.Configuration.ServerSettings: instance GHC.Exception.Type.Exception Client.Configuration.ServerSettings.SecretException
+ Client.Configuration.ServerSettings: instance GHC.Show.Show Client.Configuration.ServerSettings.SaslMechanism
+ Client.Configuration.ServerSettings: instance GHC.Show.Show Client.Configuration.ServerSettings.Secret
+ Client.Configuration.ServerSettings: instance GHC.Show.Show Client.Configuration.ServerSettings.SecretException
+ Client.Configuration.ServerSettings: loadSecrets :: ServerSettings -> IO ServerSettings
+ Client.Configuration.ServerSettings: ssBindHostName :: Lens' ServerSettings (Maybe HostName)
+ Client.Configuration.ServerSettings: ssSaslMechanism :: Lens' ServerSettings (Maybe SaslMechanism)
+ Client.Configuration.ServerSettings: ssTlsClientKeyPassword :: Lens' ServerSettings (Maybe Secret)
+ Client.State: clientFilter :: ClientState -> (a -> Text) -> [a] -> [a]
+ Client.State: clientIsFiltered :: ClientState -> Bool
+ Client.State.Network: [_csSeed] :: NetworkState -> Seed
+ Client.State.Network: csSeed :: Lens' NetworkState Seed
- Client.Configuration.ServerSettings: ServerSettings :: !NonEmpty Text -> !Text -> !Text -> !Maybe Text -> !Maybe Text -> !Maybe Text -> !Maybe FilePath -> !HostName -> !Maybe PortNumber -> !UseTls -> !Maybe FilePath -> !Maybe FilePath -> !Maybe FilePath -> String -> ![[ExpansionChunk]] -> !Maybe HostName -> !PortNumber -> ![Identifier] -> !Rational -> !Rational -> ![HookConfig] -> !Maybe Text -> !Int -> !Maybe KnownRegex -> !Bool -> WordCompletionMode -> Maybe FilePath -> Maybe Family -> !Bool -> !Maybe Fingerprint -> !Maybe Fingerprint -> !Bool -> ![Text] -> ServerSettings
+ Client.Configuration.ServerSettings: ServerSettings :: !NonEmpty Text -> !Text -> !Text -> !Maybe Secret -> !Maybe SaslMechanism -> !HostName -> !Maybe PortNumber -> !UseTls -> !Maybe FilePath -> !Maybe FilePath -> !Maybe Secret -> !Maybe FilePath -> String -> ![[ExpansionChunk]] -> !Maybe HostName -> !PortNumber -> ![Identifier] -> !Rational -> !Rational -> ![HookConfig] -> !Maybe Text -> !Int -> !Maybe KnownRegex -> !Bool -> WordCompletionMode -> Maybe FilePath -> Maybe HostName -> !Bool -> !Maybe Fingerprint -> !Maybe Fingerprint -> !Bool -> ![Text] -> ServerSettings
- Client.Configuration.ServerSettings: [_ssPassword] :: ServerSettings -> !Maybe Text
+ Client.Configuration.ServerSettings: [_ssPassword] :: ServerSettings -> !Maybe Secret
- Client.Configuration.ServerSettings: ssPassword :: Lens' ServerSettings (Maybe Text)
+ Client.Configuration.ServerSettings: ssPassword :: Lens' ServerSettings (Maybe Secret)
- Client.Image.Message: MessageRendererParams :: [Char] -> [Char] -> HashSet Identifier -> HashSet Identifier -> Palette -> HashMap Identifier UserAndHost -> MessageRendererParams
+ Client.Image.Message: MessageRendererParams :: [Char] -> [Char] -> HashSet Identifier -> HashSet Identifier -> Palette -> Maybe (HashMap Identifier UserAndHost) -> MessageRendererParams
- Client.Image.Message: [rendAccounts] :: MessageRendererParams -> HashMap Identifier UserAndHost
+ Client.Image.Message: [rendAccounts] :: MessageRendererParams -> Maybe (HashMap Identifier UserAndHost)
- Client.Image.StatusLine: minorStatusLineImage :: Focus -> Int -> Bool -> ClientState -> Image'
+ Client.Image.StatusLine: minorStatusLineImage :: Focus -> Subfocus -> Int -> Bool -> ClientState -> Image'
- Client.State: ClientState :: !Map Focus Window -> !Focus -> !Maybe Focus -> !Focus -> !Subfocus -> ![Focus] -> !HashMap Text NetworkState -> !TQueue NetworkEvent -> !DCCState -> !TChan DCCUpdate -> !Configuration -> !FilePath -> !EditBox -> !Int -> !Int -> !Int -> !Int -> !Bool -> !Bool -> !Bool -> Maybe Matcher -> !LayoutMode -> !Bool -> !HashSet Identifier -> Mask -> !ExtensionState -> ![LogLine] -> Maybe Text -> Maybe Stats -> !HashMap Text StsPolicy -> ClientState
+ Client.State: ClientState :: !Map Focus Window -> !Focus -> !Maybe Focus -> !Focus -> !Subfocus -> ![(Focus, Subfocus)] -> !HashMap Text NetworkState -> !TQueue NetworkEvent -> !DCCState -> !TChan DCCUpdate -> !Configuration -> !FilePath -> !EditBox -> !Int -> !Int -> !Int -> !Int -> !Bool -> !Bool -> !Bool -> Maybe Matcher -> !LayoutMode -> !Bool -> !HashSet Identifier -> Mask -> !ExtensionState -> ![LogLine] -> Maybe Text -> Maybe Stats -> !HashMap Text StsPolicy -> ClientState
- Client.State: [_clientExtraFocus] :: ClientState -> ![Focus]
+ Client.State: [_clientExtraFocus] :: ClientState -> ![(Focus, Subfocus)]
- Client.State: clientExtraFocus :: Lens' ClientState [Focus]
+ Client.State: clientExtraFocus :: Lens' ClientState [(Focus, Subfocus)]
- Client.State: clientExtraFocuses :: ClientState -> [Focus]
+ Client.State: clientExtraFocuses :: ClientState -> [(Focus, Subfocus)]
- Client.State: setExtraFocus :: [Focus] -> ClientState -> ClientState
+ Client.State: setExtraFocus :: [(Focus, Subfocus)] -> ClientState -> ClientState
- Client.State.EditBox: class HasLine c_a193H
+ Client.State.EditBox: class HasLine c_a19Z8
- Client.State.EditBox: line :: HasLine c_a193H => Lens' c_a193H Line
+ Client.State.EditBox: line :: HasLine c_a19Z8 => Lens' c_a19Z8 Line
- Client.State.EditBox: pos :: HasLine c_a193H => Lens' c_a193H Int
+ Client.State.EditBox: pos :: HasLine c_a19Z8 => Lens' c_a19Z8 Int
- Client.State.EditBox: text :: HasLine c_a193H => Lens' c_a193H String
+ Client.State.EditBox: text :: HasLine c_a19Z8 => Lens' c_a19Z8 String
- Client.State.EditBox.Content: class HasLine c_a193H
+ Client.State.EditBox.Content: class HasLine c_a19Z8
- Client.State.EditBox.Content: line :: HasLine c_a193H => Lens' c_a193H Line
+ Client.State.EditBox.Content: line :: HasLine c_a19Z8 => Lens' c_a19Z8 Line
- Client.State.EditBox.Content: pos :: HasLine c_a193H => Lens' c_a193H Int
+ Client.State.EditBox.Content: pos :: HasLine c_a19Z8 => Lens' c_a19Z8 Int
- Client.State.EditBox.Content: text :: HasLine c_a193H => Lens' c_a193H String
+ Client.State.EditBox.Content: text :: HasLine c_a19Z8 => Lens' c_a19Z8 String
- Client.State.Network: NetworkState :: !HashMap Identifier ChannelState -> !NetworkConnection -> !ModeTypes -> !ModeTypes -> ![Char] -> !Transaction -> ![Char] -> ![Char] -> ![Char] -> !ServerSettings -> !UserInfo -> !HashMap Identifier UserAndHost -> !Int -> !Text -> ![MessageHook] -> !AuthenticateState -> !Maybe UTCTime -> !Maybe NominalDiffTime -> !PingStatus -> !Maybe UTCTime -> ![Text] -> NetworkState
+ Client.State.Network: NetworkState :: !HashMap Identifier ChannelState -> !NetworkConnection -> !ModeTypes -> !ModeTypes -> ![Char] -> !Transaction -> ![Char] -> ![Char] -> ![Char] -> !ServerSettings -> !UserInfo -> !HashMap Identifier UserAndHost -> !Int -> !Text -> ![MessageHook] -> !AuthenticateState -> !Maybe UTCTime -> !Maybe NominalDiffTime -> !PingStatus -> !Maybe UTCTime -> ![Text] -> Seed -> NetworkState
- Client.State.Network: newNetworkState :: Text -> ServerSettings -> NetworkConnection -> PingStatus -> NetworkState
+ Client.State.Network: newNetworkState :: Text -> ServerSettings -> NetworkConnection -> PingStatus -> Seed -> NetworkState

Files

ChangeLog.md view
@@ -1,4 +1,40 @@-# Revision history for glirc2+# Revision history for glirc++## 2.36++* **Changed executable name to `glirc`**+* Allow all windows (not just chat message windows) to be added as split-screen targets+* Chose a random nickname when list of alternatives is exhausted+* Add simple man page+* Add `/monitor`+* Support specifying `bind-hostname` to pick a local address to connect+  from in favor of `protocol-family`. To pick a protocol family you+  can now specify `bind-hostname: "0.0.0.0"` or `bind-hostname: "::"`+* Add TLS key PEM password configuration+* Make all configuration file passwords able to be loaded from executing a command+* Make SASL mechanism specification specific and support an optional+  authorization identity `authzid` used in rare circumstances but+  supported by SASL.++  ```+  sasl: -- example 1+    mechanism: plain -- sasl defaults to plain if not specified+    username: "myuser"+    password: "mypassword"++  sasl: -- example 2+    mechanism: external+    authzid: "otheruser" -- optional++  sasl: -- example 3+    mechanism: ecdsa-nist256p-challenge+    username: "myuser"+    private-key: "path/to/my.key"+  ```++  Note that now using a client-side TLS certificate is an independent+  configuration from using EXTERNAL. To use both you need to configure+  both!  ## 2.35 * Added client certificate information to `/cert`
README.md view
@@ -87,7 +87,7 @@ -- vim: filetype=config-value -- Grab the Vim syntax highlighting file from the config-value package --- Learn more about these settings with `glirc2 --config-format`+-- Learn more about these settings with `glirc --config-format`  -- Defaults used when not specified on command line defaults:
exec/Exports.hs view
@@ -5,7 +5,7 @@ License     : ISC Maintainer  : emertens@gmail.com -Entry point into glirc2 from the C API+Entry point into glirc from the C API  -} module Exports () where
exec/Main.hs view
@@ -5,7 +5,7 @@ License     : ISC Maintainer  : emertens@gmail.com -Entry point into glirc2. This module sets up VTY and launches the client.+Entry point into glirc. This module sets up VTY and launches the client. -} module Main where 
+ glirc.1 view
@@ -0,0 +1,64 @@+.TH GLIRC 1 2020-07-27 "Glirc IRC client"+.SH NAME+Glirc \- an IRC client+.SH SYNOPSIS+.B glirc+[\-\-config=PATH] [\-\-noconnect] [\-\-version] [\-\-full\-version]+[\-\-config-format] [\-\-help] [network...]+.SH DESCRIPTION+.B Glirc+is a fully-featured console IRC client. It focuses on providing both+high-detail and concise views of an IRC connection. It provides an+advanced line-editing environment including syntax-highlighting,+multi-line buffering, and argument placeholders.+.SH OPTIONS+.TP+.BI "\-c, \-\-config="PATH+use+.I PATH+instead of ~/.config/glirc/config+.TP+.BI "\-!, \-\-noconnect"+disable autoconnecting+.TP+.BI "\-v, \-\-version"+print version information+.TP+.BI "\-\-full\-version"+show detailed version information+.TP+.BI "\-\-config\-format"+show configuration file format+.TP+.BI "\-h, \-\-help"+show help message+.TP+.I network+networks to connect to on startup+.SH NOTES+.B Glirc+has online documentation available inside with /help and online at+https://github.com/glguy/irc-core/wiki+.SH FILES+.TP+.I ~/.config/glirc/config+configuration file+.TP+.I ~/.config/glirc/sts.cfg+Strict Transport Security state+.SH EXAMPLES+Sample simple configuration file. More detail available at+https://github.com/glguy/irc-core/wiki/Configuration-File++.RS+defaults:+  nick: "glircuser"+  tls:  yes++servers:+  * name:     "freenode"+    hostname: "chat.freenode.net"++  * name:     "oftc"+    hostname: "irc.oftc.net"+.RE
glirc.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.2 name:                glirc-version:             2.35+version:             2.36 synopsis:            Console IRC client description:         Console IRC client                      .@@ -17,12 +17,13 @@ extra-source-files:  ChangeLog.md README.md                      exec/linux_exported_symbols.txt                      exec/macos_exported_symbols.txt+extra-doc-files:     glirc.1 homepage:            https://github.com/glguy/irc-core bug-reports:         https://github.com/glguy/irc-core/issues tested-with:         GHC==8.6.5  custom-setup-  setup-depends: base     >=4.12 && <4.14,+  setup-depends: base     >=4.12 && <4.15,                  filepath >=1.4  && <1.5,                  Cabal    >=2.2  && <4 @@ -31,7 +32,7 @@   location: git://github.com/glguy/irc-core.git   branch: v2 -executable glirc2+executable glirc   main-is:             Main.hs   other-modules:       Exports   ghc-options:         -threaded -rtsopts@@ -61,13 +62,24 @@                        Client.CApi.Exports                        Client.CApi.Types                        Client.Commands-                       Client.Commands.Arguments.Spec                        Client.Commands.Arguments.Parser                        Client.Commands.Arguments.Renderer+                       Client.Commands.Arguments.Spec+                       Client.Commands.Channel+                       Client.Commands.Chat+                       Client.Commands.Connection+                       Client.Commands.DCC                        Client.Commands.Exec                        Client.Commands.Interpolation+                       Client.Commands.Operator+                       Client.Commands.Queries                        Client.Commands.Recognizer+                       Client.Commands.TabCompletion+                       Client.Commands.Toggles+                       Client.Commands.Types+                       Client.Commands.Window                        Client.Commands.WordCompletion+                       Client.Commands.ZNC                        Client.Configuration                        Client.Configuration.Colors                        Client.Configuration.Macros@@ -137,11 +149,11 @@   autogen-modules:     Paths_glirc                        Build_glirc -  build-depends:       base                 >=4.11   && <4.14,+  build-depends:       base                 >=4.11   && <4.15,                        HsOpenSSL            >=0.11   && <0.12,                        async                >=2.2    && <2.3,                        attoparsec           >=0.13   && <0.14,-                       base64-bytestring    >=1.0.0.1&& <1.1,+                       base64-bytestring    >=1.0.0.1&& <1.2,                        bytestring           >=0.10.8 && <0.11,                        config-schema        ^>=1.2.0.0,                        config-value         ^>=0.7,@@ -151,10 +163,11 @@                        free                 >=4.12   && <5.2,                        gitrev               >=1.2    && <1.4,                        hashable             >=1.2.4  && <1.4,-                       hookup               >=0.3.1  && <0.4,-                       irc-core             >=2.7.1  && <2.8,+                       hookup               >=0.4    && <0.5,+                       irc-core             >=2.8    && <2.9,                        kan-extensions       >=5.0    && <5.3,                        lens                 >=4.14   && <4.20,+                       mwc-random           >=0.14   && <0.15,                        network              >=2.6.2  && <3.2,                        process              >=1.4.2  && <1.7,                        psqueues             >=0.2.7  && <0.3,@@ -162,14 +175,14 @@                        semigroupoids        >=5.1    && <5.4,                        split                >=0.2    && <0.3,                        stm                  >=2.4    && <2.6,-                       template-haskell     >=2.11   && <2.16,+                       template-haskell     >=2.11   && <2.17,                        text                 >=1.2.2  && <1.3,-                       time                 >=1.6    && <1.10,+                       time                 >=1.6    && <1.11,                        transformers         >=0.5.2  && <0.6,                        unix                 >=2.7    && <2.8,                        unordered-containers >=0.2.7  && <0.3,                        vector               >=0.11   && <0.13,-                       vty                  >=5.23.1 && <5.28+                       vty                  >=5.23.1 && <5.31  test-suite test   type:                exitcode-stdio-1.0
src/Client/Authentication/Ecdsa.hs view
@@ -16,7 +16,7 @@ -} module Client.Authentication.Ecdsa   ( authenticationMode-  , encodeUsername+  , encodeAuthentication   , computeResponse   ) where @@ -37,10 +37,12 @@   -- | Encode a username as specified in this authentication mode.-encodeUsername ::-  Text {- ^ username                 -} ->-  AuthenticatePayload {- ^ base-64 encoded username -}-encodeUsername = AuthenticatePayload . Text.encodeUtf8+encodeAuthentication ::+  Text {- ^ authorization identity  -} ->+  Text {- ^ authentication identity -} ->+  AuthenticatePayload+encodeAuthentication authz authc =+  AuthenticatePayload (Text.encodeUtf8 (authz <> "\0" <> authc <> "\0"))   -- | Compute the response for a given challenge using the @ecdsatool@
src/Client/Commands.hs view
@@ -32,2475 +32,500 @@ import           Client.Commands.Recognizer import           Client.Commands.WordCompletion import           Client.Configuration-import           Client.Mask-import           Client.Message-import           Client.Image.PackedImage (imageText)-import           Client.State-import           Client.State.Channel-import           Client.State.DCC-import qualified Client.State.EditBox as Edit-import           Client.State.Extensions-import           Client.State.Focus-import           Client.State.Network-import           Client.State.Window-import           Client.UserHost-import           Control.Applicative-import           Control.Concurrent.Async (cancel)-import           Control.Exception (displayException, try, SomeException)-import           Control.Lens-import           Control.Monad-import           Data.Foldable-import           Data.HashSet (HashSet)-import           Data.List (nub, (\\))-import           Data.List.NonEmpty (NonEmpty((:|)))-import           Data.List.Split-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map as Map-import           Data.Maybe (fromMaybe)-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as LText-import qualified Data.Text.Lazy.IO as LText-import           Data.Time-import           Irc.Commands-import           Irc.Identifier-import           Irc.RawIrcMsg-import           Irc.Message-import           Irc.UserInfo-import           Irc.Modes-import           LensUtils-import           RtsStats (getStats)-import           System.Directory (doesDirectoryExist)-import           System.FilePath ((</>))-import           System.Process---- | Possible results of running a command-data CommandResult-  -- | Continue running the client, consume input if command was from input-  = CommandSuccess ClientState-  -- | Continue running the client, report an error-  | CommandFailure ClientState-  -- | Client should close-  | CommandQuit ClientState----- | Type of commands that always work-type ClientCommand a = ClientState -> a {- ^ arguments -} -> IO CommandResult---- | Type of commands that require an active network to be focused-type NetworkCommand a = NetworkState {- ^ current network -} -> ClientCommand a---- | Type of commands that require an active channel to be focused-type ChannelCommand a = Identifier {- ^ focused channel -} -> NetworkCommand a----- | Pair of implementations for executing a command and tab completing one.--- The tab-completion logic is extended with a Bool--- indicating that tab completion should be reversed-data CommandImpl a-  -- | no requirements-  = ClientCommand  (ClientCommand  a) (Bool -> ClientCommand  String)-  -- | requires an active network-  | NetworkCommand (NetworkCommand a) (Bool -> NetworkCommand String)-  -- | requires an active chat window-  | ChatCommand    (ChannelCommand a) (Bool -> ChannelCommand String)-  -- | requires an active channel window-  | ChannelCommand (ChannelCommand a) (Bool -> ChannelCommand String)----- | A command is a list of aliases, an argument specification, implementation,--- and documentation. The arguments and implementation must match so that--- the parsed arguments will match what the implementation expects.-data Command = forall a. Command-  { -- | Names of this command, first in the list is the "primary" name-    cmdNames          :: NonEmpty Text-  -- | Specification of the arguments of the command-  , cmdArgumentSpec   :: Args ClientState a-  -- | Multi-line IRC-formatted documentation text used for @/help@-  , cmdDocumentation  :: Text-  -- | Implementation of the command for both execution and tab completion-  , cmdImplementation :: CommandImpl a-  }---- | A command section is a logical grouping of commands. This allows for--- showing more structure in the help menu system.-data CommandSection = CommandSection-  { cmdSectionName :: Text-  , cmdSectionCmds :: [Command]-  }---- | Consider the text entry successful and resume the client-commandSuccess :: Monad m => ClientState -> m CommandResult-commandSuccess = return . CommandSuccess---- | Consider the text entry a failure and resume the client-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 =-  return $! CommandFailure $! set clientErrorMsg (Just e) st---- | 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. Leading spaces before the @/@ are ignored when checking for--- commands.-execute ::-  String           {- ^ chat or command -} ->-  ClientState      {- ^ client state    -} ->-  IO CommandResult {- ^ command result  -}-execute str st =-  let st' = set clientErrorMsg Nothing st in-  case dropWhile (' '==) str of-    []          -> commandFailure st-    '/':command -> executeUserCommand Nothing command st'-    _           -> executeChat str st'---- | Execute command provided by user, resolve aliases if necessary.------ The last disconnection time is stored in text form and is available--- for substitutions in macros. It is only provided when running startup--- commands during a reconnect event.-executeUserCommand ::-  Maybe Text       {- ^ disconnection time -} ->-  String           {- ^ command            -} ->-  ClientState      {- ^ client state       -} ->-  IO CommandResult {- ^ command result     -}-executeUserCommand discoTime command st = do-  let key = Text.takeWhile (/=' ') (Text.pack command)-      rest = dropWhile (==' ') (dropWhile (/=' ') command)--  case views (clientConfig . configMacros) (recognize key) st of-    Exact (Macro (MacroSpec spec) cmdExs) ->-      case doExpansion spec cmdExs rest of-        Nothing   -> commandFailureMsg "macro expansions failed" st-        Just cmds -> process cmds st-    _ -> executeCommand Nothing command st-  where-    doExpansion spec cmdExs rest =-      do args <- parse st spec rest-         traverse (resolveMacro (map Text.pack args)) cmdExs--    resolveMacro args = resolveMacroExpansions (commandExpansion discoTime st) (expandInt args)--    expandInt :: [a] -> Integer -> Maybe a-    expandInt args i = preview (ix (fromInteger i)) args----    process [] st0 = commandSuccess st0-    process (c:cs) st0 =-      do res <- executeCommand Nothing (Text.unpack c) st0-         case res of-           CommandSuccess st1 -> process cs st1-           CommandFailure st1 -> process cs st1 -- ?-           CommandQuit st1    -> return (CommandQuit st1)---- | Compute the replacement value for the given expansion variable.-commandExpansion ::-  Maybe Text  {- ^ disconnect time    -} ->-  ClientState {- ^ client state       -} ->-  Text        {- ^ expansion variable -} ->-  Maybe Text  {- ^ expansion value    -}-commandExpansion discoTime st v =-  case v of-    "network" -> views clientFocus focusNetwork st-    "channel" -> previews (clientFocus . _ChannelFocus . _2) idText st-    "nick"    -> do net <- views clientFocus focusNetwork st-                    cs  <- preview (clientConnection net) st-                    return (views csNick idText cs)-    "disconnect" -> discoTime-    _         -> Nothing----- | Respond to the TAB key being pressed. This can dispatch to a command--- specific completion mode when relevant. Otherwise this will complete--- input based on the users of the channel related to the current buffer.-tabCompletion ::-  Bool             {- ^ reversed       -} ->-  ClientState      {- ^ client state   -} ->-  IO CommandResult {- ^ command result -}-tabCompletion isReversed st =-  case dropWhile (' ' ==) $ snd $ clientLine st of-    '/':command -> executeCommand (Just isReversed) command st-    _           -> nickTabCompletion isReversed st---- | Treat the current text input as a chat message and send it.-executeChat ::-  String           {- ^ chat message   -} ->-  ClientState      {- ^ client state   -} ->-  IO CommandResult {- ^ command result -}-executeChat msg st =-  case view clientFocus st of-    ChannelFocus network channel-      | Just !cs <- preview (clientConnection network) st ->-          do now <- getZonedTime-             let msgTxt = Text.pack $ takeWhile (/='\n') msg-                 tgtTxt = idText channel--             (st1,allow) <- clientChatExtension network tgtTxt msgTxt st--             when allow (sendMsg cs (ircPrivmsg tgtTxt msgTxt))--             let myNick = UserInfo (view csNick cs) "" ""-                 entry = ClientMessage-                   { _msgTime    = now-                   , _msgNetwork = network-                   , _msgBody    = IrcBody (Privmsg myNick channel msgTxt) }-             commandSuccess $! recordChannelMessage network channel entry st1--    _ -> commandFailureMsg "cannot send chat messages to this window" st----- | Parse and execute the given command. When the first argument is Nothing--- the command is executed, otherwise the first argument is the cursor--- position for tab-completion-executeCommand ::-  Maybe Bool       {- ^ tab-completion direction -} ->-  String           {- ^ command                  -} ->-  ClientState      {- ^ client state             -} ->-  IO CommandResult {- ^ command result           -}--executeCommand (Just isReversed) _ st-  | Just st' <- commandNameCompletion isReversed st = commandSuccess st'--executeCommand tabCompleteReversed str st =-  let (cmd, rest) = break (==' ') str-      cmdTxt      = Text.toLower (Text.pack cmd)--      finish spec exec tab =-        case tabCompleteReversed of-          Just isReversed -> tab isReversed st rest-          Nothing ->-            case parse st spec rest of-              Nothing -> commandFailureMsg "bad command arguments" st-              Just arg -> exec st arg-  in-  case recognize cmdTxt commands of--    Exact Command{cmdImplementation=impl, cmdArgumentSpec=argSpec} ->-      case impl of-        ClientCommand exec tab ->-          finish argSpec exec tab--        NetworkCommand exec tab-          | Just network <- views clientFocus focusNetwork st-          , Just cs      <- preview (clientConnection network) st ->-              finish argSpec (exec cs) (\x -> tab x cs)-          | otherwise -> commandFailureMsg "command requires focused network" st--        ChannelCommand exec tab-          | ChannelFocus network channelId <- view clientFocus st-          , Just cs <- preview (clientConnection network) st-          , isChannelIdentifier cs channelId ->-              finish argSpec (exec channelId cs) (\x -> tab x channelId cs)-          | otherwise -> commandFailureMsg "command requires focused channel" st--        ChatCommand exec tab-          | ChannelFocus network channelId <- view clientFocus st-          , Just cs <- preview (clientConnection network) st ->-              finish argSpec (exec channelId cs) (\x -> tab x channelId cs)-          | otherwise -> commandFailureMsg "command requires focused chat window" st--    _ -> case tabCompleteReversed of-           Just isReversed -> nickTabCompletion isReversed st-           Nothing         -> commandFailureMsg "unknown command" st----- | Expands each alias to have its own copy of the command callbacks-expandAliases :: [Command] -> [(Text,Command)]-expandAliases xs =-  [ (name, cmd) | cmd <- xs, name <- toList (cmdNames cmd) ]----- | Map of built-in client commands to their implementations, tab completion--- logic, and argument structures.-commands :: Recognizer Command-commands = fromCommands (expandAliases (concatMap cmdSectionCmds commandsList))----- | Raw list of commands in the order used for @/help@-commandsList :: [CommandSection]-commandsList =--  -------------------------------------------------------------------------  [ CommandSection "Client commands"-  --------------------------------------------------------------------------  [ Command-      (pure "exit")-      (pure ())-      "Exit the client immediately.\n"-    $ ClientCommand cmdExit noClientTab--  , Command-      (pure "reload")-      (optionalArg (simpleToken "[filename]"))-      "Reload the client configuration file.\n\-      \\n\-      \If \^Bfilename\^B is provided it will be used to reload.\n\-      \Otherwise the previously loaded configuration file will be reloaded.\n"-    $ ClientCommand cmdReload tabReload--  , Command-      (pure "extension")-      (liftA2 (,) (simpleToken "extension") (remainingArg "arguments"))-      "Calls the process_command callback of the given extension.\n\-      \\n\-      \\^Bextension\^B should be the name of the loaded extension.\n"-    $ ClientCommand cmdExtension simpleClientTab--  , Command-      (pure "palette")-      (pure ())-      "Show the current palette settings and a color chart to help pick new colors.\n"-    $ ClientCommand cmdPalette noClientTab--  , Command-      (pure "digraphs")-      (pure ())-      "\^BDescription:\^B\n\-      \\n\-      \    Show the table of digraphs. A digraph is a pair of characters\n\-      \    can be used together to represent an uncommon character. Type\n\-      \    the two-character digraph corresponding to the desired output\n\-      \    character and then press M-k (default binding).\n\-      \\n\-      \    Note that the digraphs list is searchable with /grep.\n\-      \\n\-      \\^BSee also:\^B grep\n"-    $ ClientCommand cmdDigraphs noClientTab--  , Command-      (pure "keymap")-      (pure ())-      "Show the key binding map.\n\-      \\n\-      \Key bindings can be changed in configuration file. See `glirc2 --config-format`.\n"-    $ ClientCommand cmdKeyMap noClientTab--  , Command-      (pure "rtsstats")-      (pure ())-      "Show the GHC RTS statistics.\n"-    $ ClientCommand cmdRtsStats noClientTab--  , Command-      (pure "exec")-      (remainingArg "arguments")-      "Execute a command synchnonously sending the to a configuration destination.\n\-      \\n\-      \\^Barguments\^B: [-n[network]] [-c[channel]] [-i input] command [arguments...]\n\-      \\n\-      \When \^Binput\^B is specified it is sent to the stdin.\n\-      \\n\-      \When neither \^Bnetwork\^B nor \^Bchannel\^B are specified output goes to client window (*).\n\-      \When \^Bnetwork\^B is specified output is sent as raw IRC traffic to the network.\n\-      \When \^Bchannel\^B is specified output is sent as chat to the given channel on the current network.\n\-      \When \^Bnetwork\^B and \^Bchannel\^B are specified output is sent as chat to the given channel on the given network.\n\-      \\n\-      \\^Barguments\^B is divided on spaces into words before being processed\-      \ by getopt. Use Haskell string literal syntax to create arguments with\-      \ escaped characters and spaces inside.\n\-      \\n"-    $ ClientCommand cmdExec simpleClientTab--  , Command-      (pure "url")-      optionalNumberArg-      "Open a URL seen in chat.\n\-      \\n\-      \The URL is opened using the executable configured under \^Burl-opener\^B.\n\-      \\n\-      \When this command is active in the textbox, chat messages are filtered to\-      \ only show ones with URLs.\n\-      \\n\-      \When \^Bnumber\^B is omitted it defaults to \^B1\^B. The number selects the\-      \ URL to open counting back from the most recent.\n"-    $ ClientCommand cmdUrl noClientTab--  , Command-      (pure "cert")-      (pure ())-      "Show the TLS certificate for the current connection.\n"-    $ NetworkCommand cmdCert noNetworkTab--  , Command-      (pure "dump")-      (simpleToken "filename")-      "Dump current buffer to file.\n"-    $ ClientCommand cmdDump simpleClientTab--  , Command-      (pure "help")-      (optionalArg (simpleToken "[command]"))-      "Show command documentation.\n\-      \\n\-      \When \^Bcommand\^B is omitted a list of all commands is displayed.\n\-      \When \^Bcommand\^B is specified detailed help for that command is shown.\n"-    $ ClientCommand cmdHelp tabHelp--  -------------------------------------------------------------------------  ] , CommandSection "View toggles"-  --------------------------------------------------------------------------  [ Command-      (pure "toggle-detail")-      (pure ())-      "Toggle detailed message view.\n"-    $ ClientCommand cmdToggleDetail noClientTab--  , Command-      (pure "toggle-activity-bar")-      (pure ())-      "Toggle detailed detailed activity information in status bar.\n"-    $ ClientCommand cmdToggleActivityBar noClientTab--  , Command-      (pure "toggle-show-ping")-      (pure ())-      "Toggle visibility of ping round-trip time.\n"-    $ ClientCommand cmdToggleShowPing noClientTab--  , Command-      (pure "toggle-metadata")-      (pure ())-      "Toggle visibility of metadata in chat windows.\n"-    $ ClientCommand cmdToggleMetadata noClientTab--  , Command-      (pure "toggle-layout")-      (pure ())-      "Toggle multi-window layout mode.\n"-    $ ClientCommand cmdToggleLayout noClientTab--  -------------------------------------------------------------------------  ] , CommandSection "Connection commands"-  --------------------------------------------------------------------------  [ Command-      (pure "connect")-      (simpleToken "network")-      "Connect to \^Bnetwork\^B by name.\n\-      \\n\-      \If no name is configured the hostname is the 'name'.\n"-    $ ClientCommand cmdConnect tabConnect--  , Command-      (pure "reconnect")-      (pure ())-      "Reconnect to the current network.\n"-    $ ClientCommand cmdReconnect noClientTab--  , Command-      (pure "disconnect")-      (pure ())-      "Immediately terminate the current network connection.\n\-      \\n\-      \See also: /quit /exit\n"-    $ NetworkCommand cmdDisconnect noNetworkTab--  , Command-      (pure "quit")-      (remainingArg "reason")-      "Gracefully disconnect the current network connection.\n\-      \\n\-      \\^Breason\^B: optional quit reason\n\-      \\n\-      \See also: /disconnect /exit\n"-    $ NetworkCommand cmdQuit   simpleNetworkTab--  -------------------------------------------------------------------------  ] , CommandSection "Window management"-  --------------------------------------------------------------------------  [ Command-      (pure "focus")-      (liftA2 (,) (simpleToken "network") (optionalArg (simpleToken "[target]")))-      "Change the focused window.\n\-      \\n\-      \When only \^Bnetwork\^B is specified this switches to the network status window.\n\-      \When \^Bnetwork\^B and \^Btarget\^B are specified this switches to that chat window.\n\-      \\n\-      \Nickname and channels can be specified in the \^Btarget\^B parameter.\n\-      \See also: /query (aliased /c /channel) to switch to a target on the current network.\n"-    $ ClientCommand cmdFocus tabFocus--  , Command-      ("query" :| ["q"])-      (liftA2 (,) (simpleToken "target") (remainingArg "message"))-      "\^BParameters:\^B\n\-      \\n\-      \    target: Focus name\n\-      \    message: Optional message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    This command switches the client focus to the given\n\-      \    target and optionally sends a message to that target.\n\-      \\n\-      \    Channel: \^_#channel\^_\n\-      \    Channel: \^_network\^_:\^_#channel\^_\n\-      \    User:    \^_nick\^_\n\-      \    User:    \^_network\^_:\^_nick\^_\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /q fn:#haskell\n\-      \    /q #haskell\n\-      \    /q lambdabot @messages\n\-      \    /q irc_friend How are you?\n\-      \\n\-      \\^BSee also:\^B msg channel focus\n"-    $ ClientCommand cmdQuery simpleClientTab--  , Command-      ("c" :| ["channel"])-      (simpleToken "focus")-      "\^BParameters:\^B\n\-      \\n\-      \    focuses: Focus name\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    This command sets the current window focus. When\n\-      \    no network is specified, the current network will\n\-      \    be used.\n\-      \\n\-      \    Client:  *\n\-      \    Network: \^_network\^_:\n\-      \    Channel: \^_#channel\^_\n\-      \    Channel: \^_network\^_:\^_#channel\^_\n\-      \    User:    \^_nick\^_\n\-      \    User:    \^_network\^_:\^_nick\^_\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /c fn:#haskell\n\-      \    /c #haskell\n\-      \    /c fn:\n\-      \    /c *:\n\-      \\n\-      \\^BSee also:\^B focus\n"-    $ ClientCommand cmdChannel tabChannel--  , Command-      (pure "clear")-      (optionalArg (liftA2 (,) (simpleToken "[network]") (optionalArg (simpleToken "[channel]"))))-      "Clear a window.\n\-      \\n\-      \If no arguments are provided the current window is cleared.\n\-      \If \^Bnetwork\^B is provided the that network window is cleared.\n\-      \If \^Bnetwork\^B and \^Bchannel\^B are provided that chat window is cleared.\n\-      \If \^Bnetwork\^B is provided and \^Bchannel\^B is \^B*\^O all windows for that network are cleared.\n\-      \\n\-      \If a window is cleared and no longer active that window will be removed from the client.\n"-    $ ClientCommand cmdClear tabFocus--  , Command-      (pure "windows")-      (optionalArg (simpleToken "[kind]"))-      "Show a list of all windows with an optional argument to limit the kinds of windows listed.\n\-      \\n\-      \\^Bkind\^O: one of \^Bnetworks\^O, \^Bchannels\^O, \^Busers\^O\n\-      \\n"-    $ ClientCommand cmdWindows tabWindows--  , Command-      (pure "splits")-      (remainingArg "focuses")-      "\^BParameters:\^B\n\-      \\n\-      \    focuses: List of focus names\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    This command sents the set of focuses that will always\n\-      \    be visible, even when unfocused. When the client is focused\n\-      \    to an active network, the network can be omitted when\n\-      \    specifying a focus. If no focuses are listed, they will\n\-      \    all be cleared.\n\-      \\n\-      \    Client:  *\n\-      \    Network: \^_network\^_:\n\-      \    Channel: \^_#channel\^_\n\-      \    Channel: \^_network\^_:\^_#channel\^_\n\-      \    User:    \^_nick\^_\n\-      \    User:    \^_network\^_:\^_nick\^_\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /splits * fn:#haskell fn:chanserv\n\-      \    /splits #haskell #haskell-lens nickserv\n\-      \    /splits\n\-      \\n\-      \\^BSee also:\^B splits+, splits-\n"-    $ ClientCommand cmdSplits tabSplits--  , Command-      (pure "splits+")-      (remainingArg "focuses")-      "Add windows to the splits list. Omit the list of focuses to add the\-      \ current window.\n\-      \\n\-      \\^Bfocuses\^B: space delimited list of focus names.\n\-      \\n\-      \Client:  *\n\-      \Network: \^BNETWORK\^B\n\-      \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\-      \User:    \^BNETWORK\^B:\^BNICK\^B\n\-      \\n\-      \If the network part is omitted, the current network will be used.\n"-    $ ClientCommand cmdSplitsAdd tabSplits--  , Command-      (pure "splits-")-      (remainingArg "focuses")-      "Remove windows from the splits list. Omit the list of focuses to\-      \ remove the current window.\n\-      \\n\-      \\^Bfocuses\^B: space delimited list of focus names.\n\-      \\n\-      \Client:  *\n\-      \Network: \^BNETWORK\^B\n\-      \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\-      \User:    \^BNETWORK\^B:\^BNICK\^B\n\-      \\n\-      \If the network part is omitted, the current network will be used.\n"-    $ ClientCommand cmdSplitsDel tabActiveSplits--  , Command-      (pure "ignore")-      (remainingArg "masks")-      "\^BParameters:\^B\n\-      \\n\-      \    masks: List of masks\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Toggle the soft-ignore on each of the space-delimited given\n\-      \    nicknames. Ignores can use \^B*\^B (many) and \^B?\^B (one) wildcards.\n\-      \    Masks can be of the form: nick[[!user]@host]\n\-      \    Masks use a case-insensitive comparison.\n\-      \\n\-      \    If no masks are specified the current ignore list is displayed.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /ignore\n\-      \    /ignore nick1 nick2 nick3\n\-      \    /ignore nick@host\n\-      \    /ignore nick!user@host\n\-      \    /ignore *@host\n\-      \    /ignore *!baduser@*\n"-    $ ClientCommand cmdIgnore tabIgnore--  , Command-      (pure "grep")-      (remainingArg "regular-expression")-      "Set the persistent regular expression.\n\-      \\n\-      \\^BFlags:\^B\n\-      \    -An  Show n messages after match\n\-      \    -Bn  Show n messages before match\n\-      \    -Cn  Show n messages before and after match\n\-      \    -i   Case insensitive match\n\-      \    --   Stop processing flags\n\-      \\n\-      \Clear the regular expression by calling this without an argument.\n\-      \\n\-      \\^B/grep\^O is case-sensitive.\n"-    $ ClientCommand cmdGrep simpleClientTab--  , Command-      (pure "mentions")-      (pure ())-      "Show a list of all message that were highlighted as important.\n\-      \\n\-      \When using \^B/grep\^B the important messages are those matching\n\-      \the regular expression instead.\n"-    $ ClientCommand cmdMentions noClientTab--  -------------------------------------------------------------------------  ] , CommandSection "IRC commands"-  --------------------------------------------------------------------------  [ Command-      ("join" :| ["j"])-      (liftA2 (,) (simpleToken "channels") (optionalArg (simpleToken "[keys]")))-      "\^BParameters:\^B\n\-      \\n\-      \    channels: Comma-separated list of channels\n\-      \    keys:     Comma-separated list of keys\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Join the given channels. When keys are provided, they should\n\-      \    occur in the same order as the channels.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /join #friends\n\-      \    /join #secret thekey\n\-      \    /join #secret1,#secret2 key1,key2\n\-      \\n\-      \\^BSee also:\^B channel, clear, part\n"-    $ NetworkCommand cmdJoin simpleNetworkTab--  , Command-      (pure "part")-      (remainingArg "reason")-      "\^BParameters:\^B\n\-      \\n\-      \    reason: Optional message sent to channel as part reason\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Part from the current channel.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /part\n\-      \    /part It's not me, it's you\n\-      \\n\-      \\^BSee also:\^B clear, join, quit\n"-    $ ChannelCommand cmdPart simpleChannelTab--  , Command-      (pure "msg")-      (liftA2 (,) (simpleToken "target") (remainingArg "message"))-      "\^BParameters:\^B\n\-      \\n\-      \    target:  Comma-separated list of nicknames and channels\n\-      \    message: Formatted message body\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a chat message to a user or a channel. On servers\n\-      \    with STATUSMSG support, the channel name can be prefixed\n\-      \    with a sigil to restrict the recipients to those with the\n\-      \    given mode.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /msg buddy I'm sending you a message.\n\-      \    /msg #friends This message is for the whole channel.\n\-      \    /msg him,her I'm chatting with two people.\n\-      \    /msg @#users This message is only for ops!\n\-      \\n\-      \\^BSee also:\^B notice, me, say\n"-    $ NetworkCommand cmdMsg simpleNetworkTab--  , Command-      (pure "me")-      (remainingArg "message")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Body of action message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Sends an action message to the currently focused channel.\n\-      \    Most clients will render these messages prefixed with\n\-      \    only your nickname as though describing an action.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /me shrugs\n\-      \\n\-      \\^BSee also:\^B notice, msg, say\n"-    $ ChatCommand cmdMe simpleChannelTab--  , Command-      (pure "say")-      (remainingArg "message")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Body of message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a message to the current chat window.  This can be useful\n\-      \    for sending a chat message with a leading '/' to the current\n\-      \    chat window.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /say /help is the right place to start!\n\-      \\n\-      \\^BSee also:\^B notice, me, msg\n"-    $ ChatCommand cmdSay simpleChannelTab--  , Command-      (pure "notice")-      (liftA2 (,) (simpleToken "target") (remainingArg "message"))-      "\^BParameters:\^B\n\-      \\n\-      \    target:  Comma-separated list of nicknames and channels\n\-      \    message: Formatted message body\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a chat notice to a user or a channel. On servers\n\-      \    with STATUSMSG support, the channel name can be prefixed\n\-      \    with a sigil to restrict the recipients to those with the\n\-      \    given mode. Notice messages were originally intended to be\n\-      \    used by bots. Different clients will render these in different\n\-      \    ways.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /notice buddy I'm sending you a message.\n\-      \    /notice #friends This message is for the whole channel.\n\-      \    /notice him,her I'm chatting with two people.\n\-      \    /notice @#users This message is only for ops!\n\-      \\n\-      \\^BSee also:\^B me, msg, say\n"-    $ NetworkCommand cmdNotice simpleNetworkTab--  , Command-      (pure "ctcp")-      (liftA3 (,,) (simpleToken "target") (simpleToken "command") (remainingArg "arguments"))-      "\^BParameters:\^B\n\-      \\n\-      \    target:    Comma-separated list of nicknames and channels\n\-      \    command:   CTCP command name\n\-      \    arguments: CTCP command arguments\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Client-to-client protocol (CTCP) commands can be used\n\-      \    to query information from another user's client application\n\-      \    directly. Common CTCP commands include: ACTION, PING, VERSION,\n\-      \    USERINFO, CLIENTINFO, and TIME. glirc does not automatically\n\-      \    respond to CTCP commands.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /ctcp myfriend VERSION\n\-      \    /ctcp myfriend CLIENTINFO\n"-    $ NetworkCommand cmdCtcp simpleNetworkTab--  , Command-      (pure "nick")-      (simpleToken "nick")-      "\^BParameters:\^B\n\-      \\n\-      \    nick: New nickname\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Change your nickname on the currently focused server.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /nick guest123\n\-      \    /nick better_nick\n"-    $ NetworkCommand cmdNick simpleNetworkTab--  , Command-      (pure "away")-      (remainingArg "message")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Optional away message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Change your nickname on the currently focused server.\n\-      \    Omit the message parameter to clear your away status.\n\-      \    The away message is only used by the server to update\n\-      \    status in /whois and to provide automated responses.\n\-      \    It is not used by this client directly.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /away\n\-      \    /away Out getting some sun\n"-    $ NetworkCommand cmdAway simpleNetworkTab--  , Command-      ("users" :| ["names"])-      (pure ())-      "\^BDescription:\^B\n\-      \\n\-      \    Show the user list for the current channel.\n\-      \    Detailed view (default key F2) shows full hostmask.\n\-      \    Hostmasks can be populated with /who #channel.\n\-      \    Press ESC to exit the userlist.\n\-      \\n\-      \\^BSee also:\^B channelinfo, masks\n"-    $ ChannelCommand cmdUsers  noChannelTab--  , Command-      (pure "channelinfo")-      (pure ())-      "\^BDescription:\^B\n\-      \\n\-      \    Show information about the current channel.\n\-      \    Press ESC to exit the channel info window.\n\-      \\n\-      \    Information includes topic, creation time, URL, and modes.\n\-      \\n\-      \\^BSee also:\^B masks, mode, topic, users\n"-    $ ChannelCommand cmdChannelInfo noChannelTab--  , Command-      (pure "quote")-      (remainingArg "raw IRC command")-      "Send a raw IRC command.\n"-    $ NetworkCommand cmdQuote simpleNetworkTab--  , Command-      (pure "dcc")-      (liftA2 (,) (optionalArg (simpleToken "[accept|cancel|clear|resume]"))-                               optionalNumberArg)-      "Main access to the DCC subsystem with the following subcommands:\n\n\-       \  /dcc           : Access to a list of pending offer and downloads\n\-       \  /dcc accept #n : start downloading the #n pending offer\n\-       \  /dcc resume #n : same as accept but appending to the file on `download-dir`\n\-       \  /dcc clear  #n : remove the #n offer from the list \n\-       \  /dcc cancel #n : cancel the download #n \n\n"-    $ ClientCommand cmdDcc noClientTab-  -------------------------------------------------------------------------  ] , CommandSection "IRC queries"-  --------------------------------------------------------------------------  [ Command-      (pure "who")-      (remainingArg "arguments")-      "Send WHO query to server with given arguments.\n"-    $ NetworkCommand cmdWho simpleNetworkTab--  , Command-      (pure "whois")-      (remainingArg "arguments")-      "Send WHOIS query to server with given arguments.\n"-    $ NetworkCommand cmdWhois simpleNetworkTab--  , Command-      (pure "whowas")-      (remainingArg "arguments")-      "Send WHOWAS query to server with given arguments.\n"-    $ NetworkCommand cmdWhowas simpleNetworkTab--  , Command-      (pure "ison")-      (remainingArg "arguments")-      "Send ISON query to server with given arguments.\n"-    $ NetworkCommand cmdIson   simpleNetworkTab--  , Command-      (pure "userhost")-      (remainingArg "arguments")-      "Send USERHOST query to server with given arguments.\n"-    $ NetworkCommand cmdUserhost simpleNetworkTab--  , Command-      (pure "time")-      (optionalArg (simpleToken "[servername]"))-      "Send TIME query to server with given arguments.\n"-    $ NetworkCommand cmdTime simpleNetworkTab--  , Command-      (pure "stats")-      (remainingArg "arguments")-      "Send STATS query to server with given arguments.\n"-    $ NetworkCommand cmdStats simpleNetworkTab--  , Command-      (pure "lusers")-      (optionalArg (liftA2 (,) (simpleToken "mask") (optionalArg (simpleToken "[servername]"))))-      "Send LUSERS query to server with given arguments.\n"-    $ NetworkCommand cmdLusers simpleNetworkTab--  , Command-      (pure "motd") (optionalArg (simpleToken "[servername]"))-      "Send MOTD query to server.\n"-    $ NetworkCommand cmdMotd simpleNetworkTab--  , Command-      (pure "admin") (optionalArg (simpleToken "[servername]"))-      "Send ADMIN query to server.\n"-    $ NetworkCommand cmdAdmin simpleNetworkTab--  , Command-      (pure "rules") (optionalArg (simpleToken "[servername]"))-      "Send RULES query to server.\n"-    $ NetworkCommand cmdRules simpleNetworkTab--  , Command-      (pure "info") (pure ())-      "Send INFO query to server.\n"-    $ NetworkCommand cmdInfo noNetworkTab--  , Command-      (pure "list") (remainingArg "arguments")-      "Send LIST query to server.\n"-    $ NetworkCommand cmdList simpleNetworkTab--  , Command-      (pure "version") (optionalArg (simpleToken "[servername]"))-      "Send VERSION query to server.\n"-    $ NetworkCommand cmdVersion simpleNetworkTab--  -------------------------------------------------------------------------  ] , CommandSection "IRC channel management"-  --------------------------------------------------------------------------  [ Command-      (pure "mode")-      (fromMaybe [] <$> optionalArg (extensionArg "[modes]" modeParamArgs))-      "Sets IRC modes.\n\-      \\n\-      \Examples:\n\-      \Setting a ban:           /mode +b *!*@hostname\n\-      \Removing a quiet:        /mode -q *!*@hostname\n\-      \Voicing two users:       /mode +vv user1 user2\n\-      \Demoting an op to voice: /mode +v-o user1 user1\n\-      \\n\-      \When executed in a network window, mode changes are applied to your user.\n\-      \When executed in a channel window, mode changes are applied to the channel.\n\-      \\n\-      \This command has parameter sensitive tab-completion.\n\-      \\n\-      \See also: /masks /channelinfo\n"-    $ NetworkCommand cmdMode tabMode--  , Command-      (pure "masks")-      (simpleToken "mode")-      "Show mask lists for current channel.\n\-      \\n\-      \Common \^Bmode\^B values:\n\-      \\^Bb\^B: bans\n\-      \\^Bq\^B: quiets\n\-      \\^BI\^B: invite exemptions (op view only)\n\-      \\^Be\^B: ban exemption (op view only)s\n\-      \\n\-      \To populate the mask lists for the first time use: /mode \^Bmode\^B\n\-      \\n\-      \See also: /mode\n"-    $ ChannelCommand cmdMasks noChannelTab--  , Command-      (pure "invite")-      (simpleToken "nick")-      "Invite a user to the current channel.\n"-    $ ChannelCommand cmdInvite simpleChannelTab--  , Command-      (pure "topic")-      (remainingArg "message")-      "Set the topic on the current channel.\n\-      \\n\-      \Tab-completion with no \^Bmessage\^B specified will load the current topic for editing.\n"-    $ ChannelCommand cmdTopic tabTopic--  , Command-      (pure "kick")-      (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))-      "Kick a user from the current channel.\n\-      \\n\-      \See also: /kickban /remove\n"-    $ ChannelCommand cmdKick simpleChannelTab--  , Command-      (pure "kickban")-      (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))-      "Ban and kick a user from the current channel.\n\-      \\n\-      \Users are banned by hostname match.\n\-      \See also: /kick /remove\n"-    $ ChannelCommand cmdKickBan simpleChannelTab--  , Command-      (pure "remove")-      (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))-      "Remove a user from the current channel.\n\-      \\n\-      \Remove works like /kick except it results in a PART.\n\-      \See also: /kick /kickban\n"-    $ ChannelCommand cmdRemove simpleChannelTab--  , Command-      (pure "knock")-      (liftA2 (,) (simpleToken "channel") (remainingArg "message"))-      "Request entry to an invite-only channel.\n"-    $ NetworkCommand cmdKnock simpleNetworkTab--  -------------------------------------------------------------------------  ] , CommandSection "ZNC Support"-  --------------------------------------------------------------------------  [ Command-      (pure "znc")-      (remainingArg "arguments")-      "Send command directly to ZNC.\n\-      \\n\-      \The advantage of this over /msg is that responses are not broadcast to call clients.\n"-    $ NetworkCommand cmdZnc simpleNetworkTab--  , Command-      (pure "znc-playback")-      (optionalArg (liftA2 (,) (simpleToken "[time]") (optionalArg (simpleToken "[date]"))))-      "Request playback from the ZNC 'playback' module.\n\-      \\n\-      \\^Btime\^B determines the time to playback since.\n\-      \\^Bdate\^B determines the date to playback since.\n\-      \\n\-      \When both \^Btime\^B and \^Bdate\^B are omitted, all playback is requested.\n\-      \When both \^Bdate\^B is omitted it is defaulted the most recent date in the past that makes sense.\n\-      \\n\-      \Time format: HOURS:MINUTES (example: 7:00)\n\-      \Date format: YEAR-MONTH-DAY (example: 2016-06-16)\n\-      \\n\-      \Note that the playback module is not installed in ZNC by default!\n"-    $ NetworkCommand cmdZncPlayback noNetworkTab--  ] , CommandSection "Network operator commands"--  [ Command-      (pure "oper")-      (liftA2 (,) (simpleToken "user") (simpleToken "password"))-      "Authenticate as a server operator.\n"-    $ NetworkCommand cmdOper noNetworkTab--  , Command-      (pure "kill")-      (liftA2 (,) (simpleToken "client") (remainingArg "reason"))-      "Kill a client connection to the server.\n"-    $ NetworkCommand cmdKill simpleNetworkTab--  , Command-      (pure "kline")-      (liftA3 (,,) (simpleToken "minutes") (simpleToken "user@host") (remainingArg "reason"))-      "Ban a client from the server.\n"-    $ NetworkCommand cmdKline simpleNetworkTab--  , Command-      (pure "unkline")-      (simpleToken "user@host")-      "Unban a client from the server.\n"-    $ NetworkCommand cmdUnkline simpleNetworkTab--  , Command-      (pure "testline")-      (simpleToken "[[nick!]user@]host")-      "Check matching I/K/D lines for a [[nick!]user@]host\n"-    $ NetworkCommand cmdTestline simpleNetworkTab--  , Command-      (pure "testmask")-      (liftA2 (,) (simpleToken "[nick!]user@host") (remainingArg "[gecos]"))-      "Test how many local and global clients match a mask.\n"-    $ NetworkCommand cmdTestmask simpleNetworkTab--  , Command-      (pure "masktrace")-      (liftA2 (,) (simpleToken "[nick!]user@host") (remainingArg "[gecos]"))-      "Outputs a list of local users matching the given masks.\n"-    $ NetworkCommand cmdMasktrace simpleNetworkTab--  , Command-      (pure "trace")-      (optionalArg (liftA2 (,) (simpleToken "[server | nick]") (optionalArg (simpleToken "[server]"))))-      "Outputs a list users on a server.\n"-    $ NetworkCommand cmdTrace simpleNetworkTab--  , Command-      (pure "map")-      (pure ())-      "Display network map.\n"-    $ NetworkCommand cmdMap simpleNetworkTab--  , Command-      (pure "links")-      (remainingArg "arguments")-      "Send LINKS query to server with given arguments.\n"-    $ NetworkCommand cmdLinks simpleNetworkTab--  ]]---- | Provides no tab completion for client commands-noClientTab :: Bool -> ClientCommand String-noClientTab _ st _ = commandFailure st---- | Provides no tab completion for network commands-noNetworkTab :: Bool -> NetworkCommand String-noNetworkTab _ _ st _ = commandFailure st---- | Provides no tab completion for channel commands-noChannelTab :: Bool -> ChannelCommand String-noChannelTab _ _ _ st _ = commandFailure st---- | Provides nickname based tab completion for client commands-simpleClientTab :: Bool -> ClientCommand String-simpleClientTab isReversed st _ =-  nickTabCompletion isReversed st---- | Provides nickname based tab completion for network commands-simpleNetworkTab :: Bool -> NetworkCommand String-simpleNetworkTab isReversed _ st _ =-  nickTabCompletion isReversed st---- | Provides nickname based tab completion for channel commands-simpleChannelTab :: Bool -> ChannelCommand String-simpleChannelTab isReversed _ _ st _ =-  nickTabCompletion isReversed st---- | Implementation of @/exit@ command.-cmdExit :: ClientCommand ()-cmdExit st _ = return (CommandQuit st)---cmdToggleDetail :: ClientCommand ()-cmdToggleDetail st _ = commandSuccess (over clientDetailView not st)--cmdToggleActivityBar :: ClientCommand ()-cmdToggleActivityBar st _ = commandSuccess (over clientActivityBar not st)--cmdToggleShowPing :: ClientCommand ()-cmdToggleShowPing st _ = commandSuccess (over clientShowPing not st)--cmdToggleMetadata :: ClientCommand ()-cmdToggleMetadata st _ = commandSuccess (clientToggleHideMeta st)--cmdToggleLayout :: ClientCommand ()-cmdToggleLayout st _ = commandSuccess (set clientScroll 0 (over clientLayout aux st))-  where-    aux OneColumn = TwoColumn-    aux TwoColumn = OneColumn---- | When used on a channel that the user is currently--- joined to this command will clear the messages but--- preserve the window. When used on a window that the--- user is not joined to this command will delete the window.-cmdClear :: ClientCommand (Maybe (String, Maybe String))-cmdClear st args =-  case args of-    Nothing                 -> clearFocus (view clientFocus st)-    Just ("*", Nothing)     -> clearFocus Unfocused-    Just (network, Nothing) -> clearFocus (NetworkFocus (Text.pack network))-    Just (network, Just "*") -> clearNetworkWindows network-    Just (network, Just channel) ->-        clearFocus (ChannelFocus (Text.pack network) (mkId (Text.pack channel)))-  where-    clearNetworkWindows network-      = commandSuccess-      $ foldl' (flip clearFocus1) st-      $ filter (\x -> focusNetwork x == Just (Text.pack network))-      $ views clientWindows Map.keys st--    clearFocus focus = commandSuccess (clearFocus1 focus st)--    clearFocus1 focus st' = focusEffect (windowEffect st')-      where-        windowEffect-          | isActive  = setWindow (Just emptyWindow)-          | otherwise = setWindow Nothing--        focusEffect-          | not isActive && view clientFocus st' == focus =-                 if has (clientWindows . ix prev) st'-                 then changeFocus prev-                 else advanceFocus-          | otherwise = id-          where-            prev = view clientPrevFocus st--        setWindow = set (clientWindows . at focus)--        isActive =-          case focus of-            Unfocused                    -> False-            NetworkFocus network         -> has (clientConnection network) st'-            ChannelFocus network channel -> has (clientConnection network-                                                .csChannels . ix channel) st'---- | Implementation of @/quote@. Parses arguments as a raw IRC command and--- sends to the current network.-cmdQuote :: NetworkCommand String-cmdQuote cs st rest =-  case parseRawIrcMsg (Text.pack (dropWhile (' '==) rest)) of-    Nothing  -> commandFailureMsg "failed to parse raw IRC command" st-    Just raw ->-      do sendMsg cs raw-         commandSuccess st---- | Implementation of @/dcc [(cancel|accept|resume)] [key]-cmdDcc :: ClientCommand (Maybe String, Maybe Int)-cmdDcc st (Nothing, Nothing) = commandSuccess (changeSubfocus FocusDCC st)-cmdDcc st (Just cmd, Just key) = checkAndBranch st cmd key-cmdDcc st _ = commandFailureMsg "Invalid syntax" st--checkAndBranch :: ClientState -> String -> Int -> IO CommandResult-checkAndBranch st cmd key-  | isCancel, NotExist <- curKeyStatus-      = commandFailureMsg "No such DCC entry" st-  | isCancel, curKeyStatus == Pending-      = commandSuccess-      $ set (clientDCC . dsOffers . ix key . dccStatus) UserKilled st-  | isCancel, curKeyStatus /= Downloading-      = commandFailureMsg "Transfer already stopped" st-  | isCancel = cancel threadId *> commandSuccess st--  | isClear, NotExist <- curKeyStatus-      = commandFailureMsg "No such DCC entry" st-  | isClear, curKeyStatus `elem` [Downloading, Pending]-      = commandFailureMsg "Cancel the download first" st-  | isClear = commandSuccess-            $ set (clientDCC . dsOffers    . at key) Nothing-            $ set (clientDCC . dsTransfers . at key) Nothing st--  | isAcceptOrResume, curKeyStatus `elem` alreadyAcceptedSet-      = commandFailureMsg "Offer already accepted" st-  | isAcceptOrResume, NotExist <- curKeyStatus-      = commandFailureMsg "No such DCC entry" st-  | isAcceptOrResume-      = do isDirectory <- doesDirectoryExist downloadPath-           msize       <- getFileOffset downloadPath-           case (isDirectory, msize, cmd, mcs) of-             (True, _, _, _)      -> commandFailureMsg "DCC transfer would overwrite a directory" st-             (_, Nothing, _, _)   -> acceptOffer -- resume from 0 is accept-             (_, _, "accept", _)  -> acceptOffer -- overwrite file-             (_, Just size, "resume", Just cs) -> resumeOffer size cs-             _ -> commandFailureMsg "Unknown case" st--  | otherwise = commandFailureMsg "Invalid syntax" st-  where-    -- General-    isAcceptOrResume = cmd `elem` ["accept", "resume"]-    isCancel         = cmd == "cancel"-    isClear          = cmd == "clear"-    dccState         = view clientDCC st-    curKeyStatus     = statusAtKey key dccState-    alreadyAcceptedSet = [ CorrectlyFinished, UserKilled, LostConnection-                         , Downloading]--    -- For cancel, other cases handled on the guards-    threadId = st ^?! clientDCC . dsTransfers . ix key . dtThread . _Just--    -- Common values for resume or accept-    Just offer   = view (clientDCC . dsOffers . at key) st -- guarded exist-    updChan      = view clientDCCUpdates st-    downloadDir  = view (clientConfig . configDownloadDir) st-    downloadPath = downloadDir </> _dccFileName offer-    mcs          = preview (clientConnection (_dccNetwork offer)) st--    -- Actual workhorses for the commands-    acceptOffer =-        do newDCCState <- supervisedDownload downloadDir key updChan dccState-           commandSuccess (set clientDCC newDCCState st)--    resumeOffer size cs =-        let newOffer = offer { _dccOffset = size }-            (target, txt) = resumeMsg size newOffer-            st' = set (clientDCC . dsOffers . at key) (Just newOffer) st-        in cmdCtcp cs st' (target, "DCC", txt)----- | Implementation of @/me@-cmdMe :: ChannelCommand String-cmdMe channelId cs 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-                    , _msgBody = IrcBody (Ctcp myNick channelId "ACTION" (Text.pack rest))-                    }-     sendMsg cs (ircPrivmsg (idText channelId) actionTxt)-     commandSuccess-       $! recordChannelMessage network channelId entry st---- | Implementation of @/ctcp@-cmdCtcp :: NetworkCommand (String, String, String)-cmdCtcp cs st (target, cmd, args) =-  do let cmdTxt = Text.toUpper (Text.pack cmd)-         argTxt = Text.pack args-         tgtTxt = Text.pack target--     sendMsg cs (ircPrivmsg tgtTxt ("\^A" <> cmdTxt <> " " <> argTxt <> "\^A"))-     chatCommand-        (\src tgt -> Ctcp src tgt cmdTxt argTxt)-        tgtTxt cs st---- | Implementation of @/notice@-cmdNotice :: NetworkCommand (String, String)-cmdNotice cs st (target, rest)-  | null rest = commandFailureMsg "empty message" st-  | otherwise =-      do let restTxt = Text.pack rest-             tgtTxt = Text.pack target--         sendMsg cs (ircNotice tgtTxt restTxt)-         chatCommand-            (\src tgt -> Notice src tgt restTxt)-            tgtTxt cs st---- | Implementation of @/msg@-cmdMsg :: NetworkCommand (String, String)-cmdMsg cs st (target, rest)-  | null rest = commandFailureMsg "empty message" st-  | otherwise =-      do let restTxt = Text.pack rest-             tgtTxt = Text.pack target--         sendMsg cs (ircPrivmsg tgtTxt restTxt)-         chatCommand-            (\src tgt -> Privmsg src tgt restTxt)-            tgtTxt cs st---- | Common logic for @/msg@ and @/notice@-chatCommand ::-  (UserInfo -> Identifier -> IrcMsg) ->-  Text {- ^ target  -} ->-  NetworkState         ->-  ClientState          ->-  IO CommandResult-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-                          , _msgNetwork = network-                          , _msgBody = IrcBody (con myNick targetId)-                          })-                       | targetId <- targetIds ]--     return $! foldl' (\acc (targetId, entry) ->-                        recordChannelMessage network targetId entry acc)-                      st-                      entries---cmdConnect :: ClientCommand String-cmdConnect st networkStr =-  do -- abort any existing connection before connecting-     let network = Text.pack networkStr-     st' <- addConnection 0 Nothing Nothing network =<< abortNetwork network st-     commandSuccess-       $ changeFocus (NetworkFocus network) st'--cmdFocus :: ClientCommand (String, Maybe String)-cmdFocus st (network, mbChannel)-  | network == "*" = commandSuccess (changeFocus Unfocused st)-  | otherwise =-     case mbChannel of-       Nothing ->-         let focus = NetworkFocus (Text.pack network) in-         commandSuccess (changeFocus focus st)-       Just channel ->-         let focus = ChannelFocus (Text.pack network) (mkId (Text.pack channel)) in-         commandSuccess-           $ changeFocus focus st---tabWindows :: Bool -> ClientCommand String-tabWindows isReversed st _ =-  simpleTabCompletion plainWordCompleteMode [] completions isReversed st-  where-    completions = ["networks","channels","users"] :: [Text]----- | Implementation of @/windows@ command. Set subfocus to Windows.-cmdWindows :: ClientCommand (Maybe String)-cmdWindows st arg =-  case arg of-    Nothing         -> success AllWindows-    Just "networks" -> success NetworkWindows-    Just "channels" -> success ChannelWindows-    Just "users"    -> success UserWindows-    _               -> commandFailureMsg errmsg st-  where-    errmsg = "/windows expected networks, channels, or users"-    success x =-      commandSuccess (changeSubfocus (FocusWindows x) st)---- | Implementation of @/mentions@ command. Set subfocus to Mentions.-cmdMentions :: ClientCommand ()-cmdMentions st _ = commandSuccess (changeSubfocus FocusMentions st)---- | Implementation of @/palette@ command. Set subfocus to Palette.-cmdPalette :: ClientCommand ()-cmdPalette st _ = commandSuccess (changeSubfocus FocusPalette st)---- | Implementation of @/digraphs@ command. Set subfocus to Digraphs.-cmdDigraphs :: ClientCommand ()-cmdDigraphs st _ = commandSuccess (changeSubfocus FocusDigraphs st)---- | Implementation of @/keymap@ command. Set subfocus to Keymap.-cmdKeyMap :: ClientCommand ()-cmdKeyMap st _ = commandSuccess (changeSubfocus FocusKeyMap st)---- | Implementation of @/rtsstats@ command. Set subfocus to RtsStats.--- Update cached rts stats in client state.-cmdRtsStats :: ClientCommand ()-cmdRtsStats st _ =-  do mb <- getStats-     case mb of-       Nothing -> commandFailureMsg "RTS statistics not available. (Use +RTS -T)" st-       Just{}  -> commandSuccess $ set clientRtsStats mb-                                 $ changeSubfocus FocusRtsStats st---- | Implementation of @/help@ command. Set subfocus to Help.-cmdHelp :: ClientCommand (Maybe String)-cmdHelp st mb = commandSuccess (changeSubfocus focus st)-  where-    focus = FocusHelp (fmap Text.pack mb)---- | Implementation of @/dump@. Writes detailed contents of focused buffer--- to the given filename.-cmdDump :: ClientCommand String-cmdDump st fp =-  do res <- try (LText.writeFile fp (LText.unlines outputLines))-     case res of-       Left e  -> commandFailureMsg (Text.pack (displayException (e :: SomeException))) st-       Right{} -> commandSuccess st--  where-    focus = view clientFocus st-    msgs  = preview (clientWindows . ix focus . winMessages) st-    outputLines =-      case msgs of-        Nothing  -> []-        Just wls -> convert [] wls-    convert acc Nil = acc-    convert acc (wl :- wls) = convert (views wlFullImage imageText wl : acc) wls---- | Tab completion for @/splits[+]@. When given no arguments this--- populates the current list of splits, otherwise it tab completes--- all of the currently available windows.-tabSplits :: Bool -> ClientCommand String-tabSplits isReversed st rest--  -- If no arguments, populate the current splits-  | all (' '==) rest =-     let cmd = unwords $ "/splits"-                       : map (Text.unpack . renderSplitFocus) currentExtras--         currentExtras = view clientExtraFocus st-         newline = Edit.endLine cmd-     in commandSuccess (set (clientTextBox . Edit.line) newline st)--  -- Tab complete the available windows. Accepts either fully qualified-  -- window names or current network names without the ':'-  | otherwise =-     let completions = currentNet <> allWindows-         allWindows  = renderSplitFocus <$> views clientWindows Map.keys st-         currentNet  = case views clientFocus focusNetwork st of-                         Just net -> idText <$> channelWindowsOnNetwork net st-                         Nothing  -> []-     in simpleTabCompletion plainWordCompleteMode [] completions isReversed st----- | Tab completion for @/splits-@. This completes only from the list of active--- entries in the splits list.-tabActiveSplits :: Bool -> ClientCommand String-tabActiveSplits isReversed st _ =-  simpleTabCompletion plainWordCompleteMode [] completions isReversed st-  where-    completions = currentNetSplits <> currentSplits-    currentSplits = renderSplitFocus <$> view clientExtraFocus st-    currentNetSplits =-      [ idText chan-        | ChannelFocus net chan <- view clientExtraFocus st-        , views clientFocus focusNetwork st == Just net-        ]---withSplitFocuses ::-  ClientState                   ->-  String                        ->-  ([Focus] -> IO CommandResult) ->-  IO CommandResult-withSplitFocuses st str k =-  case mb of-    Nothing   -> commandFailureMsg "unable to parse arguments" st-    Just args -> k args-  where-    mb = traverse-           (parseFocus (views clientFocus focusNetwork st))-           (words str)---- | Parses a single focus name given a default network.------ The default is parameterized over an arbitrary 'Applicative'--- instance so that if you know the network you can use 'Identity'--- and if you might not, you can use 'Maybe'-parseFocus ::-  Applicative f =>-  f Text {- ^ default network    -} ->-  String {- ^ @[network:]target@ -} ->-  f Focus-parseFocus mbNet x =-  case break (==':') x of-    ("*","")     -> pure Unfocused-    (net,_:"")   -> pure (NetworkFocus (Text.pack net))-    (net,_:chan) -> pure (ChannelFocus (Text.pack net) (mkId (Text.pack chan)))-    (chan,"")    -> mbNet <&> \net ->-                    ChannelFocus net (mkId (Text.pack chan))---- | Render a entry from splits back to the textual format.-renderSplitFocus :: Focus -> Text-renderSplitFocus Unfocused          = "*"-renderSplitFocus (NetworkFocus x)   = x <> ":"-renderSplitFocus (ChannelFocus x y) = x <> ":" <> idText y----- | Implementation of @/splits@-cmdSplits :: ClientCommand String-cmdSplits st str =-  withSplitFocuses st str $ \args ->-    commandSuccess (setExtraFocus (nub args) st)----- | Implementation of @/splits+@. When no focuses are provided--- the current focus is used instead.-cmdSplitsAdd :: ClientCommand String-cmdSplitsAdd st str =-  withSplitFocuses st str $ \args ->-    let args'-          | null args = st ^.. clientFocus-          | otherwise = args-        extras = nub (args' ++ view clientExtraFocus st)--    in commandSuccess (setExtraFocus extras st)---- | Implementation of @/splits-@. When no focuses are provided--- the current focus is used instead.-cmdSplitsDel :: ClientCommand String-cmdSplitsDel st str =-  withSplitFocuses st str $ \args ->-    let args'-          | null args = st ^.. clientFocus-          | otherwise = args-        extras = view clientExtraFocus st \\ args'--    in commandSuccess (setExtraFocus extras st)---tabHelp :: Bool -> ClientCommand String-tabHelp isReversed st _ =-  simpleTabCompletion plainWordCompleteMode [] commandNames isReversed st-  where-    commandNames = fst <$> expandAliases (concatMap cmdSectionCmds commandsList)--simpleTabCompletion ::-  Prefix a =>-  WordCompletionMode {- ^ word completion mode -} ->-  [a]                {- ^ hints                -} ->-  [a]                {- ^ all completions      -} ->-  Bool               {- ^ reversed order       -} ->-  ClientState        {- ^ client state         -} ->-  IO CommandResult-simpleTabCompletion mode hints completions isReversed st =-  case traverseOf clientTextBox tryCompletion st of-    Nothing  -> commandFailure st-    Just st' -> commandSuccess st'-  where-    tryCompletion = wordComplete mode isReversed hints completions---- | @/connect@ tab completes known server names-tabConnect :: Bool -> ClientCommand String-tabConnect isReversed st _ =-  simpleTabCompletion plainWordCompleteMode [] networks isReversed st-  where-    networks = views clientConnections              HashMap.keys st-            ++ views (clientConfig . configServers) HashMap.keys st----- | When tab completing the first parameter of the focus command--- the current networks are used.-tabFocus :: Bool -> ClientCommand String-tabFocus isReversed st _ =-  simpleTabCompletion plainWordCompleteMode [] completions isReversed st-  where-    networks   = map mkId $ HashMap.keys $ view clientConnections st-    params     = words $ uncurry take $ clientLine st--    completions =-      case params of-        [_cmd,_net]      -> networks-        [_cmd,net,_chan] -> channelWindowsOnNetwork (Text.pack net) st-        _                -> []--cmdWhois :: NetworkCommand String-cmdWhois cs st rest =-  do sendMsg cs (ircWhois (Text.pack <$> words rest))-     commandSuccess st--cmdWho :: NetworkCommand String-cmdWho cs st rest =-  do sendMsg cs (ircWho (Text.pack <$> words rest))-     commandSuccess st--cmdWhowas :: NetworkCommand String-cmdWhowas cs st rest =-  do sendMsg cs (ircWhowas (Text.pack <$> words rest))-     commandSuccess st--cmdIson :: NetworkCommand String-cmdIson cs st rest =-  do sendMsg cs (ircIson (Text.pack <$> words rest))-     commandSuccess st--cmdUserhost :: NetworkCommand String-cmdUserhost cs st rest =-  do sendMsg cs (ircUserhost (Text.pack <$> words rest))-     commandSuccess st--cmdStats :: NetworkCommand String-cmdStats cs st rest =-  do sendMsg cs (ircStats (Text.pack <$> words rest))-     commandSuccess st--cmdLusers :: NetworkCommand (Maybe (String, Maybe String))-cmdLusers cs st arg =-  do sendMsg cs $ ircLusers $ fmap Text.pack $-       case arg of-         Nothing           -> []-         Just (x, Nothing) -> [x]-         Just (x, Just y)  -> [x,y]-     commandSuccess st--cmdMotd :: NetworkCommand (Maybe String)-cmdMotd cs st mbservername =-  do sendMsg cs $ ircMotd $ case mbservername of-                              Just s  -> Text.pack s-                              Nothing -> ""-     commandSuccess st--cmdAdmin :: NetworkCommand (Maybe String)-cmdAdmin cs st mbservername =-  do sendMsg cs $ ircAdmin $ case mbservername of-                              Just s  -> Text.pack s-                              Nothing -> ""-     commandSuccess st--cmdRules :: NetworkCommand (Maybe String)-cmdRules cs st mbservername =-  do sendMsg cs $ ircRules $-       case mbservername of-         Just s  -> Text.pack s-         Nothing -> ""-     commandSuccess st--cmdMap :: NetworkCommand ()-cmdMap cs st _ =-  do sendMsg cs ircMap-     commandSuccess st--cmdInfo :: NetworkCommand ()-cmdInfo cs st _ =-  do sendMsg cs ircInfo-     commandSuccess st--cmdVersion :: NetworkCommand (Maybe String)-cmdVersion cs st mbservername =-  do sendMsg cs $ ircVersion $ case mbservername of-                                Just s  -> Text.pack s-                                Nothing -> ""-     commandSuccess st--cmdList :: NetworkCommand String-cmdList cs st rest =-  do sendMsg cs (ircList (Text.pack <$> words rest))-     commandSuccess st--cmdKill :: NetworkCommand (String, String)-cmdKill cs st (client,rest) =-  do sendMsg cs (ircKill (Text.pack client) (Text.pack rest))-     commandSuccess st--cmdKline :: NetworkCommand (String, String, String)-cmdKline cs st (minutes, mask, reason) =-  do sendMsg cs (ircKline (Text.pack minutes) (Text.pack mask) (Text.pack reason))-     commandSuccess st--cmdUnkline :: NetworkCommand String-cmdUnkline cs st mask =-  do sendMsg cs (ircUnkline (Text.pack mask))-     commandSuccess st--cmdTestline :: NetworkCommand String-cmdTestline cs st mask =-  do sendMsg cs (ircTestline (Text.pack mask))-     commandSuccess st--cmdTestmask :: NetworkCommand (String, String)-cmdTestmask cs st (mask, gecos) =-  do sendMsg cs (ircTestmask (Text.pack mask) (Text.pack gecos))-     commandSuccess st--cmdMasktrace :: NetworkCommand (String, String)-cmdMasktrace cs st (mask, gecos) =-  do sendMsg cs (ircMasktrace (Text.pack mask) (Text.pack gecos))-     commandSuccess st--cmdTrace :: NetworkCommand (Maybe (String, Maybe String))-cmdTrace cs st args =-  do let argsList =-           case args of-            Nothing           -> []-            Just (x, Nothing) -> [x]-            Just (x, Just y)  -> [x, y]-     sendMsg cs (rawIrcMsg "TRACE" (map Text.pack argsList))-     commandSuccess st--cmdAway :: NetworkCommand String-cmdAway cs st rest =-  do sendMsg cs (ircAway (Text.pack rest))-     commandSuccess st--cmdLinks :: NetworkCommand String-cmdLinks cs st rest =-  do sendMsg cs (ircLinks (Text.pack <$> words rest))-     commandSuccess st--cmdTime :: NetworkCommand (Maybe String)-cmdTime cs st arg =-  do sendMsg cs $ ircTime $-       case arg of-         Nothing -> ""-         Just x  -> Text.pack x-     commandSuccess st--cmdZnc :: NetworkCommand String-cmdZnc cs st rest =-  do sendMsg cs (ircZnc (Text.words (Text.pack rest)))-     commandSuccess st--cmdZncPlayback :: NetworkCommand (Maybe (String, Maybe String))-cmdZncPlayback cs st args =-  case args of--    -- request everything-    Nothing -> success "0"--    -- current date explicit time-    Just (timeStr, Nothing)-       | Just tod <- parseFormats timeFormats timeStr ->-          do now <- getZonedTime-             let (nowTod,t) = (zonedTimeLocalTime . localTimeTimeOfDay <<.~ tod) now-                 yesterday = over (zonedTimeLocalTime . localTimeDay) (addDays (-1))-                 fixDay-                   | tod <= nowTod = id-                   | otherwise     = yesterday-             successZoned (fixDay t)--    -- explicit date and time-    Just (dateStr, Just timeStr)-       | Just day  <- parseFormats dateFormats dateStr-       , Just tod  <- parseFormats timeFormats timeStr ->-          do tz <- getCurrentTimeZone-             successZoned ZonedTime-               { zonedTimeZone = tz-               , zonedTimeToLocalTime = LocalTime-                   { localTimeOfDay = tod-                   , localDay       = day } }--    _ -> commandFailureMsg "unable to parse date/time arguments" st--  where-    -- %k doesn't require a leading 0 for times before 10AM-    timeFormats = ["%k:%M:%S","%k:%M"]-    dateFormats = ["%F"]-    parseFormats formats str =-      asum (map (parseTimeM False defaultTimeLocale ?? str) formats)--    successZoned = success . formatTime defaultTimeLocale "%s"--    success start =-      do sendMsg cs (ircZnc ["*playback", "play", "*", Text.pack start])-         commandSuccess st--cmdMode :: NetworkCommand [String]-cmdMode cs st xs = modeCommand (Text.pack <$> xs) cs st--cmdNick :: NetworkCommand String-cmdNick cs st nick =-  do sendMsg cs (ircNick (Text.pack nick))-     commandSuccess st--cmdPart :: ChannelCommand String-cmdPart channelId cs 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 String-cmdSay _ _ st rest = executeChat rest st--cmdInvite :: ChannelCommand String-cmdInvite channelId cs st nick =-  do let freeTarget = has (csChannels . ix channelId . chanModes . ix 'g') cs-         cmd = ircInvite (Text.pack nick) channelId-     cs' <- if freeTarget-              then cs <$ sendMsg cs cmd-              else sendModeration channelId [cmd] cs-     commandSuccessUpdateCS cs' st--commandSuccessUpdateCS :: NetworkState -> ClientState -> IO CommandResult-commandSuccessUpdateCS cs st =-  do let network = view csNetwork cs-     commandSuccess-       $ setStrict (clientConnection network) cs st--cmdTopic :: ChannelCommand String-cmdTopic channelId cs st rest =-  do sendTopic channelId (Text.pack rest) cs-     commandSuccess st--tabTopic ::-  Bool {- ^ reversed -} ->-  ChannelCommand String-tabTopic _ channelId cs st rest--  | all (==' ') rest-  , Just topic <- preview (csChannels . ix channelId . chanTopic) cs =-     do let textBox = set Edit.line (Edit.endLine $ "/topic " ++ Text.unpack topic)-        commandSuccess (over clientTextBox textBox st)--  | otherwise = commandFailure st---cmdUsers :: ChannelCommand ()-cmdUsers _ _ st _ = commandSuccess (changeSubfocus FocusUsers st)--cmdChannelInfo :: ChannelCommand ()-cmdChannelInfo _ _ st _ = commandSuccess (changeSubfocus FocusInfo st)--cmdMasks :: ChannelCommand String-cmdMasks channel cs st rest =-  case rest of-    [mode] | mode `elem` view (csModeTypes . modesLists) cs ->--        do let connecting = has (csPingStatus . _PingConnecting) cs-               listLoaded = has (csChannels . ix channel . chanLists . ix mode) cs-           unless (connecting || listLoaded)-             (sendMsg cs (ircMode channel [Text.singleton mode]))--           commandSuccess (changeSubfocus (FocusMasks mode) st)--    _ -> commandFailureMsg "unknown mask mode" st--cmdKick :: ChannelCommand (String, String)-cmdKick channelId cs st (who,reason) =-  do let msg = Text.pack reason-         cmd = ircKick channelId (Text.pack who) msg-     cs' <- sendModeration channelId [cmd] cs-     commandSuccessUpdateCS cs' st---cmdKickBan :: ChannelCommand (String, String)-cmdKickBan channelId cs st (who,reason) =-  do let msg = Text.pack reason--         whoTxt     = Text.pack who--         mask = renderUserInfo (computeBanUserInfo (mkId whoTxt) cs)-         cmds = [ ircMode channelId ["b", mask]-                , ircKick channelId whoTxt msg-                ]-     cs' <- sendModeration channelId cmds cs-     commandSuccessUpdateCS cs' st--computeBanUserInfo :: Identifier -> NetworkState    -> UserInfo-computeBanUserInfo who cs =-  case view (csUser who) cs of-    Nothing                     -> UserInfo who "*" "*"-    Just (UserAndHost _ host _) -> UserInfo "*" "*" host--cmdRemove :: ChannelCommand (String, String)-cmdRemove channelId cs st (who,reason) =-  do let msg = Text.pack reason-         cmd = ircRemove channelId (Text.pack who) msg-     cs' <- sendModeration channelId [cmd] cs-     commandSuccessUpdateCS cs' st--cmdKnock :: NetworkCommand (String, String)-cmdKnock cs st (chan,message) =-  do sendMsg cs (ircKnock (Text.pack chan) (Text.pack message))-     commandSuccess st--cmdJoin :: NetworkCommand (String, Maybe String)-cmdJoin cs st (channels, mbKeys) =-  do let network = view csNetwork cs-     let channelId = mkId (Text.pack (takeWhile (/=',') channels))-     sendMsg cs (ircJoin (Text.pack channels) (Text.pack <$> mbKeys))-     commandSuccess-        $ changeFocus (ChannelFocus network channelId) st---- | @/query@ command. Takes a channel or nickname and switches--- focus to that target on the current network.-cmdQuery :: ClientCommand (String, String)-cmdQuery st (target, msg) =-  case parseFocus (views clientFocus focusNetwork st) target of-    Just (ChannelFocus net tgt)--      | null msg -> commandSuccess st'--      | Just cs <- preview (clientConnection net) st ->-           do let tgtTxt = idText tgt-                  msgTxt = Text.pack msg-              sendMsg cs (ircPrivmsg tgtTxt msgTxt)-              chatCommand-                 (\src tgt1 -> Privmsg src tgt1 msgTxt)-                 tgtTxt cs st'-      where-       firstTgt = mkId (Text.takeWhile (','/=) (idText tgt))-       st' = changeFocus (ChannelFocus net firstTgt) st--    _ -> commandFailureMsg "Bad target" st----- | @/channel@ command. Takes a channel or nickname and switches--- focus to that target on the current network.-cmdChannel :: ClientCommand String-cmdChannel st channel =-  case parseFocus (views clientFocus focusNetwork st) channel of-    Just focus -> commandSuccess (changeFocus focus st)-    Nothing    -> commandFailureMsg "No current network" st---- | Tab completion for @/channel@. Tab completion uses pre-existing--- windows.-tabChannel ::-  Bool {- ^ reversed order -} ->-  ClientCommand String-tabChannel isReversed st _ =-  simpleTabCompletion plainWordCompleteMode [] completions isReversed st-  where-    completions = currentNet <> allWindows-    allWindows  = renderSplitFocus <$> views clientWindows Map.keys st-    currentNet  = case views clientFocus focusNetwork st of-                    Just net -> idText <$> channelWindowsOnNetwork net st-                    Nothing  -> []---- | Return the list of identifiers for open channel windows on--- the given network name.-channelWindowsOnNetwork ::-  Text         {- ^ network              -} ->-  ClientState  {- ^ client state         -} ->-  [Identifier] {- ^ open channel windows -}-channelWindowsOnNetwork network st =-  [ chan | ChannelFocus net chan <- Map.keys (view clientWindows st)-         , net == network ]---cmdQuit :: NetworkCommand String-cmdQuit cs st rest =-  do let msg = Text.pack rest-     sendMsg cs (ircQuit msg)-     commandSuccess st--cmdDisconnect :: NetworkCommand ()-cmdDisconnect cs st _ =-  do st' <- abortNetwork (view csNetwork cs) st-     commandSuccess st'---- | Reconnect to the currently focused network. It's possible--- that we're not currently connected to a network, so--- this is implemented as a client command.-cmdReconnect :: ClientCommand ()-cmdReconnect st _-  | Just network <- views clientFocus focusNetwork st =--      do let tm = preview (clientConnection network . csLastReceived . folded) st-         st' <- addConnection 0 tm Nothing network =<< abortNetwork network st-         commandSuccess-           $ changeFocus (NetworkFocus network) st'--  | otherwise = commandFailureMsg "command requires focused network" st--cmdIgnore :: ClientCommand String-cmdIgnore st rest =-  case mkId <$> Text.words (Text.pack rest) of-    [] -> commandSuccess (changeSubfocus FocusIgnoreList st)-    xs -> commandSuccess st2-      where-        (newIgnores, st1) = (clientIgnores <%~ updateIgnores) st-        st2 = set clientIgnoreMask (buildMask (toList newIgnores)) st1--        updateIgnores :: HashSet Identifier -> HashSet Identifier-        updateIgnores s = foldl' updateIgnore s xs--        updateIgnore s x = over (contains x) not s---- | Complete the nickname at the current cursor position using the--- userlist for the currently focused channel (if any)-tabIgnore :: Bool {- ^ reversed -} -> ClientCommand String-tabIgnore isReversed st _ =-  simpleTabCompletion mode hint completions isReversed st-  where-    hint          = activeNicks st-    completions   = currentCompletionList st ++ views clientIgnores toList st-    mode          = currentNickCompletionMode st---- | Implementation of @/reload@------ Attempt to reload the configuration file-cmdReload :: ClientCommand (Maybe String)-cmdReload st mbPath =-  do let path = mbPath <|> Just (view clientConfigPath st)-     res <- loadConfiguration path-     case res of-       Left e -> commandFailureMsg (describeProblem e) st-       Right (path',cfg) ->-         do st1 <- clientStartExtensions-                 $ set clientConfig cfg-                 $ set clientConfigPath path' 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.------ /NOT IMPLEMENTED/-tabReload :: Bool {- ^ reversed -} -> ClientCommand String-tabReload _ st _ = commandFailure st---modeParamArgs :: ClientState -> String -> Maybe (Args ClientState [String])-modeParamArgs st str =-  case view clientFocus st of-    Unfocused      -> Nothing-    NetworkFocus _ -> Just (pure [str])-    ChannelFocus net _ ->--         -- determine current mode types-      do cs <- preview (clientConnection net) st-         let types = view csModeTypes cs--         -- parse the list of modes being set-         flags <- splitModes types (Text.pack str) []--         -- generate the argument specification-         let (req,opt) = foldr (countFlags types) ([],[]) flags-         return ((str:) <$> tokenList req (map (++"?") opt))----- | This function computes the list of required and optional parameters--- corresponding to the flags that have been entered.-countFlags ::-  ModeTypes           {- ^ network's mode behaviors              -} ->-  (Bool, Char, Text)  {- ^ polarity mode-letter unused-parameter -} ->-  ([String],[String]) {- ^ required-names optional-names         -} ->-  ([String],[String]) {- ^ required-names optional-names         -}-countFlags types (pol, flag, _)-  |        flag `elem` view modesLists       types = addOpt-  | pol && flag `elem` view modesSetArg      types = addReq-  |        flag `elem` view modesAlwaysArg   types = addReq-  | elemOf (modesPrefixModes . folded . _1) flag types = addReq-  | otherwise                                      = id-  where-    addReq (req,opt) = ((flag:" param"):req,opt)-    addOpt ([] ,opt) = ([], (flag:" param"):opt)-    addOpt (req,opt) = ((flag:" param"):req,opt)---modeCommand ::-  [Text] {- mode parameters -} ->-  NetworkState                 ->-  ClientState                  ->-  IO CommandResult-modeCommand modes cs st =-  case view clientFocus st of--    NetworkFocus _ ->-      do sendMsg cs (ircMode (view csNick cs) modes)-         commandSuccess st--    ChannelFocus _ chan ->-      case modes of-        [] -> success False [[]]-        flags:params ->-          case splitModes (view csModeTypes cs) flags params of-            Nothing -> commandFailureMsg "failed to parse modes" st-            Just parsedModes ->-              success needOp (unsplitModes <$> chunksOf (view csModeCount cs) parsedModes')-              where-                parsedModes'-                  | useChanServ chan cs = filter (not . isOpMe) parsedModes-                  | otherwise           = parsedModes--                needOp = not (all isPublicChannelMode parsedModes)-      where-        isOpMe (True, 'o', param) = mkId param == view csNick cs-        isOpMe _                  = False--        success needOp argss =-          do let cmds = ircMode chan <$> argss-             cs' <- if needOp-                      then sendModeration chan cmds cs-                      else cs <$ traverse_ (sendMsg cs) cmds-             commandSuccessUpdateCS cs' st--    _ -> commandFailure st--tabMode :: Bool -> NetworkCommand String-tabMode isReversed cs st rest =-  case view clientFocus st of--    ChannelFocus _ channel-      | flags:params     <- Text.words (Text.pack rest)-      , Just parsedModes <- splitModes (view csModeTypes cs) flags params-      , let parsedModesWithParams =-              [ (pol,mode) | (pol,mode,arg) <- parsedModes, not (Text.null arg) ]-      , (pol,mode):_      <- drop (paramIndex-3) parsedModesWithParams-      , let (hint, completions) = computeModeCompletion pol mode channel cs st-      -> simpleTabCompletion plainWordCompleteMode hint completions isReversed st--    _ -> commandFailure st--  where-    paramIndex = length $ words $ uncurry take $ clientLine st--activeNicks ::-  ClientState ->-  [Identifier]-activeNicks st =-  case view clientFocus st of-    focus@(ChannelFocus network channel) ->-      toListOf-        ( clientWindows    . ix focus-        . winMessages      . each-        . wlSummary        . folding summaryActor-        . filtered isActive-        . filtered isNotSelf ) st-      where-        isActive n = HashMap.member n userMap-        self = preview ( clientConnection network . csNick ) st-        isNotSelf n = case self of-                        Nothing -> True-                        Just s -> n /= s-        userMap = view ( clientConnection network-                       . csChannels . ix channel-                       . chanUsers) st--    _ -> []--  where-    -- Returns the 'Identifier' of the nickname responsible for-    -- the window line when that action was significant enough to-    -- be considered a hint for tab completion.-    summaryActor :: IrcSummary -> Maybe Identifier-    summaryActor (ChatSummary who) = Just $! userNick who-    summaryActor _                 = Nothing---- | Use the *!*@host masks of users for channel lists when setting list modes------ Use the channel's mask list for removing modes------ Use the nick list otherwise-computeModeCompletion ::-  Bool {- ^ mode polarity -} ->-  Char {- ^ mode          -} ->-  Identifier {- ^ channel -} ->-  NetworkState    ->-  ClientState ->-  ([Identifier],[Identifier]) {- ^ (hint, complete) -}-computeModeCompletion pol mode channel cs st-  | mode `elem` view modesLists modeSettings =-        if pol then ([],usermasks) else ([],masks)-  | otherwise = (activeNicks st, nicks)-  where-    modeSettings = view csModeTypes cs-    nicks = HashMap.keys (view (csChannels . ix channel . chanUsers) cs)--    masks = mkId <$> HashMap.keys (view (csChannels . ix channel . chanLists . ix mode) cs)--    usermasks =-      [ mkId ("*!*@" <> host)-        | nick <- HashMap.keys (view (csChannels . ix channel . chanUsers) cs)-        , UserAndHost _ host _ <- toListOf (csUsers . ix nick) cs-        ]---- | Predicate for mode commands that can be performed without ops-isPublicChannelMode :: (Bool, Char, Text) -> Bool-isPublicChannelMode (True, 'b', param) = Text.null param -- query ban list-isPublicChannelMode (True, 'q', param) = Text.null param -- query quiet list-isPublicChannelMode _                  = False--commandNameCompletion :: Bool -> ClientState -> Maybe ClientState-commandNameCompletion isReversed st =-  do guard (cursorPos == n)-     clientTextBox (wordComplete plainWordCompleteMode isReversed [] possibilities) st-  where-    n = length white + length leadingPart-    (cursorPos, line) = clientLine st-    (white, leadingPart) = takeWhile (' ' /=) <$> span (' '==) line-    possibilities = Text.cons '/' <$> commandNames-    commandNames = keys commands-                ++ keys (view (clientConfig . configMacros) st)---- | Complete the nickname at the current cursor position using the--- userlist for the currently focused channel (if any)-nickTabCompletion :: Bool {- ^ reversed -} -> ClientState -> IO CommandResult-nickTabCompletion isReversed st =-  simpleTabCompletion mode hint completions isReversed st-  where-    hint          = activeNicks st-    completions   = currentCompletionList st-    mode          = currentNickCompletionMode st--cmdExtension :: ClientCommand (String, String)-cmdExtension st (name,command) =-  do res <- clientCommandExtension (Text.pack name) (Text.pack command) st-     case res of-       Nothing  -> commandFailureMsg "unknown extension" st-       Just st' -> commandSuccess st'---- | Implementation of @/exec@ command.-cmdExec :: ClientCommand String-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--        (Unspecified, Unspecified) -> Right (sendToClient now)--        (Specified network, Specified channel) ->-          case preview (clientConnection network) st of-            Nothing -> Left ["Unknown network"]-            Just cs -> Right (sendToChannel cs channel)--        (_ , Specified channel) ->-          case currentNetworkState of-            Nothing -> Left ["No current network"]-            Just cs -> Right (sendToChannel cs channel)--        (Specified network, _) ->-          case preview (clientConnection network) st of-            Nothing -> Left ["Unknown network"]-            Just cs -> Right (sendToNetwork now cs)--        (_, Current) ->-          case currentNetworkState of-            Nothing -> Left ["No current network"]-            Just cs ->-              case view clientFocus st of-                ChannelFocus _ channel -> Right (sendToChannel cs (idText channel))-                _                      -> Left ["No current channel"]--        (Current, _) ->-          case currentNetworkState of-            Nothing -> Left ["No current network"]-            Just cs -> Right (sendToNetwork now cs)--    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 "" ("Bad raw message: " <> msg) st1-             Just raw ->-               do sendMsg cs raw-                  return st1) st msgs--    sendToChannel cs channel msgs =-      commandSuccess =<<-      foldM (\st1 msg ->-        do sendMsg cs (ircPrivmsg 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' (flip (recordError now "")) st (map Text.pack es)--recordSuccess :: ZonedTime -> ClientState -> Text -> ClientState-recordSuccess now ste m =-  recordNetworkMessage ClientMessage-    { _msgTime    = now-    , _msgBody    = NormalBody m-    , _msgNetwork = ""-    } ste---cmdUrl :: ClientCommand (Maybe Int)-cmdUrl st arg =-  case view (clientConfig . configUrlOpener) st of-    Nothing     -> commandFailureMsg "url-opener not configured" st-    Just opener -> doUrlOpen opener (maybe 0 (subtract 1) arg)-  where-    focus = view clientFocus st--    urls = toListOf ( clientWindows . ix focus . winMessages . each . wlText-                    . folding urlMatches) st--    doUrlOpen opener n =-      case preview (ix n) urls of-        Just url -> openUrl opener (Text.unpack url) st-        Nothing  -> commandFailureMsg "bad url number" st--openUrl :: FilePath -> String -> ClientState -> IO CommandResult-openUrl opener url st =-  do res <- try (callProcess opener [url])-     case res of-       Left e  -> commandFailureMsg (Text.pack (displayException (e :: IOError))) st-       Right{} -> commandSuccess st---- | Implementation of @/grep@-cmdGrep :: ClientCommand String-cmdGrep st str-  | null str  = commandSuccess (set clientRegex Nothing st)-  | otherwise =-      case buildMatcher str of-        Nothing -> commandFailureMsg "bad grep" st-        Just  r -> commandSuccess (set clientRegex (Just r) st)--cmdOper :: NetworkCommand (String, String)-cmdOper cs st (user, pass) =-  do sendMsg cs (ircOper (Text.pack user) (Text.pack pass))-     commandSuccess st--cmdCert :: NetworkCommand ()-cmdCert _ st _ = commandSuccess (changeSubfocus FocusCert st)+import           Client.Message+import           Client.State+import           Client.State.Extensions+import           Client.State.Focus+import           Client.State.Network+import           Client.State.Window+import           Control.Applicative+import           Control.Exception (displayException, try)+import           Control.Lens+import           Control.Monad+import           Data.Foldable+import           Data.Text (Text)+import qualified Data.Text as Text+import           Data.Time (ZonedTime, getZonedTime)+import           Irc.Commands+import           Irc.Identifier+import           Irc.RawIrcMsg+import           Irc.Message+import           RtsStats (getStats)+import           System.Process++import           Client.Commands.Channel (channelCommands)+import           Client.Commands.Chat (chatCommands, chatCommand', executeChat)+import           Client.Commands.Connection (connectionCommands)+import           Client.Commands.DCC (dccCommands)+import           Client.Commands.Operator (operatorCommands)+import           Client.Commands.Queries (queryCommands)+import           Client.Commands.Toggles (togglesCommands)+import           Client.Commands.Window (windowCommands)+import           Client.Commands.ZNC (zncCommands)+import           Client.Commands.TabCompletion+import           Client.Commands.Types++-- | 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. Leading spaces before the @/@ are ignored when checking for+-- commands.+execute ::+  String           {- ^ chat or command -} ->+  ClientState      {- ^ client state    -} ->+  IO CommandResult {- ^ command result  -}+execute str st =+  let st' = set clientErrorMsg Nothing st in+  case dropWhile (' '==) str of+    []          -> commandFailure st+    '/':command -> executeUserCommand Nothing command st'+    _           -> executeChat str st'++-- | Execute command provided by user, resolve aliases if necessary.+--+-- The last disconnection time is stored in text form and is available+-- for substitutions in macros. It is only provided when running startup+-- commands during a reconnect event.+executeUserCommand ::+  Maybe Text       {- ^ disconnection time -} ->+  String           {- ^ command            -} ->+  ClientState      {- ^ client state       -} ->+  IO CommandResult {- ^ command result     -}+executeUserCommand discoTime command st = do+  let key = Text.takeWhile (/=' ') (Text.pack command)+      rest = dropWhile (==' ') (dropWhile (/=' ') command)++  case views (clientConfig . configMacros) (recognize key) st of+    Exact (Macro (MacroSpec spec) cmdExs) ->+      case doExpansion spec cmdExs rest of+        Nothing   -> commandFailureMsg "macro expansions failed" st+        Just cmds -> process cmds st+    _ -> executeCommand Nothing command st+  where+    doExpansion spec cmdExs rest =+      do args <- parse st spec rest+         traverse (resolveMacro (map Text.pack args)) cmdExs++    resolveMacro args = resolveMacroExpansions (commandExpansion discoTime st) (expandInt args)++    expandInt :: [a] -> Integer -> Maybe a+    expandInt args i = preview (ix (fromInteger i)) args++++    process [] st0 = commandSuccess st0+    process (c:cs) st0 =+      do res <- executeCommand Nothing (Text.unpack c) st0+         case res of+           CommandSuccess st1 -> process cs st1+           CommandFailure st1 -> process cs st1 -- ?+           CommandQuit st1    -> return (CommandQuit st1)++-- | Compute the replacement value for the given expansion variable.+commandExpansion ::+  Maybe Text  {- ^ disconnect time    -} ->+  ClientState {- ^ client state       -} ->+  Text        {- ^ expansion variable -} ->+  Maybe Text  {- ^ expansion value    -}+commandExpansion discoTime st v =+  case v of+    "network" -> views clientFocus focusNetwork st+    "channel" -> previews (clientFocus . _ChannelFocus . _2) idText st+    "nick"    -> do net <- views clientFocus focusNetwork st+                    cs  <- preview (clientConnection net) st+                    return (views csNick idText cs)+    "disconnect" -> discoTime+    _         -> Nothing+++-- | Respond to the TAB key being pressed. This can dispatch to a command+-- specific completion mode when relevant. Otherwise this will complete+-- input based on the users of the channel related to the current buffer.+tabCompletion ::+  Bool             {- ^ reversed       -} ->+  ClientState      {- ^ client state   -} ->+  IO CommandResult {- ^ command result -}+tabCompletion isReversed st =+  case dropWhile (' ' ==) $ snd $ clientLine st of+    '/':command -> executeCommand (Just isReversed) command st+    _           -> nickTabCompletion isReversed st+++-- | Parse and execute the given command. When the first argument is Nothing+-- the command is executed, otherwise the first argument is the cursor+-- position for tab-completion+executeCommand ::+  Maybe Bool       {- ^ tab-completion direction -} ->+  String           {- ^ command                  -} ->+  ClientState      {- ^ client state             -} ->+  IO CommandResult {- ^ command result           -}++executeCommand (Just isReversed) _ st+  | Just st' <- commandNameCompletion isReversed st = commandSuccess st'++executeCommand tabCompleteReversed str st =+  let (cmd, rest) = break (==' ') str+      cmdTxt      = Text.toLower (Text.pack cmd)++      finish spec exec tab =+        case tabCompleteReversed of+          Just isReversed -> tab isReversed st rest+          Nothing ->+            case parse st spec rest of+              Nothing -> commandFailureMsg "bad command arguments" st+              Just arg -> exec st arg+  in+  case recognize cmdTxt commands of++    Exact Command{cmdImplementation=impl, cmdArgumentSpec=argSpec} ->+      case impl of+        ClientCommand exec tab ->+          finish argSpec exec tab++        NetworkCommand exec tab+          | Just network <- views clientFocus focusNetwork st+          , Just cs      <- preview (clientConnection network) st ->+              finish argSpec (exec cs) (\x -> tab x cs)+          | otherwise -> commandFailureMsg "command requires focused network" st++        ChannelCommand exec tab+          | ChannelFocus network channelId <- view clientFocus st+          , Just cs <- preview (clientConnection network) st+          , isChannelIdentifier cs channelId ->+              finish argSpec (exec channelId cs) (\x -> tab x channelId cs)+          | otherwise -> commandFailureMsg "command requires focused channel" st++        ChatCommand exec tab+          | ChannelFocus network channelId <- view clientFocus st+          , Just cs <- preview (clientConnection network) st ->+              finish argSpec (exec channelId cs) (\x -> tab x channelId cs)+          | otherwise -> commandFailureMsg "command requires focused chat window" st++    _ -> case tabCompleteReversed of+           Just isReversed -> nickTabCompletion isReversed st+           Nothing         -> commandFailureMsg "unknown command" st+++-- | Expands each alias to have its own copy of the command callbacks+expandAliases :: [Command] -> [(Text,Command)]+expandAliases xs =+  [ (name, cmd) | cmd <- xs, name <- toList (cmdNames cmd) ]+++-- | Map of built-in client commands to their implementations, tab completion+-- logic, and argument structures.+commands :: Recognizer Command+commands = fromCommands (expandAliases (concatMap cmdSectionCmds commandsList))+++-- | Raw list of commands in the order used for @/help@+commandsList :: [CommandSection]+commandsList =++  ------------------------------------------------------------------------+  [ CommandSection "Client commands"+  ------------------------------------------------------------------------++  [ Command+      (pure "exit")+      (pure ())+      "Exit the client immediately.\n"+    $ ClientCommand cmdExit noClientTab++  , Command+      (pure "reload")+      (optionalArg (simpleToken "[filename]"))+      "Reload the client configuration file.\n\+      \\n\+      \If \^Bfilename\^B is provided it will be used to reload.\n\+      \Otherwise the previously loaded configuration file will be reloaded.\n"+    $ ClientCommand cmdReload tabReload++  , Command+      (pure "extension")+      (liftA2 (,) (simpleToken "extension") (remainingArg "arguments"))+      "Calls the process_command callback of the given extension.\n\+      \\n\+      \\^Bextension\^B should be the name of the loaded extension.\n"+    $ ClientCommand cmdExtension simpleClientTab++  , Command+      (pure "palette")+      (pure ())+      "Show the current palette settings and a color chart to help pick new colors.\n"+    $ ClientCommand cmdPalette noClientTab++  , Command+      (pure "digraphs")+      (pure ())+      "\^BDescription:\^B\n\+      \\n\+      \    Show the table of digraphs. A digraph is a pair of characters\n\+      \    can be used together to represent an uncommon character. Type\n\+      \    the two-character digraph corresponding to the desired output\n\+      \    character and then press M-k (default binding).\n\+      \\n\+      \    Note that the digraphs list is searchable with /grep.\n\+      \\n\+      \\^BSee also:\^B grep\n"+    $ ClientCommand cmdDigraphs noClientTab++  , Command+      (pure "keymap")+      (pure ())+      "Show the key binding map.\n\+      \\n\+      \Key bindings can be changed in configuration file. See `glirc --config-format`.\n"+    $ ClientCommand cmdKeyMap noClientTab++  , Command+      (pure "rtsstats")+      (pure ())+      "Show the GHC RTS statistics.\n"+    $ ClientCommand cmdRtsStats noClientTab++  , Command+      (pure "exec")+      (remainingArg "arguments")+      "Execute a command synchnonously sending the to a configuration destination.\n\+      \\n\+      \\^Barguments\^B: [-n[network]] [-c[channel]] [-i input] command [arguments...]\n\+      \\n\+      \When \^Binput\^B is specified it is sent to the stdin.\n\+      \\n\+      \When neither \^Bnetwork\^B nor \^Bchannel\^B are specified output goes to client window (*).\n\+      \When \^Bnetwork\^B is specified output is sent as raw IRC traffic to the network.\n\+      \When \^Bchannel\^B is specified output is sent as chat to the given channel on the current network.\n\+      \When \^Bnetwork\^B and \^Bchannel\^B are specified output is sent as chat to the given channel on the given network.\n\+      \\n\+      \\^Barguments\^B is divided on spaces into words before being processed\+      \ by getopt. Use Haskell string literal syntax to create arguments with\+      \ escaped characters and spaces inside.\n\+      \\n"+    $ ClientCommand cmdExec simpleClientTab++  , Command+      (pure "url")+      optionalNumberArg+      "Open a URL seen in chat.\n\+      \\n\+      \The URL is opened using the executable configured under \^Burl-opener\^B.\n\+      \\n\+      \When this command is active in the textbox, chat messages are filtered to\+      \ only show ones with URLs.\n\+      \\n\+      \When \^Bnumber\^B is omitted it defaults to \^B1\^B. The number selects the\+      \ URL to open counting back from the most recent.\n"+    $ ClientCommand cmdUrl noClientTab++  , Command+      (pure "help")+      (optionalArg (simpleToken "[command]"))+      "Show command documentation.\n\+      \\n\+      \When \^Bcommand\^B is omitted a list of all commands is displayed.\n\+      \When \^Bcommand\^B is specified detailed help for that command is shown.\n"+    $ ClientCommand cmdHelp tabHelp++  ------------------------------------------------------------------------+  ],++  togglesCommands, connectionCommands, windowCommands, chatCommands,+  queryCommands, channelCommands, zncCommands, operatorCommands,+  dccCommands ]++-- | Implementation of @/exit@ command.+cmdExit :: ClientCommand ()+cmdExit st _ = return (CommandQuit st)++-- | Implementation of @/palette@ command. Set subfocus to Palette.+cmdPalette :: ClientCommand ()+cmdPalette st _ = commandSuccess (changeSubfocus FocusPalette st)++-- | Implementation of @/digraphs@ command. Set subfocus to Digraphs.+cmdDigraphs :: ClientCommand ()+cmdDigraphs st _ = commandSuccess (changeSubfocus FocusDigraphs st)++-- | Implementation of @/keymap@ command. Set subfocus to Keymap.+cmdKeyMap :: ClientCommand ()+cmdKeyMap st _ = commandSuccess (changeSubfocus FocusKeyMap st)++-- | Implementation of @/rtsstats@ command. Set subfocus to RtsStats.+-- Update cached rts stats in client state.+cmdRtsStats :: ClientCommand ()+cmdRtsStats st _ =+  do mb <- getStats+     case mb of+       Nothing -> commandFailureMsg "RTS statistics not available. (Use +RTS -T)" st+       Just{}  -> commandSuccess $ set clientRtsStats mb+                                 $ changeSubfocus FocusRtsStats st++-- | Implementation of @/help@ command. Set subfocus to Help.+cmdHelp :: ClientCommand (Maybe String)+cmdHelp st mb = commandSuccess (changeSubfocus focus st)+  where+    focus = FocusHelp (fmap Text.pack mb)++tabHelp :: Bool -> ClientCommand String+tabHelp isReversed st _ =+  simpleTabCompletion plainWordCompleteMode [] commandNames isReversed st+  where+    commandNames = fst <$> expandAliases (concatMap cmdSectionCmds commandsList)++-- | Implementation of @/reload@+--+-- Attempt to reload the configuration file+cmdReload :: ClientCommand (Maybe String)+cmdReload st mbPath =+  do let path = mbPath <|> Just (view clientConfigPath st)+     res <- loadConfiguration path+     case res of+       Left e -> commandFailureMsg (describeProblem e) st+       Right (path',cfg) ->+         do st1 <- clientStartExtensions+                 $ set clientConfig cfg+                 $ set clientConfigPath path' 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.+--+-- /NOT IMPLEMENTED/+tabReload :: Bool {- ^ reversed -} -> ClientCommand String+tabReload _ st _ = commandFailure st++commandNameCompletion :: Bool -> ClientState -> Maybe ClientState+commandNameCompletion isReversed st =+  do guard (cursorPos == n)+     clientTextBox (wordComplete plainWordCompleteMode isReversed [] possibilities) st+  where+    n = length white + length leadingPart+    (cursorPos, line) = clientLine st+    (white, leadingPart) = takeWhile (' ' /=) <$> span (' '==) line+    possibilities = Text.cons '/' <$> commandNames+    commandNames = keys commands+                ++ keys (view (clientConfig . configMacros) st)++cmdExtension :: ClientCommand (String, String)+cmdExtension st (name,command) =+  do res <- clientCommandExtension (Text.pack name) (Text.pack command) st+     case res of+       Nothing  -> commandFailureMsg "unknown extension" st+       Just st' -> commandSuccess st'++-- | Implementation of @/exec@ command.+cmdExec :: ClientCommand String+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++        (Unspecified, Unspecified) -> Right (sendToClient now)++        (Specified network, Specified channel) ->+          case preview (clientConnection network) st of+            Nothing -> Left ["Unknown network"]+            Just cs -> Right (sendToChannel cs channel)++        (_ , Specified channel) ->+          case currentNetworkState of+            Nothing -> Left ["No current network"]+            Just cs -> Right (sendToChannel cs channel)++        (Specified network, _) ->+          case preview (clientConnection network) st of+            Nothing -> Left ["Unknown network"]+            Just cs -> Right (sendToNetwork now cs)++        (_, Current) ->+          case currentNetworkState of+            Nothing -> Left ["No current network"]+            Just cs ->+              case view clientFocus st of+                ChannelFocus _ channel -> Right (sendToChannel cs (idText channel))+                _                      -> Left ["No current channel"]++        (Current, _) ->+          case currentNetworkState of+            Nothing -> Left ["No current network"]+            Just cs -> Right (sendToNetwork now cs)++    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 "" ("Bad raw message: " <> msg) st1+             Just raw ->+               do sendMsg cs raw+                  return st1) st msgs++    sendToChannel cs channel msgs =+      commandSuccess =<<+      foldM (\st1 msg ->+        do sendMsg cs (ircPrivmsg 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' (flip (recordError now "")) st (map Text.pack es)++recordSuccess :: ZonedTime -> ClientState -> Text -> ClientState+recordSuccess now ste m =+  recordNetworkMessage ClientMessage+    { _msgTime    = now+    , _msgBody    = NormalBody m+    , _msgNetwork = ""+    } ste+++cmdUrl :: ClientCommand (Maybe Int)+cmdUrl st arg =+  case view (clientConfig . configUrlOpener) st of+    Nothing     -> commandFailureMsg "url-opener not configured" st+    Just opener -> doUrlOpen opener (maybe 0 (subtract 1) arg)+  where+    focus = view clientFocus st++    urls = toListOf ( clientWindows . ix focus . winMessages . each . wlText+                    . folding urlMatches) st++    doUrlOpen opener n =+      case preview (ix n) urls of+        Just url -> openUrl opener (Text.unpack url) st+        Nothing  -> commandFailureMsg "bad url number" st++openUrl :: FilePath -> String -> ClientState -> IO CommandResult+openUrl opener url st =+  do res <- try (callProcess opener [url])+     case res of+       Left e  -> commandFailureMsg (Text.pack (displayException (e :: IOError))) st+       Right{} -> commandSuccess st
+ src/Client/Commands/Channel.hs view
@@ -0,0 +1,326 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.Channel+Description : Channel management command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Channel (channelCommands) where++import           Client.Commands.Arguments.Spec+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.Commands.WordCompletion+import           Client.State+import           Client.State.Channel+import           Client.State.Focus+import           Client.State.Network+import           Client.UserHost+import           Control.Applicative+import           Control.Lens+import           Control.Monad+import           Data.Foldable (traverse_)+import           Data.List.Split (chunksOf)+import           Data.Maybe (fromMaybe)+import           Data.Text (Text)+import qualified Client.State.EditBox as Edit+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Text as Text+import           Irc.Commands+import           Irc.Modes+import           Irc.UserInfo+import           Irc.Identifier+import           LensUtils (setStrict)++channelCommands :: CommandSection+channelCommands = CommandSection "IRC channel management"++  [ Command+      (pure "mode")+      (fromMaybe [] <$> optionalArg (extensionArg "[modes]" modeParamArgs))+      "Sets IRC modes.\n\+      \\n\+      \Examples:\n\+      \Setting a ban:           /mode +b *!*@hostname\n\+      \Removing a quiet:        /mode -q *!*@hostname\n\+      \Voicing two users:       /mode +vv user1 user2\n\+      \Demoting an op to voice: /mode +v-o user1 user1\n\+      \\n\+      \When executed in a network window, mode changes are applied to your user.\n\+      \When executed in a channel window, mode changes are applied to the channel.\n\+      \\n\+      \This command has parameter sensitive tab-completion.\n\+      \\n\+      \See also: /masks /channelinfo\n"+    $ NetworkCommand cmdMode tabMode++  , Command+      (pure "masks")+      (simpleToken "mode")+      "Show mask lists for current channel.\n\+      \\n\+      \Common \^Bmode\^B values:\n\+      \\^Bb\^B: bans\n\+      \\^Bq\^B: quiets\n\+      \\^BI\^B: invite exemptions (op view only)\n\+      \\^Be\^B: ban exemption (op view only)s\n\+      \\n\+      \To populate the mask lists for the first time use: /mode \^Bmode\^B\n\+      \\n\+      \See also: /mode\n"+    $ ChannelCommand cmdMasks noChannelTab++  , Command+      (pure "invite")+      (simpleToken "nick")+      "Invite a user to the current channel.\n"+    $ ChannelCommand cmdInvite simpleChannelTab++  , Command+      (pure "topic")+      (remainingArg "message")+      "Set the topic on the current channel.\n\+      \\n\+      \Tab-completion with no \^Bmessage\^B specified will load the current topic for editing.\n"+    $ ChannelCommand cmdTopic tabTopic++  , Command+      (pure "kick")+      (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))+      "Kick a user from the current channel.\n\+      \\n\+      \See also: /kickban /remove\n"+    $ ChannelCommand cmdKick simpleChannelTab++  , Command+      (pure "kickban")+      (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))+      "Ban and kick a user from the current channel.\n\+      \\n\+      \Users are banned by hostname match.\n\+      \See also: /kick /remove\n"+    $ ChannelCommand cmdKickBan simpleChannelTab++  , Command+      (pure "remove")+      (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))+      "Remove a user from the current channel.\n\+      \\n\+      \Remove works like /kick except it results in a PART.\n\+      \See also: /kick /kickban\n"+    $ ChannelCommand cmdRemove simpleChannelTab++  ]++cmdRemove :: ChannelCommand (String, String)+cmdRemove channelId cs st (who,reason) =+  do let msg = Text.pack reason+         cmd = ircRemove channelId (Text.pack who) msg+     cs' <- sendModeration channelId [cmd] cs+     commandSuccessUpdateCS cs' st++cmdKick :: ChannelCommand (String, String)+cmdKick channelId cs st (who,reason) =+  do let msg = Text.pack reason+         cmd = ircKick channelId (Text.pack who) msg+     cs' <- sendModeration channelId [cmd] cs+     commandSuccessUpdateCS cs' st+++cmdKickBan :: ChannelCommand (String, String)+cmdKickBan channelId cs st (who,reason) =+  do let msg = Text.pack reason++         whoTxt     = Text.pack who++         mask = renderUserInfo (computeBanUserInfo (mkId whoTxt) cs)+         cmds = [ ircMode channelId ["b", mask]+                , ircKick channelId whoTxt msg+                ]+     cs' <- sendModeration channelId cmds cs+     commandSuccessUpdateCS cs' st++cmdInvite :: ChannelCommand String+cmdInvite channelId cs st nick =+  do let freeTarget = has (csChannels . ix channelId . chanModes . ix 'g') cs+         cmd = ircInvite (Text.pack nick) channelId+     cs' <- if freeTarget+              then cs <$ sendMsg cs cmd+              else sendModeration channelId [cmd] cs+     commandSuccessUpdateCS cs' st++commandSuccessUpdateCS :: NetworkState -> ClientState -> IO CommandResult+commandSuccessUpdateCS cs st =+  do let network = view csNetwork cs+     commandSuccess+       $ setStrict (clientConnection network) cs st++cmdMasks :: ChannelCommand String+cmdMasks channel cs st rest =+  case rest of+    [mode] | mode `elem` view (csModeTypes . modesLists) cs ->++        do let connecting = has (csPingStatus . _PingConnecting) cs+               listLoaded = has (csChannels . ix channel . chanLists . ix mode) cs+           unless (connecting || listLoaded)+             (sendMsg cs (ircMode channel [Text.singleton mode]))++           commandSuccess (changeSubfocus (FocusMasks mode) st)++    _ -> commandFailureMsg "unknown mask mode" st++computeBanUserInfo :: Identifier -> NetworkState    -> UserInfo+computeBanUserInfo who cs =+  case view (csUser who) cs of+    Nothing                     -> UserInfo who "*" "*"+    Just (UserAndHost _ host _) -> UserInfo "*" "*" host++cmdTopic :: ChannelCommand String+cmdTopic channelId cs st rest =+  do sendTopic channelId (Text.pack rest) cs+     commandSuccess st++tabTopic ::+  Bool {- ^ reversed -} ->+  ChannelCommand String+tabTopic _ channelId cs st rest++  | all (==' ') rest+  , Just topic <- preview (csChannels . ix channelId . chanTopic) cs =+     do let textBox = set Edit.line (Edit.endLine $ "/topic " ++ Text.unpack topic)+        commandSuccess (over clientTextBox textBox st)++  | otherwise = commandFailure st++cmdMode :: NetworkCommand [String]+cmdMode cs st xs = modeCommand (Text.pack <$> xs) cs st++modeCommand ::+  [Text] {- mode parameters -} ->+  NetworkState                 ->+  ClientState                  ->+  IO CommandResult+modeCommand modes cs st =+  case view clientFocus st of++    NetworkFocus _ ->+      do sendMsg cs (ircMode (view csNick cs) modes)+         commandSuccess st++    ChannelFocus _ chan ->+      case modes of+        [] -> success False [[]]+        flags:params ->+          case splitModes (view csModeTypes cs) flags params of+            Nothing -> commandFailureMsg "failed to parse modes" st+            Just parsedModes ->+              success needOp (unsplitModes <$> chunksOf (view csModeCount cs) parsedModes')+              where+                parsedModes'+                  | useChanServ chan cs = filter (not . isOpMe) parsedModes+                  | otherwise           = parsedModes++                needOp = not (all isPublicChannelMode parsedModes)+      where+        isOpMe (True, 'o', param) = mkId param == view csNick cs+        isOpMe _                  = False++        success needOp argss =+          do let cmds = ircMode chan <$> argss+             cs' <- if needOp+                      then sendModeration chan cmds cs+                      else cs <$ traverse_ (sendMsg cs) cmds+             commandSuccessUpdateCS cs' st++    _ -> commandFailure st++tabMode :: Bool -> NetworkCommand String+tabMode isReversed cs st rest =+  case view clientFocus st of++    ChannelFocus _ channel+      | flags:params     <- Text.words (Text.pack rest)+      , Just parsedModes <- splitModes (view csModeTypes cs) flags params+      , let parsedModesWithParams =+              [ (pol,mode) | (pol,mode,arg) <- parsedModes, not (Text.null arg) ]+      , (pol,mode):_      <- drop (paramIndex-3) parsedModesWithParams+      , let (hint, completions) = computeModeCompletion pol mode channel cs st+      -> simpleTabCompletion plainWordCompleteMode hint completions isReversed st++    _ -> commandFailure st++  where+    paramIndex = length $ words $ uncurry take $ clientLine st++modeParamArgs :: ClientState -> String -> Maybe (Args ClientState [String])+modeParamArgs st str =+  case view clientFocus st of+    Unfocused      -> Nothing+    NetworkFocus _ -> Just (pure [str])+    ChannelFocus net _ ->++         -- determine current mode types+      do cs <- preview (clientConnection net) st+         let types = view csModeTypes cs++         -- parse the list of modes being set+         flags <- splitModes types (Text.pack str) []++         -- generate the argument specification+         let (req,opt) = foldr (countFlags types) ([],[]) flags+         return ((str:) <$> tokenList req (map (++"?") opt))++-- | This function computes the list of required and optional parameters+-- corresponding to the flags that have been entered.+countFlags ::+  ModeTypes           {- ^ network's mode behaviors              -} ->+  (Bool, Char, Text)  {- ^ polarity mode-letter unused-parameter -} ->+  ([String],[String]) {- ^ required-names optional-names         -} ->+  ([String],[String]) {- ^ required-names optional-names         -}+countFlags types (pol, flag, _)+  |        flag `elem` view modesLists       types = addOpt+  | pol && flag `elem` view modesSetArg      types = addReq+  |        flag `elem` view modesAlwaysArg   types = addReq+  | elemOf (modesPrefixModes . folded . _1) flag types = addReq+  | otherwise                                      = id+  where+    addReq (req,opt) = ((flag:" param"):req,opt)+    addOpt ([] ,opt) = ([], (flag:" param"):opt)+    addOpt (req,opt) = ((flag:" param"):req,opt)+++-- | Use the *!*@host masks of users for channel lists when setting list modes+--+-- Use the channel's mask list for removing modes+--+-- Use the nick list otherwise+computeModeCompletion ::+  Bool {- ^ mode polarity -} ->+  Char {- ^ mode          -} ->+  Identifier {- ^ channel -} ->+  NetworkState    ->+  ClientState ->+  ([Identifier],[Identifier]) {- ^ (hint, complete) -}+computeModeCompletion pol mode channel cs st+  | mode `elem` view modesLists modeSettings =+        if pol then ([],usermasks) else ([],masks)+  | otherwise = (activeNicks st, nicks)+  where+    modeSettings = view csModeTypes cs+    nicks = HashMap.keys (view (csChannels . ix channel . chanUsers) cs)++    masks = mkId <$> HashMap.keys (view (csChannels . ix channel . chanLists . ix mode) cs)++    usermasks =+      [ mkId ("*!*@" <> host)+        | nick <- HashMap.keys (view (csChannels . ix channel . chanUsers) cs)+        , UserAndHost _ host _ <- toListOf (csUsers . ix nick) cs+        ]++-- | Predicate for mode commands that can be performed without ops+isPublicChannelMode :: (Bool, Char, Text) -> Bool+isPublicChannelMode (True, 'b', param) = Text.null param -- query ban list+isPublicChannelMode (True, 'q', param) = Text.null param -- query quiet list+isPublicChannelMode _                  = False
+ src/Client/Commands/Chat.hs view
@@ -0,0 +1,522 @@+{-# Language BangPatterns, OverloadedStrings #-}+{-|+Module      : Client.Commands.Chat+Description : Common user IRC commands+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Chat (chatCommands, chatCommand', executeChat, cmdCtcp) where++import           Client.Commands.Arguments.Spec+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.Commands.Window (parseFocus)+import           Client.Message+import           Client.State+import           Client.State.Extensions (clientChatExtension)+import           Client.State.Focus+import           Client.State.Network+import           Control.Applicative+import           Control.Lens+import           Control.Monad (when)+import           Data.Char (toUpper)+import           Data.Foldable+import           Data.List.NonEmpty (NonEmpty((:|)))+import           Data.Text (Text)+import qualified Data.Text as Text+import           Data.Time+import           Irc.Commands+import           Irc.Identifier+import           Irc.Message+import           Irc.RawIrcMsg+import           Irc.UserInfo++chatCommands :: CommandSection+chatCommands = CommandSection "IRC commands"+  ------------------------------------------------------------------------++  [ Command+      ("join" :| ["j"])+      (liftA2 (,) (simpleToken "channels") (optionalArg (simpleToken "[keys]")))+      "\^BParameters:\^B\n\+      \\n\+      \    channels: Comma-separated list of channels\n\+      \    keys:     Comma-separated list of keys\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Join the given channels. When keys are provided, they should\n\+      \    occur in the same order as the channels.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /join #friends\n\+      \    /join #secret thekey\n\+      \    /join #secret1,#secret2 key1,key2\n\+      \\n\+      \\^BSee also:\^B channel, clear, part\n"+    $ NetworkCommand cmdJoin simpleNetworkTab++  , Command+      (pure "part")+      (remainingArg "reason")+      "\^BParameters:\^B\n\+      \\n\+      \    reason: Optional message sent to channel as part reason\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Part from the current channel.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /part\n\+      \    /part It's not me, it's you\n\+      \\n\+      \\^BSee also:\^B clear, join, quit\n"+    $ ChannelCommand cmdPart simpleChannelTab++  , Command+      (pure "msg")+      (liftA2 (,) (simpleToken "target") (remainingArg "message"))+      "\^BParameters:\^B\n\+      \\n\+      \    target:  Comma-separated list of nicknames and channels\n\+      \    message: Formatted message body\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Send a chat message to a user or a channel. On servers\n\+      \    with STATUSMSG support, the channel name can be prefixed\n\+      \    with a sigil to restrict the recipients to those with the\n\+      \    given mode.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /msg buddy I'm sending you a message.\n\+      \    /msg #friends This message is for the whole channel.\n\+      \    /msg him,her I'm chatting with two people.\n\+      \    /msg @#users This message is only for ops!\n\+      \\n\+      \\^BSee also:\^B notice, me, say\n"+    $ NetworkCommand cmdMsg simpleNetworkTab++  , Command+      (pure "me")+      (remainingArg "message")+      "\^BParameters:\^B\n\+      \\n\+      \    message: Body of action message\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Sends an action message to the currently focused channel.\n\+      \    Most clients will render these messages prefixed with\n\+      \    only your nickname as though describing an action.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /me shrugs\n\+      \\n\+      \\^BSee also:\^B notice, msg, say\n"+    $ ChatCommand cmdMe simpleChannelTab++  , Command+      (pure "say")+      (remainingArg "message")+      "\^BParameters:\^B\n\+      \\n\+      \    message: Body of message\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Send a message to the current chat window.  This can be useful\n\+      \    for sending a chat message with a leading '/' to the current\n\+      \    chat window.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /say /help is the right place to start!\n\+      \\n\+      \\^BSee also:\^B notice, me, msg\n"+    $ ChatCommand cmdSay simpleChannelTab++  , Command+      ("query" :| ["q"])+      (liftA2 (,) (simpleToken "target") (remainingArg "message"))+      "\^BParameters:\^B\n\+      \\n\+      \    target: Focus name\n\+      \    message: Optional message\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    This command switches the client focus to the given\n\+      \    target and optionally sends a message to that target.\n\+      \\n\+      \    Channel: \^_#channel\^_\n\+      \    Channel: \^_network\^_:\^_#channel\^_\n\+      \    User:    \^_nick\^_\n\+      \    User:    \^_network\^_:\^_nick\^_\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /q fn:#haskell\n\+      \    /q #haskell\n\+      \    /q lambdabot @messages\n\+      \    /q irc_friend How are you?\n\+      \\n\+      \\^BSee also:\^B msg channel focus\n"+    $ ClientCommand cmdQuery simpleClientTab++  , Command+      (pure "notice")+      (liftA2 (,) (simpleToken "target") (remainingArg "message"))+      "\^BParameters:\^B\n\+      \\n\+      \    target:  Comma-separated list of nicknames and channels\n\+      \    message: Formatted message body\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Send a chat notice to a user or a channel. On servers\n\+      \    with STATUSMSG support, the channel name can be prefixed\n\+      \    with a sigil to restrict the recipients to those with the\n\+      \    given mode. Notice messages were originally intended to be\n\+      \    used by bots. Different clients will render these in different\n\+      \    ways.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /notice buddy I'm sending you a message.\n\+      \    /notice #friends This message is for the whole channel.\n\+      \    /notice him,her I'm chatting with two people.\n\+      \    /notice @#users This message is only for ops!\n\+      \\n\+      \\^BSee also:\^B me, msg, say\n"+    $ NetworkCommand cmdNotice simpleNetworkTab++  , Command+      (pure "ctcp")+      (liftA3 (,,) (simpleToken "target") (simpleToken "command") (remainingArg "arguments"))+      "\^BParameters:\^B\n\+      \\n\+      \    target:    Comma-separated list of nicknames and channels\n\+      \    command:   CTCP command name\n\+      \    arguments: CTCP command arguments\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Client-to-client protocol (CTCP) commands can be used\n\+      \    to query information from another user's client application\n\+      \    directly. Common CTCP commands include: ACTION, PING, VERSION,\n\+      \    USERINFO, CLIENTINFO, and TIME. glirc does not automatically\n\+      \    respond to CTCP commands.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /ctcp myfriend VERSION\n\+      \    /ctcp myfriend CLIENTINFO\n"+    $ NetworkCommand cmdCtcp simpleNetworkTab++  , Command+      (pure "nick")+      (simpleToken "nick")+      "\^BParameters:\^B\n\+      \\n\+      \    nick: New nickname\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Change your nickname on the currently focused server.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /nick guest123\n\+      \    /nick better_nick\n"+    $ NetworkCommand cmdNick simpleNetworkTab++  , Command+      (pure "away")+      (remainingArg "message")+      "\^BParameters:\^B\n\+      \\n\+      \    message: Optional away message\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Change your nickname on the currently focused server.\n\+      \    Omit the message parameter to clear your away status.\n\+      \    The away message is only used by the server to update\n\+      \    status in /whois and to provide automated responses.\n\+      \    It is not used by this client directly.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /away\n\+      \    /away Out getting some sun\n"+    $ NetworkCommand cmdAway simpleNetworkTab++  , Command+      ("users" :| ["names"])+      (pure ())+      "\^BDescription:\^B\n\+      \\n\+      \    Show the user list for the current channel.\n\+      \    Detailed view (default key F2) shows full hostmask.\n\+      \    Hostmasks can be populated with /who #channel.\n\+      \    Press ESC to exit the userlist.\n\+      \\n\+      \\^BSee also:\^B channelinfo, masks\n"+    $ ChannelCommand cmdUsers  noChannelTab++  , Command+      (pure "channelinfo")+      (pure ())+      "\^BDescription:\^B\n\+      \\n\+      \    Show information about the current channel.\n\+      \    Press ESC to exit the channel info window.\n\+      \\n\+      \    Information includes topic, creation time, URL, and modes.\n\+      \\n\+      \\^BSee also:\^B masks, mode, topic, users\n"+    $ ChannelCommand cmdChannelInfo noChannelTab++  , Command+      (pure "knock")+      (liftA2 (,) (simpleToken "channel") (remainingArg "message"))+      "Request entry to an invite-only channel.\n"+    $ NetworkCommand cmdKnock simpleNetworkTab++  , Command+      (pure "quote")+      (remainingArg "raw IRC command")+      "Send a raw IRC command.\n"+    $ NetworkCommand cmdQuote simpleNetworkTab++  , Command+      (pure "monitor")+      (extensionArg "[+-CLS]" monitorArgs)+      "\^BSubcommands:\^B\n\+      \\n\+      \    /monitor + target[,target2]* - Add nicknames to monitor list\n\+      \    /monitor - target[,target2]* - Remove nicknames to monitor list\n\+      \    /monitor C                   - Clear monitor list\n\+      \    /monitor L                   - Show monitor list\n\+      \    /monitor S                   - Show status of nicknames on monitor list\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Monitor is a protocol for getting server-side notifications\n\+      \    when users become online/offline.\n"+    $ NetworkCommand cmdMonitor simpleNetworkTab++    ]++monitorArgs :: ClientState -> String -> Maybe (Args ClientState [String])+monitorArgs _ str =+  case toUpper <$> str of+    "+" -> Just (wrap '+' (simpleToken "target[,target2]*"))+    "-" -> Just (wrap '-' (simpleToken "target[,target2]*"))+    "C" -> Just (pure ["C"])+    "L" -> Just (pure ["L"])+    "S" -> Just (pure ["S"])+    _   -> Nothing+  where+    wrap c = fmap (\s -> [[c], s])++cmdMonitor :: NetworkCommand [String]+cmdMonitor cs st args =+  do sendMsg cs (ircMonitor (fmap Text.pack args))+     commandSuccess st++cmdUsers :: ChannelCommand ()+cmdUsers _ _ st _ = commandSuccess (changeSubfocus FocusUsers st)++cmdChannelInfo :: ChannelCommand ()+cmdChannelInfo _ _ st _ = commandSuccess (changeSubfocus FocusInfo st)++cmdKnock :: NetworkCommand (String, String)+cmdKnock cs st (chan,message) =+  do sendMsg cs (ircKnock (Text.pack chan) (Text.pack message))+     commandSuccess st++cmdJoin :: NetworkCommand (String, Maybe String)+cmdJoin cs st (channels, mbKeys) =+  do let network = view csNetwork cs+     let channelId = mkId (Text.pack (takeWhile (/=',') channels))+     sendMsg cs (ircJoin (Text.pack channels) (Text.pack <$> mbKeys))+     commandSuccess+        $ changeFocus (ChannelFocus network channelId) st++-- | @/query@ command. Takes a channel or nickname and switches+-- focus to that target on the current network.+cmdQuery :: ClientCommand (String, String)+cmdQuery st (target, msg) =+  case parseFocus (views clientFocus focusNetwork st) target of+    Just (ChannelFocus net tgt)++      | null msg -> commandSuccess st'++      | Just cs <- preview (clientConnection net) st ->+           do let tgtTxt = idText tgt+                  msgTxt = Text.pack msg+              sendMsg cs (ircPrivmsg tgtTxt msgTxt)+              chatCommand+                 (\src tgt1 -> Privmsg src tgt1 msgTxt)+                 tgtTxt cs st'+      where+       firstTgt = mkId (Text.takeWhile (','/=) (idText tgt))+       st' = changeFocus (ChannelFocus net firstTgt) st++    _ -> commandFailureMsg "Bad target" st++-- | Implementation of @/ctcp@+cmdCtcp :: NetworkCommand (String, String, String)+cmdCtcp cs st (target, cmd, args) =+  do let cmdTxt = Text.toUpper (Text.pack cmd)+         argTxt = Text.pack args+         tgtTxt = Text.pack target++     sendMsg cs (ircPrivmsg tgtTxt ("\^A" <> cmdTxt <> " " <> argTxt <> "\^A"))+     chatCommand+        (\src tgt -> Ctcp src tgt cmdTxt argTxt)+        tgtTxt cs st++-- | Implementation of @/notice@+cmdNotice :: NetworkCommand (String, String)+cmdNotice cs st (target, rest)+  | null rest = commandFailureMsg "empty message" st+  | otherwise =+      do let restTxt = Text.pack rest+             tgtTxt = Text.pack target++         sendMsg cs (ircNotice tgtTxt restTxt)+         chatCommand+            (\src tgt -> Notice src tgt restTxt)+            tgtTxt cs st++-- | Implementation of @/msg@+cmdMsg :: NetworkCommand (String, String)+cmdMsg cs st (target, rest)+  | null rest = commandFailureMsg "empty message" st+  | otherwise =+      do let restTxt = Text.pack rest+             tgtTxt = Text.pack target++         sendMsg cs (ircPrivmsg tgtTxt restTxt)+         chatCommand+            (\src tgt -> Privmsg src tgt restTxt)+            tgtTxt cs st++-- | Common logic for @/msg@ and @/notice@+chatCommand ::+  (UserInfo -> Identifier -> IrcMsg) ->+  Text {- ^ target  -} ->+  NetworkState         ->+  ClientState          ->+  IO CommandResult+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+                          , _msgNetwork = network+                          , _msgBody = IrcBody (con myNick targetId)+                          })+                       | targetId <- targetIds ]++     return $! foldl' (\acc (targetId, entry) ->+                        recordChannelMessage network targetId entry acc)+                      st+                      entries++-- | Implementation of @/quote@. Parses arguments as a raw IRC command and+-- sends to the current network.+cmdQuote :: NetworkCommand String+cmdQuote cs st rest =+  case parseRawIrcMsg (Text.pack (dropWhile (' '==) rest)) of+    Nothing  -> commandFailureMsg "failed to parse raw IRC command" st+    Just raw ->+      do sendMsg cs raw+         commandSuccess st++cmdAway :: NetworkCommand String+cmdAway cs st rest =+  do sendMsg cs (ircAway (Text.pack rest))+     commandSuccess st++cmdNick :: NetworkCommand String+cmdNick cs st nick =+  do sendMsg cs (ircNick (Text.pack nick))+     commandSuccess st++cmdPart :: ChannelCommand String+cmdPart channelId cs 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 String+cmdSay _ _ st rest = executeChat rest st++-- | Implementation of @/me@+cmdMe :: ChannelCommand String+cmdMe channelId cs 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+                    , _msgBody = IrcBody (Ctcp myNick channelId "ACTION" (Text.pack rest))+                    }+     sendMsg cs (ircPrivmsg (idText channelId) actionTxt)+     commandSuccess+       $! recordChannelMessage network channelId entry st++-- | Treat the current text input as a chat message and send it.+executeChat ::+  String           {- ^ chat message   -} ->+  ClientState      {- ^ client state   -} ->+  IO CommandResult {- ^ command result -}+executeChat msg st =+  case view clientFocus st of+    ChannelFocus network channel+      | Just !cs <- preview (clientConnection network) st ->+          do now <- getZonedTime+             let msgTxt = Text.pack $ takeWhile (/='\n') msg+                 tgtTxt = idText channel++             (st1,allow) <- clientChatExtension network tgtTxt msgTxt st++             when allow (sendMsg cs (ircPrivmsg tgtTxt msgTxt))++             let myNick = UserInfo (view csNick cs) "" ""+                 entry = ClientMessage+                   { _msgTime    = now+                   , _msgNetwork = network+                   , _msgBody    = IrcBody (Privmsg myNick channel msgTxt) }+             commandSuccess $! recordChannelMessage network channel entry st1++    _ -> commandFailureMsg "cannot send chat messages to this window" st
+ src/Client/Commands/Connection.hs view
@@ -0,0 +1,110 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.Connection+Description : Connection command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Connection (connectionCommands) where++import           Client.Commands.Arguments.Spec+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.Commands.WordCompletion+import           Client.Configuration+import           Client.State+import           Client.State.Focus+import           Client.State.Network+import           Control.Lens+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Text as Text+import           Irc.Commands (ircQuit)++connectionCommands :: CommandSection+connectionCommands = CommandSection "Connection commands"++  [ Command+      (pure "connect")+      (simpleToken "network")+      "Connect to \^Bnetwork\^B by name.\n\+      \\n\+      \If no name is configured the hostname is the 'name'.\n"+    $ ClientCommand cmdConnect tabConnect++  , Command+      (pure "reconnect")+      (pure ())+      "Reconnect to the current network.\n"+    $ ClientCommand cmdReconnect noClientTab++  , Command+      (pure "disconnect")+      (pure ())+      "Immediately terminate the current network connection.\n\+      \\n\+      \See also: /quit /exit\n"+    $ NetworkCommand cmdDisconnect noNetworkTab++  , Command+      (pure "quit")+      (remainingArg "reason")+      "Gracefully disconnect the current network connection.\n\+      \\n\+      \\^Breason\^B: optional quit reason\n\+      \\n\+      \See also: /disconnect /exit\n"+    $ NetworkCommand cmdQuit simpleNetworkTab++  , Command+      (pure "cert")+      (pure ())+      "Show the TLS certificate for the current connection.\n"+    $ NetworkCommand cmdCert noNetworkTab++  ]++cmdConnect :: ClientCommand String+cmdConnect st networkStr =+  do -- abort any existing connection before connecting+     let network = Text.pack networkStr+     st' <- addConnection 0 Nothing Nothing network =<< abortNetwork network st+     commandSuccess+       $ changeFocus (NetworkFocus network) st'++cmdQuit :: NetworkCommand String+cmdQuit cs st rest =+  do let msg = Text.pack rest+     sendMsg cs (ircQuit msg)+     commandSuccess st++cmdDisconnect :: NetworkCommand ()+cmdDisconnect cs st _ =+  do st' <- abortNetwork (view csNetwork cs) st+     commandSuccess st'++-- | Reconnect to the currently focused network. It's possible+-- that we're not currently connected to a network, so+-- this is implemented as a client command.+cmdReconnect :: ClientCommand ()+cmdReconnect st _+  | Just network <- views clientFocus focusNetwork st =++      do let tm = preview (clientConnection network . csLastReceived . folded) st+         st' <- addConnection 0 tm Nothing network =<< abortNetwork network st+         commandSuccess+           $ changeFocus (NetworkFocus network) st'++  | otherwise = commandFailureMsg "command requires focused network" st++-- | @/connect@ tab completes known server names+tabConnect :: Bool -> ClientCommand String+tabConnect isReversed st _ =+  simpleTabCompletion plainWordCompleteMode [] networks isReversed st+  where+    networks = views clientConnections              HashMap.keys st+            ++ views (clientConfig . configServers) HashMap.keys st++cmdCert :: NetworkCommand ()+cmdCert _ st _ = commandSuccess (changeSubfocus FocusCert st)
+ src/Client/Commands/DCC.hs view
@@ -0,0 +1,111 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.DCC+Description : DCC command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.DCC (dccCommands) where++import           Client.Commands.Arguments.Spec+import           Client.Commands.Chat (cmdCtcp)+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.Configuration+import           Client.State+import           Client.State.DCC+import           Client.State.Focus+import           Control.Applicative+import qualified Control.Concurrent.Async as Async+import           Control.Lens+import           System.Directory (doesDirectoryExist)+import           System.FilePath ((</>))++dccCommands :: CommandSection+dccCommands = CommandSection "DCC"++  [ Command+      (pure "dcc")+      (liftA2 (,) (optionalArg (simpleToken "[accept|cancel|clear|resume]"))+                               optionalNumberArg)+      "Main access to the DCC subsystem with the following subcommands:\n\n\+       \  /dcc           : Access to a list of pending offer and downloads\n\+       \  /dcc accept #n : start downloading the #n pending offer\n\+       \  /dcc resume #n : same as accept but appending to the file on `download-dir`\n\+       \  /dcc clear  #n : remove the #n offer from the list \n\+       \  /dcc cancel #n : cancel the download #n \n\n"+    $ ClientCommand cmdDcc noClientTab+  ]++-- | Implementation of @/dcc [(cancel|accept|resume)] [key]+cmdDcc :: ClientCommand (Maybe String, Maybe Int)+cmdDcc st (Nothing, Nothing) = commandSuccess (changeSubfocus FocusDCC st)+cmdDcc st (Just cmd, Just key) = checkAndBranch st cmd key+cmdDcc st _ = commandFailureMsg "Invalid syntax" st++checkAndBranch :: ClientState -> String -> Int -> IO CommandResult+checkAndBranch st cmd key+  | isCancel, NotExist <- curKeyStatus+      = commandFailureMsg "No such DCC entry" st+  | isCancel, curKeyStatus == Pending+      = commandSuccess+      $ set (clientDCC . dsOffers . ix key . dccStatus) UserKilled st+  | isCancel, curKeyStatus /= Downloading+      = commandFailureMsg "Transfer already stopped" st+  | isCancel = Async.cancel threadId *> commandSuccess st++  | isClear, NotExist <- curKeyStatus+      = commandFailureMsg "No such DCC entry" st+  | isClear, curKeyStatus `elem` [Downloading, Pending]+      = commandFailureMsg "Cancel the download first" st+  | isClear = commandSuccess+            $ set (clientDCC . dsOffers    . at key) Nothing+            $ set (clientDCC . dsTransfers . at key) Nothing st++  | isAcceptOrResume, curKeyStatus `elem` alreadyAcceptedSet+      = commandFailureMsg "Offer already accepted" st+  | isAcceptOrResume, NotExist <- curKeyStatus+      = commandFailureMsg "No such DCC entry" st+  | isAcceptOrResume+      = do isDirectory <- doesDirectoryExist downloadPath+           msize       <- getFileOffset downloadPath+           case (isDirectory, msize, cmd, mcs) of+             (True, _, _, _)      -> commandFailureMsg "DCC transfer would overwrite a directory" st+             (_, Nothing, _, _)   -> acceptOffer -- resume from 0 is accept+             (_, _, "accept", _)  -> acceptOffer -- overwrite file+             (_, Just size, "resume", Just cs) -> resumeOffer size cs+             _ -> commandFailureMsg "Unknown case" st++  | otherwise = commandFailureMsg "Invalid syntax" st+  where+    -- General+    isAcceptOrResume = cmd `elem` ["accept", "resume"]+    isCancel         = cmd == "cancel"+    isClear          = cmd == "clear"+    dccState         = view clientDCC st+    curKeyStatus     = statusAtKey key dccState+    alreadyAcceptedSet = [ CorrectlyFinished, UserKilled, LostConnection+                         , Downloading]++    -- For cancel, other cases handled on the guards+    threadId = st ^?! clientDCC . dsTransfers . ix key . dtThread . _Just++    -- Common values for resume or accept+    Just offer   = view (clientDCC . dsOffers . at key) st -- guarded exist+    updChan      = view clientDCCUpdates st+    downloadDir  = view (clientConfig . configDownloadDir) st+    downloadPath = downloadDir </> _dccFileName offer+    mcs          = preview (clientConnection (_dccNetwork offer)) st++    -- Actual workhorses for the commands+    acceptOffer =+        do newDCCState <- supervisedDownload downloadDir key updChan dccState+           commandSuccess (set clientDCC newDCCState st)++    resumeOffer size cs =+        let newOffer = offer { _dccOffset = size }+            (target, txt) = resumeMsg size newOffer+            st' = set (clientDCC . dsOffers . at key) (Just newOffer) st+        in cmdCtcp cs st' (target, "DCC", txt)
+ src/Client/Commands/Operator.hs view
@@ -0,0 +1,127 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.Operator+Description : Operator command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Operator (operatorCommands) where++import           Control.Applicative+import           Client.Commands.Arguments.Spec+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.State.Network (sendMsg)+import qualified Data.Text as Text+import           Irc.Commands++operatorCommands :: CommandSection+operatorCommands = CommandSection "Network operator commands"++  [ Command+      (pure "oper")+      (liftA2 (,) (simpleToken "user") (simpleToken "password"))+      "Authenticate as a server operator.\n"+    $ NetworkCommand cmdOper noNetworkTab++  , Command+      (pure "kill")+      (liftA2 (,) (simpleToken "client") (remainingArg "reason"))+      "Kill a client connection to the server.\n"+    $ NetworkCommand cmdKill simpleNetworkTab++  , Command+      (pure "kline")+      (liftA3 (,,) (simpleToken "minutes") (simpleToken "user@host") (remainingArg "reason"))+      "Ban a client from the server.\n"+    $ NetworkCommand cmdKline simpleNetworkTab++  , Command+      (pure "unkline")+      (simpleToken "user@host")+      "Unban a client from the server.\n"+    $ NetworkCommand cmdUnkline simpleNetworkTab++  , Command+      (pure "testline")+      (simpleToken "[[nick!]user@]host")+      "Check matching I/K/D lines for a [[nick!]user@]host\n"+    $ NetworkCommand cmdTestline simpleNetworkTab++  , Command+      (pure "testmask")+      (liftA2 (,) (simpleToken "[nick!]user@host") (remainingArg "[gecos]"))+      "Test how many local and global clients match a mask.\n"+    $ NetworkCommand cmdTestmask simpleNetworkTab++  , Command+      (pure "masktrace")+      (liftA2 (,) (simpleToken "[nick!]user@host") (remainingArg "[gecos]"))+      "Outputs a list of local users matching the given masks.\n"+    $ NetworkCommand cmdMasktrace simpleNetworkTab++  , Command+      (pure "trace")+      (optionalArg (liftA2 (,) (simpleToken "[server | nick]") (optionalArg (simpleToken "[server]"))))+      "Outputs a list users on a server.\n"+    $ NetworkCommand cmdTrace simpleNetworkTab++  , Command+      (pure "map")+      (pure ())+      "Display network map.\n"+    $ NetworkCommand cmdMap simpleNetworkTab++  ]++cmdKill :: NetworkCommand (String, String)+cmdKill cs st (client,rest) =+  do sendMsg cs (ircKill (Text.pack client) (Text.pack rest))+     commandSuccess st++cmdKline :: NetworkCommand (String, String, String)+cmdKline cs st (minutes, mask, reason) =+  do sendMsg cs (ircKline (Text.pack minutes) (Text.pack mask) (Text.pack reason))+     commandSuccess st++cmdUnkline :: NetworkCommand String+cmdUnkline cs st mask =+  do sendMsg cs (ircUnkline (Text.pack mask))+     commandSuccess st++cmdTestline :: NetworkCommand String+cmdTestline cs st mask =+  do sendMsg cs (ircTestline (Text.pack mask))+     commandSuccess st++cmdTestmask :: NetworkCommand (String, String)+cmdTestmask cs st (mask, gecos) =+  do sendMsg cs (ircTestmask (Text.pack mask) (Text.pack gecos))+     commandSuccess st++cmdMasktrace :: NetworkCommand (String, String)+cmdMasktrace cs st (mask, gecos) =+  do sendMsg cs (ircMasktrace (Text.pack mask) (Text.pack gecos))+     commandSuccess st++cmdTrace :: NetworkCommand (Maybe (String, Maybe String))+cmdTrace cs st args =+  do let argsList =+           case args of+            Nothing           -> []+            Just (x, Nothing) -> [x]+            Just (x, Just y)  -> [x, y]+     sendMsg cs (ircTrace (map Text.pack argsList))+     commandSuccess st++cmdMap :: NetworkCommand ()+cmdMap cs st _ =+  do sendMsg cs ircMap+     commandSuccess st++cmdOper :: NetworkCommand (String, String)+cmdOper cs st (user, pass) =+  do sendMsg cs (ircOper (Text.pack user) (Text.pack pass))+     commandSuccess st
+ src/Client/Commands/Queries.hs view
@@ -0,0 +1,195 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.Queries+Description : Query command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Queries (queryCommands) where++import           Control.Applicative+import           Client.Commands.Arguments.Spec+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.State.Network (sendMsg)+import qualified Data.Text as Text+import           Irc.Commands++queryCommands :: CommandSection+queryCommands = CommandSection "Queries"++  [ Command+      (pure "who")+      (remainingArg "arguments")+      "Send WHO query to server with given arguments.\n"+    $ NetworkCommand cmdWho simpleNetworkTab++  , Command+      (pure "whois")+      (remainingArg "arguments")+      "Send WHOIS query to server with given arguments.\n"+    $ NetworkCommand cmdWhois simpleNetworkTab++  , Command+      (pure "whowas")+      (remainingArg "arguments")+      "Send WHOWAS query to server with given arguments.\n"+    $ NetworkCommand cmdWhowas simpleNetworkTab++  , Command+      (pure "ison")+      (remainingArg "arguments")+      "Send ISON query to server with given arguments.\n"+    $ NetworkCommand cmdIson   simpleNetworkTab++  , Command+      (pure "userhost")+      (remainingArg "arguments")+      "Send USERHOST query to server with given arguments.\n"+    $ NetworkCommand cmdUserhost simpleNetworkTab++  , Command+      (pure "time")+      (optionalArg (simpleToken "[servername]"))+      "Send TIME query to server with given arguments.\n"+    $ NetworkCommand cmdTime simpleNetworkTab++  , Command+      (pure "stats")+      (remainingArg "arguments")+      "Send STATS query to server with given arguments.\n"+    $ NetworkCommand cmdStats simpleNetworkTab++  , Command+      (pure "lusers")+      (optionalArg (liftA2 (,) (simpleToken "mask") (optionalArg (simpleToken "[servername]"))))+      "Send LUSERS query to server with given arguments.\n"+    $ NetworkCommand cmdLusers simpleNetworkTab++  , Command+      (pure "motd") (optionalArg (simpleToken "[servername]"))+      "Send MOTD query to server.\n"+    $ NetworkCommand cmdMotd simpleNetworkTab++  , Command+      (pure "admin") (optionalArg (simpleToken "[servername]"))+      "Send ADMIN query to server.\n"+    $ NetworkCommand cmdAdmin simpleNetworkTab++  , Command+      (pure "rules") (optionalArg (simpleToken "[servername]"))+      "Send RULES query to server.\n"+    $ NetworkCommand cmdRules simpleNetworkTab++  , Command+      (pure "info") (pure ())+      "Send INFO query to server.\n"+    $ NetworkCommand cmdInfo noNetworkTab++  , Command+      (pure "list") (remainingArg "arguments")+      "Send LIST query to server.\n"+    $ NetworkCommand cmdList simpleNetworkTab++  , Command+      (pure "links")+      (remainingArg "arguments")+      "Send LINKS query to server with given arguments.\n"+    $ NetworkCommand cmdLinks simpleNetworkTab++  , Command+      (pure "version") (optionalArg (simpleToken "[servername]"))+      "Send VERSION query to server.\n"+    $ NetworkCommand cmdVersion simpleNetworkTab++  ]++cmdInfo :: NetworkCommand ()+cmdInfo cs st _ =+  do sendMsg cs ircInfo+     commandSuccess st++cmdVersion :: NetworkCommand (Maybe String)+cmdVersion cs st mbservername =+  do sendMsg cs $ ircVersion $ case mbservername of+                                Just s  -> Text.pack s+                                Nothing -> ""+     commandSuccess st++cmdList :: NetworkCommand String+cmdList cs st rest =+  do sendMsg cs (ircList (Text.pack <$> words rest))+     commandSuccess st++cmdLusers :: NetworkCommand (Maybe (String, Maybe String))+cmdLusers cs st arg =+  do sendMsg cs $ ircLusers $ fmap Text.pack $+       case arg of+         Nothing           -> []+         Just (x, Nothing) -> [x]+         Just (x, Just y)  -> [x,y]+     commandSuccess st++cmdMotd :: NetworkCommand (Maybe String)+cmdMotd cs st mbservername =+  do sendMsg cs $ ircMotd $ case mbservername of+                              Just s  -> Text.pack s+                              Nothing -> ""+     commandSuccess st++cmdAdmin :: NetworkCommand (Maybe String)+cmdAdmin cs st mbservername =+  do sendMsg cs $ ircAdmin $ case mbservername of+                              Just s  -> Text.pack s+                              Nothing -> ""+     commandSuccess st++cmdRules :: NetworkCommand (Maybe String)+cmdRules cs st mbservername =+  do sendMsg cs $ ircRules $+       case mbservername of+         Just s  -> Text.pack s+         Nothing -> ""+     commandSuccess st++cmdStats :: NetworkCommand String+cmdStats cs st rest =+  do sendMsg cs (ircStats (Text.pack <$> words rest))+     commandSuccess st++cmdLinks :: NetworkCommand String+cmdLinks cs st rest =+  do sendMsg cs (ircLinks (Text.pack <$> words rest))+     commandSuccess st++cmdTime :: NetworkCommand (Maybe String)+cmdTime cs st arg =+  do sendMsg cs (ircTime (maybe "" Text.pack arg))+     commandSuccess st++cmdWhois :: NetworkCommand String+cmdWhois cs st rest =+  do sendMsg cs (ircWhois (Text.pack <$> words rest))+     commandSuccess st++cmdWho :: NetworkCommand String+cmdWho cs st rest =+  do sendMsg cs (ircWho (Text.pack <$> words rest))+     commandSuccess st++cmdWhowas :: NetworkCommand String+cmdWhowas cs st rest =+  do sendMsg cs (ircWhowas (Text.pack <$> words rest))+     commandSuccess st++cmdIson :: NetworkCommand String+cmdIson cs st rest =+  do sendMsg cs (ircIson (Text.pack <$> words rest))+     commandSuccess st++cmdUserhost :: NetworkCommand String+cmdUserhost cs st rest =+  do sendMsg cs (ircUserhost (Text.pack <$> words rest))+     commandSuccess st
+ src/Client/Commands/TabCompletion.hs view
@@ -0,0 +1,107 @@+{-|+Module      : Client.Commands.TabCompletion+Description : Common tab-completion logic+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.TabCompletion where++import           Client.Commands.Types+import           Client.Commands.WordCompletion+import           Client.Message+import           Client.State+import           Client.State.Focus+import           Client.State.Network+import           Client.State.Window+import           Client.State.Channel+import           Control.Lens+import qualified Data.HashMap.Strict as HashMap+import           Irc.Identifier+import           Irc.UserInfo++-- | Provides no tab completion for client commands+noClientTab :: Bool -> ClientCommand String+noClientTab _ st _ = commandFailure st++-- | Provides no tab completion for network commands+noNetworkTab :: Bool -> NetworkCommand String+noNetworkTab _ _ st _ = commandFailure st++-- | Provides no tab completion for channel commands+noChannelTab :: Bool -> ChannelCommand String+noChannelTab _ _ _ st _ = commandFailure st++-- | Provides nickname based tab completion for client commands+simpleClientTab :: Bool -> ClientCommand String+simpleClientTab isReversed st _ =+  nickTabCompletion isReversed st++-- | Provides nickname based tab completion for network commands+simpleNetworkTab :: Bool -> NetworkCommand String+simpleNetworkTab isReversed _ st _ =+  nickTabCompletion isReversed st++-- | Provides nickname based tab completion for channel commands+simpleChannelTab :: Bool -> ChannelCommand String+simpleChannelTab isReversed _ _ st _ =+  nickTabCompletion isReversed st++simpleTabCompletion ::+  Prefix a =>+  WordCompletionMode {- ^ word completion mode -} ->+  [a]                {- ^ hints                -} ->+  [a]                {- ^ all completions      -} ->+  Bool               {- ^ reversed order       -} ->+  ClientState        {- ^ client state         -} ->+  IO CommandResult+simpleTabCompletion mode hints completions isReversed st =+  case traverseOf clientTextBox tryCompletion st of+    Nothing  -> commandFailure st+    Just st' -> commandSuccess st'+  where+    tryCompletion = wordComplete mode isReversed hints completions++-- | Complete the nickname at the current cursor position using the+-- userlist for the currently focused channel (if any)+nickTabCompletion :: Bool {- ^ reversed -} -> ClientState -> IO CommandResult+nickTabCompletion isReversed st =+  simpleTabCompletion mode hint completions isReversed st+  where+    hint          = activeNicks st+    completions   = currentCompletionList st+    mode          = currentNickCompletionMode st++activeNicks ::+  ClientState ->+  [Identifier]+activeNicks st =+  case view clientFocus st of+    focus@(ChannelFocus network channel) ->+      toListOf+        ( clientWindows    . ix focus+        . winMessages      . each+        . wlSummary        . folding summaryActor+        . filtered isActive+        . filtered isNotSelf ) st+      where+        isActive n = HashMap.member n userMap+        self = preview ( clientConnection network . csNick ) st+        isNotSelf n = case self of+                        Nothing -> True+                        Just s -> n /= s+        userMap = view ( clientConnection network+                       . csChannels . ix channel+                       . chanUsers) st++    _ -> []++  where+    -- Returns the 'Identifier' of the nickname responsible for+    -- the window line when that action was significant enough to+    -- be considered a hint for tab completion.+    summaryActor :: IrcSummary -> Maybe Identifier+    summaryActor (ChatSummary who) = Just $! userNick who+    summaryActor _                 = Nothing+
+ src/Client/Commands/Toggles.hs view
@@ -0,0 +1,69 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.Toggles+Description : View modality command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Toggles (togglesCommands) where++import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.Configuration+import           Client.State+import           Control.Lens++togglesCommands :: CommandSection+togglesCommands = CommandSection "View toggles"++  [ Command+      (pure "toggle-detail")+      (pure ())+      "Toggle detailed message view.\n"+    $ ClientCommand cmdToggleDetail noClientTab++  , Command+      (pure "toggle-activity-bar")+      (pure ())+      "Toggle detailed detailed activity information in status bar.\n"+    $ ClientCommand cmdToggleActivityBar noClientTab++  , Command+      (pure "toggle-show-ping")+      (pure ())+      "Toggle visibility of ping round-trip time.\n"+    $ ClientCommand cmdToggleShowPing noClientTab++  , Command+      (pure "toggle-metadata")+      (pure ())+      "Toggle visibility of metadata in chat windows.\n"+    $ ClientCommand cmdToggleMetadata noClientTab++  , Command+      (pure "toggle-layout")+      (pure ())+      "Toggle multi-window layout mode.\n"+    $ ClientCommand cmdToggleLayout noClientTab++  ]++cmdToggleDetail :: ClientCommand ()+cmdToggleDetail st _ = commandSuccess (over clientDetailView not st)++cmdToggleActivityBar :: ClientCommand ()+cmdToggleActivityBar st _ = commandSuccess (over clientActivityBar not st)++cmdToggleShowPing :: ClientCommand ()+cmdToggleShowPing st _ = commandSuccess (over clientShowPing not st)++cmdToggleMetadata :: ClientCommand ()+cmdToggleMetadata st _ = commandSuccess (clientToggleHideMeta st)++cmdToggleLayout :: ClientCommand ()+cmdToggleLayout st _ = commandSuccess (set clientScroll 0 (over clientLayout aux st))+  where+    aux OneColumn = TwoColumn+    aux TwoColumn = OneColumn
+ src/Client/Commands/Types.hs view
@@ -0,0 +1,87 @@+{-# Language ExistentialQuantification #-}+{-|+Module      : Client.Commands.Types+Description : Types used to implement client commands+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Types where++import           Client.State (ClientState, clientErrorMsg)+import           Client.State.Network (NetworkState)+import           Data.Text (Text)+import           Data.List.NonEmpty (NonEmpty)+import           Client.Commands.Arguments.Spec+import           Irc.Identifier (Identifier)+import           Control.Lens++-- | Possible results of running a command+data CommandResult+  -- | Continue running the client, consume input if command was from input+  = CommandSuccess ClientState+  -- | Continue running the client, report an error+  | CommandFailure ClientState+  -- | Client should close+  | CommandQuit ClientState+++-- | Type of commands that always work+type ClientCommand a = ClientState -> a {- ^ arguments -} -> IO CommandResult++-- | Type of commands that require an active network to be focused+type NetworkCommand a = NetworkState {- ^ current network -} -> ClientCommand a++-- | Type of commands that require an active channel to be focused+type ChannelCommand a = Identifier {- ^ focused channel -} -> NetworkCommand a+++-- | Pair of implementations for executing a command and tab completing one.+-- The tab-completion logic is extended with a Bool+-- indicating that tab completion should be reversed+data CommandImpl a+  -- | no requirements+  = ClientCommand  (ClientCommand  a) (Bool -> ClientCommand  String)+  -- | requires an active network+  | NetworkCommand (NetworkCommand a) (Bool -> NetworkCommand String)+  -- | requires an active chat window+  | ChatCommand    (ChannelCommand a) (Bool -> ChannelCommand String)+  -- | requires an active channel window+  | ChannelCommand (ChannelCommand a) (Bool -> ChannelCommand String)+++-- | A command is a list of aliases, an argument specification, implementation,+-- and documentation. The arguments and implementation must match so that+-- the parsed arguments will match what the implementation expects.+data Command = forall a. Command+  { -- | Names of this command, first in the list is the "primary" name+    cmdNames          :: NonEmpty Text+  -- | Specification of the arguments of the command+  , cmdArgumentSpec   :: Args ClientState a+  -- | Multi-line IRC-formatted documentation text used for @/help@+  , cmdDocumentation  :: Text+  -- | Implementation of the command for both execution and tab completion+  , cmdImplementation :: CommandImpl a+  }++-- | A command section is a logical grouping of commands. This allows for+-- showing more structure in the help menu system.+data CommandSection = CommandSection+  { cmdSectionName :: Text+  , cmdSectionCmds :: [Command]+  }++-- | Consider the text entry successful and resume the client+commandSuccess :: Monad m => ClientState -> m CommandResult+commandSuccess = return . CommandSuccess++-- | Consider the text entry a failure and resume the client+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 =+  return $! CommandFailure $! set clientErrorMsg (Just e) st+
+ src/Client/Commands/Window.hs view
@@ -0,0 +1,508 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.Window+Description : Window command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.Window (windowCommands, parseFocus) where++import           Client.Commands.Arguments.Spec+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.Commands.WordCompletion+import           Client.Image.PackedImage+import           Client.Mask (buildMask)+import           Client.State+import           Client.State.Focus+import           Client.State.Network+import           Client.State.Window (emptyWindow, WindowLines((:-), Nil), wlFullImage, winMessages)+import           Control.Applicative+import           Control.Exception+import           Control.Lens+import           Data.Foldable+import           Data.List ((\\), nub)+import qualified Client.State.EditBox as Edit+import           Data.HashSet (HashSet)+import           Data.List.NonEmpty (NonEmpty((:|)))+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Map as Map+import           Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Lazy as LText+import qualified Data.Text.Lazy.IO as LText+import           Irc.Identifier++windowCommands :: CommandSection+windowCommands = CommandSection "Window management"+  ------------------------------------------------------------------------++  [ Command+      (pure "focus")+      (liftA2 (,) (simpleToken "network") (optionalArg (simpleToken "[target]")))+      "Change the focused window.\n\+      \\n\+      \When only \^Bnetwork\^B is specified this switches to the network status window.\n\+      \When \^Bnetwork\^B and \^Btarget\^B are specified this switches to that chat window.\n\+      \\n\+      \Nickname and channels can be specified in the \^Btarget\^B parameter.\n\+      \See also: /query (aliased /c /channel) to switch to a target on the current network.\n"+    $ ClientCommand cmdFocus tabFocus++  , Command+      ("c" :| ["channel"])+      (simpleToken "focus")+      "\^BParameters:\^B\n\+      \\n\+      \    focuses: Focus name\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    This command sets the current window focus. When\n\+      \    no network is specified, the current network will\n\+      \    be used.\n\+      \\n\+      \    Client:  *\n\+      \    Network: \^_network\^_:\n\+      \    Channel: \^_#channel\^_\n\+      \    Channel: \^_network\^_:\^_#channel\^_\n\+      \    User:    \^_nick\^_\n\+      \    User:    \^_network\^_:\^_nick\^_\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /c fn:#haskell\n\+      \    /c #haskell\n\+      \    /c fn:\n\+      \    /c *:\n\+      \\n\+      \\^BSee also:\^B focus\n"+    $ ClientCommand cmdChannel tabChannel++  , Command+      (pure "clear")+      (optionalArg (liftA2 (,) (simpleToken "[network]") (optionalArg (simpleToken "[channel]"))))+      "Clear a window.\n\+      \\n\+      \If no arguments are provided the current window is cleared.\n\+      \If \^Bnetwork\^B is provided the that network window is cleared.\n\+      \If \^Bnetwork\^B and \^Bchannel\^B are provided that chat window is cleared.\n\+      \If \^Bnetwork\^B is provided and \^Bchannel\^B is \^B*\^O all windows for that network are cleared.\n\+      \\n\+      \If a window is cleared and no longer active that window will be removed from the client.\n"+    $ ClientCommand cmdClear tabFocus++  , Command+      (pure "windows")+      (optionalArg (simpleToken "[kind]"))+      "Show a list of all windows with an optional argument to limit the kinds of windows listed.\n\+      \\n\+      \\^Bkind\^O: one of \^Bnetworks\^O, \^Bchannels\^O, \^Busers\^O\n\+      \\n"+    $ ClientCommand cmdWindows tabWindows++  , Command+      (pure "splits")+      (remainingArg "focuses")+      "\^BParameters:\^B\n\+      \\n\+      \    focuses: List of focus names\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    This command sents the set of focuses that will always\n\+      \    be visible, even when unfocused. When the client is focused\n\+      \    to an active network, the network can be omitted when\n\+      \    specifying a focus. If no focuses are listed, they will\n\+      \    all be cleared.\n\+      \\n\+      \    Client:  *\n\+      \    Network: \^_network\^_:\n\+      \    Channel: \^_#channel\^_\n\+      \    Channel: \^_network\^_:\^_#channel\^_\n\+      \    User:    \^_nick\^_\n\+      \    User:    \^_network\^_:\^_nick\^_\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /splits * fn:#haskell fn:chanserv\n\+      \    /splits #haskell #haskell-lens nickserv\n\+      \    /splits\n\+      \\n\+      \\^BSee also:\^B splits+, splits-\n"+    $ ClientCommand cmdSplits tabSplits++  , Command+      (pure "splits+")+      (remainingArg "focuses")+      "Add windows to the splits list. Omit the list of focuses to add the\+      \ current window.\n\+      \\n\+      \\^Bfocuses\^B: space delimited list of focus names.\n\+      \\n\+      \Client:  *\n\+      \Network: \^BNETWORK\^B\n\+      \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\+      \User:    \^BNETWORK\^B:\^BNICK\^B\n\+      \\n\+      \If the network part is omitted, the current network will be used.\n"+    $ ClientCommand cmdSplitsAdd tabSplits++  , Command+      (pure "splits-")+      (remainingArg "focuses")+      "Remove windows from the splits list. Omit the list of focuses to\+      \ remove the current window.\n\+      \\n\+      \\^Bfocuses\^B: space delimited list of focus names.\n\+      \\n\+      \Client:  *\n\+      \Network: \^BNETWORK\^B\n\+      \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\+      \User:    \^BNETWORK\^B:\^BNICK\^B\n\+      \\n\+      \If the network part is omitted, the current network will be used.\n"+    $ ClientCommand cmdSplitsDel tabActiveSplits++  , Command+      (pure "ignore")+      (remainingArg "masks")+      "\^BParameters:\^B\n\+      \\n\+      \    masks: List of masks\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    Toggle the soft-ignore on each of the space-delimited given\n\+      \    nicknames. Ignores can use \^B*\^B (many) and \^B?\^B (one) wildcards.\n\+      \    Masks can be of the form: nick[[!user]@host]\n\+      \    Masks use a case-insensitive comparison.\n\+      \\n\+      \    If no masks are specified the current ignore list is displayed.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /ignore\n\+      \    /ignore nick1 nick2 nick3\n\+      \    /ignore nick@host\n\+      \    /ignore nick!user@host\n\+      \    /ignore *@host\n\+      \    /ignore *!baduser@*\n"+    $ ClientCommand cmdIgnore tabIgnore++  , Command+      (pure "grep")+      (remainingArg "regular-expression")+      "Set the persistent regular expression.\n\+      \\n\+      \\^BFlags:\^B\n\+      \    -An  Show n messages after match\n\+      \    -Bn  Show n messages before match\n\+      \    -Cn  Show n messages before and after match\n\+      \    -i   Case insensitive match\n\+      \    -v   Invert pattern match\n\+      \    --   Stop processing flags\n\+      \\n\+      \Clear the regular expression by calling this without an argument.\n\+      \\n\+      \\^B/grep\^O is case-sensitive.\n"+    $ ClientCommand cmdGrep simpleClientTab++  , Command+      (pure "dump")+      (simpleToken "filename")+      "Dump current buffer to file.\n"+    $ ClientCommand cmdDump simpleClientTab++  , Command+      (pure "mentions")+      (pure ())+      "Show a list of all message that were highlighted as important.\n\+      \\n\+      \When using \^B/grep\^B the important messages are those matching\n\+      \the regular expression instead.\n"+    $ ClientCommand cmdMentions noClientTab++  ]++-- | Implementation of @/grep@+cmdGrep :: ClientCommand String+cmdGrep st str+  | null str  = commandSuccess (set clientRegex Nothing st)+  | otherwise =+      case buildMatcher str of+        Nothing -> commandFailureMsg "bad grep" st+        Just  r -> commandSuccess (set clientRegex (Just r) st)++-- | Implementation of @/windows@ command. Set subfocus to Windows.+cmdWindows :: ClientCommand (Maybe String)+cmdWindows st arg =+  case arg of+    Nothing         -> success AllWindows+    Just "networks" -> success NetworkWindows+    Just "channels" -> success ChannelWindows+    Just "users"    -> success UserWindows+    _               -> commandFailureMsg errmsg st+  where+    errmsg = "/windows expected networks, channels, or users"+    success x =+      commandSuccess (changeSubfocus (FocusWindows x) st)++-- | Implementation of @/mentions@ command. Set subfocus to Mentions.+cmdMentions :: ClientCommand ()+cmdMentions st _ = commandSuccess (changeSubfocus FocusMentions st)++cmdIgnore :: ClientCommand String+cmdIgnore st rest =+  case mkId <$> Text.words (Text.pack rest) of+    [] -> commandSuccess (changeSubfocus FocusIgnoreList st)+    xs -> commandSuccess st2+      where+        (newIgnores, st1) = (clientIgnores <%~ updateIgnores) st+        st2 = set clientIgnoreMask (buildMask (toList newIgnores)) st1++        updateIgnores :: HashSet Identifier -> HashSet Identifier+        updateIgnores s = foldl' updateIgnore s xs++        updateIgnore s x = over (contains x) not s++-- | Complete the nickname at the current cursor position using the+-- userlist for the currently focused channel (if any)+tabIgnore :: Bool {- ^ reversed -} -> ClientCommand String+tabIgnore isReversed st _ =+  simpleTabCompletion mode hint completions isReversed st+  where+    hint          = activeNicks st+    completions   = currentCompletionList st ++ views clientIgnores toList st+    mode          = currentNickCompletionMode st++-- | Implementation of @/splits@+cmdSplits :: ClientCommand String+cmdSplits st str =+  withSplitFocuses st str $ \args ->+    commandSuccess (setExtraFocus (nub args) st)+++-- | Implementation of @/splits+@. When no focuses are provided+-- the current focus is used instead.+cmdSplitsAdd :: ClientCommand String+cmdSplitsAdd st str =+  withSplitFocuses st str $ \args ->+    let args'+          | null args = [(view clientFocus st, view clientSubfocus st)]+          | otherwise = args+        extras = nub (args' ++ view clientExtraFocus st)++    in commandSuccess (setExtraFocus extras st)++-- | Implementation of @/splits-@. When no focuses are provided+-- the current focus is used instead.+cmdSplitsDel :: ClientCommand String+cmdSplitsDel st str =+  withSplitFocuses st str $ \args ->+    let args'+          | null args = [(view clientFocus st, view clientSubfocus st)]+          | otherwise = args+        extras = view clientExtraFocus st \\ args'++    in commandSuccess (setExtraFocus extras st)++withSplitFocuses ::+  ClientState                   ->+  String                        ->+  ([(Focus, Subfocus)] -> IO CommandResult) ->+  IO CommandResult+withSplitFocuses st str k =+  case mb of+    Nothing   -> commandFailureMsg "unable to parse arguments" st+    Just args -> k [(x, FocusMessages) | x <- args]+  where+    mb = traverse+           (parseFocus (views clientFocus focusNetwork st))+           (words str)++-- | Parses a single focus name given a default network.+parseFocus ::+  Maybe Text {- ^ default network    -} ->+  String {- ^ @[network:]target@ -} ->+  Maybe Focus+parseFocus mbNet x =+  case break (==':') x of+    ("*","")     -> pure Unfocused+    (net,_:"")   -> pure (NetworkFocus (Text.pack net))+    (net,_:chan) -> pure (ChannelFocus (Text.pack net) (mkId (Text.pack chan)))+    (chan,"")    -> mbNet <&> \net ->+                    ChannelFocus net (mkId (Text.pack chan))++cmdFocus :: ClientCommand (String, Maybe String)+cmdFocus st (network, mbChannel)+  | network == "*" = commandSuccess (changeFocus Unfocused st)+  | otherwise =+     case mbChannel of+       Nothing ->+         let focus = NetworkFocus (Text.pack network) in+         commandSuccess (changeFocus focus st)+       Just channel ->+         let focus = ChannelFocus (Text.pack network) (mkId (Text.pack channel)) in+         commandSuccess+           $ changeFocus focus st++tabWindows :: Bool -> ClientCommand String+tabWindows isReversed st _ =+  simpleTabCompletion plainWordCompleteMode [] completions isReversed st+  where+    completions = ["networks","channels","users"] :: [Text]++-- | Tab completion for @/splits-@. This completes only from the list of active+-- entries in the splits list.+tabActiveSplits :: Bool -> ClientCommand String+tabActiveSplits isReversed st _ =+  simpleTabCompletion plainWordCompleteMode [] completions isReversed st+  where+    completions = currentNetSplits <> currentSplits+    currentSplits = [renderSplitFocus x | (x, FocusMessages) <- view clientExtraFocus st]+    currentNetSplits =+      [ idText chan+        | (ChannelFocus net chan, FocusMessages) <- view clientExtraFocus st+        , views clientFocus focusNetwork st == Just net+        ]++-- | When used on a channel that the user is currently+-- joined to this command will clear the messages but+-- preserve the window. When used on a window that the+-- user is not joined to this command will delete the window.+cmdClear :: ClientCommand (Maybe (String, Maybe String))+cmdClear st args =+  case args of+    Nothing                      -> clearFocus (view clientFocus st)+    Just ("*",     Nothing     ) -> clearFocus Unfocused+    Just (network, Nothing     ) -> clearFocus (NetworkFocus (Text.pack network))+    Just (network, Just "*"    ) -> clearNetworkWindows network+    Just (network, Just channel) -> clearFocus (ChannelFocus (Text.pack network) (mkId (Text.pack channel)))+  where+    clearNetworkWindows network+      = commandSuccess+      $ foldl' (flip clearFocus1) st+      $ filter (\x -> focusNetwork x == Just (Text.pack network))+      $ views clientWindows Map.keys st++    clearFocus focus = commandSuccess (clearFocus1 focus st)++    clearFocus1 focus st' = focusEffect (windowEffect st')+      where+        windowEffect = set (clientWindows . at focus)+                           (if isActive then Just emptyWindow else Nothing)++        focusEffect+          | noChangeNeeded    = id+          | prevExists        = changeFocus prev+          | otherwise         = advanceFocus+          where+            noChangeNeeded    = isActive || view clientFocus st' /= focus+            prevExists        = has (clientWindows . ix prev) st'++            prev              = view clientPrevFocus st++        isActive =+          case focus of+            Unfocused                    -> False+            NetworkFocus network         -> has (clientConnection network) st'+            ChannelFocus network channel -> has (clientConnection network+                                                .csChannels . ix channel) st'++-- | Tab completion for @/splits[+]@. When given no arguments this+-- populates the current list of splits, otherwise it tab completes+-- all of the currently available windows.+tabSplits :: Bool -> ClientCommand String+tabSplits isReversed st rest++  -- If no arguments, populate the current splits+  | all (' '==) rest =+     let cmd = unwords $ "/splits"+                       : [Text.unpack (renderSplitFocus x) | (x, FocusMessages) <- view clientExtraFocus st]+         newline = Edit.endLine cmd+     in commandSuccess (set (clientTextBox . Edit.line) newline st)++  -- Tab complete the available windows. Accepts either fully qualified+  -- window names or current network names without the ':'+  | otherwise =+     let completions = currentNet <> allWindows+         allWindows  = renderSplitFocus <$> views clientWindows Map.keys st+         currentNet  = case views clientFocus focusNetwork st of+                         Just net -> idText <$> channelWindowsOnNetwork net st+                         Nothing  -> []+     in simpleTabCompletion plainWordCompleteMode [] completions isReversed st++-- | Render a entry from splits back to the textual format.+renderSplitFocus :: Focus -> Text+renderSplitFocus Unfocused          = "*"+renderSplitFocus (NetworkFocus x)   = x <> ":"+renderSplitFocus (ChannelFocus x y) = x <> ":" <> idText y++-- | When tab completing the first parameter of the focus command+-- the current networks are used.+tabFocus :: Bool -> ClientCommand String+tabFocus isReversed st _ =+  simpleTabCompletion plainWordCompleteMode [] completions isReversed st+  where+    networks   = map mkId $ HashMap.keys $ view clientConnections st+    params     = words $ uncurry take $ clientLine st++    completions =+      case params of+        [_cmd,_net]      -> networks+        [_cmd,net,_chan] -> channelWindowsOnNetwork (Text.pack net) st+        _                -> []++-- | @/channel@ command. Takes a channel or nickname and switches+-- focus to that target on the current network.+cmdChannel :: ClientCommand String+cmdChannel st channel =+  case parseFocus (views clientFocus focusNetwork st) channel of+    Just focus -> commandSuccess (changeFocus focus st)+    Nothing    -> commandFailureMsg "No current network" st++-- | Tab completion for @/channel@. Tab completion uses pre-existing+-- windows.+tabChannel ::+  Bool {- ^ reversed order -} ->+  ClientCommand String+tabChannel isReversed st _ =+  simpleTabCompletion plainWordCompleteMode [] completions isReversed st+  where+    completions = currentNet <> allWindows+    allWindows  = renderSplitFocus <$> views clientWindows Map.keys st+    currentNet  = case views clientFocus focusNetwork st of+                    Just net -> idText <$> channelWindowsOnNetwork net st+                    Nothing  -> []++-- | Return the list of identifiers for open channel windows on+-- the given network name.+channelWindowsOnNetwork ::+  Text         {- ^ network              -} ->+  ClientState  {- ^ client state         -} ->+  [Identifier] {- ^ open channel windows -}+channelWindowsOnNetwork network st =+  [ chan | ChannelFocus net chan <- Map.keys (view clientWindows st)+         , net == network ]++-- | Implementation of @/dump@. Writes detailed contents of focused buffer+-- to the given filename.+cmdDump :: ClientCommand String+cmdDump st fp =+  do res <- try (LText.writeFile fp (LText.unlines outputLines))+     case res of+       Left e  -> commandFailureMsg (Text.pack (displayException (e :: SomeException))) st+       Right{} -> commandSuccess st++  where+    focus = view clientFocus st+    msgs  = preview (clientWindows . ix focus . winMessages) st+    outputLines =+      case msgs of+        Nothing  -> []+        Just wls -> convert [] wls+    convert acc Nil = acc+    convert acc (wl :- wls) = convert (views wlFullImage imageText wl : acc) wls
+ src/Client/Commands/ZNC.hs view
@@ -0,0 +1,101 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Commands.ZNC+Description : ZNC command implementations+Copyright   : (c) Eric Mertens, 2016-2020+License     : ISC+Maintainer  : emertens@gmail.com+-}++module Client.Commands.ZNC (zncCommands) where++import           Control.Applicative+import           Client.Commands.Arguments.Spec+import           Client.Commands.TabCompletion+import           Client.Commands.Types+import           Client.State.Network (sendMsg)+import           Data.Foldable (asum)+import qualified Data.Text as Text+import           Data.Time+import           Irc.Commands+import           Control.Lens+import           LensUtils (localTimeDay, localTimeTimeOfDay, zonedTimeLocalTime)++zncCommands :: CommandSection+zncCommands = CommandSection "ZNC Support"++  [ Command+      (pure "znc")+      (remainingArg "arguments")+      "Send command directly to ZNC.\n\+      \\n\+      \The advantage of this over /msg is that responses are not broadcast to call clients.\n"+    $ NetworkCommand cmdZnc simpleNetworkTab++  , Command+      (pure "znc-playback")+      (optionalArg (liftA2 (,) (simpleToken "[time]") (optionalArg (simpleToken "[date]"))))+      "Request playback from the ZNC 'playback' module.\n\+      \\n\+      \\^Btime\^B determines the time to playback since.\n\+      \\^Bdate\^B determines the date to playback since.\n\+      \\n\+      \When both \^Btime\^B and \^Bdate\^B are omitted, all playback is requested.\n\+      \When both \^Bdate\^B is omitted it is defaulted the most recent date in the past that makes sense.\n\+      \\n\+      \Time format: HOURS:MINUTES (example: 7:00)\n\+      \Date format: YEAR-MONTH-DAY (example: 2016-06-16)\n\+      \\n\+      \Note that the playback module is not installed in ZNC by default!\n"+    $ NetworkCommand cmdZncPlayback noNetworkTab++  ]++cmdZnc :: NetworkCommand String+cmdZnc cs st rest =+  do sendMsg cs (ircZnc (Text.words (Text.pack rest)))+     commandSuccess st++cmdZncPlayback :: NetworkCommand (Maybe (String, Maybe String))+cmdZncPlayback cs st args =+  case args of++    -- request everything+    Nothing -> success "0"++    -- current date explicit time+    Just (timeStr, Nothing)+       | Just tod <- parseFormats timeFormats timeStr ->+          do now <- getZonedTime+             let (nowTod,t) = (zonedTimeLocalTime . localTimeTimeOfDay <<.~ tod) now+                 yesterday = over (zonedTimeLocalTime . localTimeDay) (addDays (-1))+                 fixDay+                   | tod <= nowTod = id+                   | otherwise     = yesterday+             successZoned (fixDay t)++    -- explicit date and time+    Just (dateStr, Just timeStr)+       | Just day  <- parseFormats dateFormats dateStr+       , Just tod  <- parseFormats timeFormats timeStr ->+          do tz <- getCurrentTimeZone+             successZoned ZonedTime+               { zonedTimeZone = tz+               , zonedTimeToLocalTime = LocalTime+                   { localTimeOfDay = tod+                   , localDay       = day } }++    _ -> commandFailureMsg "unable to parse date/time arguments" st++  where+    -- %k doesn't require a leading 0 for times before 10AM+    timeFormats = ["%k:%M:%S","%k:%M"]+    dateFormats = ["%F"]+    parseFormats formats str =+      asum (map (parseTimeM False defaultTimeLocale ?? str) formats)++    successZoned = success . formatTime defaultTimeLocale "%s"++    success start =+      do sendMsg cs (ircZnc ["*playback", "play", "*", Text.pack start])+         commandSuccess st
src/Client/Configuration.hs view
@@ -232,7 +232,6 @@   IO (Either ConfigurationFailure (FilePath, Configuration)) loadConfiguration mbPath = try $   do (path,txt) <- readConfigurationFile mbPath-     def  <- loadDefaultServerSettings      home <- getHomeDirectory       rawcfg <-@@ -245,7 +244,7 @@                $ ConfigurationMalformed path                $ displayException e        Right cfg ->-         do cfg' <- resolvePaths path (cfg def home)+         do cfg' <- resolvePaths path (cfg defaultServerSettings home)                     >>= validateDirectories path             return (path, cfg') @@ -257,7 +256,7 @@      let resolveServerFilePaths = over (ssTlsClientCert . mapped) res                                 . over (ssTlsClientKey  . mapped) res                                 . over (ssTlsServerCert . mapped) res-                                . over (ssSaslEcdsaFile . mapped) res+                                . over (ssSaslMechanism . mapped . _SaslEcdsa . _3) res                                 . over (ssLogDir        . mapped) res      return $! over (configExtensions . mapped . extensionPath) res              . over (configServers    . mapped) resolveServerFilePaths
src/Client/Configuration/ServerSettings.hs view
@@ -25,14 +25,13 @@   , ssUser   , ssReal   , ssPassword-  , ssSaslUsername-  , ssSaslPassword-  , ssSaslEcdsaFile+  , ssSaslMechanism   , ssHostName   , ssPort   , ssTls   , ssTlsClientCert   , ssTlsClientKey+  , ssTlsClientKeyPassword   , ssTlsServerCert   , ssTlsCiphers   , ssConnectCmds@@ -48,16 +47,27 @@   , ssAutoconnect   , ssNickCompletion   , ssLogDir-  , ssProtocolFamily+  , ssBindHostName   , ssSts   , ssTlsPubkeyFingerprint   , ssTlsCertFingerprint   , ssShowAccounts   , ssCapabilities -  -- * Load function-  , loadDefaultServerSettings+  -- * SASL Mechanisms+  , SaslMechanism(..)+  , _SaslExternal+  , _SaslEcdsa+  , _SaslPlain +  -- * Secrets+  , Secret(..)+  , SecretException(..)+  , loadSecrets++  -- * Defaults+  , defaultServerSettings+   -- * TLS settings   , UseTls(..)   , Fingerprint(..)@@ -71,20 +81,23 @@ import           Client.Commands.WordCompletion import           Client.Configuration.Macros (macroCommandSpec) import           Config.Schema.Spec+import           Control.Exception (Exception, displayException, throwIO, try) import           Control.Lens+import           Control.Monad ((>=>)) import qualified Data.ByteString as B import           Data.Functor.Alt                    ((<!>)) import           Data.List.NonEmpty (NonEmpty((:|))) import           Data.ByteString (ByteString)-import           Data.Maybe (fromMaybe) import           Data.Monoid import           Data.Text (Text)+import qualified Data.List.NonEmpty as NonEmpty import           Data.List.Split (chunksOf, splitOn) import qualified Data.Text as Text import           Irc.Identifier (Identifier, mkId)-import           Network.Socket (HostName, PortNumber, Family(..))+import           Network.Socket (HostName, PortNumber) import           Numeric (readHex)-import           System.Environment+import qualified System.Exit as Exit+import qualified System.Process as Process import           Text.Regex.TDFA import           Text.Regex.TDFA.Text (compile) @@ -93,15 +106,14 @@   { _ssNicks            :: !(NonEmpty Text) -- ^ connection nicknames   , _ssUser             :: !Text -- ^ connection username   , _ssReal             :: !Text -- ^ connection realname / GECOS-  , _ssPassword         :: !(Maybe Text) -- ^ server password-  , _ssSaslUsername     :: !(Maybe Text) -- ^ SASL username-  , _ssSaslPassword     :: !(Maybe Text) -- ^ SASL plain password-  , _ssSaslEcdsaFile    :: !(Maybe FilePath) -- ^ SASL ecdsa private key+  , _ssPassword         :: !(Maybe Secret) -- ^ server password+  , _ssSaslMechanism    :: !(Maybe SaslMechanism) -- ^ SASL mechanism   , _ssHostName         :: !HostName -- ^ server hostname   , _ssPort             :: !(Maybe PortNumber) -- ^ server port   , _ssTls              :: !UseTls -- ^ use TLS to connect   , _ssTlsClientCert    :: !(Maybe FilePath) -- ^ path to client TLS certificate   , _ssTlsClientKey     :: !(Maybe FilePath) -- ^ path to client TLS key+  , _ssTlsClientKeyPassword :: !(Maybe Secret) -- ^ client key PEM password   , _ssTlsServerCert    :: !(Maybe FilePath) -- ^ additional CA certificates for validating server   , _ssTlsCiphers       :: String            -- ^ OpenSSL cipher suite   , _ssConnectCmds      :: ![[ExpansionChunk]] -- ^ commands to execute upon successful connection@@ -117,7 +129,7 @@   , _ssAutoconnect      :: !Bool -- ^ Connect to this network on server startup   , _ssNickCompletion   :: WordCompletionMode -- ^ Nick completion mode for this server   , _ssLogDir           :: Maybe FilePath -- ^ Directory to save logs of chat-  , _ssProtocolFamily   :: Maybe Family -- ^ Protocol family to connect with+  , _ssBindHostName     :: Maybe HostName -- ^ Local bind host   , _ssSts              :: !Bool -- ^ Honor STS policies when true   , _ssTlsPubkeyFingerprint :: !(Maybe Fingerprint) -- ^ optional acceptable public key fingerprint   , _ssTlsCertFingerprint   :: !(Maybe Fingerprint) -- ^ optional acceptable certificate fingerprint@@ -126,6 +138,18 @@   }   deriving Show +data Secret+  = SecretText Text    -- ^ Constant text+  | SecretCommand (NonEmpty Text) -- ^ Command to generate text+  deriving Show++-- | SASL mechanisms and configuration data.+data SaslMechanism+  = SaslPlain    (Maybe Text) Text Secret -- ^ SASL PLAIN RFC4616 - authzid authcid password+  | SaslEcdsa    (Maybe Text) Text FilePath -- ^ SASL NIST - https://github.com/kaniini/ecdsatool - authzid keypath+  | SaslExternal (Maybe Text)      -- ^ SASL EXTERNAL RFC4422 - authzid+  deriving Show+ -- | Regular expression matched with original source to help with debugging. data KnownRegex = KnownRegex Text Regex @@ -153,28 +177,23 @@   deriving Show  makeLenses ''ServerSettings+makePrisms ''SaslMechanism --- | Load the defaults for server settings based on the environment--- variables.------ Environment variables @USER@, @IRCPASSSWORD@, and @SASLPASSWORD@ are used.-loadDefaultServerSettings :: IO ServerSettings-loadDefaultServerSettings =-  do env  <- getEnvironment-     let username = Text.pack (fromMaybe "guest" (lookup "USER" env))-     return ServerSettings-       { _ssNicks         = pure username-       , _ssUser          = username-       , _ssReal          = username-       , _ssPassword      = Text.pack <$> lookup "IRCPASSWORD" env-       , _ssSaslUsername  = Nothing-       , _ssSaslPassword  = Text.pack <$> lookup "SASLPASSWORD" env-       , _ssSaslEcdsaFile = Nothing+-- | The defaults for server settings.+defaultServerSettings :: ServerSettings+defaultServerSettings =+  ServerSettings+       { _ssNicks         = pure "guest"+       , _ssUser          = "username"+       , _ssReal          = "realname"+       , _ssPassword      = Nothing+       , _ssSaslMechanism = Nothing        , _ssHostName      = ""        , _ssPort          = Nothing        , _ssTls           = UseInsecure        , _ssTlsClientCert = Nothing        , _ssTlsClientKey  = Nothing+       , _ssTlsClientKeyPassword = Nothing        , _ssTlsServerCert = Nothing        , _ssTlsCiphers    = "HIGH"        , _ssConnectCmds   = []@@ -190,7 +209,7 @@        , _ssAutoconnect      = False        , _ssNickCompletion   = defaultNickWordCompleteMode        , _ssLogDir           = Nothing-       , _ssProtocolFamily   = Nothing+       , _ssBindHostName     = Nothing        , _ssSts              = True        , _ssTlsPubkeyFingerprint = Nothing        , _ssTlsCertFingerprint   = Nothing@@ -238,14 +257,8 @@       , req "realname" ssReal anySpec         "\"GECOS\" name sent to server visible in /whois" -      , opt "sasl-username" ssSaslUsername anySpec-        "Username for SASL authentication to NickServ"--      , opt "sasl-password" ssSaslPassword anySpec-        "Password for SASL authentication to NickServ"--      , opt "sasl-ecdsa-key" ssSaslEcdsaFile stringSpec-        "Path to ECDSA key for non-password SASL authentication"+      , opt "sasl" ssSaslMechanism saslMechanismSpec+        "SASL settings"        , req "tls" ssTls useTlsSpec         "Set to `yes` to enable secure connect. Set to `yes-insecure` to disable certificate checking."@@ -256,6 +269,9 @@       , opt "tls-client-key" ssTlsClientKey stringSpec         "Path to TLS client key" +      , opt "tls-client-key-password" ssTlsClientKeyPassword anySpec+        "Password for decrypting TLS client key PEM file"+       , opt "tls-server-cert" ssTlsServerCert stringSpec         "Path to CA certificate bundle" @@ -298,8 +314,8 @@       , opt "log-dir" ssLogDir stringSpec         "Path to log file directory for this server" -      , opt "protocol-family" ssProtocolFamily protocolFamilySpec-        "IP protocol family to use for this connection"+      , opt "bind-hostname" ssBindHostName stringSpec+        "Source address to bind to before connecting"        , req "sts" ssSts yesOrNoSpec         "Honor server STS policies forcing TLS connections"@@ -317,6 +333,28 @@         "Extra capabilities to unconditionally request from the server"       ] +saslMechanismSpec :: ValueSpec SaslMechanism+saslMechanismSpec = plain <!> external <!> ecdsa+  where+    mech m   = reqSection' "mechanism" (atomSpec m) "Mechanism"+    authzid  = optSection "authzid" "Authorization identity"+    username = reqSection "username" "Authentication identity"++    plain =+      sectionsSpec "sasl-plain" $ SaslPlain <$+      optSection' "mechanism" (atomSpec "plain") "Mechanism" <*>+      authzid <*> username <*> reqSection "password" "Password"++    external =+      sectionsSpec "sasl-external" $ SaslExternal <$ mech "external" <*>+      authzid++    ecdsa =+      sectionsSpec "sasl-ecdsa-nist256p-challenge-mech" $+      SaslEcdsa <$ mech "ecdsa-nist256p-challenge" <*>+      authzid <*> username <*>+      reqSection' "private-key" stringSpec "Private key file"+ hookSpec :: ValueSpec HookConfig hookSpec =   flip HookConfig [] <$> anySpec <!>@@ -352,17 +390,9 @@       | ':' `elem` str = splitOn ":" str       | otherwise      = chunksOf 2  str --- | Specification for IP protocol family.-protocolFamilySpec :: ValueSpec Family-protocolFamilySpec =-      AF_INET   <$ atomSpec "inet"-  <!> AF_INET6  <$ atomSpec "inet6"-- nicksSpec :: ValueSpec (NonEmpty Text) nicksSpec = oneOrNonemptySpec anySpec - useTlsSpec :: ValueSpec UseTls useTlsSpec =       UseTls         <$ atomSpec "yes"@@ -384,3 +414,31 @@   case compile defaultCompOpt ExecOption{captureGroups = False} str of     Left e  -> Left  (Text.pack e)     Right r -> Right (KnownRegex str r)++instance HasSpec Secret where+  anySpec = SecretText <$> textSpec <!>+            SecretCommand <$> sectionsSpec "command" (reqSection "command" "Command and arguments to execute to secret")++data SecretException = SecretException String String+  deriving Show++instance Exception SecretException++-- | Run the secret commands in a server configuration replacing them with secret text.+-- Throws 'SecretException'+loadSecrets :: ServerSettings -> IO ServerSettings+loadSecrets =+  traverseOf (ssPassword             . _Just                  ) (loadSecret "server password") >=>+  traverseOf (ssSaslMechanism        . _Just . _SaslPlain . _3) (loadSecret "SASL password") >=>+  traverseOf (ssTlsClientKeyPassword . _Just                  ) (loadSecret "TLS key password")++-- | Run a command if found and replace it with the first line of stdout result.+loadSecret :: String -> Secret -> IO Secret+loadSecret _ (SecretText txt) = pure (SecretText txt)+loadSecret label (SecretCommand (cmd NonEmpty.:| args)) =+  do let u = Text.unpack+     res <- try (Process.readProcessWithExitCode (u cmd) (map u args) "")+     case res of+       Right (Exit.ExitSuccess,out,_) -> pure (SecretText (Text.pack (takeWhile ('\n' /=) out)))+       Right (Exit.ExitFailure{},_,err) -> throwIO (SecretException label err)+       Left ioe -> throwIO (SecretException label (displayException (ioe::IOError)))
src/Client/EventLoop/Network.hs view
@@ -125,12 +125,8 @@   ClientState  {- ^ client state  -} ->   IO ClientState processSaslEcdsa now challenge cs st =-  case view ssSaslEcdsaFile ss of-    Nothing ->-      do sendMsg cs ircCapEnd-         return $! recordError now (view csNetwork cs) "panic: ecdsatool malformed output" st--    Just path ->+  case view ssSaslMechanism ss of+    Just (SaslEcdsa _ _ path) ->       do res <- Ecdsa.computeResponse path challenge          case res of            Left e ->@@ -139,6 +135,10 @@            Right resp ->              do sendMsg cs (ircAuthenticate resp)                 return $! set asLens AS_None st++    _ ->+      do sendMsg cs ircCapEnd+         return $! recordError now (view csNetwork cs) "panic: ecdsa mechanism not configured" st   where     ss = view csSettings cs     asLens = clientConnection (view csNetwork cs) . csAuthenticationState
src/Client/Hook/FreRelay.hs view
@@ -126,6 +126,7 @@     (UserInfo (mkId (nick <> "@" <> srv)) user host)     chan     "" -- account+    "" -- gecos  partMsg ::   Identifier {- ^ channel        -} ->
src/Client/Hook/Znc/Buffextras.hs view
@@ -54,7 +54,7 @@ prefixedParser chan = do     pfx <- prefixParser     choice-      [ Join pfx chan "" <$ skipToken "joined"+      [ Join pfx chan "" "" <$ skipToken "joined"       , Quit pfx . filterEmpty <$ skipToken "quit:" <*> P.takeText       , Part pfx chan . filterEmpty <$ skipToken "parted:" <*> P.takeText       , Nick pfx . mkId <$ skipToken "is now known as" <*> simpleTokenParser
src/Client/Image/Layout.hs view
@@ -35,7 +35,7 @@ -- | Layout algorithm for all windows in a single column. drawLayoutOne ::   ClientState            {- ^ client state                 -} ->-  [Focus]                {- ^ extra window names           -} ->+  [(Focus, Subfocus)]    {- ^ extra windows                -} ->   (Int, Int, Int, Image) {- ^ overscroll and final image   -} drawLayoutOne st extrafocus =   (overscroll, pos, nextOffset, output)@@ -49,21 +49,21 @@      output = vertCat $ reverse            $ main-           : [ drawExtra st w h' foc imgs-                 | (h', (foc, imgs)) <- zip hs extraLines]+           : [ drawExtra st w h' foc subfoc imgs+                 | (h', (foc, subfoc, imgs)) <- zip hs extraLines]      rows = view clientHeight st      -- don't count textbox or the main status line against the main window's height     saveRows = 1 + imageHeight (statusLineImage w st) -    extraLines = [ (focus', viewLines focus' FocusMessages w st)-                   | focus' <- extrafocus ]+    extraLines = [ (focus, subfocus, viewLines focus subfocus w st)+                   | (focus, subfocus) <- extrafocus ]  -- | Layout algorithm for all windows in a single column. drawLayoutTwo ::   ClientState            {- ^ client state                                -} ->-  [Focus]                {- ^ extra window names                          -} ->+  [(Focus, Subfocus)]    {- ^ extra windows                               -} ->   (Int, Int, Int, Image) {- ^ overscroll, cursor pos, offset, final image -} drawLayoutTwo st extrafocus =   (overscroll, pos, nextOffset, output)@@ -75,8 +75,8 @@     output = main <|> divider <|> extraImgs      extraImgs = vertCat $ reverse-             [ drawExtra st wr h' foc imgs-                 | (h', (foc, imgs)) <- zip hs extraLines]+             [ drawExtra st wr h' foc subfoc imgs+                 | (h', (foc, subfoc, imgs)) <- zip hs extraLines]      (overscroll, pos, nextOffset, main) =         drawMain wl rows scroll st@@ -85,8 +85,8 @@     divider = charFill (view palWindowDivider pal) ' ' 1 rows     rows    = view clientHeight st -    extraLines = [ (focus', viewLines focus' FocusMessages wr st)-                   | focus' <- extrafocus ]+    extraLines = [ (focus, subfocus, viewLines focus subfocus wr st)+                   | (focus, subfocus) <- extrafocus ]  drawMain ::   Int         {- ^ draw width      -} ->@@ -115,10 +115,11 @@   Int         {- ^ draw width      -} ->   Int         {- ^ draw height     -} ->   Focus       {- ^ focus           -} ->+  Subfocus    {- ^ subfocus        -} ->   [Image']    {- ^ image lines     -} ->   Image       {- ^ rendered window -}-drawExtra st w h focus lineImages =-    msgImg <-> unpackImage (minorStatusLineImage focus w True st)+drawExtra st w h focus subfocus lineImages =+    msgImg <-> unpackImage (minorStatusLineImage focus subfocus w True st)   where     (_, msgImg) = messagePane w h 0 lineImages 
src/Client/Image/Message.hs view
@@ -41,7 +41,6 @@ import           Data.Char import           Data.Hashable (hash) import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap import           Data.HashSet (HashSet) import qualified Data.HashSet as HashSet import           Data.List@@ -64,7 +63,7 @@   , rendNicks      :: HashSet Identifier -- ^ nicknames to highlight   , rendMyNicks    :: HashSet Identifier -- ^ nicknames to highlight in red   , rendPalette    :: Palette -- ^ nick color palette-  , rendAccounts   :: HashMap Identifier UserAndHost+  , rendAccounts   :: Maybe (HashMap Identifier UserAndHost)   }  -- | Default 'MessageRendererParams' with no sigils or nicknames specified@@ -75,7 +74,7 @@   , rendNicks       = HashSet.empty   , rendMyNicks     = HashSet.empty   , rendPalette     = defaultPalette-  , rendAccounts    = HashMap.empty+  , rendAccounts    = Nothing   }  @@ -215,11 +214,19 @@       who n   = string (view palSigil pal) sigils <> ui         where           baseUI    = coloredUserInfo pal rm myNicks n-          ui = case rendAccounts rp ^? ix (userNick n) . uhAccount of-                 Just acct-                   | Text.null acct -> "~" <> baseUI-                   | mkId acct /= userNick n -> baseUI <> "(" <> text' defAttr (cleanText acct) <> ")"-                 _ -> baseUI+          ui = case rendAccounts rp of+                 Nothing -> baseUI -- not tracking any accounts+                 Just accts ->+                   let isKnown acct = not (Text.null acct || acct == "*")+                       mbAcct = accts+                             ^? ix (userNick n)+                              . uhAccount+                              . filtered isKnown in+                   case mbAcct of+                     Just acct+                       | mkId acct == userNick n -> baseUI+                       | otherwise -> baseUI <> "(" <> text' defAttr (cleanText acct) <> ")"+                     Nothing -> "~" <> baseUI   in   case body of     Join       {} -> mempty@@ -342,7 +349,7 @@         -- nick!user@host         plainWho n <> -        case rendAccounts rp ^? ix (userNick n) . uhAccount of+        case rendAccounts rp ^? folded . ix (userNick n) . uhAccount of           Just acct             | not (Text.null acct) -> text' quietAttr ("(" <> cleanText acct <> ")")           _ -> ""@@ -354,12 +361,17 @@       " is now known as " <>       coloredIdentifier pal NormalIdentifier myNicks new -    Join nick _chan acct ->+    Join nick _chan acct gecos ->       string quietAttr "join " <>       plainWho nick <>-      if Text.null acct-        then mempty-        else text' quietAttr ("(" <> cleanText acct <> ")")+      accountPart <> gecosPart+      where+        accountPart+          | Text.null acct = mempty+          | otherwise      = text' quietAttr ("(" <> cleanText acct <> ")")+        gecosPart+          | Text.null gecos = mempty+          | otherwise       = text' quietAttr (" [" <> cleanText gecos <> "]")      Part nick _chan mbreason ->       string quietAttr "part " <>
src/Client/Image/StatusLine.hs view
@@ -52,7 +52,7 @@       myNickImage st :       map unpackImage       [ focusImage (view clientFocus st) st-      , subfocusImage st+      , subfocusImage (view clientSubfocus st) st       , detailImage st       , nometaImage (view clientFocus st) st       , scrollImage st@@ -86,15 +86,17 @@ -- | The minor status line is used when rendering the @/splits@ and -- @/mentions@ views to show the associated window name. minorStatusLineImage ::-  Focus {- ^ window name          -} ->-  Int   {- ^ draw width           -} ->-  Bool  {- ^ show hidemeta status -} ->+  Focus       {- ^ window name          -} ->+  Subfocus    {- ^ subfocus             -} ->+  Int         {- ^ draw width           -} ->+  Bool        {- ^ show hidemeta status -} ->   ClientState {- ^ client state -} ->   Image'-minorStatusLineImage focus w showHideMeta st =+minorStatusLineImage focus subfocus w showHideMeta st =   content <> mconcat (replicate fillSize bar)   where     content = focusImage focus st <>+              subfocusImage subfocus st <>               if showHideMeta then nometaImage focus st else mempty      fillSize = max 0 (w - imageWidth content)@@ -113,10 +115,9 @@ -- | Indicate when the client is potentially showing a subset of the -- available chat messages. filterImage :: ClientState -> Image'-filterImage st =-  case clientMatcher st of-    Nothing -> mempty-    Just {} -> infoBubble (string attr "filtered")+filterImage st+  | clientIsFiltered st = infoBubble (string attr "filtered")+  | otherwise           = mempty   where     pal  = clientPalette st     attr = view palError pal@@ -306,11 +307,10 @@     modeImage m =       char (fromMaybe defAttr (view (at m) pal)) m -subfocusImage :: ClientState -> Image'-subfocusImage st = foldMap infoBubble (viewSubfocusLabel pal subfocus)+subfocusImage :: Subfocus -> ClientState -> Image'+subfocusImage subfocus st = foldMap infoBubble (viewSubfocusLabel pal subfocus)   where     pal         = clientPalette st-    subfocus    = view clientSubfocus st  focusImage :: Focus -> ClientState -> Image' focusImage focus st = infoBubble $ mconcat
src/Client/Message.hs view
@@ -87,7 +87,7 @@ ircSummary :: IrcMsg -> IrcSummary ircSummary msg =   case msg of-    Join who _ _    -> JoinSummary (userNick who)+    Join who _ _ _  -> JoinSummary (userNick who)     Part who _ _    -> PartSummary (userNick who)     Quit who _      -> QuitSummary (userNick who)     Nick who who'   -> NickSummary (userNick who) who'
src/Client/Network/Connect.hs view
@@ -23,22 +23,27 @@ import           Control.Applicative import           Control.Exception  (bracket) import           Control.Lens-import           Network.Socket     (PortNumber)+import qualified Data.Text.Encoding as Text+import           Network.Socket (PortNumber) import           Hookup  buildConnectionParams :: ServerSettings -> ConnectionParams buildConnectionParams args =-  let tlsParams = TlsParams-                    (view ssTlsClientCert args)-                    (view ssTlsClientKey  args <|> view ssTlsClientCert args)-                    (view ssTlsServerCert args)-                    (view ssTlsCiphers    args) -      family =-        case view ssProtocolFamily args of-          Nothing -> defaultFamily-          Just pf -> pf+  let tlsParams insecure = TlsParams+        { tpClientCertificate  = view ssTlsClientCert args+        , tpClientPrivateKey   = view ssTlsClientKey args <|> view ssTlsClientCert args+        , tpClientPrivateKeyPassword = privateKeyPassword+        , tpServerCertificate  = view ssTlsServerCert args+        , tpCipherSuite        = view ssTlsCiphers args+        , tpInsecure           = insecure+        } +      privateKeyPassword =+        case view ssTlsClientKeyPassword args of+          Just (SecretText str) -> PwBS (Text.encodeUtf8 str)+          _                     -> PwNone+       useSecure =         case view ssTls args of           UseInsecure    -> Nothing@@ -51,11 +56,11 @@                           (view ssSocksPort args)    in ConnectionParams-    { cpFamily = family-    , cpHost  = view ssHostName args+    { cpHost  = view ssHostName args     , cpPort  = ircPort args     , cpTls   = useSecure     , cpSocks = proxySettings+    , cpBind  = view ssBindHostName args     }  
src/Client/Options.hs view
@@ -126,18 +126,18 @@      print (generateDocs configurationSpec)  helpTxt :: String-helpTxt = usageInfo "glirc2 [FLAGS] INITIAL_NETWORKS..." options+helpTxt = usageInfo "glirc [FLAGS] INITIAL_NETWORKS..." options  tryHelpTxt :: String tryHelpTxt =-  "Run 'glirc2 --help' to see a list of available command line options."+  "Run 'glirc --help' to see a list of available command line options."  -- version information ---------------------------------------------  versionTxt :: String versionTxt = unlines   [ "glirc-" ++ showVersion version ++ gitHashTxt ++ gitDirtyTxt-  , "Copyright 2016 Eric Mertens"+  , "Copyright 2016-2020 Eric Mertens"   ]  fullVersionTxt :: String
src/Client/State.hs view
@@ -50,7 +50,9 @@    -- * Client operations   , withClientState-  , clientMatcher, Matcher(..), buildMatcher+  , clientIsFiltered+  , clientFilter+  , buildMatcher   , clientToggleHideMeta   , clientHighlightsNetwork   , channelUserList@@ -124,6 +126,7 @@ import           Client.State.Network import           Client.State.Window import           Client.State.DCC+import           ContextFilter import           Control.Applicative import           Control.Concurrent.MVar import           Control.Concurrent.STM@@ -153,6 +156,7 @@ import           Irc.UserInfo import           LensUtils import           RtsStats (Stats)+import qualified System.Random.MWC as Random import           Text.Regex.TDFA import           Text.Regex.TDFA.String (compile) @@ -164,7 +168,7 @@   , _clientActivityReturn    :: !(Maybe Focus)      -- ^ focus prior to jumping to activity   , _clientFocus             :: !Focus              -- ^ currently focused buffer   , _clientSubfocus          :: !Subfocus           -- ^ current view mode-  , _clientExtraFocus        :: ![Focus]            -- ^ extra messages windows to view+  , _clientExtraFocus        :: ![(Focus, Subfocus)]-- ^ extra messages windows to view    , _clientConnections       :: !(HashMap Text NetworkState) -- ^ state of active connections   , _clientEvents            :: !(TQueue NetworkEvent)    -- ^ incoming network event queue@@ -337,8 +341,8 @@      accounts =       if view (csSettings . ssShowAccounts) cs-      then view csUsers cs-      else HashMap.empty+      then Just (view csUsers cs)+      else Nothing   recordLogLine ::@@ -592,16 +596,15 @@  -- | Mark the messages on the current window (and any splits) as seen. markSeen :: ClientState -> ClientState-markSeen st =-  case view clientSubfocus st of-    FocusMessages -> foldl' aux st focuses-    _             -> st+markSeen st = foldl' aux st messageFocuses   where     aux acc focus = overStrict (clientWindows . ix focus) windowSeen acc -    focuses = view clientFocus st-            : view clientExtraFocus st+    messageFocuses = [focus | (focus, FocusMessages) <- allFocuses] +    allFocuses = (view clientFocus st, view clientSubfocus st)+               : view clientExtraFocus st+ -- | Add the textbox input to the edit history and clear the textbox. consumeInput :: ClientState -> ClientState consumeInput = over clientTextBox Edit.success@@ -650,31 +653,63 @@            Nothing -> Nothing            Just r  -> Just r +clientIsFiltered :: ClientState -> Bool+clientIsFiltered = isJust . clientMatcher++clientFilter :: ClientState -> (a -> LText.Text) -> [a] -> [a]+clientFilter st f xs =+  case clientMatcher st of+    Nothing -> xs+    Just m  ->+      filterContext+        (matcherBefore m)+        (matcherAfter m)+        (matcherPred m . f)+        xs++data MatcherArgs = MatcherArgs+  { argAfter     :: !Int+  , argBefore    :: !Int+  , argInvert    :: !Bool+  , argSensitive :: !Bool+  }++defaultMatcherArgs :: MatcherArgs+defaultMatcherArgs = MatcherArgs+  { argAfter     = 0+  , argBefore    = 0+  , argInvert    = False+  , argSensitive = True+  }+ buildMatcher :: String -> Maybe Matcher-buildMatcher = go (True, 0, 0)+buildMatcher = go defaultMatcherArgs   where-    go (sensitive, b, a) reStr =+    go !args reStr =       case dropWhile (' '==) reStr of-        '-' : 'i' : ' ' : reStr'                                            -> go (False, b, a) reStr'-        '-' : 'A' : reStr' | [(a' , ' ':reStr'')] <- reads reStr', a'  >= 0 -> go (sensitive, b, a') reStr''-        '-' : 'B' : reStr' | [(b' , ' ':reStr'')] <- reads reStr', b'  >= 0 -> go (sensitive, b', a) reStr''-        '-' : 'C' : reStr' | [(num, ' ':reStr'')] <- reads reStr', num >= 0 -> go (sensitive, num, num) reStr''-        '-' : '-' : reStr' -> finish (sensitive, b, a) (drop 1 reStr')-        _ -> finish (sensitive, b, a) reStr+        '-' : 'i' : ' ' : reStr' -> go args{argSensitive=False} reStr'+        '-' : 'v' : ' ' : reStr' -> go args{argInvert=True} reStr'+        '-' : 'A' : reStr' | [(a,' ':reStr'')] <- reads reStr', a>=0 -> go args{argAfter=a} reStr''+        '-' : 'B' : reStr' | [(b,' ':reStr'')] <- reads reStr', b>=0 -> go args{argBefore=b} reStr''+        '-' : 'C' : reStr' | [(c,' ':reStr'')] <- reads reStr', c>=0 -> go args{argAfter=c,argBefore=c} reStr''+        '-' : '-' : ' ' : reStr' -> finish args reStr'+        _ -> finish args reStr -    finish (sensitive, b, a) reStr =-      case compile defaultCompOpt{caseSensitive=sensitive}+    finish args reStr =+      case compile defaultCompOpt{caseSensitive=argSensitive args}                    defaultExecOpt{captureGroups=False}                    reStr of         Left{}  -> Nothing-        Right r -> Just (Matcher b a (matchTest r . LText.unpack))+        Right r+          | argInvert args -> Just (Matcher (argBefore args) (argAfter args) (not . matchTest r . LText.unpack))+          | otherwise      -> Just (Matcher (argBefore args) (argAfter args) (      matchTest r . LText.unpack))  -- | Compute the command and arguments currently in the textbox. clientActiveCommand ::   ClientState           {- ^ client state                     -} ->   Maybe (String,String) {- ^ command name and argument string -} clientActiveCommand st =-  case break (==' ') (clientFirstLine st) of+  case break (==' ') (dropWhile (' '==) (clientFirstLine st)) of     ('/':cmd,_:args) -> Just (cmd,args)     _                -> Nothing @@ -729,10 +764,31 @@                      , _ssHostName = Text.unpack network                      } -         settings = fromMaybe defSettings-                  $ preview (clientConfig . configServers . ix network) st+     eSettings0 <-+       try $+       loadSecrets $+       fromMaybe defSettings $+       preview (clientConfig . configServers . ix network) st -     now <- getCurrentTime+     case eSettings0 of+       Left (SecretException label err) ->+         do now <- getZonedTime+            let txt = "Failed loading secret \x02" <> label <> "\x02: " <> err+            pure $! recordError now network (Text.pack txt) st++       Right settings0 ->+         do settings1 <- applyStsPolicy stsUpgrade settings0 st+            -- don't bother delaying on the first reconnect+            let delay = 15 * max 0 (attempts - 1)+            c <- createConnection delay settings1+            seed <- Random.withSystemRandom (Random.asGenIO Random.save)+            let cs = newNetworkState network settings1 c (PingConnecting attempts lastTime) seed+            traverse_ (sendMsg cs) (initialMessages cs)+            pure (set (clientConnections . at network) (Just cs) st)++applyStsPolicy :: Maybe Int -> ServerSettings -> ClientState -> IO ServerSettings+applyStsPolicy stsUpgrade settings st =+  do now <- getCurrentTime      let stsUpgrade'            | Just{} <- stsUpgrade = stsUpgrade            | UseInsecure <- view ssTls settings@@ -741,26 +797,10 @@            , now < view stsExpiration policy            = Just (view stsPort policy)            | otherwise = Nothing--         settings1 =-           case stsUpgrade' of-             Just port -> set ssPort (Just (fromIntegral port))-                        $ set ssTls UseTls settings-             Nothing   -> settings---         -- don't bother delaying on the first reconnect-         delay = 15 * max 0 (attempts - 1)--     c <- createConnection-            delay-            settings1--     let cs = newNetworkState network settings1 c (PingConnecting attempts lastTime)-     traverse_ (sendMsg cs) (initialMessages cs)--     return $ set (clientConnections . at network) (Just cs) st-+     pure $ case stsUpgrade' of+              Just port -> set ssPort (Just (fromIntegral port))+                         $ set ssTls UseTls settings+              Nothing   -> settings  applyMessageToClientState ::   ZonedTime                  {- ^ timestamp                -} ->@@ -832,12 +872,11 @@   -- | List of extra focuses to display as split windows-clientExtraFocuses :: ClientState -> [Focus]+clientExtraFocuses :: ClientState -> [(Focus, Subfocus)] clientExtraFocuses st =-  case view clientSubfocus st of-    FocusMessages -> view clientFocus st `delete` view clientExtraFocus st-    _             -> []-+  delete+    (view clientFocus st, view clientSubfocus st)+    (view clientExtraFocus st)  ------------------------------------------------------------------------ -- Focus Management@@ -899,21 +938,26 @@      -- Don't deactivate a window if it's going to stay active     deactivatePrevious-      | oldFocus `elem` focus : view clientExtraFocus st = id+      | (oldFocus, FocusMessages) `elem` (focus, FocusMessages) : view clientExtraFocus st = id       | otherwise = over (clientWindows . ix oldFocus) windowDeactivate   -- | Unified logic for assigning to the extra focuses field that activates -- and deactivates windows as needed.-setExtraFocus :: [Focus] -> ClientState -> ClientState+setExtraFocus :: [(Focus, Subfocus)] -> ClientState -> ClientState setExtraFocus newFocuses st   = aux windowDeactivate newlyInactive   $ aux windowActivate   newlyActive   $ set clientExtraFocus newFocuses st   where-    newlyActive = newFocuses \\ (view clientFocus st : view clientExtraFocus st)+    messagePart x = [focus | (focus, FocusMessages) <- x] -    newlyInactive = view clientExtraFocus st \\ (view clientFocus st : newFocuses)+    current = (view clientFocus st, view clientSubfocus st)++    newlyActive = messagePart newFocuses \\ messagePart (current : view clientExtraFocus st)++    newlyInactive = messagePart (view clientExtraFocus st)+                 \\ messagePart (current : newFocuses)      aux f xs st1 =       foldl' (\acc w -> overStrict (clientWindows . ix w) f acc) st1 xs
src/Client/State/DCC.hs view
@@ -73,8 +73,7 @@ import           Irc.Identifier (Identifier, idText) import           Irc.Message (IrcMsg(..)) import           Irc.UserInfo (UserInfo(..), uiNick)-import           Network.Socket ( HostName, PortNumber, Family(..)-                                , hostAddressToTuple )+import           Network.Socket (HostName, PortNumber, hostAddressToTuple) import           System.FilePath ((</>), takeFileName) import           System.IO (withFile, IOMode(..), openFile, hClose, hFileSize) @@ -185,7 +184,7 @@                                     wait outThread   where     param = ConnectionParams-              { cpFamily = AF_INET+              { cpBind   = Just "0.0.0.0" -- only support IPv4               , cpHost   = from               , cpPort   = port               , cpSocks  = Nothing
src/Client/State/Network.hs view
@@ -46,6 +46,7 @@   , csCertificate   , csMessageHooks   , csAuthenticationState+  , csSeed    -- * Cross-message state   , Transaction(..)@@ -81,6 +82,7 @@ import           Client.Hook (MessageHook) import           Client.Hooks (messageHooks) import           Control.Lens+import qualified Control.Monad.ST as ST import           Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import qualified Data.HashSet as HashSet@@ -105,6 +107,7 @@ import           Irc.RawIrcMsg import           Irc.UserInfo import           LensUtils+import qualified System.Random.MWC as Random  -- | State tracked for each IRC connection data NetworkState = NetworkState@@ -131,6 +134,9 @@   , _csPingStatus   :: !PingStatus      -- ^ state of ping timer   , _csLastReceived :: !(Maybe UTCTime) -- ^ time of last message received   , _csCertificate  :: ![Text]++  -- Randomization+  , _csSeed         :: Random.Seed   }  -- | State of the authentication transaction@@ -161,7 +167,6 @@   | NamesTransaction [Text]   | BanTransaction [(Text,MaskListEntry)]   | WhoTransaction [UserInfo]-  | CapTransaction   | CapLsTransaction [(Text, Maybe Text)]   deriving Show @@ -242,15 +247,16 @@   ServerSettings    {- ^ server settings           -} ->   NetworkConnection {- ^ active network connection -} ->   PingStatus        {- ^ initial ping status       -} ->+  Random.Seed       {- ^ initial random seed       -} ->   NetworkState      {- ^ new network state         -}-newNetworkState network settings sock ping = NetworkState+newNetworkState network settings sock ping seed = NetworkState   { _csUserInfo     = UserInfo "*" "" ""   , _csChannels     = HashMap.empty   , _csSocket       = sock   , _csChannelTypes = defaultChannelTypes   , _csModeTypes    = defaultModeTypes   , _csUmodeTypes   = defaultUmodeTypes-  , _csTransaction  = CapTransaction+  , _csTransaction  = NoTransaction   , _csModes        = ""   , _csSnomask      = ""   , _csStatusMsg    = ""@@ -265,6 +271,7 @@   , _csNextPingTime = Nothing   , _csLastReceived = Nothing   , _csCertificate  = []+  , _csSeed         = seed   }  buildMessageHooks :: [HookConfig] -> [MessageHook]@@ -294,7 +301,7 @@   case msg of     Ping args -> ([ircPong args], cs)     Pong _    -> noReply $ doPong msgWhen cs-    Join user chan acct ->+    Join user chan acct _ ->          ( reply          , recordUser user acct          $ overChannel chan (joinChannel (userNick user))@@ -331,16 +338,22 @@          $ overChannels (nickChange (userNick oldNick) newNick) cs      Reply RPL_WELCOME (me:_) -> doWelcome msgWhen (mkId me) cs-    Reply RPL_SASLSUCCESS _ -> endCapTransaction cs-    Reply RPL_SASLFAIL _ -> endCapTransaction cs+    Reply RPL_SASLSUCCESS _ -> ([ircCapEnd], cs)+    Reply RPL_SASLFAIL _ -> ([ircCapEnd], cs)+     Reply ERR_NICKNAMEINUSE (_:badnick:_)       | PingConnecting{} <- view csPingStatus cs -> doBadNick badnick cs+    Reply ERR_BANNEDNICK (_:badnick:_)+      | PingConnecting{} <- view csPingStatus cs -> doBadNick badnick cs+    Reply ERR_ERRONEUSNICKNAME (_:badnick:_)+      | PingConnecting{} <- view csPingStatus cs -> doBadNick badnick cs+     Reply RPL_HOSTHIDDEN (_:host:_) ->         noReply (set (csUserInfo . uiHost) host cs)      -- /who <#channel> %tuhna,616     Reply RPL_WHOSPCRPL [_me,"616",user,host,nick,acct] ->-       let acct' = if acct == "0" then "" else acct+       let acct' = if acct == "0" then "*" else acct        in noReply (recordUser (UserInfo (mkId nick) user host) acct' cs)      Reply code args        -> noReply (doRpl code msgWhen args cs)@@ -377,7 +390,6 @@   . set csNick me   . set csNextPingTime (Just $! addUTCTime 30 (zonedTimeToUTC msgWhen))   . set csPingStatus PingNone-  . set csTransaction NoTransaction -- wipe out CapTransaction if it was active  -- | Handle 'ERR_NICKNAMEINUSE' errors when connecting. doBadNick ::@@ -387,8 +399,23 @@ doBadNick badNick cs =   case NonEmpty.dropWhile (badNick/=) (view (csSettings . ssNicks) cs) of     _:next:_ -> ([ircNick next], cs)-    _        -> ([], cs)+    _        -> doRandomNick cs +-- | Pick a random nickname now that we've run out of choices+doRandomNick :: NetworkState -> ([RawIrcMsg], NetworkState)+doRandomNick cs = ([ircNick candidate], cs')+  where+    limit       = 9 -- RFC 2812 puts the maximum nickname length as low as 9!+    range       = (0, 99999::Int) -- up to 5 random digits+    suffix      = show n+    primaryNick = NonEmpty.head (view (csSettings . ssNicks) cs)+    candidate   = Text.take (limit-length suffix) primaryNick <> Text.pack suffix+    cs'         = set csSeed seed' cs++    (n, seed')  = ST.runST (do gen <- Random.restore (view csSeed cs)+                               (,) <$> Random.uniformR range gen <*> Random.save gen+                           )+ doTopic :: ZonedTime -> UserInfo -> Identifier -> Text -> NetworkState -> NetworkState doTopic when user chan topic =   overChannel chan (setTopic topic . set chanTopicProvenance (Just $! prov))@@ -709,31 +736,30 @@       | otherwise                                    = []      ss = view csSettings cs-    sasl = ["sasl" | isJust (view ssSaslUsername ss)-                   , isJust (view ssSaslPassword ss) ||-                     isJust (view ssSaslEcdsaFile ss) ||-                     isJust (view ssTlsClientCert ss) ]+    sasl = ["sasl" | isJust (view ssSaslMechanism ss) ]  doAuthenticate :: Text -> NetworkState -> ([RawIrcMsg], NetworkState) doAuthenticate param cs =   case view csAuthenticationState cs of     AS_PlainStarted-      | "+"       <- param-      , Just user <- view ssSaslUsername ss-      , Just pass <- view ssSaslPassword ss-      -> (ircAuthenticates (encodePlainAuthentication user pass),+      | "+" <- param+      , Just (SaslPlain mbAuthz authc (SecretText pass)) <- view ssSaslMechanism ss+      , let authz = fromMaybe "" mbAuthz+      -> (ircAuthenticates (encodePlainAuthentication authz authc pass),           set csAuthenticationState AS_None cs)      AS_ExternalStarted-      | "+"       <- param-      , Just user <- view ssSaslUsername ss-      -> (ircAuthenticates (encodeExternalAuthentication user),+      | "+" <- param+      , Just (SaslExternal mbAuthz) <- view ssSaslMechanism ss+      , let authz = fromMaybe "" mbAuthz+      -> (ircAuthenticates (encodeExternalAuthentication authz),           set csAuthenticationState AS_None cs)      AS_EcdsaStarted-      | "+"       <- param-      , Just user <- view ssSaslUsername ss-      -> (ircAuthenticates (Ecdsa.encodeUsername user),+      | "+" <- param+      , Just (SaslEcdsa mbAuthz authc _) <- view ssSaslMechanism ss+      , let authz = fromMaybe authc mbAuthz+      -> (ircAuthenticates (Ecdsa.encodeAuthentication authz authc),           set csAuthenticationState AS_EcdsaWaitChallenge cs)      AS_EcdsaWaitChallenge -> ([], cs) -- handled in Client.EventLoop!@@ -753,10 +779,11 @@         prevCaps = view (csTransaction . _CapLsTransaction) cs      CapLs CapDone caps-      | null reqCaps -> endCapTransaction cs-      | otherwise    -> ([ircCapReq reqCaps], cs)+      | null reqCaps -> ([ircCapEnd], cs')+      | otherwise    -> ([ircCapReq reqCaps], cs')       where         reqCaps = selectCaps cs (caps ++ view (csTransaction . _CapLsTransaction) cs)+        cs' = set csTransaction NoTransaction cs      CapNew caps       | null reqCaps -> ([], cs)@@ -767,32 +794,26 @@     CapDel _ -> ([],cs)      CapAck caps-      | "sasl" `elem` caps && isJust (view ssSaslUsername ss) ->-          if isJust (view ssSaslEcdsaFile ss)-            then ( [ircAuthenticate Ecdsa.authenticationMode]-                 , set csAuthenticationState AS_EcdsaStarted cs)-          else if isJust (view ssSaslPassword ss)-            then ( [ircAuthenticate "PLAIN"]-                 , set csAuthenticationState AS_PlainStarted cs)-          else if isJust (view ssTlsClientCert ss)-            then ( [ircAuthenticate "EXTERNAL"]-                 , set csAuthenticationState AS_ExternalStarted cs)-          else endCapTransaction cs-      where-        ss = view csSettings cs--    _ -> endCapTransaction cs+      | let ss = view csSettings cs+      , "sasl" `elem` caps+      , Just mech <- view ssSaslMechanism ss ->+        case mech of+          SaslEcdsa{} ->+            ([ircAuthenticate Ecdsa.authenticationMode],+             set csAuthenticationState AS_EcdsaStarted cs)+          SaslPlain{} ->+            ([ircAuthenticate "PLAIN"],+             set csAuthenticationState AS_PlainStarted cs)+          SaslExternal{} ->+            ([ircAuthenticate "EXTERNAL"],+             set csAuthenticationState AS_ExternalStarted cs) -endCapTransaction :: NetworkState -> ([RawIrcMsg], NetworkState)-endCapTransaction cs =-  case view csTransaction cs of-    CapTransaction -> ([ircCapEnd], set csTransaction NoTransaction cs)-    _              -> ([], cs)+    _ -> ([ircCapEnd], cs)  initialMessages :: NetworkState -> [RawIrcMsg] initialMessages cs    = [ ircCapLs ]-  ++ [ ircPass pass | Just pass <- [view ssPassword ss]]+  ++ [ ircPass pass | Just (SecretText pass) <- [view ssPassword ss]]   ++ [ ircNick (views ssNicks NonEmpty.head ss)      , ircUser (view ssUser ss) (view ssReal ss)      ]
src/Client/View/Digraphs.hs view
@@ -32,11 +32,10 @@   = map (mconcat . intersperse sep)   $ chunksOf entriesPerLine   $ map (text' defAttr)-  $ matcher+  $ clientFilter st LText.fromStrict   $ map (Text.pack . drawEntry)   $ Text.chunksOf 3 digraphs   where-    matcher        = maybe id (\m -> filter (matcherPred m . LText.fromStrict)) (clientMatcher st)     entriesPerLine = max 1 -- just in case?                    $ (w + sepWidth) `quot` (entryWidth + sepWidth) 
src/Client/View/KeyMap.hs view
@@ -21,14 +21,15 @@ import           Graphics.Vty.Attributes import           Graphics.Vty.Input --- | Render the lines of a table showing all of the available digraph entries+-- | Show the client keybindings keyMapLines ::   ClientState {- ^ client state -} ->   [Image']    {- ^ output lines -}-keyMapLines-  = renderEntries-  . keyMapEntries-  . view (clientConfig . configKeyMap)+keyMapLines st+  = clientFilter st imageText+  $ renderEntries+  $ keyMapEntries+  $ view (clientConfig . configKeyMap) st  renderEntries :: [([Modifier], Key, Action)] -> [Image'] renderEntries entries =
src/Client/View/MaskList.hs view
@@ -59,12 +59,10 @@                  char (view palLabel pal) '/' <>                  string defAttr (show (HashMap.size entries)) -    matcher = maybe (const True) matcherPred (clientMatcher st) . LText.fromStrict--    matcher' (mask,entry) = matcher mask || matcher (view maskListSetter entry)+    filterOn (mask,entry) = LText.fromChunks [mask, " ", view maskListSetter entry]      entryList = sortBy (flip (comparing (view (_2 . maskListTime))))-              $ filter matcher'+              $ clientFilter st filterOn               $ HashMap.toList entries      renderWhen = formatTime defaultTimeLocale " %F %T"
src/Client/View/Mentions.hs view
@@ -26,7 +26,6 @@ import           Control.Lens import qualified Data.Map as Map import           Data.Time (UTCTime)-import           ContextFilter (filterContext)  -- | Generate the list of message lines marked important ordered by -- time. Each run of lines from the same channel will be grouped@@ -42,10 +41,9 @@     padAmt = view (clientConfig . configNickPadding) st     palette = clientPalette st -    filt =-      case clientMatcher st of-        Nothing -> filter (\x -> WLImportant == view wlImportance x)-        Just (Matcher b a p) -> filterContext b a (views wlText p)+    filt+      | clientIsFiltered st = filter (\x -> WLImportant == view wlImportance x)+      | otherwise           = clientFilter st (view wlText)      entries = merge               [windowEntries filt palette w padAmt detail n focus v@@ -68,7 +66,7 @@   [Image']      {- ^ mention images and channel labels -} addMarkers _ _ [] = [] addMarkers w !st (!ml : xs)-  = minorStatusLineImage (mlFocus ml) w False st+  = minorStatusLineImage (mlFocus ml) FocusMessages w False st   : concatMap mlImage (ml:same)  ++ addMarkers w st rest   where
src/Client/View/Messages.hs view
@@ -31,7 +31,6 @@ import           Irc.Identifier import           Irc.Message import           Irc.UserInfo-import           ContextFilter (filterContext)   chatMessageImages :: Focus -> Int -> ClientState -> [Image']@@ -41,9 +40,9 @@     Just win ->       let msgs     = toListOf each (view winMessages win)           hideMeta = view winHideMeta win in-      case clientMatcher st of-        Just (Matcher a b p) -> windowLineProcessor hideMeta (filterContext b a (views wlText p) msgs)-        Nothing ->+      if clientIsFiltered st+        then windowLineProcessor hideMeta (clientFilter st (view wlText) msgs)+        else           case view winMarker win of             Nothing -> windowLineProcessor hideMeta msgs             Just n  ->
src/Client/View/UserList.hs view
@@ -53,8 +53,6 @@   where     countImage = drawSigilCount pal (map snd usersList) -    matcher = maybe (const True) matcherPred (clientMatcher st)-     myNicks = clientHighlights cs st      renderUser (ident, sigils) =@@ -63,10 +61,10 @@      gap = char defAttr ' ' -    matcher' (ident,sigils) = matcher (LText.fromChunks [Text.pack sigils, idText ident])+    filterOn (ident,sigils) = LText.fromChunks [Text.pack sigils, idText ident]      usersList = sortBy (comparing fst)-              $ filter matcher'+              $ clientFilter st filterOn               $ HashMap.toList usersHashMap      pal = clientPalette st@@ -106,8 +104,6 @@ userInfoImages' :: NetworkState -> Identifier -> ClientState -> [Image'] userInfoImages' cs channel st = countImage : map renderEntry usersList   where-    matcher = maybe (const True) matcherPred (clientMatcher st)-     countImage = drawSigilCount pal (map snd usersList)      myNicks = clientHighlights cs st@@ -119,8 +115,8 @@       coloredUserInfo pal DetailedRender myNicks info <>       " " <> text' (view palMeta pal) (cleanText acct) -    matcher' ((info, acct),sigils) =-      matcher (LText.fromChunks [Text.pack sigils, renderUserInfo info, " ", acct])+    filterOn ((info, acct),sigils) =+      LText.fromChunks [Text.pack sigils, renderUserInfo info, " ", acct]      userInfos = view csUsers cs @@ -130,7 +126,7 @@         Nothing                  -> (UserInfo nick "" "", "")      usersList = sortBy (flip (comparing (userNick . fst . fst)))-              $ filter matcher'+              $ clientFilter st filterOn               $ map (over _1 toInfo)               $ HashMap.toList usersHashMap