packages feed

glirc 2.16 → 2.17

raw patch · 25 files changed

+1023/−550 lines, 25 filesdep ~vtyPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: vty

API changes (from Hackage documentation)

- Client.State: clientWindowHeights :: ClientState -> (Int, Int)
- Client.State: pageDown :: ClientState -> ClientState
- Client.State: pageUp :: ClientState -> ClientState
+ Client.Commands: [cmdArgumentSpec] :: Command -> ArgumentSpec a
+ Client.Commands: [cmdDocumentation] :: Command -> Text
+ Client.Commands: [cmdImplementation] :: Command -> CommandImpl a
+ Client.Commands: [cmdNames] :: Command -> NonEmpty Text
+ Client.Commands.WordCompletion: WordCompletionMode :: String -> WordCompletionMode
+ Client.Commands.WordCompletion: [wcmStartPrefix, wcmStartSuffix, wcmMiddlePrefix, wcmMiddleSuffix] :: WordCompletionMode -> String
+ Client.Commands.WordCompletion: data WordCompletionMode
+ Client.Commands.WordCompletion: defaultNickWordCompleteMode :: WordCompletionMode
+ Client.Commands.WordCompletion: instance GHC.Show.Show Client.Commands.WordCompletion.WordCompletionMode
+ Client.Commands.WordCompletion: plainWordCompleteMode :: WordCompletionMode
+ Client.Commands.WordCompletion: slackNickWordCompleteMode :: WordCompletionMode
+ Client.Configuration.ServerSettings: [_ssAutoconnect] :: ServerSettings -> Bool
+ Client.Configuration.ServerSettings: [_ssNickCompletion] :: ServerSettings -> WordCompletionMode
+ Client.Configuration.ServerSettings: [_ssReconnectAttempts] :: ServerSettings -> !Int
+ Client.Configuration.ServerSettings: ssAutoconnect :: Lens' ServerSettings Bool
+ Client.Configuration.ServerSettings: ssNickCompletion :: Lens' ServerSettings WordCompletionMode
+ Client.Configuration.ServerSettings: ssReconnectAttempts :: Lens' ServerSettings Int
+ Client.Image: scrollAmount :: ClientState -> Int
+ Client.Image.Message: cleanText :: Text -> Text
+ Client.Image.StatusLine: activityBarImage :: ClientState -> Image
+ Client.Options: [_optNoConnect] :: Options -> Bool
+ Client.Options: optNoConnect :: Lens' Options Bool
+ Client.State: [_clientRegex] :: ClientState -> (Maybe Regex)
+ Client.State: clientActiveCommand :: ClientState -> Maybe (String, String)
+ Client.State: clientActiveRegex :: ClientState -> Maybe Regex
+ Client.State: clientAutoconnects :: ClientState -> [Text]
+ Client.State: clientRegex :: Lens' ClientState (Maybe Regex)
+ Client.State: currentNickCompletionMode :: ClientState -> WordCompletionMode
+ Client.State: scrollClient :: Int -> ClientState -> ClientState
+ Client.State.Focus: AllWindows :: WindowsFilter
+ Client.State.Focus: ChannelWindows :: WindowsFilter
+ Client.State.Focus: NetworkWindows :: WindowsFilter
+ Client.State.Focus: UserWindows :: WindowsFilter
+ Client.State.Focus: data WindowsFilter
+ Client.State.Focus: instance GHC.Classes.Eq Client.State.Focus.WindowsFilter
+ Client.State.Focus: instance GHC.Show.Show Client.State.Focus.WindowsFilter
+ Client.State.Network: sendModeration :: Identifier -> [RawIrcMsg] -> NetworkState -> IO NetworkState
+ Client.State.Network: sendTopic :: Identifier -> Text -> NetworkState -> IO ()
+ Client.State.Network: useChanServ :: Identifier -> NetworkState -> Bool
+ Client.View.UrlSelection: urlSelectionView :: Focus -> ClientState -> [Image]
+ Config.FromConfig: instance Config.FromConfig.FromConfig GHC.Types.Int
- Client.Commands: Command :: (ArgumentSpec a) -> Text -> (CommandImpl a) -> Command
+ Client.Commands: Command :: NonEmpty Text -> ArgumentSpec a -> Text -> CommandImpl a -> Command
- Client.Commands: commandsList :: [(NonEmpty Text, Command)]
+ Client.Commands: commandsList :: [Command]
- Client.Commands.WordCompletion: wordComplete :: Prefix a => (String -> String) -> Bool -> [a] -> [a] -> EditBox -> Maybe EditBox
+ Client.Commands.WordCompletion: wordComplete :: Prefix a => WordCompletionMode -> Bool -> [a] -> [a] -> EditBox -> Maybe EditBox
- Client.Configuration.ServerSettings: ServerSettings :: !(NonEmpty Text) -> !Text -> !Text -> !Text -> !(Maybe Text) -> !(Maybe Text) -> !(Maybe Text) -> !HostName -> !(Maybe PortNumber) -> !UseTls -> !(Maybe FilePath) -> !(Maybe FilePath) -> ![[ExpansionChunk]] -> !(Maybe HostName) -> !PortNumber -> ![FilePath] -> ![Identifier] -> !Rational -> !Rational -> ![Text] -> !(Maybe Text) -> ServerSettings
+ Client.Configuration.ServerSettings: ServerSettings :: !(NonEmpty Text) -> !Text -> !Text -> !Text -> !(Maybe Text) -> !(Maybe Text) -> !(Maybe Text) -> !HostName -> !(Maybe PortNumber) -> !UseTls -> !(Maybe FilePath) -> !(Maybe FilePath) -> ![[ExpansionChunk]] -> !(Maybe HostName) -> !PortNumber -> ![FilePath] -> ![Identifier] -> !Rational -> !Rational -> ![Text] -> !(Maybe Text) -> !Int -> Bool -> WordCompletionMode -> ServerSettings
- Client.Options: Options :: Maybe FilePath -> [Text] -> Bool -> Bool -> Options
+ Client.Options: Options :: Maybe FilePath -> [Text] -> Bool -> Bool -> Bool -> Options
- Client.State: ClientState :: !(Map Focus Window) -> !Focus -> !Focus -> !Subfocus -> ![Focus] -> !(IntMap NetworkState) -> !Int -> !ConnectionContext -> !(TQueue NetworkEvent) -> !(HashMap Text NetworkId) -> !Configuration -> !Vty -> !EditBox -> !Int -> !Int -> !Int -> !Bool -> !Bool -> !Bool -> !Bool -> !(HashSet Identifier) -> !ExtensionState -> ClientState
+ Client.State: ClientState :: !(Map Focus Window) -> !Focus -> !Focus -> !Subfocus -> ![Focus] -> !(IntMap NetworkState) -> !Int -> !ConnectionContext -> !(TQueue NetworkEvent) -> !(HashMap Text NetworkId) -> !Configuration -> !Vty -> !EditBox -> !Int -> !Int -> !Int -> !Bool -> !Bool -> !Bool -> (Maybe Regex) -> !Bool -> !(HashSet Identifier) -> !ExtensionState -> ClientState
- Client.State.EditBox: class HasLine c_aKQH where pos = (.) line pos text = (.) line text
+ Client.State.EditBox: class HasLine c_aJgF where pos = (.) line pos text = (.) line text
- Client.State.EditBox: line :: HasLine c_aKQH => Lens' c_aKQH Line
+ Client.State.EditBox: line :: HasLine c_aJgF => Lens' c_aJgF Line
- Client.State.EditBox: pos :: HasLine c_aKQH => Lens' c_aKQH Int
+ Client.State.EditBox: pos :: HasLine c_aJgF => Lens' c_aJgF Int
- Client.State.EditBox: text :: HasLine c_aKQH => Lens' c_aKQH String
+ Client.State.EditBox: text :: HasLine c_aJgF => Lens' c_aJgF String
- Client.State.EditBox.Content: class HasLine c_aKQH where pos = (.) line pos text = (.) line text
+ Client.State.EditBox.Content: class HasLine c_aJgF where pos = (.) line pos text = (.) line text
- Client.State.EditBox.Content: line :: HasLine c_aKQH => Lens' c_aKQH Line
+ Client.State.EditBox.Content: line :: HasLine c_aJgF => Lens' c_aJgF Line
- Client.State.EditBox.Content: pos :: HasLine c_aKQH => Lens' c_aKQH Int
+ Client.State.EditBox.Content: pos :: HasLine c_aJgF => Lens' c_aJgF Int
- Client.State.EditBox.Content: text :: HasLine c_aKQH => Lens' c_aKQH String
+ Client.State.EditBox.Content: text :: HasLine c_aJgF => Lens' c_aJgF String
- Client.State.Focus: FocusWindows :: Subfocus
+ Client.State.Focus: FocusWindows :: WindowsFilter -> Subfocus
- Client.View.Windows: windowsImages :: ClientState -> [Image]
+ Client.View.Windows: windowsImages :: WindowsFilter -> ClientState -> [Image]

Files

