packages feed

matterhorn 90000.0.1 → 90000.1.0

raw patch · 14 files changed

+223/−73 lines, 14 filesdep ~brickdep ~vtyPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: brick, vty

API changes (from Hackage documentation)

- Matterhorn.Config: defaultConfig :: Config
+ Matterhorn.Config: [configChannelSelectCaseInsensitive] :: Config -> Bool
+ Matterhorn.Config: [configCharacterWidths] :: Config -> Maybe CharWidths
+ Matterhorn.FilePaths: charWidthsFileName :: FilePath
+ Matterhorn.Types: VMTabAuthorInfo :: ViewMessageWindowTab
+ Matterhorn.Types: [configChannelSelectCaseInsensitive] :: Config -> Bool
+ Matterhorn.Types: [configCharacterWidths] :: Config -> Maybe CharWidths
+ Matterhorn.Types: buildWidthMap :: CharWidths -> UnicodeWidthTable
+ Matterhorn.Types: configChannelSelectCaseInsensitiveL :: Lens' Config Bool
+ Matterhorn.Types: configCharacterWidthsL :: Lens' Config (Maybe CharWidths)
+ Matterhorn.Types: data CharWidths
+ Matterhorn.Types: instance GHC.Classes.Eq Matterhorn.Types.CharWidths
+ Matterhorn.Types: instance GHC.Show.Show Matterhorn.Types.CharWidths
+ Matterhorn.Types: newCharWidths :: [(Char, Int)] -> CharWidths
+ Matterhorn.Types.Core: ViewMessageAuthorArea :: TeamId -> Name
- Matterhorn.Config: Config :: Maybe Text -> Maybe Text -> Maybe Text -> Int -> Maybe Text -> Maybe PasswordSource -> Maybe TokenSource -> Maybe OTPTokenSource -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Bool -> Bool -> Maybe Text -> Bool -> Maybe Text -> NotificationVersion -> Bool -> Maybe Int -> Bool -> BackgroundInfo -> Bool -> Bool -> Bool -> Bool -> Maybe Text -> Bool -> Bool -> ChannelListWidth -> Int -> Bool -> ChannelListSorting -> TeamListSorting -> Bool -> Bool -> Maybe FilePath -> KeyConfig KeyEvent -> Bool -> [FilePath] -> Int -> CPUUsagePolicy -> Maybe FilePath -> ChannelListOrientation -> ThreadOrientation -> Bool -> Bool -> Config
+ Matterhorn.Config: Config :: Maybe Text -> Maybe Text -> Maybe Text -> Int -> Maybe Text -> Maybe PasswordSource -> Maybe TokenSource -> Maybe OTPTokenSource -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Bool -> Bool -> Maybe Text -> Bool -> Maybe Text -> NotificationVersion -> Bool -> Maybe Int -> Bool -> BackgroundInfo -> Bool -> Bool -> Bool -> Bool -> Maybe Text -> Bool -> Bool -> ChannelListWidth -> Int -> Bool -> ChannelListSorting -> TeamListSorting -> Bool -> Bool -> Maybe FilePath -> KeyConfig KeyEvent -> Bool -> [FilePath] -> Int -> CPUUsagePolicy -> Maybe FilePath -> ChannelListOrientation -> ThreadOrientation -> Bool -> Bool -> Bool -> Maybe CharWidths -> Config
- Matterhorn.Types: Config :: Maybe Text -> Maybe Text -> Maybe Text -> Int -> Maybe Text -> Maybe PasswordSource -> Maybe TokenSource -> Maybe OTPTokenSource -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Bool -> Bool -> Maybe Text -> Bool -> Maybe Text -> NotificationVersion -> Bool -> Maybe Int -> Bool -> BackgroundInfo -> Bool -> Bool -> Bool -> Bool -> Maybe Text -> Bool -> Bool -> ChannelListWidth -> Int -> Bool -> ChannelListSorting -> TeamListSorting -> Bool -> Bool -> Maybe FilePath -> KeyConfig KeyEvent -> Bool -> [FilePath] -> Int -> CPUUsagePolicy -> Maybe FilePath -> ChannelListOrientation -> ThreadOrientation -> Bool -> Bool -> Config
+ Matterhorn.Types: Config :: Maybe Text -> Maybe Text -> Maybe Text -> Int -> Maybe Text -> Maybe PasswordSource -> Maybe TokenSource -> Maybe OTPTokenSource -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Bool -> Bool -> Maybe Text -> Bool -> Maybe Text -> NotificationVersion -> Bool -> Maybe Int -> Bool -> BackgroundInfo -> Bool -> Bool -> Bool -> Bool -> Maybe Text -> Bool -> Bool -> ChannelListWidth -> Int -> Bool -> ChannelListSorting -> TeamListSorting -> Bool -> Bool -> Maybe FilePath -> KeyConfig KeyEvent -> Bool -> [FilePath] -> Int -> CPUUsagePolicy -> Maybe FilePath -> ChannelListOrientation -> ThreadOrientation -> Bool -> Bool -> Bool -> Maybe CharWidths -> Config
- Matterhorn.Types: listWindowEnterHandler :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) (a_a1oXd -> MH Bool)
+ Matterhorn.Types: listWindowEnterHandler :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) (a_a1pcH -> MH Bool)
- Matterhorn.Types: listWindowFetchResults :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) (b_a1oXe -> Session -> Text -> IO (Vector a_a1oXd))
+ Matterhorn.Types: listWindowFetchResults :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) (b_a1pcI -> Session -> Text -> IO (Vector a_a1pcH))
- Matterhorn.Types: listWindowNewList :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) (Vector a_a1oXd -> List Name a_a1oXd)
+ Matterhorn.Types: listWindowNewList :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) (Vector a_a1pcH -> List Name a_a1pcH)
- Matterhorn.Types: listWindowRecordCount :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) (Maybe Int)
+ Matterhorn.Types: listWindowRecordCount :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) (Maybe Int)
- Matterhorn.Types: listWindowSearchInput :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) (Editor Text Name)
+ Matterhorn.Types: listWindowSearchInput :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) (Editor Text Name)
- Matterhorn.Types: listWindowSearchResults :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) (List Name a_a1oXd)
+ Matterhorn.Types: listWindowSearchResults :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) (List Name a_a1pcH)
- Matterhorn.Types: listWindowSearchScope :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) b_a1oXe
+ Matterhorn.Types: listWindowSearchScope :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) b_a1pcI
- Matterhorn.Types: listWindowSearching :: forall a_a1oXd b_a1oXe. Lens' (ListWindowState a_a1oXd b_a1oXe) Bool
+ Matterhorn.Types: listWindowSearching :: forall a_a1pcH b_a1pcI. Lens' (ListWindowState a_a1pcH b_a1pcI) Bool
- Matterhorn.Types.EditState: acCachedResponses :: forall n_a18J0. Lens' (AutocompleteState n_a18J0) (HashMap Text [AutocompleteAlternative])
+ Matterhorn.Types.EditState: acCachedResponses :: forall n_a18Yp. Lens' (AutocompleteState n_a18Yp) (HashMap Text [AutocompleteAlternative])
- Matterhorn.Types.EditState: acCompletionList :: forall n_a18J0 n_a19kn. Lens (AutocompleteState n_a18J0) (AutocompleteState n_a19kn) (List n_a18J0 AutocompleteAlternative) (List n_a19kn AutocompleteAlternative)
+ Matterhorn.Types.EditState: acCompletionList :: forall n_a18Yp n_a19zM. Lens (AutocompleteState n_a18Yp) (AutocompleteState n_a19zM) (List n_a18Yp AutocompleteAlternative) (List n_a19zM AutocompleteAlternative)
- Matterhorn.Types.EditState: acPreviousSearchString :: forall n_a18J0. Lens' (AutocompleteState n_a18J0) Text
+ Matterhorn.Types.EditState: acPreviousSearchString :: forall n_a18Yp. Lens' (AutocompleteState n_a18Yp) Text
- Matterhorn.Types.EditState: acType :: forall n_a18J0. Lens' (AutocompleteState n_a18J0) AutocompletionType
+ Matterhorn.Types.EditState: acType :: forall n_a18Yp. Lens' (AutocompleteState n_a18Yp) AutocompletionType
- Matterhorn.Types.EditState: esAttachmentList :: forall n_a18IZ. Lens' (EditState n_a18IZ) (List n_a18IZ AttachmentData)
+ Matterhorn.Types.EditState: esAttachmentList :: forall n_a18Yo. Lens' (EditState n_a18Yo) (List n_a18Yo AttachmentData)
- Matterhorn.Types.EditState: esAutocomplete :: forall n_a18IZ. Lens' (EditState n_a18IZ) (Maybe (AutocompleteState n_a18IZ))
+ Matterhorn.Types.EditState: esAutocomplete :: forall n_a18Yo. Lens' (EditState n_a18Yo) (Maybe (AutocompleteState n_a18Yo))
- Matterhorn.Types.EditState: esAutocompletePending :: forall n_a18IZ. Lens' (EditState n_a18IZ) (Maybe Text)
+ Matterhorn.Types.EditState: esAutocompletePending :: forall n_a18Yo. Lens' (EditState n_a18Yo) (Maybe Text)
- Matterhorn.Types.EditState: esChannelId :: forall n_a18IZ. Lens' (EditState n_a18IZ) ChannelId
+ Matterhorn.Types.EditState: esChannelId :: forall n_a18Yo. Lens' (EditState n_a18Yo) ChannelId
- Matterhorn.Types.EditState: esEditMode :: forall n_a18IZ. Lens' (EditState n_a18IZ) EditMode
+ Matterhorn.Types.EditState: esEditMode :: forall n_a18Yo. Lens' (EditState n_a18Yo) EditMode
- Matterhorn.Types.EditState: esEditor :: forall n_a18IZ. Lens' (EditState n_a18IZ) (Editor Text n_a18IZ)
+ Matterhorn.Types.EditState: esEditor :: forall n_a18Yo. Lens' (EditState n_a18Yo) (Editor Text n_a18Yo)
- Matterhorn.Types.EditState: esEphemeral :: forall n_a18IZ. Lens' (EditState n_a18IZ) EphemeralEditState
+ Matterhorn.Types.EditState: esEphemeral :: forall n_a18Yo. Lens' (EditState n_a18Yo) EphemeralEditState
- Matterhorn.Types.EditState: esFileBrowser :: forall n_a18IZ. Lens' (EditState n_a18IZ) (Maybe (FileBrowser n_a18IZ))
+ Matterhorn.Types.EditState: esFileBrowser :: forall n_a18Yo. Lens' (EditState n_a18Yo) (Maybe (FileBrowser n_a18Yo))
- Matterhorn.Types.EditState: esJustCompleted :: forall n_a18IZ. Lens' (EditState n_a18IZ) Bool
+ Matterhorn.Types.EditState: esJustCompleted :: forall n_a18Yo. Lens' (EditState n_a18Yo) Bool
- Matterhorn.Types.EditState: esMisspellings :: forall n_a18IZ. Lens' (EditState n_a18IZ) (Set Text)
+ Matterhorn.Types.EditState: esMisspellings :: forall n_a18Yo. Lens' (EditState n_a18Yo) (Set Text)
- Matterhorn.Types.EditState: esResetEditMode :: forall n_a18IZ. Lens' (EditState n_a18IZ) EditMode
+ Matterhorn.Types.EditState: esResetEditMode :: forall n_a18Yo. Lens' (EditState n_a18Yo) EditMode
- Matterhorn.Types.EditState: esShowReplyPrompt :: forall n_a18IZ. Lens' (EditState n_a18IZ) Bool
+ Matterhorn.Types.EditState: esShowReplyPrompt :: forall n_a18Yo. Lens' (EditState n_a18Yo) Bool
- Matterhorn.Types.EditState: esSpellCheckTimerReset :: forall n_a18IZ. Lens' (EditState n_a18IZ) (Maybe (IO ()))
+ Matterhorn.Types.EditState: esSpellCheckTimerReset :: forall n_a18Yo. Lens' (EditState n_a18Yo) (Maybe (IO ()))
- Matterhorn.Types.EditState: esTarget :: forall n_a18IZ. Lens' (EditState n_a18IZ) EditorTarget
+ Matterhorn.Types.EditState: esTarget :: forall n_a18Yo. Lens' (EditState n_a18Yo) EditorTarget
- Matterhorn.Types.EditState: esTeamId :: forall n_a18IZ. Lens' (EditState n_a18IZ) (Maybe TeamId)
+ Matterhorn.Types.EditState: esTeamId :: forall n_a18Yo. Lens' (EditState n_a18Yo) (Maybe TeamId)
- Matterhorn.Types.MessageInterface: attachmentPathDialogFocus :: forall n_a1cF2. Lens' (SaveAttachmentDialogState n_a1cF2) (FocusRing n_a1cF2)
+ Matterhorn.Types.MessageInterface: attachmentPathDialogFocus :: forall n_a1cUr. Lens' (SaveAttachmentDialogState n_a1cUr) (FocusRing n_a1cUr)
- Matterhorn.Types.MessageInterface: attachmentPathEditor :: forall n_a1cF2. Lens' (SaveAttachmentDialogState n_a1cF2) (Editor Text n_a1cF2)
+ Matterhorn.Types.MessageInterface: attachmentPathEditor :: forall n_a1cUr. Lens' (SaveAttachmentDialogState n_a1cUr) (Editor Text n_a1cUr)
- Matterhorn.Types.MessageInterface: miChannelId :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) ChannelId
+ Matterhorn.Types.MessageInterface: miChannelId :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) ChannelId
- Matterhorn.Types.MessageInterface: miEditor :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) (EditState n_a1cF4)
+ Matterhorn.Types.MessageInterface: miEditor :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) (EditState n_a1cUt)
- Matterhorn.Types.MessageInterface: miMessageSelect :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) MessageSelectState
+ Matterhorn.Types.MessageInterface: miMessageSelect :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) MessageSelectState
- Matterhorn.Types.MessageInterface: miMessages :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) Messages
+ Matterhorn.Types.MessageInterface: miMessages :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) Messages
- Matterhorn.Types.MessageInterface: miMode :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) MessageInterfaceMode
+ Matterhorn.Types.MessageInterface: miMode :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) MessageInterfaceMode
- Matterhorn.Types.MessageInterface: miRootPostId :: forall n_a1cF4 i_a1cF5 i_a1cQj. Lens (MessageInterface n_a1cF4 i_a1cF5) (MessageInterface n_a1cF4 i_a1cQj) i_a1cF5 i_a1cQj
+ Matterhorn.Types.MessageInterface: miRootPostId :: forall n_a1cUt i_a1cUu i_a1d5I. Lens (MessageInterface n_a1cUt i_a1cUu) (MessageInterface n_a1cUt i_a1d5I) i_a1cUu i_a1d5I
- Matterhorn.Types.MessageInterface: miSaveAttachmentDialog :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) (SaveAttachmentDialogState n_a1cF4)
+ Matterhorn.Types.MessageInterface: miSaveAttachmentDialog :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) (SaveAttachmentDialogState n_a1cUt)
- Matterhorn.Types.MessageInterface: miTarget :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) MessageInterfaceTarget
+ Matterhorn.Types.MessageInterface: miTarget :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) MessageInterfaceTarget
- Matterhorn.Types.MessageInterface: miUrlList :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) (URLList n_a1cF4)
+ Matterhorn.Types.MessageInterface: miUrlList :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) (URLList n_a1cUt)
- Matterhorn.Types.MessageInterface: miUrlListSource :: forall n_a1cF4 i_a1cF5. Lens' (MessageInterface n_a1cF4 i_a1cF5) URLListSource
+ Matterhorn.Types.MessageInterface: miUrlListSource :: forall n_a1cUt i_a1cUu. Lens' (MessageInterface n_a1cUt i_a1cUu) URLListSource
- Matterhorn.Types.MessageInterface: ulList :: forall n_a1cF3 n_a1d0e. Lens (URLList n_a1cF3) (URLList n_a1d0e) (List n_a1cF3 (Int, LinkChoice)) (List n_a1d0e (Int, LinkChoice))
+ Matterhorn.Types.MessageInterface: ulList :: forall n_a1cUs n_a1dfD. Lens (URLList n_a1cUs) (URLList n_a1dfD) (List n_a1cUs (Int, LinkChoice)) (List n_a1dfD (Int, LinkChoice))
- Matterhorn.Types.MessageInterface: ulSource :: forall n_a1cF3. Lens' (URLList n_a1cF3) (Maybe URLListSource)
+ Matterhorn.Types.MessageInterface: ulSource :: forall n_a1cUs. Lens' (URLList n_a1cUs) (Maybe URLListSource)

