mirror of
https://github.com/junegunn/fzf.git
synced 2026-03-07 23:52:32 +08:00
Compare commits
546 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce58d08ee3 | ||
|
|
997a7e5947 | ||
|
|
88e48619d6 | ||
|
|
2db14b4308 | ||
|
|
90c4269d4e | ||
|
|
6087055305 | ||
|
|
2f9df91171 | ||
|
|
12e24d368c | ||
|
|
55193ee4dc | ||
|
|
ff6a3bbee0 | ||
|
|
dce248ac6d | ||
|
|
0ff13dcfbe | ||
|
|
4d6a7757b8 | ||
|
|
b9804f5873 | ||
|
|
98a3b1fff8 | ||
|
|
6df5ca17e8 | ||
|
|
09ca45f7db | ||
|
|
09fe3a4180 | ||
|
|
b908f7a0ec | ||
|
|
1a50a3c082 | ||
|
|
fefea8d885 | ||
|
|
385cccd362 | ||
|
|
4a684b6c78 | ||
|
|
4a195e6323 | ||
|
|
0ecbf3f475 | ||
|
|
4522868fc0 | ||
|
|
111a62f1ea | ||
|
|
33cac3f0e7 | ||
|
|
74e98cac5c | ||
|
|
c338df02c4 | ||
|
|
69e9abdab4 | ||
|
|
b6411beaa1 | ||
|
|
b56d614ba2 | ||
|
|
49ab253555 | ||
|
|
0e859a18ed | ||
|
|
880dd20b18 | ||
|
|
91aa25c863 | ||
|
|
7e62b34087 | ||
|
|
f9f0014c16 | ||
|
|
ab3b9fef52 | ||
|
|
7d9724157c | ||
|
|
bc8967632b | ||
|
|
6360c9261c | ||
|
|
e653628458 | ||
|
|
28747e5cb6 | ||
|
|
9725eac314 | ||
|
|
b389616030 | ||
|
|
25b2248f11 | ||
|
|
14564e4fc7 | ||
|
|
d01eaa9de3 | ||
|
|
aad805d0d3 | ||
|
|
d1f037059a | ||
|
|
3f94bcb5bf | ||
|
|
3c7cbc9d47 | ||
|
|
28e2a067e5 | ||
|
|
b29e2ee2d1 | ||
|
|
029b241dbb | ||
|
|
d6ded42026 | ||
|
|
6eb4b41e34 | ||
|
|
14b5e1d88c | ||
|
|
603240122e | ||
|
|
8d688521fe | ||
|
|
775129367a | ||
|
|
b3b221854b | ||
|
|
c8cf0992c1 | ||
|
|
33d8d51c8a | ||
|
|
b473477c22 | ||
|
|
fcc4178bca | ||
|
|
cfc37caabc | ||
|
|
af2a81dc02 | ||
|
|
be5a687281 | ||
|
|
771e35b972 | ||
|
|
60a5be1e65 | ||
|
|
1d5e87f5e4 | ||
|
|
3db63f5e52 | ||
|
|
2ab923f3ae | ||
|
|
c3e6d9a8f9 | ||
|
|
2471edf3ff | ||
|
|
53a8aeeb72 | ||
|
|
60b35e748b | ||
|
|
3f499f055e | ||
|
|
1df99db0b2 | ||
|
|
535b610a6b | ||
|
|
91fab3b3c2 | ||
|
|
b9f2bf64ff | ||
|
|
07d53cb7e4 | ||
|
|
ead534a1be | ||
|
|
8a05083503 | ||
|
|
e659b46ff5 | ||
|
|
991c36453c | ||
|
|
4d563c6dfa | ||
|
|
5cb695744f | ||
|
|
c1b259c042 | ||
|
|
1a0371e2c7 | ||
|
|
aa259fdc19 | ||
|
|
b852dc8a56 | ||
|
|
a0cabe021d | ||
|
|
8cdfb23df6 | ||
|
|
4ffde48e2f | ||
|
|
f2b33f038a | ||
|
|
d5913bf86e | ||
|
|
0e9026b817 | ||
|
|
ab407c4645 | ||
|
|
91c4bef35f | ||
|
|
bf77206221 | ||
|
|
0cb1be3f04 | ||
|
|
01cb38a5fb | ||
|
|
c38c6cad79 | ||
|
|
ba6fc40cfd | ||
|
|
dd46a256c0 | ||
|
|
d19ce0ad8d | ||
|
|
ed7becfb47 | ||
|
|
9ace1351ff | ||
|
|
e1de29bc40 | ||
|
|
0df7d10550 | ||
|
|
91e119a77e | ||
|
|
3984161f6c | ||
|
|
91beacf0f4 | ||
|
|
e6ad01fb90 | ||
|
|
ce2200e908 | ||
|
|
548061dbde | ||
|
|
8f0c91545d | ||
|
|
0eefcf348e | ||
|
|
c1f8d18a0c | ||
|
|
8585969d6d | ||
|
|
8a943a9b1a | ||
|
|
c87a8eccd4 | ||
|
|
65df0abf0e | ||
|
|
b51bc6b50e | ||
|
|
febaadbee5 | ||
|
|
0e67c5aa7a | ||
|
|
760d1b7c58 | ||
|
|
9bdacc8df2 | ||
|
|
8e936ecfa7 | ||
|
|
db2e95b1f2 | ||
|
|
687074e772 | ||
|
|
3401c2e0c7 | ||
|
|
e8cb315419 | ||
|
|
f0c4ee4047 | ||
|
|
de0df2422a | ||
|
|
148b0a94cd | ||
|
|
ca294109c3 | ||
|
|
9cad2686e9 | ||
|
|
9a45172232 | ||
|
|
2a92c7d792 | ||
|
|
f5975cf870 | ||
|
|
a67aa85820 | ||
|
|
c5cabe1691 | ||
|
|
cbed41cd82 | ||
|
|
6684771cbf | ||
|
|
f5f894ea47 | ||
|
|
a0a334fc8d | ||
|
|
ae12e94b1f | ||
|
|
9ed971cc90 | ||
|
|
129cb23078 | ||
|
|
d22812e917 | ||
|
|
10d712824a | ||
|
|
de4059c8fa | ||
|
|
416aff86e9 | ||
|
|
59dc7f178f | ||
|
|
a3c9f8bfee | ||
|
|
5546c65491 | ||
|
|
f2179f015c | ||
|
|
9a53d84b9c | ||
|
|
0a8ff7899c | ||
|
|
f9d7877d8b | ||
|
|
9fe9976591 | ||
|
|
de1824f71d | ||
|
|
19a9296c47 | ||
|
|
49967f3d45 | ||
|
|
978b6254c7 | ||
|
|
1afd143810 | ||
|
|
e5cd7f0a3a | ||
|
|
51d3940c63 | ||
|
|
179aec1578 | ||
|
|
af0014aba8 | ||
|
|
da3d995709 | ||
|
|
04c4269db3 | ||
|
|
78f238294f | ||
|
|
354d0468c1 | ||
|
|
4efcc344c3 | ||
|
|
5818b58350 | ||
|
|
7941129cc4 | ||
|
|
069d71a840 | ||
|
|
08027e7a79 | ||
|
|
ead302981c | ||
|
|
fe0ffa14ff | ||
|
|
821b8e70a8 | ||
|
|
8ceda54c7d | ||
|
|
84e515bd6e | ||
|
|
dea1df6878 | ||
|
|
0076ec2e8d | ||
|
|
82c9671f79 | ||
|
|
d364a1122e | ||
|
|
fb570e94e7 | ||
|
|
6e3c830cd2 | ||
|
|
d7db7fc132 | ||
|
|
ff1550bb38 | ||
|
|
976001e474 | ||
|
|
531dd6fb4f | ||
|
|
ba035f2a76 | ||
|
|
d34675d3c9 | ||
|
|
ce95adc66c | ||
|
|
397fe8e395 | ||
|
|
111266d832 | ||
|
|
19d858f9b6 | ||
|
|
79690724d8 | ||
|
|
5ed87ffcb9 | ||
|
|
b99cb6323f | ||
|
|
debf3d8a8a | ||
|
|
4811e52af3 | ||
|
|
8d81730ec2 | ||
|
|
330a85c25c | ||
|
|
3a21116307 | ||
|
|
247d168af6 | ||
|
|
b2a8a283c7 | ||
|
|
c36ddce36f | ||
|
|
c35d9cff7d | ||
|
|
549ce3cf6c | ||
|
|
575bc0768c | ||
|
|
89334e881e | ||
|
|
dcec6354f5 | ||
|
|
16d338da84 | ||
|
|
27258f7207 | ||
|
|
4d2d6a5ced | ||
|
|
0c00b203e6 | ||
|
|
3b68dcdd81 | ||
|
|
39db026161 | ||
|
|
f6c589c606 | ||
|
|
2bd29c3172 | ||
|
|
4a61f53b85 | ||
|
|
adc9ad28da | ||
|
|
585cfaef8b | ||
|
|
b5cd8880b1 | ||
|
|
44ddab881e | ||
|
|
bfa287b66d | ||
|
|
243e52fa11 | ||
|
|
c166eaba6d | ||
|
|
09194c24f2 | ||
|
|
ec521e47aa | ||
|
|
e3f4a51c18 | ||
|
|
0a06fd6f63 | ||
|
|
70eace5290 | ||
|
|
40f9f254a9 | ||
|
|
15d6c17390 | ||
|
|
a9d1d42436 | ||
|
|
1ecfa38eee | ||
|
|
54fd92b7dd | ||
|
|
835906d392 | ||
|
|
1721e6a1ed | ||
|
|
c7ee3b833f | ||
|
|
ffb6e28ca7 | ||
|
|
a4c6846851 | ||
|
|
d18c0bf694 | ||
|
|
4e3f9854e6 | ||
|
|
b27943423e | ||
|
|
894a1016bc | ||
|
|
efe6cddd34 | ||
|
|
f1c6bdf3e8 | ||
|
|
710659bcf5 | ||
|
|
be67775da4 | ||
|
|
2c6381499c | ||
|
|
4df842e78c | ||
|
|
b81696fb64 | ||
|
|
d226d841a1 | ||
|
|
c6d83047e5 | ||
|
|
46dabccdf1 | ||
|
|
cd9517b679 | ||
|
|
cd6677ba1d | ||
|
|
9c1a47acf7 | ||
|
|
0c280a3ce1 | ||
|
|
53e8b6e705 | ||
|
|
ad33165fa7 | ||
|
|
2055db61c8 | ||
|
|
d2c662e54f | ||
|
|
d24b58ef3f | ||
|
|
06ae9b0f3b | ||
|
|
2a9c1c06a4 | ||
|
|
90ad1b7f22 | ||
|
|
f22fbcd1af | ||
|
|
1d761684c5 | ||
|
|
e491770f1c | ||
|
|
a41be61506 | ||
|
|
1a8f633611 | ||
|
|
af8fe918d8 | ||
|
|
8ef9dfd9a2 | ||
|
|
66df24040f | ||
|
|
ed4442d9ea | ||
|
|
0edb5d5ebb | ||
|
|
9ffc2c7ca3 | ||
|
|
93cb3758b5 | ||
|
|
d22e75dcdd | ||
|
|
a1b2a6fe2c | ||
|
|
e15cba0c8c | ||
|
|
31fd207ba2 | ||
|
|
ba6d1b8772 | ||
|
|
0dce561ec9 | ||
|
|
376142eb0d | ||
|
|
664ee1f483 | ||
|
|
dac5b6fde1 | ||
|
|
998c57442b | ||
|
|
4a0ab6c926 | ||
|
|
f43e82f17f | ||
|
|
62238620a5 | ||
|
|
200745011a | ||
|
|
82fd88339b | ||
|
|
de0f2efbfb | ||
|
|
29cf28d845 | ||
|
|
7e4dbb5f3b | ||
|
|
923c3a814d | ||
|
|
779e3cc5b5 | ||
|
|
3f3d1ef8f5 | ||
|
|
f92f9f137a | ||
|
|
87f7f436e8 | ||
|
|
4298c0b1eb | ||
|
|
6c104d771e | ||
|
|
aefb9a5bc4 | ||
|
|
8868d7cbb8 | ||
|
|
10cbac20f9 | ||
|
|
26bcd0c90d | ||
|
|
fbece2bb67 | ||
|
|
0012183ede | ||
|
|
8916cbc6ab | ||
|
|
21ce70054f | ||
|
|
3ba82b6d87 | ||
|
|
e771c5d057 | ||
|
|
4e5e925e39 | ||
|
|
b7248d4115 | ||
|
|
639253840f | ||
|
|
710ebdf9c1 | ||
|
|
bb64d84ce4 | ||
|
|
cd1da27ff2 | ||
|
|
c1accc2e5b | ||
|
|
e4489dcbc1 | ||
|
|
c0d407f7ce | ||
|
|
461115afde | ||
|
|
bae1965231 | ||
|
|
b89c77ec9a | ||
|
|
1ca5f09d7b | ||
|
|
d79902ae59 | ||
|
|
77568e114f | ||
|
|
a24d274a3c | ||
|
|
dac81432d6 | ||
|
|
309b5081ef | ||
|
|
91bc4f2671 | ||
|
|
4c9d37d919 | ||
|
|
7e9566f66a | ||
|
|
3f7e8a475d | ||
|
|
1cf7c0f334 | ||
|
|
ff8ee9ee4e | ||
|
|
cbbd939a94 | ||
|
|
f232df2887 | ||
|
|
16bfb2c80c | ||
|
|
0ba066123e | ||
|
|
81c51c26cc | ||
|
|
6fa8295ac5 | ||
|
|
f975b40236 | ||
|
|
01d9d9c8c8 | ||
|
|
1eafc4e5d9 | ||
|
|
38e4020aa8 | ||
|
|
ac32fbb3b2 | ||
|
|
7d26eca5cc | ||
|
|
3347d61591 | ||
|
|
9abf2c8c9c | ||
|
|
84e2262ad6 | ||
|
|
378137d34a | ||
|
|
66ca16f836 | ||
|
|
282884ad83 | ||
|
|
7877ac42f0 | ||
|
|
19ef8891e3 | ||
|
|
bfea9e53a6 | ||
|
|
a2420026ab | ||
|
|
1be1991299 | ||
|
|
67dd7e1923 | ||
|
|
2b584586ed | ||
|
|
a1994ff0ab | ||
|
|
ca0e858871 | ||
|
|
06c6615507 | ||
|
|
818d0be436 | ||
|
|
fcd2baa945 | ||
|
|
62e0a2824a | ||
|
|
bbe1721a18 | ||
|
|
c1470a51b8 | ||
|
|
6ee31d5dc5 | ||
|
|
65d74387e7 | ||
|
|
7d0ea599c4 | ||
|
|
b7795a3dea | ||
|
|
323f6f6202 | ||
|
|
0c61223884 | ||
|
|
32234be7a2 | ||
|
|
178b49832e | ||
|
|
18cbb4a84d | ||
|
|
e84afe196a | ||
|
|
e1e171a3c4 | ||
|
|
d075c00015 | ||
|
|
6c0ca4a64a | ||
|
|
6b5d461411 | ||
|
|
7419e0dde1 | ||
|
|
cf2bb5e40e | ||
|
|
f466e94d65 | ||
|
|
eb0257d48f | ||
|
|
b83dd6c6b4 | ||
|
|
51c207448d | ||
|
|
a6a558da30 | ||
|
|
2bf5fa27be | ||
|
|
af7940746f | ||
|
|
a2aa1a156c | ||
|
|
2f8a72a42a | ||
|
|
8179ca5eaa | ||
|
|
4b74f882c7 | ||
|
|
7cf45af502 | ||
|
|
46c21158d8 | ||
|
|
80da0776f8 | ||
|
|
e91f10ab16 | ||
|
|
2c15cd7923 | ||
|
|
d6584543e9 | ||
|
|
c13228f346 | ||
|
|
7220d8233e | ||
|
|
0237bf09bf | ||
|
|
04017c25bb | ||
|
|
02199cd609 | ||
|
|
26b9f5831a | ||
|
|
243a76002c | ||
|
|
c71e4ddee4 | ||
|
|
32eb8c1be9 | ||
|
|
c587017830 | ||
|
|
fb885652cc | ||
|
|
afc2f05e5e | ||
|
|
06547d0cbe | ||
|
|
578108280e | ||
|
|
65db7352b7 | ||
|
|
a4db8bd7b5 | ||
|
|
f1c1b02d77 | ||
|
|
6580f32b43 | ||
|
|
b028cbd8bd | ||
|
|
a1a5418318 | ||
|
|
5a32634b74 | ||
|
|
c1875af70b | ||
|
|
0a10d14e19 | ||
|
|
ed8ceec66f | ||
|
|
03760011d7 | ||
|
|
0d5aebb806 | ||
|
|
1313510890 | ||
|
|
b712f2bb6a | ||
|
|
938c15ec63 | ||
|
|
3e7f032ec2 | ||
|
|
b42f5bfb19 | ||
|
|
717562b264 | ||
|
|
9d6637c1b3 | ||
|
|
56fef7c8df | ||
|
|
ba0935c71f | ||
|
|
d83eb2800a | ||
|
|
6f943112a9 | ||
|
|
f422893b8e | ||
|
|
22b498489c | ||
|
|
5460517bd2 | ||
|
|
9a6e557e52 | ||
|
|
4fdc07927f | ||
|
|
9030b67e4f | ||
|
|
43eafdf4b7 | ||
|
|
dfb88edb5e | ||
|
|
bd3e65df4d | ||
|
|
d7b13f3408 | ||
|
|
14ef8e8051 | ||
|
|
cc1d9f124e | ||
|
|
93c0299606 | ||
|
|
55e3c73221 | ||
|
|
6783417504 | ||
|
|
fa3f706e71 | ||
|
|
9c2f6cae88 | ||
|
|
a30181e240 | ||
|
|
b4ccf64e62 | ||
|
|
88d768bf6b | ||
|
|
6444cc7905 | ||
|
|
328af1f397 | ||
|
|
5ae60e2e80 | ||
|
|
0e0b868342 | ||
|
|
a5beb08ed7 | ||
|
|
45fc7b903d | ||
|
|
4f2c274942 | ||
|
|
93415493b4 | ||
|
|
8e4d338de9 | ||
|
|
8a71e091a8 | ||
|
|
120cd7f25a | ||
|
|
fb3bf6c984 | ||
|
|
d57e1f8baa | ||
|
|
15ca9ad8eb | ||
|
|
c2e1861747 | ||
|
|
543d41f3dd | ||
|
|
e5cfc988ec | ||
|
|
ee3916be17 | ||
|
|
fd513f8af8 | ||
|
|
9a2b7f559c | ||
|
|
b8d2b0df7e | ||
|
|
fe3a9c603e | ||
|
|
97030d4cb1 | ||
|
|
b2c3e567da | ||
|
|
ca5e633399 | ||
|
|
e60a9a628b | ||
|
|
0167691941 | ||
|
|
3b0f976380 | ||
|
|
7bd298b536 | ||
|
|
0476a65fca | ||
|
|
2cb2af115a | ||
|
|
789226ff6d | ||
|
|
805efc5bf1 | ||
|
|
cdcab26766 | ||
|
|
ec3acb1932 | ||
|
|
d30e37434e | ||
|
|
20d5b2e20e | ||
|
|
6c6be4ab1a | ||
|
|
d004eb1f7c | ||
|
|
3148b0f3e8 | ||
|
|
3fc0bd26a5 | ||
|
|
6c9025ff17 | ||
|
|
289997e373 | ||
|
|
db44cbdff0 | ||
|
|
da9179335c | ||
|
|
cdf641fa3e | ||
|
|
66dbee10f5 | ||
|
|
19e9b620ba | ||
|
|
e4e4700aff | ||
|
|
bb55045596 | ||
|
|
d7e51cdeb5 | ||
|
|
7f4964b366 | ||
|
|
a6957aba11 | ||
|
|
b5f94f961d | ||
|
|
e182d3db7a | ||
|
|
3e6e0528a6 | ||
|
|
ac508a1ce4 | ||
|
|
d7fc1e09b1 | ||
|
|
3b0c86e401 | ||
|
|
61d10d8ffa | ||
|
|
7d9548919e | ||
|
|
bee80a730f | ||
|
|
ac3e24c99c | ||
|
|
e7e852bdb3 | ||
|
|
2b7f168571 | ||
|
|
5b3da1d878 | ||
|
|
99f1bc0177 | ||
|
|
ed76f076dd | ||
|
|
4d357d1063 | ||
|
|
961ae1541c | ||
|
|
add1aec685 | ||
|
|
03d6ba7496 | ||
|
|
71e4d5cc51 |
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
||||
root = true
|
||||
|
||||
[*.{sh,bash}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
simplify = true
|
||||
binary_next_line = false
|
||||
switch_case_indent = true
|
||||
space_redirects = true
|
||||
function_next_line = false
|
||||
|
||||
# also bash scripts.
|
||||
[{install,uninstall,bin/fzf-preview.sh,bin/fzf-tmux}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
simplify = true
|
||||
binary_next_line = false
|
||||
switch_case_indent = true
|
||||
space_redirects = true
|
||||
function_next_line = false
|
||||
64
.github/labeler.yml
vendored
Normal file
64
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
go:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/**
|
||||
- main.go
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
shell:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**
|
||||
|
||||
bash:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**/*.bash
|
||||
|
||||
zsh:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**/*.zsh
|
||||
|
||||
fish:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**/*.fish
|
||||
|
||||
vim:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- plugin/**
|
||||
|
||||
docs:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- '*.md'
|
||||
- doc/**
|
||||
- man/**
|
||||
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/**
|
||||
|
||||
build:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- Makefile
|
||||
- .goreleaser.yml
|
||||
- Dockerfile
|
||||
|
||||
test:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- test/**
|
||||
- src/**/*_test.go
|
||||
|
||||
install:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- install
|
||||
- install.ps1
|
||||
- uninstall
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -27,18 +27,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
2
.github/workflows/depsreview.yaml
vendored
2
.github/workflows/depsreview.yaml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
||||
17
.github/workflows/labeler.yml
vendored
Normal file
17
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Label PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
configuration-path: .github/labeler.yml
|
||||
20
.github/workflows/linux.yml
vendored
20
.github/workflows/linux.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Test fzf on Linux
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,33 +16,33 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.20"
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.0
|
||||
ruby-version: 3.4.6
|
||||
|
||||
- name: Install packages
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
run: sudo apt-get install --yes zsh fish tmux shfmt
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
|
||||
run: bundle install
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
run: make lint
|
||||
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||
|
||||
8
.github/workflows/macos.yml
vendored
8
.github/workflows/macos.yml
vendored
@@ -15,14 +15,14 @@ jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.20"
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
- name: Install packages
|
||||
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
|
||||
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux shfmt
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
|
||||
4
.github/workflows/sponsors.yml
vendored
4
.github/workflows/sponsors.yml
vendored
@@ -3,13 +3,13 @@ name: Generate Sponsors README
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 0 * * 0
|
||||
- cron: 0 15 * * 6
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
|
||||
4
.github/workflows/typos.yml
vendored
4
.github/workflows/typos.yml
vendored
@@ -6,5 +6,5 @@ jobs:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.27.3
|
||||
- uses: actions/checkout@v5
|
||||
- uses: crate-ci/typos@v1.29.4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,7 +3,6 @@ bin/fzf.exe
|
||||
dist
|
||||
target
|
||||
pkg
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
@@ -12,3 +11,4 @@ gopath
|
||||
fzf
|
||||
tmp
|
||||
*.patch
|
||||
.idea
|
||||
|
||||
@@ -14,6 +14,7 @@ builds:
|
||||
- windows
|
||||
- freebsd
|
||||
- openbsd
|
||||
- android
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
@@ -21,10 +22,11 @@ builds:
|
||||
- loong64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- riscv64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
@@ -38,6 +40,12 @@ builds:
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: riscv64
|
||||
- goos: android
|
||||
goarch: amd64
|
||||
- goos: android
|
||||
goarch: arm
|
||||
|
||||
# .goreleaser.yaml
|
||||
notarize:
|
||||
@@ -77,12 +85,14 @@ notarize:
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
builds:
|
||||
ids:
|
||||
- fzf
|
||||
format: tar.gz
|
||||
formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats:
|
||||
- zip
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
@@ -94,7 +104,7 @@ release:
|
||||
name_template: '{{ .Version }}'
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Version }}-devel"
|
||||
version_template: "{{ .Version }}-devel"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
|
||||
10
.rubocop.yml
10
.rubocop.yml
@@ -1,9 +1,13 @@
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Metrics:
|
||||
Enabled: false
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Lint/NestedMethodDefinition:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
AllowedMethods:
|
||||
@@ -28,5 +32,11 @@ Style/WordArray:
|
||||
MinSize: 1
|
||||
Minitest/AssertEqual:
|
||||
Enabled: false
|
||||
Minitest/EmptyLineBeforeAssertionMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Lint/EmptyBlock:
|
||||
Enabled: false
|
||||
Style/SafeNavigationChainLength:
|
||||
Enabled: false
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
golang 1.20.13
|
||||
golang 1.23
|
||||
ruby 3.4
|
||||
shfmt 3.12
|
||||
|
||||
52
ADVANCED.md
52
ADVANCED.md
@@ -1,8 +1,8 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2024/06/24*
|
||||
* *Requires fzf 0.54.0 or later*
|
||||
* *Last update: 2025/02/02*
|
||||
* *Requires fzf 0.59.0 or later*
|
||||
|
||||
---
|
||||
|
||||
@@ -22,6 +22,7 @@ Advanced fzf examples
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
||||
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
@@ -92,7 +93,7 @@ fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
|
||||
|
||||

|
||||
|
||||
*(See `Layout` section of the man page to see the full list of options)*
|
||||
*(See man page to see the full list of options)*
|
||||
|
||||
But you definitely don't want to repeat `--height=40% --layout=reverse
|
||||
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||
@@ -128,7 +129,7 @@ fzf --height 70% --tmux 70%
|
||||
You can also specify the position, width, and height of the popup window in
|
||||
the following format:
|
||||
|
||||
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
|
||||
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]]`
|
||||
|
||||
```sh
|
||||
# 100% width and 60% height
|
||||
@@ -362,7 +363,7 @@ projects, and it will free up memory as you narrow down the results.
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "start:reload:$RG_PREFIX {q} || true" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
@@ -500,6 +501,44 @@ fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
### Controlling Ripgrep search and fzf search simultaneously
|
||||
|
||||
`search` and `transform-search` action allow you to trigger an fzf search with
|
||||
an arbitrary query string. This frees fzf from strictly following the prompt
|
||||
input, enabling custom search syntax.
|
||||
|
||||
In the example below, `transform` action is used to conditionally trigger
|
||||
`reload` for ripgrep, followed by `search` for fzf. The first word of the
|
||||
query initiates the Ripgrep process to generate the initial results, while the
|
||||
remainder of the query is passed to fzf for secondary filtering.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export TEMP=$(mktemp -u)
|
||||
trap 'rm -f "$TEMP"' EXIT
|
||||
|
||||
INITIAL_QUERY="${*:-}"
|
||||
TRANSFORMER='
|
||||
rg_pat={q:1} # The first word is passed to ripgrep
|
||||
fzf_pat={q:2..} # The rest are passed to fzf
|
||||
|
||||
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||
echo "$rg_pat" > "$TEMP"
|
||||
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||
fi
|
||||
echo "+search:$fzf_pat"
|
||||
'
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--with-shell 'bash -c' \
|
||||
--bind "start,change:transform:$TRANSFORMER" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-line,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
@@ -529,8 +568,7 @@ pods() {
|
||||
--info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'start:reload:$command' \
|
||||
--bind 'ctrl-r:reload:$command' \
|
||||
--bind 'start,ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||
|
||||
16
BUILD.md
16
BUILD.md
@@ -6,7 +6,7 @@ Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.20 or above
|
||||
- Go 1.23 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
@@ -41,6 +41,20 @@ make release
|
||||
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
|
||||
> ```
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
```sh
|
||||
# Run go unit tests
|
||||
make test
|
||||
|
||||
# Run integration tests (requires to be on tmux)
|
||||
make itest
|
||||
|
||||
# Run a single test case
|
||||
ruby test/runner.rb --name test_something
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
|
||||
861
CHANGELOG.md
861
CHANGELOG.md
@@ -1,6 +1,865 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.68.0
|
||||
------
|
||||
- Implemented word wrapping in the list section
|
||||
- Added `--wrap=word` (or `--wrap-word`) option and `toggle-wrap-word` action for word-level line wrapping in the list section
|
||||
- Changed default binding of `ctrl-/` and `alt-/` from `toggle-wrap` to `toggle-wrap-word`
|
||||
```sh
|
||||
fzf --wrap=word
|
||||
```
|
||||
- Implemented word wrapping in the preview window
|
||||
- Added `wrap-word` flag for `--preview-window` to enable word-level wrapping
|
||||
- Added `toggle-preview-wrap-word` action
|
||||
```sh
|
||||
fzf --preview 'bat --style=plain --color=always {}' \
|
||||
--preview-window wrap-word \
|
||||
--bind space:toggle-preview-wrap-word
|
||||
```
|
||||
- Added support for underline style variants in `--color`: `underline-double`, `underline-curly`, `underline-dotted`, `underline-dashed`
|
||||
```sh
|
||||
fzf --color 'fg:underline-curly,current-fg:underline-dashed'
|
||||
```
|
||||
- Added support for underline styles (`4:N`) and underline colors (SGR 58/59)
|
||||
```sh
|
||||
# In the list section
|
||||
printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n' | fzf --ansi
|
||||
|
||||
# In the preview window
|
||||
fzf --preview "printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n'"
|
||||
```
|
||||
- Added `--preview-wrap-sign` to set a different wrap indicator for the preview window
|
||||
- Added `alt-gutter` color option (#4602) (@hedgieinsocks)
|
||||
- Added `$FZF_WRAP` environment variable to child processes (`char` or `word` when wrapping is enabled) (#4672) (@bitraid)
|
||||
- fish: Improved command history (CTRL-R) (#4672) (@bitraid)
|
||||
- Enabled syntax highlighting in the list on fish 4.3.3+
|
||||
- Added syntax-highlighted preview window that auto-shows for long or multi-line commands
|
||||
- Added `ALT-ENTER` to reformat and insert selected commands
|
||||
- Improved handling of bulk deletion of selected history entries (`SHIFT-DELETE`)
|
||||
- Added fish completion support (#4605) (@lalvarezt)
|
||||
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)
|
||||
- Bug fixes
|
||||
- Fixed `_fzf_compgen_{path,dir}` to respect `FZF_COMPLETION_{PATH,DIR}_OPTS` (#4592) (@shtse8, @LangLangBart)
|
||||
- Fixed `--preview-window follow` not working correctly with wrapping (#3243, #4258)
|
||||
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
|
||||
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
|
||||
- Fixed preview process not killed on exit (#4667)
|
||||
- Fixed coloring of items with zero-width characters (#4620)
|
||||
- Fixed `track-current` unset after a combined movement action (#4649)
|
||||
- Fixed `--accept-nth` being ignored in filter mode (#4636) (@charemma)
|
||||
- Fixed display width calculation with `maxWidth` (#4596) (@LangLangBart)
|
||||
- Fixed clearing of the rest of the current line on start (#4652)
|
||||
- Fixed `x-api-key` header not required for GET requests (#4627)
|
||||
- Fixed key reading not cancelled when `execute` triggered via a server request (#4653)
|
||||
- Fixed rebind of readline command `redraw-current-line` (#4635) (@jameslazo)
|
||||
- Fixed `fzf-tmux` `TERM` quoting and added `mktemp` usage (#4664) (@Goofygiraffe06)
|
||||
- Do not allow very long queries in `FuzzyMatchV2` (#4608)
|
||||
|
||||
0.67.0
|
||||
------
|
||||
- Added `--freeze-left=N` option to keep the leftmost N columns always visible.
|
||||
```sh
|
||||
# Keep the file name column fixed and always visible
|
||||
git grep --line-number --color=always -- '' |
|
||||
fzf --ansi --delimiter : --freeze-left 1
|
||||
|
||||
# Can be used with --keep-right
|
||||
git grep --line-number --color=always -- '' |
|
||||
fzf --ansi --delimiter : --freeze-left 1 --keep-right
|
||||
```
|
||||
- Also added `--freeze-right=N` option to keep the rightmost N columns always visible.
|
||||
```sh
|
||||
# Stronger version of --keep-right that always keeps the right-end visible
|
||||
fd | fzf --freeze-right 1
|
||||
|
||||
# Keep the base name always visible
|
||||
fd | fzf --freeze-right 1 --delimiter /
|
||||
|
||||
# Keep both leftmost and rightmost components visible
|
||||
fd | fzf --freeze-left 1 --freeze-right 1 --delimiter /
|
||||
```
|
||||
- Updated `--info=inline` to print the spinner (load indicator).
|
||||
- Bug fixes
|
||||
|
||||
0.66.1
|
||||
------
|
||||
- Bug fixes
|
||||
- Fixed a bug preventing 'ctrl-h' from being bound to an action (#4556)
|
||||
- Fixed `--no-color` / `NO_COLOR` theme (#4561)
|
||||
|
||||
0.66.0
|
||||
------
|
||||
|
||||
### Quick summary
|
||||
|
||||
This version introduces many new features centered around the new "raw" mode.
|
||||
|
||||
| Type | Class | Name | Description |
|
||||
| :-- | :-- | :-- | :-- |
|
||||
| New | Option | `--raw` | Enable raw mode by default |
|
||||
| New | Option | `--gutter CHAR` | Set the gutter column character |
|
||||
| New | Option | `--gutter-raw CHAR` | Set the gutter column character in raw mode |
|
||||
| Enhancement | Option | `--listen SOCKET` | Added support for Unix domain sockets |
|
||||
| New | Action | `toggle-raw` | Toggle raw mode |
|
||||
| New | Action | `enable-raw` | Enable raw mode |
|
||||
| New | Action | `disable-raw` | Disable raw mode |
|
||||
| New | Action | `up-match` | Move up to the matching item |
|
||||
| New | Action | `down-match` | Move down to the matching item |
|
||||
| New | Action | `best` | Move to the matching item with the best score |
|
||||
| New | Color | `nomatch` | Color for non-matching items in raw mode |
|
||||
| New | Env Var | `FZF_RAW` | Matching status in raw mode (0, 1, or undefined) |
|
||||
| New | Env Var | `FZF_DIRECTION` | `up` or `down` depending on the layout |
|
||||
| New | Env Var | `FZF_SOCK` | Path to the Unix domain socket fzf is listening on |
|
||||
| Enhancement | Key | `CTRL-N` | `down` -> `down-match` |
|
||||
| Enhancement | Key | `CTRL-P` | `up` -> `up-match` |
|
||||
| Enhancement | Shell | `CTRL-R` binding | Toggle raw mode with `ALT-R` |
|
||||
| Enhancement | Shell | `CTRL-R` binding | Opt-out with an empty `FZF_CTRL_R_COMMAND` |
|
||||
|
||||
### 1. Introducing "raw" mode
|
||||
|
||||

|
||||
|
||||
This version introduces a new "raw" mode (named so because it shows the list
|
||||
"unfiltered"). In raw mode, non-matching items stay in their original positions,
|
||||
but appear dimmed. This allows you to see the surrounding items of a match and
|
||||
better understand the context of it. You can enable raw mode by default with
|
||||
`--raw`, but it's often more useful when toggled dynamically with the
|
||||
`toggle-raw` action.
|
||||
|
||||
```sh
|
||||
tree | fzf --reverse --bind alt-r:toggle-raw
|
||||
```
|
||||
|
||||
While non-matching items are displayed in a dimmed color, they are treated just
|
||||
like matching items, so you place the cursor on them and perform any action. If
|
||||
you prefer to navigate only through matching items, use the `down-match` and
|
||||
`up-match` actions, which are from now on bound to `CTRL-N` and `CTRL-P`
|
||||
respectively, and also to `ALT-DOWN` and `ALT-UP`.
|
||||
|
||||
| Key | Action | With `--history` |
|
||||
| :-- | :-- | :-- |
|
||||
| `down` | `down` | |
|
||||
| `up` | `up` | |
|
||||
| `ctrl-j` | `down` | |
|
||||
| `ctrl-k` | `up` | |
|
||||
| `ctrl-n` | `down-match` | `next-history` |
|
||||
| `ctrl-p` | `up-match` | `prev-history` |
|
||||
| `alt-down` | `down-match` | |
|
||||
| `alt-up` | `up-match` | |
|
||||
|
||||
> [!NOTE]
|
||||
> `CTRL-N` and `CTRL-P` are bound to `next-history` and `prev-history` when
|
||||
> `--history` option is enabled, so in that case, you'll need to manually bind
|
||||
> them, or use `ALT-DOWN` and `ALT-UP` instead.
|
||||
|
||||
> [!TIP]
|
||||
> `up-match` and `down-match` are equivalent to `up` and `down` when not in
|
||||
> raw mode, so you can safely bind them to `up` and `arrow` keys if you prefer.
|
||||
> ```sh
|
||||
> fzf --bind up:up-match,down:down-match
|
||||
> ```
|
||||
|
||||
#### Customizing the behavior
|
||||
|
||||
In raw mode, the input list is presented in its original order, unfiltered, and
|
||||
your cursor will not move to the matching item automatically. Here are ways to
|
||||
customize the behavior.
|
||||
|
||||
```sh
|
||||
# When the result list is updated, move the cursor to the item with the best score
|
||||
# (assuming sorting is not disabled)
|
||||
fzf --raw --bind result:best
|
||||
|
||||
# Move to the first matching item in the original list
|
||||
# - $FZF_RAW is set to 0 when raw mode is enabled and the current item is a non-match
|
||||
# - $FZF_DIRECTION is set to either 'up' or 'down' depending on the layout direction
|
||||
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
|
||||
```
|
||||
|
||||
#### Customizing the look
|
||||
|
||||
##### Gutter
|
||||
|
||||
To make the mode visually distinct, the gutter column is rendered in a dashed
|
||||
line using `▖` character. But you can customize it with the `--gutter-raw CHAR`
|
||||
option.
|
||||
|
||||
```sh
|
||||
# Use a thinner gutter instead of the default dashed line
|
||||
fzf --bind alt-r:toggle-raw --gutter-raw ▎
|
||||
```
|
||||
|
||||
##### Color and style of non-matching items
|
||||
|
||||
Non-matching items are displayed in a dimmed color by default, but you can
|
||||
change it with the `--color nomatch:...` option.
|
||||
|
||||
```sh
|
||||
fzf --raw --color nomatch:red
|
||||
fzf --raw --color nomatch:red:dim
|
||||
fzf --raw --color nomatch:red:dim:strikethrough
|
||||
fzf --raw --color nomatch:red:dim:strikethrough:italic
|
||||
```
|
||||
|
||||
For colored input, dimming alone may not be enough, and you may prefer to remove
|
||||
colors entirely. For that case, a new special style attribute `strip` has been
|
||||
added.
|
||||
|
||||
```sh
|
||||
fd --color always | fzf --ansi --raw --color nomatch:dim:strip:strikethrough
|
||||
```
|
||||
|
||||
#### Conditional actions for raw mode
|
||||
|
||||
You may want to perform different actions depending on whether the current item
|
||||
is a match or not. For that, fzf now exports `$FZF_RAW` environment variable.
|
||||
|
||||
It's:
|
||||
|
||||
- Undefined if raw mode is disabled
|
||||
- `1` if the current item is a match
|
||||
- `0` otherwise
|
||||
|
||||
```sh
|
||||
# Do not allow selecting non-matching items
|
||||
fzf --raw --bind 'enter:transform:[[ ${FZF_RAW-1} = 1 ]] && echo accept || echo bell'
|
||||
```
|
||||
|
||||
#### Leveraging raw mode in shell integration
|
||||
|
||||
The `CTRL-R` binding (command history) now lets you toggle raw mode with `ALT-R`.
|
||||
|
||||
### 2. Style changes
|
||||
|
||||
The screenshot on the right shows the updated gutter style:
|
||||
|
||||

|
||||
|
||||
This version includes a few minor updates to fzf's classic visual style:
|
||||
|
||||
- The gutter column is now narrower, rendered with the left-half block character (`▌`).
|
||||
- Markers no longer use background colors.
|
||||
- The `--color base16` theme (alias: `16`) has been updated for better compatibility with both dark and light themes.
|
||||
|
||||
### 3. `--listen` now supports Unix domain sockets
|
||||
|
||||
If an argument to `--listen` ends with `.sock`, fzf will listen on a Unix
|
||||
domain socket at the specified path.
|
||||
|
||||
```sh
|
||||
fzf --listen /tmp/fzf.sock --no-tmux
|
||||
|
||||
# GET
|
||||
curl --unix-socket /tmp/fzf.sock http
|
||||
|
||||
# POST
|
||||
curl --unix-socket /tmp/fzf.sock http -d up
|
||||
```
|
||||
|
||||
Note that any existing file at the given path will be removed before creating
|
||||
the socket, so avoid using an important file path.
|
||||
|
||||
### 4. Added options
|
||||
|
||||
#### `--gutter CHAR`
|
||||
|
||||
The gutter column can now be customized using `--gutter CHAR` and styled with
|
||||
`--color gutter:...`. Examples:
|
||||
|
||||
```sh
|
||||
# Right-aligned gutter
|
||||
fzf --gutter '▐'
|
||||
|
||||
# Even thinner gutter
|
||||
fzf --gutter '▎'
|
||||
|
||||
# Yellow checker pattern
|
||||
fzf --gutter '▚' --color gutter:yellow
|
||||
|
||||
# Classic style
|
||||
fzf --gutter ' ' --color gutter:reverse
|
||||
```
|
||||
|
||||
#### `--gutter-raw CHAR`
|
||||
|
||||
As noted above, the `--gutter-raw CHAR` option was also added for customizing the gutter column in raw mode.
|
||||
|
||||
### 5. Added actions
|
||||
|
||||
The following actions were introduced to support working with raw mode:
|
||||
|
||||
| Action | Description |
|
||||
| :-- | :-- |
|
||||
| `toggle-raw` | Toggle raw mode |
|
||||
| `enable-raw` | Enable raw mode |
|
||||
| `disable-raw` | Disable raw mode |
|
||||
| `up-match` | Move up to the matching item; identical to `up` if raw mode is disabled |
|
||||
| `down-match` | Move down to the matching item; identical to `down` if raw mode is disabled |
|
||||
| `best` | Move to the matching item with the best score; identical to `first` if raw mode is disabled |
|
||||
|
||||
### 6. Added environment variables
|
||||
|
||||
#### `$FZF_DIRECTION`
|
||||
|
||||
`$FZF_DIRECTION` is now exported to child processes, indicating the list direction of the current layout:
|
||||
|
||||
- `up` for the default layout
|
||||
- `down` for `reverse` or `reverse-list`
|
||||
|
||||
This simplifies writing transform actions involving layout-dependent actions
|
||||
like `{up,down}-match`, `{up,down}-selected`, and `toggle+{up,down}`.
|
||||
|
||||
```sh
|
||||
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
|
||||
```
|
||||
|
||||
#### `$FZF_SOCK`
|
||||
|
||||
When fzf is listening on a Unix domain socket using `--listen`, the path to the
|
||||
socket is exported as `$FZF_SOCK`, analogous to `$FZF_PORT` for TCP sockets.
|
||||
|
||||
#### `$FZF_RAW`
|
||||
|
||||
As described above, `$FZF_RAW` is now exported to child processes in raw mode,
|
||||
indicating whether the current item is a match (`1`) or not (`0`). It is not
|
||||
defined when not in raw mode.
|
||||
|
||||
#### `$FZF_CTRL_R_COMMAND`
|
||||
|
||||
You can opt-out `CTRL-R` binding from the shell integration by setting
|
||||
`FZF_CTRL_R_COMMAND` to an empty string. Setting it to any other value is not
|
||||
supported and will result in a warning.
|
||||
|
||||
```sh
|
||||
# Disable the CTRL-R binding from the shell integration
|
||||
FZF_CTRL_R_COMMAND= eval "$(fzf --bash)"
|
||||
```
|
||||
|
||||
### 7. Added key support for `--bind`
|
||||
|
||||
Pull request [#3996](https://github.com/junegunn/fzf/pull/3996) added support
|
||||
for many additional keys for `--bind` option, such as `ctrl-backspace`.
|
||||
|
||||
### 8. Breaking changes
|
||||
|
||||
#### Hiding the gutter column
|
||||
|
||||
In the previous versions, the recommended way to hide the gutter column was to
|
||||
set `--color gutter:-1`. That's because the gutter column was just a space
|
||||
character, reversed. But now that it's using a visible character (`▌`), applying
|
||||
the default color is no longer enough to hide it. Instead, you can set it to
|
||||
a space character.
|
||||
|
||||
```sh
|
||||
# Hide the gutter column
|
||||
fzf --gutter ' '
|
||||
|
||||
# Classic style
|
||||
fzf --gutter ' ' --color gutter:reverse
|
||||
```
|
||||
|
||||
#### `--color` option
|
||||
|
||||
In the previous versions, some elements had default style attributes applied and
|
||||
you would have to explicitly unset them with `regular` attribute if you wanted
|
||||
to reset them. This is no longer needed now, as the default style attributes
|
||||
are applied only when you do not specify any color or style for that element.
|
||||
|
||||
```sh
|
||||
# No 'dim', just red and italic.
|
||||
fzf --ghost 'Type to search' --color ghost:red:italic
|
||||
```
|
||||
|
||||
#### Compatibility changes
|
||||
|
||||
Starting with this release, fzf is built with Go 1.23. Support for some old OS versions has been dropped.
|
||||
|
||||
See https://go.dev/wiki/MinimumRequirements.
|
||||
|
||||
0.65.2
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- Fix incorrect truncation of `--info-command` with `--info=inline-right` (#4479)
|
||||
- [install] Support old uname in macOS (#4492)
|
||||
- [bash 3] Fix `CTRL-T` and `ALT-C` to preserve the last yank (#4496)
|
||||
- Do not unset `FZF_DEFAULT_*` variables when using winpty (#4497) (#4400)
|
||||
- Fix rendering of items with tabs when using a non-default ellipsis (#4505)
|
||||
- **This is the final release to support Windows 7.**
|
||||
- Future versions will be built with the latest Go toolchain, which has dropped support for Windows 7.
|
||||
|
||||
0.65.1
|
||||
------
|
||||
- Fixed incorrect `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_FOOTER_WORD` when the header or footer contains ANSI escape sequences and tab characters.
|
||||
- Fixed a bug where you cannot unset the default `--nth` using `change-nth` action.
|
||||
- Fixed a highlighting bug when using `--color fg:dim,nth:regular` pattern over ANSI-colored items.
|
||||
|
||||
0.65.0
|
||||
------
|
||||
- Added `click-footer` event that is triggered when the footer section is clicked. When the event is triggered, the following environment variables are set:
|
||||
- `$FZF_CLICK_FOOTER_COLUMN` - clicked column (1-based)
|
||||
- `$FZF_CLICK_FOOTER_LINE` - clicked line (1-based)
|
||||
- `$FZF_CLICK_FOOTER_WORD` - the word under the cursor
|
||||
```sh
|
||||
fzf --footer $'[Edit] [View]\n[Copy to clipboard]' \
|
||||
--with-shell 'bash -c' \
|
||||
--bind 'click-footer:transform:
|
||||
[[ $FZF_CLICK_FOOTER_WORD =~ Edit ]] && echo "execute:vim \{}"
|
||||
[[ $FZF_CLICK_FOOTER_WORD =~ View ]] && echo "execute:view \{}"
|
||||
(( FZF_CLICK_FOOTER_LINE == 2 )) && (( FZF_CLICK_FOOTER_COLUMN < 20 )) &&
|
||||
echo "execute-silent(echo -n \{} | pbcopy)+bell"
|
||||
'
|
||||
```
|
||||
- Added `trigger(...)` action that triggers events bound to another key or event.
|
||||
```sh
|
||||
# You can click on each key name to trigger the actions bound to that key
|
||||
fzf --footer 'Ctrl-E: Edit / Ctrl-V: View / Ctrl-Y: Copy to clipboard' \
|
||||
--with-shell 'bash -c' \
|
||||
--bind 'ctrl-e:execute:vim {}' \
|
||||
--bind 'ctrl-v:execute:view {}' \
|
||||
--bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell' \
|
||||
--bind 'click-footer:transform:
|
||||
[[ $FZF_CLICK_FOOTER_WORD =~ Ctrl ]] && echo "trigger(${FZF_CLICK_FOOTER_WORD%:})"
|
||||
'
|
||||
```
|
||||
- You can specify a series of keys and events
|
||||
```sh
|
||||
fzf --bind 'a:up,b:trigger(a,a,a)'
|
||||
```
|
||||
- Added support for `{*n}` and `{*nf}` placeholder.
|
||||
- `{*n}` evaluates to the zero-based ordinal index of all matched items.
|
||||
- `{*nf}` evaluates to the temporary file containing that.
|
||||
- Bug fixes and improvements
|
||||
- [neovim] Fixed margin background color when `&winborder` is used (#4453)
|
||||
- Fixed rendering error when hiding a preview window without border (#4465)
|
||||
- fix(shell): check for mawk existence before version check (#4468)
|
||||
- Thanks to @LangLangBart and @akinomyoga
|
||||
- Fixed `--no-header-lines-border` behavior (08027e7a)
|
||||
|
||||
0.64.0
|
||||
------
|
||||
- Added `multi` event that is triggered when the multi-selection has changed.
|
||||
```sh
|
||||
fzf --multi \
|
||||
--bind 'ctrl-a:select-all,ctrl-d:deselect-all' \
|
||||
--bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'
|
||||
```
|
||||
- [Halfwidth and fullwidth alphanumeric and punctuation characters](https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)) are now internally normalized to their ASCII equivalents to allow matching with ASCII queries.
|
||||
```sh
|
||||
echo ABC| fzf -q abc
|
||||
```
|
||||
- Renamed `clear-selection` action to `clear-multi` for consistency.
|
||||
- `clear-selection` remains supported as an alias for backward compatibility.
|
||||
- Bug fixes
|
||||
- Fixed a bug that could cause fzf to abort due to incorrect update ordering.
|
||||
- Fixed a bug where some multi-selections were lost when using `exclude` or `change-nth`.
|
||||
|
||||
0.63.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.63.0/_
|
||||
|
||||
- Added footer. The default border style for footer is `line`, which draws a single separator line.
|
||||
```sh
|
||||
fzf --reverse --footer "fzf: friend zone forever"
|
||||
```
|
||||
- Options
|
||||
- `--footer[=STRING]`
|
||||
- `--footer-border[=STYLE]`
|
||||
- `--footer-label=LABEL`
|
||||
- `--footer-label-pos=COL[:bottom]`
|
||||
- Colors
|
||||
- `footer`
|
||||
- `footer-bg`
|
||||
- `footer-border`
|
||||
- `footer-label`
|
||||
- Actions
|
||||
- `change-footer`
|
||||
- `transform-footer`
|
||||
- `bg-transform-footer`
|
||||
- `change-footer-label`
|
||||
- `transform-footer-label`
|
||||
- `bg-transform-footer-label`
|
||||
- `line` border style is now allowed for all types of border except for `--list-border`.
|
||||
```sh
|
||||
fzf --height 50% --style full:line --preview 'cat {}' \
|
||||
--bind 'focus:bg-transform-header(file {})+bg-transform-footer(wc {})'
|
||||
```
|
||||
- Added `{*}` placeholder flag that evaluates to all matched items.
|
||||
```bash
|
||||
seq 10000 | fzf --preview "awk '{sum += \$1} END {print sum}' {*f}"
|
||||
```
|
||||
- Use this with caution, as it can make fzf sluggish for large lists.
|
||||
- Added asynchronous transform actions with `bg-` prefix that run asynchronously in the background, along with `bg-cancel` action to cancel currently running `bg-transform` actions.
|
||||
```sh
|
||||
# Implement popup that disappears after 1 second
|
||||
# * Use footer as the popup
|
||||
# * Use `bell` to ring the terminal bell
|
||||
# * Use `bg-transform-footer` to clear the footer after 1 second
|
||||
# * Use `bg-cancel` to cancel currently running background transform actions
|
||||
fzf --multi --list-border \
|
||||
--bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \
|
||||
--bind 'enter:+transform-footer(echo Copied {} to clipboard)' \
|
||||
--bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)'
|
||||
|
||||
# It's okay for the commands to take a little while because they run in the background
|
||||
GETTER='curl -s http://metaphorpsum.com/sentences/1'
|
||||
fzf --style full --border --preview : \
|
||||
--bind "focus:bg-transform-header:$GETTER" \
|
||||
--bind "focus:+bg-transform-footer:$GETTER" \
|
||||
--bind "focus:+bg-transform-border-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-preview-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-input-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-list-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-header-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-footer-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-ghost:$GETTER" \
|
||||
--bind "focus:+bg-transform-prompt:$GETTER"
|
||||
```
|
||||
- Added support for full-line background color in the list section
|
||||
```sh
|
||||
for i in $(seq 16 255); do
|
||||
echo -e "\x1b[48;5;${i}m\x1b[0Khello"
|
||||
done | fzf --ansi
|
||||
```
|
||||
- SSH completion enhancements by @akinomyoga
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.62.0
|
||||
------
|
||||
- Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read
|
||||
```sh
|
||||
# seoul256-light
|
||||
fzf --style full --color='
|
||||
fg:#616161 fg+:#616161
|
||||
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
|
||||
hl:#719872 hl+:#719899
|
||||
pointer:#e12672 marker:#e17899
|
||||
header:#719872
|
||||
spinner:#719899 info:#727100
|
||||
prompt:#0099bd query:#616161
|
||||
border:#e1e1e1
|
||||
'
|
||||
```
|
||||
- Added `alt-bg` color to create striped lines to visually separate rows
|
||||
```sh
|
||||
fzf --color bg:237,alt-bg:238,current-bg:236 --highlight-line
|
||||
|
||||
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||
bat --plain --language bash --color always |
|
||||
fzf --read0 --ansi --reverse --multi \
|
||||
--color bg:237,alt-bg:238,current-bg:236 --highlight-line
|
||||
```
|
||||
- [fish] Improvements in CTRL-R binding (@bitraid)
|
||||
- You can trigger CTRL-R in the middle of a command to insert the selected item
|
||||
- You can delete history items with SHIFT-DEL
|
||||
- Bug fixes and improvements
|
||||
- Fixed unnecessary 100ms delay after `reload` (#4364)
|
||||
- Fixed `selected-bg` not applied to colored items (#4372)
|
||||
|
||||
0.61.3
|
||||
------
|
||||
- Reverted #4351 as it caused `tmux run-shell 'fzf --tmux'` to fail (#4559 #4560)
|
||||
- More environment variables for child processes (#4356)
|
||||
|
||||
0.61.2
|
||||
------
|
||||
- Fixed panic when using header border without pointer/marker (@phanen)
|
||||
- Fixed `--tmux` option when already inside a tmux popup (@peikk0)
|
||||
- Bug fixes and improvements in CTRL-T binding of fish (#4334) (@bitraid)
|
||||
- Added `--no-tty-default` option to make fzf search for the current TTY device instead of defaulting to `/dev/tty` (#4242)
|
||||
|
||||
0.61.1
|
||||
------
|
||||
- Disable bracketed-paste mode on exit. This fixes issue where pasting breaks after running fzf on old bash versions that don't support the mode.
|
||||
|
||||
0.61.0
|
||||
------
|
||||
- Added `--ghost=TEXT` to display a ghost text when the input is empty
|
||||
```sh
|
||||
# Display "Type to search" when the input is empty
|
||||
fzf --ghost "Type to search"
|
||||
```
|
||||
- Added `change-ghost` and `transform-ghost` actions for dynamically changing the ghost text
|
||||
- Added `change-pointer` and `transform-pointer` actions for dynamically changing the pointer sign
|
||||
- Added `r` flag for placeholder expression (raw mode) for unquoted output
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.60.3
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- [fish] Enable multiple history commands insertion (#4280) (@bitraid)
|
||||
- [walker] Append '/' to directory entries on MSYS2 (#4281)
|
||||
- Trim trailing whitespaces after processing ANSI sequences (#4282)
|
||||
- Remove temp files before `become` when using `--tmux` option (#4283)
|
||||
- Fix condition for using item numlines cache (#4285) (@alex-huff)
|
||||
- Make `--accept-nth` compatible with `--select-1` (#4287)
|
||||
- Increase the query length limit from 300 to 1000 (#4292)
|
||||
- [windows] Prevent fzf from consuming user input while paused (#4260)
|
||||
|
||||
0.60.2
|
||||
------
|
||||
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
|
||||
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
|
||||
- Thanks to @phanen for the fix
|
||||
- Fixed 'jump' action when the pointer is an empty string
|
||||
|
||||
0.60.1
|
||||
------
|
||||
- Bug fixes and minor improvements
|
||||
- Built-in walker now prints directory entries with a trailing slash
|
||||
- Fixed a bug causing unexpected behavior with [fzf-tab](https://github.com/Aloxaf/fzf-tab). Please upgrade if you use it.
|
||||
- Thanks to @alexeisersun, @bitraid, @Lompik, and @fsc0 for the contributions
|
||||
|
||||
0.60.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
|
||||
|
||||
- Added `--accept-nth` for choosing output fields
|
||||
```sh
|
||||
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
|
||||
# Becomes
|
||||
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
|
||||
|
||||
git branch | fzf | cut -c3-
|
||||
# Can be rewritten as
|
||||
git branch | fzf --accept-nth -1
|
||||
```
|
||||
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
|
||||
```sh
|
||||
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
|
||||
# foo, baz, bar
|
||||
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
|
||||
# foo,baz,bar,foo,bar
|
||||
```
|
||||
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
|
||||
```sh
|
||||
seq 100 | fzf --bind 'ctrl-x:exclude'
|
||||
|
||||
# 'exclude-multi' will exclude the selected items or the current item
|
||||
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
|
||||
```
|
||||
- Preview window now prints wrap indicator when wrapping is enabled
|
||||
```sh
|
||||
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
|
||||
```
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.59.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
|
||||
|
||||
- Prioritizing file name matches (#4192)
|
||||
- Added a new tiebreak option `pathname` for prioritizing file name matches
|
||||
- `--scheme=path` now sets `--tiebreak=pathname,length`
|
||||
- fzf will automatically choose `path` scheme
|
||||
* when the input is a TTY device, where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND` which is usually a command for listing files,
|
||||
* but not when `reload` or `transform` action is bound to `start` event, because in that case, fzf can't be sure of the input type.
|
||||
- Added `--header-lines-border` to display header from `--header-lines` with a separate border
|
||||
```sh
|
||||
# Use --header-lines-border to separate two headers
|
||||
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
|
||||
--header-lines-border bottom --no-list-border
|
||||
```
|
||||
- `click-header` event now sets `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use them to implement a clickable header for changing the search scope using the new `transform-nth` action.
|
||||
```sh
|
||||
# Click on the header line to limit search scope
|
||||
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||
--header-lines-border bottom --no-list-border \
|
||||
--color fg:dim,nth:regular \
|
||||
--bind 'click-header:transform-nth(
|
||||
echo $FZF_CLICK_HEADER_NTH
|
||||
)+transform-prompt(
|
||||
echo "$FZF_CLICK_HEADER_WORD> "
|
||||
)'
|
||||
```
|
||||
- `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior.
|
||||
- `kill` completion for bash and zsh were updated to use this feature
|
||||
- Added `--no-input` option to completely disable and hide the input section
|
||||
```sh
|
||||
# Click header to trigger search
|
||||
fzf --header '[src] [test]' --no-input --layout reverse \
|
||||
--header-border bottom --input-border \
|
||||
--bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'
|
||||
|
||||
# Vim-like mode switch
|
||||
fzf --layout reverse-list --no-input \
|
||||
--bind 'j:down,k:up,/:show-input+unbind(j,k,/)' \
|
||||
--bind 'enter,esc,ctrl-c:transform:
|
||||
if [[ $FZF_INPUT_STATE = enabled ]]; then
|
||||
echo "rebind(j,k,/)+hide-input"
|
||||
elif [[ $FZF_KEY = enter ]]; then
|
||||
echo accept
|
||||
else
|
||||
echo abort
|
||||
fi
|
||||
'
|
||||
```
|
||||
- You can later show the input section using `show-input` or `toggle-input` action, and hide it again using `hide-input`, or `toggle-input`.
|
||||
- Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc.
|
||||
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
|
||||
```sh
|
||||
export TEMP=$(mktemp -u)
|
||||
trap 'rm -f "$TEMP"' EXIT
|
||||
|
||||
TRANSFORMER='
|
||||
rg_pat={q:1} # The first word is passed to ripgrep
|
||||
fzf_pat={q:2..} # The rest are passed to fzf
|
||||
|
||||
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||
echo "$rg_pat" > "$TEMP"
|
||||
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||
fi
|
||||
echo "+search:$fzf_pat"
|
||||
'
|
||||
fzf --ansi --disabled \
|
||||
--with-shell 'bash -c' \
|
||||
--bind "start,change:transform:$TRANSFORMER"
|
||||
```
|
||||
- You can now bind actions to multiple keys and events at once by writing a comma-separated list of keys and events before the colon
|
||||
```sh
|
||||
# Load 'ps -ef' output on start and reload it on CTRL-R
|
||||
fzf --bind 'start,ctrl-r:reload:ps -ef'
|
||||
```
|
||||
- `--min-height` option now takes a number followed by `+`, which tells fzf to show at least that many items in the list section. The default value is now changed to `10+`.
|
||||
```sh
|
||||
# You will only see the input section which takes 3 lines
|
||||
fzf --style=full --height 1% --min-height 3
|
||||
|
||||
# You will see 3 items in the list section
|
||||
fzf --style full --height 1% --min-height 3+
|
||||
```
|
||||
- Shell integration scripts were updated to use `--min-height 20+` by default
|
||||
- `--header-lines` will be displayed at the top in `reverse-list` layout
|
||||
- Added `bell` action to ring the terminal bell
|
||||
```sh
|
||||
# Press CTRL-Y to copy the current line to the clipboard and ring the bell
|
||||
fzf --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell'
|
||||
```
|
||||
- Added `toggle-bind` action
|
||||
- Bug fixes and improvements
|
||||
- Fixed fish script to support fish 3.1.2 or later (@bitraid)
|
||||
|
||||
0.58.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
|
||||
|
||||
This version introduces three new border types, `--list-border`, `--input-border`, and `--header-border`, offering much greater flexibility for customizing the user interface.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-4-borders.png" />
|
||||
|
||||
Also, fzf now offers "style presets" for quick customization, which can be activated using the `--style` option.
|
||||
|
||||
| Preset | Screenshot |
|
||||
| :--- | :--- |
|
||||
| `default` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-default.png"/> |
|
||||
| `full` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-full.png"/> |
|
||||
| `minimal` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-minimal.png"/> |
|
||||
|
||||
- Style presets (#4160)
|
||||
- `--style=full[:BORDER_STYLE]`
|
||||
- `--style=default`
|
||||
- `--style=minimal`
|
||||
- Border and label for the list section (#4148)
|
||||
- Options
|
||||
- `--list-border[=STYLE]`
|
||||
- `--list-label=LABEL`
|
||||
- `--list-label-pos=COL[:bottom]`
|
||||
- Colors
|
||||
- `list-fg`
|
||||
- `list-bg`
|
||||
- `list-border`
|
||||
- `list-label`
|
||||
- Actions
|
||||
- `change-list-label`
|
||||
- `transform-list-label`
|
||||
- Border and label for the input section (prompt line and info line) (#4154)
|
||||
- Options
|
||||
- `--input-border[=STYLE]`
|
||||
- `--input-label=LABEL`
|
||||
- `--input-label-pos=COL[:bottom]`
|
||||
- Colors
|
||||
- `input-fg` (`query`)
|
||||
- `input-bg`
|
||||
- `input-border`
|
||||
- `input-label`
|
||||
- Actions
|
||||
- `change-input-label`
|
||||
- `transform-input-label`
|
||||
- Border and label for the header section (#4159)
|
||||
- Options
|
||||
- `--header-border[=STYLE]`
|
||||
- `--header-label=LABEL`
|
||||
- `--header-label-pos=COL[:bottom]`
|
||||
- Colors
|
||||
- `header-fg` (`header`)
|
||||
- `header-bg`
|
||||
- `header-border`
|
||||
- `header-label`
|
||||
- Actions
|
||||
- `change-header-label`
|
||||
- `transform-header-label`
|
||||
- Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]`
|
||||
- Added new preview border style `line` which draws a single separator line between the preview window and the rest of the interface
|
||||
- fzf will now render a dashed line (`┈┈`) in each `--gap` for better visual separation.
|
||||
```sh
|
||||
# All bash/zsh functions, highlighted
|
||||
declare -f |
|
||||
perl -0 -pe 's/^}\n/}\0/gm' |
|
||||
bat --plain --language bash --color always |
|
||||
fzf --read0 --ansi --layout reverse --multi --highlight-line --gap
|
||||
```
|
||||
* You can customize the line using `--gap-line[=STR]`.
|
||||
- You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup.
|
||||
```sh
|
||||
fzf --tmux border-native --bind 'enter:execute:less {}'
|
||||
```
|
||||
- Added `toggle-multi-line` action
|
||||
- Added `toggle-hscroll` action
|
||||
- Added `change-nth` action for dynamically changing the value of the `--nth` option
|
||||
```sh
|
||||
# Start with --nth 1, then 2, then 3, then back to the default, 1
|
||||
echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo
|
||||
```
|
||||
- `--nth` parts of each line can now be rendered in a different text style
|
||||
```sh
|
||||
# nth in a different style
|
||||
ls -al | fzf --nth -1 --color nth:italic
|
||||
ls -al | fzf --nth -1 --color nth:reverse
|
||||
ls -al | fzf --nth -1 --color nth:reverse:bold
|
||||
|
||||
# Dim the other parts
|
||||
ls -al | fzf --nth -1 --color nth:regular,fg:dim
|
||||
|
||||
# With 'change-nth'. The current nth option is exported as $FZF_NTH.
|
||||
ps -ef | fzf --reverse --header-lines 1 --header-border bottom --input-border \
|
||||
--color nth:regular,fg:dim \
|
||||
--bind 'ctrl-n:change-nth(8..|1|2|3|4|5|6|7|)' \
|
||||
--bind 'result:transform-prompt:echo "${FZF_NTH}> "'
|
||||
```
|
||||
- A single-character delimiter is now treated as a plain string delimiter rather than a regular expression delimiter, even if it's a regular expression meta-character.
|
||||
- This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'`
|
||||
- Bug fixes
|
||||
- Bug fixes and improvements in fish scripts (thanks to @bitraid)
|
||||
|
||||
0.57.0
|
||||
------
|
||||
- You can now resize the preview window by dragging the border
|
||||
- Built-in walker improvements
|
||||
- `--walker-root` can take multiple directory arguments. e.g. `--walker-root include src lib`
|
||||
- `--walker-skip` can handle multi-component patterns. e.g. `--walker-skip target/build`
|
||||
- Removed long processing delay when displaying images in the preview window
|
||||
- `FZF_PREVIEW_*` environment variables are exported to all child processes (#4098)
|
||||
- Bug fixes in fish scripts
|
||||
|
||||
0.56.3
|
||||
------
|
||||
- Bug fixes in zsh scripts
|
||||
- fix(zsh): handle backtick trigger edge case (#4090)
|
||||
- revert(zsh): remove 'fc -RI' call in the history widget (#4093)
|
||||
- Thanks to @LangLangBart for the contributions
|
||||
|
||||
0.56.2
|
||||
------
|
||||
- Bug fixes
|
||||
@@ -150,7 +1009,7 @@ _Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
|
||||
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
|
||||
```sh
|
||||
# Now this will work as expected. Previously, this would print an invalid header line.
|
||||
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
|
||||
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
|
||||
# `load` event would fire and the header would be prematurely updated.
|
||||
fzf --header 'Loading ...' --header-lines 1 \
|
||||
--bind 'start:reload:sleep 1; ps -ef' \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
|
||||
FROM rubylang/ruby:3.4.1-noble
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
|
||||
RUN gem install --no-document -v 5.22.3 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
@@ -9,4 +9,4 @@ RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
ENV LANG=C.UTF-8
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/runner.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
|
||||
8
Gemfile
Normal file
8
Gemfile
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'minitest', '5.25.4'
|
||||
gem 'rubocop', '1.71.0'
|
||||
gem 'rubocop-minitest', '0.36.0'
|
||||
gem 'rubocop-performance', '1.23.1'
|
||||
47
Gemfile.lock
Normal file
47
Gemfile.lock
Normal file
@@ -0,0 +1,47 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
json (2.9.1)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.25.4)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.71.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.36.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-performance (1.23.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
unicode-display_width (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-23
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (= 5.25.4)
|
||||
rubocop (= 1.71.0)
|
||||
rubocop-minitest (= 0.36.0)
|
||||
rubocop-performance (= 1.23.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.2
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
35
Makefile
35
Makefile
@@ -1,11 +1,20 @@
|
||||
SHELL := bash
|
||||
GO ?= go
|
||||
DOCKER ?= docker
|
||||
GOOS ?= $(shell $(GO) env GOOS)
|
||||
|
||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||
|
||||
BASH_SCRIPTS := $(ROOT_DIR)/bin/fzf-preview.sh \
|
||||
$(ROOT_DIR)/bin/fzf-tmux \
|
||||
$(ROOT_DIR)/install \
|
||||
$(ROOT_DIR)/uninstall \
|
||||
$(ROOT_DIR)/shell/common.sh \
|
||||
$(ROOT_DIR)/shell/update.sh \
|
||||
$(ROOT_DIR)/shell/completion.bash \
|
||||
$(ROOT_DIR)/shell/key-bindings.bash
|
||||
|
||||
ifdef FZF_VERSION
|
||||
VERSION := $(FZF_VERSION)
|
||||
else
|
||||
@@ -14,7 +23,7 @@ endif
|
||||
ifeq ($(VERSION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
||||
endif
|
||||
VERSION_TRIM := $(shell sed "s/^v//; s/-.*//" <<< $(VERSION))
|
||||
VERSION_TRIM := $(shell echo $(VERSION) | sed "s/^v//; s/-.*//")
|
||||
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
|
||||
ifdef FZF_REVISION
|
||||
@@ -83,12 +92,20 @@ test: $(SOURCES)
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
itest:
|
||||
ruby test/runner.rb
|
||||
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
lint: $(SOURCES) test/test_go.rb
|
||||
lint: $(SOURCES) test/*.rb test/lib/*.rb ${BASH_SCRIPTS}
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
rubocop --require rubocop-minitest --require rubocop-performance
|
||||
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
||||
shell/update.sh --check ${BASH_SCRIPTS}
|
||||
|
||||
fmt: $(SOURCES) $(BASH_SCRIPTS)
|
||||
gofmt -s -w src
|
||||
shell/update.sh ${BASH_SCRIPTS}
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
@@ -176,15 +193,15 @@ bin/fzf: target/$(BINARY) | bin
|
||||
cp -f target/$(BINARY) bin/fzf
|
||||
|
||||
docker:
|
||||
docker build -t fzf-ubuntu .
|
||||
docker run -it fzf-ubuntu tmux
|
||||
$(DOCKER) build -t fzf-ubuntu .
|
||||
$(DOCKER) run -it fzf-ubuntu tmux
|
||||
|
||||
docker-test:
|
||||
docker build -t fzf-ubuntu .
|
||||
docker run -it fzf-ubuntu
|
||||
$(DOCKER) build -t fzf-ubuntu .
|
||||
$(DOCKER) run -it fzf-ubuntu
|
||||
|
||||
update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all generate build release test bench lint install clean docker docker-test update
|
||||
.PHONY: all generate build release test itest bench lint install clean docker docker-test update fmt
|
||||
|
||||
@@ -155,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ 'query': ['fg', 'Normal'],
|
||||
\ 'hl': ['fg', 'Comment'],
|
||||
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||
@@ -492,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
33
SECURITY.md
Normal file
33
SECURITY.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Security Reporting
|
||||
|
||||
If you wish to report a security vulnerability privately, we appreciate your diligence. Please follow the guidelines below to submit your report.
|
||||
|
||||
## Reporting
|
||||
|
||||
To report a security vulnerability, please provide the following information:
|
||||
|
||||
1. **PROJECT**
|
||||
- https://github.com/junegunn/fzf
|
||||
|
||||
2. **PUBLIC**
|
||||
- Indicate whether this vulnerability has already been publicly discussed or disclosed.
|
||||
- If so, provide relevant links.
|
||||
|
||||
3. **DESCRIPTION**
|
||||
- Provide a detailed description of the security vulnerability.
|
||||
- Include as much information as possible to help us understand and address the issue.
|
||||
|
||||
Send this information, along with any additional relevant details, to <junegunn.c AT gmail DOT com>.
|
||||
|
||||
## Confidentiality
|
||||
|
||||
We kindly ask you to keep the report confidential until a public announcement is made.
|
||||
|
||||
## Notes
|
||||
|
||||
- Vulnerabilities will be handled on a best-effort basis.
|
||||
- You may request an advance copy of the patched release, but we cannot guarantee early access before the public release.
|
||||
- You will be notified via email simultaneously with the public announcement.
|
||||
- We will respond within a few weeks to confirm whether your report has been accepted or rejected.
|
||||
|
||||
Thank you for helping to improve the security of our project!
|
||||
@@ -9,12 +9,24 @@
|
||||
# - https://iterm2.com/utilities/imgcat
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
>&2 echo "usage: $0 FILENAME"
|
||||
>&2 echo "usage: $0 FILENAME[:LINENO][:IGNORED]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file=${1/#\~\//$HOME/}
|
||||
type=$(file --dereference --mime -- "$file")
|
||||
|
||||
center=0
|
||||
if [[ ! -r $file ]]; then
|
||||
if [[ $file =~ ^(.+):([0-9]+)\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
|
||||
file=${BASH_REMATCH[1]}
|
||||
center=${BASH_REMATCH[2]}
|
||||
elif [[ $file =~ ^(.+):([0-9]+):[0-9]+\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
|
||||
file=${BASH_REMATCH[1]}
|
||||
center=${BASH_REMATCH[2]}
|
||||
fi
|
||||
fi
|
||||
|
||||
type=$(file --brief --dereference --mime -- "$file")
|
||||
|
||||
if [[ ! $type =~ image/ ]]; then
|
||||
if [[ $type =~ =binary ]]; then
|
||||
@@ -32,28 +44,28 @@ if [[ ! $type =~ image/ ]]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
|
||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never --highlight-line="${center:-0}" -- "$file"
|
||||
exit
|
||||
fi
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim = x ]]; then
|
||||
if [[ $dim == x ]]; then
|
||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}'))); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. Use icat (from Kitty) if kitten is installed
|
||||
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
|
||||
98
bin/fzf-tmux
98
bin/fzf-tmux
@@ -8,7 +8,7 @@ fail() {
|
||||
}
|
||||
|
||||
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
|
||||
[[ -x "$fzf" ]] || fail 'fzf executable not found'
|
||||
[[ -x $fzf ]] || fail 'fzf executable not found'
|
||||
|
||||
args=()
|
||||
opt=""
|
||||
@@ -16,8 +16,8 @@ skip=""
|
||||
swap=""
|
||||
close=""
|
||||
term=""
|
||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||
[[ -n $LINES ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||
[[ -n $COLUMNS ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
||||
@@ -47,7 +47,7 @@ help() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
arg="$1"
|
||||
shift
|
||||
[[ -z "$skip" ]] && case "$arg" in
|
||||
[[ -z $skip ]] && case "$arg" in
|
||||
-)
|
||||
term=1
|
||||
;;
|
||||
@@ -58,19 +58,19 @@ while [[ $# -gt 0 ]]; do
|
||||
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
||||
exit
|
||||
;;
|
||||
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
|
||||
if [[ "$arg" =~ ^-[pwhxy] ]]; then
|
||||
[[ "$opt" =~ "-E" ]] || opt="-E"
|
||||
elif [[ "$arg" =~ ^.[lr] ]]; then
|
||||
-p* | -w* | -h* | -x* | -y* | -d* | -u* | -r* | -l*)
|
||||
if [[ $arg =~ ^-[pwhxy] ]]; then
|
||||
[[ $opt =~ "-E" ]] || opt="-E"
|
||||
elif [[ $arg =~ ^.[lr] ]]; then
|
||||
opt="-h"
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
if [[ $arg =~ ^.l ]]; then
|
||||
opt="$opt -d"
|
||||
swap="; swap-pane -D ; select-pane -L"
|
||||
close="; tmux swap-pane -D"
|
||||
fi
|
||||
else
|
||||
opt=""
|
||||
if [[ "$arg" =~ ^.u ]]; then
|
||||
if [[ $arg =~ ^.u ]]; then
|
||||
opt="$opt -d"
|
||||
swap="; swap-pane -D ; select-pane -U"
|
||||
close="; tmux swap-pane -D"
|
||||
@@ -79,7 +79,7 @@ while [[ $# -gt 0 ]]; do
|
||||
if [[ ${#arg} -gt 2 ]]; then
|
||||
size="${arg:2}"
|
||||
else
|
||||
if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then
|
||||
if [[ $1 =~ ^[0-9%,]+$ ]] || [[ $1 =~ ^[A-Z]$ ]]; then
|
||||
size="$1"
|
||||
shift
|
||||
else
|
||||
@@ -87,37 +87,37 @@ while [[ $# -gt 0 ]]; do
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$arg" =~ ^-p ]]; then
|
||||
if [[ -n "$size" ]]; then
|
||||
if [[ $arg =~ ^-p ]]; then
|
||||
if [[ -n $size ]]; then
|
||||
w=${size%%,*}
|
||||
h=${size##*,}
|
||||
opt="$opt -w$w -h$h"
|
||||
fi
|
||||
elif [[ "$arg" =~ ^-[whxy] ]]; then
|
||||
elif [[ $arg =~ ^-[whxy] ]]; then
|
||||
opt="$opt ${arg:0:2}$size"
|
||||
elif [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [[ $tmux_32 = 1 ]]; then
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
elif [[ $size =~ %$ ]]; then
|
||||
size=${size:0:${#size}-1}
|
||||
if [[ $tmux_32 == 1 ]]; then
|
||||
if [[ -n $swap ]]; then
|
||||
opt="$opt -l $((100 - size))%"
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
if [[ -n $swap ]]; then
|
||||
opt="$opt -p $((100 - size))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
if [[ -n $swap ]]; then
|
||||
if [[ $arg =~ ^.l ]]; then
|
||||
max=$columns
|
||||
else
|
||||
max=$lines
|
||||
fi
|
||||
size=$(( max - size ))
|
||||
size=$((max - size))
|
||||
[[ $size -lt 0 ]] && size=0
|
||||
opt="$opt -l $size"
|
||||
else
|
||||
@@ -135,10 +135,10 @@ while [[ $# -gt 0 ]]; do
|
||||
args+=("$arg")
|
||||
;;
|
||||
esac
|
||||
[[ -n "$skip" ]] && args+=("$arg")
|
||||
[[ -n $skip ]] && args+=("$arg")
|
||||
done
|
||||
|
||||
if [[ -z "$TMUX" ]]; then
|
||||
if [[ -z $TMUX ]]; then
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
fi
|
||||
@@ -149,7 +149,7 @@ fi
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
||||
|
||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
if [[ ! $opt =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed_without_popup=1
|
||||
original_window=$(tmux display-message -p "#{window_id}")
|
||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
||||
@@ -159,28 +159,27 @@ fi
|
||||
set -e
|
||||
|
||||
# Clean up named pipes on exit
|
||||
id=$RANDOM
|
||||
argsf="${TMPDIR:-/tmp}/fzf-args-$id"
|
||||
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/fzf-tmux-XXXXXX")
|
||||
argsf="$tmpdir/args"
|
||||
fifo1="$tmpdir/fifo1"
|
||||
fifo2="$tmpdir/fifo2"
|
||||
fifo3="$tmpdir/fifo3"
|
||||
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
||||
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
|
||||
tmux_win_opts=($(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts"))
|
||||
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
|
||||
else
|
||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
||||
tmux_win_opts=($(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/'))
|
||||
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
|
||||
fi
|
||||
cleanup() {
|
||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
\rm -rf "$tmpdir"
|
||||
# Restore tmux window options
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
|
||||
if [[ ${#tmux_win_opts[@]} -gt 1 ]]; then
|
||||
eval "tmux ${tmux_win_opts[*]}"
|
||||
fi
|
||||
|
||||
# Remove temp window if we were zoomed without popup options
|
||||
if [[ -n "$zoomed_without_popup" ]]; then
|
||||
if [[ -n $zoomed_without_popup ]]; then
|
||||
tmux display-message -p "#{window_id}" > /dev/null
|
||||
tmux swap-pane -t $original_window \; \
|
||||
select-window -t $original_window \; \
|
||||
@@ -196,11 +195,11 @@ cleanup() {
|
||||
trap 'cleanup 1' SIGUSR1
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
if [[ $tmux_version = 3.2 ]]; then
|
||||
envs="export TERM=$(printf %q "$TERM") "
|
||||
if [[ $opt =~ "-E" ]]; then
|
||||
if [[ $tmux_version == 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
elif [[ $tmux_32 = 1 ]]; then
|
||||
elif [[ $tmux_32 == 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
else
|
||||
@@ -211,8 +210,8 @@ fi
|
||||
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
|
||||
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||
[[ -n $RUNEWIDTH_EASTASIAN ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||
[[ -n $BAT_THEME ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||
echo "$envs;" > "$argsf"
|
||||
|
||||
# Build arguments to fzf
|
||||
@@ -224,9 +223,9 @@ close="; trap - EXIT SIGINT SIGTERM $close"
|
||||
|
||||
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||
mkfifo -m o+w $fifo2
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
if [[ $opt =~ "-E" ]]; then
|
||||
cat $fifo2 &
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
else
|
||||
mkfifo $fifo1
|
||||
@@ -239,7 +238,7 @@ if [[ "$opt" =~ "-E" ]]; then
|
||||
fi
|
||||
|
||||
mkfifo -m o+w $fifo3
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||
else
|
||||
mkfifo $fifo1
|
||||
@@ -249,6 +248,9 @@ fi
|
||||
tmux \
|
||||
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
$tmux_off_opts \
|
||||
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
|
||||
> /dev/null 2>&1 || {
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
}
|
||||
cat $fifo2
|
||||
exit "$(cat $fifo3)"
|
||||
|
||||
@@ -503,7 +503,7 @@ LICENSE *fzf-license*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
|
||||
18
go.mod
18
go.mod
@@ -1,20 +1,20 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.9
|
||||
github.com/gdamore/tcell/v2 v2.7.4
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
|
||||
github.com/charlievieth/fastwalk v1.0.14
|
||||
github.com/gdamore/tcell/v2 v2.9.0
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.27.0
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/term v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
go 1.20
|
||||
go 1.23.0
|
||||
|
||||
34
go.sum
34
go.sum
@@ -1,19 +1,18 @@
|
||||
github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0=
|
||||
github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
|
||||
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
|
||||
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@@ -35,21 +34,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
||||
215
install
215
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.56.2
|
||||
version=0.68.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -46,16 +46,16 @@ for opt in "$@"; do
|
||||
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
|
||||
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf"
|
||||
;;
|
||||
--key-bindings) key_bindings=1 ;;
|
||||
--no-key-bindings) key_bindings=0 ;;
|
||||
--completion) auto_completion=1 ;;
|
||||
--no-completion) auto_completion=0 ;;
|
||||
--update-rc) update_config=1 ;;
|
||||
--no-update-rc) update_config=0 ;;
|
||||
--bin) ;;
|
||||
--no-bash) shells=${shells/bash/} ;;
|
||||
--no-zsh) shells=${shells/zsh/} ;;
|
||||
--no-fish) shells=${shells/fish/} ;;
|
||||
--key-bindings) key_bindings=1 ;;
|
||||
--no-key-bindings) key_bindings=0 ;;
|
||||
--completion) auto_completion=1 ;;
|
||||
--no-completion) auto_completion=0 ;;
|
||||
--update-rc) update_config=1 ;;
|
||||
--no-update-rc) update_config=0 ;;
|
||||
--bin) ;;
|
||||
--no-bash) shells=${shells/bash/} ;;
|
||||
--no-zsh) shells=${shells/zsh/} ;;
|
||||
--no-fish) shells=${shells/fish/} ;;
|
||||
*)
|
||||
echo "unknown option: $opt"
|
||||
help
|
||||
@@ -83,7 +83,7 @@ ask() {
|
||||
check_binary() {
|
||||
echo -n " - Checking fzf executable ... "
|
||||
local output
|
||||
output=$("$fzf_base"/bin/fzf --version 2>&1)
|
||||
output=$(FZF_DEFAULT_OPTS= "$fzf_base"/bin/fzf --version 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: $output"
|
||||
binary_error="Invalid binary"
|
||||
@@ -104,7 +104,7 @@ check_binary() {
|
||||
|
||||
link_fzf_in_path() {
|
||||
if which_fzf="$(command -v fzf)"; then
|
||||
echo " - Found in \$PATH"
|
||||
echo ' - Found in $PATH'
|
||||
echo " - Creating symlink: bin/fzf -> $which_fzf"
|
||||
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
|
||||
check_binary && return
|
||||
@@ -114,22 +114,22 @@ link_fzf_in_path() {
|
||||
|
||||
try_curl() {
|
||||
command -v curl > /dev/null &&
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
curl -fL $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
curl -fL $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
}
|
||||
|
||||
try_wget() {
|
||||
command -v wget > /dev/null &&
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
wget -O - $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
wget -O - $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
}
|
||||
|
||||
download() {
|
||||
@@ -164,28 +164,30 @@ download() {
|
||||
}
|
||||
|
||||
# Try to download binary executable
|
||||
archi=$(uname -sm)
|
||||
archi=$(uname -smo 2> /dev/null || uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
MINGW*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
MSYS*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
Windows*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
Darwin\ arm64*) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||
Darwin\ x86_64*) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ riscv64*) download fzf-$version-linux_riscv64.tar.gz ;;
|
||||
Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MINGW*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MSYS*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
Windows*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
esac
|
||||
|
||||
cd "$fzf_base"
|
||||
@@ -214,7 +216,7 @@ if [ -n "$binary_error" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ "$*" =~ "--bin" ]] && exit 0
|
||||
[[ $* =~ "--bin" ]] && exit 0
|
||||
|
||||
for s in $shells; do
|
||||
if ! command -v "$s" > /dev/null; then
|
||||
@@ -241,16 +243,16 @@ fi
|
||||
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && continue
|
||||
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
|
||||
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
|
||||
[[ $shell == fish ]] && continue
|
||||
src=${prefix_expand}.${shell}
|
||||
echo -n "Generate $src ... "
|
||||
|
||||
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
|
||||
if [ $auto_completion -eq 0 ]; then
|
||||
fzf_completion="# $fzf_completion"
|
||||
fi
|
||||
|
||||
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
|
||||
if [ $key_bindings -eq 0 ]; then
|
||||
fzf_key_bindings="# $fzf_key_bindings"
|
||||
fi
|
||||
@@ -265,7 +267,7 @@ fi
|
||||
EOF
|
||||
|
||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||
if [[ "$shell" = zsh ]]; then
|
||||
if [[ $shell == zsh ]]; then
|
||||
echo "source <(fzf --$shell)" >> "$src"
|
||||
else
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
@@ -285,7 +287,7 @@ EOF
|
||||
done
|
||||
|
||||
# fish
|
||||
if [[ "$shells" =~ fish ]]; then
|
||||
if [[ $shells =~ fish ]]; then
|
||||
echo -n "Update fish_user_paths ... "
|
||||
fish << EOF
|
||||
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
|
||||
@@ -295,35 +297,49 @@ EOF
|
||||
fi
|
||||
|
||||
append_line() {
|
||||
set -e
|
||||
|
||||
local update line file pat lno
|
||||
local update line file pat lines
|
||||
update="$1"
|
||||
line="$2"
|
||||
file="$3"
|
||||
pat="${4:-}"
|
||||
lno=""
|
||||
at_lno="${5:-}"
|
||||
lines=""
|
||||
|
||||
echo "Update $file:"
|
||||
echo " - $line"
|
||||
if [ -f "$file" ]; then
|
||||
if [ $# -lt 4 ]; then
|
||||
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -n $pat ]]; then
|
||||
lines=$(\grep -nF "$pat" "$file")
|
||||
else
|
||||
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
lines=$(\grep -nF "${line#"${line%%[![:space:]]*}"}" "$file")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$lno" ]; then
|
||||
echo " - Already exists: line #$lno"
|
||||
else
|
||||
if [ $update -eq 1 ]; then
|
||||
|
||||
if [ -n "$lines" ]; then
|
||||
echo " - Already exists:"
|
||||
sed 's/^/ Line /' <<< "$lines"
|
||||
|
||||
update=0
|
||||
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines"; then
|
||||
echo " - But they all seem to be commented"
|
||||
ask " - Continue modifying $file?"
|
||||
update=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
set -e
|
||||
if [ "$update" -eq 1 ]; then
|
||||
if [[ -z $at_lno ]]; then
|
||||
[ -f "$file" ] && echo >> "$file"
|
||||
echo "$line" >> "$file"
|
||||
echo " + Added"
|
||||
else
|
||||
echo " ~ Skipped"
|
||||
sed -i.~fzf_bak "${at_lno}a\\"$'\n'"$line" "$file" && rm "$file.~fzf_bak"
|
||||
fi
|
||||
echo " + Added"
|
||||
else
|
||||
echo " ~ Skipped"
|
||||
fi
|
||||
|
||||
echo
|
||||
set +e
|
||||
}
|
||||
@@ -346,43 +362,84 @@ if [ $update_config -eq 2 ]; then
|
||||
fi
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && continue
|
||||
[[ $shell == fish ]] && continue
|
||||
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
|
||||
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
|
||||
done
|
||||
|
||||
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
|
||||
if [[ $shells =~ fish ]]; then
|
||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||
if [ ! -e "$bind_file" ]; then
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
' fzf --fish | source' \
|
||||
'end'
|
||||
if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
' fzf --fish | source' \
|
||||
'end'
|
||||
elif [[ $key_bindings -eq 1 ]]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
" $fzf_key_bindings" \
|
||||
'end'
|
||||
elif [[ $auto_completion -eq 1 ]]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
" $fzf_completion" \
|
||||
'end'
|
||||
fi
|
||||
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
else
|
||||
lno_func=0
|
||||
fi
|
||||
else
|
||||
echo "Check $bind_file:"
|
||||
lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -n $lno ]]; then
|
||||
echo " ** Found 'fzf_key_bindings' in line #$lno"
|
||||
echo " ** You have to replace the line to 'fzf --fish | source'"
|
||||
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -z $lno_func ]]; then
|
||||
echo -e "function fish_user_key_bindings\nend" >> "$bind_file"
|
||||
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
fi
|
||||
lno_keys=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -n $lno_keys ]]; then
|
||||
echo " ** Found 'fzf_key_bindings' in line #$lno_keys"
|
||||
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||
echo " ** You have to replace the line to 'fzf --fish | source'"
|
||||
elif [[ $key_bindings -eq 1 ]]; then
|
||||
echo " ** You have to replace the line to '$fzf_key_bindings'"
|
||||
else
|
||||
echo " ** You have to remove the line"
|
||||
fi
|
||||
echo
|
||||
else
|
||||
echo " - Clear"
|
||||
echo
|
||||
append_line $update_config "fzf --fish | source" "$bind_file"
|
||||
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
append_line $update_config " fzf --fish | source" "$bind_file" "" "$lno_func"
|
||||
else
|
||||
sed -i.~fzf_bak '/fzf --fish \| source/d' "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
if [[ $key_bindings -eq 1 ]]; then
|
||||
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
append_line $update_config " $fzf_key_bindings" "$bind_file" "" "$lno_func"
|
||||
elif [[ $auto_completion -eq 1 ]]; then
|
||||
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
append_line $update_config " $fzf_completion" "$bind_file" "" "$lno_func"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $update_config -eq 1 ]; then
|
||||
echo 'Finished. Restart your shell or reload config file.'
|
||||
if [[ "$shells" =~ bash ]]; then
|
||||
if [[ $shells =~ bash ]]; then
|
||||
echo -n ' source ~/.bashrc # bash'
|
||||
[[ "$archi" =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
||||
[[ $archi =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
||||
echo
|
||||
fi
|
||||
[[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||
[[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
|
||||
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fzf_user_key_bindings # fish'
|
||||
echo
|
||||
echo 'Use uninstall script to remove fzf.'
|
||||
echo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.56.2"
|
||||
$version="0.68.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
7
main.go
7
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.56"
|
||||
var version = "0.68"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
@@ -29,6 +29,9 @@ var zshCompletion []byte
|
||||
//go:embed shell/key-bindings.fish
|
||||
var fishKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.fish
|
||||
var fishCompletion []byte
|
||||
|
||||
//go:embed man/man1/fzf.1
|
||||
var manPage []byte
|
||||
|
||||
@@ -65,7 +68,7 @@ func main() {
|
||||
}
|
||||
if options.Fish {
|
||||
printScript("key-bindings.fish", fishKeyBindings)
|
||||
fmt.Println("fzf_key_bindings")
|
||||
printScript("completion.fish", fishCompletion)
|
||||
return
|
||||
}
|
||||
if options.Help {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf\-tmux 1 "Nov 2024" "fzf 0.56.2" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Feb 2026" "fzf 0.68.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
1361
man/man1/fzf.1
1361
man/man1/fzf.1
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2013-2024 Junegunn Choi
|
||||
" Copyright (c) 2013-2026 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -358,7 +358,7 @@ endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && (has('gui_running') || has('termguicolors') && &termguicolors))
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
@@ -553,8 +553,15 @@ try
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --no-tmux --height='.height
|
||||
endif
|
||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
||||
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
|
||||
|
||||
if exists('&winborder') && &winborder !=# '' && &winborder !=# 'none'
|
||||
" Add 1-column horizontal margin
|
||||
let optstr = join(['--margin 0,1', optstr])
|
||||
else
|
||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
||||
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
|
||||
endif
|
||||
|
||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if use_term
|
||||
@@ -1020,8 +1027,23 @@ if has('nvim')
|
||||
let buf = nvim_create_buf(v:false, v:true)
|
||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||
let win = nvim_open_win(buf, v:true, opts)
|
||||
silent! call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
|
||||
call setwinvar(win, '&colorcolumn', '')
|
||||
|
||||
" Colors
|
||||
try
|
||||
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
|
||||
let rules = get(g:, 'fzf_colors', {})
|
||||
if has_key(rules, 'bg')
|
||||
let color = call('s:get_color', rules.bg)
|
||||
if len(color)
|
||||
let ns = nvim_create_namespace('fzf_popup')
|
||||
let hl = nvim_set_hl(ns, 'Normal',
|
||||
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
|
||||
call nvim_win_set_hl_ns(win, ns)
|
||||
endif
|
||||
endif
|
||||
catch
|
||||
endtry
|
||||
return buf
|
||||
endfunction
|
||||
else
|
||||
@@ -1081,7 +1103,7 @@ endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': ['--multi'] }
|
||||
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
|
||||
141
shell/common.fish
Normal file
141
shell/common.fish
Normal file
@@ -0,0 +1,141 @@
|
||||
function __fzf_defaults
|
||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||
# Get tokens - use version-appropriate flags
|
||||
set -l tokens
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
set -- tokens (commandline -xpc)
|
||||
else
|
||||
set -- tokens (commandline -opc)
|
||||
end
|
||||
|
||||
# Filter out leading environment variable assignments
|
||||
set -l -- var_count 0
|
||||
for i in $tokens
|
||||
if string match -qr -- '^[\w]+=' $i
|
||||
set var_count (math $var_count + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
set -e -- tokens[0..$var_count]
|
||||
|
||||
# Skip command prefixes so callers see the actual command name,
|
||||
# e.g. "builtin cd" → "cd", "env VAR=1 command cd" → "cd"
|
||||
while true
|
||||
switch "$tokens[1]"
|
||||
case builtin command
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
case env
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||
set -e -- tokens[1]
|
||||
end
|
||||
case '*'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- $tokens
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
# Set variables containing the major and minor fish version numbers, using
|
||||
# a method compatible with all supported fish versions.
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
if test -n "$fzf_query"
|
||||
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
# fish v3.5.0 and newer
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.4.1
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish v3.1b1 - v3.1.2
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
# Strip $dir from $fzf_query - preserve trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
40
shell/common.sh
Normal file
40
shell/common.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
# This function performs `exec awk "$@"` safely by working around awk
|
||||
# compatibility issues.
|
||||
#
|
||||
# To reduce an extra fork, this function performs "exec" so is expected to be
|
||||
# run as the last command in a subshell.
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
# Note: Solaris awk at /usr/bin/awk is meant for backward compatibility
|
||||
# with an ancient implementation of 1977 awk in the original UNIX. It
|
||||
# lacks many features of POSIX awk, so it is essentially useless in the
|
||||
# modern point of view. To use a standard-conforming version in Solaris,
|
||||
# one needs to explicitly use /usr/xpg4/bin/awk.
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
# choose the faster mawk if: it's installed && build date >= 20230322 &&
|
||||
# version >= 1.3.4
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
# Note: macOS awk has a quirk that it stops processing at all when it sees
|
||||
# any data not following UTF-8 in the input stream when the current LC_CTYPE
|
||||
# specifies the UTF-8 encoding. To work around this quirk, one needs to
|
||||
# specify LC_ALL=C to change the current encoding to the plain one.
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
@@ -31,21 +31,40 @@ if [[ $- =~ i ]]; then
|
||||
|
||||
###########################################################
|
||||
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
#----BEGIN shfmt
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
||||
if [[ "$(type -t _fzf_comprun 2>&1)" == function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
elif [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; }; then
|
||||
elif [[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; }; then
|
||||
shift
|
||||
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
||||
else
|
||||
@@ -57,13 +76,13 @@ __fzf_comprun() {
|
||||
__fzf_orig_completion() {
|
||||
local l comp f cmd
|
||||
while read -r l; do
|
||||
if [[ "$l" =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
||||
if [[ $l =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
||||
comp="${BASH_REMATCH[1]}"
|
||||
f="${BASH_REMATCH[2]}"
|
||||
cmd="${BASH_REMATCH[3]}"
|
||||
[[ "$f" = _fzf_* ]] && continue
|
||||
[[ $f == _fzf_* ]] && continue
|
||||
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||
if [[ "$l" = *" -o nospace "* ]] && [[ ! "${__fzf_nospace_commands-}" = *" $cmd "* ]]; then
|
||||
if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
|
||||
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
||||
fi
|
||||
fi
|
||||
@@ -99,12 +118,13 @@ _fzf_opts_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD - 1]}"
|
||||
opts="
|
||||
+c --no-color
|
||||
+i --no-ignore-case
|
||||
+s --no-sort
|
||||
+x --no-extended
|
||||
--accept-nth
|
||||
--ansi
|
||||
--bash
|
||||
--bind
|
||||
@@ -118,56 +138,88 @@ _fzf_opts_completion() {
|
||||
--expect
|
||||
--filepath-word
|
||||
--fish
|
||||
--footer
|
||||
--footer-border
|
||||
--footer-label
|
||||
--footer-label-pos
|
||||
--freeze-left
|
||||
--freeze-right
|
||||
--gap
|
||||
--gap-line
|
||||
--ghost
|
||||
--gutter
|
||||
--gutter-raw
|
||||
--header
|
||||
--header-border
|
||||
--header-first
|
||||
--header-label
|
||||
--header-label-pos
|
||||
--header-lines
|
||||
--header-lines-border
|
||||
--height
|
||||
--highlight-line
|
||||
--history
|
||||
--history-size
|
||||
--hscroll-off
|
||||
--info
|
||||
--info-command
|
||||
--input-border
|
||||
--input-label
|
||||
--input-label-pos
|
||||
--jump-labels
|
||||
--keep-right
|
||||
--layout
|
||||
--listen
|
||||
--listen-unsafe
|
||||
--list-border
|
||||
--list-label
|
||||
--list-label-pos
|
||||
--literal
|
||||
--man
|
||||
--margin
|
||||
--marker
|
||||
--marker-multi-line
|
||||
--min-height
|
||||
--no-bold
|
||||
--no-clear
|
||||
--no-hscroll
|
||||
--no-mouse
|
||||
--no-input
|
||||
--no-multi-line
|
||||
--no-scrollbar
|
||||
--no-separator
|
||||
--no-unicode
|
||||
--padding
|
||||
--pointer
|
||||
--preview
|
||||
--preview-border
|
||||
--preview-label
|
||||
--preview-label-pos
|
||||
--preview-window
|
||||
--print-query
|
||||
--print0
|
||||
--prompt
|
||||
--raw
|
||||
--read0
|
||||
--reverse
|
||||
--scheme
|
||||
--scroll-off
|
||||
--scrollbar
|
||||
--separator
|
||||
--smart-case
|
||||
--style
|
||||
--sync
|
||||
--tabstop
|
||||
--tac
|
||||
--tail
|
||||
--tiebreak
|
||||
--tmux
|
||||
--track
|
||||
--version
|
||||
--walker
|
||||
--walker-root
|
||||
--walker-skip
|
||||
--with-nth
|
||||
--with-shell
|
||||
--wrap
|
||||
--wrap-sign
|
||||
--preview-wrap-sign
|
||||
--zsh
|
||||
-0 --exit-0
|
||||
-1 --select-1
|
||||
@@ -182,32 +234,41 @@ _fzf_opts_completion() {
|
||||
--"
|
||||
|
||||
case "${prev}" in
|
||||
--scheme)
|
||||
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--tiebreak)
|
||||
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--layout)
|
||||
COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--info)
|
||||
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--preview-window)
|
||||
COMPREPLY=( $(compgen -W "
|
||||
--scheme)
|
||||
COMPREPLY=($(compgen -W "default path history" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--tiebreak)
|
||||
COMPREPLY=($(compgen -W "length chunk pathname begin end index" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=($(compgen -W "dark light base16 16 bw no" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--layout)
|
||||
COMPREPLY=($(compgen -W "default reverse reverse-list" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--info)
|
||||
COMPREPLY=($(compgen -W "default right hidden inline inline-right" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--wrap)
|
||||
COMPREPLY=($(compgen -W "char word" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--style)
|
||||
COMPREPLY=($(compgen -W "default minimal full" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--preview-window)
|
||||
COMPREPLY=($(compgen -W "
|
||||
default
|
||||
hidden
|
||||
nohidden
|
||||
wrap
|
||||
wrap-word
|
||||
nowrap
|
||||
cycle
|
||||
nocycle
|
||||
@@ -216,6 +277,7 @@ _fzf_opts_completion() {
|
||||
left
|
||||
right
|
||||
rounded border border-rounded
|
||||
border-line
|
||||
sharp border-sharp
|
||||
border-bold
|
||||
border-block
|
||||
@@ -229,21 +291,23 @@ _fzf_opts_completion() {
|
||||
border-left
|
||||
border-right
|
||||
follow
|
||||
nofollow" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--border)
|
||||
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--border-label-pos|--preview-label-pos)
|
||||
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
nofollow
|
||||
info
|
||||
noinfo" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--border | --list-border | --header-border | --header-lines-border | --footer-border | --input-border | --preview-border)
|
||||
COMPREPLY=($(compgen -W "line rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--border-label-pos | --preview-label-pos | --list-label-pos | --header-label-pos | --footer-label-pos | --input-label-pos)
|
||||
COMPREPLY=($(compgen -W "center bottom top" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$cur" =~ ^-|\+ ]]; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
|
||||
if [[ $cur =~ ^-|\+ ]]; then
|
||||
COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -257,7 +321,7 @@ _fzf_handle_dynamic_completion() {
|
||||
orig_cmd="$1"
|
||||
if __fzf_orig_completion_get_orig_func "$cmd"; then
|
||||
"$REPLY" "$@"
|
||||
elif [[ -n "${_fzf_completion_loader-}" ]]; then
|
||||
elif [[ -n ${_fzf_completion_loader-} ]]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
$_fzf_completion_loader "$@"
|
||||
ret=$?
|
||||
@@ -271,7 +335,7 @@ _fzf_handle_dynamic_completion() {
|
||||
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
|
||||
orig_complete=$REPLY
|
||||
|
||||
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
|
||||
if [[ ${__fzf_nospace_commands-} == *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
eval "$orig_complete"
|
||||
@@ -291,48 +355,53 @@ __fzf_generic_path_completion() {
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
|
||||
dir=
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
[[ $base == *"/"* ]] && dir="$base"
|
||||
while true; do
|
||||
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
|
||||
leftover=${base/#"$dir"}
|
||||
leftover=${leftover/#\/}
|
||||
[[ -z "$dir" ]] && dir='.'
|
||||
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||
if [[ -z $dir ]] || [[ -d $dir ]]; then
|
||||
leftover=${base/#"$dir"/}
|
||||
leftover=${leftover/#\//}
|
||||
[[ -z $dir ]] && dir='.'
|
||||
[[ $dir != "/" ]] && dir="${dir/%\//}"
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if [[ $1 =~ dir ]]; then
|
||||
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
||||
else
|
||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||
fi
|
||||
if declare -F "$1" > /dev/null; then
|
||||
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
|
||||
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
|
||||
else
|
||||
if [[ $1 =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||
fi | while read -r item; do
|
||||
printf "%q " "${item%$3}$3"
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [[ -n "$matches" ]]; then
|
||||
COMPREPLY=( "$matches" )
|
||||
[[ -z $3 ]] && [[ ${__fzf_nospace_commands-} == *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [[ -n $matches ]]; then
|
||||
COMPREPLY=("$matches")
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(command dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
[[ $dir =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
shift
|
||||
@@ -348,15 +417,15 @@ _fzf_complete() {
|
||||
args=("$@")
|
||||
sep=
|
||||
for i in "${!args[@]}"; do
|
||||
if [[ "${args[$i]}" = -- ]]; then
|
||||
if [[ ${args[$i]} == -- ]]; then
|
||||
sep=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n "$sep" ]]; then
|
||||
if [[ -n $sep ]]; then
|
||||
str_arg=
|
||||
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
||||
args=("${args[@]:0:$sep}")
|
||||
args=("${args[@]:0:sep}")
|
||||
else
|
||||
str_arg=$1
|
||||
args=()
|
||||
@@ -365,25 +434,27 @@ _fzf_complete() {
|
||||
fi
|
||||
|
||||
local cur selected trigger cmd post
|
||||
post="$(caller 0 | command awk '{print $2}')_post"
|
||||
post="$(caller 0 | __fzf_exec_awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post='command cat'
|
||||
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
selected=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' '
|
||||
)
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [[ -n "$selected" ]]; then
|
||||
if [[ -n $selected ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
else
|
||||
@@ -409,15 +480,41 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_proc_completion_post() {
|
||||
command awk '{print $2}'
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
# To use custom hostname lists, override __fzf_list_hosts.
|
||||
@@ -434,10 +531,54 @@ _fzf_proc_completion_post() {
|
||||
# }
|
||||
if ! declare -F __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "set -f" and "GLOBIGNORE", which disable the pathname expansion
|
||||
# totally or partially. We need to unset "dotglob" and "nocaseglob" to
|
||||
# avoid matching unwanted files. We need to unset "failglob" to avoid
|
||||
# outputting the error messages to the terminal when no matching is
|
||||
# found. We need to set "nullglob" to avoid attempting to read the
|
||||
# literal filename '~/.ssh/config.d/*' when no matching is found.
|
||||
set +f
|
||||
GLOBIGNORE=
|
||||
shopt -u dotglob nocaseglob failglob
|
||||
shopt -s nullglob
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -452,13 +593,13 @@ _fzf_host_completion() {
|
||||
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
|
||||
_fzf_complete_ssh() {
|
||||
case $3 in
|
||||
-i|-F|-E)
|
||||
-i | -F | -E)
|
||||
_fzf_path_completion "$@"
|
||||
;;
|
||||
*)
|
||||
local user=
|
||||
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | command awk -v user="$user" '{print user $0}')
|
||||
[[ $2 =~ '@' ]] && user="${2%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -546,7 +687,7 @@ __fzf_defc() {
|
||||
if __fzf_orig_completion_instantiate "$cmd" "$func"; then
|
||||
eval "$REPLY"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
eval "complete -F \"$func\" $opts \"$cmd\""
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -588,12 +729,13 @@ _fzf_setup_completion() {
|
||||
__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)
|
||||
for cmd in "$@"; do
|
||||
case "$kind" in
|
||||
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
||||
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
||||
alias) __fzf_defc "$cmd" "$fn" "-a" ;;
|
||||
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
#----END shfmt
|
||||
|
||||
fi
|
||||
|
||||
241
shell/completion.fish
Normal file
241
shell/completion.fish
Normal file
@@ -0,0 +1,241 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.fish
|
||||
#
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
|
||||
function fzf_completion_setup
|
||||
|
||||
#----BEGIN INCLUDE common.fish
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.fish".
|
||||
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.fish" for the implementation details.
|
||||
|
||||
function __fzf_defaults
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||
set -l tokens
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
set -- tokens (commandline -xpc)
|
||||
else
|
||||
set -- tokens (commandline -opc)
|
||||
end
|
||||
|
||||
set -l -- var_count 0
|
||||
for i in $tokens
|
||||
if string match -qr -- '^[\w]+=' $i
|
||||
set var_count (math $var_count + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
set -e -- tokens[0..$var_count]
|
||||
|
||||
while true
|
||||
switch "$tokens[1]"
|
||||
case builtin command
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
case env
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||
set -e -- tokens[1]
|
||||
end
|
||||
case '*'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- $tokens
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
if test -n "$fzf_query"
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
#----END INCLUDE
|
||||
|
||||
# Use complete builtin for specific commands
|
||||
function __fzf_complete_native
|
||||
set -l -- token (commandline -t)
|
||||
set -l -- completions (eval complete -C \"$argv[1]\")
|
||||
test -n "$completions"; or begin commandline -f repaint; return; end
|
||||
|
||||
# Calculate tabstop based on longest completion item (sample first 500 for performance)
|
||||
set -l -- tabstop 20
|
||||
set -l -- sample_size (math "min(500, "(count $completions)")")
|
||||
for c in $completions[1..$sample_size]
|
||||
set -l -- len (string length -V -- (string split -- \t $c))
|
||||
test -n "$len[2]" -a "$len[1]" -gt "$tabstop"
|
||||
and set -- tabstop $len[1]
|
||||
end
|
||||
# limit to 120 to prevent long lines
|
||||
set -- tabstop (math "min($tabstop + 4, 120)")
|
||||
|
||||
set -l result
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --delimiter=\\t --nth=1 --tabstop=$tabstop --color=fg:dim,nth:regular" \
|
||||
$FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1 --read0 --print0)
|
||||
set -- result (string join0 -- $completions | eval (__fzfcmd) | string split0)
|
||||
and begin
|
||||
set -l -- tail ' '
|
||||
# Append / to bare ~username results (fish omits it unlike other shells)
|
||||
set -- result (string replace -r -- '^(~\w+)\s?$' '$1/' $result)
|
||||
# Don't add trailing space if single result is a directory
|
||||
test (count $result) -eq 1
|
||||
and string match -q -- '*/' "$result"; and set -- tail ''
|
||||
|
||||
set -l -- result (string escape -n -- $result)
|
||||
|
||||
string match -q -- '~*' "$token"
|
||||
and set result (string replace -r -- '^\\\\~' '~' $result)
|
||||
|
||||
string match -q -- '$*' "$token"
|
||||
and set result (string replace -r -- '^\\\\\$' '\$' $result)
|
||||
|
||||
commandline -rt -- (string join ' ' -- $result)$tail
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function _fzf_complete
|
||||
set -l -- args (string escape -- $argv | string join ' ' | string split -- ' -- ')
|
||||
set -l -- post_func (status function)_(string split -- ' ' $args[2])[1]_post
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS $args[1])
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND
|
||||
set -l -- fzf_query (commandline -t | string escape)
|
||||
set -l result
|
||||
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
|
||||
and if functions -q $post_func
|
||||
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
|
||||
else
|
||||
commandline -rt -- (string join -- ' ' (string escape -- $result))' '
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
# Kill completion (process selection)
|
||||
function _fzf_complete_kill
|
||||
set -l -- fzf_query (commandline -t | string escape)
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
|
||||
--accept-nth=2 -m --header-lines=1 --no-preview --wrap)
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
if type -q ps
|
||||
set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
|
||||
'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
|
||||
'or command ps --everyone --full --windows 2>/dev/null; end'
|
||||
set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$fzf_query)
|
||||
and commandline -rt -- (string join ' ' -- $result)" "
|
||||
else
|
||||
__fzf_complete_native "kill " --multi --query=$fzf_query
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
# Main completion function
|
||||
function fzf-completion
|
||||
set -l -- tokens (__fzf_cmd_tokens)
|
||||
set -l -- current_token (commandline -t)
|
||||
set -l -- cmd_name $tokens[1]
|
||||
|
||||
# Route to appropriate completion function
|
||||
if test -n "$tokens"; and functions -q _fzf_complete_$cmd_name
|
||||
_fzf_complete_$cmd_name $tokens
|
||||
else
|
||||
set -l -- fzf_opt --query=$current_token --multi
|
||||
__fzf_complete_native "$tokens $current_token" $fzf_opt
|
||||
end
|
||||
end
|
||||
|
||||
# Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
bind shift-tab fzf-completion
|
||||
bind -M insert shift-tab fzf-completion
|
||||
else
|
||||
bind -k btab fzf-completion
|
||||
bind -M insert -k btab fzf-completion
|
||||
end
|
||||
end
|
||||
|
||||
# Run setup
|
||||
fzf_completion_setup
|
||||
@@ -96,14 +96,35 @@ if [[ -o interactive ]]; then
|
||||
|
||||
###########################################################
|
||||
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
@@ -122,11 +143,10 @@ __fzf_comprun() {
|
||||
|
||||
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
|
||||
__fzf_extract_command() {
|
||||
setopt localoptions noksh_arrays
|
||||
# Control completion with the "compstate" parameter, insert and list noting
|
||||
# Control completion with the "compstate" parameter, insert and list nothing
|
||||
compstate[insert]=
|
||||
compstate[list]=
|
||||
cmd_word="${words[1]}"
|
||||
cmd_word="${(Q)words[1]}"
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
@@ -154,15 +174,18 @@ __fzf_generic_path_completion() {
|
||||
export FZF_DEFAULT_OPTS
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if [[ $compgen =~ dir ]]; then
|
||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||
else
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
if declare -f "$compgen" > /dev/null; then
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" ${(Q)${(Z+n+)rest}}
|
||||
else
|
||||
if [[ $compgen =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
fi | while read -r item; do
|
||||
@@ -243,11 +266,50 @@ _fzf_complete() {
|
||||
# desired sorting and with any duplicates removed, to standard output.
|
||||
if ! declare -f __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
setopt localoptions nonomatch
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||
awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "NO_GLOB" (or reset "GLOB"), which disable the pathname
|
||||
# expansion totally. We need to unset "DOT_GLOB" and set "CASE_GLOB"
|
||||
# to avoid matching unwanted files. We need to set "NULL_GLOB" to
|
||||
# avoid attempting to read the literal filename '~/.ssh/config.d/*'
|
||||
# when no matching is found.
|
||||
setopt GLOB NO_DOT_GLOB CASE_GLOB NO_NOMATCH NULL_GLOB
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -267,7 +329,7 @@ _fzf_complete_ssh() {
|
||||
*)
|
||||
local user
|
||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -291,7 +353,33 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
@@ -299,20 +387,13 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill_post() {
|
||||
awk '{print $2}'
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
fzf-completion() {
|
||||
trap 'unset cmd_word' EXIT
|
||||
local tokens prefix trigger tail matches lbuf d_cmds
|
||||
local tokens prefix trigger tail matches lbuf d_cmds cursor_pos cmd_word
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# Check if at least one completion system (old or new) is active
|
||||
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
|
||||
if ! zmodload -e zsh/compctl; then
|
||||
zmodload -i zsh/compctl
|
||||
fi
|
||||
fi
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
||||
tokens=(${(z)LBUFFER})
|
||||
@@ -323,7 +404,7 @@ fzf-completion() {
|
||||
|
||||
# Explicitly allow for empty trigger.
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
||||
[[ -z $trigger && ${LBUFFER[-1]} == ' ' ]] && tokens+=("")
|
||||
|
||||
# When the trigger starts with ';', it becomes a separate token
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
|
||||
@@ -338,9 +419,26 @@ fzf-completion() {
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
|
||||
|
||||
# Make the 'cmd_word' global
|
||||
zle __fzf_extract_command || :
|
||||
[[ -z "$cmd_word" ]] && return
|
||||
{
|
||||
cursor_pos=$CURSOR
|
||||
# Move the cursor before the trigger to preserve word array elements when
|
||||
# trigger chars like ';' or '`' would otherwise reset the 'words' array.
|
||||
CURSOR=$((cursor_pos - ${#trigger} - 1))
|
||||
# Check if at least one completion system (old or new) is active.
|
||||
# If at least one user-defined completion widget is detected, nothing will
|
||||
# be completed if neither the old nor the new completion system is enabled.
|
||||
# In such cases, the 'zsh/compctl' module is loaded as a fallback.
|
||||
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
|
||||
zmodload -F zsh/compctl 2>/dev/null
|
||||
fi
|
||||
# Create a completion widget to access the 'words' array (man zshcompwid)
|
||||
zle -C __fzf_extract_command .complete-word __fzf_extract_command
|
||||
zle __fzf_extract_command
|
||||
} always {
|
||||
CURSOR=$cursor_pos
|
||||
# Delete the completion widget
|
||||
zle -D __fzf_extract_command 2>/dev/null
|
||||
}
|
||||
|
||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
|
||||
@@ -348,7 +446,7 @@ fzf-completion() {
|
||||
fi
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _fzf_complete_${cmd_word} > /dev/null"; then
|
||||
if eval "noglob type _fzf_complete_${cmd_word} >/dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
|
||||
zle reset-prompt
|
||||
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
|
||||
@@ -368,8 +466,6 @@ fzf-completion() {
|
||||
unset binding
|
||||
}
|
||||
|
||||
# Completion widget to gain access to the 'words' array (man zshcompwid)
|
||||
zle -C __fzf_extract_command .complete-word __fzf_extract_command
|
||||
# Normal widget
|
||||
zle -N fzf-completion
|
||||
bindkey '^I' fzf-completion
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
@@ -17,40 +18,62 @@ if [[ $- =~ i ]]; then
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
#----BEGIN shfmt
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_select__() {
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
|
||||
while read -r item; do
|
||||
printf '%q ' "$item" # escape special chars
|
||||
printf '%q ' "$item" # escape special chars
|
||||
done
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
|
||||
[[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
local selected="$(__fzf_select__ "$@")"
|
||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
||||
READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}$selected${READLINE_LINE:READLINE_POINT}"
|
||||
READLINE_POINT=$((READLINE_POINT + ${#selected}))
|
||||
}
|
||||
|
||||
__fzf_cd__() {
|
||||
local dir
|
||||
dir=$(
|
||||
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
|
||||
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
|
||||
}
|
||||
|
||||
@@ -62,11 +85,11 @@ if command -v perl > /dev/null; then
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
@@ -74,14 +97,8 @@ if command -v perl > /dev/null; then
|
||||
}
|
||||
else # awk - fallback for POSIX systems
|
||||
__fzf_history__() {
|
||||
local output script n x y z d
|
||||
if [[ -z $__fzf_awk ]]; then
|
||||
__fzf_awk=awk
|
||||
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
|
||||
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
|
||||
fi
|
||||
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
||||
local output script
|
||||
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
||||
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
|
||||
NR==1 { b = substr($0, 2); next }
|
||||
/^\t/ { P(b); b = substr($0, 2); next }
|
||||
@@ -89,13 +106,13 @@ else # awk - fallback for POSIX systems
|
||||
END { if (NR) P(b) }'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
@@ -104,43 +121,54 @@ else # awk - fallback for POSIX systems
|
||||
fi
|
||||
|
||||
# Required to refresh the prompt after fzf
|
||||
bind -m emacs-standard '"\er": redraw-current-line'
|
||||
bind -m emacs-standard '"\C-\e(": redraw-current-line'
|
||||
|
||||
bind -m vi-command '"\C-z": emacs-editing-mode'
|
||||
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
||||
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||
|
||||
if (( BASH_VERSINFO[0] < 4 )); then
|
||||
if ((BASH_VERSINFO[0] < 4)); then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\C-\e(\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\C-\e("'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
fi
|
||||
else
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-command -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-insert -x '"\C-r": __fzf_history__'
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-command -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-insert -x '"\C-r": __fzf_history__'
|
||||
fi
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
if [[ ${FZF_ALT_C_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\C-\e(\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
fi
|
||||
#----END shfmt
|
||||
|
||||
fi
|
||||
|
||||
@@ -7,26 +7,155 @@
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
status is-interactive; or exit 0
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
|
||||
# command substitution syntax $(cmd) should never be used, even behind a version
|
||||
# check, otherwise the source command will fail on fish versions older than 3.4.0.
|
||||
function fzf_key_bindings
|
||||
|
||||
function __fzf_defaults
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
|
||||
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
|
||||
echo $FZF_DEFAULT_OPTS $argv[2]
|
||||
# Check fish version
|
||||
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
|
||||
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
|
||||
echo "This script requires fish version 3.1b1 or newer." >&2
|
||||
return 1
|
||||
else if not type -q fzf
|
||||
echo "fzf was not found in path." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
#----BEGIN INCLUDE common.fish
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.fish".
|
||||
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.fish" for the implementation details.
|
||||
|
||||
function __fzf_defaults
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||
set -l tokens
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
set -- tokens (commandline -xpc)
|
||||
else
|
||||
set -- tokens (commandline -opc)
|
||||
end
|
||||
|
||||
set -l -- var_count 0
|
||||
for i in $tokens
|
||||
if string match -qr -- '^[\w]+=' $i
|
||||
set var_count (math $var_count + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
set -e -- tokens[0..$var_count]
|
||||
|
||||
while true
|
||||
switch "$tokens[1]"
|
||||
case builtin command
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
case env
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||
set -e -- tokens[1]
|
||||
end
|
||||
case '*'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- $tokens
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
if test -n "$fzf_query"
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
#----END INCLUDE
|
||||
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
@@ -34,59 +163,81 @@ function fzf_key_bindings
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
commandline -f repaint
|
||||
return
|
||||
else
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- $prefix
|
||||
commandline -it -- (string escape $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||
"--multi $FZF_CTRL_T_OPTS --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||
and commandline -rt -- (string join -- ' ' $prefix(string escape --no-quoted -- $result))' '
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
||||
set -l -- command_line (commandline)
|
||||
set -l -- current_line (commandline -L)
|
||||
set -l -- total_lines (count $command_line)
|
||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||
|
||||
# merge history from other sessions before searching
|
||||
if test -z "$fish_private_mode"
|
||||
builtin history merge
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||
'--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
|
||||
'--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
|
||||
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
|
||||
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
|
||||
'--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||
|
||||
# Add dynamic preview options if preview command isn't already set by user
|
||||
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
|
||||
# Convert the highlighted timestamp using the date command if available
|
||||
set -l -- date_cmd '{1}'
|
||||
if type -q date
|
||||
if date -d @0 '+%s' 2>/dev/null | string match -q 0
|
||||
# GNU date
|
||||
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
|
||||
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
|
||||
# BSD date
|
||||
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
|
||||
end
|
||||
end
|
||||
|
||||
# history's -z flag is needed for multi-line support.
|
||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
||||
# before 2.4.0.
|
||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
||||
if type -P perl > /dev/null 2>&1
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
end
|
||||
# Prepend the options to allow user customizations
|
||||
set -p -- FZF_DEFAULT_OPTS \
|
||||
'--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
|
||||
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
|
||||
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
|
||||
end
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
|
||||
|
||||
# Enable syntax highlighting colors on fish v4.3.3 and newer
|
||||
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
|
||||
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
|
||||
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
|
||||
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
|
||||
|
||||
set -a -- FZF_DEFAULT_OPTS '--ansi'
|
||||
set -a -- FZF_DEFAULT_COMMAND '--color=always'
|
||||
end
|
||||
|
||||
# Merge history from other sessions before searching
|
||||
test -z "$fish_private_mode"; and builtin history merge
|
||||
|
||||
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
|
||||
if test "$total_lines" -eq 1
|
||||
commandline -- $result
|
||||
else
|
||||
builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
set -l a (math $current_line - 1)
|
||||
set -l b (math $current_line + 1)
|
||||
commandline -- $command_line[1..$a] $result
|
||||
commandline -a -- '' $command_line[$b..-1]
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
@@ -96,102 +247,40 @@ function fzf_key_bindings
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=dir,follow,hidden --scheme=path" \
|
||||
"$FZF_ALT_C_OPTS --no-multi --print0")
|
||||
|
||||
if [ -n "$result" ]
|
||||
cd -- $result
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
commandline -it -- $prefix
|
||||
end
|
||||
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
|
||||
cd -- $result
|
||||
commandline -rt -- $prefix
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
if [ -n "$FZF_TMUX_OPTS" ]
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if [ $FZF_TMUX -eq 1 ]
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
if not set -q FZF_CTRL_R_COMMAND; or test -n "$FZF_CTRL_R_COMMAND"
|
||||
if test -n "$FZF_CTRL_R_COMMAND"
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
end
|
||||
bind \cr fzf-history-widget
|
||||
bind -M insert \cr fzf-history-widget
|
||||
end
|
||||
|
||||
bind \cr fzf-history-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind \ct fzf-file-widget
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
if bind -M insert > /dev/null 2>&1
|
||||
bind -M insert \cr fzf-history-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l commandline (commandline -t)
|
||||
|
||||
# strip -option= from token if present
|
||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||
set commandline (string replace -- "$prefix" '' $commandline)
|
||||
|
||||
# eval is used to do shell expansion on paths
|
||||
eval set commandline $commandline
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
|
||||
if [ "$dir" = "." -a (string sub -l 1 -- $commandline) != '.' ]
|
||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||
set fzf_query $commandline
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r "^$dir/?" -- '' "$commandline")
|
||||
end
|
||||
end
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
echo $prefix
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
|
||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||
if [ (string length -- $dir) -gt 1 ]
|
||||
set dir (string replace -r '/*$' -- '' $dir)
|
||||
end
|
||||
|
||||
# Iteratively check if dir exists and strip tail end of path
|
||||
while [ ! -d "$dir" ]
|
||||
# If path is absolute, this can keep going until ends up at /
|
||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||
set dir (dirname -- "$dir")
|
||||
end
|
||||
|
||||
echo $dir
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Run setup
|
||||
fzf_key_bindings
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
@@ -38,14 +39,35 @@ fi
|
||||
{
|
||||
if [[ -o interactive ]]; then
|
||||
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__fzf_select() {
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
@@ -106,31 +128,52 @@ fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||
local selected extracted_with_perl=0
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_ksharrays extendedglob 2> /dev/null
|
||||
# Ensure the module is loaded if not already, and the required features, such
|
||||
# as the associative 'history' array, which maps event numbers to full history
|
||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||
# Import commands from other shells if SHARE_HISTORY is enabled, as the
|
||||
# 'history' array only updates after executing a non-empty command.
|
||||
selected="$(
|
||||
if [[ -o sharehistory ]]; then
|
||||
fc -RI
|
||||
fi
|
||||
printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
extracted_with_perl=1
|
||||
else
|
||||
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||
selected="$(fc -rl 1 | __fzf_exec_awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER}") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
fi
|
||||
local ret=$?
|
||||
local -a cmds
|
||||
# Avoid leaking auto assigned values when using backreferences '(#b)'
|
||||
local -a mbegin mend match
|
||||
if [ -n "$selected" ]; then
|
||||
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
||||
zle vi-fetch-history -n $MATCH
|
||||
# Heuristic to check if the selected value is from history or a custom query
|
||||
if ((( extracted_with_perl )) && [[ $selected == <->$'\t'* ]]) ||
|
||||
((( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->( |\* )* ]]); then
|
||||
# Split at newlines
|
||||
for line in ${(ps:\n:)selected}; do
|
||||
if (( extracted_with_perl )); then
|
||||
if [[ $line == (#b)(<->)(#B)$'\t'* ]]; then
|
||||
(( ${+history[${match[1]}]} )) && cmds+=("${history[${match[1]}]}")
|
||||
fi
|
||||
elif [[ $line == [[:blank:]]#(#b)(<->)(#B)( |\* )* ]]; then
|
||||
# Avoid $history array: lags behind 'fc' on foreign commands (*)
|
||||
# https://zsh.org/mla/users/2024/msg00692.html
|
||||
# Push BUFFER onto stack; fetch and save history entry from BUFFER; restore
|
||||
zle .push-line
|
||||
zle vi-fetch-history -n ${match[1]}
|
||||
(( ${#BUFFER} )) && cmds+=("${BUFFER}")
|
||||
BUFFER=""
|
||||
zle .get-line
|
||||
fi
|
||||
done
|
||||
if (( ${#cmds[@]} )); then
|
||||
# Join by newline after stripping trailing newlines from each command
|
||||
BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"
|
||||
CURSOR=${#BUFFER}
|
||||
fi
|
||||
else # selected is a custom query, not from history
|
||||
LBUFFER="$selected"
|
||||
fi
|
||||
@@ -138,10 +181,15 @@ fzf-history-widget() {
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey -M emacs '^R' fzf-history-widget
|
||||
bindkey -M vicmd '^R' fzf-history-widget
|
||||
bindkey -M viins '^R' fzf-history-widget
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
zle -N fzf-history-widget
|
||||
bindkey -M emacs '^R' fzf-history-widget
|
||||
bindkey -M vicmd '^R' fzf-history-widget
|
||||
bindkey -M viins '^R' fzf-history-widget
|
||||
fi
|
||||
fi
|
||||
|
||||
} always {
|
||||
|
||||
70
shell/update.sh
Executable file
70
shell/update.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script applies the contents of "common.sh" to the other files.
|
||||
|
||||
set -e
|
||||
|
||||
dir=${0%"${0##*/}"}
|
||||
|
||||
update() {
|
||||
{
|
||||
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
|
||||
cat << EOF
|
||||
# NOTE: Do not directly edit this section, which is copied from "$1".
|
||||
# To modify it, one can edit "$1" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "$1" for the implementation details.
|
||||
EOF
|
||||
echo
|
||||
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
|
||||
sed -n '/^#----END INCLUDE/,$p' "$2"
|
||||
} > "$2.part"
|
||||
|
||||
mv -f "$2.part" "$2"
|
||||
}
|
||||
|
||||
update "common.sh" "$dir/completion.bash"
|
||||
update "common.sh" "$dir/completion.zsh"
|
||||
update "common.sh" "$dir/key-bindings.bash"
|
||||
update "common.sh" "$dir/key-bindings.zsh"
|
||||
update "common.fish" "$dir/completion.fish"
|
||||
update "common.fish" "$dir/key-bindings.fish"
|
||||
|
||||
# Check if --check is in ARGV
|
||||
check=0
|
||||
rest=()
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--check) check=1 ;;
|
||||
*) rest+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
fmt() {
|
||||
if ! grep -q "^#----BEGIN shfmt" "$1"; then
|
||||
if [[ $check == 1 ]]; then
|
||||
shfmt -d "$1"
|
||||
return $?
|
||||
else
|
||||
shfmt -w "$1"
|
||||
fi
|
||||
else
|
||||
{
|
||||
sed -n '1,/^#----BEGIN shfmt/p' "$1" | sed '$d'
|
||||
sed -n '/^#----BEGIN shfmt/,/^#----END shfmt/p' "$1" | shfmt --filename "$1"
|
||||
sed -n '/^#----END shfmt/,$p' "$1" | sed '1d'
|
||||
} > "$1.part"
|
||||
|
||||
if [[ $check == 1 ]]; then
|
||||
diff -q "$1" "$1.part"
|
||||
ret=$?
|
||||
rm -f "$1.part"
|
||||
return $ret
|
||||
fi
|
||||
|
||||
mv -f "$1.part" "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
for file in "${rest[@]}"; do
|
||||
fmt "$file" || exit $?
|
||||
done
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -12,120 +12,182 @@ func _() {
|
||||
_ = x[actStart-1]
|
||||
_ = x[actClick-2]
|
||||
_ = x[actInvalid-3]
|
||||
_ = x[actChar-4]
|
||||
_ = x[actMouse-5]
|
||||
_ = x[actBeginningOfLine-6]
|
||||
_ = x[actAbort-7]
|
||||
_ = x[actAccept-8]
|
||||
_ = x[actAcceptNonEmpty-9]
|
||||
_ = x[actAcceptOrPrintQuery-10]
|
||||
_ = x[actBackwardChar-11]
|
||||
_ = x[actBackwardDeleteChar-12]
|
||||
_ = x[actBackwardDeleteCharEof-13]
|
||||
_ = x[actBackwardWord-14]
|
||||
_ = x[actCancel-15]
|
||||
_ = x[actChangeBorderLabel-16]
|
||||
_ = x[actChangeHeader-17]
|
||||
_ = x[actChangeMulti-18]
|
||||
_ = x[actChangePreviewLabel-19]
|
||||
_ = x[actChangePrompt-20]
|
||||
_ = x[actChangeQuery-21]
|
||||
_ = x[actClearScreen-22]
|
||||
_ = x[actClearQuery-23]
|
||||
_ = x[actClearSelection-24]
|
||||
_ = x[actClose-25]
|
||||
_ = x[actDeleteChar-26]
|
||||
_ = x[actDeleteCharEof-27]
|
||||
_ = x[actEndOfLine-28]
|
||||
_ = x[actFatal-29]
|
||||
_ = x[actForwardChar-30]
|
||||
_ = x[actForwardWord-31]
|
||||
_ = x[actKillLine-32]
|
||||
_ = x[actKillWord-33]
|
||||
_ = x[actUnixLineDiscard-34]
|
||||
_ = x[actUnixWordRubout-35]
|
||||
_ = x[actYank-36]
|
||||
_ = x[actBackwardKillWord-37]
|
||||
_ = x[actSelectAll-38]
|
||||
_ = x[actDeselectAll-39]
|
||||
_ = x[actToggle-40]
|
||||
_ = x[actToggleSearch-41]
|
||||
_ = x[actToggleAll-42]
|
||||
_ = x[actToggleDown-43]
|
||||
_ = x[actToggleUp-44]
|
||||
_ = x[actToggleIn-45]
|
||||
_ = x[actToggleOut-46]
|
||||
_ = x[actToggleTrack-47]
|
||||
_ = x[actToggleTrackCurrent-48]
|
||||
_ = x[actToggleHeader-49]
|
||||
_ = x[actToggleWrap-50]
|
||||
_ = x[actTrackCurrent-51]
|
||||
_ = x[actUntrackCurrent-52]
|
||||
_ = x[actDown-53]
|
||||
_ = x[actUp-54]
|
||||
_ = x[actPageUp-55]
|
||||
_ = x[actPageDown-56]
|
||||
_ = x[actPosition-57]
|
||||
_ = x[actHalfPageUp-58]
|
||||
_ = x[actHalfPageDown-59]
|
||||
_ = x[actOffsetUp-60]
|
||||
_ = x[actOffsetDown-61]
|
||||
_ = x[actOffsetMiddle-62]
|
||||
_ = x[actJump-63]
|
||||
_ = x[actJumpAccept-64]
|
||||
_ = x[actPrintQuery-65]
|
||||
_ = x[actRefreshPreview-66]
|
||||
_ = x[actReplaceQuery-67]
|
||||
_ = x[actToggleSort-68]
|
||||
_ = x[actShowPreview-69]
|
||||
_ = x[actHidePreview-70]
|
||||
_ = x[actTogglePreview-71]
|
||||
_ = x[actTogglePreviewWrap-72]
|
||||
_ = x[actTransform-73]
|
||||
_ = x[actTransformBorderLabel-74]
|
||||
_ = x[actTransformHeader-75]
|
||||
_ = x[actTransformPreviewLabel-76]
|
||||
_ = x[actTransformPrompt-77]
|
||||
_ = x[actTransformQuery-78]
|
||||
_ = x[actPreview-79]
|
||||
_ = x[actChangePreview-80]
|
||||
_ = x[actChangePreviewWindow-81]
|
||||
_ = x[actPreviewTop-82]
|
||||
_ = x[actPreviewBottom-83]
|
||||
_ = x[actPreviewUp-84]
|
||||
_ = x[actPreviewDown-85]
|
||||
_ = x[actPreviewPageUp-86]
|
||||
_ = x[actPreviewPageDown-87]
|
||||
_ = x[actPreviewHalfPageUp-88]
|
||||
_ = x[actPreviewHalfPageDown-89]
|
||||
_ = x[actPrevHistory-90]
|
||||
_ = x[actPrevSelected-91]
|
||||
_ = x[actPrint-92]
|
||||
_ = x[actPut-93]
|
||||
_ = x[actNextHistory-94]
|
||||
_ = x[actNextSelected-95]
|
||||
_ = x[actExecute-96]
|
||||
_ = x[actExecuteSilent-97]
|
||||
_ = x[actExecuteMulti-98]
|
||||
_ = x[actSigStop-99]
|
||||
_ = x[actFirst-100]
|
||||
_ = x[actLast-101]
|
||||
_ = x[actReload-102]
|
||||
_ = x[actReloadSync-103]
|
||||
_ = x[actDisableSearch-104]
|
||||
_ = x[actEnableSearch-105]
|
||||
_ = x[actSelect-106]
|
||||
_ = x[actDeselect-107]
|
||||
_ = x[actUnbind-108]
|
||||
_ = x[actRebind-109]
|
||||
_ = x[actBecome-110]
|
||||
_ = x[actShowHeader-111]
|
||||
_ = x[actHideHeader-112]
|
||||
_ = x[actBracketedPasteBegin-4]
|
||||
_ = x[actBracketedPasteEnd-5]
|
||||
_ = x[actChar-6]
|
||||
_ = x[actMouse-7]
|
||||
_ = x[actBeginningOfLine-8]
|
||||
_ = x[actAbort-9]
|
||||
_ = x[actAccept-10]
|
||||
_ = x[actAcceptNonEmpty-11]
|
||||
_ = x[actAcceptOrPrintQuery-12]
|
||||
_ = x[actBackwardChar-13]
|
||||
_ = x[actBackwardDeleteChar-14]
|
||||
_ = x[actBackwardDeleteCharEof-15]
|
||||
_ = x[actBackwardWord-16]
|
||||
_ = x[actBackwardSubWord-17]
|
||||
_ = x[actCancel-18]
|
||||
_ = x[actChangeBorderLabel-19]
|
||||
_ = x[actChangeGhost-20]
|
||||
_ = x[actChangeHeader-21]
|
||||
_ = x[actChangeHeaderLines-22]
|
||||
_ = x[actChangeFooter-23]
|
||||
_ = x[actChangeHeaderLabel-24]
|
||||
_ = x[actChangeFooterLabel-25]
|
||||
_ = x[actChangeInputLabel-26]
|
||||
_ = x[actChangeListLabel-27]
|
||||
_ = x[actChangeMulti-28]
|
||||
_ = x[actChangeNth-29]
|
||||
_ = x[actChangePointer-30]
|
||||
_ = x[actChangePreview-31]
|
||||
_ = x[actChangePreviewLabel-32]
|
||||
_ = x[actChangePreviewWindow-33]
|
||||
_ = x[actChangePrompt-34]
|
||||
_ = x[actChangeQuery-35]
|
||||
_ = x[actClearScreen-36]
|
||||
_ = x[actClearQuery-37]
|
||||
_ = x[actClearSelection-38]
|
||||
_ = x[actClose-39]
|
||||
_ = x[actDeleteChar-40]
|
||||
_ = x[actDeleteCharEof-41]
|
||||
_ = x[actEndOfLine-42]
|
||||
_ = x[actFatal-43]
|
||||
_ = x[actForwardChar-44]
|
||||
_ = x[actForwardWord-45]
|
||||
_ = x[actForwardSubWord-46]
|
||||
_ = x[actKillLine-47]
|
||||
_ = x[actKillWord-48]
|
||||
_ = x[actKillSubWord-49]
|
||||
_ = x[actUnixLineDiscard-50]
|
||||
_ = x[actUnixWordRubout-51]
|
||||
_ = x[actYank-52]
|
||||
_ = x[actBackwardKillWord-53]
|
||||
_ = x[actBackwardKillSubWord-54]
|
||||
_ = x[actSelectAll-55]
|
||||
_ = x[actDeselectAll-56]
|
||||
_ = x[actToggle-57]
|
||||
_ = x[actToggleSearch-58]
|
||||
_ = x[actToggleAll-59]
|
||||
_ = x[actToggleDown-60]
|
||||
_ = x[actToggleUp-61]
|
||||
_ = x[actToggleIn-62]
|
||||
_ = x[actToggleOut-63]
|
||||
_ = x[actToggleTrack-64]
|
||||
_ = x[actToggleTrackCurrent-65]
|
||||
_ = x[actToggleHeader-66]
|
||||
_ = x[actToggleWrap-67]
|
||||
_ = x[actToggleWrapWord-68]
|
||||
_ = x[actToggleMultiLine-69]
|
||||
_ = x[actToggleHscroll-70]
|
||||
_ = x[actToggleRaw-71]
|
||||
_ = x[actEnableRaw-72]
|
||||
_ = x[actDisableRaw-73]
|
||||
_ = x[actTrackCurrent-74]
|
||||
_ = x[actToggleInput-75]
|
||||
_ = x[actHideInput-76]
|
||||
_ = x[actShowInput-77]
|
||||
_ = x[actUntrackCurrent-78]
|
||||
_ = x[actDown-79]
|
||||
_ = x[actDownMatch-80]
|
||||
_ = x[actUp-81]
|
||||
_ = x[actUpMatch-82]
|
||||
_ = x[actPageUp-83]
|
||||
_ = x[actPageDown-84]
|
||||
_ = x[actPosition-85]
|
||||
_ = x[actHalfPageUp-86]
|
||||
_ = x[actHalfPageDown-87]
|
||||
_ = x[actOffsetUp-88]
|
||||
_ = x[actOffsetDown-89]
|
||||
_ = x[actOffsetMiddle-90]
|
||||
_ = x[actJump-91]
|
||||
_ = x[actJumpAccept-92]
|
||||
_ = x[actPrintQuery-93]
|
||||
_ = x[actRefreshPreview-94]
|
||||
_ = x[actReplaceQuery-95]
|
||||
_ = x[actToggleSort-96]
|
||||
_ = x[actShowPreview-97]
|
||||
_ = x[actHidePreview-98]
|
||||
_ = x[actTogglePreview-99]
|
||||
_ = x[actTogglePreviewWrap-100]
|
||||
_ = x[actTogglePreviewWrapWord-101]
|
||||
_ = x[actTransform-102]
|
||||
_ = x[actTransformBorderLabel-103]
|
||||
_ = x[actTransformGhost-104]
|
||||
_ = x[actTransformHeader-105]
|
||||
_ = x[actTransformHeaderLines-106]
|
||||
_ = x[actTransformFooter-107]
|
||||
_ = x[actTransformHeaderLabel-108]
|
||||
_ = x[actTransformFooterLabel-109]
|
||||
_ = x[actTransformInputLabel-110]
|
||||
_ = x[actTransformListLabel-111]
|
||||
_ = x[actTransformNth-112]
|
||||
_ = x[actTransformPointer-113]
|
||||
_ = x[actTransformPreviewLabel-114]
|
||||
_ = x[actTransformPrompt-115]
|
||||
_ = x[actTransformQuery-116]
|
||||
_ = x[actTransformSearch-117]
|
||||
_ = x[actTrigger-118]
|
||||
_ = x[actBgTransform-119]
|
||||
_ = x[actBgTransformBorderLabel-120]
|
||||
_ = x[actBgTransformGhost-121]
|
||||
_ = x[actBgTransformHeader-122]
|
||||
_ = x[actBgTransformHeaderLines-123]
|
||||
_ = x[actBgTransformFooter-124]
|
||||
_ = x[actBgTransformHeaderLabel-125]
|
||||
_ = x[actBgTransformFooterLabel-126]
|
||||
_ = x[actBgTransformInputLabel-127]
|
||||
_ = x[actBgTransformListLabel-128]
|
||||
_ = x[actBgTransformNth-129]
|
||||
_ = x[actBgTransformPointer-130]
|
||||
_ = x[actBgTransformPreviewLabel-131]
|
||||
_ = x[actBgTransformPrompt-132]
|
||||
_ = x[actBgTransformQuery-133]
|
||||
_ = x[actBgTransformSearch-134]
|
||||
_ = x[actBgCancel-135]
|
||||
_ = x[actSearch-136]
|
||||
_ = x[actPreview-137]
|
||||
_ = x[actPreviewTop-138]
|
||||
_ = x[actPreviewBottom-139]
|
||||
_ = x[actPreviewUp-140]
|
||||
_ = x[actPreviewDown-141]
|
||||
_ = x[actPreviewPageUp-142]
|
||||
_ = x[actPreviewPageDown-143]
|
||||
_ = x[actPreviewHalfPageUp-144]
|
||||
_ = x[actPreviewHalfPageDown-145]
|
||||
_ = x[actPrevHistory-146]
|
||||
_ = x[actPrevSelected-147]
|
||||
_ = x[actPrint-148]
|
||||
_ = x[actPut-149]
|
||||
_ = x[actNextHistory-150]
|
||||
_ = x[actNextSelected-151]
|
||||
_ = x[actExecute-152]
|
||||
_ = x[actExecuteSilent-153]
|
||||
_ = x[actExecuteMulti-154]
|
||||
_ = x[actSigStop-155]
|
||||
_ = x[actBest-156]
|
||||
_ = x[actFirst-157]
|
||||
_ = x[actLast-158]
|
||||
_ = x[actReload-159]
|
||||
_ = x[actReloadSync-160]
|
||||
_ = x[actDisableSearch-161]
|
||||
_ = x[actEnableSearch-162]
|
||||
_ = x[actSelect-163]
|
||||
_ = x[actDeselect-164]
|
||||
_ = x[actUnbind-165]
|
||||
_ = x[actRebind-166]
|
||||
_ = x[actToggleBind-167]
|
||||
_ = x[actBecome-168]
|
||||
_ = x[actShowHeader-169]
|
||||
_ = x[actHideHeader-170]
|
||||
_ = x[actBell-171]
|
||||
_ = x[actExclude-172]
|
||||
_ = x[actExcludeMulti-173]
|
||||
_ = x[actAsync-174]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 690, 705, 722, 729, 734, 743, 754, 765, 778, 793, 804, 817, 832, 839, 852, 865, 882, 897, 910, 924, 938, 954, 974, 986, 1009, 1027, 1051, 1069, 1086, 1096, 1112, 1134, 1147, 1163, 1175, 1189, 1205, 1223, 1243, 1265, 1279, 1294, 1302, 1308, 1322, 1337, 1347, 1363, 1378, 1388, 1396, 1403, 1412, 1425, 1441, 1456, 1465, 1476, 1485, 1494, 1503, 1516, 1529}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 507, 529, 544, 558, 572, 585, 602, 610, 623, 639, 651, 659, 673, 687, 704, 715, 726, 740, 758, 775, 782, 801, 823, 835, 849, 858, 873, 885, 898, 909, 920, 932, 946, 967, 982, 995, 1012, 1030, 1046, 1058, 1070, 1083, 1098, 1112, 1124, 1136, 1153, 1160, 1172, 1177, 1187, 1196, 1207, 1218, 1231, 1246, 1257, 1270, 1285, 1292, 1305, 1318, 1335, 1350, 1363, 1377, 1391, 1407, 1427, 1451, 1463, 1486, 1503, 1521, 1544, 1562, 1585, 1608, 1630, 1651, 1666, 1685, 1709, 1727, 1744, 1762, 1772, 1786, 1811, 1830, 1850, 1875, 1895, 1920, 1945, 1969, 1992, 2009, 2030, 2056, 2076, 2095, 2115, 2126, 2135, 2145, 2158, 2174, 2186, 2200, 2216, 2234, 2254, 2276, 2290, 2305, 2313, 2319, 2333, 2348, 2358, 2374, 2389, 2399, 2406, 2414, 2421, 2430, 2443, 2459, 2474, 2483, 2494, 2503, 2512, 2525, 2534, 2547, 2560, 2567, 2577, 2592, 2600}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
||||
@@ -303,7 +303,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
|
||||
}
|
||||
|
||||
func normalizeRune(r rune) rune {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int
|
||||
|
||||
firstIdx, idx, lastIdx := 0, 0, 0
|
||||
var b byte
|
||||
for pidx := 0; pidx < len(pattern); pidx++ {
|
||||
for pidx := range pattern {
|
||||
b = byte(pattern[pidx])
|
||||
idx = trySkip(input, caseSensitive, b, idx)
|
||||
if idx < 0 {
|
||||
@@ -401,7 +401,7 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in
|
||||
if i == 0 {
|
||||
fmt.Print(" ")
|
||||
for j := int(f); j <= lastIdx; j++ {
|
||||
fmt.Printf(" " + string(T[j]) + " ")
|
||||
fmt.Print(" " + string(T[j]) + " ")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -445,7 +445,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
|
||||
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
||||
// we fall back to the greedy algorithm.
|
||||
if slab != nil && N*M > cap(slab.I16) {
|
||||
// Also, we should not allow a very long pattern to avoid 16-bit integer
|
||||
// overflow in the score matrix. 1000 is a safe limit.
|
||||
if slab != nil && N*M > cap(slab.I16) || M > 1000 {
|
||||
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
@@ -501,7 +503,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
if pidx < M {
|
||||
F[pidx] = int32(off)
|
||||
pidx++
|
||||
pchar = pattern[util.Min(pidx, M-1)]
|
||||
pchar = pattern[min(pidx, M-1)]
|
||||
}
|
||||
lastIdx = off
|
||||
}
|
||||
@@ -519,9 +521,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
inGap = false
|
||||
} else {
|
||||
if inGap {
|
||||
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||
H0[off] = max(prevH0+scoreGapExtension, 0)
|
||||
} else {
|
||||
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
H0[off] = max(prevH0+scoreGapStart, 0)
|
||||
}
|
||||
C0[off] = 0
|
||||
inGap = true
|
||||
@@ -587,7 +589,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
if b >= bonusBoundary && b > fb {
|
||||
consecutive = 1
|
||||
} else {
|
||||
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
|
||||
b = max(b, bonusConsecutive, fb)
|
||||
}
|
||||
}
|
||||
if s1+b < s2 {
|
||||
@@ -600,7 +602,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
Csub[off] = consecutive
|
||||
|
||||
inGap = s1 < s2
|
||||
score := util.Max16(util.Max16(s1, s2), 0)
|
||||
score := max(s1, s2, 0)
|
||||
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, col
|
||||
}
|
||||
@@ -684,7 +686,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
if bonus >= bonusBoundary && bonus > firstBonus {
|
||||
firstBonus = bonus
|
||||
}
|
||||
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
||||
bonus = max(bonus, firstBonus, bonusConsecutive)
|
||||
}
|
||||
if pidx == 0 {
|
||||
score += int(bonus * bonusFirstCharMultiplier)
|
||||
@@ -726,7 +728,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
lenRunes := text.Length()
|
||||
lenPattern := len(pattern)
|
||||
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
for index := range lenRunes {
|
||||
char := text.Get(indexAt(index, lenRunes, forward))
|
||||
// This is considerably faster than blindly applying strings.ToLower to the
|
||||
// whole string
|
||||
@@ -767,6 +769,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
@@ -824,7 +829,7 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
|
||||
|
||||
// For simplicity, only look at the bonus at the first character position
|
||||
pidx := 0
|
||||
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
|
||||
bestPos, bonus, bbonus, bestBonus := -1, int16(0), int16(0), int16(-1)
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
index_ := indexAt(index, lenRunes, forward)
|
||||
char := text.Get(index_)
|
||||
@@ -846,7 +851,16 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
|
||||
bonus = bonusAt(text, index_)
|
||||
}
|
||||
if boundaryCheck {
|
||||
ok = bonus >= bonusBoundary
|
||||
if forward && pidx_ == 0 {
|
||||
bbonus = bonus
|
||||
} else if !forward && pidx_ == lenPattern-1 {
|
||||
if index_ < lenRunes-1 {
|
||||
bbonus = bonusAt(text, index_+1)
|
||||
} else {
|
||||
bbonus = bonusBoundaryWhite
|
||||
}
|
||||
}
|
||||
ok = bbonus >= bonusBoundary
|
||||
if ok && pidx_ == 0 {
|
||||
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
scoreGapStart*2+scoreGapExtension*2)
|
||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
|
||||
util.Max(bonusCamel123, int(bonusBoundaryWhite)))
|
||||
max(bonusCamel123, int(bonusBoundaryWhite)))
|
||||
|
||||
// Consecutive bonus updated
|
||||
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
||||
@@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
|
||||
bytes[math.MaxUint16] = 'z'
|
||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
||||
}
|
||||
|
||||
func TestLongStringWithNormalize(t *testing.T) {
|
||||
bytes := make([]byte, 30000)
|
||||
for i := range bytes {
|
||||
bytes[i] = 'x'
|
||||
}
|
||||
unicodeString := string(bytes) + " Minímal example"
|
||||
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
|
||||
}
|
||||
|
||||
@@ -473,6 +473,103 @@ var normalized = map[rune]rune{
|
||||
'ử': 'u',
|
||||
'ữ': 'u',
|
||||
'ự': 'u',
|
||||
|
||||
// https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
|
||||
0xFF01: '!', // Fullwidth exclamation
|
||||
0xFF02: '"', // Fullwidth quotation mark
|
||||
0xFF03: '#', // Fullwidth number sign
|
||||
0xFF04: '$', // Fullwidth dollar sign
|
||||
0xFF05: '%', // Fullwidth percent
|
||||
0xFF06: '&', // Fullwidth ampersand
|
||||
0xFF07: '\'', // Fullwidth apostrophe
|
||||
0xFF08: '(', // Fullwidth left parenthesis
|
||||
0xFF09: ')', // Fullwidth right parenthesis
|
||||
0xFF0A: '*', // Fullwidth asterisk
|
||||
0xFF0B: '+', // Fullwidth plus
|
||||
0xFF0C: ',', // Fullwidth comma
|
||||
0xFF0D: '-', // Fullwidth hyphen-minus
|
||||
0xFF0E: '.', // Fullwidth period
|
||||
0xFF0F: '/', // Fullwidth slash
|
||||
0xFF10: '0',
|
||||
0xFF11: '1',
|
||||
0xFF12: '2',
|
||||
0xFF13: '3',
|
||||
0xFF14: '4',
|
||||
0xFF15: '5',
|
||||
0xFF16: '6',
|
||||
0xFF17: '7',
|
||||
0xFF18: '8',
|
||||
0xFF19: '9',
|
||||
0xFF1A: ':', // Fullwidth colon
|
||||
0xFF1B: ';', // Fullwidth semicolon
|
||||
0xFF1C: '<', // Fullwidth less-than
|
||||
0xFF1D: '=', // Fullwidth equal
|
||||
0xFF1E: '>', // Fullwidth greater-than
|
||||
0xFF1F: '?', // Fullwidth question mark
|
||||
0xFF20: '@', // Fullwidth at sign
|
||||
0xFF21: 'A',
|
||||
0xFF22: 'B',
|
||||
0xFF23: 'C',
|
||||
0xFF24: 'D',
|
||||
0xFF25: 'E',
|
||||
0xFF26: 'F',
|
||||
0xFF27: 'G',
|
||||
0xFF28: 'H',
|
||||
0xFF29: 'I',
|
||||
0xFF2A: 'J',
|
||||
0xFF2B: 'K',
|
||||
0xFF2C: 'L',
|
||||
0xFF2D: 'M',
|
||||
0xFF2E: 'N',
|
||||
0xFF2F: 'O',
|
||||
0xFF30: 'P',
|
||||
0xFF31: 'Q',
|
||||
0xFF32: 'R',
|
||||
0xFF33: 'S',
|
||||
0xFF34: 'T',
|
||||
0xFF35: 'U',
|
||||
0xFF36: 'V',
|
||||
0xFF37: 'W',
|
||||
0xFF38: 'X',
|
||||
0xFF39: 'Y',
|
||||
0xFF3A: 'Z',
|
||||
0xFF3B: '[', // Fullwidth left bracket
|
||||
0xFF3C: '\\', // Fullwidth backslash
|
||||
0xFF3D: ']', // Fullwidth right bracket
|
||||
0xFF3E: '^', // Fullwidth circumflex
|
||||
0xFF3F: '_', // Fullwidth underscore
|
||||
0xFF40: '`', // Fullwidth grave accent
|
||||
0xFF41: 'a',
|
||||
0xFF42: 'b',
|
||||
0xFF43: 'c',
|
||||
0xFF44: 'd',
|
||||
0xFF45: 'e',
|
||||
0xFF46: 'f',
|
||||
0xFF47: 'g',
|
||||
0xFF48: 'h',
|
||||
0xFF49: 'i',
|
||||
0xFF4A: 'j',
|
||||
0xFF4B: 'k',
|
||||
0xFF4C: 'l',
|
||||
0xFF4D: 'm',
|
||||
0xFF4E: 'n',
|
||||
0xFF4F: 'o',
|
||||
0xFF50: 'p',
|
||||
0xFF51: 'q',
|
||||
0xFF52: 'r',
|
||||
0xFF53: 's',
|
||||
0xFF54: 't',
|
||||
0xFF55: 'u',
|
||||
0xFF56: 'v',
|
||||
0xFF57: 'w',
|
||||
0xFF58: 'x',
|
||||
0xFF59: 'y',
|
||||
0xFF5A: 'z',
|
||||
0xFF5B: '{', // Fullwidth left brace
|
||||
0xFF5C: '|', // Fullwidth vertical bar
|
||||
0xFF5D: '}', // Fullwidth right brace
|
||||
0xFF5E: '~', // Fullwidth tilde
|
||||
0xFF61: '.', // Halfwidth ideographic full stop
|
||||
}
|
||||
|
||||
// NormalizeRunes normalizes latin script letters
|
||||
@@ -480,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
|
||||
ret := make([]rune, len(runes))
|
||||
copy(ret, runes)
|
||||
for idx, r := range runes {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
continue
|
||||
}
|
||||
n := normalized[r]
|
||||
|
||||
192
src/ansi.go
192
src/ansi.go
@@ -22,20 +22,21 @@ type url struct {
|
||||
type ansiState struct {
|
||||
fg tui.Color
|
||||
bg tui.Color
|
||||
ul tui.Color
|
||||
attr tui.Attr
|
||||
lbg tui.Color
|
||||
url *url
|
||||
}
|
||||
|
||||
func (s *ansiState) colored() bool {
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||
return s.fg != -1 || s.bg != -1 || s.ul != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||
}
|
||||
|
||||
func (s *ansiState) equals(t *ansiState) bool {
|
||||
if t == nil {
|
||||
return !s.colored()
|
||||
}
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||
return s.fg == t.fg && s.bg == t.bg && s.ul == t.ul && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||
}
|
||||
|
||||
func (s *ansiState) ToString() string {
|
||||
@@ -44,7 +45,7 @@ func (s *ansiState) ToString() string {
|
||||
}
|
||||
|
||||
ret := ""
|
||||
if s.attr&tui.Bold > 0 {
|
||||
if s.attr&tui.Bold > 0 || s.attr&tui.BoldForce > 0 {
|
||||
ret += "1;"
|
||||
}
|
||||
if s.attr&tui.Dim > 0 {
|
||||
@@ -54,7 +55,18 @@ func (s *ansiState) ToString() string {
|
||||
ret += "3;"
|
||||
}
|
||||
if s.attr&tui.Underline > 0 {
|
||||
ret += "4;"
|
||||
switch s.attr.UnderlineStyle() {
|
||||
case tui.UlStyleDouble:
|
||||
ret += "4:2;"
|
||||
case tui.UlStyleCurly:
|
||||
ret += "4:3;"
|
||||
case tui.UlStyleDotted:
|
||||
ret += "4:4;"
|
||||
case tui.UlStyleDashed:
|
||||
ret += "4:5;"
|
||||
default:
|
||||
ret += "4;"
|
||||
}
|
||||
}
|
||||
if s.attr&tui.Blink > 0 {
|
||||
ret += "5;"
|
||||
@@ -66,6 +78,9 @@ func (s *ansiState) ToString() string {
|
||||
ret += "9;"
|
||||
}
|
||||
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
||||
if s.ul != -1 {
|
||||
ret += toAnsiStringUl(s.ul)
|
||||
}
|
||||
|
||||
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
if s.url != nil {
|
||||
@@ -74,6 +89,20 @@ func (s *ansiState) ToString() string {
|
||||
return ret
|
||||
}
|
||||
|
||||
func toAnsiStringUl(color tui.Color) string {
|
||||
col := int(color)
|
||||
if col < 0 {
|
||||
return ""
|
||||
}
|
||||
if col >= (1 << 24) {
|
||||
r := strconv.Itoa((col >> 16) & 0xff)
|
||||
g := strconv.Itoa((col >> 8) & 0xff)
|
||||
b := strconv.Itoa(col & 0xff)
|
||||
return "58;2;" + r + ";" + g + ";" + b + ";"
|
||||
}
|
||||
return "58;5;" + strconv.Itoa(col) + ";"
|
||||
}
|
||||
|
||||
func toAnsiString(color tui.Color, offset int) string {
|
||||
col := int(color)
|
||||
ret := ""
|
||||
@@ -98,11 +127,11 @@ func isPrint(c uint8) bool {
|
||||
return '\x20' <= c && c <= '\x7e'
|
||||
}
|
||||
|
||||
func matchOperatingSystemCommand(s string) int {
|
||||
func matchOperatingSystemCommand(s string, start int) int {
|
||||
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
// ^ match starting here
|
||||
// ^ match starting here after the first printable character
|
||||
//
|
||||
i := 5 // prefix matched in nextAnsiEscapeSequence()
|
||||
i := start // prefix matched in nextAnsiEscapeSequence()
|
||||
for ; i < len(s) && isPrint(s[i]); i++ {
|
||||
}
|
||||
if i < len(s) {
|
||||
@@ -156,13 +185,13 @@ func isCtrlSeqStart(c uint8) bool {
|
||||
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||
// calling FindStringIndex() on the below regex (which was originally used):
|
||||
//
|
||||
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
|
||||
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)"
|
||||
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
// fast check for ANSI escape sequences
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x0e', '\x0f', '\x1b', '\x08':
|
||||
case '\x0e', '\x0f', '\x1b', '\x08', '\n':
|
||||
// We ignore the fact that '\x08' cannot be the first char
|
||||
// in the string and be an escape sequence for the sake of
|
||||
// speed and simplicity.
|
||||
@@ -174,6 +203,9 @@ func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
Loop:
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\n':
|
||||
// match: `\n`
|
||||
return i, i + 1
|
||||
case '\x08':
|
||||
// backtrack to match: `.\x08`
|
||||
if i > 0 && s[i-1] != '\n' {
|
||||
@@ -191,12 +223,20 @@ Loop:
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
|
||||
(s[i+3] == ';' || s[i+3] == ':') && isPrint(s[i+4]) {
|
||||
// match: `\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
if i+5 < len(s) && s[i+1] == ']' {
|
||||
j := 2
|
||||
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||
// ------
|
||||
for ; i+j < len(s) && isNumeric(s[i+j]); j++ {
|
||||
}
|
||||
|
||||
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
|
||||
return i, i + j
|
||||
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||
// ---------------
|
||||
if j > 2 && i+j+1 < len(s) && (s[i+j] == ';' || s[i+j] == ':') && isPrint(s[i+j+1]) {
|
||||
if k := matchOperatingSystemCommand(s[i:], j+2); k != -1 {
|
||||
return i, i + k
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,13 +297,30 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
output.WriteString(prev)
|
||||
}
|
||||
|
||||
newState := interpretCode(str[start:idx], state)
|
||||
if !newState.equals(state) {
|
||||
code := str[start:idx]
|
||||
newState := interpretCode(code, state)
|
||||
if code == "\n" || !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
|
||||
if code == "\n" {
|
||||
output.WriteRune('\n')
|
||||
runeCount++
|
||||
// Full-background marker
|
||||
if newState.lbg >= 0 {
|
||||
marker := newState
|
||||
marker.attr |= tui.FullBg
|
||||
offsets = append(offsets, ansiOffset{
|
||||
[2]int32{int32(runeCount), int32(runeCount)},
|
||||
marker,
|
||||
})
|
||||
// Reset the full-line background color
|
||||
newState.lbg = -1
|
||||
}
|
||||
}
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
if pstate == nil {
|
||||
@@ -310,20 +367,19 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
return trimmed, nil, state
|
||||
}
|
||||
|
||||
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
|
||||
func parseAnsiCode(s string) (int, byte, string) {
|
||||
var remaining string
|
||||
var i int
|
||||
if delimiter == 0 {
|
||||
// Faster than strings.IndexAny(";:")
|
||||
i = strings.IndexByte(s, ';')
|
||||
if i < 0 {
|
||||
i = strings.IndexByte(s, ':')
|
||||
var sep byte
|
||||
// Find the first separator (either ; or :)
|
||||
i := -1
|
||||
for j := 0; j < len(s); j++ {
|
||||
if s[j] == ';' || s[j] == ':' {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
} else {
|
||||
i = strings.IndexByte(s, delimiter)
|
||||
}
|
||||
if i >= 0 {
|
||||
delimiter = s[i]
|
||||
sep = s[i]
|
||||
remaining = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
@@ -335,42 +391,59 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
|
||||
for _, ch := range stringBytes(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return -1, delimiter, remaining
|
||||
return -1, sep, remaining
|
||||
}
|
||||
code = code*10 + int(ch)
|
||||
}
|
||||
return code, delimiter, remaining
|
||||
return code, sep, remaining
|
||||
}
|
||||
|
||||
return -1, delimiter, remaining
|
||||
return -1, sep, remaining
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
if ansiCode == "\n" {
|
||||
if prevState != nil {
|
||||
return *prevState
|
||||
}
|
||||
return ansiState{-1, -1, -1, 0, -1, nil}
|
||||
}
|
||||
|
||||
var state ansiState
|
||||
if prevState == nil {
|
||||
state = ansiState{-1, -1, 0, -1, nil}
|
||||
state = ansiState{-1, -1, -1, 0, -1, nil}
|
||||
} else {
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.ul, prevState.attr, prevState.lbg, prevState.url}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
|
||||
state.lbg = prevState.bg
|
||||
} else if ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
|
||||
state.url = nil
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
|
||||
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
|
||||
stLen := 2
|
||||
if strings.HasSuffix(ansiCode, "\a") {
|
||||
stLen = 1
|
||||
}
|
||||
// "\x1b]8;;\x1b\\" or "\x1b]8;;\a"
|
||||
if len(ansiCode) == 5+stLen && ansiCode[4] == ';' {
|
||||
state.url = nil
|
||||
} else if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
params := ansiCode[4 : 4+paramsEnd]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
|
||||
state.url = &url{uri: uri, params: params}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
if len(ansiCode) <= 3 {
|
||||
reset := func() {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.ul = -1
|
||||
state.attr = 0
|
||||
}
|
||||
|
||||
if len(ansiCode) <= 3 {
|
||||
reset()
|
||||
return state
|
||||
}
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
@@ -378,11 +451,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
state256 := 0
|
||||
ptr := &state.fg
|
||||
|
||||
var delimiter byte
|
||||
count := 0
|
||||
for len(ansiCode) != 0 {
|
||||
var num int
|
||||
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
|
||||
var sep byte
|
||||
if num, sep, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||
count++
|
||||
switch state256 {
|
||||
case 0:
|
||||
@@ -393,10 +466,15 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
case 48:
|
||||
ptr = &state.bg
|
||||
state256++
|
||||
case 58:
|
||||
ptr = &state.ul
|
||||
state256++
|
||||
case 39:
|
||||
state.fg = -1
|
||||
case 49:
|
||||
state.bg = -1
|
||||
case 59:
|
||||
state.ul = -1
|
||||
case 1:
|
||||
state.attr = state.attr | tui.Bold
|
||||
case 2:
|
||||
@@ -404,7 +482,30 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
case 3:
|
||||
state.attr = state.attr | tui.Italic
|
||||
case 4:
|
||||
state.attr = state.attr | tui.Underline
|
||||
if sep == ':' {
|
||||
// SGR 4:N — underline style sub-parameter
|
||||
var subNum int
|
||||
subNum, _, ansiCode = parseAnsiCode(ansiCode)
|
||||
state.attr = state.attr &^ tui.UnderlineStyleMask
|
||||
switch subNum {
|
||||
case 0:
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
case 1:
|
||||
state.attr = state.attr | tui.Underline
|
||||
case 2:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleDouble
|
||||
case 3:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleCurly
|
||||
case 4:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleDotted
|
||||
case 5:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleDashed
|
||||
default:
|
||||
state.attr = state.attr | tui.Underline
|
||||
}
|
||||
} else {
|
||||
state.attr = state.attr | tui.Underline
|
||||
}
|
||||
case 5:
|
||||
state.attr = state.attr | tui.Blink
|
||||
case 7:
|
||||
@@ -418,6 +519,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
state.attr = state.attr &^ tui.Italic
|
||||
case 24: // tput rmul
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
state.attr = state.attr &^ tui.UnderlineStyleMask
|
||||
case 25:
|
||||
state.attr = state.attr &^ tui.Blink
|
||||
case 27:
|
||||
@@ -425,9 +527,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
case 29:
|
||||
state.attr = state.attr &^ tui.StrikeThrough
|
||||
case 0:
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
reset()
|
||||
state256 = 0
|
||||
default:
|
||||
if num >= 30 && num <= 37 {
|
||||
@@ -467,9 +567,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
|
||||
// Empty sequence: reset
|
||||
if count == 0 {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
reset()
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
|
||||
172
src/ansi_test.go
172
src/ansi_test.go
@@ -22,7 +22,7 @@ import (
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)")
|
||||
|
||||
func testParserReference(t testing.TB, str string) {
|
||||
t.Helper()
|
||||
@@ -41,7 +41,7 @@ func testParserReference(t testing.TB, str string) {
|
||||
|
||||
equal := len(got) == len(exp)
|
||||
if equal {
|
||||
for i := 0; i < len(got); i++ {
|
||||
for i := range got {
|
||||
if got[i] != exp[i] {
|
||||
equal = false
|
||||
break
|
||||
@@ -167,9 +167,9 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
||||
randomString := func(rr *rand.Rand) string {
|
||||
numChars := rand.Intn(50)
|
||||
codePoints := make([]rune, numChars)
|
||||
for i := 0; i < len(codePoints); i++ {
|
||||
for i := range codePoints {
|
||||
var r rune
|
||||
for n := 0; n < 1000; n++ {
|
||||
for range 1000 {
|
||||
r = rune(rr.Intn(utf8.MaxRune))
|
||||
// Allow 10% of runes to be invalid
|
||||
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
||||
@@ -182,7 +182,7 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
||||
}
|
||||
|
||||
rr := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 100_000; i++ {
|
||||
for range 100_000 {
|
||||
testParserReference(t, randomString(rr))
|
||||
}
|
||||
}
|
||||
@@ -335,6 +335,28 @@ func TestExtractColor(t *testing.T) {
|
||||
assert((*offsets)[0], 0, 6, 2, -1, true)
|
||||
assert((*offsets)[1], 6, 11, 200, 100, false)
|
||||
})
|
||||
|
||||
state = nil
|
||||
var color24 tui.Color = (1 << 24) + (180 << 16) + (190 << 8) + 254
|
||||
src = "\x1b[1mhello \x1b[22;1;38:2:180:190:254mworld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != color24 || state.attr != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 6, -1, -1, true)
|
||||
assert((*offsets)[1], 6, 11, color24, -1, true)
|
||||
})
|
||||
|
||||
src = "\x1b]133;A\x1b\\hello \x1b]133;C\x1b\\world"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 11, color24, -1, true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
@@ -347,10 +369,10 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
|
||||
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink, ul: -1, lbg: -1}, "")
|
||||
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||
|
||||
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||
@@ -358,36 +380,142 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
assert("\x1b[92m", nil, "\x1b[92;49m")
|
||||
assert("\x1b[102m", nil, "\x1b[39;102m")
|
||||
|
||||
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m")
|
||||
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
|
||||
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "\x1b[31;44m")
|
||||
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, ul: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
|
||||
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
|
||||
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
|
||||
assert("\x1b[48;5;100;38;2;10;20;30;7m",
|
||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1, ul: -1},
|
||||
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||
|
||||
// Underline styles
|
||||
assert("\x1b[4:3m", nil, "\x1b[4:3;39;49m")
|
||||
assert("\x1b[4:2m", nil, "\x1b[4:2;39;49m")
|
||||
assert("\x1b[4:4m", nil, "\x1b[4:4;39;49m")
|
||||
assert("\x1b[4:5m", nil, "\x1b[4:5;39;49m")
|
||||
assert("\x1b[4:1m", nil, "\x1b[4;39;49m")
|
||||
|
||||
// Underline color (256-color)
|
||||
assert("\x1b[4;58;5;100m", nil, "\x1b[4;39;49;58;5;100m")
|
||||
// Underline color (24-bit)
|
||||
assert("\x1b[4;58;2;255;0;128m", nil, "\x1b[4;39;49;58;2;255;0;128m")
|
||||
// Curly underline + underline color
|
||||
assert("\x1b[4:3;58;2;255;0;0m", nil, "\x1b[4:3;39;49;58;2;255;0;0m")
|
||||
// SGR 59 resets underline color
|
||||
assert("\x1b[59m", &ansiState{fg: 1, bg: -1, ul: 100, lbg: -1}, "\x1b[31;49m")
|
||||
}
|
||||
|
||||
func TestParseAnsiCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
In, Exp string
|
||||
N int
|
||||
In string
|
||||
Exp string
|
||||
N int
|
||||
Sep byte
|
||||
}{
|
||||
{"123", "", 123},
|
||||
{"1a", "", -1},
|
||||
{"1a;12", "12", -1},
|
||||
{"12;a", "a", 12},
|
||||
{"-2", "", -1},
|
||||
{"123", "", 123, 0},
|
||||
{"1a", "", -1, 0},
|
||||
{"1a;12", "12", -1, ';'},
|
||||
{"12;a", "a", 12, ';'},
|
||||
{"-2", "", -1, 0},
|
||||
// Colon sub-parameters: earliest separator wins (@shtse8)
|
||||
{"4:3", "3", 4, ':'},
|
||||
{"4:3;31", "3;31", 4, ':'},
|
||||
{"38:2:255:0:0", "2:255:0:0", 38, ':'},
|
||||
{"58:5:200", "5:200", 58, ':'},
|
||||
// Semicolon before colon
|
||||
{"4;38:2:0:0:0", "38:2:0:0:0", 4, ';'},
|
||||
}
|
||||
for _, x := range tests {
|
||||
n, _, s := parseAnsiCode(x.In, 0)
|
||||
if n != x.N || s != x.Exp {
|
||||
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
||||
n, sep, s := parseAnsiCode(x.In)
|
||||
if n != x.N || s != x.Exp || sep != x.Sep {
|
||||
t.Fatalf("%q: got: (%d %q %q) want: (%d %q %q)", x.In, n, s, string(sep), x.N, x.Exp, string(x.Sep))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases adapted from @shtse8 (PR #4678)
|
||||
func TestInterpretCodeUnderlineStyles(t *testing.T) {
|
||||
// 4:0 = no underline
|
||||
state := interpretCode("\x1b[4:0m", nil)
|
||||
if state.attr&tui.Underline != 0 {
|
||||
t.Error("4:0 should not set underline")
|
||||
}
|
||||
|
||||
// 4:1 = single underline
|
||||
state = interpretCode("\x1b[4:1m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4:1 should set underline")
|
||||
}
|
||||
|
||||
// 4:3 = curly underline
|
||||
state = interpretCode("\x1b[4:3m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4:3 should set underline")
|
||||
}
|
||||
if state.attr.UnderlineStyle() != tui.UlStyleCurly {
|
||||
t.Error("4:3 should set curly underline style")
|
||||
}
|
||||
|
||||
// 4:3 should NOT set italic (3 is a sub-param, not SGR 3)
|
||||
if state.attr&tui.Italic != 0 {
|
||||
t.Error("4:3 should not set italic")
|
||||
}
|
||||
|
||||
// 4:2;31 = double underline + red fg
|
||||
state = interpretCode("\x1b[4:2;31m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4:2;31 should set underline")
|
||||
}
|
||||
if state.fg != 1 {
|
||||
t.Errorf("4:2;31 should set fg to red (1), got %d", state.fg)
|
||||
}
|
||||
if state.attr&tui.Dim != 0 {
|
||||
t.Error("4:2;31 should not set dim")
|
||||
}
|
||||
|
||||
// Plain 4 still works
|
||||
state = interpretCode("\x1b[4m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4 should set underline")
|
||||
}
|
||||
|
||||
// 4;2 (semicolon) = underline + dim
|
||||
state = interpretCode("\x1b[4;2m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4;2 should set underline")
|
||||
}
|
||||
if state.attr&tui.Dim == 0 {
|
||||
t.Error("4;2 should set dim")
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases adapted from @shtse8 (PR #4678)
|
||||
func TestInterpretCodeUnderlineColor(t *testing.T) {
|
||||
// 58:2:R:G:B should not affect fg or bg
|
||||
state := interpretCode("\x1b[58:2:255:0:0m", nil)
|
||||
if state.fg != -1 || state.bg != -1 {
|
||||
t.Errorf("58:2:R:G:B should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
|
||||
}
|
||||
|
||||
// 58:5:200 should not affect fg or bg
|
||||
state = interpretCode("\x1b[58:5:200m", nil)
|
||||
if state.fg != -1 || state.bg != -1 {
|
||||
t.Errorf("58:5:N should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
|
||||
}
|
||||
|
||||
// 58:2:R:G:B combined with 38:2:R:G:B should only set fg
|
||||
state = interpretCode("\x1b[58:2:255:0:0;38:2:0:255:0m", nil)
|
||||
expectedFg := tui.Color(1<<24 | 0<<16 | 255<<8 | 0)
|
||||
if state.fg != expectedFg {
|
||||
t.Errorf("expected fg=%d, got %d", expectedFg, state.fg)
|
||||
}
|
||||
if state.bg != -1 {
|
||||
t.Errorf("bg should be -1, got %d", state.bg)
|
||||
}
|
||||
}
|
||||
|
||||
// kernel/bpf/preload/iterators/README
|
||||
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" +
|
||||
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||
|
||||
@@ -41,10 +41,31 @@ func (c *Chunk) IsFull() bool {
|
||||
return c.count == chunkSize
|
||||
}
|
||||
|
||||
func (c *Chunk) lastIndex(minValue int32) int32 {
|
||||
if c.count == 0 {
|
||||
return minValue
|
||||
}
|
||||
return c.items[c.count-1].Index() + 1 // Exclusive
|
||||
}
|
||||
|
||||
func (cl *ChunkList) lastChunk() *Chunk {
|
||||
return cl.chunks[len(cl.chunks)-1]
|
||||
}
|
||||
|
||||
// GetItems returns the first n items from the given chunks
|
||||
func GetItems(chunks []*Chunk, n int) []Item {
|
||||
items := make([]Item, 0, n)
|
||||
for _, chunk := range chunks {
|
||||
for i := 0; i < chunk.count && len(items) < n; i++ {
|
||||
items = append(items, chunk.items[i])
|
||||
}
|
||||
if len(items) >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// CountItems returns the total number of Items
|
||||
func CountItems(cs []*Chunk) int {
|
||||
if len(cs) == 0 {
|
||||
|
||||
@@ -51,8 +51,8 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// Add more data
|
||||
for i := 0; i < chunkSize*2; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
for i := range chunkSize * 2 {
|
||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||
}
|
||||
|
||||
// Previous snapshot should remain the same
|
||||
@@ -85,8 +85,8 @@ func TestChunkListTail(t *testing.T) {
|
||||
return true
|
||||
})
|
||||
total := chunkSize*2 + chunkSize/2
|
||||
for i := 0; i < total; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
for i := range total {
|
||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||
}
|
||||
|
||||
snapshot, count, changed := cl.Snapshot(0)
|
||||
|
||||
@@ -26,16 +26,20 @@ const (
|
||||
previewCancelWait = 500 * time.Millisecond
|
||||
previewChunkDelay = 100 * time.Millisecond
|
||||
previewDelayed = 500 * time.Millisecond
|
||||
maxPatternLength = 300
|
||||
maxPatternLength = 1000
|
||||
maxMulti = math.MaxInt32
|
||||
|
||||
// Background processes
|
||||
maxBgProcesses = 30
|
||||
maxBgProcessesPerAction = 3
|
||||
|
||||
// Matcher
|
||||
numPartitionsMultiplier = 8
|
||||
maxPartitions = 32
|
||||
progressMinDuration = 200 * time.Millisecond
|
||||
|
||||
// Capacity of each chunk
|
||||
chunkSize int = 100
|
||||
chunkSize int = 1000
|
||||
|
||||
// Pre-allocated memory slices to minimize GC
|
||||
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
|
||||
@@ -61,7 +65,6 @@ const (
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
EvtHeader
|
||||
EvtReady
|
||||
EvtQuit
|
||||
)
|
||||
|
||||
250
src/core.go
250
src/core.go
@@ -2,10 +2,13 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
@@ -15,7 +18,6 @@ Reader -> EvtReadNew -> Matcher (restart)
|
||||
Terminal -> EvtSearchNew:bool -> Matcher (restart)
|
||||
Matcher -> EvtSearchProgress -> Terminal (update info)
|
||||
Matcher -> EvtSearchFin -> Terminal (update list)
|
||||
Matcher -> EvtHeader -> Terminal (update header)
|
||||
*/
|
||||
|
||||
type revision struct {
|
||||
@@ -36,10 +38,22 @@ func (r revision) compatible(other revision) bool {
|
||||
return r.major == other.major
|
||||
}
|
||||
|
||||
func buildItemTransformer(opts *Options) func(*Item) string {
|
||||
if opts.AcceptNth != nil {
|
||||
fn := opts.AcceptNth(opts.Delimiter)
|
||||
return func(item *Item) string {
|
||||
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
|
||||
}
|
||||
}
|
||||
return func(item *Item) string {
|
||||
return item.AsString(opts.Ansi)
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options) (int, error) {
|
||||
if opts.Filter == nil {
|
||||
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index {
|
||||
if opts.useTmux() {
|
||||
return runTmux(os.Args, opts)
|
||||
}
|
||||
|
||||
@@ -74,20 +88,24 @@ func Run(opts *Options) (int, error) {
|
||||
|
||||
var lineAnsiState, prevLineAnsiState *ansiState
|
||||
if opts.Ansi {
|
||||
if opts.Theme.Colored {
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
prevLineAnsiState = lineAnsiState
|
||||
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
return util.ToChars(stringBytes(trimmed)), offsets
|
||||
}
|
||||
} else {
|
||||
// When color is disabled but ansi option is given,
|
||||
// we simply strip out ANSI codes from the input
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, _, _ := extractColor(byteString(data), nil, nil)
|
||||
return util.ToChars(stringBytes(trimmed)), nil
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
prevLineAnsiState = lineAnsiState
|
||||
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
|
||||
// Full line background is found. Add a special marker.
|
||||
if offsets != nil && newState != nil && len(*offsets) > 0 && newState.lbg >= 0 {
|
||||
marker := (*offsets)[len(*offsets)-1]
|
||||
marker.offset[0] = marker.offset[1]
|
||||
marker.color.bg = newState.lbg
|
||||
marker.color.attr = marker.color.attr | tui.FullBg
|
||||
newOffsets := append(*offsets, marker)
|
||||
offsets = &newOffsets
|
||||
|
||||
// Reset the full-line background color
|
||||
lineAnsiState.lbg = -1
|
||||
}
|
||||
return util.ToChars(stringBytes(trimmed)), offsets
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,23 +113,18 @@ func Run(opts *Options) (int, error) {
|
||||
cache := NewChunkCache()
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
if opts.WithNth == nil {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, byteString(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
item.text, item.colors = ansiProcessor(data)
|
||||
item.text.Index = itemIndex
|
||||
itemIndex++
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
if opts.Ansi && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
if prevLineAnsiState != nil {
|
||||
ansiStateDup := *prevLineAnsiState
|
||||
@@ -127,15 +140,19 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
transformed := joinTokens(trans)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, transformed)
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
transformed := nthTransformer(tokens, itemIndex)
|
||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||
item.text.TrimTrailingWhitespaces()
|
||||
|
||||
// We should not trim trailing whitespaces with background colors
|
||||
var maxColorOffset int32
|
||||
if item.colors != nil {
|
||||
for _, ansi := range *item.colors {
|
||||
if ansi.color.bg >= 0 {
|
||||
maxColorOffset = max(maxColorOffset, ansi.offset[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
|
||||
item.text.Index = itemIndex
|
||||
item.origText = &data
|
||||
itemIndex++
|
||||
@@ -165,7 +182,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
// Reader
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
|
||||
var reader *Reader
|
||||
if !streamingFilter {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
@@ -188,17 +205,37 @@ func Run(opts *Options) (int, error) {
|
||||
forward = false
|
||||
case byBegin:
|
||||
forward = true
|
||||
case byPathname:
|
||||
withPos = true
|
||||
forward = false
|
||||
}
|
||||
}
|
||||
patternCache := make(map[string]*Pattern)
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
}
|
||||
|
||||
nth := opts.Nth
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
patternCache := make(map[string]*Pattern)
|
||||
denyMutex := sync.Mutex{}
|
||||
denylist := make(map[int32]struct{})
|
||||
clearDenylist := func() {
|
||||
denyMutex.Lock()
|
||||
if len(denylist) > 0 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
denylist = make(map[int32]struct{})
|
||||
denyMutex.Unlock()
|
||||
}
|
||||
headerLines := int32(opts.HeaderLines)
|
||||
headerUpdated := false
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
denyMutex.Lock()
|
||||
denylistCopy := maps.Clone(denylist)
|
||||
denyMutex.Unlock()
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines)
|
||||
}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision, opts.Threads)
|
||||
|
||||
// Filtering mode
|
||||
if opts.Filter != nil {
|
||||
@@ -209,6 +246,8 @@ func Run(opts *Options) (int, error) {
|
||||
pattern := patternBuilder([]rune(*opts.Filter))
|
||||
matcher.sort = pattern.sortable
|
||||
|
||||
transformer := buildItemTransformer(opts)
|
||||
|
||||
found := false
|
||||
if streamingFilter {
|
||||
slab := util.MakeSlab(slab16Size, slab32Size)
|
||||
@@ -217,9 +256,12 @@ func Run(opts *Options) (int, error) {
|
||||
func(runes []byte) bool {
|
||||
item := Item{}
|
||||
if chunkList.trans(&item, runes) {
|
||||
if item.Index() < headerLines {
|
||||
return false
|
||||
}
|
||||
mutex.Lock()
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
||||
opts.Printer(item.text.ToString())
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result.item != nil {
|
||||
opts.Printer(transformer(&item))
|
||||
found = true
|
||||
}
|
||||
mutex.Unlock()
|
||||
@@ -233,11 +275,51 @@ func Run(opts *Options) (int, error) {
|
||||
|
||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||
merger, _ := matcher.scan(MatchRequest{
|
||||
|
||||
if opts.Bench > 0 {
|
||||
// Benchmark mode: repeat scan for the given duration
|
||||
totalItems := CountItems(snapshot)
|
||||
var matchCount int
|
||||
var times []time.Duration
|
||||
deadline := time.Now().Add(opts.Bench)
|
||||
for time.Now().Before(deadline) {
|
||||
cache.Clear()
|
||||
start := time.Now()
|
||||
result := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
times = append(times, time.Since(start))
|
||||
matchCount = result.merger.Length()
|
||||
}
|
||||
// Print stats
|
||||
var total time.Duration
|
||||
minD, maxD := times[0], times[0]
|
||||
for _, d := range times {
|
||||
total += d
|
||||
if d < minD {
|
||||
minD = d
|
||||
}
|
||||
if d > maxD {
|
||||
maxD = d
|
||||
}
|
||||
}
|
||||
avg := total / time.Duration(len(times))
|
||||
selectivity := float64(matchCount) / float64(totalItems) * 100
|
||||
fmt.Printf(" %d iterations avg: %.2fms min: %.2fms max: %.2fms total: %.2fs items: %d matches: %d (%.2f%%)\n",
|
||||
len(times),
|
||||
float64(avg.Microseconds())/1000,
|
||||
float64(minD.Microseconds())/1000,
|
||||
float64(maxD.Microseconds())/1000,
|
||||
total.Seconds(),
|
||||
totalItems, matchCount, selectivity)
|
||||
return ExitOk, nil
|
||||
}
|
||||
|
||||
result := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
for i := 0; i < merger.Length(); i++ {
|
||||
opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
|
||||
for i := 0; i < result.merger.Length(); i++ {
|
||||
opts.Printer(transformer(result.merger.Get(i).item))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
@@ -274,6 +356,7 @@ func Run(opts *Options) (int, error) {
|
||||
// Event coordination
|
||||
reading := true
|
||||
ticks := 0
|
||||
startTick := 0
|
||||
var nextCommand *commandSpec
|
||||
var nextEnviron []string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
@@ -281,10 +364,11 @@ func Run(opts *Options) (int, error) {
|
||||
query := []rune{}
|
||||
determine := func(final bool) {
|
||||
if heightUnknown {
|
||||
if total >= maxFit || final {
|
||||
items := max(0, total-int(headerLines))
|
||||
if items >= maxFit || final {
|
||||
deferred = false
|
||||
heightUnknown = false
|
||||
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
|
||||
terminal.startChan <- fitpad{min(items, maxFit), padHeight}
|
||||
}
|
||||
} else if deferred {
|
||||
deferred = false
|
||||
@@ -296,11 +380,15 @@ func Run(opts *Options) (int, error) {
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command commandSpec, environ []string) {
|
||||
if !useSnapshot {
|
||||
clearDenylist()
|
||||
}
|
||||
reading = true
|
||||
headerUpdated = false
|
||||
startTick = ticks
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision.bumpMajor()
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
readyChan := make(chan bool)
|
||||
go reader.restart(command, environ, readyChan)
|
||||
<-readyChan
|
||||
@@ -342,7 +430,8 @@ func Run(opts *Options) (int, error) {
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||
clearDenylist()
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
@@ -357,7 +446,11 @@ func Run(opts *Options) (int, error) {
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(total, !reading, value.(*string))
|
||||
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
|
||||
if headerLines > 0 && !headerUpdated {
|
||||
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||
headerUpdated = int32(total) >= headerLines
|
||||
}
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
@@ -367,12 +460,38 @@ func Run(opts *Options) (int, error) {
|
||||
var command *commandSpec
|
||||
var environ []string
|
||||
var changed bool
|
||||
headerLinesChanged := false
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
bump := false
|
||||
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||
denyMutex.Lock()
|
||||
for _, itemIndex := range val.denylist {
|
||||
denylist[itemIndex] = struct{}{}
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
bump = true
|
||||
}
|
||||
if val.nth != nil {
|
||||
// Change nth and clear caches
|
||||
nth = *val.nth
|
||||
bump = true
|
||||
}
|
||||
if val.headerLines != nil {
|
||||
headerLines = int32(*val.headerLines)
|
||||
headerUpdated = false
|
||||
headerLinesChanged = true
|
||||
bump = true
|
||||
}
|
||||
if bump {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
}
|
||||
@@ -404,6 +523,14 @@ func Run(opts *Options) (int, error) {
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
}
|
||||
if headerLinesChanged {
|
||||
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, nil)
|
||||
if headerLines > 0 {
|
||||
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||
} else {
|
||||
terminal.UpdateHeader(nil)
|
||||
}
|
||||
}
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||
delay = false
|
||||
|
||||
@@ -413,19 +540,15 @@ func Run(opts *Options) (int, error) {
|
||||
terminal.UpdateProgress(val)
|
||||
}
|
||||
|
||||
case EvtHeader:
|
||||
headerPadded := make([]string, opts.HeaderLines)
|
||||
copy(headerPadded, value.([]string))
|
||||
terminal.UpdateHeader(headerPadded)
|
||||
|
||||
case EvtSearchFin:
|
||||
switch val := value.(type) {
|
||||
case *Merger:
|
||||
case MatchResult:
|
||||
merger := val.merger
|
||||
if deferred {
|
||||
count := val.Length()
|
||||
count := merger.Length()
|
||||
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
||||
determine(val.final)
|
||||
} else if val.final {
|
||||
determine(merger.final)
|
||||
} else if merger.final {
|
||||
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
||||
if opts.PrintQuery {
|
||||
opts.Printer(opts.Query)
|
||||
@@ -433,8 +556,9 @@ func Run(opts *Options) (int, error) {
|
||||
if len(opts.Expect) > 0 {
|
||||
opts.Printer("")
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
||||
transformer := buildItemTransformer(opts)
|
||||
for i := range count {
|
||||
opts.Printer(transformer(merger.Get(i).item))
|
||||
}
|
||||
if count == 0 {
|
||||
exitCode = ExitNoMatch
|
||||
@@ -442,7 +566,7 @@ func Run(opts *Options) (int, error) {
|
||||
stop = true
|
||||
return
|
||||
}
|
||||
determine(val.final)
|
||||
determine(merger.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val)
|
||||
@@ -455,8 +579,8 @@ func Run(opts *Options) (int, error) {
|
||||
break
|
||||
}
|
||||
if delay && reading {
|
||||
dur := util.DurWithin(
|
||||
time.Duration(ticks)*coordinatorDelayStep,
|
||||
dur := util.Constrain(
|
||||
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
||||
0, coordinatorDelayMax)
|
||||
time.Sleep(dur)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestHistory(t *testing.T) {
|
||||
if len(h.lines) != maxHistory+1 {
|
||||
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
|
||||
}
|
||||
for i := 0; i < maxHistory; i++ {
|
||||
for i := range maxHistory {
|
||||
if h.lines[i] != "foobar" {
|
||||
t.Error("Expected: foobar, actual: " + h.lines[i])
|
||||
}
|
||||
|
||||
15
src/item.go
15
src/item.go
@@ -6,10 +6,17 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
type transformed struct {
|
||||
// Because nth can be changed dynamically by change-nth action, we need to
|
||||
// keep the revision number at the time of transformation.
|
||||
revision revision
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
// Item represents each input line. 56 bytes.
|
||||
type Item struct {
|
||||
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||
transformed *[]Token // 8
|
||||
transformed *transformed // 8
|
||||
origText *[]byte // 8
|
||||
colors *[]ansiOffset // 8
|
||||
}
|
||||
@@ -44,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
|
||||
}
|
||||
return item.text.ToString()
|
||||
}
|
||||
|
||||
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
transformed := transformer(tokens, item.Index())
|
||||
return StripLastDelimiter(transformed, delimiter)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package fzf
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -19,6 +18,20 @@ type MatchRequest struct {
|
||||
revision revision
|
||||
}
|
||||
|
||||
type MatchResult struct {
|
||||
merger *Merger
|
||||
passMerger *Merger
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
func (mr MatchResult) cacheable() bool {
|
||||
return mr.merger != nil && mr.merger.cacheable()
|
||||
}
|
||||
|
||||
func (mr MatchResult) final() bool {
|
||||
return mr.merger != nil && mr.merger.final
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
type Matcher struct {
|
||||
cache *ChunkCache
|
||||
@@ -29,7 +42,8 @@ type Matcher struct {
|
||||
reqBox *util.EventBox
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
sortBuf [][]Result
|
||||
mergerCache map[string]MatchResult
|
||||
revision revision
|
||||
}
|
||||
|
||||
@@ -40,8 +54,11 @@ const (
|
||||
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
|
||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
|
||||
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
if threads > 0 {
|
||||
partitions = threads
|
||||
}
|
||||
return &Matcher{
|
||||
cache: cache,
|
||||
patternBuilder: patternBuilder,
|
||||
@@ -51,7 +68,8 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: partitions,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
mergerCache: make(map[string]*Merger),
|
||||
sortBuf: make([][]Result, partitions),
|
||||
mergerCache: make(map[string]MatchResult),
|
||||
revision: revision}
|
||||
}
|
||||
|
||||
@@ -85,43 +103,42 @@ func (m *Matcher) Loop() {
|
||||
cacheCleared := false
|
||||
if request.sort != m.sort || request.revision != m.revision {
|
||||
m.sort = request.sort
|
||||
m.revision = request.revision
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
m.mergerCache = make(map[string]MatchResult)
|
||||
if !request.revision.compatible(m.revision) {
|
||||
m.cache.Clear()
|
||||
}
|
||||
m.revision = request.revision
|
||||
cacheCleared = true
|
||||
}
|
||||
|
||||
// Restart search
|
||||
patternString := request.pattern.AsString()
|
||||
var merger *Merger
|
||||
cancelled := false
|
||||
var result MatchResult
|
||||
count := CountItems(request.chunks)
|
||||
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
||||
merger = cached
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final() == request.final {
|
||||
result = cached
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
m.mergerCache = make(map[string]MatchResult)
|
||||
}
|
||||
}
|
||||
|
||||
if merger == nil {
|
||||
merger, cancelled = m.scan(request)
|
||||
if result.merger == nil {
|
||||
result = m.scan(request)
|
||||
}
|
||||
|
||||
if !cancelled {
|
||||
if merger.cacheable() {
|
||||
m.mergerCache[patternString] = merger
|
||||
if !result.cancelled {
|
||||
if result.cacheable() {
|
||||
m.mergerCache[patternString] = result
|
||||
}
|
||||
merger.final = request.final
|
||||
m.eventBox.Set(EvtSearchFin, merger)
|
||||
result.merger.final = request.final
|
||||
m.eventBox.Set(EvtSearchFin, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,19 +169,22 @@ type partialResult struct {
|
||||
matches []Result
|
||||
}
|
||||
|
||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
func (m *Matcher) scan(request MatchRequest) MatchResult {
|
||||
startedAt := time.Now()
|
||||
|
||||
numChunks := len(request.chunks)
|
||||
if numChunks == 0 {
|
||||
return EmptyMerger(request.revision), false
|
||||
m := EmptyMerger(request.revision)
|
||||
return MatchResult{m, m, false}
|
||||
}
|
||||
pattern := request.pattern
|
||||
passMerger := PassMerger(&request.chunks, m.tac, request.revision, pattern.startIndex)
|
||||
if pattern.IsEmpty() {
|
||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||
return MatchResult{passMerger, passMerger, false}
|
||||
}
|
||||
|
||||
minIndex := request.chunks[0].items[0].Index()
|
||||
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
@@ -196,11 +216,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
if m.sort && request.pattern.sortable {
|
||||
if m.tac {
|
||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||
} else {
|
||||
sort.Sort(ByRelevance(sliceMatches))
|
||||
}
|
||||
m.sortBuf[idx] = radixSortResults(sliceMatches, m.tac, m.sortBuf[idx])
|
||||
}
|
||||
resultChan <- partialResult{idx, sliceMatches}
|
||||
}(idx, m.slab[idx], chunks)
|
||||
@@ -223,7 +239,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
|
||||
if m.reqBox.Peek(reqReset) {
|
||||
return nil, wait()
|
||||
return MatchResult{nil, nil, wait()}
|
||||
}
|
||||
|
||||
if time.Since(startedAt) > progressMinDuration {
|
||||
@@ -236,7 +252,8 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false
|
||||
merger := NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex)
|
||||
return MatchResult{merger, passMerger, false}
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
|
||||
@@ -4,50 +4,57 @@ import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
func EmptyMerger(revision revision) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0)
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0, 0)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
revision revision
|
||||
minIndex int32
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
startIndex int
|
||||
revision revision
|
||||
minIndex int32
|
||||
maxIndex int32
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
var minIndex int32
|
||||
// original order. startIndex items are skipped from the beginning.
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger {
|
||||
var minIndex, maxIndex int32
|
||||
if len(*chunks) > 0 {
|
||||
minIndex = (*chunks)[0].items[0].Index()
|
||||
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
||||
}
|
||||
si := int(startIndex)
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
startIndex: si,
|
||||
revision: revision,
|
||||
minIndex: minIndex + startIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
}
|
||||
mg.count = max(0, mg.count-si)
|
||||
return &mg
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32, maxIndex int32) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
@@ -59,7 +66,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -109,6 +117,7 @@ func (mg *Merger) Get(idx int) Result {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
idx += mg.startIndex
|
||||
firstChunk := (*mg.chunks)[0]
|
||||
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||
idx -= firstChunk.count
|
||||
@@ -137,6 +146,15 @@ func (mg *Merger) Get(idx int) Result {
|
||||
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
|
||||
}
|
||||
|
||||
func (mg *Merger) ToMap() map[int32]Result {
|
||||
ret := make(map[int32]Result, mg.count)
|
||||
for i := 0; i < mg.count; i++ {
|
||||
result := mg.Get(i)
|
||||
ret[result.Index()] = result
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (mg *Merger) cacheable() bool {
|
||||
return mg.count < mergerCacheMax
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@ func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
numLists := 4
|
||||
lists := make([][]Result, numLists)
|
||||
cnt := 0
|
||||
for i := 0; i < numLists; i++ {
|
||||
for i := range numLists {
|
||||
numResults := rand.Int() % 20
|
||||
cnt += numResults
|
||||
lists[i] = make([]Result, numResults)
|
||||
for j := 0; j < numResults; j++ {
|
||||
for j := range numResults {
|
||||
item := randResult()
|
||||
lists[i][j] = item
|
||||
}
|
||||
@@ -58,9 +58,9 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0)
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
for i := range cnt {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
}
|
||||
}
|
||||
@@ -70,17 +70,17 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
for i := range cnt {
|
||||
if items[i] != mg.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg.Get(i))
|
||||
}
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
||||
2172
src/options.go
2172
src/options.go
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,13 @@ import (
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
// Valid regex
|
||||
// Valid regex, but a single character -> string
|
||||
delim := delimiterRegexp(".")
|
||||
if delim.regex == nil || delim.str != nil {
|
||||
if delim.regex != nil || *delim.str != "." {
|
||||
t.Error(delim)
|
||||
}
|
||||
delim = delimiterRegexp("|")
|
||||
if delim.regex != nil || *delim.str != "|" {
|
||||
t.Error(delim)
|
||||
}
|
||||
// Broken regex -> string
|
||||
@@ -138,7 +142,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseKeys(t *testing.T) {
|
||||
pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
pairs, _, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
checkEvent := func(e tui.Event, s string) {
|
||||
if pairs[e] != s {
|
||||
t.Errorf("%s != %s", pairs[e], s)
|
||||
@@ -164,11 +168,11 @@ func TestParseKeys(t *testing.T) {
|
||||
checkEvent(tui.AltKey(' '), "alt-SPACE")
|
||||
|
||||
// Synonyms
|
||||
pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
if len(pairs) != 9 {
|
||||
t.Error(9)
|
||||
}
|
||||
check(tui.CtrlM, "Return")
|
||||
check(tui.Enter, "Return")
|
||||
checkEvent(tui.Key(' '), "space")
|
||||
check(tui.Tab, "tab")
|
||||
check(tui.ShiftTab, "btab")
|
||||
@@ -178,7 +182,7 @@ func TestParseKeys(t *testing.T) {
|
||||
check(tui.Left, "left")
|
||||
check(tui.Right, "right")
|
||||
|
||||
pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
pairs, _, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
if len(pairs) != 11 {
|
||||
t.Error(11)
|
||||
}
|
||||
@@ -191,7 +195,7 @@ func TestParseKeys(t *testing.T) {
|
||||
check(tui.ShiftLeft, "shift-left")
|
||||
check(tui.ShiftRight, "shift-right")
|
||||
check(tui.ShiftTab, "shift-tab")
|
||||
check(tui.CtrlM, "Enter")
|
||||
check(tui.Enter, "Enter")
|
||||
check(tui.Backspace, "bspace")
|
||||
}
|
||||
|
||||
@@ -207,40 +211,40 @@ func TestParseKeysWithComma(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pairs, _ := parseKeyChords(",", "")
|
||||
pairs, _, _ := parseKeyChords(",", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords(",,a,b", "")
|
||||
pairs, _, _ = parseKeyChords(",,a,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords("a,b,,", "")
|
||||
pairs, _, _ = parseKeyChords("a,b,,", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords("a,,,b", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords("a,,,b,c", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b,c", "")
|
||||
checkN(len(pairs), 4)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key('c'), "c")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords(",,,", "")
|
||||
pairs, _, _ = parseKeyChords(",,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords(",ALT-,,", "")
|
||||
pairs, _, _ = parseKeyChords(",ALT-,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltKey(','), "ALT-,")
|
||||
}
|
||||
@@ -296,8 +300,12 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
var base *tui.ColorTheme
|
||||
theme := tui.Dark256
|
||||
dark, _ := parseTheme(theme, "dark")
|
||||
base, dark, _ := parseTheme(theme, "dark")
|
||||
if *dark != *base {
|
||||
t.Errorf("incorrect base theme returned")
|
||||
}
|
||||
if *dark != *theme {
|
||||
t.Errorf("colors should be equivalent")
|
||||
}
|
||||
@@ -305,7 +313,10 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
light, _ := parseTheme(theme, "dark,light")
|
||||
base, light, _ := parseTheme(theme, "dark,light")
|
||||
if *light != *base {
|
||||
t.Errorf("incorrect base theme returned")
|
||||
}
|
||||
if *light == *theme {
|
||||
t.Errorf("should not be equivalent")
|
||||
}
|
||||
@@ -316,7 +327,7 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
customized, _ := parseTheme(theme, "fg:231,bg:232")
|
||||
_, customized, _ := parseTheme(theme, "fg:231,bg:232")
|
||||
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
@@ -329,7 +340,7 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
||||
}
|
||||
|
||||
customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
|
||||
_, customized, _ = parseTheme(theme, "fg:231,dark bg:232")
|
||||
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
@@ -346,8 +357,8 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
check([]string{}, tui.CtrlN, actDown)
|
||||
check([]string{}, tui.CtrlP, actUp)
|
||||
check([]string{}, tui.CtrlN, actDownMatch)
|
||||
check([]string{}, tui.CtrlP, actUpMatch)
|
||||
|
||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
@@ -437,6 +448,64 @@ func TestPreviewOpts(t *testing.T) {
|
||||
opts.Preview.size.size == 70) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
|
||||
// wrap-word tests
|
||||
opts = optsFor("--preview-window=wrap-word")
|
||||
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == true) {
|
||||
t.Errorf("wrap-word: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
opts = optsFor("--preview-window=wrap-word,nowrap")
|
||||
if !(opts.Preview.wrap == false && opts.Preview.wrapWord == false) {
|
||||
t.Errorf("wrap-word,nowrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
opts = optsFor("--preview-window=wrap-word,wrap")
|
||||
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == false) {
|
||||
t.Errorf("wrap-word,wrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewWrapSign(t *testing.T) {
|
||||
// Default: no preview wrap sign override
|
||||
opts := optsFor()
|
||||
if opts.PreviewWrapSign != nil {
|
||||
t.Errorf("expected nil PreviewWrapSign, got %v", *opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// --preview-wrap-sign sets PreviewWrapSign
|
||||
opts = optsFor("--preview-wrap-sign", ">> ")
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
|
||||
t.Errorf("expected '>> ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// --preview-wrap-sign is independent of --wrap-sign
|
||||
opts = optsFor("--wrap-sign", "| ", "--preview-wrap-sign", ">> ")
|
||||
if opts.WrapSign == nil || *opts.WrapSign != "| " {
|
||||
t.Errorf("expected WrapSign '| ', got %v", opts.WrapSign)
|
||||
}
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
|
||||
t.Errorf("expected PreviewWrapSign '>> ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// --preview-wrap-sign without --wrap-sign
|
||||
opts = optsFor("--preview-wrap-sign", "→ ")
|
||||
if opts.WrapSign != nil {
|
||||
t.Errorf("expected nil WrapSign, got %v", *opts.WrapSign)
|
||||
}
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "→ " {
|
||||
t.Errorf("expected PreviewWrapSign '→ ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// Last --preview-wrap-sign wins
|
||||
opts = optsFor("--preview-wrap-sign", "A ", "--preview-wrap-sign", "B ")
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "B " {
|
||||
t.Errorf("expected PreviewWrapSign 'B ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// Empty string is allowed
|
||||
opts = optsFor("--preview-wrap-sign", "")
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "" {
|
||||
t.Errorf("expected empty PreviewWrapSign, got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdditiveExpect(t *testing.T) {
|
||||
@@ -458,7 +527,7 @@ func TestValidateSign(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := validateSign(testCase.inputSign, "")
|
||||
err := validateSign(testCase.inputSign, "", 2)
|
||||
if testCase.isValid && err != nil {
|
||||
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
|
||||
}
|
||||
|
||||
131
src/pattern.go
131
src/pattern.go
@@ -60,8 +60,13 @@ type Pattern struct {
|
||||
cacheKey string
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
revision revision
|
||||
procFun map[termType]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
startIndex int32
|
||||
directAlgo algo.Algo
|
||||
directTerm *term
|
||||
}
|
||||
|
||||
var _splitRegex *regexp.Regexp
|
||||
@@ -72,7 +77,7 @@ func init() {
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -140,11 +145,15 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
sortable: sortable,
|
||||
cacheable: cacheable,
|
||||
nth: nth,
|
||||
revision: revision,
|
||||
delimiter: delimiter,
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
startIndex: startIndex,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
|
||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||
ptr.procFun[termEqual] = algo.EqualMatch
|
||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||
@@ -241,6 +250,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if len(p.denylist) > 0 {
|
||||
return false
|
||||
}
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
@@ -265,6 +277,22 @@ func (p *Pattern) buildCacheKey() string {
|
||||
return strings.Join(cacheableTerms, "\t")
|
||||
}
|
||||
|
||||
// buildDirectAlgo returns the algo function and term for the direct fast path
|
||||
// in matchChunk. Returns (nil, nil) if the pattern is not suitable.
|
||||
// Requirements: extended mode, single term set with single non-inverse fuzzy term, no nth.
|
||||
func (p *Pattern) buildDirectAlgo(fuzzyAlgo algo.Algo) (algo.Algo, *term) {
|
||||
if !p.extended || len(p.nth) > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(p.termSets) == 1 && len(p.termSets[0]) == 1 {
|
||||
t := &p.termSets[0][0]
|
||||
if !t.inv && t.typ == termFuzzy {
|
||||
return fuzzyAlgo, t
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CacheKey is used to build string to be used as the key of result cache
|
||||
func (p *Pattern) CacheKey() string {
|
||||
return p.cacheKey
|
||||
@@ -294,38 +322,99 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
// Skip header items in chunks that contain them
|
||||
startIdx := 0
|
||||
if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex {
|
||||
startIdx = int(p.startIndex - chunk.items[0].Index())
|
||||
if startIdx >= chunk.count {
|
||||
return matches
|
||||
}
|
||||
}
|
||||
|
||||
// Fast path: single fuzzy term, no nth, no denylist.
|
||||
// Calls the algo function directly, bypassing MatchItem/extendedMatch/iter
|
||||
// and avoiding per-match []Offset heap allocation.
|
||||
if p.directAlgo != nil && len(p.denylist) == 0 {
|
||||
t := p.directTerm
|
||||
if space == nil {
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||
&chunk.items[idx].text, t.text, p.withPos, slab)
|
||||
if res.Start >= 0 {
|
||||
matches = append(matches, buildResultFromBounds(
|
||||
&chunk.items[idx], res.Score,
|
||||
int(res.Start), int(res.End), int(res.End), true))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||
&result.item.text, t.text, p.withPos, slab)
|
||||
if res.Start >= 0 {
|
||||
matches = append(matches, buildResultFromBounds(
|
||||
result.item, res.Score,
|
||||
int(res.Start), int(res.End), int(res.End), true))
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
if len(p.denylist) == 0 {
|
||||
// Huge code duplication for minimizing unnecessary map lookups
|
||||
if space == nil {
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// MatchItem returns true if the Item is a match
|
||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
||||
// MatchItem returns the match result if the Item is a match.
|
||||
// A zero-value Result (with item == nil) indicates no match.
|
||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (Result, []Offset, *[]int) {
|
||||
if p.extended {
|
||||
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
return buildResult(item, offsets, bonus), offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
return Result{}, nil, nil
|
||||
}
|
||||
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
||||
if sidx := offset[0]; sidx >= 0 {
|
||||
offsets := []Offset{offset}
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
return buildResult(item, offsets, bonus), offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
return Result{}, nil, nil
|
||||
}
|
||||
|
||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||
@@ -393,12 +482,22 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
|
||||
func (p *Pattern) transformInput(item *Item) []Token {
|
||||
if item.transformed != nil {
|
||||
return *item.transformed
|
||||
transformed := *item.transformed
|
||||
if transformed.revision == p.revision {
|
||||
return transformed.tokens
|
||||
}
|
||||
}
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
item.transformed = &ret
|
||||
// Strip the last delimiter to allow suffix match
|
||||
if len(ret) > 0 && !p.delimiter.IsAwk() {
|
||||
chars := ret[len(ret)-1].text
|
||||
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
|
||||
newChars := util.ToChars(stringBytes(stripped))
|
||||
ret[len(ret)-1].text = &newChars
|
||||
}
|
||||
item.transformed = &transformed{p.revision, ret}
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||
withPos, cacheable, nth, delimiter, runes)
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil, 0)
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
@@ -135,12 +135,12 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
chunk.items[0] = Item{
|
||||
text: util.ToChars([]byte("junegunn")),
|
||||
origText: &origBytes,
|
||||
transformed: &trans}
|
||||
transformed: &transformed{pattern.revision, trans}}
|
||||
pattern.extended = extended
|
||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||
string(*matches[0].item.origText) == "junegunn.choi" &&
|
||||
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
|
||||
reflect.DeepEqual((*matches[0].item.transformed).tokens, trans)) {
|
||||
t.Error("Invalid match result", matches)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
if !(match.item.text.ToString() == "junegunn" &&
|
||||
string(*match.item.origText) == "junegunn.choi" &&
|
||||
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
||||
reflect.DeepEqual(*match.item.transformed, trans)) {
|
||||
reflect.DeepEqual((*match.item.transformed).tokens, trans)) {
|
||||
t.Error("Invalid match result", match, offsets, extended)
|
||||
}
|
||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||
|
||||
14
src/proxy.go
14
src/proxy.go
@@ -59,12 +59,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
})
|
||||
}()
|
||||
|
||||
var command string
|
||||
var command, input string
|
||||
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
|
||||
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
|
||||
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
|
||||
} else {
|
||||
input, err := fifo("proxy-input")
|
||||
input, err = fifo("proxy-input")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
@@ -90,11 +90,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
}
|
||||
}
|
||||
|
||||
// To ensure that the options are processed by a POSIX-compliant shell,
|
||||
// we need to write the command to a temporary file and execute it with sh.
|
||||
// Write the command to a temporary file and run it with sh to ensure POSIX compliance.
|
||||
var exports []string
|
||||
needBash := false
|
||||
if withExports {
|
||||
// Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
|
||||
exports = []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
for _, pairStr := range os.Environ() {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
@@ -144,10 +145,13 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
env = elems[1:]
|
||||
}
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
ttyin, err := tui.TtyIn()
|
||||
ttyin, err := tui.TtyIn(opts.TtyDefault)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
os.Remove(temp)
|
||||
os.Remove(input)
|
||||
os.Remove(output)
|
||||
executor.Become(ttyin, env, command)
|
||||
}
|
||||
return code, err
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -120,7 +122,7 @@ func (r *Reader) readChannel(inputChan chan string) bool {
|
||||
}
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
|
||||
func (r *Reader) ReadSource(inputChan chan string, roots []string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
signalReady := func() {
|
||||
@@ -137,7 +139,7 @@ func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts,
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
signalReady()
|
||||
success = r.readFiles(root, opts, ignores)
|
||||
success = r.readFiles(roots, opts, ignores)
|
||||
} else {
|
||||
success = r.readFromCommand(cmd, initEnv, signalReady)
|
||||
}
|
||||
@@ -176,8 +178,8 @@ func (r *Reader) feed(src io.Reader) {
|
||||
var err error
|
||||
for {
|
||||
n := 0
|
||||
scope := slab[:util.Min(len(slab), readerBufferSize)]
|
||||
for i := 0; i < 100; i++ {
|
||||
scope := slab[:min(len(slab), readerBufferSize)]
|
||||
for range 100 {
|
||||
n, err = src.Read(scope)
|
||||
if n > 0 || err != nil {
|
||||
break
|
||||
@@ -265,30 +267,66 @@ func trimPath(path string) string {
|
||||
return byteString(bytes)
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
|
||||
func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bool {
|
||||
conf := fastwalk.Config{
|
||||
Follow: opts.follow,
|
||||
// Use forward slashes when running a Windows binary under WSL or MSYS
|
||||
ToSlash: fastwalk.DefaultToSlash(),
|
||||
Sort: fastwalk.SortFilesFirst,
|
||||
}
|
||||
ignoresBase := []string{}
|
||||
ignoresFull := []string{}
|
||||
ignoresSuffix := []string{}
|
||||
sep := string(os.PathSeparator)
|
||||
if _, ok := os.LookupEnv("MSYSTEM"); ok {
|
||||
sep = "/"
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
if strings.ContainsRune(ignore, os.PathSeparator) {
|
||||
if strings.HasPrefix(ignore, sep) {
|
||||
ignoresSuffix = append(ignoresSuffix, ignore)
|
||||
} else {
|
||||
// 'foo/bar' should match
|
||||
// * 'foo/bar'
|
||||
// * 'baz/foo/bar'
|
||||
// * but NOT 'bazfoo/bar'
|
||||
ignoresFull = append(ignoresFull, ignore)
|
||||
ignoresSuffix = append(ignoresSuffix, sep+ignore)
|
||||
}
|
||||
} else {
|
||||
ignoresBase = append(ignoresBase, ignore)
|
||||
}
|
||||
}
|
||||
fn := func(path string, de os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path = trimPath(path)
|
||||
if path != "." {
|
||||
isDir := de.IsDir()
|
||||
if isDir || opts.follow && isSymlinkToDir(path, de) {
|
||||
isDirSymlink := isSymlinkToDir(path, de)
|
||||
if isDirSymlink && !opts.follow {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
isDir := de.IsDir() || isDirSymlink
|
||||
if isDir {
|
||||
base := filepath.Base(path)
|
||||
if !opts.hidden && base[0] == '.' && base != ".." {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
if ignore == base {
|
||||
if slices.Contains(ignoresBase, base) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if slices.Contains(ignoresFull, path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, ignore := range ignoresSuffix {
|
||||
if strings.HasSuffix(path, ignore) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
if path != sep {
|
||||
path += sep
|
||||
}
|
||||
}
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
@@ -301,7 +339,11 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fastwalk.Walk(&conf, root, fn) == nil
|
||||
noerr := true
|
||||
for _, root := range roots {
|
||||
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
|
||||
}
|
||||
return noerr
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
|
||||
|
||||
229
src/result.go
229
src/result.go
@@ -19,6 +19,10 @@ type colorOffset struct {
|
||||
url *url
|
||||
}
|
||||
|
||||
func (co colorOffset) IsFullBgMarker(at int32) bool {
|
||||
return at == co.offset[0] && at == co.offset[1] && co.color.Attr()&tui.FullBg > 0
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
item *Item
|
||||
points [4]uint16
|
||||
@@ -29,8 +33,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
sort.Sort(ByOrder(offsets))
|
||||
}
|
||||
|
||||
result := Result{item: item}
|
||||
numChars := item.text.Length()
|
||||
minBegin := math.MaxUint16
|
||||
minEnd := math.MaxUint16
|
||||
maxEnd := 0
|
||||
@@ -38,13 +40,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
for _, offset := range offsets {
|
||||
b, e := int(offset[0]), int(offset[1])
|
||||
if b < e {
|
||||
minBegin = util.Min(b, minBegin)
|
||||
minEnd = util.Min(e, minEnd)
|
||||
maxEnd = util.Max(e, maxEnd)
|
||||
minBegin = min(b, minBegin)
|
||||
minEnd = min(e, minEnd)
|
||||
maxEnd = max(e, maxEnd)
|
||||
validOffsetFound = true
|
||||
}
|
||||
}
|
||||
|
||||
return buildResultFromBounds(item, score, minBegin, minEnd, maxEnd, validOffsetFound)
|
||||
}
|
||||
|
||||
// buildResultFromBounds builds a Result from pre-computed offset bounds.
|
||||
func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int, validOffsetFound bool) Result {
|
||||
result := Result{item: item}
|
||||
numChars := item.text.Length()
|
||||
|
||||
for idx, criterion := range sortCriteria {
|
||||
val := uint16(math.MaxUint16)
|
||||
switch criterion {
|
||||
@@ -69,10 +79,24 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
}
|
||||
case byLength:
|
||||
val = item.TrimLength()
|
||||
case byPathname:
|
||||
if validOffsetFound {
|
||||
lastDelim := -1
|
||||
s := item.text.ToString()
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == '/' || s[i] == '\\' {
|
||||
lastDelim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastDelim <= minBegin {
|
||||
val = util.AsUint16(minBegin - lastDelim)
|
||||
}
|
||||
}
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
for idx := 0; idx < numChars; idx++ {
|
||||
for idx := range numChars {
|
||||
r := item.text.Get(idx)
|
||||
whitePrefixLen = idx
|
||||
if idx == minBegin || !unicode.IsSpace(r) {
|
||||
@@ -104,21 +128,21 @@ func minRank() Result {
|
||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset {
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, hidden bool) []colorOffset {
|
||||
itemColors := result.item.Colors()
|
||||
|
||||
// No ANSI codes
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
|
||||
if len(itemColors) == 0 && len(nthOffsets) == 0 {
|
||||
offsets := make([]colorOffset, len(matchOffsets))
|
||||
for i, off := range matchOffsets {
|
||||
offsets[i] = colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// Find max column
|
||||
var maxCol int32
|
||||
for _, off := range matchOffsets {
|
||||
for _, off := range append(matchOffsets, nthOffsets...) {
|
||||
if off[1] > maxCol {
|
||||
maxCol = off[1]
|
||||
}
|
||||
@@ -129,20 +153,37 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
}
|
||||
}
|
||||
|
||||
cols := make([]int, maxCol)
|
||||
type cellInfo struct {
|
||||
index int
|
||||
color bool
|
||||
match bool
|
||||
nth bool
|
||||
fbg tui.Color
|
||||
}
|
||||
|
||||
cols := make([]cellInfo, maxCol+1)
|
||||
for idx := range cols {
|
||||
cols[idx].fbg = -1
|
||||
}
|
||||
for colorIndex, ansi := range itemColors {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = colorIndex + 1 // 1-based index of itemColors
|
||||
if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
|
||||
cols[ansi.offset[0]].fbg = ansi.color.lbg
|
||||
} else {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = cellInfo{colorIndex, true, false, false, cols[i].fbg}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range matchOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
// Negative of 1-based index of itemColors
|
||||
// - The extra -1 means highlighted
|
||||
if cols[i] >= 0 {
|
||||
cols[i] = cols[i]*-1 - 1
|
||||
}
|
||||
cols[i].match = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range nthOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
cols[i].nth = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,35 +193,46 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
// ------------ ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
curr := 0
|
||||
curr := cellInfo{0, false, false, false, -1}
|
||||
start := 0
|
||||
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
||||
if !theme.Colored {
|
||||
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
// fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
|
||||
if base.ShouldStripColors() {
|
||||
return base
|
||||
}
|
||||
fg := ansi.color.fg
|
||||
bg := ansi.color.bg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = theme.Current.Color
|
||||
} else {
|
||||
fg = theme.Fg.Color
|
||||
}
|
||||
fg = colBase.Fg()
|
||||
}
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = theme.DarkBg.Color
|
||||
} else {
|
||||
bg = theme.Bg.Color
|
||||
}
|
||||
bg = colBase.Bg()
|
||||
}
|
||||
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
||||
return tui.NewColorPair(fg, bg, ansi.color.attr).WithUl(ansi.color.ul).MergeAttr(base)
|
||||
}
|
||||
var colors []colorOffset
|
||||
add := func(idx int) {
|
||||
if curr != 0 && idx > start {
|
||||
if curr < 0 {
|
||||
color := colMatch
|
||||
if curr.fbg >= 0 {
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(start)},
|
||||
color: tui.NewColorPair(-1, curr.fbg, tui.FullBg),
|
||||
match: false,
|
||||
url: nil})
|
||||
}
|
||||
if (curr.color || curr.nth || curr.match) && idx > start {
|
||||
if curr.match {
|
||||
var color tui.ColorPair
|
||||
if curr.nth {
|
||||
color = colBase.WithAttr(attrNth).Merge(colMatch)
|
||||
} else {
|
||||
color = colBase.Merge(colMatch)
|
||||
}
|
||||
var url *url
|
||||
if curr < -1 && theme.Colored {
|
||||
ansi := itemColors[-curr-2]
|
||||
if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
url = ansi.color.url
|
||||
origColor := ansiToColorPair(ansi, colMatch)
|
||||
// hl or hl+ only sets the foreground color, so colMatch is the
|
||||
@@ -193,19 +245,40 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||
color = origColor
|
||||
if curr.nth {
|
||||
color = color.WithAttr(attrNth &^ tui.AttrRegular)
|
||||
}
|
||||
} else {
|
||||
color = origColor.MergeNonDefault(color)
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
} else if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
base := colBase
|
||||
if curr.nth {
|
||||
base = base.WithAttr(attrNth)
|
||||
}
|
||||
if hidden {
|
||||
base = base.WithFg(theme.Nomatch)
|
||||
}
|
||||
color := ansiToColorPair(ansi, base)
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: ansiToColorPair(ansi, colBase),
|
||||
color: color,
|
||||
match: false,
|
||||
url: ansi.color.url})
|
||||
} else {
|
||||
color := colBase.WithAttr(attrNth)
|
||||
if hidden {
|
||||
color = color.WithFg(theme.Nomatch)
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: color,
|
||||
match: false,
|
||||
url: nil})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,3 +339,79 @@ func (a ByRelevanceTac) Swap(i, j int) {
|
||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||
return compareRanks(a[i], a[j], true)
|
||||
}
|
||||
|
||||
// radixSortResults sorts Results by their points key using LSD radix sort.
|
||||
// O(n) time complexity vs O(n log n) for comparison sort.
|
||||
// The sort is stable, so equal-key items maintain original (item-index) order.
|
||||
// For tac mode, runs of equal keys are reversed after sorting.
|
||||
func radixSortResults(a []Result, tac bool, scratch []Result) []Result {
|
||||
n := len(a)
|
||||
if n < 128 {
|
||||
if tac {
|
||||
sort.Sort(ByRelevanceTac(a))
|
||||
} else {
|
||||
sort.Sort(ByRelevance(a))
|
||||
}
|
||||
return scratch[:0]
|
||||
}
|
||||
|
||||
if cap(scratch) < n {
|
||||
scratch = make([]Result, n)
|
||||
}
|
||||
buf := scratch[:n]
|
||||
src, dst := a, buf
|
||||
scattered := 0
|
||||
|
||||
for pass := range 8 {
|
||||
shift := uint(pass) * 8
|
||||
|
||||
var count [256]int
|
||||
for i := range src {
|
||||
count[byte(sortKey(&src[i])>>shift)]++
|
||||
}
|
||||
|
||||
// Skip if all items have the same byte value at this position
|
||||
if count[byte(sortKey(&src[0])>>shift)] == n {
|
||||
continue
|
||||
}
|
||||
|
||||
var offset [256]int
|
||||
for i := 1; i < 256; i++ {
|
||||
offset[i] = offset[i-1] + count[i-1]
|
||||
}
|
||||
|
||||
for i := range src {
|
||||
b := byte(sortKey(&src[i]) >> shift)
|
||||
dst[offset[b]] = src[i]
|
||||
offset[b]++
|
||||
}
|
||||
|
||||
src, dst = dst, src
|
||||
scattered++
|
||||
}
|
||||
|
||||
// If odd number of scatters, data is in buf, copy back to a
|
||||
if scattered%2 == 1 {
|
||||
copy(a, src)
|
||||
}
|
||||
|
||||
// Handle tac: reverse runs of equal keys so equal-key items
|
||||
// are in reverse item-index order
|
||||
if tac {
|
||||
i := 0
|
||||
for i < n {
|
||||
ki := sortKey(&a[i])
|
||||
j := i + 1
|
||||
for j < n && sortKey(&a[j]) == ki {
|
||||
j++
|
||||
}
|
||||
if j-i > 1 {
|
||||
for l, r := i, j-1; l < r; l, r = l+1, r-1 {
|
||||
a[l], a[r] = a[r], a[l]
|
||||
}
|
||||
}
|
||||
i = j
|
||||
}
|
||||
}
|
||||
return scratch
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !386 && !amd64
|
||||
//go:build !386 && !amd64 && !arm64
|
||||
|
||||
package fzf
|
||||
|
||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
}
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
|
||||
func sortKey(r *Result) uint64 {
|
||||
return uint64(r.points[0]) | uint64(r.points[1])<<16 | uint64(r.points[2])<<32 | uint64(r.points[3])<<48
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
@@ -124,14 +125,14 @@ func TestColorOffset(t *testing.T) {
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
|
||||
{[2]int32{0, 20}, ansiState{1, 5, -1, 0, -1, nil}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, -1, tui.Bold, -1, nil}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, -1, 0, -1, nil}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, -1, tui.Bold, -1, nil}}}}}
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
colors := item.colorOffsets(offsets, tui.Dark256, colBase, colMatch, true)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, false)
|
||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||
@@ -155,20 +156,87 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
|
||||
colUnderline := tui.NewColorPair(-1, -1, tui.Underline)
|
||||
colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true)
|
||||
|
||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
||||
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
|
||||
// {[35 40] {4 8 1}}]
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
|
||||
assert(5, 27, 30, colUnderline)
|
||||
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||
assert(7, 32, 33, colUnderline)
|
||||
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||
assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
||||
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, false)
|
||||
|
||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
||||
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
|
||||
// {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}]
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
|
||||
assert(5, 27, 30, colUnderline)
|
||||
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||
assert(7, 32, 33, colUnderline)
|
||||
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
|
||||
expected := tui.Bold | attr
|
||||
if attr == tui.AttrRegular {
|
||||
expected = tui.Bold
|
||||
}
|
||||
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
||||
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRadixSortResults(t *testing.T) {
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
|
||||
for _, n := range []int{128, 256, 500, 1000} {
|
||||
for _, tac := range []bool{false, true} {
|
||||
// Build items with random points and indices
|
||||
items := make([]*Item, n)
|
||||
for i := range items {
|
||||
items[i] = &Item{text: util.Chars{Index: int32(i)}}
|
||||
}
|
||||
|
||||
results := make([]Result, n)
|
||||
for i := range results {
|
||||
results[i] = Result{
|
||||
item: items[i],
|
||||
points: [4]uint16{
|
||||
uint16(rng.Intn(256)),
|
||||
uint16(rng.Intn(256)),
|
||||
uint16(rng.Intn(256)),
|
||||
uint16(rng.Intn(256)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Make some duplicates to test stability
|
||||
for i := 0; i < n/4; i++ {
|
||||
j := rng.Intn(n)
|
||||
k := rng.Intn(n)
|
||||
results[j].points = results[k].points
|
||||
}
|
||||
|
||||
// Copy for reference sort
|
||||
expected := make([]Result, n)
|
||||
copy(expected, results)
|
||||
if tac {
|
||||
sort.Sort(ByRelevanceTac(expected))
|
||||
} else {
|
||||
sort.Sort(ByRelevance(expected))
|
||||
}
|
||||
|
||||
// Radix sort
|
||||
var scratch []Result
|
||||
scratch = radixSortResults(results, tac, scratch)
|
||||
|
||||
for i := range results {
|
||||
if results[i] != expected[i] {
|
||||
t.Errorf("n=%d tac=%v: mismatch at index %d: got item %d, want item %d",
|
||||
n, tac, i, results[i].item.Index(), expected[i].item.Index())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build 386 || amd64
|
||||
//go:build 386 || amd64 || arm64
|
||||
|
||||
package fzf
|
||||
|
||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
}
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
|
||||
func sortKey(r *Result) uint64 {
|
||||
return *(*uint64)(unsafe.Pointer(&r.points[0]))
|
||||
}
|
||||
|
||||
@@ -46,15 +46,20 @@ type httpServer struct {
|
||||
type listenAddress struct {
|
||||
host string
|
||||
port int
|
||||
sock string
|
||||
}
|
||||
|
||||
func (addr listenAddress) IsLocal() bool {
|
||||
return addr.host == "localhost" || addr.host == "127.0.0.1"
|
||||
return addr.host == "localhost" || addr.host == "127.0.0.1" || len(addr.sock) > 0
|
||||
}
|
||||
|
||||
var defaultListenAddr = listenAddress{"localhost", 0}
|
||||
var defaultListenAddr = listenAddress{"localhost", 0, ""}
|
||||
|
||||
func parseListenAddress(address string) (listenAddress, error) {
|
||||
if strings.HasSuffix(address, ".sock") {
|
||||
return listenAddress{"", 0, address}, nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(address, ":", 3)
|
||||
if len(parts) == 1 {
|
||||
parts = []string{"localhost", parts[0]}
|
||||
@@ -70,7 +75,7 @@ func parseListenAddress(address string) (listenAddress, error) {
|
||||
if len(parts[0]) == 0 {
|
||||
parts[0] = "localhost"
|
||||
}
|
||||
return listenAddress{parts[0], port}, nil
|
||||
return listenAddress{parts[0], port, ""}, nil
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||
@@ -80,21 +85,40 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
|
||||
if !address.IsLocal() && len(apiKey) == 0 {
|
||||
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
|
||||
}
|
||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||
listener, err := net.Listen("tcp", addrStr)
|
||||
if err != nil {
|
||||
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.Split(addr, ":")
|
||||
if len(parts) < 2 {
|
||||
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
|
||||
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if len(address.sock) > 0 {
|
||||
if _, err := os.Stat(address.sock); err == nil {
|
||||
// Check if the socket is already in use
|
||||
if conn, err := net.Dial("unix", address.sock); err == nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("socket already in use: %s", address.sock)
|
||||
}
|
||||
os.Remove(address.sock)
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||
listener, err = net.Listen("unix", address.sock)
|
||||
if err != nil {
|
||||
return nil, port, err
|
||||
return nil, 0, fmt.Errorf("failed to listen on %s", address.sock)
|
||||
}
|
||||
os.Chmod(address.sock, 0600)
|
||||
} else {
|
||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||
listener, err = net.Listen("tcp", addrStr)
|
||||
if err != nil {
|
||||
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.Split(addr, ":")
|
||||
if len(parts) < 2 {
|
||||
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return nil, port, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,23 +183,22 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
})
|
||||
|
||||
section := 0
|
||||
var getMatch []string
|
||||
Loop:
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
switch section {
|
||||
case 0:
|
||||
getMatch := getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) > 0 {
|
||||
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||
if len(response) > 0 {
|
||||
return good(response)
|
||||
}
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
case 0: // Request line
|
||||
getMatch = getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) == 0 && !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
section++
|
||||
case 1:
|
||||
if text == crlf {
|
||||
case 1: // Request headers
|
||||
if text == crlf { // End of headers
|
||||
if len(getMatch) > 0 {
|
||||
break Loop
|
||||
}
|
||||
if contentLength == 0 {
|
||||
return bad("content-length header missing")
|
||||
}
|
||||
@@ -195,7 +218,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
apiKey = strings.TrimSpace(pair[1])
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
case 2: // Request body
|
||||
body += text
|
||||
}
|
||||
}
|
||||
@@ -204,6 +227,14 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
return unauthorized("invalid api key")
|
||||
}
|
||||
|
||||
if len(getMatch) > 0 {
|
||||
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||
if len(response) > 0 {
|
||||
return good(response)
|
||||
}
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
}
|
||||
|
||||
if len(body) < contentLength {
|
||||
return bad("incomplete request")
|
||||
}
|
||||
|
||||
4206
src/terminal.go
4206
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems [3][]*Item) string {
|
||||
replaced, _ := replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: stripAnsi,
|
||||
@@ -30,11 +30,11 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
items2 := []*Item{
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||
items1 := [3][]*Item{{item1}, {item1}, nil}
|
||||
items2 := [3][]*Item{
|
||||
{newItem("foo'bar \x1b[31mbaz\x1b[m")},
|
||||
{newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}, nil}
|
||||
|
||||
delim := "'"
|
||||
var regex *regexp.Regexp
|
||||
@@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {r}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {r..}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
@@ -137,11 +145,11 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, {item1}, nil})
|
||||
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// String delimiter
|
||||
@@ -158,17 +166,18 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
|
||||
see: TestParsePlaceholder
|
||||
*/
|
||||
items3 := []*Item{
|
||||
items3 := [3][]*Item{
|
||||
// single line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
{newItem("1a 1b 1c 1d 1e 1f")},
|
||||
// multi line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
newItem("2a 2b 2c 2d 2e 2f"),
|
||||
newItem("3a 3b 3c 3d 3e 3f"),
|
||||
newItem("4a 4b 4c 4d 4e 4f"),
|
||||
newItem("5a 5b 5c 5d 5e 5f"),
|
||||
newItem("6a 6b 6c 6d 6e 6f"),
|
||||
newItem("7a 7b 7c 7d 7e 7f"),
|
||||
{newItem("1a 1b 1c 1d 1e 1f"),
|
||||
newItem("2a 2b 2c 2d 2e 2f"),
|
||||
newItem("3a 3b 3c 3d 3e 3f"),
|
||||
newItem("4a 4b 4c 4d 4e 4f"),
|
||||
newItem("5a 5b 5c 5d 5e 5f"),
|
||||
newItem("6a 6b 6c 6d 6e 6f"),
|
||||
newItem("7a 7b 7c 7d 7e 7f")},
|
||||
nil,
|
||||
}
|
||||
stripAnsi := false
|
||||
forcePlus := false
|
||||
@@ -484,7 +493,12 @@ func TestParsePlaceholder(t *testing.T) {
|
||||
// III. query type placeholder
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
`{q}`: `{qq}`,
|
||||
`{q}`: `{qq}`,
|
||||
`{q:1}`: `{qq:1}`,
|
||||
`{q:2..}`: `{qq:2..}`,
|
||||
`{q:..}`: `{qq:..}`,
|
||||
`{q:2..-1}`: `{qq:2..-1}`,
|
||||
`{q:s2..-1}`: `{sqq:2..-1}`, // FIXME
|
||||
|
||||
// IV. escaping placeholder
|
||||
`\{}`: `{}`,
|
||||
@@ -507,6 +521,34 @@ func TestParsePlaceholder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPassthroughs(t *testing.T) {
|
||||
for _, middle := range []string{
|
||||
"\x1bPtmux;\x1b\x1bbar\x1b\\",
|
||||
"\x1bPtmux;\x1b\x1bbar\x1bbar\x1b\\",
|
||||
"\x1b]1337;bar\x1b\\",
|
||||
"\x1b]1337;bar\x1bbar\x1b\\",
|
||||
"\x1b]1337;bar\a",
|
||||
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\",
|
||||
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\\r",
|
||||
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1bbar\x1b\\\r",
|
||||
"\x1b_Gm=1;AAAAAAAAA=\x1b\\",
|
||||
"\x1b_Gm=1;AAAAAAAAA=\x1b\\\r",
|
||||
"\x1b_Gm=1;\x1bAAAAAAAAA=\x1b\\\r",
|
||||
} {
|
||||
line := "foo" + middle + "baz"
|
||||
loc := findPassThrough(line)
|
||||
if loc == nil || line[0:loc[0]] != "foo" || line[loc[1]:] != "baz" {
|
||||
t.Error("failed to find passthrough")
|
||||
}
|
||||
garbage := "\x1bPtmux;\x1b]1337;\x1b_Ga=\x1b]1337;bar\x1b."
|
||||
line = strings.Repeat("foo"+middle+middle+"baz", 3) + garbage
|
||||
passthroughs, result := extractPassThroughs(line)
|
||||
if result != "foobazfoobazfoobaz"+garbage || len(passthroughs) != 6 {
|
||||
t.Error("failed to extract passthroughs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* utilities section */
|
||||
|
||||
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||
@@ -516,14 +558,14 @@ func newItem(str string) *Item {
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
// Functions tested in this file require array of items (allItems). The array needs
|
||||
// to consist of at least two nils. This is helper function.
|
||||
func newItems(str ...string) []*Item {
|
||||
result := make([]*Item, util.Max(len(str), 2))
|
||||
// Functions tested in this file require array of items (allItems).
|
||||
// This is helper function.
|
||||
func newItems(str ...string) [3][]*Item {
|
||||
result := make([]*Item, len(str))
|
||||
for i, s := range str {
|
||||
result[i] = newItem(s)
|
||||
}
|
||||
return result
|
||||
return [3][]*Item{result, nil, nil}
|
||||
}
|
||||
|
||||
// (for logging purposes)
|
||||
@@ -532,7 +574,7 @@ func (item *Item) String() string {
|
||||
}
|
||||
|
||||
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
||||
func templateToString(format string, data interface{}) string {
|
||||
func templateToString(format string, data any) string {
|
||||
bb := &bytes.Buffer{}
|
||||
|
||||
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||
@@ -547,7 +589,7 @@ func templateToString(format string, data interface{}) string {
|
||||
type give struct {
|
||||
template string
|
||||
query string
|
||||
allItems []*Item
|
||||
allItems [3][]*Item
|
||||
}
|
||||
type want struct {
|
||||
/*
|
||||
@@ -585,25 +627,25 @@ func testCommands(t *testing.T, tests []testCase) {
|
||||
// evaluate the test cases
|
||||
for idx, test := range tests {
|
||||
gotOutput := replacePlaceholderTest(
|
||||
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.give.query,
|
||||
test.give.allItems)
|
||||
test.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.query,
|
||||
test.allItems)
|
||||
switch {
|
||||
case test.want.output != "":
|
||||
if gotOutput != test.want.output {
|
||||
case test.output != "":
|
||||
if gotOutput != test.output {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.output)
|
||||
test.template, test.query, test.allItems,
|
||||
gotOutput, test.output)
|
||||
}
|
||||
case test.want.match != "":
|
||||
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
|
||||
case test.match != "":
|
||||
wantMatch := strings.ReplaceAll(test.match, `\`, `\\`)
|
||||
wantRegex := regexp.MustCompile(wantMatch)
|
||||
if !wantRegex.MatchString(gotOutput) {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.match)
|
||||
test.template, test.query, test.allItems,
|
||||
gotOutput, test.match)
|
||||
}
|
||||
default:
|
||||
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
|
||||
@@ -657,3 +699,72 @@ func readFile(path string) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordWrapAnsiLine(t *testing.T) {
|
||||
term := &Terminal{}
|
||||
|
||||
// Simple wrapping
|
||||
result := term.wordWrapAnsiLine("hello world", 7, 2)
|
||||
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
|
||||
t.Errorf("Simple: %q", result)
|
||||
}
|
||||
|
||||
// No wrapping needed
|
||||
result = term.wordWrapAnsiLine("hello", 10, 2)
|
||||
if len(result) != 1 || result[0] != "hello" {
|
||||
t.Errorf("No wrap: %q", result)
|
||||
}
|
||||
|
||||
// ANSI codes preserved across split
|
||||
result = term.wordWrapAnsiLine("\x1b[31mhello \x1b[32mworld", 8, 2)
|
||||
if len(result) != 2 || result[0] != "\x1b[31mhello" || result[1] != "\x1b[32mworld" {
|
||||
t.Errorf("ANSI: %q", result)
|
||||
}
|
||||
|
||||
// Long word (no space) — no break, let character wrapping handle it
|
||||
result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
|
||||
if len(result) != 1 || result[0] != "abcdefghij" {
|
||||
t.Errorf("Long word: %q", result)
|
||||
}
|
||||
|
||||
// Multiple words with continuation wrapSignWidth
|
||||
result = term.wordWrapAnsiLine("aa bb cc dd", 5, 2)
|
||||
// max=5 for first line, max=3 for continuations (5-2)
|
||||
// "aa bb" (5 wide), split at second space -> "aa bb" | "cc" | "dd"
|
||||
if len(result) != 3 || result[0] != "aa bb" || result[1] != "cc" || result[2] != "dd" {
|
||||
t.Errorf("Multiple words: %q", result)
|
||||
}
|
||||
|
||||
// Empty string
|
||||
result = term.wordWrapAnsiLine("", 10, 2)
|
||||
if len(result) != 1 || result[0] != "" {
|
||||
t.Errorf("Empty: %q", result)
|
||||
}
|
||||
|
||||
// OSC 8 hyperlink preserved
|
||||
result = term.wordWrapAnsiLine("\x1b]8;;http://example.com\x1b\\click here\x1b]8;;\x1b\\", 8, 2)
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Hyperlink split count: %d, %q", len(result), result)
|
||||
}
|
||||
|
||||
// Tab handling: tab expands to tabstop-aligned width
|
||||
term.tabstop = 8
|
||||
// "\thi there" — tab at column 0 expands to 8, total "hi" starts at 8
|
||||
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
|
||||
result = term.wordWrapAnsiLine("\thi there", 15, 2)
|
||||
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
|
||||
t.Errorf("Tab: %q", result)
|
||||
}
|
||||
|
||||
// Tab as word boundary: "hello"(5) + tab(3→col8) + "world"(5) = 13 total
|
||||
// maxWidth=13: fits without wrapping
|
||||
result = term.wordWrapAnsiLine("hello\tworld", 13, 2)
|
||||
if len(result) != 1 || result[0] != "hello\tworld" {
|
||||
t.Errorf("Tab no wrap: %q", result)
|
||||
}
|
||||
// maxWidth=12: 13 > 12, wraps at tab
|
||||
result = term.wordWrapAnsiLine("hello\tworld", 12, 2)
|
||||
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
|
||||
t.Errorf("Tab wrap: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
24
src/tmux.go
24
src/tmux.go
@@ -9,13 +9,22 @@ import (
|
||||
|
||||
func runTmux(args []string, opts *Options) (int, error) {
|
||||
// Prepare arguments
|
||||
fzf := args[0]
|
||||
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
|
||||
if opts.BorderShape == tui.BorderUndefined {
|
||||
args = append(args, "--border")
|
||||
fzf, rest := args[0], args[1:]
|
||||
args = []string{"--bind=ctrl-z:ignore"}
|
||||
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
|
||||
// We append --border option at the end, because `--style=full:STYLE`
|
||||
// may have changed the default border style.
|
||||
if tui.DefaultBorderShape == tui.BorderRounded {
|
||||
rest = append(rest, "--border=rounded")
|
||||
} else {
|
||||
rest = append(rest, "--border=sharp")
|
||||
}
|
||||
}
|
||||
if opts.Tmux.border && opts.Margin == defaultMargin() {
|
||||
args = append(args, "--margin=0,1")
|
||||
}
|
||||
argStr := escapeSingleQuote(fzf)
|
||||
for _, arg := range args {
|
||||
for _, arg := range append(args, rest...) {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-tmux --no-height`
|
||||
@@ -33,7 +42,10 @@ func runTmux(args []string, opts *Options) (int, error) {
|
||||
// M Both The mouse position
|
||||
// W Both The window position on the status line
|
||||
// S -y The line above or below the status line
|
||||
tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
|
||||
tmuxArgs := []string{"display-popup", "-E", "-d", dir}
|
||||
if !opts.Tmux.border {
|
||||
tmuxArgs = append(tmuxArgs, "-B")
|
||||
}
|
||||
switch opts.Tmux.position {
|
||||
case posUp:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
@@ -18,6 +19,48 @@ type Range struct {
|
||||
end int
|
||||
}
|
||||
|
||||
func (r Range) IsFull() bool {
|
||||
return r.begin == rangeEllipsis && r.end == rangeEllipsis
|
||||
}
|
||||
|
||||
func compareRanges(r1 []Range, r2 []Range) bool {
|
||||
if len(r1) != len(r2) {
|
||||
return false
|
||||
}
|
||||
for idx := range r1 {
|
||||
if r1[idx] != r2[idx] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RangesToString(ranges []Range) string {
|
||||
strs := []string{}
|
||||
for _, r := range ranges {
|
||||
s := ""
|
||||
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
|
||||
s = ".."
|
||||
} else if r.begin == r.end {
|
||||
s = strconv.Itoa(r.begin)
|
||||
} else {
|
||||
if r.begin != rangeEllipsis {
|
||||
s += strconv.Itoa(r.begin)
|
||||
}
|
||||
|
||||
if r.begin != -1 {
|
||||
s += ".."
|
||||
if r.end != rangeEllipsis {
|
||||
s += strconv.Itoa(r.end)
|
||||
}
|
||||
}
|
||||
}
|
||||
strs = append(strs, s)
|
||||
}
|
||||
|
||||
return strings.Join(strs, ",")
|
||||
}
|
||||
|
||||
// Token contains the tokenized part of the strings and its prefix length
|
||||
type Token struct {
|
||||
text *util.Chars
|
||||
@@ -35,13 +78,18 @@ type Delimiter struct {
|
||||
str *string
|
||||
}
|
||||
|
||||
// IsAwk returns true if the delimiter is an AWK-style delimiter
|
||||
func (d Delimiter) IsAwk() bool {
|
||||
return d.regex == nil && d.str == nil
|
||||
}
|
||||
|
||||
// String returns the string representation of a Delimiter.
|
||||
func (d Delimiter) String() string {
|
||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||
}
|
||||
|
||||
func newRange(begin int, end int) Range {
|
||||
if begin == 1 {
|
||||
if begin == 1 && end != 1 {
|
||||
begin = rangeEllipsis
|
||||
}
|
||||
if end == -1 {
|
||||
@@ -73,7 +121,7 @@ func ParseRange(str *string) (Range, bool) {
|
||||
}
|
||||
begin, err1 := strconv.Atoi(ns[0])
|
||||
end, err2 := strconv.Atoi(ns[1])
|
||||
if err1 != nil || err2 != nil || begin == 0 || end == 0 {
|
||||
if err1 != nil || err2 != nil || begin == 0 || end == 0 || begin < 0 && end > 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
return newRange(begin, end), true
|
||||
@@ -158,8 +206,9 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(text, -1)
|
||||
begin := 0
|
||||
for _, loc := range locs {
|
||||
tokens = append(tokens, text[begin:loc[1]])
|
||||
tokens = make([]string, len(locs))
|
||||
for i, loc := range locs {
|
||||
tokens[i] = text[begin:loc[1]]
|
||||
begin = loc[1]
|
||||
}
|
||||
if begin < len(text) {
|
||||
@@ -169,7 +218,41 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) string {
|
||||
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(locs) > 0 {
|
||||
lastLoc := locs[len(locs)-1]
|
||||
if lastLoc[1] == len(str) {
|
||||
str = str[:lastLoc[0]]
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||
}
|
||||
|
||||
func GetLastDelimiter(str string, delimiter Delimiter) string {
|
||||
if delimiter.str != nil {
|
||||
if strings.HasSuffix(str, *delimiter.str) {
|
||||
return *delimiter.str
|
||||
}
|
||||
} else if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(locs) > 0 {
|
||||
lastLoc := locs[len(locs)-1]
|
||||
if lastLoc[1] == len(str) {
|
||||
return str[lastLoc[0]:]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// JoinTokens concatenates the tokens into a single string
|
||||
func JoinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
output.WriteString(token.text.ToString())
|
||||
@@ -187,7 +270,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.ToChars(stringBytes(joinTokens(tokens)))
|
||||
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
@@ -219,7 +302,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
end += numTokens + 1
|
||||
}
|
||||
}
|
||||
minIdx = util.Max(0, begin-1)
|
||||
minIdx = max(0, begin-1)
|
||||
for idx := begin; idx <= end; idx++ {
|
||||
if idx >= 1 && idx <= numTokens {
|
||||
parts = append(parts, tokens[idx-1].text)
|
||||
|
||||
@@ -40,6 +40,18 @@ func TestParseRange(t *testing.T) {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
{
|
||||
i := "1..3..5"
|
||||
if r, ok := ParseRange(&i); ok {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
{
|
||||
i := "-3..3"
|
||||
if r, ok := ParseRange(&i); ok {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenize(t *testing.T) {
|
||||
@@ -73,14 +85,14 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != "abc: def: ghi: " {
|
||||
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
@@ -95,7 +107,7 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
|
||||
@@ -2,23 +2,7 @@
|
||||
|
||||
package tui
|
||||
|
||||
type Attr int32
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var DefaultBorderShape = BorderRounded
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 8)
|
||||
AttrClear = Attr(1 << 9)
|
||||
|
||||
Bold = Attr(1)
|
||||
Dim = Attr(1 << 1)
|
||||
Italic = Attr(1 << 2)
|
||||
@@ -29,7 +13,14 @@ const (
|
||||
StrikeThrough = Attr(1 << 7)
|
||||
)
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var DefaultBorderShape = BorderRounded
|
||||
|
||||
func (r *FullscreenRenderer) Init() error { return nil }
|
||||
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
|
||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||
func (r *FullscreenRenderer) Pause(bool) {}
|
||||
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||
@@ -37,17 +28,20 @@ func (r *FullscreenRenderer) PassThrough(string) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
|
||||
func (r *FullscreenRenderer) Bell() {}
|
||||
func (r *FullscreenRenderer) HideCursor() {}
|
||||
func (r *FullscreenRenderer) ShowCursor() {}
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||
func (r *FullscreenRenderer) Top() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||
func (r *FullscreenRenderer) Top() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||
func (r *FullscreenRenderer) GetChar(bool) Event { return Event{} }
|
||||
func (r *FullscreenRenderer) CancelGetChar() {}
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func _() {
|
||||
_ = x[CtrlJ-10]
|
||||
_ = x[CtrlK-11]
|
||||
_ = x[CtrlL-12]
|
||||
_ = x[CtrlM-13]
|
||||
_ = x[Enter-13]
|
||||
_ = x[CtrlN-14]
|
||||
_ = x[CtrlO-15]
|
||||
_ = x[CtrlP-16]
|
||||
@@ -37,82 +37,137 @@ func _() {
|
||||
_ = x[CtrlZ-26]
|
||||
_ = x[Esc-27]
|
||||
_ = x[CtrlSpace-28]
|
||||
_ = x[CtrlDelete-29]
|
||||
_ = x[CtrlBackSlash-30]
|
||||
_ = x[CtrlRightBracket-31]
|
||||
_ = x[CtrlCaret-32]
|
||||
_ = x[CtrlSlash-33]
|
||||
_ = x[ShiftTab-34]
|
||||
_ = x[Backspace-35]
|
||||
_ = x[Delete-36]
|
||||
_ = x[PageUp-37]
|
||||
_ = x[PageDown-38]
|
||||
_ = x[Up-39]
|
||||
_ = x[Down-40]
|
||||
_ = x[Left-41]
|
||||
_ = x[Right-42]
|
||||
_ = x[Home-43]
|
||||
_ = x[End-44]
|
||||
_ = x[Insert-45]
|
||||
_ = x[ShiftUp-46]
|
||||
_ = x[ShiftDown-47]
|
||||
_ = x[ShiftLeft-48]
|
||||
_ = x[ShiftRight-49]
|
||||
_ = x[ShiftDelete-50]
|
||||
_ = x[F1-51]
|
||||
_ = x[F2-52]
|
||||
_ = x[F3-53]
|
||||
_ = x[F4-54]
|
||||
_ = x[F5-55]
|
||||
_ = x[F6-56]
|
||||
_ = x[F7-57]
|
||||
_ = x[F8-58]
|
||||
_ = x[F9-59]
|
||||
_ = x[F10-60]
|
||||
_ = x[F11-61]
|
||||
_ = x[F12-62]
|
||||
_ = x[AltBackspace-63]
|
||||
_ = x[AltUp-64]
|
||||
_ = x[AltDown-65]
|
||||
_ = x[AltLeft-66]
|
||||
_ = x[AltRight-67]
|
||||
_ = x[AltShiftUp-68]
|
||||
_ = x[AltShiftDown-69]
|
||||
_ = x[AltShiftLeft-70]
|
||||
_ = x[AltShiftRight-71]
|
||||
_ = x[Alt-72]
|
||||
_ = x[CtrlAlt-73]
|
||||
_ = x[Invalid-74]
|
||||
_ = x[Fatal-75]
|
||||
_ = x[Mouse-76]
|
||||
_ = x[DoubleClick-77]
|
||||
_ = x[LeftClick-78]
|
||||
_ = x[RightClick-79]
|
||||
_ = x[SLeftClick-80]
|
||||
_ = x[SRightClick-81]
|
||||
_ = x[ScrollUp-82]
|
||||
_ = x[ScrollDown-83]
|
||||
_ = x[SScrollUp-84]
|
||||
_ = x[SScrollDown-85]
|
||||
_ = x[PreviewScrollUp-86]
|
||||
_ = x[PreviewScrollDown-87]
|
||||
_ = x[Resize-88]
|
||||
_ = x[Change-89]
|
||||
_ = x[BackwardEOF-90]
|
||||
_ = x[Start-91]
|
||||
_ = x[Load-92]
|
||||
_ = x[Focus-93]
|
||||
_ = x[One-94]
|
||||
_ = x[Zero-95]
|
||||
_ = x[Result-96]
|
||||
_ = x[Jump-97]
|
||||
_ = x[JumpCancel-98]
|
||||
_ = x[ClickHeader-99]
|
||||
_ = x[CtrlBackSlash-29]
|
||||
_ = x[CtrlRightBracket-30]
|
||||
_ = x[CtrlCaret-31]
|
||||
_ = x[CtrlSlash-32]
|
||||
_ = x[ShiftTab-33]
|
||||
_ = x[Backspace-34]
|
||||
_ = x[Delete-35]
|
||||
_ = x[PageUp-36]
|
||||
_ = x[PageDown-37]
|
||||
_ = x[Up-38]
|
||||
_ = x[Down-39]
|
||||
_ = x[Left-40]
|
||||
_ = x[Right-41]
|
||||
_ = x[Home-42]
|
||||
_ = x[End-43]
|
||||
_ = x[Insert-44]
|
||||
_ = x[ShiftUp-45]
|
||||
_ = x[ShiftDown-46]
|
||||
_ = x[ShiftLeft-47]
|
||||
_ = x[ShiftRight-48]
|
||||
_ = x[ShiftDelete-49]
|
||||
_ = x[ShiftHome-50]
|
||||
_ = x[ShiftEnd-51]
|
||||
_ = x[ShiftPageUp-52]
|
||||
_ = x[ShiftPageDown-53]
|
||||
_ = x[F1-54]
|
||||
_ = x[F2-55]
|
||||
_ = x[F3-56]
|
||||
_ = x[F4-57]
|
||||
_ = x[F5-58]
|
||||
_ = x[F6-59]
|
||||
_ = x[F7-60]
|
||||
_ = x[F8-61]
|
||||
_ = x[F9-62]
|
||||
_ = x[F10-63]
|
||||
_ = x[F11-64]
|
||||
_ = x[F12-65]
|
||||
_ = x[AltBackspace-66]
|
||||
_ = x[AltUp-67]
|
||||
_ = x[AltDown-68]
|
||||
_ = x[AltLeft-69]
|
||||
_ = x[AltRight-70]
|
||||
_ = x[AltDelete-71]
|
||||
_ = x[AltHome-72]
|
||||
_ = x[AltEnd-73]
|
||||
_ = x[AltPageUp-74]
|
||||
_ = x[AltPageDown-75]
|
||||
_ = x[AltShiftUp-76]
|
||||
_ = x[AltShiftDown-77]
|
||||
_ = x[AltShiftLeft-78]
|
||||
_ = x[AltShiftRight-79]
|
||||
_ = x[AltShiftDelete-80]
|
||||
_ = x[AltShiftHome-81]
|
||||
_ = x[AltShiftEnd-82]
|
||||
_ = x[AltShiftPageUp-83]
|
||||
_ = x[AltShiftPageDown-84]
|
||||
_ = x[CtrlUp-85]
|
||||
_ = x[CtrlDown-86]
|
||||
_ = x[CtrlLeft-87]
|
||||
_ = x[CtrlRight-88]
|
||||
_ = x[CtrlHome-89]
|
||||
_ = x[CtrlEnd-90]
|
||||
_ = x[CtrlBackspace-91]
|
||||
_ = x[CtrlDelete-92]
|
||||
_ = x[CtrlPageUp-93]
|
||||
_ = x[CtrlPageDown-94]
|
||||
_ = x[Alt-95]
|
||||
_ = x[CtrlAlt-96]
|
||||
_ = x[CtrlAltUp-97]
|
||||
_ = x[CtrlAltDown-98]
|
||||
_ = x[CtrlAltLeft-99]
|
||||
_ = x[CtrlAltRight-100]
|
||||
_ = x[CtrlAltHome-101]
|
||||
_ = x[CtrlAltEnd-102]
|
||||
_ = x[CtrlAltBackspace-103]
|
||||
_ = x[CtrlAltDelete-104]
|
||||
_ = x[CtrlAltPageUp-105]
|
||||
_ = x[CtrlAltPageDown-106]
|
||||
_ = x[CtrlShiftUp-107]
|
||||
_ = x[CtrlShiftDown-108]
|
||||
_ = x[CtrlShiftLeft-109]
|
||||
_ = x[CtrlShiftRight-110]
|
||||
_ = x[CtrlShiftHome-111]
|
||||
_ = x[CtrlShiftEnd-112]
|
||||
_ = x[CtrlShiftDelete-113]
|
||||
_ = x[CtrlShiftPageUp-114]
|
||||
_ = x[CtrlShiftPageDown-115]
|
||||
_ = x[CtrlAltShiftUp-116]
|
||||
_ = x[CtrlAltShiftDown-117]
|
||||
_ = x[CtrlAltShiftLeft-118]
|
||||
_ = x[CtrlAltShiftRight-119]
|
||||
_ = x[CtrlAltShiftHome-120]
|
||||
_ = x[CtrlAltShiftEnd-121]
|
||||
_ = x[CtrlAltShiftDelete-122]
|
||||
_ = x[CtrlAltShiftPageUp-123]
|
||||
_ = x[CtrlAltShiftPageDown-124]
|
||||
_ = x[Invalid-125]
|
||||
_ = x[Fatal-126]
|
||||
_ = x[BracketedPasteBegin-127]
|
||||
_ = x[BracketedPasteEnd-128]
|
||||
_ = x[Mouse-129]
|
||||
_ = x[DoubleClick-130]
|
||||
_ = x[LeftClick-131]
|
||||
_ = x[RightClick-132]
|
||||
_ = x[SLeftClick-133]
|
||||
_ = x[SRightClick-134]
|
||||
_ = x[ScrollUp-135]
|
||||
_ = x[ScrollDown-136]
|
||||
_ = x[SScrollUp-137]
|
||||
_ = x[SScrollDown-138]
|
||||
_ = x[PreviewScrollUp-139]
|
||||
_ = x[PreviewScrollDown-140]
|
||||
_ = x[Resize-141]
|
||||
_ = x[Change-142]
|
||||
_ = x[BackwardEOF-143]
|
||||
_ = x[Start-144]
|
||||
_ = x[Load-145]
|
||||
_ = x[Focus-146]
|
||||
_ = x[One-147]
|
||||
_ = x[Zero-148]
|
||||
_ = x[Result-149]
|
||||
_ = x[Jump-150]
|
||||
_ = x[JumpCancel-151]
|
||||
_ = x[ClickHeader-152]
|
||||
_ = x[ClickFooter-153]
|
||||
_ = x[Multi-154]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
|
||||
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1067, 1072, 1091, 1108, 1113, 1124, 1133, 1143, 1153, 1164, 1172, 1182, 1191, 1202, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
||||
701
src/tui/light.go
701
src/tui/light.go
File diff suppressed because it is too large
Load Diff
352
src/tui/light_test.go
Normal file
352
src/tui/light_test.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestLightRenderer(t *testing.T) {
|
||||
tty_file, _ := os.Open("")
|
||||
renderer, _ := NewLightRenderer(
|
||||
"", tty_file, &ColorTheme{}, true, false, 0, false, true,
|
||||
func(h int) int { return h })
|
||||
|
||||
light_renderer := renderer.(*LightRenderer)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
light_renderer.mutex.Lock()
|
||||
ready := light_renderer.cancel != nil
|
||||
light_renderer.mutex.Unlock()
|
||||
|
||||
if ready {
|
||||
light_renderer.CancelGetChar()
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
event := light_renderer.GetChar(true)
|
||||
if event.Type != Invalid {
|
||||
t.Error("Not cancelled")
|
||||
}
|
||||
|
||||
assertCharSequence := func(sequence string, name string) {
|
||||
bytes := []byte(sequence)
|
||||
light_renderer.buffer = bytes
|
||||
event := light_renderer.GetChar(true)
|
||||
if event.KeyName() != name {
|
||||
t.Errorf(
|
||||
"sequence: %q | %v | '%s' (%s) != %s",
|
||||
string(bytes), bytes,
|
||||
event.KeyName(), event.Type.String(), name)
|
||||
}
|
||||
}
|
||||
|
||||
assertEscSequence := func(sequence string, name string) {
|
||||
bytes := []byte(sequence)
|
||||
light_renderer.buffer = bytes
|
||||
|
||||
sz := 1
|
||||
event := light_renderer.escSequence(&sz)
|
||||
if fmt.Sprintf("!%s", event.Type.String()) == name {
|
||||
// this is fine
|
||||
} else if event.KeyName() != name {
|
||||
t.Errorf(
|
||||
"sequence: %q | %v | '%s' (%s) != %s",
|
||||
string(bytes), bytes,
|
||||
event.KeyName(), event.Type.String(), name)
|
||||
}
|
||||
}
|
||||
|
||||
// invalid
|
||||
assertEscSequence("\x1b[<", "!Invalid")
|
||||
assertEscSequence("\x1b[1;1R", "!Invalid")
|
||||
assertEscSequence("\x1b[", "!Invalid")
|
||||
assertEscSequence("\x1b[1", "!Invalid")
|
||||
assertEscSequence("\x1b[3;3~1", "!Invalid")
|
||||
assertEscSequence("\x1b[13", "!Invalid")
|
||||
assertEscSequence("\x1b[1;3", "!Invalid")
|
||||
assertEscSequence("\x1b[1;10", "!Invalid")
|
||||
assertEscSequence("\x1b[220~", "!Invalid")
|
||||
assertEscSequence("\x1b[5;30~", "!Invalid")
|
||||
assertEscSequence("\x1b[6;30~", "!Invalid")
|
||||
|
||||
// general
|
||||
for r := 'a'; r < 'z'; r++ {
|
||||
lower_r := fmt.Sprintf("%c", r)
|
||||
upper_r := fmt.Sprintf("%c", unicode.ToUpper(r))
|
||||
assertCharSequence(lower_r, lower_r)
|
||||
assertCharSequence(upper_r, upper_r)
|
||||
}
|
||||
|
||||
assertCharSequence("\x01", "ctrl-a")
|
||||
assertCharSequence("\x02", "ctrl-b")
|
||||
assertCharSequence("\x03", "ctrl-c")
|
||||
assertCharSequence("\x04", "ctrl-d")
|
||||
assertCharSequence("\x05", "ctrl-e")
|
||||
assertCharSequence("\x06", "ctrl-f")
|
||||
assertCharSequence("\x07", "ctrl-g")
|
||||
// ctrl-h is the same as ctrl-backspace
|
||||
// ctrl-i is the same as tab
|
||||
assertCharSequence("\n", "ctrl-j")
|
||||
assertCharSequence("\x0b", "ctrl-k")
|
||||
assertCharSequence("\x0c", "ctrl-l")
|
||||
assertCharSequence("\r", "enter") // enter
|
||||
assertCharSequence("\x0e", "ctrl-n")
|
||||
assertCharSequence("\x0f", "ctrl-o")
|
||||
assertCharSequence("\x10", "ctrl-p")
|
||||
assertCharSequence("\x11", "ctrl-q")
|
||||
assertCharSequence("\x12", "ctrl-r")
|
||||
assertCharSequence("\x13", "ctrl-s")
|
||||
assertCharSequence("\x14", "ctrl-t")
|
||||
assertCharSequence("\x15", "ctrl-u")
|
||||
assertCharSequence("\x16", "ctrl-v")
|
||||
assertCharSequence("\x17", "ctrl-w")
|
||||
assertCharSequence("\x18", "ctrl-x")
|
||||
assertCharSequence("\x19", "ctrl-y")
|
||||
assertCharSequence("\x1a", "ctrl-z")
|
||||
|
||||
assertCharSequence("\x00", "ctrl-space")
|
||||
assertCharSequence("\x1c", "ctrl-\\")
|
||||
assertCharSequence("\x1d", "ctrl-]")
|
||||
assertCharSequence("\x1e", "ctrl-^")
|
||||
assertCharSequence("\x1f", "ctrl-/")
|
||||
|
||||
assertEscSequence("\x1ba", "alt-a")
|
||||
assertEscSequence("\x1bb", "alt-b")
|
||||
assertEscSequence("\x1bc", "alt-c")
|
||||
assertEscSequence("\x1bd", "alt-d")
|
||||
assertEscSequence("\x1be", "alt-e")
|
||||
assertEscSequence("\x1bf", "alt-f")
|
||||
assertEscSequence("\x1bg", "alt-g")
|
||||
assertEscSequence("\x1bh", "alt-h")
|
||||
assertEscSequence("\x1bi", "alt-i")
|
||||
assertEscSequence("\x1bj", "alt-j")
|
||||
assertEscSequence("\x1bk", "alt-k")
|
||||
assertEscSequence("\x1bl", "alt-l")
|
||||
assertEscSequence("\x1bm", "alt-m")
|
||||
assertEscSequence("\x1bn", "alt-n")
|
||||
assertEscSequence("\x1bo", "alt-o")
|
||||
assertEscSequence("\x1bp", "alt-p")
|
||||
assertEscSequence("\x1bq", "alt-q")
|
||||
assertEscSequence("\x1br", "alt-r")
|
||||
assertEscSequence("\x1bs", "alt-s")
|
||||
assertEscSequence("\x1bt", "alt-t")
|
||||
assertEscSequence("\x1bu", "alt-u")
|
||||
assertEscSequence("\x1bv", "alt-v")
|
||||
assertEscSequence("\x1bw", "alt-w")
|
||||
assertEscSequence("\x1bx", "alt-x")
|
||||
assertEscSequence("\x1by", "alt-y")
|
||||
assertEscSequence("\x1bz", "alt-z")
|
||||
|
||||
assertEscSequence("\x1bOP", "f1")
|
||||
assertEscSequence("\x1bOQ", "f2")
|
||||
assertEscSequence("\x1bOR", "f3")
|
||||
assertEscSequence("\x1bOS", "f4")
|
||||
assertEscSequence("\x1b[15~", "f5")
|
||||
assertEscSequence("\x1b[17~", "f6")
|
||||
assertEscSequence("\x1b[18~", "f7")
|
||||
assertEscSequence("\x1b[19~", "f8")
|
||||
assertEscSequence("\x1b[20~", "f9")
|
||||
assertEscSequence("\x1b[21~", "f10")
|
||||
assertEscSequence("\x1b[23~", "f11")
|
||||
assertEscSequence("\x1b[24~", "f12")
|
||||
|
||||
assertEscSequence("\x1b", "esc")
|
||||
assertCharSequence("\t", "tab")
|
||||
assertEscSequence("\x1b[Z", "shift-tab")
|
||||
|
||||
assertCharSequence("\x7f", "backspace")
|
||||
assertEscSequence("\x1b\x7f", "alt-backspace")
|
||||
assertCharSequence("\b", "ctrl-backspace")
|
||||
assertEscSequence("\x1b\b", "ctrl-alt-backspace")
|
||||
|
||||
assertEscSequence("\x1b[A", "up")
|
||||
assertEscSequence("\x1b[B", "down")
|
||||
assertEscSequence("\x1b[C", "right")
|
||||
assertEscSequence("\x1b[D", "left")
|
||||
assertEscSequence("\x1b[H", "home")
|
||||
assertEscSequence("\x1b[F", "end")
|
||||
assertEscSequence("\x1b[2~", "insert")
|
||||
assertEscSequence("\x1b[3~", "delete")
|
||||
assertEscSequence("\x1b[5~", "page-up")
|
||||
assertEscSequence("\x1b[6~", "page-down")
|
||||
assertEscSequence("\x1b[7~", "home")
|
||||
assertEscSequence("\x1b[8~", "end")
|
||||
|
||||
assertEscSequence("\x1b[1;2A", "shift-up")
|
||||
assertEscSequence("\x1b[1;2B", "shift-down")
|
||||
assertEscSequence("\x1b[1;2C", "shift-right")
|
||||
assertEscSequence("\x1b[1;2D", "shift-left")
|
||||
assertEscSequence("\x1b[1;2H", "shift-home")
|
||||
assertEscSequence("\x1b[1;2F", "shift-end")
|
||||
assertEscSequence("\x1b[3;2~", "shift-delete")
|
||||
assertEscSequence("\x1b[5;2~", "shift-page-up")
|
||||
assertEscSequence("\x1b[6;2~", "shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b\x1b", "esc")
|
||||
assertEscSequence("\x1b\x1b[A", "alt-up")
|
||||
assertEscSequence("\x1b\x1b[B", "alt-down")
|
||||
assertEscSequence("\x1b\x1b[C", "alt-right")
|
||||
assertEscSequence("\x1b\x1b[D", "alt-left")
|
||||
|
||||
assertEscSequence("\x1b[1;3A", "alt-up")
|
||||
assertEscSequence("\x1b[1;3B", "alt-down")
|
||||
assertEscSequence("\x1b[1;3C", "alt-right")
|
||||
assertEscSequence("\x1b[1;3D", "alt-left")
|
||||
assertEscSequence("\x1b[1;3H", "alt-home")
|
||||
assertEscSequence("\x1b[1;3F", "alt-end")
|
||||
assertEscSequence("\x1b[3;3~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;3~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;3~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;4A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;4B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;4C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;4D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;4H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;4F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;4~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;4~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;4~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;5A", "ctrl-up")
|
||||
assertEscSequence("\x1b[1;5B", "ctrl-down")
|
||||
assertEscSequence("\x1b[1;5C", "ctrl-right")
|
||||
assertEscSequence("\x1b[1;5D", "ctrl-left")
|
||||
assertEscSequence("\x1b[1;5H", "ctrl-home")
|
||||
assertEscSequence("\x1b[1;5F", "ctrl-end")
|
||||
assertEscSequence("\x1b[3;5~", "ctrl-delete")
|
||||
assertEscSequence("\x1b[5;5~", "ctrl-page-up")
|
||||
assertEscSequence("\x1b[6;5~", "ctrl-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;7A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;7B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;7C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;7D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;7H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;7F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;7~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;7~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;7~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;6A", "ctrl-shift-up")
|
||||
assertEscSequence("\x1b[1;6B", "ctrl-shift-down")
|
||||
assertEscSequence("\x1b[1;6C", "ctrl-shift-right")
|
||||
assertEscSequence("\x1b[1;6D", "ctrl-shift-left")
|
||||
assertEscSequence("\x1b[1;6H", "ctrl-shift-home")
|
||||
assertEscSequence("\x1b[1;6F", "ctrl-shift-end")
|
||||
assertEscSequence("\x1b[3;6~", "ctrl-shift-delete")
|
||||
assertEscSequence("\x1b[5;6~", "ctrl-shift-page-up")
|
||||
assertEscSequence("\x1b[6;6~", "ctrl-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;8A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;8B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;8C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;8D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;8H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;8F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;8~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;8~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;8~", "ctrl-alt-shift-page-down")
|
||||
|
||||
// xterm meta & mac
|
||||
assertEscSequence("\x1b[1;9A", "alt-up")
|
||||
assertEscSequence("\x1b[1;9B", "alt-down")
|
||||
assertEscSequence("\x1b[1;9C", "alt-right")
|
||||
assertEscSequence("\x1b[1;9D", "alt-left")
|
||||
assertEscSequence("\x1b[1;9H", "alt-home")
|
||||
assertEscSequence("\x1b[1;9F", "alt-end")
|
||||
assertEscSequence("\x1b[3;9~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;9~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;9~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;10A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;10B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;10C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;10D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;10H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;10F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;10~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;10~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;10~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;11A", "alt-up")
|
||||
assertEscSequence("\x1b[1;11B", "alt-down")
|
||||
assertEscSequence("\x1b[1;11C", "alt-right")
|
||||
assertEscSequence("\x1b[1;11D", "alt-left")
|
||||
assertEscSequence("\x1b[1;11H", "alt-home")
|
||||
assertEscSequence("\x1b[1;11F", "alt-end")
|
||||
assertEscSequence("\x1b[3;11~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;11~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;11~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;12A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;12B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;12C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;12D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;12H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;12F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;12~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;12~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;12~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;13A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;13B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;13C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;13D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;13H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;13F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;13~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;13~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;13~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;14A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;14B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;14C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;14D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;14H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;14F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;14~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;14~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;14~", "ctrl-alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;15A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;15B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;15C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;15D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;15H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;15F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;15~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;15~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;15~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;16A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;16B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;16C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;16D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;16H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;16F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;16~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;16~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;16~", "ctrl-alt-shift-page-down")
|
||||
|
||||
// tmux & emacs
|
||||
assertEscSequence("\x1bOA", "up")
|
||||
assertEscSequence("\x1bOB", "down")
|
||||
assertEscSequence("\x1bOC", "right")
|
||||
assertEscSequence("\x1bOD", "left")
|
||||
assertEscSequence("\x1bOH", "home")
|
||||
assertEscSequence("\x1bOF", "end")
|
||||
|
||||
// rrvt
|
||||
assertEscSequence("\x1b[1~", "home")
|
||||
assertEscSequence("\x1b[4~", "end")
|
||||
assertEscSequence("\x1b[11~", "f1")
|
||||
assertEscSequence("\x1b[12~", "f2")
|
||||
assertEscSequence("\x1b[13~", "f3")
|
||||
assertEscSequence("\x1b[14~", "f4")
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func IsLightRendererSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||
func (r *LightRenderer) DefaultTheme() *ColorTheme {
|
||||
if strings.Contains(os.Getenv("TERM"), "256") {
|
||||
return Dark256
|
||||
}
|
||||
@@ -42,26 +42,35 @@ func (r *LightRenderer) closePlatform() {
|
||||
r.ttyout.Close()
|
||||
}
|
||||
|
||||
func openTty(mode int) (*os.File, error) {
|
||||
in, err := os.OpenFile(consoleDevice, mode, 0)
|
||||
if err != nil {
|
||||
func openTty(ttyDefault string, mode int) (*os.File, error) {
|
||||
var in *os.File
|
||||
var err error
|
||||
if len(ttyDefault) > 0 {
|
||||
in, err = os.OpenFile(ttyDefault, mode, 0)
|
||||
}
|
||||
if in == nil || err != nil || ttyDefault != DefaultTtyDevice && !util.IsTty(in) {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to open " + consoleDevice)
|
||||
if ttyDefault != DefaultTtyDevice {
|
||||
if in, err = os.OpenFile(DefaultTtyDevice, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to open " + DefaultTtyDevice)
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
return openTty(syscall.O_RDONLY)
|
||||
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||
return openTty(ttyDefault, syscall.O_RDONLY)
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
return openTty(syscall.O_WRONLY)
|
||||
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||
return openTty(ttyDefault, syscall.O_WRONLY)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
@@ -89,8 +98,8 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
r.flush()
|
||||
var err error
|
||||
bytes := []byte{}
|
||||
for tries := 0; tries < offsetPollTries; tries++ {
|
||||
bytes, err = r.getBytesInternal(bytes, tries > 0)
|
||||
for tries := range offsetPollTries {
|
||||
bytes, _, err = r.getBytesInternal(false, bytes, tries > 0)
|
||||
if err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
@@ -105,15 +114,62 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
b := make([]byte, 1)
|
||||
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
|
||||
fd := r.fd()
|
||||
util.SetNonblock(r.ttyin, nonblock)
|
||||
_, err := util.Read(fd, b)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
getter := func() (int, getCharResult) {
|
||||
b := make([]byte, 1)
|
||||
util.SetNonblock(r.ttyin, nonblock)
|
||||
_, err := util.Read(fd, b)
|
||||
if err != nil {
|
||||
return 0, getCharError
|
||||
}
|
||||
return int(b[0]), getCharSuccess
|
||||
}
|
||||
return int(b[0]), true
|
||||
if nonblock || !cancellable {
|
||||
return getter()
|
||||
}
|
||||
|
||||
rpipe, wpipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
// Fallback to blocking read without cancellation
|
||||
return getter()
|
||||
}
|
||||
r.setCancel(func() {
|
||||
wpipe.Write([]byte{0})
|
||||
})
|
||||
defer func() {
|
||||
r.setCancel(nil)
|
||||
rpipe.Close()
|
||||
wpipe.Close()
|
||||
}()
|
||||
|
||||
cancelFd := int(rpipe.Fd())
|
||||
for range maxSelectTries {
|
||||
var rfds unix.FdSet
|
||||
limit := len(rfds.Bits) * unix.NFDBITS
|
||||
if fd >= limit || cancelFd >= limit {
|
||||
return getter()
|
||||
}
|
||||
|
||||
rfds.Set(fd)
|
||||
rfds.Set(cancelFd)
|
||||
_, err := unix.Select(max(fd, cancelFd)+1, &rfds, nil, nil, nil)
|
||||
if err != nil {
|
||||
if err == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
return 0, getCharError
|
||||
}
|
||||
|
||||
if rfds.IsSet(cancelFd) {
|
||||
return 0, getCharCancelled
|
||||
}
|
||||
|
||||
if rfds.IsSet(fd) {
|
||||
return getter()
|
||||
}
|
||||
}
|
||||
return 0, getCharError
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Size() TermSize {
|
||||
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
var (
|
||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
||||
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
counter = uint64(0)
|
||||
)
|
||||
|
||||
// IsLightRendererSupported checks to see if the Light renderer is supported
|
||||
@@ -39,7 +40,7 @@ func IsLightRendererSupported() bool {
|
||||
return canSetVt100
|
||||
}
|
||||
|
||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||
func (r *LightRenderer) DefaultTheme() *ColorTheme {
|
||||
// the getenv check is borrowed from here: https://github.com/gdamore/tcell/commit/0c473b86d82f68226a142e96cc5a34c5a29b3690#diff-b008fcd5e6934bf31bc3d33bf49f47d8R178:
|
||||
if !IsLightRendererSupported() || os.Getenv("ConEmuPID") != "" || os.Getenv("TCELL_TRUECOLOR") == "disable" {
|
||||
return Default16
|
||||
@@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
|
||||
}
|
||||
r.inHandle = uintptr(inHandle)
|
||||
|
||||
r.setupTerminal()
|
||||
|
||||
// channel for non-blocking reads. Buffer to make sure
|
||||
// we get the ESC sets:
|
||||
r.ttyinChannel = make(chan byte, 1024)
|
||||
|
||||
// the following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for !r.closed.Get() {
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
_, err := util.Read(fd, b)
|
||||
if err == nil {
|
||||
r.ttyinChannel <- b[0]
|
||||
}
|
||||
}
|
||||
}()
|
||||
r.setupTerminal()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -91,27 +76,51 @@ func (r *LightRenderer) closePlatform() {
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||
// not used
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||
return os.Stderr, nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
// The following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
current := counter
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
if _, err := util.Read(fd, b); err == nil {
|
||||
r.mutex.Lock()
|
||||
// This condition prevents the goroutine from running after the renderer
|
||||
// has been closed or paused.
|
||||
if current != counter {
|
||||
r.mutex.Unlock()
|
||||
break
|
||||
}
|
||||
r.ttyinChannel <- b[0]
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) restoreTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
func (r *LightRenderer) restoreTerminal() {
|
||||
r.mutex.Lock()
|
||||
counter++
|
||||
// We're setting ENABLE_VIRTUAL_TERMINAL_INPUT to allow escape sequences to be read during 'execute'.
|
||||
// e.g. fzf --bind 'enter:execute:less {}'
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput|windows.ENABLE_VIRTUAL_TERMINAL_INPUT)
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Size() TermSize {
|
||||
@@ -142,16 +151,33 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
if nonblock {
|
||||
select {
|
||||
case bc := <-r.ttyinChannel:
|
||||
return int(bc), true
|
||||
case <-time.After(timeoutInterval * time.Millisecond):
|
||||
return 0, false
|
||||
}
|
||||
} else {
|
||||
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
|
||||
if !nonblock && !cancellable {
|
||||
bc := <-r.ttyinChannel
|
||||
return int(bc), true
|
||||
return int(bc), getCharSuccess
|
||||
}
|
||||
|
||||
var timeout <-chan time.Time
|
||||
if nonblock {
|
||||
timeout = time.After(timeoutInterval * time.Millisecond)
|
||||
}
|
||||
|
||||
var cancel chan struct{}
|
||||
if cancellable {
|
||||
cancel = make(chan struct{})
|
||||
r.setCancel(func() {
|
||||
close(cancel)
|
||||
})
|
||||
defer r.setCancel(nil)
|
||||
}
|
||||
|
||||
select {
|
||||
case bc := <-r.ttyinChannel:
|
||||
return int(bc), getCharSuccess
|
||||
case <-cancel:
|
||||
return 0, getCharCancelled
|
||||
case <-timeout:
|
||||
// NOTE: not really an error
|
||||
return 0, getCharError
|
||||
}
|
||||
}
|
||||
|
||||
464
src/tui/tcell.go
464
src/tui/tcell.go
@@ -5,6 +5,7 @@ package tui
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@@ -36,22 +37,24 @@ func (p ColorPair) style() tcell.Style {
|
||||
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
|
||||
}
|
||||
|
||||
type Attr int32
|
||||
|
||||
type TcellWindow struct {
|
||||
color bool
|
||||
preview bool
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
normal ColorPair
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
uri *string
|
||||
params *string
|
||||
color bool
|
||||
windowType WindowType
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
normal ColorPair
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
uri *string
|
||||
params *string
|
||||
showCursor bool
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
tabstop int
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -72,7 +75,9 @@ func (w *TcellWindow) Height() int {
|
||||
|
||||
func (w *TcellWindow) Refresh() {
|
||||
if w.moveCursor {
|
||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||
if w.showCursor {
|
||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||
}
|
||||
w.moveCursor = false
|
||||
}
|
||||
w.lastX = 0
|
||||
@@ -93,11 +98,17 @@ const (
|
||||
Italic = Attr(tcell.AttrItalic)
|
||||
)
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 7)
|
||||
AttrClear = Attr(1 << 8)
|
||||
)
|
||||
func (r *FullscreenRenderer) Bell() {
|
||||
_screen.Beep()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) HideCursor() {
|
||||
r.showCursor = false
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) ShowCursor() {
|
||||
r.showCursor = true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||
// No-op
|
||||
@@ -106,8 +117,12 @@ func (r *FullscreenRenderer) PassThrough(str string) {
|
||||
|
||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||
|
||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
||||
if _screen.Colors() >= 256 {
|
||||
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme {
|
||||
s, e := r.getScreen()
|
||||
if e != nil {
|
||||
return Default16
|
||||
}
|
||||
if s.Colors() >= 256 {
|
||||
return Dark256
|
||||
}
|
||||
return Default16
|
||||
@@ -136,10 +151,6 @@ func (c Color) Style() tcell.Color {
|
||||
}
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
// handle the following as private members of FullscreenRenderer instance
|
||||
// they are declared here to prevent introducing tcell library in non-windows builds
|
||||
var (
|
||||
@@ -148,20 +159,34 @@ var (
|
||||
_initialResize bool = true
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
|
||||
if _screen == nil {
|
||||
s, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if !r.showCursor {
|
||||
s.HideCursor()
|
||||
}
|
||||
_screen = s
|
||||
}
|
||||
return _screen, nil
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) initScreen() error {
|
||||
s, e := tcell.NewScreen()
|
||||
s, e := r.getScreen()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if e = s.Init(); e != nil {
|
||||
return e
|
||||
}
|
||||
s.EnablePaste()
|
||||
if r.mouse {
|
||||
s.EnableMouse()
|
||||
} else {
|
||||
s.DisableMouse()
|
||||
}
|
||||
_screen = s
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -174,7 +199,6 @@ func (r *FullscreenRenderer) Init() error {
|
||||
if err := r.initScreen(); err != nil {
|
||||
return err
|
||||
}
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -224,9 +248,14 @@ func (r *FullscreenRenderer) Size() TermSize {
|
||||
return TermSize{lines, cols, 0, 0}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event {
|
||||
func (r *FullscreenRenderer) GetChar(cancellable bool) Event {
|
||||
ev := _screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventPaste:
|
||||
if ev.Start() {
|
||||
return Event{BracketedPasteBegin, 0, nil}
|
||||
}
|
||||
return Event{BracketedPasteEnd, 0, nil}
|
||||
case *tcell.EventResize:
|
||||
// Ignore the first resize event
|
||||
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
|
||||
@@ -243,7 +272,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
||||
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||
x, y := ev.Position()
|
||||
mod := ev.Modifiers() != 0
|
||||
|
||||
mod := ev.Modifiers()
|
||||
ctrl := (mod & tcell.ModCtrl) > 0
|
||||
alt := (mod & tcell.ModAlt) > 0
|
||||
shift := (mod & tcell.ModShift) > 0
|
||||
|
||||
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||
@@ -252,9 +285,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
switch {
|
||||
case button&tcell.WheelDown != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, ctrl, alt, shift}}
|
||||
case button&tcell.WheelUp != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, ctrl, alt, shift}}
|
||||
case button&tcell.Button1 != 0:
|
||||
double := false
|
||||
if !drag {
|
||||
@@ -277,9 +310,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
}
|
||||
// fire single or double click event
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, ctrl, alt, shift}}
|
||||
case button&tcell.Button2 != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, ctrl, alt, shift}}
|
||||
default:
|
||||
// double and single taps on Windows don't quite work due to
|
||||
// the console acting on the events and not allowing us
|
||||
@@ -288,7 +321,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
down := left || button&tcell.Button3 != 0
|
||||
double := false
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
// No need to report mouse movement events when no button is pressed
|
||||
if drag {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
|
||||
}
|
||||
|
||||
// process keyboard:
|
||||
@@ -300,6 +337,8 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
shift := (mods & tcell.ModShift) > 0
|
||||
ctrlAlt := ctrl && alt
|
||||
altShift := alt && shift
|
||||
ctrlShift := ctrl && shift
|
||||
ctrlAltShift := ctrl && alt && shift
|
||||
|
||||
keyfn := func(r rune) Event {
|
||||
if alt {
|
||||
@@ -326,8 +365,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyCtrlH:
|
||||
switch ev.Rune() {
|
||||
case 0:
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltBackspace, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{Backspace, 0, nil}
|
||||
return Event{CtrlBackspace, 0, nil}
|
||||
}
|
||||
case rune(tcell.KeyCtrlH):
|
||||
switch {
|
||||
@@ -388,6 +430,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{CtrlSlash, 0, nil}
|
||||
// section 3: (Alt)+Backspace2
|
||||
case tcell.KeyBackspace2:
|
||||
if ctrl {
|
||||
return Event{CtrlBackspace, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltBackspace, 0, nil}
|
||||
}
|
||||
@@ -395,9 +440,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
case tcell.KeyUp:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftUp, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltUp, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftUp, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftUp, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlUp, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftUp, 0, nil}
|
||||
}
|
||||
@@ -406,9 +463,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
return Event{Up, 0, nil}
|
||||
case tcell.KeyDown:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftDown, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltDown, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftDown, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftDown, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlDown, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftDown, 0, nil}
|
||||
}
|
||||
@@ -417,9 +486,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
return Event{Down, 0, nil}
|
||||
case tcell.KeyLeft:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftLeft, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltLeft, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftLeft, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftLeft, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlLeft, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftLeft, 0, nil}
|
||||
}
|
||||
@@ -428,9 +509,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
return Event{Left, 0, nil}
|
||||
case tcell.KeyRight:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftRight, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltRight, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftRight, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftRight, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlRight, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftRight, 0, nil}
|
||||
}
|
||||
@@ -443,20 +536,119 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyInsert:
|
||||
return Event{Insert, 0, nil}
|
||||
case tcell.KeyHome:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftHome, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltHome, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftHome, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftHome, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlHome, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftHome, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltHome, 0, nil}
|
||||
}
|
||||
return Event{Home, 0, nil}
|
||||
case tcell.KeyDelete:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftDelete, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltDelete, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftDelete, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftDelete, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltDelete, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftDelete, 0, nil}
|
||||
}
|
||||
return Event{Delete, 0, nil}
|
||||
case tcell.KeyEnd:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftEnd, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltEnd, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftEnd, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftEnd, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlEnd, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftEnd, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltEnd, 0, nil}
|
||||
}
|
||||
return Event{End, 0, nil}
|
||||
case tcell.KeyPgUp:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftPageUp, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltPageUp, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftPageUp, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftPageUp, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlPageUp, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftPageUp, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltPageUp, 0, nil}
|
||||
}
|
||||
return Event{PageUp, 0, nil}
|
||||
case tcell.KeyPgDn:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftPageDown, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltPageDown, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftPageDown, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftPageDown, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlPageDown, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftPageDown, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltPageDown, 0, nil}
|
||||
}
|
||||
return Event{PageDown, 0, nil}
|
||||
case tcell.KeyBacktab:
|
||||
return Event{ShiftTab, 0, nil}
|
||||
@@ -513,20 +705,25 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) CancelGetChar() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||
if clear {
|
||||
_screen.Fini()
|
||||
_screen.Suspend()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
||||
if clear {
|
||||
r.initScreen()
|
||||
_screen.Resume()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
_screen.Fini()
|
||||
_screen = nil
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
@@ -537,28 +734,37 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
_screen.Show()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||
normal := ColNormal
|
||||
if preview {
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
||||
width = max(0, width)
|
||||
height = max(0, height)
|
||||
normal := ColBorder
|
||||
switch windowType {
|
||||
case WindowList:
|
||||
normal = ColNormal
|
||||
case WindowHeader:
|
||||
normal = ColHeader
|
||||
case WindowFooter:
|
||||
normal = ColFooter
|
||||
case WindowInput:
|
||||
normal = ColInput
|
||||
case WindowPreview:
|
||||
normal = ColPreview
|
||||
}
|
||||
w := &TcellWindow{
|
||||
color: r.theme.Colored,
|
||||
preview: preview,
|
||||
windowType: windowType,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle}
|
||||
borderStyle: borderStyle,
|
||||
showCursor: r.showCursor,
|
||||
tabstop: r.tabstop}
|
||||
w.Erase()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Close() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||
for ly := 0; ly <= h; ly++ {
|
||||
for lx := 0; lx <= w; lx++ {
|
||||
@@ -568,11 +774,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Erase() {
|
||||
if w.borderStyle.shape.HasLeft() {
|
||||
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
|
||||
} else {
|
||||
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
|
||||
}
|
||||
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
|
||||
w.drawBorder(false)
|
||||
}
|
||||
|
||||
@@ -581,9 +783,21 @@ func (w *TcellWindow) EraseMaybe() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *TcellWindow) SetWrapSign(sign string, width int) {
|
||||
w.wrapSign = sign
|
||||
w.wrapSignWidth = width
|
||||
}
|
||||
|
||||
func (w *TcellWindow) EncloseX(x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) EncloseY(y int) bool {
|
||||
return y >= w.top && y < (w.top+w.height)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
return w.EncloseX(x) && w.EncloseY(y)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Move(y int, x int) {
|
||||
@@ -614,6 +828,21 @@ func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
|
||||
return style
|
||||
}
|
||||
|
||||
func underlineStyleFromAttr(a Attr) tcell.UnderlineStyle {
|
||||
switch a.UnderlineStyle() {
|
||||
case UlStyleDouble:
|
||||
return tcell.UnderlineStyleDouble
|
||||
case UlStyleCurly:
|
||||
return tcell.UnderlineStyleCurly
|
||||
case UlStyleDotted:
|
||||
return tcell.UnderlineStyleDotted
|
||||
case UlStyleDashed:
|
||||
return tcell.UnderlineStyleDashed
|
||||
default:
|
||||
return tcell.UnderlineStyleSolid
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
lx := 0
|
||||
a := pair.Attr()
|
||||
@@ -622,11 +851,18 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
if a&AttrClear == 0 {
|
||||
style = style.
|
||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0).
|
||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||
if a&Attr(tcell.AttrUnderline) != 0 {
|
||||
style = style.Underline(underlineStyleFromAttr(a))
|
||||
if pair.Ul() != colDefault {
|
||||
style = style.Underline(asTcellColor(pair.Ul()))
|
||||
}
|
||||
} else {
|
||||
style = style.Underline(false)
|
||||
}
|
||||
}
|
||||
style = w.withUrl(style)
|
||||
|
||||
@@ -661,10 +897,8 @@ func (w *TcellWindow) CPrint(pair ColorPair, text string) {
|
||||
w.printString(text, pair)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
lx := 0
|
||||
func (w *TcellWindow) pairStyle(pair ColorPair) tcell.Style {
|
||||
a := pair.Attr()
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
style = pair.style()
|
||||
@@ -673,52 +907,76 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
}
|
||||
style = style.
|
||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||
Bold(a&Attr(tcell.AttrBold) != 0).
|
||||
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
|
||||
Dim(a&Attr(tcell.AttrDim) != 0).
|
||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||
style = w.withUrl(style)
|
||||
if a&Attr(tcell.AttrUnderline) != 0 {
|
||||
style = style.Underline(underlineStyleFromAttr(a))
|
||||
if pair.Ul() != colDefault {
|
||||
style = style.Underline(asTcellColor(pair.Ul()))
|
||||
}
|
||||
} else {
|
||||
style = style.Underline(false)
|
||||
}
|
||||
return w.withUrl(style)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) renderGraphemes(text string, style tcell.Style) {
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
Loop:
|
||||
for gr.Next() {
|
||||
st := style
|
||||
rs := gr.Runes()
|
||||
if len(rs) == 1 {
|
||||
r := rs[0]
|
||||
switch r {
|
||||
case '\r':
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
case '\n':
|
||||
if len(rs) == 1 && rs[0] == '\r' {
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
}
|
||||
|
||||
xPos := w.left + w.lastX
|
||||
yPos := w.top + w.lastY
|
||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
}
|
||||
w.lastX += util.StringWidth(string(rs))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) renderWrapSign(style tcell.Style) {
|
||||
sign := w.wrapSign
|
||||
if w.wrapSignWidth > w.width {
|
||||
runes, _ := util.Truncate(sign, w.width)
|
||||
sign = string(runes)
|
||||
}
|
||||
gr := uniseg.NewGraphemes(sign)
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
_screen.SetContent(w.left+w.lastX, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||
w.lastX += uniseg.StringWidth(string(rs))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
style := w.pairStyle(pair)
|
||||
|
||||
for i, segment := range strings.Split(text, "\n") {
|
||||
for j, wl := range WrapLine(segment, w.lastX, w.width, w.tabstop, w.wrapSignWidth) {
|
||||
if i > 0 || j > 0 {
|
||||
w.lastY++
|
||||
if w.lastY >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
continue Loop
|
||||
if j > 0 {
|
||||
w.renderWrapSign(style)
|
||||
}
|
||||
}
|
||||
if w.lastX < w.width {
|
||||
w.renderGraphemes(wl.Text, style)
|
||||
}
|
||||
}
|
||||
|
||||
// word wrap:
|
||||
xPos := w.left + w.lastX + lx
|
||||
if xPos >= (w.left + w.width) {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
xPos = w.left
|
||||
}
|
||||
|
||||
yPos := w.top + w.lastY
|
||||
if yPos >= (w.top + w.height) {
|
||||
return FillSuspend
|
||||
}
|
||||
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
lx += util.StringWidth(string(rs))
|
||||
}
|
||||
w.lastX += lx
|
||||
if w.lastX == w.width {
|
||||
if w.lastX >= w.width {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
return FillNextLine
|
||||
@@ -741,14 +999,14 @@ func (w *TcellWindow) LinkEnd() {
|
||||
w.params = nil
|
||||
}
|
||||
|
||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
func (w *TcellWindow) CFill(fg Color, bg Color, ul Color, a Attr, str string) FillReturn {
|
||||
if fg == colDefault {
|
||||
fg = w.normal.Fg()
|
||||
}
|
||||
if bg == colDefault {
|
||||
bg = w.normal.Bg()
|
||||
}
|
||||
return w.fillString(str, NewColorPair(fg, bg, a))
|
||||
return w.fillString(str, NewColorPair(fg, bg, a).WithUl(ul))
|
||||
}
|
||||
|
||||
func (w *TcellWindow) DrawBorder() {
|
||||
@@ -760,6 +1018,9 @@ func (w *TcellWindow) DrawHBorder() {
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
if w.height == 0 {
|
||||
return
|
||||
}
|
||||
shape := w.borderStyle.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
@@ -772,10 +1033,19 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
if w.preview {
|
||||
style = ColPreviewBorder.style()
|
||||
} else {
|
||||
switch w.windowType {
|
||||
case WindowBase:
|
||||
style = ColBorder.style()
|
||||
case WindowList:
|
||||
style = ColListBorder.style()
|
||||
case WindowHeader:
|
||||
style = ColHeaderBorder.style()
|
||||
case WindowFooter:
|
||||
style = ColFooterBorder.style()
|
||||
case WindowInput:
|
||||
style = ColInputBorder.style()
|
||||
case WindowPreview:
|
||||
style = ColPreviewBorder.style()
|
||||
}
|
||||
} else {
|
||||
style = w.normal.style()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
|
||||
func assert(t *testing.T, context string, got any, want any) bool {
|
||||
if got == want {
|
||||
return true
|
||||
} else {
|
||||
@@ -82,9 +82,9 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
|
||||
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
||||
// KeyEnter is alias for KeyCR
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
// Ctrl+Alt keys
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
@@ -107,18 +107,20 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
|
||||
@@ -126,9 +128,41 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl}, wantKey{CtrlUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl}, wantKey{CtrlDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl}, wantKey{CtrlRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl}, wantKey{CtrlLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModShift}, wantKey{ShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModShift}, wantKey{ShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift}, wantKey{ShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModAlt}, wantKey{AltUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModAlt}, wantKey{AltRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModAlt}, wantKey{AltLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
@@ -137,6 +171,46 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
|
||||
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModNone}, wantKey{Home, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModNone}, wantKey{End, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModNone}, wantKey{PageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModNone}, wantKey{PageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl}, wantKey{CtrlHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl}, wantKey{CtrlEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl}, wantKey{CtrlDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl}, wantKey{CtrlPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl}, wantKey{CtrlPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModShift}, wantKey{ShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModShift}, wantKey{ShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModShift}, wantKey{ShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift}, wantKey{ShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift}, wantKey{ShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModAlt}, wantKey{AltHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModAlt}, wantKey{AltEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModAlt}, wantKey{AltPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModAlt}, wantKey{AltPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageDown, 0, nil}},
|
||||
// section 6: (Ctrl+Alt)+'rune'
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||
@@ -179,7 +253,7 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
||||
|
||||
}
|
||||
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
|
||||
r := NewFullscreenRenderer(&ColorTheme{}, false, false, 8)
|
||||
r.Init()
|
||||
|
||||
// run and evaluate the tests
|
||||
@@ -191,18 +265,22 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
|
||||
|
||||
// process the event in fzf and evaluate the test
|
||||
gotEvent := r.GetChar()
|
||||
gotEvent := r.GetChar(true)
|
||||
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
||||
t.Logf("Resize as Invalid swallowed")
|
||||
initialResizeAsInvalid = false
|
||||
gotEvent = r.GetChar()
|
||||
gotEvent = r.GetChar(true)
|
||||
}
|
||||
if gotEvent.Type == Resize {
|
||||
t.Logf("Resize swallowed")
|
||||
gotEvent = r.GetChar(true)
|
||||
}
|
||||
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
||||
|
||||
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
|
||||
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
|
||||
assert(t, "r.GetChar(true).Type", gotEvent.Type, test.wantKey.Type)
|
||||
assert(t, "r.GetChar(true).Char", gotEvent.Char, test.wantKey.Char)
|
||||
}
|
||||
|
||||
r.Close()
|
||||
@@ -233,7 +311,7 @@ Quick reference
|
||||
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter Enter
|
||||
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||
|
||||
@@ -44,11 +44,11 @@ func ttyname() string {
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to read user input
|
||||
func TtyIn() (*os.File, error) {
|
||||
return openTtyIn()
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return openTtyIn(ttyDefault)
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to write to
|
||||
func TtyOut() (*os.File, error) {
|
||||
return openTtyOut()
|
||||
// TtyOut returns terminal device to write to
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return openTtyOut(ttyDefault)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ func ttyname() string {
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns os.Stdin
|
||||
func TtyIn() (*os.File, error) {
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return os.Stdin, nil
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns nil
|
||||
func TtyOut() (*os.File, error) {
|
||||
// TtyOut on Windows returns nil
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
873
src/tui/tui.go
873
src/tui/tui.go
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,46 @@ package tui
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWrapLine(t *testing.T) {
|
||||
// Basic wrapping
|
||||
lines := WrapLine("hello world", 0, 7, 8, 2)
|
||||
if len(lines) != 2 || lines[0].Text != "hello w" || lines[1].Text != "orld" {
|
||||
t.Errorf("Basic wrap: %v", lines)
|
||||
}
|
||||
|
||||
// Exact fit — no wrapping needed
|
||||
lines = WrapLine("hello", 0, 5, 8, 2)
|
||||
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
|
||||
t.Errorf("Exact fit: %v", lines)
|
||||
}
|
||||
|
||||
// With prefix length
|
||||
lines = WrapLine("hello", 3, 5, 8, 2)
|
||||
if len(lines) != 2 || lines[0].Text != "he" || lines[1].Text != "llo" {
|
||||
t.Errorf("Prefix length: %v", lines)
|
||||
}
|
||||
|
||||
// Empty string
|
||||
lines = WrapLine("", 0, 10, 8, 2)
|
||||
if len(lines) != 1 || lines[0].Text != "" || lines[0].DisplayWidth != 0 {
|
||||
t.Errorf("Empty string: %v", lines)
|
||||
}
|
||||
|
||||
// Continuation lines account for wrapSignWidth
|
||||
lines = WrapLine("abcdefghij", 0, 5, 8, 2)
|
||||
// First line: "abcde" (5 chars fit in width 5)
|
||||
// Continuation max: 5-2=3, so "fgh" then "ij"
|
||||
if len(lines) != 3 || lines[0].Text != "abcde" || lines[1].Text != "fgh" || lines[2].Text != "ij" {
|
||||
t.Errorf("Continuation: %v", lines)
|
||||
}
|
||||
|
||||
// Tab expansion
|
||||
lines = WrapLine("\there", 0, 10, 4, 2)
|
||||
if len(lines) != 1 || lines[0].DisplayWidth != 8 {
|
||||
t.Errorf("Tab: %v", lines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexToColor(t *testing.T) {
|
||||
assert := func(expr string, r, g, b int) {
|
||||
color := HexToColor(expr)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
func TestAtExit(t *testing.T) {
|
||||
want := []int{3, 2, 1, 0}
|
||||
var called []int
|
||||
for i := 0; i < 4; i++ {
|
||||
for i := range 4 {
|
||||
n := i
|
||||
AtExit(func() { called = append(called, n) })
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func ToChars(bytes []byte) Chars {
|
||||
}
|
||||
|
||||
runes := make([]rune, bytesUntil, len(bytes))
|
||||
for i := 0; i < bytesUntil; i++ {
|
||||
for i := range bytesUntil {
|
||||
runes[i] = rune(bytes[i])
|
||||
}
|
||||
for i := bytesUntil; i < len(bytes); {
|
||||
@@ -184,9 +184,31 @@ func (chars *Chars) TrailingWhitespaces() int {
|
||||
return whitespaces
|
||||
}
|
||||
|
||||
func (chars *Chars) TrimTrailingWhitespaces() {
|
||||
func (chars *Chars) TrimTrailingWhitespaces(maxIndex int) {
|
||||
whitespaces := chars.TrailingWhitespaces()
|
||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
||||
end := len(chars.slice) - whitespaces
|
||||
chars.slice = chars.slice[0:max(end, maxIndex)]
|
||||
}
|
||||
|
||||
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||
lastIdx := len(chars.slice)
|
||||
firstIdx := lastIdx - len(runes)
|
||||
if firstIdx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := firstIdx; i < lastIdx; i++ {
|
||||
char := chars.Get(i)
|
||||
if char != runes[i-firstIdx] {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
chars.slice = chars.slice[0:firstIdx]
|
||||
}
|
||||
|
||||
func (chars *Chars) SliceRight(last int) {
|
||||
chars.slice = chars.slice[:last]
|
||||
}
|
||||
|
||||
func (chars *Chars) ToString() string {
|
||||
@@ -227,7 +249,7 @@ func (chars *Chars) Prepend(prefix string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
|
||||
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, wrapWord bool) ([][]rune, bool) {
|
||||
text := make([]rune, chars.Length())
|
||||
copy(text, chars.ToRunes())
|
||||
|
||||
@@ -237,7 +259,7 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
lines = append(lines, text)
|
||||
} else {
|
||||
from := 0
|
||||
for off := 0; off < len(text); off++ {
|
||||
for off := range text {
|
||||
if text[off] == '\n' {
|
||||
lines = append(lines, text[from:off+1]) // Include '\n'
|
||||
from = off + 1
|
||||
@@ -273,9 +295,10 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
hasWrapSign := false
|
||||
for {
|
||||
cols := wrapCols
|
||||
if len(wrapped) > 0 {
|
||||
if hasWrapSign {
|
||||
cols -= wrapSignWidth
|
||||
}
|
||||
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
|
||||
@@ -284,13 +307,28 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
if overflowIdx == 0 {
|
||||
overflowIdx = 1
|
||||
}
|
||||
if wrapWord {
|
||||
// Find last space/tab at or before overflowIdx
|
||||
breakIdx := -1
|
||||
for k := overflowIdx; k > 0; k-- {
|
||||
if line[k-1] == ' ' || line[k-1] == '\t' {
|
||||
breakIdx = k
|
||||
break
|
||||
}
|
||||
}
|
||||
if breakIdx > 0 {
|
||||
overflowIdx = breakIdx
|
||||
}
|
||||
}
|
||||
if len(wrapped) >= maxLines {
|
||||
return wrapped, true
|
||||
}
|
||||
wrapped = append(wrapped, line[:overflowIdx])
|
||||
hasWrapSign = true
|
||||
line = line[overflowIdx:]
|
||||
continue
|
||||
}
|
||||
hasWrapSign = false
|
||||
|
||||
// Restore trailing '\n'
|
||||
if newline {
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestTrimLength(t *testing.T) {
|
||||
func TestCharsLines(t *testing.T) {
|
||||
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
|
||||
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
|
||||
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
|
||||
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop, false)
|
||||
fmt.Println(lines, overflow)
|
||||
if len(lines) != expectedNumLines || overflow != expectedOverflow {
|
||||
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
|
||||
@@ -76,8 +76,56 @@ func TestCharsLines(t *testing.T) {
|
||||
check(true, 100, 3, 1, 1, 8, false)
|
||||
|
||||
// With wrap sign (3 + 2)
|
||||
check(true, 100, 3, 2, 1, 12, false)
|
||||
check(true, 100, 3, 2, 1, 10, false)
|
||||
|
||||
// With wrap sign (3 + 2) and no multi-line
|
||||
check(false, 100, 3, 2, 1, 13, false)
|
||||
}
|
||||
|
||||
func TestCharsLinesWrapWord(t *testing.T) {
|
||||
// "hello world foo bar" with width 12 should break at word boundaries
|
||||
chars := ToChars([]byte("hello world foo bar"))
|
||||
lines, overflow := chars.Lines(false, 100, 12, 0, 8, true)
|
||||
// "hello world " (12) | "foo bar" (7)
|
||||
if len(lines) != 2 || overflow {
|
||||
t.Errorf("Expected 2 lines, got %d (overflow: %v): %v", len(lines), overflow, lines)
|
||||
}
|
||||
if string(lines[0]) != "hello world " {
|
||||
t.Errorf("Expected first line 'hello world ', got %q", string(lines[0]))
|
||||
}
|
||||
if string(lines[1]) != "foo bar" {
|
||||
t.Errorf("Expected second line 'foo bar', got %q", string(lines[1]))
|
||||
}
|
||||
|
||||
// No word boundary: a single long word falls back to character wrap
|
||||
chars2 := ToChars([]byte("abcdefghijklmnop"))
|
||||
lines2, _ := chars2.Lines(false, 100, 10, 0, 8, true)
|
||||
if len(lines2) != 2 {
|
||||
t.Errorf("Expected 2 lines for long word, got %d: %v", len(lines2), lines2)
|
||||
}
|
||||
if string(lines2[0]) != "abcdefghij" {
|
||||
t.Errorf("Expected first line 'abcdefghij', got %q", string(lines2[0]))
|
||||
}
|
||||
|
||||
// Tab as word boundary
|
||||
chars3 := ToChars([]byte("hello\tworld"))
|
||||
lines3, _ := chars3.Lines(false, 100, 7, 0, 8, true)
|
||||
// "hello\t" should break at tab (width of tab at pos 5 with tabstop 8 = 3, total width = 8 > 7)
|
||||
// Actually RunesWidth: 'h'=1,'e'=1,'l'=1,'l'=1,'o'=1,'\t'=3 = 8 > 7, overflowIdx=5
|
||||
// Then word-wrap scans back and finds no space/tab before idx 5 (tab IS at idx 5 but we check line[k-1])
|
||||
// Wait - let me think: overflowIdx=5, we check k=5 -> line[4]='o', k=4 -> line[3]='l'... no space/tab found
|
||||
// Falls back to character wrap: "hello" | "\tworld"
|
||||
if len(lines3) < 2 {
|
||||
t.Errorf("Expected at least 2 lines for tab test, got %d: %v", len(lines3), lines3)
|
||||
}
|
||||
|
||||
// wrapWord=false still character-wraps
|
||||
chars4 := ToChars([]byte("hello world"))
|
||||
lines4, _ := chars4.Lines(false, 100, 8, 0, 8, false)
|
||||
if len(lines4) != 2 {
|
||||
t.Errorf("Expected 2 lines with wrapWord=false, got %d: %v", len(lines4), lines4)
|
||||
}
|
||||
if string(lines4[0]) != "hello wo" {
|
||||
t.Errorf("Expected first line 'hello wo', got %q", string(lines4[0]))
|
||||
}
|
||||
}
|
||||
|
||||
39
src/util/concurrent_set.go
Normal file
39
src/util/concurrent_set.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package util
|
||||
|
||||
import "sync"
|
||||
|
||||
// ConcurrentSet is a thread-safe set implementation.
|
||||
type ConcurrentSet[T comparable] struct {
|
||||
lock sync.RWMutex
|
||||
items map[T]struct{}
|
||||
}
|
||||
|
||||
// NewConcurrentSet creates a new ConcurrentSet.
|
||||
func NewConcurrentSet[T comparable]() *ConcurrentSet[T] {
|
||||
return &ConcurrentSet[T]{
|
||||
items: make(map[T]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds an item to the set.
|
||||
func (s *ConcurrentSet[T]) Add(item T) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.items[item] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove removes an item from the set.
|
||||
func (s *ConcurrentSet[T]) Remove(item T) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
delete(s.items, item)
|
||||
}
|
||||
|
||||
// ForEach iterates over each item in the set and applies the provided function.
|
||||
func (s *ConcurrentSet[T]) ForEach(fn func(item T)) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
for item := range s.items {
|
||||
fn(item)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import "sync"
|
||||
type EventType int
|
||||
|
||||
// Events is a type that associates EventType to any data
|
||||
type Events map[EventType]interface{}
|
||||
type Events map[EventType]any
|
||||
|
||||
// EventBox is used for coordinating events
|
||||
type EventBox struct {
|
||||
@@ -36,7 +36,7 @@ func (b *EventBox) Wait(callback func(*Events)) {
|
||||
}
|
||||
|
||||
// Set turns on the event type on the box
|
||||
func (b *EventBox) Set(event EventType, value interface{}) {
|
||||
func (b *EventBox) Set(event EventType, value any) {
|
||||
b.cond.L.Lock()
|
||||
b.events[event] = value
|
||||
if _, found := b.ignore[event]; !found {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/rivo/uniseg"
|
||||
@@ -18,8 +18,13 @@ func StringWidth(s string) int {
|
||||
|
||||
// RunesWidth returns runes width
|
||||
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||
return StringsWidth(string(runes), prefixWidth, tabstop, limit)
|
||||
}
|
||||
|
||||
// StringsWidth returns the width of the string
|
||||
func StringsWidth(str string, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||
width := 0
|
||||
gr := uniseg.NewGraphemes(string(runes))
|
||||
gr := uniseg.NewGraphemes(str)
|
||||
idx := 0
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
@@ -55,66 +60,8 @@ func Truncate(input string, limit int) ([]rune, int) {
|
||||
return runes, width
|
||||
}
|
||||
|
||||
// Max returns the largest integer
|
||||
func Max(first int, second int) int {
|
||||
if first >= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Max16 returns the largest integer
|
||||
func Max16(first int16, second int16) int16 {
|
||||
if first >= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Max32 returns the largest 32-bit integer
|
||||
func Max32(first int32, second int32) int32 {
|
||||
if first > second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Min returns the smallest integer
|
||||
func Min(first int, second int) int {
|
||||
if first <= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Min32 returns the smallest 32-bit integer
|
||||
func Min32(first int32, second int32) int32 {
|
||||
if first <= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
|
||||
func Constrain32(val int32, min int32, max int32) int32 {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Constrain limits the given integer with the upper and lower bounds
|
||||
func Constrain(val int, min int, max int) int {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
func Constrain[T cmp.Ordered](val, minimum, maximum T) T {
|
||||
return max(min(val, maximum), minimum)
|
||||
}
|
||||
|
||||
func AsUint16(val int) uint16 {
|
||||
@@ -126,18 +73,6 @@ func AsUint16(val int) uint16 {
|
||||
return uint16(val)
|
||||
}
|
||||
|
||||
// DurWithin limits the given time.Duration with the upper and lower bounds
|
||||
func DurWithin(
|
||||
val time.Duration, min time.Duration, max time.Duration) time.Duration {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// IsTty returns true if the file is a terminal
|
||||
func IsTty(file *os.File) bool {
|
||||
fd := file.Fd()
|
||||
@@ -209,7 +144,7 @@ func CompareVersions(v1, v2 string) int {
|
||||
return n
|
||||
}
|
||||
|
||||
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
|
||||
for i := 0; i < max(len(parts1), len(parts2)); i++ {
|
||||
var p1, p2 int
|
||||
if i < len(parts1) {
|
||||
p1 = atoi(parts1[i])
|
||||
|
||||
@@ -4,72 +4,8 @@ import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
if Max(10, 1) != 10 {
|
||||
t.Error("Expected", 10)
|
||||
}
|
||||
if Max(-2, 5) != 5 {
|
||||
t.Error("Expected", 5)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax16(t *testing.T) {
|
||||
if Max16(10, 1) != 10 {
|
||||
t.Error("Expected", 10)
|
||||
}
|
||||
if Max16(-2, 5) != 5 {
|
||||
t.Error("Expected", 5)
|
||||
}
|
||||
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
|
||||
t.Error("Expected", math.MaxInt16)
|
||||
}
|
||||
if Max16(0, math.MinInt16) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax32(t *testing.T) {
|
||||
if Max32(10, 1) != 10 {
|
||||
t.Error("Expected", 10)
|
||||
}
|
||||
if Max32(-2, 5) != 5 {
|
||||
t.Error("Expected", 5)
|
||||
}
|
||||
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
|
||||
t.Error("Expected", math.MaxInt32)
|
||||
}
|
||||
if Max32(0, math.MinInt32) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin(t *testing.T) {
|
||||
if Min(10, 1) != 1 {
|
||||
t.Error("Expected", 1)
|
||||
}
|
||||
if Min(-2, 5) != -2 {
|
||||
t.Error("Expected", -2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin32(t *testing.T) {
|
||||
if Min32(10, 1) != 1 {
|
||||
t.Error("Expected", 1)
|
||||
}
|
||||
if Min32(-2, 5) != -2 {
|
||||
t.Error("Expected", -2)
|
||||
}
|
||||
if Min32(math.MaxInt32, 0) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
if Min32(0, math.MinInt32) != math.MinInt32 {
|
||||
t.Error("Expected", math.MinInt32)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstrain(t *testing.T) {
|
||||
if Constrain(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
@@ -83,22 +19,6 @@ func TestConstrain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstrain32(t *testing.T) {
|
||||
if Constrain32(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
}
|
||||
if Constrain32(2, -1, 3) != 2 {
|
||||
t.Error("Expected", 2)
|
||||
}
|
||||
|
||||
if Constrain32(5, -1, 3) != 3 {
|
||||
t.Error("Expected", 3)
|
||||
}
|
||||
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsUint16(t *testing.T) {
|
||||
if AsUint16(5) != 5 {
|
||||
t.Error("Expected", 5)
|
||||
@@ -120,18 +40,6 @@ func TestAsUint16(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurWithIn(t *testing.T) {
|
||||
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
|
||||
t.Error("Expected", time.Duration(0))
|
||||
}
|
||||
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
|
||||
t.Error("Expected", time.Second)
|
||||
}
|
||||
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
|
||||
t.Error("Expected", time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnce(t *testing.T) {
|
||||
o := Once(false)
|
||||
if o() {
|
||||
|
||||
17
test/lib/common.fish
Normal file
17
test/lib/common.fish
Normal file
@@ -0,0 +1,17 @@
|
||||
# Unset fzf variables
|
||||
set -e FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE FZF_TMUX FZF_TMUX_OPTS
|
||||
set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
set -e FZF_API_KEY
|
||||
# Unset completion-specific variables
|
||||
set -e FZF_COMPLETION_TRIGGER FZF_COMPLETION_OPTS
|
||||
|
||||
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
|
||||
set -gx FZF_COMPLETION_TRIGGER '++'
|
||||
set -gx fish_history fzf_test
|
||||
|
||||
# Add fzf to PATH
|
||||
fish_add_path <%= BASE %>/bin
|
||||
|
||||
# Source key bindings and completion
|
||||
source "<%= BASE %>/shell/key-bindings.fish"
|
||||
source "<%= BASE %>/shell/completion.fish"
|
||||
251
test/lib/common.rb
Normal file
251
test/lib/common.rb
Normal file
@@ -0,0 +1,251 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'minitest/autorun'
|
||||
require 'fileutils'
|
||||
require 'English'
|
||||
require 'shellwords'
|
||||
require 'erb'
|
||||
require 'tempfile'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
|
||||
FISH_TEMPLATE = File.read(File.expand_path('common.fish', __dir__))
|
||||
UNSETS = %w[
|
||||
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
|
||||
FZF_TMUX FZF_TMUX_OPTS
|
||||
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
||||
FZF_ALT_C_COMMAND
|
||||
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
FZF_API_KEY
|
||||
].freeze
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
FILE = File.expand_path(__FILE__)
|
||||
BASE = File.expand_path('../..', __dir__)
|
||||
Dir.chdir(BASE)
|
||||
FZF = %(FZF_DEFAULT_OPTS="--no-scrollbar --gutter ' ' --pointer '>' --marker '>'" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf).freeze
|
||||
|
||||
def wait(timeout = DEFAULT_TIMEOUT)
|
||||
since = Time.now
|
||||
begin
|
||||
yield or raise Minitest::Assertion, 'Assertion failure'
|
||||
rescue Minitest::Assertion
|
||||
raise if Time.now - since > timeout
|
||||
|
||||
sleep(0.05)
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
class Shell
|
||||
class << self
|
||||
def bash
|
||||
@bash ||=
|
||||
begin
|
||||
bashrc = '/tmp/fzf.bash'
|
||||
File.open(bashrc, 'w') do |f|
|
||||
f.puts ERB.new(TEMPLATE).result(binding)
|
||||
end
|
||||
|
||||
"bash --rcfile #{bashrc}"
|
||||
end
|
||||
end
|
||||
|
||||
def zsh
|
||||
@zsh ||=
|
||||
begin
|
||||
zdotdir = '/tmp/fzf-zsh'
|
||||
FileUtils.rm_rf(zdotdir)
|
||||
FileUtils.mkdir_p(zdotdir)
|
||||
File.open("#{zdotdir}/.zshrc", 'w') do |f|
|
||||
f.puts ERB.new(TEMPLATE).result(binding)
|
||||
end
|
||||
"ZDOTDIR=#{zdotdir} zsh"
|
||||
end
|
||||
end
|
||||
|
||||
def fish
|
||||
@fish ||=
|
||||
begin
|
||||
confdir = '/tmp/fzf-fish'
|
||||
FileUtils.rm_rf(confdir)
|
||||
FileUtils.mkdir_p("#{confdir}/fish/conf.d")
|
||||
File.open("#{confdir}/fish/conf.d/fzf.fish", 'w') do |f|
|
||||
f.puts ERB.new(FISH_TEMPLATE).result(binding)
|
||||
end
|
||||
"rm -f ~/.local/share/fish/fzf_test_history; XDG_CONFIG_HOME=#{confdir} fish"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Tmux
|
||||
attr_reader :win
|
||||
|
||||
def initialize(shell = :bash)
|
||||
@win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first
|
||||
go(%W[set-window-option -t #{@win} pane-base-index 0])
|
||||
return unless shell == :fish
|
||||
|
||||
send_keys 'function fish_prompt; end; clear', :Enter
|
||||
self.until(&:empty?)
|
||||
end
|
||||
|
||||
def kill
|
||||
go(%W[kill-window -t #{win}])
|
||||
end
|
||||
|
||||
def focus
|
||||
go(%W[select-window -t #{win}])
|
||||
end
|
||||
|
||||
def send_keys(*args)
|
||||
go(%W[send-keys -t #{win}] + args.map(&:to_s))
|
||||
end
|
||||
|
||||
def paste(str)
|
||||
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
|
||||
end
|
||||
|
||||
def capture
|
||||
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
||||
end
|
||||
|
||||
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
||||
lines = nil
|
||||
begin
|
||||
wait(timeout) do
|
||||
lines = capture
|
||||
class << lines
|
||||
def counts
|
||||
lazy
|
||||
.map { |l| l.scan(%r{^. ([0-9]+)/([0-9]+)( \(([0-9]+)\))?}) }
|
||||
.reject(&:empty?)
|
||||
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
|
||||
end
|
||||
|
||||
def match_count
|
||||
counts[0]
|
||||
end
|
||||
|
||||
def item_count
|
||||
counts[1]
|
||||
end
|
||||
|
||||
def select_count
|
||||
counts[2]
|
||||
end
|
||||
|
||||
def any_include?(val)
|
||||
method = val.is_a?(Regexp) ? :match : :include?
|
||||
find { |line| line.send(method, val) }
|
||||
end
|
||||
end
|
||||
yield(lines).tap do |ok|
|
||||
send_keys 'C-l' if refresh && !ok
|
||||
end
|
||||
end
|
||||
rescue Minitest::Assertion
|
||||
puts $ERROR_INFO.backtrace
|
||||
puts '>' * 80
|
||||
puts lines
|
||||
puts '<' * 80
|
||||
raise
|
||||
end
|
||||
lines
|
||||
end
|
||||
|
||||
def prepare
|
||||
tries = 0
|
||||
begin
|
||||
self.until(true) do |lines|
|
||||
message = "Prepare[#{tries}]"
|
||||
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
||||
sleep(0.15)
|
||||
lines[-1] == message
|
||||
end
|
||||
rescue Minitest::Assertion
|
||||
(tries += 1) < 5 ? retry : raise
|
||||
end
|
||||
send_keys 'C-u', 'C-l'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def go(args)
|
||||
IO.popen(%w[tmux] + args) { |io| io.readlines(chomp: true) }
|
||||
end
|
||||
end
|
||||
|
||||
class TestBase < Minitest::Test
|
||||
TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
|
||||
FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
|
||||
|
||||
def writelines(lines)
|
||||
File.write(TEMPNAME, lines.join("\n"))
|
||||
end
|
||||
|
||||
def tempname
|
||||
TEMPNAME
|
||||
end
|
||||
|
||||
def fzf_output
|
||||
@thread.join.value.chomp.tap { @thread = nil }
|
||||
end
|
||||
|
||||
def fzf_output_lines
|
||||
fzf_output.lines(chomp: true)
|
||||
end
|
||||
|
||||
def setup
|
||||
File.mkfifo(FIFONAME)
|
||||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.rm_f([TEMPNAME, FIFONAME])
|
||||
end
|
||||
|
||||
alias assert_equal_org assert_equal
|
||||
def assert_equal(expected, actual)
|
||||
# Ignore info separator
|
||||
actual = actual&.sub(/\s*─+$/, '') if actual.is_a?(String) && actual&.match?(%r{\d+/\d+})
|
||||
assert_equal_org(expected, actual)
|
||||
end
|
||||
|
||||
# Run fzf with its output piped to a fifo
|
||||
def fzf(*opts)
|
||||
raise 'fzf_output not taken' if @thread
|
||||
|
||||
@thread = Thread.new { File.read(FIFONAME) }
|
||||
fzf!(*opts) + " > #{FIFONAME.shellescape}"
|
||||
end
|
||||
|
||||
def fzf!(*opts)
|
||||
opts = opts.filter_map do |o|
|
||||
case o
|
||||
when Symbol
|
||||
o = o.to_s
|
||||
o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}"
|
||||
when String, Numeric
|
||||
o.to_s
|
||||
end
|
||||
end
|
||||
"#{FZF} #{opts.join(' ')}"
|
||||
end
|
||||
end
|
||||
|
||||
class TestInteractive < TestBase
|
||||
attr_reader :tmux
|
||||
|
||||
def setup
|
||||
super
|
||||
@tmux = Tmux.new
|
||||
end
|
||||
|
||||
def teardown
|
||||
super
|
||||
@tmux.kill
|
||||
end
|
||||
end
|
||||
59
test/lib/common.sh
Normal file
59
test/lib/common.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
set -u
|
||||
PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100
|
||||
unset <%= UNSETS.join(' ') %>
|
||||
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
|
||||
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
|
||||
|
||||
export FZF_DEFAULT_OPTS="--no-scrollbar --pointer '>' --marker '>'"
|
||||
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then
|
||||
export PATH="${PATH:+${PATH}:}<%= BASE %>/bin"
|
||||
fi
|
||||
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
[[ $- == *i* ]] && source "<%= BASE %>/shell/completion.<%= __method__ %>" 2> /dev/null
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
source "<%= BASE %>/shell/key-bindings.<%= __method__ %>"
|
||||
|
||||
# Old API
|
||||
_fzf_complete_f() {
|
||||
_fzf_complete "+m --multi --prompt \"prompt-f> \"" "$@" < <(
|
||||
echo foo
|
||||
echo bar
|
||||
)
|
||||
}
|
||||
|
||||
# New API
|
||||
_fzf_complete_g() {
|
||||
_fzf_complete +m --multi --prompt "prompt-g> " -- "$@" < <(
|
||||
echo foo
|
||||
echo bar
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_f_post() {
|
||||
awk '{print "f" $0 $0}'
|
||||
}
|
||||
|
||||
_fzf_complete_g_post() {
|
||||
awk '{print "g" $0 $0}'
|
||||
}
|
||||
|
||||
[ -n "${BASH-}" ] && complete -F _fzf_complete_f -o default -o bashdefault f
|
||||
[ -n "${BASH-}" ] && complete -F _fzf_complete_g -o default -o bashdefault g
|
||||
|
||||
_comprun() {
|
||||
local command=$1
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
f) fzf "$@" --preview 'echo preview-f-{}' ;;
|
||||
g) fzf "$@" --preview 'echo preview-g-{}' ;;
|
||||
*) fzf "$@" ;;
|
||||
esac
|
||||
}
|
||||
5
test/runner.rb
Normal file
5
test/runner.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Dir[File.join(__dir__, 'test_*.rb')].each { require it }
|
||||
|
||||
require 'minitest/autorun'
|
||||
2261
test/test_core.rb
Normal file
2261
test/test_core.rb
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user