feat: Add help command and help definitions
Add support for the `/help` command, which dynamically builds a list of help commands based on the `help()` procs defined for each command type, ie `Login`. The `help` proc returns a type of `HelpText`, which contains all the information necessary to render help information for that command. It contains the command, parameters (each parameters contains a name and description), aliases, and a description. The help text is generated (at compile time) by a macro called `buildCommandHelp()`. Once the `/help` command is executed, the output of `buildCommandHelp()` is passed to the task runner so that a `HelpResult` event can be created and the help text can be displayed on screen. Because the text is generated at compile time, it is not necessary to pass this text through the task runner, and is displayed directly on screen in the main thread. ## NOTES 1. The help text is quite long, especially given there are blank spaces in between each command. This would require the chat client to be able to scroll unless it is sufficiently tall. 2. I am not married to the display output at all, even after experimenting with a few different layouts. There is some leftover rendering code in the `HelpResult` action which allows for the the command description to be written on the same line as the command name and keep the description spaced in line with all other descriptions, but it didn’t appear as readable as the format that I settled on in the end. I left it in there in case we wanted to change up how help is rendered.
This commit is contained in:
parent
3d0137e865
commit
b6b1424116
|
@ -48,6 +48,9 @@ proc connect*(self: ChatClient, username: string) {.async.} =
|
|||
proc disconnect*(self: ChatClient) {.async.} =
|
||||
asyncSpawn stopWakuChat2(self.taskRunner, status)
|
||||
|
||||
proc generateMultiAccount*(self: ChatClient, password: string) {.async.} =
|
||||
asyncSpawn generateMultiAccount(self.taskRunner, status, password)
|
||||
|
||||
proc listAccounts*(self: ChatClient) {.async.} =
|
||||
asyncSpawn listAccounts(self.taskRunner, status)
|
||||
|
||||
|
@ -57,8 +60,5 @@ proc login*(self: ChatClient, account: int, password: string) {.async.} =
|
|||
proc logout*(self: ChatClient) {.async.} =
|
||||
discard
|
||||
|
||||
proc generateMultiAccount*(self: ChatClient, password: string) {.async.} =
|
||||
asyncSpawn generateMultiAccount(self.taskRunner, status, password)
|
||||
|
||||
proc sendMessage*(self: ChatClient, message: string) {.async.} =
|
||||
asyncSpawn publishWakuChat2(self.taskRunner, status, message)
|
||||
|
|
|
@ -17,6 +17,8 @@ type
|
|||
|
||||
EventChannel* = AsyncChannel[ThreadSafeString]
|
||||
|
||||
# TODO: alphabetise ChatClient above HelpText -- didn't want to interfere
|
||||
# with ongoing work
|
||||
ChatClient* = ref object
|
||||
account*: Account
|
||||
chatConfig*: ChatConfig
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import # std libs
|
||||
std/strformat
|
||||
std/[strformat, strutils]
|
||||
|
||||
import # chat libs
|
||||
./parser
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import # std libs
|
||||
std/strutils
|
||||
std/[sequtils, strformat, strutils, sugar]
|
||||
|
||||
import # chat libs
|
||||
./screen, ./tasks
|
||||
./common, ./macros, ./screen, ./tasks
|
||||
|
||||
export screen, strutils, tasks
|
||||
export common, screen, strutils, tasks
|
||||
|
||||
logScope:
|
||||
topics = "chat tui"
|
||||
|
@ -27,6 +27,10 @@ logScope:
|
|||
|
||||
# Connect ----------------------------------------------------------------------
|
||||
|
||||
proc help*(T: type Connect): HelpText =
|
||||
let command = "connect"
|
||||
HelpText(command: command, description: "Connects to the waku network.")
|
||||
|
||||
proc new*(T: type Connect, args: varargs[string]): T {.raises: [].} =
|
||||
T()
|
||||
|
||||
|
@ -42,6 +46,13 @@ proc command*(self: ChatTUI, command: Connect) {.async, gcsafe, nimcall.} =
|
|||
|
||||
# CreateAccount ----------------------------------------------------------------
|
||||
|
||||
proc help*(T: type CreateAccount): HelpText =
|
||||
let command = "createaccount"
|
||||
HelpText(command: command, aliases: aliased[command], parameters: @[
|
||||
CommandParameter(name: "password", description: "Password for the new " &
|
||||
"account.")
|
||||
], description: "Creates a new Status account.")
|
||||
|
||||
proc new*(T: type CreateAccount, args: varargs[string]): T =
|
||||
T(password: args[0])
|
||||
|
||||
|
@ -59,6 +70,10 @@ proc command*(self: ChatTUI, command: CreateAccount) {.async, gcsafe,
|
|||
|
||||
# Disconnect -------------------------------------------------------------------
|
||||
|
||||
proc help*(T: type Disconnect): HelpText =
|
||||
let command = "disconnect"
|
||||
HelpText(command: command, description: "Disconnects from the waku network.")
|
||||
|
||||
proc new*(T: type Disconnect, args: varargs[string]): T =
|
||||
T()
|
||||
|
||||
|
@ -71,20 +86,14 @@ proc command*(self: ChatTUI, command: Disconnect) {.async, gcsafe, nimcall.} =
|
|||
else:
|
||||
self.wprintFormatError(epochTime().int64, "client is not online.")
|
||||
|
||||
# Help -------------------------------------------------------------------------
|
||||
|
||||
proc new*(T: type Help, args: varargs[string]): T =
|
||||
T(command: args[0])
|
||||
|
||||
proc split*(T: type Help, argsRaw: string): seq[string] =
|
||||
@[argsRaw.split(" ")[0]]
|
||||
|
||||
proc command*(self: ChatTUI, command: Help) {.async, gcsafe, nimcall.} =
|
||||
let command = command.command
|
||||
discard
|
||||
|
||||
# ListAccounts -----------------------------------------------------------------
|
||||
|
||||
proc help*(T: type ListAccounts): HelpText =
|
||||
let command = "listaccounts"
|
||||
HelpText(command: command, aliases: aliased[command], description: "Lists " &
|
||||
"all existing Status accounts.")
|
||||
|
||||
proc new*(T: type ListAccounts, args: varargs[string]): T =
|
||||
T()
|
||||
|
||||
|
@ -96,6 +105,14 @@ proc command*(self: ChatTUI, command: ListAccounts) {.async, gcsafe, nimcall.} =
|
|||
|
||||
# Login ------------------------------------------------------------------------
|
||||
|
||||
proc help*(T: type Login): HelpText =
|
||||
let command = "login"
|
||||
HelpText(command: command, parameters: @[
|
||||
CommandParameter(name: "index", description: "Index of existing account, " &
|
||||
"which can be retrieved using the `/list` command."),
|
||||
CommandParameter(name: "password", description: "Account password.")
|
||||
], description: "Logs in to the Status account.")
|
||||
|
||||
proc new*(T: type Login, args: varargs[string]): T {.raises: [].} =
|
||||
T(account: args[0], password: args[1])
|
||||
|
||||
|
@ -130,6 +147,11 @@ proc command*(self: ChatTUI, command: Login) {.async, gcsafe, nimcall.} =
|
|||
|
||||
# Logout -----------------------------------------------------------------------
|
||||
|
||||
proc help*(T: type Logout): HelpText =
|
||||
let command = "logout"
|
||||
HelpText(command: command, description: "Logs out of the currently logged " &
|
||||
"in Status account.")
|
||||
|
||||
proc new*(T: type Logout, args: varargs[string]): T =
|
||||
T()
|
||||
|
||||
|
@ -141,6 +163,10 @@ proc command*(self: ChatTUI, command: Logout) {.async, gcsafe, nimcall.} =
|
|||
|
||||
# Quit -------------------------------------------------------------------------
|
||||
|
||||
proc help*(T: type Quit): HelpText =
|
||||
let command = "quit"
|
||||
HelpText(command: command, description: "Quits the chat client.")
|
||||
|
||||
proc new*(T: type Quit, args: varargs[string]): T =
|
||||
T()
|
||||
|
||||
|
@ -152,6 +178,16 @@ proc command*(self: ChatTUI, command: Quit) {.async, gcsafe, nimcall.} =
|
|||
|
||||
# SendMessage ------------------------------------------------------------------
|
||||
|
||||
proc help*(T: type SendMessage): HelpText =
|
||||
let command = ""
|
||||
HelpText(command: command, aliases: aliased[command], parameters: @[
|
||||
CommandParameter(name: "message", description: "Message to send on the " &
|
||||
"network. Max length ???")
|
||||
], description: "Sends a message on the waku network. By default, no " &
|
||||
"command is needed. Text entered that is not preceded by a command will " &
|
||||
"be interpreted as a send command, ie typing `hello` will be interpreted " &
|
||||
"as `/send hello`.")
|
||||
|
||||
proc new*(T: type SendMessage, args: varargs[string]): T =
|
||||
T(message: args[0])
|
||||
|
||||
|
@ -164,3 +200,99 @@ proc command*(self: ChatTUI, command: SendMessage) {.async, gcsafe, nimcall.} =
|
|||
"client is not online, cannot send message.")
|
||||
else:
|
||||
asyncSpawn self.client.sendMessage(command.message)
|
||||
|
||||
# Help -------------------------------------------------------------------------
|
||||
# Note: although "Help" is not alphabetically last, we need to keep this below
|
||||
# all other `help()` definitions so that they are available to the
|
||||
# `buildCommandHelp()` macro. Alternatively, we could use forward declarations
|
||||
# at the top of the file, but this introduces an additional update for a
|
||||
# developer implementing a new command.
|
||||
|
||||
proc help*(T: type Help): HelpText =
|
||||
let command = "help"
|
||||
HelpText(command: command, aliases: aliased[command], description:
|
||||
"Show this help text.")
|
||||
|
||||
proc new*(T: type Help, args: varargs[string]): T =
|
||||
T(command: args[0])
|
||||
|
||||
proc split*(T: type Help, argsRaw: string): seq[string] =
|
||||
@[argsRaw.split(" ")[0]]
|
||||
|
||||
proc command*(self: ChatTUI, command: Help) {.async, gcsafe, nimcall.} =
|
||||
let
|
||||
command = command.command
|
||||
helpTexts = buildCommandHelp()
|
||||
timestamp = epochTime().int64
|
||||
|
||||
# display on screen
|
||||
trace "TUI showing cli help", helpTexts=(%helpTexts)
|
||||
|
||||
proc forDisplay(help: HelpText): (string, string) =
|
||||
let
|
||||
command = help.command
|
||||
hasAliases = help.aliases.len > 0
|
||||
aliasPrefix = if hasAliases: ", /" else: ""
|
||||
aliases = aliasPrefix & help.aliases.join(", /")
|
||||
(command, aliases)
|
||||
|
||||
proc commandLength(help: HelpText): int =
|
||||
let (command, aliases) = help.forDisplay
|
||||
result = command.len + aliases.len
|
||||
|
||||
proc longest(helps: seq[HelpText]): int =
|
||||
result = 0
|
||||
for help in helps:
|
||||
let length = help.commandLength
|
||||
if length > result:
|
||||
result = length
|
||||
|
||||
proc longest(params: seq[CommandParameter]): int =
|
||||
result = 0
|
||||
for param in params:
|
||||
let length = param.name.len
|
||||
if length > result:
|
||||
result = length
|
||||
|
||||
if self.outputReady:
|
||||
self.printResult("Available commands:", timestamp)
|
||||
self.printResult("===================", timestamp)
|
||||
self.printResult("", timestamp) # print blank line
|
||||
|
||||
let
|
||||
spacing = 2
|
||||
totalSpace = helpTexts.longest + spacing
|
||||
|
||||
for helpText in helpTexts:
|
||||
let
|
||||
(command, aliases) = helpText.forDisplay
|
||||
desc = helpText.description
|
||||
spaces = totalSpace - helpText.commandLength
|
||||
paramsJoined = helpText.parameters.map(p => p.name).join("> <")
|
||||
hasParams = helpText.parameters.len > 0
|
||||
paramsList = if hasParams: fmt" <{paramsJoined}>" else: ""
|
||||
finalText = fmt"{spacing.indent()}/{command}{aliases}{paramsList}"
|
||||
|
||||
self.printResult(finalText.replace("/, ", ""), timestamp)
|
||||
self.printResult(fmt"{(spacing * 2).indent}{desc}", timestamp)
|
||||
|
||||
let
|
||||
params = helpText.parameters
|
||||
totalParamSpace = params.longest + spacing
|
||||
|
||||
if params.len > 0:
|
||||
self.printResult(fmt"{(spacing * 2).indent}Parameters:", timestamp)
|
||||
|
||||
for param in params:
|
||||
let
|
||||
name = param.name
|
||||
paramSpaces = totalParamSpace - name.len
|
||||
desc = param.description
|
||||
self.printResult(
|
||||
fmt"{(spacing * 3).indent()}<{name}>{paramSpaces.indent}{desc}", timestamp)
|
||||
|
||||
# print a blank line
|
||||
self.printResult("", timestamp)
|
||||
|
||||
trace "Sending help texts to the client", help=(%helpTexts)
|
||||
|
||||
|
|
|
@ -43,6 +43,10 @@ type
|
|||
|
||||
Connect* = ref object of Command
|
||||
|
||||
CommandParameter* = ref object of RootObj
|
||||
name*: string
|
||||
description*: string
|
||||
|
||||
CreateAccount* = ref object of Command
|
||||
password*: string
|
||||
|
||||
|
@ -51,6 +55,12 @@ type
|
|||
Help* = ref object of Command
|
||||
command*: string
|
||||
|
||||
HelpText* = ref object of RootObj
|
||||
command*: string
|
||||
parameters*: seq[CommandParameter]
|
||||
aliases*: seq[string]
|
||||
description*: string
|
||||
|
||||
ListAccounts* = ref object of Command
|
||||
|
||||
Login* = ref object of Command
|
||||
|
|
|
@ -2,7 +2,7 @@ import # std libs
|
|||
std/macros
|
||||
|
||||
import # chat libs
|
||||
./common
|
||||
./common, ../client/common as client_common
|
||||
|
||||
export common
|
||||
|
||||
|
@ -103,3 +103,18 @@ macro commandSplitCases*(): untyped =
|
|||
casenode.add(ofbranch)
|
||||
|
||||
result.add(casenode)
|
||||
|
||||
macro buildCommandHelp*(): untyped =
|
||||
result = newStmtList()
|
||||
|
||||
let helpId = ident("_help_")
|
||||
result.add quote do:
|
||||
var `helpId`: seq[HelpText] = @[]
|
||||
|
||||
for command in commands:
|
||||
let
|
||||
commandType = ident(command)
|
||||
result.add(quote do: `helpId`.add(`commandType`.help()))
|
||||
|
||||
result.add quote do:
|
||||
`helpId`
|
||||
|
|
Loading…
Reference in New Issue