Repository navigation
Expand file tree
/
Copy pathiskios.bash
More file actions
856 lines (814 loc) · 30.6 KB
/
Copy pathiskios.bash
File metadata and controls
856 lines (814 loc) · 30.6 KB
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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
# iskios.bash - fish-style autosuggestions for GNU bash, in pure bash.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Ioannis Anagnostakis (r-tz)
#
# ίσκιος: shadow. The suggestion is the shadow of what you are about
# to type.
#
# As you type, the rest of the most recent matching history entry is
# drawn in gray after the cursor. If history has nothing, the word under
# the cursor is completed: command names, files and directories (also
# after ~/ and $VAR/), $VARIABLE names. Like fish, history entries whose
# command no longer exists, or `cd` into a directory that is gone, are
# skipped.
#
# Right, End, C-f, C-e accept the whole suggestion
# Alt-Right, Ctrl-Right, M-f accept the next word of it
# Enter runs only what you typed
# everything else works as before (Tab, C-r, M-., C-w ...)
#
# Install: source it at the END of ~/.bashrc (after anything that sets
# PROMPT_COMMAND, key bindings or `set -o vi`):
#
# [[ -f /usr/share/iskios/iskios.bash ]] && . /usr/share/iskios/iskios.bash
#
# Settings (environment variables, can be changed any time):
# ISKIOS_COLOR escape sequence for the gray text (default $'\e[90m',
# $'\e[1;30m' on the Linux console)
# ISKIOS_SOURCES "history complete" (default), "history" or "complete"
#
# Command: iskios on | off | reload | status
#
# Emacs mode and vi insert mode. bash 4.4 or newer (tested on 5.1, 5.2,
# 5.3). No external programs, no background jobs.
#
# How it works: printable keys (and the cursor keys) are bound with
# `bind -x` to shell functions that edit READLINE_LINE themselves.
# Before bash redraws the line after a `bind -x` command, the function
# paints the ghost text at the spot where the typed text will end.
# Readline's forced redraw writes only the prompt and the text, it does
# not clear to end of line, so the ghost stays on screen. Native readline
# commands that could draw over a visible ghost (Tab, M-., C-y, history
# search, ...) are run through a small macro that erases the ghost first,
# and only while a ghost is visible, so commands that look at "what was
# the previous command" (second Tab, M-. M-., C-y M-y) keep working.
[[ $- == *i* ]] || return 0
[[ -n ${BASH_VERSION-} ]] || return 0
(( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )) || return 0
case ${TERM-dumb} in dumb|'') return 0 ;; esac
[[ -t 0 && -t 2 ]] || return 0
# sourced again: put the keys back the way they were before the first time
if declare -F _isk_unbind >/dev/null && ((${_isk_on:-0})); then
_isk_unbind
_isk_unhook
fi
if [[ $TERM == linux ]]; then
: "${ISKIOS_COLOR=$'\e[1;30m'}" # dark gray on the Linux console
else
: "${ISKIOS_COLOR=$'\e[90m'}"
fi
: "${ISKIOS_SOURCES=history complete}"
# ---------------------------------------------------------------- state
declare -g _isk_H=$'\n\t' # history cache, see _isk_hist_add
declare -g _isk_hre='[ *]'
declare -gi _isk_hready=0
declare -g _isk_hlast= # newest entry we know about
declare -g _isk_sug= # suggestion suffix for _isk_sugline
declare -g _isk_sugline=
declare -gi _isk_shown=0 # a ghost is on screen right now
declare -gi _isk_srow=0 _isk_scol=0
declare -gi _isk_pw=0 _isk_pwok=0
declare -gi _isk_ps=1 # 1: primary prompt, 2: PS2 / read -e / other
declare -g _isk_lp= _isk_lr= # last history prefix searched and its result
declare -gA _isk_memo=()
declare -gA _isk_okw=() # command word -> 1/0 exists (per prompt)
declare -g _isk_C=$'\n' # command names, best first
declare -g _isk_Cpath=
declare -gi _isk_Cstamp=-100000
declare -gi _isk_dyn=0 # dynamic "erase first" bindings are active
declare -ga _isk_dynks=() _isk_dynfn=()
declare -gA _isk_priv=() # readline function -> private key sequence
declare -gA _isk_orig=() # keymap US keyseq -> function it had before us
declare -ga _isk_chr=()
declare -gi _isk_on=0 _isk_npriv=2 _isk_echoctl=1
declare -g _isk_w= _isk_t= _isk_m= _isk_q= _isk_ch= _isk_rb= _isk_found=
declare -g _isk_ell='~'
case ${LC_ALL:-${LC_CTYPE:-${LANG-}}} in *[Uu][Tt][Ff]-8*|*[Uu][Tt][Ff]8*) _isk_ell=$'…' ;; esac
_isk_PRE='\e[990~' # erase ghost
_isk_PREA='\e[988~' # erase ghost, a line is being accepted
_isk_POST='\e[991~' # recompute and draw ghost
_isk_maps='emacs vi-insert'
_isk_US=$'\x1f' # separates keymap and key sequence in the tables
# ---------------------------------------------------------------- width
# Column widths the way readline draws text: ^X for control chars, tabs
# to the next multiple of 8, East Asian wide chars and most emoji take 2
# columns, combining marks 0. ASCII-only strings take the fast path.
# _isk_cw CHAR COL -> _isk_w (one char that is not printable ASCII)
_isk_cw() {
local -i c
printf -v c %d "'$1"
if ((c == 9)); then _isk_w=$((8 - $2 % 8))
elif ((c < 32 || c == 127)); then _isk_w=2
elif ((c >= 0x300 && c <= 0x36f || c >= 0x200b && c <= 0x200f ||
c >= 0xfe00 && c <= 0xfe0f || c >= 0x20d0 && c <= 0x20ff ||
c >= 0x1ab0 && c <= 0x1aff || c >= 0x1dc0 && c <= 0x1dff ||
c >= 0xfe20 && c <= 0xfe2f)); then _isk_w=0
elif ((c >= 0x1100 && c <= 0x115f || c >= 0x2e80 && c <= 0xa4cf && c != 0x303f ||
c >= 0xac00 && c <= 0xd7a3 || c >= 0xf900 && c <= 0xfaff ||
c >= 0xfe30 && c <= 0xfe4f || c >= 0xff00 && c <= 0xff60 ||
c >= 0xffe0 && c <= 0xffe6 || c >= 0x1f300 && c <= 0x1f64f ||
c >= 0x1f900 && c <= 0x1f9ff || c >= 0x20000 && c <= 0x3fffd)); then _isk_w=2
else _isk_w=1
fi
}
# _isk_width STRING [STARTCOL] -> _isk_w
_isk_width() {
local s=$1
if [[ -z ${s//[ -~]/} ]]; then _isk_w=${#s}; return; fi
local -i col=${2:-0} start=${2:-0} i n=${#s}
local ch
for ((i = 0; i < n; i++)); do
ch=${s:i:1}
case $ch in [\ -~]) ((col++)); continue ;; esac
_isk_cw "$ch" "$col"
((col += _isk_w))
done
_isk_w=$((col - start))
}
# _isk_trunc STRING MAXCOLS [STARTCOL] -> _isk_t = longest prefix that fits
_isk_trunc() {
local s=$1
local -i max=$2 col=${3:-0} start=${3:-0} i n=${#1}
if [[ -z ${s//[ -~]/} ]]; then _isk_t=${s:0:max}; return; fi
local ch
for ((i = 0; i < n; i++)); do
ch=${s:i:1}
case $ch in [\ -~]) _isk_w=1 ;; *) _isk_cw "$ch" "$col" ;; esac
((col + _isk_w - start > max)) && break
((col += _isk_w))
done
_isk_t=${s:0:i}
}
# ---------------------------------------------------------------- prompt
# Width of the last line of the prompt readline is showing. ${PS1@P}
# gives \[ \] as \001 \002; everything between them is invisible.
_isk_promptw() {
local p
if ((_isk_ps == 1)); then p=${PS1@P}; else p=${PS2@P}; fi
local pre rest
while [[ $p == *$'\001'* ]]; do
pre=${p%%$'\001'*} rest=${p#*$'\001'}
[[ $rest == *$'\002'* ]] || { p=$pre; break; }
p=$pre${rest#*$'\002'}
done
p=${p//$'\002'/}
p=${p##*$'\n'}
_isk_width "$p"
_isk_pw=$_isk_w _isk_pwok=1
}
# ---------------------------------------------------------------- history
# Regex-escape $1 (POSIX ERE) -> _isk_q
_isk_re_escape() {
local s=$1 out= c
local -i i n=${#1}
if [[ $s != *[][\\.\(\)*+?{}\|^\$]* ]]; then _isk_q=$s; return; fi
for ((i = 0; i < n; i++)); do
c=${s:i:1}
case $c in [][\\.\(\)*+?{}\|^\$]) out+=\\$c ;; *) out+=$c ;; esac
done
_isk_q=$out
}
# _isk_find PREFIX VAR -> _isk_m = first line of $VAR ("\n" separated)
# that starts with PREFIX and is longer than it. The haystack is named,
# not passed: bash copies big arguments slowly. Runs in the C locale:
# glibc regex is several times faster there, and a byte-wise match of a
# UTF-8 prefix at a line start is still exact.
_isk_find() {
local LC_ALL=C re
_isk_re_escape "$1"
if [[ $2 == _isk_C ]]; then
re=$'\n'"(${_isk_q}[^"$'\n'"]+)"
else
re=$'\n\t'"$_isk_hre(${_isk_q}[^"$'\n'"]+)"$'\n\t'
fi
[[ ${!2} =~ $re ]] || return 1
_isk_m=${BASH_REMATCH[1]}
}
# Is history entry $1 still sensible here? Like fish: the command must
# exist (this also makes ./script suggestions depend on the directory)
# and a `cd DIR` needs DIR to exist.
_isk_valid() {
local e=$1 w rest
[[ $e =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*([^[:space:]\;\|\&\<\>\(\)]+)(.*)$ ]] || return 0
w=${BASH_REMATCH[2]} rest=${BASH_REMATCH[3]}
case $w in
*[\$\`\"\'\\\*\?\[\{]*) return 0 ;;
'~/'*) w=$HOME/${w#'~/'} ;;
esac
if [[ $w == cd || $w == pushd ]]; then
rest=${rest#"${rest%%[![:space:]]*}"}
rest=${rest%"${rest##*[![:space:]]}"}
[[ $rest == - && -z ${OLDPWD-} ]] && return 1 # new shell: `cd -` fails
case $rest in
''|-*|*[\$\`\*\?\[\;\|\&\<\>\(\)\{]*) return 0 ;;
\'*\') rest=${rest:1:${#rest}-2} ;;
\"*\") rest=${rest:1:${#rest}-2} ;;
*[[:space:]]*) [[ $rest == *\\[[:space:]]* ]] || return 0; rest=${rest//\\/} ;;
*) rest=${rest//\\/} ;;
esac
case $rest in '~') rest=$HOME ;; '~/'*) rest=$HOME/${rest:2} ;; esac
[[ -d $rest ]] && return 0
[[ -n ${CDPATH-} && $rest != [/.]* ]] && return 0
return 1
fi
if [[ -z ${_isk_okw[$w]-} ]]; then
if type -t -- "$w" >/dev/null 2>&1; then _isk_okw[$w]=1; else _isk_okw[$w]=0; fi
fi
((_isk_okw[$w]))
}
# _isk_hist PREFIX -> _isk_found = newest valid history entry longer
# than PREFIX that starts with it. Typing usually just extends the
# previous prefix, so the previous answer is reused when it still fits.
_isk_hist() {
local p=$1 m hay
local -i n=0
# PROMPT_COMMAND may have been replaced after we were sourced
((_isk_hready)) || _isk_hist_build
if [[ -n $_isk_lp && $p == "$_isk_lp"* ]]; then
if [[ -z $_isk_lr ]]; then _isk_lp=$p; return 1; fi
if [[ $_isk_lr == "$p"?* ]]; then _isk_lp=$p _isk_found=$_isk_lr; return 0; fi
fi
if [[ -n ${_isk_memo[$p]+x} ]]; then
m=${_isk_memo[$p]}
else
m=
if _isk_find "$p" _isk_H; then
if _isk_valid "$_isk_m"; then
m=$_isk_m
else
# newest match is stale: look further, without it
hay=$_isk_H
_isk_drop "$_isk_m" hay
while _isk_find "$p" hay; do
if _isk_valid "$_isk_m"; then m=$_isk_m; break; fi
((++n > 8)) && break
_isk_drop "$_isk_m" hay
done
fi
fi
_isk_memo[$p]=$m
fi
_isk_lp=$p _isk_lr=$m
[[ -n $m ]] || return 1
_isk_found=$m
}
# _isk_drop LINE VAR: remove history line LINE from the variable named
# VAR, in place (C locale: byte-wise, fast). Patch by pan64 (LQ).
_isk_drop() { local LC_ALL=C; local -n _h=$2; _h=${_h/$'\n\t'$_isk_hre"$1"$'\n\t'/$'\n\t'}; }
# The cache keeps `fc -lnr` format: "\n\t " before every entry, newest
# first, "\n\t" at the very end. Continuation lines of multi-line
# entries do not start with a tab, so requiring "\n\t" right after a
# match skips those entries for free. $_isk_hre is "[ *]": fc marks
# edited entries with '*' (in POSIX mode there is no mark at all).
# Put entry $1 first (removing an older copy).
_isk_hist_add() {
local e=$1
[[ $e == *[![:space:]]* && $e != *[[:cntrl:]]* ]] || return 0
local LC_ALL=C
_isk_H=$'\n\t '$e${_isk_H/$'\n\t'$_isk_hre"$e"$'\n\t'/$'\n\t'}
}
# Build the cache from bash's in-memory history in one go.
_isk_hist_build() {
_isk_hre='[ *]'
shopt -oq posix && _isk_hre=
_isk_H=$'\n'$(builtin fc -lnr 1 2>/dev/null)$'\n\t'
_isk_hready=1
}
# After each command: make sure the newest history entry is first.
_isk_hist_last() {
local h
h=$(HISTTIMEFORMAT= builtin history 1 2>/dev/null) || return 0
[[ $h =~ ^[[:space:]]*[0-9]+[\ \*]\ (.*)$ ]] || return 0
h=${BASH_REMATCH[1]}
[[ $h == "$_isk_hlast" ]] && return 0
_isk_hlast=$h
_isk_hist_add "$h"
}
# ---------------------------------------------------------------- completion
# Split the line just enough to know the word under the cursor:
# _isk_word the raw text of the last word (empty: nothing to complete)
# _isk_wcmd 1 if that word is in command position
# _isk_wprog the command of the simple command the word belongs to
_isk_lex() {
local s=$1 c q= w cmd=
local -i i n=${#1} inw=0 ws=0 esc=0 cmdpos=1 wcmd=0 redir=0 wredir=0
for ((i = 0; i < n; i++)); do
c=${s:i:1}
if ((esc)); then esc=0; continue; fi
if [[ -n $q ]]; then
if [[ $q == \' ]]; then
[[ $c == \' ]] && q=
else
case $c in \\) esc=1 ;; \") q= ;; esac
fi
continue
fi
case $c in
[[:space:]])
((inw)) && { _isk_lex_end; inw=0; } ;;
[\;\&\|\(\)\`])
((inw)) && { _isk_lex_end; inw=0; }
cmdpos=1 redir=0 cmd= ;;
[\<\>])
((inw)) && { _isk_lex_end; inw=0; }
redir=1 ;;
*)
if ((!inw)); then
inw=1 ws=i wredir=redir
wcmd=$((cmdpos && !redir))
fi
case $c in \\) esc=1 ;; [\'\"]) q=$c ;; esac ;;
esac
done
_isk_word= _isk_wcmd=0 _isk_wprog=$cmd
((inw)) || return 1
_isk_word=${s:ws} _isk_wcmd=$wcmd
}
_isk_lex_end() { # uses _isk_lex's locals
if ((wredir)); then redir=0; return; fi
((wcmd)) || return
w=${s:ws:i-ws}
case $w in
[A-Za-z_]*=*|sudo|doas|env|time|nohup|exec|command|builtin|nice|ionice|\
xargs|watch|strace|ltrace|fakeroot|chroot|stdbuf|then|do|else|elif|if|\
while|until|'!'|'{') ;;
*) cmdpos=0 cmd=$w ;;
esac
}
# Suggest from completion: $VAR names, command names, files.
_isk_complete() {
_isk_lex "$1" || return 1
local w=$_isk_word
if [[ $w =~ \$\{?([A-Za-z_][A-Za-z0-9_]*)$ ]]; then
_isk_var "${BASH_REMATCH[1]}"
return
fi
if ((_isk_wcmd)) && [[ $w != */* ]]; then
[[ $w == *[\'\"\\\$\`=]* ]] && return 1
_isk_cmd "$w"
return
fi
case $_isk_wprog in
cd|pushd|rmdir) _isk_path "$w" d ;;
*) _isk_path "$w" ;;
esac
}
_isk_var() {
local v
local -a names=()
eval "names=(\"\${!$1@}\")"
for v in "${names[@]}"; do
[[ $v == "$1" || $v == _isk_* ]] && continue
_isk_sug=${v:${#1}}
return 0
done
return 1
}
_isk_cmd() {
if [[ $PATH != "$_isk_Cpath" ]] || ((SECONDS - _isk_Cstamp > 300)); then
_isk_cmds_build
fi
local LC_ALL=C
[[ $_isk_C == *$'\n'"$1"$'\n'* ]] && return 1 # already a whole command name
_isk_find "$1" _isk_C || return 1
_isk_sug=${_isk_m:${#1}}
}
# Command names, best first: commands recently used in history, then all
# others shortest first (each PATH directory comes out of the glob sorted).
_isk_cmds_build() {
local -A known=() taken=()
local -a bylen=() L=() P=()
local c d f C=$'\n' l
local -i i fg=0 ng=0
local -
mapfile -t L < <(compgen -A alias -A function -A builtin -A keyword 2>/dev/null)
for c in "${L[@]}"; do
[[ -z $c || $c == [_.]* || $c == *[!A-Za-z0-9._+-]* || -n ${known[$c]+x} ]] && continue
known[$c]=1 bylen[${#c}]+=$c$'\n'
done
shopt -q failglob && { fg=1; shopt -u failglob; }
shopt -q nullglob || { ng=1; shopt -s nullglob; }
local IFS=:
set -f; P=($PATH); set +f
IFS=$' \t\n'
for d in "${P[@]}"; do
[[ -n $d && -d $d ]] || continue
for f in "$d"/*; do
c=${f##*/}
[[ $c == .* || $c == *[!A-Za-z0-9._+-]* || -n ${known[$c]+x} ]] && continue
[[ -x $f && ! -d $f ]] || continue
known[$c]=1 bylen[${#c}]+=$c$'\n'
done
done
((fg)) && shopt -s failglob
((ng)) && shopt -u nullglob
mapfile -t -n 3000 L <<<"${_isk_H:1}"
for l in "${L[@]}"; do
[[ $l == $'\t'* ]] || continue
l=${l:1} l=${l#[ *]}
c=${l%%[[:space:]]*}
[[ -n $c && -n ${known[$c]+x} && -z ${taken[$c]+x} ]] || continue
taken[$c]=1 C+=$c$'\n'
done
for i in "${!bylen[@]}"; do C+=${bylen[i]}; done
_isk_C=$C _isk_Cpath=$PATH _isk_Cstamp=$SECONDS
}
# backslash-escape what the shell would split or expand -> _isk_t
_isk_esc() {
local s=$1 out= c
local -i i n=${#1}
if [[ $s != *[[:space:]\\\"\'\$\`\!\&\(\)\*\;\<\>\?\[\]\{\}\|]* ]]; then _isk_t=$s; return; fi
for ((i = 0; i < n; i++)); do
c=${s:i:1}
case $c in [[:space:]\\\"\'\$\`\!\&\(\)\*\;\<\>\?\[\]\{\}\|]) out+=\\$c ;; *) out+=$c ;; esac
done
_isk_t=$out
}
# _isk_path WORD [d] -> _isk_sug: rest of the first file (or directory,
# with d) whose name starts with WORD. Directories get a trailing /.
_isk_path() {
local raw=$1 q= w
case $raw in
\'*) q=\' w=${raw:1} ;;
\"*) q=\" w=${raw:1} ;;
*) w=$raw ;;
esac
[[ -n $w ]] || return 1
if [[ -z $q ]]; then
[[ $w == *[\'\"]* ]] && return 1
if [[ $w == *\\* ]]; then
local u= c
local -i i
for ((i = 0; i < ${#w}; i++)); do
c=${w:i:1}
[[ $c == \\ ]] && { ((i++)); c=${w:i:1}; }
u+=$c
done
w=$u
fi
else
[[ $w == *[\\$q]* ]] && return 1
fi
local g=$w chk=$w vn
case $w in
'~/'*) g=$HOME/${w:2} ;;
'~'*) return 1 ;;
\$*)
[[ $w =~ ^\$(\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(/.*)$ ]] || return 1
vn=${BASH_REMATCH[2]}${BASH_REMATCH[3]} chk=${BASH_REMATCH[4]}
[[ -n ${!vn-} ]] || return 1
g=${!vn}$chk ;;
esac
[[ $chk == *[\*\?\[\$\`]* ]] && return 1
local base=${g##*/} f cand=
local -a m=()
local -i fg=0 ng=0 cg=0
local -
set +f
shopt -q failglob && { fg=1; shopt -u failglob; }
shopt -q nocaseglob && { cg=1; shopt -u nocaseglob; }
shopt -q nullglob || { ng=1; shopt -s nullglob; }
if [[ $2 == d ]]; then m=("$g"*/); else m=("$g"*); fi
((fg)) && shopt -s failglob
((cg)) && shopt -s nocaseglob
((ng)) && shopt -u nullglob
for f in "${m[@]}"; do
[[ $2 == d ]] && f=${f%/}
[[ -z $base && ${f##*/} == .* ]] && continue
if [[ $f == "$g" ]]; then
[[ -d $f ]] && { cand=$f; break; }
continue
fi
cand=$f
break
done
[[ -n $cand ]] || return 1
local rest=${cand:${#g}}
[[ -d $cand && $cand != */ ]] && rest+=/
[[ -n $rest && $rest != *[[:cntrl:]]* ]] || return 1
if [[ -z $q ]]; then
_isk_esc "$rest"
rest=$_isk_t
elif [[ $q == \" ]]; then
[[ $rest == *[\"\$\`\\!]* ]] && return 1
else
[[ $rest == *\'* ]] && return 1
fi
_isk_sug=$rest
}
# ---------------------------------------------------------------- engine
_isk_suggest() { # $1 = the line, cursor at its end -> _isk_sug
_isk_sug=
[[ $1 == *[![:space:]]* ]] || return 1
if [[ " $ISKIOS_SOURCES " == *' history '* ]] && _isk_hist "$1"; then
_isk_sug=${_isk_found:${#1}}
return 0
fi
[[ " $ISKIOS_SOURCES " == *' complete '* ]] && _isk_complete "$1"
}
# Paint the ghost. Called from a bind -x function: bash has just erased
# the line and left the cursor at column 0 of the prompt's last line, and
# will redraw prompt + text as soon as we return. So: go to where the
# text will end, paint, come back to column 0. The ghost never wraps:
# what does not fit is cut with an ellipsis, like fish does.
_isk_draw() {
_isk_shown=0
[[ -n $_isk_sug ]] || return 0
((_isk_pwok)) || _isk_promptw
local g=$_isk_sug out=
[[ $g == *[[:cntrl:]]* ]] && { _isk_sug=; return 0; }
local -i cols=${COLUMNS:-80} tot row col avail
((cols > 1)) || cols=80
_isk_width "$READLINE_LINE" "$_isk_pw"
tot=_isk_pw+_isk_w
row=tot/cols col=tot%cols avail=cols-col-1
if ((avail < 2)); then _isk_sug=; return 0; fi
_isk_width "$g" "$col"
if ((_isk_w > avail)); then
_isk_trunc "$g" $((avail - 1)) "$col"
g=$_isk_t$_isk_ell
fi
if ((row)); then printf -v out '%*s' "$row" ''; out=${out// /$'\n'}; fi
out+=$'\r'
((col)) && out+=$'\e['$col'C'
out+=$ISKIOS_COLOR$g$'\e[0m'
((row)) && out+=$'\e['$row'A'
out+=$'\r'
printf '%s' "$out" >&2
_isk_shown=1 _isk_srow=row _isk_scol=col
}
_isk_refresh() {
_isk_sug=
if ((_isk_ps == 1 && READLINE_POINT == ${#READLINE_LINE})); then
if shopt -q nocasematch; then # prefix tests must be exact
shopt -u nocasematch
_isk_suggest "$READLINE_LINE"
shopt -s nocasematch
else
_isk_suggest "$READLINE_LINE"
fi
fi
_isk_sugline=$READLINE_LINE
_isk_draw
if ((_isk_shown)); then
((_isk_dyn)) || _isk_dynset 1
elif ((_isk_dyn)); then
_isk_dynset 0
fi
}
_isk_has() { [[ -n $_isk_sug && $_isk_sugline == "$READLINE_LINE" ]]; }
# The keys: insert, delete, move, accept. READLINE_POINT counts characters.
_isk_key() {
local line=$READLINE_LINE
local -i pt=$READLINE_POINT len=${#READLINE_LINE}
case $1 in
i) line=${line:0:pt}${_isk_chr[$2]}${line:pt}; pt+=1 ;;
u) _isk_u8 "$2" || return 0
line=${line:0:pt}$_isk_ch${line:pt}; pt+=1 ;;
f) if ((pt == len)) && _isk_has; then line+=$_isk_sug; pt=${#line}
elif ((pt < len)); then pt+=1; fi ;;
b) ((pt > 0)) && pt+=-1 ;;
e) ((pt == len)) && _isk_has && line+=$_isk_sug
pt=${#line} ;;
a) pt=0 ;;
fw) if ((pt == len)) && _isk_has; then
[[ $_isk_sug =~ ^[^[:alnum:]]*[[:alnum:]]* ]]
line+=${BASH_REMATCH[0]:-$_isk_sug}; pt=${#line}
else
[[ ${line:pt} =~ ^[^[:alnum:]]*[[:alnum:]]* ]]
pt+=${#BASH_REMATCH[0]}
fi ;;
bw) [[ ${line:0:pt} =~ [[:alnum:]]*[^[:alnum:]]*$ ]]
pt+=-${#BASH_REMATCH[0]} ;;
esac
READLINE_LINE=$line READLINE_POINT=$pt
_isk_refresh
}
# A multibyte UTF-8 character: readline gave us the lead byte ($1), the
# continuation bytes are still waiting on the terminal.
_isk_u8() {
local -i lead=$1 n=1
if ((lead >= 0xf0)); then n=3; elif ((lead >= 0xe0)); then n=2; fi
local o b
printf -v o '\\%03o' "$lead"
printf -v b "$o"
_isk_rbytes "$n" || return 1
_isk_ch=$b$_isk_rb
}
_isk_rbytes() { local LC_ALL=C; IFS= read -rsN "$1" -t 1 _isk_rb; }
# Runs (as part of a macro) right before a native readline command that
# could draw over the ghost. Returning is enough: bash erased the line
# and redraws it, and there is nothing to paint. "a": a line is being
# accepted, so what comes next is PS2, `read -e` or a new prompt.
_isk_pre() {
_isk_sug= _isk_sugline= _isk_shown=0
((_isk_dyn)) && _isk_dynset 0
if [[ ${1-} == a ]]; then
_isk_ps=2
_isk_hook
fi
return 0
}
# While a ghost is on screen, native commands that insert text are bound
# to "erase ghost, then the command". _isk_pre switches them back at once,
# so a second M-. / Tab / M-y sees its own previous run, as readline needs.
_isk_dynset() {
local -i i
local km ks fn
for i in "${!_isk_dynks[@]}"; do
km=${_isk_dynks[i]%%"$_isk_US"*} ks=${_isk_dynks[i]#*"$_isk_US"} fn=${_isk_dynfn[i]}
if (($1)); then
bind -m "$km" "\"$ks\": \"$_isk_PRE${_isk_priv[$fn]}\""
else
bind -m "$km" "\"$ks\": $fn"
fi
done
_isk_dyn=$1
}
# ---------------------------------------------------------------- prompt hook
_isk_precmd() {
local -i st=$?
if ((_isk_shown)); then
# the line was given up (Ctrl-C) with a ghost on it: wipe the ghost
((st == 130)) && _isk_intr
_isk_shown=0
fi
_isk_ps=1 _isk_pwok=0 _isk_sug= _isk_sugline= _isk_lp= _isk_lr=
_isk_memo=() _isk_okw=()
((_isk_dyn)) && _isk_dynset 0
((_isk_hready)) || _isk_hist_build
_isk_hist_last
if ((_isk_Cstamp >= 0)) && { [[ $PATH != "$_isk_Cpath" ]] || ((SECONDS - _isk_Cstamp > 300)); }; then
_isk_cmds_build
fi
return $st
}
# After Ctrl-C readline printed "^C" where the cursor was (the start of
# the ghost) and bash moved to the next line. Clear the rest of the ghost.
_isk_intr() {
local -i cols=${COLUMNS:-80} c=_isk_scol
((_isk_echoctl)) && c+=2
((c < cols)) || return 0
printf '\e[A\e[%dG\e[K\r\n' $((c + 1)) >&2
}
_isk_hook() {
if [[ ${PROMPT_COMMAND@a} == *a* ]]; then
[[ " ${PROMPT_COMMAND[*]} " == *' _isk_precmd '* ]] ||
PROMPT_COMMAND=(_isk_precmd "${PROMPT_COMMAND[@]}")
else
[[ ${PROMPT_COMMAND-} == *_isk_precmd* ]] ||
PROMPT_COMMAND="_isk_precmd${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
fi
}
_isk_unhook() {
local -i i
if [[ ${PROMPT_COMMAND@a} == *a* ]]; then
for i in "${!PROMPT_COMMAND[@]}"; do
[[ ${PROMPT_COMMAND[i]} == _isk_precmd ]] && unset 'PROMPT_COMMAND[i]'
done
PROMPT_COMMAND=("${PROMPT_COMMAND[@]}")
else
PROMPT_COMMAND=${PROMPT_COMMAND//_isk_precmd;/}
PROMPT_COMMAND=${PROMPT_COMMAND//_isk_precmd/}
fi
}
# ---------------------------------------------------------------- key setup
_isk_privkey() { # private key sequence that runs readline function $1
[[ -n ${_isk_priv[$1]-} ]] && return
_isk_priv[$1]="\\e[97;$((_isk_npriv++))~"
local km
for km in $_isk_maps; do bind -m "$km" "\"${_isk_priv[$1]}\": $1"; done
}
_isk_bind() {
local km vars
_isk_dynks=() _isk_dynfn=() _isk_orig=() _isk_dyn=0
vars=$(bind -v 2>/dev/null)
[[ $vars == *'echo-control-characters off'* ]] && _isk_echoctl=0 || _isk_echoctl=1
for km in $_isk_maps; do _isk_bind_map "$km"; done
}
# Take over the keys of keymap $1 (emacs, vi-insert).
_isk_bind_map() {
local km=$1 l ks fn o c
local -i i
local -A sins=()
bind -m "$km" -x "\"$_isk_PRE\": _isk_pre"
bind -m "$km" -x "\"$_isk_PREA\": _isk_pre a"
bind -m "$km" -x "\"$_isk_POST\": _isk_key post"
while IFS= read -r l; do
[[ $l =~ ^\"(.+)\":\ ([a-z-]+)$ ]] || continue
ks=${BASH_REMATCH[1]} fn=${BASH_REMATCH[2]}
[[ $ks == '\e[97;'* || $ks == '\M-[97;'* ]] && continue
ks=${ks%'\000'} # "\e\000" is how bind -p spells a lone ESC
[[ -n $ks ]] || continue
o=
case $fn in
self-insert) sins[$ks]=1; continue ;;
forward-char) o=f ;;
backward-char) o=b ;;
end-of-line) o=e ;;
beginning-of-line) o=a ;;
forward-word) o=fw ;;
backward-word) o=bw ;;
accept-line|operate-and-get-next|insert-comment|edit-and-execute-command)
_isk_privkey "$fn"
bind -m "$km" "\"$ks\": \"$_isk_PREA${_isk_priv[$fn]}\"" ;;
previous-history|next-history|beginning-of-history|end-of-history|\
reverse-search-history|forward-search-history|yank|quoted-insert|\
bracketed-paste-begin|possible-completions|insert-completions|\
non-incremental-reverse-search-history|non-incremental-forward-search-history|\
vi-movement-mode)
_isk_privkey "$fn"
bind -m "$km" "\"$ks\": \"$_isk_PRE${_isk_priv[$fn]}\"" ;;
backward-delete-char|clear-screen|clear-display|undo|revert-line|magic-space)
_isk_privkey "$fn"
bind -m "$km" "\"$ks\": \"${_isk_priv[$fn]}$_isk_POST\"" ;;
complete|menu-complete|menu-complete-backward|yank-last-arg|yank-nth-arg|\
yank-pop|history-search-backward|history-search-forward|\
history-substring-search-backward|history-substring-search-forward|\
dynamic-complete-history|complete-filename|complete-command|\
complete-variable|complete-username|complete-hostname|complete-into-braces|\
glob-complete-word|glob-expand-word|shell-expand-line|history-expand-line|\
alias-expand-line|history-and-alias-expand-line|insert-last-argument|\
tab-insert|transpose-chars|transpose-words|spell-correct-word|fetch-history)
_isk_privkey "$fn"
_isk_dynks+=("$km$_isk_US$ks") _isk_dynfn+=("$fn")
continue ;;
*) continue ;;
esac
_isk_orig[$km$_isk_US$ks]=$fn
[[ -n $o ]] && bind -m "$km" -x "\"$ks\": _isk_key $o"
done < <(bind -m "$km" -p 2>/dev/null)
# Alt/Ctrl + arrows accept a word (fish), if nothing else uses them
for ks in '\e[1;3C' '\e[1;5C' '\e[5C' '\e[1;3D' '\e[1;5D' '\e[5D'; do
[[ -n ${_isk_orig[$km$_isk_US$ks]+x} ]] && continue
_isk_orig[$km$_isk_US$ks]=
case $ks in *C) o=fw ;; *) o=bw ;; esac
bind -m "$km" -x "\"$ks\": _isk_key $o"
done
# printable ASCII that still self-inserts
for ((i = 32; i < 127; i++)); do
c=${_isk_chr[i]}
case $c in \") ks='\"' ;; \\) ks='\\' ;; *) ks=$c ;; esac
[[ -n ${sins[$ks]+x} ]] || continue
printf -v o '\\%03o' "$i"
_isk_orig[$km$_isk_US$o]=self-insert
bind -m "$km" -x "\"$o\": _isk_key i $i"
done
# lead bytes of UTF-8 characters (only when readline passes 8-bit
# bytes through, which it does in UTF-8 locales)
if [[ $_isk_ell != '~' && $vars == *'convert-meta off'* ]]; then
for ((i = 0xc2; i <= 0xf4; i++)); do
printf -v o '\\%03o' "$i"
_isk_orig[$km$_isk_US$o]=self-insert
bind -m "$km" -x "\"$o\": _isk_key u $i"
done
fi
}
_isk_unbind() {
local k km ks
((_isk_dyn)) && _isk_dynset 0
for k in "${!_isk_orig[@]}"; do
km=${k%%"$_isk_US"*} ks=${k#*"$_isk_US"}
if [[ -n ${_isk_orig[$k]} ]]; then
bind -m "$km" "\"$ks\": ${_isk_orig[$k]}"
else
bind -m "$km" -r "$ks"
fi
done
_isk_orig=()
}
# ---------------------------------------------------------------- command
iskios() {
case ${1-status} in
on)
((_isk_on)) && _isk_unbind
_isk_bind
_isk_hook
_isk_on=1 ;;
off)
((_isk_on)) || return 0
_isk_unbind
_isk_unhook
_isk_on=0 ;;
reload)
_isk_hist_build
_isk_cmds_build
_isk_memo=() _isk_lp= _isk_lr= ;;
status)
local st=off
((_isk_on)) && st=on
printf 'iskios: %s, history cache %d KiB, sources: %s\n' \
"$st" $(( ${#_isk_H} / 1024 )) "$ISKIOS_SOURCES" ;;
*)
printf 'usage: iskios on|off|reload|status\n' >&2
return 2 ;;
esac
}
# ---------------------------------------------------------------- init
for ((_isk_i = 32; _isk_i < 127; _isk_i++)); do
printf -v _isk_o '\\%03o' "$_isk_i"
printf -v '_isk_chr[_isk_i]' "$_isk_o"
done
unset _isk_i _isk_o
iskios on