-
Notifications
You must be signed in to change notification settings - Fork 0
/
sneak.bash
221 lines (165 loc) · 4.59 KB
/
sneak.bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
SNEAK_VERSION="0.1.0"
if [[ ${BASH_VERSINFO[0]} -lt 4 ]]; then
echo "sneak.bash requires Bash 4+, you currently have $BASH_VERSION" >&2
fi
declare __sneak_last_search
__sneak() {
local search
local direction="$1"
local prompt="${SNEAK_PROMPT-/ }"
local num_chars="${SNEAK_NUM_CHARS:-2}"
local binding_char=$(__sneak_get_bind_char "$direction")
local num_columns="$(tput cols)"
local prompt_cursor_col=$(( ${#prompt} + $READLINE_POINT ))
local prompt_cursor_row=$(( $prompt_cursor_col / $num_columns ))
prompt_cursor_col=$(( $prompt_cursor_col - ( $prompt_cursor_row * $num_columns ) ))
if __sneak_bash_version "<=" "4.2"; then
# move up one line
tput cuu1
# erase line
tput el
fi
# save cursor (col 0)
tput sc
# re-show the current line
echo -n "${prompt}${READLINE_LINE}"
# restore cursor (col 0)
tput rc
[[ "$prompt_cursor_row" -gt 0 ]] && tput cud "$prompt_cursor_row"
[[ "$prompt_cursor_col" -gt 0 ]] && tput cuf "$prompt_cursor_col"
while true; do
local save_stty=$(stty -g)
# disable turning Ctrl-C into SIGINT
stty intr undef
# read a single key
read -rs -n1 key
local read_success=$?
stty "$save_stty"
if [[ "$read_success" -ne 0 ]]; then
search=""
break
fi
case "$key" in
$'\e'|$'\cc')
# escape and ctrl-c quits
search=""
break
;;
$binding_char)
# doubling binding char redoes last search
search="${__sneak_last_search}"
break
;;
"")
# ENTER does search with what we've got so far
break
;;
[[:print:]])
# only accept printable characters
search="$search$key"
;;
esac
if [[ "${#search}" -eq "${num_chars}" ]]; then
# we've got what we asked for, let's get down to business
break
fi
done
# restore cursor (col 0)
tput rc
# erase line
tput el
if __sneak_bash_version "<=" 4.3; then
__sneak_old_bash_restore_prompt
fi
if [[ -z "${search}" ]]; then
return
fi
# save search
__sneak_last_search="${search}"
READLINE_POINT=$(
echo "$READLINE_LINE" | awk \
-v point="$READLINE_POINT" \
-v direction="$direction" \
-v search="$search" \
'{
if (direction == "forward") {
point += index(substr($0, point+2), search);
}
else {
pos = 0
str = substr($0, 0, point + 1)
while (i = index(substr(str, pos+1), search)) {
pos += i
}
if (pos != 0) point = pos - 1
}
print point
}'
)
}
__sneak_old_bash_restore_prompt() {
local i
local numlines=$(echo "$PS1" | grep -o '\\n' | wc -l)
for (( i = 0; i < "$numlines"; i++ )); do
# move up one line
tput cuu1
# erase line
tput el
done
}
__sneak_get_bind_char() {
local uppercase_direction="$(echo "$1" | tr '[a-z]' '[A-Z]')"
local option_name="SNEAK_BINDING_${uppercase_direction}"
local option_value="${!option_name}"
if [[ ! "$option_value" =~ ^\\C-.$ ]]; then
return
fi
# extract the letter (ie: "x" from "\C-x")
# make sure it's lowercase
# get ASCII decimal code
# compute ASCII for for Ctrl-x
# convert to octal number
# print control character
# ...Bash 4.4 does allow for `qq="\\ct"; echo ${qq@E}` for expanding variables in the same way as $'...'
echo -n "${option_value:3:1}" \
| tr '[A-Z]' '[a-z]' \
| od -An -d \
| xargs -I'{}' expr '{}' - 96 \
| xargs printf "%o" \
| xargs -I'{}' printf "%b" "\0{}"
}
# Compares current BASH_VERSION, succeeds if true
# Usage: __sneak_bash_version "<" "4.4.5"
__sneak_bash_version() {
local op="$1"
if [[ ! $op =~ \<=?|\>=?|!?==? ]]; then
echo "Invalid operator: $op"
return 2
fi
local comparison=$(echo "$2" | \
awk -F. \
-v major="${BASH_VERSINFO[0]}" \
-v minor="${BASH_VERSINFO[1]}" \
-v patch="${BASH_VERSINFO[2]}" \
'{
comp_version_number = sprintf("%03d%03d%03d", $1, $2, $3)
bash_version_number = sprintf("%03d%03d%03d", major, minor, patch)
if (bash_version_number < comp_version_number) print "<"
else if (bash_version_number > comp_version_number) print ">"
else print "="
}'
)
case "$comparison" in
"<") [[ $op =~ \<=?|!= ]]; return $? ;;
">") [[ $op =~ \>=?|!= ]]; return $? ;;
"=") [[ $op =~ == ]]; return $? ;;
esac
}
__sneak_forward() {
__sneak "forward"
}
__sneak_backward() {
__sneak "backward"
}
bind -x "\"${SNEAK_BINDING_FORWARD=\C-g}\": __sneak_forward"
bind -x "\"${SNEAK_BINDING_BACKWARD=\C-t}\": __sneak_backward"