More features added. License file added.

This commit is contained in:
Storm Dragon
2026-05-15 00:12:34 -04:00
parent 8464427637
commit 3299a15348
2 changed files with 492 additions and 5 deletions
+14
View File
@@ -0,0 +1,14 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
+478 -5
View File
@@ -1,6 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -uo pipefail set -uo pipefail
# Released under the terms of the WTFPL license https://www.wtfpl.net/
# Bim was partially created with bim
# Does that make it a recursive text editor?
# Behold! Vim in bash. Has anyone else ever created something so diabolically evil?
filePath="${1:-}" filePath="${1:-}"
mode="command" mode="command"
statusMessage="" statusMessage=""
@@ -8,7 +15,11 @@ renderedMessage=$'\001'
pendingInput="" pendingInput=""
pendingCommand="" pendingCommand=""
commandCount="" commandCount=""
lastChangeKeys=""
insertChangeKeys=""
replayKeysRemaining=0
hasYank=0 hasYank=0
yankType="line"
dirty=0 dirty=0
cursorRow=0 cursorRow=0
cursorCol=0 cursorCol=0
@@ -85,9 +96,14 @@ file_info() {
} }
draw_message() { draw_message() {
if [[ "$statusMessage" != "$renderedMessage" ]]; then local message="$statusMessage"
printf '\033[%s;1H%-*.*s' "$screenRows" "$screenCols" "$screenCols" "$statusMessage" if [[ -n "$pendingCommand" || -n "$commandCount" ]]; then
renderedMessage="$statusMessage" message="${commandCount}${pendingCommand}"
fi
if [[ "$message" != "$renderedMessage" ]]; then
printf '\033[%s;1H%-*.*s' "$screenRows" "$screenCols" "$screenCols" "$message"
renderedMessage="$message"
fi fi
} }
@@ -109,6 +125,9 @@ draw_screen() {
done done
draw_message draw_message
if [[ "$mode" == "command" ]] && ((${#lines[cursorRow]} > 0 && cursorCol >= ${#lines[cursorRow]})); then
cursorCol=$(( ${#lines[cursorRow]} - 1 ))
fi
printf '\033[%s;%sH' "$((cursorRow - topRow + 1))" "$((cursorCol + 1))" printf '\033[%s;%sH' "$((cursorRow - topRow + 1))" "$((cursorCol + 1))"
} }
@@ -117,6 +136,7 @@ read_key() {
if [[ -n "$pendingInput" ]]; then if [[ -n "$pendingInput" ]]; then
key="${pendingInput:0:1}" key="${pendingInput:0:1}"
pendingInput="${pendingInput:1}" pendingInput="${pendingInput:1}"
((replayKeysRemaining > 0)) && ((replayKeysRemaining--))
return 0 return 0
fi fi
@@ -340,6 +360,53 @@ move_word_end_forward() {
done done
} }
move_word_end_backward() {
local count="$1"
local i row col char
for ((i = 0; i < count; i++)); do
row="$cursorRow"
col="$cursorCol"
if ((row == 0 && col == 0)); then
break
fi
step_position_backward row col
while ((row > 0 || col > 0)); do
if ((col >= ${#lines[row]})); then
step_position_backward row col
continue
fi
char="${lines[row]:col:1}"
! is_blank_char "$char" && break
step_position_backward row col
done
cursorRow="$row"
cursorCol="$col"
done
}
move_to_first_nonblank() {
local line="${lines[cursorRow]}"
local col=0
while ((col < ${#line})); do
if ! is_blank_char "${line:col:1}"; then
break
fi
((col++))
done
cursorCol="$col"
}
move_to_line() {
local lineNumber="$1"
((lineNumber < 1)) && lineNumber=1
((lineNumber > ${#lines[@]})) && lineNumber="${#lines[@]}"
cursorRow=$((lineNumber - 1))
clamp_cursor
}
command_count() { command_count() {
if [[ -n "$commandCount" ]]; then if [[ -n "$commandCount" ]]; then
printf '%s' "$commandCount" printf '%s' "$commandCount"
@@ -348,6 +415,44 @@ command_count() {
fi fi
} }
record_last_change() {
local keys="$1"
if ((replayKeysRemaining == 0)); then
lastChangeKeys="$keys"
fi
}
start_insert_change() {
local keys="$1"
if ((replayKeysRemaining == 0)); then
insertChangeKeys="$keys"
fi
}
finish_insert_change() {
if ((replayKeysRemaining == 0 && ${#insertChangeKeys} > 0)); then
lastChangeKeys="${insertChangeKeys}"$'\033'
fi
insertChangeKeys=""
}
repeat_last_change() {
local count="$1"
local repeatedKeys=""
local i
if [[ -z "$lastChangeKeys" ]]; then
statusMessage="No change to repeat"
return
fi
for ((i = 0; i < count; i++)); do
repeatedKeys+="$lastChangeKeys"
done
pendingInput="${repeatedKeys}${pendingInput}"
replayKeysRemaining=$((replayKeysRemaining + ${#repeatedKeys}))
statusMessage=""
}
save_undo() { save_undo() {
undoLines=("${lines[@]}") undoLines=("${lines[@]}")
undoCursorRow="$cursorRow" undoCursorRow="$cursorRow"
@@ -491,6 +596,43 @@ delete_range() {
clamp_cursor clamp_cursor
} }
line_end_for_operator() {
local row="$1"
printf '%s' "${#lines[row]}"
}
yank_range() {
local startRow="$1"
local startCol="$2"
local endRow="$3"
local endCol="$4"
local comparison row
comparison="$(compare_positions "$startRow" "$startCol" "$endRow" "$endCol")"
if [[ "$comparison" == "0" ]]; then
return 1
elif [[ "$comparison" == "1" ]]; then
local tempRow="$startRow"
local tempCol="$startCol"
startRow="$endRow"
startCol="$endCol"
endRow="$tempRow"
endCol="$tempCol"
fi
yankType="char"
if ((startRow == endRow)); then
yankBuffer=("${lines[startRow]:startCol:endCol-startCol}")
else
yankBuffer=("${lines[startRow]:startCol}")
for ((row = startRow + 1; row < endRow; row++)); do
yankBuffer+=("${lines[row]}")
done
yankBuffer+=("${lines[endRow]:0:endCol}")
fi
hasYank=1
}
delete_to_end_of_line() { delete_to_end_of_line() {
local startRow="$cursorRow" local startRow="$cursorRow"
local startCol="$cursorCol" local startCol="$cursorCol"
@@ -509,6 +651,7 @@ delete_word_forward() {
local startCol="$cursorCol" local startCol="$cursorCol"
save_undo save_undo
move_word_forward "$count" move_word_forward "$count"
yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol" delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
} }
@@ -518,9 +661,50 @@ delete_word_backward() {
local startCol="$cursorCol" local startCol="$cursorCol"
save_undo save_undo
move_word_backward "$count" move_word_backward "$count"
yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol" delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
} }
yank_word_forward() {
local count="$1"
local startRow="$cursorRow"
local startCol="$cursorCol"
move_word_forward "$count"
if yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"; then
statusMessage="text yanked"
else
statusMessage="Nothing to yank"
fi
cursorRow="$startRow"
cursorCol="$startCol"
}
yank_word_backward() {
local count="$1"
local startRow="$cursorRow"
local startCol="$cursorCol"
move_word_backward "$count"
if yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"; then
statusMessage="text yanked"
else
statusMessage="Nothing to yank"
fi
cursorRow="$startRow"
cursorCol="$startCol"
}
yank_to_end_of_line() {
local startRow="$cursorRow"
local startCol="$cursorCol"
local endCol
endCol="$(line_end_for_operator "$startRow")"
if yank_range "$startRow" "$startCol" "$startRow" "$endCol"; then
statusMessage="text yanked"
else
statusMessage="Nothing to yank"
fi
}
change_word() { change_word() {
local count="$1" local count="$1"
local startRow="$cursorRow" local startRow="$cursorRow"
@@ -595,6 +779,150 @@ replace_chars() {
statusMessage="" statusMessage=""
} }
join_lines() {
local count="$1"
local i nextLine
if ((cursorRow + 1 >= ${#lines[@]})); then
return
fi
save_undo
for ((i = 0; i < count && cursorRow + 1 < ${#lines[@]}; i++)); do
nextLine="${lines[cursorRow+1]}"
lines[cursorRow]="${lines[cursorRow]} ${nextLine#"${nextLine%%[![:space:]]*}"}"
lines=("${lines[@]:0:cursorRow+1}" "${lines[@]:cursorRow+2}")
done
dirty=1
statusMessage=""
}
search_from_position() {
local pattern="$1"
local direction="$2"
local startRow="$3"
local startCol="$4"
local offset row col line searchPart matchPrefix
if [[ -z "$pattern" ]]; then
return 1
fi
for ((offset = 0; offset < ${#lines[@]}; offset++)); do
if [[ "$direction" == "forward" ]]; then
row=$(( (startRow + offset) % ${#lines[@]} ))
col=0
((offset == 0)) && col="$startCol"
else
row=$(( (startRow - offset + ${#lines[@]}) % ${#lines[@]} ))
col="${#lines[row]}"
((offset == 0)) && col="$startCol"
fi
line="${lines[row]}"
if [[ "$direction" == "forward" ]]; then
((col < 0)) && col=0
((col > ${#line})) && col="${#line}"
searchPart="${line:col}"
if [[ "$searchPart" == *"$pattern"* ]]; then
matchPrefix="${searchPart%%"$pattern"*}"
cursorRow="$row"
cursorCol=$((col + ${#matchPrefix}))
((offset > 0 && row <= startRow)) && statusMessage="search wrapped"
return 0
fi
else
((col < 0)) && col=0
((col > ${#line})) && col="${#line}"
searchPart="${line:0:col}"
if [[ "$searchPart" == *"$pattern"* ]]; then
matchPrefix="${searchPart%"$pattern"*}"
cursorRow="$row"
cursorCol="${#matchPrefix}"
((offset > 0 && row >= startRow)) && statusMessage="search wrapped"
return 0
fi
fi
done
return 1
}
lastSearchPattern=""
lastSearchDirection="forward"
run_search() {
local pattern="$1"
local direction="$2"
local startRow="$cursorRow"
local startCol="$cursorCol"
if [[ "$direction" == "forward" ]]; then
((startCol++))
else
((startCol--))
fi
if search_from_position "$pattern" "$direction" "$startRow" "$startCol"; then
lastSearchPattern="$pattern"
lastSearchDirection="$direction"
[[ "$statusMessage" == "search wrapped" ]] || statusMessage=""
clamp_cursor
else
statusMessage="Pattern not found: $pattern"
fi
}
repeat_search() {
local direction="$1"
if [[ -z "$lastSearchPattern" ]]; then
statusMessage="No previous search"
return
fi
run_search "$lastSearchPattern" "$direction"
}
prompt_search() {
local direction="$1"
local prompt key pattern
if [[ "$direction" == "forward" ]]; then
prompt="/"
else
prompt="?"
fi
pattern="$prompt"
while true; do
statusMessage="$pattern"
draw_screen
read_key || return
case "$key" in
$'\033')
statusMessage=""
return
;;
$'\r'|$'\n')
break
;;
$'\177'|$'\b')
if ((${#pattern} > 1)); then
pattern="${pattern:0:${#pattern}-1}"
fi
;;
*)
if [[ "$key" =~ [[:print:]] ]]; then
pattern+="$key"
fi
;;
esac
done
pattern="${pattern#"$prompt"}"
if [[ -z "$pattern" ]]; then
statusMessage="Empty search"
return
fi
run_search "$pattern" "$direction"
}
open_line_below() { open_line_below() {
lines=("${lines[@]:0:cursorRow+1}" "" "${lines[@]:cursorRow+1}") lines=("${lines[@]:0:cursorRow+1}" "" "${lines[@]:cursorRow+1}")
((cursorRow++)) ((cursorRow++))
@@ -617,6 +945,7 @@ yank_lines() {
((actual > available)) && actual="$available" ((actual > available)) && actual="$available"
((actual < 1)) && actual=1 ((actual < 1)) && actual=1
yankType="line"
yankBuffer=("${lines[@]:cursorRow:actual}") yankBuffer=("${lines[@]:cursorRow:actual}")
hasYank=1 hasYank=1
statusMessage="$actual line yanked" statusMessage="$actual line yanked"
@@ -631,6 +960,7 @@ delete_lines() {
((actual < 1)) && actual=1 ((actual < 1)) && actual=1
save_undo save_undo
yankType="line"
yankBuffer=("${lines[@]:cursorRow:actual}") yankBuffer=("${lines[@]:cursorRow:actual}")
hasYank=1 hasYank=1
@@ -669,6 +999,55 @@ paste_line_below() {
dirty=1 dirty=1
} }
paste_char_after_cursor() {
local count="$1"
local pasteRow="$cursorRow"
local pasteCol="$cursorCol"
local line prefix suffix i insertAt
if ((hasYank == 0)); then
statusMessage="Nothing in yank buffer"
return
fi
if [[ "$yankType" == "line" ]]; then
paste_line_below "$count"
return
fi
save_undo
if ((pasteCol < ${#lines[pasteRow]})); then
((pasteCol++))
fi
for ((i = 0; i < count; i++)); do
line="${lines[pasteRow]}"
prefix="${line:0:pasteCol}"
suffix="${line:pasteCol}"
if ((${#yankBuffer[@]} == 1)); then
lines[pasteRow]="${prefix}${yankBuffer[0]}${suffix}"
cursorRow="$pasteRow"
cursorCol=$((pasteCol + ${#yankBuffer[0]} - 1))
pasteCol=$((cursorCol + 1))
else
lines[pasteRow]="${prefix}${yankBuffer[0]}"
insertAt=$((pasteRow + 1))
if ((${#yankBuffer[@]} > 2)); then
lines=("${lines[@]:0:insertAt}" "${yankBuffer[@]:1:${#yankBuffer[@]}-2}" "${lines[@]:insertAt}")
insertAt=$((insertAt + ${#yankBuffer[@]} - 2))
fi
lines=("${lines[@]:0:insertAt}" "${yankBuffer[-1]}${suffix}" "${lines[@]:insertAt}")
cursorRow="$insertAt"
cursorCol=$(( ${#yankBuffer[-1]} - 1 ))
pasteRow="$cursorRow"
pasteCol=$((cursorCol + 1))
fi
done
dirty=1
statusMessage=""
clamp_cursor
}
save_file() { save_file() {
local target="$1" local target="$1"
[[ -z "$target" ]] && return 1 [[ -z "$target" ]] && return 1
@@ -756,6 +1135,7 @@ handle_command_key() {
if [[ -n "$pendingCommand" ]]; then if [[ -n "$pendingCommand" ]]; then
count="$(command_count)" count="$(command_count)"
if [[ "$pendingCommand" == "r" ]]; then if [[ "$pendingCommand" == "r" ]]; then
record_last_change "${commandCount}${pendingCommand}${key}"
replace_chars "$count" "$key" replace_chars "$count" "$key"
pendingCommand="" pendingCommand=""
commandCount="" commandCount=""
@@ -763,27 +1143,54 @@ handle_command_key() {
fi fi
case "${pendingCommand}${key}" in case "${pendingCommand}${key}" in
dd) dd)
record_last_change "${commandCount}${pendingCommand}${key}"
delete_lines "$count" delete_lines "$count"
;; ;;
d'$') d'$')
record_last_change "${commandCount}${pendingCommand}${key}"
save_undo save_undo
yank_to_end_of_line
delete_to_end_of_line delete_to_end_of_line
;; ;;
dw) dw)
record_last_change "${commandCount}${pendingCommand}${key}"
delete_word_forward "$count" delete_word_forward "$count"
;; ;;
db) db)
record_last_change "${commandCount}${pendingCommand}${key}"
delete_word_backward "$count" delete_word_backward "$count"
;; ;;
y'$')
yank_to_end_of_line
;;
yw)
yank_word_forward "$count"
;;
yb)
yank_word_backward "$count"
;;
cc) cc)
start_insert_change "${commandCount}${pendingCommand}${key}"
change_current_line change_current_line
;; ;;
c'$') c'$')
start_insert_change "${commandCount}${pendingCommand}${key}"
change_to_end_of_line change_to_end_of_line
;; ;;
cw) cw)
start_insert_change "${commandCount}${pendingCommand}${key}"
change_word "$count" change_word "$count"
;; ;;
ge)
move_word_end_backward "$count"
;;
gg)
if [[ -n "$commandCount" ]]; then
move_to_line "$commandCount"
else
move_to_line 1
fi
;;
yy) yy)
yank_lines "$count" yank_lines "$count"
;; ;;
@@ -810,10 +1217,12 @@ handle_command_key() {
fi fi
;; ;;
i) i)
start_insert_change "${commandCount}${key}"
commandCount="" commandCount=""
enter_insert_mode enter_insert_mode
;; ;;
a) a)
start_insert_change "${commandCount}${key}"
commandCount="" commandCount=""
if ((cursorCol < $(line_length "$cursorRow"))); then if ((cursorCol < $(line_length "$cursorRow"))); then
((cursorCol++)) ((cursorCol++))
@@ -821,11 +1230,13 @@ handle_command_key() {
enter_insert_mode enter_insert_mode
;; ;;
I) I)
start_insert_change "${commandCount}${key}"
commandCount="" commandCount=""
cursorCol=0 cursorCol=0
enter_insert_mode enter_insert_mode
;; ;;
A) A)
start_insert_change "${commandCount}${key}"
commandCount="" commandCount=""
cursorCol="$(line_length "$cursorRow")" cursorCol="$(line_length "$cursorRow")"
enter_insert_mode enter_insert_mode
@@ -865,27 +1276,46 @@ handle_command_key() {
move_word_backward "$count" move_word_backward "$count"
commandCount="" commandCount=""
;; ;;
G)
if [[ -n "$commandCount" ]]; then
move_to_line "$commandCount"
else
move_to_line "${#lines[@]}"
fi
commandCount=""
;;
'^')
commandCount=""
move_to_first_nonblank
;;
'$') '$')
commandCount="" commandCount=""
cursorCol="$(line_length "$cursorRow")" cursorCol="$(line_length "$cursorRow")"
;; ;;
x) x)
count="$(command_count)" count="$(command_count)"
record_last_change "${commandCount}${key}"
save_undo save_undo
yank_range "$cursorRow" "$cursorCol" "$cursorRow" "$((cursorCol + count))"
for ((i = 0; i < count; i++)); do delete_under_cursor; done for ((i = 0; i < count; i++)); do delete_under_cursor; done
commandCount="" commandCount=""
;; ;;
D) D)
record_last_change "${commandCount}${key}"
commandCount="" commandCount=""
save_undo save_undo
yank_to_end_of_line
delete_to_end_of_line delete_to_end_of_line
;; ;;
C) C)
start_insert_change "${commandCount}${key}"
commandCount="" commandCount=""
change_to_end_of_line change_to_end_of_line
;; ;;
s) s)
count="$(command_count)" count="$(command_count)"
start_insert_change "${commandCount}${key}"
yank_range "$cursorRow" "$cursorCol" "$cursorRow" "$((cursorCol + count))"
substitute_chars "$count" substitute_chars "$count"
commandCount="" commandCount=""
;; ;;
@@ -893,27 +1323,61 @@ handle_command_key() {
pendingCommand="$key" pendingCommand="$key"
statusMessage="" statusMessage=""
;; ;;
d|y|c) d|y|c|g)
pendingCommand="$key" pendingCommand="$key"
statusMessage="" statusMessage=""
;; ;;
p) p)
count="$(command_count)" count="$(command_count)"
paste_line_below "$count" record_last_change "${commandCount}${key}"
paste_char_after_cursor "$count"
commandCount=""
;;
J)
count="$(command_count)"
record_last_change "${commandCount}${key}"
join_lines "$count"
commandCount="" commandCount=""
;; ;;
o) o)
count="$(command_count)" count="$(command_count)"
start_insert_change "${commandCount}${key}"
save_undo save_undo
for ((i = 0; i < count; i++)); do open_line_below; done for ((i = 0; i < count; i++)); do open_line_below; done
commandCount="" commandCount=""
;; ;;
O) O)
count="$(command_count)" count="$(command_count)"
start_insert_change "${commandCount}${key}"
save_undo save_undo
for ((i = 0; i < count; i++)); do open_line_above; done for ((i = 0; i < count; i++)); do open_line_above; done
commandCount="" commandCount=""
;; ;;
/)
commandCount=""
prompt_search forward
;;
'?')
commandCount=""
prompt_search backward
;;
n)
commandCount=""
repeat_search "$lastSearchDirection"
;;
N)
commandCount=""
if [[ "$lastSearchDirection" == "forward" ]]; then
repeat_search backward
else
repeat_search forward
fi
;;
'.')
count="$(command_count)"
commandCount=""
repeat_last_change "$count"
;;
:) :)
commandCount="" commandCount=""
prompt_command prompt_command
@@ -940,38 +1404,47 @@ handle_insert_key() {
local key="$1" local key="$1"
case "$key" in case "$key" in
$'\033') $'\033')
finish_insert_change
mode="command" mode="command"
insertUndoSaved=0 insertUndoSaved=0
statusMessage="" statusMessage=""
;; ;;
$'\033[D') $'\033[D')
insertChangeKeys+="$key"
move_left move_left
;; ;;
$'\033[C') $'\033[C')
insertChangeKeys+="$key"
move_right move_right
;; ;;
$'\033[A') $'\033[A')
insertChangeKeys+="$key"
move_up move_up
;; ;;
$'\033[B') $'\033[B')
insertChangeKeys+="$key"
move_down move_down
;; ;;
$'\r'|$'\n') $'\r'|$'\n')
insertChangeKeys+="$key"
split_line split_line
;; ;;
$'\177'|$'\b') $'\177'|$'\b')
insertChangeKeys+="$key"
backspace_insert backspace_insert
;; ;;
$'\007') $'\007')
file_info file_info
;; ;;
$'\003') $'\003')
finish_insert_change
mode="command" mode="command"
insertUndoSaved=0 insertUndoSaved=0
statusMessage="Use :q to quit" statusMessage="Use :q to quit"
;; ;;
*) *)
if [[ "$key" =~ [[:print:]] ]]; then if [[ "$key" =~ [[:print:]] ]]; then
insertChangeKeys+="$key"
insert_char "$key" insert_char "$key"
fi fi
;; ;;