Friday, November 5, 2010

GNU Readline Vi Mode Visualization #2

In the last post I've described my GNU readline patch providing the means for VI editing modes visualization. Here I will describe my improved version of that patch.

It no longer uses environment variables for configuration but the configuration is done in '.inputrc' file. The configuration properties added are:

  • set vi-command-prompt {VALUE}
  • This sets the prompt in COMMAND mode to VALUE.
  • set vi-insert-prompt {VALUE}
  • This sets the prompt in INSERT mode to VALUE.
  • set vi-mode-changed-bin {PATH}
  • This sets the executable designated by PATH to be executed when mode changes. First parameter 'insert' or 'command' is passed to designate the mode we're in. Note that the executable is executed synchronously with STDIN disconnected, and STDOUT connected to the terminal device.

Note that you don't need to do any quoting in the configuration file in case of spaces. Default readline parsing of configuration properties works the way that value of the property is starting with first non whitespace character after property name until the end of line. Also note that if the prompt value contains '{}' this will be replaced by last line of prompt set by application.

I've updated an Ach Linux PKGBUILD which you can find here.

For reference below are all related files.

  • .inputrc - readline configuration file
  • .inputrc_vimode_changed - shell script configured to be executed on mode change
  • vimode-changed-hook.patch - source patch itself
.inputrc
# no terminal bell
set bell-style none

# show completion matches immediately
set show-all-if-ambiguous on

# show completion matches only if there is no partial completion
set show-all-if-unmodified on

# do not insert the completion text if it is already present
set skip-completed-text on

# vi editing mode
set editing-mode vi

