Add /me, /slap, /q, /topic, tab completion, bold events
- /me <action> sends CTCP ACTION - /slap <nick> slaps with a large trout - /q <nick> sets query target, /q clears it - /topic to view/set channel topic - Tab completion for nicks (cycling, : suffix at line start) - Nick list tracking via NAMES/JOIN/PART/QUIT/NICK - Bold formatting for joins, parts, quits, mode changes - Incoming CTCP ACTION displayed as * nick action - Updated README with all commands and features
This commit is contained in:
@@ -8,8 +8,10 @@ A lightweight terminal IRC client written in C with automatic charset conversion
|
|||||||
- **Window levels** — isolated windows with independent 500-line scrollback:
|
- **Window levels** — isolated windows with independent 500-line scrollback:
|
||||||
- Window 1: Status and private messages
|
- Window 1: Status and private messages
|
||||||
- Windows 2–9: Channels
|
- Windows 2–9: Channels
|
||||||
- **Status bar** — shows current window, channel, nick prefix (@/+), and channel modes
|
- **Status bar** — shows current window, channel, nick prefix (@/+), channel modes, and activity indicator
|
||||||
- **UTF-8 terminal support** — full multi-byte input editing
|
- **UTF-8 terminal support** — full multi-byte input editing
|
||||||
|
- **Tab completion** — nick completion with cycling (`: ` suffix at line start, space mid-line)
|
||||||
|
- **Query mode** — `/q nick` to set a default PM target
|
||||||
- **CTCP VERSION** reply with OS info
|
- **CTCP VERSION** reply with OS info
|
||||||
- **SIGWINCH** handling (terminal resize)
|
- **SIGWINCH** handling (terminal resize)
|
||||||
- **Ident** — works with system identd on port 113
|
- **Ident** — works with system identd on port 113
|
||||||
@@ -37,6 +39,7 @@ Port defaults to 6667.
|
|||||||
| Key | Action |
|
| Key | Action |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| ESC+1–9 | Switch window |
|
| ESC+1–9 | Switch window |
|
||||||
|
| Tab | Nick completion (cycle with repeated Tab) |
|
||||||
| Ctrl-A | Beginning of line |
|
| Ctrl-A | Beginning of line |
|
||||||
| Ctrl-E | End of line |
|
| Ctrl-E | End of line |
|
||||||
| Ctrl-U | Kill to beginning (yank buffer) |
|
| Ctrl-U | Kill to beginning (yank buffer) |
|
||||||
@@ -54,15 +57,21 @@ Port defaults to 6667.
|
|||||||
| `/join #channel` | Join channel (assigned to current window) |
|
| `/join #channel` | Join channel (assigned to current window) |
|
||||||
| `/part [#channel]` | Part channel (defaults to current) |
|
| `/part [#channel]` | Part channel (defaults to current) |
|
||||||
| `/msg <target> <text>` | Send private message |
|
| `/msg <target> <text>` | Send private message |
|
||||||
|
| `/q <nick>` | Set query target (type text to send to them) |
|
||||||
|
| `/q` | Clear query target |
|
||||||
|
| `/me <action>` | Send action to channel/query |
|
||||||
|
| `/slap <nick>` | Slap with a large trout |
|
||||||
| `/nick <newnick>` | Change nickname |
|
| `/nick <newnick>` | Change nickname |
|
||||||
| `/mode <target> <modes>` | Set mode |
|
| `/mode <target> <modes>` | Set mode |
|
||||||
|
| `/topic [#channel]` | View topic |
|
||||||
|
| `/topic #channel <text>` | Set topic |
|
||||||
| `/names [#channel]` | List users in channel |
|
| `/names [#channel]` | List users in channel |
|
||||||
| `/whois <nick>` | WHOIS query |
|
| `/whois <nick>` | WHOIS query |
|
||||||
| `/wii <nick>` | Extended WHOIS (queries remote server) |
|
| `/wii <nick>` | Extended WHOIS (queries remote server) |
|
||||||
| `/quit [reason]` | Quit (default: "See you later") |
|
| `/quit [reason]` | Quit (default: "See you later") |
|
||||||
| `/raw <line>` | Send raw IRC command |
|
| `/raw <line>` | Send raw IRC command |
|
||||||
|
|
||||||
Typing text without a `/` prefix sends to the channel on the current window.
|
Typing text without a `/` prefix sends to the channel on the current window (or query target on window 1).
|
||||||
|
|
||||||
## Window Workflow
|
## Window Workflow
|
||||||
|
|
||||||
|
|||||||
@@ -61,13 +61,20 @@ static struct {
|
|||||||
int head; /* next write position (circular) */
|
int head; /* next write position (circular) */
|
||||||
} win_buf[WL_MAX];
|
} win_buf[WL_MAX];
|
||||||
|
|
||||||
/* Per-channel window state */
|
/* Per-channel nick list */
|
||||||
|
#define MAX_NICKS 256
|
||||||
|
#define NICK_LEN 32
|
||||||
static struct {
|
static struct {
|
||||||
char name[128]; /* channel name */
|
char name[128]; /* channel name */
|
||||||
char modes[64]; /* channel modes (e.g. "+nt") */
|
char modes[64]; /* channel modes (e.g. "+nt") */
|
||||||
char my_prefix; /* '@', '+', or '\0' */
|
char my_prefix; /* '@', '+', or '\0' */
|
||||||
|
char nicks[MAX_NICKS][NICK_LEN];
|
||||||
|
int nick_count;
|
||||||
} win_chans[MAX_CHAN_WINS];
|
} win_chans[MAX_CHAN_WINS];
|
||||||
|
|
||||||
|
/* Query target (for /q) */
|
||||||
|
static char query_target[64] = "";
|
||||||
|
|
||||||
/* Get the active channel for current window, or "" */
|
/* Get the active channel for current window, or "" */
|
||||||
static const char *current_channel(void)
|
static const char *current_channel(void)
|
||||||
{
|
{
|
||||||
@@ -306,6 +313,48 @@ static int irc_connect(const char *host, const char *port)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Find window index for a channel, or -1 */
|
/* Find window index for a channel, or -1 */
|
||||||
|
/* Nick list management */
|
||||||
|
static void nicklist_add(int idx, const char *n)
|
||||||
|
{
|
||||||
|
if (idx < 0 || idx >= MAX_CHAN_WINS) return;
|
||||||
|
/* Skip prefix chars */
|
||||||
|
if (*n == '@' || *n == '+' || *n == '%') n++;
|
||||||
|
if (!*n) return;
|
||||||
|
/* Check if already present */
|
||||||
|
for (int i = 0; i < win_chans[idx].nick_count; i++)
|
||||||
|
if (strcasecmp(win_chans[idx].nicks[i], n) == 0) return;
|
||||||
|
if (win_chans[idx].nick_count < MAX_NICKS)
|
||||||
|
snprintf(win_chans[idx].nicks[win_chans[idx].nick_count++],
|
||||||
|
NICK_LEN, "%s", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nicklist_remove(int idx, const char *n)
|
||||||
|
{
|
||||||
|
if (idx < 0 || idx >= MAX_CHAN_WINS) return;
|
||||||
|
for (int i = 0; i < win_chans[idx].nick_count; i++) {
|
||||||
|
if (strcasecmp(win_chans[idx].nicks[i], n) == 0) {
|
||||||
|
win_chans[idx].nick_count--;
|
||||||
|
if (i < win_chans[idx].nick_count)
|
||||||
|
memcpy(win_chans[idx].nicks[i],
|
||||||
|
win_chans[idx].nicks[win_chans[idx].nick_count],
|
||||||
|
NICK_LEN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nicklist_rename(const char *old, const char *new_nick)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < MAX_CHAN_WINS; i++) {
|
||||||
|
for (int j = 0; j < win_chans[i].nick_count; j++) {
|
||||||
|
if (strcasecmp(win_chans[i].nicks[j], old) == 0) {
|
||||||
|
snprintf(win_chans[i].nicks[j], NICK_LEN, "%s", new_nick);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int chan_win_idx(const char *chan)
|
static int chan_win_idx(const char *chan)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
@@ -517,9 +566,22 @@ static void handle_line(char *line)
|
|||||||
" :: This space available for rent\x01",
|
" :: This space available for rent\x01",
|
||||||
sender, ut.sysname,
|
sender, ut.sysname,
|
||||||
ut.release, ut.machine);
|
ut.release, ut.machine);
|
||||||
|
wprintf(WL_STATUS, "* CTCP VERSION from %s\n", sender);
|
||||||
|
} else if (strncmp(text, "\x01" "ACTION ", 8) == 0) {
|
||||||
|
/* /me action */
|
||||||
|
char *action = text + 8;
|
||||||
|
char *end = strchr(action, '\x01');
|
||||||
|
if (end) *end = '\0';
|
||||||
|
if (strcasecmp(target, nick) == 0) {
|
||||||
|
wprintf(WL_MSG, "* %s %s\n", sender, action);
|
||||||
|
} else {
|
||||||
|
int lvl = chan_to_level(target);
|
||||||
|
wprintf(lvl, "* %s %s\n", sender, action);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
wprintf(WL_STATUS, "* CTCP from %s: %s\n",
|
wprintf(WL_STATUS, "* CTCP from %s: %s\n",
|
||||||
sender, text + 1);
|
sender, text + 1);
|
||||||
|
}
|
||||||
} else if (strcasecmp(target, nick) == 0) {
|
} else if (strcasecmp(target, nick) == 0) {
|
||||||
wprintf(WL_MSG, "<%s> %s\n", sender, text);
|
wprintf(WL_MSG, "<%s> %s\n", sender, text);
|
||||||
} else {
|
} else {
|
||||||
@@ -536,7 +598,8 @@ static void handle_line(char *line)
|
|||||||
char *chan = params;
|
char *chan = params;
|
||||||
if (chan && chan[0] == ':') chan++;
|
if (chan && chan[0] == ':') chan++;
|
||||||
int lvl = chan ? chan_to_level(chan) : WL_STATUS;
|
int lvl = chan ? chan_to_level(chan) : WL_STATUS;
|
||||||
wprintf(lvl, "* %s has joined %s\n", sender, chan ? chan : "");
|
if (chan) nicklist_add(chan_win_idx(chan), sender);
|
||||||
|
wprintf(lvl, "\033[1m* %s has joined %s\033[0m\n", sender, chan ? chan : "");
|
||||||
draw_statusbar();
|
draw_statusbar();
|
||||||
} else if (strcmp(cmd, "PART") == 0) {
|
} else if (strcmp(cmd, "PART") == 0) {
|
||||||
char *chan = params;
|
char *chan = params;
|
||||||
@@ -550,7 +613,8 @@ static void handle_line(char *line)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
int lvl = chan ? chan_to_level(chan) : WL_STATUS;
|
int lvl = chan ? chan_to_level(chan) : WL_STATUS;
|
||||||
wprintf(lvl, "* %s has left %s (%s)\n", sender,
|
if (chan) nicklist_remove(chan_win_idx(chan), sender);
|
||||||
|
wprintf(lvl, "\033[1m* %s has left %s (%s)\033[0m\n", sender,
|
||||||
chan ? chan : "", reason ? reason : "");
|
chan ? chan : "", reason ? reason : "");
|
||||||
/* If we parted, clear the window */
|
/* If we parted, clear the window */
|
||||||
if (strcasecmp(sender, nick) == 0 && chan) {
|
if (strcasecmp(sender, nick) == 0 && chan) {
|
||||||
@@ -559,19 +623,31 @@ static void handle_line(char *line)
|
|||||||
win_chans[idx].name[0] = '\0';
|
win_chans[idx].name[0] = '\0';
|
||||||
win_chans[idx].modes[0] = '\0';
|
win_chans[idx].modes[0] = '\0';
|
||||||
win_chans[idx].my_prefix = '\0';
|
win_chans[idx].my_prefix = '\0';
|
||||||
|
win_chans[idx].nick_count = 0;
|
||||||
draw_statusbar();
|
draw_statusbar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (strcmp(cmd, "QUIT") == 0) {
|
} else if (strcmp(cmd, "QUIT") == 0) {
|
||||||
char *reason = params;
|
char *reason = params;
|
||||||
if (reason && reason[0] == ':') reason++;
|
if (reason && reason[0] == ':') reason++;
|
||||||
wprintf(WL_STATUS, "* %s has quit (%s)\n",
|
/* Show quit in all active channel windows and remove nick */
|
||||||
|
for (int i = 0; i < MAX_CHAN_WINS; i++) {
|
||||||
|
if (win_chans[i].name[0]) {
|
||||||
|
nicklist_remove(i, sender);
|
||||||
|
wprintf(WL_CHAN + i, "\033[1m* %s has quit (%s)\033[0m\n",
|
||||||
sender, reason ? reason : "");
|
sender, reason ? reason : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (strcmp(cmd, "NICK") == 0) {
|
} else if (strcmp(cmd, "NICK") == 0) {
|
||||||
char *newnick = params;
|
char *newnick = params;
|
||||||
if (newnick && newnick[0] == ':') newnick++;
|
if (newnick && newnick[0] == ':') newnick++;
|
||||||
wprintf(WL_STATUS, "* %s is now known as %s\n", sender,
|
/* Update nick lists and show in all active channel windows */
|
||||||
newnick ? newnick : "");
|
if (newnick) nicklist_rename(sender, newnick);
|
||||||
|
for (int i = 0; i < MAX_CHAN_WINS; i++) {
|
||||||
|
if (win_chans[i].name[0])
|
||||||
|
wprintf(WL_CHAN + i, "* %s is now known as %s\n",
|
||||||
|
sender, newnick ? newnick : "");
|
||||||
|
}
|
||||||
if (strcasecmp(sender, nick) == 0 && newnick) {
|
if (strcasecmp(sender, nick) == 0 && newnick) {
|
||||||
snprintf(nick, sizeof(nick), "%s", newnick);
|
snprintf(nick, sizeof(nick), "%s", newnick);
|
||||||
draw_statusbar();
|
draw_statusbar();
|
||||||
@@ -588,12 +664,12 @@ static void handle_line(char *line)
|
|||||||
if (target[0] == '#' || target[0] == '&') {
|
if (target[0] == '#' || target[0] == '&') {
|
||||||
update_my_prefix(target, modestr, modeargs);
|
update_my_prefix(target, modestr, modeargs);
|
||||||
int lvl = chan_to_level(target);
|
int lvl = chan_to_level(target);
|
||||||
wprintf(lvl, "* %s sets mode %s %s\n",
|
wprintf(lvl, "\033[1m* %s sets mode %s %s\033[0m\n",
|
||||||
sender[0] ? sender : "*", modestr,
|
sender[0] ? sender : "*", modestr,
|
||||||
modeargs ? modeargs : "");
|
modeargs ? modeargs : "");
|
||||||
draw_statusbar();
|
draw_statusbar();
|
||||||
} else {
|
} else {
|
||||||
wprintf(WL_STATUS, "* %s sets mode %s\n",
|
wprintf(WL_STATUS, "\033[1m* %s sets mode %s\033[0m\n",
|
||||||
sender[0] ? sender : "*", modestr);
|
sender[0] ? sender : "*", modestr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,6 +697,33 @@ static void handle_line(char *line)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (strcmp(cmd, "332") == 0 && params) {
|
||||||
|
/* RPL_TOPIC: <nick> <channel> :<topic> */
|
||||||
|
char *p = params;
|
||||||
|
char *sp = strchr(p, ' ');
|
||||||
|
if (sp) {
|
||||||
|
p = sp + 1; /* skip our nick */
|
||||||
|
char *chan = p;
|
||||||
|
char *topic = strchr(p, ':');
|
||||||
|
if (topic) {
|
||||||
|
sp = strchr(chan, ' ');
|
||||||
|
if (sp) *sp = '\0';
|
||||||
|
topic++;
|
||||||
|
int lvl = chan_to_level(chan);
|
||||||
|
wprintf(lvl, "* Topic for %s: %s\n", chan, topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp(cmd, "TOPIC") == 0 && params) {
|
||||||
|
/* :nick TOPIC #channel :new topic */
|
||||||
|
char *chan = params;
|
||||||
|
char *topic = strchr(params, ':');
|
||||||
|
if (topic) {
|
||||||
|
char *sp = strchr(chan, ' ');
|
||||||
|
if (sp) *sp = '\0';
|
||||||
|
topic++;
|
||||||
|
int lvl = chan_to_level(chan);
|
||||||
|
wprintf(lvl, "* %s changed topic to: %s\n", sender, topic);
|
||||||
|
}
|
||||||
} else if (strcmp(cmd, "353") == 0 && params) {
|
} else if (strcmp(cmd, "353") == 0 && params) {
|
||||||
/* RPL_NAMREPLY: <nick> = <channel> :names... */
|
/* RPL_NAMREPLY: <nick> = <channel> :names... */
|
||||||
char *colon = strchr(params, ':');
|
char *colon = strchr(params, ':');
|
||||||
@@ -642,7 +745,6 @@ static void handle_line(char *line)
|
|||||||
|
|
||||||
int idx = chan_win_idx(chan);
|
int idx = chan_win_idx(chan);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
/* Check our prefix */
|
|
||||||
char namecopy[512];
|
char namecopy[512];
|
||||||
snprintf(namecopy, sizeof(namecopy), "%s", names);
|
snprintf(namecopy, sizeof(namecopy), "%s", names);
|
||||||
char *tok = strtok(namecopy, " ");
|
char *tok = strtok(namecopy, " ");
|
||||||
@@ -653,10 +755,10 @@ static void handle_line(char *line)
|
|||||||
pf = *n;
|
pf = *n;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
|
nicklist_add(idx, n);
|
||||||
if (strcasecmp(n, nick) == 0) {
|
if (strcasecmp(n, nick) == 0) {
|
||||||
win_chans[idx].my_prefix = pf;
|
win_chans[idx].my_prefix = pf;
|
||||||
draw_statusbar();
|
draw_statusbar();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
tok = strtok(NULL, " ");
|
tok = strtok(NULL, " ");
|
||||||
}
|
}
|
||||||
@@ -722,10 +824,40 @@ static void handle_input(char *line)
|
|||||||
strlen(text));
|
strlen(text));
|
||||||
wprintf(WL_MSG, "-> %s: %s\n", target, text);
|
wprintf(WL_MSG, "-> %s: %s\n", target, text);
|
||||||
}
|
}
|
||||||
|
} else if (strcasecmp(cmd, "q") == 0) {
|
||||||
|
if (args && args[0]) {
|
||||||
|
snprintf(query_target, sizeof(query_target), "%s", args);
|
||||||
|
wprintf(WL_MSG, "* Now talking to %s\n", query_target);
|
||||||
|
} else {
|
||||||
|
if (query_target[0])
|
||||||
|
wprintf(WL_MSG, "* No longer talking to %s\n", query_target);
|
||||||
|
query_target[0] = '\0';
|
||||||
|
}
|
||||||
} else if (strcasecmp(cmd, "nick") == 0 && args) {
|
} else if (strcasecmp(cmd, "nick") == 0 && args) {
|
||||||
snprintf(nick, sizeof(nick), "%s", args);
|
snprintf(nick, sizeof(nick), "%s", args);
|
||||||
irc_send_raw("NICK %s", args);
|
irc_send_raw("NICK %s", args);
|
||||||
draw_statusbar();
|
draw_statusbar();
|
||||||
|
} else if (strcasecmp(cmd, "me") == 0 && args) {
|
||||||
|
const char *chan = current_channel();
|
||||||
|
if (chan[0]) {
|
||||||
|
irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", chan, args);
|
||||||
|
wprintf(current_level, "* %s %s\n", nick, args);
|
||||||
|
} else if (query_target[0]) {
|
||||||
|
irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", query_target, args);
|
||||||
|
wprintf(WL_MSG, "* %s %s\n", nick, args);
|
||||||
|
}
|
||||||
|
} else if (strcasecmp(cmd, "slap") == 0 && args) {
|
||||||
|
const char *chan = current_channel();
|
||||||
|
char slap[256];
|
||||||
|
snprintf(slap, sizeof(slap),
|
||||||
|
"slaps %s around a bit with a large trout", args);
|
||||||
|
if (chan[0]) {
|
||||||
|
irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", chan, slap);
|
||||||
|
wprintf(current_level, "* %s %s\n", nick, slap);
|
||||||
|
} else if (query_target[0]) {
|
||||||
|
irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", query_target, slap);
|
||||||
|
wprintf(WL_MSG, "* %s %s\n", nick, slap);
|
||||||
|
}
|
||||||
} else if (strcasecmp(cmd, "mode") == 0 && args) {
|
} else if (strcasecmp(cmd, "mode") == 0 && args) {
|
||||||
irc_send_raw("MODE %s", args);
|
irc_send_raw("MODE %s", args);
|
||||||
} else if (strcasecmp(cmd, "whois") == 0 && args) {
|
} else if (strcasecmp(cmd, "whois") == 0 && args) {
|
||||||
@@ -739,6 +871,19 @@ static void handle_input(char *line)
|
|||||||
const char *chan = args ? args : current_channel();
|
const char *chan = args ? args : current_channel();
|
||||||
if (chan[0])
|
if (chan[0])
|
||||||
irc_send_raw("NAMES %s", chan);
|
irc_send_raw("NAMES %s", chan);
|
||||||
|
} else if (strcasecmp(cmd, "topic") == 0) {
|
||||||
|
if (args && strchr(args, ' ')) {
|
||||||
|
/* /topic #channel new topic */
|
||||||
|
char *chan = args;
|
||||||
|
char *text = strchr(args, ' ');
|
||||||
|
*text++ = '\0';
|
||||||
|
irc_send_raw("TOPIC %s :%s", chan, text);
|
||||||
|
} else {
|
||||||
|
/* /topic or /topic #channel — query topic */
|
||||||
|
const char *chan = args ? args : current_channel();
|
||||||
|
if (chan[0])
|
||||||
|
irc_send_raw("TOPIC %s", chan);
|
||||||
|
}
|
||||||
} else if (strcasecmp(cmd, "quit") == 0) {
|
} else if (strcasecmp(cmd, "quit") == 0) {
|
||||||
irc_send_raw("QUIT :%s", args ? args : "See you later");
|
irc_send_raw("QUIT :%s", args ? args : "See you later");
|
||||||
close(sock_fd);
|
close(sock_fd);
|
||||||
@@ -750,6 +895,14 @@ static void handle_input(char *line)
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const char *chan = current_channel();
|
const char *chan = current_channel();
|
||||||
|
/* If on status/msg window with a query target, send there */
|
||||||
|
if (chan[0] == '\0' && query_target[0]) {
|
||||||
|
char pfx[IRC_MAX];
|
||||||
|
snprintf(pfx, sizeof(pfx), "PRIVMSG %s :", query_target);
|
||||||
|
irc_send_converted(pfx, (unsigned char *)line, len);
|
||||||
|
wprintf(WL_MSG, "-> %s: %s\n", query_target, line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (chan[0] == '\0') {
|
if (chan[0] == '\0') {
|
||||||
wprintf(current_level,
|
wprintf(current_level,
|
||||||
"* Not in a channel on this window.\n");
|
"* Not in a channel on this window.\n");
|
||||||
@@ -927,6 +1080,9 @@ int main(int argc, char *argv[])
|
|||||||
char yank_buf[BUF_SIZE] = "";
|
char yank_buf[BUF_SIZE] = "";
|
||||||
size_t yank_len = 0;
|
size_t yank_len = 0;
|
||||||
int esc_pending = 0;
|
int esc_pending = 0;
|
||||||
|
int tab_idx = -1; /* current tab completion index */
|
||||||
|
size_t tab_prefix_len = 0; /* length of prefix being completed */
|
||||||
|
size_t tab_start = 0; /* byte position where completion word starts */
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
FD_ZERO(&fds);
|
FD_ZERO(&fds);
|
||||||
@@ -1027,6 +1183,9 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reset tab completion on any non-tab key */
|
||||||
|
if (ch != 0x09) tab_idx = -1;
|
||||||
|
|
||||||
if (ch == 0x1B) {
|
if (ch == 0x1B) {
|
||||||
esc_pending = 1;
|
esc_pending = 1;
|
||||||
} else if (ch == '\r' || ch == '\n') {
|
} else if (ch == '\r' || ch == '\n') {
|
||||||
@@ -1074,6 +1233,62 @@ int main(int argc, char *argv[])
|
|||||||
memcpy(yank_buf, input_line + input_pos, yank_len);
|
memcpy(yank_buf, input_line + input_pos, yank_len);
|
||||||
input_len = input_pos;
|
input_len = input_pos;
|
||||||
redraw_input(input_line, input_len, input_pos);
|
redraw_input(input_line, input_len, input_pos);
|
||||||
|
} else if (ch == 0x09) {
|
||||||
|
/* Tab: nick completion */
|
||||||
|
int cidx = -1;
|
||||||
|
if (current_level >= WL_CHAN)
|
||||||
|
cidx = current_level - WL_CHAN;
|
||||||
|
|
||||||
|
if (cidx >= 0 && win_chans[cidx].nick_count > 0) {
|
||||||
|
/* Find word start */
|
||||||
|
if (tab_idx < 0) {
|
||||||
|
tab_start = input_pos;
|
||||||
|
while (tab_start > 0 &&
|
||||||
|
input_line[tab_start-1] != ' ')
|
||||||
|
tab_start--;
|
||||||
|
tab_prefix_len = input_pos - tab_start;
|
||||||
|
tab_idx = 0;
|
||||||
|
} else {
|
||||||
|
tab_idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find next matching nick */
|
||||||
|
int found = 0;
|
||||||
|
for (int tries = 0; tries < win_chans[cidx].nick_count; tries++) {
|
||||||
|
int ni = (tab_idx + tries) % win_chans[cidx].nick_count;
|
||||||
|
if (tab_prefix_len == 0 ||
|
||||||
|
strncasecmp(win_chans[cidx].nicks[ni],
|
||||||
|
input_line + tab_start,
|
||||||
|
tab_prefix_len) == 0) {
|
||||||
|
tab_idx = ni;
|
||||||
|
/* Replace from tab_start to input_pos */
|
||||||
|
const char *compl = win_chans[cidx].nicks[ni];
|
||||||
|
size_t clen = strlen(compl);
|
||||||
|
/* Add ": " if at start of line */
|
||||||
|
char suffix[4] = "";
|
||||||
|
if (tab_start == 0)
|
||||||
|
strcpy(suffix, ": ");
|
||||||
|
else
|
||||||
|
strcpy(suffix, " ");
|
||||||
|
size_t slen = strlen(suffix);
|
||||||
|
size_t tail = input_len - input_pos;
|
||||||
|
input_len = tab_start + clen + slen + tail;
|
||||||
|
if (input_len >= sizeof(input_line) - 1)
|
||||||
|
input_len = sizeof(input_line) - 1;
|
||||||
|
memmove(input_line + tab_start + clen + slen,
|
||||||
|
input_line + input_pos, tail);
|
||||||
|
memcpy(input_line + tab_start, compl, clen);
|
||||||
|
memcpy(input_line + tab_start + clen, suffix, slen);
|
||||||
|
input_pos = tab_start + clen + slen;
|
||||||
|
redraw_input(input_line, input_len, input_pos);
|
||||||
|
tab_idx++;
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) tab_idx = -1;
|
||||||
|
}
|
||||||
|
continue; /* don't reset tab state */
|
||||||
} else if (ch == 0x10) {
|
} else if (ch == 0x10) {
|
||||||
/* Ctrl-P: page up in scrollback */
|
/* Ctrl-P: page up in scrollback */
|
||||||
int page = term_rows - 3;
|
int page = term_rows - 3;
|
||||||
|
|||||||
Reference in New Issue
Block a user