# HG changeset patch # User Holger Weiß # Date 1437585922 -7200 # Node ID fa8365fb6ac23c430905a1b24201f6f39873eddb # Parent 4f3821bda633f128aa653a3744e22cad5dd8eab2 [PATCH 1/3] New option: vi_mode If the new vi_mode option is set to 1, let MCabber's non-chat mode accept a few commands loosely based on those available in vi(1)'s normal mode, e.g.: A Call "/roster unread_first". a Call "/roster unread_next". F Call "/roster group_prev". f Call "/roster group_next". G Call "/roster bottom". gg Call "/roster top". i Enter chat mode. []j Call "/roster down []". []k Call "/roster up []". n Repeat the previous search (if any). O Call "/roster unread_first" and open chat window. o Call "/roster unread_next" and open chat window. ZZ Call "/quit". zM Call "/group fold" for all groups. zR Call "/group unfold" for all groups. Call "/group toggle" for the current group. '' Call "/roster alternate". ! Toggle attention flag for current buddy. # Toggle unread messages flag for current buddy. / Call "/roster search ". :q Call "/quit". :wq Call "/quit". :x Call "/quit". : Jump to line in the roster. : Call "/" (unless matches one of the above commands). diff -r 4f3821bda633 -r fa8365fb6ac2 mcabber/doc/mcabber.1 --- a/mcabber/doc/mcabber.1 Mon Jan 30 18:46:15 2017 +0100 +++ b/mcabber/doc/mcabber.1 Wed Jul 22 19:25:22 2015 +0200 @@ -186,7 +186,7 @@ .sp Two status lines surround the log window\&. The bottom status line is the "main status line" and reflects mcabber general status\&. The other line is the "chat status line" and shows the status of the currently selected buddy\&. .sp -To display buddies chat buffers, you will have to enter \fIchat mode\fR\&. You can enter chat mode by pressing enter, and leave chat mode with the ESC key\&. Simply sending a message will also enable chat mode\&. +To display buddies chat buffers, you will have to enter \fIchat mode\fR\&. You can enter chat mode by pressing enter (unless \fIvi_mode\fR is enabled), and leave chat mode with the ESC key\&. Simply sending a message will also enable chat mode (unless \fIvi_mode\fR is enabled)\&. .sp There are several advantages to the two\-mode implementation: first, it allows accurate "unread" message functionality, as described in the next section; without this, merely scrolling to a specific buddy will "read" the new messages of all buddies in\-between\&. Second, it allows quickly hiding the conversation with a single keystroke\&. Third, it allows jumping between the few buddies with whom you are conversing with the \fI/roster alternate\fR command described in another section, without having to manually scroll back and forth\&. .SH "KEYS" @@ -292,6 +292,130 @@ .RE .sp Additional key bindings may be specified using the \fI/bind\fR command described in the COMMANDS section\&. +.SH "VI MODE" +.sp +If the \fIvi_mode\fR option is set to \fI1\fR, MCabber accepts a few commands loosely based on those available in \fBvi\fR(1)'s normal mode\&. In this case, chat mode can \fInot\fR be entered by pressing enter, and messages cannot be composed outside of the chat mode\&. The following commands are accepted: +.PP +A +.RS 4 +Call "/roster unread_first"\&. +.RE +.PP +a +.RS 4 +Call "/roster unread_next"\&. +.RE +.PP +F +.RS 4 +Call "/roster group_prev"\&. +.RE +.PP +f +.RS 4 +Call "/roster group_next"\&. +.RE +.PP +G +.RS 4 +Call "/roster bottom"\&. +.RE +.PP +gg +.RS 4 +Call "/roster top"\&. +.RE +.PP +i +.RS 4 +Enter chat mode\&. +.RE +.PP +[\fIn\fR]j +.RS 4 +Call "/roster down [\fIn\fR]"\&. +.RE +.PP +[\fIn\fR]k +.RS 4 +Call "/roster up [\fIn\fR]"\&. +.RE +.PP +n +.RS 4 +Repeat the previous search (if any)\&. +.RE +.PP +O +.RS 4 +Call "/roster unread_first" and open chat window\&. +.RE +.PP +o +.RS 4 +Call "/roster unread_next" and open chat window\&. +.RE +.PP +ZZ +.RS 4 +Call "/quit"\&. +.RE +.PP +zM +.RS 4 +Call "/group fold" for all groups\&. +.RE +.PP +zR +.RS 4 +Call "/group unfold" for all groups\&. +.RE +.PP +\&'' +.RS 4 +Call "/roster alternate"\&. +.RE +.PP +! +.RS 4 +Toggle attention flag for current buddy\&. +.RE +.PP +# +.RS 4 +Toggle unread messages flag for current buddy\&. +.RE +.PP + +.RS 4 +Call "/group toggle" for the current group\&. +.RE +.PP +A leading slash enables search mode: +.PP +/\fIstring\fR +.RS 4 +Call "/roster search \fIstring\fR"\&. +.RE +.PP +A leading colon enabled command-line mode: +.PP +:q +.RS 4 +Call "/quit"\&. +.RE +.PP +:\fIn\fR +.RS 4 +Jump to line \fIn\fR in the roster\&. +.RE +.PP +:\fIcommand-line\fR +.RS 4 +Call "/\fIcommand-line\fR" (unless the \fIcommand-line\fR matches one of the above commands)\&. +.RE +.PP +Commands entered with a leading colon and searches are either submitted by pressing enter or aborted by hitting escape\&. In either case, MCabber returns to the normal (non-chat) mode\&. History editing is supported in command-line mode and in search mode\&. In command-line mode, tab completion is supported as well\&. .SH "MCABBER\(cqS ROSTER" .sp The first listed item on the roster is \fI[status]\fR, which keeps a log of everything that appears in the short log window below the main chat area\&. While the log window was designed for showing the latest few elements, the dedicated \fI[status]\fR buffer allows more comfortable viewing of the log, as well as scrolling it in a standard manner\&. diff -r 4f3821bda633 -r fa8365fb6ac2 mcabber/doc/mcabber.1.txt --- a/mcabber/doc/mcabber.1.txt Mon Jan 30 18:46:15 2017 +0100 +++ b/mcabber/doc/mcabber.1.txt Wed Jul 22 19:25:22 2015 +0200 @@ -63,8 +63,9 @@ buddy. To display buddies chat buffers, you will have to enter 'chat mode'. -You can enter chat mode by pressing enter, and leave chat mode with the ESC -key. Simply sending a message will also enable chat mode. +You can enter chat mode by pressing enter (unless 'vi mode' is enabled), and +leave chat mode with the ESC key. Simply sending a message will also enable +chat mode (unless 'vi mode' is enabled). There are several advantages to the two-mode implementation: first, it allows accurate "unread" message functionality, as described in the next section; @@ -115,6 +116,50 @@ Additional key bindings may be specified using the '/bind' command described in the COMMANDS section. +VI MODE +------- +If the 'vi_mode' option is set to 1, `mcabber(1)` accepts a few commands +loosely based on those available in `vi(1)`'s normal mode. In this case, chat +mode is not entered by pressing enter, and messages cannot be composed outside +of the chat mode. The following commands are accepted: + +A:: Call "/roster unread_first". +a:: Call "/roster unread_next". +F:: Call "/roster group_prev". +f:: Call "/roster group_next". +G:: Call "/roster bottom". +gg:: Call "/roster top". +i:: Enter chat mode. +['n']j:: Call "/roster down ['n']". +['n']k:: Call "/roster up ['n']". +n:: Repeat the previous search (if any). +O:: Call "/roster unread_first" and open chat window. +o:: Call "/roster unread_next" and open chat window. +ZZ:: Call "/quit". +zM:: Call "/group fold" for all groups. +zR:: Call "/group unfold" for all groups. +\'':: Call "/roster alternate". +!:: Toggle attention flag for current buddy. +#:: Toggle unread messages flag for current buddy. +:: Call "/group toggle" for the current group. + +A leading slash enables search mode: + +/'string':: Call "/roster search 'string'". + +A leading colon enabled command-line mode: + +:q:: Call "/quit". +:'n':: Jump to line 'n' in the roster. +:'cmd-line':: Call "/'cmd-line'" (unless the 'cmd-line' matches one of the + above commands). + +Commands entered with a leading colon and searches are either submitted by +pressing enter or aborted by hitting escape. In either case, `mcabber(1)` +returns to the normal (non-chat) mode. History editing is supported in +command-line mode and in search mode. In command-line mode, tab completion is +supported as well. + MCABBER'S ROSTER ---------------- The first listed item on the roster is '[status]', which keeps a log of diff -r 4f3821bda633 -r fa8365fb6ac2 mcabber/mcabber/screen.c --- a/mcabber/mcabber/screen.c Mon Jan 30 18:46:15 2017 +0100 +++ b/mcabber/mcabber/screen.c Wed Jul 22 19:25:22 2015 +0200 @@ -95,6 +95,9 @@ static void spellcheck(char *, char *); #endif +static void open_chat_window(void); +static void clear_inputline(void); + static GHashTable *winbufhash; typedef struct { @@ -4175,7 +4178,8 @@ static inline void refresh_inputline(void) { #if defined(WITH_ENCHANT) || defined(WITH_ASPELL) - if (settings_opt_get_int("spell_enable")) { + if (settings_opt_get_int("spell_enable") && + (chatmode || !settings_opt_get_int("vi_mode"))) { memset(maskLine, 0, INPUTLINE_LENGTH+1); spellcheck(inputLine, maskLine); } @@ -4426,12 +4430,53 @@ #endif } +static void scr_process_vi_arrow_key(int key) +{ + const char *l; + char mask[INPUTLINE_LENGTH+1] = "/roster search "; + size_t cmd_len = strlen(mask); + size_t str_len = strlen(inputLine) - 1; + + switch (inputLine[0]) { + case ':': + inputLine[0] = '/'; + if (key == KEY_UP) + l = scr_cmdhisto_prev(inputLine, ptr_inputline - inputLine); + else + l = scr_cmdhisto_next(inputLine, ptr_inputline - inputLine); + if (l) + strcpy(inputLine, l); + inputLine[0] = ':'; + break; + case '/': + if (cmd_len + str_len > INPUTLINE_LENGTH) + return; + + memcpy(mask + cmd_len, inputLine + 1, str_len + 1); + if (key == KEY_UP) + l = scr_cmdhisto_prev(mask, ptr_inputline - inputLine + cmd_len - 1); + else + l = scr_cmdhisto_next(mask, ptr_inputline - inputLine + cmd_len - 1); + if (l) + strcpy(inputLine + 1, l + cmd_len); + break; + default: + if (key == KEY_UP) + process_command(mkcmdstr("roster up"), TRUE); + else + process_command(mkcmdstr("roster down"), TRUE); + break; + } +} + // scr_process_key(key) // Handle the pressed key, in the command line (bottom). void scr_process_key(keycode kcode) { int key = kcode.value; int display_char = FALSE; + int vi_completion = FALSE; + static int ex_or_search_mode = FALSE; lock_chatstate = FALSE; @@ -4448,6 +4493,253 @@ key = ERR; // Do not process any further } + if (settings_opt_get_int("vi_mode") && !chatmode) { + int got_cmd_prefix = FALSE; + int unrecognized = FALSE; + static char search_cmd[INPUTLINE_LENGTH+1] = "/roster search "; + + if (key == KEY_UP || key == KEY_DOWN) { + scr_process_vi_arrow_key(key); + key = ERR; // Do not process any further + } else if (ex_or_search_mode) { + switch (key) { + case 27: // Escape + clear_inputline(); + ex_or_search_mode = FALSE; + break; + case 9: // Tab + case 353: // Shift-Tab + if (inputLine[0] == ':') { + inputLine[0] = '/'; + vi_completion = TRUE; + } + break; + case 13: // Enter + case 343: // Enter on Maemo + switch (inputLine[0]) { + case ':': + { + char *p = strchr(inputLine, 0); + + while (*--p == ' ' && p > inputLine) + *p = 0; + } + if (!strcmp(inputLine, ":x") || + !strcmp(inputLine, ":q") || + !strcmp(inputLine, ":wq")) + strcpy(inputLine, ":quit"); + if (isdigit((int)(unsigned char)inputLine[1]) && + strlen(inputLine) <= 9) { + process_command(mkcmdstr("roster top"), TRUE); + memcpy(inputLine + 13, inputLine + 1, 10); + memcpy(inputLine + 1, "roster down ", 12); + } + inputLine[0] = '/'; + process_command(inputLine, TRUE); + scr_cmdhisto_addline(inputLine); + break; + case '/': + { + size_t cmd_len = sizeof("/roster search ") - 1; + size_t str_len = strlen(inputLine) - 1; + + if (cmd_len + str_len > INPUTLINE_LENGTH) + return; + + memcpy(search_cmd + cmd_len, inputLine + 1, + str_len + 1); + } + process_command(search_cmd, TRUE); + scr_cmdhisto_addline(search_cmd); + break; + } + ex_or_search_mode = FALSE; + break; + } + } else if (key >= '0' && key <= '9') { + got_cmd_prefix = TRUE; + } else { + switch (key) { + case '/': + case ':': + ex_or_search_mode = TRUE; + break; + case ' ': + process_command(mkcmdstr("group toggle"), TRUE); + break; + case '!': + { + const char *bjid = buddy_getjid(BUDDATA(current_buddy)); + + if (bjid) { + guint type = buddy_gettype(BUDDATA(current_buddy)); + guint prio = buddy_getuiprio(BUDDATA(current_buddy)); + + if (type & ROSTER_TYPE_ROOM && + prio < ROSTER_UI_PRIO_MUC_HL_MESSAGE) { + roster_setuiprio(bjid, FALSE, + ROSTER_UI_PRIO_MUC_HL_MESSAGE, prio_set); + roster_msg_setflag(bjid, FALSE, TRUE); + } else if (type & ROSTER_TYPE_USER && + prio < ROSTER_UI_PRIO_ATTENTION_MESSAGE) { + roster_setuiprio(bjid, FALSE, + ROSTER_UI_PRIO_ATTENTION_MESSAGE, prio_set); + roster_msg_setflag(bjid, FALSE, TRUE); + } else { + roster_msg_setflag(bjid, FALSE, FALSE); + } + scr_update_roster(); + } + } + break; + case '#': + { + const char *bjid = buddy_getjid(BUDDATA(current_buddy)); + + if (bjid) { + unsigned short bflags = buddy_getflags(BUDDATA(current_buddy)); + + if (bflags & ROSTER_FLAG_MSG) + roster_msg_setflag(bjid, FALSE, FALSE); + else + roster_msg_setflag(bjid, FALSE, TRUE); + + scr_update_roster(); + } + } + break; + case '\'': + if (inputLine[0] == '\'') + process_command(mkcmdstr("roster alternate"), TRUE); + else + got_cmd_prefix = TRUE; + break; + case 'A': + process_command(mkcmdstr("roster unread_first"), TRUE); + break; + case 'a': + process_command(mkcmdstr("roster unread_next"), TRUE); + break; + case 'F': + process_command(mkcmdstr("roster group_prev"), TRUE); + break; + case 'f': + process_command(mkcmdstr("roster group_next"), TRUE); + break; + case 'G': + process_command(mkcmdstr("roster bottom"), TRUE); + break; + case 'g': + if (inputLine[0] == 'g') + process_command(mkcmdstr("roster top"), TRUE); + else { + clear_inputline(); + got_cmd_prefix = TRUE; + } + break; + case 'i': + open_chat_window(); + break; + case 'j': + if (isdigit((int)(unsigned char)inputLine[0]) && + strlen(inputLine) <= 9) { + char down_cmd[32] = "/roster down "; + + strcat(down_cmd, inputLine); + process_command(down_cmd, TRUE); + } else + process_command(mkcmdstr("roster down"), TRUE); + break; + case 'k': + if (isdigit((int)(unsigned char)inputLine[0]) && + strlen(inputLine) <= 9) { + char up_cmd[32] = "/roster up "; + + strcat(up_cmd, inputLine); + process_command(up_cmd, TRUE); + } else + process_command(mkcmdstr("roster up "), TRUE); + break; + case 'M': + if (inputLine[0] == 'z') { + GSList *groups = compl_list(ROSTER_TYPE_GROUP); + GSList *g; + + for (g = groups; g; g = g_slist_next(g)) { + char fold_cmd[128] = "/group fold "; + size_t cmd_len = strlen(fold_cmd); + size_t grp_len = strlen(g->data); + + if (cmd_len + grp_len + 1 > sizeof(fold_cmd)) + continue; + memcpy(fold_cmd + cmd_len, g->data, grp_len + 1); + process_command(fold_cmd, TRUE); + g_free(g->data); + } + g_slist_free(groups); + } else + unrecognized = TRUE; + break; + case 'n': + process_command(search_cmd, TRUE); + break; + case 'O': + process_command(mkcmdstr("roster unread_first"), TRUE); + open_chat_window(); + break; + case 'o': + process_command(mkcmdstr("roster unread_next"), TRUE); + open_chat_window(); + break; + case 'R': + if (inputLine[0] == 'z') { + GSList *groups = compl_list(ROSTER_TYPE_GROUP); + GSList *g; + + for (g = groups; g; g = g_slist_next(g)) { + char fold_cmd[128] = "/group unfold "; + size_t cmd_len = strlen(fold_cmd); + size_t grp_len = strlen(g->data); + + if (cmd_len + grp_len + 1 > sizeof(fold_cmd)) + continue; + memcpy(fold_cmd + cmd_len, g->data, grp_len + 1); + process_command(fold_cmd, TRUE); + g_free(g->data); + } + g_slist_free(groups); + } else + unrecognized = TRUE; + break; + case 'Z': + if (inputLine[0] == 'Z') + process_command(mkcmdstr("quit"), TRUE); + else { + clear_inputline(); + got_cmd_prefix = TRUE; + } + break; + case 'z': + clear_inputline(); + got_cmd_prefix = TRUE; + break; + case 13: // Enter + case 343: // Enter on Maemo + break; + default: + unrecognized = TRUE; + break; + } + cmdhisto_cur = NULL; + } + if (!ex_or_search_mode && !got_cmd_prefix) { + clear_inputline(); + if (!unrecognized) + key = ERR; // Do not process any further + } + lock_chatstate = TRUE; + } + if (kcode.utf8) { if (key != ERR && !kcode.mcode) display_char = TRUE; @@ -4513,8 +4805,13 @@ if (completion_started && key != 9 && key != 353 && key != KEY_RESIZE) scr_end_current_completion(); + else if (vi_completion) + inputLine[0] = ':'; refresh_inputline(); + if (ex_or_search_mode && inputLine[0] != ':' && inputLine[0] != '/') + ex_or_search_mode = FALSE; + if (!lock_chatstate) { // Set chat state to composing (1) if the user is currently composing, // i.e. not an empty line and not a command line. @@ -4693,4 +4990,19 @@ } #endif +static void open_chat_window(void) +{ + last_activity_buddy = current_buddy; + scr_check_auto_away(TRUE); + scr_set_chatmode(TRUE); + scr_show_buddy_window(); +} + +static void clear_inputline(void) +{ + ptr_inputline = inputLine; + *ptr_inputline = 0; + inputline_offset = 0; +} + /* vim: set et cindent cinoptions=>2\:2(0 ts=2 sw=2: For Vim users... */ diff -r 4f3821bda633 -r fa8365fb6ac2 mcabber/mcabberrc.example --- a/mcabber/mcabberrc.example Mon Jan 30 18:46:15 2017 +0100 +++ b/mcabber/mcabberrc.example Wed Jul 22 19:25:22 2015 +0200 @@ -142,6 +142,10 @@ # (default: 0, unlimited). set cmdhistory_lines = 250 +# Let MCabber accept some vi(1)-like "normal mode" commands by setting the +# option 'vi_mode' to 1 (default: 0). +#set vi_mode = 1 + # You can set up a mask to filter buddies and display them according to # their status. The mask should contain the shortcut letters of the # status you want to see ([o]nline, [f]ree_for_chat, [d]o_not_disturb,