packages feed

glirc 2.20.6 → 2.21

raw patch · 36 files changed

+1698/−892 lines, 36 filesdep ~config-schemadep ~config-valuedep ~semigroupoidsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: config-schema, config-value, semigroupoids

API changes (from Hackage documentation)

- Client.Image: scrollAmount :: ClientState -> Int
- Client.Image.PackedImage: _Image' :: Iso' Image' Image
- Client.Image.PackedImage: instance (Client.Image.PackedImage.GMirror f1 g1, Client.Image.PackedImage.GMirror f2 g2) => Client.Image.PackedImage.GMirror (f1 GHC.Generics.:*: f2) (g1 GHC.Generics.:*: g2)
- Client.Image.PackedImage: instance (Client.Image.PackedImage.GMirror f1 g1, Client.Image.PackedImage.GMirror f2 g2) => Client.Image.PackedImage.GMirror (f1 GHC.Generics.:+: f2) (g1 GHC.Generics.:+: g2)
- Client.Image.PackedImage: instance Client.Image.PackedImage.GMirror GHC.Generics.U1 GHC.Generics.U1
- Client.Image.PackedImage: instance Client.Image.PackedImage.GMirror f g => Client.Image.PackedImage.GMirror (GHC.Generics.M1 i c f) (GHC.Generics.M1 j d g)
- Client.Image.PackedImage: instance Client.Image.PackedImage.Mirror Client.Image.PackedImage.Image' Graphics.Vty.Image.Internal.Image
- Client.Image.PackedImage: instance Client.Image.PackedImage.Mirror Data.Text.Internal.Lazy.Text Data.Text.Internal.Text
- Client.Image.PackedImage: instance Client.Image.PackedImage.Mirror Data.Text.Internal.Text Data.Text.Internal.Lazy.Text
- Client.Image.PackedImage: instance Client.Image.PackedImage.Mirror GHC.Types.Int GHC.Types.Int
- Client.Image.PackedImage: instance Client.Image.PackedImage.Mirror Graphics.Vty.Attributes.Attr Graphics.Vty.Attributes.Attr
- Client.Image.PackedImage: instance Client.Image.PackedImage.Mirror Graphics.Vty.Image.Internal.Image Client.Image.PackedImage.Image'
- Client.Image.PackedImage: instance Client.Image.PackedImage.Mirror a b => Client.Image.PackedImage.GMirror (GHC.Generics.K1 i a) (GHC.Generics.K1 j b)
- Client.Image.PackedImage: instance GHC.Generics.Generic Client.Image.PackedImage.Image'
- Client.Image.Utils: lineWrap :: Int -> Maybe Int -> Image -> Image
- Client.State: [_clientShowMetadata] :: ClientState -> !Bool
- Client.State: clientShowMetadata :: Lens' ClientState Bool
- Client.State.Window: [_wlFullImage'] :: WindowLine -> !Image'
- Client.State.Window: [_wlImage'] :: WindowLine -> !Image'
+ Client.Commands.Recognizer: instance Data.Semigroup.Semigroup (Client.Commands.Recognizer.Recognizer a)
+ Client.Configuration: OneColumn :: LayoutMode
+ Client.Configuration: TwoColumn :: LayoutMode
+ Client.Configuration: [_configHideMeta] :: Configuration -> Bool
+ Client.Configuration: [_configJumpModifier] :: Configuration -> [Modifier]
+ Client.Configuration: [_configKeyMap] :: Configuration -> KeyMap
+ Client.Configuration: [_configLayout] :: Configuration -> LayoutMode
+ Client.Configuration: configHideMeta :: Lens' Configuration Bool
+ Client.Configuration: configJumpModifier :: Lens' Configuration [Modifier]
+ Client.Configuration: configKeyMap :: Lens' Configuration KeyMap
+ Client.Configuration: configLayout :: Lens' Configuration LayoutMode
+ Client.Configuration: data LayoutMode
+ Client.Configuration: getNewConfigPath :: IO FilePath
+ Client.Configuration: instance GHC.Show.Show Client.Configuration.LayoutMode
+ Client.Configuration.Macros: macroCommandSpec :: ValueSpecs [ExpansionChunk]
+ Client.Configuration.Macros: macroMapSpec :: ValueSpecs (Recognizer Macro)
+ Client.Configuration.ServerSettings: identifierSpec :: ValueSpecs Identifier
+ Client.Configuration.ServerSettings: serverSpec :: ValueSpecs (ServerSettings -> ServerSettings)
+ Client.EventLoop.Actions: ActAdvanceFocus :: Action
+ Client.EventLoop.Actions: ActAdvanceNetwork :: Action
+ Client.EventLoop.Actions: ActBackWord :: Action
+ Client.EventLoop.Actions: ActBackspace :: Action
+ Client.EventLoop.Actions: ActBold :: Action
+ Client.EventLoop.Actions: ActClearFormat :: Action
+ Client.EventLoop.Actions: ActColor :: Action
+ Client.EventLoop.Actions: ActCommand :: Text -> Action
+ Client.EventLoop.Actions: ActDelete :: Action
+ Client.EventLoop.Actions: ActDigraph :: Action
+ Client.EventLoop.Actions: ActEnd :: Action
+ Client.EventLoop.Actions: ActEnter :: Action
+ Client.EventLoop.Actions: ActForwardWord :: Action
+ Client.EventLoop.Actions: ActHome :: Action
+ Client.EventLoop.Actions: ActIgnored :: Action
+ Client.EventLoop.Actions: ActInsert :: Char -> Action
+ Client.EventLoop.Actions: ActInsertEnter :: Action
+ Client.EventLoop.Actions: ActItalic :: Action
+ Client.EventLoop.Actions: ActJump :: Int -> Action
+ Client.EventLoop.Actions: ActJumpPrevious :: Action
+ Client.EventLoop.Actions: ActJumpToActivity :: Action
+ Client.EventLoop.Actions: ActKillEnd :: Action
+ Client.EventLoop.Actions: ActKillHome :: Action
+ Client.EventLoop.Actions: ActKillWordBack :: Action
+ Client.EventLoop.Actions: ActKillWordForward :: Action
+ Client.EventLoop.Actions: ActLeft :: Action
+ Client.EventLoop.Actions: ActNewerLine :: Action
+ Client.EventLoop.Actions: ActOlderLine :: Action
+ Client.EventLoop.Actions: ActRefresh :: Action
+ Client.EventLoop.Actions: ActReset :: Action
+ Client.EventLoop.Actions: ActRetreatFocus :: Action
+ Client.EventLoop.Actions: ActReverseVideo :: Action
+ Client.EventLoop.Actions: ActRight :: Action
+ Client.EventLoop.Actions: ActScrollDown :: Action
+ Client.EventLoop.Actions: ActScrollUp :: Action
+ Client.EventLoop.Actions: ActTabComplete :: Action
+ Client.EventLoop.Actions: ActTabCompleteBack :: Action
+ Client.EventLoop.Actions: ActToggle :: Action
+ Client.EventLoop.Actions: ActUnderline :: Action
+ Client.EventLoop.Actions: ActYank :: Action
+ Client.EventLoop.Actions: actionName :: Action -> Text
+ Client.EventLoop.Actions: addKeyBinding :: [Modifier] -> Key -> Action -> KeyMap -> KeyMap
+ Client.EventLoop.Actions: data Action
+ Client.EventLoop.Actions: data KeyMap
+ Client.EventLoop.Actions: initialKeyMap :: KeyMap
+ Client.EventLoop.Actions: instance Config.Schema.Spec.Spec Client.EventLoop.Actions.Action
+ Client.EventLoop.Actions: instance GHC.Classes.Eq Client.EventLoop.Actions.Action
+ Client.EventLoop.Actions: instance GHC.Classes.Ord Client.EventLoop.Actions.Action
+ Client.EventLoop.Actions: instance GHC.Read.Read Client.EventLoop.Actions.Action
+ Client.EventLoop.Actions: instance GHC.Show.Show Client.EventLoop.Actions.Action
+ Client.EventLoop.Actions: instance GHC.Show.Show Client.EventLoop.Actions.KeyMap
+ Client.EventLoop.Actions: keyMapEntries :: KeyMap -> [([Modifier], Key, Action)]
+ Client.EventLoop.Actions: keyToAction :: KeyMap -> [Modifier] -> Text -> [Modifier] -> Key -> Action
+ Client.EventLoop.Actions: parseKey :: String -> Maybe ([Modifier], Key)
+ Client.EventLoop.Actions: prettyModifierKey :: [Modifier] -> Key -> String
+ Client.EventLoop.Actions: removeKeyBinding :: [Modifier] -> Key -> KeyMap -> KeyMap
+ Client.Image.Layout: drawLayout :: ClientState -> (Int, Int, Int, Image)
+ Client.Image.Layout: scrollAmount :: ClientState -> Int
+ Client.Image.LineWrap: lineWrap :: Int -> Image -> Image
+ Client.Image.LineWrap: lineWrapChat :: Int -> Maybe Int -> Image' -> [Image']
+ Client.Image.PackedImage: char :: Attr -> Char -> Image'
+ Client.Image.PackedImage: imageText :: Image' -> Text
+ Client.Image.PackedImage: imageWidth :: Image' -> Int
+ Client.Image.PackedImage: instance Data.Semigroup.Semigroup Client.Image.PackedImage.Image'
+ Client.Image.PackedImage: instance Data.String.IsString Client.Image.PackedImage.Image'
+ Client.Image.PackedImage: instance GHC.Base.Monoid Client.Image.PackedImage.Image'
+ Client.Image.PackedImage: splitImage :: Int -> Image' -> (Image', Image')
+ Client.Image.PackedImage: string :: Attr -> String -> Image'
+ Client.Image.PackedImage: text' :: Attr -> Text -> Image'
+ Client.Image.PackedImage: unpackImage :: Image' -> Image
+ Client.State: [_clientErrorMsg] :: ClientState -> Maybe Text
+ Client.State: [_clientLayout] :: ClientState -> !LayoutMode
+ Client.State: clientErrorMsg :: Lens' ClientState (Maybe Text)
+ Client.State: clientLayout :: Lens' ClientState LayoutMode
+ Client.State: clientToggleHideMeta :: ClientState -> ClientState
+ Client.State.Focus: FocusKeyMap :: Subfocus
+ Client.State.Window: [_winHideMeta] :: Window -> !Bool
+ Client.State.Window: [_wlFullImage] :: WindowLine -> !Image'
+ Client.State.Window: [_wlImage] :: WindowLine -> !Image'
+ Client.State.Window: winHideMeta :: Lens' Window Bool
+ Client.View.KeyMap: keyMapLines :: ClientState -> [Image]
- Client.Configuration: Configuration :: ServerSettings -> (HashMap Text ServerSettings) -> Palette -> Text -> HashSet Identifier -> Maybe Integer -> Maybe Int -> Maybe FilePath -> Recognizer Macro -> [FilePath] -> Maybe FilePath -> HashSet Identifier -> Bool -> Bool -> Configuration
+ Client.Configuration: Configuration :: ServerSettings -> (HashMap Text ServerSettings) -> Palette -> Text -> HashSet Identifier -> Maybe Integer -> Maybe Int -> Maybe FilePath -> Recognizer Macro -> [FilePath] -> Maybe FilePath -> HashSet Identifier -> Bool -> Bool -> Bool -> KeyMap -> LayoutMode -> [Modifier] -> Configuration
- Client.Configuration: ConfigurationMalformed :: String -> ConfigurationFailure
+ Client.Configuration: ConfigurationMalformed :: FilePath -> String -> ConfigurationFailure
- Client.Configuration: ConfigurationParseFailed :: String -> ConfigurationFailure
+ Client.Configuration: ConfigurationParseFailed :: FilePath -> String -> ConfigurationFailure
- Client.Image.Arguments: argumentsImage :: Palette -> ArgumentSpec a -> String -> Image
+ Client.Image.Arguments: argumentsImage :: Palette -> ArgumentSpec a -> String -> Image'
- Client.Image.Message: coloredIdentifier :: Palette -> IdentifierColorMode -> HashSet Identifier -> Identifier -> Image
+ Client.Image.Message: coloredIdentifier :: Palette -> IdentifierColorMode -> HashSet Identifier -> Identifier -> Image'
- Client.Image.Message: coloredUserInfo :: Palette -> RenderMode -> HashSet Identifier -> UserInfo -> Image
+ Client.Image.Message: coloredUserInfo :: Palette -> RenderMode -> HashSet Identifier -> UserInfo -> Image'
- Client.Image.Message: ignoreImage :: Image
+ Client.Image.Message: ignoreImage :: Image'
- Client.Image.Message: metadataImg :: IrcSummary -> Maybe (Image, Identifier, Maybe Identifier)
+ Client.Image.Message: metadataImg :: IrcSummary -> Maybe (Image', Identifier, Maybe Identifier)
- Client.Image.Message: msgImage :: RenderMode -> ZonedTime -> MessageRendererParams -> MessageBody -> Image
+ Client.Image.Message: msgImage :: RenderMode -> ZonedTime -> MessageRendererParams -> MessageBody -> Image'
- Client.Image.Message: quietIdentifier :: Palette -> Identifier -> Image
+ Client.Image.Message: quietIdentifier :: Palette -> Identifier -> Image'
- Client.Image.Message: rightPad :: RenderMode -> Maybe Integer -> Image -> Image
+ Client.Image.Message: rightPad :: RenderMode -> Maybe Integer -> Image' -> Image'
- Client.Image.MircFormatting: controlImage :: Char -> Image
+ Client.Image.MircFormatting: controlImage :: Char -> Image'
- Client.Image.MircFormatting: parseIrcText :: Text -> Image
+ Client.Image.MircFormatting: parseIrcText :: Text -> Image'
- Client.Image.MircFormatting: parseIrcTextExplicit :: Text -> Image
+ Client.Image.MircFormatting: parseIrcTextExplicit :: Text -> Image'
- Client.Image.MircFormatting: plainText :: String -> Image
+ Client.Image.MircFormatting: plainText :: String -> Image'
- Client.Image.StatusLine: minorStatusLineImage :: Focus -> ClientState -> Image
+ Client.Image.StatusLine: minorStatusLineImage :: Focus -> Int -> Bool -> ClientState -> Image
- Client.Image.StatusLine: statusLineImage :: ClientState -> Image
+ Client.Image.StatusLine: statusLineImage :: Int -> ClientState -> Image
- Client.Image.Textbox: textboxImage :: ClientState -> (Int, Int, Image)
+ Client.Image.Textbox: textboxImage :: Int -> ClientState -> (Int, Int, Image)
- Client.State: ClientState :: !(Map Focus Window) -> !Focus -> !Focus -> !Focus -> !Subfocus -> ![Focus] -> !(IntMap NetworkState) -> !NetworkId -> !(TQueue NetworkEvent) -> !(HashMap Text NetworkId) -> !Configuration -> !EditBox -> !Int -> !Int -> !Int -> !Int -> !Bool -> !Bool -> !Bool -> Maybe Regex -> !Bool -> !(HashSet Identifier) -> !ExtensionState -> ![LogLine] -> ClientState
+ Client.State: ClientState :: !(Map Focus Window) -> !Focus -> !Focus -> !Focus -> !Subfocus -> ![Focus] -> !(IntMap NetworkState) -> !NetworkId -> !(TQueue NetworkEvent) -> !(HashMap Text NetworkId) -> !Configuration -> !EditBox -> !Int -> !Int -> !Int -> !Int -> !Bool -> !Bool -> Maybe Regex -> !LayoutMode -> !Bool -> !(HashSet Identifier) -> !ExtensionState -> ![LogLine] -> Maybe Text -> ClientState
- Client.State.EditBox: class HasLine c_atSp where pos = (.) line pos text = (.) line text
+ Client.State.EditBox: class HasLine c_au6a where pos = (.) line pos text = (.) line text
- Client.State.EditBox: line :: HasLine c_atSp => Lens' c_atSp Line
+ Client.State.EditBox: line :: HasLine c_au6a => Lens' c_au6a Line
- Client.State.EditBox: pos :: HasLine c_atSp => Lens' c_atSp Int
+ Client.State.EditBox: pos :: HasLine c_au6a => Lens' c_au6a Int
- Client.State.EditBox: text :: HasLine c_atSp => Lens' c_atSp String
+ Client.State.EditBox: text :: HasLine c_au6a => Lens' c_au6a String
- Client.State.EditBox.Content: class HasLine c_atSp where pos = (.) line pos text = (.) line text
+ Client.State.EditBox.Content: class HasLine c_au6a where pos = (.) line pos text = (.) line text
- Client.State.EditBox.Content: line :: HasLine c_atSp => Lens' c_atSp Line
+ Client.State.EditBox.Content: line :: HasLine c_au6a => Lens' c_au6a Line
- Client.State.EditBox.Content: pos :: HasLine c_atSp => Lens' c_atSp Int
+ Client.State.EditBox.Content: pos :: HasLine c_au6a => Lens' c_au6a Int
- Client.State.EditBox.Content: text :: HasLine c_atSp => Lens' c_atSp String
+ Client.State.EditBox.Content: text :: HasLine c_au6a => Lens' c_au6a String
- Client.State.Window: Window :: !WindowLines -> !(Maybe Int) -> !Int -> !Int -> !WindowLineImportance -> Window
+ Client.State.Window: Window :: !WindowLines -> !(Maybe Int) -> !Int -> !Int -> !WindowLineImportance -> !Bool -> Window
- Client.State.Window: wlFullImage :: Lens' WindowLine Image
+ Client.State.Window: wlFullImage :: Lens' WindowLine Image'
- Client.State.Window: wlImage :: Lens' WindowLine Image
+ Client.State.Window: wlImage :: Lens' WindowLine Image'
- Client.View: viewLines :: Focus -> Subfocus -> ClientState -> [Image]
+ Client.View: viewLines :: Focus -> Subfocus -> Int -> ClientState -> [Image]
- Client.View.Digraphs: digraphLines :: ClientState -> [Image]
+ Client.View.Digraphs: digraphLines :: Int -> ClientState -> [Image]
- Client.View.MaskList: maskListImages :: Char -> Text -> Identifier -> ClientState -> [Image]
+ Client.View.MaskList: maskListImages :: Char -> Text -> Identifier -> Int -> ClientState -> [Image]
- Client.View.Mentions: mentionsViewLines :: ClientState -> [Image]
+ Client.View.Mentions: mentionsViewLines :: Int -> ClientState -> [Image]
- Client.View.Messages: chatMessageImages :: Focus -> ClientState -> [Image]
+ Client.View.Messages: chatMessageImages :: Focus -> Int -> ClientState -> [Image]

Files

ChangeLog.md view
@@ -1,5 +1,14 @@ # Revision history for glirc2 +## 2.21++* Make metadata toggle (F4) a window-level setting instead of client level+* Add configuration option to hide metadata by default `hide-metadata`+* Make keymap configurable under `key-bindings`, add `/keymap` command+* Add transient error message view, press ESC to clear+* Implement two-column split window mode: `/toggle-layout` and F5+* Implement word-boundary based line wrapping+ ## 2.20.6  * Switch to new `config-schema` package for configuration file loading.
exec/Main.hs view
@@ -54,10 +54,10 @@        Right cfg -> return cfg        Left (ConfigurationReadFailed e) ->          report "Failed to open configuration:" e-       Left (ConfigurationParseFailed e) ->-         report "Failed to parse configuration:" e-       Left (ConfigurationMalformed e) ->-         report "Configuration malformed (try --config-format): " e+       Left (ConfigurationParseFailed path e) ->+         report ("Failed to parse configuration file: " ++ path) e+       Left (ConfigurationMalformed path e) ->+         report ("Malformed configuration file: " ++ path ++ "\n(try --config-format)") e   where     report problem msg =       do hPutStrLn stderr problem
glirc.cabal view
@@ -1,5 +1,5 @@ name:                glirc-version:             2.20.6+version:             2.21 synopsis:            Console IRC client description:         Console IRC client                      .@@ -54,6 +54,7 @@   build-depends:       base, glirc, lens, text, vty  library+  ghc-options: -Wall   hs-source-dirs:      src   include-dirs:        include   includes:            include/glirc-api.h@@ -73,21 +74,24 @@                        Client.Commands.WordCompletion                        Client.Configuration                        Client.Configuration.Colors+                       Client.Configuration.Macros                        Client.Configuration.ServerSettings                        Client.EventLoop+                       Client.EventLoop.Actions                        Client.EventLoop.Errors                        Client.Hook                        Client.Hook.Znc.Buffextras                        Client.Hooks                        Client.Image                        Client.Image.Arguments+                       Client.Image.Layout+                       Client.Image.LineWrap                        Client.Image.Message                        Client.Image.MircFormatting                        Client.Image.PackedImage                        Client.Image.Palette                        Client.Image.StatusLine                        Client.Image.Textbox-                       Client.Image.Utils                        Client.Log                        Client.Message                        Client.Network.Async@@ -104,6 +108,7 @@                        Client.View.ChannelInfo                        Client.View.Digraphs                        Client.View.Help+                       Client.View.KeyMap                        Client.View.MaskList                        Client.View.Mentions                        Client.View.Messages@@ -123,8 +128,8 @@                        attoparsec           >=0.13   && <0.14,                        bytestring           >=0.10.8 && <0.11,                        base64-bytestring    >=1.0.0.1 && <1.1,-                       config-value         >=0.5    && <0.6,-                       config-schema        >=0.1    && <0.2,+                       config-value         >=0.6    && <0.7,+                       config-schema        >=0.3    && <0.4,                        containers           >=0.5.7  && <0.6,                        directory            >=1.2.6  && <1.4,                        filepath             >=1.4.1  && <1.5,@@ -137,7 +142,7 @@                        network              >=2.6.2  && <2.7,                        process              >=1.4.2  && <1.7,                        regex-tdfa           >=1.2    && <1.3,-                       semigroupoids        >=5.2    && <5.3,+                       semigroupoids        >=5.1    && <5.3,                        socks                >=0.5.5  && <0.6,                        split                >=0.2    && <0.3,                        stm                  >=2.4    && <2.5,
src/Client/Commands.hs view
@@ -132,8 +132,7 @@ -- | Command failure with an error message printed to client window commandFailureMsg :: Text -> ClientState -> IO CommandResult commandFailureMsg e st =-  do now <- getZonedTime-     return $! CommandFailure $! recordError now st e+  return $! CommandFailure $! set clientErrorMsg (Just e) st  -- | Interpret the given chat message or command. Leading @/@ indicates a -- command. Otherwise if a channel or user query is focused a chat message@@ -156,9 +155,9 @@   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+        Nothing   -> commandFailureMsg "macro expansions failed" st         Just cmds -> process cmds st-    _ ->  executeCommand Nothing command st+    _ -> executeCommand Nothing command st   where     resolveMacro = resolveMacroExpansions (commandExpansion discoTime st) expandInt @@ -219,7 +218,7 @@              sendMsg cs ircMsg              commandSuccess $! recordChannelMessage network channel entry st -    _ -> commandFailureMsg "This command requires an active channel" st+    _ -> commandFailureMsg "cannot send chat messages to this window" st   -- | Parse and execute the given command. When the first argument is Nothing@@ -239,7 +238,7 @@           Just isReversed -> tab isReversed st rest           Nothing ->             case parseArguments spec rest of-              Nothing -> commandFailure st+              Nothing -> commandFailureMsg "bad command arguments" st               Just arg -> exec st arg   in   case recognize cmdTxt commands of@@ -253,24 +252,24 @@           | Just network <- views clientFocus focusNetwork st           , Just cs      <- preview (clientConnection network) st ->               finish argSpec (exec cs) (\x -> tab x cs)-          | otherwise -> commandFailureMsg "This command requires an active network" st+          | otherwise -> commandFailureMsg "command requires focused network" st          ChannelCommand exec tab           | ChannelFocus network channelId <- view clientFocus st           , Just cs <- preview (clientConnection network) st           , isChannelIdentifier cs channelId ->               finish argSpec (exec channelId cs) (\x -> tab x channelId cs)-          | otherwise -> commandFailureMsg "This command requires an active channel" st+          | otherwise -> commandFailureMsg "command requires focused channel" st          ChatCommand exec tab           | ChannelFocus network channelId <- view clientFocus st           , Just cs <- preview (clientConnection network) st ->               finish argSpec (exec channelId cs) (\x -> tab x channelId cs)-          | otherwise -> commandFailureMsg "This command requires an active chat window" st+          | otherwise -> commandFailureMsg "command requires focused chat window" st      _ -> case tabCompleteReversed of            Just isReversed -> nickTabCompletion isReversed st-           Nothing         -> commandFailureMsg "Unknown command" st+           Nothing         -> commandFailureMsg "unknown command" st   -- | Expands each alias to have its own copy of the command callbacks@@ -331,6 +330,14 @@     $ ClientCommand cmdDigraphs noClientTab    , Command+      (pure "keymap")+      NoArg+      "Show the key binding map.\n\+      \\n\+      \Key bindings can be changed in configuration file. See `glirc2 --config-format`.\n"+    $ ClientCommand cmdKeyMap noClientTab++  , Command       (pure "exec")       (RemainingArg "arguments")       "Execute a command synchnonously sending the to a configuration destination.\n\@@ -372,6 +379,34 @@     $ ClientCommand cmdHelp tabHelp    ------------------------------------------------------------------------+  ] , CommandSection "View toggles"+  ------------------------------------------------------------------------++  [ Command+      (pure "toggle-detail")+      NoArg+      "Toggle detailed message view.\n"+    $ ClientCommand cmdToggleDetail noClientTab++  , Command+      (pure "toggle-activity-bar")+      NoArg+      "Toggle detailed detailed activity information in status bar.\n"+    $ ClientCommand cmdToggleActivityBar noClientTab++  , Command+      (pure "toggle-metadata")+      NoArg+      "Toggle visibility of metadata in chat windows.\n"+    $ ClientCommand cmdToggleMetadata noClientTab++  , Command+      (pure "toggle-layout")+      NoArg+      "Toggle multi-window layout mode.\n"+    $ ClientCommand cmdToggleLayout noClientTab++  ------------------------------------------------------------------------   ] , CommandSection "Connection commands"   ------------------------------------------------------------------------ @@ -464,37 +499,45 @@       \\^Bfocuses\^B: space delimited list of focus names.\n\       \\n\       \Client:  *\n\-      \Network: \^BNETWORK\^B\n\+      \Network: \^BNETWORK\^B:\n\       \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\       \User:    \^BNETWORK\^B:\^BNICK\^B\n\       \\n\+      \If the network part is omitted, the current network will be used\n\+      \\n\       \Not providing an argument unsplits the current windows.\n"     $ ClientCommand cmdSplits tabSplits    , Command       (pure "splits+")       (RemainingArg "focuses")-      "Add windows to the splits list.\n\+      "Add windows to the splits list. Omit the list of focuses to add the\+      \ current window.\n\       \\n\       \\^Bfocuses\^B: space delimited list of focus names.\n\       \\n\       \Client:  *\n\       \Network: \^BNETWORK\^B\n\       \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\-      \User:    \^BNETWORK\^B:\^BNICK\^B\n"+      \User:    \^BNETWORK\^B:\^BNICK\^B\n\+      \\n\+      \If the network part is omitted, the current network will be used\n"     $ ClientCommand cmdSplitsAdd tabSplits    , Command       (pure "splits-")       (RemainingArg "focuses")-      "Remove windows from the splits list.\n\+      "Remove windows from the splits list. Omit the list of focuses to\+      \ remove the current window.\n\       \\n\       \\^Bfocuses\^B: space delimited list of focus names.\n\       \\n\       \Client:  *\n\       \Network: \^BNETWORK\^B\n\       \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\-      \User:    \^BNETWORK\^B:\^BNICK\^B\n"+      \User:    \^BNETWORK\^B:\^BNICK\^B\n\+      \\n\+      \If the network part is omitted, the current network will be used\n"     $ ClientCommand cmdSplitsDel tabActiveSplits    , Command@@ -818,6 +861,22 @@ cmdExit :: ClientCommand () cmdExit st _ = return (CommandQuit st) ++cmdToggleDetail :: ClientCommand ()+cmdToggleDetail st _ = commandSuccess (over clientDetailView not st)++cmdToggleActivityBar :: ClientCommand ()+cmdToggleActivityBar st _ = commandSuccess (over clientActivityBar not st)++cmdToggleMetadata :: ClientCommand ()+cmdToggleMetadata st _ = commandSuccess (clientToggleHideMeta st)++cmdToggleLayout :: ClientCommand ()+cmdToggleLayout st _ = commandSuccess (set clientScroll 0 (over clientLayout aux st))+  where+    aux OneColumn = TwoColumn+    aux TwoColumn = OneColumn+ -- | When used on a channel that the user is currently -- joined to this command will clear the messages but -- preserve the window. When used on a window that the@@ -869,7 +928,7 @@ cmdQuote :: NetworkCommand String cmdQuote cs st rest =   case parseRawIrcMsg (Text.pack rest) of-    Nothing  -> commandFailureMsg "Failed to parse IRC command" st+    Nothing  -> commandFailureMsg "failed to parse raw IRC command" st     Just raw ->       do sendMsg cs raw          commandSuccess st@@ -905,7 +964,7 @@ -- | Implementation of @/notice@ cmdNotice :: NetworkCommand (String, String) cmdNotice cs st (target, rest)-  | null rest = commandFailure st+  | null rest = commandFailureMsg "empty message" st   | otherwise =       do let restTxt = Text.pack rest              tgtTxt = Text.pack target@@ -918,7 +977,7 @@ -- | Implementation of @/msg@ cmdMsg :: NetworkCommand (String, String) cmdMsg cs st (target, rest)-  | null rest = commandFailure st+  | null rest = commandFailureMsg "empty message" st   | otherwise =       do let restTxt = Text.pack rest              tgtTxt = Text.pack target@@ -1020,6 +1079,10 @@ cmdDigraphs :: ClientCommand () cmdDigraphs st _ = commandSuccess (changeSubfocus FocusDigraphs st) +-- | Implementation of @/keymap@ command. Set subfocus to Keymap.+cmdKeyMap :: ClientCommand ()+cmdKeyMap st _ = commandSuccess (changeSubfocus FocusKeyMap st)+ -- | Implementation of @/help@ command. Set subfocus to Help. cmdHelp :: ClientCommand (Maybe (String, ())) cmdHelp st mb = commandSuccess (changeSubfocus focus st)@@ -1031,19 +1094,26 @@ -- all of the currently available windows. tabSplits :: Bool -> ClientCommand String tabSplits isReversed st rest++  -- If no arguments, populate the current splits   | all (' '==) rest =-     do let cmd = unwords ("/splits" : map (Text.unpack . renderFocus) currentExtras)-            newline = Edit.endLine cmd-        commandSuccess (set (clientTextBox . Edit.line) newline st)+     let cmd = unwords $ "/splits"+                       : map (Text.unpack . renderSplitFocus) currentExtras +         currentExtras = view clientExtraFocus st+         newline = Edit.endLine cmd+     in commandSuccess (set (clientTextBox . Edit.line) newline st)++  -- Tab complete the available windows. Accepts either fully qualified+  -- window names or current network names without the ':'   | otherwise =-        simpleTabCompletion plainWordCompleteMode [] completions isReversed st-  where-    currentExtras = view clientExtraFocus st+     let completions = currentNet <> allWindows+         allWindows  = renderSplitFocus <$> views clientWindows Map.keys st+         currentNet  = case views clientFocus focusNetwork st of+                         Just net -> idText <$> channelWindowsOnNetwork net st+                         Nothing  -> []+     in simpleTabCompletion plainWordCompleteMode [] completions isReversed st -    completions = map renderFocus-                $ Map.keys-                $ view clientWindows st  -- | Tab completion for @/splits-@. This completes only from the list of active -- entries in the splits list.@@ -1051,48 +1121,79 @@ tabActiveSplits isReversed st _ =   simpleTabCompletion plainWordCompleteMode [] completions isReversed st   where-    completions = renderFocus <$> view clientExtraFocus st+    completions = currentNetSplits <> currentSplits+    currentSplits = renderSplitFocus <$> view clientExtraFocus st+    currentNetSplits =+      [ idText chan+        | ChannelFocus net chan <- view clientExtraFocus st+        , views clientFocus focusNetwork st == Just net+        ]  --- | Parses a list of entries in the format used by @/splits[+-]@ to specify windows.-parseFocuses :: String -> [Focus]-parseFocuses = map parseFocus . words+withSplitFocuses ::+  ClientState                   ->+  String                        ->+  ([Focus] -> IO CommandResult) ->+  IO CommandResult+withSplitFocuses st str k =+  case mb of+    Nothing   -> commandFailureMsg "unable to parse arguments" st+    Just args -> k args+  where+    mb = traverse+           (parseSplitFocus (views clientFocus focusNetwork st))+           (words str) --- | Parses a single entry in the format used by @/splits[+-]@ to specify windows.-parseFocus :: String -> Focus-parseFocus x =+-- | Parses a single entry in the format used by @/splits[+-]@ to specify+-- windows.+parseSplitFocus :: Maybe Text -> String -> Maybe Focus+parseSplitFocus mbNet x =   case break (==':') x of-    ("*","")     -> Unfocused-    (net,"")     -> NetworkFocus (Text.pack net)-    (net,_:chan) -> ChannelFocus (Text.pack net) (mkId (Text.pack chan))+    ("*","")     -> Just Unfocused+    (net,_:"")   -> Just (NetworkFocus (Text.pack net))+    (net,_:chan) -> Just (ChannelFocus (Text.pack net) (mkId (Text.pack chan)))+    (chan,"") -> do net <- mbNet+                    Just (ChannelFocus net (mkId (Text.pack chan)))  -- | Render a entry from splits back to the textual format.-renderFocus :: Focus -> Text-renderFocus Unfocused          = "*"-renderFocus (NetworkFocus x)   = x-renderFocus (ChannelFocus x y) = x <> ":" <> idText y+renderSplitFocus :: Focus -> Text+renderSplitFocus Unfocused          = "*"+renderSplitFocus (NetworkFocus x)   = x <> ":"+renderSplitFocus (ChannelFocus x y) = x <> ":" <> idText y   -- | Implementation of @/splits@ cmdSplits :: ClientCommand String-cmdSplits st str = commandSuccess (setExtraFocus extras st)-  where-    extras = nub (parseFocuses str)+cmdSplits st str =+  withSplitFocuses st str $ \args ->+    commandSuccess (setExtraFocus (nub args) st)  --- | Implementation of @/splits+@+-- | Implementation of @/splits+@. When no focuses are provided+-- the current focus is used instead. cmdSplitsAdd :: ClientCommand String-cmdSplitsAdd st str = commandSuccess (setExtraFocus extras st)-  where-    extras = nub (parseFocuses str ++ view clientExtraFocus st)+cmdSplitsAdd st str =+  withSplitFocuses st str $ \args ->+    let args'+          | null args = st ^.. clientFocus+          | otherwise = args+        extras = nub (args' ++ view clientExtraFocus st) --- | Implementation of @/splits-@+    in commandSuccess (setExtraFocus extras st)++-- | Implementation of @/splits-@. When no focuses are provided+-- the current focus is used instead. cmdSplitsDel :: ClientCommand String-cmdSplitsDel st str = commandSuccess (setExtraFocus extras st)-  where-    extras = view clientExtraFocus st \\ parseFocuses str+cmdSplitsDel st str =+  withSplitFocuses st str $ \args ->+    let args'+          | null args = st ^.. clientFocus+          | otherwise = args+        extras = view clientExtraFocus st \\ args' +    in commandSuccess (setExtraFocus extras st) + tabHelp :: Bool -> ClientCommand String tabHelp isReversed st _ =   simpleTabCompletion plainWordCompleteMode [] commandNames isReversed st@@ -1217,7 +1318,7 @@                    { localTimeOfDay = tod                    , localDay       = day } } -    _ -> commandFailureMsg "Unable to parse date/time arguments" st+    _ -> commandFailureMsg "unable to parse date/time arguments" st    where     -- %k doesn't require a leading 0 for times before 10AM@@ -1302,7 +1403,7 @@             commandSuccess (changeSubfocus (FocusMasks mode) st) -    _ -> commandFailureMsg "Unknown mask mode" st+    _ -> commandFailureMsg "unknown mask mode" st  cmdKick :: ChannelCommand (String, String) cmdKick channelId cs st (who,reason) =@@ -1397,12 +1498,12 @@          commandSuccess            $ changeFocus (NetworkFocus network) st' -  | otherwise = commandFailureMsg "/reconnect requires focused network" st+  | otherwise = commandFailureMsg "command requires focused network" st  cmdIgnore :: ClientCommand String cmdIgnore st rest =   case mkId . Text.pack <$> words rest of-    [] -> commandFailure st+    [] -> commandFailureMsg "bad arguments" st     xs -> commandSuccess             $ over clientIgnores updateIgnores st       where@@ -1428,9 +1529,9 @@     describeProblem err =       Text.pack $       case err of-       ConfigurationReadFailed e  -> "Failed to open configuration:" ++ e-       ConfigurationParseFailed e -> "Failed to parse configuration:" ++ e-       ConfigurationMalformed e   -> "Configuration malformed: " ++ e+       ConfigurationReadFailed    e -> "Failed to open configuration: "  ++ e+       ConfigurationParseFailed _ e -> "Failed to parse configuration: " ++ e+       ConfigurationMalformed   _ e -> "Configuration malformed: "       ++ e  -- | Support file name tab completion when providing an alternative -- configuration file.@@ -1456,7 +1557,7 @@         [] -> success False [[]]         flags:params ->           case splitModes (view csModeTypes cs) flags params of-            Nothing -> commandFailureMsg "Failed to parse modes" st+            Nothing -> commandFailureMsg "failed to parse modes" st             Just parsedModes ->               success needOp (unsplitModes <$> chunksOf (view csModeCount cs) parsedModes')               where@@ -1589,7 +1690,7 @@ cmdExtension st (name,params) =   case find (\ae -> aeName ae == Text.pack name)             (view (clientExtensions . esActive) st) of-        Nothing -> commandFailureMsg "Unknown extension" st+        Nothing -> commandFailureMsg "unknown extension" st         Just ae ->           do (st',_) <- clientPark st $ \ptr ->                           commandExtension ptr (Text.pack <$> words params) ae@@ -1676,14 +1777,14 @@ cmdUrl :: ClientCommand (Maybe (String, ())) cmdUrl st mbArg =   case view (clientConfig . configUrlOpener) st of-    Nothing -> commandFailureMsg "/url requires url-opener to be configured" st+    Nothing -> commandFailureMsg "url-opener not configured" st     Just opener ->       case mbArg of         Nothing -> doUrlOpen opener 0         Just (arg,_) ->           case readMaybe arg of             Just n | n > 0 -> doUrlOpen opener (n-1)-            _ -> commandFailureMsg "/url expected positive integer argument" st+            _ -> commandFailureMsg "bad url number" st   where     focus = view clientFocus st @@ -1693,7 +1794,7 @@     doUrlOpen opener n =       case preview (ix n) urls of         Just url -> openUrl opener (Text.unpack url) st-        Nothing  -> commandFailureMsg "/url couldn't find requested URL" st+        Nothing  -> commandFailureMsg "bad url number" st  openUrl :: FilePath -> String -> ClientState -> IO CommandResult openUrl opener url st =
src/Client/Commands/Recognizer.hs view
@@ -25,7 +25,7 @@ import Control.Applicative hiding (empty)  import           Data.HashMap.Strict (lookup,insertWith,HashMap,empty,unionWith,fromList,toList)-import           Data.Monoid+import           Data.Semigroup import           Data.Text (Text) import qualified Data.Text as Text import           Data.Maybe@@ -41,6 +41,9 @@ instance Monoid (Recognizer a) where   mempty = Branch "" Nothing empty   mappend = both++instance Semigroup (Recognizer a) where+  (<>) = both  -- | Possible results of recognizing text. data Recognition a
src/Client/Configuration.hs view
@@ -19,6 +19,7 @@   -- * Configuration type     Configuration(..)   , ConfigurationFailure(..)+  , LayoutMode(..)    -- * Lenses   , configDefaults@@ -35,12 +36,17 @@   , configIgnores   , configActivityBar   , configBellOnMention+  , configHideMeta+  , configKeyMap+  , configLayout+  , configJumpModifier    -- * Loading configuration   , loadConfiguration    -- * Resolving paths   , resolveConfigurationPath+  , getNewConfigPath    -- * Specification   , configurationSpec@@ -48,30 +54,30 @@  import           Client.Commands.Interpolation import           Client.Commands.Recognizer-import           Client.Commands.WordCompletion import           Client.Configuration.Colors+import           Client.Configuration.Macros (macroMapSpec) import           Client.Configuration.ServerSettings+import           Client.EventLoop.Actions import           Client.Image.Palette import           Config import           Config.Schema-import           Control.Applicative import           Control.Exception import           Control.Lens                        hiding (List)-import           Data.Foldable                       (find, foldl')+import           Data.Foldable                       (toList, find) import           Data.Functor.Alt                    ((<!>)) import           Data.HashMap.Strict                 (HashMap) import qualified Data.HashMap.Strict                 as HashMap import           Data.HashSet                        (HashSet) import qualified Data.HashSet                        as HashSet-import           Data.List.NonEmpty                  (NonEmpty) import qualified Data.List.NonEmpty                  as NonEmpty import           Data.Maybe-import           Data.Monoid                         ((<>))+import           Data.Monoid                         (Endo(..), (<>)) import           Data.Text                           (Text) import qualified Data.Text                           as Text import qualified Data.Text.IO                        as Text import qualified Data.Vector                         as Vector-import           Irc.Identifier                      (Identifier, mkId)+import           Graphics.Vty.Input.Events (Modifier(..), Key(..))+import           Irc.Identifier                      (Identifier) import           System.Directory import           System.FilePath import           System.IO.Error@@ -95,9 +101,20 @@   , _configIgnores         :: HashSet Identifier -- ^ initial ignore list   , _configActivityBar     :: Bool -- ^ initially visibility of the activity bar   , _configBellOnMention   :: Bool -- ^ notify terminal on mention+  , _configHideMeta        :: Bool -- ^ default setting for hidemeta on new windows+  , _configKeyMap          :: KeyMap -- ^ keyboard bindings+  , _configLayout          :: LayoutMode -- ^ Default layout on startup+  , _configJumpModifier    :: [Modifier] -- ^ Modifier used for jumping windows   }   deriving Show +data LayoutMode+  -- | Vertically stack all windows in a single column+  = OneColumn+  -- | Vertically stack extra windows in a second column+  | TwoColumn+  deriving Show+ makeLenses ''Configuration  -- | Failure cases when loading a configuration file.@@ -107,10 +124,10 @@   = ConfigurationReadFailed String    -- | Error message from parser or lexer-  | ConfigurationParseFailed String+  | ConfigurationParseFailed FilePath String    -- | Error message from loading parsed configuration-  | ConfigurationMalformed String+  | ConfigurationMalformed FilePath String   deriving Show  -- | default instance@@ -141,14 +158,14 @@ -- exception is throw. readFileCatchNotFound ::   FilePath {- ^ file to read -} ->-  (IOError -> IO Text) {- ^ error handler for not found case -} ->-  IO Text+  (IOError -> IO (FilePath, Text)) {- ^ error handler for not found case -} ->+  IO (FilePath, Text) readFileCatchNotFound path onNotFound =   do res <- try (Text.readFile path)      case res of        Left e | isDoesNotExistError e -> onNotFound e               | otherwise -> throwIO (ConfigurationReadFailed (show e))-       Right txt -> return txt+       Right txt -> return (path, txt)  -- | Either read a configuration file from one of the default -- locations, in which case no configuration found is equivalent@@ -156,7 +173,7 @@ -- no configuration found is an error. readConfigurationFile ::   Maybe FilePath {- ^ just file or use default search paths -} ->-  IO Text+  IO (FilePath, Text) readConfigurationFile mbPath =   case mbPath of @@ -169,7 +186,7 @@          readFileCatchNotFound newPath $ \_ ->            do oldPath <- getOldConfigPath               readFileCatchNotFound oldPath $ \_ ->-                return emptyConfigFile+                return ("", emptyConfigFile)   -- | Load the configuration file defaulting to @~/.glirc/config@.@@ -177,69 +194,131 @@   Maybe FilePath {- ^ path to configuration file -} ->   IO (Either ConfigurationFailure Configuration) loadConfiguration mbPath = try $-  do file <- readConfigurationFile mbPath+  do (path,txt) <- readConfigurationFile mbPath      def  <- loadDefaultServerSettings       rawcfg <--       case parse file of-         Left parseError -> throwIO (ConfigurationParseFailed parseError)+       case parse txt of+         Left e -> throwIO (ConfigurationParseFailed path (displayException e))          Right rawcfg -> return rawcfg       case loadValue configurationSpec rawcfg of        Left es -> throwIO-                $ ConfigurationMalformed+                $ ConfigurationMalformed path                 $ Text.unpack-                $ Text.unlines $ map explainLoadError es+                $ Text.unlines+                $ map explainLoadError (toList es)        Right cfg -> return (cfg mbPath def)   explainLoadError :: LoadError -> Text-explainLoadError (LoadError path problem) =-  Text.intercalate "." path <> ": " <>-  case problem of-    UnusedSections xs -> "Unknown sections: " <> Text.intercalate ", " xs-    MissingSection s  -> "Missing required section: " <> s-    SpecMismatch   s  -> "Expected " <> s+explainLoadError (LoadError pos path problem) =+  Text.concat [ positionText, " at ", pathText, ": ", problemText] +  where+    positionText =+     Text.unwords ["line"  , Text.pack (show (posLine   pos)),+                   "column", Text.pack (show (posColumn pos))] -configurationSpec :: ValueSpecs (Maybe FilePath -> ServerSettings -> Configuration)-configurationSpec = sectionsSpec "" $+    pathText+      | null path = "top-level"+      | otherwise = Text.intercalate ":" path -  do ssDefUpdate <- fromMaybe id <$> optSection' "defaults" "" serverSpec-     ssUpdates   <- fromMaybe [] <$> optSection' "servers" "" (listSpec serverSpec)+    problemText =+      case problem of+        UnusedSection  s -> "unknown section `"          <> s <> "`"+        MissingSection s -> "missing required section `" <> s <> "`"+        SpecMismatch   t -> "expected "                  <> t -     _configPalette <- fromMaybe defaultPalette-                    <$> optSection' "palette" "" paletteSpec -     _configWindowNames <- fromMaybe defaultWindowNames-                    <$> optSection "window-names" ""+configurationSpec :: ValueSpecs (Maybe FilePath -> ServerSettings -> Configuration)+configurationSpec = sectionsSpec "" $ -     _configMacros <- fromMaybe mempty-                    <$> optSection' "macros" "" macroMapSpec+  do let sec' def name spec info = fromMaybe def <$> optSection' name spec info+         identifierSetSpec       = HashSet.fromList <$> listSpec identifierSpec -     _configExtensions <- fromMaybe [] <$> optSection' "extensions" "" (listSpec stringSpec)+     ssDefUpdate            <- sec' id "defaults" serverSpec+                               "Default values for use across all server configurations"+     ssUpdates              <- sec' [] "servers" (listSpec serverSpec)+                               "Configuration parameters for IRC servers"+     _configPalette         <- sec' defaultPalette "palette" paletteSpec+                               "Customize the client color choices"+     _configWindowNames     <- sec' defaultWindowNames "window-names" valuesSpec+                               "Window names to use for quick jumping with jump-modifier key"+     _configJumpModifier    <- sec' [MMeta] "jump-modifier" modifierSpec+                               "Modifier used to jump to a window by name. Defaults to `meta`."+     _configMacros          <- sec' mempty "macros" macroMapSpec+                               "Programmable macro commands"+     _configExtensions      <- sec' [] "extensions" (listSpec stringSpec)+                               "Filenames of extension libraries to load at startup"+     _configUrlOpener       <- optSection' "url-opener" stringSpec+                               "External command used by /url command"+     _configExtraHighlights <- sec' mempty "extra-highlights" identifierSetSpec+                               "Extra words to highlight in chat messages"+     _configNickPadding     <- optSection' "nick-padding" nonnegativeSpec+                               "Amount of space to reserve for nicknames in chat messages"+     _configIndentWrapped   <- optSection' "indent-wrapped-lines" nonnegativeSpec+                               "Amount of indentation for wrapped message lines"+     _configIgnores         <- sec' mempty "ignores" identifierSetSpec+                               "Set of nicknames to ignore on startup"+     _configActivityBar     <- sec' False  "activity-bar" yesOrNoSpec+                               "Show channel names and message counts for activity on\+                               \ unfocused channels."+     _configBellOnMention   <- sec' False  "bell-on-mention" yesOrNoSpec+                               "Emit bell character to terminal on mention"+     _configHideMeta        <- sec' False  "hide-metadata" yesOrNoSpec+                               "Initial setting for hiding metadata on new windows"+     bindings               <- sec' [] "key-bindings" (listSpec keyBindingSpec)+                               "Extra key bindings"+     _configLayout          <- sec' OneColumn "layout" layoutSpec+                               "Initial setting for window layout"+     return (\_configConfigPath def ->+             let _configDefaults = ssDefUpdate def+                 _configServers  = buildServerMap _configDefaults ssUpdates+                 _configKeyMap   = foldl (\acc f -> f acc) initialKeyMap bindings+             in Configuration{..}) -     _configUrlOpener <- optSection' "url-opener" "" stringSpec+modifierSpec :: ValueSpecs [Modifier]+modifierSpec = toList <$> oneOrNonemptySpec modifier1Spec+  where+    modifier1Spec = namedSpec "modifier"+                  $ MMeta <$ atomSpec "meta"+                <!> MAlt  <$ atomSpec "alt"+                <!> MCtrl <$ atomSpec "ctrl" -     _configExtraHighlights <- maybe HashSet.empty (HashSet.fromList . map mkId)-                    <$> optSection "extra-highlights" ""+layoutSpec :: ValueSpecs LayoutMode+layoutSpec = OneColumn <$ atomSpec "one-column"+         <!> TwoColumn <$ atomSpec "two-column" -     _configNickPadding <- optSection' "nick-padding" "" nonnegativeSpec+keyBindingSpec :: ValueSpecs (KeyMap -> KeyMap)+keyBindingSpec = actBindingSpec <!> cmdBindingSpec <!> unbindingSpec -     _configIndentWrapped <- optSection' "indent-wrapped-lines" "" nonnegativeSpec+actBindingSpec :: ValueSpecs (KeyMap -> KeyMap)+actBindingSpec = sectionsSpec "action-binding" $+  do (m,k) <- reqSection' "bind" keySpec+              "Key to be bound (e.g. a, C-b, M-c C-M-d)"+     a     <- reqSection "action"+              "Action name (see `/keymap`)"+     return (addKeyBinding m k a) -     _configIgnores <- maybe HashSet.empty (HashSet.fromList . map mkId)-                    <$> optSection "ignores" ""+cmdBindingSpec :: ValueSpecs (KeyMap -> KeyMap)+cmdBindingSpec = sectionsSpec "command-binding" $+  do (m,k) <- reqSection' "bind" keySpec+              "Key to be bound (e.g. a, C-b, M-c C-M-d)"+     cmd   <- reqSection "command"+              "Client command to execute (exclude leading `/`)"+     return (addKeyBinding m k (ActCommand cmd)) -     _configActivityBar <- fromMaybe False-                    <$> optSection' "activity-bar" "" yesOrNoSpec+unbindingSpec :: ValueSpecs (KeyMap -> KeyMap)+unbindingSpec = sectionsSpec "remove-binding" $+  do (m,k) <- reqSection' "unbind" keySpec+              "Key to be unbound (e.g. a, C-b, M-c C-M-d)"+     return (removeKeyBinding m k) -     _configBellOnMention <- fromMaybe False <$> optSection' "bell-on-mention" "" yesOrNoSpec -     return (\_configConfigPath def ->-             let _configDefaults = ssDefUpdate def-                 _configServers  = buildServerMap _configDefaults ssUpdates-             in Configuration{..})+-- | Custom configuration specification for emacs-style key descriptions+keySpec :: ValueSpecs ([Modifier], Key)+keySpec = customSpec "emacs-key" stringSpec parseKey   nonnegativeSpec :: (Ord a, Num a) => ValueSpecs a@@ -248,18 +327,17 @@  paletteSpec :: ValueSpecs Palette paletteSpec = sectionsSpec "palette" $-  do updates <- catMaybes <$> sequenceA-       [ fmap (set l) <$> optSection' lbl "" attrSpec | (lbl, Lens l) <- paletteMap ]-     nickColors <- optSection' "nick-colors" "" (nonemptyList attrSpec)-     return (let pal1 = foldl' (\acc f -> f acc) defaultPalette updates-             in case nickColors of-                  Nothing -> pal1-                  Just xs -> set palNicks (Vector.fromList (NonEmpty.toList xs)) pal1)+  (ala Endo (foldMap . foldMap) ?? defaultPalette) <$> sequenceA fields -nonemptyList :: ValueSpecs a -> ValueSpecs (NonEmpty a)-nonemptyList s = customSpec "non-empty" (listSpec s) NonEmpty.nonEmpty+  where+    nickColorsSpec = set palNicks . Vector.fromList . NonEmpty.toList <$> nonemptySpec attrSpec +    fields :: [SectionSpecs (Maybe (Palette -> Palette))]+    fields = optSection' "nick-colors" nickColorsSpec+             "Colors used to highlight nicknames"+           : [ optSection' lbl (set l <$> attrSpec) "" | (lbl, Lens l) <- paletteMap ] + buildServerMap :: ServerSettings -> [ServerSettings -> ServerSettings] -> HashMap Text ServerSettings buildServerMap def ups =   HashMap.fromList [ (serverSettingName ss, ss) | up <- ups, let ss = up def ]@@ -268,88 +346,7 @@       fromMaybe (views ssHostName Text.pack ss)                 (view ssName ss) -serverSpec :: ValueSpecs (ServerSettings -> ServerSettings)-serverSpec = sectionsSpec "server-settings" $-  do updates <- catMaybes <$> sequenceA settings-     return (foldr (.) id updates)-  where-    req l s = set l <$> s -    opt l s = set l . Just <$> s-          <!> set l Nothing <$ atomSpec "clear"--    settings =-      [ optSection' "name" "The name used to identify this server in the client"-      $ opt ssName valuesSpec-      , optSection' "hostname" "Hostname of server"-      $ req ssHostName stringSpec-      , optSection' "port" "Port number of server. Default 6667 without TLS or 6697 with TLS"-      $ opt ssPort numSpec-      , optSection' "nick" "Nicknames to connect with in order"-      $ req ssNicks nicksSpec-      , optSection' "password" "Server password"-      $ opt ssPassword valuesSpec-      , optSection' "username" "Second component of _!_@_ usermask"-      $ req ssUser valuesSpec-      , optSection' "realname" "\"GECOS\" name sent to server visible in /whois"-      $ req ssReal valuesSpec-      , optSection' "userinfo" "CTCP userinfo (currently unused)"-      $ req ssUserInfo valuesSpec-      , optSection' "sasl-username" "Username for SASL authentication to NickServ"-      $ opt ssSaslUsername valuesSpec-      , optSection' "sasl-password" "Password for SASL authentication to NickServ"-      $ opt ssSaslPassword valuesSpec-      , optSection' "sasl-ecdsa-key" "Path to ECDSA key for non-password SASL authentication"-      $ opt ssSaslEcdsaFile stringSpec-      , optSection' "tls" "Set to `yes` to enable secure connect. Set to `yes-insecure` to disable certificate checking."-      $ req ssTls useTlsSpec-      , optSection' "tls-client-cert" "Path to TLS client certificate"-      $ opt ssTlsClientCert stringSpec-      , optSection' "tls-client-key" "Path to TLS client key"-      $ opt ssTlsClientKey stringSpec-      , optSection' "tls-server-cert" "Path to CA certificate bundle"-      $ opt ssTlsServerCert stringSpec-      , optSection' "tls-ciphers" "OpenSSL cipher specification. Default to \"HIGH\""-      $ req ssTlsCiphers stringSpec-      , optSection' "socks-host" "Hostname of SOCKS5 proxy server"-      $ opt ssSocksHost stringSpec-      , optSection' "socks-port" "Port number of SOCKS5 proxy server"-      $ req ssSocksPort numSpec-      , optSection' "connect-cmds" "Command to be run upon successful connection to server"-      $ req ssConnectCmds $ listSpec macroCommandSpec-      , optSection' "chanserv-channels" "Channels with ChanServ permissions available"-      $ req ssChanservChannels  $ listSpec identifierSpec-      , optSection' "flood-penalty" "RFC 1459 rate limiting, seconds of penalty per message (default 2)"-      $ req ssFloodPenalty valuesSpec-      , optSection' "flood-threshold" "RFC 1459 rate limiting, seconds of allowed penalty accumulation (default 10)"-      $ req ssFloodThreshold valuesSpec-      , optSection' "message-hooks" "Special message hooks to enable: \"buffextras\" available"-      $ req ssMessageHooks valuesSpec-      , optSection' "reconnect-attempts" "Number of reconnection attempts on lost connection"-      $ req ssReconnectAttempts valuesSpec-      , optSection' "autoconnect" "Set to `yes` to automatically connect at client startup"-      $ req ssAutoconnect yesOrNoSpec-      , optSection' "nick-completion" "Behavior for nickname completion with TAB"-      $ req ssNickCompletion nickCompletionSpec-      , optSection' "log-dir" "Path to log file directory for this server"-      $ opt ssLogDir stringSpec-      ]---nicksSpec :: ValueSpecs (NonEmpty Text)-nicksSpec = pure <$> valuesSpec-        <!> nonemptyList valuesSpec---useTlsSpec :: ValueSpecs UseTls-useTlsSpec =-      UseTls         <$ atomSpec "yes"-  <!> UseInsecureTls <$ atomSpec "yes-insecure"-  <!> UseInsecure    <$ atomSpec "no"--identifierSpec :: ValueSpecs Identifier-identifierSpec = mkId <$> valuesSpec- -- | Resolve relative paths starting at the home directory rather than -- the current directory of the client. resolveConfigurationPath :: FilePath -> IO FilePath@@ -357,25 +354,3 @@   | isAbsolute path = return path   | otherwise = do home <- getHomeDirectory                    return (home </> path)--macroMapSpec :: ValueSpecs (Recognizer Macro)-macroMapSpec = fromCommands <$> listSpec macroValueSpecs--macroValueSpecs :: ValueSpecs (Text, Macro)-macroValueSpecs = sectionsSpec "macro" $-  do name     <- reqSection "name" ""-     spec     <- fromMaybe noMacroArguments-             <$> optSection' "arguments" "" macroArgumentsSpec-     commands <- reqSection' "commands" "" (listSpec macroCommandSpec)-     return (name, Macro spec commands)--macroArgumentsSpec :: ValueSpecs MacroSpec-macroArgumentsSpec = customSpec "macro arguments" valuesSpec parseMacroSpecs--macroCommandSpec :: ValueSpecs [ExpansionChunk]-macroCommandSpec = customSpec "macro command" valuesSpec parseExpansion--nickCompletionSpec :: ValueSpecs WordCompletionMode-nickCompletionSpec =-      defaultNickWordCompleteMode <$ atomSpec "default"-  <!> slackNickWordCompleteMode   <$ atomSpec "slack"
src/Client/Configuration/Colors.hs view
@@ -17,7 +17,6 @@   ) where  import           Config.Schema-import           Control.Applicative import           Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import           Data.Functor.Alt ((<!>))@@ -34,9 +33,9 @@  fullAttrSpec :: ValueSpecs Attr fullAttrSpec = sectionsSpec "full-attr" $-  do mbFg <- optSection' "fg"    "Foreground color" colorSpec-     mbBg <- optSection' "bg"    "Background color" colorSpec-     mbSt <- optSection' "style" "Terminal font style" stylesSpec+  do mbFg <- optSection' "fg"    colorSpec "Foreground color"+     mbBg <- optSection' "bg"    colorSpec "Background color"+     mbSt <- optSection' "style" stylesSpec "Terminal font style"      return ( aux withForeColor mbFg             $ aux withBackColor mbBg             $ aux (foldl withStyle) mbSt
+ src/Client/Configuration/Macros.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE ApplicativeDo, OverloadedStrings #-}++{-|+Module      : Client.Configuration.Macros+Description : Configuration schema for macros+Copyright   : (c) Eric Mertens, 2017+License     : ISC+Maintainer  : emertens@gmail.com++-}++module Client.Configuration.Macros+  ( macroMapSpec+  , macroCommandSpec+  ) where++import           Config.Schema.Spec+import           Client.Commands.Interpolation+import           Client.Commands.Recognizer+import           Data.Maybe (fromMaybe)+import           Data.Text (Text)++macroMapSpec :: ValueSpecs (Recognizer Macro)+macroMapSpec = fromCommands <$> listSpec macroValueSpecs++macroValueSpecs :: ValueSpecs (Text, Macro)+macroValueSpecs = sectionsSpec "macro" $+  do name     <- reqSection "name" ""+     spec     <- fromMaybe noMacroArguments+             <$> optSection' "arguments" macroArgumentsSpec ""+     commands <- reqSection' "commands" (listSpec macroCommandSpec) ""+     return (name, Macro spec commands)++macroArgumentsSpec :: ValueSpecs MacroSpec+macroArgumentsSpec = customSpec "macro arguments" valuesSpec parseMacroSpecs++macroCommandSpec :: ValueSpecs [ExpansionChunk]+macroCommandSpec = customSpec "macro command" valuesSpec parseExpansion
src/Client/Configuration/ServerSettings.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ApplicativeDo, TemplateHaskell, OverloadedStrings #-}  {-| Module      : Client.Configuration.ServerSettings@@ -16,6 +16,8 @@   (   -- * Server settings type     ServerSettings(..)+  , serverSpec+  , identifierSpec    -- * Lenses   , ssNicks@@ -56,13 +58,16 @@  import           Client.Commands.Interpolation import           Client.Commands.WordCompletion+import           Client.Configuration.Macros (macroCommandSpec)+import           Config.Schema.Spec import           Control.Lens+import           Data.Functor.Alt                    ((<!>)) import           Data.List.NonEmpty (NonEmpty)-import qualified Data.List.NonEmpty as NonEmpty import           Data.Maybe (fromMaybe)+import           Data.Monoid import           Data.Text (Text) import qualified Data.Text as Text-import           Irc.Identifier (Identifier)+import           Irc.Identifier (Identifier, mkId) import           Network.Socket (HostName, PortNumber) import           System.Environment @@ -98,6 +103,7 @@   }   deriving Show +-- | Security setting for network connection data UseTls   = UseTls         -- ^ TLS connection   | UseInsecureTls -- ^ TLS connection without certificate checking@@ -109,13 +115,13 @@ -- | Load the defaults for server settings based on the environment -- variables. ----- @USER@, @IRCPASSSWORD@, and @SASLPASSWORD@ are used.+-- Environment variables @USER@, @IRCPASSSWORD@, and @SASLPASSWORD@ are used. loadDefaultServerSettings :: IO ServerSettings loadDefaultServerSettings =   do env  <- getEnvironment      let username = Text.pack (fromMaybe "guest" (lookup "USER" env))      return ServerSettings-       { _ssNicks         = username NonEmpty.:| []+       { _ssNicks         = pure username        , _ssUser          = username        , _ssReal          = username        , _ssUserInfo      = username@@ -143,3 +149,121 @@        , _ssNickCompletion   = defaultNickWordCompleteMode        , _ssLogDir           = Nothing        }++serverSpec :: ValueSpecs (ServerSettings -> ServerSettings)+serverSpec = sectionsSpec "server-settings" $+  composeMaybe <$> sequenceA settings+  where+    composeMaybe :: [Maybe (a -> a)] -> a -> a+    composeMaybe = ala Endo (foldMap . foldMap)++    req name l s info = optSection' name ?? info+                      $ set l <$> s++    opt name l s info = optSection' name ?? info+                      $ set l . Just <$> s+                    <!> set l Nothing <$ atomSpec "clear"++    settings =+      [ opt "name" ssName valuesSpec+        "The name used to identify this server in the client"++      , req "hostname" ssHostName stringSpec+        "Hostname of server"++      , opt "port" ssPort numSpec+        "Port number of server. Default 6667 without TLS or 6697 with TLS"++      , req "nick" ssNicks nicksSpec+        "Nicknames to connect with in order"++      , opt "password" ssPassword valuesSpec+        "Server password"++      , req "username" ssUser valuesSpec+        "Second component of _!_@_ usermask"++      , req "realname" ssReal valuesSpec+        "\"GECOS\" name sent to server visible in /whois"++      , req "userinfo" ssUserInfo valuesSpec+        "CTCP userinfo (currently unused)"++      , opt "sasl-username" ssSaslUsername valuesSpec+        "Username for SASL authentication to NickServ"++      , opt "sasl-password" ssSaslPassword valuesSpec+        "Password for SASL authentication to NickServ"++      , opt "sasl-ecdsa-key" ssSaslEcdsaFile stringSpec+        "Path to ECDSA key for non-password SASL authentication"++      , req "tls" ssTls useTlsSpec+        "Set to `yes` to enable secure connect. Set to `yes-insecure` to disable certificate checking."++      , opt "tls-client-cert" ssTlsClientCert stringSpec+        "Path to TLS client certificate"++      , opt "tls-client-key" ssTlsClientKey stringSpec+        "Path to TLS client key"++      , opt "tls-server-cert" ssTlsServerCert stringSpec+        "Path to CA certificate bundle"++      , req "tls-ciphers" ssTlsCiphers stringSpec+        "OpenSSL cipher specification. Default to \"HIGH\""++      , opt "socks-host" ssSocksHost stringSpec+        "Hostname of SOCKS5 proxy server"++      , req "socks-port" ssSocksPort numSpec+        "Port number of SOCKS5 proxy server"++      , req "connect-cmds" ssConnectCmds (listSpec macroCommandSpec)+        "Command to be run upon successful connection to server"++      , req "chanserv-channels" ssChanservChannels (listSpec identifierSpec)+        "Channels with ChanServ permissions available"++      , req "flood-penalty" ssFloodPenalty valuesSpec+        "RFC 1459 rate limiting, seconds of penalty per message (default 2)"++      , req "flood-threshold" ssFloodThreshold valuesSpec+        "RFC 1459 rate limiting, seconds of allowed penalty accumulation (default 10)"++      , req "message-hooks" ssMessageHooks valuesSpec+        "Special message hooks to enable: \"buffextras\" available"++      , req "reconnect-attempts" ssReconnectAttempts valuesSpec+        "Number of reconnection attempts on lost connection"++      , req "autoconnect" ssAutoconnect yesOrNoSpec+        "Set to `yes` to automatically connect at client startup"++      , req "nick-completion" ssNickCompletion nickCompletionSpec+        "Behavior for nickname completion with TAB"++      , opt "log-dir" ssLogDir stringSpec+        "Path to log file directory for this server"+      ]+++nicksSpec :: ValueSpecs (NonEmpty Text)+nicksSpec = oneOrNonemptySpec valuesSpec+++useTlsSpec :: ValueSpecs UseTls+useTlsSpec =+      UseTls         <$ atomSpec "yes"+  <!> UseInsecureTls <$ atomSpec "yes-insecure"+  <!> UseInsecure    <$ atomSpec "no"+++nickCompletionSpec :: ValueSpecs WordCompletionMode+nickCompletionSpec =+      defaultNickWordCompleteMode <$ atomSpec "default"+  <!> slackNickWordCompleteMode   <$ atomSpec "slack"+++identifierSpec :: ValueSpecs Identifier+identifierSpec = mkId <$> valuesSpec
src/Client/EventLoop.hs view
@@ -20,11 +20,14 @@ import           Client.CApi import           Client.Commands import           Client.Commands.Interpolation+import           Client.Configuration (configJumpModifier, configKeyMap, configWindowNames) import           Client.Configuration.ServerSettings+import           Client.EventLoop.Actions import           Client.EventLoop.Errors (exceptionToLines) import           Client.Hook import           Client.Hooks import           Client.Image+import           Client.Image.Layout (scrollAmount) import           Client.Log import           Client.Message import           Client.Network.Async@@ -392,7 +395,13 @@   IO (Maybe ClientState) {- ^ nothing when finished -} doVtyEvent vty vtyEvent st =   case vtyEvent of-    EvKey k modifier -> doKey vty k modifier st+    EvKey k modifier ->+      let cfg      = view clientConfig st+          keymap   = view configKeyMap       cfg+          winnames = view configWindowNames  cfg+          winmods  = view configJumpModifier cfg+          action = keyToAction keymap winmods winnames modifier k+      in doAction vty action st     -- ignore event parameters due to raw TChan use     EvResize{} -> Just <$> updateTerminalSize vty st     EvPaste utf8 ->@@ -402,16 +411,15 @@   -- | Map keyboard inputs to actions in the client-doKey ::+doAction ::   Vty         {- ^ vty handle     -} ->-  Key         {- ^ key pressed    -} ->-  [Modifier]  {- ^ modifiers held -} ->+  Action      {- ^ action         -} ->   ClientState {- ^ client state   -} ->   IO (Maybe ClientState)-doKey vty key modifier st =+doAction vty action st =    let continue !out -- detect when chains of M-a are broken-        | modifier == [MMeta] && key == KChar 'a' = return (Just out)+        | action == ActJumpToActivity = return (Just out)         | otherwise = return $! Just $! set clientActivityReturn (view clientFocus out) out        changeEditor  f = continue (over clientTextBox f st)@@ -424,75 +432,62 @@           Nothing -> continue $! set clientBell True st           Just st' -> continue st'   in-  case modifier of-    [MCtrl] ->-      case key of-        KChar 'd' -> changeContent Edit.delete-        KChar 'a' -> changeEditor Edit.home-        KChar 'e' -> changeEditor Edit.end-        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')-        KChar ']' -> changeEditor (Edit.insert '\^]')-        KChar '_' -> changeEditor (Edit.insert '\^_')-        KChar 'o' -> changeEditor (Edit.insert '\^O')-        KChar 'v' -> changeEditor (Edit.insert '\^V')-        KChar 'p' -> continue (retreatFocus st)-        KChar 'n' -> continue (advanceFocus st)-        KChar 'x' -> continue (advanceNetworkFocus st)-        KChar 'l' -> do refresh vty-                        continue st-        _         -> continue st+  case action of+    -- movements+    ActHome              -> changeEditor Edit.home+    ActEnd               -> changeEditor Edit.end+    ActLeft              -> changeContent Edit.left+    ActRight             -> changeContent Edit.right+    ActBackWord          -> changeContent Edit.leftWord+    ActForwardWord       -> changeContent Edit.rightWord -    [MMeta] ->-      case key of-        KChar c   | let names = clientWindowNames st-                  , Just i <- elemIndex c names ->-                            continue (jumpFocus i st)-        KEnter    -> changeEditor (Edit.insert '\^J')-        KBS       -> changeEditor (Edit.killWordBackward True)-        KChar 'd' -> changeEditor (Edit.killWordForward True)-        KChar 'b' -> changeContent Edit.leftWord-        KChar 'f' -> changeContent Edit.rightWord-        KLeft     -> changeContent Edit.leftWord-        KRight    -> changeContent Edit.rightWord-        KChar 'a' -> continue (jumpToActivity st)-        KChar 's' -> continue (returnFocus st)-        KChar 'k' -> mbChangeEditor Edit.insertDigraph-        _ -> continue st+    -- edits+    ActKillHome          -> changeEditor Edit.killHome+    ActKillEnd           -> changeEditor Edit.killEnd+    ActKillWordBack      -> changeEditor (Edit.killWordBackward True)+    ActKillWordForward   -> changeEditor (Edit.killWordForward True)+    ActYank              -> changeEditor Edit.yank+    ActToggle            -> changeContent Edit.toggle+    ActDelete            -> changeContent Edit.delete+    ActBackspace         -> changeContent Edit.backspace -    [] -> -- no modifier-      case key of-        KEsc       -> continue (changeSubfocus FocusMessages st)-        KBS        -> changeContent Edit.backspace-        KDel       -> changeContent Edit.delete-        KLeft      -> changeContent Edit.left-        KRight     -> changeContent Edit.right-        KHome      -> changeEditor Edit.home-        KEnd       -> changeEditor Edit.end-        KUp        -> changeEditor $ \ed -> fromMaybe ed $ Edit.earlier ed-        KDown      -> changeEditor $ \ed -> fromMaybe ed $ Edit.later ed-        KPageUp    -> continue (scrollClient ( scrollAmount st) st)-        KPageDown  -> continue (scrollClient (-scrollAmount st) st)+    -- special inserts+    ActBold              -> changeEditor (Edit.insert '\^B')+    ActColor             -> changeEditor (Edit.insert '\^C')+    ActItalic            -> changeEditor (Edit.insert '\^]')+    ActUnderline         -> changeEditor (Edit.insert '\^_')+    ActClearFormat       -> changeEditor (Edit.insert '\^O')+    ActReverseVideo      -> changeEditor (Edit.insert '\^V')+    ActDigraph           -> mbChangeEditor Edit.insertDigraph+    ActInsertEnter       -> changeEditor (Edit.insert '\^J') -        KEnter     -> doCommandResult True  =<< executeInput st-        KBackTab   -> doCommandResult False =<< tabCompletion True  st-        KChar '\t' -> doCommandResult False =<< tabCompletion False st+    -- focus jumps+    ActJump i            -> continue (jumpFocus i st)+    ActJumpToActivity    -> continue (jumpToActivity st)+    ActJumpPrevious      -> continue (returnFocus st)+    ActRetreatFocus      -> continue (retreatFocus st)+    ActAdvanceFocus      -> continue (advanceFocus st)+    ActAdvanceNetwork    -> continue (advanceNetworkFocus st) -        KChar c    -> changeEditor (Edit.insert c)+    ActReset             -> continue (changeSubfocus FocusMessages st)+    ActOlderLine         -> changeEditor $ \ed      -> fromMaybe ed $ Edit.earlier ed+    ActNewerLine         -> changeEditor $ \ed      -> fromMaybe ed $ Edit.later ed+    ActScrollUp          -> continue (scrollClient ( scrollAmount st) st)+    ActScrollDown        -> continue (scrollClient (-scrollAmount st) st) -        -- toggles-        KFun 2     -> continue (over clientDetailView  not st)-        KFun 3     -> continue (over clientActivityBar not st)-        KFun 4     -> continue (over clientShowMetadata not st)+    ActTabCompleteBack   -> doCommandResult False =<< tabCompletion True  st+    ActTabComplete       -> doCommandResult False =<< tabCompletion False st -        _          -> continue st+    ActInsert c          -> changeEditor (Edit.insert c)+    ActEnter             -> doCommandResult True =<< executeInput st+    ActRefresh           -> refresh vty >> continue st+    ActCommand cmd       -> do resp <- executeUserCommand Nothing (Text.unpack cmd) st+                               case resp of+                                 CommandSuccess st1 -> continue st1+                                 CommandFailure st1 -> continue st1+                                 CommandQuit    _   -> return Nothing -    _ -> continue st -- unsupported modifier+    ActIgnored           -> continue st   -- | Process 'CommandResult' and update the 'ClientState' textbox
+ src/Client/EventLoop/Actions.hs view
@@ -0,0 +1,293 @@+{-# Language RankNTypes, OverloadedStrings #-}+{-|+Module      : Client.EventLoop.Actions+Description : Programmable keyboard actions+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++-}++module Client.EventLoop.Actions+  ( Action(..)+  , KeyMap+  , keyToAction+  , initialKeyMap+  , addKeyBinding+  , removeKeyBinding+  , keyMapEntries++  -- * Keys as text+  , parseKey+  , prettyModifierKey+  , actionName+  ) where++import           Graphics.Vty.Input.Events+import           Config.Schema.Spec+import           Control.Applicative+import           Control.Lens+import           Data.Char (showLitChar)+import           Data.Functor.Compose+import           Data.List+import           Data.Map (Map)+import qualified Data.Map as Map+import           Data.HashMap.Lazy (HashMap)+import qualified Data.HashMap.Lazy as HashMap+import           Data.Semigroup ((<>))+import           Data.Text (Text)+import qualified Data.Text as Text+import           Text.Read++-- | Actions that can be invoked using the keyboard.+data Action++  = ActBackspace+  | ActDelete+  | ActLeft+  | ActRight+  | ActHome+  | ActEnd+  | ActOlderLine+  | ActNewerLine+  | ActScrollUp+  | ActScrollDown+  | ActBackWord+  | ActForwardWord++  | ActYank+  | ActKillHome+  | ActKillEnd+  | ActKillWordBack+  | ActKillWordForward+  | ActToggle++  | ActBold+  | ActColor+  | ActItalic+  | ActUnderline+  | ActReverseVideo+  | ActClearFormat+  | ActInsertEnter+  | ActDigraph++  | ActRetreatFocus+  | ActAdvanceFocus+  | ActAdvanceNetwork+  | ActJumpToActivity+  | ActJumpPrevious+  | ActJump Int++  | ActTabComplete+  | ActTabCompleteBack++  | ActEnter+  | ActReset+  | ActRefresh+  | ActCommand Text+  | ActInsert Char+  | ActIgnored+  deriving (Eq, Ord, Read, Show)++-- | Lookup table for keyboard events to actions. Use with+-- keyToAction.+newtype KeyMap = KeyMap (Map [Modifier] (Map Key Action))+  deriving (Show)++keyMapEntries :: KeyMap -> [([Modifier], Key, Action)]+keyMapEntries (KeyMap m) =+  [ (mods, k, a)+     | (mods, m1) <- Map.toList m+     , (k,a) <- Map.toList m1+     ]++instance Spec Action where+  valuesSpec = customSpec "action" anyAtomSpec $ \a ->+                fst <$> HashMap.lookup a actionInfos+++-- | Names and default key bindings for each action.+--+-- Note that Jump, Insert and Ignored are excluded. These will+-- be computed on demand by keyToAction.+actionInfos :: HashMap Text (Action, [([Modifier],Key)])+actionInfos =+  let norm = (,) [     ]+      ctrl = (,) [MCtrl]+      meta = (,) [MMeta] in++  HashMap.fromList++  [("delete"            , (ActDelete           , [meta (KChar 'd'), norm KDel]))+  ,("backspace"         , (ActBackspace        , [norm KBS]))+  ,("home"              , (ActHome             , [norm KHome, ctrl (KChar 'a')]))+  ,("end"               , (ActEnd              , [norm KEnd , ctrl (KChar 'e')]))+  ,("kill-home"         , (ActKillHome         , [ctrl (KChar 'u')]))+  ,("kill-end"          , (ActKillEnd          , [ctrl (KChar 'k')]))+  ,("yank"              , (ActYank             , [ctrl (KChar 'y')]))+  ,("toggle"            , (ActToggle           , [ctrl (KChar 't')]))+  ,("kill-word-left"    , (ActKillWordBack     , [ctrl (KChar 'w'), meta KBS]))+  ,("kill-word-right"   , (ActKillWordForward  , [meta (KChar 'd')]))++  ,("bold"              , (ActBold             , [ctrl (KChar 'b')]))+  ,("color"             , (ActColor            , [ctrl (KChar 'c')]))+  ,("italic"            , (ActItalic           , [ctrl (KChar ']')]))+  ,("underline"         , (ActUnderline        , [ctrl (KChar '_')]))+  ,("clear-format"      , (ActClearFormat      , [ctrl (KChar 'o')]))+  ,("reverse-video"     , (ActReverseVideo     , [ctrl (KChar 'v')]))++  ,("insert-newline"    , (ActInsertEnter      , [meta KEnter]))+  ,("insert-digraph"    , (ActDigraph          , [meta (KChar 'k')]))++  ,("next-window"       , (ActAdvanceFocus     , [ctrl (KChar 'n')]))+  ,("prev-window"       , (ActRetreatFocus     , [ctrl (KChar 'p')]))+  ,("next-network"      , (ActAdvanceNetwork   , [ctrl (KChar 'x')]))+  ,("refresh"           , (ActRefresh          , [ctrl (KChar 'l')]))+  ,("jump-to-activity"  , (ActJumpToActivity   , [meta (KChar 'a')]))+  ,("jump-to-previous"  , (ActJumpPrevious     , [meta (KChar 's')]))++  ,("reset"             , (ActReset            , [norm KEsc]))++  ,("left-word"         , (ActBackWord         , [meta KLeft, meta (KChar 'b')]))+  ,("right-word"        , (ActForwardWord      , [meta KRight, meta (KChar 'f')]))+  ,("left"              , (ActLeft             , [norm KLeft]))+  ,("right"             , (ActRight            , [norm KRight]))+  ,("up"                , (ActOlderLine        , [norm KUp]))+  ,("down"              , (ActNewerLine        , [norm KDown]))+  ,("scroll-up"         , (ActScrollUp         , [norm KPageUp]))+  ,("scroll-down"       , (ActScrollDown       , [norm KPageDown]))+  ,("enter"             , (ActEnter            , [norm KEnter]))+  ,("word-complete-back", (ActTabCompleteBack  , [norm KBackTab]))+  ,("word-complete"     , (ActTabComplete      , [norm (KChar '\t')]))+  ]+++actionNames :: Map Action Text+actionNames = Map.fromList+  [ (action, name) | (name, (action,_)) <- HashMap.toList actionInfos ]+++-- | Render action as human-readable text.+actionName :: Action -> Text+actionName (ActCommand txt) = "command: " <> txt+actionName a = Map.findWithDefault (Text.pack (show a)) a actionNames+++keyMapLens :: [Modifier] -> Key -> Lens' KeyMap (Maybe Action)+keyMapLens mods key f (KeyMap m) =+  KeyMap <$> (at (normalizeModifiers mods) . non' _Empty . at key) f m+++-- | Lookup the action to perform in response to a particular key event.+keyToAction ::+  KeyMap     {- ^ actions         -} ->+  [Modifier] {- ^ jump modifier   -} ->+  Text       {- ^ window names    -} ->+  [Modifier] {- ^ actual modifier -} ->+  Key        {- ^ key             -} ->+  Action     {- ^ action          -}+keyToAction _ jumpMods names mods (KChar c)+  | normalizeModifiers jumpMods == normalizeModifiers mods+  , Just i <- Text.findIndex (c==) names = ActJump i+keyToAction m _ _ modifier key =+  case m ^. keyMapLens modifier key of+    Just a -> a+    Nothing | KChar c <- key, null modifier -> ActInsert c+            | otherwise                     -> ActIgnored+++-- | Bind a keypress event to a new action.+addKeyBinding ::+  [Modifier] {- ^ modifiers -} ->+  Key        {- ^ key       -} ->+  Action     {- ^ action    -} ->+  KeyMap     {- ^ actions   -} ->+  KeyMap+addKeyBinding mods k a = keyMapLens mods k ?~ a++-- | Unbind the action associated with a key.+removeKeyBinding ::+  [Modifier] {- ^ modifiers -} ->+  Key        {- ^ key       -} ->+  KeyMap     {- ^ actions   -} ->+  KeyMap+removeKeyBinding mods k = set (keyMapLens mods k) Nothing+++normalizeModifiers :: [Modifier] -> [Modifier]+normalizeModifiers = nub . sort+++-- | Default key bindings+initialKeyMap :: KeyMap+initialKeyMap = KeyMap $+  Map.fromListWith Map.union $++   ([], Map.fromList+          [ (KFun 2, ActCommand "toggle-detail")+          , (KFun 3, ActCommand "toggle-activity-bar")+          , (KFun 4, ActCommand "toggle-metadata")+          , (KFun 5, ActCommand "toggle-layout")+          ])+    :++    [ (mods, Map.singleton k act)+      | (act, mks) <- HashMap.elems actionInfos+      , (mods, k)  <- mks+      ]+++parseKey :: String -> Maybe ([Modifier], Key)+parseKey = getCompose . go+  where+    modifier x   = Compose (Just ([x], ()))+    liftMaybe mb = Compose ((,)[] <$> mb)+    go str =+      case str of+        "Tab"       -> pure (KChar '\t')+        "BackTab"   -> pure KBackTab+        "Enter"     -> pure KEnter+        "Home"      -> pure KHome+        "End"       -> pure KEnd+        "Esc"       -> pure KEsc+        "PageUp"    -> pure KPageUp+        "PageDown"  -> pure KPageDown+        "Backspace" -> pure KBS+        "Delete"    -> pure KDel+        "Left"      -> pure KLeft+        "Right"     -> pure KRight+        [c]         -> pure (KChar c)+        'F':xs      -> KFun <$> liftMaybe (readMaybe xs)+        'C':'-':xs  -> modifier MCtrl  *> go xs+        'M':'-':xs  -> modifier MMeta  *> go xs+        'S':'-':xs  -> modifier MShift *> go xs+        'A':'-':xs  -> modifier MAlt   *> go xs+        _           -> empty+++prettyModifierKey :: [Modifier] -> Key -> String+prettyModifierKey mods k+  = foldr prettyModifier (prettyKey k) mods++prettyModifier :: Modifier -> ShowS+prettyModifier MCtrl  = showString "C-"+prettyModifier MMeta  = showString "M-"+prettyModifier MShift = showString "S-"+prettyModifier MAlt   = showString "A-"++prettyKey :: Key -> String+prettyKey (KChar '\t') = "Tab"+prettyKey (KChar c) = showLitChar c "" -- escapes anything non-ascii+prettyKey (KFun n)  = 'F' : show n+prettyKey KBackTab  = "BackTab"+prettyKey KEnter    = "Enter"+prettyKey KEsc      = "Esc"+prettyKey KHome     = "Home"+prettyKey KEnd      = "End"+prettyKey KPageUp   = "PageUp"+prettyKey KPageDown = "PageDn"+prettyKey KDel      = "Delete"+prettyKey KBS       = "Backspace"+prettyKey KLeft     = "Left"+prettyKey KRight    = "Right"+prettyKey k         = show k
src/Client/EventLoop/Errors.hs view
@@ -40,7 +40,7 @@   | Just err  <- fromException ex = explainHookupError err    -- HsOpenSSL errors-  | Just err <- fromException ex :: Maybe ConnectionAbruptlyTerminated =+  | Just _ <- fromException ex :: Maybe ConnectionAbruptlyTerminated =      "Connection abruptly terminated" :| []   | Just (ProtocolError e) <- fromException ex =      ("TLS protocol error: " ++ e) :| []
src/Client/Image.hs view
@@ -9,18 +9,10 @@ This module provides the renderer for the client's UI.  -}-module Client.Image-  ( clientPicture-  , scrollAmount-  ) where+module Client.Image (clientPicture) where -import           Client.Image.Palette-import           Client.Image.StatusLine-import           Client.Image.Textbox-import           Client.Image.Utils+import           Client.Image.Layout import           Client.State-import           Client.State.Focus-import           Client.View import           Control.Lens import           Graphics.Vty            (Background (..), Cursor (..),                                           Picture (..))@@ -45,85 +37,8 @@   (Int, Image, ClientState) {- ^ text box cursor position, image, updated state -} clientImage st = (pos, img, st')   where-    (mainHeight, splitHeight) = clientWindowHeights (imageHeight statusLine) st-    splitFocuses              = clientExtraFocuses st-    focus                     = view clientFocus st-    (pos , nextOffset, tbImg) = textboxImage st-     -- update client state for scroll clamp     !st' = set clientTextBoxOffset nextOffset          $ over clientScroll (max 0 . subtract overscroll) st -    (overscroll, msgs) = messagePane mainHeight focus (view clientSubfocus st) st-    splits = renderExtra st' <$> splitFocuses-    -- outgoing state is ignored here, splits don't get to truncate scrollback--    img = vertCat splits      <->-          msgs                <->-          statusLine          <->-          tbImg--    statusLine = statusLineImage st--    renderExtra stIn focus1 = outImg-      where-        (_,msgImg) = messagePane splitHeight focus1 FocusMessages stIn-        pal = clientPalette stIn-        divider = view palWindowDivider pal-        outImg = msgImg <-> minorStatusLineImage focus1 stIn-                        <-> charFill divider ' ' (view clientWidth stIn) 1---- | Generate an image corresponding to the image lines of the given--- focus and subfocus. Returns the number of lines overscrolled to--- assist in clamping scroll to the lines available in the window.-messagePane ::-  Int          {- ^ available rows                -} ->-  Focus        {- ^ focused window                -} ->-  Subfocus     {- ^ subfocus to render            -} ->-  ClientState  {- ^ client state                  -} ->-  (Int, Image) {- ^ overscroll, rendered messages -}-messagePane h focus subfocus st = (overscroll, img)-  where-    images = viewLines focus subfocus st-    vimg   = assemble emptyImage images-    vimg1  = cropBottom h vimg-    img    = pad 0 (h - imageHeight vimg1) 0 0 vimg1--    overscroll = vh - imageHeight vimg--    assemble acc _ | imageHeight acc >= vh = cropTop vh acc-    assemble acc [] = acc-    assemble acc (x:xs) = assemble (lineWrap w Nothing x <-> acc) xs--    scroll = view clientScroll st-    vh     = h + scroll--    w      = view clientWidth st----- | Compute the number of lines in a page at the current window size-scrollAmount ::-  ClientState {- ^ client state              -} ->-  Int         {- ^ scroll amount             -}-scrollAmount st = max 1 (snd (clientWindowHeights actSize st))-               -- extra will be equal to main or 1 smaller-  where-    actSize = imageHeight (statusLineImage st)----- | Number of lines to allocate for the focused window and the--- main window. This doesn't include the textbox, activity bar,--- or status line.-clientWindowHeights ::-  Int         {- ^ status bar height         -} ->-  ClientState {- ^ client state              -} ->-  (Int,Int)   {- ^ main height, extra height -}-clientWindowHeights statusBar st = (mainH, splitH)-  where-    h        = max 0 (view clientHeight st - overhead)-    splitH   = h `quot` (1+extras)-    mainH    = h - splitH*extras--    extras   = length (clientExtraFocuses st)-    textbox  = 1-    overhead = textbox + statusBar + 2*extras+    (overscroll, pos, nextOffset, img) = drawLayout st
src/Client/Image/Arguments.hs view
@@ -18,26 +18,27 @@  import           Client.Commands.Arguments import           Client.Image.MircFormatting+import           Client.Image.PackedImage import           Client.Image.Palette import           Control.Lens+import           Data.Semigroup import qualified Data.Text as Text import           Graphics.Vty.Attributes-import           Graphics.Vty.Image  -- | Parse command arguments against a given 'ArgumentSpec'. -- The given text will be rendered and then any missing arguments -- will be indicated by extra placeholder values appended onto the -- image. Parameters are rendered with 'plainText' except for -- the case of 'RemainingArg' which supports WYSIWYG.-argumentsImage :: Palette -> ArgumentSpec a -> String -> Image+argumentsImage :: Palette -> ArgumentSpec a -> String -> Image' argumentsImage pal spec xs   | all (==' ') xs = placeholders-                 <|> string defAttr (drop (imageWidth placeholders) xs)+                  <> string defAttr (drop (imageWidth placeholders) xs)   | otherwise =      case spec of        NoArg           -> plainText xs-       ReqTokenArg _ a -> plainText token <|> argumentsImage pal a xs'-       OptTokenArg _ a -> plainText token <|> argumentsImage pal a xs'+       ReqTokenArg _ a -> plainText token <> argumentsImage pal a xs'+       OptTokenArg _ a -> plainText token <> argumentsImage pal a xs'        RemainingArg _  -> parseIrcTextExplicit (Text.pack xs)    where@@ -49,17 +50,17 @@  -- | Construct an 'Image' containing placeholders for each -- of the remaining arguments.-mkPlaceholders :: Palette -> ArgumentSpec a -> Image+mkPlaceholders :: Palette -> ArgumentSpec a -> Image' mkPlaceholders pal arg =   case arg of-    NoArg           -> emptyImage+    NoArg           -> mempty     ReqTokenArg n a -> leader-                   <|> string (view palCommandPlaceholder pal) n-                   <|> mkPlaceholders pal a+                    <> string (view palCommandPlaceholder pal) n+                    <> mkPlaceholders pal a     OptTokenArg n a -> leader-                   <|> string (view palCommandPlaceholder pal) (n ++ "?")-                   <|> mkPlaceholders pal a+                    <> string (view palCommandPlaceholder pal) (n ++ "?")+                    <> mkPlaceholders pal a     RemainingArg n  -> leader-                   <|> string (view palCommandPlaceholder pal) (n ++ "…")+                    <> string (view palCommandPlaceholder pal) (n ++ "…")   where     leader = char defAttr ' '
+ src/Client/Image/Layout.hs view
@@ -0,0 +1,191 @@+{-|+Module      : Client.Image.Layout+Description : Layout code for the multi-window splits+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++-}+module Client.Image.Layout (scrollAmount, drawLayout) where++import Control.Lens+import Client.State+import Client.State.Focus+import Client.Configuration (LayoutMode(..))+import Client.Image.StatusLine (statusLineImage, minorStatusLineImage)+import Client.Image.Textbox+import Client.Image.LineWrap (lineWrap)+import Client.Image.Palette+import Client.View+import Graphics.Vty.Image+import Graphics.Vty.Attributes (defAttr)++-- | Compute the combined image for all the visible message windows.+drawLayout ::+  ClientState            {- ^ client state                                     -} ->+  (Int, Int, Int, Image) {- ^ overscroll, cursor pos, next offset, final image -}+drawLayout st =+  case view clientLayout st of+    TwoColumn | not (null extrafocus) -> drawLayoutTwo st extrafocus+    _                                 -> drawLayoutOne st extrafocus+  where+    extrafocus = clientExtraFocuses st++-- | Layout algorithm for all windows in a single column.+drawLayoutOne ::+  ClientState            {- ^ client state                 -} ->+  [Focus]                {- ^ extra window names           -} ->+  (Int, Int, Int, Image) {- ^ overscroll and final image   -}+drawLayoutOne st extrafocus =+  (overscroll, pos, nextOffset, output)+  where+    w      = view clientWidth st+    h:hs   = splitHeights (rows - saveRows) (length extraLines)+    scroll = view clientScroll st++    (overscroll, pos, nextOffset, main) =+        drawMain w (saveRows + h) scroll st++    output = vertCat $ reverse+           $ main+           : [ drawExtra st w h' foc imgs+                 | (h', (foc, imgs)) <- zip hs extraLines]++    rows = view clientHeight st++    -- don't count textbox or the main status line against the main window's height+    saveRows = 1 + imageHeight (statusLineImage w st)++    extraLines = [ (focus', viewLines focus' FocusMessages w st)+                   | focus' <- extrafocus ]++-- | Layout algorithm for all windows in a single column.+drawLayoutTwo ::+  ClientState            {- ^ client state                                -} ->+  [Focus]                {- ^ extra window names                          -} ->+  (Int, Int, Int, Image) {- ^ overscroll, cursor pos, offset, final image -}+drawLayoutTwo st extrafocus =+  (overscroll, pos, nextOffset, output)+  where+    [wl,wr] = divisions (view clientWidth st - 1) 2+    hs      = divisions (rows - length extraLines) (length extraLines)+    scroll = view clientScroll st++    output = main <|> divider <|> extraImgs++    extraImgs = vertCat $ reverse+             [ drawExtra st wr h' foc imgs+                 | (h', (foc, imgs)) <- zip hs extraLines]++    (overscroll, pos, nextOffset, main) =+        drawMain wl rows scroll st++    pal     = clientPalette st+    divider = charFill (view palWindowDivider pal) ' ' 1 rows+    rows    = view clientHeight st++    extraLines = [ (focus', viewLines focus' FocusMessages wr st)+                   | focus' <- extrafocus ]++drawMain ::+  Int         {- ^ draw width      -} ->+  Int         {- ^ draw height     -} ->+  Int         {- ^ scroll amount   -} ->+  ClientState {- ^ client state    -} ->+  (Int,Int,Int,Image)+drawMain w h scroll st = (overscroll, pos, nextOffset, msgs <-> bottomImg)+  where+    focus = view clientFocus st+    subfocus = view clientSubfocus st++    msgLines = viewLines focus subfocus w st++    (overscroll, msgs) = messagePane w h' scroll msgLines++    h' = max 0 (h - imageHeight bottomImg)++    bottomImg = statusLineImage w st <-> tbImage+    (pos, nextOffset, tbImage) = textboxImage w st+++-- | Draw one of the extra windows from @/splits@+drawExtra ::+  ClientState {- ^ client state    -} ->+  Int         {- ^ draw width      -} ->+  Int         {- ^ draw height     -} ->+  Focus       {- ^ focus           -} ->+  [Image]     {- ^ image lines     -} ->+  Image       {- ^ rendered window -}+drawExtra st w h focus lineImages =+    msgImg <-> minorStatusLineImage focus w True st+  where+    (_, msgImg) = messagePane w h 0 lineImages+++-- | Generate an image corresponding to the image lines of the given+-- focus and subfocus. Returns the number of lines overscrolled to+-- assist in clamping scroll to the lines available in the window.+messagePane ::+  Int          {- ^ client width                  -} ->+  Int          {- ^ available rows                -} ->+  Int          {- ^ current scroll                -} ->+  [Image]      {- ^ focused window                -} ->+  (Int, Image) {- ^ overscroll, rendered messages -}+messagePane w h scroll images = (overscroll, img)+  where+    vimg   = assemble emptyImage images+    vimg1  = cropBottom h vimg+    img    = charFill defAttr ' ' w (h - imageHeight vimg1)+             <-> vimg1++    overscroll = vh - imageHeight vimg+    vh         = h + scroll++    assemble acc _ | imageHeight acc >= vh = cropTop vh acc+    assemble acc [] = acc+    assemble acc (x:xs) = assemble (lineWrap w x <-> acc) xs+++splitHeights ::+  Int   {- ^ screen rows to fill               -} ->+  Int   {- ^ number of extra windows           -} ->+  [Int] {- ^ list of heights for each division -}+splitHeights h ex = divisions (h - ex) (1 + ex)+++-- | Constructs a list of numbers with the length of the divisor+-- and that sums to the dividend. Each element will be within+-- one of the quotient.+divisions ::+  Int {- ^ dividend -} ->+  Int {- ^ divisor  -} ->+  [Int]+divisions x y+  | y <= 0    = []+  | otherwise = replicate r (q+1) ++ replicate (y-r) q+  where+    (q,r) = quotRem (max 0 x) y++++-- | Compute the number of lines in a page at the current window size+scrollAmount ::+  ClientState {- ^ client state  -} ->+  Int         {- ^ scroll amount -}+scrollAmount st =+  case view clientLayout st of+    TwoColumn -> h+    OneColumn -> head (splitHeights h ex) -- extra will be equal to main or 1 smaller+  where+    layout = view clientLayout st++    h = view clientHeight st - bottomSize+    ex = length (clientExtraFocuses st)++    bottomSize = 1 -- textbox+               + imageHeight (statusLineImage mainWidth st)++    mainWidth =+      case layout of+        TwoColumn -> head (divisions (view clientWidth st - 1) 2)+        OneColumn -> view clientWidth st
+ src/Client/Image/LineWrap.hs view
@@ -0,0 +1,103 @@+{-|+Module      : Client.Image.LineWrap+Description : Chat message view+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++Provides utilities for line wrapping images.+-}++module Client.Image.LineWrap (lineWrap, lineWrapChat) where++import           Client.Image.PackedImage+import           Data.Semigroup+import qualified Graphics.Vty.Image as Vty+import           Graphics.Vty.Attributes+import qualified Data.Text.Lazy as L++-- | Given an image, break the image up into chunks of at most the+-- given width and stack the resulting chunks vertically top-to-bottom.+lineWrap ::+  Int       {- ^ terminal width       -} ->+  Vty.Image {- ^ unwrapped image      -} ->+  Vty.Image {- ^ wrapped image        -}+lineWrap w img+  | Vty.imageWidth img == 0 = Vty.emptyImage+  | Vty.imageWidth img <= w = terminate w img+  | otherwise =+      terminate w (Vty.cropRight w img) Vty.<->+      lineWrap w (Vty.cropLeft (Vty.imageWidth img - w) img)++-- | Trailing space with default attributes deals with bug in VTY+-- where the formatting will continue past the end of chat messages.+-- This adds an extra space if a line doesn't end on the terminal edge.+terminate ::+  Int       {- ^ terminal width  -} ->+  Vty.Image {- ^ unwrapped image -} ->+  Vty.Image {- ^ wrapped image   -}+terminate n img+  | Vty.imageWidth img == n = img+  | otherwise               = img Vty.<|> Vty.char defAttr ' '+++lineWrapChat ::+  Int       {- ^ terminal width       -} ->+  Maybe Int {- ^ optional indentation -} ->+  Image'    {- ^ unwrapped image      -} ->+  [Image']  {- ^ wrapped image        -}+lineWrapChat w (Just i)+  | 2*i <= w = reverse . addPadding i . wordLineWrap w (w-i)+lineWrapChat w _  = reverse . wordLineWrap w w+++addPadding :: Int -> [Image'] -> [Image']+addPadding _ [] = []+addPadding i (x:xs) = x : map indent xs+  where indent = (string defAttr (replicate i ' ') <>)+++wordLineWrap ::+  Int      {- ^ first line length     -} ->+  Int      {- ^ secondary line length -} ->+  Image'   {- ^ image                 -} ->+  [Image'] {- ^ splits                -}+wordLineWrap w wNext img+  | imgW == 0 = []+  | imgW <= w = [img]+  | otherwise = l : wordLineWrap wNext wNext (dropSpaces r)+  where+    imgW = imageWidth img+    x:xs = splitOptions img++    (l,r) = splitImage width img++    width+      | x <= w = go x xs+      | otherwise = w++    go y [] = min y w+    go y (z:zs)+      | z-y > wNext = w+      | z > w = y+      | otherwise = go z zs+++-- | List of image widths suitable for breaking the image on+-- that correspond to word breaks.+splitOptions :: Image' -> [Int]+splitOptions+  = dropWhile (0==)+  . scanl1 (\x y -> 1 + x + y)+  . map (Vty.wcswidth . L.unpack)+  . L.split (' '==)+  . imageText+++-- | Drop the leading spaces from an image+dropSpaces :: Image' -> Image'+dropSpaces img+  | n == 0    = img+  | otherwise = snd (splitImage n img)+  where+    n = fromIntegral $ L.length $ L.takeWhile (' '==) $ imageText img
src/Client/Image/Message.hs view
@@ -26,6 +26,7 @@   ) where  import           Client.Image.MircFormatting+import           Client.Image.PackedImage import           Client.Image.Palette import           Client.Message import           Control.Lens@@ -34,12 +35,12 @@ import           Data.HashSet (HashSet) import qualified Data.HashSet as HashSet import           Data.List+import           Data.Semigroup import           Data.Text (Text) import qualified Data.Text as Text import           Data.Time import qualified Data.Vector as Vector import           Graphics.Vty.Attributes-import           Graphics.Vty.Image import           Irc.Codes import           Irc.Identifier import           Irc.Message@@ -73,8 +74,8 @@ msgImage ::   RenderMode ->   ZonedTime {- ^ time of message -} ->-  MessageRendererParams -> MessageBody -> Image-msgImage rm when params body = horizCat+  MessageRendererParams -> MessageBody -> Image'+msgImage rm when params body = mconcat   [ renderTime rm (rendPalette params) when   , statusMsgImage (rendStatusMsg params)   , bodyImage rm params body@@ -93,8 +94,8 @@ errorImage ::   MessageRendererParams ->   Text {- ^ error message -} ->-  Image-errorImage params txt = horizCat+  Image'+errorImage params txt = mconcat   [ text' (view palError (rendPalette params)) "error "   , text' defAttr (cleanText txt)   ]@@ -102,23 +103,23 @@ normalImage ::   MessageRendererParams ->   Text {- ^ message -} ->-  Image-normalImage params txt = horizCat+  Image'+normalImage params txt = mconcat   [ text' (view palLabel (rendPalette params)) "client "   , text' defAttr (cleanText txt)   ]  -- | Render the given time according to the current mode and palette.-renderTime :: RenderMode -> Palette -> ZonedTime -> Image+renderTime :: RenderMode -> Palette -> ZonedTime -> Image' renderTime DetailedRender = datetimeImage renderTime NormalRender   = timeImage  -- | Render the sigils for a restricted message.-statusMsgImage :: [Char] {- ^ sigils -} -> Image+statusMsgImage :: [Char] {- ^ sigils -} -> Image' statusMsgImage modes-  | null modes = emptyImage-  | otherwise  = string defAttr "(" <|>-                 string statusMsgColor modes <|>+  | null modes = mempty+  | otherwise  = string defAttr "(" <>+                 string statusMsgColor modes <>                  string defAttr ") "   where     statusMsgColor = withForeColor defAttr red@@ -128,7 +129,7 @@ bodyImage ::   RenderMode ->   MessageRendererParams ->-  MessageBody -> Image+  MessageBody -> Image' bodyImage rm params body =   case body of     IrcBody    irc -> ircLineImage rm params irc@@ -140,7 +141,7 @@ -- @ -- 23:15 -- @-timeImage :: Palette -> ZonedTime -> Image+timeImage :: Palette -> ZonedTime -> Image' timeImage palette   = string (view palTime palette)   . formatTime defaultTimeLocale "%R "@@ -150,7 +151,7 @@ -- @ -- 2016-07-24 23:15:10 -- @-datetimeImage :: Palette -> ZonedTime -> Image+datetimeImage :: Palette -> ZonedTime -> Image' datetimeImage palette   = string (view palTime palette)   . formatTime defaultTimeLocale "%F %T "@@ -162,10 +163,10 @@  -- | Optionally insert padding on the right of an 'Image' until it has -- the minimum width.-rightPad :: RenderMode -> Maybe Integer -> Image -> Image+rightPad :: RenderMode -> Maybe Integer -> Image' -> Image' rightPad NormalRender (Just minWidth) i =   let w = max 0 (fromIntegral minWidth - imageWidth i)-  in i <|> string defAttr (replicate w ' ')+  in i <> string defAttr (replicate w ' ') rightPad _ _ i = i  -- | Render a chat message given a rendering mode, the sigils of the user@@ -173,7 +174,7 @@ ircLineImage ::   RenderMode ->   MessageRendererParams ->-  IrcMsg -> Image+  IrcMsg -> Image' ircLineImage rm !rp body =   let quietAttr = view palMeta pal       pal     = rendPalette rp@@ -182,132 +183,132 @@       nicks   = rendNicks rp       detail img =         case rm of-          NormalRender -> emptyImage+          NormalRender   -> mempty           DetailedRender -> img   in   case body of     Nick old new ->-      detail (string quietAttr "nick ") <|>-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal rm myNicks old <|>-      string defAttr " is now known as " <|>+      detail (string quietAttr "nick ") <>+      string (view palSigil pal) sigils <>+      coloredUserInfo pal rm myNicks old <>+      string defAttr " is now known as " <>       coloredIdentifier pal NormalIdentifier myNicks new      Join nick _chan ->-      string quietAttr "join " <|>+      string quietAttr "join " <>       coloredUserInfo pal rm myNicks nick      Part nick _chan mbreason ->-      string quietAttr "part " <|>-      coloredUserInfo pal rm myNicks nick <|>-      foldMap (\reason -> string quietAttr " (" <|>-                          parseIrcText reason <|>+      string quietAttr "part " <>+      coloredUserInfo pal rm myNicks nick <>+      foldMap (\reason -> string quietAttr " (" <>+                          parseIrcText reason <>                           string quietAttr ")") mbreason      Quit nick mbreason ->-      string quietAttr "quit "   <|>-      coloredUserInfo pal rm myNicks nick   <|>-      foldMap (\reason -> string quietAttr " (" <|>-                          parseIrcText reason <|>+      string quietAttr "quit "   <>+      coloredUserInfo pal rm myNicks nick   <>+      foldMap (\reason -> string quietAttr " (" <>+                          parseIrcText reason <>                           string quietAttr ")") mbreason      Kick kicker _channel kickee reason ->-      detail (string quietAttr "kick ") <|>-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal rm myNicks kicker <|>-      string defAttr " kicked " <|>-      coloredIdentifier pal NormalIdentifier myNicks kickee <|>-      string defAttr ": " <|>+      detail (string quietAttr "kick ") <>+      string (view palSigil pal) sigils <>+      coloredUserInfo pal rm myNicks kicker <>+      string defAttr " kicked " <>+      coloredIdentifier pal NormalIdentifier myNicks kickee <>+      string defAttr ": " <>       parseIrcText reason      Topic src _dst txt ->-      detail (string quietAttr "tpic ") <|>-      coloredUserInfo pal rm myNicks src <|>-      string defAttr " changed the topic to: " <|>+      detail (string quietAttr "tpic ") <>+      coloredUserInfo pal rm myNicks src <>+      string defAttr " changed the topic to: " <>       parseIrcText txt      Notice src _dst txt ->-      detail (string quietAttr "note ") <|>+      detail (string quietAttr "note ") <>       rightPad rm (rendNickPadding rp)-        (string (view palSigil pal) sigils <|>-         coloredUserInfo pal rm myNicks src) <|>-      string (withForeColor defAttr red) ": " <|>+        (string (view palSigil pal) sigils <>+         coloredUserInfo pal rm myNicks src) <>+      string (withForeColor defAttr red) ": " <>       parseIrcTextWithNicks pal myNicks nicks txt      Privmsg src _dst txt ->-      detail (string quietAttr "chat ") <|>+      detail (string quietAttr "chat ") <>       rightPad rm (rendNickPadding rp)-        (string (view palSigil pal) sigils <|>-         coloredUserInfo pal rm myNicks src) <|>-      string defAttr ": " <|>+        (string (view palSigil pal) sigils <>+         coloredUserInfo pal rm myNicks src) <>+      string defAttr ": " <>       parseIrcTextWithNicks pal myNicks nicks txt      Ctcp src _dst "ACTION" txt ->-      detail (string quietAttr "actp ") <|>-      string (withForeColor defAttr blue) "* " <|>-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal rm myNicks src <|>-      string defAttr " " <|>+      detail (string quietAttr "actp ") <>+      string (withForeColor defAttr blue) "* " <>+      string (view palSigil pal) sigils <>+      coloredUserInfo pal rm myNicks src <>+      string defAttr " " <>       parseIrcTextWithNicks pal myNicks nicks txt      CtcpNotice src _dst "ACTION" txt ->-      detail (string quietAttr "actn ") <|>-      string (withForeColor defAttr red) "* " <|>-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal rm myNicks src <|>-      string defAttr " " <|>+      detail (string quietAttr "actn ") <>+      string (withForeColor defAttr red) "* " <>+      string (view palSigil pal) sigils <>+      coloredUserInfo pal rm myNicks src <>+      string defAttr " " <>       parseIrcTextWithNicks pal myNicks nicks txt      Ctcp src _dst cmd txt ->-      detail (string quietAttr "ctcp ") <|>-      string (withForeColor defAttr blue) "! " <|>-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal rm myNicks src <|>-      string defAttr " " <|>-      parseIrcText cmd <|>-      separatorImage <|>+      detail (string quietAttr "ctcp ") <>+      string (withForeColor defAttr blue) "! " <>+      string (view palSigil pal) sigils <>+      coloredUserInfo pal rm myNicks src <>+      string defAttr " " <>+      parseIrcText cmd <>+      separatorImage <>       parseIrcText txt      CtcpNotice src _dst cmd txt ->-      detail (string quietAttr "ctcp ") <|>-      string (withForeColor defAttr red) "! " <|>-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal rm myNicks src <|>-      string defAttr " " <|>-      parseIrcText cmd <|>-      separatorImage <|>+      detail (string quietAttr "ctcp ") <>+      string (withForeColor defAttr red) "! " <>+      string (view palSigil pal) sigils <>+      coloredUserInfo pal rm myNicks src <>+      string defAttr " " <>+      parseIrcText cmd <>+      separatorImage <>       parseIrcText txt      Ping params ->-      string defAttr "PING " <|> separatedParams params+      string defAttr "PING " <> separatedParams params      Pong params ->-      string defAttr "PONG " <|> separatedParams params+      string defAttr "PONG " <> separatedParams params      Error reason ->-      string (view palError pal) "ERROR " <|>+      string (view palError pal) "ERROR " <>       parseIrcText reason      Reply code params ->       renderReplyCode rm rp code params      UnknownMsg irc ->-      maybe emptyImage (\ui -> coloredUserInfo pal rm myNicks ui <|> char defAttr ' ')-        (view msgPrefix irc) <|>-      text' defAttr (view msgCommand irc) <|>-      char defAttr ' ' <|>+      foldMap (\ui -> coloredUserInfo pal rm myNicks ui <> char defAttr ' ')+        (view msgPrefix irc) <>+      text' defAttr (view msgCommand irc) <>+      char defAttr ' ' <>       separatedParams (view msgParams irc)      Cap cmd args ->-      text' (withForeColor defAttr magenta) (renderCapCmd cmd) <|>-      text' defAttr ": " <|>+      text' (withForeColor defAttr magenta) (renderCapCmd cmd) <>+      text' defAttr ": " <>       separatedParams args      Mode nick _chan params ->-      detail (string quietAttr "mode ") <|>-      string (view palSigil pal) sigils <|>-      coloredUserInfo pal rm myNicks nick <|>-      string defAttr " set mode: " <|>+      detail (string quietAttr "mode ") <>+      string (view palSigil pal) sigils <>+      coloredUserInfo pal rm myNicks nick <>+      string defAttr " set mode: " <>       ircWords params      Authenticate{} -> string defAttr "AUTHENTICATE ***"@@ -325,34 +326,34 @@     CapEnd  -> "caps finished" -- server shouldn't send this     CapReq  -> "caps requested" -- server shouldn't send this -separatorImage :: Image+separatorImage :: Image' separatorImage = char (withForeColor defAttr blue) '·'  -- | Process list of 'Text' as individual IRC formatted words -- separated by a special separator to distinguish parameters -- from words within parameters.-separatedParams :: [Text] -> Image-separatedParams = horizCat . intersperse separatorImage . map parseIrcText+separatedParams :: [Text] -> Image'+separatedParams = mconcat . intersperse separatorImage . map parseIrcText  -- | Process list of 'Text' as individual IRC formatted words-ircWords :: [Text] -> Image-ircWords = horizCat . intersperse (char defAttr ' ') . map parseIrcText+ircWords :: [Text] -> Image'+ircWords = mconcat . intersperse (char defAttr ' ') . map parseIrcText -renderReplyCode :: RenderMode -> MessageRendererParams -> ReplyCode -> [Text] -> Image+renderReplyCode :: RenderMode -> MessageRendererParams -> ReplyCode -> [Text] -> Image' renderReplyCode rm rp code@(ReplyCode w) params =   case rm of-    DetailedRender -> string attr (show w) <|> rawParamsImage+    DetailedRender -> string attr (show w) <> rawParamsImage     NormalRender   ->       rightPad rm (rendNickPadding rp)-        (text' attr (Text.toLower (replyCodeText info))) <|>-      char defAttr ':' <|>+        (text' attr (replyCodeText info)) <>+      char defAttr ':' <>        case code of         RPL_WHOISIDLE -> whoisIdleParamsImage         _             -> rawParamsImage   where     rawParamsImage =-      char defAttr ' ' <|>+      char defAttr ' ' <>       separatedParams params'      params' = case rm of@@ -372,11 +373,11 @@     whoisIdleParamsImage =       case params' of         [name, idle, signon, _txt] ->-          char defAttr ' ' <|>-          text' defAttr name <|>-          text' defAttr " idle: " <|>-          string defAttr (prettySeconds (Text.unpack idle)) <|>-          text' defAttr " sign-on: " <|>+          char defAttr ' ' <>+          text' defAttr name <>+          text' defAttr " idle: " <>+          string defAttr (prettySeconds (Text.unpack idle)) <>+          text' defAttr " sign-on: " <>           string defAttr (prettyUnixTime (Text.unpack signon))          _ -> rawParamsImage@@ -413,7 +414,7 @@   IdentifierColorMode {- ^ draw mode          -} ->   HashSet Identifier  {- ^ my nicknames       -} ->   Identifier          {- ^ identifier to draw -} ->-  Image+  Image' coloredIdentifier palette icm myNicks ident =   text' color (idText ident)   where@@ -436,11 +437,11 @@   RenderMode         {- ^ mode            -} ->   HashSet Identifier {- ^ my nicks        -} ->   UserInfo           {- ^ userinfo to draw-} ->-  Image+  Image' coloredUserInfo palette NormalRender myNicks ui =   coloredIdentifier palette NormalIdentifier myNicks (userNick ui) coloredUserInfo palette DetailedRender myNicks !ui =-  horizCat+  mconcat     [ coloredIdentifier palette NormalIdentifier myNicks (userNick ui)     , aux '!' (userName ui)     , aux '@' (userHost ui)@@ -448,11 +449,11 @@   where     quietAttr = view palMeta palette     aux x xs-      | Text.null xs = emptyImage-      | otherwise    = char quietAttr x <|> text' quietAttr xs+      | Text.null xs = mempty+      | otherwise    = char quietAttr x <> text' quietAttr xs  -- | Render an identifier without using colors. This is useful for metadata.-quietIdentifier :: Palette -> Identifier -> Image+quietIdentifier :: Palette -> Identifier -> Image' quietIdentifier palette ident =   text' (view palMeta palette) (idText ident) @@ -464,7 +465,7 @@   Palette ->   HashSet Identifier {- ^ my nicks    -} ->   HashSet Identifier {- ^ other nicks -} ->-  Text -> Image+  Text -> Image' parseIrcTextWithNicks palette myNicks nicks txt   | Text.any isControl txt = parseIrcText txt   | otherwise              = highlightNicks palette myNicks nicks txt@@ -475,8 +476,8 @@   Palette ->   HashSet Identifier {- ^ my nicks    -} ->   HashSet Identifier {- ^ other nicks -} ->-  Text -> Image-highlightNicks palette myNicks nicks txt = horizCat (highlight1 <$> txtParts)+  Text -> Image'+highlightNicks palette myNicks nicks txt = mconcat (highlight1 <$> txtParts)   where     txtParts = nickSplit txt     allNicks = HashSet.union myNicks nicks@@ -488,7 +489,7 @@  -- | Returns image and identifier to be used when collapsing metadata -- messages.-metadataImg :: IrcSummary -> Maybe (Image, Identifier, Maybe Identifier)+metadataImg :: IrcSummary -> Maybe (Image', Identifier, Maybe Identifier) metadataImg msg =   case msg of     QuitSummary who     -> Just (char (withForeColor defAttr red   ) 'x', who, Nothing)@@ -499,5 +500,5 @@     _                   -> Nothing  -- | Image used when treating ignored chat messages as metadata-ignoreImage :: Image+ignoreImage :: Image' ignoreImage = char (withForeColor defAttr yellow) 'I'
src/Client/Image/MircFormatting.hs view
@@ -17,16 +17,16 @@   , controlImage   ) where +import           Client.Image.PackedImage as I import           Control.Applicative ((<|>)) import           Control.Lens import           Data.Attoparsec.Text as Parse import           Data.Bits import           Data.Char import           Data.Maybe+import           Data.Semigroup ((<>)) import           Data.Text (Text) import           Graphics.Vty.Attributes-import           Graphics.Vty.Image hiding ((<|>))-import qualified Graphics.Vty as Vty  data FormatState = FormatState   { _fmtFore :: Maybe Color@@ -56,16 +56,16 @@ defaultFormatState = FormatState Nothing Nothing False False False False  -- | Parse mIRC encoded format characters and hide the control characters.-parseIrcText :: Text -> Image+parseIrcText :: Text -> Image' parseIrcText = parseIrcText' False  -- | Parse mIRC encoded format characters and render the control characters -- explicitly. This view is useful when inputting control characters to make -- it clear where they are in the text.-parseIrcTextExplicit :: Text -> Image+parseIrcTextExplicit :: Text -> Image' parseIrcTextExplicit = parseIrcText' True -parseIrcText' :: Bool -> Text -> Image+parseIrcText' :: Bool -> Text -> Image' parseIrcText' explicit = either plainText id                        . parseOnly (pIrcLine explicit defaultFormatState) @@ -75,27 +75,27 @@ pSegment = TextSegment    <$> takeWhile1 (not . isControl)        <|> ControlSegment <$> satisfy isControl -pIrcLine :: Bool -> FormatState -> Parser Image+pIrcLine :: Bool -> FormatState -> Parser Image' pIrcLine explicit fmt =   do seg <- option Nothing (Just <$> pSegment)      case seg of-       Nothing -> return emptyImage+       Nothing -> return mempty        Just (TextSegment txt) ->            do rest <- pIrcLine explicit fmt-              return (text' (formatAttr fmt) txt Vty.<|> rest)+              return (text' (formatAttr fmt) txt <> rest)        Just (ControlSegment '\^C') ->            do (numberText, colorNumbers) <- match pColorNumbers               rest <- pIrcLine explicit (applyColors colorNumbers fmt)               return $ if explicit                          then controlImage '\^C'-                              Vty.<|> text' defAttr numberText-                              Vty.<|> rest+                              <> text' defAttr numberText+                              <> rest                           else rest        Just (ControlSegment c)           -- always render control codes that we don't understand           | isNothing mbFmt' || explicit ->                 do rest <- next-                   return (controlImage c Vty.<|> rest)+                   return (controlImage c <> rest)           | otherwise -> next           where             mbFmt' = applyControlEffect c fmt@@ -152,8 +152,8 @@ applyControlEffect _     = const Nothing  -- | Safely render a control character.-controlImage :: Char -> Image-controlImage = Vty.char attr . controlName+controlImage :: Char -> Image'+controlImage = I.char attr . controlName   where     attr          = withStyle defAttr reverseVideo     controlName c@@ -163,11 +163,11 @@ -- | Render a 'String' with default attributes and replacing all of the -- control characters with reverse-video letters corresponding to caret -- notation.-plainText :: String -> Image-plainText "" = emptyImage+plainText :: String -> Image'+plainText "" = mempty plainText xs =   case break isControl xs of-    (first, ""       ) -> Vty.string defAttr first-    (first, cntl:rest) -> Vty.string defAttr first Vty.<|>-                          controlImage cntl Vty.<|>+    (first, ""       ) -> I.string defAttr first+    (first, cntl:rest) -> I.string defAttr first <>+                          controlImage cntl <>                           plainText rest
src/Client/Image/PackedImage.hs view
@@ -1,5 +1,4 @@-{-# Language TypeOperators, MultiParamTypeClasses, DeriveGeneric #-}-{-# OPTIONS_GHC -Wno-orphans -funfolding-creation-threshold=1500 -funfolding-use-threshold=5000 #-}+{-# Language OverloadedStrings #-} {-| Module      : Client.Image.PackedImage Description : Packed vty Image type@@ -12,39 +11,32 @@ -} module Client.Image.PackedImage   ( Image'-  , _Image'+  , unpackImage++  -- * Packed image construction+  , char+  , text'+  , string+  , imageWidth+  , splitImage+  , imageText   ) where -import           Control.Lens (Iso', iso)+import           Data.List (findIndex) import qualified Data.Text as S import qualified Data.Text.Lazy as L-import           Data.List-import           GHC.Generics+import           Data.Semigroup+import           Data.String import           Graphics.Vty.Attributes-import           Graphics.Vty.Image-import           Graphics.Vty.Image.Internal----- | Isomorphism between packed images and normal images.-_Image' :: Iso' Image' Image-_Image' = iso mirror (mirror . compress)-{-# INLINE _Image' #-}----- | Attempts to locate adjacent text sections with equal attributes--- so that they can be merged.-compress :: Image -> Image-compress = horizCat . map horizCat . groupBy textsWithEqAttr . flip horizList []---textsWithEqAttr :: Image -> Image -> Bool-textsWithEqAttr (HorizText a _ _ _) (HorizText b _ _ _) = a == b-textsWithEqAttr _                 _                     = False+import           Graphics.Vty.Image ((<|>), wcswidth, wcwidth)+import           Graphics.Vty.Image.Internal (Image(..))  -horizList :: Image -> [Image] -> [Image]-horizList (HorizJoin x y _ _) = horizList x . horizList y-horizList x = (x:)+unpackImage :: Image' -> Image+unpackImage i =+  case i of+    EmptyImage'          -> EmptyImage+    HorizText' a b c d e -> HorizText a (L.fromStrict b) c d <|> unpackImage e   -- | Packed, strict version of 'Image' used for long-term storage of images.@@ -54,67 +46,58 @@       {-# UNPACK #-} !S.Text       {-# UNPACK #-} !Int       {-# UNPACK #-} !Int-  | HorizJoin'       !Image'-      !Image'-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-  | VertJoin'-      !Image'-      !Image'-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-  | BGFill'-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-  | CropRight'-      !Image'-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-  | CropLeft'-      !Image'-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-  | CropBottom'-      !Image'-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-  | CropTop'-      !Image'-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int-      {-# UNPACK #-} !Int   | EmptyImage'-  deriving (Show, Generic)+  deriving (Show) -------------------------------------------------------------------------+instance Monoid Image' where+  mempty  = EmptyImage'+  mappend = (<>) -class    Mirror a      b      where mirror :: a -> b-instance Mirror Attr   Attr   where mirror = id-instance Mirror Int    Int    where mirror = id-instance Mirror L.Text S.Text where mirror = L.toStrict-instance Mirror S.Text L.Text where mirror = L.fromStrict-instance Mirror Image  Image' where mirror = to . gmirror . from-instance Mirror Image' Image  where mirror = to . gmirror . from+instance Semigroup Image' where+  -- maintain compressed form+  HorizText' a b c d EmptyImage' <> HorizText' a' b' c' d' rest+    | a == a' = HorizText' a (b <> b') (c + c') (d + d') rest -------------------------------------------------------------------------+  EmptyImage'          <> y = y+  HorizText' a b c d e <> y = HorizText' a b c d (e <> y) -class GMirror f g where-  gmirror :: f p -> g q+instance IsString Image' where fromString = string defAttr -instance GMirror f g => GMirror (M1 i c f) (M1 j d g) where-  gmirror (M1 x) = M1 (gmirror x)+text' :: Attr -> S.Text -> Image'+text' a s+  | S.null s  = EmptyImage'+  | otherwise = HorizText' a s (wcswidth (S.unpack s)) (S.length s) EmptyImage' -instance (GMirror f1 g1, GMirror f2 g2) => GMirror (f1 :*: f2) (g1 :*: g2) where-  gmirror (x :*: y) = gmirror x :*: gmirror y+char :: Attr -> Char -> Image'+char a c = HorizText' a (S.singleton c) (wcwidth c) 1 EmptyImage' -instance (GMirror f1 g1, GMirror f2 g2) => GMirror (f1 :+: f2) (g1 :+: g2) where-  gmirror (L1 x) = L1 (gmirror x)-  gmirror (R1 x) = R1 (gmirror x)+string :: Attr -> String -> Image'+string a s+  | null s    = EmptyImage'+  | otherwise = HorizText' a t (wcswidth s) (S.length t) EmptyImage'+  where t = S.pack s -instance GMirror U1 U1 where-  gmirror _ = U1+splitImage :: Int {- ^ image width -} -> Image' -> (Image',Image')+splitImage _ EmptyImage' = (EmptyImage', EmptyImage')+splitImage w (HorizText' a t w' l rest)+  | w >= w' = case splitImage (w-w') rest of+                (x,y) -> (HorizText' a t w' l x, y)+  | otherwise = (text' a (S.take i t), text' a (S.drop i t) <> rest)+  where+    ws = scanl1 (+) (map wcwidth (S.unpack t))+    i  = case findIndex (> w) ws of+           Nothing -> 0+           Just ix -> ix -instance Mirror a b => GMirror (K1 i a) (K1 j b) where-  gmirror (K1 x) = K1 (mirror x)+imageWidth :: Image' -> Int+imageWidth = go 0+  where+    go acc EmptyImage'            = acc+    go acc (HorizText' _ _ w _ x) = go (acc + w) x++imageText :: Image' -> L.Text+imageText = L.fromChunks . go+  where+    go EmptyImage' = []+    go (HorizText' _ t _ _ xs) = t : go xs
src/Client/Image/StatusLine.hs view
@@ -16,6 +16,7 @@   , minorStatusLineImage   ) where +import           Client.Image.Message (cleanText) import           Client.Image.Palette import           Client.State import           Client.State.Channel@@ -32,39 +33,63 @@ import           Irc.Identifier (Identifier, idText) import           Numeric +bar :: Char+bar = '━'+ -- | Renders the status line between messages and the textbox. statusLineImage ::+  Int         {- ^ draw width   -} ->   ClientState {- ^ client state -} ->   Image       {- ^ status bar   -}-statusLineImage st = content <|> charFill defAttr '─' fillSize 1+statusLineImage w st =+  makeLines w (common : activity ++ errorImgs)   where-    fillSize = max 0 (view clientWidth st - imageWidth content)-    contentSansActivity = horizCat+    common = horizCat       [ myNickImage st-      , focusImage st+      , focusImage (view clientFocus st) st+      , subfocusImage st       , detailImage st-      , nometaImage st+      , nometaImage (view clientFocus st) st       , scrollImage st       , filterImage st       , latencyImage st       ] -    content-      | view clientActivityBar st =-          makeLines (view clientWidth st) (contentSansActivity : activityBarImages st)-      | otherwise = contentSansActivity <|> activitySummary st+    activity+      | view clientActivityBar st = activityBarImages st+      | otherwise                 = [activitySummary st] +    errorImgs =+      transientErrorImage <$> maybeToList (view clientErrorMsg st) ++-- Generates an error message notification image.+transientErrorImage ::+  Text  {- ^ @error-message@           -} ->+  Image {- ^ @─[error: error-message]@ -}+transientErrorImage txt =+  text' defAttr "─[" <|>+  text' (withForeColor defAttr red) "error: " <|>+  text' defAttr (cleanText txt) <|>+  text' defAttr "]"++ -- | The minor status line is used when rendering the @/splits@ and -- @/mentions@ views to show the associated window name.-minorStatusLineImage :: Focus -> ClientState -> Image-minorStatusLineImage focus st =-  content <|> charFill defAttr '─' fillSize 1+minorStatusLineImage ::+  Focus {- ^ window name          -} ->+  Int   {- ^ draw width           -} ->+  Bool  {- ^ show hidemeta status -} ->+  ClientState -> Image+minorStatusLineImage focus w showHideMeta st =+  content <|> charFill defAttr bar fillSize 1   where-    content = infoBubble (focusImageMajor focus st)-    fillSize = max 0 (view clientWidth st - imageWidth content)+    content = focusImage focus st <|>+              if showHideMeta then nometaImage focus st else mempty +    fillSize = max 0 (w - imageWidth content) + -- | Indicate when the client is scrolling and old messages are being shown. scrollImage :: ClientState -> Image scrollImage st@@ -118,7 +143,7 @@ -- | Wrap some text in parentheses to make it suitable for inclusion in the -- status line. infoBubble :: Image -> Image-infoBubble img = string defAttr "─(" <|> img <|> string defAttr ")"+infoBubble img = string defAttr (bar:"(") <|> img <|> string defAttr ")"   -- | Indicate that the client is in the /detailed/ view.@@ -133,13 +158,14 @@  -- | Indicate that the client isn't showing the metadata lines in /normal/ -- view.-nometaImage :: ClientState -> Image-nometaImage st-  | view clientShowMetadata st = emptyImage-  | otherwise = infoBubble (string attr "nometa")+nometaImage :: Focus -> ClientState -> Image+nometaImage focus st+  | metaHidden = infoBubble (string attr "nometa")+  | otherwise  = emptyImage   where-    pal  = clientPalette st-    attr = view palLabel pal+    pal        = clientPalette st+    attr       = view palLabel pal+    metaHidden = orOf (clientWindows . ix focus . winHideMeta) st  -- | Image for little box with active window names: --@@ -147,7 +173,7 @@ activitySummary :: ClientState -> Image activitySummary st   | null indicators = emptyImage-  | otherwise       = string defAttr "─[" <|>+  | otherwise       = string defAttr (bar:"[") <|>                       horizCat indicators <|>                       string defAttr "]"   where@@ -179,7 +205,7 @@     baraux i (focus,w)       | n == 0 = Nothing -- todo: make configurable       | otherwise = Just-                  $ string defAttr "─[" <|>+                  $ string defAttr (bar:"[") <|>                     char (view palWindowName pal) i <|>                     char defAttr              ':' <|>                     text' (view palLabel pal) focusText <|>@@ -218,7 +244,7 @@       = go acc' ys      go acc ys = makeLines w ys-            <-> acc <|> charFill defAttr '─' (max 0 (w - imageWidth acc)) 1+            <-> acc <|> charFill defAttr bar (max 0 (w - imageWidth acc)) 1   myNickImage :: ClientState -> Image@@ -243,19 +269,14 @@                 Just chan -> view (csChannels . ix chan . chanUsers . ix nick) cs  -focusImage :: ClientState -> Image-focusImage st =-    infoBubble (focusImageMajor focus st) <|>-    foldMap infoBubble (viewSubfocusLabel pal subfocus)+subfocusImage :: ClientState -> Image+subfocusImage st = foldMap infoBubble (viewSubfocusLabel pal subfocus)   where--    !pal        = clientPalette st-    focus       = view clientFocus st+    pal         = clientPalette st     subfocus    = view clientSubfocus st -focusImageMajor :: Focus -> ClientState -> Image-focusImageMajor focus st =-  horizCat+focusImage :: Focus -> ClientState -> Image+focusImage focus st = infoBubble $ horizCat     [ char (view palWindowName pal) windowName     , char defAttr ':'     , viewFocusLabel st focus@@ -306,6 +327,7 @@     FocusMentions -> Just $ string (view palLabel pal) "mentions"     FocusPalette  -> Just $ string (view palLabel pal) "palette"     FocusDigraphs -> Just $ string (view palLabel pal) "digraphs"+    FocusKeyMap   -> Just $ string (view palLabel pal) "keymap"     FocusHelp mb  -> Just $ string (view palLabel pal) "help" <|>                             opt mb     FocusMasks m  -> Just $ horizCat
src/Client/Image/Textbox.hs view
@@ -22,34 +22,35 @@ import           Client.Commands.Recognizer import           Client.Image.Arguments import           Client.Image.MircFormatting+import           Client.Image.PackedImage import           Client.Image.Palette import           Client.State import qualified Client.State.EditBox as Edit import           Control.Lens import           Data.Char import           Data.List-import           Data.Monoid+import           Data.Semigroup import qualified Data.Text as Text import           Graphics.Vty.Attributes-import           Graphics.Vty.Image+import qualified Graphics.Vty.Image as Vty  -- | Compute the UI image for the text input box. This computes -- the logical cursor position on the screen to compensate for -- VTY's cursor placement behavior.-textboxImage :: ClientState -> (Int, Int, Image) -- ^ cursor column, new offset, image-textboxImage st+textboxImage :: Int -> ClientState -> (Int, Int, Vty.Image) -- ^ cursor column, new offset, image+textboxImage width st   = (newPos, newOffset, croppedImage)   where-  width = view clientWidth st   macros = views (clientConfig . configMacros) (fmap macroSpec) st   (txt, content) =      views (clientTextBox . Edit.content) (renderContent macros pal) st -  lineImage = beginning <|> content <|> ending+  lineImage = unpackImage (beginning <> content <> ending)    leftOfCurWidth = myWcswidth ('^':txt) -  croppedImage = cropLeft (imageWidth lineImage - newOffset) lineImage+  croppedImage = Vty.resizeWidth width+               $ Vty.cropLeft (Vty.imageWidth lineImage - newOffset) lineImage    cursorAnchor = width * 3 `quot` 4 @@ -76,9 +77,9 @@ -- 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 -}+  Palette              {- ^ palette                               -} ->+  Edit.Content         {- ^ content                               -} ->+  (String, Image')     {- ^ plain text rendering, image rendering -} renderContent macros pal c = (txt, wholeImg)   where   as  = reverse (view Edit.above c)@@ -91,7 +92,7 @@   -- ["one","two"] "three" --> "two one three"   txt = foldl (\acc x -> x ++ ' ' : acc) leftCur as -  wholeImg = horizCat+  wholeImg = mconcat            $ intersperse (plainText "\n")            $ map renderOtherLine as           ++ renderLine macros pal curTxt@@ -103,7 +104,7 @@ myWcwidth :: Char -> Int myWcwidth x   | isControl x = 1-  | otherwise   = wcwidth x+  | otherwise   = Vty.wcwidth x  -- | Version of 'wcswidth' that accounts for how control characters are -- rendered@@ -112,40 +113,39 @@   -- | Render an unfocused line-renderOtherLine :: String -> Image+renderOtherLine :: String -> Image' renderOtherLine = parseIrcTextExplicit . Text.pack  -- | Render the active text box line using command highlighting and -- placeholders, and WYSIWYG mIRC formatting control characters.-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 :: 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 - (cmd, rest) = break isSpace xs- allCommands = (Left <$> macros) <> (Right <$> commands)- (attr, continue)-   = case recognize (Text.pack cmd) allCommands of-       Exact (Right Command{cmdArgumentSpec = 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-         )+    (cmd, rest) = break isSpace xs+    allCommands = (Left <$> macros) <> (Right <$> commands)+    (attr, continue)+      = case recognize (Text.pack cmd) allCommands of+          Exact (Right Command{cmdArgumentSpec = 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)-
− src/Client/Image/Utils.hs
@@ -1,67 +0,0 @@-{-|-Module      : Client.Image.Utils-Description : Chat message view-Copyright   : (c) Eric Mertens, 2016-License     : ISC-Maintainer  : emertens@gmail.com--Provides utilities for formatting Vty Images.--}--module Client.Image.Utils (lineWrap) where--import           Graphics.Vty.Image-import           Graphics.Vty.Attributes---- | Given an image, break the image up into chunks of at most the--- given width and stack the resulting chunks vertically top-to-bottom.-lineWrap ::-  Int       {- ^ terminal width       -} ->-  Maybe Int {- ^ optional indentation -} ->-  Image     {- ^ unwrapped image      -} ->-  Image     {- ^ wrapped image        -}-lineWrap w mi img-  | imageWidth img == 0 = emptyImage-  | imageWidth img <= w = terminate w img-  | otherwise =-      terminate w (cropRight w img) <->-      maybe (lineWrapNoIndent w) (lineWrapIndent w) mi-            (cropLeft (imageWidth img - w) img)---- | Trailing space with default attributes deals with bug in VTY--- where the formatting will continue past the end of chat messages.--- This adds an extra space if a line doesn't end on the terminal edge.-terminate ::-  Int   {- ^ terminal width  -} ->-  Image {- ^ unwrapped image -} ->-  Image {- ^ wrapped image   -}-terminate n img-  | imageWidth img == n = img-  | otherwise           = img <|> char defAttr ' '--lineWrapNoIndent ::-  Int   {- ^ terminal width  -} ->-  Image {- ^ unwrapped image -} ->-  Image {- ^ wrapped image   -}-lineWrapNoIndent w img-  | iw <= w   = terminate w img-  | otherwise = cropRight w img <->-                lineWrapNoIndent w (cropLeft (iw - w) img)-  where-    iw = imageWidth img--lineWrapIndent ::-  Int   {- ^ terminal width  -} ->-  Int   {- ^ indentation     -} ->-  Image {- ^ unwrapped image -} ->-  Image {- ^ wrapped image   -}-lineWrapIndent w i img-  | 20 + i >  w = lineWrapNoIndent w img -- ensure we stop wrapping when it doesn't make sense-  | iw + i <= w = terminate w (leftPad i img)-  | otherwise   = leftPad i (cropRight (w-i) img) <->-                  lineWrapIndent w i (cropLeft (iw - w + i) img)-  where-    iw = imageWidth img--leftPad :: Int -> Image -> Image-leftPad i = pad i 0 0 0
src/Client/Options.hs view
@@ -38,7 +38,6 @@ import           Data.List import           Data.Text (Text) import qualified Data.Text as Text-import qualified Data.Text.IO as Text import           Data.Version import           Development.GitRev (gitHash, gitDirty) import           System.Console.GetOpt@@ -111,12 +110,20 @@               traverse_ (hPutStr stderr) (map bullet errors)               hPutStrLn stderr tryHelpTxt -     if | view optShowHelp    opts -> putStr helpTxt >> exitSuccess-        | view optShowFullVersion opts -> putStr fullVersionTxt >> exitSuccess-        | view optShowVersion opts -> putStr versionTxt >> exitSuccess-        | view optShowConfigFormat opts -> Text.putStr (generateDocs configurationSpec) >> exitSuccess-        | null errors              -> return opts-        | otherwise                -> reportErrors >> exitFailure+     if | view optShowHelp         opts -> putStr helpTxt        >> exitSuccess+        | view optShowFullVersion  opts -> putStr fullVersionTxt >> exitSuccess+        | view optShowVersion      opts -> putStr versionTxt     >> exitSuccess+        | view optShowConfigFormat opts -> printConfigFormat     >> exitSuccess+        | null errors                   -> return opts+        | otherwise                     -> reportErrors          >> exitFailure++printConfigFormat :: IO ()+printConfigFormat =+  do path <- getNewConfigPath+     putStrLn ""+     putStrLn ("Default configuration file path: " ++ path)+     putStrLn ""+     print (generateDocs configurationSpec)  helpTxt :: String helpTxt = usageInfo "glirc2 [FLAGS] INITIAL_NETWORKS..." options
src/Client/State.hs view
@@ -30,7 +30,6 @@   , clientScroll   , clientDetailView   , clientActivityBar-  , clientShowMetadata   , clientSubfocus   , clientNextConnectionId   , clientNetworkMap@@ -41,6 +40,8 @@   , clientRegex   , clientLogQueue   , clientActivityReturn+  , clientErrorMsg+  , clientLayout    -- * Client operations   , withClientState@@ -49,6 +50,7 @@   , clientPark   , clientMatcher   , clientActiveRegex+  , clientToggleHideMeta    , consumeInput   , currentCompletionList@@ -103,7 +105,6 @@ import           Client.Configuration import           Client.Configuration.ServerSettings import           Client.Image.Message-import           Client.Image.PackedImage import           Client.Image.Palette import           Client.Log import           Client.Message@@ -168,8 +169,8 @@   , _clientScroll            :: !Int                      -- ^ buffer scroll lines   , _clientDetailView        :: !Bool                     -- ^ use detailed rendering mode   , _clientActivityBar       :: !Bool                     -- ^ visible activity bar-  , _clientShowMetadata      :: !Bool                     -- ^ visible activity bar   , _clientRegex             :: Maybe Regex               -- ^ optional persistent filter+  , _clientLayout            :: !LayoutMode               -- ^ layout mode for split screen    , _clientBell              :: !Bool                     -- ^ sound a bell next draw @@ -177,6 +178,7 @@    , _clientExtensions        :: !ExtensionState           -- ^ state of loaded extensions   , _clientLogQueue          :: ![LogLine]                -- ^ log lines ready to write+  , _clientErrorMsg          :: Maybe Text                -- ^ transient error box text   }  @@ -249,13 +251,14 @@         , _clientConfig            = cfg         , _clientScroll            = 0         , _clientDetailView        = False-        , _clientShowMetadata      = True         , _clientRegex             = Nothing+        , _clientLayout            = view configLayout cfg         , _clientActivityBar       = view configActivityBar cfg         , _clientNextConnectionId  = 0         , _clientBell              = False         , _clientExtensions        = exts         , _clientLogQueue          = []+        , _clientErrorMsg          = Nothing         }  withExtensionState :: (ExtensionState -> IO a) -> IO a@@ -410,6 +413,7 @@   ClientMessage ->   ClientState -> ClientState recordIrcMessage network target msg st =+  updateTransientError msg $   case target of     TargetHidden      -> st     TargetNetwork     -> recordNetworkMessage msg st@@ -452,9 +456,28 @@          . csChannels . ix channel          . chanUsers  . ix user ++-- | Detect /error/ messages and add the message text to the transient+-- error display.+updateTransientError :: ClientMessage -> ClientState -> ClientState+updateTransientError msg =++  let err = set clientErrorMsg . Just in++  case view msgBody msg of+    ErrorBody txt       -> err txt+    IrcBody (Error txt) -> err txt+    IrcBody (Reply code args)+      | let info = replyCodeInfo code+      , ErrorReply <- replyCodeType info ->+          err (Text.intercalate " " (replyCodeText info : drop 1 args))+    _ -> id++ -- | Record a message on a network window recordNetworkMessage :: ClientMessage -> ClientState -> ClientState-recordNetworkMessage msg st = recordWindowLine focus wl st+recordNetworkMessage msg st = updateTransientError msg+                            $ recordWindowLine focus wl st   where     network    = view msgNetwork msg     focus      | Text.null network = Unfocused@@ -464,6 +487,7 @@      cfg        = view clientConfig st + -- | Record window line at the given focus creating the window if necessary recordWindowLine ::   Focus ->@@ -472,8 +496,9 @@   ClientState recordWindowLine focus wl st = st2   where+    freshWindow = emptyWindow { _winHideMeta = view (clientConfig . configHideMeta) st }     st1 = over (clientWindows . at focus)-               (\w -> Just $! addToWindow wl (fromMaybe emptyWindow w))+               (\w -> Just $! addToWindow wl (fromMaybe freshWindow w))                st      st2@@ -490,8 +515,8 @@ toWindowLine params importance msg = WindowLine   { _wlSummary    = msgSummary (view msgBody msg)   , _wlText       = msgText (view msgBody msg)-  , _wlImage'     = _Image' # mkImage NormalRender-  , _wlFullImage' = _Image' # mkImage DetailedRender+  , _wlImage      = mkImage NormalRender+  , _wlFullImage  = mkImage DetailedRender   , _wlImportance = importance   , _wlTimestamp  = zonedTimeToUTC (view msgTime msg)   }@@ -859,7 +884,8 @@   ClientState {- ^ client state -} ->   ClientState changeSubfocus focus-  = set clientScroll 0+  = set clientErrorMsg Nothing+  . set clientScroll 0   . set clientSubfocus focus  -- | Return to previously focused window.@@ -946,3 +972,8 @@   [ network | (network, cfg) <- views (clientConfig . configServers) HashMap.toList st             , view ssAutoconnect cfg             ]++-- | Toggle the /hide metadata/ setting for the focused window.+clientToggleHideMeta :: ClientState -> ClientState+clientToggleHideMeta st =+  overStrict (clientWindows . ix (view clientFocus st) . winHideMeta) not st
src/Client/State/Focus.hs view
@@ -51,6 +51,7 @@   | FocusPalette     -- ^ Show current palette   | FocusMentions    -- ^ Show all mentions   | FocusDigraphs    -- ^ Show all digraphs+  | FocusKeyMap      -- ^ Show key bindings   | FocusHelp (Maybe Text) -- ^ Show help window with optional command   deriving (Eq,Show) 
src/Client/State/Window.hs view
@@ -20,6 +20,7 @@   , winTotal   , winMention   , winMarker+  , winHideMeta    -- * Window lines   , WindowLine(..)@@ -46,14 +47,13 @@ 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   { _wlSummary    :: !IrcSummary  -- ^ Summary value   , _wlText       :: {-# UNPACK #-} !Text -- ^ Searchable text form-  , _wlImage'     :: !Image'      -- ^ Normal rendered image-  , _wlFullImage' :: !Image'      -- ^ Detailed rendered image+  , _wlImage      :: !Image'      -- ^ Normal rendered image+  , _wlFullImage  :: !Image'      -- ^ Detailed rendered image   , _wlImportance :: !WindowLineImportance -- ^ Importance of message   , _wlTimestamp  :: {-# UNPACK #-} !UTCTime   }@@ -70,6 +70,7 @@   , _winUnread   :: !Int           -- ^ Messages added since buffer was visible   , _winTotal    :: !Int           -- ^ Messages in buffer   , _winMention  :: !WindowLineImportance -- ^ Indicates an important event is unread+  , _winHideMeta :: !Bool          -- ^ Hide metadata messages   }  data ActivityLevel = NoActivity | NormalActivity | HighActivity@@ -86,17 +87,6 @@ makeLenses ''WindowLine  --- | Lens for the '_wlImage' field viewed in unpacked form.-wlImage :: Lens' WindowLine Image-wlImage = wlImage' . _Image'-{-# INLINE wlImage #-}---- | Lens for the '_wlFullImage' field viewed in unpacked form.-wlFullImage :: Lens' WindowLine Image-wlFullImage = wlFullImage' . _Image'-{-# INLINE wlFullImage #-}-- -- | A window with no messages emptyWindow :: Window emptyWindow = Window@@ -105,6 +95,7 @@   , _winUnread   = 0   , _winTotal    = 0   , _winMention  = WLBoring+  , _winHideMeta = False   }  -- | Adds a given line to a window as the newest message. Window's@@ -118,6 +109,7 @@                      then view winUnread win                      else view winUnread win + 1     , _winMention  = max (view winMention win) (view wlImportance msg)+    , _winHideMeta = view winHideMeta win     }  -- | Update the window clearing the unread count and important flag.
src/Client/View.hs view
@@ -18,6 +18,7 @@ import           Client.View.ChannelInfo import           Client.View.Digraphs import           Client.View.Help+import           Client.View.KeyMap import           Client.View.MaskList import           Client.View.Mentions import           Client.View.Messages@@ -28,8 +29,8 @@ import           Control.Lens import           Graphics.Vty.Image -viewLines :: Focus -> Subfocus -> ClientState -> [Image]-viewLines focus subfocus !st =+viewLines :: Focus -> Subfocus -> Int -> ClientState -> [Image]+viewLines focus subfocus w !st =   case (focus, subfocus) of     _ | Just ("url",arg) <- clientActiveCommand st ->       urlSelectionView focus arg st@@ -39,12 +40,13 @@       | view clientDetailView st -> userInfoImages network channel st       | otherwise                -> userListImages network channel st     (ChannelFocus network channel, FocusMasks mode) ->-      maskListImages mode network channel st+      maskListImages mode network channel w st     (_, FocusWindows filt) -> windowsImages filt st-    (_, FocusMentions) -> mentionsViewLines st+    (_, FocusMentions) -> mentionsViewLines w st     (_, FocusPalette) -> paletteViewLines pal-    (_, FocusDigraphs) -> digraphLines st+    (_, FocusDigraphs) -> digraphLines w st+    (_, FocusKeyMap) -> keyMapLines st     (_, FocusHelp mb) -> helpImageLines mb pal-    _ -> chatMessageImages focus st+    _ -> chatMessageImages focus w st   where     pal = clientPalette st
src/Client/View/ChannelInfo.hs view
@@ -18,6 +18,7 @@  import           Client.Image.Message import           Client.Image.MircFormatting+import           Client.Image.PackedImage (unpackImage) import           Client.Image.Palette import           Client.State import           Client.State.Channel@@ -55,7 +56,8 @@   where     label = text' (view palLabel pal) -    topicLine = label "Topic: " <|> parseIrcText (view chanTopic channel)+    topicLine = label "Topic: " <|>+                unpackImage (parseIrcText (view chanTopic channel))       utcTimeImage = string defAttr . formatTime defaultTimeLocale "%F %T"@@ -65,7 +67,9 @@           Nothing -> []           Just !prov ->             [ label "Topic set by: " <|>-                coloredUserInfo pal DetailedRender myNicks (view topicAuthor prov)+                unpackImage+                  (coloredUserInfo+                    pal DetailedRender myNicks (view topicAuthor prov))             , label "Topic set on: " <|> utcTimeImage (view topicTime prov)             ] @@ -77,5 +81,5 @@     urlLines =         case view chanUrl channel of           Nothing -> []-          Just url -> [ label "Channel URL: " <|> parseIrcText url ]+          Just url -> [ label "Channel URL: " <|> unpackImage (parseIrcText url) ] 
src/Client/View/Digraphs.hs view
@@ -6,14 +6,13 @@ License     : ISC Maintainer  : emertens@gmail.com -This module provides an view of the built-in digraph list.+This module provides a view of the built-in digraph list.  -} module Client.View.Digraphs (digraphLines) where  import           Client.Image.Message (cleanChar) import           Client.State-import           Control.Lens import           Data.List import           Data.List.Split import qualified Data.Text as Text@@ -23,9 +22,10 @@  -- | Render the lines of a table showing all of the available digraph entries digraphLines ::+  Int         {- ^ draw width   -} ->   ClientState {- ^ client state -} ->   [Image]     {- ^ output lines -}-digraphLines st+digraphLines w st   = map (horizCat . intersperse sep)   $ chunksOf entriesPerLine   $ map (text' defAttr)@@ -33,7 +33,6 @@   $ map (Text.pack . drawEntry)   $ digraphListToList digraphs   where-    w              = view clientWidth st     matcher        = maybe id filter (clientMatcher st)     entriesPerLine = max 1 -- just in case?                    $ (w + sepWidth) `quot` (entryWidth + sepWidth)
src/Client/View/Help.hs view
@@ -18,6 +18,7 @@ import           Client.Commands.Arguments import           Client.Image.Arguments import           Client.Image.MircFormatting+import           Client.Image.PackedImage import           Client.Image.Palette import           Client.Commands.Recognizer import           Control.Lens@@ -28,7 +29,7 @@ import           Data.Text (Text) import qualified Data.Text as Text import           Graphics.Vty.Attributes-import           Graphics.Vty.Image+import           Graphics.Vty.Image (Image)  -- | Generate either the list of all commands and their arguments, -- or when given a command name generate the detailed help text@@ -37,16 +38,16 @@   Maybe Text {- ^ optional command name -} ->   Palette    {- ^ palette               -} ->   [Image]    {- ^ help lines            -}-helpImageLines mbCmd pal =+helpImageLines mbCmd pal = map unpackImage $   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        -}+  Text     {- ^ command name -} ->+  Palette  {- ^ palette      -} ->+  [Image'] {- ^ lines        -} commandHelpLines cmdName pal =   case recognize cmdName commands of     Invalid -> [string (view palError pal) "Unknown command, try /help"]@@ -73,7 +74,7 @@ -- implementation will be valid. explainContext ::   CommandImpl a {- ^ command implementation -} ->-  Image         {- ^ help line              -}+  Image'        {- ^ help line              -} explainContext impl =   case impl of     ClientCommand {} -> go "client command" "works everywhere"@@ -81,13 +82,13 @@     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 <|>+    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 -}+  Palette  {- ^ palette    -} ->+  [Image'] {- ^ help lines -} listAllCommands pal   = intercalate [emptyLine]   $ map reverse@@ -96,7 +97,7 @@ listCommandSection ::   Palette        {- ^ palette         -} ->   CommandSection {- ^ command section -} ->-  [Image]        {- ^ help lines      -}+  [Image']       {- ^ help lines      -} listCommandSection pal sec   = text' (withStyle defAttr bold) (cmdSectionName sec)   : [ commandSummary pal names spec@@ -112,15 +113,15 @@   Palette        {- ^ palette                  -} ->   NonEmpty Text  {- ^ command name and aliases -} ->   ArgumentSpec a {- ^ argument specification   -} ->-  Image          {- ^ summary help line        -}+  Image'         {- ^ summary help line        -} commandSummary pal (cmd :| _) args  =-  char defAttr '/' <|>-  text' (view palCommand pal) cmd <|>+  char defAttr '/' <>+  text' (view palCommand pal) cmd <>   argumentsImage pal' args ""    where     pal' = set palCommandPlaceholder defAttr pal  -- Empty line used as a separator-emptyLine :: Image-emptyLine = text' defAttr " "+emptyLine :: Image'+emptyLine = char defAttr ' '
+ src/Client/View/KeyMap.hs view
@@ -0,0 +1,47 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.View.KeyMap+Description : List of the current key map+Copyright   : (c) Eric Mertens, 2017+License     : ISC+Maintainer  : emertens@gmail.com++This module provides a view of the key bindings map++-}+module Client.View.KeyMap (keyMapLines) where++import           Client.Configuration+import           Client.EventLoop.Actions+import           Client.State+import           Control.Lens+import           Data.List+import           Data.Ord+import           Graphics.Vty.Attributes+import           Graphics.Vty.Image+import           Graphics.Vty.Input++-- | Render the lines of a table showing all of the available digraph entries+keyMapLines ::+  ClientState {- ^ client state -} ->+  [Image]     {- ^ output lines -}+keyMapLines+  = renderEntries+  . keyMapEntries+  . view (clientConfig . configKeyMap)++renderEntries :: [([Modifier], Key, Action)] -> [Image]+renderEntries entries =+  [ resizeWidth keyColWidth key <|> act | (key,act) <- images ]++  where+    third (_,_,x) = x++    images =+      [ ( string defAttr (prettyModifierKey mods key)+        , text'  defAttr (actionName act            )+        )+      | (mods,key,act) <- sortBy (comparing third) entries+      ]++    keyColWidth = 1 + maximum (0 : map (imageWidth . fst) images)
src/Client/View/MaskList.hs view
@@ -31,14 +31,15 @@  -- | Render the lines used in a channel mask list maskListImages ::-  Char        {- ^ Mask mode -} ->-  Text        {- ^ network   -} ->-  Identifier  {- ^ channel   -} ->+  Char        {- ^ Mask mode  -} ->+  Text        {- ^ network    -} ->+  Identifier  {- ^ channel    -} ->+  Int         {- ^ draw width -} ->   ClientState -> [Image]-maskListImages mode network channel st =+maskListImages mode network channel w st =   case mbEntries of     Nothing      -> [text' (view palError pal) "Mask list not loaded"]-    Just entries -> maskListImages' entries st+    Just entries -> maskListImages' entries w st    where     pal = clientPalette st@@ -48,8 +49,8 @@                 . chanLists . ix mode                 ) st -maskListImages' :: HashMap Text MaskListEntry -> ClientState -> [Image]-maskListImages' entries st = countImage : images+maskListImages' :: HashMap Text MaskListEntry -> Int -> ClientState -> [Image]+maskListImages' entries w st = countImage : images   where     pal = clientPalette st @@ -72,7 +73,7 @@     maskImages       = text' defAttr <$> masks     maskColumnWidth  = maximum (imageWidth <$> maskImages) + 1     paddedMaskImages = resizeWidth maskColumnWidth <$> maskImages-    width            = max 1 (view clientWidth st)+    width            = max 1 w      images = [ cropLine $ mask <|>                           text' defAttr who <|>
src/Client/View/Mentions.hs view
@@ -14,6 +14,7 @@   ( mentionsViewLines   ) where +import           Client.Image.PackedImage (Image', unpackImage) import           Client.Image.StatusLine import           Client.State import           Client.State.Focus@@ -26,8 +27,8 @@ -- | 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 st entries+mentionsViewLines :: Int -> ClientState -> [Image]+mentionsViewLines w st = addMarkers w st entries    where     names = clientWindowNames st ++ repeat '?'@@ -43,18 +44,19 @@   { mlTimestamp  :: UTCTime   , mlWindowName :: Char   , mlFocus      :: Focus-  , mlImage      :: Image+  , mlImage      :: Image'   }  addMarkers ::+  Int           {- ^ draw width                        -} ->   ClientState   {- ^ client state                      -} ->   [MentionLine] {- ^ list of mentions in time order    -} ->   [Image]       {- ^ mention images and channel labels -}-addMarkers _ [] = []-addMarkers !st (!ml : xs)-  = map mlImage (ml:same)- ++ minorStatusLineImage (mlFocus ml) st-  : addMarkers st rest+addMarkers _ _ [] = []+addMarkers w !st (!ml : xs)+  = map (unpackImage . mlImage) (ml:same)+ ++ minorStatusLineImage (mlFocus ml) w False st+  : addMarkers w st rest   where     isSame ml' = mlFocus ml == mlFocus ml' 
src/Client/View/Messages.hs view
@@ -15,9 +15,10 @@   ) where  import           Client.Configuration+import           Client.Image.LineWrap import           Client.Image.Message+import           Client.Image.PackedImage import           Client.Image.Palette-import           Client.Image.Utils import           Client.Message import           Client.State import           Client.State.Focus@@ -25,111 +26,135 @@ import           Client.State.Window import           Control.Lens import           Control.Monad+import           Data.Semigroup import           Graphics.Vty.Attributes-import           Graphics.Vty.Image+import qualified Graphics.Vty.Image as Vty import           Irc.Identifier import           Irc.Message  -chatMessageImages :: Focus -> ClientState -> [Image]-chatMessageImages focus st =+chatMessageImages :: Focus -> Int -> ClientState -> [Vty.Image]+chatMessageImages focus w st =   case preview (clientWindows . ix focus) st of     Nothing  -> []     Just win ->-      let msgs = toListOf each (view winMessages win) in+      let msgs     = toListOf each (view winMessages win)+          hideMeta = view winHideMeta win in       case clientMatcher st of-        Just matcher -> windowLineProcessor (filter (views wlText matcher) msgs)+        Just matcher -> windowLineProcessor hideMeta (filter (views wlText matcher) msgs)         Nothing ->           case view winMarker win of-            Nothing -> windowLineProcessor msgs+            Nothing -> windowLineProcessor hideMeta msgs             Just n  ->-              windowLineProcessor l +++              windowLineProcessor hideMeta l ++               [marker] ++-              windowLineProcessor r+              windowLineProcessor hideMeta r               where                 (l,r) = splitAt n msgs    where     palette = clientPalette st-    marker = string (view palLineMarker palette) (replicate (view clientWidth st) '-')-    windowLineProcessor+    marker = Vty.string (view palLineMarker palette) (replicate w '-')+    windowLineProcessor hideMeta       | view clientDetailView st =-          if view clientShowMetadata st-            then map (view wlFullImage)-            else detailedImagesWithoutMetadata st+          if hideMeta+            then detailedImagesWithoutMetadata st+            else map (views wlFullImage unpackImage) -      | otherwise = windowLinesToImages st . filter (not . isNoisy)+      | otherwise = windowLinesToImages st w hideMeta . filter (not . isNoisy)      isNoisy msg =       case view wlSummary msg of         ReplySummary code -> squelchIrcMsg (Reply code [])         _                 -> False -detailedImagesWithoutMetadata :: ClientState -> [WindowLine] -> [Image]+detailedImagesWithoutMetadata :: ClientState -> [WindowLine] -> [Vty.Image] detailedImagesWithoutMetadata st wwls =   case gatherMetadataLines st wwls of     ([], [])   -> []-    ([], w:ws) -> view wlFullImage w : detailedImagesWithoutMetadata st ws+    ([], w:ws) -> views wlFullImage unpackImage w+                : detailedImagesWithoutMetadata st ws     (_:_, wls) -> detailedImagesWithoutMetadata st wls -windowLinesToImages :: ClientState -> [WindowLine] -> [Image]-windowLinesToImages st wwls =++windowLinesToImages ::+  ClientState  {- ^ client state  -} ->+  Int          {- ^ draw width    -} ->+  Bool         {- ^ hide metadata -} ->+  [WindowLine] {- ^ window lines  -} ->+  [Vty.Image]  {- ^ image lines   -}+windowLinesToImages st w hideMeta wwls =   case gatherMetadataLines st wwls of     ([], [])   -> []-    ([], w:ws) -> lineWrap (view clientWidth st)+    ([], wl:wls) ->+                   map unpackImage+                         (lineWrapChat w                            (view (clientConfig . configIndentWrapped) st)-                           (view wlImage w)-                 : windowLinesToImages st ws+                           (view wlImage wl))+                ++ windowLinesToImages st w hideMeta wls     ((img,who,mbnext):mds, wls) -      | view clientShowMetadata st ->-         startMetadata img mbnext who mds palette-       : windowLinesToImages st wls+      | hideMeta -> windowLinesToImages st w hideMeta wls -      | otherwise -> windowLinesToImages st wls+      | otherwise ->+         mkLines w (startMetadata img mbnext who mds palette)+      ++ windowLinesToImages st w hideMeta wls+   where     palette = clientPalette st +mkLines :: Int -> [Vty.Image] -> [Vty.Image]+mkLines _ []     = []+mkLines w (x:xs) = reverse (mkLines1 w x xs)++mkLines1 :: Int -> Vty.Image -> [Vty.Image] -> [Vty.Image]+mkLines1 _ x []            = [x]+mkLines1 w x (y:ys)+  | Vty.imageWidth x' <= w = mkLines1 w x' ys+  | otherwise              = x : mkLines1 w y ys+  where+    x' = x Vty.<|> Vty.char defAttr ' ' Vty.<|> y+ ------------------------------------------------------------------------  type MetadataState =-  Identifier                            {- ^ current nick -} ->-  [(Image,Identifier,Maybe Identifier)] {- ^ metadata     -} ->-  Palette                               {- ^ palette      -} ->-  Image+  Identifier                             {- ^ current nick -} ->+  [(Image',Identifier,Maybe Identifier)] {- ^ metadata     -} ->+  Palette                                {- ^ palette      -} ->+  [Vty.Image]  startMetadata ::-  Image            {- ^ metadata image           -} ->+  Image'           {- ^ metadata image           -} ->   Maybe Identifier {- ^ possible nick transition -} ->   MetadataState startMetadata img mbnext who mds palette =-        quietIdentifier palette who-    <|> img-    <|> transitionMetadata mbnext who mds palette+  let acc = unpackImage (quietIdentifier palette who <> img)+  in transitionMetadata acc mbnext who mds palette  transitionMetadata ::+  Vty.Image ->   Maybe Identifier {- ^ possible nick transition -} ->   MetadataState-transitionMetadata mbwho who mds palette =+transitionMetadata acc mbwho who mds palette =   case mbwho of-    Nothing   -> continueMetadata who  mds palette-    Just who' -> quietIdentifier palette who'-             <|> continueMetadata who' mds palette+    Nothing   -> continueMetadata acc who mds palette+    Just who' ->+      let acc' = acc Vty.<|> unpackImage (quietIdentifier palette who')+      in continueMetadata acc' 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+continueMetadata :: Vty.Image -> MetadataState+continueMetadata acc _ [] _ = [acc]+continueMetadata acc who1 ((img, who2, mbwho3):mds) palette+  | who1 == who2 = let acc' = acc Vty.<|> unpackImage img+                   in transitionMetadata acc' mbwho3 who2 mds palette+  | otherwise    = acc : startMetadata img mbwho3 who2 mds palette  ------------------------------------------------------------------------  gatherMetadataLines ::   ClientState ->   [WindowLine] ->-  ( [(Image, Identifier, Maybe Identifier)] , [ WindowLine ] )+  ( [(Image', Identifier, Maybe Identifier)] , [ WindowLine ] )   -- ^ metadata entries are reversed gatherMetadataLines st = go []   where@@ -144,7 +169,7 @@ metadataWindowLine ::   ClientState ->   WindowLine ->-  Maybe (Image, Identifier, Maybe Identifier)+  Maybe (Image', Identifier, Maybe Identifier)         {- ^ Image, incoming identifier, outgoing identifier if changed -} metadataWindowLine st wl =   case view wlSummary wl of
src/Client/View/UrlSelection.hs view
@@ -15,6 +15,7 @@  import           Client.Configuration import           Client.Image.Message+import           Client.Image.PackedImage (unpackImage) import           Client.Image.Palette import           Client.Message import           Client.State@@ -84,8 +85,9 @@   (Maybe Identifier, Text)  {- ^ sender and url text       -} ->   Image                     {- ^ rendered line             -} draw me pal padding selected i (who,url) =-  rightPad NormalRender padding-    (foldMap (coloredIdentifier pal NormalIdentifier me) who) <|>+  unpackImage+   (rightPad NormalRender padding+    (foldMap (coloredIdentifier pal NormalIdentifier me) who)) <|>   string defAttr ": " <|>   string attr (shows i ". ") <|>   text' attr (cleanText url)
src/Client/View/UserList.hs view
@@ -14,6 +14,7 @@   ) where  import           Client.Image.Message+import           Client.Image.PackedImage (unpackImage) import           Client.Image.Palette import           Client.State import           Client.State.Channel@@ -59,7 +60,7 @@      renderUser (ident, sigils) =       string (view palSigil pal) sigils <|>-      coloredIdentifier pal NormalIdentifier myNicks ident+      unpackImage (coloredIdentifier pal NormalIdentifier myNicks ident)      gap = char defAttr ' ' @@ -109,7 +110,7 @@      renderEntry (info, sigils) =       string (view palSigil pal) sigils <|>-      coloredUserInfo pal DetailedRender myNicks info+      unpackImage (coloredUserInfo pal DetailedRender myNicks info)      matcher' (info,sigils) =       matcher (Text.pack sigils `Text.append` renderUserInfo info)