[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.
[<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.
<Space> Call "/group toggle" for the current group.
'' Call "/roster alternate".
! Toggle attention flag for current buddy.
# Toggle unread messages flag for current buddy.
/<str> Call "/roster search <str>".
:q Call "/quit".
:wq Call "/quit".
:x Call "/quit".
:<n> Jump to line <n> in the roster.
:<cmd> Call "/<cmd>" (unless <cmd> matches one of the above commands).
--- 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
+<Space>
+.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\&.
--- 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.
+<Space>:: 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
--- 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... */
--- 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,