Skip to content

Instantly share code, notes, and snippets.

@gitmpr
Created October 8, 2025 12:45
Show Gist options
  • Select an option

  • Save gitmpr/063a084eb3c69247db25df703b58ff4f to your computer and use it in GitHub Desktop.

Select an option

Save gitmpr/063a084eb3c69247db25df703b58ff4f to your computer and use it in GitHub Desktop.
# ===== Bash: edit current command line in (Neo)Vim with cursor round-trip =====
# Key: F12 -> edit without executing, then return to prompt with cursor restored.
# Pick an editor (prefer VISUAL, then EDITOR, then nvim, vim, vi)
__bash_edit__pick_editor() {
if [[ -n "$VISUAL" ]]; then echo "$VISUAL"; return; fi
if [[ -n "$EDITOR" ]]; then echo "$EDITOR"; return; fi
if command -v nvim >/dev/null 2>&1; then echo nvim; return; fi
if command -v vim >/dev/null 2>&1; then echo vim; return; fi
echo vi
}
# Convert READLINE_POINT to 1-based (line, col) within READLINE_LINE
__bash_edit__point_to_lc() {
local buf="$READLINE_LINE" point="$READLINE_POINT"
local before="${buf:0:point}"
# count newlines in "before" for line number
local line=$(( $(printf '%s' "$before" | grep -o $'\n' | wc -l) + 1 ))
# column is bytes since last newline, 1-based
local lastnl="${before##*$'\n'}"
local col=$(( ${#lastnl} + 1 ))
printf '%d %d\n' "$line" "$col"
}
# Clamp integer to [0, len]
__bash_edit__clamp() {
local val="$1" len="$2"
(( val < 0 )) && val=0
(( val > len )) && val=$len
printf '%d\n' "$val"
}
# Core: edit without executing, with cursor round-trip
__bash_edit_command_buffer_safe() {
local tmpfile curfile editor line col
tmpfile=$(mktemp /tmp/bash-edit-buffer.XXXXXX) || return
curfile=$(mktemp /tmp/bash-edit-cursor.XXXXXX) || { rm -f "$tmpfile"; return; }
# dump current buffer
printf '%s' "$READLINE_LINE" > "$tmpfile"
read -r line col < <(__bash_edit__point_to_lc)
editor=$(__bash_edit__pick_editor)
# pass sidecar path via env var
BASH_EDIT_CURSOR_FILE="$curfile"
if command -v nvim >/dev/null 2>&1 && [[ "$editor" == *nvim* ]]; then
BASH_EDIT_CURSOR_FILE="$curfile" \
"$editor" \
-c "augroup BashEditBuffer | autocmd! | autocmd CursorMoved,InsertLeave,BufLeave,VimLeavePre * call writefile([string(line2byte(line('.')) + col('.') - 2)], getenv('BASH_EDIT_CURSOR_FILE')) | augroup END" \
"+call cursor($line, $col)" \
-- "$tmpfile"
elif command -v vim >/dev/null 2>&1 && [[ "$editor" == *vim* ]]; then
BASH_EDIT_CURSOR_FILE="$curfile" \
"$editor" \
-c "augroup BashEditBuffer | autocmd! | autocmd CursorMoved,InsertLeave,BufLeave,VimLeavePre * call writefile([string(line2byte(line('.')) + col('.') - 2)], expand('\$BASH_EDIT_CURSOR_FILE')) | augroup END" \
"+call cursor($line, $col)" \
-- "$tmpfile"
else
# unknown editor: position by line only, no cursor return
"$editor" "+$line" -- "$tmpfile"
fi
# bring edited text back
READLINE_LINE=$(cat -- "$tmpfile")
# restore cursor if sidecar has a numeric index
if [[ -s "$curfile" ]]; then
local idx len
idx=$(tr -d '\n\r' < "$curfile")
if [[ "$idx" =~ ^[0-9]+$ ]]; then
len=${#READLINE_LINE}
idx=$(__bash_edit__clamp "$idx" "$len")
READLINE_POINT=$idx
else
READLINE_POINT=${#READLINE_LINE}
fi
else
READLINE_POINT=${#READLINE_LINE}
fi
rm -f -- "$tmpfile" "$curfile"
}
# Key bindings:
# F12 is typically unbound in readline. If your terminal differs, adjust the escape.
bind -x '"\e[24~":__bash_edit_command_buffer_safe' # F12 -> edit without running
# Alt+e to edit current READLINE_LINE in $EDITOR.
bind -x '"\ee":__bash_edit_command_buffer_safe'
# Optional: edit then execute directly, we don't bind this to prevent accidental command execution
# This mimics what bash control+x, control+e does: the edit-and-execute readline function, which is a bad bash design quirk
__bash_edit_command_buffer_exec() {
__bash_edit_command_buffer_safe
local cmd="$READLINE_LINE"
READLINE_LINE=
READLINE_POINT=0
history -s "$cmd"
builtin eval "$cmd"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment