packages feed

glirc 2.14 → 2.15

raw patch · 40 files changed

+1477/−944 lines, 40 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Client.CommandArguments: CommandArguments :: Maybe FilePath -> [Text] -> Bool -> Bool -> CommandArguments
- Client.CommandArguments: [_cmdArgConfigFile] :: CommandArguments -> Maybe FilePath
- Client.CommandArguments: [_cmdArgInitialNetworks] :: CommandArguments -> [Text]
- Client.CommandArguments: [_cmdArgShowHelp] :: CommandArguments -> Bool
- Client.CommandArguments: [_cmdArgShowVersion] :: CommandArguments -> Bool
- Client.CommandArguments: cmdArgConfigFile :: Lens' CommandArguments (Maybe FilePath)
- Client.CommandArguments: cmdArgInitialNetworks :: Lens' CommandArguments [Text]
- Client.CommandArguments: data CommandArguments
- Client.CommandArguments: getCommandArguments :: IO CommandArguments
- Client.Image.ChannelInfo: channelInfoImages :: Text -> Identifier -> ClientState -> [Image]
- Client.Image.Help: helpImageLines :: Maybe Text -> Palette -> [Image]
- Client.Image.MaskList: maskListImages :: Char -> Text -> Identifier -> ClientState -> [Image]
- Client.Image.PaletteView: paletteViewLines :: Palette -> [Image]
- Client.Image.UserList: userInfoImages :: Text -> Identifier -> ClientState -> [Image]
- Client.Image.UserList: userListImages :: Text -> Identifier -> ClientState -> [Image]
- Client.Image.Windows: windowsImages :: ClientState -> [Image]
- Client.State.Focus: _FocusInfo :: Prism' Subfocus ()
- Client.State.Focus: _FocusMasks :: Prism' Subfocus Char
- Client.State.Focus: _FocusMessages :: Prism' Subfocus ()
- Client.State.Focus: _FocusUsers :: Prism' Subfocus ()
- Client.State.Focus: _FocusWindows :: Prism' Subfocus ()
+ Client.Commands.Arguments: instance GHC.Show.Show (Client.Commands.Arguments.ArgumentSpec s)
+ Client.Commands.Interpolation: Macro :: MacroSpec -> [[ExpansionChunk]] -> Macro
+ Client.Commands.Interpolation: [MacroSpec] :: ArgumentSpec s -> MacroSpec
+ Client.Commands.Interpolation: [macroCommands] :: Macro -> [[ExpansionChunk]]
+ Client.Commands.Interpolation: [macroSpec] :: Macro -> MacroSpec
+ Client.Commands.Interpolation: data Macro
+ Client.Commands.Interpolation: data MacroSpec
+ Client.Commands.Interpolation: instance GHC.Show.Show Client.Commands.Interpolation.Macro
+ Client.Commands.Interpolation: instance GHC.Show.Show Client.Commands.Interpolation.MacroSpec
+ Client.Commands.Interpolation: noMacroArguments :: MacroSpec
+ Client.Commands.Interpolation: parseMacroSpecs :: Text -> Maybe MacroSpec
+ Client.Commands.Recognizer: Exact :: a -> Recognition a
+ Client.Commands.Recognizer: Invalid :: Recognition a
+ Client.Commands.Recognizer: Prefix :: [Text] -> Recognition a
+ Client.Commands.Recognizer: addCommand :: Text -> a -> Recognizer a -> Recognizer a
+ Client.Commands.Recognizer: data Recognition a
+ Client.Commands.Recognizer: data Recognizer a
+ Client.Commands.Recognizer: fromCommands :: [(Text, a)] -> Recognizer a
+ Client.Commands.Recognizer: instance GHC.Base.Functor Client.Commands.Recognizer.Recognition
+ Client.Commands.Recognizer: instance GHC.Base.Functor Client.Commands.Recognizer.Recognizer
+ Client.Commands.Recognizer: instance GHC.Base.Monoid (Client.Commands.Recognizer.Recognizer a)
+ Client.Commands.Recognizer: instance GHC.Show.Show a => GHC.Show.Show (Client.Commands.Recognizer.Recognition a)
+ Client.Commands.Recognizer: instance GHC.Show.Show a => GHC.Show.Show (Client.Commands.Recognizer.Recognizer a)
+ Client.Commands.Recognizer: keys :: Recognizer a -> [Text]
+ Client.Commands.Recognizer: recognize :: Text -> Recognizer a -> Recognition a
+ Client.Image.Palette: [_palCommandError] :: Palette -> Attr
+ Client.Image.Palette: [_palCommandPrefix] :: Palette -> Attr
+ Client.Image.Palette: palCommandError :: Lens' Palette Attr
+ Client.Image.Palette: palCommandPrefix :: Lens' Palette Attr
+ Client.Network.Async: NetworkOpen :: !NetworkId -> !ZonedTime -> NetworkEvent
+ Client.Options: Options :: Maybe FilePath -> [Text] -> Bool -> Bool -> Options
+ Client.Options: [_optConfigFile] :: Options -> Maybe FilePath
+ Client.Options: [_optInitialNetworks] :: Options -> [Text]
+ Client.Options: [_optShowHelp] :: Options -> Bool
+ Client.Options: [_optShowVersion] :: Options -> Bool
+ Client.Options: data Options
+ Client.Options: getOptions :: IO Options
+ Client.Options: optConfigFile :: Lens' Options (Maybe FilePath)
+ Client.Options: optInitialNetworks :: Lens' Options [Text]
+ Client.State: [_clientShowMetadata] :: ClientState -> !Bool
+ Client.State: clientPalette :: ClientState -> Palette
+ Client.State: clientShowMetadata :: Lens' ClientState Bool
+ Client.State: clientWindowNames :: ClientState -> [Char]
+ Client.State.EditBox: toggle :: Content -> Content
+ Client.State.EditBox.Content: toggle :: Content -> Content
+ Client.State.Focus: FocusMentions :: Subfocus
+ Client.State.Window: [_wlImportance] :: WindowLine -> !WindowLineImportance
+ Client.State.Window: [_wlTimestamp] :: WindowLine -> {-# UNPACK #-} !UTCTime
+ Client.State.Window: wlImportance :: Lens' WindowLine WindowLineImportance
+ Client.State.Window: wlTimestamp :: Lens' WindowLine UTCTime
+ Client.View: viewLines :: ClientState -> [Image]
+ Client.View.ChannelInfo: channelInfoImages :: Text -> Identifier -> ClientState -> [Image]
+ Client.View.Help: helpImageLines :: Maybe Text -> Palette -> [Image]
+ Client.View.MaskList: maskListImages :: Char -> Text -> Identifier -> ClientState -> [Image]
+ Client.View.Mentions: mentionsViewLines :: ClientState -> [Image]
+ Client.View.Messages: chatMessageImages :: ClientState -> [Image]
+ Client.View.Palette: paletteViewLines :: Palette -> [Image]
+ Client.View.UserList: userInfoImages :: Text -> Identifier -> ClientState -> [Image]
+ Client.View.UserList: userListImages :: Text -> Identifier -> ClientState -> [Image]
+ Client.View.Windows: windowsImages :: ClientState -> [Image]
- Client.Commands: commands :: HashMap Text Command
+ Client.Commands: commands :: Recognizer Command
- Client.Configuration: Configuration :: ServerSettings -> (HashMap Text ServerSettings) -> Palette -> Text -> HashSet Identifier -> Maybe Integer -> Maybe FilePath -> HashMap Text [[ExpansionChunk]] -> [FilePath] -> Maybe FilePath -> HashSet Identifier -> Bool -> Configuration
+ Client.Configuration: Configuration :: ServerSettings -> (HashMap Text ServerSettings) -> Palette -> Text -> HashSet Identifier -> Maybe Integer -> Maybe FilePath -> Recognizer Macro -> [FilePath] -> Maybe FilePath -> HashSet Identifier -> Bool -> Configuration
- Client.Configuration: [_configMacros] :: Configuration -> HashMap Text [[ExpansionChunk]]
+ Client.Configuration: [_configMacros] :: Configuration -> Recognizer Macro
- Client.Configuration: configMacros :: Lens' Configuration (HashMap Text [[ExpansionChunk]])
+ Client.Configuration: configMacros :: Lens' Configuration (Recognizer Macro)
- Client.Image.Palette: Palette :: Vector Attr -> Attr -> Maybe Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Palette
+ Client.Image.Palette: Palette :: Vector Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Attr -> Palette
- Client.Image.Palette: [_palSelfHighlight] :: Palette -> Maybe Attr
+ Client.Image.Palette: [_palSelfHighlight] :: Palette -> Attr
- Client.Image.Palette: palSelfHighlight :: Lens' Palette (Maybe Attr)
+ Client.Image.Palette: palSelfHighlight :: Lens' Palette Attr
- Client.State: ClientState :: !(Map Focus Window) -> !Focus -> !Focus -> !Subfocus -> !(IntMap NetworkState) -> !Int -> !ConnectionContext -> !(TQueue NetworkEvent) -> !(HashMap Text NetworkId) -> !Vty -> !EditBox -> !Int -> !Int -> !Configuration -> !Int -> !Bool -> !Bool -> !Bool -> !(HashSet Identifier) -> !ExtensionState -> ClientState
+ Client.State: ClientState :: !(Map Focus Window) -> !Focus -> !Focus -> !Subfocus -> !(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.EditBox: class HasLine c_aiUU where pos = (.) line pos text = (.) line text
+ Client.State.EditBox: class HasLine c_aKQ0 where pos = (.) line pos text = (.) line text
- Client.State.EditBox: line :: HasLine c_aiUU => Lens' c_aiUU Line
+ Client.State.EditBox: line :: HasLine c_aKQ0 => Lens' c_aKQ0 Line
- Client.State.EditBox: pos :: HasLine c_aiUU => Lens' c_aiUU Int
+ Client.State.EditBox: pos :: HasLine c_aKQ0 => Lens' c_aKQ0 Int
- Client.State.EditBox: text :: HasLine c_aiUU => Lens' c_aiUU String
+ Client.State.EditBox: text :: HasLine c_aKQ0 => Lens' c_aKQ0 String
- Client.State.EditBox.Content: class HasLine c_aiUU where pos = (.) line pos text = (.) line text
+ Client.State.EditBox.Content: class HasLine c_aKQ0 where pos = (.) line pos text = (.) line text
- Client.State.EditBox.Content: line :: HasLine c_aiUU => Lens' c_aiUU Line
+ Client.State.EditBox.Content: line :: HasLine c_aKQ0 => Lens' c_aKQ0 Line
- Client.State.EditBox.Content: pos :: HasLine c_aiUU => Lens' c_aiUU Int
+ Client.State.EditBox.Content: pos :: HasLine c_aKQ0 => Lens' c_aKQ0 Int
- Client.State.EditBox.Content: text :: HasLine c_aiUU => Lens' c_aiUU String
+ Client.State.EditBox.Content: text :: HasLine c_aKQ0 => Lens' c_aKQ0 String
- Client.State.Window: WindowLine :: !MessageBody -> {-# UNPACK #-} !Text -> !Image -> !Image -> WindowLine
+ Client.State.Window: WindowLine :: !MessageBody -> {-# UNPACK #-} !Text -> !Image -> !Image -> !WindowLineImportance -> {-# UNPACK #-} !UTCTime -> WindowLine
- Client.State.Window: addToWindow :: WindowLineImportance -> WindowLine -> Window -> Window
+ Client.State.Window: addToWindow :: WindowLine -> Window -> Window

Files

ChangeLog.md view
@@ -1,5 +1,16 @@ # Revision history for glirc2 +## 2.15++* Add `/mentions`+* Add macro argument declarations+* Add indication when a command is still a prefix or not of a valid command+* Support quoted strings arguments to /exec+* Add F4 to toggle visibility of metadata lines+* tls-insecure setting was incorrectly behaving like normal insecure+* Add `C-t` to swap characters+* Add `ESC` to return to messages window+ ## 2.14  * Add `/help`
README.md view
@@ -195,7 +195,7 @@  * `nick-colors` - List of attr - Use for nick highlights * `self` - attr - attr of our own nickname(s) outside of mentions-* `self-highlight` - attr - attr of our own nickname(s) in mentions (defaults to `self`)+* `self-highlight` - attr - attr of our own nickname(s) in mentions * `time` - attr - attr for timestamp * `meta` - attr - attr for metadata * `sigil` - attr - attr for sigils@@ -207,7 +207,8 @@ * `activity` - attr - attr for activity notification * `mention` - attr - attr for mention notification * `command` - attr - attr for recognized command-* `command-ready` - attr - attr for command with successful parse+* `command-prefix` - attr - attr for prefix of known command+* `command-ready` - attr - attr for recognized command with arguments filled * `command-placeholder` - attr - attr for command argument placeholder  Text Attributes@@ -247,6 +248,8 @@ * `/reconnect` - Reconnect to the current server * `/reload [path]` - Load a new configuration file (optional path) * `/windows` - List all open windows+* `/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 * `/exec [-n network] [-c channel] <command> <arguments...>` - Execute a command, If no network or channel are provided send output to client window, if network and channel are provided send output as messages, if network is provided send output as raw IRC messages. * `/url [n]` - Execute url-opener on the nth URL in the current window (defaults to first)@@ -322,35 +325,57 @@ Keyboard Shortcuts ================== -* `^N` next channel-* `^P` previous channel+Note that these keybindings are using *Emacs* syntax. `C-a` means "hold+control and press A". `M-a` means "hold meta key and press A". On most+modern keyboards the *Meta* key is labeled *Alt* or *Option*.++Window navigation++* `C-n` next channel+* `C-p` previous channel * `M-#` jump to window - `1234567890qwertyuiop!@#$%^&*()QWERTYUIOP`-* `M-A` jump to activity-* `M-S` jump to previous window-* `^A` beginning of line-* `^E` end of line-* `^K` delete to end-* `^U` delete to beginning-* `^D` delete at cursor-* `^W` delete word backwards-* `^Y` paste from yank buffer-* `M-F` forward word-* `M-B` backward word-* `M-BACKSPACE` delete word backwards-* `M-D` delete word forwards-* `TAB` nickname completion+* `M-a` jump to activity+* `M-s` jump to previous window+* `ESC` return to messages view (from userlist, masklist, help, etc)++Editing++* `C-a` beginning of line+* `C-e` end of line+* `C-k` delete to end+* `C-u` delete to beginning+* `C-d` delete at cursor+* `C-w` delete word backwards+* `C-y` paste from yank buffer+* `C-t` swap characters at cursor+* `M-f` forward word+* `M-b` backward word+* `M-Backspace` delete word backwards+* `M-d` delete word forwards+* `M-Enter` insert newline++* `Tab` nickname completion++Client settings+ * `F2` toggle detailed view * `F3` toggle detailed activity bar+* `F4` toggle metadata visibility++Scrolling+ * `Page Up` scroll up * `Page Down` scroll down-* `^B` bold-* `^C` color-* `^V` reverse video-* `^_` underline-* `^]` italic-* `^O` reset formatting-* `M-Enter` insert newline +Formatting++* `C-b` bold+* `C-c` color+* `C-v` reverse video+* `C-_` underline+* `C-]` italic+* `C-o` reset formatting+ Macros ====== @@ -361,6 +386,7 @@ -------------  * `name` - text - name of macro+* `arguments` - text - space separated list of argument names (suffix name with `?` when optional) * `commands` - list of text - commands to send after expansion  Macro Expansions
exec/Main.hs view
@@ -17,21 +17,21 @@ import System.Exit import System.IO -import Client.CommandArguments import Client.Configuration import Client.EventLoop+import Client.Options import Client.State import Client.State.Focus  -- | Main action for IRC client main :: IO () main =-  do args <- getCommandArguments-     cfg  <- loadConfiguration' (view cmdArgConfigFile args)+  do opts <- getOptions+     cfg  <- loadConfiguration' (view optConfigFile opts)      runInUnboundThread $        withClientState cfg $        clientStartExtensions >=>-       addInitialNetworks (view cmdArgInitialNetworks args) >=>+       addInitialNetworks (view optInitialNetworks opts) >=>        eventLoop  -- | Load configuration and handle errors along the way.
glirc.cabal view
@@ -1,5 +1,5 @@ name:                glirc-version:             2.14+version:             2.15 synopsis:            Console IRC client description:         Console IRC client license:             ISC@@ -49,14 +49,14 @@   default-language:    Haskell2010   build-tools:         hsc2hs -  exposed-modules:     Client.CommandArguments-                       Client.CApi+  exposed-modules:     Client.CApi                        Client.CApi.Exports                        Client.CApi.Types                        Client.Commands                        Client.Commands.Arguments                        Client.Commands.Exec                        Client.Commands.Interpolation+                       Client.Commands.Recognizer                        Client.Commands.WordCompletion                        Client.Configuration                        Client.Configuration.Colors@@ -68,20 +68,15 @@                        Client.Hooks                        Client.Image                        Client.Image.Arguments-                       Client.Image.ChannelInfo-                       Client.Image.Help-                       Client.Image.MaskList                        Client.Image.Message                        Client.Image.MircFormatting                        Client.Image.Palette-                       Client.Image.PaletteView                        Client.Image.StatusLine                        Client.Image.Textbox-                       Client.Image.UserList-                       Client.Image.Windows                        Client.Message                        Client.Network.Async                        Client.Network.Connect+                       Client.Options                        Client.State                        Client.State.Channel                        Client.State.EditBox@@ -89,6 +84,15 @@                        Client.State.Focus                        Client.State.Network                        Client.State.Window+                       Client.View+                       Client.View.ChannelInfo+                       Client.View.Help+                       Client.View.MaskList+                       Client.View.Mentions+                       Client.View.Messages+                       Client.View.Palette+                       Client.View.UserList+                       Client.View.Windows                        Config.FromConfig    other-modules:       LensUtils
− src/Client/CommandArguments.hs
@@ -1,111 +0,0 @@-{-# Language TemplateHaskell #-}-{-|-Module      : Client.CommandArguments-Description : Processing of command-line arguments-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--This module process command-line arguments provided when-launching the client.---}-module Client.CommandArguments-  (-  -- * Command-line argument type-    CommandArguments(..)--  -- * Lenses-  , cmdArgConfigFile-  , cmdArgInitialNetworks--  -- * Argument loader-  , getCommandArguments-  ) where--import           Control.Lens-import           Data.Foldable-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Version-import           Development.GitRev (gitHash, gitDirty)-import           System.Console.GetOpt-import           System.Environment-import           System.Exit-import           System.IO-import           Paths_glirc (version)---- | Command-line arguments-data CommandArguments = CommandArguments-  { _cmdArgConfigFile      :: Maybe FilePath -- ^ configuration file path-  , _cmdArgInitialNetworks :: [Text]         -- ^ initial networks-  , _cmdArgShowHelp        :: Bool           -- ^ show help message-  , _cmdArgShowVersion     :: Bool           -- ^ show version message-  }--makeLenses ''CommandArguments---- | Default values for arguments-defaultCommandArguments :: CommandArguments-defaultCommandArguments = CommandArguments-  { _cmdArgConfigFile      = Nothing-  , _cmdArgInitialNetworks = []-  , _cmdArgShowHelp        = False-  , _cmdArgShowVersion     = False-  }---- | Option descriptions-options :: [OptDescr (CommandArguments -> CommandArguments)]-options =-  [ Option "c" ["config"]  (ReqArg (set cmdArgConfigFile . Just) "PATH")-    "Configuration file path"-  , Option "h" ["help"]    (NoArg (set cmdArgShowHelp True))-    "Show help"-  , Option "v" ["version"] (NoArg (set cmdArgShowVersion True))-    "Show version"-  ]---- | Load command line arguments. This action will terminate early--- in the case of the version flag, help flag, or an error.-getCommandArguments :: IO CommandArguments-getCommandArguments =-  do args <- getArgs-     case getOpt Permute options args of-       (flags, networks, [])-         | view cmdArgShowHelp    cmdArgs -> putStr helpTxt    >> exitSuccess-         | view cmdArgShowVersion cmdArgs -> putStr versionTxt >> exitSuccess-         | otherwise                      -> return cmdArgs-         where-           cmdArgs = assembleCommandArguments flags networks-       (_, _, errors) ->-         do traverse_ (hPutStr stderr) errors-            hPutStrLn stderr "Run 'glirc2 --help' to see a list of available command line options."-            exitFailure--assembleCommandArguments :: [CommandArguments -> CommandArguments] -> [String] -> CommandArguments-assembleCommandArguments flags networks =-  let flagArgs = foldl' (\acc f -> f acc) defaultCommandArguments flags-  in flagArgs { _cmdArgInitialNetworks = Text.pack <$> networks }--helpTxt :: String-helpTxt = usageInfo "glirc2 [FLAGS] INITIAL_NETWORKS..." options--versionTxt :: String-versionTxt = unlines-  [ "glirc-" ++ showVersion version ++ gitHashTxt ++ gitDirtyTxt-  , "Copyright 2016 Eric Mertens"-  ]---- | Returns @"-SOMEHASH"@ when in a git repository, @""@ otherwise.-gitHashTxt :: String-gitHashTxt-  | hashTxt == "UNKNOWN" = ""-  | otherwise            = '-':hashTxt-  where-    hashTxt = $gitHash---- | Returns @"-dirty"@ when in a dirty git repository, @""@ otherwise.-gitDirtyTxt :: String-gitDirtyTxt-  | $gitDirty = "-dirty"-  | otherwise = ""
src/Client/Commands.hs view
@@ -28,6 +28,7 @@ import           Client.Commands.Arguments import           Client.Commands.Exec import           Client.Commands.Interpolation+import           Client.Commands.Recognizer import           Client.Commands.WordCompletion import           Client.Configuration import           Client.Configuration.ServerSettings@@ -42,9 +43,7 @@ import           Control.Exception (displayException, try) import           Control.Lens import           Control.Monad-import           Data.Char import           Data.Foldable-import           Data.HashMap.Strict (HashMap) import           Data.HashSet (HashSet) import           Data.List.NonEmpty (NonEmpty((:|))) import           Data.List.Split@@ -130,13 +129,14 @@ executeUserCommand :: Maybe Text -> String -> ClientState -> IO CommandResult executeUserCommand discoTime command st = do   let key = Text.takeWhile (/=' ') tcmd+      rest = dropWhile (==' ') . dropWhile (/=' ') $ command -  case preview (clientConfig . configMacros . ix key) st of-    Nothing     -> executeCommand Nothing command st-    Just cmdExs ->-      case traverse resolveMacro cmdExs of+  case views (clientConfig . configMacros) (recognize key) st of+    Exact (Macro (MacroSpec spec) cmdExs) ->+      case parseArguments spec rest *> traverse resolveMacro cmdExs of         Nothing   -> commandFailureMsg "Macro expansions failed" st         Just cmds -> process cmds st+    _ ->  executeCommand Nothing command st   where     resolveMacro = resolveMacroExpansions (commandExpansion discoTime st) expandInt @@ -220,14 +220,9 @@               Nothing -> commandFailure st               Just arg -> exec st arg   in-  case HashMap.lookup cmdTxt commands of--    Nothing ->-      case tabCompleteReversed of-        Nothing         -> commandFailureMsg "Unknown command" st-        Just isReversed -> nickTabCompletion isReversed st+  case recognize cmdTxt commands of -    Just (Command argSpec _docs impl) ->+    Exact (Command argSpec _docs impl) ->       case impl of         ClientCommand exec tab ->           finish argSpec exec tab@@ -251,6 +246,11 @@               finish argSpec (exec channelId cs) (\x -> tab x channelId cs)           | otherwise -> commandFailureMsg "This command requires an active chat window" st +    _ -> case tabCompleteReversed of+           Just isReversed -> nickTabCompletion isReversed st+           Nothing         -> commandFailureMsg "Unknown command" st++ -- | Expands each alias to have its own copy of the command callbacks expandAliases :: [(NonEmpty a,b)] -> [(a,b)] expandAliases xs = [ (a,b) | (as,b) <- xs, a <- toList as ]@@ -258,8 +258,8 @@  -- | Map of built-in client commands to their implementations, tab completion -- logic, and argument structures.-commands :: HashMap Text Command-commands = HashMap.fromList (expandAliases commandsList)+commands :: Recognizer Command+commands = fromCommands (expandAliases commandsList)  commandsList :: [(NonEmpty Text,Command)] commandsList =@@ -330,6 +330,11 @@       "Show a list of all client message windows.\n"     $ ClientCommand cmdWindows noClientTab     )+  , ( pure "mentions"+    , Command NoArg+      "Show a list of all message that were highlighted as important.\n"+    $ ClientCommand cmdMentions noClientTab+    )   , ( pure "palette"     , Command NoArg       "Show the current palette settings and a color chart to help pick new colors.\n"@@ -347,6 +352,10 @@       \When \^Bnetwork\^B is specified output is sent as raw IRC traffic to the network.\n\       \When \^Bchannel\^B is specified output is sent as chat to the given channel on the current network.\n\       \When \^Bnetwork\^B and \^Bchannel\^B are specified output is sent as chat to the given channel on the given network.\n\+      \\n\+      \\^Barguments\^B is divided on spaces into words before being processed\+      \ by getopt. Use Haskell string literal syntax to create arguments with\+      \ escaped characters and spaces inside.\n\       \\n"     $ ClientCommand cmdExec simpleClientTab     )@@ -802,6 +811,10 @@ cmdWindows :: ClientCommand () cmdWindows st _ = commandSuccess (changeSubfocus FocusWindows st) +-- | Implementation of @/mentions@ command. Set subfocus to Windows.+cmdMentions :: ClientCommand ()+cmdMentions st _ = commandSuccess (changeSubfocus FocusMentions st)+ -- | Implementation of @/palette@ command. Set subfocus to Windows. cmdPalette :: ClientCommand () cmdPalette st _ = commandSuccess (changeSubfocus FocusPalette st)@@ -998,7 +1011,7 @@   ChannelCommand String tabTopic _ channelId cs st rest -  | all isSpace rest+  | all (==' ') rest   , Just topic <- preview (csChannels . ix channelId . chanTopic) cs =      do let textBox = set Edit.line (Edit.endLine $ "/topic " ++ Text.unpack topic)         commandSuccess (over clientTextBox textBox st)@@ -1258,10 +1271,10 @@   where     n = length leadingPart     (cursorPos, line) = clientLine st-    leadingPart = takeWhile (not . isSpace) line+    leadingPart = takeWhile (/=' ') line     possibilities = Text.cons '/' <$> commandNames-    commandNames = HashMap.keys commands-                ++ HashMap.keys (view (clientConfig . configMacros) st)+    commandNames = keys commands+                ++ keys (view (clientConfig . configMacros) st)  -- | Complete the nickname at the current cursor position using the -- userlist for the currently focused channel (if any)
src/Client/Commands/Arguments.hs view
@@ -35,6 +35,18 @@   -- | No arguments   NoArg        :: ArgumentSpec () +instance Show (ArgumentSpec s) where+  showsPrec p spec =+    case spec of+      ReqTokenArg s rest -> showParen (p >= 11)+                          $ showString "ReqTokenArg "+                          . showsPrec 11 s . showChar ' ' . showsPrec 11 rest+      OptTokenArg s rest -> showParen (p >= 11)+                          $ showString "OptTokenArg "+                          . showsPrec 11 s . showChar ' ' . showsPrec 11 rest+      RemainingArg s     -> showParen (p >= 11)+                          $ showString "RemainingArg " . showsPrec 11 s+      NoArg              -> showString "NoArg"  -- | Parse the given input string using an argument specification. -- The arguments should start with a space but might have more.
src/Client/Commands/Exec.hs view
@@ -26,6 +26,7 @@  import           Control.Exception import           Control.Lens+import           Data.List import           System.Console.GetOpt import           System.Process @@ -78,13 +79,11 @@  -- | Parse the arguments to @/exec@ looking for various flags -- and the command and its arguments.------ TODO: support quoted strings parseExecCmd ::   String                  {- ^ exec arguments          -} ->   Either [String] ExecCmd {- ^ error or parsed command -} parseExecCmd str =-  case getOpt RequireOrder options (words str) of+  case getOpt RequireOrder options (powerWords str) of     (_, [] , errs) -> Left ("No command specified":errs)     (fs, cmd:args, []) -> Right                         $ foldl (\x f -> f x) ?? fs@@ -105,3 +104,16 @@      return $ case res of        Left er -> Left [show (er :: IOError)]        Right x -> Right (lines x)++-- | Power words is similar to 'words' except that when it encounters+-- a word formatted as a Haskell 'String' literal it parses it as+-- such. Only space is used as a delimiter.+powerWords :: String -> [String]+powerWords = unfoldr (splitWord . dropWhile isSp)+  where+    isSp x = x == ' '++    splitWord xs+      | null xs         = Nothing+      | [x] <- reads xs = Just x+      | otherwise       = Just (break isSp xs)
src/Client/Commands/Interpolation.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, GADTs #-}  {-| Module      : Client.Commands.Interpolation@@ -15,6 +15,10 @@   ( ExpansionChunk(..)   , parseExpansion   , resolveMacroExpansions+  , Macro(..)+  , MacroSpec(..)+  , parseMacroSpecs+  , noMacroArguments   ) where  import           Control.Applicative@@ -23,6 +27,8 @@ import qualified Data.Text as Text import           Data.Text (Text) +import           Client.Commands.Arguments+ -- | Parsed chunk of an expandable command data ExpansionChunk   -- | regular text@@ -34,6 +40,41 @@   -- | bracketed variable with default @${x|lit}@   | DefaultChunk ExpansionChunk Text   deriving Show++data Macro+  = Macro+  { macroSpec :: MacroSpec+  , macroCommands :: [[ExpansionChunk]]+  } deriving Show++data MacroSpec where+  MacroSpec :: ArgumentSpec s -> MacroSpec++instance Show MacroSpec where+  showsPrec p (MacroSpec as)+    = showParen (p >= 11)+    $ showString "MacroSpec " . showsPrec 11 as++-- | Specification used when unspecified, no arguments.+noMacroArguments :: MacroSpec+noMacroArguments = MacroSpec NoArg++parseMacroSpecs :: Text -> Maybe MacroSpec+parseMacroSpecs txt =+  case parseOnly (macroSpecs <* endOfInput) txt of+    Left{}     -> Nothing+    Right spec -> Just spec++macroSpecs :: Parser MacroSpec+macroSpecs =+  cons <$> P.takeWhile1 isAlpha+       <*> optional (char '?')+       <*  P.skipSpace+       <*> macroSpecs+    <|> pure (MacroSpec NoArg)+ where+ cons desc (Just _) (MacroSpec rest) = MacroSpec (OptTokenArg (Text.unpack desc) rest)+ cons desc Nothing  (MacroSpec rest) = MacroSpec (ReqTokenArg (Text.unpack desc) rest)  -- | Parse a 'Text' searching for the expansions as specified in -- 'ExpansionChunk'. @$$@ is used to escape a single @$@.
+ src/Client/Commands/Recognizer.hs view
@@ -0,0 +1,127 @@+{-# Language OverloadedStrings, DeriveFunctor #-}++{-|+Module      : Client.Commands.Recognizer+Description : Trie for recognizing commands+Copyright   : (c) Dan Doel, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module implements a trie for recognizing valid commands. This+allows entered strings to be classified as either a valid command+(with an associated value), the prefix of a valid command, or invalid.+-}++module Client.Commands.Recognizer+  ( Recognizer+  , recognize+  , Recognition(..)+  , fromCommands+  , addCommand+  , keys+  ) where++import Control.Monad+import Control.Applicative hiding (empty)++import           Data.HashMap.Strict (lookup,insertWith,HashMap,empty,unionWith,fromList,toList)+import           Data.Monoid+import           Data.Text (Text)+import qualified Data.Text as Text+import           Data.Maybe++import Prelude hiding (all,lookup)++-- | A map from 'Text' values to 'a' values that is capable of yielding more+-- detailed information when looking up keys that are not actually in the map.+data Recognizer a+  = Branch !Text !(Maybe a) !(HashMap Char (Recognizer a))+  deriving (Show, Functor)++instance Monoid (Recognizer a) where+  mempty = Branch "" Nothing empty+  mappend = both++-- | Possible results of recognizing text.+data Recognition a+  = Exact a       -- ^ text matched exactly, yielding the given value+  | Prefix [Text] -- ^ text would be recognized if joined to the given suffixes+  | Invalid       -- ^ text could not possibly be recognized+  deriving (Show, Functor)++-- | Match common prefixes of two strings in a more convenient form than+-- available from 'Data.Text'+splitCommon :: Text -> Text -> (Text, Text, Text)+splitCommon l r = fromMaybe ("", l, r) $ Text.commonPrefixes l r++-- | Attempt to recognize a string, yielding a 'Recognition' result.+recognize :: Text -> Recognizer a -> Recognition a+recognize tx (Branch pf contained children)+  = case splitCommon pf tx of+      (_, pfsfx, txsfx) -> case Text.uncons txsfx of+        Nothing+          | Text.null pfsfx+          , Just a <- contained -> Exact a+          | otherwise -> Prefix $ keys (Branch pfsfx contained children)+        Just (c, txrest)+          | Text.null pfsfx+          , Just rec <- lookup c children+          -> recognize txrest rec+        _ -> Invalid++-- | Create a singleton 'Recognizer' associating the given 'Text' and value.+single :: Text -> a -> Recognizer a+single tx v = Branch tx (Just $! v) empty++-- | Union two 'Recognizers'. The stored values in the result are biased to the+-- left if there is key overlap.+both :: Recognizer a -> Recognizer a -> Recognizer a+both l@(Branch pfl conl chil) r@(Branch pfr conr chir)+  | Text.null pfl && null conl && null chil = r+  | Text.null pfr && null conr && null chir = l+  | otherwise+  = case splitCommon pfl pfr of+      (common, lsfx, rsfx) -> Branch common contained children+        where+        contained = (guard (Text.null lsfx) *> conl)+                <|> (guard (Text.null rsfx) *> conr)+        children = case (Text.uncons lsfx, Text.uncons rsfx) of+          (Nothing, Nothing)+            -> unionWith both chil chir+          (Just (l',lest), Nothing)+            -> insertWith (flip both) l' (Branch lest conl chil) chir+          (Nothing, Just (r',rest))+            -> insertWith both r' (Branch rest conr chir) chil+          (Just (l',lest), Just (r',rest))+            -> fromList [ (l', Branch lest conl chil)+                        , (r', Branch rest conr chir)+                        ]++-- | Union an arbitrary number of 'Recognizers' as with 'both'.+all :: [Recognizer a] -> Recognizer a+all [] = mempty+all [r] = r+all rs = all $ pair rs+ where+ pair (l:r:rest) = both l r : pair rest+ pair rest       = rest++-- | Create a 'Recognizer' from an association list. If a key appears twice, the+-- earliest associated value will be used.+fromCommands :: [(Text, a)] -> Recognizer a+fromCommands = all . map (uncurry single)++-- | Add a key-value pair to a 'Recognizer'. This will override the value+-- already present if one exists.+addCommand :: Text -> a -> Recognizer a -> Recognizer a+addCommand tx v = both $ single tx v++-- | Compute all strings that will be recognized by a 'Recognizer'.+keys :: Recognizer a -> [Text]+keys (Branch pf contained children)+  = maybeToList (pf <$ contained)+  ++ (mappend pf <$> childKeys children)++-- | Auxiliary function for 'keys'.+childKeys :: HashMap Char (Recognizer a) -> [Text]+childKeys children = toList children >>= \(c,rec) -> Text.cons c <$> keys rec
src/Client/Commands/WordCompletion.hs view
@@ -101,20 +101,20 @@   toString = Text.unpack  -tabSearch :: Prefix a => Bool -> a -> a -> [a] -> Maybe a+tabSearch ::+  Prefix a =>+  Bool {- ^ reversed        -} ->+  a    {- ^ search prefix   -} ->+  a    {- ^ previous result -} ->+  [a]  {- ^ posibilities    -} ->+  Maybe a tabSearch isReversed pat cur vals-  | Just next <- advanceFun cur valSet-  , isPrefix pat next-  = Just next--  | isReversed = find (isPrefix pat) (Set.toDescList valSet)--  | otherwise  = do x <- Set.lookupGE pat valSet-                    guard (isPrefix pat x)-                    Just x+  | Set.null valSet                    = Nothing+  | Just next <- advanceFun cur valSet = Just next+  | isReversed                         = Just $! Set.findMax valSet+  | otherwise                          = Just $! Set.findMin valSet   where-    valSet = Set.fromList vals+    valSet = Set.fromList (filter (isPrefix pat) vals)      advanceFun | isReversed = Set.lookupLT                | otherwise  = Set.lookupGT-
src/Client/Configuration.hs view
@@ -44,6 +44,7 @@ import           Client.Configuration.Colors import           Client.Configuration.ServerSettings import           Client.Commands.Interpolation+import           Client.Commands.Recognizer import           Control.Exception import           Control.Monad import           Config@@ -78,7 +79,7 @@   , _configNickPadding      :: Maybe Integer -- ^ Padding of nicks   , _configConfigPath       :: Maybe FilePath         -- ^ manually specified configuration path, used for reloading-  , _configMacros           :: HashMap Text [[ExpansionChunk]] -- ^ command macros+  , _configMacros           :: Recognizer Macro -- ^ command macros   , _configExtensions       :: [FilePath] -- ^ paths to shared library   , _configUrlOpener        :: Maybe FilePath -- ^ paths to url opening executable   , _configIgnores          :: HashSet Identifier -- ^ initial ignore list@@ -197,7 +198,7 @@      _configWindowNames <- fromMaybe defaultWindowNames                     <$> sectionOpt "window-names" -     _configMacros <- fromMaybe HashMap.empty+     _configMacros <- fromMaybe mempty                     <$> sectionOptWith parseMacroMap "macros"       _configExtensions <- fromMaybe []@@ -231,31 +232,10 @@     "nick-colors" -> do xs <- Vector.fromList <$> parseList parseAttr v                         when (null xs) (failure "Empty palette")                         return $! set palNicks xs p--    "self"              -> setAttr palSelf-    "self-highlight"    -> setAttrMb palSelfHighlight-    "time"              -> setAttr palTime-    "meta"              -> setAttr palMeta-    "sigil"             -> setAttr palSigil-    "label"             -> setAttr palLabel-    "latency"           -> setAttr palLatency-    "error"             -> setAttr palError-    "textbox"           -> setAttr palTextBox-    "window-name"       -> setAttr palWindowName-    "activity"          -> setAttr palActivity-    "mention"           -> setAttr palMention-    "command"           -> setAttr palCommand-    "command-ready"     -> setAttr palCommandReady-    "command-placeholder" -> setAttr palCommandPlaceholder-    _                   -> failure "Unknown palette entry"-  where-    setAttr l =-      do !attr <- parseAttr v-         return $! set l attr p--    setAttrMb l =-      do !attr <- parseAttr v-         return $! set l (Just attr) p+    _ | Just (Lens l) <- lookup k paletteMap ->+          do !attr <- parseAttr v+             return $! set l attr p+    _ -> failure "Unknown palette entry"  parseServers :: ServerSettings -> Value -> ConfigParser (HashMap Text ServerSettings) parseServers def v =@@ -317,7 +297,7 @@  parseUseTls :: Value -> ConfigParser UseTls parseUseTls (Atom "yes")          = return UseTls-parseUseTls (Atom "yes-insecure") = return UseInsecure+parseUseTls (Atom "yes-insecure") = return UseInsecureTls parseUseTls (Atom "no")           = return UseInsecure parseUseTls _                     = failure "expected yes, yes-insecure, or no" @@ -338,14 +318,23 @@   | otherwise = do home <- getHomeDirectory                    return (home </> path) -parseMacroMap :: Value -> ConfigParser (HashMap Text [[ExpansionChunk]])-parseMacroMap v = HashMap.fromList <$> parseList parseMacro v+parseMacroMap :: Value -> ConfigParser (Recognizer Macro)+parseMacroMap v = fromCommands <$> parseList parseMacro v -parseMacro :: Value -> ConfigParser (Text, [[ExpansionChunk]])+parseMacro :: Value -> ConfigParser (Text, Macro) parseMacro = parseSections $   do name     <- sectionReq "name"+     spec     <- fromMaybe noMacroArguments+             <$> sectionOptWith parseMacroArguments "arguments"      commands <- sectionReqWith (parseList parseMacroCommand) "commands"-     return (name, commands)+     return (name, Macro spec commands)++parseMacroArguments :: Value -> ConfigParser MacroSpec+parseMacroArguments v =+  do txt <- parseConfig v+     case parseMacroSpecs txt of+       Nothing -> failure "bad macro argument specs"+       Just ex -> return ex  parseMacroCommand :: Value -> ConfigParser [ExpansionChunk] parseMacroCommand v =
src/Client/EventLoop.hs view
@@ -18,7 +18,6 @@ import           Client.CApi import           Client.Commands import           Client.Commands.Interpolation-import           Client.Configuration import           Client.Configuration.ServerSettings import           Client.EventLoop.Errors (exceptionToLines) import           Client.Hook@@ -28,6 +27,7 @@ import           Client.Network.Async import           Client.State import qualified Client.State.EditBox     as Edit+import           Client.State.Focus import           Client.State.Network import           Control.Concurrent.STM import           Control.Exception@@ -50,6 +50,7 @@ import           Irc.Codes import           Irc.Message import           Irc.RawIrcMsg+import           LensUtils import           Network.Connection  reconnectAttempts :: Int@@ -115,6 +116,7 @@          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  -- | Sound the terminal bell assuming that the @BEL@ control code@@ -122,10 +124,29 @@ beep :: Vty -> IO () beep = ringTerminalBell . outputIface +-- | Respond to a network connection successfully connecting.+doNetworkOpen ::+  NetworkId   {- ^ network id   -} ->+  ZonedTime   {- ^ event time   -} ->+  ClientState {- ^ client state -} ->+  IO ()+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+ -- | Respond to a network connection closing normally. doNetworkClose ::-  NetworkId {- ^ finished network -} ->-  ZonedTime {- ^ current time     -} ->+  NetworkId {- ^ network id -} ->+  ZonedTime {- ^ event time -} ->   ClientState -> IO () doNetworkClose networkId time st =   let (cs,st') = removeNetwork networkId st@@ -355,6 +376,7 @@         KChar 'u' -> changeEditor Edit.killHome         KChar 'k' -> changeEditor Edit.killEnd         KChar 'y' -> changeEditor Edit.yank+        KChar 't' -> changeContent Edit.toggle         KChar 'w' -> changeEditor (Edit.killWordBackward True)         KChar 'b' -> changeEditor (Edit.insert '\^B')         KChar 'c' -> changeEditor (Edit.insert '\^C')@@ -376,13 +398,14 @@         KChar 'f' -> changeContent Edit.rightWord         KChar 'a' -> eventLoop (jumpToActivity st)         KChar 's' -> eventLoop (returnFocus st)-        KChar c   | let names = view (clientConfig . configWindowNames) st-                  , Just i <- Text.findIndex (==c) names ->+        KChar c   | let names = clientWindowNames st+                  , Just i <- elemIndex c names ->                             eventLoop (jumpFocus i st)         _ -> eventLoop st      [] -> -- no modifier       case key of+        KEsc       -> eventLoop (changeSubfocus FocusMessages st)         KBS        -> changeContent Edit.backspace         KDel       -> changeContent Edit.delete         KLeft      -> changeContent Edit.left@@ -399,8 +422,12 @@         KChar '\t' -> doCommandResult False =<< tabCompletion False st          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)+         _          -> eventLoop st      _ -> eventLoop st -- unsupported modifier
src/Client/Image.hs view
@@ -11,26 +11,13 @@ -} module Client.Image (clientPicture) where -import           Client.Configuration-import           Client.Image.ChannelInfo-import           Client.Image.Help-import           Client.Image.MaskList-import           Client.Image.Message-import           Client.Image.Palette-import           Client.Image.PaletteView import           Client.Image.StatusLine import           Client.Image.Textbox-import           Client.Image.UserList-import           Client.Image.Windows-import           Client.Message import           Client.State-import           Client.State.Focus-import           Client.State.Network-import           Client.State.Window+import           Client.View import           Control.Lens import           Graphics.Vty (Background(..), Picture(..), Cursor(..)) import           Graphics.Vty.Image-import           Irc.Identifier (Identifier)  -- | Generate a 'Picture' for the current client state. The resulting -- client state is updated for render specific information like scrolling.@@ -53,46 +40,10 @@     (pos, tbImg) = textboxImage st'     img = mp <-> statusLineImage st' <-> tbImg -messagePaneImages :: ClientState -> [Image]-messagePaneImages !st =-  case (view clientFocus st, view clientSubfocus st) of-    (ChannelFocus network channel, FocusInfo) ->-      channelInfoImages network channel st-    (ChannelFocus network channel, FocusUsers)-      | view clientDetailView st -> userInfoImages network channel st-      | otherwise                -> userListImages network channel st-    (ChannelFocus network channel, FocusMasks mode) ->-      maskListImages mode network channel st-    (_, FocusWindows) -> windowsImages st-    (_, FocusPalette) -> paletteViewLines pal-    (_, FocusHelp mb) -> helpImageLines mb pal--    _ -> chatMessageImages st-  where-    pal = view (clientConfig . configPalette) st--chatMessageImages :: ClientState -> [Image]-chatMessageImages st = windowLineProcessor focusedMessages-  where-    matcher = clientMatcher st--    focusedMessages-        = filter (views wlText matcher)-        $ view (clientWindows . ix (view clientFocus st) . winMessages) st--    windowLineProcessor-      | view clientDetailView st = map (view wlFullImage)-      | otherwise                = windowLinesToImages st . filter (not . isNoisy)--    isNoisy msg =-      case view wlBody msg of-        IrcBody irc -> squelchIrcMsg irc-        _           -> False- messagePane :: ClientState -> (Image, ClientState) messagePane st = (img, st')   where-    images = messagePaneImages st+    images = viewLines st     vimg   = assemble emptyImage images     vimg1  = cropBottom h vimg     img    = pad 0 (h - imageHeight vimg1) 0 0 vimg1@@ -114,80 +65,6 @@      h      = view clientHeight st - reservedLines     w      = view clientWidth st--windowLinesToImages :: ClientState -> [WindowLine] -> [Image]-windowLinesToImages st wwls =-  case gatherMetadataLines st wwls of-    ([], [])   -> []-    ([], w:ws) -> view wlImage w : windowLinesToImages st ws-    ((img,who,mbnext):mds, wls) ->-         startMetadata img mbnext who mds palette-       : windowLinesToImages st wls-  where-    palette = view (clientConfig . configPalette) st----------------------------------------------------------------------------type MetadataState =-  Identifier                            {- ^ current nick -} ->-  [(Image,Identifier,Maybe Identifier)] {- ^ metadata     -} ->-  Palette                               {- ^ palette      -} ->-  Image--startMetadata ::-  Image            {- ^ metadata image           -} ->-  Maybe Identifier {- ^ possible nick transition -} ->-  MetadataState-startMetadata img mbnext who mds palette =-        quietIdentifier palette who-    <|> img-    <|> transitionMetadata mbnext who mds palette--transitionMetadata ::-  Maybe Identifier {- ^ possible nick transition -} ->-  MetadataState-transitionMetadata mbwho who mds palette =-  case mbwho of-    Nothing   -> continueMetadata who  mds palette-    Just who' -> quietIdentifier palette who'-             <|> continueMetadata who' mds palette--continueMetadata :: MetadataState-continueMetadata _ [] _ = emptyImage-continueMetadata who1 ((img, who2, mbwho3):mds) palette-  | who1 == who2 = img-               <|> transitionMetadata mbwho3 who2 mds palette-  | otherwise    = char defAttr ' '-               <|> startMetadata img mbwho3 who2 mds palette----------------------------------------------------------------------------gatherMetadataLines ::-  ClientState ->-  [WindowLine] ->-  ( [(Image, Identifier, Maybe Identifier)] , [ WindowLine ] )-  -- ^ metadata entries are reversed-gatherMetadataLines st = go []-  where-    go acc (w:ws)-      | Just (img,who,mbnext) <- metadataWindowLine st w =-          go ((img,who,mbnext) : acc) ws--    go acc ws = (acc,ws)----- | Classify window lines for metadata coalesence-metadataWindowLine ::-  ClientState ->-  WindowLine ->-  Maybe (Image, Identifier, Maybe Identifier)-        {- ^ Image, incoming identifier, outgoing identifier if changed -}-metadataWindowLine st wl =-  case view wlBody wl of-    IrcBody irc-      | Just who <- ircIgnorable irc st -> Just (ignoreImage, who, Nothing)-      | otherwise                       -> metadataImg irc-    _                                   -> Nothing  lineWrap :: Int -> Image -> Image lineWrap w img
− src/Client/Image/ChannelInfo.hs
@@ -1,81 +0,0 @@-{-# Language OverloadedStrings #-}-{-# Language BangPatterns #-}--{-|-Module      : Client.Image.ChannelInfo-Description : Channel information renderer-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--This module implements a renderer for the window that shows-channel metadata.---}-module Client.Image.ChannelInfo-  ( channelInfoImages-  ) where--import           Client.Configuration-import           Client.Image.Message-import           Client.Image.MircFormatting-import           Client.Image.Palette-import           Client.State-import           Client.State.Channel-import           Client.State.Network-import           Control.Lens-import           Data.HashSet (HashSet)-import           Data.Text (Text)-import           Data.Time-import           Graphics.Vty.Image-import           Irc.Identifier---- | Render the lines used in a channel mask list-channelInfoImages ::-  Text        {- ^ network -} ->-  Identifier  {- ^ channel -} ->-  ClientState -> [Image]-channelInfoImages network channelId st--  | Just cs      <- preview (clientConnection network) st-  , Just channel <- preview (csChannels . ix channelId) cs-  = channelInfoImages' pal (clientHighlights cs st) channel--  | otherwise = [text' (view palError pal) "No channel information"]-  where-    pal = view (clientConfig . configPalette) st--channelInfoImages' :: Palette -> HashSet Identifier -> ChannelState -> [Image]-channelInfoImages' pal myNicks !channel-    = topicLine-    : provenanceLines-   ++ creationLines-   ++ urlLines--  where-    label = text' (view palLabel pal)--    topicLine = label "Topic: " <|> parseIrcText (view chanTopic channel)---    utcTimeImage = string defAttr . formatTime defaultTimeLocale "%F %T"--    provenanceLines =-        case view chanTopicProvenance channel of-          Nothing -> []-          Just !prov ->-            [ label "Topic set by: " <|>-                coloredUserInfo pal DetailedRender myNicks (view topicAuthor prov)-            , label "Topic set on: " <|> utcTimeImage (view topicTime prov)-            ]--    creationLines =-        case view chanCreation channel of-          Nothing   -> []-          Just time -> [label "Created on: " <|> utcTimeImage time]--    urlLines =-        case view chanUrl channel of-          Nothing -> []-          Just url -> [ label "Channel URL: " <|> parseIrcText url ]-
− src/Client/Image/Help.hs
@@ -1,75 +0,0 @@-{-|-Module      : Client.Image.Help-Description : Renderer for help lines-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--This module provides the rendering used for the @/help@ command.---}-module Client.Image.Help-  ( helpImageLines-  ) where--import           Client.Image.Arguments-import           Client.Image.Palette-import           Client.Image.MircFormatting-import           Client.Commands-import           Client.Commands.Arguments-import           Control.Lens-import           Data.List.NonEmpty (NonEmpty((:|)))-import           Data.Text (Text)-import qualified Data.Text as Text-import           Graphics.Vty.Image---- | Generate either the list of all commands and their arguments,--- or when given a command name generate the detailed help text--- for that command.-helpImageLines ::-  Maybe Text {- ^ optional command name -} ->-  Palette    {- ^ palette               -} ->-  [Image]    {- ^ lines                 -}-helpImageLines mbCmd pal =-  case mbCmd of-    Nothing  -> listAllCommands pal-    Just cmd -> commandHelpLines cmd pal--commandHelpLines :: Text -> Palette -> [Image]-commandHelpLines cmdName pal =-  case view (at cmdName) commands of-    Nothing -> [string (view palError pal) "Unknown command, try /help"]-    Just (Command args doc impl) ->-      reverse $ commandSummary pal (pure cmdName) args-              : emptyImage-              : explainContext impl-              : emptyImage-              : map parseIrcText docs-      where-        docs = Text.lines doc--explainContext :: CommandImpl a -> Image-explainContext impl =-  case impl of-    ClientCommand {} -> go "client command" "works everywhere"-    NetworkCommand{} -> go "network command" "works when focused on active network"-    ChannelCommand{} -> go "channel command" "works when focused on active channel"-    ChatCommand   {} -> go "chat command" "works when focused on an active channel or private message"-  where-    go x y = string (withStyle defAttr bold) x <|>-             string defAttr (": " ++ y)--listAllCommands :: Palette -> [Image]-listAllCommands pal =-  reverse-    [ commandSummary pal name args-    | (name, Command args _ _) <- commandsList ]--commandSummary :: Palette -> NonEmpty Text -> ArgumentSpec a -> Image-commandSummary pal (cmd :| _) args  =-  char defAttr '/' <|>-  text' (view palCommand pal) cmd <|>-  argumentsImage pal' args ""--  where-    pal' = set palCommandPlaceholder defAttr pal
− src/Client/Image/MaskList.hs
@@ -1,83 +0,0 @@-{-# Language OverloadedStrings #-}-{-|-Module      : Client.Image.MaskList-Description : Line renderers for channel mask list view-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--This module renders the lines used in the channel mask list. A mask list-can show channel bans, quiets, invites, and exceptions.--}-module Client.Image.MaskList-  ( maskListImages-  ) where--import           Client.Configuration-import           Client.Image.Palette-import           Client.State-import           Client.State.Channel-import           Client.State.Network-import           Control.Lens-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import           Data.List-import           Data.Ord-import           Data.Text (Text)-import           Data.Time-import           Graphics.Vty.Image-import           Irc.Identifier---- | Render the lines used in a channel mask list-maskListImages ::-  Char        {- ^ Mask mode -} ->-  Text        {- ^ network   -} ->-  Identifier  {- ^ channel   -} ->-  ClientState -> [Image]-maskListImages mode network channel st =-  case mbEntries of-    Nothing      -> [text' (view palError pal) "Mask list not loaded"]-    Just entries -> maskListImages' entries st--  where-    pal = view (clientConfig . configPalette) st-    mbEntries = preview-                ( clientConnection network-                . csChannels . ix channel-                . chanLists . ix mode-                ) st--maskListImages' :: HashMap Text MaskListEntry -> ClientState -> [Image]-maskListImages' entries st = countImage : images-  where-    pal = view (clientConfig . configPalette) st--    countImage = text' (view palLabel pal) "Masks (visible/total): " <|>-                 string defAttr (show (length entryList)) <|>-                 char (view palLabel pal) '/' <|>-                 string defAttr (show (HashMap.size entries))--    matcher = clientMatcher st--    matcher' (mask,entry) = matcher mask || matcher (view maskListSetter entry)--    entryList = sortBy (flip (comparing (view (_2 . maskListTime))))-              $ filter matcher'-              $ HashMap.toList entries--    renderWhen = formatTime defaultTimeLocale " %F %T"--    (masks, whoWhens) = unzip entryList-    maskImages       = text' defAttr <$> masks-    maskColumnWidth  = maximum (imageWidth <$> maskImages) + 1-    paddedMaskImages = resizeWidth maskColumnWidth <$> maskImages-    width            = max 1 (view clientWidth st)--    images = [ cropLine $ mask <|>-                          text' defAttr who <|>-                          string defAttr (renderWhen when)-             | (mask, MaskListEntry who when) <- zip paddedMaskImages whoWhens ]--    cropLine img-      | imageWidth img > width = cropRight width img-      | otherwise              = img
src/Client/Image/Message.hs view
@@ -31,7 +31,6 @@ import           Data.HashSet (HashSet) import qualified Data.HashSet as HashSet import           Data.List-import           Data.Maybe import           Data.Text (Text) import qualified Data.Text as Text import           Data.Time@@ -53,7 +52,7 @@   , rendNickPadding :: Maybe Integer -- ^ nick padding   } --- | Default 'MessageRenderParams' with no sigils or nicknames specified+-- | Default 'MessageRendererParams' with no sigils or nicknames specified defaultRenderParams :: MessageRendererParams defaultRenderParams = MessageRendererParams   { rendStatusMsg   = ""@@ -368,9 +367,7 @@     color       | ident `HashSet.member` myNicks =           case icm of-            PrivmsgIdentifier -> fromMaybe-                                   (view palSelf palette)-                                   (view palSelfHighlight palette)+            PrivmsgIdentifier -> view palSelfHighlight palette             NormalIdentifier  -> view palSelf palette        | otherwise = v Vector.! i
src/Client/Image/Palette.hs view
@@ -30,6 +30,8 @@   , palCommand   , palCommandReady   , palCommandPlaceholder+  , palCommandPrefix+  , palCommandError    , paletteMap @@ -46,7 +48,7 @@ data Palette = Palette   { _palNicks         :: Vector Attr -- ^ highlighting nicknames   , _palSelf          :: Attr -- ^ own nickname(s)-  , _palSelfHighlight :: Maybe Attr -- ^ own nickname(s) in mentions+  , _palSelfHighlight :: Attr -- ^ own nickname(s) in mentions   , _palTime          :: Attr -- ^ message timestamps   , _palMeta          :: Attr -- ^ coalesced metadata   , _palSigil         :: Attr -- ^ sigils (e.g. @+)@@ -59,6 +61,8 @@   , _palMention       :: Attr -- ^ window name with mention   , _palCommand       :: Attr -- ^ known command   , _palCommandReady  :: Attr -- ^ known command with complete arguments+  , _palCommandPrefix :: Attr -- ^ prefix of known command+  , _palCommandError  :: Attr -- ^ unknown command   , _palCommandPlaceholder :: Attr -- ^ command argument placeholder   }   deriving Show@@ -70,7 +74,7 @@ defaultPalette = Palette   { _palNicks                   = defaultNickColorPalette   , _palSelf                    = withForeColor defAttr red-  , _palSelfHighlight           = Nothing+  , _palSelfHighlight           = withForeColor defAttr red   , _palTime                    = withForeColor defAttr brightBlack   , _palMeta                    = withForeColor defAttr brightBlack   , _palSigil                   = withForeColor defAttr cyan@@ -83,6 +87,8 @@   , _palMention                 = withForeColor defAttr red   , _palCommand                 = withForeColor defAttr yellow   , _palCommandReady            = withForeColor defAttr green+  , _palCommandPrefix           = withForeColor defAttr blue+  , _palCommandError            = withForeColor defAttr red   , _palCommandPlaceholder      = withStyle defAttr reverseVideo   } @@ -97,6 +103,7 @@ paletteMap :: [(Text, ReifiedLens' Palette Attr)] paletteMap =   [ ("self"             , Lens palSelf)+  , ("self-highlight"   , Lens palSelfHighlight)   , ("time"             , Lens palTime)   , ("meta"             , Lens palMeta)   , ("sigil"            , Lens palSigil)@@ -110,4 +117,6 @@   , ("command"          , Lens palCommand)   , ("command-ready"    , Lens palCommandReady)   , ("command-placeholder", Lens palCommandPlaceholder)+  , ("command-prefix"   , Lens palCommandPrefix)+  , ("command-error"    , Lens palCommandError)   ]
− src/Client/Image/PaletteView.hs
@@ -1,61 +0,0 @@-{-|-Module      : Client.Image.PaletteView-Description : View current palette and to see all terminal colors-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--Lines for the @/palette@ command. This view shows all the colors of-the current palette as well as the colors available in the terminal.---}--module Client.Image.PaletteView-  ( paletteViewLines-  ) where--import           Client.Image.Palette-import           Control.Lens-import           Data.List-import           Graphics.Vty.Image--digits :: String-digits = "0123456789ABCDEF"--digitImage :: Char -> Image-digitImage d = string defAttr [' ',d,' ']--columns :: [Image] -> Image-columns = horizCat . intersperse (char defAttr ' ')---- | Generate lines used for @/palette@. These lines show--- all the colors used in the current palette as well as--- the colors available for use in palettes.-paletteViewLines :: Palette -> [Image]-paletteViewLines pal =-  [ columns-  $ digitImage digit-  : [ string (withBackColor defAttr c) "   "-    | col <- [0 .. 15]-    , let c = Color240 (row * 16 + col)-    ]-  | (digit,row) <- reverse $ take 15 $ zip (drop 1 digits) [0 ..]-  ] ++-  [ columns-  $ digitImage '0'-  : [ string (withBackColor defAttr (ISOColor c)) "   "-    | c <- [0..15]-    ]--  , columns (map digitImage (' ':digits))-  , emptyImage-  , columns-    [ text' (view l pal) name-    | (name, Lens l) <- paletteMap-    ]-  , emptyImage-  , columns-    [ string attr "nicks"-    | attr <- toListOf (palNicks . folded) pal-    ]-  ]
src/Client/Image/StatusLine.hs view
@@ -14,7 +14,6 @@   ( statusLineImage   ) where -import           Client.Configuration import           Client.Image.Palette import           Client.State import           Client.State.Channel@@ -23,6 +22,7 @@ import           Client.State.Window import           Control.Lens import qualified Data.Map.Strict as Map+import           Data.Maybe import           Data.Text (Text) import qualified Data.Text as Text import           Graphics.Vty.Image@@ -42,6 +42,7 @@       , focusImage st       , activitySummary       , detailImage st+      , nometaImage st       , scrollImage st       , latencyImage st       ]@@ -56,7 +57,8 @@       , string defAttr ")"       ]   where-    attr = view (clientConfig . configPalette . palLabel) st+    pal  = clientPalette st+    attr = view palLabel pal  latencyImage :: ClientState -> Image latencyImage st@@ -76,7 +78,7 @@           | otherwise = emptyImage   | otherwise = emptyImage   where-    pal = view (clientConfig . configPalette) st+    pal = clientPalette st  infoBubble :: Image -> Image infoBubble img = string defAttr "─(" <|> img <|> string defAttr ")"@@ -86,8 +88,17 @@   | view clientDetailView st = infoBubble (string attr "detail")   | otherwise = emptyImage   where-    attr = view (clientConfig . configPalette . palLabel) st+    pal  = clientPalette st+    attr = view palLabel pal +nometaImage :: ClientState -> Image+nometaImage st+  | view clientShowMetadata st = emptyImage+  | otherwise = infoBubble (string attr "nometa")+  where+    pal  = clientPalette st+    attr = view palLabel pal+ activityImages :: ClientState -> (Image, Image) activityImages st = (summary, activityBar)   where@@ -121,7 +132,7 @@                     string defAttr "]" <|> rest       where         n   = view winUnread w-        pal = view (clientConfig . configPalette) st+        pal = clientPalette st         attr | view winMention w = view palMention pal              | otherwise         = view palActivity pal         focusText =@@ -130,16 +141,15 @@             NetworkFocus net    -> net             ChannelFocus _ chan -> idText chan -    windows     = views clientWindows Map.elems st-    windowNames = view (clientConfig . configWindowNames) st-    winNames    = Text.unpack windowNames ++ repeat '?'+    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 = view (clientConfig . configPalette) st+        pal = clientPalette st         attr | view winMention w = view palMention pal              | otherwise         = view palActivity pal @@ -151,7 +161,7 @@     ChannelFocus network chan -> nickPart network (Just chan)     Unfocused                 -> emptyImage   where-    pal = view (clientConfig . configPalette) st+    pal = clientPalette st     nickPart network mbChan =       case preview (clientConnection network) st of         Nothing -> emptyImage@@ -176,14 +186,13 @@       , renderedFocus       ] -    pal = view (clientConfig . configPalette) st-    focus = view clientFocus st-    windowNames = view (clientConfig . configWindowNames) st+    pal         = clientPalette st+    focus       = view clientFocus st+    windowNames = clientWindowNames st -    windowName =-      case Map.lookupIndex focus (view clientWindows st) of-        Just i | i < Text.length windowNames -> Text.index windowNames i-        _ -> '?'+    windowName = fromMaybe '?'+               $ do i <- Map.lookupIndex focus (view clientWindows st)+                    preview (ix i) windowNames      subfocusName =       case view clientSubfocus st of@@ -191,6 +200,7 @@         FocusWindows  -> Just $ string (view palLabel pal) "windows"         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 ':' <|>
src/Client/Image/Textbox.hs view
@@ -18,6 +18,8 @@ import           Client.Configuration import           Client.Commands import           Client.Commands.Arguments+import           Client.Commands.Interpolation+import           Client.Commands.Recognizer import           Client.Image.Arguments import           Client.Image.MircFormatting import           Client.Image.Palette@@ -26,6 +28,7 @@ import           Control.Lens import           Data.Char import           Data.List+import           Data.Monoid import qualified Data.Text as Text import           Graphics.Vty.Image @@ -37,13 +40,12 @@   = (pos, croppedImage)   where   width = view clientWidth st+  macros = views (clientConfig . configMacros) (fmap macroSpec) st   (txt, content) =-     views (clientTextBox . Edit.content) (renderContent pal) st+     views (clientTextBox . Edit.content) (renderContent macros pal) st    pos = min (width-1) leftOfCurWidth -  pal = view (clientConfig . configPalette) st-   lineImage = beginning <|> content <|> ending    leftOfCurWidth = myWcswidth ('^':txt)@@ -52,7 +54,8 @@     | leftOfCurWidth < width = lineImage     | otherwise = cropLeft width (cropRight (leftOfCurWidth+1) lineImage) -  attr      = view (clientConfig . configPalette . palTextBox) st+  pal       = clientPalette st+  attr      = view palTextBox pal   beginning = char attr '^'   ending    = char attr '$' @@ -60,10 +63,11 @@ -- corresponding to the rendered image which can be used for computing -- the logical cursor position of the cropped version of the text box. renderContent ::+  Recognizer MacroSpec {- ^ macro completions                     -} ->   Palette         {- ^ palette                               -} ->   Edit.Content    {- ^ content                               -} ->   (String, Image) {- ^ plain text rendering, image rendering -}-renderContent pal c = (txt, wholeImg)+renderContent macros pal c = (txt, wholeImg)   where   as  = reverse (view Edit.above c)   bs  = view Edit.below c@@ -78,7 +82,7 @@   wholeImg = horizCat            $ intersperse (plainText "\n")            $ map renderOtherLine as-          ++ renderLine pal curTxt+          ++ renderLine macros pal curTxt            : map renderOtherLine bs  @@ -101,17 +105,35 @@  -- | Render the active text box line using command highlighting and -- placeholders, and WYSIWYG mIRC formatting control characters.-renderLine :: Palette -> String -> Image+renderLine :: Recognizer MacroSpec -> Palette -> String -> Image+renderLine macros pal ('/':xs)+  = char defAttr '/' <|> string attr cmd <|> continue rest+ where+ specAttr spec =+   case parseArguments spec rest of+     Nothing -> view palCommand      pal+     Just{}  -> view palCommandReady pal -renderLine pal ('/':xs)-  | (cmd,rest)              <- break isSpace xs-  , Just (Command spec _ _) <- view (at (Text.pack cmd)) commands-  , let attr =-          case parseArguments spec rest of-            Nothing -> view palCommand      pal-            Just{}  -> view palCommandReady pal-  = char defAttr '/' <|>-    string attr cmd <|>-    argumentsImage pal spec rest+ (cmd, rest) = break isSpace xs+ allCommands = (Left <$> macros) <> (Right <$> commands)+ (attr, continue)+   = case recognize (Text.pack cmd) allCommands of+       Exact (Right (Command spec _ _)) ->+         ( specAttr spec+         , argumentsImage pal spec+         )+       Exact (Left (MacroSpec spec)) ->+         ( specAttr spec+         , argumentsImage pal spec+         )+       Prefix _ ->+         ( view palCommandPrefix pal+         , renderOtherLine+         )+       Invalid ->+         ( view palCommandError pal+         , renderOtherLine+         ) -renderLine _ xs = parseIrcTextExplicit (Text.pack xs)+renderLine _ _ xs = parseIrcTextExplicit (Text.pack xs)+
− src/Client/Image/UserList.hs
@@ -1,128 +0,0 @@-{-# Language OverloadedStrings #-}-{-|-Module      : Client.Image.UserList-Description : Line renderers for channel user list view-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--This module renders the lines used in the channel user list.--}-module Client.Image.UserList-  ( userListImages-  , userInfoImages-  ) where--import           Client.Configuration-import           Client.Image.Message-import           Client.Image.Palette-import           Client.State-import           Client.State.Channel-import           Client.State.Network-import           Control.Lens-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map.Strict as Map-import           Data.List-import           Data.Ord-import           Data.Text (Text)-import qualified Data.Text as Text-import           Graphics.Vty.Image-import           Irc.Identifier-import           Irc.UserInfo---- | Render the lines used by the @/users@ command in normal mode.--- These lines show the count of users having each channel mode--- in addition to the nicknames of the users.-userListImages ::-  Text        {- ^ network -} ->-  Identifier  {- ^ channel -} ->-  ClientState                 ->-  [Image]-userListImages network channel st =-  case preview (clientConnection network) st of-    Just cs -> userListImages' cs channel st-    Nothing -> [text' (view palError pal) "No connection"]-  where-    pal = view (clientConfig . configPalette) st--userListImages' :: NetworkState -> Identifier -> ClientState -> [Image]-userListImages' cs channel st =-    [countImage, horizCat (intersperse gap (map renderUser usersList))]-  where-    countImage = text' (view palLabel pal) "Users:" <|>-                 sigilCountImage--    matcher = clientMatcher st--    myNicks = clientHighlights cs st--    renderUser (ident, sigils) =-      string (view palSigil pal) sigils <|>-      coloredIdentifier pal NormalIdentifier myNicks ident--    gap = char defAttr ' '--    matcher' (ident,sigils) = matcher (Text.pack sigils `Text.append` idText ident)--    usersList = sortBy (comparing fst)-              $ filter matcher'-              $ HashMap.toList usersHashMap--    sigilCounts = Map.fromListWith (+)-                    [ (take 1 sigil, 1::Int) | (_,sigil) <- usersList ]--    sigilCountImage = horizCat-      [ string (view palSigil pal) (' ':sigil) <|>-        string defAttr (show n)-      | (sigil,n) <- Map.toList sigilCounts-      ]--    pal = view (clientConfig . configPalette) st--    usersHashMap =-      view (csChannels . ix channel . chanUsers) cs---- | Render lines for the @/users@ command in detailed view.--- Each user will be rendered on a separate line with username--- and host visible when known.-userInfoImages ::-  Text        {- ^ network -} ->-  Identifier  {- ^ channel -} ->-  ClientState                 ->-  [Image]-userInfoImages network channel st =-  case preview (clientConnection network) st of-    Just cs -> userInfoImages' cs channel st-    Nothing -> [text' (view palError pal) "No connection"]-  where-    pal = view (clientConfig . configPalette) st--userInfoImages' :: NetworkState -> Identifier -> ClientState -> [Image]-userInfoImages' cs channel st = renderEntry <$> usersList-  where-    matcher = clientMatcher st--    myNicks = clientHighlights cs st--    pal = view (clientConfig . configPalette) st--    renderEntry (info, sigils) =-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal DetailedRender myNicks info--    matcher' (info,sigils) =-      matcher (Text.pack sigils `Text.append` renderUserInfo info)--    userInfos = view csUsers cs--    toInfo nick =-      case view (at nick) userInfos of-        Just (UserAndHost n h) -> UserInfo nick n h-        Nothing                -> UserInfo nick "" ""--    usersList = sortBy (flip (comparing (userNick . fst)))-              $ filter matcher'-              $ map (over _1 toInfo)-              $ HashMap.toList usersHashMap--    usersHashMap = view (csChannels . ix channel . chanUsers) cs
− src/Client/Image/Windows.hs
@@ -1,75 +0,0 @@-{-|-Module      : Client.Image.Windows-Description : View of the list of open windows-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--This module implements the rendering of the client window list.---}-module Client.Image.Windows-  ( windowsImages-  ) where--import           Client.Configuration-import           Client.Image.Palette-import           Client.State-import           Client.State.Focus-import           Client.State.Window-import           Control.Lens-import           Data.List-import qualified Data.Map as Map-import qualified Data.Text as Text-import           Graphics.Vty.Image-import           Irc.Identifier---- | Draw the image lines associated with the @/windows@ command.-windowsImages :: ClientState -> [Image]-windowsImages st-  = reverse-  $ createColumns-  $ zipWith (renderWindowColumns pal) names windows-  where-    cfg     = view clientConfig st-    windows = views clientWindows Map.toAscList st--    pal     = view configPalette cfg-    names   = views configWindowNames Text.unpack cfg ++ repeat '?'--renderWindowColumns :: Palette -> Char -> (Focus, Window) -> [Image]-renderWindowColumns pal name (focus, win) =-  [ char (view palWindowName pal) name-  , renderedFocus pal focus-  , renderedWindowInfo pal win-  ]--createColumns :: [[Image]] -> [Image]-createColumns xs = map makeRow xs-  where-    columnWidths = maximum . map imageWidth <$> transpose xs-    makeRow = horizCat-            . intersperse (char defAttr ' ')-            . zipWith resizeWidth columnWidths--renderedFocus :: Palette -> Focus -> Image-renderedFocus pal focus =-  case focus of-    Unfocused ->-      char (view palError pal) '*'-    NetworkFocus network ->-      text' (view palLabel pal) network-    ChannelFocus network channel ->-      text' (view palLabel pal) network <|>-      char defAttr ':' <|>-      text' (view palLabel pal) (idText channel)--renderedWindowInfo :: Palette -> Window -> Image-renderedWindowInfo pal win =-  string (view newMsgAttrLens pal) (views winUnread show win) <|>-  char defAttr '/' <|>-  string (view palActivity pal) (views winTotal show win)-  where-    newMsgAttrLens-      | view winMention win = palMention-      | otherwise           = palActivity
src/Client/Network/Async.hs view
@@ -62,12 +62,14 @@ -- are annotated with a network ID matching that given when the connection -- was created as well as the time at which the message was recieved. data NetworkEvent-  = NetworkLine  !NetworkId !ZonedTime !ByteString-    -- ^ Event for a new recieved line (newline removed)+  -- | Event for successful connection to host+  = NetworkOpen  !NetworkId !ZonedTime+  -- | Event for a new recieved line (newline removed)+  | NetworkLine  !NetworkId !ZonedTime !ByteString+  -- | Final message indicating the network connection failed   | NetworkError !NetworkId !ZonedTime !SomeException-    -- ^ Final message indicating the network connection failed+  -- | Final message indicating the network connection finished   | NetworkClose !NetworkId !ZonedTime-    -- ^ Final message indicating the network connection finished  instance Show NetworkConnection where   showsPrec p _ = showParen (p > 10)@@ -142,19 +144,25 @@   TQueue NetworkEvent ->   TQueue ByteString ->   IO ()-startConnection network cxt settings onInput outQueue =+startConnection network cxt settings inQueue outQueue =   do rate <- newRateLimit                (view ssFloodPenalty settings)                (view ssFloodThreshold settings)      withConnection cxt settings $ \h ->-       withAsync (sendLoop h outQueue rate)      $ \sender ->-       withAsync (receiveLoop network h onInput) $ \receiver ->-         do res <- waitEitherCatch sender receiver-            case res of-              Left  Right{}  -> fail "PANIC: sendLoop returned"-              Right Right{}  -> return ()-              Left  (Left e) -> throwIO e-              Right (Left e) -> throwIO e+       do reportNetworkOpen network inQueue+          withAsync (sendLoop h outQueue rate) $ \sender ->+            withAsync (receiveLoop network h inQueue) $ \receiver ->+              do res <- waitEitherCatch sender receiver+                 case res of+                   Left  Right{}  -> fail "PANIC: sendLoop returned"+                   Right Right{}  -> return ()+                   Left  (Left e) -> throwIO e+                   Right (Left e) -> throwIO e++reportNetworkOpen :: NetworkId -> TQueue NetworkEvent -> IO ()+reportNetworkOpen network inQueue =+  do now <- getZonedTime+     atomically (writeTQueue inQueue (NetworkOpen network now))  sendLoop :: Connection -> TQueue ByteString -> RateLimit -> IO () sendLoop h outQueue rate =
+ src/Client/Options.hs view
@@ -0,0 +1,117 @@+{-# Language TemplateHaskell, MultiWayIf #-}+{-|+Module      : Client.Options+Description : Processing of command-line options+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module process command-line options provided when launching the client.++-}+module Client.Options+  (+  -- * Command-line options+    Options(..)++  -- * Lenses+  , optConfigFile+  , optInitialNetworks++  -- * Options loader+  , getOptions+  ) where++import           Control.Lens+import           Data.Foldable+import           Data.Text (Text)+import qualified Data.Text as Text+import           Data.Version+import           Development.GitRev (gitHash, gitDirty)+import           System.Console.GetOpt+import           System.Environment+import           System.Exit+import           System.IO+import           Paths_glirc (version)++-- | Command-line options+data Options = Options+  { _optConfigFile      :: Maybe FilePath -- ^ configuration file path+  , _optInitialNetworks :: [Text]         -- ^ initial networks+  , _optShowHelp        :: Bool           -- ^ show help message+  , _optShowVersion     :: Bool           -- ^ show version message+  }++makeLenses ''Options++-- | Default values for 'Options'+defaultOptions :: Options+defaultOptions = Options+  { _optConfigFile      = Nothing+  , _optInitialNetworks = []+  , _optShowHelp        = False+  , _optShowVersion     = False+  }++-- | Option descriptions+options :: [OptDescr (Options -> Options)]+options =+  [ Option "c" ["config"]  (ReqArg (set optConfigFile . Just) "PATH")+    "Configuration file path"+  , Option "h" ["help"]    (NoArg (set optShowHelp True))+    "Show help"+  , Option "v" ["version"] (NoArg (set optShowVersion True))+    "Show version"+  ]++optOrder :: ArgOrder (Options -> Options)+optOrder = ReturnInOrder (\x -> optInitialNetworks <>~ [Text.pack x])++-- | Load command line options. This action will terminate early+-- in the case of the version flag, help flag, or an error.+getOptions :: IO Options+getOptions =+  do (flags, _, errors) <- getOpt optOrder options <$> getArgs++     let opts = foldl' (\acc f -> f acc) defaultOptions flags++         bullet x = "• " ++ x++         reportErrors =+           do hPutStrLn stderr "Errors processing command-line options:"+              traverse_ (hPutStr stderr) (map bullet errors)+              hPutStrLn stderr tryHelpTxt++     if | view optShowHelp    opts -> putStr helpTxt    >> exitSuccess+        | view optShowVersion opts -> putStr versionTxt >> exitSuccess+        | null errors              -> return opts+        | otherwise                -> reportErrors      >> exitFailure++helpTxt :: String+helpTxt = usageInfo "glirc2 [FLAGS] INITIAL_NETWORKS..." options++tryHelpTxt :: String+tryHelpTxt =+  "Run 'glirc2 --help' to see a list of available command line options."++versionTxt :: String+versionTxt = unlines+  [ "glirc-" ++ showVersion version ++ gitHashTxt ++ gitDirtyTxt+  , "Copyright 2016 Eric Mertens"+  ]++-- git version information ---------------------------------------------++-- | Returns @"-SOMEHASH"@ when in a git repository, @""@ otherwise.+gitHashTxt :: String+gitHashTxt+  | hashTxt == "UNKNOWN" = ""+  | otherwise            = '-':hashTxt+  where+    hashTxt = $gitHash++-- | Returns @"-dirty"@ when in a dirty git repository, @""@ otherwise.+gitDirtyTxt :: String+gitDirtyTxt+  | $gitDirty = "-dirty"+  | otherwise = ""
src/Client/State.hs view
@@ -29,6 +29,7 @@   , clientScroll   , clientDetailView   , clientActivityBar+  , clientShowMetadata   , clientSubfocus   , clientNextConnectionId   , clientNetworkMap@@ -56,6 +57,8 @@   , clientTick   , applyMessageToClientState   , clientHighlights+  , clientWindowNames+  , clientPalette    -- * Add messages to buffers   , recordChannelMessage@@ -85,6 +88,7 @@ import           Client.Configuration import           Client.Configuration.ServerSettings import           Client.Image.Message+import           Client.Image.Palette import           Client.Message import           Client.Network.Async import           Client.State.Channel@@ -139,14 +143,18 @@   , _clientEvents            :: !(TQueue NetworkEvent)    -- ^ incoming network event queue   , _clientNetworkMap        :: !(HashMap Text NetworkId) -- ^ network name to connection ID +  , _clientConfig            :: !Configuration            -- ^ client configuration+   , _clientVty               :: !Vty                      -- ^ VTY handle   , _clientTextBox           :: !Edit.EditBox             -- ^ primary text box   , _clientWidth             :: !Int                      -- ^ current terminal width   , _clientHeight            :: !Int                      -- ^ current terminal height-  , _clientConfig            :: !Configuration            -- ^ client configuration+   , _clientScroll            :: !Int                      -- ^ buffer scroll lines   , _clientDetailView        :: !Bool                     -- ^ use detailed rendering mode   , _clientActivityBar       :: !Bool                     -- ^ visible activity bar+  , _clientShowMetadata      :: !Bool                     -- ^ visible activity bar+   , _clientBell              :: !Bool                     -- ^ sound a bell next draw    , _clientIgnores           :: !(HashSet Identifier)     -- ^ ignored nicknames@@ -218,6 +226,7 @@         , _clientConfig            = cfg         , _clientScroll            = 0         , _clientDetailView        = False+        , _clientShowMetadata      = True         , _clientActivityBar       = view configActivityBar cfg         , _clientNextConnectionId  = 0         , _clientBell              = False@@ -257,17 +266,17 @@   ClientMessage ->   ClientState -> ClientState recordChannelMessage network channel msg st =-  recordWindowLine focus importance wl st+  recordWindowLine focus wl st   where     focus      = ChannelFocus network channel'-    wl         = toWindowLine rendParams msg+    wl         = toWindowLine rendParams importance msg      rendParams = MessageRendererParams       { rendStatusMsg   = statusModes       , rendUserSigils  = computeMsgLineSigils network channel' msg st       , rendNicks       = HashSet.fromList (channelUserList network channel' st)       , rendMyNicks     = highlights-      , rendPalette     = view (clientConfig . configPalette) st+      , rendPalette     = clientPalette st       , rendNickPadding = view (clientConfig . configNickPadding) st       } @@ -364,11 +373,11 @@     TargetUser user   ->       foldl' (\st' chan -> overStrict                              (clientWindows . ix (ChannelFocus network chan))-                             (addToWindow WLBoring wl) st')+                             (addToWindow wl) st')            st chans       where         cfg   = view clientConfig st-        wl    = toWindowLine' cfg msg+        wl    = toWindowLine' cfg WLBoring msg         chans = user               : case preview (clientConnection network . csChannels) st of                   Nothing -> []@@ -401,39 +410,40 @@  -- | Record a message on a network window recordNetworkMessage :: ClientMessage -> ClientState -> ClientState-recordNetworkMessage msg st = recordWindowLine focus importance wl st+recordNetworkMessage msg st = recordWindowLine focus wl st   where     network    = view msgNetwork msg     focus      | Text.null network = Unfocused                | otherwise         = NetworkFocus (view msgNetwork msg)     importance = msgImportance msg st-    wl         = toWindowLine' cfg msg+    wl         = toWindowLine' cfg importance msg      cfg        = view clientConfig st  -- | Record window line at the given focus creating the window if necessary recordWindowLine ::   Focus ->-  WindowLineImportance ->   WindowLine ->   ClientState -> ClientState-recordWindowLine focus importance wl =+recordWindowLine focus wl =   over (clientWindows . at focus)-       (\w -> Just $! addToWindow importance wl (fromMaybe emptyWindow w))+       (\w -> Just $! addToWindow wl (fromMaybe emptyWindow w)) -toWindowLine :: MessageRendererParams -> ClientMessage -> WindowLine-toWindowLine params msg = WindowLine-  { _wlBody      = view msgBody msg-  , _wlText      = msgText (view msgBody msg)-  , _wlImage     = mkImage NormalRender-  , _wlFullImage = mkImage DetailedRender+toWindowLine :: MessageRendererParams -> WindowLineImportance -> ClientMessage -> WindowLine+toWindowLine params importance msg = WindowLine+  { _wlBody       = view msgBody msg+  , _wlText       = msgText (view msgBody msg)+  , _wlImage      = mkImage NormalRender+  , _wlFullImage  = mkImage DetailedRender+  , _wlImportance = importance+  , _wlTimestamp  = zonedTimeToUTC (view msgTime msg)   }   where     mkImage mode =       force (msgImage mode (view msgTime msg) params (view msgBody msg))  -- | 'toWindowLine' but with mostly defaulted parameters.-toWindowLine' :: Configuration -> ClientMessage -> WindowLine+toWindowLine' :: Configuration -> WindowLineImportance -> ClientMessage -> WindowLine toWindowLine' config =   toWindowLine defaultRenderParams     { rendPalette     = view configPalette     config@@ -755,3 +765,13 @@   case preview (clientConnection network) st of     Just cs -> clientHighlights cs st     Nothing -> view (clientConfig . configExtraHighlights) st++-- | Produce the list of window names configured for the client.+clientWindowNames ::+  ClientState ->+  [Char]+clientWindowNames = views (clientConfig . configWindowNames) Text.unpack++-- | Produce the list of window names configured for the client.+clientPalette :: ClientState -> Palette+clientPalette = view (clientConfig . configPalette)
src/Client/State/EditBox.hs view
@@ -43,6 +43,7 @@   , killWordBackward   , killWordForward   , yank+  , toggle   , left   , right   , leftWord
src/Client/State/EditBox/Content.hs view
@@ -1,4 +1,4 @@-{-# Language TemplateHaskell #-}+{-# Language TemplateHaskell, BangPatterns #-} {-| Module      : Client.State.EditBox.Content Description : Multiline text container with cursor@@ -42,6 +42,7 @@   , insertPastedString   , insertString   , insertChar+  , toggle   ) where  import           Control.Lens hiding (below)@@ -253,3 +254,18 @@          $! over below (view text c :)           $ set above as           $ set line (endLine a) c++toggle :: Content -> Content+toggle !c+  | p < 1     = c+  | n < 2     = c+  | n == p    = over text (swapAt (p-2)) c+  | otherwise = set pos (p+1)+              $ over text (swapAt (p-1)) c+  where+    p = view pos c+    n = views text length c++    swapAt 0 (x:y:z) = y:x:z+    swapAt i (x:xs)  = x:swapAt (i-1) xs+    swapAt _ _       = error "toggle: PANIC! Invalid argument"
src/Client/State/Focus.hs view
@@ -24,13 +24,6 @@   , _ChannelFocus   , _NetworkFocus   , _Unfocused--  -- * Subfocus Prisms-  , _FocusMessages-  , _FocusInfo-  , _FocusUsers-  , _FocusMasks-  , _FocusWindows   ) where  import           Control.Lens@@ -55,10 +48,9 @@   | FocusMasks !Char -- ^ Show channel mask list for given mode   | FocusWindows     -- ^ Show client windows   | FocusPalette     -- ^ Show current palette+  | FocusMentions    -- ^ Show all mentions   | FocusHelp (Maybe Text) -- ^ Show help window with optional command   deriving (Eq,Show)--makePrisms ''Subfocus  -- | Unfocused first, followed by focuses sorted by network. -- Within the same network the network focus comes first and
src/Client/State/Window.hs view
@@ -26,6 +26,8 @@   , wlText   , wlImage   , wlFullImage+  , wlImportance+  , wlTimestamp    -- * Window line importance   , WindowLineImportance(..)@@ -39,14 +41,17 @@ import           Client.Message import           Control.Lens import           Data.Text (Text)+import           Data.Time (UTCTime) import           Graphics.Vty.Image (Image)  -- | A single message to be displayed in a window data WindowLine = WindowLine-  { _wlBody      :: !MessageBody -- ^ Original Haskell value-  , _wlText      :: {-# UNPACK #-} !Text -- ^ Searchable text form-  , _wlImage     :: !Image       -- ^ Normal rendered image-  , _wlFullImage :: !Image       -- ^ Detailed rendered image+  { _wlBody       :: !MessageBody -- ^ Original Haskell value+  , _wlText       :: {-# UNPACK #-} !Text -- ^ Searchable text form+  , _wlImage      :: !Image       -- ^ Normal rendered image+  , _wlFullImage  :: !Image       -- ^ Detailed rendered image+  , _wlImportance :: !WindowLineImportance -- ^ Importance of message+  , _wlTimestamp  :: {-# UNPACK #-} !UTCTime   }  -- | A 'Window' tracks all of the messages and metadata for a particular@@ -79,14 +84,14 @@  -- | Adds a given line to a window as the newest message. Window's -- unread count will be updated according to the given importance.-addToWindow :: WindowLineImportance -> WindowLine -> Window -> Window-addToWindow importance !msg !win = Window-    { _winMessages = msg : _winMessages win-    , _winTotal    = _winTotal win + 1-    , _winUnread   = _winUnread win-                   + (if importance == WLBoring then 0 else 1)-    , _winMention  = _winMention win-                  || importance == WLImportant+addToWindow :: WindowLine -> Window -> Window+addToWindow !msg !win = Window+    { _winMessages = msg : view winMessages win+    , _winTotal    = view winTotal win + 1+    , _winUnread   = view winUnread win+                   + (if view wlImportance msg == WLBoring then 0 else 1)+    , _winMention  = view winMention win+                  || view wlImportance msg == WLImportant     }  -- | Update the window clearing the unread count and important flag.
+ src/Client/View.hs view
@@ -0,0 +1,46 @@+{-# Language BangPatterns #-}+{-|+Module      : Client.View+Description : View selection module+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module selects the correct view based on the current state.++-}+module Client.View+  ( viewLines+  ) where++import           Client.State+import           Client.State.Focus+import           Client.View.ChannelInfo+import           Client.View.Help+import           Client.View.MaskList+import           Client.View.Mentions+import           Client.View.Messages+import           Client.View.Palette+import           Client.View.UserList+import           Client.View.Windows+import           Control.Lens+import           Graphics.Vty.Image++viewLines :: ClientState -> [Image]+viewLines !st =+  case (view clientFocus st, view clientSubfocus st) of+    (ChannelFocus network channel, FocusInfo) ->+      channelInfoImages network channel st+    (ChannelFocus network channel, FocusUsers)+      | view clientDetailView st -> userInfoImages network channel st+      | otherwise                -> userListImages network channel st+    (ChannelFocus network channel, FocusMasks mode) ->+      maskListImages mode network channel st+    (_, FocusWindows) -> windowsImages st+    (_, FocusMentions) -> mentionsViewLines st+    (_, FocusPalette) -> paletteViewLines pal+    (_, FocusHelp mb) -> helpImageLines mb pal++    _ -> chatMessageImages st+  where+    pal = clientPalette st
+ src/Client/View/ChannelInfo.hs view
@@ -0,0 +1,80 @@+{-# Language OverloadedStrings #-}+{-# Language BangPatterns #-}++{-|+Module      : Client.View.ChannelInfo+Description : Channel information renderer+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module implements a renderer for the window that shows+channel metadata.++-}+module Client.View.ChannelInfo+  ( channelInfoImages+  ) where++import           Client.Image.Message+import           Client.Image.MircFormatting+import           Client.Image.Palette+import           Client.State+import           Client.State.Channel+import           Client.State.Network+import           Control.Lens+import           Data.HashSet (HashSet)+import           Data.Text (Text)+import           Data.Time+import           Graphics.Vty.Image+import           Irc.Identifier++-- | Render the lines used in a channel mask list+channelInfoImages ::+  Text        {- ^ network -} ->+  Identifier  {- ^ channel -} ->+  ClientState -> [Image]+channelInfoImages network channelId st++  | Just cs      <- preview (clientConnection network) st+  , Just channel <- preview (csChannels . ix channelId) cs+  = channelInfoImages' pal (clientHighlights cs st) channel++  | otherwise = [text' (view palError pal) "No channel information"]+  where+    pal = clientPalette st++channelInfoImages' :: Palette -> HashSet Identifier -> ChannelState -> [Image]+channelInfoImages' pal myNicks !channel+    = topicLine+    : provenanceLines+   ++ creationLines+   ++ urlLines++  where+    label = text' (view palLabel pal)++    topicLine = label "Topic: " <|> parseIrcText (view chanTopic channel)+++    utcTimeImage = string defAttr . formatTime defaultTimeLocale "%F %T"++    provenanceLines =+        case view chanTopicProvenance channel of+          Nothing -> []+          Just !prov ->+            [ label "Topic set by: " <|>+                coloredUserInfo pal DetailedRender myNicks (view topicAuthor prov)+            , label "Topic set on: " <|> utcTimeImage (view topicTime prov)+            ]++    creationLines =+        case view chanCreation channel of+          Nothing   -> []+          Just time -> [label "Created on: " <|> utcTimeImage time]++    urlLines =+        case view chanUrl channel of+          Nothing -> []+          Just url -> [ label "Channel URL: " <|> parseIrcText url ]+
+ src/Client/View/Help.hs view
@@ -0,0 +1,100 @@+{-# Language OverloadedStrings #-}++{-|+Module      : Client.View.Help+Description : Renderer for help lines+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module provides the rendering used for the @/help@ command.++-}+module Client.View.Help+  ( helpImageLines+  ) where++import           Client.Commands+import           Client.Commands.Arguments+import           Client.Image.Arguments+import           Client.Image.MircFormatting+import           Client.Image.Palette+import           Client.Commands.Recognizer+import           Control.Lens+import           Data.List.NonEmpty (NonEmpty((:|)))+import           Data.Monoid ((<>))+import           Data.Text (Text)+import qualified Data.Text as Text+import           Graphics.Vty.Image++-- | Generate either the list of all commands and their arguments,+-- or when given a command name generate the detailed help text+-- for that command.+helpImageLines ::+  Maybe Text {- ^ optional command name -} ->+  Palette    {- ^ palette               -} ->+  [Image]    {- ^ help lines            -}+helpImageLines mbCmd pal =+  case mbCmd of+    Nothing  -> listAllCommands pal+    Just cmd -> commandHelpLines cmd pal++-- | Generate detailed help lines for the command with the given name.+commandHelpLines ::+  Text    {- ^ command name -} ->+  Palette {- ^ palette      -} ->+  [Image] {- ^ lines        -}+commandHelpLines cmdName pal =+  case recognize cmdName commands of+    Invalid -> [string (view palError pal) "Unknown command, try /help"]+    Prefix sfxs ->+      [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+              : emptyImage+              : explainContext impl+              : emptyImage+              : map parseIrcText docs+      where+        docs = Text.lines doc++-- | Generate an explanation of the context where the given command+-- implementation will be valid.+explainContext ::+  CommandImpl a {- ^ command implementation -} ->+  Image         {- ^ help line              -}+explainContext impl =+  case impl of+    ClientCommand {} -> go "client command" "works everywhere"+    NetworkCommand{} -> go "network command" "works when focused on active network"+    ChannelCommand{} -> go "channel command" "works when focused on active channel"+    ChatCommand   {} -> go "chat command" "works when focused on an active channel or private message"+  where+    go x y = string (withStyle defAttr bold) x <|>+             string defAttr (": " ++ y)++-- | Generate the lines for the help window showing all commands.+listAllCommands ::+  Palette {- ^ palette    -} ->+  [Image] {- ^ help lines -}+listAllCommands pal =+  reverse+    [ commandSummary pal name args+    | (name, Command args _ _) <- commandsList ]++-- | Generate the help line for the given command and its+-- specification for use in the list of commands.+commandSummary ::+  Palette        {- ^ palette                  -} ->+  NonEmpty Text  {- ^ command name and aliases -} ->+  ArgumentSpec a {- ^ argument specification   -} ->+  Image          {- ^ summary help line        -}+commandSummary pal (cmd :| _) args  =+  char defAttr '/' <|>+  text' (view palCommand pal) cmd <|>+  argumentsImage pal' args ""++  where+    pal' = set palCommandPlaceholder defAttr pal
+ src/Client/View/MaskList.hs view
@@ -0,0 +1,82 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.View.MaskList+Description : Line renderers for channel mask list view+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module renders the lines used in the channel mask list. A mask list+can show channel bans, quiets, invites, and exceptions.+-}+module Client.View.MaskList+  ( maskListImages+  ) where++import           Client.Image.Palette+import           Client.State+import           Client.State.Channel+import           Client.State.Network+import           Control.Lens+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import           Data.List+import           Data.Ord+import           Data.Text (Text)+import           Data.Time+import           Graphics.Vty.Image+import           Irc.Identifier++-- | Render the lines used in a channel mask list+maskListImages ::+  Char        {- ^ Mask mode -} ->+  Text        {- ^ network   -} ->+  Identifier  {- ^ channel   -} ->+  ClientState -> [Image]+maskListImages mode network channel st =+  case mbEntries of+    Nothing      -> [text' (view palError pal) "Mask list not loaded"]+    Just entries -> maskListImages' entries st++  where+    pal = clientPalette st+    mbEntries = preview+                ( clientConnection network+                . csChannels . ix channel+                . chanLists . ix mode+                ) st++maskListImages' :: HashMap Text MaskListEntry -> ClientState -> [Image]+maskListImages' entries st = countImage : images+  where+    pal = clientPalette st++    countImage = text' (view palLabel pal) "Masks (visible/total): " <|>+                 string defAttr (show (length entryList)) <|>+                 char (view palLabel pal) '/' <|>+                 string defAttr (show (HashMap.size entries))++    matcher = clientMatcher st++    matcher' (mask,entry) = matcher mask || matcher (view maskListSetter entry)++    entryList = sortBy (flip (comparing (view (_2 . maskListTime))))+              $ filter matcher'+              $ HashMap.toList entries++    renderWhen = formatTime defaultTimeLocale " %F %T"++    (masks, whoWhens) = unzip entryList+    maskImages       = text' defAttr <$> masks+    maskColumnWidth  = maximum (imageWidth <$> maskImages) + 1+    paddedMaskImages = resizeWidth maskColumnWidth <$> maskImages+    width            = max 1 (view clientWidth st)++    images = [ cropLine $ mask <|>+                          text' defAttr who <|>+                          string defAttr (renderWhen when)+             | (mask, MaskListEntry who when) <- zip paddedMaskImages whoWhens ]++    cropLine img+      | imageWidth img > width = cropRight width img+      | otherwise              = img
+ src/Client/View/Mentions.hs view
@@ -0,0 +1,109 @@+{-# Language BangPatterns #-}+{-|+Module      : Client.View.Mentions+Description : Mentions view+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module provides the lines that have been highlighted+across the client in sorted order.++-}+module Client.View.Mentions+  ( mentionsViewLines+  ) where++import           Client.Image.Palette+import           Client.State+import           Client.State.Focus+import           Client.State.Window+import qualified Data.Map as Map+import           Control.Lens+import           Data.Text (Text)+import           Data.Time (UTCTime)+import           Irc.Identifier (idText)+import           Graphics.Vty.Image++-- | Generate the list of message lines marked important ordered by+-- time. Each run of lines from the same channel will be grouped+-- together. Messages are headed by their window, network, and channel.+mentionsViewLines :: ClientState -> [Image]+mentionsViewLines st = addMarkers pal entries++  where+    pal = clientPalette st++    names = clientWindowNames st ++ repeat '?'++    detail = view clientDetailView st++    entries = merge+              [windowEntries detail n net (idText chan) v+              | (n,(ChannelFocus net chan, v))+                <- names `zip` Map.toList (view clientWindows st) ]++data MentionLine = MentionLine+  { mlTimestamp  :: UTCTime+  , mlWindowName :: Char+  , mlNetwork    :: Text+  , mlChannel    :: Text+  , mlImage      :: Image+  }++addMarkers ::+  Palette       {- ^ palette                           -} ->+  [MentionLine] {- ^ list of mentions in time order    -} ->+  [Image]       {- ^ mention images and channel labels -}+addMarkers _ [] = []+addMarkers !pal (!ml : xs) =+  mlImage ml : map mlImage same ++ windowMarker : addMarkers pal rest+  where+    isSame ml' = mlWindowName ml == mlWindowName ml'+              && mlNetwork    ml == mlNetwork    ml'+              && mlChannel    ml == mlChannel    ml'++    (same,rest) = span isSame xs++    windowMarker = char (view palWindowName pal) (mlWindowName ml) <|>+                   char defAttr ':' <|>+                   text' (view palLabel pal) (mlNetwork ml) <|>+                   char defAttr ':' <|>+                   text' (view palLabel pal) (mlChannel ml)++windowEntries ::+  Bool   {- ^ detailed view -} ->+  Char   {- ^ window name   -} ->+  Text   {- ^ network name  -} ->+  Text   {- ^ channel name  -} ->+  Window {- ^ window        -} ->+  [MentionLine]+windowEntries !detailed name net chan w =+  [ MentionLine+      { mlTimestamp  = view wlTimestamp l+      , mlWindowName = name+      , mlNetwork    = net+      , mlChannel    = chan+      , mlImage      = if detailed then view wlFullImage l else view wlImage l+      }+  | l <- view winMessages w+  , WLImportant == view wlImportance l+  ]++-- | Merge a list of sorted lists of mention lines into a single sorted list+-- in descending order.+merge :: [[MentionLine]] -> [MentionLine]+merge []  = []+merge [x] = x+merge xss = merge (merge2s xss)++merge2s :: [[MentionLine]] -> [[MentionLine]]+merge2s (x:y:z) = merge2 x y : merge2s z+merge2s xs      = xs++merge2 :: [MentionLine] -> [MentionLine] -> [MentionLine]+merge2 [] ys = ys+merge2 xs [] = xs+merge2 xxs@(x:xs) yys@(y:ys)+  | mlTimestamp x >= mlTimestamp y = x : merge2 xs yys+  | otherwise                      = y : merge2 xxs ys
+ src/Client/View/Messages.hs view
@@ -0,0 +1,134 @@+{-|+Module      : Client.View.Messages+Description : Chat message view+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module returns the chat messages for the currently focused+window in message view and gathers metadata entries into single+lines.++-}+module Client.View.Messages+  ( chatMessageImages+  ) where++import           Client.Image.Palette+import           Client.Image.Message+import           Client.State+import           Client.State.Network+import           Client.State.Window+import           Client.Message+import           Control.Lens+import           Irc.Identifier+import           Graphics.Vty.Image++chatMessageImages :: ClientState -> [Image]+chatMessageImages st = windowLineProcessor focusedMessages+  where+    matcher = clientMatcher st++    focusedMessages+        = filter (views wlText matcher)+        $ view (clientWindows . ix (view clientFocus st) . winMessages) st++    windowLineProcessor++      | view clientDetailView st =+          if view clientShowMetadata st+            then map (view wlFullImage)+            else detailedImagesWithoutMetadata st++      | otherwise = windowLinesToImages st . filter (not . isNoisy)++    isNoisy msg =+      case view wlBody msg of+        IrcBody irc -> squelchIrcMsg irc+        _           -> False++detailedImagesWithoutMetadata :: ClientState -> [WindowLine] -> [Image]+detailedImagesWithoutMetadata st wwls =+  case gatherMetadataLines st wwls of+    ([], [])   -> []+    ([], w:ws) -> view wlFullImage w : detailedImagesWithoutMetadata st ws+    (_:_, wls) -> detailedImagesWithoutMetadata st wls++windowLinesToImages :: ClientState -> [WindowLine] -> [Image]+windowLinesToImages st wwls =+  case gatherMetadataLines st wwls of+    ([], [])   -> []+    ([], w:ws) -> view wlImage w : windowLinesToImages st ws+    ((img,who,mbnext):mds, wls)++      | view clientShowMetadata st ->+         startMetadata img mbnext who mds palette+       : windowLinesToImages st wls++      | otherwise -> windowLinesToImages st wls+  where+    palette = clientPalette st++------------------------------------------------------------------------++type MetadataState =+  Identifier                            {- ^ current nick -} ->+  [(Image,Identifier,Maybe Identifier)] {- ^ metadata     -} ->+  Palette                               {- ^ palette      -} ->+  Image++startMetadata ::+  Image            {- ^ metadata image           -} ->+  Maybe Identifier {- ^ possible nick transition -} ->+  MetadataState+startMetadata img mbnext who mds palette =+        quietIdentifier palette who+    <|> img+    <|> transitionMetadata mbnext who mds palette++transitionMetadata ::+  Maybe Identifier {- ^ possible nick transition -} ->+  MetadataState+transitionMetadata mbwho who mds palette =+  case mbwho of+    Nothing   -> continueMetadata who  mds palette+    Just who' -> quietIdentifier palette who'+             <|> continueMetadata who' mds palette++continueMetadata :: MetadataState+continueMetadata _ [] _ = emptyImage+continueMetadata who1 ((img, who2, mbwho3):mds) palette+  | who1 == who2 = img+               <|> transitionMetadata mbwho3 who2 mds palette+  | otherwise    = char defAttr ' '+               <|> startMetadata img mbwho3 who2 mds palette++------------------------------------------------------------------------++gatherMetadataLines ::+  ClientState ->+  [WindowLine] ->+  ( [(Image, Identifier, Maybe Identifier)] , [ WindowLine ] )+  -- ^ metadata entries are reversed+gatherMetadataLines st = go []+  where+    go acc (w:ws)+      | Just (img,who,mbnext) <- metadataWindowLine st w =+          go ((img,who,mbnext) : acc) ws++    go acc ws = (acc,ws)+++-- | Classify window lines for metadata coalesence+metadataWindowLine ::+  ClientState ->+  WindowLine ->+  Maybe (Image, Identifier, Maybe Identifier)+        {- ^ Image, incoming identifier, outgoing identifier if changed -}+metadataWindowLine st wl =+  case view wlBody wl of+    IrcBody irc+      | Just who <- ircIgnorable irc st -> Just (ignoreImage, who, Nothing)+      | otherwise                       -> metadataImg irc+    _                                   -> Nothing+
+ src/Client/View/Palette.hs view
@@ -0,0 +1,61 @@+{-|+Module      : Client.View.Palette+Description : View current palette and to see all terminal colors+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++Lines for the @/palette@ command. This view shows all the colors of+the current palette as well as the colors available in the terminal.++-}++module Client.View.Palette+  ( paletteViewLines+  ) where++import           Client.Image.Palette+import           Control.Lens+import           Data.List+import           Graphics.Vty.Image++digits :: String+digits = "0123456789ABCDEF"++digitImage :: Char -> Image+digitImage d = string defAttr [' ',d,' ']++columns :: [Image] -> Image+columns = horizCat . intersperse (char defAttr ' ')++-- | Generate lines used for @/palette@. These lines show+-- all the colors used in the current palette as well as+-- the colors available for use in palettes.+paletteViewLines :: Palette -> [Image]+paletteViewLines pal =+  [ columns+  $ digitImage digit+  : [ string (withBackColor defAttr c) "   "+    | col <- [0 .. 15]+    , let c = Color240 (row * 16 + col)+    ]+  | (digit,row) <- reverse $ take 15 $ zip (drop 1 digits) [0 ..]+  ] +++  [ columns+  $ digitImage '0'+  : [ string (withBackColor defAttr (ISOColor c)) "   "+    | c <- [0..15]+    ]++  , columns (map digitImage (' ':digits))+  , emptyImage+  , columns+    [ text' (view l pal) name+    | (name, Lens l) <- paletteMap+    ]+  , emptyImage+  , columns+    [ string attr "nicks"+    | attr <- toListOf (palNicks . folded) pal+    ]+  ]
+ src/Client/View/UserList.hs view
@@ -0,0 +1,127 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.View.UserList+Description : Line renderers for channel user list view+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module renders the lines used in the channel user list.+-}+module Client.View.UserList+  ( userListImages+  , userInfoImages+  ) where++import           Client.Image.Message+import           Client.Image.Palette+import           Client.State+import           Client.State.Channel+import           Client.State.Network+import           Control.Lens+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Map.Strict as Map+import           Data.List+import           Data.Ord+import           Data.Text (Text)+import qualified Data.Text as Text+import           Graphics.Vty.Image+import           Irc.Identifier+import           Irc.UserInfo++-- | Render the lines used by the @/users@ command in normal mode.+-- These lines show the count of users having each channel mode+-- in addition to the nicknames of the users.+userListImages ::+  Text        {- ^ network -} ->+  Identifier  {- ^ channel -} ->+  ClientState                 ->+  [Image]+userListImages network channel st =+  case preview (clientConnection network) st of+    Just cs -> userListImages' cs channel st+    Nothing -> [text' (view palError pal) "No connection"]+  where+    pal = clientPalette st++userListImages' :: NetworkState -> Identifier -> ClientState -> [Image]+userListImages' cs channel st =+    [countImage, horizCat (intersperse gap (map renderUser usersList))]+  where+    countImage = text' (view palLabel pal) "Users:" <|>+                 sigilCountImage++    matcher = clientMatcher st++    myNicks = clientHighlights cs st++    renderUser (ident, sigils) =+      string (view palSigil pal) sigils <|>+      coloredIdentifier pal NormalIdentifier myNicks ident++    gap = char defAttr ' '++    matcher' (ident,sigils) = matcher (Text.pack sigils `Text.append` idText ident)++    usersList = sortBy (comparing fst)+              $ filter matcher'+              $ HashMap.toList usersHashMap++    sigilCounts = Map.fromListWith (+)+                    [ (take 1 sigil, 1::Int) | (_,sigil) <- usersList ]++    sigilCountImage = horizCat+      [ string (view palSigil pal) (' ':sigil) <|>+        string defAttr (show n)+      | (sigil,n) <- Map.toList sigilCounts+      ]++    pal = clientPalette st++    usersHashMap =+      view (csChannels . ix channel . chanUsers) cs++-- | Render lines for the @/users@ command in detailed view.+-- Each user will be rendered on a separate line with username+-- and host visible when known.+userInfoImages ::+  Text        {- ^ network -} ->+  Identifier  {- ^ channel -} ->+  ClientState                 ->+  [Image]+userInfoImages network channel st =+  case preview (clientConnection network) st of+    Just cs -> userInfoImages' cs channel st+    Nothing -> [text' (view palError pal) "No connection"]+  where+    pal = clientPalette st++userInfoImages' :: NetworkState -> Identifier -> ClientState -> [Image]+userInfoImages' cs channel st = renderEntry <$> usersList+  where+    matcher = clientMatcher st++    myNicks = clientHighlights cs st++    pal = clientPalette st++    renderEntry (info, sigils) =+      string (view palSigil pal) sigils <|>+      coloredUserInfo pal DetailedRender myNicks info++    matcher' (info,sigils) =+      matcher (Text.pack sigils `Text.append` renderUserInfo info)++    userInfos = view csUsers cs++    toInfo nick =+      case view (at nick) userInfos of+        Just (UserAndHost n h) -> UserInfo nick n h+        Nothing                -> UserInfo nick "" ""++    usersList = sortBy (flip (comparing (userNick . fst)))+              $ filter matcher'+              $ map (over _1 toInfo)+              $ HashMap.toList usersHashMap++    usersHashMap = view (csChannels . ix channel . chanUsers) cs
+ src/Client/View/Windows.hs view
@@ -0,0 +1,72 @@+{-|+Module      : Client.View.Windows+Description : View of the list of open windows+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++This module implements the rendering of the client window list.++-}+module Client.View.Windows+  ( windowsImages+  ) where++import           Client.Image.Palette+import           Client.State+import           Client.State.Focus+import           Client.State.Window+import           Control.Lens+import           Data.List+import qualified Data.Map as Map+import           Graphics.Vty.Image+import           Irc.Identifier++-- | Draw the image lines associated with the @/windows@ command.+windowsImages :: ClientState -> [Image]+windowsImages st+  = reverse+  $ createColumns+  $ zipWith (renderWindowColumns pal) names windows+  where+    windows = views clientWindows Map.toAscList st++    pal     = clientPalette st+    names   = clientWindowNames st ++ repeat '?'++renderWindowColumns :: Palette -> Char -> (Focus, Window) -> [Image]+renderWindowColumns pal name (focus, win) =+  [ char (view palWindowName pal) name+  , renderedFocus pal focus+  , renderedWindowInfo pal win+  ]++createColumns :: [[Image]] -> [Image]+createColumns xs = map makeRow xs+  where+    columnWidths = maximum . map imageWidth <$> transpose xs+    makeRow = horizCat+            . intersperse (char defAttr ' ')+            . zipWith resizeWidth columnWidths++renderedFocus :: Palette -> Focus -> Image+renderedFocus pal focus =+  case focus of+    Unfocused ->+      char (view palError pal) '*'+    NetworkFocus network ->+      text' (view palLabel pal) network+    ChannelFocus network channel ->+      text' (view palLabel pal) network <|>+      char defAttr ':' <|>+      text' (view palLabel pal) (idText channel)++renderedWindowInfo :: Palette -> Window -> Image+renderedWindowInfo pal win =+  string (view newMsgAttrLens pal) (views winUnread show win) <|>+  char defAttr '/' <|>+  string (view palActivity pal) (views winTotal show win)+  where+    newMsgAttrLens+      | view winMention win = palMention+      | otherwise           = palActivity