2019-12-14

Terminalでの、なんちゃってViモドキ

近頃、ようやくKarabiner-Elementsに慣れてきたので、 Terminalで動作する「擬似Vi-Mode」を作って見たので、ご紹介します。

『概要』

「擬似Vi-Mode」の所以は、方向キー「←↓↑→」を通常の「hjkl」ではなくて「jkil」としました。これは私の小指が短めで、中指が長めのために選択してみました。恐らく、Vim系列では、このようなキー設定の変更は事実上不可能と推測しています。

当初は、zshの.zshrcで使い慣れている「bindkey -e」をベースに、下記の設定から始めました;
  • "^J" backward-char
  • "^K" down-line-or-history
  • "^I" up-line-or-history
  • "^L" forward-char
  • "^A" beginning-of-line
  • "^B" backward-delete-char
  • "^D" delete-char-or-list
  • "^E" end-of-line
  • "^G" expand-or-complete
  • "^M" accept-line
  • "^U" undo
この変則的なキーマッピングは文字(chr)単位だけの操作だけでしたが、予想以上に快適だったので、「Esc」を使って、語(word)や列(line)の操作をしようとしましたが、小指が短めの私には、「Esc」は遠すぎです。

実は、Karabiner-Elementsにより、下記のキー操作が実現されています;
  • スペース・キーを長押ししていると「lコントロール・キー (^)」
  • スペース・キーの左右には「コマンド・キー(⌘)」
  • また、キャップスロック・キーには(FN)
そこで、Karabiner-ElementsでTerminalについての.jsonを検索すると、"Navigation in Terminal Apps"があったので、これを参考に語(word)や列(line)の操作ができました’
  • "tViModoki.2g.json"
  • 以降、ViModokiがニックネームとなりました。
  • 起動キーは「左コマンド(スペース・キーを長押し)」で、
  • 修飾キーは「コマンド・キー(⌘)」

