Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ Ctrl+C - Exit chat
:help - Show concise manual
:clear - Clear command output
:q, :quit, :exit - Disconnect
Tab - Complete command name; ambiguous prefixes list candidates
(also completes :theme/:lang values and :msg usernames)
Up/Down - Browse command history
ESC - Return to NORMAL mode
```
Expand Down
1 change: 1 addition & 0 deletions docs/QUICKREF.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ COMMANDS (COMMAND mode, prefix with :)
lang [en|zh] show or switch UI language
clear clear output
q / quit / exit disconnect
Tab complete command / argument (lists candidates if ambiguous)

INSERT MODE
/me <action> action message
Expand Down
11 changes: 11 additions & 0 deletions include/command_catalog.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ bool command_catalog_match(const char *line, tnt_command_id_t *id,
const char **args);
bool command_catalog_args_valid(tnt_command_id_t id, const char *args);
const char *command_catalog_suggest(const char *name);

/* Prefix-complete a command name for Tab completion.
*
* Case-insensitively matches `prefix` against canonical command names. An
* empty or NULL prefix matches every command. Up to `max` matching canonical
* names are written to `out` (pointers to static storage). The longest common
* prefix of *all* matches is written to `lcp` (NUL-terminated, truncated to
* `lcp_size`); it is empty when matches share no common prefix. Returns the
* total number of matches (which may exceed `max`). */
size_t command_catalog_complete(const char *prefix, const char **out,
size_t max, char *lcp, size_t lcp_size);
void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos,
ui_lang_t lang);
void command_catalog_append_manual(char *buffer, size_t buf_size, size_t *pos,
Expand Down
1 change: 1 addition & 0 deletions include/i18n.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ typedef enum {
I18N_TITLE_MUTED,
I18N_TITLE_HELP_HINT,
I18N_EMPTY_ROOM,
I18N_WELCOME_GUIDE,
I18N_EMPTY_FILTERED,
I18N_IDLE_TIMEOUT_FORMAT,
I18N_SYSTEM_USERNAME,
Expand Down
6 changes: 6 additions & 0 deletions include/tui.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ void tui_render_input(struct client *client, const char *input);
/* Render only the command input/status line */
void tui_render_command_input(struct client *client);

/* Render the command input/status line followed by a dim completion hint
* (e.g. a space-separated list of candidate commands). The hint is
* truncated to the remaining terminal width. A NULL or empty hint behaves
* like tui_render_command_input(). */
void tui_render_command_hint(struct client *client, const char *hint);

/* Clear the screen */
void tui_clear_screen(struct client *client);

Expand Down
38 changes: 38 additions & 0 deletions packaging/completions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# tntctl shell completions

Tab-completion for the `tntctl` control client: option flags, the stable
subcommands (`health`, `stats`, `users`, `tail`, `dump`, `post`, `help`,
`exit`), `--json` after `users`/`stats`, and the `--host-key-checking` modes.

These complete `tntctl`'s own interface only; the remote host argument is
free-form and is not completed.

## bash

```sh
# user-local
echo 'source /path/to/TNT/packaging/completions/tntctl.bash' >> ~/.bashrc
# or system-wide
sudo cp packaging/completions/tntctl.bash \
/usr/share/bash-completion/completions/tntctl
```

## zsh

```sh
mkdir -p ~/.zsh/completions
cp packaging/completions/_tntctl ~/.zsh/completions/_tntctl
# ensure these are in ~/.zshrc:
# fpath=(~/.zsh/completions $fpath)
# autoload -U compinit && compinit
```

## fish

```sh
cp packaging/completions/tntctl.fish ~/.config/fish/completions/tntctl.fish
```

> Note: this is shell completion for the `tntctl` command line. Inside the
> interactive TNT chat (`ssh -p 2222 host`), COMMAND mode has its own built-in
> Tab completion for `:` commands and their arguments.
37 changes: 37 additions & 0 deletions packaging/completions/_tntctl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#compdef tntctl
# zsh completion for tntctl
#
# Install: place this file (named _tntctl) in a directory on your $fpath,
# e.g. ~/.zsh/completions, and ensure `autoload -U compinit && compinit`
# runs in ~/.zshrc.

_tntctl() {
local -a opts commands
local prev
opts=(-p --port -l --login --host-key-checking --known-hosts -h --help -V --version)
commands=(health stats users tail dump post help exit)

prev=${words[CURRENT-1]}
case $prev in
--host-key-checking)
compadd yes accept-new no
return ;;
--known-hosts)
_files
return ;;
-p|--port|-l|--login)
return ;;
esac

if [[ ${words[CURRENT]} == -* ]]; then
if (( ${words[(I)users]} || ${words[(I)stats]} )); then
compadd -- $opts --json
else
compadd -- $opts
fi
else
compadd -- $commands
fi
}

_tntctl "$@"
51 changes: 51 additions & 0 deletions packaging/completions/tntctl.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# bash completion for tntctl
#
# Install: source this file from ~/.bashrc, or copy it to
# /usr/share/bash-completion/completions/tntctl
# (or /etc/bash_completion.d/tntctl).

_tntctl() {
local cur prev opts commands cmd i
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="-p --port -l --login --host-key-checking --known-hosts -h --help -V --version"
commands="health stats users tail dump post help exit"

case "$prev" in
--host-key-checking)
COMPREPLY=( $(compgen -W "yes accept-new no" -- "$cur") )
return ;;
--known-hosts)
COMPREPLY=( $(compgen -f -- "$cur") )
return ;;
-p|--port|-l|--login)
return ;; # free-form value
esac

# Detect a subcommand already on the line.
cmd=""
for ((i = 1; i < COMP_CWORD; i++)); do
case "${COMP_WORDS[i]}" in
health|stats|users|tail|dump|post|help|exit)
cmd="${COMP_WORDS[i]}"; break ;;
esac
done

if [[ "$cur" == -* ]]; then
local extra=""
case "$cmd" in
users|stats) extra="--json" ;;
esac
COMPREPLY=( $(compgen -W "$opts $extra" -- "$cur") )
return
fi

# Positional args after a subcommand are free-form.
if [[ -n "$cmd" ]]; then
return
fi

# Otherwise offer the known subcommands (typed after the host).
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
}
complete -F _tntctl tntctl
22 changes: 22 additions & 0 deletions packaging/completions/tntctl.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# fish completion for tntctl
#
# Install: copy this file to ~/.config/fish/completions/tntctl.fish

# Options
complete -c tntctl -s p -l port -d 'Server port' -x
complete -c tntctl -s l -l login -d 'Login user' -x
complete -c tntctl -l host-key-checking -d 'SSH host key checking' -xa 'yes accept-new no'
complete -c tntctl -l known-hosts -d 'known_hosts file' -r
complete -c tntctl -s h -l help -d 'Show help'
complete -c tntctl -s V -l version -d 'Show version'
complete -c tntctl -l json -d 'JSON output' -n '__fish_seen_subcommand_from users stats'

# Subcommands (typed after the host)
complete -c tntctl -f -a health -d 'Print service health'
complete -c tntctl -f -a stats -d 'Print room statistics'
complete -c tntctl -f -a users -d 'List online users'
complete -c tntctl -f -a tail -d 'Print recent messages'
complete -c tntctl -f -a dump -d 'Export persisted messages'
complete -c tntctl -f -a post -d 'Post a message'
complete -c tntctl -f -a help -d 'Show exec help'
complete -c tntctl -f -a exit -d 'Exit successfully'
46 changes: 46 additions & 0 deletions src/command_catalog.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "i18n.h"

#include <string.h>
#include <strings.h>

typedef struct {
tnt_command_spec_t spec;
Expand Down Expand Up @@ -285,6 +286,51 @@ const char *command_catalog_suggest(const char *name) {
return best_distance <= 2 ? best : NULL;
}

size_t command_catalog_complete(const char *prefix, const char **out,
size_t max, char *lcp, size_t lcp_size) {
size_t count = 0;
size_t plen;

if (lcp && lcp_size > 0) {
lcp[0] = '\0';
}
if (!prefix) {
prefix = "";
}
plen = strlen(prefix);

for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
const char *name = entries[i].spec.canonical;

if (!name) {
continue;
}
if (plen > 0 && strncasecmp(name, prefix, plen) != 0) {
continue;
}

if (out && count < max) {
out[count] = name;
}

if (lcp && lcp_size > 0) {
if (count == 0) {
snprintf(lcp, lcp_size, "%s", name);
} else {
size_t k = 0;
while (lcp[k] && name[k] && lcp[k] == name[k]) {
k++;
}
lcp[k] = '\0';
}
}

count++;
}

return count;
}

void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos,
ui_lang_t lang) {
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
Expand Down
8 changes: 6 additions & 2 deletions src/i18n_text.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = {
"TNT %s - SSH 匿名聊天室\r\n\r\n"
),
[I18N_INSERT_HINT_WIDE] = I18N_STRING(
"Enter send · Esc NORMAL",
"Enter 发送 · Esc NORMAL"
"Enter send · : commands · :help guide · Esc NORMAL",
"Enter 发送 · : 命令 · :help 指引 · Esc NORMAL"
),
[I18N_INSERT_HINT_NARROW] = I18N_STRING(
"Enter · Esc",
Expand Down Expand Up @@ -85,6 +85,10 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = {
"No messages yet",
"暂无消息"
),
[I18N_WELCOME_GUIDE] = I18N_STRING(
"i type · Enter send · : commands · :help guide",
"i 输入 · Enter 发送 · : 命令 · :help 指引"
),
[I18N_EMPTY_FILTERED] = I18N_STRING(
"No visible messages",
"暂无可见消息"
Expand Down
Loading
Loading