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 +29/−0
- matterhorn.cabal +3/−3
- programs/Main.hs +4/−4
- src/Matterhorn/App.hs +13/−0
- src/Matterhorn/Config.hs +64/−18
- src/Matterhorn/Draw/Messages.hs +1/−3
- src/Matterhorn/Draw/RichText.hs +2/−5
- src/Matterhorn/FilePaths.hs +4/−0
- src/Matterhorn/State/ChannelSelect.hs +15/−10
- src/Matterhorn/State/Messages.hs +7/−5
- src/Matterhorn/State/Setup/Threads.hs +1/−0
- src/Matterhorn/Types.hs +42/−24
- src/Matterhorn/Types/Core.hs +3/−0
- src/Matterhorn/Windows/ViewMessage.hs +35/−1
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