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
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:-}"
mode="command"
statusMessage=""
@@ -8,7 +15,11 @@ renderedMessage=$'\001'
pendingInput=""
pendingCommand=""
commandCount=""
lastChangeKeys=""
insertChangeKeys=""
replayKeysRemaining=0
hasYank=0
yankType="line"
dirty=0
cursorRow=0
cursorCol=0
@@ -85,9 +96,14 @@ file_info() {
}
draw_message() {
if [[ "$statusMessage" != "$renderedMessage" ]]; then
printf '\033[%s;1H%-*.*s' "$screenRows" "$screenCols" "$screenCols" "$statusMessage"
renderedMessage="$statusMessage"
local message="$statusMessage"
if [[ -n "$pendingCommand" || -n "$commandCount" ]]; then
message="${commandCount}${pendingCommand}"
fi
if [[ "$message" != "$renderedMessage" ]]; then
printf '\033[%s;1H%-*.*s' "$screenRows" "$screenCols" "$screenCols" "$message"
renderedMessage="$message"
fi
}
@@ -109,6 +125,9 @@ draw_screen() {
done
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))"
}
@@ -117,6 +136,7 @@ read_key() {
if [[ -n "$pendingInput" ]]; then
key="${pendingInput:0:1}"
pendingInput="${pendingInput:1}"
((replayKeysRemaining > 0)) && ((replayKeysRemaining--))
return 0
fi
@@ -340,6 +360,53 @@ move_word_end_forward() {
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() {
if [[ -n "$commandCount" ]]; then
printf '%s' "$commandCount"
@@ -348,6 +415,44 @@ command_count() {
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() {
undoLines=("${lines[@]}")
undoCursorRow="$cursorRow"
@@ -491,6 +596,43 @@ delete_range() {
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() {
local startRow="$cursorRow"
local startCol="$cursorCol"
@@ -509,6 +651,7 @@ delete_word_forward() {
local startCol="$cursorCol"
save_undo
move_word_forward "$count"
yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
}
@@ -518,9 +661,50 @@ delete_word_backward() {
local startCol="$cursorCol"
save_undo
move_word_backward "$count"
yank_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() {
local count="$1"
local startRow="$cursorRow"
@@ -595,6 +779,150 @@ replace_chars() {
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() {
lines=("${lines[@]:0:cursorRow+1}" "" "${lines[@]:cursorRow+1}")
((cursorRow++))
@@ -617,6 +945,7 @@ yank_lines() {
((actual > available)) && actual="$available"
((actual < 1)) && actual=1
yankType="line"
yankBuffer=("${lines[@]:cursorRow:actual}")
hasYank=1
statusMessage="$actual line yanked"
@@ -631,6 +960,7 @@ delete_lines() {
((actual < 1)) && actual=1
save_undo
yankType="line"
yankBuffer=("${lines[@]:cursorRow:actual}")
hasYank=1
@@ -669,6 +999,55 @@ paste_line_below() {
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() {
local target="$1"
[[ -z "$target" ]] && return 1
@@ -756,6 +1135,7 @@ handle_command_key() {
if [[ -n "$pendingCommand" ]]; then
count="$(command_count)"
if [[ "$pendingCommand" == "r" ]]; then
record_last_change "${commandCount}${pendingCommand}${key}"
replace_chars "$count" "$key"
pendingCommand=""
commandCount=""
@@ -763,27 +1143,54 @@ handle_command_key() {
fi
case "${pendingCommand}${key}" in
dd)
record_last_change "${commandCount}${pendingCommand}${key}"
delete_lines "$count"
;;
d'$')
record_last_change "${commandCount}${pendingCommand}${key}"
save_undo
yank_to_end_of_line
delete_to_end_of_line
;;
dw)
record_last_change "${commandCount}${pendingCommand}${key}"
delete_word_forward "$count"
;;
db)
record_last_change "${commandCount}${pendingCommand}${key}"
delete_word_backward "$count"
;;
y'$')
yank_to_end_of_line
;;
yw)
yank_word_forward "$count"
;;
yb)
yank_word_backward "$count"
;;
cc)
start_insert_change "${commandCount}${pendingCommand}${key}"
change_current_line
;;
c'$')
start_insert_change "${commandCount}${pendingCommand}${key}"
change_to_end_of_line
;;
cw)
start_insert_change "${commandCount}${pendingCommand}${key}"
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)
yank_lines "$count"
;;
@@ -810,10 +1217,12 @@ handle_command_key() {
fi
;;
i)
start_insert_change "${commandCount}${key}"
commandCount=""
enter_insert_mode
;;
a)
start_insert_change "${commandCount}${key}"
commandCount=""
if ((cursorCol < $(line_length "$cursorRow"))); then
((cursorCol++))
@@ -821,11 +1230,13 @@ handle_command_key() {
enter_insert_mode
;;
I)
start_insert_change "${commandCount}${key}"
commandCount=""
cursorCol=0
enter_insert_mode
;;
A)
start_insert_change "${commandCount}${key}"
commandCount=""
cursorCol="$(line_length "$cursorRow")"
enter_insert_mode
@@ -865,27 +1276,46 @@ handle_command_key() {
move_word_backward "$count"
commandCount=""
;;
G)
if [[ -n "$commandCount" ]]; then
move_to_line "$commandCount"
else
move_to_line "${#lines[@]}"
fi
commandCount=""
;;
'^')
commandCount=""
move_to_first_nonblank
;;
'$')
commandCount=""
cursorCol="$(line_length "$cursorRow")"
;;
x)
count="$(command_count)"
record_last_change "${commandCount}${key}"
save_undo
yank_range "$cursorRow" "$cursorCol" "$cursorRow" "$((cursorCol + count))"
for ((i = 0; i < count; i++)); do delete_under_cursor; done
commandCount=""
;;
D)
record_last_change "${commandCount}${key}"
commandCount=""
save_undo
yank_to_end_of_line
delete_to_end_of_line
;;
C)
start_insert_change "${commandCount}${key}"
commandCount=""
change_to_end_of_line
;;
s)
count="$(command_count)"
start_insert_change "${commandCount}${key}"
yank_range "$cursorRow" "$cursorCol" "$cursorRow" "$((cursorCol + count))"
substitute_chars "$count"
commandCount=""
;;
@@ -893,27 +1323,61 @@ handle_command_key() {
pendingCommand="$key"
statusMessage=""
;;
d|y|c)
d|y|c|g)
pendingCommand="$key"
statusMessage=""
;;
p)
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=""
;;
o)
count="$(command_count)"
start_insert_change "${commandCount}${key}"
save_undo
for ((i = 0; i < count; i++)); do open_line_below; done
commandCount=""
;;
O)
count="$(command_count)"
start_insert_change "${commandCount}${key}"
save_undo
for ((i = 0; i < count; i++)); do open_line_above; done
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=""
prompt_command
@@ -940,38 +1404,47 @@ handle_insert_key() {
local key="$1"
case "$key" in
$'\033')
finish_insert_change
mode="command"
insertUndoSaved=0
statusMessage=""
;;
$'\033[D')
insertChangeKeys+="$key"
move_left
;;
$'\033[C')
insertChangeKeys+="$key"
move_right
;;
$'\033[A')
insertChangeKeys+="$key"
move_up
;;
$'\033[B')
insertChangeKeys+="$key"
move_down
;;
$'\r'|$'\n')
insertChangeKeys+="$key"
split_line
;;
$'\177'|$'\b')
insertChangeKeys+="$key"
backspace_insert
;;
$'\007')
file_info
;;
$'\003')
finish_insert_change
mode="command"
insertUndoSaved=0
statusMessage="Use :q to quit"
;;
*)
if [[ "$key" =~ [[:print:]] ]]; then
insertChangeKeys+="$key"
insert_char "$key"
fi
;;