glirc-2.41: src/Client/Commands/Help.hs
{-# LANGUAGE TransformListComp, OverloadedStrings #-}
{-|
Module : Client.Commands.Help
Description : Implementation of the /help command
Copyright : (c) Eric Mertens, 2016
License : ISC
Maintainer : emertens@gmail.com
Implements the /help command, which is responsible for managing the help buffer.
Unfortunately, this makes this command somewhat unique in that it's responsible for rendering.
-}
module Client.Commands.Help ( cmdHelp ) where
import Client.Commands.Arguments.Renderer
import Client.Commands.Arguments.Spec
import Client.Commands.Interpolation
import Client.Commands.Recognizer
import Client.Commands.Types
import Client.Configuration (configMacros)
import Client.Image.MircFormatting
import Client.Image.PackedImage
import Client.Image.Palette
import Client.Image.Message
import Client.State
import Client.State.Focus (focusNetwork, Subfocus(FocusHelp))
import Client.State.Help
import Client.State.Network (sendMsg)
import Control.Lens
import Data.Foldable (toList)
import Data.List (delete, intercalate, sortOn)
import Data.List.NonEmpty (NonEmpty((:|)))
import Data.Text (Text)
import qualified Data.Text as Text
import Graphics.Vty.Attributes
import Irc.Commands (ircHelp)
displayHelp :: ClientState -> HelpState -> IO CommandResult
displayHelp st help = commandSuccess . changeSubfocus FocusHelp $ set clientHelp help st
-- | Implementation of @/help@ command.
cmdHelp :: [CommandSection] -> Recognizer Command -> WindowCommand (Maybe String)
cmdHelp commandsList commands focus st (Just (':':queryStr)) =
case focusNetwork focus of
Nothing -> commandFailureMsg "empty network prefix requires focused network" st
Just net -> cmdHelp commandsList commands focus st (Just (Text.unpack net ++ ':' : queryStr))
-- Network name better not start with a colon! ;)
cmdHelp _ commands _ st (Just queryStr)
| Just queryText == savedQueryText = commandSuccess (changeSubfocus FocusHelp st)
| otherwise = loadHelp commands queryText st
where
savedQueryText = views (clientHelp . hsQuery) helpQueryToText st
queryText = Text.pack queryStr
cmdHelp commandsList _ _ st Nothing = loadHelpList commandsList st
loadHelpList :: [CommandSection] -> ClientState -> IO CommandResult
loadHelpList commandList st = displayHelp st $ makeHelp Nothing $ listAllCommands commandList st
loadHelp :: Recognizer Command -> Text -> ClientState -> IO CommandResult
loadHelp commands query st =
case Text.break (':' ==) query of
(cmdName, "") -> loadHelpCmd commands cmdName st
(net, topic) -> sendHelpQuery net (Text.tail topic) st
sendHelpQuery :: Text -> Text -> ClientState -> IO CommandResult
sendHelpQuery net topic st =
case preview (clientConnection net) st of
Just cs -> do
sendMsg cs (ircHelp topic)
displayHelp st (awaitHelp net topic)
Nothing -> commandFailureMsg ("not connected to " <> net) st
loadHelpCmd :: Recognizer Command -> Text -> ClientState -> IO CommandResult
loadHelpCmd commands cmdName st =
case views (clientConfig . configMacros) (recognize cmdName) st of
Exact macro -> displayHelp st $ makeHelp (Just cmdName) $ macroHelpLines st cmdName macro
Invalid -> commandCase []
Prefix sfxs -> commandCase sfxs
where
commandCase macroSfxs =
case recognize cmdName commands of
Invalid -> failureCase macroSfxs
Prefix sfxs -> failureCase (macroSfxs <> sfxs)
Exact cmd -> displayHelp st $ makeHelp (Just cmdName) $ commandHelpLines st cmdName cmd
failureCase sfxs
| null sfxs = commandFailureMsg "unknown command, try /help with no argument" st
| otherwise = commandFailureMsg ("unknown command, did you mean: " <> suggestions) st
where suggestions = Text.intercalate " " ((cmdName <>) <$> sfxs)
-- | Generate detailed help for macro expansions
macroHelpLines ::
ClientState {- ^ client state -} ->
Text {- ^ name -} ->
Macro {- ^ macro -} ->
[Image'] {- ^ lines -}
macroHelpLines
st
name
Macro{ macroSpec = MacroSpec spec, macroCommands = commands }
= reverse
$ heading "Syntax: " <> commandSummary (makeArgsContext st) pal (pure name) spec
: emptyLine
: heading "Macro Expansion:"
: map (\x -> " " <> foldMap explainExpansion x) commands
where
pal = clientPalette st
attr = withForeColor defAttr cyan
explainExpansion chunk =
case chunk of
LiteralChunk txt -> text' defAttr (cleanText txt)
VariableChunk var -> text' attr ("${" <> cleanText var <> "}")
IntegerChunk i -> string attr ("${" <> show i <> "}")
DefaultChunk c txt ->
text' attr "${" <>
explainExpansion c <>
text' attr "|" <>
text' defAttr (cleanText txt) <>
text' attr "}"
-- | Generate detailed help lines for the command with the given name.
commandHelpLines ::
ClientState {- ^ client state -} ->
Text {- ^ command name -} ->
Command {- ^ command -} ->
[Image'] {- ^ lines -}
commandHelpLines
st
cmdName
Command{cmdNames = names, cmdImplementation = impl, cmdArgumentSpec = spec, cmdDocumentation = doc}
= reverse $ heading "Syntax: " <> commandSummary (makeArgsContext st) pal (pure cmdName) spec
: emptyLine
: aliasLines
++ explainContext impl
: emptyLine
: map (parseIrcText pal) (Text.lines doc)
where
pal = clientPalette st
aliasLines =
case delete cmdName (toList names) of
[] -> []
ns -> [ heading "Aliases: " <> text' defAttr (Text.intercalate ", " ns), emptyLine ]
heading :: Text -> Image'
heading = text' (withStyle defAttr bold)
-- | Generate an explanation of the context where the given command
-- implementation will be valid.
explainContext ::
CommandImpl a {- ^ command implementation -} ->
Image' {- ^ help line -}
explainContext impl =
heading "Context: " <>
case impl of
ClientCommand {} -> "client (works everywhere)"
WindowCommand {} -> "window (works on the current window)"
NetworkCommand{} -> "network (works when focused on active network)"
MaybeChatCommand{} -> "network (works when focused on active network)" -- Intentional duplicate.
ChatCommand{} -> "chat (works when focused on an active channel or private message)"
ChannelCommand{} -> "channel (works when focused on active channel)"
-- | Generate the lines for the help window showing all commands.
listAllCommands ::
[CommandSection] {- ^ commands -} ->
ClientState {- ^ client state -} ->
[Image'] {- ^ help lines -}
listAllCommands commandsList st
= intercalate [emptyLine]
$ map reverse
$ (listCommandSection st pal <$> commandsList)
++ [macroCommandSection st pal]
where pal = clientPalette st
macroCommandSection ::
ClientState {- ^ client state -} ->
Palette {- ^ palette -} ->
[Image'] {- ^ help lines -}
macroCommandSection st pal
| null macros = []
| otherwise =
text' (withStyle defAttr bold) "Macros" :
[ commandSummary st pal (pure name) spec
| Macro name (MacroSpec spec) _ <- macros
, then sortOn by name
]
where
macros = toListOf (clientConfig . configMacros . folded) st
listCommandSection ::
ClientState {- ^ client state -} ->
Palette {- ^ palette -} ->
CommandSection {- ^ command section -} ->
[Image'] {- ^ help lines -}
listCommandSection st pal sec
= text' (withStyle defAttr bold) (cmdSectionName sec)
: [ commandSummary (makeArgsContext st) pal names spec
| -- pattern needed due to existential quantification
Command { cmdNames = names
, cmdArgumentSpec = spec
} <- cmdSectionCmds sec
]
-- | Generate the help line for the given command and its
-- specification for use in the list of commands.
commandSummary ::
r {- ^ client state -} ->
Palette {- ^ palette -} ->
NonEmpty Text {- ^ command name and aliases -} ->
Args r a {- ^ argument specification -} ->
Image' {- ^ summary help line -}
commandSummary st pal (cmd :| _) args =
char defAttr '/' <>
text' (view palCommandReady pal) cmd <>
render pal' st True args ""
where
pal' = set palCommandPlaceholder defAttr pal
-- Empty line used as a separator
emptyLine :: Image'
emptyLine = char defAttr ' '