changeset 2304:fa8365fb6ac2

[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).
author Holger Weiß <holger@zedat.fu-berlin.de>
date Wed, 22 Jul 2015 19:25:22 +0200
parents 4f3821bda633
children 5b1a63dc2b1a
files mcabber/doc/mcabber.1 mcabber/doc/mcabber.1.txt mcabber/mcabber/screen.c mcabber/mcabberrc.example
diffstat 4 files changed, 489 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- 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,