packages feed

glirc 2.39.0.1 → 2.40

raw patch · 87 files changed

+2341/−1279 lines, 87 filesdep +semigroupoidsdep +typed-processdep +vty-unixdep −processdep ~basedep ~bytestringdep ~config-schemasetup-changed

Dependencies added: semigroupoids, typed-process, vty-unix

Dependencies removed: process

Dependency ranges changed: base, bytestring, config-schema, containers, filepath, free, hookup, irc-core, lens, template-haskell, text, time, unix, vector, vty

Files

ChangeLog.md view
@@ -1,5 +1,17 @@ # Revision history for glirc +* vty-6 support+* deduplicated /url+* locally processed /who+* locally processed /list+* colorized channel identifiers+* better channel and user-mode highlighting. highlights are now per-server so configuration files will need to be updated+* support for 24-bit color output+* OS notification support+* More notification levels for each window+* Better away status handling+* Config file level window hint configuration+ ## 2.39  * Extra oper commands and reply parsing
README.md view
@@ -142,7 +142,7 @@    side: left -- try right if you don't like left padding    width: 13 -url-opener: "open" -- This works on macOS, "gnome-open" for GNOME+url-opener: "open" -- This works on macOS, "xdg-open" for most Linuxes  key-bindings:   * bind: "C-M-b"@@ -178,7 +178,7 @@ | `indent-wrapped-lines` | nonnegative integer | How far to indent lines when they are wrapped                                              | | `extra-highlights`     | list of text        | Extra words/nicks to highlight                                                             | | `extensions`           | list of text        | Filenames of extension to load                                                             |-| `url-opener`           | text                | Command to execute with URL parameter for `/url` e.g. gnome-open on GNOME or open on macOS |+| `url-opener`           | text                | Command to execute with URL parameter for `/url` e.g. xdg-open on most Linuxes or open on macOS | | `ignores`              | list of text        | Initial list of nicknames to ignore                                                        | | `activity-bar`         | yes or no           | Initial setting for visibility of activity bar (default no)                                | | `bell-on-mention`      | yes or no           | Sound terminal bell on transition from not mentioned to mentioned (default no)             |
Setup.hs view
@@ -9,7 +9,7 @@ transitive dependencies of this package use free licenses and generates a Build module detailing the versions of build tools and transitive library dependencies.- + -}  module Main (main) where@@ -98,10 +98,12 @@ validateLicenses pkgs =   do let toLicense = either licenseFromSPDX id          isBad pkg = toLicense (license pkg) `notElem` freeLicenses+                  && pkgName (sourcePackageId pkg) `notElem` exemptions          badPkgs   = filter isBad pkgs+         exemptions = [mkPackageName "system-cxx-std-lib"]       unless (null badPkgs) $-       do mapM_ print [ toLicense (license pkg) | pkg <- badPkgs ]+       do mapM_ print [ (sourcePackageId pkg, toLicense (license pkg)) | pkg <- badPkgs ]           fail "BAD LICENSE"  
exec/Exports.hs view
@@ -12,6 +12,8 @@  import Client.CApi.Exports import Client.CApi.Types+import Data.Int+import Data.Word import Foreign.C  foreign export ccall glirc_send_message       :: Glirc_send_message
exec/Main.hs view
@@ -17,6 +17,7 @@ import System.Exit import System.IO import Graphics.Vty+import Graphics.Vty.Platform.Unix (mkVty)  import Client.Configuration import Client.EventLoop@@ -79,4 +80,12 @@ -- | Initialize a 'Vty' value and run a continuation. Shutdown the 'Vty' -- once the continuation finishes. withVty :: (Vty -> IO a) -> IO a-withVty = bracket (mkVty mempty{bracketedPasteMode = Just True}) shutdown+withVty = bracket buildVty shutdown++-- | Generate the initial 'Vty' value and enable the features glirc uses.+buildVty :: IO Vty+buildVty =+ do vty <- mkVty defaultConfig+    setMode (outputIface vty) BracketedPaste True+    setMode (outputIface vty) Focus True+    pure vty
glirc.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.2 name:                glirc-version:             2.39.0.1+version:             2.40 synopsis:            Console IRC client description:         Console IRC client                      .@@ -14,17 +14,16 @@ maintainer:          emertens@gmail.com copyright:           2016-2019 Eric Mertens category:            Network-extra-source-files:  ChangeLog.md README.md-                     exec/linux_exported_symbols.txt+extra-source-files:  exec/linux_exported_symbols.txt                      exec/macos_exported_symbols.txt-extra-doc-files:     glirc.1+extra-doc-files:     glirc.1 ChangeLog.md README.md homepage:            https://github.com/glguy/irc-core bug-reports:         https://github.com/glguy/irc-core/issues-tested-with:         GHC==8.6.5+tested-with:         GHC==9.4.7  custom-setup-  setup-depends: base     >=4.12 && <4.17,-                 filepath >=1.4  && <1.5,+  setup-depends: base     >=4.12 && <4.20,+                 filepath >=1.4  && <1.6,                  Cabal    >=2.2  && <4  source-repository head@@ -41,7 +40,9 @@   default-language:    Haskell2010    -- Constraints can be found on the library itself-  build-depends:       base, glirc, lens, text, vty+  build-depends:+    base, glirc, lens, text, vty,+    vty-unix             ^>=0.2.0.0,    if os(Linux)     ld-options: -Wl,--dynamic-list=exec/linux_exported_symbols.txt@@ -57,6 +58,9 @@   default-language:    Haskell2010   build-tool-depends:  hsc2hs:hsc2hs +  default-extensions:+    ImportQualifiedPost+   exposed-modules:     Client.Authentication.Ecdsa     Client.Authentication.Ecdh@@ -86,6 +90,7 @@     Client.Configuration     Client.Configuration.Colors     Client.Configuration.Macros+    Client.Configuration.Notifications     Client.Configuration.ServerSettings     Client.Configuration.Sts     Client.EventLoop@@ -120,10 +125,13 @@     Client.State.Extensions     Client.State.Focus     Client.State.Network+    Client.State.Url     Client.State.Window     Client.UserHost+    Client.WhoReply     Client.View     Client.View.Cert+    Client.View.ChannelList     Client.View.ChannelInfo     Client.View.Digraphs     Client.View.Help@@ -136,6 +144,7 @@     Client.View.RtsStats     Client.View.UrlSelection     Client.View.UserList+    Client.View.Who     Client.View.Windows    other-modules:@@ -154,40 +163,41 @@     Build_glirc    build-depends:-    base                 >=4.11   && <4.17,+    base                 >=4.11   && <4.20,     HsOpenSSL            >=0.11   && <0.12,     async                >=2.2    && <2.3,     attoparsec           ^>=0.14,     base64-bytestring    >=1.0.0.1&& <1.3,-    bytestring           >=0.10.8 && <0.12,-    config-schema        ^>=1.2.1.0,+    bytestring           >=0.10.8 && <0.13,+    config-schema        ^>=1.3.0.0,     config-value         ^>=0.8,-    containers           >=0.5.7  && <0.7,+    containers           >=0.5.7  && <0.8,     curve25519           ^>=0.2.5,     directory            >=1.2.6  && <1.4,-    filepath             >=1.4.1  && <1.5,-    free                 >=4.12   && <5.2,+    filepath             >=1.4.1  && <1.6,+    free                 >=4.12   && <5.3,     githash              ^>=0.1.6,     hashable             >=1.2.4  && <1.5,-    hookup               ^>=0.7,+    hookup               ^>=0.8,     irc-core             ^>=2.11,     kan-extensions       >=5.0    && <5.3,-    lens                 >=4.14   && <5.2,+    lens                 >=4.14   && <5.3,     random               >=1.1    && <1.3,     network              >=2.6.2  && <3.2,-    process              >=1.4.2  && <1.7,+    typed-process        ^>=0.2.10,     psqueues             >=0.2.7  && <0.3,     regex-tdfa           >=1.3.1  && <1.4,+    semigroupoids        >=5.1    && <6.1,     split                >=0.2    && <0.3,     stm                  >=2.4    && <2.6,-    template-haskell     >=2.11   && <2.19,-    text                 >=1.2.2  && <2.1,-    time                 >=1.6    && <1.14,+    template-haskell     >=2.11   && <2.22,+    text                 >=1.2.2  && <2.2,+    time                 >=1.10   && <1.14,     transformers         >=0.5.2  && <0.7,-    unix                 >=2.7    && <2.8,+    unix                 >=2.7    && <2.9,     unordered-containers >=0.2.11 && <0.3,-    vector               >=0.11   && <0.13,-    vty                  >=5.35   && <5.36,+    vector               >=0.11   && <0.14,+    vty                  ^>=6.1,  test-suite test   type:                exitcode-stdio-1.0
src/Client/Authentication/Ecdh.hs view
@@ -19,8 +19,8 @@ import Data.Text (Text) import Data.Text.Encoding qualified as Text import Irc.Commands (AuthenticatePayload (AuthenticatePayload))-import OpenSSL.EVP.Digest ( digestBS, getDigestByName, hmacBS, Digest )-import System.IO.Unsafe ( unsafePerformIO )+import OpenSSL.EVP.Digest (digestBS, getDigestByName, hmacBS, Digest)+import System.IO.Unsafe (unsafePerformIO)  newtype Phase1 = Phase1 Curve.PrivateKey 
src/Client/Authentication/Ecdsa.hs view
@@ -19,12 +19,13 @@   , computeResponse   ) where -import           Control.Exception (displayException, try)-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text-import           System.Process (readProcess)-import           Irc.Commands (AuthenticatePayload(..))+import Control.Exception (displayException, try)+import Data.ByteString.Lazy qualified as L+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Irc.Commands (AuthenticatePayload(..))+import System.Process.Typed (readProcessStdout_, proc)   -- | Identifier for SASL ECDSA challenge response authentication@@ -53,11 +54,11 @@   Text                    {- ^ challenge string                 -} ->   IO (Either String Text) {- ^ error message or response string -} computeResponse privateKeyFile challenge =-  do res <- try $ readProcess-                    "ecdsatool"-                    ["sign", privateKeyFile, Text.unpack challenge]-                    "" -- stdin-     return $! case words <$> res of-                 Right [resp] -> Right $! Text.pack resp-                 Right _      -> Left "bad sasl ecdsa response message"-                 Left e       -> Left (displayException (e :: IOError))+  do res <- try (readProcessStdout_ (proc "ecdsatool" ["sign", privateKeyFile, Text.unpack challenge]))+     return $! case res of+                 Left e -> Left (displayException (e :: IOError))+                 Right resp ->+                     case Text.words <$> Text.decodeUtf8' (L.toStrict resp) of+                         Left e      -> Left (displayException e)+                         Right [str] -> Right str+                         Right _     -> Left "bad sasl ecdsa response message"
src/Client/Authentication/Scram.hs view
@@ -25,8 +25,8 @@ import Data.ByteString.Char8 qualified as B8 import Data.List (foldl1') import Data.Text (Text)-import OpenSSL.EVP.Digest ( Digest, digestBS, hmacBS, getDigestByName) import Irc.Commands (AuthenticatePayload (AuthenticatePayload))+import OpenSSL.EVP.Digest (Digest, digestBS, hmacBS, getDigestByName) import System.IO.Unsafe (unsafePerformIO)  data ScramDigest
src/Client/CApi.hs view
@@ -36,27 +36,25 @@   , withRawIrcMsg   ) where -import           Client.Configuration-                   (ExtensionConfiguration,-                    extensionPath, extensionRtldFlags, extensionArgs)-import           Client.CApi.Types-import           Control.Lens (view)-import           Control.Monad-import           Control.Monad.IO.Class-import           Control.Monad.Codensity-import           Data.IntPSQ (IntPSQ)-import qualified Data.IntPSQ as IntPSQ-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Time-import           Foreign.C-import           Foreign.Marshal-import           Foreign.Ptr-import           Foreign.Storable-import           Irc.Identifier-import           Irc.RawIrcMsg-import           Irc.UserInfo-import           System.Posix.DynamicLinker+import Client.CApi.Types+import Client.Configuration (ExtensionConfiguration, extensionPath, extensionRtldFlags, extensionArgs)+import Control.Lens (view)+import Control.Monad (guard, unless)+import Control.Monad.Codensity (Codensity(Codensity), lowerCodensity)+import Control.Monad.IO.Class (MonadIO(..))+import Data.IntPSQ (IntPSQ)+import Data.IntPSQ qualified as IntPSQ+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (UTCTime)+import Foreign.C (peekCString, withCString)+import Foreign.Marshal (withArray, withArrayLen, with)+import Foreign.Ptr (Ptr, FunPtr, castFunPtrToPtr, nullFunPtr, nullPtr)+import Foreign.Storable (Storable(peek))+import Irc.Identifier (idText)+import Irc.RawIrcMsg (RawIrcMsg(..), TagEntry(..))+import Irc.UserInfo (UserInfo(userHost, userNick, userName))+import System.Posix.DynamicLinker (dlopen, dlclose, dlsym, DL)  ------------------------------------------------------------------------ 
src/Client/CApi/Exports.hs view
@@ -89,43 +89,43 @@  , glirc_thread  ) where -import           Client.CApi (cancelTimer, pushTimer, ThreadEntry(..), ActiveExtension(aeThreads))-import           Client.CApi.Types-import           Client.Configuration-import           Client.Message-import           Client.State-import           Client.State.Channel-import           Client.State.Focus-import           Client.State.Network-import           Client.State.Window (windowClear, windowSeen, winMessages, wlText)-import           Client.UserHost-import           Control.Concurrent (forkOS)-import           Control.Concurrent.MVar-import           Control.Concurrent.STM (atomically, writeTQueue)-import           Control.Exception-import           Control.Lens-import           Control.Monad (unless)-import           Data.Char (chr)-import           Data.Foldable (traverse_)-import           Data.Functor.Compose-import qualified Data.Map as Map-import           Data.Monoid (First(..))-import qualified Data.HashMap.Strict as HashMap-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as LText-import qualified Data.Text.Foreign as Text-import           Data.Time-import           Foreign.C-import           Foreign.Marshal-import           Foreign.Ptr-import           Foreign.StablePtr-import           Foreign.Storable-import           Irc.Identifier-import           Irc.RawIrcMsg-import           Irc.UserInfo-import           Irc.Message-import           LensUtils+import Client.CApi (cancelTimer, pushTimer, ThreadEntry(..), ActiveExtension(aeThreads))+import Client.CApi.Types+import Client.Configuration ( newFilePathContext, resolveFilePath )+import Client.Message+import Client.State+import Client.State.Channel ( chanLists, chanModes, chanUsers )+import Client.State.Focus (Focus(ChannelFocus, Unfocused, NetworkFocus))+import Client.State.Network (csChannels, csNick, csUsers, isChannelIdentifier, sendMsg)+import Client.State.Window (windowClear, windowSeen, winMessages, wlText)+import Client.UserHost (uhAccount)+import Control.Concurrent (forkOS)+import Control.Concurrent.MVar (MVar, modifyMVar, modifyMVar_, readMVar)+import Control.Concurrent.STM (atomically, writeTQueue)+import Control.Exception (SomeException(SomeException), catch)+import Control.Lens+import Control.Monad (unless)+import Data.Char (chr)+import Data.Foldable (traverse_)+import Data.Functor.Compose ( Compose(Compose) )+import Data.HashMap.Strict qualified as HashMap+import Data.Map qualified as Map+import Data.Monoid (First(..))+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Foreign qualified as Text+import Data.Text.Lazy qualified as LText+import Data.Time (addUTCTime, getCurrentTime, getZonedTime)+import Foreign.C (CString, CInt, CChar, CSize, newCString, CULong)+import Foreign.Marshal (newArray0, peekArray, peekArray0, free)+import Foreign.Ptr (Ptr, FunPtr, nullPtr)+import Foreign.StablePtr (castPtrToStablePtr, deRefStablePtr)+import Foreign.Storable (Storable(peek))+import Irc.Identifier (idText, mkId)+import Irc.Message (IrcMsg(Privmsg), Source(Source))+import Irc.RawIrcMsg (RawIrcMsg(..), TagEntry(TagEntry))+import Irc.UserInfo (UserInfo(UserInfo), parseUserInfo)+import LensUtils (overStrict)  ------------------------------------------------------------------------ 
src/Client/CApi/Types.hsc view
@@ -1,4 +1,4 @@-{-# Language RecordWildCards #-}+{-# Language RecordWildCards, ImportQualifiedPost #-}  {-| Module      : Client.CApi.Types@@ -60,15 +60,15 @@   , poke'   ) where -import           Control.Monad-import           Data.Text (Text)-import qualified Data.Text.Foreign as Text-import           Data.Word-import           Data.Int-import           Foreign.C-import           Foreign.Marshal.Array-import           Foreign.Ptr-import           Foreign.Storable+import Control.Monad+import Data.Int+import Data.Text (Text)+import Data.Text.Foreign qualified as Text+import Data.Word+import Foreign.C+import Foreign.Marshal.Array+import Foreign.Ptr+import Foreign.Storable  -- | Tag for describing the kind of message to display in the client -- as used in `glirc_print`. See 'normalMessage' and 'errorMessage'.
src/Client/Commands.hs view
@@ -25,44 +25,44 @@   , commandsList   ) where -import           Client.Commands.Arguments.Spec-import           Client.Commands.Arguments.Parser-import           Client.Commands.Exec-import           Client.Commands.Interpolation-import           Client.Commands.Recognizer-import           Client.Commands.WordCompletion-import           Client.Configuration-import           Client.State-import           Client.State.Extensions-import           Client.State.Focus-import           Client.State.Network-import           Client.State.Window-import           Control.Applicative-import           Control.Exception (displayException, try)-import           Control.Lens-import           Control.Monad-import           Data.Foldable-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Time (getZonedTime)-import           Irc.Commands-import           Irc.Identifier-import           Irc.RawIrcMsg-import           Irc.Message-import           RtsStats (getStats)-import           System.Process+import Client.Commands.Arguments.Parser (parse)+import Client.Commands.Arguments.Spec (optionalArg, optionalNumberArg, remainingArg, simpleToken)+import Client.Commands.Exec+import Client.Commands.Interpolation (resolveMacroExpansions, Macro(Macro), MacroSpec(MacroSpec))+import Client.Commands.Recognizer (fromCommands, keys, recognize, Recognition(Exact), Recognizer)+import Client.Commands.WordCompletion (caseText, plainWordCompleteMode, wordComplete)+import Client.Configuration+import Client.State+import Client.State.Extensions (clientCommandExtension, clientStartExtensions)+import Client.State.Focus+import Client.State.Network (csNick, isChannelIdentifier, sendMsg)+import Client.State.Url+import Control.Applicative (liftA2, (<|>))+import Control.Exception (displayException, try)+import Control.Lens+import Control.Monad (guard, foldM)+import Data.Foldable (foldl', toList)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (getZonedTime)+import Irc.Commands (ircPrivmsg)+import Irc.Identifier (idText)+import Irc.Message (IrcMsg(Privmsg))+import Irc.RawIrcMsg (parseRawIrcMsg)+import RtsStats (getStats)+import System.Process.Typed (proc, runProcess_) -import           Client.Commands.Channel (channelCommands)-import           Client.Commands.Certificate (newCertificateCommand)-import           Client.Commands.Chat (chatCommands, chatCommand', executeChat)-import           Client.Commands.Connection (connectionCommands)-import           Client.Commands.Operator (operatorCommands)-import           Client.Commands.Queries (queryCommands)-import           Client.Commands.Toggles (togglesCommands)-import           Client.Commands.Window (windowCommands)-import           Client.Commands.ZNC (zncCommands)-import           Client.Commands.TabCompletion-import           Client.Commands.Types+import Client.Commands.Certificate (newCertificateCommand)+import Client.Commands.Channel (channelCommands)+import Client.Commands.Chat (chatCommands, chatCommand', executeChat)+import Client.Commands.Connection (connectionCommands)+import Client.Commands.Operator (operatorCommands)+import Client.Commands.Queries (queryCommands)+import Client.Commands.TabCompletion+import Client.Commands.Toggles (togglesCommands)+import Client.Commands.Types+import Client.Commands.Window (windowCommands)+import Client.Commands.ZNC (zncCommands)  -- | Interpret the given chat message or command. Leading @/@ indicates a -- command. Otherwise if a channel or user query is focused a chat message will@@ -507,13 +507,8 @@     Nothing     -> commandFailureMsg "url-opener not configured" st     Just opener -> doUrlOpen opener (maybe 0 (subtract 1) arg)   where-    focus = view clientFocus st--    urls = toListOf ( clientWindows . ix focus . winMessages . each . wlText-                    . folding urlMatches) st-     doUrlOpen opener n =-      case preview (ix n) urls of+      case preview (ix n) (map fst (urlList st)) of         Just url -> openUrl opener (Text.unpack url) st         Nothing  -> commandFailureMsg "bad url number" st @@ -521,7 +516,7 @@ openUrl (UrlOpener opener args) url st =   do let argStr (UrlArgLiteral str) = str          argStr UrlArgUrl           = url-     res <- try (callProcess opener (map argStr args))+     res <- try (runProcess_ (proc opener (map argStr args)))      case res of        Left e  -> commandFailureMsg (Text.pack (displayException (e :: IOError))) st        Right{} -> commandSuccess st
src/Client/Commands/Arguments/Parser.hs view
@@ -11,12 +11,12 @@  module Client.Commands.Arguments.Parser (parse) where -import Client.Commands.Arguments.Spec-import Control.Applicative-import Control.Monad-import Control.Monad.Trans.State-import Control.Monad.Trans.Class-import Control.Applicative.Free+import Client.Commands.Arguments.Spec (Arg(..), Args, ArgumentShape(..))+import Control.Applicative (optional)+import Control.Applicative.Free (runAp)+import Control.Monad (guard)+import Control.Monad.Trans.Class (MonadTrans(lift))+import Control.Monad.Trans.State (StateT(runStateT), get, put)  ------------------------------------------------------------------------ -- Parser@@ -30,7 +30,7 @@ type Parser = StateT String Maybe  parseArgs :: r -> Args r a -> Parser a-parseArgs env spec = runAp (parseArg env) spec+parseArgs env = runAp (parseArg env)  parseArg :: r -> Arg r a -> Parser a parseArg env spec =
src/Client/Commands/Arguments/Renderer.hs view
@@ -11,17 +11,17 @@  module Client.Commands.Arguments.Renderer (render) where -import           Client.Commands.Arguments.Spec-import           Client.Image.MircFormatting-import           Client.Image.PackedImage-import           Client.Image.Palette-import           Control.Applicative.Free-import           Control.Lens-import           Control.Monad.Trans.State-import           Data.Functor.Compose-import qualified Data.Text as Text-import           Graphics.Vty (wcswidth)-import           Graphics.Vty.Attributes+import Client.Commands.Arguments.Spec (Arg(..), Args, ArgumentShape(RemainingArgument, TokenArgument))+import Client.Image.MircFormatting (parseIrcText')+import Client.Image.PackedImage (imageWidth, resizeImage, string, Image')+import Client.Image.Palette (palCommandPlaceholder, Palette)+import Control.Applicative.Free (runAp)+import Control.Lens (Const(..), view)+import Control.Monad.Trans.State (State, runState, state)+import Data.Functor.Compose (Compose(..))+import Data.Text qualified as Text+import Graphics.Vty (wcswidth)+import Graphics.Vty.Attributes (defAttr)  render ::   Palette  {- ^ palette             -} ->
src/Client/Commands/Arguments/Spec.hs view
@@ -24,8 +24,9 @@   , Arg(..)   ) where -import Control.Applicative-import Control.Applicative.Free+import Control.Applicative (liftA2)+import Control.Applicative.Free (Ap, liftAp)+import Data.Kind (Type) import Data.Maybe (fromMaybe) import Text.Read (readMaybe) @@ -33,7 +34,7 @@  data ArgumentShape = TokenArgument | RemainingArgument -data Arg :: * -> * -> * where+data Arg :: Type -> Type -> Type where   Argument  :: ArgumentShape -> String -> (r -> String -> Maybe a) -> Arg r a   Optional  :: Args r a -> Arg r (Maybe a)   Extension :: String -> (r -> String -> Maybe (Args r a)) -> Arg r a
src/Client/Commands/Certificate.hs view
@@ -9,27 +9,27 @@  module Client.Commands.Certificate (newCertificateCommand) where -import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.State-import           Control.Applicative-import           Control.Exception-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as L-import           Data.Foldable (foldl')-import           Data.Maybe (fromMaybe)-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Time-import           Hookup.OpenSSL (getPubKeyDer)-import qualified OpenSSL.RSA as RSA-import qualified OpenSSL.X509 as X509-import qualified OpenSSL.PEM as PEM-import qualified OpenSSL.EVP.Cipher as Cipher-import qualified OpenSSL.EVP.Digest as Digest-import           Text.Read (readMaybe)-import           Text.Printf (printf)+import Client.Commands.Arguments.Spec+import Client.Commands.TabCompletion (noClientTab)+import Client.Commands.Types+import Client.State (recordError, recordSuccess)+import Control.Applicative (liftA2)+import Control.Exception (displayException, try)+import Data.ByteString qualified as B+import Data.ByteString.Lazy qualified as L+import Data.Foldable (foldl')+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (UTCTime(UTCTime), Day(ModifiedJulianDay), getZonedTime)+import Hookup.OpenSSL (getPubKeyDer)+import OpenSSL.EVP.Cipher qualified as Cipher+import OpenSSL.EVP.Digest qualified as Digest+import OpenSSL.PEM qualified as PEM+import OpenSSL.RSA qualified as RSA+import OpenSSL.X509 qualified as X509+import Text.Printf (printf)+import Text.Read (readMaybe)  keysizeArg :: Args a (Maybe (Int, String)) keysizeArg = optionalArg (liftA2 (,) (tokenArg "[keysize]" (const parseSize)) (remainingArg "[passphrase]"))@@ -72,52 +72,54 @@  cmdNewCert :: ClientCommand (String, Maybe (Int, String)) cmdNewCert st (path, mbExtra) =-  do now <- getZonedTime+ do now <- getZonedTime -     let size = case mbExtra of-                  Nothing -> 2048-                  Just (n,_) -> n-     pass <- case mbExtra of-               Just (_,p) | not (null p) ->-                 do cipher <- fromMaybe (error "No aes128!") <$> Cipher.getCipherByName "aes128"-                    pure (Just (cipher, PEM.PwStr p))-               _ -> pure Nothing+    let size =+          case mbExtra of+            Nothing -> 2048+            Just (n,_) -> n+    pass <-+      case mbExtra of+        Just (_,p) | not (null p) ->+         do cipher <- fromMaybe (error "No aes128!") <$> Cipher.getCipherByName "aes128"+            pure (Just (cipher, PEM.PwStr p))+        _ -> pure Nothing -     rsa  <- RSA.generateRSAKey' size 65537-     x509 <- X509.newX509-     X509.setVersion      x509 2-     X509.setSerialNumber x509 1-     X509.setIssuerName   x509 [("CN","glirc")]-     X509.setSubjectName  x509 [("CN","glirc")]-     X509.setNotBefore    x509 (UTCTime (ModifiedJulianDay 40587) 0) -- 1970-01-01-     X509.setNotAfter     x509 (UTCTime (ModifiedJulianDay 77112) 0) -- 2070-01-01-     X509.setPublicKey    x509 rsa-     X509.signX509        x509 rsa Nothing+    rsa  <- RSA.generateRSAKey' size 65537+    x509 <- X509.newX509+    X509.setVersion      x509 2+    X509.setSerialNumber x509 1+    X509.setIssuerName   x509 [("CN","glirc")]+    X509.setSubjectName  x509 [("CN","glirc")]+    X509.setNotBefore    x509 (UTCTime (ModifiedJulianDay 40587) 0) -- 1970-01-01+    X509.setNotAfter     x509 (UTCTime (ModifiedJulianDay 77112) 0) -- 2070-01-01+    X509.setPublicKey    x509 rsa+    X509.signX509        x509 rsa Nothing -     ctder <- X509.writeDerX509 x509-     pkder <- getPubKeyDer x509-     msgss <- traverse (getFingerprint ctder pkder) ["sha1", "sha256", "sha512"]+    ctder <- X509.writeDerX509 x509+    pkder <- getPubKeyDer x509+    msgss <- traverse (getFingerprint ctder pkder) ["sha1", "sha256", "sha512"] -     pem1 <- PEM.writePKCS8PrivateKey rsa pass-     pem2 <- PEM.writeX509 x509-     res  <- try (writeFile path (pem1 ++ pem2))+    pem1 <- PEM.writePKCS8PrivateKey rsa pass+    pem2 <- PEM.writeX509 x509+    res  <- try (writeFile path (pem1 ++ pem2)) -     case res of-       Left e ->-        commandFailure (recordError now "" (Text.pack (displayException (e :: IOError))) st)-       Right () ->-        do let msg = "Certificate saved: \x02" <> path <> "\x02"-           commandSuccess (foldl' (recordSuccess now) st (concat msgss ++ [Text.pack msg]))+    case res of+      Left e ->+          commandFailure (recordError now "" (Text.pack (displayException (e :: IOError))) st)+      Right () ->+       do let msg = "Certificate saved: \x02" <> path <> "\x02"+          commandSuccess (foldl' (recordSuccess now) st (concat msgss ++ [Text.pack msg]))  getFingerprint :: L.ByteString -> B.ByteString -> String -> IO [Text] getFingerprint crt pub name =-  do mb <- Digest.getDigestByName name-     pure $ case mb of-       Nothing -> []-       Just d  -> map Text.pack-         [printf "CERT %-6s fingerprint: \^C07%s" name (hexString (Digest.digestLBS d crt))-         ,printf "SPKI %-6s fingerprint: \^C07%s" name (hexString (Digest.digestBS  d pub))-         ]+ do mb <- Digest.getDigestByName name+    pure $! case mb of+      Nothing -> []+      Just d  -> map Text.pack+        [ printf "CERT %-6s fingerprint: \^C07%s" name (hexString (Digest.digestLBS d crt))+        , printf "SPKI %-6s fingerprint: \^C07%s" name (hexString (Digest.digestBS  d pub))+        ]  hexString :: B.ByteString -> String hexString = B.foldr (printf "%02x%s") ""
src/Client/Commands/Channel.hs view
@@ -9,30 +9,29 @@  module Client.Commands.Channel (channelCommands) where -import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.Commands.WordCompletion-import           Client.State-import           Client.State.Channel-import           Client.State.Focus-import           Client.State.Network-import           Client.UserHost-import           Control.Applicative-import           Control.Lens-import           Control.Monad-import           Data.Foldable (traverse_)-import           Data.List.Split (chunksOf)-import           Data.Maybe (fromMaybe)-import           Data.Text (Text)-import qualified Client.State.EditBox as Edit-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Text as Text-import           Irc.Commands-import           Irc.Modes-import           Irc.UserInfo-import           Irc.Identifier-import           LensUtils (setStrict)+import Client.Commands.Arguments.Spec+import Client.Commands.TabCompletion (activeNicks, noChannelTab, simpleChannelTab, simpleTabCompletion)+import Client.Commands.Types+import Client.Commands.WordCompletion (plainWordCompleteMode)+import Client.State+import Client.State.Channel (chanLists, chanModes, chanTopic, chanUsers)+import Client.State.EditBox qualified as Edit+import Client.State.Focus+import Client.State.Network+import Client.UserHost ( UserAndHost(UserAndHost) )+import Control.Applicative (liftA2)+import Control.Lens+import Control.Monad (unless)+import Data.Foldable (traverse_)+import Data.HashMap.Strict qualified as HashMap+import Data.List.Split (chunksOf)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text qualified as Text+import Irc.Commands (ircInvite, ircKick, ircMode, ircRemove)+import Irc.Identifier (Identifier, mkId)+import Irc.Modes+import Irc.UserInfo (UserInfo(UserInfo), renderUserInfo)  channelCommands :: CommandSection channelCommands = CommandSection "IRC channel management"@@ -150,12 +149,6 @@               then cs <$ sendMsg cs cmd               else sendModeration channelId [cmd] cs      commandSuccessUpdateCS cs' st--commandSuccessUpdateCS :: NetworkState -> ClientState -> IO CommandResult-commandSuccessUpdateCS cs st =-  do let network = view csNetwork cs-     commandSuccess-       $ setStrict (clientConnection network) cs st  cmdMasks :: ChannelCommand String cmdMasks channel cs st rest =
src/Client/Commands/Chat.hs view
@@ -9,28 +9,28 @@  module Client.Commands.Chat (chatCommands, chatCommand', executeChat, cmdCtcp) where -import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.Commands.Window (parseFocus)-import           Client.Message-import           Client.State-import           Client.State.Extensions (clientChatExtension)-import           Client.State.Focus-import           Client.State.Network-import           Control.Applicative-import           Control.Lens-import           Control.Monad (when)-import           Data.Char (toUpper)-import           Data.Foldable-import           Data.List.NonEmpty (NonEmpty((:|)))-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Time-import           Irc.Commands-import           Irc.Identifier-import           Irc.Message-import           Irc.RawIrcMsg+import Client.Commands.Arguments.Spec+import Client.Commands.TabCompletion+import Client.Commands.Types+import Client.Commands.Window (parseFocus)+import Client.Message+import Client.State+import Client.State.Extensions (clientChatExtension)+import Client.State.Focus (focusNetwork, Focus(ChannelFocus), Subfocus(FocusInfo, FocusUsers))+import Client.State.Network (csNetwork, csUserInfo, sendMsg, NetworkState)+import Control.Applicative (liftA2, liftA3)+import Control.Lens (view, preview, views)+import Control.Monad (when)+import Data.Char (toUpper)+import Data.Foldable (foldl')+import Data.List.NonEmpty (NonEmpty((:|)))+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (getZonedTime)+import Irc.Commands+import Irc.Identifier (Identifier, idText, mkId)+import Irc.Message (IrcMsg(Privmsg, Notice, Ctcp), Source(Source))+import Irc.RawIrcMsg (RawIrcMsg, parseRawIrcMsg)  chatCommands :: CommandSection chatCommands = CommandSection "IRC commands"
src/Client/Commands/Connection.hs view
@@ -9,18 +9,18 @@  module Client.Commands.Connection (connectionCommands) where -import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.Commands.WordCompletion-import           Client.Configuration-import           Client.State-import           Client.State.Focus-import           Client.State.Network-import           Control.Lens-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Text as Text-import           Irc.Commands (ircMode, ircQuit)+import Client.Commands.Arguments.Spec ( remainingArg, simpleToken )+import Client.Commands.TabCompletion+import Client.Commands.Types+import Client.Commands.WordCompletion ( plainWordCompleteMode )+import Client.Configuration ( configServers )+import Client.State+import Client.State.Focus (focusNetwork, Focus(NetworkFocus), Subfocus(FocusCert))+import Client.State.Network (csLastReceived, csNetwork, csNick, sendMsg)+import Control.Lens (view, folded, preview, views)+import Data.HashMap.Strict qualified as HashMap+import Data.Text qualified as Text+import Irc.Commands (ircMode, ircQuit)  connectionCommands :: CommandSection connectionCommands = CommandSection "Connection commands"
src/Client/Commands/Exec.hs view
@@ -25,11 +25,14 @@   , runExecCmd   ) where -import           Control.Exception-import           Control.Lens-import           Data.List-import           System.Console.GetOpt-import           System.Process+import Control.Exception (Exception(displayException), try)+import Control.Lens (view, (??), set, makeLenses)+import Data.ByteString.Lazy qualified as L+import Data.List (unfoldr)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import System.Console.GetOpt (getOpt, ArgDescr(ReqArg, OptArg), ArgOrder(RequireOrder), OptDescr(..))+import System.Process.Typed (byteStringInput, proc, readProcessStdout_, setStdin)  -- | Settings for @/exec@ command. --@@ -103,13 +106,15 @@   ExecCmd                       {- ^ exec configuration          -} ->   IO (Either [String] [String]) {- ^ error lines or output lines -} runExecCmd cmd =-  do res <- try (readProcessWithExitCode-                   (view execCommand   cmd)-                   (view execArguments cmd)-                   (view execStdIn     cmd))+  do res <- try (readProcessStdout_+                   (setStdin (byteStringInput (L.fromStrict (Text.encodeUtf8 (Text.pack (view execStdIn cmd)))))+                   (proc (view execCommand   cmd) (view execArguments cmd))))      return $! case res of-       Left er                  -> Left [displayException (er :: IOError)]-       Right (_code, out, _err) -> Right (lines out)+       Left er   -> Left [displayException (er :: IOError)]+       Right out ->+          case Text.decodeUtf8' (L.toStrict out) of+             Right str -> Right (lines (Text.unpack str))+             Left e -> Left [displayException e]  -- | Power words is similar to 'words' except that when it encounters -- a word formatted as a Haskell 'String' literal it parses it as
src/Client/Commands/Interpolation.hs view
@@ -28,14 +28,13 @@   , noMacroArguments   ) where -import           Control.Applicative-import           Data.Attoparsec.Text as P-import           Data.Char-import           Data.Maybe-import qualified Data.Text as Text-import           Data.Text (Text)--import           Client.Commands.Arguments.Spec+import Client.Commands.Arguments.Spec (optionalArg, remainingArg, simpleToken, Args)+import Control.Applicative (Alternative, liftA2, (<|>), many, optional)+import Data.Attoparsec.Text as P+import Data.Char (isAlpha)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text qualified as Text  -- | Parsed chunk of an expandable command data ExpansionChunk
src/Client/Commands/Operator.hs view
@@ -9,15 +9,15 @@  module Client.Commands.Operator (operatorCommands) where -import           Control.Applicative-import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.State.Network (sendMsg)-import           Data.Maybe (fromMaybe, maybeToList)-import qualified Data.Text as Text-import           Irc.Commands-import           Irc.RawIrcMsg+import Client.Commands.Arguments.Spec (optionalArg, remainingArg, simpleToken)+import Client.Commands.TabCompletion (noNetworkTab, simpleNetworkTab)+import Client.Commands.Types+import Client.State.Network (sendMsg)+import Control.Applicative (liftA2, liftA3)+import Data.Maybe (fromMaybe, maybeToList)+import Data.Text qualified as Text+import Irc.Commands+import Irc.RawIrcMsg (rawIrcMsg)  operatorCommands :: CommandSection operatorCommands = CommandSection "Network operator commands"
src/Client/Commands/Queries.hs view
@@ -1,4 +1,6 @@ {-# Language OverloadedStrings #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE ViewPatterns #-} {-| Module      : Client.Commands.Queries Description : Query command implementations@@ -9,20 +11,27 @@  module Client.Commands.Queries (queryCommands) where -import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.State.Network (sendMsg)-import qualified Data.Text as Text-import           Irc.Commands+import Client.Commands.Arguments.Spec (optionalArg, remainingArg, simpleToken, extensionArg, Args)+import Client.Commands.TabCompletion (noNetworkTab, simpleNetworkTab)+import Client.Commands.Types (commandSuccess, commandSuccessUpdateCS, Command(Command), CommandImpl(NetworkCommand), CommandSection(CommandSection), NetworkCommand)+import Client.State (changeSubfocus, ClientState)+import Client.State.Focus (Subfocus(FocusChanList, FocusWho))+import Client.State.Network (sendMsg, csChannelList, clsElist, csPingStatus, _PingConnecting, csWhoReply)+import Client.WhoReply (newWhoReply)+import Control.Applicative (liftA2)+import Control.Lens (has, set, view)+import Control.Monad (unless)+import Data.Maybe (fromMaybe, maybeToList)+import Data.Text qualified as Text+import Irc.Commands  queryCommands :: CommandSection queryCommands = CommandSection "Queries"    [ Command       (pure "who")-      (remainingArg "arguments")-      "Send WHO query to server with given arguments.\n"+      (optionalArg (liftA2 (,) (simpleToken "[channel|nick|mask]") (optionalArg (simpleToken "[options]"))))+      "Send WHO query to server with given arguments, or just show the who view.\n"     $ NetworkCommand cmdWho simpleNetworkTab    , Command@@ -94,8 +103,32 @@     $ NetworkCommand cmdInfo noNetworkTab    , Command-      (pure "list") (remainingArg "arguments")-      "Send LIST query to server.\n"+      (pure "list")+      (optionalArg (extensionArg "[clientarg]" listArgs))+      "\^BParameters:\^B\n\+      \\n\+      \    clientarg: An optionally-comma-separated list of\n\+      \               flags for controlling the list.\n\+      \        ~: Always refresh the list.\n\+      \        >n: Show only channels with more than \^Bn\^B users.\n\+      \        <n: Show only channels with less than \^Bn\^B users.\n\+      \\n\+      \    serverarg: The ELIST argument to send to the server.\n\+      \\n\+      \\^BDescription:\^B\n\+      \\n\+      \    View the list of public channels on the server.\n\+      \\n\+      \    Sends a LIST query and caches the result;\n\+      \    on larger networks on slower connections,\n\+      \    this may take a while to complete.\n\+      \\n\+      \\^BExamples:\^B\n\+      \\n\+      \    /list\n\+      \    /list >100\n\+      \    /list ~ <20\n\+      \    /list , *-ops"     $ NetworkCommand cmdList simpleNetworkTab    , Command@@ -123,11 +156,51 @@                                 Nothing -> ""      commandSuccess st -cmdList :: NetworkCommand String+cmdList :: NetworkCommand (Maybe ListArgs) cmdList cs st rest =-  do sendMsg cs (ircList (Text.pack <$> words rest))-     commandSuccess st+    do+      let lsa = fromMaybe lsaDefault rest+      let connecting = has (csPingStatus . _PingConnecting) cs+      let elist = Just (Text.pack (fromMaybe "" (_lsaElist lsa)))+      let cached = elist == view (csChannelList . clsElist) cs+      let sendM = sendMsg cs (ircList (Text.pack <$> maybeToList (_lsaElist lsa)))+      unless (connecting || (cached && not (_lsaRefresh lsa))) sendM+      let cs' = set (csChannelList . clsElist) elist cs +      let subfocus = FocusChanList (_lsaMin lsa) (_lsaMax lsa)+      commandSuccessUpdateCS cs' (changeSubfocus subfocus st) +listArgs :: ClientState -> String -> Maybe (Args ClientState ListArgs)+listArgs _ = fmap (withElist (optionalArg (simpleToken "[serverarg]"))) . lsaParse+    where withElist arg a = fmap (\s -> a { _lsaElist = s }) arg++data ListArgs = ListArgs+  { _lsaElist   :: Maybe String+  , _lsaRefresh :: Bool+  , _lsaMin     :: Maybe Int+  , _lsaMax     :: Maybe Int+  }++lsaDefault :: ListArgs+lsaDefault = ListArgs+  { _lsaElist = Nothing+  , _lsaRefresh = False+  , _lsaMin = Nothing+  , _lsaMax = Nothing+  }++lsaParse :: String -> Maybe ListArgs+lsaParse = lsaParse' lsaDefault+  where+    lsaParse' lsa str = case str of+      '~':rest -> lsaParse' lsa{ _lsaRefresh = True } rest+      ',':rest -> lsaParse' lsa rest+      '>':(reads -> [(min', rest)]) | min' >= 0 ->+        lsaParse' lsa{ _lsaMin = Just min'} rest+      '<':(reads -> [(max', rest)]) | max' >= 0 ->+        lsaParse' lsa{ _lsaMax = Just max'} rest+      "" -> Just lsa+      _ -> Nothing+ cmdLusers :: NetworkCommand (Maybe String) cmdLusers cs st arg =   do sendMsg cs $ ircLusers $ fmap Text.pack $@@ -183,10 +256,15 @@   do sendMsg cs (ircWhois (Text.pack <$> words rest))      commandSuccess st -cmdWho :: NetworkCommand String-cmdWho cs st rest =-  do sendMsg cs (ircWho (Text.pack <$> words rest))-     commandSuccess st+cmdWho :: NetworkCommand (Maybe (String, Maybe String))+cmdWho _  st Nothing = commandSuccess (changeSubfocus FocusWho st)+cmdWho cs st (Just (query, arg)) =+  do+    let query' = Text.pack query+    let arg' = fromMaybe "" arg+    let cs' = set csWhoReply (newWhoReply query' arg') cs+    sendMsg cs (ircWho (query' : maybeToList (Text.pack <$> arg)))+    commandSuccessUpdateCS cs' (changeSubfocus FocusWho st)  cmdWhowas :: NetworkCommand String cmdWhowas cs st rest =
src/Client/Commands/Recognizer.hs view
@@ -21,14 +21,12 @@   , keys   ) where -import Control.Monad import Control.Applicative hiding (empty)--import           Data.HashMap.Strict (lookup,insertWith,HashMap,empty,unionWith,fromList,toList)-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Maybe-+import Control.Monad (guard)+import Data.HashMap.Strict (lookup,insertWith,HashMap,empty,unionWith,fromList,toList)+import Data.Maybe (fromMaybe, maybeToList)+import Data.Text (Text)+import Data.Text qualified as Text import Prelude hiding (all,lookup)  -- | A map from 'Text' values to 'a' values that is capable of yielding more
src/Client/Commands/TabCompletion.hs view
@@ -8,18 +8,18 @@  module Client.Commands.TabCompletion where -import           Client.Commands.Types-import           Client.Commands.WordCompletion-import           Client.Message-import           Client.State-import           Client.State.Focus-import           Client.State.Network-import           Client.State.Window-import           Client.State.Channel-import           Control.Lens+import Client.Commands.Types+import Client.Commands.WordCompletion (wordComplete, Prefix, WordCompletionMode)+import Client.Message (IrcSummary(ChatSummary))+import Client.State+import Client.State.Channel (chanUsers)+import Client.State.Focus (Focus(ChannelFocus))+import Client.State.Network (csChannels, csNick)+import Client.State.Window (winMessages, wlSummary)+import Control.Lens (view, filtered, folding, preview, toListOf, traverseOf, Ixed(ix), Each(each))+import Irc.Identifier (Identifier)+import Irc.UserInfo (UserInfo(userNick)) import qualified Data.HashMap.Strict as HashMap-import           Irc.Identifier-import           Irc.UserInfo  -- | Provides no tab completion for client commands noClientTab :: Bool -> ClientCommand String
src/Client/Commands/Toggles.hs view
@@ -9,11 +9,11 @@  module Client.Commands.Toggles (togglesCommands) where -import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.Configuration-import           Client.State-import           Control.Lens+import Client.Commands.TabCompletion (noClientTab)+import Client.Commands.Types+import Client.Configuration (EditMode(SingleLineEditor, MultiLineEditor), LayoutMode(OneColumn, TwoColumn))+import Client.State+import Control.Lens (over, set)  togglesCommands :: CommandSection togglesCommands = CommandSection "View toggles"
src/Client/Commands/Types.hs view
@@ -9,13 +9,14 @@  module Client.Commands.Types where -import           Client.State (ClientState, clientErrorMsg)-import           Client.State.Network (NetworkState)-import           Data.Text (Text)-import           Data.List.NonEmpty (NonEmpty)-import           Client.Commands.Arguments.Spec-import           Irc.Identifier (Identifier)-import           Control.Lens+import Client.Commands.Arguments.Spec (Args)+import Client.State (ClientState, clientErrorMsg, clientConnection)+import Client.State.Network (NetworkState, csNetwork)+import Control.Lens (set, view)+import Data.List.NonEmpty (NonEmpty)+import Data.Text (Text)+import Irc.Identifier (Identifier)+import LensUtils (setStrict)  -- | Possible results of running a command data CommandResult@@ -75,6 +76,14 @@ -- | Consider the text entry successful and resume the client commandSuccess :: Monad m => ClientState -> m CommandResult commandSuccess = return . CommandSuccess++-- | Consider the text entry successful, and resume the client with+-- a particular network updated.+commandSuccessUpdateCS :: NetworkState -> ClientState -> IO CommandResult+commandSuccessUpdateCS cs st =+  do let network = view csNetwork cs+     commandSuccess+       $ setStrict (clientConnection network) cs st  -- | Consider the text entry a failure and resume the client commandFailure :: Monad m => ClientState -> m CommandResult
src/Client/Commands/Window.hs view
@@ -9,30 +9,30 @@  module Client.Commands.Window (windowCommands, parseFocus) where -import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.Commands.WordCompletion-import           Client.Mask (buildMask)-import           Client.State-import           Client.State.Focus-import           Client.State.Network-import           Client.State.Window (windowClear, wlText, winMessages, winHidden, winSilent, winName)-import           Control.Applicative-import           Control.Exception-import           Control.Lens-import           Data.Foldable-import           Data.List ((\\), nub)-import qualified Client.State.EditBox as Edit-import           Data.HashSet (HashSet)-import           Data.List.NonEmpty (NonEmpty((:|)))-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map as Map-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as LText-import qualified Data.Text.Lazy.IO as LText-import           Irc.Identifier+import Client.Commands.Arguments.Spec+import Client.Commands.TabCompletion+import Client.Commands.Types+import Client.Commands.WordCompletion (plainWordCompleteMode)+import Client.Mask (buildMask)+import Client.State+import Client.State.EditBox qualified as Edit+import Client.State.Focus+import Client.State.Network (csChannels)+import Client.State.Window (windowClear, wlText, winMessages, winHidden, winActivityFilter, winName, activityFilterStrings, readActivityFilter)+import Control.Applicative (liftA2)+import Control.Exception (SomeException, Exception(displayException), try)+import Control.Lens+import Data.Foldable (Foldable(foldl', toList))+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.List ((\\), nub)+import Data.List.NonEmpty (NonEmpty((:|)))+import Data.Map qualified as Map+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Lazy qualified as LText+import Data.Text.Lazy.IO qualified as LText+import Irc.Identifier (Identifier, idText, mkId)  windowCommands :: CommandSection windowCommands = CommandSection "Window management"@@ -228,12 +228,18 @@    , Command       (pure "setwindow")-      (simpleToken "hide|show|loud|silent")+      (simpleToken ("hide|show" ++ concatMap ('|':) activityFilterStrings))       "Set window property.\n\       \\n\-      \\^Bloud\^B / \^Bsilent\^B\n\-      \    Toggles if window activity appears in the status bar.\n\-      \n\+      \\^Bsilent\^B / \^Bquieter\^B / \^Bquiet\^B / \^Bimponly\^B / \^Bloud\^B / \^Blouder\^B\n\+      \    Changes the importance of normal and important messages:\n\+      \      \^Blouder\^B: Upgrades normal to important.\n\+      \      \^Bloud\^B: Uses default values.\n\+      \      \^Bimponly\^B: Downgrades normal to boring.\n\+      \      \^Bquiet\^B: Downgrades important to normal.\n\+      \      \^Bquieter\^B: Downgrades both one step.\n\+      \      \^Bsilent\^B: Downgrades both to boring.\n\+      \\n\       \\^Bshow\^B / \^Bhide\^B\n\       \    Toggles if window appears in window command shortcuts.\n"     $ ClientCommand cmdSetWindow tabSetWindow@@ -256,7 +262,7 @@   case mbSt1 of     Nothing -> commandFailureMsg "no current window" st     Just st1 ->-      let next = clientNextWindowName st+      let next = clientNextWindowName (clientWindowHint (view clientFocus st) st) st           mbName =             case arg of               Just [n] | n `elem` clientWindowNames st -> Right n@@ -284,17 +290,15 @@   where     mbFun =       case cmd of-        "show"   -> Just (set winHidden False)-        "hide"   -> Just (set winName Nothing . set winHidden True)-        "loud"   -> Just (set winSilent False)-        "silent" -> Just (set winSilent True)-        _        -> Nothing+        "show"    -> Just (set winHidden False)+        "hide"    -> Just (set winName Nothing . set winHidden True)+        other     -> set winActivityFilter <$> readActivityFilter other  tabSetWindow :: Bool {- ^ reversed -} -> ClientCommand String tabSetWindow isReversed st _ =   simpleTabCompletion plainWordCompleteMode [] completions isReversed st   where-    completions = ["hide", "show", "loud", "silent"] :: [Text]+    completions = "hide":"show": map Text.pack activityFilterStrings  -- | Implementation of @/grep@ cmdGrep :: ClientCommand String
src/Client/Commands/WordCompletion.hs view
@@ -21,16 +21,16 @@   , CaseText, caseText   ) where -import qualified Client.State.EditBox as Edit-import           Control.Applicative-import           Control.Lens-import           Control.Monad-import           Data.List-import qualified Data.Set as Set-import           Data.String (IsString(..))-import qualified Data.Text as Text-import           Data.Text (Text)-import           Irc.Identifier+import Client.State.EditBox qualified as Edit+import Control.Applicative ((<|>))+import Control.Lens (view, over, set)+import Control.Monad (guard)+import Data.List (find)+import Data.Set qualified as Set+import Data.String (IsString(..))+import Data.Text (Text)+import Data.Text qualified as Text+import Irc.Identifier (Identifier, idPrefix, idText)  -- | Word completion prefix and suffix data WordCompletionMode = WordCompletionMode
src/Client/Commands/ZNC.hs view
@@ -9,17 +9,16 @@  module Client.Commands.ZNC (zncCommands) where -import           Control.Applicative-import           Client.Commands.Arguments.Spec-import           Client.Commands.TabCompletion-import           Client.Commands.Types-import           Client.State.Network (sendMsg)-import           Data.Foldable (asum)-import qualified Data.Text as Text-import           Data.Time-import           Irc.Commands-import           Control.Lens-import           LensUtils (localTimeDay, localTimeTimeOfDay, zonedTimeLocalTime)+import Control.Applicative (asum, liftA2)+import Client.Commands.Arguments.Spec (optionalArg, remainingArg, simpleToken)+import Client.Commands.TabCompletion (noNetworkTab, simpleNetworkTab)+import Client.Commands.Types+import Client.State.Network (sendMsg)+import Data.Text qualified as Text+import Data.Time+import Irc.Commands (ircZnc)+import Control.Lens ((<<.~), (??), over)+import LensUtils (localTimeDay, localTimeTimeOfDay, zonedTimeLocalTime)  zncCommands :: CommandSection zncCommands = CommandSection "ZNC Support"
src/Client/Configuration.hs view
@@ -46,7 +46,9 @@   , configShowPing   , configJumpModifier   , configDigraphs+  , configNotifications +  , configNetworkPalette   , extensionPath   , extensionRtldFlags   , extensionArgs@@ -70,37 +72,37 @@   , UrlArgument(..)   ) where -import           Client.Commands.Interpolation-import           Client.Commands.Recognizer-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.Macro-import           Config.Schema-import           Control.Exception-import           Control.Lens                        hiding (List)-import           Data.Foldable                       (foldl', toList)-import           Data.HashMap.Strict                 (HashMap)-import qualified Data.HashMap.Strict                 as HashMap-import qualified Data.List.NonEmpty                  as NonEmpty-import           Data.Map                            (Map)-import qualified Data.Map                            as Map-import           Data.Maybe-import           Data.Monoid                         (Endo(..))-import           Data.Text                           (Text)-import qualified Data.Text                           as Text-import qualified Data.Vector                         as Vector-import           Digraphs (Digraph(..))-import           Graphics.Vty.Input.Events (Modifier(..), Key(..))-import           Graphics.Vty.Attributes             (Attr)-import           Irc.Identifier                      (Identifier)-import           System.Directory-import           System.FilePath-import           System.Posix.DynamicLinker          (RTLDFlags(..))-import           System.IO.Error+import Client.Commands.Interpolation (Macro)+import Client.Commands.Recognizer (Recognizer)+import Client.Configuration.Colors (attrSpec)+import Client.Configuration.Macros (macroMapSpec)+import Client.Configuration.Notifications (NotifyWith, notifySpec, notifyWithDefault)+import Client.Configuration.ServerSettings+import Client.EventLoop.Actions+import Client.Image.Palette+import Config+import Config.Macro+import Config.Schema+import Control.Exception+import Control.Lens hiding (List)+import Data.Foldable (foldl', toList)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.List.NonEmpty qualified as NonEmpty+import Data.Map  (Map)+import Data.Map qualified as Map+import Data.Maybe (fromMaybe, isNothing)+import Data.Monoid (Endo(..))+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Vector qualified as Vector+import Digraphs (Digraph(..))+import Graphics.Vty.Input.Events (Modifier(..), Key(..))+import Irc.Identifier (Identifier, mkId)+import System.Directory ( getHomeDirectory, getXdgDirectory, XdgDirectory(XdgConfig))+import System.FilePath ((</>), isAbsolute, joinPath, splitDirectories, takeDirectory)+import System.IO.Error (ioeGetFileName, isDoesNotExistError)+import System.Posix.DynamicLinker (RTLDFlags(..))  -- | Top-level client configuration information. When connecting to a -- server configuration from '_configServers' is used where possible,@@ -125,6 +127,7 @@   , _configShowPing        :: Bool -- ^ visibility of ping time   , _configJumpModifier    :: [Modifier] -- ^ Modifier used for jumping windows   , _configDigraphs        :: Map Digraph Text -- ^ Extra digraphs+  , _configNotifications   :: NotifyWith   }   deriving Show @@ -294,6 +297,8 @@                                "Initial setting for visibility of ping times"      _configDigraphs        <- sec' mempty "extra-digraphs" (Map.fromList <$> listSpec digraphSpec)                                "Extra digraphs"+     _configNotifications   <- sec' notifyWithDefault "notifications" notifySpec+                               "Whether and how to show desktop notifications"      return (\def ->              let _configDefaults = snd ssDefUpdate def                  _configServers  = buildServerMap _configDefaults ssUpdates@@ -403,29 +408,18 @@     nickColorsSpec = set palNicks . Vector.fromList . NonEmpty.toList                  <$> nonemptySpec attrSpec -    modeColorsSpec :: Lens' Palette (HashMap Char Attr) -> ValueSpec (Palette -> Palette)-    modeColorsSpec l-      = fmap (set l)-      $ customSpec "modes" (assocSpec attrSpec)-      $ fmap HashMap.fromList-      . traverse (\(mode, attr) ->-          case Text.unpack mode of-            [m] -> Right (m, attr)-            _   -> Left "expected single letter")+    idOverridesSpec = set palIdOverride . HashMap.fromList . concat <$> listSpec idOverrideSpec+    idOverrideSpec =+      sectionsSpec "ids-to-attr" $+      do ids <- reqSection' "ids" (oneOrList stringSpec) "One or more identifiers that this override applies to."+         attr <- reqSection' "color" attrSpec "The style to use."+         pure [(mkId $ Text.pack id', attr) | id' <- ids]      fields :: [SectionsSpec (Maybe (Palette -> Palette))]-    fields = optSection' "nick-colors" nickColorsSpec-             "Colors used to highlight nicknames"--           : optSection' "cmodes" (modeColorsSpec palCModes)-             "Colors used to highlight channel modes"--           : optSection' "umodes" (modeColorsSpec palUModes)-             "Colors used to highlight user modes"--           : optSection' "snomask" (modeColorsSpec palSnomask)-             "Colors used to highlight server notice mask"-+    fields = optSection' "identifier-colors" nickColorsSpec+             "Colors used to highlight identifiers (nicks, channel names)."+           : optSection' "identifier-overrides" idOverridesSpec+             "Colors used to highlight specific identifiers (nicks, channel names)."            : [ optSection' lbl (set l <$> attrSpec) "" | (lbl, Lens l) <- paletteMap ]  extensionSpec :: ValueSpec ExtensionConfiguration@@ -529,3 +523,10 @@   | isAbsolute path                   = path   | "~":rest <- splitDirectories path = joinPath (fpHome fpc : rest)   | otherwise                         = fpBase fpc </> path++-- | Returns a NetworkPalette for the given network name.+configNetworkPalette :: Text -> Configuration -> NetworkPalette+configNetworkPalette net cfg = unifyNetworkPalette palDefault $ fromMaybe defaultNetworkPalette palNet+  where+    palNet = view ssPalette <$> view (configServers . at net) cfg+    palDefault = view (configDefaults. ssPalette) cfg
src/Client/Configuration/Colors.hs view
@@ -1,5 +1,4 @@-{-# Language OverloadedStrings #-}-{-# Language ApplicativeDo #-}+{-# Language OverloadedStrings, ApplicativeDo, LambdaCase, BlockArguments #-}  {-| Module      : Client.Configuration@@ -16,11 +15,11 @@   , attrSpec   ) where -import           Config.Schema-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import           Data.Text (Text)-import           Graphics.Vty.Attributes+import Config.Schema+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Text (Text)+import Graphics.Vty.Attributes  -- | Parse a text attribute. This value should be a sections with the @fg@ and/or -- @bg@ attributes. Otherwise it should be a color entry that will be used@@ -83,10 +82,9 @@ -- | Configuration section that matches 3 integers in the range 0-255 -- representing red, green, and blue values. rgbSpec :: ValueSpec Color-rgbSpec = customSpec "RGB" anySpec $ \rgb ->-  case rgb of-    [r,g,b] -> rgbColor <$> valid r <*> valid g <*> valid b-    _ -> Left "expected 3 numbers"+rgbSpec = customSpec "RGB" anySpec \case+  [r,g,b] -> rgbColor <$> valid r <*> valid g <*> valid b+  _       -> Left "expected 3 numbers"   where     valid x       | x < 0     = Left "minimum color value is 0"
src/Client/Configuration/Macros.hs view
@@ -14,11 +14,11 @@   , macroCommandSpec   ) where -import           Config.Schema.Spec-import           Client.Commands.Interpolation-import           Client.Commands.Recognizer-import           Data.Maybe (fromMaybe)-import           Data.Text (Text)+import Client.Commands.Interpolation+import Client.Commands.Recognizer (fromCommands, Recognizer)+import Config.Schema.Spec+import Data.Maybe (fromMaybe)+import Data.Text (Text)  macroMapSpec :: ValueSpec (Recognizer Macro) macroMapSpec = fromCommands <$> listSpec macroValueSpec
+ src/Client/Configuration/Notifications.hs view
@@ -0,0 +1,51 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.Configuration.Notifications+Description : Options for running commands to notify users+Copyright   : (c) TheDaemoness, 2023+License     : ISC+Maintainer  : emertens@gmail.com+-}+module Client.Configuration.Notifications ( NotifyWith(..), notifyCmd, notifySpec, notifyWithDefault ) where++import           Config.Schema (ValueSpec, atomSpec, nonemptySpec, stringSpec, (<!>))+import qualified Data.Text.Lazy as LText+import           System.Process.Typed (ProcessConfig, proc, setEnv)+import           System.Info (os)+import qualified Data.List.NonEmpty as NonEmpty++data NotifyWith+  = NotifyWithCustom [String]+  | NotifyWithNotifySend+  | NotifyWithOsaScript+  | NotifyWithTerminalNotifier+  deriving Show++notifyCmd :: NotifyWith -> Maybe ((LText.Text, LText.Text) -> ProcessConfig () () ())+notifyCmd (NotifyWithCustom (cmd:args)) = Just $ \(header, body) ->+  proc cmd (args ++ [LText.unpack header, LText.unpack body])+notifyCmd NotifyWithNotifySend = Just $ \(header, body) ->+  proc "notify-send" ["-a", "glirc", LText.unpack header, LText.unpack body]+notifyCmd NotifyWithOsaScript = Just $ \(header, body) ->+  setEnv [("_GLIRC_NOTIF_HEADER", LText.unpack header), ("_GLIRC_NOTIF_BODY", LText.unpack body)] $+  proc "osascript" ["-e", script]+  where+    script = "display notification (system attribute \"_GLIRC_NOTIF_BODY\") with title \"glirc\" subtitle (system attribute \"_GLIRC_NOTIF_HEADER\")"+notifyCmd NotifyWithTerminalNotifier = Just $ \(header, body) ->+  proc "terminal-notifier" ["-title", "glirc", "-subtitle", LText.unpack header, "-message", "\\" <> LText.unpack body]+notifyCmd _ = Nothing++notifyWithDefault :: NotifyWith+notifyWithDefault = case os of+  "darwin" -> NotifyWithOsaScript+  "linux"  -> NotifyWithNotifySend+  _        -> NotifyWithCustom []++notifySpec :: ValueSpec NotifyWith+notifySpec =+  NotifyWithCustom []        <$ atomSpec "no"  <!>+  notifyWithDefault          <$ atomSpec "yes" <!>+  NotifyWithNotifySend       <$ atomSpec "notify-send" <!>+  NotifyWithOsaScript        <$ atomSpec "osascript" <!>+  NotifyWithTerminalNotifier <$ atomSpec "terminal-notifier" <!>+  NotifyWithCustom . NonEmpty.toList <$> nonemptySpec stringSpec
src/Client/Configuration/ServerSettings.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE ApplicativeDo, TemplateHaskell, OverloadedStrings, RecordWildCards, BlockArguments #-}+{-# LANGUAGE LambdaCase, ApplicativeDo, TemplateHaskell, OverloadedStrings, RecordWildCards, BlockArguments #-}  {-| Module      : Client.Configuration.ServerSettings@@ -39,6 +39,8 @@   , ssConnectCmds   , ssSocksHost   , ssSocksPort+  , ssSocksUsername+  , ssSocksPassword   , ssChanservChannels   , ssFloodPenalty   , ssFloodThreshold@@ -55,6 +57,8 @@   , ssTlsCertFingerprint   , ssShowAccounts   , ssCapabilities+  , ssWindowHints+  , ssPalette    -- * SASL Mechanisms   , SaslMechanism(..)@@ -68,6 +72,9 @@   , SecretException(..)   , loadSecrets +  -- * Window Hints+  , WindowHint(..)+   -- * Defaults   , defaultServerSettings @@ -81,31 +88,42 @@   , getRegex   ) where -import           Client.Authentication.Scram (ScramDigest(..))-import           Client.Commands.Interpolation-import           Client.Commands.WordCompletion-import           Client.Configuration.Macros (macroCommandSpec)-import           Config.Schema.Spec-import           Control.Exception (Exception, displayException, throwIO, try)-import           Control.Lens-import           Control.Monad ((>=>))-import qualified Data.ByteString as B-import           Data.ByteString (ByteString)-import           Data.List.NonEmpty (NonEmpty((:|)))-import qualified Data.List.NonEmpty as NonEmpty-import           Data.List.Split (chunksOf, splitOn)-import           Data.Maybe (fromMaybe)-import           Data.Monoid-import           Data.Text (Text)-import qualified Data.Text as Text-import           Irc.Identifier (Identifier, mkId)-import           Network.Socket (HostName, PortNumber)-import           Numeric (readHex)-import qualified System.Exit as Exit-import qualified System.Process as Process-import           Text.Regex.TDFA-import           Text.Regex.TDFA.Text (compile)-import           Hookup (TlsVerify(..))+import Client.Authentication.Scram (ScramDigest(..))+import Client.Commands.Interpolation (ExpansionChunk)+import Client.Commands.WordCompletion+import Client.Configuration.Colors (attrSpec)+import Client.Configuration.Macros (macroCommandSpec)+import Client.Image.Palette (NetworkPalette (..), defaultNetworkPalette)+import Client.State.Focus ( Focus (NetworkFocus, ChannelFocus) )+import Client.State.Window (ActivityFilter (..))+import Config.Schema.Spec+import Control.Exception (Exception, displayException, throwIO, try)+import Control.Lens+import Control.Monad ((>=>))+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.ByteString.Lazy qualified as L+import Data.Char (isLetter)+import qualified Data.HashMap.Strict as HashMap+import Data.List.NonEmpty (NonEmpty((:|)))+import Data.List.NonEmpty qualified as NonEmpty+import Data.List.Split (chunksOf, splitOn)+import Data.Map (Map)+import Data.Map qualified as Map+import Data.Maybe (fromMaybe)+import Data.Monoid (Endo(Endo))+import Data.Semigroup.Foldable (asum1)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Hookup (TlsVerify(..))+import Irc.Identifier (Identifier, mkId)+import Network.Socket (HostName, PortNumber)+import Numeric (readHex)+import System.Exit qualified as Exit+import System.Process.Typed qualified as Process+import Text.Regex.TDFA (Regex, RegexOptions(defaultCompOpt), ExecOption(ExecOption, captureGroups))+import Text.Regex.TDFA.Text (compile)  -- | Static server-level settings data ServerSettings = ServerSettings@@ -130,6 +148,8 @@   , _ssConnectCmds      :: ![[ExpansionChunk]] -- ^ commands to execute upon successful connection   , _ssSocksHost        :: !(Maybe HostName) -- ^ hostname of SOCKS proxy   , _ssSocksPort        :: !PortNumber -- ^ port of SOCKS proxy+  , _ssSocksUsername    :: !(Maybe Text)+  , _ssSocksPassword    :: !(Maybe Secret)   , _ssChanservChannels :: ![Identifier] -- ^ Channels with chanserv permissions   , _ssFloodPenalty     :: !Rational -- ^ Flood limiter penalty (seconds)   , _ssFloodThreshold   :: !Rational -- ^ Flood limited threshold (seconds)@@ -143,6 +163,8 @@   , _ssBindHostName     :: Maybe HostName -- ^ Local bind host   , _ssShowAccounts     :: !Bool -- ^ Render account names   , _ssCapabilities     :: ![Text] -- ^ Extra capabilities to unconditionally request+  , _ssWindowHints      :: Map Focus WindowHint+  , _ssPalette          :: NetworkPalette   }   deriving Show @@ -163,6 +185,13 @@   | SaslEcdh     (Maybe Text) Text Secret -- ^ SASL ECDH-X25519-CHALLENGE - authzid authcid private-key   deriving Show +data WindowHint = WindowHint+  { windowHintName     :: Maybe Char+  , windowHintHideMeta :: Maybe Bool+  , windowHintHidden   :: Maybe Bool+  , windowHintActivity :: Maybe ActivityFilter+  } deriving Show+ -- | Regular expression matched with original source to help with debugging. data KnownRegex = KnownRegex Text Regex @@ -217,6 +246,8 @@        , _ssConnectCmds   = []        , _ssSocksHost     = Nothing        , _ssSocksPort     = 1080+       , _ssSocksUsername = Nothing+       , _ssSocksPassword = Nothing        , _ssChanservChannels = []        , _ssFloodPenalty     = 2 -- RFC 1459 defaults        , _ssFloodThreshold   = 10@@ -230,6 +261,8 @@        , _ssBindHostName     = Nothing        , _ssShowAccounts     = False        , _ssCapabilities     = []+       , _ssWindowHints      = Map.empty+       , _ssPalette          = defaultNetworkPalette        }  serverSpec :: ValueSpec (Maybe Text, ServerSettings -> ServerSettings)@@ -307,6 +340,12 @@       , req "socks-port" ssSocksPort numSpec         "Port number of SOCKS5 proxy server" +      , opt "socks-username" ssSocksUsername textSpec+        "Username of SOCKS5 proxy server"++      , opt "socks-password" ssSocksPassword anySpec+        "Password of SOCKS5 proxy server"+       , req "connect-cmds" ssConnectCmds (listSpec macroCommandSpec)         "Command to be run upon successful connection to server" @@ -354,14 +393,48 @@        , req "capabilities" ssCapabilities anySpec         "Extra capabilities to unconditionally request from the server"++      , req "window-hints" ssWindowHints windowHintsSpec+        "Persistent settings for windows"+      +      , req "palette" ssPalette netPaletteSpec+        "Network-specific palette overrides"       ] +windowHintsSpec :: ValueSpec (Map Focus WindowHint)+windowHintsSpec = Map.fromList <$> listSpec entrySpec+  where+    entrySpec =+      sectionsSpec "window-hint"+        do focus              <- reqSection' "window"    focusSpec   "channel name or network"+           windowHintName     <- optSection' "hotkey"    hotkeySpec  "reserved hotkey"+           windowHintHidden   <- optSection' "hidden"    yesOrNoSpec "hide from statusbar"+           windowHintHideMeta <- optSection' "hide-meta" yesOrNoSpec "hide metadata by default"+           windowHintActivity <- optSection' "activity"  activitySpec "activity indicators"+           pure (focus, WindowHint{..})++    focusSpec =+      NetworkFocus "" <$ atomSpec "network" <!>+      ChannelFocus "" . mkId <$> textSpec++    hotkeySpec =+      '\0' <$ atomSpec "none" <!>+      customSpec "single letter" stringSpec \case+        [x] -> Right x+        _   -> Left "expected a single letter"+ tlsModeSpec :: ValueSpec TlsMode tlsModeSpec =   TlsYes   <$ atomSpec "yes"      <!>   TlsNo    <$ atomSpec "no"       <!>   TlsStart <$ atomSpec "starttls" +-- TODO: May be nice to be able to do this for all Show Enums.+activitySpec :: ValueSpec ActivityFilter+activitySpec = asum1 (NonEmpty.fromList (map mkSpec [(toEnum 0)..]))+  where+    mkSpec a = a <$ atomSpec (Text.pack (show a))+ tlsVerifySpec :: ValueSpec TlsVerify tlsVerifySpec =   VerifyDefault  <$ atomSpec "yes"      <!>@@ -411,8 +484,6 @@       authzid <*> username <*> reqSection "private-key" "Private Key"  -- filepathSpec :: ValueSpec FilePath filepathSpec = customSpec "path" stringSpec $ \str ->   if null str@@ -487,8 +558,9 @@     Right r -> Right (KnownRegex str r)  instance HasSpec Secret where-  anySpec = SecretText <$> textSpec <!>-            SecretCommand <$> sectionsSpec "command" (reqSection "command" "Command and arguments to execute to secret")+  anySpec =+    SecretText <$> textSpec <!>+    SecretCommand <$> sectionsSpec "command" (reqSection "command" "Command and arguments to execute to secret")  data SecretException = SecretException String String   deriving Show@@ -499,19 +571,47 @@ -- Throws 'SecretException' loadSecrets :: ServerSettings -> IO ServerSettings loadSecrets =-  traverseOf (ssPassword             . _Just                  ) (loadSecret "server password") >=>-  traverseOf (ssSaslMechanism        . _Just . _SaslPlain . _3) (loadSecret "SASL password") >=>-  traverseOf (ssTlsClientKeyPassword . _Just                  ) (loadSecret "TLS key password") >=>-  traverseOf (ssSaslMechanism        . _Just . _SaslScram . _4) (loadSecret "SASL password") >=>-  traverseOf (ssSaslMechanism        . _Just . _SaslEcdh  . _3) (loadSecret "SASL private key")+  traverseOf (ssPassword             . _Just) (loadSecret "server password") >=>+  traverseOf (ssSocksPassword        . _Just) (loadSecret "socks password") >=>+  traverseOf (ssTlsClientKeyPassword . _Just) (loadSecret "TLS key password") >=>+  traverseOf (ssSaslMechanism        . _Just) loadSaslSecret +-- | Populate secrets from sasl mechanism+loadSaslSecret :: SaslMechanism -> IO SaslMechanism+loadSaslSecret =+  traverseOf (_SaslPlain . _3) (loadSecret "SASL plain password") >=>+  traverseOf (_SaslScram . _4) (loadSecret "SASL scram password") >=>+  traverseOf (_SaslEcdh  . _3) (loadSecret "SASL ecdh private key")+ -- | Run a command if found and replace it with the first line of stdout result. loadSecret :: String -> Secret -> IO Secret loadSecret _ (SecretText txt) = pure (SecretText txt) loadSecret label (SecretCommand (cmd NonEmpty.:| args)) =   do let u = Text.unpack-     res <- try (Process.readProcessWithExitCode (u cmd) (map u args) "")+     res <- try (Process.readProcess (Process.proc (u cmd) (map u args)))      case res of-       Right (Exit.ExitSuccess,out,_) -> pure (SecretText (Text.pack (takeWhile ('\n' /=) out)))-       Right (Exit.ExitFailure{},_,err) -> throwIO (SecretException label err)+       Right (Exit.ExitSuccess,out,_) ->+         case Text.decodeUtf8' (L.toStrict out) of+             Right str -> pure (SecretText (Text.takeWhile ('\n' /=) str))+             Left e -> throwIO (SecretException label (displayException e))+       Right (Exit.ExitFailure{},_,err) ->+         case Text.decodeUtf8' (L.toStrict err) of+             Right str -> throwIO (SecretException label (Text.unpack str))+             Left e -> throwIO (SecretException label (displayException e))        Left ioe -> throwIO (SecretException label (displayException (ioe::IOError)))+++netPaletteSpec :: ValueSpec NetworkPalette+netPaletteSpec = +  sectionsSpec "palette-net" $+  do _palCModes <- fromMaybe HashMap.empty <$> optSection' "cmodes" colorMapSpec+                   "Overrides for the styles used for specific channel mode letters."+     _palUModes <- fromMaybe HashMap.empty <$> optSection' "umodes" colorMapSpec+                   "Overrides for the styles used for specific user mode letters."+     _palSnomask <- fromMaybe HashMap.empty <$> optSection' "snomask" colorMapSpec+                    "Overrides for the styles used for specific snomask letters."+     pure NetworkPalette{..}+  where+    colorMapSpec = HashMap.fromList . concatMap expand <$> assocSpec attrSpec+      where+        expand (modes, style) = [(mode, style) | mode <- Text.unpack modes, isLetter mode]
src/Client/Configuration/Sts.hs view
@@ -19,21 +19,22 @@   , savePolicyFile   ) where -import           Config (Value(..), Section(..), parse, pretty)-import           Config.Number (integerToNumber)-import           Config.Schema.Spec-import           Config.Schema.Load (loadValue)-import           Control.Exception (try)-import           Control.Lens (makeLenses)-import           Data.Maybe (fromMaybe)-import           Data.Time (UTCTime, defaultTimeLocale, formatTime, parseTimeM, iso8601DateFormat)-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.IO as Text-import           System.Directory (getXdgDirectory, XdgDirectory(XdgConfig), createDirectoryIfMissing)-import           System.FilePath ((</>), takeDirectory)-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap+import Config (Value(..), Section(..), parse, pretty)+import Config.Number (integerToNumber)+import Config.Schema.Load (loadValue)+import Config.Schema.Spec+import Control.Exception (try)+import Control.Lens (makeLenses)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.IO qualified as Text+import Data.Time (UTCTime)+import Data.Time.Format.ISO8601 (formatParseM, formatShow, ISO8601(iso8601Format))+import System.Directory (getXdgDirectory, XdgDirectory(XdgConfig), createDirectoryIfMissing)+import System.FilePath ((</>), takeDirectory)  data StsPolicy = StsPolicy   { _stsExpiration :: !UTCTime@@ -68,7 +69,7 @@           Section () "until"             (Text ()                (Text.pack-                 (formatTime defaultTimeLocale dateTimeFormat+                 (formatShow iso8601Format                    (_stsExpiration v))))         ]     | (k, v) <- HashMap.toList p ]@@ -106,7 +107,4 @@ dateTimeSpec   = customSpec "date-time" stringSpec   $ maybe (Left "unable to parse") Right-  . parseTimeM False defaultTimeLocale dateTimeFormat--dateTimeFormat :: String-dateTimeFormat = iso8601DateFormat (Just "%H:%M:%S")+  . formatParseM iso8601Format
src/Client/EventLoop.hs view
@@ -1,4 +1,4 @@-{-# Language BangPatterns, OverloadedStrings, NondecreasingIndentation #-}+{-# Language BangPatterns, OverloadedStrings, NondecreasingIndentation, PatternSynonyms #-}  {-| Module      : Client.EventLoop@@ -17,51 +17,53 @@   , ClientEvent(..)   ) where -import           Client.CApi (ThreadEntry, popTimer)-import           Client.Commands-import           Client.Configuration (configJumpModifier, configKeyMap, configWindowNames, configDigraphs)-import           Client.Configuration.ServerSettings-import           Client.EventLoop.Actions-import           Client.EventLoop.Errors (exceptionToLines)-import           Client.EventLoop.Network (clientResponse)-import           Client.Hook-import           Client.Image-import           Client.Image.Layout (scrollAmount)-import           Client.Image.StatusLine (clientTitle)-import           Client.Log-import           Client.Message-import           Client.Network.Async-import           Client.State-import qualified Client.State.EditBox as Edit-import           Client.State.Extensions-import           Client.State.Focus-import           Client.State.Network-import           Control.Concurrent.STM-import           Control.Exception-import           Control.Lens-import           Control.Monad-import           Data.ByteString (ByteString)-import           Data.Char (isSpace)-import           Data.Foldable-import           Data.Traversable-import           Data.List-import           Data.List.NonEmpty (NonEmpty, nonEmpty)-import           Data.Maybe-import           Data.Ord-import           Data.Text (Text)-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text-import qualified Data.Text.Encoding.Error as Text-import           Data.Time-import           GHC.IO.Exception (IOErrorType(..), ioe_type)-import           Graphics.Vty-import           Graphics.Vty.Input.Events-import           Irc.Message-import           Irc.Codes-import           Irc.RawIrcMsg-import           LensUtils-import           Hookup (ConnectionFailure(..))+import Client.CApi (ThreadEntry, popTimer)+import Client.Commands (CommandResult(..), execute, executeUserCommand, tabCompletion)+import Client.Configuration (configJumpModifier, configKeyMap, configWindowNames, configDigraphs, configNotifications)+import Client.Configuration.Notifications (notifyCmd)+import Client.Configuration.ServerSettings ( ssReconnectAttempts )+import Client.EventLoop.Actions (keyToAction, Action(..))+import Client.EventLoop.Errors (exceptionToLines)+import Client.EventLoop.Network (clientResponse)+import Client.Hook (applyMessageHooks, messageHookStateful)+import Client.Image (clientPicture)+import Client.Image.Layout (scrollAmount)+import Client.Image.StatusLine (clientTitle)+import Client.Log ( writeLogLine )+import Client.Message+import Client.Network.Async+import Client.State+import Client.State.EditBox qualified as Edit+import Client.State.Extensions+import Client.State.Focus (Subfocus(FocusMessages))+import Client.State.Network+import Control.Concurrent.STM+import Control.Exception (SomeException, Exception(fromException), catch)+import Control.Lens+import Control.Monad (when, MonadPlus(mplus), foldM, unless, void)+import Data.ByteString (ByteString)+import Data.Char (isSpace)+import Data.Foldable (Foldable(foldl'), find, asum, traverse_)+import Data.HashMap.Strict qualified as HashMap+import Data.List (partition)+import Data.List.NonEmpty (NonEmpty, nonEmpty)+import Data.Maybe (fromMaybe)+import Data.Ord (comparing)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Data.Text.Encoding.Error qualified as Text+import Data.Time+import Data.Time.Format.ISO8601 (formatParseM, iso8601Format)+import Data.Traversable (for)+import GHC.IO.Exception (IOErrorType(..), ioe_type)+import Graphics.Vty+import Hookup (ConnectionFailure(..))+import Irc.Codes (pattern RPL_STARTTLS)+import Irc.Message (IrcMsg(Reply, Notice), cookIrcMsg, msgTarget)+import Irc.RawIrcMsg (RawIrcMsg, TagEntry(..), asUtf8, msgTags, parseRawIrcMsg)+import LensUtils (setStrict)+import System.Process.Typed (startProcess, setStdin, setStdout, setStderr, nullStream)   -- | Sum of the five possible event types the event loop handles@@ -83,7 +85,7 @@   do timer <- prepareTimer      atomically (asum [timer, vtyEvent, networkEvents, threadJoin])   where-    vtyEvent = VtyEvent <$> readTChan (_eventChannel (inputIface vty))+    vtyEvent = VtyEvent <$> readTChan (eventChannel (inputIface vty))      networkEvents =       do xs <- for (HashMap.toList (view clientConnections st)) $ \(network, conn) ->@@ -135,6 +137,7 @@ eventLoop :: Vty -> ClientState -> IO () eventLoop vty st =   do when (view clientBell st) (beep vty)+     processNotifications st      processLogEntries st       let (pic, st') = clientPicture (clientTick st)@@ -151,7 +154,7 @@          eventLoop vty =<< doTimerEvent networkId action st'        VtyEvent (InputEvent vtyEvent) ->          traverse_ (eventLoop vty) =<< doVtyEvent vty vtyEvent st'-       VtyEvent ResumeAfterSignal ->+       VtyEvent ResumeAfterInterrupt ->          eventLoop vty =<< updateTerminalSize vty st        NetworkEvents networkEvents ->          eventLoop vty =<< foldM doNetworkEvent st' networkEvents@@ -175,6 +178,20 @@ processLogEntries =   traverse_ writeLogLine . reverse . view clientLogQueue +processNotifications :: ClientState -> IO ()+processNotifications st =+  case notifyCmd (view (clientConfig . configNotifications) st) of+    Just cmd | not (view clientUiFocused st) -> traverse_ (spawn cmd) (view clientNotifications st)+    _ -> return ()+  where+    -- TODO: May be a nicer way to handle notification failure than just silently squashing the exception+    handleException :: SomeException -> IO ()+    handleException _ = return ()+    spawn cmd pair = do+      let procCfg = setStdin nullStream . setStdout nullStream . setStderr nullStream $ cmd pair+      -- Maybe find a nicer way to get an error out of here.+      catch (void (startProcess procCfg)) handleException+ -- | Respond to a network connection successfully connecting. doNetworkOpen ::   Text        {- ^ network name -} ->@@ -370,8 +387,7 @@  -- | Parses the time format used by ZNC for buffer playback parseZncTime :: String -> Maybe UTCTime-parseZncTime = parseTimeM True defaultTimeLocale-             $ iso8601DateFormat (Just "%T%Q%Z")+parseZncTime = formatParseM iso8601Format   -- | Update the height and width fields of the client state@@ -401,6 +417,10 @@     EvPaste utf8 ->        do let str = Text.unpack (Text.decodeUtf8With Text.lenientDecode utf8)           return $! Just $! over clientTextBox (Edit.insertPaste str) st+    EvLostFocus ->+      return (Just $! set clientUiFocused False st)+    EvGainedFocus ->+      return (Just $! set clientUiFocused True st)     _ -> return (Just st)  
src/Client/EventLoop/Actions.hs view
@@ -23,20 +23,20 @@   , 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.Text (Text)-import qualified Data.Text as Text-import           Text.Read+import Config.Schema.Spec (anyAtomSpec, customSpec, HasSpec(..))+import Control.Applicative (Alternative(empty))+import Control.Lens (Lens', (^.), non', (?~), set, At(at), AsEmpty(_Empty))+import Data.Char (showLitChar)+import Data.Functor.Compose (Compose(Compose, getCompose))+import Data.HashMap.Lazy (HashMap)+import Data.HashMap.Lazy qualified as HashMap+import Data.List (nub, sort)+import Data.Map (Map)+import Data.Map qualified as Map+import Data.Text (Text)+import Data.Text qualified as Text+import Graphics.Vty.Input.Events+import Text.Read (readMaybe)  -- | Actions that can be invoked using the keyboard. data Action
src/Client/EventLoop/Errors.hs view
@@ -14,10 +14,10 @@   ( exceptionToLines   ) where -import           Control.Exception-import           Data.List.NonEmpty (NonEmpty(..))-import           OpenSSL.Session-import           Hookup (ConnectionFailure(..))+import Control.Exception (SomeException, Exception(displayException, fromException))+import Data.List.NonEmpty (NonEmpty(..))+import OpenSSL.Session (ConnectionAbruptlyTerminated, ProtocolError(ProtocolError))+import Hookup (ConnectionFailure(..))  -- | Compute the message message text to be used for a connection error exceptionToLines ::
src/Client/EventLoop/Network.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, PatternSynonyms #-} {-| Module      : Client.EventLoop.Network Description : Event handlers for network messages affecting the client state@@ -13,27 +13,27 @@   ( clientResponse   ) where -import           Client.Commands-import           Client.Commands.Interpolation-import           Client.Configuration.ServerSettings-import           Client.Configuration.Sts-import           Client.Network.Async-import           Client.Network.Connect-import           Client.State-import           Client.State.Focus-import           Client.State.Network-import           Control.Lens-import           Control.Monad-import           Data.Text (Text)-import           Data.Time-import           Irc.Codes-import           Irc.Commands-import           Irc.Identifier-import           Irc.Message-import qualified Client.Authentication.Ecdsa as Ecdsa-import qualified Data.Text as Text-import qualified Data.Text.Read as Text-import           Text.Regex.TDFA.Text as Regex+import Client.Authentication.Ecdsa qualified as Ecdsa+import Client.Commands (CommandResult(CommandQuit, CommandFailure, CommandSuccess), commandExpansion, executeUserCommand)+import Client.Commands.Interpolation (resolveMacroExpansions, ExpansionChunk)+import Client.Configuration.ServerSettings+import Client.Configuration.Sts (savePolicyFile, StsPolicy(StsPolicy, _stsPort, _stsExpiration))+import Client.Network.Async (abortConnection, TerminationReason(StsUpgrade))+import Client.Network.Connect (ircPort)+import Client.State+import Client.State.Focus (Focus(ChannelFocus, NetworkFocus))+import Client.State.Network+import Control.Lens (view, (&), folded, previews, views, (?~), set, At(at))+import Control.Monad (join, foldM, forM)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Read qualified as Text+import Data.Time (ZonedTime, addUTCTime, getCurrentTime, formatTime, defaultTimeLocale, utcToLocalZonedTime)+import Irc.Codes (pattern ERR_LINKCHANNEL, pattern RPL_WELCOME)+import Irc.Commands (ircAuthenticate, ircCapEnd)+import Irc.Identifier (mkId)+import Irc.Message (IrcMsg(Error, Reply, Authenticate, Cap), CapCmd(CapNew, CapLs))+import Text.Regex.TDFA.Text qualified as Regex  -- | Client-level responses to specific IRC messages. -- This is in contrast to the connection state tracking logic in
src/Client/Hook.hs view
@@ -22,10 +22,9 @@   , applyMessageHooks   ) where -import Control.Lens-import Data.Text--import Irc.Message+import Control.Lens (view, makeLenses)+import Data.Text (Text)+import Irc.Message (IrcMsg)  -- | The possible results of a 'MessageHook' action. A hook can decline to -- handle a message ('PassMessage'), filter out a message ('OmitMessage'),
src/Client/Hook/DroneBLRelay.hs view
@@ -15,19 +15,17 @@   ( droneblRelayHook   ) where -import           Data.List (uncons)-import qualified Data.Text as Text-import           Data.Text (Text)-import           Data.Foldable (asum)--import           Text.Regex.TDFA (match, defaultCompOpt, defaultExecOpt, Regex)-import           Text.Regex.TDFA.String (compile)--import           Client.Hook (MessageHook(..), MessageResult(..))-import           Irc.Message-import           Irc.Identifier (mkId, Identifier)-import           Irc.UserInfo (UserInfo(..))-import           StrQuote (str)+import Client.Hook (MessageHook(..), MessageResult(..))+import Data.Foldable (asum)+import Data.List (uncons)+import Data.Text (Text)+import Data.Text qualified as Text+import Irc.Identifier (mkId, Identifier)+import Irc.Message (IrcMsg(Mode, Privmsg, Ctcp, Join, Part, Quit, Nick, Kick), Source(Source))+import Irc.UserInfo (UserInfo(..))+import StrQuote (str)+import Text.Regex.TDFA (match, defaultCompOpt, defaultExecOpt, Regex)+import Text.Regex.TDFA.String (compile)  -- | Hook for mapping messages in #dronebl -- to appear like native messages.
src/Client/Hook/Matterbridge.hs view
@@ -25,16 +25,13 @@ -} module Client.Hook.Matterbridge (matterbridgeHook) where -import Data.Text (Text)--import Control.Lens (set, view)--import Text.Regex.TDFA ((=~))- import Client.Hook (MessageHook(..), MessageResult(..))-import Irc.Message+import Control.Lens (set, view)+import Data.Text (Text) import Irc.Identifier (mkId, Identifier)+import Irc.Message (IrcMsg(Ctcp, Privmsg), Source(Source)) import Irc.UserInfo (UserInfo(..), uiNick)+import Text.Regex.TDFA ((=~))  data MbMsg = Msg | Act @@ -57,9 +54,9 @@  remap' :: MbMsg -> UserInfo -> Identifier -> Text -> MessageResult remap' mbmsg ui chan msg =-  case msg =~ ("^<([^>]+)> (.*)$"::Text) of-    [_,nick,msg']:_ -> RemapMessage (newmsg mbmsg (fakeUser nick ui) chan msg')-    _               -> PassMessage+  case msg =~ ("^(\x03\&[0-9]{2})?<([^>]+)> \x0f?(.*)$"::Text) of+    [_,_,nick,msg']:_ -> RemapMessage (newmsg mbmsg (fakeUser nick ui) chan msg')+    _                 -> PassMessage  newmsg :: MbMsg -> Source -> Identifier -> Text -> IrcMsg newmsg Msg src chan msg = Privmsg src chan msg
src/Client/Hook/Snotice.hs view
@@ -13,17 +13,16 @@   ( snoticeHook   ) where -import qualified Data.Text as Text-import           Data.Text (Text)-import           Data.List (find)-import           Text.Regex.TDFA-import           Text.Regex.TDFA.String--import           Client.Hook-import           Irc.Message-import           Irc.Identifier (mkId, Identifier)-import           Irc.UserInfo-import           StrQuote (str)+import Client.Hook (MessageHook(MessageHook), MessageResult(..))+import Data.List (find)+import Data.Text (Text)+import Data.Text qualified as Text+import Irc.Identifier (mkId, Identifier)+import Irc.Message (IrcMsg(Notice), Source(Source))+import Irc.UserInfo (UserInfo(UserInfo))+import StrQuote (str)+import Text.Regex.TDFA+import Text.Regex.TDFA.String (compile)  snoticeHook :: MessageHook snoticeHook = MessageHook "snotice" True remap@@ -98,6 +97,7 @@     (2, "k", [str|^[^ ]+ added temporary [0-9]+ min. K-Line for |]),     -- Nick collision     (1, "m", [str|^Nick collision on|]),+    (1, "m", [str|^Nick collision due to services forced nick change on|]),     -- KILLs     (0, "k", [str|^Received KILL message for [^ ]+\. From NickServ |]),     (0, "k", [str|^Received KILL message for [^ ]+\. From syn Path: [^ ]+ \(Facility Blocked\)|]),@@ -114,8 +114,8 @@     (1, "f", [str|^Failed (OPER|CHALLENGE) attempt - host mismatch|]),     (3, "f", [str|^Failed (OPER|CHALLENGE) attempt|]), -- ORDER IMPORTANT - catch all failed attempts that aren't host mismatch -    (1, "k", [str|^Rejecting [XK]-Lined user|]),-    (1, "k", [str|^Disconnecting [XK]-Lined user|]),+    (1, "k", [str|^Rejecting [DKX]-Lined user|]),+    (1, "k", [str|^Disconnecting [DKX]-Lined user|]),     (1, "k", [str|^KLINE active for|]),     (1, "k", [str|^XLINE active for|]),     (3, "k", [str|^KLINE over-ruled for |]),@@ -127,6 +127,7 @@     (2, "k", [str|^[^ ]+![^ ]+@[^ ]+\{[^ ]+\} has removed the X-Line for:|]),     (2, "k", [str|^[^ ]+![^ ]+@[^ ]+\{[^ ]+\} is removing the X-Line for|]),     (2, "k", [str|^[^ ]+![^ ]+@[^ ]+\{[^ ]+\} has removed the temporary D-Line for:|]),+    (2, "k", [str|^User [^ ]+ \([^ ]+@[^ ]+\) is attempting to join locally juped channel [^ ]+ \(.*\)$|]),      (0, "m", [str|^Received SAVE message for|]),     (0, "m", [str|^Ignored noop SAVE message for|]),
src/Client/Hook/Znc/Buffextras.hs view
@@ -17,14 +17,13 @@   ( buffextrasHook   ) where -import           Data.Attoparsec.Text as P-import           Data.Text as Text hiding (head)--import           Client.Hook-import           Irc.Identifier-import           Irc.Message-import           Irc.RawIrcMsg-import           Irc.UserInfo+import Client.Hook (MessageHook(MessageHook), MessageResult(..))+import Data.Attoparsec.Text as P+import Data.Text as Text (Text, null, words)+import Irc.Identifier (Identifier, mkId)+import Irc.Message (IrcMsg(Topic, Privmsg, Join, Quit, Part, Nick, Mode, Kick), Source(Source, srcUser))+import Irc.RawIrcMsg (prefixParser, simpleTokenParser)+import Irc.UserInfo (UserInfo(userNick))  -- | Map ZNC's buffextras messages to native client messages. -- Set debugging to pass through buffextras messages that
src/Client/Hooks.hs view
@@ -15,14 +15,14 @@   ( messageHooks   ) where -import Data.Text-import Data.HashMap.Strict-import Client.Hook+import Client.Hook (MessageHook)+import Data.HashMap.Strict (HashMap, fromList)+import Data.Text (Text) -import Client.Hook.DroneBLRelay-import Client.Hook.Matterbridge-import Client.Hook.Snotice-import Client.Hook.Znc.Buffextras+import Client.Hook.DroneBLRelay (droneblRelayHook)+import Client.Hook.Matterbridge (matterbridgeHook)+import Client.Hook.Snotice (snoticeHook)+import Client.Hook.Znc.Buffextras (buffextrasHook)  -- | All the available message hooks. messageHooks :: HashMap Text ([Text] -> Maybe MessageHook)
src/Client/Image.hs view
@@ -11,12 +11,11 @@ -} module Client.Image (clientPicture) where -import           Client.Image.Layout-import           Client.State-import           Control.Lens-import           Graphics.Vty            (Background (..), Cursor (..),-                                          Picture (..))-import           Graphics.Vty.Image+import Client.Image.Layout (drawLayout)+import Client.State (clientHeight, clientScroll, clientTextBoxOffset, ClientState)+import Control.Lens (view, over, set)+import Graphics.Vty (Background (..), Cursor (..),  Picture (..))+import Graphics.Vty.Image (Image)  -- | Generate a 'Picture' for the current client state. The resulting -- client state is updated for render specific information like scrolling.
src/Client/Image/Layout.hs view
@@ -8,18 +8,18 @@ -} module Client.Image.Layout (scrollAmount, drawLayout) where -import Control.Lens-import Client.State-import Client.State.Focus import Client.Configuration (LayoutMode(..))+import Client.Image.LineWrap (lineWrap, terminate) import Client.Image.PackedImage (Image', unpackImage)+import Client.Image.Palette (palWindowDivider) import Client.Image.StatusLine (statusLineImage, minorStatusLineImage)-import Client.Image.Textbox-import Client.Image.LineWrap (lineWrap, terminate)-import Client.Image.Palette-import Client.View-import Graphics.Vty.Image+import Client.Image.Textbox (textboxImage)+import Client.State+import Client.State.Focus ( Focus, Subfocus )+import Client.View (viewLines)+import Control.Lens (view) import Graphics.Vty.Attributes (defAttr)+import Graphics.Vty.Image  -- | Compute the combined image for all the visible message windows. drawLayout ::
src/Client/Image/LineWrap.hs view
@@ -15,10 +15,10 @@   , terminate   ) where -import           Client.Image.PackedImage-import qualified Graphics.Vty.Image as Vty-import           Graphics.Vty.Attributes-import qualified Data.Text.Lazy as L+import Client.Image.PackedImage (char, imageText, imageWidth, splitImage, string, Image')+import Graphics.Vty.Image qualified as Vty+import Graphics.Vty.Attributes (defAttr)+import Data.Text.Lazy qualified as L   -- | Trailing space with default attributes deals with bug in VTY
src/Client/Image/Message.hs view
@@ -25,37 +25,39 @@   , nickPad   , timeImage   , drawWindowLine-+  , modesImage+  , prettyTime   , parseIrcTextWithNicks   , Highlight(..)   ) where -import           Client.Configuration (PaddingMode(..))-import           Client.Image.LineWrap-import           Client.Image.MircFormatting-import           Client.Image.PackedImage-import           Client.Image.Palette-import           Client.Message-import           Client.State.Window-import           Client.UserHost-import           Control.Applicative ((<|>))-import           Control.Lens-import           Data.Char-import           Data.Hashable (hash)-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import           Data.List-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Time-import qualified Data.Vector as Vector-import           Graphics.Vty.Attributes-import           Irc.Codes-import           Irc.Identifier-import           Irc.Message-import           Irc.RawIrcMsg-import           Irc.UserInfo-import           Text.Read+import Client.Configuration (PaddingMode(..))+import Client.Image.LineWrap (lineWrapPrefix)+import Client.Image.MircFormatting (parseIrcText, parseIrcText')+import Client.Image.PackedImage (char, imageWidth, string, text', Image')+import Client.Image.Palette+import Client.Message+import Client.State.Window (unpackTimeOfDay, wlImage, wlPrefix, wlTimestamp, WindowLine)+import Client.UserHost ( uhAccount, UserAndHost )+import Control.Applicative ((<|>))+import Control.Lens (view, (^?), filtered, folded, views, Ixed(ix), At (at))+import Data.Char (ord, chr, isControl)+import Data.Hashable (hash)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.List (intercalate, intersperse)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (UTCTime, ZonedTime, TimeOfDay, formatTime, defaultTimeLocale, parseTimeM)+import Data.Vector qualified as Vector+import Graphics.Vty.Attributes+import Irc.Codes+import Irc.Identifier (Identifier, idText, mkId)+import Irc.Message+import Irc.RawIrcMsg (msgCommand, msgParams, msgPrefix)+import Irc.UserInfo (UserInfo(userHost, userNick, userName))+import Text.Read (readMaybe)  -- | Parameters used when rendering messages data MessageRendererParams = MessageRendererParams@@ -64,6 +66,8 @@   , rendHighlights :: HashMap Identifier Highlight -- ^ words to highlight   , rendPalette    :: Palette -- ^ nick color palette   , rendAccounts   :: Maybe (HashMap Identifier UserAndHost)+  , rendNetPalette :: NetworkPalette+  , rendChanTypes  :: [Char] -- ^ A list of valid channel name prefixes.   }  -- | Default 'MessageRendererParams' with no sigils or nicknames specified@@ -74,9 +78,10 @@   , rendHighlights  = HashMap.empty   , rendPalette     = defaultPalette   , rendAccounts    = Nothing+  , rendNetPalette  = defaultNetworkPalette+  , rendChanTypes   = "#&!+" -- Default for if we aren't told otherwise by ISUPPORT.   } - -- | Construct a message given the time the message was received and its -- render parameters. msgImage ::@@ -239,6 +244,7 @@     Ping       {} -> mempty     Pong       {} -> mempty     Nick       {} -> mempty+    Away       {} -> mempty      -- details in message part     Topic src _ _  -> who src@@ -301,6 +307,7 @@     BatchEnd    {} -> mempty     Nick        {} -> mempty     Authenticate{} -> "***"+    Away        {} -> mempty      Error                   txt -> parseIrcText pal txt     Topic _ _ txt ->@@ -334,10 +341,15 @@       separatedParams pal (view msgParams irc)     Cap cmd           -> ctxt (capCmdText cmd) -    Mode _ _ params ->+    Mode _ chan (modes:params) ->       "set mode: " <>+      modesImage (view palModes pal) (modesPaletteFor chan rp) (Text.unpack modes) <>+      " " <>       ircWords pal params +    Mode _ _ [] ->+      "changed no modes"+     Invite _ tgt chan ->       "invited " <>       coloredIdentifier pal NormalIdentifier hilites tgt <>@@ -375,13 +387,13 @@   in   case body of     Nick old new ->-      string quietAttr "nick " <>+      string (view palUsrChg pal) "nick " <>       who old <>       " is now known as " <>       coloredIdentifier pal NormalIdentifier hilites new      Join nick _chan acct gecos ->-      string quietAttr "join " <>+      string (view palJoin pal) "join " <>       plainWho (srcUser nick) <>       accountPart <> gecosPart       where@@ -394,21 +406,21 @@           | otherwise       = text' quietAttr (" [" <> cleanText gecos <> "]")      Part nick _chan mbreason ->-      string quietAttr "part " <>+      string (view palPart pal) "part " <>       who nick <>       foldMap (\reason -> string quietAttr " (" <>                           parseIrcText pal reason <>                           string quietAttr ")") mbreason      Quit nick mbreason ->-      string quietAttr "quit "   <>+      string (view palPart pal) "quit "   <>       who nick <>       foldMap (\reason -> string quietAttr " (" <>                           parseIrcText pal reason <>                           string quietAttr ")") mbreason      Kick kicker _channel kickee reason ->-      string quietAttr "kick " <>+      string (view palPart pal) "kick " <>       who kicker <>       " kicked " <>       coloredIdentifier pal NormalIdentifier hilites kickee <>@@ -416,7 +428,7 @@       parseIrcText pal reason      Kill killer killee reason ->-      string quietAttr "kill " <>+      string (view palPart pal) "kill " <>       who killer <>       " killed " <>       coloredIdentifier pal NormalIdentifier hilites killee <>@@ -498,26 +510,41 @@       ": " <>       ctxt (capCmdText cmd) -    Mode nick _chan params ->-      string quietAttr "mode " <>+    Mode nick chan (modes:params) ->+      string (view palModes pal) "mode " <>       who nick <> " set mode: " <>+      modesImage (view palModes pal) (modesPaletteFor chan rp) (Text.unpack modes) <>+      " " <>       ircWords pal params +    Mode nick _ [] ->+      string (view palModes pal) "mode " <>+      who nick <> " changed no modes"+     Authenticate{} -> "AUTHENTICATE ***"     BatchStart{}   -> "BATCH +"     BatchEnd{}     -> "BATCH -"      Account src acct ->-      string quietAttr "acct " <>+      string (view palUsrChg pal) "acct " <>       who src <> ": " <>       if Text.null acct then "*" else ctxt acct      Chghost user newuser newhost ->-      string quietAttr "chng " <>+      string (view palUsrChg pal) "chng " <>       who user <> ": " <>       ctxt newuser <> " " <> ctxt newhost +    Away user (Just txt) ->+      string (view palAway pal) "away " <>+      who user <> ": " <>+      parseIrcTextWithNicks pal hilites False txt +    Away user Nothing ->+      string (view palUsrChg pal) "back " <>+      who user++ renderCapCmd :: CapCmd -> Text renderCapCmd cmd =   case cmd of@@ -616,7 +643,6 @@         RPL_ETRACEFULL   -> etraceFullParamsImage         RPL_ENDOFTRACE   -> params_2_3_Image         RPL_ENDOFHELP    -> params_2_3_Image-        RPL_LIST         -> listParamsImage         RPL_LINKS        -> linksParamsImage         RPL_ENDOFLINKS   -> params_2_3_Image         RPL_PRIVS        -> privsImage@@ -881,12 +907,6 @@         [_, nick, txt] -> ctxt nick <> label " msg" <> parseIrcText pal txt         _ -> rawParamsImage -    listParamsImage =-      case params of-        [_, chan, users, topic] ->-          ctxt chan <> label " users" <> ctxt users <> label " topic" <> ctxt topic-        _ -> rawParamsImage-     linksParamsImage =       case params of         [_, name, link, Text.breakOn " " -> (hops,location)] ->@@ -1108,7 +1128,7 @@             PrivmsgIdentifier -> view palSelfHighlight palette             NormalIdentifier  -> view palSelf palette -      | otherwise = v Vector.! i+      | otherwise = fromMaybe (v Vector.! i) (HashMap.lookup ident (view palIdOverride palette))      v = view palNicks palette     i = hash ident `mod` Vector.length v@@ -1182,24 +1202,29 @@  -- | Returns image and identifier to be used when collapsing metadata -- messages.-metadataImg :: IrcSummary -> Maybe (Image', Identifier, Maybe Identifier)-metadataImg msg =+metadataImg :: Palette -> IrcSummary -> Maybe (Image', Identifier, Maybe Identifier)+metadataImg pal msg =   case msg of-    QuitSummary who _   -> Just (char (withForeColor defAttr red   ) 'x', who, Nothing)-    PartSummary who     -> Just (char (withForeColor defAttr red   ) '-', who, Nothing)-    JoinSummary who     -> Just (char (withForeColor defAttr green ) '+', who, Nothing)-    CtcpSummary who     -> Just (char (withForeColor defAttr white ) 'C', who, Nothing)-    NickSummary old new -> Just (char (withForeColor defAttr yellow) '>', old, Just new)-    ChngSummary who     -> Just (char (withForeColor defAttr blue  ) '*', who, Nothing)-    AcctSummary who     -> Just (char (withForeColor defAttr blue  ) '*', who, Nothing)-    _                   -> Nothing--+    QuitSummary who _     -> Just (char (view palPart pal)   'x', who, Nothing)+    PartSummary who       -> Just (char (view palPart pal)   '-', who, Nothing)+    JoinSummary who       -> Just (char (view palJoin pal)   '+', who, Nothing)+    CtcpSummary who       -> Just (char (view palIgnore pal) 'C', who, Nothing)+    NickSummary old new   -> Just (char (view palUsrChg pal) '>', old, Just new)+    ChngSummary who       -> Just (char (view palUsrChg pal) '@', who, Nothing)+    AcctSummary who       -> Just (char (view palUsrChg pal) '*', who, Nothing)+    AwaySummary who True  -> Just (char (view palAway pal)   'a', who, Nothing)+    AwaySummary who False -> Just (char (view palUsrChg pal) 'b', who, Nothing)+    _                     -> Nothing  -- | Image used when treating ignored chat messages as metadata-ignoreImage :: Image'-ignoreImage = char (withForeColor defAttr yellow) 'I'+ignoreImage :: Palette -> Image'+ignoreImage pal = char (view palIgnore pal) 'I' +modesImage :: Attr -> HashMap Char Attr -> String -> Image'+modesImage def pal modes = foldMap modeImage modes+  where+    modeImage m =+      char (fromMaybe def (view (at m) pal)) m  -- | Render the normal view of a chat message line padded and wrapped. drawWindowLine ::@@ -1215,3 +1240,10 @@     wrap pfx body = reverse (lineWrapPrefix w pfx body)     drawPrefix = views wlTimestamp drawTime <>                  views wlPrefix    padNick++modesPaletteFor :: Identifier -> MessageRendererParams -> HashMap Char Attr+modesPaletteFor name rp+  | isChanPrefix $ Text.head $ idText name = view palCModes (rendNetPalette rp)+  | otherwise = view palUModes (rendNetPalette rp)+  where+    isChanPrefix c = c `elem` (rendChanTypes rp)
src/Client/Image/MircFormatting.hs view
@@ -19,19 +19,19 @@   , mircColors   ) where -import           Client.Image.PackedImage as I-import           Client.Image.Palette (Palette, palMonospace)-import           Control.Applicative ((<|>), empty)-import           Control.Lens-import           Data.Attoparsec.Text as Parse-import           Data.Bits-import           Data.Char-import           Data.Maybe-import           Data.Text (Text)-import           Graphics.Vty.Attributes-import           Data.Vector (Vector)-import qualified Data.Vector as Vector-import           Numeric (readHex)+import Client.Image.PackedImage as I+import Client.Image.Palette (Palette, palMonospace)+import Control.Applicative ((<|>), empty)+import Control.Lens (view, over, set, makeLensesFor)+import Data.Attoparsec.Text as Parse+import Data.Bits (Bits(xor))+import Data.Char (ord, chr, isControl, isHexDigit)+import Data.Maybe (fromMaybe, isNothing)+import Data.Text (Text)+import Data.Vector (Vector)+import Data.Vector qualified as Vector+import Graphics.Vty.Attributes+import Numeric (readHex)  makeLensesFor   [ ("attrForeColor", "foreColorLens")@@ -166,7 +166,7 @@       235, 237, 239, 241, 244, 247, 250, 254, 231 ]  rgbColor' :: Int -> Int -> Int -> Color-rgbColor' = rgbColor -- fix the type to Int+rgbColor' = srgbColor -- fix the type to Int  applyControlEffect :: Char -> Attr -> Maybe Attr applyControlEffect '\^B' attr = Just $! toggleStyle bold attr
src/Client/Image/PackedImage.hs view
@@ -23,13 +23,13 @@   , resizeImage   ) where -import           Data.List (findIndex)-import qualified Data.Text as S-import qualified Data.Text.Lazy as L-import           Data.String-import           Graphics.Vty.Attributes-import           Graphics.Vty.Image ((<|>), wcswidth, wcwidth)-import           Graphics.Vty.Image.Internal (Image(..))+import Data.List (findIndex)+import Data.String (IsString(..))+import Data.Text qualified as S+import Data.Text.Lazy qualified as L+import Graphics.Vty.Attributes (Attr, defAttr)+import Graphics.Vty.Image ((<|>), wcswidth, wcwidth)+import Graphics.Vty.Image.Internal (Image(..))   unpackImage :: Image' -> Image
src/Client/Image/Palette.hs view
@@ -12,6 +12,7 @@   (   -- * Palette type     Palette(..)+  , NetworkPalette(..)    -- * Lenses   , palNicks@@ -32,31 +33,42 @@   , palCommandPlaceholder   , palCommandPrefix   , palCommandError-  , palCommandPrompt   , palWindowDivider   , palLineMarker+  , palAway+  , palMonospace+  , palJoin+  , palPart+  , palModes+  , palUsrChg+  , palIgnore++  -- * Lenses (Network)   , palCModes   , palUModes   , palSnomask-  , palAway-  , palMonospace+  , palIdOverride    , paletteMap+  , unifyNetworkPalette    -- * Defaults   , defaultPalette+  , defaultNetworkPalette   ) where -import           Control.Lens-import           Data.Text (Text)-import           Data.Vector (Vector)-import           Graphics.Vty.Attributes-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap+import Control.Lens (makeLenses, ReifiedLens(Lens), ReifiedLens')+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Text (Text)+import Data.Vector (Vector)+import Graphics.Vty.Attributes+import Irc.Identifier  -- | Color palette used for rendering the client UI data Palette = Palette-  { _palNicks         :: Vector Attr -- ^ highlighting nicknames+  { _palNicks         :: Vector Attr -- ^ highlighting identifiers+  , _palIdOverride    :: HashMap Identifier Attr -- ^ overrides for specific identifiers   , _palSelf          :: Attr -- ^ own nickname(s)   , _palSelfHighlight :: Attr -- ^ own nickname(s) in mentions   , _palTime          :: Attr -- ^ message timestamps@@ -74,58 +86,90 @@   , _palCommandPrefix :: Attr -- ^ prefix of known command   , _palCommandError  :: Attr -- ^ unknown command   , _palCommandPlaceholder :: Attr -- ^ command argument placeholder-  , _palCommandPrompt :: Attr -- ^ Command input prefix @CMD:@   , _palWindowDivider :: Attr -- ^ Divider between split windows   , _palLineMarker    :: Attr -- ^ Divider between new and old messages   , _palAway          :: Attr -- ^ color of nickname when away   , _palMonospace     :: Attr -- ^ rendering of monospace formatting text-  , _palCModes        :: HashMap Char Attr -- ^ channel mode attributes+  , _palModes         :: Attr -- ^ mode lines+  , _palJoin          :: Attr+  , _palPart          :: Attr+  , _palUsrChg        :: Attr+  , _palIgnore        :: Attr+  }+  deriving Show++data NetworkPalette = NetworkPalette+  { _palCModes        :: HashMap Char Attr -- ^ channel mode attributes   , _palUModes        :: HashMap Char Attr -- ^ user mode attributes   , _palSnomask       :: HashMap Char Attr -- ^ snotice mask attributes   }   deriving Show  makeLenses ''Palette+makeLenses ''NetworkPalette --- | Default UI colors that look nice in my dark solarized color scheme+-- | Default UI colors defaultPalette :: Palette defaultPalette = Palette-  { _palNicks                   = defaultNickColorPalette-  , _palSelf                    = withForeColor defAttr red-  , _palSelfHighlight           = withForeColor defAttr red-  , _palTime                    = withForeColor defAttr brightBlack-  , _palMeta                    = withForeColor defAttr brightBlack-  , _palSigil                   = withForeColor defAttr cyan-  , _palLabel                   = withForeColor defAttr green-  , _palLatency                 = withForeColor defAttr yellow-  , _palWindowName              = withForeColor defAttr cyan-  , _palError                   = withForeColor defAttr red-  , _palTextBox                 = withForeColor defAttr brightBlack-  , _palActivity                = withForeColor defAttr green-  , _palMention                 = withForeColor defAttr red-  , _palCommand                 = withForeColor defAttr yellow-  , _palCommandReady            = withForeColor defAttr green-  , _palCommandPrefix           = withForeColor defAttr blue-  , _palCommandError            = withForeColor defAttr red-  , _palCommandPlaceholder      = withStyle defAttr reverseVideo-  , _palCommandPrompt           = defAttr `withStyle` bold `withBackColor` green `withForeColor` white-  , _palWindowDivider           = withStyle defAttr reverseVideo-  , _palLineMarker              = defAttr-  , _palAway                    = withForeColor defAttr brightBlack-  , _palMonospace               = defAttr-  , _palCModes                  = HashMap.empty-  , _palUModes                  = HashMap.empty-  , _palSnomask                 = HashMap.empty+  { _palNicks              = defaultNickColorPalette+  , _palIdOverride         = HashMap.empty+  , _palSelf               = withForeColor defAttr brightWhite+  , _palSelfHighlight      = defAttr `withBackColor` brightYellow `withForeColor` black+  , _palTime               = withForeColor defAttr brightBlack+  , _palMeta               = metaNo+  , _palSigil              = defAttr `withStyle` bold `withForeColor` brightYellow+  , _palLabel              = withForeColor defAttr cyan+  , _palLatency            = withForeColor defAttr green+  , _palWindowName         = withForeColor defAttr brightCyan+  , _palError              = defAttr `withStyle` bold `withForeColor` red+  , _palTextBox            = withForeColor defAttr brightBlack+  , _palActivity           = metaLo+  , _palMention            = metaHi+  , _palCommand            = withForeColor defAttr yellow+  , _palCommandReady       = withForeColor defAttr brightGreen+  , _palCommandPrefix      = withForeColor defAttr yellow+  , _palCommandError       = withForeColor defAttr red+  , _palCommandPlaceholder = withForeColor defAttr brightBlack+  , _palWindowDivider      = withStyle defAttr reverseVideo+  , _palLineMarker         = withForeColor defAttr cyan+  , _palAway               = withForeColor defAttr blue+  , _palMonospace          = defAttr+  , _palJoin               = withForeColor defAttr brightGreen+  , _palPart               = withForeColor defAttr brightRed+  , _palModes              = metaLo+  , _palUsrChg             = metaLo+  , _palIgnore             = withForeColor defAttr white   }+  where+    metaNo = withForeColor defAttr brightBlack+    metaLo = withForeColor defAttr brightBlue+    metaHi = defAttr `withStyle` bold `withBackColor` brightMagenta `withForeColor` black +defaultNetworkPalette :: NetworkPalette+defaultNetworkPalette = NetworkPalette+  { _palCModes = HashMap.empty+  , _palUModes = HashMap.empty+  , _palSnomask= HashMap.empty+  } -- | Default nick highlighting colors that look nice in my dark solarized -- color scheme. defaultNickColorPalette :: Vector Attr defaultNickColorPalette =   withForeColor defAttr <$>-    [cyan, magenta, green, yellow, blue,-     brightCyan, brightMagenta, brightGreen, brightBlue]+    [ Color240  18, Color240  19, Color240  20, Color240  21, Color240  22, Color240  23+    , Color240  24, Color240  25, Color240  26, Color240  27, Color240  28, Color240  29+    , Color240 192, Color240 193, Color240 194, Color240 195, Color240 196, Color240 197+    , Color240 198, Color240 199, Color240 200, Color240 201, Color240 202, Color240 203+    ] +-- | Combine one NetworkPalette with another.+unifyNetworkPalette :: NetworkPalette -> NetworkPalette -> NetworkPalette+unifyNetworkPalette defaults net = NetworkPalette+  { _palCModes     = HashMap.union (_palCModes net)     (_palCModes defaults)+  , _palUModes     = HashMap.union (_palUModes net)     (_palUModes defaults)+  , _palSnomask    = HashMap.union (_palSnomask net)    (_palSnomask defaults)+  } -- TODO: Replace the above with a nicer lens pattern later.+ -- | List of palette entry names and lenses for accessing that component -- of the palette. paletteMap :: [(Text, ReifiedLens' Palette Attr)]@@ -134,6 +178,7 @@   , ("self-highlight"   , Lens palSelfHighlight)   , ("time"             , Lens palTime)   , ("meta"             , Lens palMeta)+  , ("modes"            , Lens palModes)   , ("sigil"            , Lens palSigil)   , ("label"            , Lens palLabel)   , ("latency"          , Lens palLatency)@@ -147,9 +192,12 @@   , ("command-placeholder", Lens palCommandPlaceholder)   , ("command-prefix"   , Lens palCommandPrefix)   , ("command-error"    , Lens palCommandError)-  , ("command-prompt"   , Lens palCommandPrompt)   , ("window-divider"   , Lens palWindowDivider)   , ("line-marker"      , Lens palLineMarker)   , ("away"             , Lens palAway)   , ("monospace"        , Lens palMonospace)+  , ("join"             , Lens palJoin)+  , ("part"             , Lens palPart)+  , ("user-change"      , Lens palUsrChg)+  , ("ignore"           , Lens palIgnore)   ]
src/Client/Image/StatusLine.hs view
@@ -17,26 +17,26 @@   , clientTitle   ) where -import           Client.Image.Message (cleanChar, cleanText)-import           Client.Image.PackedImage-import           Client.Image.Palette-import           Client.State-import           Client.State.Channel-import           Client.State.Focus-import           Client.State.Network-import           Client.State.Window-import           Control.Lens-import           Data.Foldable (for_)-import qualified Data.Map.Strict as Map-import           Data.Maybe-import           Data.HashMap.Strict (HashMap)-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as LText-import           Graphics.Vty.Attributes-import qualified Graphics.Vty.Image as Vty-import           Irc.Identifier (idText)-import           Numeric+import Client.Image.Message (cleanChar, cleanText, IdentifierColorMode (NormalIdentifier), coloredIdentifier, modesImage)+import Client.Image.PackedImage+import Client.Image.Palette+import Client.State+import Client.State.Channel (chanModes, chanUsers)+import Client.State.Focus (focusNetwork, Focus(..), Subfocus(..), WindowsFilter(..))+import Client.State.Network+import Client.State.Window+import Control.Lens (view, orOf, preview, views, _Just, Ixed(ix))+import Data.Foldable (for_)+import Data.Map.Strict qualified as Map+import qualified Data.HashMap.Strict as HashMap+import Data.Maybe (mapMaybe, maybeToList)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Lazy qualified as LText+import Graphics.Vty.Attributes (Attr, defAttr, bold, withForeColor, withStyle, red)+import Graphics.Vty.Image qualified as Vty+import Irc.Identifier (idText)+import Numeric (showFFloat)  clientTitle :: ClientState -> String clientTitle st@@ -224,7 +224,6 @@       let name = case view winName w of                    Nothing -> '?'                    Just i -> i in-      if view winSilent w then rest else       case view winMention w of         WLImportant -> Vty.char (view palMention  pal) name : rest         WLNormal    -> Vty.char (view palActivity pal) name : rest@@ -241,13 +240,13 @@    where     baraux (focus,w)-      | view winSilent w = Nothing+      | view winActivityFilter w == AFSilent = Nothing       | n == 0 = Nothing -- todo: make configurable       | otherwise = Just                   $ unpackImage bar Vty.<|>                     Vty.char defAttr '[' Vty.<|>                     jumpLabel Vty.<|>-                    Vty.text' (view palLabel pal) (cleanText focusText) Vty.<|>+                    focusLabel Vty.<|>                     Vty.char defAttr ':' Vty.<|>                     Vty.string attr (show n) Vty.<|>                     Vty.char defAttr ']'@@ -262,11 +261,11 @@         attr = case view winMention w of                  WLImportant -> view palMention pal                  _           -> view palActivity pal-        focusText =-          case focus of-            Unfocused           -> Text.pack "*"-            NetworkFocus net    -> net-            ChannelFocus _ chan -> idText chan+        focusLabel =+          unpackImage $ case focus of+            Unfocused           -> text' (view palLabel pal) (Text.pack "*")+            NetworkFocus net    -> text' (view palLabel pal) (cleanText net)+            ChannelFocus _ chan -> coloredIdentifier pal NormalIdentifier HashMap.empty chan   -- | Pack a list of images into a single image spanning possibly many lines.@@ -301,13 +300,14 @@     Unfocused                 -> Vty.emptyImage   where     pal = clientPalette st+    netpal = clientNetworkPalette st     nickPart network =       case preview (clientConnection network) st of         Nothing -> Vty.emptyImage         Just cs -> Vty.text' attr (cleanText (idText nick))            Vty.<|> parens defAttr                      (unpackImage $-                      modesImage (view palUModes pal) (view csModes cs) <>+                      modesImage (view palModes pal) (view palUModes netpal) ('+':view csModes cs) <>                       snomaskImage)           where             attr@@ -318,13 +318,8 @@              snomaskImage               | null (view csSnomask cs) = ""-              | otherwise                = " " <> modesImage (view palSnomask pal) (view csSnomask cs)--modesImage :: HashMap Char Attr -> String -> Image'-modesImage pal modes = "+" <> foldMap modeImage modes-  where-    modeImage m =-      char (fromMaybe defAttr (view (at m) pal)) m+              | otherwise                = " " <>+                modesImage (view palModes pal) (view palSnomask netpal) ('+':view csSnomask cs)  subfocusImage :: Subfocus -> ClientState -> Image' subfocusImage subfocus st = foldMap infoBubble (viewSubfocusLabel pal subfocus)@@ -346,8 +341,10 @@  viewFocusLabel :: ClientState -> Focus -> Image' viewFocusLabel st focus =-  let !pal = clientPalette st in-  case focus of+  let+    !pal = clientPalette st+    netpal = clientNetworkPalette st+  in case focus of     Unfocused ->       char (view palError pal) '*'     NetworkFocus network ->@@ -356,7 +353,7 @@       text' (view palLabel pal) (cleanText network) <>       char defAttr ':' <>       string (view palSigil pal) (cleanChar <$> sigils) <>-      text' (view palLabel pal) (cleanText (idText channel)) <>+      coloredIdentifier pal NormalIdentifier HashMap.empty channel <>       channelModes        where@@ -369,7 +366,7 @@                 , case preview (csChannels . ix channel . chanModes) cs of                     Just modeMap | not (null modeMap) ->-                        " " <> modesImage (view palCModes pal) (Map.keys modeMap)+                        " " <> modesImage (view palModes pal) (view palCModes netpal) ('+':Map.keys modeMap)                     _ -> mempty                ) @@ -382,7 +379,6 @@     FocusInfo         -> Just $ string (view palLabel pal) "info"     FocusUsers        -> Just $ string (view palLabel pal) "users"     FocusMentions     -> Just $ string (view palLabel pal) "mentions"-    FocusDCC          -> Just $ string (view palLabel pal) "dcc"     FocusPalette      -> Just $ string (view palLabel pal) "palette"     FocusDigraphs     -> Just $ string (view palLabel pal) "digraphs"     FocusKeyMap       -> Just $ string (view palLabel pal) "keymap"@@ -390,6 +386,8 @@     FocusIgnoreList   -> Just $ string (view palLabel pal) "ignores"     FocusRtsStats     -> Just $ string (view palLabel pal) "rtsstats"     FocusCert{}       -> Just $ string (view palLabel pal) "cert"+    FocusChanList _ _ -> Just $ string (view palLabel pal) "channels"+    FocusWho          -> Just $ string (view palLabel pal) "who"     FocusMasks m      -> Just $ mconcat       [ string (view palLabel pal) "masks"       , char defAttr ':'
src/Client/Image/Textbox.hs view
@@ -15,26 +15,26 @@   ( textboxImage   ) where -import           Client.Configuration-import           Client.Commands-import           Client.Commands.Arguments.Renderer-import           Client.Commands.Arguments.Parser-import           Client.Commands.Interpolation-import           Client.Commands.Recognizer-import           Client.Image.LineWrap (fullLineWrap, terminate)-import           Client.Image.Message-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.HashMap.Strict (HashMap)-import           Data.List-import qualified Data.Text as Text-import           Graphics.Vty.Attributes-import qualified Graphics.Vty.Image as Vty-import           Irc.Identifier+import Client.Configuration+import Client.Commands (Command(Command, cmdArgumentSpec), commands)+import Client.Commands.Arguments.Renderer (render)+import Client.Commands.Arguments.Parser (parse)+import Client.Commands.Interpolation (Macro(macroSpec), MacroSpec(..))+import Client.Commands.Recognizer+import Client.Image.LineWrap (fullLineWrap, terminate)+import Client.Image.Message (cleanChar, parseIrcTextWithNicks, Highlight)+import Client.Image.MircFormatting (parseIrcText', plainText)+import Client.Image.PackedImage (char, imageWidth, string, unpackImage, Image')+import Client.Image.Palette+import Client.State+import Client.State.EditBox qualified as Edit+import Control.Lens (view, views)+import Data.HashMap.Strict (HashMap)+import Data.List (intersperse)+import Data.Text qualified as Text+import Graphics.Vty.Attributes (defAttr)+import Graphics.Vty.Image qualified as Vty+import Irc.Identifier (Identifier)  textboxImage :: Int -> Int -> ClientState -> (Int, Int, Int, Vty.Image) -- ^ cursor column, new offset, image textboxImage maxHeight width st =
src/Client/Log.hs view
@@ -11,20 +11,20 @@ -} module Client.Log where -import           Client.Image.Message (cleanText)-import           Client.Message-import           Control.Exception-import           Control.Lens hiding ((<.>))-import           Data.Time-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as L-import qualified Data.Text.Lazy.IO as L-import           Irc.Identifier-import           Irc.Message-import           Irc.UserInfo-import           System.Directory-import           System.FilePath+import Client.Image.Message (cleanText)+import Client.Message+import Control.Exception (try)+import Control.Lens (view)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Lazy qualified as L+import Data.Text.Lazy.IO qualified as L+import Data.Time+import Irc.Identifier (Identifier, idText, idTextNorm )+import Irc.Message (IrcMsg(Ctcp, Privmsg, Notice), Source(srcUser))+import Irc.UserInfo (UserInfo(userNick))+import System.Directory (createDirectoryIfMissing)+import System.FilePath ((<.>), (</>))   -- | Log entry queued in client to be written by the event loop
src/Client/Mask.hs view
@@ -5,13 +5,13 @@   , buildMask   ) where -import Irc.UserInfo-import Irc.Identifier+import Data.List (intercalate) import Data.Text (Text)-import qualified Data.Text as Text+import Data.Text qualified as Text+import Irc.Identifier (Identifier, idTextNorm, mkId)+import Irc.UserInfo (UserInfo, renderUserInfo) import Text.Regex.TDFA import Text.Regex.TDFA.String (compile)-import Data.List  newtype Mask = Mask Regex 
src/Client/Message.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings, TemplateHaskell #-}+{-# Language OverloadedStrings, TemplateHaskell, PatternSynonyms #-} {-| Module      : Client.Message Description : Messages to be added to buffers@@ -36,13 +36,14 @@   , msgText   ) where -import           Control.Lens-import           Data.Text (Text)-import           Data.Time (ZonedTime)-import           Irc.Message-import           Irc.Identifier-import           Irc.UserInfo-import           Irc.Codes+import Control.Lens+import Data.Maybe (isJust)+import Data.Text (Text)+import Data.Time (ZonedTime)+import Irc.Codes (ReplyCode, pattern RPL_NOWAWAY, pattern RPL_UNAWAY )+import Irc.Identifier (Identifier, mkId)+import Irc.Message (IrcMsg(..), ircMsgText, Source(srcUser))+import Irc.UserInfo ( UserInfo(userNick) )  data MessageBody   = IrcBody    !IrcMsg@@ -74,6 +75,7 @@   | CtcpSummary {-# UNPACK #-} !Identifier   | ChngSummary {-# UNPACK #-} !Identifier -- ^ Chghost command   | AcctSummary {-# UNPACK #-} !Identifier -- ^ Account command+  | AwaySummary {-# UNPACK #-} !Identifier !Bool   | NoSummary   deriving (Eq, Show) @@ -103,9 +105,12 @@     Ctcp who _ "ACTION" _ -> ChatSummary (srcUser who)     Ctcp who _ _ _ -> CtcpSummary (userNick (srcUser who))     CtcpNotice who _ _ _ -> ChatSummary (srcUser who)+    Reply _ RPL_NOWAWAY (who:_) -> AwaySummary (mkId who) True+    Reply _ RPL_UNAWAY  (who:_) -> AwaySummary (mkId who) False     Reply _ code _  -> ReplySummary code     Account who _   -> AcctSummary (userNick (srcUser who))     Chghost who _ _ -> ChngSummary (userNick (srcUser who))+    Away who mb     -> AwaySummary (userNick (srcUser who)) (isJust mb)     _               -> NoSummary  quitKind :: Maybe Text -> QuitKind@@ -125,6 +130,6 @@     CtcpSummary who   -> Just who     AcctSummary who   -> Just who     ChngSummary who   -> Just who+    AwaySummary who _ -> Just who     ReplySummary {}   -> Nothing     NoSummary         -> Nothing-
src/Client/Network/Async.hs view
@@ -35,30 +35,30 @@   , TerminationReason(..)   ) where -import           Client.Configuration.ServerSettings-import           Client.Network.Connect-import           Control.Concurrent-import           Control.Concurrent.Async-import           Control.Concurrent.STM-import           Control.Exception-import           Control.Lens-import           Control.Monad-import           Data.ByteString (ByteString)-import qualified Data.ByteString as B-import           Data.Foldable-import           Data.List-import           Data.List.Split (chunksOf)-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Time-import           Data.Traversable-import           Data.Word (Word8)-import           Hookup-import           Hookup.OpenSSL (getPubKeyDer)-import           Irc.RateLimit-import           Numeric (showHex)-import qualified OpenSSL.EVP.Digest as Digest-import           OpenSSL.X509 (X509, printX509, writeDerX509)+import Client.Configuration.ServerSettings+import Client.Network.Connect (withConnection, tlsParams)+import Control.Concurrent (MVar, swapMVar, threadDelay, forkIO, newEmptyMVar, putMVar)+import Control.Concurrent.Async (Async, async, cancel, cancelWith, race_, waitCatch, withAsync, AsyncCancelled(AsyncCancelled))+import Control.Concurrent.STM+import Control.Exception (SomeException, Exception(fromException, displayException), throwIO)+import Control.Lens (view)+import Control.Monad (join, when, forever, unless)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.Foldable (for_)+import Data.List (intercalate)+import Data.List.Split (chunksOf)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (ZonedTime, getZonedTime)+import Data.Traversable (for)+import Data.Word (Word8)+import Hookup+import Hookup.OpenSSL (getPubKeyDer)+import Irc.RateLimit (RateLimit, newRateLimit, tickRateLimit)+import Numeric (showHex)+import OpenSSL.EVP.Digest qualified as Digest+import OpenSSL.X509 (X509, printX509, writeDerX509)   -- | Handle for a network connection
src/Client/Network/Connect.hs view
@@ -20,13 +20,13 @@   , tlsParams   ) where -import           Client.Configuration.ServerSettings-import           Control.Applicative-import           Control.Exception  (bracket)-import           Control.Lens-import qualified Data.Text.Encoding as Text-import           Network.Socket (PortNumber)-import           Hookup+import Client.Configuration.ServerSettings+import Control.Applicative ((<|>))+import Control.Exception  (bracket)+import Control.Lens (view, (<&>))+import Data.Text.Encoding qualified as Text+import Hookup+import Network.Socket (PortNumber)  tlsParams :: ServerSettings -> TlsParams tlsParams ss = TlsParams@@ -45,7 +45,15 @@ proxyParams :: ServerSettings -> Maybe SocksParams proxyParams ss =   view ssSocksHost ss <&> \host ->-  SocksParams host (view ssSocksPort ss)+  SocksParams {+    spHost = host,+    spPort = view ssSocksPort ss,+    spAuth =+      case (view ssSocksUsername ss, view ssSocksPassword ss) of+        (Just u, Just (SecretText p)) ->+          UsernamePasswordSocksAuthentication (Text.encodeUtf8 u) (Text.encodeUtf8 p)+        _ -> NoSocksAuthentication+  }  buildConnectionParams :: ServerSettings -> ConnectionParams buildConnectionParams ss = ConnectionParams@@ -72,5 +80,5 @@ -- | Create a new 'Connection' which will be closed when the continuation -- finishes. withConnection :: ServerSettings -> (Connection -> IO a) -> IO a-withConnection settings k =-  bracket (connect (buildConnectionParams settings)) close k+withConnection settings =+  bracket (connect (buildConnectionParams settings)) close
src/Client/Options.hs view
@@ -32,23 +32,22 @@   , getOptions   ) where -import           Config.Schema.Docs-import           Control.Lens-import           Data.Foldable-import           Data.List-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Version-import           GitHash (giHash, giDirty, tGitInfoCwdTry)-import           System.Console.GetOpt-import           System.Environment-import           System.Exit-import           System.IO-import           System.Info-import           Paths_glirc (version)-import           Build_glirc (deps)--import           Client.Configuration+import Build_glirc (deps)+import Client.Configuration (configurationSpec, getConfigPath)+import Config.Schema.Docs (generateDocs)+import Control.Lens (view, (<>~), set, makeLenses)+import Data.Foldable (Foldable(foldl'), traverse_)+import Data.List (intercalate, sort)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Version (showVersion)+import GitHash (giHash, giDirty, tGitInfoCwdTry)+import Paths_glirc (version)+import System.Console.GetOpt+import System.Environment (getArgs)+import System.Exit (exitFailure, exitSuccess)+import System.Info (arch, compilerName, compilerVersion, os)+import System.IO (hPutStr, hPutStrLn, stderr)  -- | Command-line options data Options = Options
src/Client/State.hs view
@@ -36,7 +36,9 @@   , clientIgnores   , clientIgnoreMask   , clientConnection+  , clientNotifications   , clientBell+  , clientUiFocused   , clientExtensions   , clientRegex   , clientLogQueue@@ -54,6 +56,8 @@   , withClientState   , clientIsFiltered   , clientFilter+  , clientFilterChannels+  , clientNetworkPalette   , buildMatcher   , clientToggleHideMeta   , channelUserList@@ -74,6 +78,7 @@   , clientAutoconnects   , clientActiveCommand   , clientNextWindowName+  , clientWindowHint    , clientExtraFocuses   , currentNickCompletionMode@@ -104,11 +109,6 @@   , esActive   , esMVar   , esStablePtr--  -- * URL view-  , urlPattern-  , urlMatches-   ) where  import           Client.CApi@@ -117,6 +117,7 @@ import           Client.Configuration.ServerSettings import           Client.Configuration.Sts import           Client.Image.Message+import           Client.Image.PackedImage (imageText) import           Client.Image.Palette import           Client.Log import           Client.Mask@@ -161,7 +162,6 @@ import           Text.Regex.TDFA import           Text.Regex.TDFA.String (compile) - -- | All state information for the IRC client data ClientState = ClientState   { _clientWindows           :: !(Map Focus Window) -- ^ client message buffers@@ -192,7 +192,9 @@   , _clientEditMode          :: EditMode                  -- ^ editor rendering mode   , _clientEditLock          :: Bool                      -- ^ editor locked and won't send -  , _clientBell              :: !Bool                     -- ^ sound a bell next draw+  , _clientNotifications     :: [(LText.Text, LText.Text)] -- ^ notifications to send next draw+  , _clientBell              :: !Bool                     -- ^ terminal bell on next redraw+  , _clientUiFocused         :: !Bool                     -- ^ whether the UI is focused; used by notifications    , _clientIgnores           :: !(HashSet Identifier)     -- ^ ignored masks   , _clientIgnoreMask        :: Mask                      -- ^ precomputed ignore regular expression (lazy)@@ -282,7 +284,9 @@         , _clientEditLock          = False         , _clientActivityBar       = view configActivityBar cfg         , _clientShowPing          = view configShowPing cfg+        , _clientNotifications     = []         , _clientBell              = False+        , _clientUiFocused         = True         , _clientExtensions        = exts         , _clientLogQueue          = []         , _clientErrorMsg          = Nothing@@ -322,6 +326,7 @@     , _msgNetwork = ""     } ste + -- | Add a message to the window associated with a given channel recordChannelMessage ::   Text       {- ^ network -} ->@@ -329,9 +334,18 @@   ClientMessage ->   ClientState ->   ClientState-recordChannelMessage network channel msg st+recordChannelMessage = recordChannelMessage' True++recordChannelMessage' ::+  Bool       {- ^ create  -} ->+  Text       {- ^ network -} ->+  Identifier {- ^ channel -} ->+  ClientMessage ->+  ClientState ->+  ClientState+recordChannelMessage' create network channel msg st   = recordLogLine msg statusModes channel'-  $ recordWindowLine focus wl st+  $ recordWindowLine' create focus wl st   where     focus      = ChannelFocus network channel'     wl         = toWindowLine rendParams importance msg@@ -342,6 +356,8 @@       , rendHighlights  = highlights       , rendPalette     = clientPalette st       , rendAccounts    = accounts+      , rendNetPalette  = clientNetworkPalette st+      , rendChanTypes   = "#&!+" -- TODO: Don't hardcode this, use CHANTYPES ISUPPORT.       }      -- on failure returns mempty/""@@ -420,6 +436,14 @@                           | otherwise   -> WLNormal         Error{} -> WLImportant +        -- away notices+        Reply _ RPL_AWAY _     -> WLBoring++        -- list output+        Reply _ RPL_LISTSTART _ -> WLBoring+        Reply _ RPL_LIST      _ -> WLBoring+        Reply _ RPL_LISTEND   _ -> WLBoring+         -- channel information         Reply _ RPL_TOPIC _    -> WLBoring         Reply _ RPL_INVITING _ -> WLBoring@@ -468,20 +492,10 @@ recordIrcMessage network target msg st =   updateTransientError (NetworkFocus network) msg $   case target of-    TargetNetwork     -> recordNetworkMessage msg st-    TargetWindow chan -> recordChannelMessage network chan msg st-    TargetUser user   ->-      foldl' (\st' chan -> overStrict-                             (clientWindows . ix (ChannelFocus network chan))-                             (addToWindow wl) st')-           st chans-      where-        wl    = toWindowLine' network st WLBoring msg-        chans = user-              : case preview (clientConnection network . csChannels) st of-                  Nothing -> []-                  Just m  -> [chan | (chan, cs) <- HashMap.toList m-                                   , HashMap.member user (view chanUsers cs) ]+    TargetNetwork      -> recordNetworkMessage msg st+    TargetExisting win -> recordChannelMessage' False network win  msg st+    TargetWindow chan  -> recordChannelMessage' True  network chan msg st+    TargetUser user    -> recordUserMessage network user msg st  -- | Compute the sigils of the user who sent a message. computeMsgLineSigils ::@@ -540,6 +554,27 @@     importance = msgImportance msg st     wl         = toWindowLine' network st importance msg +-- | Record a message on every window where a user is present.+recordUserMessage ::+  Text       {- ^ network -} ->+  Identifier {- ^ user -} ->+  ClientMessage ->+  ClientState ->+  ClientState+recordUserMessage network user msg st = foldl' foldFn st chans+  where+    -- FIXME: We discard the the boolean from addToWindow here,+    -- which means notifications for important cross-channel activity never happen.+    -- This currently affects nothing AFAIK, but who knows what the future holds?+    windowsLens chan = clientWindows . ix (ChannelFocus network chan)+    foldFn st' chan = overStrict (windowsLens chan) (fst . addToWindow wl) st'+    wl    = toWindowLine' network st WLBoring msg+    chans = user+          : case preview (clientConnection network . csChannels) st of+              Nothing -> []+              Just m  -> [chan | (chan, cs) <- HashMap.toList m+                               , HashMap.member user (view chanUsers cs) ]+ recordError ::   ZonedTime       {- ^ now             -} ->   Text            {- ^ network         -} ->@@ -553,40 +588,79 @@     , _msgBody    = ErrorBody msg     } -clientNextWindowName :: ClientState -> Char-clientNextWindowName st =-  case clientWindowNames st \\ usedNames of-    []  -> '\0'-    c:_ -> c+clientNextWindowName :: Maybe WindowHint -> ClientState -> Char+clientNextWindowName hint st+  | Just n <- windowHintName =<< hint, n `notElem` usedNames = n+  | c:_ <- availableNames \\ usedNames    = c+  | otherwise                             = '\0'   where     usedNames = toListOf (clientWindows . folded . winName . _Just) st +    availableNames :: String+    availableNames = clientWindowNames st \\ reservedNames++    reservedNames :: String+    reservedNames =+      toListOf (clientConnections . folded . csSettings . ssWindowHints . folded . to windowHintName  . folded) st++clientWindowHint :: Focus -> ClientState -> Maybe WindowHint+clientWindowHint focus st =+ do net <- focusNetwork focus+    let hintFocus =+          case focus of+            Unfocused -> Unfocused+            NetworkFocus {} -> NetworkFocus ""+            ChannelFocus _ x -> ChannelFocus "" x+    preview (clientConnection net . csSettings . ssWindowHints . ix hintFocus) st+ -- | Record window line at the given focus creating the window if necessary recordWindowLine ::   Focus ->   WindowLine ->   ClientState ->   ClientState-recordWindowLine focus wl st = st2+recordWindowLine = recordWindowLine' True++recordWindowLine' ::+  Bool ->+  Focus ->+  WindowLine ->+  ClientState ->+  ClientState+recordWindowLine' create focus wl st = st1   where+    hints = clientWindowHint focus st+    winActivity = fromMaybe AFLoud (windowHintActivity =<< hints)+     freshWindow = emptyWindow-      { _winName' = clientNextWindowName st-      , _winHideMeta = view (clientConfig . configHideMeta) st+      { _winName'    = clientNextWindowName hints st+      , _winHideMeta = fromMaybe (view (clientConfig . configHideMeta) st) (windowHintHideMeta =<< hints)+      , _winHidden   = fromMaybe False (windowHintHidden =<< hints)+      , _winActivityFilter   = winActivity       } -    st1 = over (clientWindows . at focus)-               (\w -> Just $! addToWindow wl (fromMaybe freshWindow w))-               st--    st2-      | not (view clientBell st)-      , view (clientConfig . configBellOnMention) st-      , view wlImportance wl == WLImportant-      , not (hasMention st) = set clientBell True st1+    add True  w = Just $! addToWindow wl (fromMaybe freshWindow w)+    add False w = addToWindow wl <$> w -      | otherwise = st1+    addedMaybe = add create $ view (clientWindows . at focus) st+    st1 = case addedMaybe of+      Just (w', notify) -> addNotify notify focus wl $ set (clientWindows . at focus) (Just w') st+      Nothing -> st -    hasMention = elemOf (clientWindows . folded . winMention) WLImportant+addNotify :: Bool -> Focus -> WindowLine -> ClientState -> ClientState+addNotify False _     _  st = st+addNotify True  focus wl st+  | focus == view clientFocus st && view clientUiFocused st = st+  | otherwise = addBell $ over clientNotifications (cons (focusText focus, bodyText)) st+  where+    addBell st'+      | not (view clientBell st')+      , view (clientConfig . configBellOnMention) st' = set clientBell True st'+      | otherwise = st'+    bodyText = imageText (view wlPrefix wl) <> " " <> imageText (view wlImage wl)+    focusText Unfocused = "Application Notice"+    focusText (NetworkFocus net) = LText.fromChunks ["Notice from ", net]+    focusText (ChannelFocus net chan) = LText.fromChunks ["Activity on ", net, ":", idText chan]  toWindowLine :: MessageRendererParams -> WindowLineImportance -> ClientMessage -> WindowLine toWindowLine params importance msg = WindowLine@@ -611,9 +685,11 @@  -- | Function applied to the client state every redraw. clientTick :: ClientState -> ClientState-clientTick = set clientBell False-           . markSeen+clientTick st = (if view clientUiFocused st then markSeen else id)+           . set clientBell False+           . set clientNotifications []            . set clientLogQueue []+           $ st   -- | Mark the messages on the current window (and any splits) as seen.@@ -692,6 +768,19 @@      where        limit = maybe id take (matcherMax m) +clientFilterChannels ::+  ClientState ->+  Maybe Int ->+  Maybe Int ->+  [(Identifier, Int, Text)] ->+  [(Identifier, Int, Text)]+clientFilterChannels st min' (Just max') =+  filter (\(_, users, _) -> users < max') . clientFilterChannels st min' Nothing+clientFilterChannels st (Just min') Nothing =+  filter (\(_, users, _) -> users > min') . clientFilterChannels st Nothing Nothing+clientFilterChannels st Nothing Nothing = clientFilter st filterOn+  where filterOn (chan, _, topic) = LText.fromChunks [idText chan, " ", topic]+ data MatcherArgs = MatcherArgs   { argAfter     :: !Int   , argBefore    :: !Int@@ -752,33 +841,6 @@     _                -> Nothing  --- | Regular expression for matching HTTP/HTTPS URLs in chat text.-urlPattern :: Regex-Right urlPattern =-  compile-    defaultCompOpt-    defaultExecOpt{captureGroups=False}-    "https?://([[:alnum:]-]+\\.)*([[:alnum:]-]+)(:[[:digit:]]+)?(/[-0-9a-zA-Z$_.+!*'(),%?&=:@/;~#]*)?|\-    \<https?://[^>]*>|\-    \\\(https?://[^\\)]*\\)"----- | Find all the URL matches using 'urlPattern' in a given 'Text' suitable--- for being opened. Surrounding @<@ and @>@ are removed.-urlMatches :: LText.Text -> [Text]-urlMatches txt = removeBrackets . extractText . (^?! ix 0)-             <$> matchAll urlPattern (LText.unpack txt)-  where-    extractText (off,len) = LText.toStrict-                          $ LText.take (fromIntegral len)-                          $ LText.drop (fromIntegral off) txt--    removeBrackets t =-      case Text.uncons t of-       Just ('<',t') | not (Text.null t') -> Text.init t'-       Just ('(',t') | not (Text.null t') -> Text.init t'-       _                                  -> t- -- | Remove a network connection and unlink it from the network map. -- This operation assumes that the network connection exists and should -- only be applied once per connection.@@ -851,7 +913,7 @@   Text                       {- ^ network name             -} ->   NetworkState               {- ^ network connection state -} ->   ClientState                {- ^ client state             -} ->-  ([RawIrcMsg], ClientState) {- ^ response , DCC updates, updated state -}+  ([RawIrcMsg], ClientState) {- ^ response , updated state -} applyMessageToClientState time irc network cs st =   cs' `seq` (reply, st')   where@@ -921,8 +983,7 @@         Just focus -> changeFocus focus st         Nothing    -> st   where-    windowList   = filter (not . view winSilent . snd)-                 $ views clientWindows Map.toAscList st+    windowList   = views clientWindows Map.toAscList st     highPriority = find (\x -> WLImportant == view winMention (snd x)) windowList     lowPriority  = find (\x -> view winUnread (snd x) > 0) windowList @@ -1103,3 +1164,9 @@ clientToggleHideMeta :: ClientState -> ClientState clientToggleHideMeta st =   overStrict (clientWindows . ix (view clientFocus st) . winHideMeta) not st++-- | Generates the NetworkPalette for the current focus.+clientNetworkPalette :: ClientState -> NetworkPalette+clientNetworkPalette st = case focusNetwork (view clientFocus st) of+  Just net -> configNetworkPalette net (view clientConfig st)+  Nothing  -> defaultNetworkPalette
src/Client/State/Channel.hs view
@@ -44,17 +44,17 @@   , nickChange   ) where -import           Control.Lens-import           Data.HashMap.Strict-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map.Strict as Map-import           Data.Map.Strict (Map)-import           Data.Text (Text)-import qualified Data.Text as Text-import           Data.Time-import           Irc.Identifier-import           Irc.RawIrcMsg (RawIrcMsg)-import           Irc.UserInfo+import Control.Lens ((&), sans, (<<.~), over, set, makeLenses, At(at))+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (UTCTime)+import Irc.Identifier (Identifier)+import Irc.RawIrcMsg (RawIrcMsg)+import Irc.UserInfo (UserInfo)  -- | Dynamic information about the state of an IRC channel data ChannelState = ChannelState
src/Client/State/EditBox.hs view
@@ -61,12 +61,12 @@    ) where -import           Client.State.EditBox.Content-import           Control.Lens hiding (below)-import           Data.List.NonEmpty (NonEmpty)-import           Digraphs (Digraph)-import           Data.Map (Map)-import           Data.Text (Text)+import Client.State.EditBox.Content+import Control.Lens hiding (below)+import Data.List.NonEmpty (NonEmpty)+import Data.Map (Map)+import Data.Text (Text)+import Digraphs (Digraph)   data EditBox = EditBox
src/Client/State/EditBox/Content.hs view
@@ -48,17 +48,17 @@   , digraph   ) where -import           Control.Applicative ((<|>))-import           Control.Lens hiding ((<|), below)-import           Control.Monad (guard)-import           Data.Char (isAlphaNum)-import           Data.List (find)-import           Data.List.NonEmpty (NonEmpty(..), (<|))-import           Data.Map (Map)-import qualified Data.Map as Map-import           Data.Text (Text)-import qualified Data.Text as Text-import           Digraphs (Digraph(..), lookupDigraph)+import Control.Applicative ((<|>))+import Control.Lens (view, views, (+~), (-~), over, set, makeClassy, makeLenses)+import Control.Monad (guard)+import Data.Char (isAlphaNum)+import Data.List (find)+import Data.List.NonEmpty (NonEmpty(..), (<|))+import Data.Map (Map)+import Data.Map qualified as Map+import Data.Text (Text)+import Data.Text qualified as Text+import Digraphs (Digraph(..), lookupDigraph)  data Line = Line   { _pos  :: !Int
src/Client/State/Extensions.hs view
@@ -20,26 +20,24 @@   , clientThreadJoin   ) where -import Control.Concurrent.MVar-import Control.Monad.IO.Class-import Control.Exception-import Control.Lens-import Control.Monad-import Data.Foldable-import Data.Text (Text)-import Data.Time-import Foreign.Ptr-import Foreign.StablePtr-import qualified Data.Text as Text-import qualified Data.IntMap as IntMap--import Irc.RawIrcMsg--import Client.State-import Client.Message import Client.CApi import Client.CApi.Types-import Client.Configuration+import Client.Configuration (configExtensions, ExtensionConfiguration)+import Client.Message+import Client.State+import Control.Concurrent.MVar (putMVar, takeMVar)+import Control.Exception (displayException, try)+import Control.Lens+import Control.Monad (foldM)+import Control.Monad.IO.Class (liftIO)+import Data.Foldable (find)+import Data.IntMap qualified as IntMap+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (getZonedTime)+import Foreign.Ptr (Ptr, nullFunPtr)+import Foreign.StablePtr (castStablePtrToPtr)+import Irc.RawIrcMsg (RawIrcMsg)  -- | Start extensions after ensuring existing ones are stopped clientStartExtensions ::
src/Client/State/Focus.hs view
@@ -27,9 +27,9 @@   , _Unfocused   ) where -import           Control.Lens-import           Data.Text (Text)-import           Irc.Identifier+import Control.Lens (makePrisms)+import Data.Text (Text)+import Irc.Identifier (Identifier)  -- | Currently focused window data Focus@@ -45,7 +45,6 @@   = FocusMessages    -- ^ Show messages   | FocusInfo        -- ^ Show channel metadata   | FocusUsers       -- ^ Show channel user list-  | FocusDCC         -- ^ Show DCC offers list   | FocusMasks !Char -- ^ Show channel mask list for given mode   | FocusWindows WindowsFilter -- ^ Show client windows   | FocusPalette     -- ^ Show current palette@@ -56,6 +55,8 @@   | FocusRtsStats    -- ^ Show GHC RTS statistics   | FocusIgnoreList  -- ^ Show ignored masks   | FocusCert        -- ^ Show rendered certificate+  | FocusChanList (Maybe Int) (Maybe Int) -- ^ Show channel list+  | FocusWho -- ^ Show last reply to a WHO query   deriving (Eq,Show)  -- | Unfocused first, followed by focuses sorted by network.
src/Client/State/Network.hs view
@@ -27,6 +27,8 @@   -- * Lenses   , csNick   , csChannels+  , csChannelList+  , csWhoReply   , csSocket   , csModeTypes   , csChannelTypes@@ -49,6 +51,9 @@   , csAuthenticationState   , csSeed   , csAway+  , clsElist+  , clsDone+  , clsItems    -- * Cross-message state   , Transaction(..)@@ -80,46 +85,50 @@   , sendTopic   ) where -import qualified Client.Authentication.Ecdsa as Ecdsa-import qualified Client.Authentication.Ecdh as Ecdh-import qualified Client.Authentication.Scram as Scram-import           Client.Configuration.ServerSettings-import           Client.Network.Async-import           Client.State.Channel-import           Client.UserHost-import           Client.Hook (MessageHook)-import           Client.Hooks (messageHooks)-import           Control.Lens-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import qualified Data.HashSet as HashSet-import qualified Data.ByteString as B-import qualified Data.Map.Strict as Map-import           Data.Bits-import           Data.Foldable-import           Data.List-import qualified Data.List.NonEmpty as NonEmpty-import           Data.Maybe-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text-import qualified Data.Text.Read as Text-import           Data.Time-import           Data.Time.Clock.POSIX-import           Irc.Codes-import           Irc.Commands-import           Irc.Identifier-import           Irc.Message-import           Irc.Modes-import           Irc.RawIrcMsg-import           Irc.UserInfo-import           LensUtils-import qualified System.Random as Random-import qualified Data.ByteString.Base64 as B64+import Client.Authentication.Ecdh qualified as Ecdh+import Client.Authentication.Ecdsa qualified as Ecdsa+import Client.Authentication.Scram qualified as Scram+import Client.Configuration.ServerSettings+import Client.Hook (MessageHook)+import Client.Hooks (messageHooks)+import Client.Network.Async (abortConnection, send, NetworkConnection, TerminationReason(PingTimeout))+import Client.State.Channel+import Client.UserHost (UserAndHost(UserAndHost, _uhAccount))+import Client.WhoReply+import Control.Lens+import Data.Bits (Bits((.&.)))+import Data.ByteString qualified as B+import Data.ByteString.Base64 qualified as B64+import Data.Either qualified as Either+import Data.Foldable (for_, traverse_ )+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet qualified as HashSet+import Data.List (foldl', delete, intersect, sort, sortBy, union)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Map.Strict qualified as Map+import Data.Maybe (fromMaybe, isJust, mapMaybe, listToMaybe)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Data.Text.Read qualified as Text+import Data.Time+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Irc.Codes+import Irc.Commands+import Irc.Identifier (Identifier, idText, mkId)+import Irc.Message+import Irc.Modes+import Irc.RawIrcMsg+import Irc.UserInfo+import LensUtils (overStrict, setStrict)+import System.Random qualified as Random  -- | State tracked for each IRC connection data NetworkState = NetworkState   { _csChannels     :: !(HashMap Identifier ChannelState) -- ^ joined channels+  , _csChannelList  :: !ChannelList -- ^ cached ELIST parameter and /list output+  , _csWhoReply     :: !WhoReply -- ^ cached reply from the last WHO query   , _csSocket       :: !NetworkConnection -- ^ network socket   , _csModeTypes    :: !ModeTypes -- ^ channel mode meanings   , _csUmodeTypes   :: !ModeTypes -- ^ user mode meanings@@ -168,6 +177,13 @@   | PingConnecting !Int !(Maybe UTCTime) !ConnectRestriction -- ^ number of attempts, last known connection time   deriving Show +-- | Cached channel information from /list and elsewhere.+data ChannelList = ChannelList+  { _clsElist :: !(Maybe Text) -- ^ The last ELIST parameter used. Nothing is also used to trigger cache purges+  , _clsDone  :: !Bool -- ^ Whether to purge the hash map on receiving a new RPL_LIST+  , _clsItems :: ![(Identifier, Int, Text)] -- ^ The list of channel infos.+  }+ data ConnectRestriction   = NoRestriction       -- ^ no message restriction   | StartTLSRestriction -- ^ STARTTLS hasn't finished@@ -189,11 +205,25 @@   | CapLsTransaction [(Text, Maybe Text)]   deriving Show + makeLenses ''NetworkState+makeLenses ''ChannelList makePrisms ''Transaction makePrisms ''PingStatus makePrisms ''TimedAction +newChannelList :: Maybe Text -> Maybe (Identifier, Int, Text) -> ChannelList+newChannelList elist Nothing = ChannelList+  { _clsElist = elist+  , _clsDone = False+  , _clsItems = []+  }+newChannelList elist (Just v) = ChannelList+  { _clsElist = elist+  , _clsDone = False+  , _clsItems = [v]+  }+ defaultChannelTypes :: String defaultChannelTypes = "#&" @@ -271,6 +301,8 @@ newNetworkState network settings sock ping seed = NetworkState   { _csUserInfo     = UserInfo "*" "" ""   , _csChannels     = HashMap.empty+  , _csChannelList  = newChannelList Nothing Nothing+  , _csWhoReply     = finishWhoReply $ newWhoReply "" ""   , _csSocket       = sock   , _csChannelTypes = defaultChannelTypes   , _csModeTypes    = defaultModeTypes@@ -387,6 +419,7 @@         noReply (set (csUserInfo . uiHost) host cs)      -- /who <#channel> %tuhna,616+    -- TODO: Use a different magic token here?     Reply _ RPL_WHOSPCRPL [_me,"616",user,host,nick,acct] ->        let acct' = if acct == "0" then "*" else acct        in noReply (recordUser (UserInfo (mkId nick) user host) acct' cs)@@ -449,6 +482,26 @@      (n, cs')    = cs & csSeed %%~ Random.randomR range +doList :: [Text] -> NetworkState -> NetworkState+doList (_:chan:users:topic) cs+  | purge = set csChannelList (newChannelList elist (Just $! value)) cs+  | otherwise = set (csChannelList . clsItems) items' cs+  where+    items' = value:(_clsItems . _csChannelList $ cs)+    value = (mkId chan, usercount, fromMaybe "" (listToMaybe topic))+    usercount = fst . Either.fromRight (0, "") . Text.decimal $ users+    elist = _clsElist . _csChannelList $ cs+    purge = _clsDone . _csChannelList $ cs+doList _ cs = cs++doListEnd :: NetworkState -> NetworkState+doListEnd cs = set csChannelList ncl cs+  where+    ncl = ChannelList { _clsElist = elist, _clsDone = True, _clsItems = sorted }+    elist = _clsElist . _csChannelList $ cs+    sorted = sortBy sorter . _clsItems . _csChannelList $ cs+    sorter (_, aU, _) (_, bU, _) = compare bU aU+ doTopic :: ZonedTime -> UserInfo -> Identifier -> Text -> NetworkState -> NetworkState doTopic when user chan topic =   overChannel chan (setTopic topic . set chanTopicProvenance (Just $! prov))@@ -583,14 +636,15 @@       case args of         _me:_tgt:uname:host:_server:nick:_ ->           noReply $+          over csWhoReply (recordWhoReply args) $           over csTransaction (\t ->             let !x  = UserInfo (mkId nick) uname host                 !xs = view _WhoTransaction t             in WhoTransaction (x : xs))             cs         _ -> noReply cs--    RPL_ENDOFWHO -> noReply (massRegistration cs)+    RPL_WHOSPCRPL -> noReply (over csWhoReply (recordWhoXReply args) cs)+    RPL_ENDOFWHO -> noReply (over csWhoReply finishWhoReply $ massRegistration cs)      RPL_CHANNELMODEIS ->       case args of@@ -605,6 +659,10 @@     RPL_NOWAWAY -> noReply (set csAway True cs)     RPL_UNAWAY  -> noReply (set csAway False cs) +    -- /list+    RPL_LIST    -> noReply (doList args cs)+    RPL_LISTEND -> noReply (doListEnd cs)+     _ -> noReply cs  @@ -668,6 +726,9 @@     RPL_CREATIONTIME    -> True     RPL_CHANNEL_URL     -> True     RPL_NOTOPIC         -> True+    RPL_LISTSTART       -> True+    RPL_LIST            -> True+    RPL_LISTEND         -> True     _                   -> False  -- | Return 'True' for messages that should be hidden outside of@@ -768,7 +829,7 @@       ["multi-prefix", "batch", "znc.in/playback", "znc.in/self-message"       , "cap-notify", "extended-join", "account-notify", "chghost"       , "userhost-in-names", "account-tag", "solanum.chat/identify-msg"-      , "solanum.chat/realhost" ]+      , "solanum.chat/realhost", "away-notify"]      -- logic for using IRCv3.2 server-time if available and falling back     -- to ZNC's specific extension otherwise.
+ src/Client/State/Url.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Module      : Client.State.Url+Description : Function for extracting URLs from text+Copyright   : (c) Eric Mertens, 2016+License     : ISC+Maintainer  : emertens@gmail.com++-}++module Client.State.Url+  ( UrlPair+  , urlList+  ) where++import           Client.Message (summaryActor)+import           Client.State+import           Client.State.Focus (Subfocus(..), focusNetwork, Focus)+import           Client.State.Network+import           Client.State.Window+import           Client.WhoReply+import           Control.Lens+import qualified Data.HashMap.Strict as HashMap+import qualified Data.List as List+import           Data.Maybe+import           Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Lazy as LText+import           Irc.Identifier (Identifier)+import           Irc.UserInfo (UserInfo(..))+import           Text.Regex.TDFA+import           Text.Regex.TDFA.ByteString (compile)++-- | A URL and identifiers of those who provided that URL.+type UrlPair = (Text, [Identifier])++-- | Regular expression for matching HTTP/HTTPS URLs in chat text.+urlPattern :: Regex+Right urlPattern =+  compile+    defaultCompOpt+    defaultExecOpt{captureGroups=False}+    "https?://([[:alnum:]-]+\\.)*([[:alnum:]-]+)(:[[:digit:]]+)?(/[-0-9a-zA-Z$_.+!*'(),%?&=:@/;~#]*)?|\+    \<https?://[^>]*>|\+    \\\(https?://[^\\)]*\\)"++-- | Find all the URL matches using 'urlPattern' in a given 'Text' suitable+-- for being opened. Surrounding @<@ and @>@ are removed.+urlMatches :: LText.Text -> [Text]+urlMatches txt = removeBrackets . extractText . (^?! ix 0)+             <$> matchAll urlPattern (LText.unpack txt)+  where+    extractText (off,len) = LText.toStrict+                          $ LText.take (fromIntegral len)+                          $ LText.drop (fromIntegral off) txt++    removeBrackets t =+      case Text.uncons t of+       Just ('<',t') | not (Text.null t') -> Text.init t'+       Just ('(',t') | not (Text.null t') -> Text.init t'+       _                                  -> t++-- | Generate a list of URLs from the current focus and subfocus.+urlList :: ClientState -> [UrlPair]+urlList st = urlDedup $ urlListForFocus focus subfocus st+  where+    focus = view clientFocus st+    subfocus = view clientSubfocus st++urlListForFocus :: Focus -> Subfocus -> ClientState -> [UrlPair]+urlListForFocus focus subfocus st = case (netM, subfocus) of+  (Just cs, FocusChanList min' max') ->+    matchesTopic st min' max' cs+  (Just cs, FocusWho) ->+    matchesWhoReply st cs+  (_, _) ->+    toListOf (clientWindows . ix focus . winMessages . each . folding (matchesMsg st)) st+  where+    netM = do+      net <- focusNetwork focus+      view (clientConnections . at net) st++matchesMsg :: ClientState -> WindowLine -> [UrlPair]+matchesMsg st wl =+  [ (url, maybeToList $ views wlSummary summaryActor wl)+  | url <- concatMap urlMatches $ clientFilter st id [views wlText id wl]+  ]++matchesTopic :: ClientState -> Maybe Int -> Maybe Int -> NetworkState -> [UrlPair]+matchesTopic st min' max' cs =+  [ (url, [chan])+  | (chan, _, topic) <- clientFilterChannels st min' max' $ view (csChannelList . clsItems) cs+  , url <- urlMatches $ LText.fromStrict topic+  ]++matchesWhoReply :: ClientState -> NetworkState -> [UrlPair]+matchesWhoReply st cs =+  [ (url, [userNick $ view whoUserInfo wri])+  | wri <- clientFilter st whoFilterText $ view (csWhoReply . whoItems) cs+  , url <- urlMatches $ LText.fromStrict $ view whoRealname wri+  ]++-- | Deduplicates URLs, combining their identifiers while preserving order.+urlDedup :: [UrlPair] -> [UrlPair]+urlDedup pairs = rebuildList hmap [] pairs+  where+    rebuildList _     pairs' [] = reverse pairs'+    rebuildList hmap' pairs' ((url, _):rest)+      | HashMap.null hmap = reverse pairs'+      | otherwise = case ids of+        Just keys -> rebuildList hmapU ((url, reverse keys):pairs') rest+        Nothing -> rebuildList hmapU pairs' rest+      where+        (ids, hmapU) = HashMap.alterF (\v -> (v, Nothing)) url hmap'+    hmap = HashMap.fromListWith List.union pairs
src/Client/State/Window.hs view
@@ -23,7 +23,7 @@   , winMarker   , winHideMeta   , winHidden-  , winSilent+  , winActivityFilter    -- * Window lines   , WindowLines(..)@@ -37,7 +37,11 @@   , wlTimestamp    -- * Window line importance+  , ActivityFilter(..)   , WindowLineImportance(..)+  , activityFilterStrings+  , applyActivityFilter+  , readActivityFilter    -- * Window operations   , emptyWindow@@ -54,14 +58,15 @@   , unpackTimeOfDay   ) where -import           Client.Image.PackedImage-import           Client.Message-import           Control.Lens-import           Control.Monad ((<$!>))-import           Data.Text.Lazy (Text)-import           Data.Time-import           Data.Word-import           Data.Bits+import Client.Image.PackedImage (Image', imageText)+import Client.Message (IrcSummary)+import Control.Lens (Lens', view, to, from, non, set, makeLenses, Each(..), Getter)+import Control.Monad ((<$!>))+import Data.Bits ((.|.), (.&.), shiftL, shiftR)+import Data.Text.Lazy (Text)+import Data.Time+import Data.Word (Word64)+import Data.List (elemIndex)  -- | A single message to be displayed in a window. -- The normal message line consists of the image prefix@@ -94,7 +99,7 @@   , _winMention  :: !WindowLineImportance -- ^ Indicates an important event is unread   , _winHideMeta :: !Bool          -- ^ Hide metadata messages   , _winHidden   :: !Bool          -- ^ Remove from jump rotation-  , _winSilent   :: !Bool          -- ^ Ignore activity+  , _winActivityFilter :: !ActivityFilter -- ^ Filters for activity   }  data ActivityLevel = NoActivity | NormalActivity | HighActivity@@ -107,6 +112,33 @@   | WLImportant -- ^ Increment unread count and set important flag   deriving (Eq, Ord, Show, Read) +data ActivityFilter+  = AFSilent+  | AFQuieter+  | AFQuiet+  | AFImpOnly+  | AFLoud+  | AFLouder+  deriving (Eq, Ord, Enum)++activityFilterStrings :: [String]+activityFilterStrings = ["silent", "quieter", "quiet", "imponly", "loud", "louder"]++applyActivityFilter :: ActivityFilter -> WindowLineImportance -> WindowLineImportance+applyActivityFilter AFSilent  _           = WLBoring+applyActivityFilter AFQuieter WLNormal    = WLBoring+applyActivityFilter AFQuieter WLImportant = WLNormal+applyActivityFilter AFImpOnly WLNormal    = WLBoring+applyActivityFilter AFQuiet   WLImportant = WLNormal+applyActivityFilter AFLouder  WLNormal    = WLImportant+applyActivityFilter _ etc = etc++instance Show ActivityFilter where+  show af = activityFilterStrings !! fromEnum af++readActivityFilter :: String -> Maybe ActivityFilter+readActivityFilter s = toEnum <$> elemIndex s activityFilterStrings+ makeLenses ''Window makeLenses ''WindowLine @@ -127,7 +159,7 @@   , _winMention  = WLBoring   , _winHideMeta = False   , _winHidden   = False-  , _winSilent   = False+  , _winActivityFilter   = AFLoud   }  windowClear :: Window -> Window@@ -141,17 +173,23 @@  -- | Adds a given line to a window as the newest message. Window's -- unread count will be updated according to the given importance.-addToWindow :: WindowLine -> Window -> Window-addToWindow !msg !win = win-    { _winMessages = msg :- view winMessages win-    , _winTotal    = view winTotal win + 1-    , _winMarker   = (+1) <$!> view winMarker win-    , _winUnread   = if view wlImportance msg == WLBoring-                     then view winUnread win-                     else view winUnread win + 1-    , _winMention  = max (view winMention win) (view wlImportance msg)-    , _winHideMeta = view winHideMeta win-    }+-- Additionally returns True if this window becomes important as a result of this line.+addToWindow :: WindowLine -> Window -> (Window, Bool)+addToWindow !msg !win = (win', nowImportant)+    where+      win' = win+        { _winMessages = msg :- view winMessages win+        , _winTotal    = view winTotal win + 1+        , _winMarker   = (+1) <$!> view winMarker win+        , _winUnread   = if msgImportance == WLBoring+                         then view winUnread win+                         else view winUnread win + 1+        , _winMention  = max oldMention msgImportance+        , _winHideMeta = view winHideMeta win+        }+      oldMention = view winMention win+      nowImportant = oldMention < WLImportant && msgImportance >= WLImportant+      msgImportance = applyActivityFilter (view winActivityFilter win) (view wlImportance msg)  -- | Update the window clearing the unread count and important flag. windowSeen :: Window -> Window
src/Client/UserHost.hs view
@@ -17,8 +17,8 @@   , uhAccount   ) where -import           Control.Lens (makeLenses)-import           Data.Text (Text)+import Control.Lens (makeLenses)+import Data.Text (Text)  -- | Pair of username and hostname. Empty strings represent missing information. data UserAndHost = UserAndHost
src/Client/View.hs view
@@ -13,46 +13,56 @@   ( viewLines   ) where -import           Client.Image.PackedImage-import           Client.State-import           Client.State.Focus-import           Client.View.Cert-import           Client.View.ChannelInfo-import           Client.View.Digraphs-import           Client.View.Help-import           Client.View.IgnoreList-import           Client.View.KeyMap-import           Client.View.MaskList-import           Client.View.Mentions-import           Client.View.Messages-import           Client.View.Palette-import           Client.View.RtsStats-import           Client.View.UrlSelection-import           Client.View.UserList-import           Client.View.Windows-import           Control.Lens+import Client.Image.PackedImage (Image')+import Client.State+import Client.State.Focus+import Client.View.Cert (certViewLines)+import Client.View.ChannelList (channelListLines)+import Client.View.ChannelInfo (channelInfoImages)+import Client.View.Digraphs (digraphLines)+import Client.View.Help (helpImageLines)+import Client.View.IgnoreList (ignoreListLines)+import Client.View.KeyMap (keyMapLines)+import Client.View.MaskList (maskListImages)+import Client.View.Mentions (mentionsViewLines)+import Client.View.Messages (chatMessageImages)+import Client.View.Palette (paletteViewLines)+import Client.View.RtsStats (rtsStatsLines)+import Client.View.UrlSelection (urlSelectionView)+import Client.View.UserList (userInfoImages, userListImages)+import Client.View.Who (whoLines)+import Client.View.Windows (windowsImages)+import Control.Lens (view)  viewLines :: Focus -> Subfocus -> Int -> ClientState -> [Image'] viewLines focus subfocus w !st =-  case (focus, subfocus) of+  case (network', channel', subfocus) of     _ | Just ("url",arg) <- clientActiveCommand st ->       urlSelectionView w focus arg st-    (ChannelFocus network channel, FocusInfo) ->+    (Just network, Just channel, FocusInfo) ->       channelInfoImages network channel st-    (ChannelFocus network channel, FocusUsers)+    (Just network, Just channel, FocusUsers)       | view clientDetailView st -> userInfoImages network channel st       | otherwise                -> userListImages network channel w st-    (ChannelFocus network channel, FocusMasks mode) ->+    (Just network, Just channel, FocusMasks mode) ->       maskListImages mode network channel w st-    (_, FocusWindows filt) -> windowsImages filt st-    (_, FocusMentions)     -> mentionsViewLines w st-    (_, FocusPalette)      -> paletteViewLines pal-    (_, FocusDigraphs)     -> digraphLines w st-    (_, FocusKeyMap)       -> keyMapLines st-    (_, FocusHelp mb)      -> helpImageLines st mb pal-    (_, FocusRtsStats)     -> rtsStatsLines (view clientRtsStats st) pal-    (_, FocusIgnoreList)   -> ignoreListLines (view clientIgnores st) pal-    (_, FocusCert)         -> certViewLines st+    (_, _, FocusWindows filt) -> windowsImages filt st+    (_, _, FocusMentions)     -> mentionsViewLines w st+    (_, _, FocusPalette)      -> paletteViewLines pal+    (_, _, FocusDigraphs)     -> digraphLines w st+    (_, _, FocusKeyMap)       -> keyMapLines st+    (_, _, FocusHelp mb)      -> helpImageLines st mb pal+    (_, _, FocusRtsStats)     -> rtsStatsLines (view clientRtsStats st) pal+    (_, _, FocusIgnoreList)   -> ignoreListLines (view clientIgnores st) pal+    (_, _, FocusCert)         -> certViewLines st+    (Just network, _, FocusChanList min' max') ->+      channelListLines network w st (min', max')+    (Just network, _, FocusWho) ->+      whoLines network w st     _ -> chatMessageImages focus w st   where+    (network', channel') = case focus of+      Unfocused -> (Nothing, Nothing)+      NetworkFocus network -> (Just network, Nothing)+      ChannelFocus network channel -> (Just network, Just channel)     pal = clientPalette st
+ src/Client/View/ChannelList.hs view
@@ -0,0 +1,76 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.View.ChannelList+Description : Line renderer for searchable channel lists+Copyright   : (c) TheDaemoness, 2023+License     : ISC+Maintainer  : emertens@gmail.com++This module renders the lines used in the channel user list.+-}+module Client.View.ChannelList ( channelListLines ) where++import           Client.Image.LineWrap (lineWrapPrefix)+import           Client.Image.Message (IdentifierColorMode(NormalIdentifier), coloredIdentifier)+import           Client.Image.PackedImage+import           Client.Image.Palette+import           Client.State+import           Client.State.Network+import           Control.Lens+import           Data.Text (Text)+import qualified Data.Text as Text+import           Graphics.Vty.Attributes (defAttr)+import           Irc.Identifier+import qualified Data.HashMap.Strict as HashMap+import Client.State.Focus (Subfocus(FocusChanList))++-- |+-- | Render the lines used by the @/list@ command in normal mode.+channelListLines ::+  Text        {- ^ network           -} ->+  Int         {- ^ window width      -} ->+  ClientState {- ^ client state      -} ->+  (Maybe Int, Maybe Int) {- ^ bounds -} ->+  [Image']+channelListLines network width st bounds =+  case preview (clientConnection network) st of+    Just cs -> channelListLines' cs width st bounds+    Nothing -> [text' (view palError pal) "No connection"]+  where+    pal = clientPalette st++channelListLines' ::+  NetworkState ->+  Int -> ClientState -> (Maybe Int, Maybe Int) -> [Image']+channelListLines' cs width st (min', max')+  | chanList^.clsDone = countImage <> queryPart : images+  | otherwise = countImagePending <> queryPart : images+  where+    chanList = cs^.csChannelList+    els = chanList^.clsElist+    pal = clientPalette st++    countImagePending = countImage <> text' (view palLabel pal) "..."+    countImage = text' (view palLabel pal) "Channels (visible/total): " <>+                 string defAttr (show (length entries')) <>+                 char (view palLabel pal) '/' <>+                 string defAttr (show (length entries))++    queryPart = mconcat $+      [text' (view palLabel pal) " More-than: " <> string defAttr (show lo) | FocusChanList (Just lo) _ <- [st^.clientSubfocus]] +++      [text' (view palLabel pal) " Less-than: " <> string defAttr (show hi) | FocusChanList _ (Just hi) <- [st^.clientSubfocus]] +++      [text' (view palLabel pal) " Elist: " <> text' defAttr txt | Just txt <- [els], not (Text.null txt)]++    entries = chanList^.clsItems+    entries' = clientFilterChannels st min' max' entries++    images = concatMap listItemImage entries'++    listItemImage :: (Identifier, Int, Text) -> [Image']+    listItemImage (chan, users, topic)+      | Text.null topic = [baseImage]+      | otherwise = reverse $ lineWrapPrefix width (baseImage <> label " topic:") (text' defAttr topic)+      where+        chanImage = coloredIdentifier pal NormalIdentifier HashMap.empty chan+        baseImage = chanImage <> label " users: " <> text' defAttr (Text.pack . show $ users)+        label = text' (view palLabel pal)
src/Client/View/Mentions.hs view
@@ -84,7 +84,7 @@   [ MentionLine       { mlTimestamp  = views wlTimestamp unpackUTCTime l       , mlFocus      = focus-      , mlImage      = case metadataImg (view wlSummary l) of+      , mlImage      = case metadataImg palette (view wlSummary l) of                          _ | detailed     -> [view wlFullImage l]                          Just (img, _, _) -> [img]                          Nothing          -> drawWindowLine palette w padAmt l
src/Client/View/Messages.hs view
@@ -28,7 +28,6 @@ import           Control.Lens import           Control.Monad import           Data.List-import           Graphics.Vty.Attributes import           Irc.Identifier import           Irc.Message import           Irc.UserInfo@@ -176,8 +175,10 @@         {- ^ Image, incoming identifier, outgoing identifier if changed -} metadataWindowLine st wl =   case view wlSummary wl of-    ChatSummary who -> (ignoreImage, userNick who, Nothing) <$ guard (identIgnored who st)-    summary         -> metadataImg summary+    ChatSummary who -> (ignoreImage pal, userNick who, Nothing) <$ guard (identIgnored who st)+    summary         -> metadataImg (clientPalette st) summary+  where+    pal = clientPalette st  bulkMetadata ::   ClientState ->@@ -187,8 +188,7 @@   | (quits, wls') <- span isMassQuit wls   , let n = length quits   , n > 10-  = Just (string (view palMeta pal) ("(split:" <> show n <> ")") <>-          char (withForeColor defAttr red) 'X', wls')+  = Just (string (view palPart pal) ("(split:" <> show n <> ")x"), wls')   where     pal = clientPalette st 
src/Client/View/UrlSelection.hs view
@@ -19,18 +19,16 @@ import           Client.Image.PackedImage import           Client.Image.Palette import           Client.Image.LineWrap-import           Client.Message import           Client.State import           Client.State.Focus-import           Client.State.Window+import           Client.State.Url import           Control.Lens import           Data.HashMap.Strict (HashMap)-import           Data.Text (Text)+import           Data.List (intersperse, foldl1') import           Graphics.Vty.Attributes import           Irc.Identifier import           Text.Read (readMaybe) - -- | Generate the lines used for the view when typing @/url@ urlSelectionView ::   Int         {- ^ render width        -} ->@@ -40,12 +38,8 @@   [Image']    {- ^ image lines         -} urlSelectionView w focus arg st   = concat-  $ zipWith (draw w hilites pal padding selected) [1..] (toListOf urled st)+  $ zipWith (draw w hilites pal selected) [1..] (urlList st)   where-    urled = clientWindows . ix focus-          . winMessages   . each-          . folding matches-     focused = focus == view clientFocus st      selected@@ -55,33 +49,29 @@       | otherwise               = 0 -- won't match      cfg     = view clientConfig st-    padding = view configNickPadding cfg     pal     = view configPalette cfg      hilites = clientHighlightsFocus focus st --matches :: WindowLine -> [(Maybe Identifier, Text)]-matches wl = [ (views wlSummary summaryActor wl, url) | url <- views wlText urlMatches wl ]-- -- | Render one line of the url list draw ::   Int                       {- ^ rendered width            -} ->   HashMap Identifier Highlight {- ^ highlights             -} ->   Palette                   {- ^ palette                   -} ->-  PaddingMode               {- ^ nick render padding       -} ->   Int                       {- ^ selected index            -} ->   Int                       {- ^ url index                 -} ->-  (Maybe Identifier, Text)  {- ^ sender and url text       -} ->+  UrlPair                   {- ^ sender and url text       -} ->   [Image']                  {- ^ rendered lines            -}-draw w hilites pal padding selected i (who,url)+draw w hilites pal selected i (url, who)   = reverse   $ lineWrapPrefix w-      (string defAttr (shows i ". ") <>-       nickPad padding-         (foldMap (coloredIdentifier pal NormalIdentifier hilites) who) <> ": ")-      (text' attr (cleanText url))+      (string defAttr (shows i "."))+      (text' attr (cleanText url) <> who')   where+    who'+      | null who = mempty+      | otherwise = " (" <> imgIds <> ")"+    imgIds = foldl1' (<>) $ intersperse ", " $ map idImg who+    idImg id' = coloredIdentifier pal NormalIdentifier hilites id'     attr | selected == i = withStyle defAttr reverseVideo          | otherwise     = defAttr
+ src/Client/View/Who.hs view
@@ -0,0 +1,88 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Client.View.Who+Description : Line renderer for /who replies+Copyright   : (c) TheDaemoness, 2023+License     : ISC+Maintainer  : emertens@gmail.com++This module renders the lines used in /who replies.+-}+module Client.View.Who ( whoLines ) where++import           Client.Image.LineWrap (lineWrapPrefix)+import           Client.Image.Message (IdentifierColorMode(NormalIdentifier), coloredIdentifier, coloredUserInfo, RenderMode (DetailedRender), prettyTime)+import           Client.Image.PackedImage+import           Client.Image.Palette+import           Client.State+import           Client.State.Network+import           Client.WhoReply+import           Control.Lens+import           Data.Text (Text)+import qualified Data.Text as Text+import           Graphics.Vty.Attributes (defAttr)+import           Irc.Identifier+import qualified Data.HashMap.Strict as HashMap+import Client.Image.MircFormatting (parseIrcText')+import Client.State.Focus (Focus(NetworkFocus))++-- |+-- | Render the lines used by the @/who@ command in normal mode.+whoLines ::+  Text        {- ^ network           -} ->+  Int         {- ^ window width      -} ->+  ClientState {- ^ client state      -} ->+  [Image']+whoLines network width st =+  -- TODO: This pattern exists in a few other views. Maybe deduplicate?+  case preview (clientConnection network) st of+    Just cs -> whoLines' cs width st+    Nothing -> [text' (view palError pal) "No connection"]+  where+    pal = clientPalette st++whoLines' :: NetworkState -> Int -> ClientState -> [Image']+whoLines' cs width st+  | Text.null $ view (csWhoReply . whoQuery . _1) cs = [text' (view palError pal) "No previous WHO query"]+  | whorpl^.whoDone = countImage <> queryPart : images+  | otherwise = countImagePending <> queryPart : images+  where+    pal = clientPalette st+    whorpl = view csWhoReply cs+    (query, arg) = view whoQuery whorpl+    entries = view whoItems whorpl+    entries' = clientFilter st whoFilterText entries++    images = concatMap renderEntry entries'+    renderEntry :: WhoReplyItem -> [Image']+    renderEntry entry = reverse $ lineWrapPrefix width (renderPrefix entry) (renderSuffix entry)+    -- Skipping rendering the channel because it doesn't add anything most of the time.+    renderPrefix entry = coloredUserInfo pal DetailedRender hilites (view whoUserInfo entry)+      where hilites = clientHighlightsFocus (NetworkFocus (view csNetwork cs)) st+    renderSuffix :: WhoReplyItem -> Image'+    renderSuffix entry = mconcat $ mapJoinWhoFields entry+      (const mempty)+      (label "$a:" . identifier)+      (label "ip: " . text' defAttr)+      (label "server: " . identifier)+      (label "away" $ text' defAttr "")+      (label "flags: " . text' (view palSigil pal))+      (label "hops: " . string defAttr . show)+      (label "idle: " . string defAttr . prettyTime 1)+      (label "oplvl: " . text' defAttr)+      (label "gecos: " . parseIrcText' False pal)++    countImagePending = countImage <> text' (view palLabel pal) "..."+    countImage = text' (view palLabel pal) "Users in " <>+                 coloredIdentifier pal NormalIdentifier HashMap.empty (mkId query) <>+                 text' (view palLabel pal) " (visible/total): " <>+                 string defAttr (show (length entries')) <>+                 char (view palLabel pal) '/' <>+                 string defAttr (show (length entries))+    queryPart = case arg of+      Just txt | not (Text.null txt) -> label " Options: " $ text' defAttr txt+      _ -> text' defAttr ""++    label txt image = text' (view palLabel pal) txt <> image <> text' defAttr " "+    -- Don't use hilites here; the identifiers are never the user's nick.+    identifier = coloredIdentifier pal NormalIdentifier HashMap.empty
src/Client/View/Windows.hs view
@@ -13,6 +13,7 @@   ( windowsImages   ) where +import           Client.Image.Message (coloredIdentifier, IdentifierColorMode (NormalIdentifier)) import           Client.Image.PackedImage import           Client.Image.Palette import           Client.State@@ -20,11 +21,11 @@ import           Client.State.Window import           Client.State.Network import           Control.Lens+import qualified Data.HashMap.Strict as HashMap import           Data.List import           Data.Maybe (fromMaybe) import qualified Data.Map as Map import           Graphics.Vty.Attributes-import           Irc.Identifier  -- | Draw the image lines associated with the @/windows@ command. windowsImages :: WindowsFilter -> ClientState -> [Image']@@ -92,13 +93,15 @@     ChannelFocus network channel ->       text' (view palLabel pal) network <>       char defAttr ':' <>-      text' (view palLabel pal) (idText channel)+      coloredIdentifier pal NormalIdentifier HashMap.empty channel  renderedWindowInfo :: Palette -> Window -> Image' renderedWindowInfo pal win =   string (view newMsgAttrLens pal) (views winUnread show win) <> "/" <>   string (view palActivity    pal) (views winTotal  show win) <>-  (if view winSilent win then text' (view palMeta pal) " silent" else mempty)+  case view winActivityFilter win of+    AFLoud -> mempty+    other -> string (view palMeta pal) (' ':show other)   where     newMsgAttrLens =       case view winMention win of
+ src/Client/WhoReply.hs view
@@ -0,0 +1,234 @@+{-# Language TemplateHaskell, OverloadedStrings, BangPatterns #-}++{-|+Module      : Client.WhoReply+Description : Parsed replies from WHO+Copyright   : (c) TheDaemoness, 2023+License     : ISC+Maintainer  : emertens@gmail.com++Because WHOX allows for a LOT of fiddliness regarding parameters,+this is extracted from Client.State.Network and given its own module.+-}++module Client.WhoReply+  ( WhoReply+  , WhoReplyItem+  , newWhoReply+  , finishWhoReply+  , recordWhoReply+  , recordWhoXReply+  , mapJoinWhoFields+  , whoFilterText+  +  -- Lenses+  , whoQuery+  , whoFields+  , whoToken+  , whoDone+  , whoItems+  , whoUserInfo+  , whoIp+  , whoServer+  , whoAway+  , whoMiscFlags+  , whoHops+  , whoIdleSecs+  , whoAcct+  , whoOpLvl+  , whoRealname+  ) where++import           Client.Image.Message (cleanText)+import           Control.Lens+import           Control.Lens.Unsound (lensProduct) -- Don't worry about it. Ctrl+F SOUNDNESS.+import           Data.List (sort)+import           Data.Set (Set)+import qualified Data.Set as Set+import           Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Lazy as LText+import           Irc.Identifier+import           Irc.UserInfo+import           Text.Read (readMaybe)+  +data WhoReply = WhoReply+  { _whoQuery  :: !(Text, Maybe Text)+  , _whoFields :: !(Set Char)+  , _whoToken  :: !String+  , _whoDone   :: !Bool+  , _whoItems  :: ![WhoReplyItem]+  }++data WhoReplyItem = WhoReplyItem+  { _whoUserInfo  :: !UserInfo+  , _whoAcct      :: !Identifier+  , _whoIp        :: !Text -- We don't have iproute; (Maybe IP) would be nice here.+  , _whoServer    :: !Identifier+  , _whoAway      :: !(Maybe Bool)+  , _whoMiscFlags :: !Text+  , _whoHops      :: !(Maybe Int)+  , _whoIdleSecs  :: !String -- This can be a Maybe Int, but prettyTime takes a String.+  , _whoOpLvl     :: !Text+  , _whoRealname  :: !Text+  } deriving (Eq, Ord)++makeLenses ''WhoReply+makeLenses ''WhoReplyItem++newWhoReply :: Text -> String -> WhoReply+newWhoReply query "" = WhoReply+  { _whoQuery = (query, Nothing)+  , _whoToken = ""+  , _whoFields = Set.empty+  , _whoDone = False+  , _whoItems = []+  }+newWhoReply query ('%':arg) = WhoReply+  { _whoQuery = (query, Just $ Text.pack ('%':arg))+  , _whoToken = if Set.member 't' fieldSet then token' else ""+  , _whoFields = fieldSet+  , _whoDone = False+  , _whoItems = []+  }+  where+    fieldSet = Set.fromList fields+    (fields, token) = over _2 (drop 1) $ break (== ',') arg+    token' = if null token then "0" else token+newWhoReply query arg = WhoReply+  { _whoQuery = (query, Just $ Text.pack arg)+  , _whoToken = ""+  , _whoFields = Set.empty+  , _whoDone = False+  , _whoItems = []+  }++splitFlags :: String -> (Maybe Bool, Text)+splitFlags ('G':rest) = (Just True,  Text.pack rest)+splitFlags ('H':rest) = (Just False, Text.pack rest)+splitFlags rest       = (Nothing,    Text.pack rest)++newWhoReplyItem :: WhoReplyItem+newWhoReplyItem = WhoReplyItem+  { _whoUserInfo  = UserInfo+    { userNick = mkId ""+    , userName = ""+    , userHost = ""+    }+  , _whoAcct      = "0"+  , _whoIp        = "255.255.255.255"+  , _whoServer    = ""+  , _whoAway      = Nothing+  , _whoMiscFlags = ""+  , _whoHops      = Nothing+  , _whoIdleSecs  = ""+  , _whoOpLvl     = "n/a"+  , _whoRealname  = ""+  }++finishWhoReply :: WhoReply -> WhoReply+finishWhoReply wr = wr { _whoDone = True, _whoItems = reverse $ sort (_whoItems wr) }++recordWhoReply :: [Text] -> WhoReply -> WhoReply+recordWhoReply [_, _, uname, host, server, nick, flags, hcrn] reply+  | _whoDone reply = reply+  | otherwise = reply { _whoItems = wri:_whoItems reply}+  where+    wri = newWhoReplyItem+      { _whoUserInfo = UserInfo { userNick = mkId nick, userName = uname, userHost = host }+      , _whoServer = mkId server+      , _whoAway = away+      , _whoMiscFlags = miscFlags+      , _whoHops = readMaybe $ Text.unpack hops+      , _whoRealname = Text.stripStart realname+      }+    (hops, realname) = Text.break (== ' ') hcrn+    (away, miscFlags) = splitFlags $ Text.unpack flags+recordWhoReply _ reply = reply++-- | Field names for WHOX replies in order, excluding 't'.+whoXReplyFields :: [Char]+whoXReplyFields = "cuihsnfdlaor"++recordWhoXReply :: [Text] -> WhoReply -> WhoReply+recordWhoXReply []       reply = reply+recordWhoXReply (_:args) reply+  | _whoDone reply = reply+  | _whoToken reply == "" = withWri args+  | null args = reply+  | _whoToken reply == Text.unpack (head args) = withWri $ tail args+  | otherwise = reply+  where+    fields = filter ((flip Set.member) (_whoFields reply)) whoXReplyFields+    withWri args' = reply { _whoItems = recordWhoXReply' (zip args' fields) newWhoReplyItem:_whoItems reply}++recordWhoXReply' :: [(Text, Char)] -> WhoReplyItem -> WhoReplyItem+recordWhoXReply' [] = id+recordWhoXReply' ((arg, kind):rest) = recordWhoXReply' rest . updateFn+  where+    updateFn = case kind of+      'a' -> set whoAcct (mkId arg)+      -- Skip c+      'd' -> set whoHops (readMaybe $ Text.unpack arg)+      -- SOUNDNESS: whoAway and whoMiscFlags project disjoint parts of WhoReplyItem+      'f' -> set (lensProduct whoAway whoMiscFlags) flagsSplit+      'h' -> set (whoUserInfo . uiHost) arg+      'i' -> set whoIp arg+      'l' -> set whoIdleSecs (Text.unpack arg)+      'n' -> set (whoUserInfo . uiNick) (mkId arg)+      'o' -> set whoOpLvl arg+      'r' -> set whoRealname arg+      's' -> set whoServer (mkId arg)+      'u' -> set (whoUserInfo . uiName) arg+      _   -> id+    flagsSplit = splitFlags $ Text.unpack arg++-- Map non-default field values and join them into a list.+mapJoinWhoFields :: WhoReplyItem ->+  (UserInfo -> a) ->+  (Identifier -> a) ->+  (Text -> a) ->+  (Identifier -> a) ->+  a ->+  (Text -> a) ->+  (Int -> a) ->+  (String -> a) ->+  (Text -> a) ->+  (Text -> a) ->+  [a]+mapJoinWhoFields wri userinfo acct ip server away flags hops idle oplvl gecos = reverse $+  addFieldIf (require notNull . view whoRealname) gecos $+  addFieldIf (require (/= "n/a") . view whoOpLvl) oplvl $+  addFieldIf (require notNullOrZero . view whoIdleSecs) idle $+  addFieldIf (\n -> view whoHops n >>= require (> 0)) hops $+  addFieldIf (require notNull . view whoMiscFlags) flags $+  addFieldIf (\n -> view whoAway n >>= require id) (const away) $+  addFieldIf (require (notNull . idText) . view whoServer) server $+  addFieldIf (require (/= "255.255.255.255") . view whoIp) ip $+  addFieldIf (require (/= "0") . view whoAcct) acct+  [userinfo $ view whoUserInfo wri]+  where+    addFieldIf :: (WhoReplyItem -> Maybe a) -> (a -> b) -> [b] -> [b]+    addFieldIf getF mapF list = case getF wri of+      Just v -> mapF v:list+      Nothing -> list+    notNull = not . Text.null+    notNullOrZero ""  = False+    notNullOrZero "0" = False+    notNullOrZero _   = True+    require f v+      | f v = Just v+      | otherwise = Nothing++whoFilterText :: WhoReplyItem -> LText.Text+whoFilterText entry = LText.fromChunks $ concat $ mapJoinWhoFields entry+  (\x -> [renderUserInfo x])+  (\x -> [" $a:", idText x])+  (\x -> [" ip: ", x])+  (\x -> [" server: ", idText x])+  [" away"]+  (\x -> [" flags: ", x])+  (\x -> [" hops: ", Text.pack $ show x])+  (\x -> [" idle: ", Text.pack x])+  (\x -> [" oplvl: ", x])+  (\x -> [" gecos: ", cleanText x])
src/ContextFilter.hs view
@@ -1,40 +1,52 @@ module ContextFilter (filterContext) where +-- | Like 'filter' except you can request the elements before+-- and after the matched elements.+--+-- >>> filterContext 2 1 (\x -> 4<=x&&x<=6) [0..10::Int]+-- [2,3,4,5,6,7]+--+-- >>> filterContext 2 1 even [0..10::Int]+-- [0,1,2,3,4,5,6,7,8,9,10]+--+-- >>> filterContext 2 1 (==10) [0..10::Int]+-- [8,9,10]+--+-- >>> filterContext 2 1 (==0) [0..10::Int]+-- [0,1]+--+-- >>> filterContext 0 0 (==0) [0..10::Int]+-- [0]+--+-- >>> filterContext 2 1 (==5) [0..10::Int]+-- [3,4,5,6]+--+-- >>> filterContext 1 2 (==5) [0..10::Int]+-- [4,5,6,7] filterContext ::   Int         {- ^ context before       -} ->   Int         {- ^ context after        -} ->   (a -> Bool) {- ^ predicate            -} ->   [a]         {- ^ inputs               -} ->   [a]         {- ^ matches with context -}-filterContext before after p xs+filterContext before after p xs0   | before < 0 = error "filterContext: bad before"   | after  < 0 = error "filterContext: bad after"-  | otherwise  = selectList (dropSelection before selects) xs+  | otherwise  = go 0 0 xs0 xs0   where     width = before + after -    selects = go 0 xs--    go n []       = replicateKeep n-    go n (y : ys)-      | p y       = Keep (go width ys)-      | n > 0     = Keep (go (n - 1) ys)-      | otherwise = Skip (go n ys)--data Selection = End | Keep Selection | Skip Selection-  deriving (Show)--replicateKeep :: Int -> Selection-replicateKeep 0 = End-replicateKeep i = Keep (replicateKeep (i - 1))--dropSelection :: Int -> Selection -> Selection-dropSelection 0 x        = x-dropSelection _ End      = End-dropSelection i (Keep x) = dropSelection (i - 1) x-dropSelection i (Skip x) = dropSelection (i - 1) x+    -- i: index+    -- m: current match window+    -- xs: list to match+    -- ys: offset list to generate results from+    go i m (x:xs) yys@(y:ys) =+      if (m > 0 || px) && i >= before then y : rest else rest+      where+        rest = go (i+1) m' xs ys'+        px = p x+        m' = max (m-1) (if px then width else 0)+        ys' = if i >= before then ys else yys -selectList :: Selection -> [a] -> [a]-selectList (Keep x) (y : ys) = y : selectList x ys-selectList (Skip x) (_ : ys) =     selectList x ys-selectList _        _        = []+    -- no more matches, so just return the remaining m window+    go _ m _ ys = take m ys
src/DigraphQuote.hs view
@@ -8,10 +8,10 @@ -} module DigraphQuote (digraphTable) where -import Data.Char-import Control.Monad-import Language.Haskell.TH-import Language.Haskell.TH.Quote+import Control.Monad ((<=<))+import Data.Char (chr)+import Language.Haskell.TH (Q, ExpQ, stringE)+import Language.Haskell.TH.Quote (QuasiQuoter(..)) import Numeric (readHex)  digraphTable :: QuasiQuoter
src/Digraphs.hs view
@@ -19,9 +19,9 @@   , digraphs   ) where -import           DigraphQuote-import           Data.Text (Text)-import qualified Data.Text as Text+import DigraphQuote (digraphTable)+import Data.Text (Text)+import Data.Text qualified as Text  -- | Two-character key for digraph lookup data Digraph = Digraph !Char !Char
src/LensUtils.hs view
@@ -20,9 +20,9 @@   , localTimeDay   ) where -import           Control.Lens-import           Data.Time-import           StrictUnit+import Control.Lens (Lens', (??), set, ASetter, LensLike)+import Data.Time (ZonedTime(ZonedTime), LocalTime(LocalTime), Day, TimeOfDay)+import StrictUnit (StrictUnit(..))  -- | Modify the target of a 'Setter' with a function. The result -- is strict in the results of applying the function. Strict version
src/RtsStats.hs view
@@ -15,13 +15,13 @@   , statsToEntries   ) where -import           Data.Int        (Int64)-import           Data.List       (intercalate)-import           Data.List.Split (chunksOf)-import           Data.Text       (Text)-import qualified Data.Text as Text-import           Data.Word       (Word32, Word64)-import           GHC.Stats+import Data.Int        (Int64)+import Data.List       (intercalate)+import Data.List.Split (chunksOf)+import Data.Text       (Text)+import Data.Text qualified as Text+import Data.Word       (Word32, Word64)+import GHC.Stats  class    Render a      where render :: a -> Text instance Render Word32 where render = prettyNum
src/StrQuote.hs view
@@ -8,8 +8,8 @@ -} module StrQuote (str) where -import Language.Haskell.TH-import Language.Haskell.TH.Quote+import Language.Haskell.TH (Pat(LitP), Type(LitT), stringE, Lit(StringL), TyLit(StrTyLit))+import Language.Haskell.TH.Quote (QuasiQuoter(..))  str :: QuasiQuoter str = QuasiQuoter
src/StrictUnit.hs view
@@ -15,7 +15,7 @@   ( StrictUnit(..)   ) where -import           Data.Semigroup (stimes, stimesIdempotent)+import Data.Semigroup (stimes, stimesIdempotent)  -- | Unit data type with a strict 'Semigroup' and 'Monoid' instances. data StrictUnit = StrictUnit