ここまで来ると、「範囲指定: ^oで開始点を設定、終点まで移動」をして、「コピー: ^w」、「削除:^x」、「ペースト:^v」が欲しくなりました。「^w」と「^x」は、下記の2つのサイトを参考に関数で対応しました;
zshで範囲選択・削除・コピー・切り取りするhttps://qiita.com/takc923/items/35d9fe81f61436c867a8
Macがzshになるなら、ZLEを習得するっきゃない!https://dev.classmethod.jp/tool/zsh-zle-introduction/

    『.zshrcでのbindkey設定』

    ## basic bindkey setting
    bindkey -d
    bindkey -e
    ## # cursor movement by char/word/line
       bindkey  '^[j' backward-word          #   ⎋j: ←.w
       bindkey   '^j' backward-char          #   ^j: ←.c
       bindkey   '^k' down-line-or-history   #   ^k: ↓.l
       bindkey   '^i' up-line-or-history     #   ^i: ↑.l
       bindkey   '^l' forward-char           #   ^l: →.c
       bindkey  '^[l' forward-word           #   ⎋f: w.→
    ## # resion & killing
       bindkey   '^o' set-mark-command       # 範囲の開始位置をマーク
       bindkey   '^x' kill-region
                         #マークから現在位置までの範囲を削除し、それをキルリングに蓄積.
       bindkey   '^v' yank
                         #キルリングの最新の内容を貼り付ける
       bindkey  '^[v' yank-pop
                         #キルリングを遡る(Ctrl-yの後にのみ有効)
    ## bindkey  '^[w' copy-region-as-kill    # カーソルの左側の語をコピー
    ## # cursor movement by line
       bindkey  '^[a' kill-line                        #   ⎋a: ⌦.eol
       bindkey   '^a' beginning-of-line          #   ^a: bol.l
       bindkey  '^[e' backward-kill-line         #   ⎋e: bol.⌫
       bindkey   '^e' end-of-line                   #    ^e: l.eol
     # bindkey   '^y' center-of-line              # function center-of-line()
     # forward delete for word & char
       bindkey  '^[d' kill-word                      #   ⎋d: w⌦
       bindkey   '^d' delete-char-or-list        #   ^d: c⌦
     # backward delete for word & char
       bindkey  '^[b' backward-kill-word       #   ⎋b: ⌫w
       bindkey   '^b' backward-delete-char   #   ^b: ⌫c
     # operation
       bindkey   '^_' undo                            #   ^_: ⎌
       bindkey   '^m'  accept-line                 #   ^m: ↩︎

    tViModoki.2g.json

    {
      "title": "tViModoki.2g.json,
      "rules": [
        {
          "description": "tViModoki.2g.json  T=^;⌘a➤⎋a,⌘b➤⎋b,⌘c➤⌘c,⌘d➤⎋d,⌘e➤⎋e,h➤⎋h,⌘j➤⎋j,l➤⎋l,p➤⎋q,q➤',u➤^_,⌘v➤⎋v,⌘x➤⌘x,⌘z➤⌘z.",
          "_note0": "^q:quote ^p:push-line ^h:run-help",
          "_note1": "rev 2f: 2019-11-11-1820  <-- rev 2e.1: 1370220828.json",
          "_note2": "rev 2g: 2019-12-12-2205  alphabetical order",
           "manipulators": [
            {
              "type": "basic",
              "from": {
                "key_code": "a",
                "modifiers": {
                  "mandatory": [
                    "control",
                    "command"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "a"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "b",
                "modifiers": {
                  "mandatory": [
                    "command",
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "b"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "c",
                "modifiers": {
                  "mandatory": [
                    "control",
                    "command"
                  ],
                  "optional": [
                    "caps_lock"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "c",
                  "modifiers": [
                    "left_command"
                  ]
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "d",
                "modifiers": {
                  "mandatory": [
                    "command",
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "d"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
         
            {
              "type": "basic",
              "from": {
                "key_code": "e",
                "modifiers": {
                  "mandatory": [
                    "control",
                    "command"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "e"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "h",
                "modifiers": {
                  "mandatory": [
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "h"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.apple\\.finder$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "j",
                "modifiers": {
                  "mandatory": [
                    "command",
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "j"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.apple\\.finder$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "l",
                "modifiers": {
                  "mandatory": [
                    "command",
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "l"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "p",
                "modifiers": {
                  "mandatory": [
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "q"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.apple\\.finder$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "q",
                "modifiers": {
                  "mandatory": [
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "quote"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.apple\\.finder$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "s",
                "modifiers": {
                  "mandatory": [
                    "control",
                    "command"
                  ],
                  "optional": [
                    "caps_lock"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "s",
                  "modifiers": [
                    "left_command"
                  ]
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "u",
                "modifiers": {
                  "mandatory": [
                    "control"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "hyphen",
                  "modifiers": [
                    "left_shift",
                    "left_control"
                  ]
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },

            {
              "type": "basic",
              "from": {
                "key_code": "v",
                "modifiers": {
                  "mandatory": [
                    "control",
                    "command"
                  ],
                  "optional": [
                    "caps_lock"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "v",
                  "modifiers": [
                    "left_command"
                  ]
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "x",
                "modifiers": {
                  "mandatory": [
                    "control",
                    "command"
                  ],
                  "optional": [
                    "caps_lock"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "escape"
                },
                {
                  "key_code": "v"
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            },
            {
              "type": "basic",
              "from": {
                "key_code": "z",
                "modifiers": {
                  "mandatory": [
                    "control",
                    "command"
                  ],
                  "optional": [
                    "caps_lock"
                  ]
                }
              },
              "to": [
                {
                  "key_code": "z",
                  "modifiers": [
                    "left_command"
                  ]
                }
              ],
              "conditions": [
                {
                  "type": "frontmost_application_if",
                  "bundle_identifiers": [
                    "^com\\.apple\\.Terminal$",
                    "^com\\.googlecode\\.iterm2$",
                    "^co\\.zeit\\.hyperterm$",
                    "^co\\.zeit\\.hyper$",
                    "^io\\.alacritty$",
                    "^net\\.kovidgoyal\\.kitty$"
                  ]
                }
              ]
            }
          ]
        }
      ]
    }


    『.zshrcでのzle関数』

    ### functions for bindkey ######################################################
    #                                                                              #
    # https://qiita.com/takc923/items/35d9fe81f61436c867a8
    # zshで範囲選択・削除・コピー・切り取りする
    # 導入: 2019-12-12-052531
    function copy-region() {
        zle copy-region-as-kill
        REGION_ACTIVE=0
    }
    zle -N copy-region
    bindkey "^w" copy-region
    function remove-region() {
    #    if [[ "$(($REGION_ACTIVE))" -eq 0 ]]; then
    #        zle backward-kill-word
    #    else
    #        zle kill-region
    # fi
    zle kill-region
    }
    zle -N remove-region
    bindkey "^x" remove-region

    # https://dev.classmethod.jp/tool/zsh-zle-introduction/
    # Macがzshになるなら、ZLEを習得するっきゃない!
    # 2019-12-12-191339 「if [[ 」は駄目だった。
    function tab-hokan() {
        [[ "${RBUFFER:0:1}" != " " ]] && BUFFER="${LBUFFER} ${RBUFFER}"
        zle expand-or-complete
        zle redisplay
    }
    zle -N tab-hokan
    bindkey "^g" tab-hokan


      『キー操作のリスト』

       •••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
       • vm: "tiny Vi-Modoki based on Navigation in Terminal Apps"     •
       ••        for Terminal.apps with zsh using bindkey -e          ••
          ##### ====== move cursor by a char/line/word =========== #####
               # ^j: ←c                   # ⌘^j: ⤆w      ☑︎
               # ^k: ↓l                   #
               # ^i: ↑l                    #
               # ^l: c→                   # ⌘^l: w⤇      ☑︎
          ##### === go to (bol|col|eol), long delete (eol|bol) === #####
               #  ^a: bol⤟               #  ^e: ⤠eol
               #  ^y: col                  # center-of-line()
               # ⌘^a: l⌦eol  ☑︎        # ⌘^e: bol⌫l   ☑︎
          ##### ======== delete by a word/char =================== #####
               # ⌘^d: w⌦     ☑︎        # ⌘^b: ⌫w      ☑︎
               #  ^d: c⌦                  #  ^b: ⌫c
          ##### ======== reginal operation ======================= #####
               #  ^o: set-mark-command
               #  ^v: yank                  #  ^v:
               # ^[v: yank-pop           # ^⌘v:        (☑︎)
               #  ^w: copy-region       # copy-region()
               #  ^x: remove-region    # remove-region()
          ##### ======== etc =========== t.ViM :   ☑︎=============== #####
               #  ^h: run_Help            #  ^h: ⎋h      ☑︎
               #  ^m: accept-line         #  ^m: ↩︎
               #  ⎋q: Push-line           #  ^p: ⎋q     ☑︎
               #  ⎋': Quote-line          #  ^q: ⎋'      ☑︎
               #  ^_: undo                  #  ^u: ^_        ☑︎
          ##### ============================================== #####




        『環境』


        • OS: Mojave Ver. 10.14.4
        • Terminal: Version 2.9.4 (421.1.1)
        • zsh 5.7.1 (x86_64-apple-darwin18.2.0)


        『感想』

        1. 初めて「bindkey」にトライできて楽しかった。まるで小学生のころ、「ナイフで鉛筆削り」をした時のことを思い出しました...。お陰で、タッチ・タイピングが戻ってきた。
        2. Viのvicmdモードに惹かれるが、いっそのこと別途に「⎋」,「^」,「Fn」などを「CapsLock」のようなトグル型にして従来のキーボードと併用するとよいかも。もう少し体力が回復したら工作したいなぁ!
        3. 今回Karabiner-Elementsを気楽に操作できたのは、「App Store」で購入した「JSON Editor.app」を、通常のエディタの隣に小さく表示するごとに、エラー部分を表示してくれるのだ。お陰で「,」のエラーは簡単に検知は容易!次に多かったのは単なるタッチ・ミスの検出にも大活躍。
        4. でも、.jsonは長くて読みづらい。孫悟空のような名前のプリプロセッサーのようなシステムがあるらしいので、調べてみたい。

        [この記事の履歴]

        1. 開始 2019-12-14

        注目の投稿

        Terminalでの、なんちゃってViモドキ

        近頃、ようやくKarabiner-Elementsに慣れてきたので、 Terminalで動作する「擬似Vi-Mode」を作って見たので、ご紹介します。 『概要』 「擬似Vi-Mode」の所以は、方向キー「←↓↑→」を通常の「hjkl」ではなくて「jkil」としました。これ...