# prompt format for command mode
set vi-command-prompt \001\033[1;37m\002{}\001\033[0m\002

$if python
    set vi-command-prompt \001\033[1;37;41m\002>\001\033[0m\002{}
$endif

# vi mode changed script
set vi-mode-changed-bin /home/miro/.inputrc_vimode_changed

# insert mode key bindings
set keymap vi-insert

"\C-l": clear-screen
"\C-p": history-search-backward
"\C-n": history-search-forward
"\C-o": menu-complete
"\C-k": menu-complete-backward

$if bash
    "\C-x\C-x": "\C-[0isudo \C-[0"
    "\C-x\C-s": "\C-[ddiscreen\n"
    "\C-x\C-t": "\C-[dditmux new -s ''\C-[i"
$endif

# command mode key bindings
set keymap vi-command

"\C-p": history-search-backward
"\C-n": history-search-forward

# effectively before 'accept-line' switch to 'insert' mode
# this can be helpfull in case of vi mode visualization
"\n": "i\n"
"\r": "i\r"

$if bash
    "\C-x\C-x": "0isudo \C-[0"
    "\C-x\C-s": "ddiscreen\n"
    "\C-x\C-t": "dditmux new -s ''\C-[i"
$endif
.inputrc_vimode_changed
#!/bin/bash

# $ man console_codes
# http://en.wikipedia.org/wiki/ANSI_escape_code
# http://rtfm.etla.org/xterm/ctlseq.html
# http://linuxgazette.net/137/anonymous.html
# http://wiki.archlinux.org/index.php/Color_Bash_Prompt
# http://www.comptechdoc.org/os/linux/howlinuxworks/linux_hlvt100.html

MODE="$1"
CURSOR_COLOR='\e]12;%s'
HSTATUS="$USER@${HOSTNAME%%.*}:${PWD/$HOME/~}"

cursor_color() {
    printf "$CURSOR_COLOR\e\\" "$1"
}

cursor_color_linux() {
    printf '\e[?16;0;%sc' "$1"
}

hstatus() {
    printf '\e]0;%s\e\\' "$1"
}

case "$MODE" in
    'insert')
        if [[ "$TERM" =~ ^(xterm|rxvt) ]]; then
            cursor_color '#C0C0C0'
            hstatus "[INS] $HSTATUS"

        elif [[ "$TERM" =~ ^screen ]]; then
            hstatus "[INS] $HSTATUS"

        elif [[ "$TERM" =~ ^linux ]]; then
            cursor_color_linux 112

        fi
        ;;
    'command')
        if [[ "$TERM" =~ ^(xterm|rxvt) ]]; then
            cursor_color 'sienna2'
            hstatus "[CMD] $HSTATUS"

        elif [[ "$TERM" =~ ^screen ]]; then
            hstatus "[CMD] $HSTATUS"

        elif [[ "$TERM" =~ ^linux ]]; then
            cursor_color_linux 64

        fi
        ;;
esac
vimode-changed-hook.patch
+++ bind.c 2010-10-28 23:09:12.788260925 +0200
@@ -1508,6 +1508,9 @@
 static int sv_histsize PARAMS((const char *));
 static int sv_isrchterm PARAMS((const char *));
 static int sv_keymap PARAMS((const char *));
+static int sv_vi_insert_prompt PARAMS((const char *));
+static int sv_vi_command_prompt PARAMS((const char *));
+static int sv_vi_mode_changed_bin PARAMS((const char *));
 
 static const struct {
   const char * const name;
@@ -1522,6 +1525,9 @@
   { "history-size", V_INT,  sv_histsize },
   { "isearch-terminators", V_STRING, sv_isrchterm },
   { "keymap",  V_STRING, sv_keymap },
+  { "vi-insert-prompt", V_STRING, sv_vi_insert_prompt },
+  { "vi-command-prompt", V_STRING, sv_vi_command_prompt },
+  { "vi-mode-changed-bin", V_STRING, sv_vi_mode_changed_bin },
   { (char *)NULL, 0 }
 };
 
@@ -1694,6 +1700,30 @@
 }
 
 static int
+sv_vi_insert_prompt (value)
+    const char *value;
+{
+  _rl_set_vi_insert_prompt (value);
+  return 0;
+}
+
+static int
+sv_vi_command_prompt (value)
+    const char *value;
+{
+  _rl_set_vi_command_prompt (value);
+  return 0;
+}
+
+static int
+sv_vi_mode_changed_bin (value)
+    const char *value;
+{
+  _rl_set_vi_mode_changed_bin (value);
+  return 0;
+}
+
+static int
 sv_bell_style (value)
      const char *value;
 {
+++ rlprivate.h 2010-10-28 23:09:45.788262324 +0200
@@ -361,6 +361,9 @@
 extern int (_rl_digit_value) PARAMS((int));
 
 /* vi_mode.c */
+extern void _rl_set_vi_insert_prompt PARAMS((const char *));
+extern void _rl_set_vi_command_prompt PARAMS((const char *));
+extern void _rl_set_vi_mode_changed_bin PARAMS((const char *));
 extern void _rl_vi_initialize_line PARAMS((void));
 extern void _rl_vi_reset_last PARAMS((void));
 extern void _rl_vi_set_last PARAMS((int, int, int));
+++ vi_mode.c 2010-10-28 23:21:54.038261767 +0200
@@ -49,6 +49,8 @@
 
 #include <stdio.h>
 
+#include <fcntl.h>
+
 /* Some standard library routines. */
 #include "rldefs.h"
 #include "rlmbutil.h"
@@ -128,16 +130,121 @@
 static int _rl_vi_callback_char_search PARAMS((_rl_callback_generic_arg *));
 #endif
 
+#define _RL_PROMPT_INITIAL_SIZE 256
+#define _VI_MODE_PROMPT_FORMAT_SIZE 256
+#define _VI_MODE_PROMPT_SIZE _RL_PROMPT_INITIAL_SIZE + _VI_MODE_PROMPT_FORMAT_SIZE
+static char _rl_prompt_initial[_RL_PROMPT_INITIAL_SIZE] = "\0";
+static char *_rl_prompt_initial_last_line;
+static char _rl_vi_insert_prompt[_VI_MODE_PROMPT_FORMAT_SIZE] = "\0";
+static char _rl_vi_command_prompt[_VI_MODE_PROMPT_FORMAT_SIZE] = "\0";
+#define _VI_MODE_CHANGED_BIN_SIZE 256
+static char _rl_vi_mode_changed_bin[_VI_MODE_CHANGED_BIN_SIZE] = "\0";
+
+void _rl_set_vi_insert_prompt PARAMS((const char *));
+void _rl_set_vi_command_prompt PARAMS((const char *));
+void _rl_set_vi_mode_changed_bin PARAMS((const char *));
+
+static void vi_mode_changed_prompt PARAMS((void));
+static void vi_mode_changed_bin PARAMS((void));
+static void vi_mode_changed PARAMS((void));
+
+static char *
+safe_strncpy (dest, src, n)
+    char *dest;
+    const char *src;
+    int n;
+{
+  if (src) {
+    strncpy (dest, src, n);
+    dest[n - 1] = '\0';
+  }
+  else {
+    dest[0] = '\0';
+  }
+  return dest;
+}
+
+static char *
+parse_prompt_format_escapes (src)
+    char *src;
+{
+  char *r, *w, c, o;
+  static char buf[4] = "000";
+
+  r = src;
+  w = r;
+  while (*r) {
+    c = *r;
+    if (c == '\\') {
+      r++;
+      if (*r == '\\') {
+        c = '\\';
+      }
+      else if (*r == '0') {
+        c = *r;
+        buf[1] = *(r + 1);
+        buf[2] = *(r + 2);
+        o = strtol (buf, NULL, 8);
+        if (o > 0) {
+          r += 2;
+          c = o;
+        }
+      }
+    }
+    r++;
+    if (c > 0) {
+      *w = c;
+      w++;
+    }
+  }
+  *w = '\0';
+  return src;
+}
+
+void
+_rl_set_vi_insert_prompt (value)
+    const char *value;
+{
+  safe_strncpy (_rl_vi_insert_prompt, value, _VI_MODE_PROMPT_FORMAT_SIZE);
+  parse_prompt_format_escapes (_rl_vi_insert_prompt);
+}
+
+void
+_rl_set_vi_command_prompt (value)
+    const char *value;
+{
+  safe_strncpy (_rl_vi_command_prompt, value, _VI_MODE_PROMPT_FORMAT_SIZE);
+  parse_prompt_format_escapes (_rl_vi_command_prompt);
+}
+
+void
+_rl_set_vi_mode_changed_bin (value)
+    const char *value;
+{
+  safe_strncpy (_rl_vi_mode_changed_bin, value, _VI_MODE_CHANGED_BIN_SIZE);
+}
+
 void
 _rl_vi_initialize_line ()
 {
   register int i, n;
+  char *p;
 
   n = sizeof (vi_mark_chars) / sizeof (vi_mark_chars[0]);
   for (i = 0; i < n; i++)
     vi_mark_chars[i] = -1;
 
   RL_UNSETSTATE(RL_STATE_VICMDONCE);
+
+  safe_strncpy (_rl_prompt_initial, rl_prompt, _RL_PROMPT_INITIAL_SIZE);
+  p = strrchr (_rl_prompt_initial, '\n');
+  if (p) {
+      p++;
+  }
+  else {
+    p = _rl_prompt_initial;
+  }
+  _rl_prompt_initial_last_line = p;
 }
 
 void
@@ -655,6 +762,86 @@
 
 /* Insertion mode stuff. */
 
+/* This is meant to be called after vi mode changes. */
+static void
+vi_mode_changed_prompt ()
+{
+  char *prompt, *p;
+  char pattern[] = "{}";
+  static char buf[_VI_MODE_PROMPT_SIZE];
+  int i, j;
+
+  if (VI_INSERT_MODE()) {
+    prompt = _rl_vi_insert_prompt;
+  }
+  else if (VI_COMMAND_MODE()) {
+    prompt = _rl_vi_command_prompt;
+  }
+  if (strlen (prompt)) {
+    i = _rl_prompt_initial_last_line - _rl_prompt_initial;
+    memcpy (buf, _rl_prompt_initial, i);
+    buf[i] = '\0';
+    p = strstr (prompt, pattern);
+    if (p) {
+      j = p - prompt;
+      memcpy (buf + i, prompt, j);
+      buf[i + j] = '\0';
+      strcat (buf, _rl_prompt_initial_last_line);
+      strcat (buf, p + strlen (pattern));
+    }
+    else {
+      strcat (buf, prompt);
+    }
+    prompt = buf;
+  }
+  else {
+    prompt = _rl_prompt_initial;
+  }
+  rl_set_prompt (prompt);
+  _rl_redisplay_after_sigwinch ();
+}
+
+static void
+vi_mode_changed_bin ()
+{
+  pid_t pid;
+  int status, fd_devnull;
+  char *bin = _rl_vi_mode_changed_bin;
+
+  if (!strlen (bin)) {
+    return;
+  }
+  pid = fork ();
+  if (pid < 0) {
+    perror ("vi_mode_changed_bin: fork failed");
+    return;
+  }
+  else if (pid == 0) {
+    close (STDIN_FILENO);
+    fd_devnull = open ("/dev/null", O_RDONLY);
+    dup2 (fd_devnull, STDIN_FILENO);
+    if (VI_INSERT_MODE()) {
+      execl (bin, bin, "insert", NULL);
+    }
+    else if (VI_COMMAND_MODE()) {
+      execl (bin, bin, "command", NULL);
+    }
+    perror ("vi_mode_changed_bin: execv failed");
+    exit (1);
+  }
+  waitpid (pid, &status, 0);
+}
+
+static void
+vi_mode_changed ()
+{
+  if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO)) {
+    return;
+  }
+  vi_mode_changed_prompt ();
+  vi_mode_changed_bin ();
+}
+
 /* Switching from one mode to the other really just involves
    switching keymaps. */
 int
@@ -663,6 +850,9 @@
 {
   _rl_keymap = vi_insertion_keymap;
   _rl_vi_last_key_before_insert = key;
+
+  vi_mode_changed ();
+
   return (0);
 }
 
@@ -747,6 +937,9 @@
     rl_free_undo_list ();
 
   RL_SETSTATE (RL_STATE_VICMDONCE);
+
+  vi_mode_changed ();
+
   return (0);
 }