Files

CHANGELOG.md view
@@ -1,4 +1,33 @@ +90000.1.0+=========++Enhancements:+* Matterhorn now provides an additional configuration file for+  specifying Unicode character widths to allow users to configure+  Matterhorn to avoid layout problems due to wide characters. See the+  Matterhorn User Guide for details.+* Channel selection case-handling behavior is now configurable. (#840)+  This change adds a new configuration setting, `channelSelectCaseInsensitive`.+  This new setting is used to control channel selection input handling:+  * When the setting is `False` (the default), the behavior is as before+    this change: lowercase input was matched on channel names+    case-insensitively and input containing any uppercase characters+    resulted in a case-insensitive match.+  * When the setting is `True`, channel names are matched+    case-insensitively regardless of case in the input, i.e., as if the+    input was all lowercase.+* Improved the layout of block quotes and bullet lists in messages.+* Messages with attachments but no text may now be sent. (#773)+* The message view window now has an Author tab with information about+  the message author. (#833)++Bug fixes:+* Matterhorn will no longer crash when its internal event queue is full+  (e.g. due to pathological system conditions). (#835)+* The `notifyV2` script now has proper JSON quoting. (#836)+* Network errors due to closed sockets are now silenced. (#837)+ 90000.0.1 ========= 
matterhorn.cabal view
@@ -1,5 +1,5 @@ name:                matterhorn-version:             90000.0.1+version:             90000.1.0 synopsis:            Terminal client for the Mattermost chat system description:         This is a terminal client for the Mattermost chat                      system. Please see the README for a list of@@ -184,9 +184,9 @@                      , config-ini           >= 0.2.2.0 && < 0.3                      , process              >= 1.4     && < 1.7                      , microlens-platform   >= 0.3     && < 0.5-                     , brick                >= 2.4     && < 2.5+                     , brick                >= 2.8.3   && < 2.9                      , brick-skylighting    >= 1.0     && < 1.1-                     , vty                  >= 6.0     && < 6.3+                     , vty                  >= 6.4     && < 6.5                      , vty-crossplatform    >= 0.2.0.0 && < 0.5.0.0                      , word-wrap            >= 0.4.0   && < 0.5                      , transformers         >= 0.4     && < 0.7
programs/Main.hs view
@@ -19,15 +19,15 @@ main = do     opts <- grabOptions -    configResult <- if optIgnoreConfig opts-                    then return $ Right defaultConfig-                    else fmap snd <$> findConfig (optConfLocation opts)+    configResult <- findConfig $ if optIgnoreConfig opts+                                 then Nothing+                                 else optConfLocation opts      config <- case configResult of         Left err -> do             putStrLn $ "Error loading config: " <> err             exitFailure-        Right c -> return c+        Right (_, c) -> return c      let keyConfig = configUserKeys config         format = optPrintFormat opts
src/Matterhorn/App.hs view
@@ -9,8 +9,10 @@  import           Brick import           Control.Monad.Trans.Except ( runExceptT )+import qualified Control.Exception as E import qualified Graphics.Vty as Vty import qualified Graphics.Vty.CrossPlatform as Vty+import qualified Graphics.Vty.UnicodeWidthTable.Install as Vty import           Text.Aspell ( stopAspell ) import           GHC.Conc (getNumProcessors, setNumCapabilities) @@ -82,9 +84,20 @@      setNumCapabilities requestedCPUs +setupCharWidthMap :: Config -> IO ()+setupCharWidthMap config = do+    case configCharacterWidths config of+        Nothing -> return ()+        Just widths -> do+            let wMap = buildWidthMap widths+            Vty.installUnicodeWidthTable wMap `E.catch`+                (\(_::Vty.TableInstallException) -> return ())+ runMatterhorn :: Options -> Config -> IO ChatState runMatterhorn opts config = do     setupCpuUsage config++    setupCharWidthMap config      let mkVty = do           vty <- Vty.mkVty Vty.defaultConfig
src/Matterhorn/Config.hs view
@@ -5,7 +5,6 @@   ( Config(..)   , PasswordSource(..)   , findConfig-  , defaultConfig   , configConnectionType   ) where@@ -16,6 +15,7 @@ import qualified Paths_matterhorn as Paths  import           Brick.Keybindings+import qualified Control.Exception as E import           Control.Monad.Trans.Except import           Control.Monad.Trans.Class ( lift ) import           Data.Char ( isDigit, isAlpha )@@ -156,9 +156,12 @@     configDefaultAttachmentPath <- fieldMbOf "defaultAttachmentPath" filePathField     configMouseMode <- fieldFlagDef "enableMouseMode"       (configMouseMode defaultConfig)+    configChannelSelectCaseInsensitive <- fieldFlagDef "channelSelectCaseInsensitive"+      (configChannelSelectCaseInsensitive defaultConfig)      let configAbsPath = Nothing         configUserKeys = newKeyConfig allEvents [] []+        configCharacterWidths = Nothing     return Config { .. }  defaultBindings :: [(KeyEvent, [Binding])]@@ -441,22 +444,58 @@            , configMouseMode                   = False            , configChannelListSorting          = ChannelListSortDefault            , configTeamListSorting             = TeamListSortDefault+           , configChannelSelectCaseInsensitive = False+           , configCharacterWidths             = Nothing            }  findConfig :: Maybe FilePath -> IO (Either String ([String], Config))-findConfig Nothing = runExceptT $ do-    cfg <- lift $ locateConfig configFileName-    (warns, config) <--      case cfg of+findConfig mPath = runExceptT $ do+    -- Load the main configuration+    locatedConfig <- lift $ locateConfig configFileName++    (warns, config) <- case mPath <|> locatedConfig of         Nothing -> return ([], defaultConfig)-        Just path -> getConfig path+        Just path -> loadConfig path+     config' <- fixupPaths config-    return (warns, config')-findConfig (Just path) =-    runExceptT $ do (warns, config) <- getConfig path-                    config' <- fixupPaths config-                    return (warns, config') +    -- If there is a char widths file, load that and add it to the+    -- configuration+    widthsResult <- liftIO loadCharWidths++    case widthsResult of+        Nothing ->+            return (warns, config')+        Just (Left e) ->+            return (warns <> [e], config')+        Just (Right widths) ->+            return (warns, config' { configCharacterWidths = Just widths })++loadCharWidths :: IO (Maybe (Either String CharWidths))+loadCharWidths = do+    locatedPath <- locateConfig charWidthsFileName++    case locatedPath of+        Nothing -> return Nothing+        Just p -> Just <$> parseCharWidthsFile p++parseCharWidthsFile :: FilePath -> IO (Either String CharWidths)+parseCharWidthsFile path = runExceptT $ do+    contents <- ExceptT $ (Right <$> T.readFile path) `E.catch`+                          (\(e::E.SomeException) -> return $ Left $ show e)++    let pairs = catMaybes (parseCharWidth <$> T.lines contents)++    case pairs of+        [] -> throwE $ path <> ": could not read any valid character width entries"+        _ -> return $ newCharWidths pairs++parseCharWidth :: T.Text -> Maybe (Char, Int)+parseCharWidth s =+    case T.words s of+        [ch, widthS] | T.length ch == 1 -> (T.head ch,) <$> readMaybe (T.unpack widthS)+        _ -> Nothing+ -- | Fix path references in the configuration: -- -- * Rewrite the syntax directory path list with 'fixupSyntaxDirs'@@ -495,8 +534,15 @@ keybindingsSectionName :: Text keybindingsSectionName = "keybindings" -getConfig :: FilePath -> ExceptT String IO ([String], Config)-getConfig fp = do+-- | Given a file path, load a Matterhorn configuration from the+-- specified path, loading any ancillary information such as passwords+-- or tokens from external sources as specified in the configuration.+--+-- Fails with a string error message; otherwise returns a list of+-- warnings generated during the loading process as well as the loaded+-- configuration itself.+loadConfig :: FilePath -> ExceptT String IO ([String], Config)+loadConfig fp = do     absPath <- convertIOException $ makeAbsolute fp     t <- (convertIOException $ T.readFile absPath) `catchE`          (\e -> throwE $ "Could not read " <> show absPath <> ": " <> e)@@ -524,7 +570,7 @@                 Just (PasswordCommand cmdString) -> do                     let (cmd, rest) = case T.unpack <$> T.words cmdString of                             (a:as) -> (a, as)-                            [] -> error $ "BUG: getConfig: got empty command string"+                            [] -> error $ "BUG: loadConfig: got empty command string"                     output <- convertIOException (readProcess cmd rest "") `catchE`                               (\e -> throwE $ "Could not execute password command: " <> e)                     return $ Just $ T.pack (takeWhile (/= '\n') output)@@ -535,22 +581,22 @@                 Just (TokenCommand cmdString) -> do                     let (cmd, rest) = case T.unpack <$> T.words cmdString of                             (a:as) -> (a, as)-                            [] -> error $ "BUG: getConfig: got empty command string"+                            [] -> error $ "BUG: loadConfig: got empty command string"                     output <- convertIOException (readProcess cmd rest "") `catchE`                               (\e -> throwE $ "Could not execute token command: " <> e)                     return $ Just $ T.pack (takeWhile (/= '\n') output)-                Just (TokenString _) -> error $ "BUG: getConfig: token in the Config was already a TokenString"+                Just (TokenString _) -> error $ "BUG: loadConfig: token in the Config was already a TokenString"                 Nothing -> return Nothing              actualOTPToken <- case configOTPToken conf of                 Just (OTPTokenCommand cmdString) -> do                     let (cmd, rest) = case T.unpack <$> T.words cmdString of                             (a:as) -> (a, as)-                            [] -> error $ "BUG: getConfig: got empty command string"+                            [] -> error $ "BUG: loadConfig: got empty command string"                     output <- convertIOException (readProcess cmd rest "") `catchE`                               (\e -> throwE $ "Could not execute OTP token command: " <> e)                     return $ Just $ T.pack (takeWhile (/= '\n') output)-                Just (OTPTokenString _) -> error $ "BUG: getConfig: otptoken in the Config was already a OTPTokenString"+                Just (OTPTokenString _) -> error $ "BUG: loadConfig: otptoken in the Config was already a OTPTokenString"                 Nothing -> return Nothing              let conf' = conf
src/Matterhorn/Draw/Messages.hs view
@@ -330,12 +330,10 @@                 if V.imageHeight (result^.imageL) >= remainingHeight                 then do                     single <- if newMessagesAbove-                              then do-                                  result' <- render $+                              then render $                                       vBox [ withDefAttr newMessageTransitionAttr $ hBorderWithLabel (txt "New Messages ↑")                                            , cropTopBy 1 $ resultToWidget croppedResult                                            ]-                                  return result'                               else do                                   return croppedResult                     return [single]
src/Matterhorn/Draw/RichText.hs view
@@ -151,13 +151,10 @@              , headerTxt              ] renderBlock (Blockquote bs) = do-    w <- asks drawLineWidth     bws <- mapM renderBlock (unBlocks bs)-    return $ maybeHLimit w $ addQuoting $ vBox bws+    return $ addQuoting $ vBox bws renderBlock (List ty spacing bs) = do-    w <- asks drawLineWidth-    lst <- renderList ty spacing bs-    return $ maybeHLimit w lst+    renderList ty spacing bs renderBlock (CodeBlock ci tx) = do     hSet <- asks drawHighlightSet 
src/Matterhorn/FilePaths.hs view
@@ -7,6 +7,7 @@   , lastRunStateFileName    , configFileName+  , charWidthsFileName    , xdgName   , locateConfig@@ -55,6 +56,9 @@  configFileName :: FilePath configFileName = "config.ini"++charWidthsFileName :: FilePath+charWidthsFileName = "char_widths.txt"  historyFilePath :: IO FilePath historyFilePath = getUserConfigFile xdgName historyFileName
src/Matterhorn/State/ChannelSelect.hs view
@@ -47,12 +47,13 @@      input <- use (csTeam(tId).tsChannelSelectState.channelSelectInput)     cconfig <- use csClientConfig+    appConfig <- use (csResources.crConfiguration)     prefs <- use (csResources.crUserPreferences) -    let pat = parseChannelSelectPattern $ T.concat $ getEditContents input+    let pat = parseChannelSelectPattern appConfig $ T.concat $ getEditContents input         chanNameMatches e = case pat of             Nothing -> const Nothing-            Just p -> applySelectPattern p e+            Just p -> applySelectPattern appConfig p e         patTy = case pat of             Nothing -> Nothing             Just CSPAny -> Nothing@@ -88,10 +89,10 @@     csTeam(tId).tsChannelSelectState.channelSelectMatches %=         (Z.updateListBy preserveFocus $ Z.toList $ Z.maybeMapZipper matches (st^.csTeam(tId).tsFocus)) -applySelectPattern :: ChannelSelectPattern -> ChannelListEntry -> Text -> Maybe ChannelSelectMatch-applySelectPattern CSPAny entry chanName = do+applySelectPattern :: Config -> ChannelSelectPattern -> ChannelListEntry -> Text -> Maybe ChannelSelectMatch+applySelectPattern _ CSPAny entry chanName = do     return $ ChannelSelectMatch "" "" chanName chanName entry-applySelectPattern (CSP ty pat) entry chanName = do+applySelectPattern config (CSP ty pat) entry chanName = do     let applyType Infix | pat `T.isInfixOf` normalizedChanName =             case T.breakOn pat normalizedChanName of                 (pre, _) ->@@ -121,7 +122,7 @@          applyType _ = Nothing -        caseSensitive = T.any isUpper pat+        caseSensitive = T.any isUpper pat && not (configChannelSelectCaseInsensitive config)         normalizedChanName = if caseSensitive                              then chanName                              else T.toLower chanName@@ -129,10 +130,14 @@     (pre, m, post) <- applyType ty     return $ ChannelSelectMatch pre m post chanName entry -parseChannelSelectPattern :: Text -> Maybe ChannelSelectPattern-parseChannelSelectPattern "" = return CSPAny-parseChannelSelectPattern pat = do-    let only = if | userSigil `T.isPrefixOf` pat -> Just $ CSP PrefixDMOnly $ T.tail pat+parseChannelSelectPattern :: Config -> Text -> Maybe ChannelSelectPattern+parseChannelSelectPattern _ "" = return CSPAny+parseChannelSelectPattern config patRaw = do+    let pat = if configChannelSelectCaseInsensitive config+              then T.toLower patRaw+              else patRaw++        only = if | userSigil `T.isPrefixOf` pat -> Just $ CSP PrefixDMOnly $ T.tail pat                   | normalChannelSigil `T.isPrefixOf` pat -> Just $ CSP PrefixNonDMOnly $ T.tail pat                   | otherwise -> Nothing 
src/Matterhorn/State/Messages.hs view
@@ -121,8 +121,9 @@  -- | Send a message and attachments to the specified channel. sendMessage :: ChannelId -> EditMode -> Text -> [AttachmentData] -> MH ()-sendMessage chanId mode msg attachments =-    when (not $ shouldSkipMessage msg) $ do+sendMessage chanId mode rawMsg attachments = do+    let msg = T.strip rawMsg+    when (not $ shouldSkipMessage msg attachments) $ do         status <- use csConnectionStatus         case status of             Disconnected -> do@@ -164,9 +165,10 @@                                                                }                             void $ MM.mmPatchPost (postId p) update session -shouldSkipMessage :: Text -> Bool-shouldSkipMessage "" = True-shouldSkipMessage s = T.all (`elem` (" \t"::String)) s+shouldSkipMessage :: Text -> [AttachmentData] -> Bool+shouldSkipMessage "" [] = True+shouldSkipMessage s [] = T.all (`elem` (" \t"::String)) s+shouldSkipMessage _ _ = False  editMessage :: Post -> MH () editMessage new = do
src/Matterhorn/State/Setup/Threads.hs view
@@ -389,6 +389,7 @@     , "Network.Socket.recvBuf"     , "Network.Socket.sendBuf"     , "resource vanished"+    , "HandshakeFailed Error_EOF"     , "timeout"     , "partial packet"     , "No route to host"
src/Matterhorn/Types.hs view
@@ -43,6 +43,10 @@   , channelTopicDialogEditor   , channelTopicDialogFocus +  , CharWidths+  , newCharWidths+  , buildWidthMap+   , resultToWidget    , MHKeyEventHandler@@ -96,6 +100,8 @@   , configThreadOrientationL   , configMouseModeL   , configShowLastOpenThreadL+  , configChannelSelectCaseInsensitiveL+  , configCharacterWidthsL    , unsafeKeyDispatcher   , bindingConflictMessage@@ -400,6 +406,7 @@ import           Data.UUID ( UUID ) import qualified Data.Vector as Vec import qualified Graphics.Vty as Vty+import qualified Graphics.Vty.UnicodeWidthTable.Types as Vty import           Lens.Micro.Platform ( at, makeLenses, lens, (^?!), (.=)                                      , (%=), (%~), (.~), _Just, Traversal', to                                      , SimpleGetter, filtered, traversed, singular@@ -498,6 +505,22 @@     -- dimensions.     deriving (Eq, Show, Ord) +newtype CharWidths = CharWidths [(Char, Int)]+                   deriving (Eq, Show)++newCharWidths :: [(Char, Int)] -> CharWidths+newCharWidths = CharWidths++buildWidthMap :: CharWidths -> Vty.UnicodeWidthTable+buildWidthMap (CharWidths pairs) =+    Vty.UnicodeWidthTable (mkRange <$> pairs)+    where+        mkRange (ch, w) =+            Vty.WidthTableRange { Vty.rangeStart = toEnum $ fromEnum ch+                                , Vty.rangeSize = 1+                                , Vty.rangeColumns = toEnum w+                                }+ -- | This is how we represent the user's configuration. Most fields -- correspond to configuration file settings (see Config.hs) but some -- are for internal book-keeping purposes only.@@ -605,6 +628,11 @@            , configShowLastOpenThread :: Bool            -- ^ Whether to re-open a thread that was open the last time            -- Matterhorn quit+           , configChannelSelectCaseInsensitive :: Bool+           -- ^ Whether channel selection input is always matched+           -- case-insensitively+           , configCharacterWidths :: Maybe CharWidths+           -- ^ Map of Unicode characters to widths to configure Vty            } deriving (Eq, Show)  -- | The policy for CPU usage.@@ -674,26 +702,17 @@             case HM.lookup tId hidden of                 Nothing -> False                 Just s -> Set.member label s-    in [ let unread = length $ filter channelListEntryUnread favEntries-             coll = isHidden ChannelGroupFavoriteChannels-         in ( ChannelListGroup ChannelGroupFavoriteChannels unread coll (length favEntries)-            , if coll then mempty else sortChannelListEntries sorting favEntries-            )-       , let unread = length $ filter channelListEntryUnread normEntries-             coll = isHidden ChannelGroupPublicChannels-         in ( ChannelListGroup ChannelGroupPublicChannels unread coll (length normEntries)-            , if coll then mempty else sortChannelListEntries sorting normEntries-            )-       , let unread = length $ filter channelListEntryUnread privEntries-             coll = isHidden ChannelGroupPrivateChannels-         in ( ChannelListGroup ChannelGroupPrivateChannels unread coll (length privEntries)-            , if coll then mempty else sortChannelListEntries sorting privEntries-            )-       , let unread = length $ filter channelListEntryUnread dmEntries-             coll = isHidden ChannelGroupDirectMessages-         in ( ChannelListGroup ChannelGroupDirectMessages unread coll (length dmEntries)-            , if coll then mempty else sortDMChannelListEntries dmEntries-            )+        mkGroup (ty, entries, sortEntries) =+            let unread = length $ filter channelListEntryUnread entries+                coll = isHidden ty+            in ( ChannelListGroup ty unread coll (length entries)+               , if coll then mempty else sortEntries entries+               )+    in mkGroup <$>+       [ (ChannelGroupFavoriteChannels, favEntries, sortChannelListEntries sorting)+       , (ChannelGroupPublicChannels, normEntries, sortChannelListEntries sorting)+       , (ChannelGroupPrivateChannels, privEntries, sortChannelListEntries sorting)+       , (ChannelGroupDirectMessages, dmEntries, sortDMChannelListEntries)        ]  sortChannelListEntries :: ChannelListSorting -> [ChannelListEntry] -> [ChannelListEntry]@@ -1336,6 +1355,8 @@     -- ^ The message tab.     | VMTabReactions     -- ^ The reactions tab.+    | VMTabAuthorInfo+    -- ^ The author info tab.     deriving (Eq, Show)  data PendingChannelChange =@@ -1966,10 +1987,7 @@     writeBChan queue (IEvent ev)  writeBChan :: (MonadIO m) => BCH.BChan MHEvent -> MHEvent -> m ()-writeBChan chan e = do-    written <- liftIO $ BCH.writeBChanNonBlocking chan e-    when (not written) $-        error $ "mhSendEvent: BChan full, please report this as a bug!"+writeBChan chan e = void $ liftIO $ BCH.writeBChanNonBlocking chan e  -- | Log and raise an error. mhError :: MHError -> MH ()
src/Matterhorn/Types/Core.hs view
@@ -86,6 +86,9 @@     | ViewMessageReactionsArea TeamId     -- ^ The viewport for the specified team's single-message view     -- window's reaction tab.+    | ViewMessageAuthorArea TeamId+    -- ^ The viewport for the specified team's single-message view+    -- window's author info tab.     | ChannelSidebar TeamId     -- ^ The cache key for the specified team's channel list viewport     -- contents.
src/Matterhorn/Windows/ViewMessage.hs view
@@ -36,6 +36,7 @@ viewMessageWindowTemplate tId =     TabbedWindowTemplate { twtEntries = [ messageEntry tId                                         , reactionsEntry tId+                                        , authorInfoEntry tId                                         ]                          , twtTitle = const $ txt "View Message"                          }@@ -58,9 +59,19 @@                       , tweShowHandler = onShow tId                       } +authorInfoEntry :: TeamId -> TabbedWindowEntry ChatState MH Name ViewMessageWindowTab+authorInfoEntry tId =+    TabbedWindowEntry { tweValue = VMTabAuthorInfo+                      , tweRender = renderTab tId+                      , tweHandleEvent = handleEvent tId+                      , tweTitle = tabTitle+                      , tweShowHandler = onShow tId+                      }+ tabTitle :: ViewMessageWindowTab -> Bool -> T.Text tabTitle VMTabMessage _ = "Message" tabTitle VMTabReactions _ = "Reactions"+tabTitle VMTabAuthorInfo _ = "Author"  -- When we show the tabs, we need to reset the viewport scroll position -- for viewports in that tab. This is because an older View Message@@ -69,7 +80,8 @@ -- in an existing window resets this state, too. onShow :: TeamId -> ViewMessageWindowTab -> MH () onShow tId VMTabMessage = resetVp $ ViewMessageArea tId-onShow tId VMTabReactions = resetVp $ ViewMessageArea tId+onShow tId VMTabReactions = resetVp $ ViewMessageReactionsArea tId+onShow tId VMTabAuthorInfo = resetVp $ ViewMessageAuthorArea tId  resetVp :: Name -> MH () resetVp n = do@@ -89,6 +101,7 @@             case tab of                 VMTabMessage -> viewMessageBox cs tId latestMessage                 VMTabReactions -> reactionsText cs tId latestMessage+                VMTabAuthorInfo -> authorInfo cs tId latestMessage  getLatestMessage :: ChatState -> TeamId -> Message -> Maybe Message getLatestMessage cs tId m =@@ -104,6 +117,27 @@     void . mhHandleKeyboardEvent (viewMessageKeybindings tId) handleEvent tId VMTabReactions =     void . mhHandleKeyboardEvent (viewMessageReactionsKeybindings tId)+handleEvent _ VMTabAuthorInfo =+    const $ return ()++authorInfo :: ChatState -> TeamId -> Message -> Widget Name+authorInfo cs tId m =+    let mkPairs u = [ ("Username:", _uiName u)+                    , ("Nickname:", fromMaybe "(none)" $ _uiNickName u)+                    , ("Full name:", _uiFirstName u <> " " <> _uiLastName u)+                    ]+        renderPair (label, value) =+            txt label <+> padBottom (Pad 1) (padLeft (Pad 1) (withDefAttr clientEmphAttr $ txt value))+    in case _mUser m of+        NoUser -> txt "No author."+        UserOverride _ label -> txt $ "User: " <> label+        UserI _ uId ->+            case userById uId cs of+                Nothing ->+                    txt "Author info not loaded yet."+                Just author ->+                    viewport (ViewMessageAuthorArea tId) Vertical $+                    vBox $ renderPair <$> mkPairs author  reactionsText :: ChatState -> TeamId -> Message -> Widget Name reactionsText st tId m = viewport vpName Vertical body