ChangeLog.md view
@@ -1,5 +1,14 @@ # Revision history for glirc2 +## 2.17++* Add `reconnect-attempts` setting+* Add peristence for `/grep` and `/grepi`+* Add filter argument to `/windows`+* Better tab completion for `/channel` and `/focus`+* Isolate and number urls in view with `/url`+* Map `M-Left` and `M-Right` to backward word and forward word+ ## 2.16  * Add `/splits` to show multiple chat windows simultaneously
README.md view
@@ -3,6 +3,8 @@  [![Build Status](https://secure.travis-ci.org/glguy/irc-core.svg)](http://travis-ci.org/glguy/irc-core) +[Wiki Documentation](https://github.com/glguy/irc-core/wiki)+ ![](https://raw.githubusercontent.com/wiki/glguy/irc-core/images/screenshot.png)  Building@@ -31,7 +33,7 @@ $ cabal build ``` -Building with stack using ghc-8 resolver (nightly resolvers can work using --solver)+Building with stack using ghc-8 resolver (lts-7 resolvers can work using --solver)  ``` $ stack init --resolver=ghc-8@@ -73,6 +75,7 @@ ``` glirc [FLAGS] INITIAL_NETWORKS...   -c PATH  --config=PATH  Configuration file path+  -!       --noconnect    Disable autoconnecting   -h       --help         Show help   -v       --version      Show version ```@@ -176,6 +179,7 @@ | `url-opener`       | text                | Command to execute with URL parameter for `/url` e.g. gnome-open on GNOME or open on macOS | | `ignores`          | list of text        | Initial list of nicknames to ignore                                                        | | `activity-bar`     | yes or no           | Initial setting for visibility of activity bar (default no)                                |+| `macros`           | list of macros      | User-configurable client commands                                                          |  Server Settings ---------------@@ -202,6 +206,9 @@ | `flood-penalty`       | number               | cost in seconds per message                                    | | `flood-threshold`     | number               | threshold in seconds for burst                                 | | `message-hooks`       | list of text         | names of hooks to enable                                       |+| `reconnect-attempts`  | int                  | number of reconnections to attempt on error                    |+| `autoconnect`         | yes or no            | automatically connect at client startup                        |+| `nick-completion`     | default or slack     | set this to slack to use `@` sigils when completing nicks      |  Palette -------@@ -263,7 +270,7 @@ * `/disconnect` - Forcefully terminate connection to the current server * `/reconnect` - Reconnect to the current server * `/reload [path]` - Load a new configuration file (optional path)-* `/windows` - List all open windows+* `/windows [filter]` - List all open windows (filters: networks, channels, users) * `/palette` - Show the client palette * `/mentions` - Show all the highlighted lines across all windows * `/extension <extension name> <params...>` - Send the given params to the named extension@@ -367,6 +374,8 @@ * `C-t` swap characters at cursor * `M-f` forward word * `M-b` backward word+* `M-Right` forward word+* `M-Left` backward word * `M-Backspace` delete word backwards * `M-d` delete word forwards * `M-Enter` insert newline
exec/Main.hs view
@@ -13,6 +13,7 @@ import Control.Concurrent import Control.Lens import Control.Monad+import Data.List (nub) import Data.Text (Text) import System.Exit import System.IO@@ -31,9 +32,16 @@      runInUnboundThread $        withClientState cfg $        clientStartExtensions >=>-       addInitialNetworks (view optInitialNetworks opts) >=>+       initialNetworkLogic opts >=>        eventLoop +initialNetworkLogic :: Options -> ClientState -> IO ClientState+initialNetworkLogic opts st+  | view optNoConnect opts = return st+  | otherwise              = addInitialNetworks networks st+  where+    networks = nub (clientAutoconnects st ++ view optInitialNetworks opts)+ -- | Load configuration and handle errors along the way. loadConfiguration' :: Maybe FilePath -> IO Configuration loadConfiguration' path =@@ -52,15 +60,13 @@          hPutStrLn stderr msg          exitFailure --- | Create connections for all the networks on the command line.+-- | Create connections for the given networks. -- Set the client focus to the first network listed. addInitialNetworks ::   [Text] {- networks -} ->   ClientState           ->   IO ClientState-addInitialNetworks networks st =-  case networks of-    []        -> return st-    network:_ ->-      do st' <- foldM (flip (addConnection 0 Nothing)) st networks-         return (set clientFocus (NetworkFocus network) st')+addInitialNetworks [] st = return st+addInitialNetworks (n:ns) st =+  do st' <- foldM (flip (addConnection 0 Nothing)) st (n:ns)+     return $! set clientFocus (NetworkFocus n) st'
glirc.cabal view
@@ -1,7 +1,12 @@ name:                glirc-version:             2.16+version:             2.17 synopsis:            Console IRC client description:         Console IRC client+                     .+                     glirc is a console IRC client with an emphasis on providing+                     dynamic views into the model of your IRC connections.+                     .+                     <https://github.com/glguy/irc-core/wiki Documentation Wiki> license:             ISC license-file:        LICENSE author:              Eric Mertens@@ -91,6 +96,7 @@                        Client.View.Mentions                        Client.View.Messages                        Client.View.Palette+                       Client.View.UrlSelection                        Client.View.UserList                        Client.View.Windows                        Config.FromConfig
src/Client/CApi/Types.hsc view
@@ -48,28 +48,42 @@ import Foreign.Ptr import Foreign.Storable +-- | Tag for describing the kind of message to display in the client+-- as used in `glirc_print`.+--+-- @enum message_code;@ newtype MessageCode = MessageCode CInt deriving Eq #enum MessageCode, MessageCode, NORMAL_MESSAGE, ERROR_MESSAGE +-- | Result used to determine what to do after processing a message with+-- the 'ProcessMessage' callback.+--+-- @enum process_result;@ newtype MessageResult = MessageResult CInt deriving Eq #enum MessageResult, MessageResult, PASS_MESSAGE, DROP_MESSAGE +--++-- | @typedef void *start(void *glirc, const char *path);@ type StartExtension =   Ptr ()      {- ^ api token                   -} ->   CString     {- ^ path to extension           -} ->   IO (Ptr ()) {- ^ initialized extension state -} +-- | @typedef void stop(void *glirc, void *S);@ type StopExtension =   Ptr () {- ^ api token       -} ->   Ptr () {- ^ extension state -} ->   IO () +-- | Type of @typedef enum process_result process_message(void *glirc, void *S, const struct glirc_message *);@ type ProcessMessage =   Ptr ()     {- ^ api token       -} ->   Ptr ()     {- ^ extention state -} ->   Ptr FgnMsg {- ^ message to send -} ->   IO MessageResult +-- | @typedef void process_command(void *glirc, void *S, const struct glirc_command *);@ type ProcessCommand =   Ptr ()     {- ^ api token       -} ->   Ptr ()     {- ^ extension state -} ->@@ -86,14 +100,14 @@  ------------------------------------------------------------------------ --- | @struct glirc_extension@+-- | @struct glirc_extension;@ data FgnExtension = FgnExtension-  { fgnStart   :: FunPtr StartExtension -- ^ Optional callback-  , fgnStop    :: FunPtr StopExtension  -- ^ Optional callback-  , fgnMessage :: FunPtr ProcessMessage -- ^ Optional callback-  , fgnCommand :: FunPtr ProcessCommand -- ^ Optional callback+  { fgnStart   :: FunPtr StartExtension -- ^ Optional startup callback+  , fgnStop    :: FunPtr StopExtension  -- ^ Optional shutdown callback+  , fgnMessage :: FunPtr ProcessMessage -- ^ Optional message received callback+  , fgnCommand :: FunPtr ProcessCommand -- ^ Optional client command callback   , fgnName    :: CString               -- ^ Null-terminated name-  , fgnMajorVersion, fgnMinorVersion :: CInt+  , fgnMajorVersion, fgnMinorVersion :: CInt -- ^ extension version   }  instance Storable FgnExtension where
src/Client/Commands.hs view
@@ -31,7 +31,6 @@ import           Client.Commands.Recognizer import           Client.Commands.WordCompletion import           Client.Configuration-import           Client.Configuration.ServerSettings import           Client.Message import           Client.State import           Client.State.Channel@@ -64,6 +63,7 @@ import           System.Process import           Text.Read import           Text.Regex.TDFA+import           Text.Regex.TDFA.String (compile)  -- | Possible results of running a command data CommandResult@@ -98,9 +98,21 @@   -- | requires an active channel window   | ChannelCommand (ChannelCommand a) (Bool -> ChannelCommand String) --- | A command is an argument specification, implementation, and documentation-data Command = forall a. Command (ArgumentSpec a) Text (CommandImpl a) +-- | 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   :: ArgumentSpec 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+  }+ -- | Consider the text entry successful and resume the client commandSuccess :: Monad m => ClientState -> m CommandResult commandSuccess = return . CommandSuccess@@ -224,7 +236,7 @@   in   case recognize cmdTxt commands of -    Exact (Command argSpec _docs impl) ->+    Exact Command{cmdImplementation=impl, cmdArgumentSpec=argSpec} ->       case impl of         ClientCommand exec tab ->           finish argSpec exec tab@@ -254,8 +266,9 @@   -- | Expands each alias to have its own copy of the command callbacks-expandAliases :: [(NonEmpty a,b)] -> [(a,b)]-expandAliases xs = [ (a,b) | (as,b) <- xs, a <- toList as ]+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@@ -263,25 +276,28 @@ commands :: Recognizer Command commands = fromCommands (expandAliases commandsList) -commandsList :: [(NonEmpty Text,Command)]+commandsList :: [Command] commandsList =   --   -- Client commands   ---  [ ( pure "connect"-    , Command (ReqTokenArg "network" NoArg)+  [ Command+      (pure "connect")+      (ReqTokenArg "network" NoArg)       "Connect to \^Bnetwork\^B by name.\n\       \\n\       \If no name is configured the hostname is the 'name'.\n"     $ ClientCommand cmdConnect tabConnect-    )-  , ( pure "exit"-    , Command NoArg++  , Command+      (pure "exit")+      NoArg       "Exit the client immediately.\n"     $ ClientCommand cmdExit noClientTab-    )-  , ( pure "focus"-    , Command (ReqTokenArg "network" (OptTokenArg "channel" NoArg))++  , Command+      (pure "focus")+      (ReqTokenArg "network" (OptTokenArg "channel" NoArg))       "Change the focused window.\n\       \\n\       \When only \^Bnetwork\^B is specified this switches to the network status window.\n\@@ -290,9 +306,10 @@       \Nicknames can be specified in the \^Bchannel\^B parameter to switch to private chat.\n\       \See also: /channel (aliased /c) to switch to a channel on the current network.\n"     $ ClientCommand cmdFocus tabFocus-    )-  , ( pure "clear"-    , Command (OptTokenArg "network" (OptTokenArg "channel" NoArg))++  , Command+      (pure "clear")+      (OptTokenArg "network" (OptTokenArg "channel" NoArg))       "Clear a window.\n\       \\n\       \If no arguments are provided the current window is cleared.\n\@@ -300,50 +317,61 @@       \If \^Bnetwork\^B and \^Bchannel\^B are provided that chat window is cleared.\n\       \\n\       \If a window is cleared and no longer active that window will be removed from the client.\n"-    $ ClientCommand cmdClear noClientTab-    )-  , ( pure "reconnect"-    , Command NoArg+    $ ClientCommand cmdClear tabFocus++  , Command+      (pure "reconnect")+      NoArg       "Reconnect to the current network.\n"     $ ClientCommand cmdReconnect noClientTab-    )-  , ( pure "ignore"-    , Command (RemainingArg "nicks")++  , Command+      (pure "ignore")+      (RemainingArg "nicks")       "Toggle the soft-ignore on each of the space-delimited given nicknames.\n"     $ ClientCommand cmdIgnore simpleClientTab-    )-  , ( pure "reload"-    , Command (OptTokenArg "filename" NoArg)++  , Command+      (pure "reload")+      (OptTokenArg "filename" NoArg)       "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-    )-  , ( pure "extension"-    , Command (ReqTokenArg "extension" (RemainingArg "arguments"))++  , Command+      (pure "extension")+      (ReqTokenArg "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-    )-  , ( pure "windows"-    , Command NoArg-      "Show a list of all client message windows.\n"-    $ ClientCommand cmdWindows noClientTab-    )-  , ( pure "mentions"-    , Command NoArg++  , Command+      (pure "windows")+      (OptTokenArg "kind" NoArg)+      "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 "mentions")+      NoArg       "Show a list of all message that were highlighted as important.\n"     $ ClientCommand cmdMentions noClientTab-    )-  , ( pure "palette"-    , Command NoArg++  , Command+      (pure "palette")+      NoArg       "Show the current palette settings and a color chart to help pick new colors.\n"     $ ClientCommand cmdPalette noClientTab-    )-  , ( pure "exec"-    , Command (RemainingArg "arguments")++  , 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 [command arguments...]\n\@@ -360,9 +388,10 @@       \ escaped characters and spaces inside.\n\       \\n"     $ ClientCommand cmdExec simpleClientTab-    )-  , ( pure "url"-    , Command (OptTokenArg "number" NoArg)++  , Command+      (pure "url")+      (OptTokenArg "number" NoArg)       "Open a URL seen in chat.\n\       \\n\       \The URL is opened using the executable configured under \^Burl-opener\^B.\n\@@ -371,48 +400,75 @@       \\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-    ) -  , ( pure "help"-    , Command (OptTokenArg "command" NoArg)+  , Command+      (pure "help")+      (OptTokenArg "command" NoArg)       "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-    ) -  , ( pure "splits"-    , Command (RemainingArg "focuses")+  , Command+      (pure "splits")+      (RemainingArg "focuses")       "Set the extra message view splits.\n\       \\n\-      \\^Bfocues\^B: space delimited list of focus names.\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"+      \User:    \^BNETWORK\^B:\^BNICK\^B\n\+      \\n\+      \Not providing an argument unsplits the current windows.\n"     $ ClientCommand cmdSplits tabSplits-    ) +  , Command+      (pure "grep")+      (RemainingArg "regular-expression")+      "Set the persistent regular expression.\n\+      \\n\+      \Clear the regular expression by calling this without an argument.\n\+      \\n\+      \\^B/grep\^O is case-sensitive.\n\+      \\^B/grepi\^O is case-insensitive.\n"+    $ ClientCommand (cmdGrep True) simpleClientTab++  , Command+      (pure "grepi")+      (RemainingArg "regular-expression")+      "Set the persistent regular expression.\n\+      \\n\+      \Clear the regular expression by calling this without an argument.\n\+      \\n\+      \\^B/grep\^O is case-sensitive.\n\+      \\^B/grepi\^O is case-insensitive.\n"+    $ ClientCommand (cmdGrep False) simpleClientTab++   --   -- Network commands   ---  , ( pure "quote"-    , Command (RemainingArg "raw IRC command")+  , Command+      (pure "quote")+      (RemainingArg "raw IRC command")       "Send a raw IRC command.\n"     $ NetworkCommand cmdQuote  simpleNetworkTab-    )-  , ( "join" :| ["j"]-    , Command (ReqTokenArg "channels" (OptTokenArg "keys" NoArg))++  , Command+      ("join" :| ["j"])+      (ReqTokenArg "channels" (OptTokenArg "keys" NoArg))       "Join a chat channel.\n\       \\n\       \\^Bchannels\^B: comma-separated list of channels\n\       \\^Bkeys\^B: comma-separated list of keys\n"     $ NetworkCommand cmdJoin   simpleNetworkTab-    )-  , ( "channel" :| ["c"]-    , Command (ReqTokenArg "channel" NoArg)++  , Command+      ("channel" :| ["c"])+      (ReqTokenArg "channel" NoArg)       "Change the focused window.\n\       \\n\       \Changes the focus to the \^Bchannel\^B chat window on the current network.\n\@@ -420,10 +476,11 @@       \Nicknames can be specified in the \^Bchannel\^B parameter to switch to private chat.\n\       \See also: /focus to switch to a channel on a different network.\n\       \See also: /focus to switch to a channel on a different network.\n"-    $ NetworkCommand cmdChannel simpleNetworkTab-    )-  , ( pure "mode"-    , Command (RemainingArg "modes and parameters")+    $ NetworkCommand cmdChannel tabChannel++  , Command+      (pure "mode")+      (RemainingArg "modes and parameters")       "Sets IRC modes.\n\       \\n\       \Examples:\n\@@ -437,23 +494,26 @@       \\n\       \This command has parameter sensitive tab-completion.\n"     $ NetworkCommand cmdMode   tabMode-    )-  , ( pure "msg"-    , Command (ReqTokenArg "target" (RemainingArg "message"))++  , Command+      (pure "msg")+      (ReqTokenArg "target" (RemainingArg "message"))       "Send a chat message to a user or a channel.\n\       \\n\       \\^Btarget\^B can be a channel or nickname.\n"     $ NetworkCommand cmdMsg    simpleNetworkTab-    )-  , ( pure "notice"-    , Command (ReqTokenArg "target" (RemainingArg "message"))++  , Command+      (pure "notice")+      (ReqTokenArg "target" (RemainingArg "message"))       "Send a notice message to a user or a channel. Notices are typically used by bots.\n\       \\n\       \\^Btarget\^B can be a channel or nickname.\n"     $ NetworkCommand cmdNotice simpleNetworkTab-    )-  , ( pure "ctcp"-    , Command (ReqTokenArg "target" (ReqTokenArg "command" (RemainingArg "arguments")))++  , Command+      (pure "ctcp")+      (ReqTokenArg "target" (ReqTokenArg "command" (RemainingArg "arguments")))       "Send a CTCP command to a user or a channel.\n\       \\n\       \Examples:\n\@@ -464,84 +524,98 @@       \\^Bcommand\^B can be any CTCP command.\n\       \\^Barguments\^B are specific to a particular command.\n"     $ NetworkCommand cmdCtcp simpleNetworkTab-    )-  , ( pure "nick"-    , Command (ReqTokenArg "nick" NoArg)++  , Command+      (pure "nick")+      (ReqTokenArg "nick" NoArg)       "Change your nickname.\n"     $ NetworkCommand cmdNick   simpleNetworkTab-    )-  , ( pure "quit"-    , Command (RemainingArg "reason")++  , 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-    )-  , ( pure "disconnect"-    , Command NoArg++  , Command+      (pure "disconnect")+      NoArg       "Immediately terminate the current network connection.\n\       \\n\       \See also: /quit /exit\n"     $ NetworkCommand cmdDisconnect noNetworkTab-    )-  , ( pure "who"-    , Command (RemainingArg "arguments")++  , Command+      (pure "who")+      (RemainingArg "arguments")       "Send WHO query to server with given arguments.\n"     $ NetworkCommand cmdWho simpleNetworkTab-    )-  , ( pure "whois"-    , Command (RemainingArg "arguments")++  , Command+      (pure "whois")+      (RemainingArg "arguments")       "Send WHOIS query to server with given arguments.\n"     $ NetworkCommand cmdWhois simpleNetworkTab-    )-  , ( pure "whowas"-    , Command (RemainingArg "arguments")++  , Command+      (pure "whowas")+      (RemainingArg "arguments")       "Send WHOWAS query to server with given arguments.\n"     $ NetworkCommand cmdWhowas simpleNetworkTab-    )-  , ( pure "ison"-    , Command (RemainingArg "arguments")++  , Command+      (pure "ison")+      (RemainingArg "arguments")       "Send ISON query to server with given arguments.\n"     $ NetworkCommand cmdIson   simpleNetworkTab-    )-  , ( pure "userhost"-    , Command (RemainingArg "arguments")++  , Command+      (pure "userhost")+      (RemainingArg "arguments")       "Send USERHOST query to server with given arguments.\n"     $ NetworkCommand cmdUserhost simpleNetworkTab-    )-  , ( pure "away"-    , Command (RemainingArg "message")++  , Command+      (pure "away")+      (RemainingArg "message")       "Set away status.\n\       \\n\       \When \^Bmessage\^B is omitted away status is cleared.\n"-    $ NetworkCommand cmdAway   simpleNetworkTab-    )-  , ( pure "links"-    , Command (RemainingArg "arguments")+    $ NetworkCommand cmdAway simpleNetworkTab++  , Command+      (pure "links")+      (RemainingArg "arguments")       "Send LINKS query to server with given arguments.\n"-    $ NetworkCommand cmdLinks  simpleNetworkTab-    )-  , ( pure "time"-    , Command (RemainingArg "arguments")+    $ NetworkCommand cmdLinks simpleNetworkTab++  , Command+      (pure "time")+      (RemainingArg "arguments")       "Send TIME query to server with given arguments.\n"-    $ NetworkCommand cmdTime   simpleNetworkTab-    )-  , ( pure "stats"-    , Command (RemainingArg "arguments")+    $ NetworkCommand cmdTime simpleNetworkTab++  , Command+      (pure "stats")+      (RemainingArg "arguments")       "Send STATS query to server with given arguments.\n"-    $ NetworkCommand cmdStats  simpleNetworkTab-    )-  , ( pure "znc"-    , Command (RemainingArg "arguments")+    $ NetworkCommand cmdStats simpleNetworkTab++  , 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-    )-  , ( pure "znc-playback"-    , Command (OptTokenArg "time" (OptTokenArg "date" NoArg))+    $ NetworkCommand cmdZnc simpleNetworkTab++  , Command+      (pure "znc-playback")+      (OptTokenArg "time" (OptTokenArg "date" NoArg))       "Request playback from the ZNC 'playback' module.\n\       \\n\       \\^Btime\^B determines the time to playback since.\n\@@ -555,64 +629,71 @@       \\n\       \Note that the playback module is not installed in ZNC by default!\n"     $ NetworkCommand cmdZncPlayback noNetworkTab-    ) -  , ( pure "invite"-    , Command (ReqTokenArg "nick" NoArg)+  , Command+      (pure "invite")+      (ReqTokenArg "nick" NoArg)       "Invite a user to the current channel.\n"     $ ChannelCommand cmdInvite simpleChannelTab-    )-  , ( pure "topic"-    , Command (RemainingArg "message")++  , 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-    )-  , ( pure "kick"-    , Command (ReqTokenArg "nick" (RemainingArg "reason"))++  , Command+      (pure "kick")+      (ReqTokenArg "nick" (RemainingArg "reason"))       "Kick a user from the current channel.\n\       \\n\       \See also: /kickban /remove\n"     $ ChannelCommand cmdKick   simpleChannelTab-    )-  , ( pure "kickban"-    , Command (ReqTokenArg "nick" (RemainingArg "reason"))++  , Command+      (pure "kickban")+      (ReqTokenArg "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-    )-  , ( pure "remove"-    , Command (ReqTokenArg "nick" (RemainingArg "reason"))++  , Command+      (pure "remove")+      (ReqTokenArg "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-    )-  , ( pure "part"-    , Command (RemainingArg "reason")++  , Command+      (pure "part")+      (RemainingArg "reason")       "Part from the current channel.\n"     $ ChannelCommand cmdPart simpleChannelTab-    ) -  , ( pure "users"-    , Command NoArg+  , Command+      (pure "users")+      NoArg       "Show the user list for the current channel.\n\       \\n\       \Detailed view (F2) shows full hostmask.\n\       \Hostmasks can be populated with /who #channel.\n"     $ ChannelCommand cmdUsers  noChannelTab-    )-  , ( pure "channelinfo"-    , Command NoArg++  , Command+      (pure "channelinfo")+      NoArg       "Show information about the current channel.\n"     $ ChannelCommand cmdChannelInfo noChannelTab-    )-  , ( pure "masks"-    , Command (ReqTokenArg "mode" NoArg)++  , Command+      (pure "masks")+      (ReqTokenArg "mode" NoArg)       "Show mask lists for current channel.\n\       \\n\       \Common \^Bmode\^B values:\n\@@ -623,20 +704,20 @@       \\n\       \To populate the mask lists for the first time use: /mode \^Bmode\^B\n"     $ ChannelCommand cmdMasks noChannelTab-    ) -  , ( pure "me"-    , Command (RemainingArg "message")+  , Command+      (pure "me")+      (RemainingArg "message")       "Send an 'action' to the current chat window.\n"     $ ChatCommand cmdMe simpleChannelTab-    )-  , ( pure "say"-    , Command (RemainingArg "message")++  , Command+      (pure "say")+      (RemainingArg "message")       "Send a message to the current chat window.\n\       \\n\       \This can be useful for sending a chat message with a leading '/'.\n"     $ ChatCommand cmdSay simpleChannelTab-    )   ]  -- | Provides no tab completion for client commands@@ -683,16 +764,17 @@     Just (network, Just (channel, _)) ->         clearFocus (ChannelFocus (Text.pack network) (mkId (Text.pack channel)))   where-    clearFocus focus = commandSuccess (windowEffect st)+    clearFocus focus = commandSuccess (focusEffect (windowEffect st))       where         windowEffect-          | isActive  = clearWindow-          | otherwise = deleteWindow+          | isActive  = setWindow (Just emptyWindow)+          | otherwise = setWindow Nothing -        deleteWindow = advanceFocus . setWindow Nothing-        clearWindow  =                setWindow (Just emptyWindow)+        focusEffect+          | not isActive && view clientFocus st == focus = advanceFocus+          | otherwise                                    = id -        setWindow = set (clientWindows . at (view clientFocus st))+        setWindow = set (clientWindows . at focus)          isActive =           case focus of@@ -823,9 +905,27 @@          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 ()-cmdWindows st _ = commandSuccess (changeSubfocus FocusWindows st)+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 Windows. cmdMentions :: ClientCommand ()@@ -849,7 +949,8 @@             newline = Edit.endLine cmd         commandSuccess (set (clientTextBox . Edit.line) newline st) -  | otherwise = simpleTabCompletion id [] completions isReversed st+  | otherwise =+        simpleTabCompletion plainWordCompleteMode [] completions isReversed st   where     currentExtras = view clientExtraFocus st @@ -874,28 +975,30 @@         (net,_:chan) -> ChannelFocus (Text.pack net) (mkId (Text.pack chan))  tabHelp :: Bool -> ClientCommand String-tabHelp isReversed st _ = simpleTabCompletion id [] commandNames isReversed st+tabHelp isReversed st _ =+  simpleTabCompletion plainWordCompleteMode [] commandNames isReversed st   where-    commandNames = [ cmd | (cmd :| _, _) <- commandsList ]+    commandNames = fst <$> expandAliases commandsList  simpleTabCompletion ::   Prefix a =>-  (String -> String) {- ^ leading transform -} ->-  [a] {- ^ hints           -} ->-  [a] {- ^ all completions -} ->-  Bool {- ^ reversed order -} ->-  ClientState -> IO CommandResult-simpleTabCompletion lead hints completions isReversed st =+  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 lead isReversed hints completions+    tryCompletion = wordComplete mode isReversed hints completions  -- | @/connect@ tab completes known server names tabConnect :: Bool -> ClientCommand String tabConnect isReversed st _ =-  simpleTabCompletion id [] networks isReversed st+  simpleTabCompletion plainWordCompleteMode [] networks isReversed st   where     networks = views clientNetworkMap               HashMap.keys st             ++ views (clientConfig . configServers) HashMap.keys st@@ -905,14 +1008,16 @@ -- the current networks are used. tabFocus :: Bool -> ClientCommand String tabFocus isReversed st _ =-  simpleTabCompletion id [] completions isReversed st+  simpleTabCompletion plainWordCompleteMode [] completions isReversed st   where     networks   = map mkId $ HashMap.keys $ view clientNetworkMap st     params     = words $ uncurry take $ clientLine st -    completions-      | length params == 2 = networks-      | otherwise          = currentCompletionList st+    completions =+      case params of+        [_cmd,_net]      -> networks+        [_cmd,net,_chan] -> channelWindowsOnNetwork (Text.pack net) st+        _                -> []  cmdWhois :: NetworkCommand String cmdWhois cs st rest =@@ -931,7 +1036,7 @@  cmdIson :: NetworkCommand String cmdIson cs st rest =-  do sendMsg cs (ircIson (Text.pack <$> words rest))+  do sendMsg cs (ircIson [Text.pack (unwords (words rest))])      commandSuccess st  cmdUserhost :: NetworkCommand String@@ -1038,20 +1143,13 @@  commandSuccessUpdateCS :: NetworkState -> ClientState -> IO CommandResult commandSuccessUpdateCS cs st =-  let networkId = view csNetworkId cs in-  commandSuccess-    $ setStrict (clientConnections . ix networkId) cs st+  do let networkId = view csNetworkId cs+     commandSuccess+       $ setStrict (clientConnections . ix networkId) cs st  cmdTopic :: ChannelCommand String cmdTopic channelId cs st rest =-  do let cmd =-           case rest of-             ""    -> ircTopic channelId ""-             topic | useChanServ channelId cs ->-                        ircPrivmsg "ChanServ"-                          ("TOPIC " <> idText channelId <> Text.pack (' ' : topic))-                   | otherwise -> ircTopic channelId (Text.pack topic)-     sendMsg cs cmd+  do sendTopic channelId (Text.pack rest) cs      commandSuccess st  tabTopic ::@@ -1130,7 +1228,26 @@   commandSuccess     $ changeFocus (ChannelFocus (view csNetwork cs) (mkId (Text.pack channel))) st +-- | Tab completion for @/channel@+tabChannel ::+  Bool {- ^ reversed order -} ->+  NetworkCommand String+tabChannel isReversed cs st _ =+  simpleTabCompletion plainWordCompleteMode [] completions isReversed st+  where+    completions = channelWindowsOnNetwork (view csNetwork cs) st +-- | 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@@ -1246,7 +1363,7 @@               [ (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 id hint completions isReversed st+      -> simpleTabCompletion plainWordCompleteMode hint completions isReversed st      _ -> commandFailure st @@ -1315,7 +1432,7 @@ commandNameCompletion :: Bool -> ClientState -> Maybe ClientState commandNameCompletion isReversed st =   do guard (cursorPos == n)-     clientTextBox (wordComplete id isReversed [] possibilities) st+     clientTextBox (wordComplete plainWordCompleteMode isReversed [] possibilities) st   where     n = length leadingPart     (cursorPos, line) = clientLine st@@ -1328,30 +1445,11 @@ -- userlist for the currently focused channel (if any) nickTabCompletion :: Bool {- ^ reversed -} -> ClientState -> IO CommandResult nickTabCompletion isReversed st =-  simpleTabCompletion (++": ") hint completions isReversed st+  simpleTabCompletion mode hint completions isReversed st   where     hint          = activeNicks st     completions   = currentCompletionList st---- | Used to send commands that require ops to perform.--- If this channel is one that the user has chanserv access and ops are needed--- then ops are requested and the commands are queued, otherwise send them--- directly.-sendModeration ::-  Identifier      {- ^ channel       -} ->-  [RawIrcMsg]     {- ^ commands      -} ->-  NetworkState    {- ^ network state -} ->-  IO NetworkState-sendModeration channel cmds cs-  | useChanServ channel cs =-      do sendMsg cs (ircPrivmsg "ChanServ" ("OP " <> idText channel))-         return $ csChannels . ix channel . chanQueuedModeration <>~ cmds $ cs-  | otherwise = cs <$ traverse_ (sendMsg cs) cmds--useChanServ :: Identifier -> NetworkState    -> Bool-useChanServ channel cs =-  channel `elem` view (csSettings . ssChanservChannels) cs &&-  not (iHaveOp channel cs)+    mode          = currentNickCompletionMode st  cmdExtension :: ClientCommand (String, String) cmdExtension st (name,params) =@@ -1440,6 +1538,7 @@     , _msgNetwork = ""     } ste + cmdUrl :: ClientCommand (Maybe (String, ())) cmdUrl st mbArg =   case view (clientConfig . configUrlOpener) st of@@ -1471,3 +1570,14 @@      case res of        Left e  -> commandFailureMsg (Text.pack (displayException (e :: IOError))) st        Right{} -> commandSuccess st++-- | Implementation of @/grep@ and @/grepi@+cmdGrep ::+  Bool {- ^ case sensitive -} ->+  ClientCommand String+cmdGrep sensitive st str+  | null str  = commandSuccess (set clientRegex Nothing st)+  | otherwise =+      case compile defaultCompOpt{caseSensitive=sensitive} defaultExecOpt str of+        Left e -> commandFailureMsg (Text.pack e) st+        Right r -> commandSuccess (set clientRegex (Just r) st)
src/Client/Commands/WordCompletion.hs view
@@ -11,13 +11,18 @@ module Client.Commands.WordCompletion   ( Prefix(..)   , wordComplete++  -- * Word completion modes+  , WordCompletionMode(..)+  , plainWordCompleteMode+  , defaultNickWordCompleteMode+  , slackNickWordCompleteMode   ) where  import qualified Client.State.EditBox as Edit import           Control.Applicative import           Control.Lens import           Control.Monad-import           Data.Char import           Data.List import qualified Data.Set as Set import           Data.String (IsString(..))@@ -25,6 +30,20 @@ import           Data.Text (Text) import           Irc.Identifier +-- | Word completion prefix and suffix+data WordCompletionMode = WordCompletionMode+  { wcmStartPrefix, wcmStartSuffix, wcmMiddlePrefix, wcmMiddleSuffix :: String }+  deriving Show++plainWordCompleteMode :: WordCompletionMode+plainWordCompleteMode = WordCompletionMode "" "" "" ""++defaultNickWordCompleteMode :: WordCompletionMode+defaultNickWordCompleteMode = WordCompletionMode "" ": " "" ""++slackNickWordCompleteMode :: WordCompletionMode+slackNickWordCompleteMode = WordCompletionMode "@" " " "@" ""+ -- | Perform word completion on a text box. -- -- The leading update operation is applied to the result of tab-completion@@ -37,13 +56,13 @@ -- completions. wordComplete ::   Prefix a =>-  (String -> String) {- ^ leading update operation -} ->+  WordCompletionMode {- ^ leading update operation -} ->   Bool               {- ^ reversed -} ->   [a]       {- ^ priority completions -} ->   [a]       {- ^ possible completions -} ->   Edit.EditBox -> Maybe Edit.EditBox-wordComplete leadingCase isReversed hint vals box =-  do let current = currentWord box+wordComplete mode isReversed hint vals box =+  do let current = currentWord mode box      guard (not (null current))      let cur = fromString current      case view Edit.lastOperation box of@@ -51,7 +70,7 @@          | isPrefix pat cur ->           do next <- tabSearch isReversed pat cur vals-            Just $ replaceWith leadingCase (toString next) box+            Just $ replaceWith mode (toString next) box          where            pat = fromString patternStr @@ -59,23 +78,27 @@          do next <- find (isPrefix cur) hint <|>                     tabSearch isReversed cur cur vals             Just $ set Edit.lastOperation (Edit.TabOperation current)-                 $ replaceWith leadingCase (toString next) box+                 $ replaceWith mode (toString next) box -replaceWith :: (String -> String) -> String -> Edit.EditBox -> Edit.EditBox-replaceWith leadingCase str box =+replaceWith :: WordCompletionMode -> String -> Edit.EditBox -> Edit.EditBox+replaceWith (WordCompletionMode spfx ssfx mpfx msfx) str box =     let box1 = Edit.killWordBackward False box-        str1 | view Edit.pos box1 == 0 = leadingCase str-             | otherwise               = str+        str1 | view Edit.pos box1 == 0 = spfx ++ str ++ ssfx+             | otherwise               = mpfx ++ str ++ msfx     in over Edit.content (Edit.insertString str1) box1 -currentWord :: Edit.EditBox -> String-currentWord box-  = reverse-  $ takeWhile (not . isSpace)-  $ dropWhile (\x -> x==' ' || x==':')+currentWord :: WordCompletionMode -> Edit.EditBox -> String+currentWord (WordCompletionMode spfx ssfx mpfx msfx) box+  = dropWhile (`elem`pfx)   $ reverse+  $ takeWhile (/= ' ')+  $ dropWhile (`elem`sfx)+  $ reverse   $ take n txt- where Edit.Line n txt = view Edit.line box+  where+    pfx = spfx++mpfx+    sfx = ssfx++msfx+    Edit.Line n txt = view Edit.line box  -- | Class for types that are isomorphic to 'String' -- and which can support a total order and a prefix
src/Client/Configuration.hs view
@@ -45,6 +45,7 @@ import           Client.Configuration.ServerSettings import           Client.Commands.Interpolation import           Client.Commands.Recognizer+import           Client.Commands.WordCompletion import           Control.Exception import           Control.Monad import           Config@@ -273,7 +274,10 @@     "flood-threshold"     -> setField       ssFloodThreshold     "message-hooks"       -> setField       ssMessageHooks     "name"                -> setFieldMb     ssName-    _                     -> failure "Unknown section"+    "reconnect-attempts"  -> setField       ssReconnectAttempts+    "autoconnect"         -> setField       ssAutoconnect+    "nick-completion"     -> setFieldWith   ssNickCompletion parseNickCompletion+    _                     -> failure "Unknown setting"   where     setField   l = setFieldWith   l parseConfig     setFieldMb l = setFieldWithMb l parseConfig@@ -342,3 +346,10 @@      case parseExpansion txt of        Nothing -> failure "bad macro line"        Just ex -> return ex++parseNickCompletion :: Value -> ConfigParser WordCompletionMode+parseNickCompletion v =+  case v of+    Atom "default" -> return defaultNickWordCompleteMode+    Atom "slack"   -> return slackNickWordCompleteMode+    _              -> failure "expected default or slack"
src/Client/Configuration/ServerSettings.hs view
@@ -39,6 +39,9 @@   , ssFloodThreshold   , ssMessageHooks   , ssName+  , ssReconnectAttempts+  , ssAutoconnect+  , ssNickCompletion    -- * Load function   , loadDefaultServerSettings@@ -49,6 +52,7 @@   ) where  import           Client.Commands.Interpolation+import           Client.Commands.WordCompletion import           Control.Lens import           Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as NonEmpty@@ -82,6 +86,9 @@   , _ssFloodThreshold   :: !Rational -- ^ Flood limited threshold (seconds)   , _ssMessageHooks     :: ![Text] -- ^ Initial message hooks   , _ssName             :: !(Maybe Text) -- ^ The name referencing the server in commands+  , _ssReconnectAttempts:: !Int -- ^ The number of reconnect attempts to make on error+  , _ssAutoconnect      :: Bool -- ^ Connect to this network on server startup+  , _ssNickCompletion   :: WordCompletionMode -- ^ Nick completion mode for this server   }   deriving Show @@ -123,4 +130,7 @@        , _ssFloodThreshold   = 10        , _ssMessageHooks     = []        , _ssName             = Nothing+       , _ssReconnectAttempts= 6 -- six feels great+       , _ssAutoconnect      = False+       , _ssNickCompletion   = defaultNickWordCompleteMode        }
src/Client/EventLoop.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings, NondecreasingIndentation #-}+{-# Language BangPatterns, OverloadedStrings, NondecreasingIndentation #-}  {-| Module      : Client.EventLoop@@ -35,7 +35,6 @@ import           Control.Monad import           Data.ByteString (ByteString) import           Data.Foldable-import qualified Data.IntMap as IntMap import           Data.List import           Data.Maybe import           Data.Monoid@@ -53,8 +52,6 @@ import           LensUtils import           Network.Connection -reconnectAttempts :: Int-reconnectAttempts = 6  -- | Sum of the three possible event types the event loop handles data ClientEvent@@ -76,20 +73,10 @@   where     vtyEventChannel = _eventChannel (inputIface (view clientVty st)) -    possibleTimedEvents =-      [ (networkId, runAt, action)-           | (networkId, cs) <- views clientConnections IntMap.toList st-           , Just (runAt, action) <- [nextTimedAction cs]-           ]--    earliestEvent-      | null possibleTimedEvents = Nothing-      | otherwise = Just (minimumBy (comparing (\(_,runAt,_) -> runAt)) possibleTimedEvents)-     prepareTimer =-      case earliestEvent of+      case earliestEvent st of         Nothing -> return retry-        Just (networkId,runAt,action) ->+        Just (networkId,(runAt,action)) ->           do now <- getCurrentTime              let microsecs = truncate (1000000 * diffUTCTime runAt now)              var <- registerDelay (max 0 microsecs)@@ -97,27 +84,33 @@                          unless ready retry                          return (TimerEvent networkId action) +-- | Compute the earliest scheduled timed action for the client+earliestEvent :: ClientState -> Maybe (NetworkId, (UTCTime, TimedAction))+earliestEvent =+  minimumByOf+    (clientConnections . (ifolded <. folding nextTimedAction) . withIndex)+    (comparing (fst . snd))+ -- | Apply this function to an initial 'ClientState' to launch the client. eventLoop :: ClientState -> IO ()-eventLoop st0 =-  do let st1 = clientTick st0-         vty = view clientVty st-         (pic, st) = clientPicture st1+eventLoop st =+  do let vty = view clientVty st+     when (view clientBell st) (beep vty) -     -- check st0 for bell, it will be always be cleared in st1-     when (view clientBell st0) (beep vty)+     let (pic, st') = clientPicture (clientTick st)      update vty pic -     event <- getEvent st+     event <- getEvent st'      case event of-       TimerEvent networkId action  -> doTimerEvent networkId action st-       VtyEvent vtyEvent         -> doVtyEvent vtyEvent st+       TimerEvent networkId action  -> eventLoop =<< doTimerEvent networkId action st'+       VtyEvent vtyEvent -> traverse_ eventLoop =<< doVtyEvent vtyEvent st'        NetworkEvent networkEvent ->+         eventLoop =<<          case networkEvent of-           NetworkLine  network time line -> doNetworkLine network time line st-           NetworkError network time ex   -> doNetworkError network time ex st-           NetworkOpen  network time      -> doNetworkOpen  network time st-           NetworkClose network time      -> doNetworkClose network time st+           NetworkLine  net time line -> doNetworkLine  net time line st'+           NetworkError net time ex   -> doNetworkError net time ex st'+           NetworkOpen  net time      -> doNetworkOpen  net time st'+           NetworkClose net time      -> doNetworkClose net time st'  -- | Sound the terminal bell assuming that the @BEL@ control code -- is supported.@@ -129,89 +122,92 @@   NetworkId   {- ^ network id   -} ->   ZonedTime   {- ^ event time   -} ->   ClientState {- ^ client state -} ->-  IO ()+  IO ClientState doNetworkOpen networkId time st =   case view (clientConnections . at networkId) st of     Nothing -> error "doNetworkOpen: Network missing"     Just cs ->-      let msg = ClientMessage-                  { _msgTime    = time-                  , _msgNetwork = view csNetwork cs-                  , _msgBody    = NormalBody "connection opened"-                  }-      in eventLoop $ recordNetworkMessage msg-                   $ overStrict (clientConnections . ix networkId . csLastReceived)-                                (\old -> old `seq` Just $! zonedTimeToUTC time) st+      do let msg = ClientMessage+                     { _msgTime    = time+                     , _msgNetwork = view csNetwork cs+                     , _msgBody    = NormalBody "connection opened"+                     }+         return $! recordNetworkMessage msg+                 $ overStrict (clientConnections . ix networkId . csLastReceived)+                              (\old -> old `seq` Just $! zonedTimeToUTC time)+                              st  -- | Respond to a network connection closing normally. doNetworkClose ::-  NetworkId {- ^ network id -} ->-  ZonedTime {- ^ event time -} ->-  ClientState -> IO ()+  NetworkId   {- ^ network id   -} ->+  ZonedTime   {- ^ event time   -} ->+  ClientState {- ^ client state -} ->+  IO ClientState doNetworkClose networkId time st =-  let (cs,st') = removeNetwork networkId st-      msg = ClientMessage-              { _msgTime    = time-              , _msgNetwork = view csNetwork cs-              , _msgBody    = NormalBody "connection closed"-              }-  in eventLoop $ recordNetworkMessage msg st'+  do let (cs,st') = removeNetwork networkId st+         msg = ClientMessage+                 { _msgTime    = time+                 , _msgNetwork = view csNetwork cs+                 , _msgBody    = NormalBody "connection closed"+                 }+     return (recordNetworkMessage msg st')   -- | Respond to a network connection closing abnormally. doNetworkError ::-  NetworkId {- ^ failed network -} ->-  ZonedTime {- ^ current time   -} ->+  NetworkId     {- ^ failed network     -} ->+  ZonedTime     {- ^ current time       -} ->   SomeException {- ^ termination reason -} ->-  ClientState -> IO ()+  ClientState   {- ^ client state       -} ->+  IO ClientState doNetworkError networkId time ex st =   do let (cs,st1) = removeNetwork networkId st-         st2 = foldl' (flip recordNetworkMessage) st1 msgs--         msgs = exceptionToLines ex <&> \e ->-                ClientMessage-                 { _msgTime    = time-                 , _msgNetwork = view csNetwork cs-                 , _msgBody    = ErrorBody (Text.pack e)-                 }--         shouldReconnect =-           case view csPingStatus cs of-             PingConnecting n _-               | n == 0 || n > reconnectAttempts -> False+         st2 = foldl' (\acc msg -> recordError time cs (Text.pack msg) acc) st1+             $ exceptionToLines ex+     reconnectLogic ex cs st2 -             _ | Just HostNotResolved{}   <-              fromException ex -> True-               | Just HostCannotConnect{} <-              fromException ex -> True-               | Just PingTimeout         <-              fromException ex -> True-               | Just ResourceVanished    <- ioe_type <$> fromException ex -> True-               | Just NoSuchThing         <- ioe_type <$> fromException ex -> True+reconnectLogic ::+  SomeException {- ^ thread failure reason -} ->+  NetworkState  {- ^ failed network        -} ->+  ClientState   {- ^ client state          -} ->+  IO ClientState+reconnectLogic ex cs st -               | otherwise -> False+  | shouldReconnect =+      do (attempts, mbDisconnectTime) <- computeRetryInfo+         addConnection attempts mbDisconnectTime (view csNetwork cs) st +  | otherwise = return st -         reconnect = do-           (attempts, mbDisconnectTime)-              <- case view csPingStatus cs of-                   PingConnecting n tm                   -> pure (n+1, tm)-                   _ | Just tm <- view csLastReceived cs -> pure (1, Just tm)-                     | otherwise -> do now <- getCurrentTime-                                       pure (1, Just now)-           addConnection attempts mbDisconnectTime (view csNetwork cs) st2+  where+    computeRetryInfo =+      case view csPingStatus cs of+        PingConnecting n tm                   -> pure (n+1, tm)+        _ | Just tm <- view csLastReceived cs -> pure (1, Just tm)+          | otherwise                         -> do now <- getCurrentTime+                                                    pure (1, Just now) -         nextAction-           | shouldReconnect = reconnect-           | otherwise       = return st2+    reconnectAttempts = view (csSettings . ssReconnectAttempts) cs -     eventLoop =<< nextAction+    shouldReconnect =+      case view csPingStatus cs of+        PingConnecting n _ | n == 0 || n > reconnectAttempts          -> False+        _ | Just HostNotResolved{}   <-              fromException ex -> True+          | Just HostCannotConnect{} <-              fromException ex -> True+          | Just PingTimeout         <-              fromException ex -> True+          | Just ResourceVanished    <- ioe_type <$> fromException ex -> True+          | Just NoSuchThing         <- ioe_type <$> fromException ex -> True+          | otherwise                                                 -> False   -- | Respond to an IRC protocol line. This will parse the message, updated the -- relevant connection state and update the UI buffers. doNetworkLine ::-  NetworkId {- ^ Network ID of message -} ->-  ZonedTime {- ^ current time          -} ->-  ByteString {- ^ Raw IRC message without newlines -} ->-  ClientState -> IO ()+  NetworkId   {- ^ Network ID of message            -} ->+  ZonedTime   {- ^ current time                     -} ->+  ByteString  {- ^ Raw IRC message without newlines -} ->+  ClientState {- ^ client state                     -} ->+  IO ClientState doNetworkLine networkId time line st =   case view (clientConnections . at networkId) st of     Nothing -> error "doNetworkLine: Network missing"@@ -219,13 +215,8 @@       let network = view csNetwork cs in       case parseRawIrcMsg (asUtf8 line) of         Nothing ->-          do let txt = Text.pack ("Malformed message: " ++ show line)-                 msg = ClientMessage-                        { _msgTime    = time-                        , _msgNetwork = network-                        , _msgBody    = ErrorBody txt-                        }-             eventLoop (recordNetworkMessage msg st)+          do let msg = Text.pack ("Malformed message: " ++ show line)+             return $! recordError time cs msg st          Just raw ->           do (st1,passed) <- clientPark st $ \ptr ->@@ -233,7 +224,7 @@                                  (view (clientExtensions . esActive) st)  -             if not passed then eventLoop st1 else do+             if not passed then return st1 else do               let time' = computeEffectiveTime time (view msgTags raw) @@ -245,10 +236,9 @@                           messageHooks               case stateHook (cookIrcMsg raw) of-               Nothing  -> eventLoop st1 -- Message ignored+               Nothing  -> return st1 -- Message ignored                Just irc -> do traverse_ (sendMsg cs) replies-                              st3 <- clientResponse time' irc cs st2-                              eventLoop st3+                              clientResponse time' irc cs st2                  where                    -- state with message recorded                    recSt = case viewHook irc of@@ -285,14 +275,14 @@ processConnectCmd ::   ZonedTime       {- ^ now             -} ->   NetworkState    {- ^ current network -} ->-  ClientState                             ->+  ClientState     {- ^ client state    -} ->   [ExpansionChunk]{- ^ command         -} ->   IO ClientState processConnectCmd now cs st0 cmdTxt =   do dc <- forM disco $ \t ->              Text.pack . formatTime defaultTimeLocale "%H:%M:%S"                <$> utcToLocalZonedTime t-     let failureCase = reportConnectCmdError now cs+     let failureCase e = recordError now cs ("Bad connect-cmd: " <> e)      case resolveMacroExpansions (commandExpansion dc st0) (const Nothing) cmdTxt of        Nothing -> return $! failureCase "Unable to expand connect command" st0        Just cmdTxt' ->@@ -307,17 +297,17 @@    _ -> Nothing  -reportConnectCmdError ::+recordError ::   ZonedTime       {- ^ now             -} ->   NetworkState    {- ^ current network -} ->-  Text            {- ^ bad command     -} ->-  ClientState ->+  Text            {- ^ error message   -} ->+  ClientState     {- ^ client state    -} ->   ClientState-reportConnectCmdError now cs cmdTxt =+recordError now cs msg =   recordNetworkMessage ClientMessage     { _msgTime    = now     , _msgNetwork = view csNetwork cs-    , _msgBody    = ErrorBody ("Bad connect-cmd: " <> cmdTxt)+    , _msgBody    = ErrorBody msg     }  -- | Find the ZNC provided server time@@ -342,7 +332,10 @@   -- | Respond to a VTY event.-doVtyEvent :: Event -> ClientState -> IO ()+doVtyEvent ::+  Event                  {- ^ vty event             -} ->+  ClientState            {- ^ client state          -} ->+  IO (Maybe ClientState) {- ^ nothing when finished -} doVtyEvent vtyEvent st =   case vtyEvent of     EvKey k modifier -> doKey k modifier st@@ -350,19 +343,23 @@       do let vty = view clientVty st          refresh vty          (w,h) <- displayBounds (outputIface vty)-         eventLoop $ set clientWidth w-                   $ set clientHeight h st+         return $! Just $! set clientWidth  w+                         $ set clientHeight h st     EvPaste utf8 ->-       let str = Text.unpack (Text.decodeUtf8With Text.lenientDecode utf8)-           st' = over clientTextBox (Edit.insertPaste str) st-       in eventLoop st'-    _ -> eventLoop st+       do let str = Text.unpack (Text.decodeUtf8With Text.lenientDecode utf8)+          return $! Just $! over clientTextBox (Edit.insertPaste str) st+    _ -> return (Just st)   -- | Map keyboard inputs to actions in the client-doKey :: Key -> [Modifier] -> ClientState -> IO ()+doKey ::+  Key         {- ^ key pressed    -} ->+  [Modifier]  {- ^ modifiers held -} ->+  ClientState {- ^ client state   -} ->+  IO (Maybe ClientState) doKey key modifier st =-  let changeEditor  f = eventLoop (over clientTextBox f st)+  let continue !out   = return (Just out)+      changeEditor  f = continue (over clientTextBox f st)       changeContent f = changeEditor                       $ over Edit.content f                       . set  Edit.lastOperation Edit.OtherOperation@@ -384,10 +381,11 @@         KChar '_' -> changeEditor (Edit.insert '\^_')         KChar 'o' -> changeEditor (Edit.insert '\^O')         KChar 'v' -> changeEditor (Edit.insert '\^V')-        KChar 'p' -> eventLoop (retreatFocus st)-        KChar 'n' -> eventLoop (advanceFocus st)-        KChar 'l' -> refresh (view clientVty st) >> eventLoop st-        _         -> eventLoop st+        KChar 'p' -> continue (retreatFocus st)+        KChar 'n' -> continue (advanceFocus st)+        KChar 'l' -> do refresh (view clientVty st)+                        continue st+        _         -> continue st      [MMeta] ->       case key of@@ -396,16 +394,18 @@         KChar 'd' -> changeEditor (Edit.killWordForward True)         KChar 'b' -> changeContent Edit.leftWord         KChar 'f' -> changeContent Edit.rightWord-        KChar 'a' -> eventLoop (jumpToActivity st)-        KChar 's' -> eventLoop (returnFocus st)+        KLeft     -> changeContent Edit.leftWord+        KRight    -> changeContent Edit.rightWord+        KChar 'a' -> continue (jumpToActivity st)+        KChar 's' -> continue (returnFocus st)         KChar c   | let names = clientWindowNames st                   , Just i <- elemIndex c names ->-                            eventLoop (jumpFocus i st)-        _ -> eventLoop st+                            continue (jumpFocus i st)+        _ -> continue st      [] -> -- no modifier       case key of-        KEsc       -> eventLoop (changeSubfocus FocusMessages st)+        KEsc       -> continue (changeSubfocus FocusMessages st)         KBS        -> changeContent Edit.backspace         KDel       -> changeContent Edit.delete         KLeft      -> changeContent Edit.left@@ -414,8 +414,8 @@         KEnd       -> changeEditor Edit.end         KUp        -> changeEditor $ \ed -> fromMaybe ed $ Edit.earlier ed         KDown      -> changeEditor $ \ed -> fromMaybe ed $ Edit.later ed-        KPageUp    -> eventLoop (pageUp st)-        KPageDown  -> eventLoop (pageDown st)+        KPageUp    -> continue (scrollClient ( scrollAmount st) st)+        KPageDown  -> continue (scrollClient (-scrollAmount st) st)          KEnter     -> doCommandResult True  =<< executeInput st         KBackTab   -> doCommandResult False =<< tabCompletion True  st@@ -424,32 +424,43 @@         KChar c    -> changeEditor (Edit.insert c)          -- toggles-        KFun 2     -> eventLoop (over clientDetailView  not st)-        KFun 3     -> eventLoop (over clientActivityBar not st)-        KFun 4     -> eventLoop (over clientShowMetadata not st)+        KFun 2     -> continue (over clientDetailView  not st)+        KFun 3     -> continue (over clientActivityBar not st)+        KFun 4     -> continue (over clientShowMetadata not st) -        _          -> eventLoop st+        _          -> continue st -    _ -> eventLoop st -- unsupported modifier+    _ -> continue st -- unsupported modifier --- | Process 'CommandResult' by either running the 'eventLoop' with the--- new 'ClientState' or returning.-doCommandResult :: Bool -> CommandResult -> IO ()++-- | Process 'CommandResult' and update the 'ClientState' textbox+-- and error state. When quitting return 'Nothing'.+doCommandResult ::+  Bool          {- ^ clear on success -} ->+  CommandResult {- ^ command result   -} ->+  IO (Maybe ClientState) doCommandResult clearOnSuccess res =+  let continue !st = return (Just st) in   case res of-    CommandQuit    st -> clientShutdown st-    CommandSuccess st -> eventLoop (if clearOnSuccess then consumeInput st else st)-    CommandFailure st -> eventLoop (set clientBell True st)+    CommandQuit    st -> Nothing <$ clientShutdown st+    CommandSuccess st -> continue (if clearOnSuccess then consumeInput st else st)+    CommandFailure st -> continue (set clientBell True st) -executeInput :: ClientState -> IO CommandResult++-- | Execute the the command on the first line of the text box+executeInput ::+  ClientState {- ^ client state -} ->+  IO CommandResult executeInput st = execute (clientFirstLine st) st   -- | Respond to a timer event. doTimerEvent ::-  NetworkId {- ^ Network related to event -} ->-  TimedAction {- ^ Action to perform -} ->-  ClientState -> IO ()+  NetworkId   {- ^ Network related to event -} ->+  TimedAction {- ^ Action to perform        -} ->+  ClientState {- ^ client state             -} ->+  IO ClientState doTimerEvent networkId action =-  eventLoop <=< traverseOf (clientConnections . ix networkId)-                           (applyTimedAction action)+  traverseOf+    (clientConnections . ix networkId)+    (applyTimedAction action)
src/Client/EventLoop/Errors.hs view
@@ -15,7 +15,6 @@   ) where  import           Control.Exception-import           Data.Char import           Data.List.NonEmpty (NonEmpty(..)) import           Network.Connection import           Network.TLS@@ -27,20 +26,10 @@   NonEmpty String {- ^ client lines  -} exceptionToLines   = indentMessages-  . fmap cleanLine   . exceptionToLines'  indentMessages :: NonEmpty String -> NonEmpty String indentMessages (x :| xs) = x :| map ("⋯ "++) xs--cleanLine :: String -> String-cleanLine = map clean1-  where-    clean1 x-      | x < '\x20'  = chr (0x2400 + ord x)-      | x == '\DEL' = '␡'-      | isControl x = '�'-      | otherwise   = x  exceptionToLines' ::   SomeException   {- ^ network error -} ->
src/Client/Image.hs view
@@ -9,7 +9,10 @@ This module provides the renderer for the client's UI.  -}-module Client.Image (clientPicture) where+module Client.Image+  ( clientPicture+  , scrollAmount+  ) where  import           Client.Image.Palette import           Client.Image.StatusLine@@ -18,7 +21,6 @@ import           Client.State.Focus import           Client.View import           Control.Lens-import           Data.List import           Graphics.Vty (Background(..), Picture(..), Cursor(..)) import           Graphics.Vty.Image @@ -34,35 +36,52 @@               , picLayers     = [img]               } +-- | Primary UI render logic clientImage ::-  ClientState ->-  (Int, Image, ClientState) -- ^ text box cursor position, image, updated state-clientImage st = (pos, img, st'')+  ClientState               {- ^ client state -} ->+  (Int, Image, ClientState) {- ^ text box cursor position, image, updated state -}+clientImage st = (pos, img, st')   where-    (st', mp) = messagePane mainHeight focus (view clientSubfocus st) st-    (st'', extras) = mapAccumL renderExtra st' splits+    (mainHeight, splitHeight) = clientWindowHeights (imageHeight activityBar) st+    splitFocuses              = clientExtraFocuses st+    focus                     = view clientFocus st+    (pos , tbImg )            = textboxImage st -    (pos, tbImg) = textboxImage st''-    img = vertCat extras <->-          mp <->-          statusLineImage st'' <->-          tbImg+    -- update client state for scroll clamp+    st' = over clientScroll (max 0 . subtract overscroll) st -    focus = view clientFocus st+    (overscroll, msgs) = messagePane mainHeight focus (view clientSubfocus st) st+    splits = renderExtra st' <$> splitFocuses+    -- outgoing state is ignored here, splits don't get to truncate scrollback -    (mainHeight, splitHeight) = clientWindowHeights st-    splits                    = clientExtraFocuses st+    img = vertCat splits      <->+          msgs                <->+          activityBar         <->+          statusLineImage st' <->+          tbImg -    renderExtra stIn focus1 = (stOut, outImg)+    activityBar = activityBarImage st+        -- must be st, not st', needed to compute window heights+        -- before rendering the message panes++    renderExtra stIn focus1 = outImg       where-        (stOut,msgImg) = messagePane splitHeight focus1 FocusMessages stIn-        pal = clientPalette st+        (_,msgImg) = messagePane splitHeight focus1 FocusMessages stIn+        pal = clientPalette stIn         divider = view palWindowDivider pal-        outImg = msgImg <-> minorStatusLineImage focus1 st-                        <-> charFill divider ' ' (view clientWidth st) 1+        outImg = msgImg <-> minorStatusLineImage focus1 stIn+                        <-> charFill divider ' ' (view clientWidth stIn) 1 -messagePane :: Int -> Focus -> Subfocus -> ClientState -> (ClientState, Image)-messagePane h focus subfocus st = (st', img)+-- | Generate an image corresponding to the image lines of the given+-- focus and subfocus. Returns the number of lines overscrolled to+-- assist in clamping scroll to the lines available in the window.+messagePane ::+  Int          {- ^ available rows                -} ->+  Focus        {- ^ focused window                -} ->+  Subfocus     {- ^ subfocus to render            -} ->+  ClientState  {- ^ client state                  -} ->+  (Int, Image) {- ^ overscroll, rendered messages -}+messagePane h focus subfocus st = (overscroll, img)   where     images = viewLines focus subfocus st     vimg   = assemble emptyImage images@@ -71,8 +90,6 @@      overscroll = vh - imageHeight vimg -    st' = over clientScroll (max 0 . subtract overscroll) st-     assemble acc _ | imageHeight acc >= vh = cropTop vh acc     assemble acc [] = acc     assemble acc (x:xs) = assemble (lineWrap w x <-> acc) xs@@ -82,9 +99,44 @@      w      = view clientWidth st -lineWrap :: Int -> Image -> Image+-- | Given an image, break the image up into chunks of at most the+-- given width and stack the resulting chunks vertically top-to-bottom.+lineWrap ::+  Int   {- ^ maximum image width -} ->+  Image {- ^ unwrapped image     -} ->+  Image {- ^ wrapped image       -} lineWrap w img-  | imageWidth img > w = cropRight w img <-> lineWrap w (cropLeft (imageWidth img - w) img)+  | imageWidth img > w = cropRight w img <->+                         lineWrap w (cropLeft (imageWidth img - w) img)   | otherwise = img <|> char defAttr ' '-                        -- trailing space with default attributes deals with bug in VTY-                        -- where the formatting will continue past the end of chat messages+      -- trailing space with default attributes deals with bug in VTY+      -- where the formatting will continue past the end of chat messages+++-- | Compute the number of lines in a page at the current window size+scrollAmount ::+  ClientState {- ^ client state              -} ->+  Int         {- ^ scroll amount             -}+scrollAmount st = max 1 (snd (clientWindowHeights actSize st))+               -- extra will be equal to main or 1 smaller+  where+    actSize = imageHeight (activityBarImage st)+++-- | Number of lines to allocate for the focused window and the+-- main window. This doesn't include the textbox, activity bar,+-- or status line.+clientWindowHeights ::+  Int         {- ^ activity bar height       -} ->+  ClientState {- ^ client state              -} ->+  (Int,Int)   {- ^ main height, extra height -}+clientWindowHeights activityBar st =+  (max 0 (h - overhead - extras*d), max 0 (d-overhead))+  where+    d        = h `quot` (1 + extras)++    h        = max 0 (view clientHeight st - activityBar) -- lines available++    extras   = length (clientExtraFocuses st)++    overhead = 2 -- status line and textbox/divider
src/Client/Image/Message.hs view
@@ -20,6 +20,7 @@   , quietIdentifier   , coloredUserInfo   , coloredIdentifier+  , cleanText   ) where  import           Client.Image.MircFormatting@@ -76,13 +77,23 @@   , bodyImage rm params body   ] +cleanChar :: Char -> Char+cleanChar x+  | x < '\x20'  = chr (0x2400 + ord x)+  | x == '\DEL' = '␡'+  | isControl x = '�'+  | otherwise   = x++cleanText :: Text -> Text+cleanText = Text.map cleanChar+ errorImage ::   MessageRendererParams ->   Text {- ^ error message -} ->   Image errorImage params txt = horizCat   [ text' (view palError (rendPalette params)) "error "-  , text' defAttr txt+  , text' defAttr (cleanText txt)   ]  normalImage ::@@ -91,7 +102,7 @@   Image normalImage params txt = horizCat   [ text' (view palLabel (rendPalette params)) "client "-  , text' defAttr txt+  , text' defAttr (cleanText txt)   ]  -- | Render the given time according to the current mode and palette.
src/Client/Image/MircFormatting.hs view
@@ -170,4 +170,3 @@     (first, cntl:rest) -> Vty.string defAttr first Vty.<|>                           controlImage cntl Vty.<|>                           plainText rest-
src/Client/Image/StatusLine.hs view
@@ -1,4 +1,4 @@-{-# Language BangPatterns #-}+{-# Language OverloadedStrings, BangPatterns #-} {-| Module      : Client.Image.StatusLine Description : Renderer for status line@@ -14,6 +14,7 @@ module Client.Image.StatusLine   ( statusLineImage   , minorStatusLineImage+  , activityBarImage   ) where  import           Client.Image.Palette@@ -32,20 +33,20 @@ import           Numeric  -- | Renders the status line between messages and the textbox.-statusLineImage :: ClientState -> Image-statusLineImage st-  = activityBar <->-    content <|> charFill defAttr '─' fillSize 1+statusLineImage ::+  ClientState {- ^ client state             -} ->+  Image       {- ^ activity bar, status bar -}+statusLineImage st = content <|> charFill defAttr '─' fillSize 1   where     fillSize = max 0 (view clientWidth st - imageWidth content)-    (activitySummary, activityBar) = activityImages st     content = horizCat       [ myNickImage st       , focusImage st-      , activitySummary+      , activitySummary st       , detailImage st       , nometaImage st       , scrollImage st+      , filterImage st       , latencyImage st       ] @@ -60,12 +61,20 @@ scrollImage :: ClientState -> Image scrollImage st   | 0 == view clientScroll st = emptyImage-  | otherwise = infoBubble-              $ string attr "scroll"+  | otherwise = infoBubble (string attr "scroll")   where     pal  = clientPalette st     attr = view palLabel pal +filterImage :: ClientState -> Image+filterImage st =+  case clientActiveRegex st of+    Nothing -> emptyImage+    Just {} -> infoBubble (string attr "filtered")+  where+    pal  = clientPalette st+    attr = view palLabel pal+ latencyImage :: ClientState -> Image latencyImage st   | Just network <- views clientFocus focusNetwork st@@ -105,37 +114,53 @@     pal  = clientPalette st     attr = view palLabel pal -activityImages :: ClientState -> (Image, Image)-activityImages st = (summary, activityBar)+-- | Image for little box with active window names:+--+-- @-[15p]@+activitySummary :: ClientState -> Image+activitySummary st+  | null indicators = emptyImage+  | otherwise       = string defAttr "─[" <|>+                      horizCat indicators <|>+                      string defAttr "]"   where-    activityBar-      | view clientActivityBar st = activityBar' <|> activityFill-      | otherwise                 = emptyImage+    winNames = clientWindowNames st ++ repeat '?' -    summary-      | null indicators = emptyImage-      | otherwise       = string defAttr "─[" <|>-                          horizCat indicators <|>-                          string defAttr "]"+    indicators = foldr aux [] (zip winNames windows)+    windows    = views clientWindows Map.elems st -    activityFill = charFill defAttr '─'-                        (max 0 (view clientWidth st - imageWidth activityBar'))-                        1+    aux (i,w) rest+      | view winUnread w == 0 = rest+      | otherwise = char attr i : rest+      where+        pal = clientPalette st+        attr | view winMention w = view palMention pal+             | otherwise         = view palActivity pal -    activityBar' = foldr baraux emptyImage-                 $ zip winNames+-- | Multi-line activity information enabled by F3+activityBarImage :: ClientState -> Image+activityBarImage st+  | view clientActivityBar st = activityBar'+  | otherwise                 = emptyImage+  where+    activityBar' = makeLines (view clientWidth st)+                 $ catMaybes+                 $ zipWith baraux winNames                  $ Map.toList                  $ view clientWindows st -    baraux (i,(focus,w)) rest-      | n == 0 = rest-      | otherwise = string defAttr "─[" <|>+    winNames = clientWindowNames st ++ repeat '?'++    baraux i (focus,w)+      | n == 0 = Nothing -- todo: make configurable+      | otherwise = Just+                  $ string defAttr "─[" <|>                     char (view palWindowName pal) i <|>                     char defAttr              ':' <|>                     text' (view palLabel pal) focusText <|>                     char defAttr              ':' <|>                     string attr               (show n) <|>-                    string defAttr "]" <|> rest+                    string defAttr "]"       where         n   = view winUnread w         pal = clientPalette st@@ -147,19 +172,25 @@             NetworkFocus net    -> net             ChannelFocus _ chan -> idText chan -    windows  = views clientWindows Map.elems st-    winNames = clientWindowNames st ++ repeat '?' -    indicators  = foldr aux [] (zip winNames windows)-    aux (i,w) rest-      | view winUnread w == 0 = rest-      | otherwise = char attr i : rest-      where-        pal = clientPalette st-        attr | view winMention w = view palMention pal-             | otherwise         = view palActivity pal +makeLines ::+  Int     {- ^ window width       -} ->+  [Image] {- ^ components to pack -} ->+  Image+makeLines _ [] = emptyImage+makeLines w (x:xs) = go x xs+  where +    go acc (y:ys)+      | let acc' = acc <|> y+      , imageWidth acc' <= w+      = go acc' ys++    go acc ys = makeLines w ys+            <-> acc <|> charFill defAttr '─' (max 0 (w - imageWidth acc)) 1++ myNickImage :: ClientState -> Image myNickImage st =   case view clientFocus st of@@ -238,16 +269,27 @@ viewSubfocusLabel pal subfocus =   case subfocus of     FocusMessages -> Nothing-    FocusWindows  -> Just $ string (view palLabel pal) "windows"+    FocusWindows filt -> Just $ string (view palLabel pal) "windows" <|>+                                opt (windowFilterName filt)     FocusInfo     -> Just $ string (view palLabel pal) "info"     FocusUsers    -> Just $ string (view palLabel pal) "users"     FocusMentions -> Just $ string (view palLabel pal) "mentions"     FocusPalette  -> Just $ string (view palLabel pal) "palette"     FocusHelp mb  -> Just $ string (view palLabel pal) "help" <|>-                            foldMap (\cmd -> char defAttr ':' <|>-                                        text' (view palLabel pal) cmd) mb+                            opt mb     FocusMasks m  -> Just $ horizCat       [ string (view palLabel pal) "masks"       , char defAttr ':'       , char (view palLabel pal) m       ]+  where+    opt = foldMap (\cmd -> char defAttr ':' <|>+                           text' (view palLabel pal) cmd)++windowFilterName :: WindowsFilter -> Maybe Text+windowFilterName x =+  case x of+    AllWindows     -> Nothing+    NetworkWindows -> Just "networks"+    ChannelWindows -> Just "channels"+    UserWindows    -> Just "users"
src/Client/Image/Textbox.hs view
@@ -118,7 +118,7 @@  allCommands = (Left <$> macros) <> (Right <$> commands)  (attr, continue)    = case recognize (Text.pack cmd) allCommands of-       Exact (Right (Command spec _ _)) ->+       Exact (Right Command{cmdArgumentSpec = spec}) ->          ( specAttr spec          , argumentsImage pal spec          )
src/Client/Options.hs view
@@ -17,6 +17,7 @@   -- * Lenses   , optConfigFile   , optInitialNetworks+  , optNoConnect    -- * Options loader   , getOptions@@ -38,6 +39,7 @@ data Options = Options   { _optConfigFile      :: Maybe FilePath -- ^ configuration file path   , _optInitialNetworks :: [Text]         -- ^ initial networks+  , _optNoConnect       :: Bool           -- ^ disable autoconnect   , _optShowHelp        :: Bool           -- ^ show help message   , _optShowVersion     :: Bool           -- ^ show version message   }@@ -51,6 +53,7 @@   , _optInitialNetworks = []   , _optShowHelp        = False   , _optShowVersion     = False+  , _optNoConnect       = False   }  -- | Option descriptions@@ -58,6 +61,8 @@ options =   [ Option "c" ["config"]  (ReqArg (set optConfigFile . Just) "PATH")     "Configuration file path"+  , Option "!" ["noconnect"] (NoArg (set optNoConnect True))+    "Disable autoconnecting"   , Option "h" ["help"]    (NoArg (set optShowHelp True))     "Show help"   , Option "v" ["version"] (NoArg (set optShowVersion True))
src/Client/State.hs view
@@ -38,6 +38,7 @@   , clientConnection   , clientBell   , clientExtensions+  , clientRegex    -- * Client operations   , withClientState@@ -45,7 +46,7 @@   , clientShutdown   , clientPark   , clientMatcher-  , urlPattern+  , clientActiveRegex    , consumeInput   , currentCompletionList@@ -60,9 +61,11 @@   , clientHighlights   , clientWindowNames   , clientPalette+  , clientAutoconnects+  , clientActiveCommand    , clientExtraFocuses-  , clientWindowHeights+  , currentNickCompletionMode    -- * Add messages to buffers   , recordChannelMessage@@ -79,16 +82,19 @@   , jumpFocus    -- * Scrolling-  , pageUp-  , pageDown+  , scrollClient    -- * Extensions   , ExtensionState   , esActive +  -- * URL view+  , urlPattern+   ) where  import           Client.CApi+import           Client.Commands.WordCompletion import           Client.Configuration import           Client.Configuration.ServerSettings import           Client.Image.Message@@ -160,6 +166,7 @@   , _clientDetailView        :: !Bool                     -- ^ use detailed rendering mode   , _clientActivityBar       :: !Bool                     -- ^ visible activity bar   , _clientShowMetadata      :: !Bool                     -- ^ visible activity bar+  , _clientRegex             :: (Maybe Regex)             -- ^ optional persistent filter    , _clientBell              :: !Bool                     -- ^ sound a bell next draw @@ -234,6 +241,7 @@         , _clientScroll            = 0         , _clientDetailView        = False         , _clientShowMetadata      = True+        , _clientRegex             = Nothing         , _clientActivityBar       = view configActivityBar cfg         , _clientNextConnectionId  = 0         , _clientBell              = False@@ -491,6 +499,13 @@       ++ channelUserList network chan st     _                         -> [] +-- | Returns the 'WordCompletionMode' associated with the current network.+currentNickCompletionMode :: ClientState -> WordCompletionMode+currentNickCompletionMode st =+  fromMaybe defaultNickWordCompleteMode $+  do network <- views clientFocus focusNetwork st+     preview (clientConnection network . csSettings . ssNickCompletion) st+ networkChannelList ::   Text         {- ^ network -} ->   ClientState                  ->@@ -506,23 +521,46 @@ channelUserList network channel =   views (clientConnection network . csChannels . ix channel . chanUsers) HashMap.keys --- | Construct a text matching predicate used to filter the message window. clientMatcher :: ClientState -> Text -> Bool clientMatcher st =-  case break (==' ') (clientFirstLine st) of-    ("/grep" ,_:reStr) -> go True  reStr-    ("/grepi",_:reStr) -> go False reStr-    ("/url"  ,_      ) -> match urlPattern-    _                  -> const True+  case clientActiveRegex st of+    Nothing -> const True+    Just r  -> matchTest r++-- | Construct a text matching predicate used to filter the message window.+clientActiveRegex :: ClientState -> Maybe Regex+clientActiveRegex st =+  case clientActiveCommand st of+    Just ("grep" ,reStr) -> go True  reStr+    Just ("grepi",reStr) -> go False reStr+    _ -> case view clientRegex st of+           Nothing -> Nothing+           Just r  -> Just r   where     go sensitive reStr =-      case compile defaultCompOpt{caseSensitive=sensitive} defaultExecOpt reStr of-        Left{}  -> const True-        Right r -> match r :: Text -> Bool+      case compile defaultCompOpt{caseSensitive=sensitive}+                   defaultExecOpt{captureGroups=False}+                   reStr of+        Left{}  -> Nothing+        Right r -> Just r ++-- | 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+    ('/':cmd,_:args) -> Just (cmd,args)+    _                -> Nothing++ urlPattern :: Regex-urlPattern = makeRegex-  ("https?://([[:alnum:]-]+\\.)*([[:alnum:]-]+)(:[[:digit:]]+)?(/[^[:space:]]*)"::String)+Right urlPattern =+  compile+    defaultCompOpt+    defaultExecOpt{captureGroups=False}+    "https?://([[:alnum:]-]+\\.)*([[:alnum:]-]+)(:[[:digit:]]+)?(/[^[:cntrl:][:space:]]*)"  -- | Remove a network connection and unlink it from the network map. -- This operation assumes that the networkconnection exists and should@@ -577,7 +615,7 @@   IrcMsg                     {- ^ message recieved         -} ->   NetworkId                  {- ^ message network          -} ->   NetworkState               {- ^ network connection state -} ->-  ClientState                                                 ->+  ClientState                {- ^ client state             -} ->   ([RawIrcMsg], ClientState) {- ^ response , updated state -} applyMessageToClientState time irc networkId cs st =   cs' `seq` (reply, st')@@ -656,21 +694,9 @@ -- Scrolling ------------------------------------------------------------------------ --- | Scroll the current buffer to show older messages-pageUp :: ClientState -> ClientState-pageUp st = over clientScroll (+ scrollAmount st) st- -- | Scroll the current buffer to show newer messages-pageDown :: ClientState -> ClientState-pageDown st = over clientScroll (max 0 . subtract (scrollAmount st)) st---- | Compute the number of lines in a page at the current window size-scrollAmount :: ClientState -> Int-scrollAmount st = max 1 (min main extra)-  where-    (main,extra) = clientWindowHeights st--+scrollClient :: Int -> ClientState -> ClientState+scrollClient amt = over clientScroll $ \n -> max 0 (n + amt)   -- | List of extra focuses to display as split windows@@ -680,28 +706,7 @@     FocusMessages -> view clientFocus st `delete` view clientExtraFocus st     _             -> [] --- | Number of lines to allocate for the focused window and the--- main window. This doesn't include the textbox, activity bar,--- or status line.-clientWindowHeights ::-  ClientState {- ^ client state              -} ->-  (Int,Int)   {- ^ main height, extra height -}-clientWindowHeights st = (max 0 (h - overhead - extras*d), max 0 (d-overhead))-  where-    d        = h `quot` (1 + extras) -    h        = max 0 (view clientHeight st - reservedLines) -- lines available--    extras   = length (clientExtraFocuses st)--    overhead = 2 -- status line and textbox/divider--    reservedLines-      | view clientActivityBar st = 1 -- activity bar-      | otherwise                 = 0--- ------------------------------------------------------------------------ -- Focus Management ------------------------------------------------------------------------@@ -727,7 +732,7 @@   | 0 <= i, i < Map.size windows = changeFocus focus st   | otherwise                    = st   where-    windows = view clientWindows st+    windows   = view clientWindows st     (focus,_) = Map.elemAt i windows  -- | Change the window focus to the given value, reset the subfocus@@ -821,3 +826,10 @@ -- | Produce the list of window names configured for the client. clientPalette :: ClientState -> Palette clientPalette = view (clientConfig . configPalette)++-- | Returns the list of network names that requested autoconnection.+clientAutoconnects :: ClientState -> [Text]+clientAutoconnects st =+  [ network | (network, cfg) <- views (clientConfig . configServers) HashMap.toList st+            , view ssAutoconnect cfg+            ]
src/Client/State/Focus.hs view
@@ -16,6 +16,7 @@   ( -- * Types     Focus(..)   , Subfocus(..)+  , WindowsFilter(..)    -- * Focus operations   , focusNetwork@@ -46,7 +47,7 @@   | FocusInfo        -- ^ Show channel metadata   | FocusUsers       -- ^ Show channel user list   | FocusMasks !Char -- ^ Show channel mask list for given mode-  | FocusWindows     -- ^ Show client windows+  | FocusWindows WindowsFilter -- ^ Show client windows   | FocusPalette     -- ^ Show current palette   | FocusMentions    -- ^ Show all mentions   | FocusHelp (Maybe Text) -- ^ Show help window with optional command@@ -71,3 +72,11 @@ focusNetwork Unfocused = Nothing focusNetwork (NetworkFocus network) = Just network focusNetwork (ChannelFocus network _) = Just network++-- | Filter argument for 'FocusWindows'+data WindowsFilter+  = AllWindows     -- ^ no filter+  | NetworkWindows -- ^ only network windows+  | ChannelWindows -- ^ only channel windows+  | UserWindows    -- ^ only user windows+  deriving (Eq, Show)
src/Client/State/Network.hs view
@@ -64,6 +64,11 @@   , TimedAction(..)   , nextTimedAction   , applyTimedAction++  -- * Moderation+  , useChanServ+  , sendModeration+  , sendTopic   ) where  import           Client.Configuration.ServerSettings@@ -820,3 +825,48 @@          sendMsg cs (ircPing ["ping"])          return $! set csNextPingTime (Just $! addUTCTime 60 now)                 $  set csPingStatus   (PingSent now) cs++------------------------------------------------------------------------+-- Moderation+------------------------------------------------------------------------++-- | Used to send commands that require ops to perform.+-- If this channel is one that the user has chanserv access and ops are needed+-- then ops are requested and the commands are queued, otherwise send them+-- directly.+sendModeration ::+  Identifier      {- ^ channel       -} ->+  [RawIrcMsg]     {- ^ commands      -} ->+  NetworkState    {- ^ network state -} ->+  IO NetworkState+sendModeration channel cmds cs+  | useChanServ channel cs =+      do let cmd = ircPrivmsg "ChanServ" (Text.unwords ["OP", idText channel])+         sendMsg cs cmd+         return $ csChannels . ix channel . chanQueuedModeration <>~ cmds $ cs+  | otherwise = cs <$ traverse_ (sendMsg cs) cmds++useChanServ ::+  Identifier   {- ^ channel            -} ->+  NetworkState {- ^ network state      -} ->+  Bool         {- ^ chanserv available -}+useChanServ channel cs =+  channel `elem` view (csSettings . ssChanservChannels) cs &&+  not (iHaveOp channel cs)++sendTopic ::+  Identifier   {- ^ channel       -} ->+  Text         {- ^ topic         -} ->+  NetworkState {- ^ network state -} ->+  IO ()+sendTopic channelId topic cs = sendMsg cs cmd+  where+    chanservTopicCmd =+      ircPrivmsg+        "ChanServ"+        (Text.unwords ["TOPIC", idText channelId, topic])++    cmd+      | Text.null topic          = ircTopic channelId ""+      | useChanServ channelId cs = chanservTopicCmd+      | otherwise                = ircTopic channelId topic
src/Client/View.hs view
@@ -21,6 +21,7 @@ import           Client.View.Mentions import           Client.View.Messages import           Client.View.Palette+import           Client.View.UrlSelection import           Client.View.UserList import           Client.View.Windows import           Control.Lens@@ -29,6 +30,7 @@ viewLines :: Focus -> Subfocus -> ClientState -> [Image] viewLines focus subfocus !st =   case (focus, subfocus) of+    _ | Just ("url",_) <- clientActiveCommand st -> urlSelectionView focus st     (ChannelFocus network channel, FocusInfo) ->       channelInfoImages network channel st     (ChannelFocus network channel, FocusUsers)@@ -36,11 +38,10 @@       | otherwise                -> userListImages network channel st     (ChannelFocus network channel, FocusMasks mode) ->       maskListImages mode network channel st-    (_, FocusWindows) -> windowsImages st+    (_, FocusWindows filt) -> windowsImages filt st     (_, FocusMentions) -> mentionsViewLines st     (_, FocusPalette) -> paletteViewLines pal     (_, FocusHelp mb) -> helpImageLines mb pal-     _ -> chatMessageImages focus st   where     pal = clientPalette st
src/Client/View/Help.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language BangPatterns, OverloadedStrings #-}  {-| Module      : Client.View.Help@@ -21,6 +21,8 @@ import           Client.Image.Palette import           Client.Commands.Recognizer import           Control.Lens+import           Data.Foldable (toList)+import           Data.List (delete) import           Data.List.NonEmpty (NonEmpty((:|))) import           Data.Monoid ((<>)) import           Data.Text (Text)@@ -51,14 +53,20 @@       [string (view palError pal) $ "Unknown command, did you mean: " ++ suggestions]       where       suggestions = Text.unpack $ Text.intercalate " " ((cmdName <>) <$> sfxs)-    Exact (Command args doc impl) ->-      reverse $ commandSummary pal (pure cmdName) args+    Exact Command{cmdNames = names, cmdImplementation = impl,+                  cmdArgumentSpec = spec, cmdDocumentation = doc} ->+      reverse $ commandSummary pal (pure cmdName) spec               : emptyImage-              : explainContext impl+              : aliasLines+             ++ explainContext impl               : emptyImage-              : map parseIrcText docs+              : map parseIrcText (Text.lines doc)       where-        docs = Text.lines doc+        aliasLines =+          case delete cmdName (toList names) of+            [] -> []+            ns -> [ text' defAttr (Text.unwords ("Aliases:":ns))+                  , emptyImage ]  -- | Generate an explanation of the context where the given command -- implementation will be valid.@@ -81,8 +89,8 @@   [Image] {- ^ help lines -} listAllCommands pal =   reverse-    [ commandSummary pal name args-    | (name, Command args _ _) <- commandsList ]+    [ commandSummary pal names spec+    | Command{ cmdNames = names, cmdArgumentSpec = spec } <- commandsList ]  -- | Generate the help line for the given command and its -- specification for use in the list of commands.
+ src/Client/View/UrlSelection.hs view
@@ -0,0 +1,37 @@+{-# Language BangPatterns #-}+{-|+Module      : Client.View.UrlSelection+Description : URL selection module+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module provides a list of the URLs found in the current message+window in order to assist in selecting one to open with @/url@++-}+module Client.View.UrlSelection+  ( urlSelectionView+  ) where++import           Client.State+import           Client.State.Window+import           Client.State.Focus+import           Client.Image.Message+import           Control.Lens+import           Graphics.Vty.Image+import           Text.Regex.TDFA+import           Data.Text (Text)+++urlSelectionView :: Focus -> ClientState -> [Image]+urlSelectionView focus st =+    zipWith draw [1..]+         $ toListOf (clientWindows . ix focus . winMessages . folded . wlText . folding textUrls) st++textUrls :: Text -> [Text]+textUrls = getAllTextMatches . match urlPattern++draw :: Int -> Text -> Image+draw i url = string defAttr (shows i ". ")+         <|> text' defAttr (cleanText url)
src/Client/View/Windows.hs view
@@ -16,6 +16,7 @@ import           Client.State import           Client.State.Focus import           Client.State.Window+import           Client.State.Network import           Control.Lens import           Data.List import qualified Data.Map as Map@@ -23,23 +24,49 @@ import           Irc.Identifier  -- | Draw the image lines associated with the @/windows@ command.-windowsImages :: ClientState -> [Image]-windowsImages st-  = reverse-  $ createColumns-  $ zipWith (renderWindowColumns pal) names windows+windowsImages :: WindowsFilter -> ClientState -> [Image]+windowsImages filt st = reverse (createColumns windows)   where-    windows = views clientWindows Map.toAscList st+    windows = [ renderWindowColumns pal n k v+              | (n,(k,v)) <- zip names+                           $ views clientWindows Map.toAscList st+              , windowMatcher filt st k+              ]      pal     = clientPalette st     names   = clientWindowNames st ++ repeat '?' -renderWindowColumns :: Palette -> Char -> (Focus, Window) -> [Image]-renderWindowColumns pal name (focus, win) =++------------------------------------------------------------------------++windowMatcher :: WindowsFilter -> ClientState -> Focus -> Bool++windowMatcher AllWindows _ _ = True++windowMatcher NetworkWindows _ NetworkFocus{} = True++windowMatcher ChannelWindows st (ChannelFocus net chan) =+  case preview (clientConnection net) st of+    Just cs -> isChannelIdentifier cs chan+    Nothing -> True++windowMatcher UserWindows st (ChannelFocus net chan) =+  case preview (clientConnection net) st of+    Just cs -> not (isChannelIdentifier cs chan)+    Nothing -> True++windowMatcher _ _ _ = False++------------------------------------------------------------------------+++renderWindowColumns :: Palette -> Char -> Focus -> Window -> [Image]+renderWindowColumns pal name focus win =   [ char (view palWindowName pal) name   , renderedFocus pal focus   , renderedWindowInfo pal win   ]+  createColumns :: [[Image]] -> [Image] createColumns xs = map makeRow xs
src/Config/FromConfig.hs view
@@ -43,6 +43,7 @@ import           Control.Monad.Trans.State import           Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap+import qualified Data.HashSet as HashSet import           Data.Monoid import           Data.Ratio import           Data.Text (Text)@@ -100,6 +101,15 @@       n = floatingToRatio c e   parseConfig _                 = failure "expected integral number" +instance FromConfig Int where+  parseConfig v =+    do i <- parseConfig v+       let small = minBound :: Int+           large = maxBound :: Int+       when (i < toInteger small || toInteger large < i)+          (failure "int out of range")+       return (fromInteger i)+ -- | Matches 'Number' values ignoring the base instance Integral a => FromConfig (Ratio a) where   parseConfig (Number _ n)   = return $! fromIntegral n@@ -133,7 +143,8 @@ -- of the sections from that value. parseSections :: SectionParser a -> Value -> ConfigParser a parseSections (SectionParser p) (Sections xs) =-  do (res, xs') <- runStateT p (toHashMap xs)+  do hm <- toHashMap xs+     (res, xs') <- runStateT p hm      let unused = HashMap.keys xs'      unless (null unused)        (failure ("unknown keys: " <> Text.intercalate ", " unused))@@ -168,8 +179,19 @@                           Nothing -> failure ("section required: " <> key)                           Just x  -> return x -toHashMap :: [Section] -> HashMap Text Value-toHashMap xs = HashMap.fromList [ (k,v) | Section k v <- xs ] -- todo: handle duplicate sections+toHashMap :: [Section] -> ConfigParser (HashMap Text Value)+toHashMap xs =+  case duplicateCheck xs of+    Just key -> failure ("duplicate section: " <> key)+    Nothing  -> return $! HashMap.fromList [ (k,v) | Section k v <- xs ]++duplicateCheck :: [Section] -> Maybe Text+duplicateCheck = go HashSet.empty+  where+    go _ [] = Nothing+    go seen (Section x _:xs)+      | HashSet.member x seen = Just x+      | otherwise             = go (HashSet.insert x seen) xs  floatingToRatio :: Integral a => Integer -> Integer -> Ratio a floatingToRatio c e = fromIntegral c * 10 ^^ e