packages feed

glirc 2.40 → 2.40.1

raw patch · 25 files changed

+1290/−885 lines, 25 filesdep ~basedep ~time

Dependency ranges changed: base, time

Files

ChangeLog.md view
@@ -1,5 +1,16 @@ # Revision history for glirc +## 2.40.1++* Fix exception on /exec with a non-zero exit code+* Externalize command documentation++## 2.40.0.1++* Build on GHC 9.0++## 2.40+ * vty-6 support * deduplicated /url * locally processed /who
README.md view
@@ -1,18 +1,46 @@ GLIRC - Advanced Console IRC Client ============= -* **glirc** [![Hackage](https://img.shields.io/hackage/v/glirc.svg)](https://hackage.haskell.org/package/glirc) [![Build Status](https://secure.travis-ci.org/glguy/irc-core.svg)](http://travis-ci.org/glguy/irc-core)+<a href="https://repology.org/project/glirc/versions">+    <img src="https://repology.org/badge/vertical-allrepos/glirc.svg?exclude_unsupported=1" alt="Packaging status" align="right">+</a>++* **glirc** [![Hackage](https://img.shields.io/hackage/v/glirc.svg)](https://hackage.haskell.org/package/glirc) * **irc-core** [![Hackage](https://img.shields.io/hackage/v/irc-core.svg)](https://hackage.haskell.org/package/irc-core) * **hookup** [![Hackage](https://img.shields.io/hackage/v/hookup.svg)](https://hackage.haskell.org/package/hookup) -[Wiki Documentation](https://github.com/glguy/irc-core/wiki)+Client Features+--------------- +* Support for [a wide variety of IRCv3 capabilities](https://ircv3.net/software/clients#desktop-clients).+* Joins, parts, and quits fold into one line to save space without sacrificing context.+Detailed view (F2) shows additional info, such as part/quit messages.+* Ignore support that folds ignored messages into the joins and parts. Detailed view shows the messages.+* Searchable channel list, who and names list, and list modes (beqI) views separate from chat messages.+* Dynamic, in-place message and list view searching using `/grep`.+* View and open long URLs with `/url`.+* Context-sensitive tab completion and command hints.+* WYSIWYG mIRC formatting input.+* Multi-line editing; Enter sends one line at a time.+* Atheme-flavored-ChanServ integration.+* Nicknames in chat messages are colored to match messages from that nickname.+* Correct handling of [STATUSMSG](https://modern.ircdocs.horse/#statusmsg-parameter), including CTCP ACTIONs.+* Command macros.+* Support for a wide variety of SASL authentication mechanisms, including `ECDSA-NIST256P-CHALLENGE`.+* Split-screen view.+* Configurable color palette.+* Desktop notifications on important messages while unfocused. Can run arbitrary commands instead (or turn them off).+* Extensions, a Lua plugin system, and trivial piping of IRC messages to arbitrary commands with `/exec`.+* Plenty more, but this list is getting a bit long.+ ![](https://raw.githubusercontent.com/wiki/glguy/irc-core/images/screenshot.png)  Building ======== -glirc uses recent versions of packages, make sure you package databases are+[![Build Status](https://github.com/glguy/irc-core/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/glguy/irc-core/actions/workflows/haskell-ci.yml)++glirc uses recent versions of packages, make sure your package databases are up-to-date:  ```@@ -32,37 +60,17 @@ $ cabal build ``` -Client Features-===============--* All views and transformation are dynamic and don't change the underlying model.-* Subsequent joins and parts fold into one line and do not scroll chat messages off the screen-* Ignore support that folds ignored messages into the joins and parts. Toggle it off to see previously hidden messages-* Detailed view to see all the messages in a channel in full detail with hostmask and timestamp (F2)-* Context sensitive tab completion-* Searchable ban, quiet, invex, and exception view separate from chat messages-* Searchable user list, detailed view shows full hostmasks-* WYSIWYG mIRC formatting input-* Multi-line editing-* Dynamic, in-place message searching-* Chanserv integration-* Nicknames in chat messages are colored to match messages from that nickname-* Support for /STATUSMSG/ messages (messages only voice or op users can see)-* Run commands upon connection-* Command macros-* CERTFP and SASL authentication-* Split-screen view-* Configurable color palette- Startup =======  ``` glirc [FLAGS] INITIAL_NETWORKS...-  -c PATH  --config=PATH  Configuration file path-  -!       --noconnect    Disable autoconnecting-  -h       --help         Show help-  -v       --version      Show version+  -c PATH  --config=PATH    Configuration file path+  -!       --noconnect      Disable autoconnecting+  -h       --help           Show help+           --config-format  Show configuration file format+  -v       --version        Show version+           --full-version   Show version and versions of all linked Haskell libraries ```  Environment variables@@ -74,52 +82,50 @@ Configuration file ================= -A configuration file can currently be used to provide some default values instead of-using command line arguments. If any value is missing the default will be used.--The default configuration file path is `~/.config/glirc/config`+Most of glirc's settings are specified using a configuration file.+The file format is [config-value](http://hackage.haskell.org/package/config-value),+an indentation-sensitive format that resembles YAML.+It has macros which are documented+[here](https://hackage.haskell.org/package/config-value/docs/Config-Macro.html). +The default configuration file path is `~/.config/glirc/config`. Relative paths are relative to the home directory. -Learn more about this file format at [config-value](http://hackage.haskell.org/package/config-value)+To view the full list of configuration variables,+run `glirc --config-format | less`.+If any variable is unspecified, a default value will be used instead.  ``` -- vim: filetype=config-value -- Grab the Vim syntax highlighting file from the config-value package --- Learn more about these settings with `glirc --config-format`- -- Defaults used when not specified on command line defaults:   nick:            "yournick"   username:        "yourusername"   realname:        "Your real name"-  password:        "IRC server password"   tls:             yes -- or: no, or: starttls                        -- enabling tls automatically uses port 6697-  tls-verify:      yes -- or: no  -- Override the defaults when connecting to specific servers servers:   * name: "libera"-    hostname:      "irc.libera.chat"+    hostname:   "irc.libera.chat"     sasl:       username: "someuser"       password: "somepass"-    socks-host:    "socks5.example.com"-    socks-port:    8080 -- defaults to 1080-    log-dir:       "/home/myuser/ircLogs"--  * name: "example"-    hostname:      "example.com"-    port:          7000 -- override the default port+    log-dir:    "/home/myuser/ircLogs"     connect-cmds:-      * "join #favoritechannel,#otherchannel"-      * "msg mybot another command"+      * "join #glirc,#someotherchannel" -    -- Specify additional certificates beyond the system CAs-    -- relative to home directory-    tls-server-cert: "extra/certificate.pem"+  * name: "znc"+    hostname:        "znc.example.com"+    port:            7000 -- Override the default port+    password:        "IRC server password"+    tls-verify:      no+    message-hooks:   ["buffextras"] -- Use this when using ZNC's "buffextra" module to get correct playback+    flood-penalty:   -1 -- Disable flood controls+    flood-threshold: -1  macros:   * name: "wipe"@@ -129,20 +135,20 @@    * name: "mysplits"     commands:-      * "splits fn:#haskell fn:#haskell-offtopic"+      * "splits libera:#haskell libera:#haskell-offtopic"    -- Example use of macro in combination with an extension   * name: "extra"     commands:-      * "extension Lua some-parameter $network $channel"+      * "extension Lua somecommand $network $channel" -extra-highlights: ["glirc", "lens"]+extra-highlights: ["glirc", "hello"]  nick-padding:-   side: left -- try right if you don't like left padding+   side: left -- Try "right" if you don't like left padding    width: 13 -url-opener: "open" -- This works on macOS, "xdg-open" for most Linuxes+url-opener: "open" -- This works on macOS; use "xdg-open" for most Linuxes  key-bindings:   * bind: "C-M-b"@@ -153,7 +159,7 @@   time:     fg: [10,10,10] -- RGB values for color for timestamps     bg: blue-  nick-colors:+  identifier-colors: -- Used for nicknames and channel names     [ cyan, magenta, green, yellow, blue     , bright-cyan, bright-magenta, bright-green, bright-blue     , 218,  88,  89, 124, 160, 205, 212, 224 -- reds@@ -163,206 +169,72 @@     ,  25,  27,  33,  39,  51,  80,  81,  75 -- blues     ,  69,  61,  56,  54, 129,  93,  99, 147 -- purples     ]-``` -Configuration sections:-----------| setting                | type                | description                                                                                |-|------------------------|---------------------|--------------------------------------------------------------------------------------------|-| `defaults`             | server              | These settings are used for all connections                                                |-| `servers`              | list of servers     | These settings are used to override defaults when the hostname matches                     |-| `palette`              | palette             | Client color overrides                                                                     |-| `window-names`         | text                | Names of windows (typically overridden on non QWERTY layouts)                              |-| `nick-padding`         | nonnegative integer | Nicks are padded until they have the specified length                                      |-| `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. 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)             |-| `macros`               | list of macros      | User-configurable client commands                                                          |--Server Settings------------------| setting               | type                 | description                                                    |-|-----------------------|----------------------|----------------------------------------------------------------|-| `name`                | text                 | name of server entry, defaults to `hostname`                   |-| `hostname`            | text                 | hostname used to connect and to specify the server             |-| `port`                | number               | port number, defaults to 6667 without TLS and 6697 with TLS    |-| `nick`                | text or list of text | nicknames to try in order                                      |-| `username`            | text                 | server username                                                |-| `realname`            | text                 | real name / GECOS                                              |-| `password`            | text                 | server password                                                |-| `sasl`                | sasl-settings        | SASL authentication settings                                   |-| `tls`                 | yes/no/starttls      | use TLS to connect                                             |-| `tls-verify`          | yes/no               | enable/ disable TLS certificate checks                         |-| `tls-client-cert`     | text                 | path to TLS client certificate                                 |-| `tls-client-key`      | text                 | path to TLS client key                                         |-| `tls-server-cert`     | text                 | CA certificate to use when validating certificates             |-| `tls-ciphers`         | text                 | OpenSSL cipher suite description string                        |-| `connect-cmds`        | list of text         | client commands to send upon connection                        |-| `socks-host`          | text                 | hostname of SOCKS proxy to connect through                     |-| `socks-port`          | number               | port number of SOCKS proxy to connect through                  |-| `chanserv-channels`   | list of text         | list of channels with chanserv op permission                   |-| `flood-penalty`       | number               | cost in seconds per message                                    |-| `flood-threshold`     | number               | threshold in seconds for burst                                 |-| `message-hooks`       | list of text         | names of hooks to enable                                       |-| `reconnect-attempts`  | int                  | number of reconnections to attempt on error                    |-| `autoconnect`         | yes or no            | automatically connect at client startup                        |-| `nick-completion`     | default or slack     | set this to slack to use `@` sigils when completing nicks      |--SASL Settings----------------By default SASL will use PLAIN mode, but you can specify one of: `plain`, `external`, or `ecdsa-nist256p-challenge`.--| setting               | type                 | description                                                    |-|-----------------------|----------------------|----------------------------------------------------------------|-| `mechanism`           | optional mechanism   | SASL mechanism (defaults to PLAIN)                             |-| `username`            | text                 | SASL username (PLAIN and ECDSA-NIST256P-CHALLENGE mode)        |-| `password`            | text                 | SASL password (PLAIN mode)                                     |-| `private-key`         | text                 | Path to ECDSA private key file (ECDSA-NIST256P-CHALLENGE mode) |-| `authzid`             | text                 | Authorization identity (very rarely needed)                    |--Palette----------| entry                 | type         | description                              |-|-----------------------|--------------|------------------------------------------|-| `nick-colors`         | list of attr | Use for nick highlights                  |-| `self`                | attr         | our own nickname(s) outside of mentions  |-| `self-highlight`      | attr         | our own nickname(s) in mentions          |-| `time`                | attr         | timestamp on messages                    |-| `meta`                | attr         | metadata (joins/parts/etc.)              |-| `sigil`               | attr         | sigils (+@)                              |-| `label`               | attr         | information labels                       |-| `latency`             | attr         | latency time                             |-| `error`               | attr         | error messages                           |-| `textbox`             | attr         | textbox edges (^$)                       |-| `window-name`         | attr         | current window name                      |-| `activity`            | attr         | activity notification                    |-| `mention`             | attr         | mention notification                     |-| `command`             | attr         | recognized command                       |-| `command-prefix`      | attr         | prefix of known command                  |-| `command-ready`       | attr         | recognized command with arguments filled |-| `command-placeholder` | attr         | command argument placeholder             |-| `window-divider`      | attr         | the dividing line between split windows  |-| `line-marker`         | attr         | the dividing line for new messages       |--Text Attributes------------------Text attributes can be specified either as a single foreground color or section of attributes.--* `<number>` - Maps to a terminal color-* `<name>` - Direct selection of standard 16 terminal colors-* `[red-number, blue-number, green-number]` - List of 3 numbers in range 0-255 map to an approximation of the RGB color.--Attributes--* `fg` - foreground color-* `bg` - background color-* `style` - single style or list of styles--Styles--* `blink`-* `bold`-* `dim`-* `standout`-* `reverse-video`-* `underline`+notifications: terminal-notifier -- Use terminal-notifier for nicer notifications (macOS only)+```  Commands ======== -Client commands+glirc has built-in documentation for all of its commands.+To view the full list of commands and what they do, use `/help`.+To view help on a specific command, use `/help <command>`. +Unlike some other clients, glirc does not send unknown commands to the server.+Use `/quote` to send arbitrary IRC commands.++The following is a curated list of commands for basic use:+ * `/help [command]` - Show in-client help * `/exit` - Terminate the client-* `/quit` - Gracefully terminate connection to the current server-* `/connect <name>` - Connect to the given server-* `/disconnect` - Forcefully terminate connection to the current server-* `/reconnect` - Reconnect to the current server * `/reload [path]` - Load a new configuration file (optional path) * `/palette` - Show the client palette-* `/digraphs` - Show the table of digraphs-* `/mentions` - Show all the highlighted lines across all windows-* `/extension <extension name> <params...>` - Send the given params to the named extension-* `/exec [-n network] [-c channel] <command> <arguments...>` - Execute a command, If no network or channel are provided send output to client window, if network and channel are provided send output as messages, if network is provided send output as raw IRC messages. * `/url [n]` - Execute url-opener on the nth URL in the current window (defaults to first)--View toggles-* `/toggle-detail` - toggle full detail view of messages * `/toggle-activity-bar` - toggle channel names in activity bar-* `/toggle-metadata` - toggle visibility of channel metadata-* `/toggle-layout` - toggle split-screen layout between 1 and 2 column view+* `/toggle-detail` - toggle full detail view of messages+* `/toggle-metadata` - toggle visibility of channel metadata (joins, parts, quits, nick changes, etc) -Connection commands+Connection +* `/connect <name>` - Connect to the given server+* `/quit [message]` - Gracefully terminate connection to the current server+* `/reconnect` - Reconnect to the current server * `/nick <nick>` - Change nickname-* `/away <message>` - Set away status+* `/away [message]` - Set away status; no message removes away status  Window management  * `/windows [filter]` - List all open windows (filters: networks, channels, users)-* `/focus <server>` - Change focus to server window-* `/focus <server> <channel>` - Change focus to channel window+* `/setname [letter]` - Assign a one-letter name to the given window.+* `/channel <channel>` - Change focus to channel/user on current network (alias: `/c`)+* `/channel <network>:[channel]` - Change focus to channel/user on the specified network (alias: `/c`) * `/clear [network] [channel]` - Clear contents of current or specified window-* `/ignore` - Show all ignore masks-* `/ignore <mask>...` - Toggle ignore status on a list of masks-* `/channel <channel>` - Change focus to channel on current network (alias: `/c`) * `/splits [focuses...]` - Enable split-screen view. Focuses should be space delimited list of NETWORK:CHANNEL * `/splits+ [focuses...]` - Incremental addition to splits * `/splits- [focuses...]` - Incremental removal from splits--Channel membership--* `/join <channel>` - Join a channel (alias: `/j`)-* `/part` - Part from current channel+* `/toggle-layout` - toggle split-screen layout between 1 and 2 column view  Chat commands -* `/query <target> [<msg>]` - Switch focus to target window on current server, optionally send message+* `/join <channel>` - Join a channel (alias: `/j`)+* `/part [msg]` - Part from current channel+* `/query <target> [msg]` - Switch focus to target window on current server, optionally send message (alias: `/q`) * `/msg <target> <msg>` - Send a message on the current server to target-* `/notice <target> <msg>` - Send a notice message on the current server to target-* `/ctcp <target> <command> <args>` - Send a ctcp command on the current server to target * `/me <msg>` - Send action message to channel-* `/say <msg>` - Send normal message to channel--Channel management--* `/mode <mode> <params>` - Change modes on the current channel (advanced tab completion)-* `/kick <nick>` - Kick a user-* `/kickban <nick>` - Kick and ban a user-* `/remove` - Gracefully kick a user-* `/topic <topic>` - Change the topic (tab completion for topic)-* `/invite <nick>` - Invite a user to the current channel--Queries--* `/who <query>` - Perform WHO query (use detailed view to see output)-* `/whois <nick>` - Perform WHOIS query-* `/whowas <nick>` - Perform WHOWAS query-* `/ison <nick>` - Perform ISON query-* `/userhost <nick>` - Perform USERHOST query-* `/links <server>` - Perform LINKS query-* `/time` - Perform TIME query-* `/stats <query>` - Perform STATS query+* `/say <msg>` - Send normal message to channel; useful for macros and messages starting with a slash+* `/ignore <mask>...` - Toggle ignore status on a list of masks+* `/topic [msg]` - Display or set the current topic of a channel -Channel information+Views -* `/users` - Show channel user list-* `/masks <mode>` - Show channel bans(b), quiets(q), exempts(e), or invex(I) * `/channelinfo` - Show channel topic, creation, url--Window filters--* `/grep` - Filter chat messages using a regular expression-* `/grepi` - Filter chat messages using a case-insensitive regular expression on the message+* `/grep [flags] <regex>` - Filter using a regular expression+* `/ignore` - Show all ignore masks+* `/list` - View the list of public channels on the network+* `/masks <mode>` - Show channel bans(b), quiets(q), exempts(e), or invex(I)+* `/mentions` - Show all the highlighted lines across all windows+* `/names` - Show the user list for the current channel+* `/who [channel] [options]` - Perform WHO query, sending options to the server, or show the results of the previous query.  ZNC-specific @@ -371,8 +243,11 @@ * `/znc-playback <time>` - ZNC playback module - play everything start at the given time today * `/znc-playback <date> <time>` - ZNC playback module - play everything start at the given time -Low-level+Miscellaneous +* `/dump <filename>` - Dump current window to file+* `/extension <extension name> <params...>` - Send the given params to the named extension+* `/exec [-n network] [-c channel] <command> <arguments...>` - Execute a command; if no network or channel are provided send output to client window, if network and channel are provided send output as messages, if network is provided send output as raw IRC messages. * `/quote <raw command>` - Send a raw IRC command to the server  Keyboard Shortcuts@@ -382,18 +257,35 @@ control and press A". `M-a` means "hold meta key and press A". On most modern keyboards the *Meta* key is labeled *Alt* or *Option*. -Window navigation+To view the full list of keybindings and what they do,+use `/keymap` from within glirc. +The following is a curated list of default keybinds for basic use:++Navigation++* `Page Up` scroll up+* `Page Down` scroll down * `C-n` next window * `C-p` previous window-* `C-x` next network-* `M-#` jump to window - `1234567890qwertyuiop!@#$%^&*()QWERTYUIOP`+* `C-x` next network window+* `M-<name>` jump to window with the given one-letter name * `M-a` jump to activity * `M-s` jump to previous window * `ESC` return to messages view (from userlist, masklist, help, etc)  Editing +* `C-b` bold+* `C-c` color+* `C-v` invert foreground/background+* `C-_` underline+* `C-]` italic+* `C-o` reset formatting++* `Tab` autocompletion+* `M-k` replace 2 characters before the cursor with a character specified in `/digraphs`+ * `C-a` beginning of line * `C-e` end of line * `C-k` delete to end@@ -409,29 +301,13 @@ * `M-Backspace` delete word backwards * `M-d` delete word forwards * `M-Enter` insert newline-* `M-k` insert digraph (2-characters before cursor) -* `Tab` nickname completion- Client settings  * `F2` toggle detailed view * `F3` toggle detailed activity bar * `F4` toggle metadata visibility--Scrolling--* `Page Up` scroll up-* `Page Down` scroll down--Formatting--* `C-b` bold-* `C-c` color-* `C-v` reverse video-* `C-_` underline-* `C-]` italic-* `C-o` reset formatting+* `F7` toggle Enter key lock  Macros ======@@ -462,12 +338,3 @@  * `0` - first argument * `1` - second argument (etc.)--Hooks-=====--buffextras-------------Enable this hook when using ZNC and the `buffextra` module in order to reinterpret-this module's messages natively in the client.
+ doc/cmds_chanop.adoc view
@@ -0,0 +1,67 @@+= Channel Management Commands+:toc:++== /invite++Invite a user to the current channel.++== /kick++Kick a user from the current channel.++See also: kickban, remove++== /kickban++Ban and kick a user from the current channel.++Users are banned by hostname match.++See also: kick, remove++== /masks++Show mask lists for current channel.++The `mode` argument is typically one of the following:++`b`: bans ++`i`: invite exemptions (op view only) ++`e`: ban exemptions (op view only) ++`q`: quiets (on Charybdis/Solanum-based networks)++== /mode++Set IRC modes.++When executed in a channel window, mode changes are applied to the channel.+When executed in a network window, mode changes are applied to your user.++This command has parameter sensitive tab-completion.++See also: masks, channelinfo++=== Examples++`+/mode -t+`               - Remove the topic lock ++`+/mode +b *!*@hostname+`  - Set a ban ++`+/mode -q *!*@hostname+`  - Remove a quiet ++`+/mode +vv user1 user2+`  - Voice two users ++`+/mode +v-o user1 user1+` - Demote an op to voice++== /remove++Remove a user from the current channel.++Remove works like /kick except it results in a PART.++Not all servers support removal in this manner.+Refer to your server/network's documentation.++See also: kick, kickban++== /topic++View or set the topic of the current channel.++Tab-completion with no `message` specified will load the current topic.
+ doc/cmds_chat.adoc view
@@ -0,0 +1,203 @@+= Chat Commands+:toc:++== /join++Join the given channels.++Multiple channels and keys may be provided as comma-separated lists.++When keys are provided, they should occur in the same order as the channels.++=== Examples++`+/join #friends+` ++`+/join #secret thekey+` ++`+/join #secret1,#secret2,#public key1,key2+`++See also: channel, clear, part++== /me++Sends an action message to the currently focused channel.+Most clients will render these messages prefixed with+only your nickname as though describing an action.++=== Examples++`+/me shrugs+`++See also: notice, msg, say++== /msg++Send a chat message to a user or a channel.++Multiple targets may be provided as a comma-separated list.++On servers with STATUSMSG support,+the channel name can be prefixed with a sigil to+restrict the recipients to those with the given mode.++=== Examples++`+/msg buddy I'm sending you a message.+` ++`+/msg #friends This message is for the whole channel.+` ++`+/msg him,her I'm chatting with two people.+` ++`+/msg @#users This message is only for ops!+`++See also: notice, me, say++== /part++Leave the currently-focused channel,+optionally with the provided message.++=== Examples++`+/part+` ++`+/part It's not me, it's you+`++See also: clear, join, quit++== /query++Switch the client focus to the given+target and optionally send a message to that target.++See also: msg, channel, focus++=== Examples++`+/q libera:#haskell+` ++`+/q #haskell+` ++`+/q lambdabot @messages+` ++`+/q irc_friend How are you?+`++== /say++Send a message to the current chat window.++This can be useful for sending a chat message with+a leading '/' to the current chat window.++=== Examples++`+/say /help is the right place to start!+`++See also: notice, me, msg++== /away++Mark yourself as away.+The away message is used by the server to update+status in /whois and to provide automated responses.++Omit the `message` parameter to clear your away status.++=== Examples++`+/away Out getting some sun+` ++`+/away+`++== /channelinfo++Show information about the current channel.+Information includes topic, creation time, URL, and modes.++See also: masks, mode, topic, users++== /ctcp++Client-to-client protocol (CTCP) commands can be used+to query information from another user's client application+directly. Common CTCP commands include: ACTION, PING, VERSION,+USERINFO, CLIENTINFO, and TIME.++glirc does not automatically respond to CTCP commands.++=== Parameters++`target`    - Comma-separated list of nicknames and channels ++`command`   - CTCP command name ++`arguments` - CTCP command arguments++=== Examples++`+/ctcp myfriend VERSION+` ++`+/ctcp myfriend TIME+`++== /knock++Request entry to an invite-only channel.++== /monitor++Monitor is a protocol for getting server-side notifications+when users become online/offline.++=== Subcommands++`+/monitor + target[,target2]*+` - Add nicknames to monitor list ++`+/monitor - target[,target2]*+` - Remove nicknames to monitor list ++`+/monitor C+`                   - Clear monitor list ++`+/monitor L+`                   - Show monitor list ++`+/monitor S+`                   - Show status of nicknames on monitor list++== /names++Show the user list for the current channel.+Detailed view (default key F2) shows full hostmask.++See also: channelinfo, masks++== /nick++Change your nickname.++=== Examples++`+/nick guest123+` ++`+/nick better_nick+`++== /notice++Send a chat notice to a user or a channel.++Notice messages were originally intended to be used by bots.+Different clients will render these in different ways.++Multiple targets may be provided as a comma-separated list.++On servers with STATUSMSG support,+the channel name can be prefixed with a sigil to+restrict the recipients to those with the given mode.++=== Examples++`+/notice buddy I'm sending you a notice.+` ++`+/notice #friends This notice is for the whole channel.+` ++`+/notice him,her I'm informing two people.+` ++`+/notice @#users This notice is only for ops!+`++See also: me, msg, say++== /operwall++Send a network-wide WALLOPS message to opers only.++See also: me, msg, say++== /wallops++Send a network-wide WALLOPS message.+These messages go out to users who have the 'w' usermode set.++See also: me, msg, say++== /quote++Send a raw IRC command.++The argument to this command is sent as-is.+No additional word-splitting is done.
+ doc/cmds_client.adoc view
@@ -0,0 +1,88 @@+= Client Commands+:toc:++== /exit++Exit the client immediately.++== /reload++Reload the client configuration file.++If `filename` is provided it will be used to reload.+Otherwise the previously loaded configuration file will be reloaded.++== /extension++Run a command provided by an extension.++`extension` should be the name of a loaded extension with+a process_command callback.++== /palette++Show the current palette settings and a color chart to help pick new colors.++== /digraphs++Show the table of digraphs. A digraph is a pair of characters+that can be used together to represent an uncommon character.+Type the two-character digraph corresponding to the desired+output character and then press M-k (default binding).++The digraphs list is searchable with `+/grep+`.++See also: grep++== /keymap++Show the key binding map.++Key bindings can be changed in configuration file.+Run `+glirc --config-format+` for details.++== /rtsstats++Show the GHC RTS statistics.++== /exec++Execute a command synchnonously, optionally sending the output to IRC.++Arguments to this command are:+`+[-n[network]] [-c[channel]] [-i input] command [arguments...]+`++`input` is sent to the standard input of the command.++When neither `network` nor `channel` are specified,+output goes to client window (focus name "*").+When `network` is specified,+output is sent as raw IRC traffic to the network.+When `channel` is specified,+output is sent as chat to the given channel on the current network.+When `network` and `channel` are specified,+output is sent as chat to the given channel on the given network.++`arguments` is split on spaces into words before being+processed by getopt. Use Haskell string literal syntax to+create arguments with escaped characters and spaces inside.++== /url++Open a URL seen in chat.++The URL is opened using the executable configured under `url-opener`.++When this command is active in the textbox,+chat messages are filtered to only show ones with URLs.++When `number` is omitted it defaults to 1.+The number selects the URL to open, counting back from the most recent.++== /help++Show command documentation.++When `command` is omitted, a list of all commands and macros is shown.+When `command` is specified, detailed help for that command is shown.+
+ doc/cmds_integration.adoc view
@@ -0,0 +1,24 @@+= Integrations+:toc:++== /znc++Send command directly to ZNC.++The advantage of this over `+/msg+` is that+responses are not broadcast to call clients.++== /znc-playback++Request playback from the ZNC `playback` module.+Note that the playback module is not installed in ZNC by default!++When `date` is omitted,+uses the most recent date in the past that makes sense.+When both `time` and `date` are omitted,+all playback is requested.++Time format: `HOURS:MINUTES` (example: 7:00)++Date format: `YEAR-MONTH-DAY` (example: 2016-06-16)+
+ doc/cmds_net.adoc view
@@ -0,0 +1,52 @@+= Connection Commands+:toc:++== /connect++Connect to `network` by name.++If `network` does not correspond to the name of a server in the config,+it is treated as a hostname.++== /reconnect++(Re)connect to the currently-focused server.++== /quit++Gracefully disconnect the current network connection,+optionally sending the provided reason.++See also: disconnect, exit++== /disconnect++Immediately terminate the current network connection.++See also: quit, exit++== /umode++Apply a user-mode change. ++== /cert++Show the TLS certificate for the current connection.++== /new-self-signed-cert++Generate a new self-signed client certificate for+network service identification using CertFP.++The generated certificate can be used via the per-server+`tls-client-cert` and `tls-client-key-password` (if applicable) keys+in your config file. ++This command generates an RSA key pair by default.+This may change in the future.++=== Parameters++`filename`:   Certificate and private key PEM output ++`keysize`:    Public-key size (default 2048, range 1024-8192) ++`passphrase`: Optional AES-128 private key passphrase
+ doc/cmds_oper.adoc view
@@ -0,0 +1,102 @@+= Operator Commands+:toc:++== /oper++Authenticate as a server operator.++== /kill++Kill a client connection to the server.++== /kline++Ban a client from the server.++== /unkline++Unban a client from the server.++== /undline++Unban a client from the server.++== /unxline++Unban a gecos from the server.++== /unresv++Unban a channel or nickname from the server.++== /testline++Check matching I/K/D lines for a `[[nick!]user@]host`.++== /testkline++Check matching K/D lines for a `[user@]host`.++== /testgecos++Check matching X lines for a `gecos`.++== /testmask++Test how many local and global clients match a mask.++== /masktrace++Outputs a list of local users matching the given masks.++== /chantrace++Outputs a list of channel members in etrace format.++== /trace++Outputs a list users on a server.++== /etrace++Outputs a list users on a server.++== /map++Display network map.++== /sconnect++Connect two servers together.++== /squit++Split a server away from your side of the network.++== /modload++Load an IRCd module.++== /modunload++Unload an IRCd module.++== /modlist++List loaded IRCd modules.++== /modrestart++Reload all IRCd modules.++== /modreload++Reload an IRCd module.++== /grant++Manually assign a privset to a user.++== /privs++Check operator privileges of the target.
+ doc/cmds_queries.adoc view
@@ -0,0 +1,95 @@+= Queries+:toc:++== /who++Send WHO query to server with given arguments.++If no query is provided, shows the results of the previous query.++== /whois++Send WHOIS query to server with given arguments.++== /whowas++Send WHOWAS query to server with given arguments.++== /ison++Send ISON query to server with given arguments.++See also: monitor++== /userhost++Send USERHOST query to server with given arguments.++== /time++Send TIME query to server with given arguments.++== /stats++Send STATS query to server with given arguments.++== /lusers++Send LUSERS query to a given server.++== /users++Send USERS query to a given server.++== /motd++Send MOTD query to server.++== /admin++Send ADMIN query to server.++== /rules++Send RULES query to server.++== /info++Send INFO query to server.++== /list++View the list of public channels on the server.++Sends a LIST query and caches the result;+on larger networks, this may take several seconds to complete.+The view may be exited while loading.++`clientarg` is an optionally-comma-separated list of options.+A single comma may be used to denote an empty list.++`serverarg` is sent as-is to the server.+It is generally used as the ELIST parameter to LIST.+glirc does not validate this parameter against the ELIST ISUPPORT token.++=== Client Options++`>n`: Show only channels with more than `n` users. ++`<n`: Show only channels with less than `n` users. ++`~`: Force a refresh of the list.++=== Examples++`+/list+`          - List public channels.+`+/list >99+`      - List public channels with at least 100 users.+`+/list >50<1000+` - List public channels with between 51 and 999 users.+`+/list ~ <20+`    - List public channels with fewer than 20 users. +`+/list , *-ops+`  - List public channels whose names end with "-ops".++== /links++Send LINKS query to server with given arguments.++== /version++Send VERSION query to server.
+ doc/cmds_toggles.adoc view
@@ -0,0 +1,32 @@+= Toggles+:toc:++== /toggle-detail++Toggle detailed message view.++== /toggle-activity-bar++Toggle detailed detailed activity information in status bar.++== /toggle-show-ping++Toggle visibility of ping round-trip time.++== /toggle-metadata++Toggle visibility of metadata in chat windows.++== /toggle-layout++Toggle multi-window layout mode.++== /toggle-editor++Toggle between single-line and multi-line editor mode.++== /toggle-edit-lock++Toggle editor lock mode.++When the editor is locked, Enter (or C-j) does nothing.
+ doc/cmds_window.adoc view
@@ -0,0 +1,148 @@+= Window Commands+:toc:++== /channel++Set the current focused window.++See also: focus++=== Examples++`+/c libera:#haskell+` ++`+/c #haskell+` ++`+/c libera:+`++== /focus++Set the current focused window.++When only `network` is specified this switches to the network status window.+When `network` and `target` are specified this switches to that chat window.++Nickname and channels can be specified in the `target` parameter.++See also: query, channel++== /clear++Clear a window.++If no arguments are provided, the current window is cleared.+If `channel` is not provided, the `network` window is cleared.+If `channel` is `*`, all windows for `network` are cleared.+If `channel` is provided, that chat window is cleared.++If a window is cleared and no longer active (e.g. due to leaving a channel),+that window will be removed entirely.++== /windows++Show a list of all windows.++`kind`, if specified, is one of `networks`, `channels` or `users`,+and limits the list to the type of window specified.++== /splits++This command defines the set of focuses that will+always be visible, even when unfocused.+If no focuses are listed, the set will be cleared.++=== Examples++`+/splits #haskell #haskell-lens nickserv+` ++`+/splits * libera:#haskell libera:chanserv+` ++`+/splits+`++See also: splits+, splits-++== /splits+++Add focuses to the splits set.+Omit the list of focuses to add the current window.++=== Examples++`+/splits+ #haskell #haskell-lens+` ++`+/splits+ libera:#libera+` ++`+/splits++`++== /splits-++Remove focuses from the splits set.+Omit the list of focuses to remove the current window.++== /ignore++Toggle the soft-ignore on each of the space-delimited given+nicknames. Ignores can use `*` (many) and `?` (one) wildcards.+Masks can be of the form `nick[[!user]@host]`+and use a case-insensitive comparison.++If no masks are specified the current ignore list is displayed.++=== Examples++`+/ignore+` ++`+/ignore nick1 nick2 nick3+` ++`+/ignore nick@host+` ++`+/ignore nick!user@host+` ++`+/ignore *@host+` ++`+/ignore *!baduser@*+`++== /grep++Filter view contents using a regular expression.++Clear the regular expression by calling this without an argument.++`/grep` is case-sensitive by default.++=== Flags++`-A n` - Show n messages after match+`-B n` - Show n messages before match+`-C n` - Show n messages before and after match+`-F`   - Use plain-text match instead of regular expression+`-i`   - Case insensitive match+`-v`   - Invert pattern match+`-m n` - Limit results to n matches+`--`   - Stop processing flags++== /dump++Dump the current window's contents to a file.++This command always outputs as if detailed mode is active.++== /mentions++Show a list of all messages that were highlighted as important.++When using `/grep` the important messages are those matching+the regular expression instead.++== /setwindow++Set window property.++=== Properties++`louder`: Upgrades normal messages to important. ++`loud`: Uses default message importance. ++`imponly`: Downgrades normal messages to boring. ++`quiet`: Downgrades important messages to normal. ++`quieter`: Downgrades message importance one step. ++`silent`: Downgrades message importance to boring.++`show` / `hide`: Toggles if window appears in window command shortcuts.++== /setname++Set window shortcut letter. If no letter is provided the next available+letter will automatically be assigned.++Available letters are configured in the `window-names` config setting,+which defaults to the characters available from+the top letter and number rows on a QWERTY keyboard.
glirc.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.2 name:                glirc-version:             2.40+version:             2.40.1 synopsis:            Console IRC client description:         Console IRC client                      .@@ -16,10 +16,10 @@ category:            Network extra-source-files:  exec/linux_exported_symbols.txt                      exec/macos_exported_symbols.txt-extra-doc-files:     glirc.1 ChangeLog.md README.md+extra-doc-files:     glirc.1 ChangeLog.md README.md doc/*.adoc homepage:            https://github.com/glguy/irc-core bug-reports:         https://github.com/glguy/irc-core/issues-tested-with:         GHC==9.4.7+tested-with:         GHC == 9.0.2, GHC == 9.4.7, GHC == 9.8.1  custom-setup   setup-depends: base     >=4.12 && <4.20,@@ -76,6 +76,7 @@     Client.Commands.Chat     Client.Commands.Certificate     Client.Commands.Connection+    Client.Commands.Docs     Client.Commands.Exec     Client.Commands.Interpolation     Client.Commands.Operator@@ -93,6 +94,7 @@     Client.Configuration.Notifications     Client.Configuration.ServerSettings     Client.Configuration.Sts+    Client.Docs     Client.EventLoop     Client.EventLoop.Actions     Client.EventLoop.Errors@@ -163,7 +165,7 @@     Build_glirc    build-depends:-    base                 >=4.11   && <4.20,+    base                 >=4.15   && <4.20,     HsOpenSSL            >=0.11   && <0.12,     async                >=2.2    && <2.3,     attoparsec           ^>=0.14,@@ -179,7 +181,7 @@     githash              ^>=0.1.6,     hashable             >=1.2.4  && <1.5,     hookup               ^>=0.8,-    irc-core             ^>=2.11,+    irc-core             ^>=2.12,     kan-extensions       >=5.0    && <5.3,     lens                 >=4.14   && <5.3,     random               >=1.1    && <1.3,@@ -192,7 +194,7 @@     stm                  >=2.4    && <2.6,     template-haskell     >=2.11   && <2.22,     text                 >=1.2.2  && <2.2,-    time                 >=1.10   && <1.14,+    time                 >=1.9    && <1.14,     transformers         >=0.5.2  && <0.7,     unix                 >=2.7    && <2.9,     unordered-containers >=0.2.11 && <0.3,
src/Client/Commands.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE BangPatterns, OverloadedStrings, ExistentialQuantification #-}+{-# LANGUAGE BangPatterns, OverloadedStrings, TemplateHaskell, ExistentialQuantification #-}  {-| Module      : Client.Commands@@ -27,6 +27,7 @@  import Client.Commands.Arguments.Parser (parse) import Client.Commands.Arguments.Spec (optionalArg, optionalNumberArg, remainingArg, simpleToken)+import Client.Commands.Docs (clientDocs, cmdDoc) import Client.Commands.Exec import Client.Commands.Interpolation (resolveMacroExpansions, Macro(Macro), MacroSpec(MacroSpec)) import Client.Commands.Recognizer (fromCommands, keys, recognize, Recognition(Exact), Recognizer)@@ -227,93 +228,55 @@   [ Command       (pure "exit")       (pure ())-      "Exit the client immediately.\n"+      $(clientDocs `cmdDoc` "exit")     $ ClientCommand cmdExit noClientTab    , Command       (pure "reload")       (optionalArg (simpleToken "[filename]"))-      "Reload the client configuration file.\n\-      \\n\-      \If \^Bfilename\^B is provided it will be used to reload.\n\-      \Otherwise the previously loaded configuration file will be reloaded.\n"+      $(clientDocs `cmdDoc` "reload")     $ ClientCommand cmdReload tabReload    , Command       (pure "extension")       (liftA2 (,) (simpleToken "extension") (remainingArg "arguments"))-      "Calls the process_command callback of the given extension.\n\-      \\n\-      \\^Bextension\^B should be the name of the loaded extension.\n"+      $(clientDocs `cmdDoc` "extension")     $ ClientCommand cmdExtension simpleClientTab    , Command       (pure "palette")       (pure ())-      "Show the current palette settings and a color chart to help pick new colors.\n"+      $(clientDocs `cmdDoc` "palette")     $ ClientCommand cmdPalette noClientTab    , Command       (pure "digraphs")       (pure ())-      "\^BDescription:\^B\n\-      \\n\-      \    Show the table of digraphs. A digraph is a pair of characters\n\-      \    can be used together to represent an uncommon character. Type\n\-      \    the two-character digraph corresponding to the desired output\n\-      \    character and then press M-k (default binding).\n\-      \\n\-      \    Note that the digraphs list is searchable with /grep.\n\-      \\n\-      \\^BSee also:\^B grep\n"+      $(clientDocs `cmdDoc` "digraphs")     $ ClientCommand cmdDigraphs noClientTab    , Command       (pure "keymap")       (pure ())-      "Show the key binding map.\n\-      \\n\-      \Key bindings can be changed in configuration file. See `glirc --config-format`.\n"+      $(clientDocs `cmdDoc` "keymap")     $ ClientCommand cmdKeyMap noClientTab    , Command       (pure "rtsstats")       (pure ())-      "Show the GHC RTS statistics.\n"+      $(clientDocs `cmdDoc` "rtsstats")     $ ClientCommand cmdRtsStats noClientTab    , Command       (pure "exec")       (remainingArg "arguments")-      "Execute a command synchnonously sending the to a configuration destination.\n\-      \\n\-      \\^Barguments\^B: [-n[network]] [-c[channel]] [-i input] command [arguments...]\n\-      \\n\-      \When \^Binput\^B is specified it is sent to the stdin.\n\-      \\n\-      \When neither \^Bnetwork\^B nor \^Bchannel\^B are specified output goes to client window (*).\n\-      \When \^Bnetwork\^B is specified output is sent as raw IRC traffic to the network.\n\-      \When \^Bchannel\^B is specified output is sent as chat to the given channel on the current network.\n\-      \When \^Bnetwork\^B and \^Bchannel\^B are specified output is sent as chat to the given channel on the given network.\n\-      \\n\-      \\^Barguments\^B is divided on spaces into words before being processed\-      \ by getopt. Use Haskell string literal syntax to create arguments with\-      \ escaped characters and spaces inside.\n\-      \\n"+      $(clientDocs `cmdDoc` "exec")     $ ClientCommand cmdExec simpleClientTab    , Command       (pure "url")       optionalNumberArg-      "Open a URL seen in chat.\n\-      \\n\-      \The URL is opened using the executable configured under \^Burl-opener\^B.\n\-      \\n\-      \When this command is active in the textbox, chat messages are filtered to\-      \ only show ones with URLs.\n\-      \\n\-      \When \^Bnumber\^B is omitted it defaults to \^B1\^B. The number selects the\-      \ URL to open counting back from the most recent.\n"+      $(clientDocs `cmdDoc` "url")     $ ClientCommand cmdUrl noClientTab    , newCertificateCommand@@ -321,10 +284,7 @@   , Command       (pure "help")       (optionalArg (simpleToken "[command]"))-      "Show command documentation.\n\-      \\n\-      \When \^Bcommand\^B is omitted a list of all commands is displayed.\n\-      \When \^Bcommand\^B is specified detailed help for that command is shown.\n"+      $(clientDocs `cmdDoc` "help")     $ ClientCommand cmdHelp tabHelp    ------------------------------------------------------------------------
src/Client/Commands/Certificate.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, TemplateHaskell #-} {-| Module      : Client.Commands.Certificate Description : Certificate management commands@@ -10,6 +10,7 @@ module Client.Commands.Certificate (newCertificateCommand) where  import Client.Commands.Arguments.Spec+import Client.Commands.Docs (netDocs, cmdDoc) import Client.Commands.TabCompletion (noClientTab) import Client.Commands.Types import Client.State (recordError, recordSuccess)@@ -45,29 +46,7 @@   Command     (pure "new-self-signed-cert")     (liftA2 (,) (simpleToken "filename") keysizeArg)-      "\^BParameters:\^B\n\-      \\n\-      \    filename:   Certificate and private key PEM output\n\-      \    keysize:    Public-key size (default 2048, range 1024-8192)\n\-      \    passphrase: Optional AES-128 private key passphrase\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Generate a new self-signed certificate for network service\n\-      \    identification.\n\-      \\n\-      \\^BExample command:\^B\n\-      \\n\-      \    /new-self-signed-cert /home/me/.glirc/config/my.pem 2048 SeCrEt\n\-      \\n\-      \\^BExample configuration:\^B\n\-      \    servers:\n\-      \      * name:                    \"fn\"\n\-      \        hostname:                \"irc.libera.chat\"\n\-      \        sasl: mechanism:         external\n\-      \        tls:                     yes\n\-      \        tls-client-cert:         \"my.pem\"\n\-      \        tls-client-key-password: \"SeCrEt\"\n"+    $(netDocs `cmdDoc` "new-self-signed-cert")     (ClientCommand cmdNewCert noClientTab)  cmdNewCert :: ClientCommand (String, Maybe (Int, String))
src/Client/Commands/Channel.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, TemplateHaskell #-} {-| Module      : Client.Commands.Channel Description : Channel management command implementations@@ -10,6 +10,7 @@ module Client.Commands.Channel (channelCommands) where  import Client.Commands.Arguments.Spec+import Client.Commands.Docs (chanopDocs, cmdDoc) import Client.Commands.TabCompletion (activeNicks, noChannelTab, simpleChannelTab, simpleTabCompletion) import Client.Commands.Types import Client.Commands.WordCompletion (plainWordCompleteMode)@@ -39,76 +40,43 @@   [ Command       (pure "mode")       (fromMaybe [] <$> optionalArg (extensionArg "[modes]" modeParamArgs))-      "Sets IRC modes.\n\-      \\n\-      \Examples:\n\-      \Setting a ban:           /mode +b *!*@hostname\n\-      \Removing a quiet:        /mode -q *!*@hostname\n\-      \Voicing two users:       /mode +vv user1 user2\n\-      \Demoting an op to voice: /mode +v-o user1 user1\n\-      \\n\-      \When executed in a network window, mode changes are applied to your user.\n\-      \When executed in a channel window, mode changes are applied to the channel.\n\-      \\n\-      \This command has parameter sensitive tab-completion.\n\-      \\n\-      \See also: /masks /channelinfo\n"+      $(chanopDocs `cmdDoc` "mode")     $ NetworkCommand cmdMode tabMode    , Command       (pure "masks")       (simpleToken "mode")-      "Show mask lists for current channel.\n\-      \\n\-      \Common \^Bmode\^B values:\n\-      \\^Bb\^B: bans\n\-      \\^Bq\^B: quiets\n\-      \\^BI\^B: invite exemptions (op view only)\n\-      \\^Be\^B: ban exemption (op view only)s\n\-      \\n\-      \To populate the mask lists for the first time use: /mode \^Bmode\^B\n\-      \\n\-      \See also: /mode\n"+      $(chanopDocs `cmdDoc` "masks")     $ ChannelCommand cmdMasks noChannelTab    , Command       (pure "invite")       (simpleToken "nick")-      "Invite a user to the current channel.\n"+      $(chanopDocs `cmdDoc` "invite")     $ ChannelCommand cmdInvite simpleChannelTab    , Command       (pure "topic")       (remainingArg "message")-      "Set the topic on the current channel.\n\-      \\n\-      \Tab-completion with no \^Bmessage\^B specified will load the current topic for editing.\n"+      $(chanopDocs `cmdDoc` "topic")     $ ChannelCommand cmdTopic tabTopic    , Command       (pure "kick")       (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))-      "Kick a user from the current channel.\n\-      \\n\-      \See also: /kickban /remove\n"+      $(chanopDocs `cmdDoc` "kick")     $ ChannelCommand cmdKick simpleChannelTab    , Command       (pure "kickban")       (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))-      "Ban and kick a user from the current channel.\n\-      \\n\-      \Users are banned by hostname match.\n\-      \See also: /kick /remove\n"+      $(chanopDocs `cmdDoc` "kickban")     $ ChannelCommand cmdKickBan simpleChannelTab    , Command       (pure "remove")       (liftA2 (,) (simpleToken "nick") (remainingArg "reason"))-      "Remove a user from the current channel.\n\-      \\n\-      \Remove works like /kick except it results in a PART.\n\-      \See also: /kick /kickban\n"+      $(chanopDocs `cmdDoc` "remove")     $ ChannelCommand cmdRemove simpleChannelTab    ]
src/Client/Commands/Chat.hs view
@@ -1,4 +1,4 @@-{-# Language BangPatterns, OverloadedStrings #-}+{-# Language BangPatterns, OverloadedStrings, TemplateHaskell #-} {-| Module      : Client.Commands.Chat Description : Common user IRC commands@@ -31,6 +31,7 @@ import Irc.Identifier (Identifier, idText, mkId) import Irc.Message (IrcMsg(Privmsg, Notice, Ctcp), Source(Source)) import Irc.RawIrcMsg (RawIrcMsg, parseRawIrcMsg)+import Client.Commands.Docs (chatDocs, cmdDoc)  chatCommands :: CommandSection chatCommands = CommandSection "IRC commands"@@ -39,319 +40,106 @@   [ Command       ("join" :| ["j"])       (liftA2 (,) (simpleToken "channels") (optionalArg (simpleToken "[keys]")))-      "\^BParameters:\^B\n\-      \\n\-      \    channels: Comma-separated list of channels\n\-      \    keys:     Comma-separated list of keys\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Join the given channels. When keys are provided, they should\n\-      \    occur in the same order as the channels.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /join #friends\n\-      \    /join #secret thekey\n\-      \    /join #secret1,#secret2 key1,key2\n\-      \\n\-      \\^BSee also:\^B channel, clear, part\n"+      $(chatDocs `cmdDoc` "join")     $ NetworkCommand cmdJoin simpleNetworkTab    , Command       (pure "part")       (remainingArg "reason")-      "\^BParameters:\^B\n\-      \\n\-      \    reason: Optional message sent to channel as part reason\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Part from the current channel.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /part\n\-      \    /part It's not me, it's you\n\-      \\n\-      \\^BSee also:\^B clear, join, quit\n"+      $(chatDocs `cmdDoc` "part")     $ ChannelCommand cmdPart simpleChannelTab    , Command       (pure "msg")       (liftA2 (,) (simpleToken "target") (remainingArg "message"))-      "\^BParameters:\^B\n\-      \\n\-      \    target:  Comma-separated list of nicknames and channels\n\-      \    message: Formatted message body\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a chat message to a user or a channel. On servers\n\-      \    with STATUSMSG support, the channel name can be prefixed\n\-      \    with a sigil to restrict the recipients to those with the\n\-      \    given mode.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /msg buddy I'm sending you a message.\n\-      \    /msg #friends This message is for the whole channel.\n\-      \    /msg him,her I'm chatting with two people.\n\-      \    /msg @#users This message is only for ops!\n\-      \\n\-      \\^BSee also:\^B notice, me, say\n"+      $(chatDocs `cmdDoc` "msg")     $ NetworkCommand cmdMsg simpleNetworkTab    , Command       (pure "me")       (remainingArg "message")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Body of action message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Sends an action message to the currently focused channel.\n\-      \    Most clients will render these messages prefixed with\n\-      \    only your nickname as though describing an action.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /me shrugs\n\-      \\n\-      \\^BSee also:\^B notice, msg, say\n"+      $(chatDocs `cmdDoc` "me")     $ ChatCommand cmdMe simpleChannelTab    , Command       (pure "say")       (remainingArg "message")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Body of message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a message to the current chat window.  This can be useful\n\-      \    for sending a chat message with a leading '/' to the current\n\-      \    chat window.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /say /help is the right place to start!\n\-      \\n\-      \\^BSee also:\^B notice, me, msg\n"+      $(chatDocs `cmdDoc` "say")     $ ChatCommand cmdSay simpleChannelTab    , Command       ("query" :| ["q"])       (liftA2 (,) (simpleToken "target") (remainingArg "message"))-      "\^BParameters:\^B\n\-      \\n\-      \    target: Focus name\n\-      \    message: Optional message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    This command switches the client focus to the given\n\-      \    target and optionally sends a message to that target.\n\-      \\n\-      \    Channel: \^_#channel\^_\n\-      \    Channel: \^_network\^_:\^_#channel\^_\n\-      \    User:    \^_nick\^_\n\-      \    User:    \^_network\^_:\^_nick\^_\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /q fn:#haskell\n\-      \    /q #haskell\n\-      \    /q lambdabot @messages\n\-      \    /q irc_friend How are you?\n\-      \\n\-      \\^BSee also:\^B msg channel focus\n"+      $(chatDocs `cmdDoc` "query")     $ ClientCommand cmdQuery simpleClientTab    , Command       (pure "notice")       (liftA2 (,) (simpleToken "target") (remainingArg "message"))-      "\^BParameters:\^B\n\-      \\n\-      \    target:  Comma-separated list of nicknames and channels\n\-      \    message: Formatted message body\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a chat notice to a user or a channel. On servers\n\-      \    with STATUSMSG support, the channel name can be prefixed\n\-      \    with a sigil to restrict the recipients to those with the\n\-      \    given mode. Notice messages were originally intended to be\n\-      \    used by bots. Different clients will render these in different\n\-      \    ways.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /notice buddy I'm sending you a message.\n\-      \    /notice #friends This message is for the whole channel.\n\-      \    /notice him,her I'm chatting with two people.\n\-      \    /notice @#users This message is only for ops!\n\-      \\n\-      \\^BSee also:\^B me, msg, say\n"+      $(chatDocs `cmdDoc` "notice")     $ NetworkCommand cmdNotice simpleNetworkTab    , Command       (pure "wallops")       (remainingArg "message to +w users")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Formatted message body\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a network-wide WALLOPS message. These message go out\n\-      \    to users who have the 'w' usermode set.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /wallops Hi everyone, thanks for using this network!\n\-      \\n\-      \\^BSee also:\^B me, msg, say\n"+      $(chatDocs `cmdDoc` "wallops")     $ NetworkCommand cmdWallops simpleNetworkTab    , Command       (pure "operwall")       (remainingArg "message to +z opers")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Formatted message body\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Send a network-wide WALLOPS message to opers. These message go\n\-      \    out to opers who have the 'z' usermode set.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /operwall What's this even for?\n\-      \\n\-      \\^BSee also:\^B me, msg, say\n"+      $(chatDocs `cmdDoc` "operwall")     $ NetworkCommand cmdOperwall simpleNetworkTab    , Command       (pure "ctcp")       (liftA3 (,,) (simpleToken "target") (simpleToken "command") (remainingArg "arguments"))-      "\^BParameters:\^B\n\-      \\n\-      \    target:    Comma-separated list of nicknames and channels\n\-      \    command:   CTCP command name\n\-      \    arguments: CTCP command arguments\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Client-to-client protocol (CTCP) commands can be used\n\-      \    to query information from another user's client application\n\-      \    directly. Common CTCP commands include: ACTION, PING, VERSION,\n\-      \    USERINFO, CLIENTINFO, and TIME. glirc does not automatically\n\-      \    respond to CTCP commands.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /ctcp myfriend VERSION\n\-      \    /ctcp myfriend CLIENTINFO\n"+      $(chatDocs `cmdDoc` "ctcp")     $ NetworkCommand cmdCtcp simpleNetworkTab    , Command       (pure "nick")       (simpleToken "nick")-      "\^BParameters:\^B\n\-      \\n\-      \    nick: New nickname\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Change your nickname on the currently focused server.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /nick guest123\n\-      \    /nick better_nick\n"+      $(chatDocs `cmdDoc` "nick")     $ NetworkCommand cmdNick simpleNetworkTab    , Command       (pure "away")       (remainingArg "message")-      "\^BParameters:\^B\n\-      \\n\-      \    message: Optional away message\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Change your nickname on the currently focused server.\n\-      \    Omit the message parameter to clear your away status.\n\-      \    The away message is only used by the server to update\n\-      \    status in /whois and to provide automated responses.\n\-      \    It is not used by this client directly.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /away\n\-      \    /away Out getting some sun\n"+      $(chatDocs `cmdDoc` "away")     $ NetworkCommand cmdAway simpleNetworkTab    , Command       (pure "names")       (pure ())-      "\^BDescription:\^B\n\-      \\n\-      \    Show the user list for the current channel.\n\-      \    Detailed view (default key F2) shows full hostmask.\n\-      \    Hostmasks can be populated with /who #channel.\n\-      \    Press ESC to exit the userlist.\n\-      \\n\-      \\^BSee also:\^B channelinfo, masks\n"+      $(chatDocs `cmdDoc` "names")     $ ChannelCommand cmdChanNames noChannelTab    , Command       (pure "channelinfo")       (pure ())-      "\^BDescription:\^B\n\-      \\n\-      \    Show information about the current channel.\n\-      \    Press ESC to exit the channel info window.\n\-      \\n\-      \    Information includes topic, creation time, URL, and modes.\n\-      \\n\-      \\^BSee also:\^B masks, mode, topic, users\n"+      $(chatDocs `cmdDoc` "channelinfo")     $ ChannelCommand cmdChannelInfo noChannelTab    , Command       (pure "knock")       (liftA2 (,) (simpleToken "channel") (remainingArg "message"))-      "Request entry to an invite-only channel.\n"+      $(chatDocs `cmdDoc` "knock")     $ NetworkCommand cmdKnock simpleNetworkTab    , Command       (pure "quote")       (remainingArg "raw IRC command")-      "Send a raw IRC command.\n"+      $(chatDocs `cmdDoc` "quote")     $ NetworkCommand cmdQuote simpleNetworkTab    , Command       (pure "monitor")       (extensionArg "[+-CLS]" monitorArgs)-      "\^BSubcommands:\^B\n\-      \\n\-      \    /monitor + target[,target2]* - Add nicknames to monitor list\n\-      \    /monitor - target[,target2]* - Remove nicknames to monitor list\n\-      \    /monitor C                   - Clear monitor list\n\-      \    /monitor L                   - Show monitor list\n\-      \    /monitor S                   - Show status of nicknames on monitor list\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Monitor is a protocol for getting server-side notifications\n\-      \    when users become online/offline.\n"+      $(chatDocs `cmdDoc` "monitor")     $ NetworkCommand cmdMonitor simpleNetworkTab -    ]+  ]  monitorArgs :: ClientState -> String -> Maybe (Args ClientState [String]) monitorArgs _ str =
src/Client/Commands/Connection.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, TemplateHaskell #-} {-| Module      : Client.Commands.Connection Description : Connection command implementations@@ -10,6 +10,7 @@ module Client.Commands.Connection (connectionCommands) where  import Client.Commands.Arguments.Spec ( remainingArg, simpleToken )+import Client.Commands.Docs (netDocs, cmdDoc) import Client.Commands.TabCompletion import Client.Commands.Types import Client.Commands.WordCompletion ( plainWordCompleteMode )@@ -28,45 +29,37 @@   [ Command       (pure "connect")       (simpleToken "network")-      "Connect to \^Bnetwork\^B by name.\n\-      \\n\-      \If no name is configured the hostname is the 'name'.\n"+      $(netDocs `cmdDoc` "connect")     $ ClientCommand cmdConnect tabConnect    , Command       (pure "reconnect")       (pure ())-      "Reconnect to the current network.\n"+      $(netDocs `cmdDoc` "reconnect")     $ ClientCommand cmdReconnect noClientTab    , Command       (pure "disconnect")       (pure ())-      "Immediately terminate the current network connection.\n\-      \\n\-      \See also: /quit /exit\n"+      $(netDocs `cmdDoc` "disconnect")     $ NetworkCommand cmdDisconnect noNetworkTab    , Command       (pure "quit")       (remainingArg "reason")-      "Gracefully disconnect the current network connection.\n\-      \\n\-      \\^Breason\^B: optional quit reason\n\-      \\n\-      \See also: /disconnect /exit\n"+      $(netDocs `cmdDoc` "quit")     $ NetworkCommand cmdQuit simpleNetworkTab    , Command       (pure "cert")       (pure ())-      "Show the TLS certificate for the current connection.\n"+      $(netDocs `cmdDoc` "cert")     $ NetworkCommand cmdCert noNetworkTab    , Command       (pure "umode")       (remainingArg "modes")-      "Apply a user-mode change.\n"+      $(netDocs `cmdDoc` "umode")     $ NetworkCommand cmdUmode noNetworkTab   ] 
+ src/Client/Commands/Docs.hs view
@@ -0,0 +1,59 @@+{-# Language OverloadedStrings, TemplateHaskell #-}++{-|+Module      : Client.Commands.Docs+Description : Command documentation+Copyright   : (c) TheDaemoness 2023+License     : ISC+Maintainer  : emertens@gmail.com++This module contains definitions for all of glirc's command docs.+-}+module Client.Commands.Docs+  ( cmdDoc+  , chanopDocs+  , chatDocs+  , clientDocs+  , integrationDocs+  , netDocs+  , operDocs+  , queriesDocs+  , togglesDocs+  , windowDocs+  ) where++import Client.Docs (Docs, loadDoc, lookupDoc, makeHeader)+import Language.Haskell.TH (Q, Exp)+import Language.Haskell.TH.Syntax (lift)++cmdDoc :: Docs -> String -> Q Exp+cmdDoc docs key = lookupDoc (makeHeader "Description") ('/':key) docs++-- TODO: Replace each id with something that splits off the command name.++chanopDocs :: Docs+chanopDocs = $(loadDoc id "cmds_chanop" >>= lift)++chatDocs :: Docs+chatDocs = $(loadDoc id "cmds_chat" >>= lift)++clientDocs :: Docs+clientDocs = $(loadDoc id "cmds_client" >>= lift)++integrationDocs :: Docs+integrationDocs = $(loadDoc id "cmds_integration" >>= lift)++netDocs :: Docs+netDocs = $(loadDoc id "cmds_net" >>= lift)++operDocs :: Docs+operDocs = $(loadDoc id "cmds_oper" >>= lift)++queriesDocs :: Docs+queriesDocs = $(loadDoc id "cmds_queries" >>= lift)++togglesDocs :: Docs+togglesDocs = $(loadDoc id "cmds_toggles" >>= lift)++windowDocs :: Docs+windowDocs = $(loadDoc id "cmds_window" >>= lift)
src/Client/Commands/Exec.hs view
@@ -31,8 +31,8 @@ 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)+import System.Console.GetOpt (getOpt, ArgDescr(ReqArg, OptArg, NoArg), ArgOrder(RequireOrder), OptDescr(..))+import System.Process.Typed (byteStringInput, proc, readProcessStdout, setStdin, ExitCode (ExitFailure))  -- | Settings for @/exec@ command. --@@ -53,6 +53,7 @@   , _execCommand       :: String        -- ^ command filename   , _execStdIn         :: String        -- ^ stdin source   , _execArguments     :: [String]      -- ^ command arguments+  , _execIgnoreError   :: Bool          -- ^ ignore the process exit code   }   deriving (Read,Show) @@ -69,6 +70,7 @@   , _execCommand       = error "no default command"   , _execStdIn         = ""   , _execArguments     = []+  , _execIgnoreError   = False   }  options :: [OptDescr (ExecCmd -> ExecCmd)]@@ -83,6 +85,9 @@   , Option "i" ["input"]         (ReqArg (set execStdIn) "INPUT")         "Use string as stdin"+  , Option "e" ["error"]+        (NoArg (set execIgnoreError True))+        "Ignore process error codes"   ]  -- | Parse the arguments to @/exec@ looking for various flags@@ -106,15 +111,17 @@   ExecCmd                       {- ^ exec configuration          -} ->   IO (Either [String] [String]) {- ^ error lines or output lines -} runExecCmd 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 out ->-          case Text.decodeUtf8' (L.toStrict out) of-             Right str -> Right (lines (Text.unpack str))-             Left e -> Left [displayException e]+ do res <-+      readProcessStdout+        (setStdin (byteStringInput (L.fromStrict (Text.encodeUtf8 (Text.pack (view execStdIn cmd)))))+        (proc (view execCommand   cmd) (view execArguments cmd)))+    return $! case res of+      (ExitFailure code, _) | not (view execIgnoreError cmd) ->+        Left ["Process failed with exit code " ++ show code]+      (_, 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/Operator.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, TemplateHaskell #-} {-| Module      : Client.Commands.Operator Description : Operator command implementations@@ -10,6 +10,7 @@ module Client.Commands.Operator (operatorCommands) where  import Client.Commands.Arguments.Spec (optionalArg, remainingArg, simpleToken)+import Client.Commands.Docs (operDocs, cmdDoc) import Client.Commands.TabCompletion (noNetworkTab, simpleNetworkTab) import Client.Commands.Types import Client.State.Network (sendMsg)@@ -25,152 +26,153 @@   [ Command       (pure "oper")       (liftA2 (,) (simpleToken "user") (simpleToken "password"))-      "Authenticate as a server operator.\n"+      $(operDocs `cmdDoc` "oper")     $ NetworkCommand cmdOper noNetworkTab    , Command       (pure "kill")       (liftA2 (,) (simpleToken "client") (remainingArg "reason"))-      "Kill a client connection to the server.\n"+      $(operDocs `cmdDoc` "kill")     $ NetworkCommand cmdKill simpleNetworkTab    , Command       (pure "kline")       (liftA3 (,,) (simpleToken "minutes") (simpleToken "user@host") (remainingArg "reason"))-      "Ban a client from the server.\n"+      $(operDocs `cmdDoc` "kline")     $ NetworkCommand cmdKline simpleNetworkTab    , Command       (pure "unkline")       (liftA2 (,) (simpleToken "[user@]host") (optionalArg (simpleToken "[servername]")))-      "Unban a client from the server.\n"+      $(operDocs `cmdDoc` "unkline")     $ NetworkCommand cmdUnkline simpleNetworkTab    , Command       (pure "undline")       (liftA2 (,) (simpleToken "host") (optionalArg (simpleToken "[servername]")))-      "Unban a client from the server.\n"+      $(operDocs `cmdDoc` "undline")     $ NetworkCommand cmdUndline simpleNetworkTab    , Command       (pure "unxline")       (liftA2 (,) (simpleToken "gecos") (optionalArg (simpleToken "[servername]")))-      "Unban a gecos from the server.\n"+      $(operDocs `cmdDoc` "unxline")     $ NetworkCommand cmdUnxline simpleNetworkTab    , Command       (pure "unresv")       (liftA2 (,) (simpleToken "channel|nick") (optionalArg (simpleToken "[servername]")))-      "Unban a channel or nickname from the server.\n"+      $(operDocs `cmdDoc` "unresv")     $ NetworkCommand cmdUnresv simpleNetworkTab    , Command       (pure "testline")       (simpleToken "[[nick!]user@]host")-      "Check matching I/K/D lines for a [[nick!]user@]host\n"+      $(operDocs `cmdDoc` "testline")     $ NetworkCommand cmdTestline simpleNetworkTab    , Command       (pure "testkline")       (simpleToken "[user@]host")-      "Check matching K/D lines for a [user@]host\n"+      $(operDocs `cmdDoc` "testkline")     $ NetworkCommand cmdTestkline simpleNetworkTab    , Command       (pure "testgecos")       (simpleToken "gecos")-      "Check matching X lines for a gecos\n"+      $(operDocs `cmdDoc` "testgecos")     $ NetworkCommand cmdTestGecos simpleNetworkTab    , Command       (pure "testmask")       (liftA2 (,) (simpleToken "[nick!]user@host") (remainingArg "[gecos]"))-      "Test how many local and global clients match a mask.\n"+      $(operDocs `cmdDoc` "testmask")     $ NetworkCommand cmdTestmask simpleNetworkTab    , Command       (pure "masktrace")       (liftA2 (,) (simpleToken "[nick!]user@host") (remainingArg "[gecos]"))-      "Outputs a list of local users matching the given masks.\n"+      $(operDocs `cmdDoc` "masktrace")     $ NetworkCommand cmdMasktrace simpleNetworkTab    , Command       (pure "chantrace")       (simpleToken "channel")-      "Outputs a list of channel members in etrace format.\n"+      $(operDocs `cmdDoc` "chantrace")     $ NetworkCommand cmdChantrace simpleNetworkTab    , Command       (pure "trace")       (optionalArg (liftA2 (,) (simpleToken "[server|nick]") (optionalArg (simpleToken "[location]"))))-      "Outputs a list users on a server.\n"+      $(operDocs `cmdDoc` "trace")     $ NetworkCommand cmdTrace simpleNetworkTab    , Command       (pure "etrace")       (optionalArg (simpleToken "[-full|-v4|-v6|nick]"))-      "Outputs a list users on a server.\n"+      $(operDocs `cmdDoc` "etrace")     $ NetworkCommand cmdEtrace simpleNetworkTab    , Command       (pure "map")       (pure ())-      "Display network map.\n"+      $(operDocs `cmdDoc` "map")     $ NetworkCommand cmdMap simpleNetworkTab    , Command       (pure "sconnect")       (liftA2 (,) (simpleToken "connect_to") (optionalArg (liftA2 (,) (simpleToken "[port]") (optionalArg (simpleToken "[remote]")))))-      "Connect two servers together.\n"+      $(operDocs `cmdDoc` "sconnect")     $ NetworkCommand cmdSconnect simpleNetworkTab    , Command       (pure "squit")       (liftA2 (,) (simpleToken "server") (remainingArg "[reason]"))-      "Split a server away from your side of the network.\n"+      $(operDocs `cmdDoc` "squit")     $ NetworkCommand cmdSquit simpleNetworkTab    , Command       (pure "modload")       (liftA2 (,) (simpleToken "[path/]module") (optionalArg (simpleToken "[remote]")))-      "Load an IRCd module.\n"+      $(operDocs `cmdDoc` "modload")     $ NetworkCommand cmdModload simpleNetworkTab    , Command       (pure "modunload")       (liftA2 (,) (simpleToken "module") (optionalArg (simpleToken "[remote]")))-      "Unload an IRCd module.\n"+      $(operDocs `cmdDoc` "modunload")     $ NetworkCommand cmdModunload simpleNetworkTab    , Command       (pure "modlist")       (optionalArg (liftA2 (,) (simpleToken "pattern") (optionalArg (simpleToken "[remote]"))))-      "List loaded IRCd modules.\n"+      $(operDocs `cmdDoc` "modlist")     $ NetworkCommand cmdModlist simpleNetworkTab    , Command       (pure "modrestart")       (optionalArg (simpleToken "[server]"))-      "Reload all IRCd modules.\n"+      $(operDocs `cmdDoc` "modrestart")     $ NetworkCommand cmdModrestart simpleNetworkTab    , Command       (pure "modreload")       (liftA2 (,) (simpleToken "module") (optionalArg (simpleToken "[remote]")))-      "Reload an IRCd module.\n"+      $(operDocs `cmdDoc` "modreload")     $ NetworkCommand cmdModreload simpleNetworkTab    , Command       (pure "grant")       (liftA2 (,) (simpleToken "target") (simpleToken "privset"))-      "Manually assign a privset to a user.\n"+      $(operDocs `cmdDoc` "grant")     $ NetworkCommand cmdGrant simpleNetworkTab    , Command       (pure "privs")       (optionalArg (simpleToken "[target]"))-      "Check operator privileges of the target.\n"+      $(operDocs `cmdDoc` "privs")     $ NetworkCommand cmdPrivs simpleNetworkTab+   ]  cmdGrant :: NetworkCommand (String, String)
src/Client/Commands/Queries.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, TemplateHaskell #-} {-# LANGUAGE BlockArguments #-} {-# LANGUAGE ViewPatterns #-} {-|@@ -12,6 +12,7 @@ module Client.Commands.Queries (queryCommands) where  import Client.Commands.Arguments.Spec (optionalArg, remainingArg, simpleToken, extensionArg, Args)+import Client.Commands.Docs (queriesDocs, cmdDoc) import Client.Commands.TabCompletion (noNetworkTab, simpleNetworkTab) import Client.Commands.Types (commandSuccess, commandSuccessUpdateCS, Command(Command), CommandImpl(NetworkCommand), CommandSection(CommandSection), NetworkCommand) import Client.State (changeSubfocus, ClientState)@@ -31,115 +32,97 @@   [ Command       (pure "who")       (optionalArg (liftA2 (,) (simpleToken "[channel|nick|mask]") (optionalArg (simpleToken "[options]"))))-      "Send WHO query to server with given arguments, or just show the who view.\n"+      $(queriesDocs `cmdDoc` "who")     $ NetworkCommand cmdWho simpleNetworkTab    , Command       (pure "whois")       (remainingArg "arguments")-      "Send WHOIS query to server with given arguments.\n"+      $(queriesDocs `cmdDoc` "whois")     $ NetworkCommand cmdWhois simpleNetworkTab    , Command       (pure "whowas")       (remainingArg "arguments")-      "Send WHOWAS query to server with given arguments.\n"+      $(queriesDocs `cmdDoc` "whowas")     $ NetworkCommand cmdWhowas simpleNetworkTab    , Command       (pure "ison")       (remainingArg "arguments")-      "Send ISON query to server with given arguments.\n"+      $(queriesDocs `cmdDoc` "ison")     $ NetworkCommand cmdIson   simpleNetworkTab    , Command       (pure "userhost")       (remainingArg "arguments")-      "Send USERHOST query to server with given arguments.\n"+      $(queriesDocs `cmdDoc` "userhost")     $ NetworkCommand cmdUserhost simpleNetworkTab    , Command       (pure "time")       (optionalArg (simpleToken "[servername]"))-      "Send TIME query to server with given arguments.\n"+      $(queriesDocs `cmdDoc` "time")     $ NetworkCommand cmdTime simpleNetworkTab    , Command       (pure "stats")       (remainingArg "arguments")-      "Send STATS query to server with given arguments.\n"+      $(queriesDocs `cmdDoc` "stats")     $ NetworkCommand cmdStats simpleNetworkTab    , Command       (pure "lusers")       (optionalArg (simpleToken "[servername]"))-      "Send LUSERS query to a given server.\n"+      $(queriesDocs `cmdDoc` "lusers")     $ NetworkCommand cmdLusers simpleNetworkTab    , Command       (pure "users")       (optionalArg (simpleToken "[servername]"))-      "Send USERS query to a given server.\n"+      $(queriesDocs `cmdDoc` "users")     $ NetworkCommand cmdUsers simpleNetworkTab    , Command-      (pure "motd") (optionalArg (simpleToken "[servername]"))-      "Send MOTD query to server.\n"+      (pure "motd")+      (optionalArg (simpleToken "[servername]"))+      $(queriesDocs `cmdDoc` "motd")     $ NetworkCommand cmdMotd simpleNetworkTab    , Command-      (pure "admin") (optionalArg (simpleToken "[servername]"))-      "Send ADMIN query to server.\n"+      (pure "admin")+      (optionalArg (simpleToken "[servername]"))+      $(queriesDocs `cmdDoc` "admin")     $ NetworkCommand cmdAdmin simpleNetworkTab    , Command-      (pure "rules") (optionalArg (simpleToken "[servername]"))-      "Send RULES query to server.\n"+      (pure "rules")+      (optionalArg (simpleToken "[servername]"))+      $(queriesDocs `cmdDoc` "rules")     $ NetworkCommand cmdRules simpleNetworkTab    , Command-      (pure "info") (pure ())-      "Send INFO query to server.\n"+      (pure "info")+      (pure ())+      $(queriesDocs `cmdDoc` "info")     $ NetworkCommand cmdInfo noNetworkTab    , Command       (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"+      $(queriesDocs `cmdDoc` "list")     $ NetworkCommand cmdList simpleNetworkTab    , Command       (pure "links")       (remainingArg "arguments")-      "Send LINKS query to server with given arguments.\n"+      $(queriesDocs `cmdDoc` "links")     $ NetworkCommand cmdLinks simpleNetworkTab    , Command-      (pure "version") (optionalArg (simpleToken "[servername]"))-      "Send VERSION query to server.\n"+      (pure "version")+      (optionalArg (simpleToken "[servername]"))+      $(queriesDocs `cmdDoc` "version")     $ NetworkCommand cmdVersion simpleNetworkTab    ]
src/Client/Commands/Toggles.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, TemplateHaskell #-} {-| Module      : Client.Commands.Toggles Description : View modality command implementations@@ -9,6 +9,7 @@  module Client.Commands.Toggles (togglesCommands) where +import Client.Commands.Docs (togglesDocs, cmdDoc) import Client.Commands.TabCompletion (noClientTab) import Client.Commands.Types import Client.Configuration (EditMode(SingleLineEditor, MultiLineEditor), LayoutMode(OneColumn, TwoColumn))@@ -21,43 +22,43 @@   [ Command       (pure "toggle-detail")       (pure ())-      "Toggle detailed message view.\n"+      $(togglesDocs `cmdDoc` "toggle-detail")     $ ClientCommand cmdToggleDetail noClientTab    , Command       (pure "toggle-activity-bar")       (pure ())-      "Toggle detailed detailed activity information in status bar.\n"+      $(togglesDocs `cmdDoc` "toggle-activity-bar")     $ ClientCommand cmdToggleActivityBar noClientTab    , Command       (pure "toggle-show-ping")       (pure ())-      "Toggle visibility of ping round-trip time.\n"+      $(togglesDocs `cmdDoc` "toggle-show-ping")     $ ClientCommand cmdToggleShowPing noClientTab    , Command       (pure "toggle-metadata")       (pure ())-      "Toggle visibility of metadata in chat windows.\n"+      $(togglesDocs `cmdDoc` "toggle-metadata")     $ ClientCommand cmdToggleMetadata noClientTab    , Command       (pure "toggle-layout")       (pure ())-      "Toggle multi-window layout mode.\n"+      $(togglesDocs `cmdDoc` "toggle-layout")     $ ClientCommand cmdToggleLayout noClientTab    , Command       (pure "toggle-editor")       (pure ())-      "Toggle between single-line and multi-line editor mode.\n"+      $(togglesDocs `cmdDoc` "toggle-editor")     $ ClientCommand cmdToggleEditor noClientTab    , Command       (pure "toggle-edit-lock")       (pure ())-      "Toggle editor lock mode. When editor is locked pressing Enter is disabled.\n"+      $(togglesDocs `cmdDoc` "toggle-edit-lock")     $ ClientCommand cmdToggleEditLock noClientTab   ] 
src/Client/Commands/Window.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language OverloadedStrings, TemplateHaskell #-} {-| Module      : Client.Commands.Window Description : Window command implementations@@ -10,6 +10,7 @@ module Client.Commands.Window (windowCommands, parseFocus) where  import Client.Commands.Arguments.Spec+import Client.Commands.Docs (windowDocs, cmdDoc) import Client.Commands.TabCompletion import Client.Commands.Types import Client.Commands.WordCompletion (plainWordCompleteMode)@@ -41,216 +42,79 @@   [ Command       (pure "focus")       (liftA2 (,) (simpleToken "network") (optionalArg (simpleToken "[target]")))-      "Change the focused window.\n\-      \\n\-      \When only \^Bnetwork\^B is specified this switches to the network status window.\n\-      \When \^Bnetwork\^B and \^Btarget\^B are specified this switches to that chat window.\n\-      \\n\-      \Nickname and channels can be specified in the \^Btarget\^B parameter.\n\-      \See also: /query (aliased /c /channel) to switch to a target on the current network.\n"+      $(windowDocs `cmdDoc` "focus")     $ ClientCommand cmdFocus tabFocus    , Command       ("c" :| ["channel"])       (simpleToken "focus")-      "\^BParameters:\^B\n\-      \\n\-      \    focuses: Focus name\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    This command sets the current window focus. When\n\-      \    no network is specified, the current network will\n\-      \    be used.\n\-      \\n\-      \    Client:  *\n\-      \    Network: \^_network\^_:\n\-      \    Channel: \^_#channel\^_\n\-      \    Channel: \^_network\^_:\^_#channel\^_\n\-      \    User:    \^_nick\^_\n\-      \    User:    \^_network\^_:\^_nick\^_\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /c fn:#haskell\n\-      \    /c #haskell\n\-      \    /c fn:\n\-      \    /c *:\n\-      \\n\-      \\^BSee also:\^B focus\n"+      $(windowDocs `cmdDoc` "channel")     $ ClientCommand cmdChannel tabChannel    , Command       (pure "clear")       (optionalArg (liftA2 (,) (simpleToken "[network]") (optionalArg (simpleToken "[channel]"))))-      "Clear a window.\n\-      \\n\-      \If no arguments are provided the current window is cleared.\n\-      \If \^Bnetwork\^B is provided the that network window is cleared.\n\-      \If \^Bnetwork\^B and \^Bchannel\^B are provided that chat window is cleared.\n\-      \If \^Bnetwork\^B is provided and \^Bchannel\^B is \^B*\^O all windows for that network are cleared.\n\-      \\n\-      \If a window is cleared and no longer active that window will be removed from the client.\n"+      $(windowDocs `cmdDoc` "clear")     $ ClientCommand cmdClear tabFocus    , Command       (pure "windows")       (optionalArg (simpleToken "[kind]"))-      "Show a list of all windows with an optional argument to limit the kinds of windows listed.\n\-      \\n\-      \\^Bkind\^O: one of \^Bnetworks\^O, \^Bchannels\^O, \^Busers\^O\n\-      \\n"+      $(windowDocs `cmdDoc` "windows")     $ ClientCommand cmdWindows tabWindows    , Command       (pure "splits")       (remainingArg "focuses")-      "\^BParameters:\^B\n\-      \\n\-      \    focuses: List of focus names\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    This command sents the set of focuses that will always\n\-      \    be visible, even when unfocused. When the client is focused\n\-      \    to an active network, the network can be omitted when\n\-      \    specifying a focus. If no focuses are listed, they will\n\-      \    all be cleared.\n\-      \\n\-      \    Client:  *\n\-      \    Network: \^_network\^_:\n\-      \    Channel: \^_#channel\^_\n\-      \    Channel: \^_network\^_:\^_#channel\^_\n\-      \    User:    \^_nick\^_\n\-      \    User:    \^_network\^_:\^_nick\^_\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /splits * fn:#haskell fn:chanserv\n\-      \    /splits #haskell #haskell-lens nickserv\n\-      \    /splits\n\-      \\n\-      \\^BSee also:\^B splits+, splits-\n"+      $(windowDocs `cmdDoc` "splits")     $ ClientCommand cmdSplits tabSplits    , Command       (pure "splits+")       (remainingArg "focuses")-      "Add windows to the splits list. Omit the list of focuses to add the\-      \ current window.\n\-      \\n\-      \\^Bfocuses\^B: space delimited list of focus names.\n\-      \\n\-      \Client:  *\n\-      \Network: \^BNETWORK\^B\n\-      \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\-      \User:    \^BNETWORK\^B:\^BNICK\^B\n\-      \\n\-      \If the network part is omitted, the current network will be used.\n"+      $(windowDocs `cmdDoc` "splits")     $ ClientCommand cmdSplitsAdd tabSplits    , Command       (pure "splits-")       (remainingArg "focuses")-      "Remove windows from the splits list. Omit the list of focuses to\-      \ remove the current window.\n\-      \\n\-      \\^Bfocuses\^B: space delimited list of focus names.\n\-      \\n\-      \Client:  *\n\-      \Network: \^BNETWORK\^B\n\-      \Channel: \^BNETWORK\^B:\^B#CHANNEL\^B\n\-      \User:    \^BNETWORK\^B:\^BNICK\^B\n\-      \\n\-      \If the network part is omitted, the current network will be used.\n"+      $(windowDocs `cmdDoc` "splits")     $ ClientCommand cmdSplitsDel tabActiveSplits    , Command       (pure "ignore")       (remainingArg "masks")-      "\^BParameters:\^B\n\-      \\n\-      \    masks: List of masks\n\-      \\n\-      \\^BDescription:\^B\n\-      \\n\-      \    Toggle the soft-ignore on each of the space-delimited given\n\-      \    nicknames. Ignores can use \^B*\^B (many) and \^B?\^B (one) wildcards.\n\-      \    Masks can be of the form: nick[[!user]@host]\n\-      \    Masks use a case-insensitive comparison.\n\-      \\n\-      \    If no masks are specified the current ignore list is displayed.\n\-      \\n\-      \\^BExamples:\^B\n\-      \\n\-      \    /ignore\n\-      \    /ignore nick1 nick2 nick3\n\-      \    /ignore nick@host\n\-      \    /ignore nick!user@host\n\-      \    /ignore *@host\n\-      \    /ignore *!baduser@*\n"+      $(windowDocs `cmdDoc` "ignore")     $ ClientCommand cmdIgnore tabIgnore    , Command       (pure "grep")       (remainingArg "regular-expression")-      "Set the persistent regular expression.\n\-      \\n\-      \\^BFlags:\^B\n\-      \    -A n Show n messages after match\n\-      \    -B n Show n messages before match\n\-      \    -C n Show n messages before and after match\n\-      \    -F   Use plain-text match instead of regular expression\n\-      \    -i   Case insensitive match\n\-      \    -v   Invert pattern match\n\-      \    -m n Limit results to n matches\n\-      \    --   Stop processing flags\n\-      \\n\-      \Clear the regular expression by calling this without an argument.\n\-      \\n\-      \\^B/grep\^O is case-sensitive.\n"+      $(windowDocs `cmdDoc` "grep")     $ ClientCommand cmdGrep simpleClientTab    , Command       (pure "dump")       (simpleToken "filename")-      "Dump current buffer to file.\n"+      $(windowDocs `cmdDoc` "dump")     $ ClientCommand cmdDump simpleClientTab    , Command       (pure "mentions")       (pure ())-      "Show a list of all message that were highlighted as important.\n\-      \\n\-      \When using \^B/grep\^B the important messages are those matching\n\-      \the regular expression instead.\n"+      $(windowDocs `cmdDoc` "mentions")     $ ClientCommand cmdMentions noClientTab    , Command       (pure "setwindow")       (simpleToken ("hide|show" ++ concatMap ('|':) activityFilterStrings))-      "Set window property.\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"+      $(windowDocs `cmdDoc` "setwindow")     $ ClientCommand cmdSetWindow tabSetWindow    , Command       (pure "setname")       (optionalArg (simpleToken "[letter]"))-      "Set window shortcut letter. If no letter is provided the next available\n\-      \letter will automatically be assigned.\n\-      \\n\-      \Available letters are configured in the 'window-names' configuration setting.\n"+      $(windowDocs `cmdDoc` "setname")     $ ClientCommand cmdSetWindowName noClientTab    ]
src/Client/Commands/ZNC.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings #-}+{-# Language TemplateHaskell, OverloadedStrings #-} {-| Module      : Client.Commands.ZNC Description : ZNC command implementations@@ -9,8 +9,9 @@  module Client.Commands.ZNC (zncCommands) where -import Control.Applicative (asum, liftA2)+import Control.Applicative ((<|>), empty, liftA2) import Client.Commands.Arguments.Spec (optionalArg, remainingArg, simpleToken)+import Client.Commands.Docs (integrationDocs, cmdDoc) import Client.Commands.TabCompletion (noNetworkTab, simpleNetworkTab) import Client.Commands.Types import Client.State.Network (sendMsg)@@ -26,26 +27,13 @@   [ Command       (pure "znc")       (remainingArg "arguments")-      "Send command directly to ZNC.\n\-      \\n\-      \The advantage of this over /msg is that responses are not broadcast to call clients.\n"+      $(integrationDocs `cmdDoc` "znc")     $ NetworkCommand cmdZnc simpleNetworkTab    , Command       (pure "znc-playback")       (optionalArg (liftA2 (,) (simpleToken "[time]") (optionalArg (simpleToken "[date]"))))-      "Request playback from the ZNC 'playback' module.\n\-      \\n\-      \\^Btime\^B determines the time to playback since.\n\-      \\^Bdate\^B determines the date to playback since.\n\-      \\n\-      \When both \^Btime\^B and \^Bdate\^B are omitted, all playback is requested.\n\-      \When both \^Bdate\^B is omitted it is defaulted the most recent date in the past that makes sense.\n\-      \\n\-      \Time format: HOURS:MINUTES (example: 7:00)\n\-      \Date format: YEAR-MONTH-DAY (example: 2016-06-16)\n\-      \\n\-      \Note that the playback module is not installed in ZNC by default!\n"+      $(integrationDocs `cmdDoc` "znc-playback")     $ NetworkCommand cmdZncPlayback noNetworkTab    ]@@ -91,7 +79,8 @@     timeFormats = ["%k:%M:%S","%k:%M"]     dateFormats = ["%F"]     parseFormats formats str =-      asum (map (parseTimeM False defaultTimeLocale ?? str) formats)+      -- asum requires base >= 4.16+      foldr (<|>) empty (map (parseTimeM False defaultTimeLocale ?? str) formats)      successZoned = success . formatTime defaultTimeLocale "%s" 
+ src/Client/Docs.hs view
@@ -0,0 +1,121 @@+{-# Language OverloadedStrings #-}++{-|+Module      : Client.Docs+Description : Compile-time documentation injection+Copyright   : (c) TheDaemoness 2023+License     : ISC+Maintainer  : emertens@gmail.com++This module adds the requisite functions to load and parse+a subset of AsciiDoc and embed it using Template Haskell.+-}+module Client.Docs+  ( Docs+  , loadDoc+  , lookupDoc+  , makeHeader+  ) where++import           Prelude hiding (readFile)++import           Control.Applicative ((<|>))+import qualified Data.Attoparsec.Text as Parse+import           Data.ByteString (readFile)+import           Data.Char (isSpace)+import           Data.HashMap.Strict (HashMap)+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           Data.Text.Encoding (decodeUtf8)+import           Language.Haskell.TH (Exp, Q, runIO)+import           Language.Haskell.TH.Syntax (lift)+import qualified Data.Text.Lazy.Builder as Builder++type Docs = HashMap String LText.Text++data Line+  = Discarded+  | Section Text+  | Subsection Text+  | Contents LText.Text++makeHeader :: LText.Text -> LText.Text+makeHeader header = LText.append "\^B" (LText.append header ":\^B\n")++loadDoc :: (String -> String) -> FilePath -> Q Docs+loadDoc keymod path = runIO (readFile splicePath >>= renderDoc)+  where+    splicePath = "doc/" ++ path ++ ".adoc"+    renderDoc doc = case Parse.parseOnly lineParser $ decodeUtf8 doc of+      Right docs -> return $ buildDocs keymod docs+      Left errorMsg -> fail ("Parser failed on `" ++ splicePath ++ "`: " ++ errorMsg)++lookupDoc :: LText.Text -> String -> Docs -> Q Exp+lookupDoc header name docs =+  case HashMap.lookup name docs of+    Just doc -> lift $ LText.toStrict $ LText.append header doc+    Nothing -> fail failMsg+  where+    failMsg = "No docs for `" ++ name ++ "` (have " ++ show (HashMap.keys docs) ++ ")"++buildDocs :: (String -> String) -> [Line] -> Docs+buildDocs keymod parsedLines = docs+  where+    folded = foldl (addLine keymod) (HashMap.empty, "", LText.empty) parsedLines+    (docs, _, _) = addLine keymod folded (Section "")++data RenderContentsState+  = NormalState+  | CodeStartState+  | CodeEndState+  | CodeBlockState++renderContents :: RenderContentsState -> LText.Text -> LText.Text+renderContents state = Builder.toLazyText . snd . foldl renderContents' (state, mempty) . LText.unpack+  where+    renderContents' (st, text) char = case (st, char) of+       (CodeStartState, '+') -> (CodeBlockState, text <> Builder.fromText "\^_")+       (CodeStartState, _  ) -> (NormalState,    text <> Builder.fromText "\^B" <> Builder.singleton char)+       (CodeEndState,   '`') -> (NormalState,    text <> Builder.fromText "\^_")+       (CodeEndState,   _  ) -> (CodeBlockState, text <> Builder.singleton '+' <> Builder.singleton char)+       (NormalState,    '`') -> (CodeStartState, text)+       (CodeBlockState, '+') -> (CodeEndState,   text)+       (_,              _  ) -> (st,             text <> Builder.singleton char)++addLine :: (String -> String) -> (Docs, Text, LText.Text) -> Line -> (Docs, Text, LText.Text)+addLine _      (docs, section, text)      Discarded    = (docs, section, text)+addLine _      (docs, "", _)              (Section s') = (docs, s', LText.empty)+addLine _      (docs, "", text)           _            = (docs, "", text)+addLine keymod (docs, section, text) line              = case line of+  -- TODO: Keep renderContents state across lines.+  -- Otherwise start in NormalState after each newline.+  Contents text'   -> (docs, section, append' $ renderContents NormalState text')+  Subsection text' -> (docs, section, append' (makeHeader (LText.fromStrict text')))+  Section s'       -> (HashMap.insert (keymod $ Text.unpack section) text docs, s', LText.empty)+  where+    append' = LText.append text++lineParser :: Parse.Parser [Line]+lineParser = Parse.many1' (sectionParser <|> contentsParser) <* Parse.endOfInput+  where+    sectionParser = Parse.char '=' >> (sectionL2Parser <|> sectionL3Parser <|> return Discarded)+      where+        sectionL2Parser = do+          _ <- Parse.string "= "+          name <- Parse.takeWhile1 (not . isSpace)+          eolParser+          return (Section name)+        sectionL3Parser = do+          _ <- Parse.takeWhile1 (== '=')+          Parse.skipWhile (== ' ')+          chars <- Parse.manyTill Parse.anyChar eolParser+          return (Subsection $ Text.pack chars)+    contentsParser = do+      chars <- Parse.manyTill Parse.anyChar eolParser+      return $ Contents $ LText.fromChunks ["  ", Text.pack chars, "\n"]+    eolParser = do+      spaces <- Parse.takeWhile (== ' ')+      _ <- if Text.null spaces then pure '+' else Parse.option '+' (Parse.char '+')+      Parse.endOfLine