blog/content/posts/vim-stt.md

3.3 KiB

title date asciinema tags categories
Vim Send To Terminal 2023-07-15T20:18:03-04:00 true
vim
development

Semi automatic scripts with vim :terminal

Sometimes, fully automating a task is not worth the effort. So I end up running a set of commands usually from a cheat sheet text file slightly modifying the arguments each time. I used the below in vim to send line under cursor to vim's :terminal open in a split

:call term_list()[0]->term_sendkeys(getline('.') .. "\<CR>")

To send another line @: and then for every other line @@. This works because, last command run is stored in register : and the last macro executed using @ is stored in register @.

To do the same another day, :call te<UP arrow> to recall from vim's command history. Or better, add a function and mapping.

def SendToTerminal()
    if term_list()->empty()
        echomsg "No Terminal windows found"
        return
    endif
    terms[0]->term_sendkeys(getline(.) .. "\<CR>")
enddef

nnoremap <silent><leader>s call SendToTerminal()<CR>

More cool features

For most use-cases that was enough. However added a few more nice features which is very helpful when you need it

  1. Support sending a range of lines (visual selection) at a time. E.g. Send a function block to a python shell
  2. Add a delay between lines in milliseconds. This is useful when the previous command reads from standard input and takes some time to complete
  3. Support sending ctrl characters like ctrl d, ctrl c etc,
vim9script
def SendRangeToTerminal(start_line: number, end_line: number, _ = 0)
    const terms = term_list()
    if terms->empty()
        echomsg "No Terminal windows found"
        return
    endif
    var line_num = start_line
    for line in getline(start_line, end_line)
        line_num += 1
        const spl_cmd = line->matchlist('\vVIMST (sleep|ctrl) ([0-9]+|[a-z])?')
        if !spl_cmd->empty()
            const [_, cmd, arg1; _] = spl_cmd
            if cmd == "sleep"
                timer_start(arg1->str2nr(), funcref('SendRangeToTerminal', [line_num, end_line]))
                return
            elseif cmd == "ctrl"
                terms[0]->term_sendkeys(nr2char(arg1->char2nr() - 96))
                continue
            endif
        endif
        terms[0]->term_sendkeys(line .. "\<CR>")
    endfor
enddef

command -range -bar SendToTerm :call <SID>SendRangeToTerminal(<line1>, <line2>)
vnoremap <silent><leader>s :SendToTerm<CR>
nnoremap <silent><leader>s :SendToTerm<CR>

More cool mapping

Wouldn't it be nice to just double-click commands with mouse? Like a simple GUI! ;)

nnoremap <silent><2-LeftMouse> :SendToTerm<CR>

Or just Enter?

nnoremap <buffer> <CR> :SendToTerm \| norm j<CR>

Of course, mapping Enter for any file is a bad idea. So lets just map in our cheat file

autocmd BufNewFile,BufRead cheat.sh {
    nnoremap <buffer> <silent><2-LeftMouse> :SendToTerm<CR>
    nnoremap <buffer> <CR> :SendToTerm \| norm j<CR>
}

Demo

{{< asciinema key="vimstt" >}}

Getting the scripts

What about neovim/tmux/screen?

I am not the only one who thought about this. See vim-slime since 2007. However it does not support adding a sleep or sending arbitrary ctrl characters without additional mappings