From 7df8b77739ae5a05e8cd87bff905ee091e5afd7f Mon Sep 17 00:00:00 2001 From: LoricAndre <57358788+LoricAndre@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:01:08 +0100 Subject: [PATCH] feat: use clap & derive for options, manpage & completions (#586) * feat: use clap & derive for options, manpage & completions * clippy & fmt * fix: correctly handle errors * fix: disable doctest * fix: readd replstr * fix: reserve --tmux until it is implemented * explicit panic messages for history files * fix after merge --------- Co-authored-by: LoricAndre --- .cargo/config.toml | 2 + Cargo.lock | 260 ++-- Cargo.toml | 51 +- man/man1/sk.1 | 1072 +++++++---------- shell/completion.bash | 581 ++++----- shell/completion.zsh | 437 ++----- skim/Cargo.toml | 49 + {examples => skim/examples}/custom_item.rs | 4 +- .../examples}/custom_keybinding_actions.rs | 2 +- {examples => skim/examples}/downcast.rs | 4 +- {examples => skim/examples}/nth.rs | 7 +- {examples => skim/examples}/option_builder.rs | 2 +- {examples => skim/examples}/sample.rs | 0 {src => skim/src}/ansi.rs | 0 skim/src/bin/main.rs | 261 ++++ skim/src/context.rs | 34 + {src => skim/src}/engine/all.rs | 0 {src => skim/src}/engine/andor.rs | 0 {src => skim/src}/engine/exact.rs | 0 {src => skim/src}/engine/factory.rs | 0 {src => skim/src}/engine/fuzzy.rs | 15 +- {src => skim/src}/engine/mod.rs | 0 {src => skim/src}/engine/regexp.rs | 0 {src => skim/src}/engine/util.rs | 0 {src => skim/src}/event.rs | 0 {src => skim/src}/field.rs | 0 {src => skim/src}/global.rs | 0 {src => skim/src}/header.rs | 12 +- {src => skim/src}/helper/item.rs | 0 {src => skim/src}/helper/item_reader.rs | 18 +- {src => skim/src}/helper/mod.rs | 0 {src => skim/src}/helper/selector.rs | 0 {src => skim/src}/input.rs | 18 +- {src => skim/src}/item.rs | 32 +- {src => skim/src}/lib.rs | 23 +- {src => skim/src}/matcher.rs | 0 {src => skim/src}/model.rs | 105 +- skim/src/options.rs | 741 ++++++++++++ {src => skim/src}/orderedvec.rs | 0 {src => skim/src}/output.rs | 0 {src => skim/src}/prelude.rs | 0 {src => skim/src}/previewer.rs | 0 {src => skim/src}/query.rs | 31 +- {src => skim/src}/reader.rs | 12 +- {src => skim/src}/selection.rs | 38 +- {src => skim/src}/spinlock.rs | 0 {src => skim/src}/theme.rs | 4 +- {src => skim/src}/util.rs | 11 +- src/bin/main.rs | 557 --------- src/options.rs | 127 -- test/test_skim.py | 7 +- xtask/Cargo.toml | 12 + xtask/src/main.rs | 81 ++ 53 files changed, 2341 insertions(+), 2269 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 skim/Cargo.toml rename {examples => skim/examples}/custom_item.rs (90%) rename {examples => skim/examples}/custom_keybinding_actions.rs (93%) rename {examples => skim/examples}/downcast.rs (94%) rename {examples => skim/examples}/nth.rs (77%) rename {examples => skim/examples}/option_builder.rs (96%) rename {examples => skim/examples}/sample.rs (100%) rename {src => skim/src}/ansi.rs (100%) create mode 100644 skim/src/bin/main.rs create mode 100644 skim/src/context.rs rename {src => skim/src}/engine/all.rs (100%) rename {src => skim/src}/engine/andor.rs (100%) rename {src => skim/src}/engine/exact.rs (100%) rename {src => skim/src}/engine/factory.rs (100%) rename {src => skim/src}/engine/fuzzy.rs (92%) rename {src => skim/src}/engine/mod.rs (100%) rename {src => skim/src}/engine/regexp.rs (100%) rename {src => skim/src}/engine/util.rs (100%) rename {src => skim/src}/event.rs (100%) rename {src => skim/src}/field.rs (100%) rename {src => skim/src}/global.rs (100%) rename {src => skim/src}/header.rs (92%) rename {src => skim/src}/helper/item.rs (100%) rename {src => skim/src}/helper/item_reader.rs (96%) rename {src => skim/src}/helper/mod.rs (100%) rename {src => skim/src}/helper/selector.rs (100%) rename {src => skim/src}/input.rs (96%) rename {src => skim/src}/item.rs (88%) rename {src => skim/src}/lib.rs (95%) rename {src => skim/src}/matcher.rs (100%) rename {src => skim/src}/model.rs (91%) create mode 100644 skim/src/options.rs rename {src => skim/src}/orderedvec.rs (100%) rename {src => skim/src}/output.rs (100%) rename {src => skim/src}/prelude.rs (100%) rename {src => skim/src}/previewer.rs (100%) rename {src => skim/src}/query.rs (95%) rename {src => skim/src}/reader.rs (89%) rename {src => skim/src}/selection.rs (93%) rename {src => skim/src}/spinlock.rs (100%) rename {src => skim/src}/theme.rs (99%) rename {src => skim/src}/util.rs (97%) delete mode 100644 src/bin/main.rs delete mode 100644 src/options.rs create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..35049cbc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/Cargo.lock b/Cargo.lock index 41500a37..7fd3c796 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,12 +137,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.38" @@ -159,27 +153,81 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.25" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ - "atty", - "bitflags 1.3.2", + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", "clap_lex", - "indexmap 1.9.3", - "once_cell", - "strsim 0.10.0", - "termcolor", - "textwrap", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_complete_fig" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d494102c8ff3951810c72baf96910b980fb065ca5d3101243e6a8dc19747c86b" +dependencies = [ + "clap", + "clap_complete", +] + +[[package]] +name = "clap_complete_nushell" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315902e790cc6e5ddd20cbd313c1d0d49db77f191e149f96397230fb82a17677" +dependencies = [ + "clap", + "clap_complete", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "clap_mangen" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" dependencies = [ - "os_str_bytes", + "clap", + "roff", ] [[package]] @@ -270,7 +318,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn", ] @@ -295,6 +343,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -353,27 +410,17 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "env_filter" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" -dependencies = [ - "log", - "regex", -] - [[package]] name = "env_logger" -version = "0.11.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ - "anstream", - "anstyle", - "env_filter", + "atty", "humantime", "log", + "regex", + "termcolor", ] [[package]] @@ -410,15 +457,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] -name = "hashbrown" -version = "0.15.1" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -464,16 +511,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.6.0" @@ -481,7 +518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown", ] [[package]] @@ -533,6 +570,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "nix" version = "0.24.3" @@ -546,16 +592,24 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ - "bitflags 2.6.0", + "autocfg", + "bitflags 1.3.2", "cfg-if", - "cfg_aliases", "libc", + "memoffset", + "pin-utils", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -572,10 +626,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] -name = "os_str_bytes" -version = "6.6.1" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" @@ -655,12 +715,38 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "rustversion" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shlex" version = "1.3.0" @@ -671,7 +757,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" name = "skim" version = "0.10.4" dependencies = [ + "atty", "beef", + "bitflags 1.3.2", "chrono", "clap", "crossbeam", @@ -679,25 +767,20 @@ dependencies = [ "derive_builder", "env_logger", "fuzzy-matcher", - "indexmap 2.6.0", + "indexmap", "lazy_static", "log", - "nix 0.29.0", + "nix 0.25.1", "rayon", "regex", "shlex", + "time", "timer", "tuikit", - "unicode-width 0.2.0", + "unicode-width", "vte", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -735,12 +818,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" - [[package]] name = "thiserror" version = "1.0.69" @@ -771,6 +848,25 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "timer" version = "0.2.0" @@ -791,7 +887,7 @@ dependencies = [ "log", "nix 0.24.3", "term", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] @@ -806,12 +902,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1012,3 +1102,15 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "clap", + "clap_complete", + "clap_complete_fig", + "clap_complete_nushell", + "clap_mangen", + "skim", +] diff --git a/Cargo.toml b/Cargo.toml index 6b0000c7..41c7466a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,47 +1,10 @@ -[package] -name = "skim" -version = "0.10.4" -authors = ["Zhang Jinzhou "] -description = "Fuzzy Finder in rust!" -homepage = "https://github.com/lotabout/skim" -repository = "https://github.com/lotabout/skim" -keywords = ["fuzzy", "menu", "util"] -license = "MIT" -edition = "2018" - -[lib] -name = "skim" -path = "src/lib.rs" - -[[bin]] -name = "sk" -path = "src/bin/main.rs" - -[dependencies] -nix = "0.29.0" -regex = "1.11.1" -lazy_static = "1.5.0" -shlex = { version = "1.3.0", optional = true } -unicode-width = "0.2.0" -log = "0.4.22" -env_logger = { version = "0.11.5", optional = true } -clap = { version = "3.2.22", optional = true, features = ["cargo"] } -tuikit = "0.5.0" -vte = "0.13.0" -fuzzy-matcher = "0.3.7" -rayon = "1.5.3" -derive_builder = "0.20.2" -timer = "0.2.0" -chrono = "0.4.22" -crossbeam = "0.8.2" -beef = "0.5.2" # compact cow -defer-drop = "1.3.0" -indexmap = "2.6.0" - -[features] -default = ["cli"] -cli = ["dep:clap", "dep:shlex", "dep:env_logger"] +[workspace] +members = [ + "skim", + "xtask" +] +resolver = "2" +default-members = ["skim"] [profile.release] lto = true -debug = false diff --git a/man/man1/sk.1 b/man/man1/sk.1 index baafb4ac..0bfce661 100644 --- a/man/man1/sk.1 +++ b/man/man1/sk.1 @@ -1,684 +1,538 @@ -.ig -The MIT License (MIT) - -Copyright (c) 2019 Jinzhou Zhang -Copyright (c) 2017 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 -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -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 sk 1 "Oct 2018" "sk 0.10.4" "sk - a command-line fuzzy finder" - +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.TH sk 1 "sk " .SH NAME -sk - fuzzy finder in rust - +sk \- sk \- fuzzy finder in Rust .SH SYNOPSIS -sk [options] - +\fBsk\fR [\fB\-\-tac\fR] [\fB\-\-no\-sort\fR] [\fB\-t\fR|\fB\-\-tiebreak\fR] [\fB\-n\fR|\fB\-\-nth\fR] [\fB\-\-with\-nth\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-e\fR|\fB\-\-exact\fR] [\fB\-\-regex\fR] [\fB\-\-algo\fR] [\fB\-\-case\fR] [\fB\-b\fR|\fB\-\-bind\fR] [\fB\-m\fR|\fB\-\-multi\fR] [\fB\-\-no\-multi\fR] [\fB\-\-no\-mouse\fR] [\fB\-c\fR|\fB\-\-cmd\fR] [\fB\-i\fR|\fB\-\-interactive\fR] [\fB\-\-color\fR] [\fB\-\-no\-hscroll\fR] [\fB\-\-keep\-right\fR] [\fB\-\-skip\-to\-pattern\fR] [\fB\-\-no\-clear\-if\-empty\fR] [\fB\-\-no\-clear\-start\fR] [\fB\-\-no\-clear\fR] [\fB\-\-show\-cmd\-error\fR] [\fB\-\-layout\fR] [\fB\-\-reverse\fR] [\fB\-\-height\fR] [\fB\-\-no\-height\fR] [\fB\-\-min\-height\fR] [\fB\-\-margin\fR] [\fB\-p\fR|\fB\-\-prompt\fR] [\fB\-\-cmd\-prompt\fR] [\fB\-\-ansi\fR] [\fB\-\-tabstop\fR] [\fB\-\-inline\-info\fR] [\fB\-\-header\fR] [\fB\-\-header\-lines\fR] [\fB\-\-tmux\fR] [\fB\-\-history\fR] [\fB\-\-history\-size\fR] [\fB\-\-cmd\-history\fR] [\fB\-\-cmd\-history\-size\fR] [\fB\-\-preview\fR] [\fB\-\-preview\-window\fR] [\fB\-q\fR|\fB\-\-query\fR] [\fB\-\-cmd\-query\fR] [\fB\-\-expect\fR] [\fB\-\-read0\fR] [\fB\-\-print0\fR] [\fB\-\-print\-query\fR] [\fB\-\-print\-cmd\fR] [\fB\-\-print\-score\fR] [\fB\-1\fR|\fB\-\-select\-1\fR] [\fB\-0\fR|\fB\-\-exit\-0\fR] [\fB\-\-sync\fR] [\fB\-\-pre\-select\-n\fR] [\fB\-\-pre\-select\-pat\fR] [\fB\-\-pre\-select\-items\fR] [\fB\-\-pre\-select\-file\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-h\fR|\fB\-\-help\fR] .SH DESCRIPTION -sk is a general-purpose command-line fuzzy finder. - +sk \- fuzzy finder in Rust +.PP +sk is a general purpose command\-line fuzzy finder. +.PP +.PP +ENVIRONMENT VARIABLES +.PP + SKIM_DEFAULT_COMMAND +.PP + Default command to use when input is tty. On *nix systems, sk runs the command with sh \-c, so make sure that + it\*(Aqs POSIX\-compliant. +.PP + SKIM_DEFAULT_OPTIONS +.PP + Default options. e.g. export SKIM_DEFAULT_OPTIONS="\-\-multi +.PP + EXTENDED SEARCH MODE +.PP + Unless specified otherwise, sk will start in "extended\-search mode". In this mode, you can specify multiple patterns + delimited by spaces, such as: \*(Aqwild ^music .mp3$ sbtrkt !rmx +.PP + You can prepend a backslash to a space (\\ ) to match a literal space character. +.PP + Exact\-match (quoted) +.PP + A term that is prefixed by a single\-quote character (\*(Aq) is interpreted as an "exact\-match" (or "non\-fuzzy") term. sk + will search for the exact occurrences of the string. +.PP + Anchored\-match +.PP + A term can be prefixed by ^, or suffixed by $ to become an anchored\-match term. Then sk will search for the lines + that start with or end with the given string. An anchored\-match term is also an exact\-match term. +.PP + Negation +.PP + If a term is prefixed by !, sk will exclude the lines that satisfy the term from the result. In this case, sk per‐ + forms exact match by default. +.PP + Exact\-match by default +.PP + If you don\*(Aqt prefer fuzzy matching and do not wish to "quote" (prefixing with \*(Aq) every word, start sk with \-e or + \-\-exact option. Note that when \-\-exact is set, \*(Aq\-prefix "unquotes" the term. +.PP + OR operator +.PP + A single bar character term acts as an OR operator. For example, the following query matches entries that start with + core and end with either go, rb, or py. +.PP + e.g. ^core go$ | rb$ | py$ +.PP +.PP +EXIT STATUS + 0 Normal exit + 1 No match + 2 Error + 130 Interrupted with CTRL\-C or ESC .SH OPTIONS -.SS Search mode .TP -.B "-e, --exact" -Enable exact-match +\fB\-\-tac\fR +Show results in reverse order + +Often used in combination with \-\-no\-sort .TP -.B "--regex" -Search with regular expression instead of fuzzy match +\fB\-\-no\-sort\fR +Do not sort the results + +Often used in combination with \-\-tac Example: `history | sk \-\-tac \-\-no\-sort` .TP -.BI "--algo=" TYPE -Fuzzy matching algorithm (default: skim_v2) +\fB\-t\fR, \fB\-\-tiebreak\fR=\fITIEBREAK\fR [default: score,begin,end] +Comma\-separated list of sort criteria to apply when the scores are tied. + score Score of the fuzzy match algorithm + index Prefers line that appeared earlier in the input stream + begin Prefers line with matched substring closer to the beginning + end Prefers line with matched substring closer to the end + length Prefers line with shorter length + + \- Each criterion could be negated, e.g. (\-index) + \- Each criterion should appear only once in the list .br -.BR skim_v2 " Almost always the one to choose -.br -.BR skim_v1 " The legacy algorithm + .br -.BR clangd " the one used by clangd for keyword completion +[\fIpossible values: \fRscore, \-score, begin, \-begin, end, \-end, length, \-length] +.TP +\fB\-n\fR, \fB\-\-nth\fR=\fINTH\fR [default: ] +Fields to be matched + +A field index expression can be a non\-zero integer or a range expression ([BEGIN]..[END]). \-\-nth +and \-\-with\-nth take a comma\-separated list of field index expressions. + +**Examples:** + 1 The 1st field + 2 The 2nd field + \-1 The last field + \-2 The 2nd to last field + 3..5 From the 3rd field to the 5th field + 2.. From the 2nd field to the last field + ..\-3 From the 1st field to the 3rd to the last field + .. All the fields +.TP +\fB\-\-with\-nth\fR=\fIWITH_NTH\fR [default: ] +Fields to be transformed + +See **nth** for the details +.TP +\fB\-d\fR, \fB\-\-delimiter\fR=\fIDELIMITER\fR [default: [\\t\\n ]+] +Delimiter between fields + +In regex format, default to AWK\-style +.TP +\fB\-e\fR, \fB\-\-exact\fR +Run in exact mode +.TP +\fB\-\-regex\fR +Start in regex mode instead of fuzzy\-match +.TP +\fB\-\-algo\fR=\fIALGORITHM\fR [default: skim_v2] +Fuzzy matching algorithm + +skim_v2 Latest skim algorithm, should be better in almost any case +skim_v1 Legacy skim algorithm +clangd Used in clangd for keyword completion .br +.br +[\fIpossible values: \fRskim_v1, skim_v2, clangd] .TP -.BI "--case=" "[smart,respect,ignore]" -To ignore case on matching or not. (default smart) +\fB\-\-case\fR=\fICASE\fR [default: smart] +Case sensitivity + +Determines whether or not to ignore case while matching .br +.br +[\fIpossible values: \fRrespect, ignore, smart] .TP -.BI "-n, --nth=" "N[,..]" -Comma-separated list of field index expressions for limiting search scope. -See \fBFIELD INDEX EXPRESSION\fR for the details. +\fB\-b\fR, \fB\-\-bind\fR=\fIBIND\fR +Comma separated list of bindings + +You can customize key bindings of sk with \-\-bind option which takes a comma\-separated list of +key binding expressions. Each key binding expression follows the following format: KEY:ACTION + +e.g. sk \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line + +AVAILABLE KEYS: (SYNONYMS) + ctrl\-[a\-z] + ctrl\-space + ctrl\-alt\-[a\-z] + alt\-[a\-zA\-Z] + alt\-[0\-9] + f[1\-12] + enter (ctrl\-m) + space + bspace (bs) + alt\-up + alt\-down + alt\-left + alt\-right + alt\-enter (alt\-ctrl\-m) + alt\-space + alt\-bspace (alt\-bs) + alt\-/ + tab + btab (shift\-tab) + esc + del + up + down + left + right + home + end + pgup (page\-up) + pgdn (page\-down) + shift\-up + shift\-down + shift\-left + shift\-right + alt\-shift\-up + alt\-shift\-down + alt\-shift\-left + alt\-shift\-right + or any single character + + ACTION: DEFAULT BINDINGS (NOTES): + abort ctrl\-c ctrl\-q esc + accept enter + append\-and\-select + backward\-char ctrl\-b left + backward\-delete\-char ctrl\-h bspace + backward\-kill\-word alt\-bs + backward\-word alt\-b shift\-left + beginning\-of\-line ctrl\-a home + clear\-screen ctrl\-l + delete\-char del + delete\-charEOF ctrl\-d + deselect\-all + down ctrl\-j ctrl\-n down + end\-of\-line ctrl\-e end + execute(...) (see below for the details) + execute\-silent(...) (see below for the details) + forward\-char ctrl\-f right + forward\-word alt\-f shift\-right + if\-non\-matched + if\-query\-empty + if\-query\-not\-empty + ignore + kill\-line + kill\-word alt\-d + next\-history (ctrl\-n on \-\-history or \-\-cmd\-history) + page\-down pgdn + page\-up pgup + half\-page\-down + half\-page\-up + preview\-up shift\-up + preview\-down shift\-down + preview\-left + preview\-right + preview\-page\-down + preview\-page\-up + previous\-history (ctrl\-p on \-\-history or \-\-cmd\-history) + select\-all + toggle + toggle\-all + toggle+down ctrl\-i (tab) + toggle\-in (\-\-layout=reverse* ? toggle+up : toggle+down) + toggle\-out (\-\-layout=reverse* ? toggle+down : toggle+up) + toggle\-preview + toggle\-preview\-wrap + toggle\-sort + toggle+up btab (shift\-tab) + unix\-line\-discard ctrl\-u + unix\-word\-rubout ctrl\-w + up ctrl\-k ctrl\-p up + yank ctrl\-y + +Multiple actions can be chained using + separator. + + sk \-\-bind \*(Aqctrl\-a:select\-all+accept\*(Aq + +With execute(...) action, you can execute arbitrary commands without leaving sk. For example, +you can turn sk into a simple file browser by binding enter key to less command like follows. + + sk \-\-bind "enter:execute(less {})" + +You can use the same placeholder expressions as in \-\-preview. + +If the command contains parentheses, sk may fail to parse the expression. In that case, you can +use any of the following alternative notations to avoid parse errors. + + execute[...] + execute\*(Aq...\*(Aq + execute"..." + execute:... + This is the special form that frees you from parse errors as it does not expect the clos‐ + ing character. The catch is that it should be the last one in the comma\-separated list of + key\-action pairs. + +sk switches to the alternate screen when executing a command. However, if the command is ex‐ +pected to complete quickly, and you are not interested in its output, you might want to use exe‐ +cute\-silent instead, which silently executes the command without the switching. Note that sk +will not be responsive until the command is complete. For asynchronous execution, start your +command as a background process (i.e. appending &). + +With if\-query\-empty and if\-query\-not\-empty action, you could specify the action to execute de‐ +pends on the query condition. For example + + sk \-\-bind \*(Aqctrl\-d:if\-query\-empty(abort)+delete\-char\*(Aq + +If the query is empty, skim will execute abort action, otherwise execute delete\-char action. It +is equal to ‘delete\-char/eof‘. +.TP +\fB\-m\fR, \fB\-\-multi\fR +Enable multiple selection + +Uses Tab and S\-Tab by default for selection +.TP +\fB\-\-no\-multi\fR +Disable multiple selection +.TP +\fB\-\-no\-mouse\fR +Disable mouse +.TP +\fB\-c\fR, \fB\-\-cmd\fR=\fICMD\fR +Command to invoke dynamically in interactive mode + +Will be invoked using `sh \-c` +.TP +\fB\-i\fR, \fB\-\-interactive\fR +Run in interactive mode +.TP +\fB\-\-color\fR=\fICOLOR\fR +Set color theme + +Format: [BASE][,COLOR:ANSI] +.TP +\fB\-\-no\-hscroll\fR +Disable horizontal scroll .TP -.BI "--with-nth=" "N[,..]" -Transform the presentation of each line using field index expressions +\fB\-\-keep\-right\fR +Keep the right end of the line visible on overflow + +Effective only when the query string is empty .TP -.BI "-d, --delimiter=" "STR" -Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) +\fB\-\-skip\-to\-pattern\fR=\fISKIP_TO_PATTERN\fR +Show the matched pattern at the line start -.SS Search result +Line will start with the start of the matched pattern. Effective only when the query +string is empty. Was designed to skip showing starts of paths of rg/grep results. + +e.g. sk \-i \-c "rg {} \-\-color=always" \-\-skip\-to\-pattern \*(Aq[^/]*:\*(Aq \-\-ansi .TP -.B "--tac" -Reverse the order of the search result(normally used together with \fB--no-sort\fR) +\fB\-\-no\-clear\-if\-empty\fR +Do not clear previous line if the command returns an empty result + +Do not clear previous items if new command returns empty result. This might be useful to +reduce flickering when typing new commands and the half\-complete commands are not valid. +This is not default however because similar usecases for grep and rg had already been op‐ +timized where empty result of a query do mean "empty" and previous results should be +cleared. +.TP +\fB\-\-no\-clear\-start\fR +Do not clear items on start .TP -.B "--no-sort" -Do not sort the search result(normally used together with \fB--tac\fR) +\fB\-\-no\-clear\fR +Do not clear screen on exit -.RS -e.g. \fBhistory | sk --tac --no-sort\fR -.RE +Do not clear finder interface on exit. If skim was started in full screen mode, it will not switch back to the original screen, so you\*(Aqll have to manually run tput rmcup to return. This option can be used to avoid flickering of the screen when your application needs to start skim multiple times in order. .TP -.BI "--tiebreak=" "CRI[,..]" -Comma-separated list of sort criteria to apply when the scores are tied. -.br +\fB\-\-show\-cmd\-error\fR +Show error message if command fails +.TP +\fB\-\-layout\fR=\fILAYOUT\fR [default: default] +Set layout +default Display from the bottom of the screen +reverse Display from the top of the screen +reverse\-list Display from the top of the screen, prompt at the bottom .br -.BR score " Score of the fuzzy match algorithm" -.br -.BR index " Prefers line that appeared earlier in the input stream" -.br -.BR begin " Prefers line with matched substring closer to the beginning" -.br -.BR end " Prefers line with matched substring closer to the end" -.br -.BR length " Prefers line with shorter length" .br -- Each criterion could be negated, e.g. (-index) -.br -- Each criterion should appear only once in the list -.SS Interface +[\fIpossible values: \fRdefault, reverse, reverse\-list] +.TP +\fB\-\-reverse\fR +Shorthand for reverse layout .TP -.B "-i, --interactive" -Start the finder in the command query +\fB\-\-height\fR=\fIHEIGHT\fR [default: 100%] +Height of skim\*(Aqs window + +Can either be a row count or a percentage .TP -.B "-c, --cmd [cmd]" -Specify the command to invoke for fetching options +\fB\-\-no\-height\fR +Disable height feature .TP -.B "-I replstr" -Replace \fBreplstr\fR with the selected item +\fB\-\-min\-height\fR=\fIMIN_HEIGHT\fR [default: 10] +Minimum height of skim\*(Aqs window + +Useful when the height is set as a percentage +Ignored when \-\-height is not specified .TP -.B "-m, --multi" -Enable multi-select with tab/shift-tab +\fB\-\-margin\fR=\fIMARGIN\fR [default: 0] +Screen margin + +For each side, can be either a row count or a percentage of the terminal size +Format can be one of: + \- TRBL + \- TB,RL + \- T,RL,B + \- T,R,B,L +Example: 1,10% .TP -.B "--no-multi" -Disable multi-select +\fB\-p\fR, \fB\-\-prompt\fR=\fIPROMPT\fR [default: > ] +Set prompt .TP -.BI "--bind=" "KEYBINDS" -Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the -details. +\fB\-\-cmd\-prompt\fR=\fICMD_PROMPT\fR [default: c> ] +Set prompt in command mode .TP -.B "--no-hscroll" -Disable horizontal scroll +\fB\-\-ansi\fR +Parse ANSI color codes in input strings .TP -.BI "--height=" "HEIGHT[%]" -Display sk window below the cursor with the given height instead of using -the full screen. +\fB\-\-tabstop\fR=\fITABSTOP\fR [default: 8] +Number of spaces that make up a tab .TP -.BI "--min-height=" "HEIGHT" -Minimum height when \fB--height\fR is given in percent (default: 10). -Ignored when \fB--height\fR is not specified. +\fB\-\-inline\-info\fR +Display info next to the query .TP -.BI "--layout=" "LAYOUT" -Choose the layout (default: default) +\fB\-\-header\fR=\fIHEADER\fR +Set header, displayed next to the info -.br -.BR default " Display from the bottom of the screen" -.br -.BR reverse " Display from the top of the screen" -.br -.BR reverse-list " Display from the top of the screen, prompt at the bottom" -.br +The given string will be printed as the sticky header. The lines are displayed in the given order from top to bottom regardless of \-\-layout option, and are not affected by \-\-with\-nth. ANSI color codes are processed even when \-\-ansi is not set. +.TP +\fB\-\-header\-lines\fR=\fIHEADER_LINES\fR [default: 0] +Number of lines of the input treated as header + +The first N lines of the input are treated as the sticky header. When \-\-with\-nth is set, the lines are transformed just like the other lines that follow. +.TP +\fB\-\-tmux\fR=\fITMUX\fR +Run in a tmux popup + +Format: sk \-\-tmux [,SIZE[%]][,SIZE[%]] +Depending on the direction, the order and behavior of the sizes varies: + \- center: (width, height) or (size, size) if only one is provided + \- top | bottom: (height, width) or height = size, width = 100% if only one is provided + \- left | right: (width, height) or height = 100%, width = size if only one is provided + +Default: center,50% .TP -.B "--reverse" -A synonym for \fB--layout=reverse\fB +\fB\-\-history\fR=\fIHISTORY\fR +History file +Load search history from the specified file and update the file on completion. When enabled, CTRL\-N and CTRL\-P are automatically remapped to next\-history and previous\-history. .TP -.BI "--margin=" MARGIN -Comma-separated expression for margins around the finder. -.br +\fB\-\-history\-size\fR=\fIHISTORY_SIZE\fR [default: 1000] +Maximum number of query history entries to keep +.TP +\fB\-\-cmd\-history\fR=\fICMD_HISTORY\fR +Command history file -.br -.RS -.BR TRBL " Same margin for top, right, bottom, and left" -.br -.BR TB,RL " Vertical, horizontal margin" -.br -.BR T,RL,B " Top, horizontal, bottom margin" -.br -.BR T,R,B,L " Top, right, bottom, left margin" -.br +Load command query history from the specified file and update the file on completion. When enabled, CTRL\-N and CTRL\-P are automatically remapped to next\-history and previous\-history. +.TP +\fB\-\-cmd\-history\-size\fR=\fICMD_HISTORY_SIZE\fR [default: 1000] +Maximum number of query history entries to keep +.TP +\fB\-\-preview\fR=\fIPREVIEW\fR +Preview command -.br -Each part can be given in absolute number or in percentage relative to the -terminal size with \fB%\fR suffix. -.br +Execute the given command for the current line and display the result on the preview window. {} in the command +is the placeholder that is replaced to the single\-quoted string of the current line. To transform the replace‐ +ment string, specify field index expressions between the braces (See FIELD INDEX EXPRESSION for the details). -.br -e.g. \fBsk --margin 10%\fR - \fBsk --margin 1,5%\fR -.RE -.TP -.B "--inline-info" -Display finder info inline with the query -.TP -.BI "-p --prompt=" "STR" -Input prompt (default: '> ') -.TP -.BI "--cmd-prompt=" "STR" -Command prompt (default: 'c> ') -.TP -.BI "--header=" "STR" -The given string will be printed as the sticky header. The lines are displayed -in the given order from top to bottom regardless of \fB--layout\fR option, and -are not affected by \fB--with-nth\fR. ANSI color codes are processed even when -\fB--ansi\fR is not set. -.TP -.BI "--header-lines=" "N" -The first N lines of the input are treated as the sticky header. When -\fB--with-nth\fR is set, the lines are transformed just like the other -lines that follow. -.TP -.BI "--keep-right" -Keep the right end of the line visible when it's too long. Effective only when -the query string is empty. -.TP -.BI "--skip-to-pattern" -Line will start with the start of the matched pattern. Effective only when -the query string is empty. Was designed to skip showing starts of paths of -rg/grep results. - - -.RS -e.g. \fBsk -i -c "rg {} --color=always" --skip-to-pattern '[^/]*:' --ansi\fR -.RE - -.TP -.BI "--no-clear-if-empty" -Do not clear previous items if new command returns empty result. This might be -useful to reduce flickering when typing new commands and the half-complete -commands are not valid. - -This is not default however because similar usecases for \fBgrep\fR and -\fBrg\fR had already been optimized where empty result of a query do mean -"empty" and previous results should be cleared. - -.TP -.BI "--show-cmd-error" -If the command fails, send the error messages and show them as items. This -option was intended to help debugging interactive commands. It's not enabled -by default because the command often fails before we complete the "cmd-query" -and error messages would be annoying. - -.SS Display -.TP -.B "--ansi" -Enable processing of ANSI color codes -.TP -.BI "--tabstop=" SPACES -Number of spaces for a tab character (default: 8) -.TP -.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" -Color configuration. The name of the base color scheme is followed by custom -color mappings. Ansi color code of -1 denotes terminal default -foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR -format. - -.RS -e.g. \fBsk --color=bg+:24\fR - \fBsk --color=light,fg:232,bg:255,bg+:116,info:27\fR -.RE - -.RS -.B BASE SCHEME: - (default: dark on 256-color terminal, otherwise 16) - - \fBdark \fRColor scheme for dark 256-color terminal - \fBlight \fRColor scheme for light 256-color terminal - \fB16 \fRColor scheme for 16-color terminal - \fBbw \fRNo colors - -.B COLOR: - \fBfg \fRText - \fBbg \fRBackground - \fBmatched|hl \fRText of highlighted substrings - \fBmatched_bg \fRBackground of highlighted substrings - \fBcurrent|fg+ \fRText (current line) - \fBcurrent_bg|bg+ \fRBackground (current line) - \fBcurrent_match|hl+ \fRText of Highlighted substrings (current line) - \fBcurrent_match_bg \fRBackground of highlighted substrings (current line) - \fBquery \fRText of Query (the texts after the prompt) - \fBquery_bg \fRBackground of Query - \fBinfo \fRInfo - \fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR) - \fBprompt \fRPrompt - \fBpointer|cursor \fRPointer to the current line (no effect now) - \fBmarker|selected \fRMulti-select marker - \fBspinner \fRStreaming input indicator - \fBheader \fRHeader -.RE -.SS History -.TP -.BI "--history=" "HISTORY_FILE" -Load search history from the specified file and update the file on completion. -When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to -\fBnext-history\fR and \fBprevious-history\fR. -.TP -.BI "--history-size=" "N" -Maximum number of entries in the history file (default: 1000). The file is -automatically truncated when the number of the lines exceeds the value. -.TP -.BI "--cmd-history=" "HISTORY_FILE" -Load command query history from the specified file and update the file on -completion. When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically -remapped to \fBnext-history\fR and \fBprevious-history\fR. -.TP -.BI "--cmd-history-size=" "N" -Maximum number of command query entries in the history file (default: 1000). -The file is automatically truncated when the number of the lines exceeds the -value. -.SS Preview -.TP -.BI "--preview=" "COMMAND" -Execute the given command for the current line and display the result on the -preview window. \fB{}\fR in the command is the placeholder that is replaced to -the single-quoted string of the current line. To transform the replacement -string, specify field index expressions between the braces (See \fBFIELD INDEX -EXPRESSION\fR for the details). - -.RS -e.g. \fBsk --preview='head -$LINES {}'\fR - \fBls -l | sk --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR - -sk overrides \fB$LINES\fR and \fB$COLUMNS\fR so that they represent the exact -size of the preview window. - -A placeholder expression starting with \fB+\fR flag will be replaced to the -space-separated list of the selected lines (or the current line if no selection -was made) individually quoted. +e.g. sk \-\-preview=\*(Aqhead \-$LINES {}\*(Aq + ls \-l | sk \-\-preview="echo user={3} when={\-4..\-2}; cat {\-1}" \-\-header\-lines=1 -e.g. - \fBsk --multi --preview='head -10 {+}' - git log --oneline | sk --multi --preview 'git show {+1}'\fR +sk overrides $LINES and $COLUMNS so that they represent the exact size of the preview window. +A placeholder expression starting with + flag will be replaced to the space\-separated list of the selected +lines (or the current line if no selection was made) individually quoted. + +e.g. + sk \-\-multi \-\-preview=\*(Aqhead \-10 {+}\*(Aq + git log \-\-oneline | sk \-\-multi \-\-preview \*(Aqgit show {+1}\*(Aq Note that you can escape a placeholder pattern by prepending a backslash. -Also, \fB{q}\fR is replaced to the current query string. \fB{cq}\fR is -replaced to the current command query string. \fB{n}\fR is replaced to -zero-based ordinal index of the line. Use \fB{+n}\fR if you want all index -numbers when multiple lines are selected - -Preview window will be updated even when there is no match for the current -query if any of the placeholder expressions evaluates to a non-empty string. -.RE -.TP -.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden][:+SCROLL[-OFFSET]]" - -.RS -.B POSITION: (default: right) - \fBup - \fBdown - \fBleft - \fBright -.RE - -Determine the layout of the preview window. If the argument ends with -\fB:hidden\fR, the preview window will be hidden by default until -\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. -Line wrap can be enabled with \fB:wrap\fR flag. - -If size is given as 0, preview window will not be visible, but sk will still -execute the command in the background. - -\fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview -window. \fBSCROLL\fR can be either a numeric integer or a single-field index -expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is -for adjusting the base offset so that you can see the text above it. It should -be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form -(\fB-/INTEGER\fR) for specifying a fraction of the preview window height. - -.RS -e.g. - \fB# Non-default scroll window positions and sizes - sk --preview="head {}" --preview-window=up:30% - sk --preview="file {}" --preview-window=down:2 +Also, {q} is replaced to the current query string. {cq} is replaced to the current command query string. {n} +is replaced to zero\-based ordinal index of the line. Use {+n} if you want all index numbers when multiple +lines are selected - # Initial scroll offset is set to the line number of each line of - # git grep output *minus* 5 lines (-5) - git grep --line-number '' | - sk --delimiter : --preview 'nl {1}' --preview-window +{2}-5 +Preview window will be updated even when there is no match for the current query if any of the placeholder ex‐ +pressions evaluates to a non\-empty string. +.TP +\fB\-\-preview\-window\fR=\fIPREVIEW_WINDOW\fR [default: right:50%] +Preview window layout - # Preview with bat, matching line in the middle of the window (-/2) - git grep --line-number '' | - sk --delimiter : \\ - --preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\ - --preview-window +{2}-/2\fR +Format: [up|down|left|right][:SIZE[%]][:hidden][:+SCROLL[\-OFFSET]] Determine the layout of the preview window. If the argument ends with :hidden, the preview window will be hidden by default until toggle\-preview action is triggered. Long lines are truncated by default. Line wrap can be enabled with :wrap flag. -.RE +If size is given as 0, preview window will not be visible, but sk will still execute the command in the background. -.SS Scripting ++SCROLL[\-OFFSET] determines the initial scroll offset of the preview window. SCROLL can be either a numeric integer or a single\-field index expression that refers to a numeric integer. The optional \-OFFSET part is for adjusting the base offset so that you can see the text above it. It should be given as a numeric integer (\-INTEGER), or as a denom‐ inator form (\-/INTEGER) for specifying a fraction of the preview window height. + +e.g. # Non\-default scroll window positions and sizes sk \-\-preview="head {}" \-\-preview\-window=up:30% sk \-\-preview="file {}" \-\-preview\-window=down:2 + +# Initial scroll offset is set to the line number of each line of # git grep output *minus* 5 lines (\-5) git grep \-\-line\-number \*(Aq\*(Aq | sk \-\-delimiter : \-\-preview \*(Aqnl {1}\*(Aq \-\-preview\-window +{2}\-5 + +# Preview with bat, matching line in the middle of the window (\-/2) git grep \-\-line\-number \*(Aq\*(Aq | sk \-\-delimiter : \\ \-\-preview \*(Aqbat \-\-style=numbers \-\-color=always \-\-highlight\-line {2} {1}\*(Aq \\ \-\-preview\-window +{2}\-/2 .TP -.BI "-q, --query=" "STR" -Start the finder with the given query +\fB\-q\fR, \fB\-\-query\fR=\fIQUERY\fR +Initial query .TP -.BI "--cmd-query=" "STR" -Specify the initial query for the command query +\fB\-\-cmd\-query\fR=\fICMD_QUERY\fR +Initial query in interactive mode .TP -.B "--print-query" -Print query as the first line +\fB\-\-expect\fR=\fIEXPECT\fR +Comma separated list of keys used to complete skim + +Comma\-separated list of keys that can be used to complete sk in addition to the default enter key. When this option is set, sk will print the name of the key pressed as the first line of its output (or as the second line if \-\-print\-query is also used). The line will be empty if sk is completed with the default enter key. If \-\-expect option is specified multiple times, sk will expect the union of the keys. \-\-no\-expect will clear the list. + +e.g. sk \-\-expect=ctrl\-v,ctrl\-t,alt\-s \-\-expect=f1,f2,~,@ .TP -.BI "-f, --filter=" "STR" -Filter mode. Do not start interactive finder. It's like a fuzzy-version of -grep. skim will output the score and the item to stdout. +\fB\-\-read0\fR +Read input delimited by ASCII NUL(\\\\0) characters .TP -.BI "--expect=" "KEY[,..]" -Comma-separated list of keys that can be used to complete sk in addition to -the default enter key. When this option is set, sk will print the name of the -key pressed as the first line of its output (or as the second line if -\fB--print-query\fR is also used). The line will be empty if sk is completed -with the default enter key. If \fB--expect\fR option is specified multiple -times, sk will expect the union of the keys. \fB--no-expect\fR will clear the -list. - -.RS -e.g. \fBsk --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR -.RE +\fB\-\-print0\fR +Print output delimited by ASCII NUL(\\\\0) characters .TP -.B "--read0" -Read input delimited by ASCII NUL characters instead of newline characters +\fB\-\-print\-query\fR +Print the query as the first line .TP -.B "--print0" -Print output delimited by ASCII NUL characters instead of newline characters +\fB\-\-print\-cmd\fR +Print the command as the first line (after print\-query) .TP -.B "--no-clear" -Do not clear finder interface on exit. If skim was started in full screen mode, -it will not switch back to the original screen, so you'll have to manually run -\fBtput rmcup\fR to return. This option can be used to avoid flickering of the -screen when your application needs to start skim multiple times in order. +\fB\-\-print\-score\fR +Print the command as the first line (after print\-cmd) .TP -.B "-1, --select-1" -Automatically select the only match +\fB\-1\fR, \fB\-\-select\-1\fR +Automatically select the match if there is only one .TP -.B "-0, --exit-0" -Exit immediately when there's no match +\fB\-0\fR, \fB\-\-exit\-0\fR +Automatically exit when no match is left .TP -.B "--sync" -Synchronous search for multi-staged filtering. If specified, skim will launch -ncurses finder only after the input stream is complete. +\fB\-\-sync\fR +Synchronous search for multi\-staged filtering -.RS -e.g. \fBsk --multi | sk --sync\fR -.RE +Synchronous search for multi\-staged filtering. If specified, skim will launch ncurses finder only after the input stream is complete. +e.g. sk \-\-multi | sk \-\-sync .TP -.B "--pre-select-n=NUM" -Pre-select the first \fBNUM\fR items in the multi-selection mode. +\fB\-\-pre\-select\-n\fR=\fIPRE_SELECT_N\fR [default: 0] +Pre\-select the first n items in multi\-selection mode .TP -.B "--pre-select-pat=REGEX" -Pre-select the items that matches the \fBREGEX\fR specified in multi-selection -mode. Check the doc for the detailed syntax: -.I https://docs.rs/regex/1.4.1/regex/ -.TP -.B "--pre-select-items=$'item1\(rsnitem2'" -Pre-select the specified items (separated by newline character) in -multi-selection mode. -.TP -.B "--pre-select-file=FILENAME" -Pre-select the items read from \fBFILENAME\fR (separated by newline -character) in multi-selection mode. +\fB\-\-pre\-select\-pat\fR=\fIPRE_SELECT_PAT\fR [default: ] +Pre\-select the matched items in multi\-selection mode +Check the doc for the detailed syntax: https://docs.rs/regex/1.4.1/regex/ .TP -.B "--version" -Display version information and exit +\fB\-\-pre\-select\-items\fR=\fIPRE_SELECT_ITEMS\fR [default: ] +Pre\-select the items separated by newline character -.SH ENVIRONMENT VARIABLES +Exemple: \*(Aqitem1\\nitem2\*(Aq .TP -.B SKIM_DEFAULT_COMMAND -Default command to use when input is tty. On *nix systems, sk runs the command -with \fBsh -c\fR, so make sure that it's POSIX-compliant. +\fB\-\-pre\-select\-file\fR=\fIPRE_SELECT_FILE\fR +Pre\-select the items read from this file .TP -.B SKIM_DEFAULT_OPTIONS -Default options. e.g. \fBexport SKIM_DEFAULT_OPTIONS="--multi\fR - -.SH EXIT STATUS -.BR 0 " Normal exit" -.br -.BR 1 " No match" -.br -.BR 2 " Error" -.br -.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" - -.SH FIELD INDEX EXPRESSION - -A field index expression can be a non-zero integer or a range expression -([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list -of field index expressions. - -.SS Examples -.BR 1 " The 1st field" -.br -.BR 2 " The 2nd field" -.br -.BR -1 " The last field" -.br -.BR -2 " The 2nd to last field" -.br -.BR 3..5 " From the 3rd field to the 5th field" -.br -.BR 2.. " From the 2nd field to the last field" -.br -.BR ..-3 " From the 1st field to the 3rd to the last field" -.br -.BR .. " All the fields" -.br - -.SH EXTENDED SEARCH MODE - -Unless specified otherwise, sk will start in "extended-search mode". In this -mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild -^music .mp3$ sbtrkt !rmx\fR - -You can prepend a backslash to a space (\fB\\ \fR) to match a literal space -character. - -.SS Exact-match (quoted) -A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as -an "exact-match" (or "non-fuzzy") term. sk will search for the exact -occurrences of the string. - -.SS Anchored-match -A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an -anchored-match term. Then sk will search for the lines that start with or end -with the given string. An anchored-match term is also an exact-match term. - -.SS Negation -If a term is prefixed by \fB!\fR, sk will exclude the lines that satisfy the -term from the result. In this case, sk performs exact match by default. - -.SS Exact-match by default -If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with -\fB'\fR) every word, start sk with \fB-e\fR or \fB--exact\fR option. Note that -when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. - -.SS OR operator -A single bar character term acts as an OR operator. For example, the following -query matches entries that start with \fBcore\fR and end with either \fBgo\fR, -\fBrb\fR, or \fBpy\fR. - -e.g. \fB^core go$ | rb$ | py$\fR - -.SH KEY BINDINGS -You can customize key bindings of sk with \fB--bind\fR option which takes -a comma-separated list of key binding expressions. Each key binding expression -follows the following format: \fBKEY:ACTION\fR - -e.g. \fBsk --bind=ctrl-j:accept,ctrl-k:kill-line\fR - -.B AVAILABLE KEYS: (SYNONYMS) - \fIctrl-[a-z]\fR - \fIctrl-space\fR - \fIctrl-alt-[a-z]\fR - \fIalt-[a-zA-Z]\fR - \fIalt-[0-9]\fR - \fIf[1-12]\fR - \fIenter\fR (\fIctrl-m\fR) - \fIspace\fR - \fIbspace\fR (\fIbs\fR) - \fIalt-up\fR - \fIalt-down\fR - \fIalt-left\fR - \fIalt-right\fR - \fIalt-enter\fR (\fIalt-ctrl-m\fR) - \fIalt-space\fR - \fIalt-bspace\fR (\fIalt-bs\fR) - \fIalt-/\fR - \fItab\fR - \fIbtab\fR (\fIshift-tab\fR) - \fIesc\fR - \fIdel\fR - \fIup\fR - \fIdown\fR - \fIleft\fR - \fIright\fR - \fIhome\fR - \fIend\fR - \fIpgup\fR (\fIpage-up\fR) - \fIpgdn\fR (\fIpage-down\fR) - \fIshift-up\fR - \fIshift-down\fR - \fIshift-left\fR - \fIshift-right\fR - \fIalt-shift-up\fR - \fIalt-shift-down\fR - \fIalt-shift-left\fR - \fIalt-shift-right\fR - or any single character - - \fBACTION: DEFAULT BINDINGS (NOTES): - \fBabort\fR \fIctrl-c ctrl-q esc\fR - \fBaccept\fR \fIenter\fR - \fBappend-and-select\fR - \fBbackward-char\fR \fIctrl-b left\fR - \fBbackward-delete-char\fR \fIctrl-h bspace\fR - \fBbackward-kill-word\fR \fIalt-bs\fR - \fBbackward-word\fR \fIalt-b shift-left\fR - \fBbeginning-of-line\fR \fIctrl-a home\fR - \fBclear-screen\fR \fIctrl-l\fR - \fBdelete-char\fR \fIdel\fR - \fBdelete-charEOF\fR \fIctrl-d\fR - \fBdeselect-all\fR - \fBdown\fR \fIctrl-j ctrl-n down\fR - \fBend-of-line\fR \fIctrl-e end\fR - \fBexecute(...)\fR (see below for the details) - \fBexecute-silent(...)\fR (see below for the details) - \fBforward-char\fR \fIctrl-f right\fR - \fBforward-word\fR \fIalt-f shift-right\fR - \fBif-non-matched\fR - \fBif-query-empty\fR - \fBif-query-not-empty\fR - \fBignore\fR - \fBkill-line\fR - \fBkill-word\fR \fIalt-d\fR - \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR or \fB--cmd-history\fR) - \fBpage-down\fR \fIpgdn\fR - \fBpage-up\fR \fIpgup\fR - \fBhalf-page-down\fR - \fBhalf-page-up\fR - \fBpreview-up\fR \fIshift-up\fR - \fBpreview-down\fR \fIshift-down\fR - \fBpreview-left\fR - \fBpreview-right\fR - \fBpreview-page-down\fR - \fBpreview-page-up\fR - \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR or \fB--cmd-history\fR) - \fBselect-all\fR - \fBtoggle\fR - \fBtoggle-all\fR - \fBtoggle+down\fR \fIctrl-i (tab)\fR - \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) - \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) - \fBtoggle-preview\fR - \fBtoggle-preview-wrap\fR - \fBtoggle-sort\fR - \fBtoggle+up\fR \fIbtab (shift-tab)\fR - \fBunix-line-discard\fR \fIctrl-u\fR - \fBunix-word-rubout\fR \fIctrl-w\fR - \fBup\fR \fIctrl-k ctrl-p up\fR - \fByank\fR \fIctrl-y\fR - -Multiple actions can be chained using \fB+\fR separator. - - \fBsk --bind 'ctrl-a:select-all+accept'\fR - -With \fBexecute(...)\fR action, you can execute arbitrary commands without -leaving sk. For example, you can turn sk into a simple file browser by -binding \fBenter\fR key to \fBless\fR command like follows. - - \fBsk --bind "enter:execute(less {})"\fR - -You can use the same placeholder expressions as in \fB--preview\fR. - -If the command contains parentheses, sk may fail to parse the expression. In -that case, you can use any of the following alternative notations to avoid -parse errors. - - \fBexecute[...]\fR - \fBexecute'...'\fR - \fBexecute"..."\fR - \fBexecute:...\fR -.RS -This is the special form that frees you from parse errors as it does not expect -the closing character. The catch is that it should be the last one in the -comma-separated list of key-action pairs. -.RE - -sk switches to the alternate screen when executing a command. However, if the -command is expected to complete quickly, and you are not interested in its -output, you might want to use \fBexecute-silent\fR instead, which silently -executes the command without the switching. Note that sk will not be -responsive until the command is complete. For asynchronous execution, start -your command as a background process (i.e. appending \fB&\fR). - -With \fBif-query-empty\fR and \fBif-query-not-empty\fR action, you could -specify the action to execute depends on the query condition. For example - - \fBsk --bind 'ctrl-d:if-query-empty(abort)+delete-char'\fR - -If the query is empty, skim will execute \fBabort\fR action, otherwise execute -\fBdelete-char\fR action. It is equal to `delete-char/eof`. - -.SH AUTHOR -Jinzhou Zhang (\fIlotabout@gmail.com\fR) - -.SH SEE ALSO -.B Project homepage: -.RS -.I https://github.com/lotabout/skim -.RE -.br - -.br -.B Extra Vim plugin: -.RS -.I https://github.com/lotabout/skim.vim -.RE - -.SH LICENSE -MIT +\fB\-f\fR, \fB\-\-filter\fR=\fIFILTER\fR +Query for filter mode +.TP +\fB\-h\fR, \fB\-\-help\fR +Print help (see a summary with \*(Aq\-h\*(Aq) diff --git a/shell/completion.bash b/shell/completion.bash index d905383a..fc26d86f 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -1,373 +1,222 @@ -# ____ ____ -# / __/___ / __/ -# / /_/_ / / /_ -# / __/ / /_/ __/ -# /_/ /___/_/ completion.bash -# -# - $SKIM_TMUX (default: 0) -# - $SKIM_TMUX_OPTS (default: empty) -# - $SKIM_COMPLETION_TRIGGER (default: '**') -# - $SKIM_COMPLETION_OPTS (default: empty) -# copied and modified from https://github.com/junegunn/fzf/blob/master/shell/completion.bash - -if [[ $- =~ i ]]; then - -# To use custom commands instead of find, override _skim_compgen_{path,dir} -if ! declare -f _skim_compgen_path > /dev/null; then - _skim_compgen_path() { - echo "$1" - command find -L "$1" \ - -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ - -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' - } -fi - -if ! declare -f _skim_compgen_dir > /dev/null; then - _skim_compgen_dir() { - command find -L "$1" \ - -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ - -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' - } -fi - -########################################################### - -# To redraw line after skim closes (printf '\e[5n') -bind '"\e[0n": redraw-current-line' - -__skim_comprun() { - if [ "$(type -t _skim_comprun 2>&1)" = function ]; then - _skim_comprun "$@" - elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then - shift - sk-tmux ${SKIM_TMUX_OPTS:--d${SKIM_TMUX_HEIGHT:-40%}} -- "$@" - else - shift - sk "$@" - fi -} - -__skim_orig_completion_filter() { - sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _skim_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__skim_nospace_commands" = *" \3 "* ]] \&\& __skim_nospace_commands="$__skim_nospace_commands \3 ";/' | - awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1' -} - -_skim_opts_completion() { - local cur prev opts - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - opts=" - -x --extended - -e --exact - --algo - -i +i - -n --nth - --with-nth - -d --delimiter - +s --no-sort - --tac - --tiebreak - -m --multi - --no-mouse - --bind - --cycle - --no-hscroll - --jump-labels - --height - --literal - --reverse - --margin - --inline-info - --prompt - --pointer - --marker - --header - --header-lines - --ansi - --tabstop - --color - --no-bold - --history - --history-size - --preview - --preview-window - -q --query - -1 --select-1 - -0 --exit-0 - -f --filter - --print-query - --expect - --sync" - - case "${prev}" in - --tiebreak) - COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") ) - return 0 - ;; - --color) - COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") ) - return 0 - ;; - --history) +_sk() { + local i cur prev opts cmd COMPREPLY=() - return 0 - ;; - esac - - if [[ "$cur" =~ ^-|\+ ]]; then - COMPREPLY=( $(compgen -W "${opts}" -- "$cur") ) - return 0 - fi - - return 0 -} - -_skim_handle_dynamic_completion() { - local cmd orig_var orig ret orig_cmd orig_complete - cmd="$1" - shift - orig_cmd="$1" - orig_var="_skim_orig_completion_$cmd" - orig="${!orig_var##*#}" - if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then - $orig "$@" - elif [ -n "$_skim_completion_loader" ]; then - orig_complete=$(complete -p "$orig_cmd" 2> /dev/null) - _completion_loader "$@" - ret=$? - # _completion_loader may not have updated completion for the command - if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then - eval "$(complete | command grep " -F.* $orig_cmd$" | __skim_orig_completion_filter)" - if [[ "$__skim_nospace_commands" = *" $orig_cmd "* ]]; then - eval "${orig_complete/ -F / -o nospace -F }" - else - eval "$orig_complete" - fi - fi - return $ret - fi -} - -__skim_generic_path_completion() { - local cur base dir leftover matches trigger cmd - cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" - COMPREPLY=() - trigger=${SKIM_COMPLETION_TRIGGER-'**'} - cur="${COMP_WORDS[COMP_CWORD]}" - if [[ "$cur" == *"$trigger" ]]; then - base=${cur:0:${#cur}-${#trigger}} - eval "base=$base" - - [[ $base = *"/"* ]] && dir="$base" - while true; do - if [ -z "$dir" ] || [ -d "$dir" ]; then - leftover=${base/#"$dir"} - leftover=${leftover/#\/} - [ -z "$dir" ] && dir='.' - [ "$dir" != "/" ] && dir="${dir/%\//}" - matches=$(eval "$1 $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $2" __skim_comprun "$4" -q "$leftover" | while read -r item; do - printf "%q$3 " "$item" - done) - matches=${matches% } - [[ -z "$3" ]] && [[ "$__skim_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches " - if [ -n "$matches" ]; then - COMPREPLY=( "$matches" ) - else - COMPREPLY=( "$cur" ) - fi - printf '\e[5n' - return 0 - fi - dir=$(dirname "$dir") - [[ "$dir" =~ /$ ]] || dir="$dir"/ + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="" + opts="" + + for i in ${COMP_WORDS[@]} + do + case "${cmd},${i}" in + ",$1") + cmd="sk" + ;; + *) + ;; + esac done - else - shift - shift - shift - _skim_handle_dynamic_completion "$cmd" "$@" - fi -} - -_skim_complete() { - # Split arguments around -- - local args rest str_arg i sep - args=("$@") - sep= - for i in "${!args[@]}"; do - if [[ "${args[$i]}" = -- ]]; then - sep=$i - break - fi - done - if [[ -n "$sep" ]]; then - str_arg= - rest=("${args[@]:$((sep + 1)):${#args[@]}}") - args=("${args[@]:0:$sep}") - else - str_arg=$1 - args=() - shift - rest=("$@") - fi - local cur selected trigger cmd post - post="$(caller 0 | awk '{print $2}')_post" - type -t "$post" > /dev/null 2>&1 || post=cat - - cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" - trigger=${SKIM_COMPLETION_TRIGGER-'**'} - cur="${COMP_WORDS[COMP_CWORD]}" - if [[ "$cur" == *"$trigger" ]]; then - cur=${cur:0:${#cur}-${#trigger}} - - selected=$(SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ') - selected=${selected% } # Strip trailing space not to repeat "-o nospace" - if [ -n "$selected" ]; then - COMPREPLY=("$selected") - else - COMPREPLY=("$cur") - fi - printf '\e[5n' - return 0 - else - _skim_handle_dynamic_completion "$cmd" "${rest[@]}" - fi -} - -_skim_path_completion() { - __skim_generic_path_completion _skim_compgen_path "-m" "" "$@" -} - -# Deprecated. No file only completion. -_skim_file_completion() { - _skim_path_completion "$@" -} - -_skim_dir_completion() { - __skim_generic_path_completion _skim_compgen_dir "" "/" "$@" -} - -_skim_complete_kill() { - local trigger=${SKIM_COMPLETION_TRIGGER-'**'} - local cur="${COMP_WORDS[COMP_CWORD]}" - if [[ -z "$cur" ]]; then - COMP_WORDS[$COMP_CWORD]=$trigger - elif [[ "$cur" != *"$trigger" ]]; then - return 1 - fi - - _skim_proc_completion "$@" -} - -_skim_proc_completion() { - _skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( - command ps -ef | sed 1d - ) -} - -_skim_proc_completion_post() { - awk '{print $2}' -} - -_skim_host_completion() { - _skim_complete --no-multi -- "$@" < <( - 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 | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ - <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | - awk '{if (length($2) > 0) {print $2}}' | sort -u - ) -} - -_skim_var_completion() { - _skim_complete -m -- "$@" < <( - declare -xp | sed 's/=.*//' | sed 's/.* //' - ) -} - -_skim_alias_completion() { - _skim_complete -m -- "$@" < <( - alias | sed 's/=.*//' | sed 's/.* //' - ) -} - -# skim options -complete -o default -F _skim_opts_completion sk - -d_cmds="${SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" -a_cmds=" - awk cat diff diff3 - emacs emacsclient ex file ftp g++ gcc gvim head hg java - javac ld less more mvim nvim patch perl python ruby - sed sftp sort source tail tee uniq vi view vim wc xdg-open - basename bunzip2 bzip2 chmod chown curl cp dirname du - find git grep gunzip gzip hg jar - ln ls mv open rm rsync scp - svn tar unzip zip" - -# Preserve existing completion -eval "$(complete | - sed -E '/-F/!d; / _skim/d; '"/ ($(echo $d_cmds $a_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' | - __skim_orig_completion_filter)" - -if type _completion_loader > /dev/null 2>&1; then - _skim_completion_loader=1 -fi - -__skim_defc() { - local cmd func opts orig_var orig def - cmd="$1" - func="$2" - opts="$3" - orig_var="_skim_orig_completion_${cmd//[^A-Za-z0-9_]/_}" - orig="${!orig_var}" - if [ -n "$orig" ]; then - printf -v def "$orig" "$func" - eval "$def" - else - complete -F "$func" $opts "$cmd" - fi -} - -# Anything -for cmd in $a_cmds; do - __skim_defc "$cmd" _skim_path_completion "-o default -o bashdefault" -done - -# Directory -for cmd in $d_cmds; do - __skim_defc "$cmd" _skim_dir_completion "-o nospace -o dirnames" -done - -# Kill completion (supports empty completion trigger) -complete -F _skim_complete_kill -o default -o bashdefault kill - -unset cmd d_cmds a_cmds - -_skim_setup_completion() { - local kind fn cmd - kind=$1 - fn=_skim_${1}_completion - if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then - echo "usage: ${FUNCNAME[0]} path|dir|var|alias|host|proc COMMANDS..." - return 1 - fi - shift - eval "$(complete -p "$@" 2> /dev/null | grep -v "$fn" | __skim_orig_completion_filter)" - for cmd in "$@"; do - case "$kind" in - dir) __skim_defc "$cmd" "$fn" "-o nospace -o dirnames" ;; - var) __skim_defc "$cmd" "$fn" "-o default -o nospace -v" ;; - alias) __skim_defc "$cmd" "$fn" "-a" ;; - *) __skim_defc "$cmd" "$fn" "-o default -o bashdefault" ;; + case "${cmd}" in + sk) + opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h --tac --no-sort --tiebreak --nth --with-nth --delimiter --exact --regex --algo --case --bind --multi --no-multi --no-mouse --cmd --interactive --color --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --ansi --tabstop --inline-info --header --header-lines --history --history-size --cmd-history --cmd-history-size --preview --preview-window --query --cmd-query --expect --read0 --print0 --print-query --print-cmd --print-score --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --tmux --extended --literal --cycle --hscroll-off --filepath-word --jump-labels --border --no-bold --info --pointer --marker --phony --help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --tiebreak) + COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length" -- "${cur}")) + return 0 + ;; + -t) + COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length" -- "${cur}")) + return 0 + ;; + --nth) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -n) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --with-nth) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --delimiter) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -d) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --algo) + COMPREPLY=($(compgen -W "skim_v1 skim_v2 clangd" -- "${cur}")) + return 0 + ;; + --case) + COMPREPLY=($(compgen -W "respect ignore smart" -- "${cur}")) + return 0 + ;; + --bind) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -b) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --cmd) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -c) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -I) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --color) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --skip-to-pattern) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --layout) + COMPREPLY=($(compgen -W "default reverse reverse-list" -- "${cur}")) + return 0 + ;; + --height) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --min-height) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --margin) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --prompt) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --cmd-prompt) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --tabstop) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --header) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --header-lines) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --history) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --history-size) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --cmd-history) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --cmd-history-size) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --preview) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --preview-window) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --query) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -q) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --cmd-query) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --expect) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --pre-select-n) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --pre-select-pat) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --pre-select-items) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --pre-select-file) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --filter) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -f) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --tmux) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --hscroll-off) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --jump-labels) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; esac - done } -# Environment variables / Aliases / Hosts -_skim_setup_completion 'var' export unset -_skim_setup_completion 'alias' unalias -_skim_setup_completion 'host' ssh telnet - +if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then + complete -F _sk -o nosort -o bashdefault -o default sk +else + complete -F _sk -o bashdefault -o default sk fi diff --git a/shell/completion.zsh b/shell/completion.zsh index 6a6b2f4f..91aba866 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -1,329 +1,120 @@ -# ____ ____ -# / __/___ / __/ -# / /_/_ / / /_ -# / __/ / /_/ __/ -# /_/ /___/_/ completion.zsh -# -# - $SKIM_TMUX (default: 0) -# - $SKIM_TMUX_OPTS (default: '-d 40%') -# - $SKIM_COMPLETION_TRIGGER (default: '**') -# - $SKIM_COMPLETION_OPTS (default: empty) +#compdef sk -# Both branches of the following `if` do the same thing -- define -# __skim_completion_options such that `eval $__skim_completion_options` sets -# all options to the same values they currently have. We'll do just that at -# the bottom of the file after changing options to what we prefer. -# -# IMPORTANT: Until we get to the `emulate` line, all words that *can* be quoted -# *must* be quoted in order to prevent alias expansion. In addition, code must -# be written in a way works with any set of zsh options. This is very tricky, so -# careful when you change it. -# -# Start by loading the builtin zsh/parameter module. It provides `options` -# associative array that stores current shell options. -if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then - # This is the fast branch and it gets taken on virtually all Zsh installations. - # - # ${(kv)options[@]} expands to array of keys (option names) and values ("on" - # or "off"). The subsequent expansion# with (j: :) flag joins all elements - # together separated by spaces. __skim_completion_options ends up with a value - # like this: "options=(shwordsplit off aliases on ...)". - __skim_completion_options="options=(${(j: :)${(kv)options[@]}})" -else - # This branch is much slower because it forks to get the names of all - # zsh options. It's possible to eliminate this fork but it's not worth the - # trouble because this branch gets taken only on very ancient or broken - # zsh installations. - () { - # That `()` above defines an anonymous function. This is essentially a scope - # for local parameters. We use it to avoid polluting global scope. - 'local' '__skim_opt' - __skim_completion_options="setopt" - # `set -o` prints one line for every zsh option. Each line contains option - # name, some spaces, and then either "on" or "off". We just want option names. - # Expansion with (@f) flag splits a string into lines. The outer expansion - # removes spaces and everything that follow them on every line. __skim_opt - # ends up iterating over option names: shwordsplit, aliases, etc. - for __skim_opt in "${(@)${(@f)$(set -o)}%% *}"; do - if [[ -o "$__skim_opt" ]]; then - # Option $__skim_opt is currently on, so remember to set it back on. - __skim_completion_options+=" -o $__skim_opt" - else - # Option $__skim_opt is currently off, so remember to set it back off. - __skim_completion_options+=" +o $__skim_opt" - fi - done - # The value of __skim_completion_options here looks like this: - # "setopt +o shwordsplit -o aliases ..." - } -fi - -# Enable the default zsh options (those marked with in `man zshoptions`) -# but without `aliases`. Aliases in functions are expanded when functions are -# defined, so if we disable aliases here, we'll be sure to have no pesky -# aliases in any of our functions. This way we won't need prefix every -# command with `command` or to quote every word to defend against global -# aliases. Note that `aliases` is not the only option that's important to -# control. There are several others that could wreck havoc if they are set -# to values we don't expect. With the following `emulate` command we -# sidestep this issue entirely. -'emulate' 'zsh' '-o' 'no_aliases' - -# This brace is the start of try-always block. The `always` part is like -# `finally` in lesser languages. We use it to *always* restore user options. -{ +autoload -U is-at-least -# Bail out if not interactive shell. -[[ -o interactive ]] || return 0 - -# To use custom commands instead of find, override _skim_compgen_{path,dir} -if ! declare -f _skim_compgen_path > /dev/null; then - _skim_compgen_path() { - echo "$1" - command find -L "$1" \ - -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ - -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' - } -fi - -if ! declare -f _skim_compgen_dir > /dev/null; then - _skim_compgen_dir() { - command find -L "$1" \ - -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ - -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' - } -fi +_sk() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 -########################################################### - -__skim_comprun() { - if [[ "$(type _skim_comprun 2>&1)" =~ function ]]; then - _skim_comprun "$@" - elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then - shift - if [ -n "$SKIM_TMUX_OPTS" ]; then - sk-tmux ${(Q)${(Z+n+)SKIM_TMUX_OPTS}} -- "$@" + if is-at-least 5.2; then + _arguments_options=(-s -S -C) else - sk-tmux -d ${SKIM_TMUX_HEIGHT:-40%} -- "$@" - fi - else - shift - sk "$@" - fi -} - -# Extract the name of the command. e.g. foo=1 bar baz** -__skim_extract_command() { - local token tokens - tokens=(${(z)1}) - for token in $tokens; do - token=${(Q)token} - if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then - echo "$token" - return - fi - done - echo "${tokens[1]}" -} - -__skim_generic_path_completion() { - local base lbuf cmd compgen skim_opts suffix tail dir leftover matches - base=$1 - lbuf=$2 - cmd=$(__skim_extract_command "$lbuf") - compgen=$3 - skim_opts=$4 - suffix=$5 - tail=$6 - - setopt localoptions nonomatch - eval "base=$base" - [[ $base = *"/"* ]] && dir="$base" - while [ 1 ]; do - if [[ -z "$dir" || -d ${dir} ]]; then - leftover=${base/#"$dir"} - leftover=${leftover/#\/} - [ -z "$dir" ] && dir='.' - [ "$dir" != "/" ] && dir="${dir/%\//}" - matches=$(eval "$compgen $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS" __skim_comprun "$cmd" ${(Q)${(Z+n+)skim_opts}} -q "$leftover" | while read item; do - echo -n "${(q)item}$suffix " - done) - matches=${matches% } - if [ -n "$matches" ]; then - LBUFFER="$lbuf$matches$tail" - fi - zle reset-prompt - break + _arguments_options=(-s -C) fi - dir=$(dirname "$dir") - dir=${dir%/}/ - done -} - -_skim_path_completion() { - __skim_generic_path_completion "$1" "$2" _skim_compgen_path \ - "-m" "" " " -} -_skim_dir_completion() { - __skim_generic_path_completion "$1" "$2" _skim_compgen_dir \ - "" "/" "" -} - -_skim_feed_fifo() ( - command rm -f "$1" - mkfifo "$1" - cat <&0 > "$1" & -) - -_skim_complete() { - setopt localoptions ksh_arrays - # Split arguments around -- - local args rest str_arg i sep - args=("$@") - sep= - for i in {0..${#args[@]}}; do - if [[ "${args[$i]}" = -- ]]; then - sep=$i - break - fi - done - if [[ -n "$sep" ]]; then - str_arg= - rest=("${args[@]:$((sep + 1)):${#args[@]}}") - args=("${args[@]:0:$sep}") - else - str_arg=$1 - args=() - shift - rest=("$@") - fi - - local fifo lbuf cmd matches post - fifo="${TMPDIR:-/tmp}/skim-complete-fifo-$$" - lbuf=${rest[0]} - cmd=$(__skim_extract_command "$lbuf") - post="${funcstack[1]}_post" - type $post > /dev/null 2>&1 || post=cat - - _skim_feed_fifo "$fifo" - matches=$(SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') - if [ -n "$matches" ]; then - LBUFFER="$lbuf$matches" - fi - zle reset-prompt - command rm -f "$fifo" -} - -_skim_complete_telnet() { - _skim_complete --no-multi -- "$@" < <( - command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | - awk '{if (length($2) > 0) {print $2}}' | sort -u - ) -} - -_skim_complete_ssh() { - _skim_complete --no-multi -- "$@" < <( - 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 | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ - <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | - awk '{if (length($2) > 0) {print $2}}' | sort -u - ) -} - -_skim_complete_export() { - _skim_complete -m -- "$@" < <( - declare -xp | sed 's/=.*//' | sed 's/.* //' - ) -} - -_skim_complete_unset() { - _skim_complete -m -- "$@" < <( - declare -xp | sed 's/=.*//' | sed 's/.* //' - ) -} - -_skim_complete_unalias() { - _skim_complete --no-multi -- "$@" < <( - alias | sed 's/=.*//' - ) -} - -_skim_complete_kill() { - _skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( - command ps -ef | sed 1d - ) -} - -_skim_complete_kill_post() { - awk '{print $2}' -} - -skim-completion() { - local tokens cmd prefix trigger tail matches lbuf d_cmds - setopt localoptions noshwordsplit noksh_arrays noposixbuiltins - - # http://zsh.sourceforge.net/FAQ/zshfaq03.html - # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags - tokens=(${(z)LBUFFER}) - if [ ${#tokens} -lt 1 ]; then - zle ${skim_default_completion:-expand-or-complete} - return - fi - - cmd=$(__skim_extract_command "$LBUFFER") - - # Explicitly allow for empty trigger. - trigger=${SKIM_COMPLETION_TRIGGER-'**'} - [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") - - # When the trigger starts with ';', it becomes a separate token - if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then - tokens[-2]="${tokens[-2]}${tokens[-1]}" - tokens=(${tokens[0,-2]}) - fi - - lbuf=$LBUFFER - tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} - # Kill completion (do not require trigger sequence) - if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then - tail=$trigger - tokens+=$trigger - lbuf="$lbuf$trigger" - fi - - # Trigger sequence given - if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then - d_cmds=(${=SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) - - [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} - [ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}} - - if eval "type _skim_complete_${cmd} > /dev/null"; then - prefix="$prefix" eval _skim_complete_${cmd} ${(q)lbuf} - elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then - _skim_dir_completion "$prefix" "$lbuf" - else - _skim_path_completion "$prefix" "$lbuf" - fi - # Fall back to default completion - else - zle ${skim_default_completion:-expand-or-complete} - fi -} - -[ -z "$skim_default_completion" ] && { - binding=$(bindkey '^I') - [[ $binding =~ 'undefined-key' ]] || skim_default_completion=$binding[(s: :w)2] - unset binding -} - -zle -N skim-completion -bindkey '^I' skim-completion - -} always { - # Restore the original options. - eval $__skim_completion_options - 'unset' '__skim_completion_options' -} + local context curcontext="$curcontext" state line + _arguments "${_arguments_options[@]}" : \ +'*-t+[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length)' \ +'*--tiebreak=[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length)' \ +'*-n+[Fields to be matched]:NTH:_default' \ +'*--nth=[Fields to be matched]:NTH:_default' \ +'*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \ +'-d+[Delimiter between fields]:DELIMITER:_default' \ +'--delimiter=[Delimiter between fields]:DELIMITER:_default' \ +'--algo=[Fuzzy matching algorithm]:ALGORITHM:(skim_v1 skim_v2 clangd)' \ +'--case=[Case sensitivity]:CASE:(respect ignore smart)' \ +'*-b+[Comma separated list of bindings]:BIND:_default' \ +'*--bind=[Comma separated list of bindings]:BIND:_default' \ +'-c+[Command to invoke dynamically in interactive mode]:CMD:_default' \ +'--cmd=[Command to invoke dynamically in interactive mode]:CMD:_default' \ +'-I+[Replace replstr with the selected item in commands]:REPLSTR:_default' \ +'--color=[Set color theme]:COLOR:_default' \ +'--skip-to-pattern=[Show the matched pattern at the line start]:SKIP_TO_PATTERN:_default' \ +'--layout=[Set layout]:LAYOUT:(default reverse reverse-list)' \ +'--height=[Height of skim'\''s window]:HEIGHT:_default' \ +'--min-height=[Minimum height of skim'\''s window]:MIN_HEIGHT:_default' \ +'--margin=[Screen margin]:MARGIN:_default' \ +'-p+[Set prompt]:PROMPT:_default' \ +'--prompt=[Set prompt]:PROMPT:_default' \ +'--cmd-prompt=[Set prompt in command mode]:CMD_PROMPT:_default' \ +'--tabstop=[Number of spaces that make up a tab]:TABSTOP:_default' \ +'--header=[Set header, displayed next to the info]:HEADER:_default' \ +'--header-lines=[Number of lines of the input treated as header]:HEADER_LINES:_default' \ +'--history=[History file]:HISTORY:_default' \ +'--history-size=[Maximum number of query history entries to keep]:HISTORY_SIZE:_default' \ +'--cmd-history=[Command history file]:CMD_HISTORY:_default' \ +'--cmd-history-size=[Maximum number of query history entries to keep]:CMD_HISTORY_SIZE:_default' \ +'--preview=[Preview command]:PREVIEW:_default' \ +'--preview-window=[Preview window layout]:PREVIEW_WINDOW:_default' \ +'-q+[Initial query]:QUERY:_default' \ +'--query=[Initial query]:QUERY:_default' \ +'--cmd-query=[Initial query in interactive mode]:CMD_QUERY:_default' \ +'*--expect=[Comma separated list of keys used to complete skim]:EXPECT:_default' \ +'--pre-select-n=[Pre-select the first n items in multi-selection mode]:PRE_SELECT_N:_default' \ +'--pre-select-pat=[Pre-select the matched items in multi-selection mode]:PRE_SELECT_PAT:_default' \ +'--pre-select-items=[Pre-select the items separated by newline character]:PRE_SELECT_ITEMS:_default' \ +'--pre-select-file=[Pre-select the items read from this file]:PRE_SELECT_FILE:_default' \ +'-f+[Query for filter mode]:FILTER:_default' \ +'--filter=[Query for filter mode]:FILTER:_default' \ +'--tmux=[Reserved for later use]' \ +'--hscroll-off=[Reserved for later use]:HSCROLL_OFF:_default' \ +'--jump-labels=[Reserved for later use]:JUMP_LABELS:_default' \ +'--tac[Show results in reverse order]' \ +'--no-sort[Do not sort the results]' \ +'-e[Run in exact mode]' \ +'--exact[Run in exact mode]' \ +'--regex[Start in regex mode instead of fuzzy-match]' \ +'-m[Enable multiple selection]' \ +'--multi[Enable multiple selection]' \ +'(-m --multi)--no-multi[Disable multiple selection]' \ +'--no-mouse[Disable mouse]' \ +'-i[Run in interactive mode]' \ +'--interactive[Run in interactive mode]' \ +'--no-hscroll[Disable horizontal scroll]' \ +'--keep-right[Keep the right end of the line visible on overflow]' \ +'--no-clear-if-empty[Do not clear previous line if the command returns an empty result]' \ +'--no-clear-start[Do not clear items on start]' \ +'--no-clear[Do not clear screen on exit]' \ +'--show-cmd-error[Show error message if command fails]' \ +'--reverse[Shorthand for reverse layout]' \ +'--no-height[Disable height feature]' \ +'--ansi[Parse ANSI color codes in input strings]' \ +'--inline-info[Display info next to the query]' \ +'--read0[Read input delimited by ASCII NUL(\\\\0) characters]' \ +'--print0[Print output delimited by ASCII NUL(\\\\0) characters]' \ +'--print-query[Print the query as the first line]' \ +'--print-cmd[Print the command as the first line (after print-query)]' \ +'--print-score[Print the command as the first line (after print-cmd)]' \ +'-1[Automatically select the match if there is only one]' \ +'--select-1[Automatically select the match if there is only one]' \ +'-0[Automatically exit when no match is left]' \ +'--exit-0[Automatically exit when no match is left]' \ +'--sync[Synchronous search for multi-staged filtering]' \ +'-x[Reserved for later use]' \ +'--extended[Reserved for later use]' \ +'--literal[Reserved for later use]' \ +'--cycle[Reserved for later use]' \ +'--filepath-word[Reserved for later use]' \ +'--border[Reserved for later use]' \ +'--no-bold[Reserved for later use]' \ +'--info[Reserved for later use]' \ +'--pointer[Reserved for later use]' \ +'--marker[Reserved for later use]' \ +'--phony[Reserved for later use]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +&& ret=0 +} + +(( $+functions[_sk_commands] )) || +_sk_commands() { + local commands; commands=() + _describe -t commands 'sk commands' commands "$@" +} + +if [ "$funcstack[1]" = "_sk" ]; then + _sk "$@" +else + compdef _sk sk +fi diff --git a/skim/Cargo.toml b/skim/Cargo.toml new file mode 100644 index 00000000..bbd4afc0 --- /dev/null +++ b/skim/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "skim" +version = "0.10.4" +authors = ["Zhang Jinzhou "] +description = "Fuzzy Finder in rust!" +documentation = "https://docs.rs/skim" +homepage = "https://github.com/lotabout/skim" +repository = "https://github.com/lotabout/skim" +readme = "README.md" +keywords = ["fuzzy", "menu", "util"] +license = "MIT" +edition = "2018" + +[lib] +name = "skim" +path = "src/lib.rs" +doctest = false + +[[bin]] +name = "sk" +path = "src/bin/main.rs" + +[dependencies] +nix = "0.25.0" +atty = { version = "0.2.14", optional = true } +regex = "1.6.0" +lazy_static = "1.4.0" +shlex = { version = "1.1.0", optional = true } +unicode-width = "0.1.9" +log = "0.4.22" +env_logger = { version = "0.9.0", optional = true } +time = "0.3.13" +clap = { version = "4.5.20", optional = true, features = ["cargo", "derive"] } +tuikit = "0.5.0" +vte = "0.13.0" +fuzzy-matcher = "0.3.7" +rayon = "1.5.3" +derive_builder = "0.20.2" +bitflags = "1.3.2" +timer = "0.2.0" +chrono = "0.4.22" +crossbeam = "0.8.2" +beef = "0.5.2" # compact cow +defer-drop = "1.3.0" +indexmap = "2.6.0" + +[features] +default = ["cli"] +cli = ["dep:clap", "dep:atty", "dep:shlex", "dep:env_logger"] diff --git a/examples/custom_item.rs b/skim/examples/custom_item.rs similarity index 90% rename from examples/custom_item.rs rename to skim/examples/custom_item.rs index d23248e1..4220dc88 100644 --- a/examples/custom_item.rs +++ b/skim/examples/custom_item.rs @@ -21,9 +21,9 @@ impl SkimItem for MyItem { pub fn main() { let options = SkimOptionsBuilder::default() - .height(Some("50%")) + .height(String::from("50%")) .multi(true) - .preview(Some("")) // preview should be specified to enable preview window + .preview(Some(String::new())) // preview should be specified to enable preview window .build() .unwrap(); diff --git a/examples/custom_keybinding_actions.rs b/skim/examples/custom_keybinding_actions.rs similarity index 93% rename from examples/custom_keybinding_actions.rs rename to skim/examples/custom_keybinding_actions.rs index 9b811247..ef232eef 100644 --- a/examples/custom_keybinding_actions.rs +++ b/skim/examples/custom_keybinding_actions.rs @@ -18,7 +18,7 @@ pub fn main() { // `delete` and `create` are arbitrary keywords used for this example. let options = SkimOptionsBuilder::default() .multi(true) - .bind(vec!["bs:abort", "Enter:accept"]) + .bind(vec![String::from("bs:abort"), String::from("Enter:accept")]) .build() .unwrap(); diff --git a/examples/downcast.rs b/skim/examples/downcast.rs similarity index 94% rename from examples/downcast.rs rename to skim/examples/downcast.rs index 5e51d862..78e13c6a 100644 --- a/examples/downcast.rs +++ b/skim/examples/downcast.rs @@ -21,9 +21,9 @@ impl SkimItem for Item { pub fn main() { let options = SkimOptionsBuilder::default() - .height(Some("50%")) + .height(String::from("50%")) .multi(true) - .preview(Some("")) + .preview(Some(String::new())) .build() .unwrap(); diff --git a/examples/nth.rs b/skim/examples/nth.rs similarity index 77% rename from examples/nth.rs rename to skim/examples/nth.rs index 93cb3afa..ef813d3e 100644 --- a/examples/nth.rs +++ b/skim/examples/nth.rs @@ -8,8 +8,11 @@ use std::io::Cursor; pub fn main() { let input = "foo 123"; - let options = SkimOptionsBuilder::default().query(Some("f")).build().unwrap(); - let item_reader = SkimItemReader::new(SkimItemReaderOption::default().nth("2").build()); + let options = SkimOptionsBuilder::default() + .query(Some(String::from("f"))) + .build() + .unwrap(); + let item_reader = SkimItemReader::new(SkimItemReaderOption::default().nth(vec!["2"].into_iter()).build()); let items = item_reader.of_bufread(Cursor::new(input)); let selected_items = Skim::run_with(&options, Some(items)) diff --git a/examples/option_builder.rs b/skim/examples/option_builder.rs similarity index 96% rename from examples/option_builder.rs rename to skim/examples/option_builder.rs index 1ba07c59..0654913d 100644 --- a/examples/option_builder.rs +++ b/skim/examples/option_builder.rs @@ -4,7 +4,7 @@ use std::io::Cursor; pub fn main() { let options = SkimOptionsBuilder::default() - .height(Some("50%")) + .height(String::from("50%")) .multi(true) .build() .unwrap(); diff --git a/examples/sample.rs b/skim/examples/sample.rs similarity index 100% rename from examples/sample.rs rename to skim/examples/sample.rs diff --git a/src/ansi.rs b/skim/src/ansi.rs similarity index 100% rename from src/ansi.rs rename to skim/src/ansi.rs diff --git a/skim/src/bin/main.rs b/skim/src/bin/main.rs new file mode 100644 index 00000000..cc1ca714 --- /dev/null +++ b/skim/src/bin/main.rs @@ -0,0 +1,261 @@ +extern crate clap; +extern crate env_logger; +extern crate log; +extern crate shlex; +extern crate skim; +extern crate time; + +use self::context::SkimContext; +use self::reader::CommandCollector; +use clap::{Error, Parser}; +use derive_builder::Builder; +use std::fs::File; +use std::io::{BufReader, BufWriter, IsTerminal, Write}; +use std::{env, io}; + +use skim::prelude::*; + +fn parse_args() -> Result { + let mut args = Vec::new(); + + args.push( + env::args() + .next() + .expect("there should be at least one arg: the application name"), + ); + args.extend( + env::var("SKIM_DEFAULT_OPTIONS") + .ok() + .and_then(|val| shlex::split(&val)) + .unwrap_or_default(), + ); + for arg in env::args().skip(1) { + args.push(arg); + } + + Ok(SkimOptions::try_parse_from(args)?.build()) +} + +//------------------------------------------------------------------------------ +fn main() { + env_logger::builder().format_timestamp_nanos().init(); + + use SkMainError::*; + match sk_main() { + Ok(exit_code) => std::process::exit(exit_code), + Err(err) => { + // if downstream pipe is closed, exit silently, see PR#279 + match err { + IoError(e) => { + if e.kind() == std::io::ErrorKind::BrokenPipe { + std::process::exit(0) + } else { + std::process::exit(2) + } + } + ArgError(e) => e.exit(), + } + } + } +} + +enum SkMainError { + IoError(std::io::Error), + ArgError(clap::Error), +} + +impl From for SkMainError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for SkMainError { + fn from(value: clap::Error) -> Self { + Self::ArgError(value) + } +} + +fn sk_main() -> Result { + let opts = parse_args()?; + + let reader_opts = SkimItemReaderOption::default() + .ansi(opts.ansi) + .delimiter(&opts.delimiter) + .with_nth(opts.with_nth.iter().map(|s| s.as_str())) + .nth(opts.nth.iter().map(|s| s.as_str())) + .read0(opts.read0) + .show_error(opts.show_cmd_error); + let mut ctx = SkimContext { + cmd_collector: Rc::new(RefCell::new(SkimItemReader::new(reader_opts))), + query_history: vec![], + cmd_history: vec![], + }; + ctx.init_histories(&opts); + + //------------------------------------------------------------------------------ + let bin_options = BinOptions { + filter: opts.filter.clone(), + print_query: opts.print_query, + print_cmd: opts.print_cmd, + output_ending: String::from(if opts.print0 { "\0" } else { "\n" }), + }; + + //------------------------------------------------------------------------------ + // read from pipe or command + let rx_item = if !io::stdin().is_terminal() { + let rx_item = ctx.cmd_collector.borrow().of_bufread(BufReader::new(std::io::stdin())); + Some(rx_item) + } else { + None + }; + + //------------------------------------------------------------------------------ + // filter mode + if opts.filter.is_some() { + return Ok(filter(&ctx, &bin_options, &opts, rx_item)?); + } + + //------------------------------------------------------------------------------ + // output + + let Some(result) = Skim::run_with(&opts, rx_item) else { + return Ok(0); + }; + + if result.is_abort { + return Ok(130); + } + + // output query + if bin_options.print_query { + print!("{}{}", result.query, bin_options.output_ending); + } + + if bin_options.print_cmd { + print!("{}{}", result.cmd, bin_options.output_ending); + } + + if !opts.expect.is_empty() { + match result.final_event { + Event::EvActAccept(Some(accept_key)) => { + print!("{}{}", accept_key, bin_options.output_ending); + } + Event::EvActAccept(None) => { + print!("{}", bin_options.output_ending); + } + _ => {} + } + } + + for item in result.selected_items.iter() { + print!("{}{}", item.output(), bin_options.output_ending); + } + + std::io::stdout().flush()?; + + //------------------------------------------------------------------------------ + // write the history with latest item + if let Some(file) = opts.history { + let limit = opts.history_size; + write_history_to_file(&ctx.query_history, &result.query, limit, &file)?; + } + + if let Some(file) = opts.cmd_history { + let limit = opts.cmd_history_size; + write_history_to_file(&ctx.cmd_history, &result.cmd, limit, &file)?; + } + + Ok(if result.selected_items.is_empty() { 1 } else { 0 }) +} + +fn write_history_to_file( + orig_history: &[String], + latest: &str, + limit: usize, + filename: &str, +) -> Result<(), std::io::Error> { + if orig_history.last().map(|l| l.as_str()) == Some(latest) { + // no point of having at the end of the history 5x the same command... + return Ok(()); + } + let additional_lines = if latest.trim().is_empty() { 0 } else { 1 }; + let start_index = if orig_history.len() + additional_lines > limit { + orig_history.len() + additional_lines - limit + } else { + 0 + }; + + let mut history = orig_history[start_index..].to_vec(); + history.push(latest.to_string()); + + let file = File::create(filename)?; + let mut file = BufWriter::new(file); + file.write_all(history.join("\n").as_bytes())?; + Ok(()) +} + +#[derive(Builder)] +pub struct BinOptions { + filter: Option, + output_ending: String, + print_query: bool, + print_cmd: bool, +} + +pub fn filter( + ctx: &SkimContext, + bin_option: &BinOptions, + options: &SkimOptions, + source: Option, +) -> Result { + let default_command = match env::var("SKIM_DEFAULT_COMMAND").as_ref().map(String::as_ref) { + Ok("") | Err(_) => "find .".to_owned(), + Ok(val) => val.to_owned(), + }; + let query = bin_option.filter.clone().unwrap_or_default(); + let cmd = options.cmd.clone().unwrap_or(default_command); + + // output query + if bin_option.print_query { + print!("{}{}", query, bin_option.output_ending); + } + + if bin_option.print_cmd { + print!("{}{}", cmd, bin_option.output_ending); + } + + //------------------------------------------------------------------------------ + // matcher + let engine_factory: Box = if options.regex { + Box::new(RegexEngineFactory::builder()) + } else { + let fuzzy_engine_factory = ExactOrFuzzyEngineFactory::builder() + .fuzzy_algorithm(options.algorithm) + .exact_mode(options.exact) + .build(); + Box::new(AndOrEngineFactory::new(fuzzy_engine_factory)) + }; + + let engine = engine_factory.create_engine_with_case(&query, options.case); + + //------------------------------------------------------------------------------ + // start + let components_to_stop = Arc::new(AtomicUsize::new(0)); + + let stream_of_item = source.unwrap_or_else(|| { + let (ret, _control) = ctx.cmd_collector.borrow_mut().invoke(&cmd, components_to_stop); + ret + }); + + let mut num_matched = 0; + stream_of_item + .into_iter() + .filter_map(|item| engine.match_item(item.clone()).map(|result| (item, result))) + .for_each(|(item, _match_result)| { + num_matched += 1; + print!("{}{}", item.output(), bin_option.output_ending) + }); + + Ok(if num_matched == 0 { 1 } else { 0 }) +} diff --git a/skim/src/context.rs b/skim/src/context.rs new file mode 100644 index 00000000..c7dacaff --- /dev/null +++ b/skim/src/context.rs @@ -0,0 +1,34 @@ +use std::{cell::RefCell, rc::Rc}; + +use util::read_file_lines; + +use crate::prelude::*; + +pub struct SkimContext { + pub cmd_collector: Rc>, + pub query_history: Vec, + pub cmd_history: Vec, +} + +impl SkimContext { + pub fn init_histories(&mut self, opts: &SkimOptions) { + if let Some(histfile) = &opts.history { + self.query_history.extend(read_file_lines(histfile).unwrap_or_default()); + } + + if let Some(cmd_histfile) = &opts.cmd_history { + self.cmd_history + .extend(read_file_lines(cmd_histfile).unwrap_or_default()); + } + } +} + +impl Default for SkimContext { + fn default() -> Self { + Self { + cmd_collector: Rc::new(RefCell::new(SkimItemReader::new(Default::default()))), + query_history: vec![], + cmd_history: vec![], + } + } +} diff --git a/src/engine/all.rs b/skim/src/engine/all.rs similarity index 100% rename from src/engine/all.rs rename to skim/src/engine/all.rs diff --git a/src/engine/andor.rs b/skim/src/engine/andor.rs similarity index 100% rename from src/engine/andor.rs rename to skim/src/engine/andor.rs diff --git a/src/engine/exact.rs b/skim/src/engine/exact.rs similarity index 100% rename from src/engine/exact.rs rename to skim/src/engine/exact.rs diff --git a/src/engine/factory.rs b/skim/src/engine/factory.rs similarity index 100% rename from src/engine/factory.rs rename to skim/src/engine/factory.rs diff --git a/src/engine/fuzzy.rs b/skim/src/engine/fuzzy.rs similarity index 92% rename from src/engine/fuzzy.rs rename to skim/src/engine/fuzzy.rs index 67117a56..21f97f81 100644 --- a/src/engine/fuzzy.rs +++ b/skim/src/engine/fuzzy.rs @@ -2,6 +2,7 @@ use std::cmp::min; use std::fmt::{Display, Error, Formatter}; use std::sync::Arc; +use clap::ValueEnum; use fuzzy_matcher::clangd::ClangdMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; @@ -11,7 +12,8 @@ use crate::{CaseMatching, MatchEngine}; use crate::{MatchRange, MatchResult, SkimItem}; //------------------------------------------------------------------------------ -#[derive(Debug, Copy, Clone, Default)] +#[derive(ValueEnum, Debug, Copy, Clone, Default)] +#[clap(rename_all = "snake_case")] pub enum FuzzyAlgorithm { SkimV1, #[default] @@ -19,17 +21,6 @@ pub enum FuzzyAlgorithm { Clangd, } -impl FuzzyAlgorithm { - pub fn of(algorithm: &str) -> Self { - match algorithm.to_ascii_lowercase().as_ref() { - "skim_v1" => FuzzyAlgorithm::SkimV1, - "skim_v2" | "skim" => FuzzyAlgorithm::SkimV2, - "clangd" => FuzzyAlgorithm::Clangd, - _ => FuzzyAlgorithm::SkimV2, - } - } -} - const BYTES_1M: usize = 1024 * 1024 * 1024; //------------------------------------------------------------------------------ diff --git a/src/engine/mod.rs b/skim/src/engine/mod.rs similarity index 100% rename from src/engine/mod.rs rename to skim/src/engine/mod.rs diff --git a/src/engine/regexp.rs b/skim/src/engine/regexp.rs similarity index 100% rename from src/engine/regexp.rs rename to skim/src/engine/regexp.rs diff --git a/src/engine/util.rs b/skim/src/engine/util.rs similarity index 100% rename from src/engine/util.rs rename to skim/src/engine/util.rs diff --git a/src/event.rs b/skim/src/event.rs similarity index 100% rename from src/event.rs rename to skim/src/event.rs diff --git a/src/field.rs b/skim/src/field.rs similarity index 100% rename from src/field.rs rename to skim/src/field.rs diff --git a/src/global.rs b/skim/src/global.rs similarity index 100% rename from src/global.rs rename to skim/src/global.rs diff --git a/src/header.rs b/skim/src/header.rs similarity index 92% rename from src/header.rs rename to skim/src/header.rs index 3394f78a..3d2c1396 100644 --- a/src/header.rs +++ b/skim/src/header.rs @@ -44,21 +44,19 @@ impl Header { } pub fn with_options(mut self, options: &SkimOptions) -> Self { - if let Some(tabstop_str) = options.tabstop { - let tabstop = tabstop_str.parse::().unwrap_or(8); - self.tabstop = max(1, tabstop); - } + self.tabstop = max(1, options.tabstop); if options.layout.starts_with("reverse") { self.reverse = true; } - match options.header { + match &options.header { None => {} - Some("") => {} Some(header) => { let mut parser = ANSIParser::default(); - self.header = str_lines(header).into_iter().map(|l| parser.parse_ansi(l)).collect(); + if !header.is_empty() { + self.header = str_lines(header).into_iter().map(|l| parser.parse_ansi(l)).collect(); + } } } self diff --git a/src/helper/item.rs b/skim/src/helper/item.rs similarity index 100% rename from src/helper/item.rs rename to skim/src/helper/item.rs diff --git a/src/helper/item_reader.rs b/skim/src/helper/item_reader.rs similarity index 96% rename from src/helper/item_reader.rs rename to skim/src/helper/item_reader.rs index 7adbe180..1b694a6b 100644 --- a/src/helper/item_reader.rs +++ b/skim/src/helper/item_reader.rs @@ -73,10 +73,11 @@ impl SkimItemReaderOption { self } - pub fn with_nth(mut self, with_nth: &str) -> Self { - if !with_nth.is_empty() { - self.transform_fields = with_nth.split(',').filter_map(FieldRange::from_str).collect(); - } + pub fn with_nth<'a, T>(mut self, with_nth: T) -> Self + where + T: Iterator, + { + self.transform_fields = with_nth.filter_map(FieldRange::from_str).collect(); self } @@ -85,10 +86,11 @@ impl SkimItemReaderOption { self } - pub fn nth(mut self, nth: &str) -> Self { - if !nth.is_empty() { - self.matching_fields = nth.split(',').filter_map(FieldRange::from_str).collect(); - } + pub fn nth<'a, T>(mut self, nth: T) -> Self + where + T: Iterator, + { + self.matching_fields = nth.filter_map(FieldRange::from_str).collect(); self } diff --git a/src/helper/mod.rs b/skim/src/helper/mod.rs similarity index 100% rename from src/helper/mod.rs rename to skim/src/helper/mod.rs diff --git a/src/helper/selector.rs b/skim/src/helper/selector.rs similarity index 100% rename from src/helper/selector.rs rename to skim/src/helper/selector.rs diff --git a/src/input.rs b/skim/src/input.rs similarity index 96% rename from src/input.rs rename to skim/src/input.rs index e2890eae..bf346751 100644 --- a/src/input.rs +++ b/skim/src/input.rs @@ -50,8 +50,11 @@ impl Input { self.keymap.entry(key).or_insert(action_chain); } - pub fn parse_keymaps(&mut self, maps: &[&str]) { - for &map in maps { + pub fn parse_keymaps<'a, T>(&mut self, maps: T) + where + T: Iterator, + { + for map in maps { self.parse_keymap(map); } } @@ -69,11 +72,12 @@ impl Input { } } - pub fn parse_expect_keys(&mut self, keys: Option<&str>) { - if let Some(keys) = keys { - for key in keys.split(',') { - self.bind(key, vec![Event::EvActAccept(Some(key.to_string()))]); - } + pub fn parse_expect_keys<'a, T>(&mut self, keys: T) + where + T: Iterator, + { + for key in keys { + self.bind(key, vec![Event::EvActAccept(Some(key.to_string()))]); } } } diff --git a/src/item.rs b/skim/src/item.rs similarity index 88% rename from src/item.rs rename to skim/src/item.rs index 96afbfe4..8a880a48 100644 --- a/src/item.rs +++ b/skim/src/item.rs @@ -6,6 +6,9 @@ use std::ops::Deref; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; +use clap::builder::PossibleValue; +use clap::ValueEnum; + use crate::spinlock::{SpinLock, SpinLockGuard}; use crate::{MatchRange, Rank, SkimItem}; @@ -208,16 +211,23 @@ pub enum RankCriteria { NegLength, } -pub fn parse_criteria(text: &str) -> Option { - match text.to_lowercase().as_ref() { - "score" => Some(RankCriteria::Score), - "begin" => Some(RankCriteria::Begin), - "end" => Some(RankCriteria::End), - "-score" => Some(RankCriteria::NegScore), - "-begin" => Some(RankCriteria::NegBegin), - "-end" => Some(RankCriteria::NegEnd), - "length" => Some(RankCriteria::Length), - "-length" => Some(RankCriteria::NegLength), - _ => None, +impl ValueEnum for RankCriteria { + fn value_variants<'a>() -> &'a [Self] { + use RankCriteria::*; + &[Score, NegScore, Begin, NegBegin, End, NegEnd, Length, NegLength] + } + + fn to_possible_value(&self) -> Option { + use RankCriteria::*; + Some(match self { + Score => PossibleValue::new("score"), + Begin => PossibleValue::new("begin"), + End => PossibleValue::new("end"), + NegScore => PossibleValue::new("-score"), + NegBegin => PossibleValue::new("-begin"), + NegEnd => PossibleValue::new("-end"), + Length => PossibleValue::new("length"), + NegLength => PossibleValue::new("-length"), + }) } } diff --git a/src/lib.rs b/skim/src/lib.rs similarity index 95% rename from src/lib.rs rename to skim/src/lib.rs index f8b9bb56..29f7f3c6 100644 --- a/src/lib.rs +++ b/skim/src/lib.rs @@ -10,6 +10,7 @@ use std::sync::mpsc::channel; use std::sync::Arc; use std::thread; +use clap::ValueEnum; use crossbeam::channel::{Receiver, Sender}; use tuikit::prelude::{Event as TermEvent, *}; @@ -22,6 +23,7 @@ pub use crate::output::SkimOutput; use crate::reader::Reader; mod ansi; +pub mod context; mod engine; mod event; pub mod field; @@ -38,7 +40,7 @@ mod output; pub mod prelude; mod previewer; mod query; -mod reader; +pub mod reader; mod selection; mod spinlock; mod theme; @@ -219,7 +221,8 @@ pub enum ItemPreview { //============================================================================== // A match engine will execute the matching algorithm -#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)] +#[derive(ValueEnum, Eq, PartialEq, Debug, Copy, Clone, Default)] +#[clap(rename_all = "snake_case")] pub enum CaseMatching { Respect, Ignore, @@ -291,14 +294,8 @@ impl Skim { /// - None: on internal errors. /// - SkimOutput: the collected key, event, query, selected items, etc. pub fn run_with(options: &SkimOptions, source: Option) -> Option { - let min_height = options - .min_height - .map(Skim::parse_height_string) - .expect("min_height should have default values"); - let height = options - .height - .map(Skim::parse_height_string) - .expect("height should have default values"); + let min_height = Skim::parse_height_string(&options.min_height); + let height = Skim::parse_height_string(&options.height); let (tx, rx): (EventSender, EventReceiver) = channel(); let term = Arc::new( @@ -309,7 +306,7 @@ impl Skim { .clear_on_exit(!options.no_clear) .disable_alternate_screen(options.no_clear_start) .clear_on_start(!options.no_clear_start) - .hold(options.select1 || options.exit0 || options.sync), + .hold(options.select_1 || options.exit_0 || options.sync), ) .unwrap(), ); @@ -320,8 +317,8 @@ impl Skim { //------------------------------------------------------------------------------ // input let mut input = input::Input::new(); - input.parse_keymaps(&options.bind); - input.parse_expect_keys(options.expect.as_deref()); + input.parse_keymaps(options.bind.iter().map(|s| s.as_str())); + input.parse_expect_keys(options.expect.iter().map(|s| s.as_str())); let tx_clone = tx.clone(); let term_clone = term.clone(); diff --git a/src/matcher.rs b/skim/src/matcher.rs similarity index 100% rename from src/matcher.rs rename to skim/src/matcher.rs diff --git a/src/model.rs b/skim/src/model.rs similarity index 91% rename from src/model.rs rename to skim/src/model.rs index d6f9478a..f0fda614 100644 --- a/src/model.rs +++ b/skim/src/model.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::env; use std::process::Command; @@ -17,7 +16,7 @@ use crate::event::{Event, EventHandler, EventReceiver, EventSender}; use crate::global::current_run_num; use crate::header::Header; use crate::input::parse_action_arg; -use crate::item::{parse_criteria, ItemPool, MatchedItem, RankBuilder, RankCriteria}; +use crate::item::{ItemPool, MatchedItem, RankBuilder, RankCriteria}; use crate::matcher::{Matcher, MatcherControl}; use crate::options::SkimOptions; use crate::output::SkimOutput; @@ -37,7 +36,6 @@ const SPINNER_DURATION: u32 = 200; // const SPINNERS: [char; 8] = ['-', '\\', '|', '/', '-', '\\', '|', '/']; const SPINNERS_INLINE: [char; 2] = ['-', '<']; const SPINNERS_UNICODE: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; -const DELIMITER_STR: &str = r"[\t\n ]+"; lazy_static! { static ref RE_FIELDS: Regex = Regex::new(r"\\?(\{-?[0-9.,q]*?})").unwrap(); @@ -51,8 +49,8 @@ pub struct Model { query: Query, selection: Selection, num_options: usize, - select1: bool, - exit0: bool, + select_1: bool, + exit_0: bool, sync: bool, use_regex: bool, @@ -111,31 +109,20 @@ impl Model { .theme(theme.clone()) .build(); - let criterion = if let Some(ref tie_breaker) = options.tiebreak { - tie_breaker.split(',').filter_map(parse_criteria).collect() - } else { - DEFAULT_CRITERION.clone() - }; - - let rank_builder = Arc::new(RankBuilder::new(criterion)); + let rank_builder = Arc::new(RankBuilder::new(options.tiebreak.clone())); let selection = Selection::with_options(options).theme(theme.clone()); let regex_engine: Rc = Rc::new(RegexEngineFactory::builder().rank_builder(rank_builder.clone()).build()); let regex_matcher = Matcher::builder(regex_engine).build(); - let matcher = if let Some(engine_factory) = options.engine_factory.as_ref() { - // use provided engine - Matcher::builder(engine_factory.clone()).case(options.case).build() - } else { - let fuzzy_engine_factory: Rc = Rc::new(AndOrEngineFactory::new( - ExactOrFuzzyEngineFactory::builder() - .exact_mode(options.exact) - .rank_builder(rank_builder.clone()) - .build(), - )); - Matcher::builder(fuzzy_engine_factory).case(options.case).build() - }; + let fuzzy_engine_factory: Rc = Rc::new(AndOrEngineFactory::new( + ExactOrFuzzyEngineFactory::builder() + .exact_mode(options.exact) + .rank_builder(rank_builder.clone()) + .build(), + )); + let matcher = Matcher::builder(fuzzy_engine_factory).case(options.case).build(); let item_pool = Arc::new(DeferDrop::new(ItemPool::new().lines_to_reserve(options.header_lines))); let header = Header::empty() @@ -143,10 +130,7 @@ impl Model { .item_pool(item_pool.clone()) .theme(theme.clone()); - let margins = options - .margin - .map(parse_margin) - .expect("option margin is should be specified (by default)"); + let margins = parse_margin(&options.margin); let (margin_top, margin_right, margin_bottom, margin_left) = margins; let mut ret = Model { @@ -154,8 +138,8 @@ impl Model { query, selection, num_options: 0, - select1: false, - exit0: false, + select_1: false, + exit_0: false, sync: false, use_regex: options.regex, regex_matcher, @@ -183,7 +167,7 @@ impl Model { margin_left, layout: "default".to_string(), - delimiter: Regex::new(DELIMITER_STR).unwrap(), + delimiter: Regex::new(r"[\t\n ]+").unwrap(), inline_info: false, no_clear_if_empty: false, theme, @@ -197,32 +181,27 @@ impl Model { } fn parse_options(&mut self, options: &SkimOptions) { - if let Some(delimiter) = options.delimiter { - self.delimiter = Regex::new(delimiter).unwrap_or_else(|_| Regex::new(DELIMITER_STR).unwrap()); - } + let Ok(delimiter) = Regex::new(&options.delimiter) else { + panic!("Could not parse delimiter {} as a valid regex", options.delimiter); + }; + self.delimiter = delimiter; - self.layout = options.layout.to_string(); + self.layout = options.layout.clone(); - if options.inline_info { - self.inline_info = true; - } + self.inline_info = options.inline_info; - if options.regex { - self.use_regex = true; - } + self.use_regex = options.regex; self.fuzzy_algorithm = options.algorithm; // preview related - let (preview_direction, preview_size, preview_wrap, preview_shown) = options - .preview_window - .map(Self::parse_preview) - .expect("option 'preview-window' should be set (by default)"); + let (preview_direction, preview_size, preview_wrap, preview_shown) = + Self::parse_preview(options.preview_window.clone()); self.preview_direction = preview_direction; self.preview_size = preview_size; self.preview_hidden = !preview_shown; - if let Some(preview_cmd) = options.preview { + if let Some(preview_cmd) = options.preview.clone() { let tx = Arc::new(SpinLock::new(self.tx.clone())); self.previewer = Some( Previewer::new(Some(preview_cmd.to_string()), move || { @@ -230,23 +209,18 @@ impl Model { }) .wrap(preview_wrap) .delimiter(self.delimiter.clone()) - .preview_offset( - options - .preview_window - .map(Self::parse_preview_offset) - .unwrap_or_default(), - ), + .preview_offset(Self::parse_preview_offset(options.preview_window.clone())), ); } - self.select1 = options.select1; - self.exit0 = options.exit0; + self.select_1 = options.select_1; + self.exit_0 = options.exit_0; self.sync = options.sync; self.no_clear_if_empty = options.no_clear_if_empty; } // -> (direction, size, wrap, shown) - fn parse_preview(preview_option: &str) -> (Direction, Size, bool, bool) { + fn parse_preview(preview_option: String) -> (Direction, Size, bool, bool) { let options = preview_option.split(':').collect::>(); let mut direction = Direction::Right; @@ -282,14 +256,14 @@ impl Model { } // -> string - fn parse_preview_offset(preview_window: &str) -> String { + fn parse_preview_offset(preview_window: String) -> String { for token in preview_window.split(':').rev() { if RE_PREVIEW_OFFSET.is_match(token) { return token.to_string(); } } - "".to_string() + String::new() } fn act_heart_beat(&mut self, env: &mut ModelEnv) { @@ -360,7 +334,7 @@ impl Model { } fn handle_select1_or_exit0(&mut self) { - if !self.select1 && !self.exit0 && !self.sync { + if !self.select_1 && !self.exit_0 && !self.sync { return; } @@ -371,16 +345,16 @@ impl Model { let processed = reader_stopped && items_consumed && matcher_stopped; let num_matched = self.selection.get_num_options(); if processed { - if num_matched == 1 && self.select1 { + if num_matched == 1 && self.select_1 { debug!("select-1 triggered, accept"); let _ = self.tx.send((Key::Null, Event::EvActAccept(None))); - } else if num_matched == 0 && self.exit0 { + } else if num_matched == 0 && self.exit_0 { debug!("exit-0 triggered, accept"); let _ = self.tx.send((Key::Null, Event::EvActAbort)); } else { // no longer need need to handle select-1, exit-1, sync, etc. - self.select1 = false; - self.exit0 = false; + self.select_1 = false; + self.exit_0 = false; self.sync = false; let _ = self.term.restart(); } @@ -439,15 +413,12 @@ impl Model { return; } - let current_selection = current_item - .as_ref() - .map(|item| item.output()) - .unwrap_or_else(|| Cow::Borrowed("")); + let current_selection = current_item.as_ref().map(|item| item.output()).unwrap_or_default(); let query = self.query.get_fz_query(); let cmd_query = self.query.get_cmd_query(); let (indices, selections) = self.selection.get_selected_indices_and_items(); - let tmp: Vec> = selections.iter().map(|item| item.text()).collect(); + let tmp: Vec = selections.into_iter().map(|item| item.text().to_string()).collect(); let selected_texts: Vec<&str> = tmp.iter().map(|cow| cow.as_ref()).collect(); let context = InjectContext { diff --git a/skim/src/options.rs b/skim/src/options.rs new file mode 100644 index 00000000..d6284367 --- /dev/null +++ b/skim/src/options.rs @@ -0,0 +1,741 @@ +use clap::Parser; +use derive_builder::Builder; + +use crate::item::RankCriteria; +use crate::{CaseMatching, FuzzyAlgorithm}; + +/// sk - fuzzy finder in Rust +/// +/// sk is a general purpose command-line fuzzy finder. +/// +/// +/// ENVIRONMENT VARIABLES +/// +/// SKIM_DEFAULT_COMMAND +/// +/// Default command to use when input is tty. On *nix systems, sk runs the command with sh -c, so make sure that +/// it's POSIX-compliant. +/// +/// SKIM_DEFAULT_OPTIONS +/// +/// Default options. e.g. export SKIM_DEFAULT_OPTIONS="--multi +/// +/// EXTENDED SEARCH MODE +/// +/// Unless specified otherwise, sk will start in "extended-search mode". In this mode, you can specify multiple patterns +/// delimited by spaces, such as: 'wild ^music .mp3$ sbtrkt !rmx +/// +/// You can prepend a backslash to a space (\ ) to match a literal space character. +/// +/// Exact-match (quoted) +/// +/// A term that is prefixed by a single-quote character (') is interpreted as an "exact-match" (or "non-fuzzy") term. sk +/// will search for the exact occurrences of the string. +/// +/// Anchored-match +/// +/// A term can be prefixed by ^, or suffixed by $ to become an anchored-match term. Then sk will search for the lines +/// that start with or end with the given string. An anchored-match term is also an exact-match term. +/// +/// Negation +/// +/// If a term is prefixed by !, sk will exclude the lines that satisfy the term from the result. In this case, sk per‐ +/// forms exact match by default. +/// +/// Exact-match by default +/// +/// If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with ') every word, start sk with -e or +/// --exact option. Note that when --exact is set, '-prefix "unquotes" the term. +/// +/// OR operator +/// +/// A single bar character term acts as an OR operator. For example, the following query matches entries that start with +/// core and end with either go, rb, or py. +/// +/// e.g. ^core go$ | rb$ | py$ +/// +/// +/// EXIT STATUS +/// 0 Normal exit +/// 1 No match +/// 2 Error +/// 130 Interrupted with CTRL-C or ESC + +#[derive(Builder)] +#[builder(build_fn(name = "final_build"))] +#[builder(default)] +#[derive(Parser)] +#[command(name = "sk", args_override_self = true, verbatim_doc_comment)] +pub struct SkimOptions { + // --- Search --- + /// Show results in reverse order + /// + /// Often used in combination with --no-sort + #[arg(long, help_heading = "Search")] + pub tac: bool, + + /// Do not sort the results + /// + /// Often used in combination with --tac + /// Example: `history | sk --tac --no-sort` + #[arg(long, help_heading = "Search")] + pub no_sort: bool, + + /// Comma-separated list of sort criteria to apply when the scores are tied. + /// + /// score Score of the fuzzy match algorithm + /// index Prefers line that appeared earlier in the input stream + /// begin Prefers line with matched substring closer to the beginning + /// end Prefers line with matched substring closer to the end + /// length Prefers line with shorter length + /// + /// - Each criterion could be negated, e.g. (-index) + /// - Each criterion should appear only once in the list + #[arg( + short, + long, + default_value = "score,begin,end", + value_enum, + value_delimiter = ',', + help_heading = "Search", + verbatim_doc_comment + )] + pub tiebreak: Vec, + + /// Fields to be matched + /// + /// A field index expression can be a non-zero integer or a range expression ([BEGIN]..[END]). --nth + /// and --with-nth take a comma-separated list of field index expressions. + /// + /// **Examples:** + /// 1 The 1st field + /// 2 The 2nd field + /// -1 The last field + /// -2 The 2nd to last field + /// 3..5 From the 3rd field to the 5th field + /// 2.. From the 2nd field to the last field + /// ..-3 From the 1st field to the 3rd to the last field + /// .. All the fields + #[arg(short, long, default_value = "", help_heading = "Search", verbatim_doc_comment)] + pub nth: Vec, + + /// Fields to be transformed + /// + /// See **nth** for the details + #[arg(long, default_value = "", help_heading = "Search")] + pub with_nth: Vec, + + /// Delimiter between fields + /// + /// In regex format, default to AWK-style + #[arg(short, long, default_value = r"[\t\n ]+", help_heading = "Search")] + pub delimiter: String, + + /// Run in exact mode + #[arg(short, long, help_heading = "Search")] + pub exact: bool, + + /// Start in regex mode instead of fuzzy-match + #[arg(long, help_heading = "Search")] + pub regex: bool, + + /// Fuzzy matching algorithm + /// + /// skim_v2 Latest skim algorithm, should be better in almost any case + /// skim_v1 Legacy skim algorithm + /// clangd Used in clangd for keyword completion + #[arg( + long = "algo", + default_value = "skim_v2", + value_enum, + help_heading = "Search", + verbatim_doc_comment + )] + pub algorithm: FuzzyAlgorithm, + + /// Case sensitivity + /// + /// Determines whether or not to ignore case while matching + #[arg(long, default_value = "smart", value_enum, help_heading = "Search")] + pub case: CaseMatching, + + // --- Interface --- + /// Comma separated list of bindings + /// + /// You can customize key bindings of sk with --bind option which takes a comma-separated list of + /// key binding expressions. Each key binding expression follows the following format: KEY:ACTION + /// + /// e.g. sk --bind=ctrl-j:accept,ctrl-k:kill-line + /// + /// AVAILABLE KEYS: (SYNONYMS) + /// ctrl-[a-z] + /// ctrl-space + /// ctrl-alt-[a-z] + /// alt-[a-zA-Z] + /// alt-[0-9] + /// f[1-12] + /// enter (ctrl-m) + /// space + /// bspace (bs) + /// alt-up + /// alt-down + /// alt-left + /// alt-right + /// alt-enter (alt-ctrl-m) + /// alt-space + /// alt-bspace (alt-bs) + /// alt-/ + /// tab + /// btab (shift-tab) + /// esc + /// del + /// up + /// down + /// left + /// right + /// home + /// end + /// pgup (page-up) + /// pgdn (page-down) + /// shift-up + /// shift-down + /// shift-left + /// shift-right + /// alt-shift-up + /// alt-shift-down + /// alt-shift-left + /// alt-shift-right + /// or any single character + /// + /// ACTION: DEFAULT BINDINGS (NOTES): + /// abort ctrl-c ctrl-q esc + /// accept enter + /// append-and-select + /// backward-char ctrl-b left + /// backward-delete-char ctrl-h bspace + /// backward-kill-word alt-bs + /// backward-word alt-b shift-left + /// beginning-of-line ctrl-a home + /// clear-screen ctrl-l + /// delete-char del + /// delete-charEOF ctrl-d + /// deselect-all + /// down ctrl-j ctrl-n down + /// end-of-line ctrl-e end + /// execute(...) (see below for the details) + /// execute-silent(...) (see below for the details) + /// forward-char ctrl-f right + /// forward-word alt-f shift-right + /// if-non-matched + /// if-query-empty + /// if-query-not-empty + /// ignore + /// kill-line + /// kill-word alt-d + /// next-history (ctrl-n on --history or --cmd-history) + /// page-down pgdn + /// page-up pgup + /// half-page-down + /// half-page-up + /// preview-up shift-up + /// preview-down shift-down + /// preview-left + /// preview-right + /// preview-page-down + /// preview-page-up + /// previous-history (ctrl-p on --history or --cmd-history) + /// select-all + /// toggle + /// toggle-all + /// toggle+down ctrl-i (tab) + /// toggle-in (--layout=reverse* ? toggle+up : toggle+down) + /// toggle-out (--layout=reverse* ? toggle+down : toggle+up) + /// toggle-preview + /// toggle-preview-wrap + /// toggle-sort + /// toggle+up btab (shift-tab) + /// unix-line-discard ctrl-u + /// unix-word-rubout ctrl-w + /// up ctrl-k ctrl-p up + /// yank ctrl-y + /// + /// Multiple actions can be chained using + separator. + /// + /// sk --bind 'ctrl-a:select-all+accept' + /// + /// With execute(...) action, you can execute arbitrary commands without leaving sk. For example, + /// you can turn sk into a simple file browser by binding enter key to less command like follows. + /// + /// sk --bind "enter:execute(less {})" + /// + /// You can use the same placeholder expressions as in --preview. + /// + /// If the command contains parentheses, sk may fail to parse the expression. In that case, you can + /// use any of the following alternative notations to avoid parse errors. + /// + /// execute[...] + /// execute'...' + /// execute"..." + /// execute:... + /// This is the special form that frees you from parse errors as it does not expect the clos‐ + /// ing character. The catch is that it should be the last one in the comma-separated list of + /// key-action pairs. + /// + /// sk switches to the alternate screen when executing a command. However, if the command is ex‐ + /// pected to complete quickly, and you are not interested in its output, you might want to use exe‐ + /// cute-silent instead, which silently executes the command without the switching. Note that sk + /// will not be responsive until the command is complete. For asynchronous execution, start your + /// command as a background process (i.e. appending &). + /// + /// With if-query-empty and if-query-not-empty action, you could specify the action to execute de‐ + /// pends on the query condition. For example + /// + /// sk --bind 'ctrl-d:if-query-empty(abort)+delete-char' + /// + /// If the query is empty, skim will execute abort action, otherwise execute delete-char action. It + /// is equal to ‘delete-char/eof‘. + #[arg(short, long, help_heading = "Interface", verbatim_doc_comment)] + pub bind: Vec, + + /// Enable multiple selection + /// + /// Uses Tab and S-Tab by default for selection + #[arg(short, long, overrides_with = "no_multi", help_heading = "Interface")] + pub multi: bool, + + /// Disable multiple selection + #[arg(long, conflicts_with = "multi", help_heading = "Interface")] + pub no_multi: bool, + + /// Disable mouse + #[arg(long, help_heading = "Interface")] + pub no_mouse: bool, + + /// Command to invoke dynamically in interactive mode + /// + /// Will be invoked using `sh -c` + #[arg(short, long, help_heading = "Interface")] + pub cmd: Option, + + /// Run in interactive mode + #[arg(short, long, help_heading = "Interface")] + pub interactive: bool, + + /// Replace replstr with the selected item in commands + #[arg(short = 'I', default_value = "{}", help_heading = "Interface")] + pub replstr: String, + + /// Set color theme + /// + /// Format: [BASE][,COLOR:ANSI] + #[arg(long, help_heading = "Interface")] + pub color: Option, + + /// Disable horizontal scroll + #[arg(long, help_heading = "Interface")] + pub no_hscroll: bool, + + /// Keep the right end of the line visible on overflow + /// + /// Effective only when the query string is empty + #[arg(long, help_heading = "Interface")] + pub keep_right: bool, + + /// Show the matched pattern at the line start + /// + /// Line will start with the start of the matched pattern. Effective only when the query + /// string is empty. Was designed to skip showing starts of paths of rg/grep results. + /// + /// e.g. sk -i -c "rg {} --color=always" --skip-to-pattern '[^/]*:' --ansi + #[arg(long, help_heading = "Interface", verbatim_doc_comment)] + pub skip_to_pattern: Option, + + /// Do not clear previous line if the command returns an empty result + /// + /// Do not clear previous items if new command returns empty result. This might be useful to + /// reduce flickering when typing new commands and the half-complete commands are not valid. + /// + /// This is not default however because similar usecases for grep and rg had already been op‐ + /// timized where empty result of a query do mean "empty" and previous results should be + /// cleared. + #[arg(long, help_heading = "Interface", verbatim_doc_comment)] + pub no_clear_if_empty: bool, + + /// Do not clear items on start + #[arg(long, help_heading = "Interface")] + pub no_clear_start: bool, + + /// Do not clear screen on exit + /// + /// Do not clear finder interface on exit. If skim was started in full screen mode, it will not switch back to the + /// original screen, so you'll have to manually run tput rmcup to return. This option can be used to avoid + /// flickering of the screen when your application needs to start skim multiple times in order. + #[arg(long, help_heading = "Interface")] + pub no_clear: bool, + + /// Show error message if command fails + #[arg(long, help_heading = "Interface")] + pub show_cmd_error: bool, + + // --- Layout --- + /// Set layout + /// + /// default Display from the bottom of the screen + /// reverse Display from the top of the screen + /// reverse-list Display from the top of the screen, prompt at the bottom + #[arg( + long, + default_value = "default", + value_parser = clap::builder::PossibleValuesParser::new( + ["default", "reverse", "reverse-list"] + ), + help_heading = "Layout", + verbatim_doc_comment + )] + pub layout: String, + + /// Shorthand for reverse layout + #[arg(long, help_heading = "Layout")] + pub reverse: bool, + + /// Height of skim's window + /// + /// Can either be a row count or a percentage + #[arg(long, default_value = "100%", help_heading = "Layout")] + pub height: String, + + /// Disable height feature + #[arg(long, help_heading = "Layout")] + pub no_height: bool, + + /// Minimum height of skim's window + /// + /// Useful when the height is set as a percentage + /// Ignored when --height is not specified + #[arg(long, default_value = "10", help_heading = "Layout", verbatim_doc_comment)] + pub min_height: String, + + /// Screen margin + /// + /// For each side, can be either a row count or a percentage of the terminal size + /// Format can be one of: + /// - TRBL + /// - TB,RL + /// - T,RL,B + /// - T,R,B,L + /// Example: 1,10% + #[arg(long, default_value = "0", help_heading = "Layout", verbatim_doc_comment)] + pub margin: String, + + /// Set prompt + #[arg(long, short, default_value = "> ", help_heading = "Layout")] + pub prompt: String, + + /// Set prompt in command mode + #[arg(long, default_value = "c> ", help_heading = "Layout")] + pub cmd_prompt: String, + + // --- Display --- + /// Parse ANSI color codes in input strings + #[arg(long, help_heading = "Display")] + pub ansi: bool, + + /// Number of spaces that make up a tab + #[arg(long, default_value = "8", help_heading = "Display")] + pub tabstop: usize, + + /// Display info next to the query + #[arg(long, help_heading = "Display")] + pub inline_info: bool, + + /// Set header, displayed next to the info + /// + /// The given string will be printed as the sticky header. The lines are displayed in the + /// given order from top to bottom regardless of --layout option, and are not affected by + /// --with-nth. ANSI color codes are processed even when --ansi is not set. + #[arg(long, help_heading = "Display")] + pub header: Option, + + /// Number of lines of the input treated as header + /// + /// The first N lines of the input are treated as the sticky header. When --with-nth is set, + /// the lines are transformed just like the other lines that follow. + #[arg(long, default_value = "0", help_heading = "Display")] + pub header_lines: usize, + + // --- History --- + /// History file + /// + /// Load search history from the specified file and update the file on completion. + /// When enabled, CTRL-N and CTRL-P are automatically remapped + /// to next-history and previous-history. + #[arg(long, help_heading = "History")] + pub history: Option, + + /// Maximum number of query history entries to keep + #[arg(long, default_value = "1000", help_heading = "History")] + pub history_size: usize, + + /// Command history file + /// + /// Load command query history from the specified file and update the file on completion. + /// When enabled, CTRL-N and CTRL-P are automatically remapped + /// to next-history and previous-history. + #[arg(long, help_heading = "History")] + pub cmd_history: Option, + + /// Maximum number of query history entries to keep + #[arg(long, default_value = "1000", help_heading = "History")] + pub cmd_history_size: usize, + + // --- Preview --- + /// Preview command + /// + /// Execute the given command for the current line and display the result on the preview window. {} in the command + /// is the placeholder that is replaced to the single-quoted string of the current line. To transform the replace‐ + /// ment string, specify field index expressions between the braces (See FIELD INDEX EXPRESSION for the details). + /// + /// e.g. sk --preview='head -$LINES {}' + /// ls -l | sk --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1 + /// + /// sk overrides $LINES and $COLUMNS so that they represent the exact size of the preview window. + /// + /// A placeholder expression starting with + flag will be replaced to the space-separated list of the selected + /// lines (or the current line if no selection was made) individually quoted. + /// + /// e.g. + /// sk --multi --preview='head -10 {+}' + /// git log --oneline | sk --multi --preview 'git show {+1}' + /// + /// Note that you can escape a placeholder pattern by prepending a backslash. + /// + /// Also, {q} is replaced to the current query string. {cq} is replaced to the current command query string. {n} + /// is replaced to zero-based ordinal index of the line. Use {+n} if you want all index numbers when multiple + /// lines are selected + /// + /// Preview window will be updated even when there is no match for the current query if any of the placeholder ex‐ + /// pressions evaluates to a non-empty string. + #[arg(long, help_heading = "Preview", verbatim_doc_comment)] + pub preview: Option, + + /// Preview window layout + /// + /// Format: [up|down|left|right][:SIZE[%]][:hidden][:+SCROLL[-OFFSET]] + /// Determine the layout of the preview window. If the argument ends with :hidden, the preview window will be hidden by + /// default until toggle-preview action is triggered. Long lines are truncated by default. Line wrap can be enabled with + /// :wrap flag. + /// + /// If size is given as 0, preview window will not be visible, but sk will still execute the command in the background. + /// + /// +SCROLL[-OFFSET] determines the initial scroll offset of the preview window. SCROLL can be either a numeric integer + /// or a single-field index expression that refers to a numeric integer. The optional -OFFSET part is for adjusting the + /// base offset so that you can see the text above it. It should be given as a numeric integer (-INTEGER), or as a denom‐ + /// inator form (-/INTEGER) for specifying a fraction of the preview window height. + /// + /// e.g. + /// # Non-default scroll window positions and sizes + /// sk --preview="head {}" --preview-window=up:30% + /// sk --preview="file {}" --preview-window=down:2 + /// + /// # Initial scroll offset is set to the line number of each line of + /// # git grep output *minus* 5 lines (-5) + /// git grep --line-number '' | + /// sk --delimiter : --preview 'nl {1}' --preview-window +{2}-5 + /// + /// # Preview with bat, matching line in the middle of the window (-/2) + /// git grep --line-number '' | + /// sk --delimiter : \ + /// --preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \ + /// --preview-window +{2}-/2 + #[arg(long, default_value = "right:50%", help_heading = "Preview")] + pub preview_window: String, + + // --- Scripting --- + /// Initial query + #[arg(long, short, help_heading = "Scripting")] + pub query: Option, + + /// Initial query in interactive mode + #[arg(long, help_heading = "Scripting")] + pub cmd_query: Option, + + /// Comma separated list of keys used to complete skim + /// + /// Comma-separated list of keys that can be used to complete sk in addition to the default enter key. When this + /// option is set, sk will print the name of the key pressed as the first line of its output (or as the second + /// line if --print-query is also used). The line will be empty if sk is completed with the default enter key. If + /// --expect option is specified multiple times, sk will expect the union of the keys. --no-expect will clear the + /// list. + /// + /// e.g. sk --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@ + #[arg(long, help_heading = "Scripting")] + pub expect: Vec, + + /// Read input delimited by ASCII NUL(\\0) characters + #[arg(long, help_heading = "Scripting")] + pub read0: bool, + + /// Print output delimited by ASCII NUL(\\0) characters + #[arg(long, help_heading = "Scripting")] + pub print0: bool, + + /// Print the query as the first line + #[arg(long, help_heading = "Scripting")] + pub print_query: bool, + + /// Print the command as the first line (after print-query) + #[arg(long, help_heading = "Scripting")] + pub print_cmd: bool, + + /// Print the command as the first line (after print-cmd) + #[arg(long, help_heading = "Scripting")] + pub print_score: bool, + + /// Automatically select the match if there is only one + #[arg(long, short = '1', help_heading = "Scripting")] + pub select_1: bool, + + /// Automatically exit when no match is left + #[arg(long, short = '0', help_heading = "Scripting")] + pub exit_0: bool, + + /// Synchronous search for multi-staged filtering + /// + /// Synchronous search for multi-staged filtering. If specified, + /// skim will launch ncurses finder only after the input stream is complete. + /// + /// e.g. sk --multi | sk --sync + #[arg(long, help_heading = "Scripting")] + pub sync: bool, + + /// Pre-select the first n items in multi-selection mode + #[arg(long, default_value = "0", help_heading = "Scripting")] + pub pre_select_n: usize, + + /// Pre-select the matched items in multi-selection mode + /// + /// Check the doc for the detailed syntax: + /// https://docs.rs/regex/1.4.1/regex/ + #[arg(long, default_value = "", help_heading = "Scripting")] + pub pre_select_pat: String, + + /// Pre-select the items separated by newline character + /// + /// Exemple: 'item1\nitem2' + #[arg(long, default_value = "", help_heading = "Scripting")] + pub pre_select_items: String, + + /// Pre-select the items read from this file + #[arg(long, help_heading = "Scripting")] + pub pre_select_file: Option, + + /// Query for filter mode + #[arg(long, short, help_heading = "Scripting")] + pub filter: Option, + + /// Reserved for later use + /// + /// Run in a tmux popup + /// + /// Format: sk --tmux [,SIZE[%]][,SIZE[%]] + /// + /// Depending on the direction, the order and behavior of the sizes varies: + /// - center: (width, height) or (size, size) if only one is provided + /// - top | bottom: (height, width) or height = size, width = 100% if only one is provided + /// - left | right: (width, height) or height = 100%, width = size if only one is provided + /// + /// Default: center,50% + #[arg(long, verbatim_doc_comment, help_heading = "Display", default_missing_value = "center,50%", num_args=0..)] + pub tmux: Option, + + /// Reserved for later use + #[arg(short = 'x', long, hide = true, help_heading = "Reserved for later use")] + pub extended: bool, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub literal: bool, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub cycle: bool, + + /// Reserved for later use + #[arg(long, hide = true, default_value = "10", help_heading = "Reserved for later use")] + pub hscroll_off: usize, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub filepath_word: bool, + + /// Reserved for later use + #[arg( + long, + hide = true, + default_value = "abcdefghijklmnopqrstuvwxyz", + help_heading = "Reserved for later use" + )] + pub jump_labels: String, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub border: bool, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub no_bold: bool, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub info: bool, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub pointer: bool, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub marker: bool, + + /// Reserved for later use + #[arg(long, hide = true, help_heading = "Reserved for later use")] + pub phony: bool, +} + +impl Default for SkimOptions { + fn default() -> Self { + Self::parse_from::<_, &str>([]) + } +} + +impl SkimOptionsBuilder { + pub fn build(&mut self) -> Result { + if let Some(true) = self.no_height { + self.height = Some("100%".to_string()); + } + + if let Some(true) = self.reverse { + self.layout = Some("reverse".to_string()); + } + + self.final_build() + } +} + +impl SkimOptions { + pub fn build(mut self) -> Self { + if self.no_height { + self.height = String::from("100%"); + } + + if self.reverse { + self.layout = String::from("reverse"); + } + let history_binds = String::from("ctrl-p:previous-history,ctrl-n:next-history"); + if self.history.is_some() || self.cmd_history.is_some() { + self.bind.push(history_binds); + } + + self + } +} diff --git a/src/orderedvec.rs b/skim/src/orderedvec.rs similarity index 100% rename from src/orderedvec.rs rename to skim/src/orderedvec.rs diff --git a/src/output.rs b/skim/src/output.rs similarity index 100% rename from src/output.rs rename to skim/src/output.rs diff --git a/src/prelude.rs b/skim/src/prelude.rs similarity index 100% rename from src/prelude.rs rename to skim/src/prelude.rs diff --git a/src/previewer.rs b/skim/src/previewer.rs similarity index 100% rename from src/previewer.rs rename to skim/src/previewer.rs diff --git a/src/query.rs b/skim/src/query.rs similarity index 95% rename from src/query.rs rename to skim/src/query.rs index 6ee1fb8b..88665a0e 100644 --- a/src/query.rs +++ b/skim/src/query.rs @@ -7,7 +7,7 @@ use unicode_width::UnicodeWidthStr; use crate::event::{Event, EventHandler, UpdateScreen}; use crate::options::SkimOptions; use crate::theme::{ColorTheme, DEFAULT_THEME}; -use crate::util::clear_canvas; +use crate::util::{clear_canvas, read_file_lines}; #[derive(Clone, Copy, PartialEq)] enum QueryMode { @@ -104,36 +104,37 @@ impl Query { fn parse_options(&mut self, options: &SkimOptions) { // some options accept multiple values, thus take the last one - if let Some(base_cmd) = options.cmd { + if let Some(base_cmd) = &options.cmd { self.base_cmd = base_cmd.to_string(); } - if let Some(query) = options.query { + if let Some(query) = &options.query { self.fz_query_before = query.chars().collect(); } - if let Some(cmd_query) = options.cmd_query { + if let Some(cmd_query) = &options.cmd_query { self.cmd_before = cmd_query.chars().collect(); } - if let Some(replstr) = options.replstr { - self.replstr = replstr.to_string(); - } - if options.interactive { self.mode = QueryMode::Cmd; } - if let Some(query_prompt) = options.prompt { - self.query_prompt = query_prompt.to_string(); - } + self.query_prompt = options.prompt.clone(); - if let Some(cmd_prompt) = options.cmd_prompt { - self.cmd_prompt = cmd_prompt.to_string(); + self.cmd_prompt = options.cmd_prompt.clone(); + + self.replstr = options.replstr.clone(); + + if let Some(history_file) = &options.history { + self.fz_query_history_before = + read_file_lines(history_file).expect(&format!("Failed to open history file {}", history_file)); } - self.fz_query_history_before = options.query_history.to_vec(); - self.cmd_history_before = options.cmd_history.to_vec(); + if let Some(cmd_history_file) = &options.cmd_history { + self.cmd_history_before = read_file_lines(cmd_history_file) + .expect(&format!("Failed to open command history file {}", cmd_history_file)); + } } pub fn in_query_mode(&self) -> bool { diff --git a/src/reader.rs b/skim/src/reader.rs similarity index 89% rename from src/reader.rs rename to skim/src/reader.rs index 4e2a5d25..c2cfe2aa 100644 --- a/src/reader.rs +++ b/skim/src/reader.rs @@ -3,6 +3,7 @@ //! After reading in a line, reader will save an item into the pool(items) use crate::global::mark_new_run; use crate::options::SkimOptions; +use crate::prelude::*; use crate::spinlock::SpinLock; use crate::{SkimItem, SkimItemReceiver}; use crossbeam::channel::{bounded, select, Sender}; @@ -64,8 +65,17 @@ pub struct Reader { impl Reader { pub fn with_options(options: &SkimOptions) -> Self { + let item_reader_option = SkimItemReaderOption::default() + .ansi(options.ansi) + .delimiter(&options.delimiter) + .with_nth(options.with_nth.iter().map(|s| s.as_str())) + .nth(options.nth.iter().map(|s| s.as_str())) + .read0(options.read0) + .show_error(options.show_cmd_error) + .build(); + let cmd_collector = Rc::new(RefCell::new(SkimItemReader::new(item_reader_option))); Self { - cmd_collector: options.cmd_collector.clone(), + cmd_collector, rx_item: None, } } diff --git a/src/selection.rs b/skim/src/selection.rs similarity index 93% rename from src/selection.rs rename to skim/src/selection.rs index a696547f..4d68c9da 100644 --- a/src/selection.rs +++ b/skim/src/selection.rs @@ -10,8 +10,10 @@ use crate::event::{Event, EventHandler, UpdateScreen}; use crate::global::current_run_num; use crate::item::MatchedItem; use crate::orderedvec::OrderedVec; +use crate::prelude::DefaultSkimSelector; use crate::theme::{ColorTheme, DEFAULT_THEME}; use crate::util::clear_canvas; +use crate::util::read_file_lines; use crate::util::{print_item, reshape_string, LinePrinter}; use crate::{DisplayContext, MatchRange, Matches, Selector, SkimItem, SkimOptions}; use indexmap::IndexMap; @@ -101,25 +103,45 @@ impl Selection { self.no_hscroll = true; } - if let Some(tabstop_str) = options.tabstop { - let tabstop = tabstop_str.parse::().unwrap_or(8); - self.tabstop = max(1, tabstop); - } + self.tabstop = max(1, options.tabstop); if options.tac { self.items.tac(true); } - if options.nosort { + if options.no_sort { self.items.nosort(true); } - if !options.skip_to_pattern.is_empty() { - self.skip_to_pattern = Regex::new(options.skip_to_pattern).ok(); + if let Some(skip_to_pattern) = options.skip_to_pattern.clone() { + self.skip_to_pattern = Regex::new(&skip_to_pattern).ok(); } self.keep_right = options.keep_right; - self.selector = options.selector.clone(); + let pre_select_items: Vec = options + .pre_select_items + .clone() + .split('\n') + .map(|item| item.to_string()) + .collect(); + + if options.pre_select_n > 0 + || !options.pre_select_pat.is_empty() + || !pre_select_items.is_empty() + || options.pre_select_file.is_some() + { + let mut preset_file_items: Vec = vec![]; + if let Some(pre_select_file) = options.pre_select_file.clone() { + preset_file_items = read_file_lines(&pre_select_file).unwrap(); + } + + let selector = DefaultSkimSelector::default() + .first_n(options.pre_select_n) + .regex(&options.pre_select_pat) + .preset(pre_select_items) + .preset(preset_file_items); + self.selector = Some(Rc::new(selector)); + } } pub fn theme(mut self, theme: Arc) -> Self { diff --git a/src/spinlock.rs b/skim/src/spinlock.rs similarity index 100% rename from src/spinlock.rs rename to skim/src/spinlock.rs diff --git a/src/theme.rs b/skim/src/theme.rs similarity index 99% rename from src/theme.rs rename to skim/src/theme.rs index a5e37628..fd21e4b4 100644 --- a/src/theme.rs +++ b/skim/src/theme.rs @@ -50,8 +50,8 @@ pub struct ColorTheme { impl ColorTheme { pub fn init_from_options(options: &SkimOptions) -> ColorTheme { // register - if let Some(color) = options.color { - ColorTheme::from_options(color) + if let Some(color) = options.color.clone() { + ColorTheme::from_options(&color) } else { ColorTheme::dark256() } diff --git a/src/util.rs b/skim/src/util.rs similarity index 97% rename from src/util.rs rename to skim/src/util.rs index 17f95380..8d1e49c0 100644 --- a/src/util.rs +++ b/skim/src/util.rs @@ -1,5 +1,7 @@ use std::borrow::Cow; use std::cmp::{max, min}; +use std::fs::File; +use std::io::{BufRead, BufReader}; use std::prelude::v1::*; use std::str::FromStr; @@ -406,6 +408,13 @@ pub fn atoi(string: &str) -> Option { RE_NUMBER.find(string).and_then(|mat| mat.as_str().parse::().ok()) } +pub fn read_file_lines(filename: &str) -> std::result::Result, std::io::Error> { + let file = File::open(filename)?; + let ret = BufReader::new(file).lines().collect(); + debug!("file content: {:?}", ret); + ret +} + #[cfg(test)] mod tests { use super::*; @@ -478,7 +487,7 @@ mod tests { fn test_atoi() { assert_eq!(None, atoi::("")); assert_eq!(Some(1), atoi::("1")); - assert_eq!(Some(8589934592), atoi::("8589934592")); + assert_eq!(Some(usize::MAX), atoi::(&format!("{}", usize::MAX))); assert_eq!(Some(1), atoi::("a1")); assert_eq!(Some(1), atoi::("1b")); assert_eq!(Some(1), atoi::("a1b")); diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index dcc1f4f7..00000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,557 +0,0 @@ -extern crate clap; -extern crate env_logger; -#[macro_use] -extern crate log; -extern crate shlex; -extern crate skim; - -use derive_builder::Builder; -use std::env; -use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, IsTerminal, Write}; - -use clap::{crate_version, App, Arg, ArgMatches}; -use skim::prelude::*; - -const USAGE: &str = " -Usage: sk [options] - - Options - -h, --help print this help menu - --version print out the current version of skim - - Search - --tac reverse the order of search result - --no-sort Do not sort the result - -t, --tiebreak [score,begin,end,-score,length...] - - comma seperated criteria - -n, --nth 1,2..5 specify the fields to be matched - --with-nth 1,2..5 specify the fields to be transformed - -d, --delimiter \\t specify the delimiter(in REGEX) for fields - -e, --exact start skim in exact mode - --regex use regex instead of fuzzy match - --algo=TYPE Fuzzy matching algorithm: - [skim_v1|skim_v2|clangd] (default: skim_v2) - --case [respect,ignore,smart] (default: smart) - case sensitive or not - - Interface - -b, --bind KEYBINDS comma seperated keybindings, in KEY:ACTION - such as 'ctrl-j:accept,ctrl-k:kill-line' - -m, --multi Enable Multiple Selection - --no-multi Disable Multiple Selection - --no-mouse Disable mouse events - -c, --cmd ag command to invoke dynamically - -i, --interactive Start skim in interactive(command) mode - --color [BASE][,COLOR:ANSI] - change color theme - --no-hscroll Disable horizontal scroll - --keep-right Keep the right end of the line visible on overflow - --skip-to-pattern Line starts with the start of matched pattern - --no-clear-if-empty Do not clear previous items if command returns empty result - --no-clear-start Do not clear on start - --show-cmd-error Send command error message if command fails - - Layout - --layout=LAYOUT Choose layout: [default|reverse|reverse-list] - --height=HEIGHT Height of skim's window (--height 40%) - --no-height Disable height feature - --min-height=HEIGHT Minimum height when --height is given by percent - (default: 10) - --margin=MARGIN Screen Margin (TRBL / TB,RL / T,RL,B / T,R,B,L) - e.g. (sk --margin 1,10%) - -p, --prompt '> ' prompt string for query mode - --cmd-prompt '> ' prompt string for command mode - - Display - --ansi parse ANSI color codes for input strings - --tabstop=SPACES Number of spaces for a tab character (default: 8) - --inline-info Display info next to query - --header=STR Display STR next to info - --header-lines=N The first N lines of the input are treated as header - - History - --history=FILE History file - --history-size=N Maximum number of query history entries (default: 1000) - --cmd-history=FILE command History file - --cmd-history-size=N Maximum number of command history entries (default: 1000) - - Preview - --preview=COMMAND command to preview current highlighted line ({}) - We can specify the fields. e.g. ({1}, {..3}, {0..}) - --preview-window=OPT Preview window layout (default: right:50%) - [up|down|left|right][:SIZE[%]][:hidden][:+SCROLL[-OFFSET]] - - Scripting - -q, --query \"\" specify the initial query - --cmd-query \"\" specify the initial query for interactive mode - --expect KEYS comma seperated keys that can be used to complete skim - --read0 Read input delimited by ASCII NUL(\\0) characters - --print0 Print output delimited by ASCII NUL(\\0) characters - --no-clear-start Do not clear screen on start - --no-clear Do not clear screen on exit - --print-query Print query as the first line - --print-cmd Print command query as the first line (after --print-query) - --print-score Print matching score in filter output (with --filter) - -1, --select-1 Automatically select the only match - -0, --exit-0 Exit immediately when there's no match - --sync Synchronous search for multi-staged filtering - --pre-select-n=NUM Pre-select the first n items in multi-selection mode - --pre-select-pat=REGEX - Pre-select the matched items in multi-selection mode - --pre-select-items=$'item1\\nitem2' - Pre-select the items separated by newline character - --pre-select-file=FILENAME - Pre-select the items read from file - - Environment variables - SKIM_DEFAULT_COMMAND Default command to use when input is tty - SKIM_DEFAULT_OPTIONS Default options (e.g. '--ansi --regex') - You should not include other environment variables - (e.g. '-c \"$HOME/bin/ag\"') - - Removed - -I replstr replace `replstr` with the selected item - - Reserved (not used for now) - --extended - --literal - --cycle - --hscroll-off=COL - --filepath-word - --jump-labels=CHARS - --border - --no-bold - --info - --pointer - --marker - --phony -"; - -const DEFAULT_HISTORY_SIZE: usize = 1000; - -//------------------------------------------------------------------------------ -fn main() { - env_logger::builder().format_timestamp_nanos().init(); - - match real_main() { - Ok(exit_code) => std::process::exit(exit_code), - Err(err) => { - // if downstream pipe is closed, exit silently, see PR#279 - if err.kind() == std::io::ErrorKind::BrokenPipe { - std::process::exit(0) - } - std::process::exit(2) - } - } -} - -#[rustfmt::skip] -fn real_main() -> Result { - let mut stdout = std::io::stdout(); - - let mut args = Vec::new(); - - args.push(env::args().next().expect("there should be at least one arg: the application name")); - args.extend(env::var("SKIM_DEFAULT_OPTIONS") - .ok() - .and_then(|val| shlex::split(&val)) - .unwrap_or_default()); - for arg in env::args().skip(1) { - args.push(arg); - } - - - //------------------------------------------------------------------------------ - // parse options - let opts = App::new("sk") - .author("Jinzhou Zhang") - .version(crate_version!()) - .args_override_self(true) - .arg(Arg::with_name("help").long("help").short('h')) - .arg(Arg::with_name("bind").long("bind").short('b').multiple(true).takes_value(true)) - .arg(Arg::with_name("multi").long("multi").short('m')) - .arg(Arg::with_name("no-multi").long("no-multi")) - .arg(Arg::with_name("prompt").long("prompt").short('p').multiple(true).takes_value(true).default_value("> ")) - .arg(Arg::with_name("cmd-prompt").long("cmd-prompt").multiple(true).takes_value(true).default_value("c> ")) - .arg(Arg::with_name("expect").long("expect").multiple(true).takes_value(true)) - .arg(Arg::with_name("tac").long("tac")) - .arg(Arg::with_name("tiebreak").long("tiebreak").short('t').multiple(true).takes_value(true)) - .arg(Arg::with_name("ansi").long("ansi")) - .arg(Arg::with_name("exact").long("exact").short('e')) - .arg(Arg::with_name("cmd").long("cmd").short('c').multiple(true).takes_value(true)) - .arg(Arg::with_name("interactive").long("interactive").short('i')) - .arg(Arg::with_name("query").long("query").short('q').multiple(true).takes_value(true)) - .arg(Arg::with_name("cmd-query").long("cmd-query").multiple(true).takes_value(true)) - .arg(Arg::with_name("regex").long("regex")) - .arg(Arg::with_name("delimiter").long("delimiter").short('d').multiple(true).takes_value(true)) - .arg(Arg::with_name("nth").long("nth").short('n').multiple(true).takes_value(true)) - .arg(Arg::with_name("with-nth").long("with-nth").multiple(true).takes_value(true)) - .arg(Arg::with_name("replstr").short('I').multiple(true).takes_value(true)) - .arg(Arg::with_name("color").long("color").multiple(true).takes_value(true)) - .arg(Arg::with_name("margin").long("margin").multiple(true).takes_value(true).default_value("0,0,0,0")) - .arg(Arg::with_name("min-height").long("min-height").multiple(true).takes_value(true).default_value("10")) - .arg(Arg::with_name("height").long("height").multiple(true).takes_value(true).default_value("100%")) - .arg(Arg::with_name("no-height").long("no-height")) - .arg(Arg::with_name("no-clear").long("no-clear")) - .arg(Arg::with_name("no-clear-start").long("no-clear-start")) - .arg(Arg::with_name("no-mouse").long("no-mouse")) - .arg(Arg::with_name("preview").long("preview").multiple(true).takes_value(true)) - .arg(Arg::with_name("preview-window").long("preview-window").multiple(true).takes_value(true).default_value("right:50%")) - .arg(Arg::with_name("reverse").long("reverse")) - - .arg(Arg::with_name("algorithm").long("algo").multiple(true).takes_value(true).default_value("skim_v2")) - .arg(Arg::with_name("case").long("case").multiple(true).takes_value(true).default_value("smart")) - .arg(Arg::with_name("literal").long("literal")) - .arg(Arg::with_name("cycle").long("cycle")) - .arg(Arg::with_name("no-hscroll").long("no-hscroll")) - .arg(Arg::with_name("hscroll-off").long("hscroll-off").multiple(true).takes_value(true).default_value("10")) - .arg(Arg::with_name("filepath-word").long("filepath-word")) - .arg(Arg::with_name("jump-labels").long("jump-labels").multiple(true).takes_value(true).default_value("abcdefghijklmnopqrstuvwxyz")) - .arg(Arg::with_name("border").long("border")) - .arg(Arg::with_name("inline-info").long("inline-info")) - .arg(Arg::with_name("header").long("header").multiple(true).takes_value(true).default_value("")) - .arg(Arg::with_name("header-lines").long("header-lines").multiple(true).takes_value(true).default_value("0")) - .arg(Arg::with_name("tabstop").long("tabstop").multiple(true).takes_value(true).default_value("8")) - .arg(Arg::with_name("no-bold").long("no-bold")) - .arg(Arg::with_name("history").long("history").multiple(true).takes_value(true)) - .arg(Arg::with_name("cmd-history").long("cmd-history").multiple(true).takes_value(true)) - .arg(Arg::with_name("history-size").long("history-size").multiple(true).takes_value(true).default_value("1000")) - .arg(Arg::with_name("cmd-history-size").long("cmd-history-size").multiple(true).takes_value(true).default_value("1000")) - .arg(Arg::with_name("print-query").long("print-query")) - .arg(Arg::with_name("print-cmd").long("print-cmd")) - .arg(Arg::with_name("print-score").long("print-score")) - .arg(Arg::with_name("read0").long("read0")) - .arg(Arg::with_name("print0").long("print0")) - .arg(Arg::with_name("sync").long("sync")) - .arg(Arg::with_name("extended").long("extended").short('x')) - .arg(Arg::with_name("no-sort").long("no-sort")) - .arg(Arg::with_name("select-1").long("select-1").short('1')) - .arg(Arg::with_name("exit-0").long("exit-0").short('0')) - .arg(Arg::with_name("filter").long("filter").short('f').takes_value(true).multiple(true)) - .arg(Arg::with_name("layout").long("layout").multiple(true).takes_value(true).default_value("default")) - .arg(Arg::with_name("keep-right").long("keep-right")) - .arg(Arg::with_name("skip-to-pattern").long("skip-to-pattern").multiple(true).takes_value(true).default_value("")) - .arg(Arg::with_name("pre-select-n").long("pre-select-n").multiple(true).takes_value(true).default_value("0")) - .arg(Arg::with_name("pre-select-pat").long("pre-select-pat").multiple(true).takes_value(true).default_value("")) - .arg(Arg::with_name("pre-select-items").long("pre-select-items").multiple(true).takes_value(true)) - .arg(Arg::with_name("pre-select-file").long("pre-select-file").multiple(true).takes_value(true).default_value("")) - .arg(Arg::with_name("no-clear-if-empty").long("no-clear-if-empty")) - .arg(Arg::with_name("show-cmd-error").long("show-cmd-error")) - .get_matches_from(args); - - if opts.is_present("help") { - write!(stdout, "{}", USAGE)?; - return Ok(0); - } - - //------------------------------------------------------------------------------ - let mut options = parse_options(&opts); - - let preview_window_joined = opts.values_of("preview-window").map(|x| x.collect::>().join(":")); - options.preview_window = preview_window_joined.as_deref(); - - //------------------------------------------------------------------------------ - // initialize collector - let item_reader_option = SkimItemReaderOption::default() - .ansi(opts.is_present("ansi")) - .delimiter(opts.values_of("delimiter").and_then(|vals| vals.last()).unwrap_or("")) - .with_nth(opts.values_of("with-nth").and_then(|vals| vals.last()).unwrap_or("")) - .nth(opts.values_of("nth").and_then(|vals| vals.last()).unwrap_or("")) - .read0(opts.is_present("read0")) - .show_error(opts.is_present("show-cmd-error")) - .build(); - - let cmd_collector = Rc::new(RefCell::new(SkimItemReader::new(item_reader_option))); - options.cmd_collector = cmd_collector.clone(); - - //------------------------------------------------------------------------------ - // read in the history file - let fz_query_histories = opts.values_of("history").and_then(|vals| vals.last()); - let cmd_query_histories = opts.values_of("cmd-history").and_then(|vals| vals.last()); - let query_history = fz_query_histories.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_default(); - let cmd_history = cmd_query_histories.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_default(); - - if fz_query_histories.is_some() || cmd_query_histories.is_some() { - options.query_history = &query_history; - options.cmd_history = &cmd_history; - // bind ctrl-n and ctrl-p to handle history - options.bind.insert(0, "ctrl-p:previous-history,ctrl-n:next-history"); - } - - //------------------------------------------------------------------------------ - // handle pre-selection options - let pre_select_n: Option = opts.values_of("pre-select-n").and_then(|vals| vals.last()).and_then(|s| s.parse().ok()); - let pre_select_pat = opts.values_of("pre-select-pat").and_then(|vals| vals.last()); - let pre_select_items: Option> = opts.values_of("pre-select-items").map(|vals| vals.flat_map(|m|m.split('\n')).map(|s|s.to_string()).collect()); - let pre_select_file = opts.values_of("pre-select-file").and_then(|vals| vals.last()); - - if pre_select_n.is_some() || pre_select_pat.is_some() || pre_select_items.is_some() || pre_select_file.is_some() { - let first_n = pre_select_n.unwrap_or(0); - let pattern = pre_select_pat.unwrap_or(""); - let preset_items = pre_select_items.unwrap_or_default(); - let preset_file = pre_select_file.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_default(); - - let selector = DefaultSkimSelector::default() - .first_n(first_n) - .regex(pattern) - .preset(preset_items) - .preset(preset_file); - options.selector = Some(Rc::new(selector)); - } - - let options = options; - - //------------------------------------------------------------------------------ - let bin_options = BinOptionsBuilder::default() - .filter(opts.values_of("filter").and_then(|vals| vals.last())) - .print_query(opts.is_present("print-query")) - .print_cmd(opts.is_present("print-cmd")) - .output_ending(if opts.is_present("print0") { "\0" } else { "\n" }) - .build() - .expect(""); - - //------------------------------------------------------------------------------ - // read from pipe or command - let rx_item = if ! std::io::stdin().is_terminal() { - let rx_item = cmd_collector.borrow().of_bufread(BufReader::new(std::io::stdin())); - Some(rx_item) - } else { - None - }; - - //------------------------------------------------------------------------------ - // filter mode - if opts.is_present("filter") { - return filter(&bin_options, &options, rx_item); - } - - //------------------------------------------------------------------------------ - let output = Skim::run_with(&options, rx_item); - if output.is_none() { // error - return Ok(135); - } - - //------------------------------------------------------------------------------ - // output - let output = output.unwrap(); - if output.is_abort { - return Ok(130); - } - - // output query - if bin_options.print_query { - write!(stdout, "{}{}", output.query, bin_options.output_ending)?; - } - - if bin_options.print_cmd { - write!(stdout, "{}{}", output.cmd, bin_options.output_ending)?; - } - - if opts.is_present("expect") { - match output.final_event { - Event::EvActAccept(Some(accept_key)) => { - write!(stdout, "{}{}", accept_key, bin_options.output_ending)?; - } - Event::EvActAccept(None) => { - write!(stdout, "{}", bin_options.output_ending)?; - } - _ => {} - } - } - - for item in output.selected_items.iter() { - write!(stdout, "{}{}", item.output(), bin_options.output_ending)?; - } - - //------------------------------------------------------------------------------ - // write the history with latest item - if let Some(file) = fz_query_histories { - let limit = opts.values_of("history-size").and_then(|vals| vals.last()) - .and_then(|size| size.parse::().ok()) - .unwrap_or(DEFAULT_HISTORY_SIZE); - write_history_to_file(&query_history, &output.query, limit, file)?; - } - - if let Some(file) = cmd_query_histories { - let limit = opts.values_of("cmd-history-size").and_then(|vals| vals.last()) - .and_then(|size| size.parse::().ok()) - .unwrap_or(DEFAULT_HISTORY_SIZE); - write_history_to_file(&cmd_history, &output.cmd, limit, file)?; - } - - Ok(if output.selected_items.is_empty() { 1 } else { 0 }) -} - -fn parse_options(options: &ArgMatches) -> SkimOptions<'_> { - SkimOptionsBuilder::default() - .color(options.values_of("color").and_then(|vals| vals.last())) - .min_height(options.values_of("min-height").and_then(|vals| vals.last())) - .no_height(options.is_present("no-height")) - .height(options.values_of("height").and_then(|vals| vals.last())) - .margin(options.values_of("margin").and_then(|vals| vals.last())) - .preview(options.values_of("preview").and_then(|vals| vals.last())) - .cmd(options.values_of("cmd").and_then(|vals| vals.last())) - .query(options.values_of("query").and_then(|vals| vals.last())) - .cmd_query(options.values_of("cmd-query").and_then(|vals| vals.last())) - .interactive(options.is_present("interactive")) - .prompt(options.values_of("prompt").and_then(|vals| vals.last())) - .cmd_prompt(options.values_of("cmd-prompt").and_then(|vals| vals.last())) - .bind( - options - .values_of("bind") - .map(|x| x.collect::>()) - .unwrap_or_default(), - ) - .expect(options.values_of("expect").map(|x| x.collect::>().join(","))) - .multi(if options.is_present("no-multi") { - false - } else { - options.is_present("multi") - }) - .layout(options.values_of("layout").and_then(|vals| vals.last()).unwrap_or("")) - .reverse(options.is_present("reverse")) - .no_hscroll(options.is_present("no-hscroll")) - .no_mouse(options.is_present("no-mouse")) - .no_clear(options.is_present("no-clear")) - .no_clear_start(options.is_present("no-clear-start")) - .tabstop(options.values_of("tabstop").and_then(|vals| vals.last())) - .tiebreak(options.values_of("tiebreak").map(|x| x.collect::>().join(","))) - .tac(options.is_present("tac")) - .nosort(options.is_present("no-sort")) - .exact(options.is_present("exact")) - .regex(options.is_present("regex")) - .delimiter(options.values_of("delimiter").and_then(|vals| vals.last())) - .inline_info(options.is_present("inline-info")) - .header(options.values_of("header").and_then(|vals| vals.last())) - .header_lines( - options - .values_of("header-lines") - .and_then(|vals| vals.last()) - .map(|s| s.parse::().unwrap_or(0)) - .unwrap_or(0), - ) - .layout(options.values_of("layout").and_then(|vals| vals.last()).unwrap_or("")) - .algorithm(FuzzyAlgorithm::of( - options.values_of("algorithm").and_then(|vals| vals.last()).unwrap(), - )) - .case(match options.value_of("case") { - Some("smart") => CaseMatching::Smart, - Some("ignore") => CaseMatching::Ignore, - _ => CaseMatching::Respect, - }) - .keep_right(options.is_present("keep-right")) - .skip_to_pattern( - options - .values_of("skip-to-pattern") - .and_then(|vals| vals.last()) - .unwrap_or(""), - ) - .select1(options.is_present("select-1")) - .exit0(options.is_present("exit-0")) - .sync(options.is_present("sync")) - .no_clear_if_empty(options.is_present("no-clear-if-empty")) - .build() - .unwrap() -} - -fn read_file_lines(filename: &str) -> Result, std::io::Error> { - let file = File::open(filename)?; - let ret = BufReader::new(file).lines().collect(); - debug!("file content: {:?}", ret); - ret -} - -fn write_history_to_file( - orig_history: &[String], - latest: &str, - limit: usize, - filename: &str, -) -> Result<(), std::io::Error> { - if orig_history.last().map(|l| l.as_str()) == Some(latest) { - // no point of having at the end of the history 5x the same command... - return Ok(()); - } - let additional_lines = if latest.trim().is_empty() { 0 } else { 1 }; - let start_index = if orig_history.len() + additional_lines > limit { - orig_history.len() + additional_lines - limit - } else { - 0 - }; - - let mut history = orig_history[start_index..].to_vec(); - history.push(latest.to_string()); - - let file = File::create(filename)?; - let mut file = BufWriter::new(file); - file.write_all(history.join("\n").as_bytes())?; - Ok(()) -} - -#[derive(Builder)] -pub struct BinOptions<'a> { - filter: Option<&'a str>, - output_ending: &'a str, - print_query: bool, - print_cmd: bool, -} - -pub fn filter( - bin_option: &BinOptions, - options: &SkimOptions, - source: Option, -) -> Result { - let mut stdout = std::io::stdout(); - - let default_command = match env::var("SKIM_DEFAULT_COMMAND").as_ref().map(String::as_ref) { - Ok("") | Err(_) => "find .".to_owned(), - Ok(val) => val.to_owned(), - }; - let query = bin_option.filter.unwrap_or(""); - let cmd = options.cmd.unwrap_or(&default_command); - - // output query - if bin_option.print_query { - write!(stdout, "{}{}", query, bin_option.output_ending)?; - } - - if bin_option.print_cmd { - write!(stdout, "{}{}", cmd, bin_option.output_ending)?; - } - - //------------------------------------------------------------------------------ - // matcher - let engine_factory: Box = if options.regex { - Box::new(RegexEngineFactory::builder()) - } else { - let fuzzy_engine_factory = ExactOrFuzzyEngineFactory::builder() - .fuzzy_algorithm(options.algorithm) - .exact_mode(options.exact) - .build(); - Box::new(AndOrEngineFactory::new(fuzzy_engine_factory)) - }; - - let engine = engine_factory.create_engine_with_case(query, options.case); - - //------------------------------------------------------------------------------ - // start - let components_to_stop = Arc::new(AtomicUsize::new(0)); - - let stream_of_item = source.unwrap_or_else(|| { - let cmd_collector = options.cmd_collector.clone(); - let (ret, _control) = cmd_collector.borrow_mut().invoke(cmd, components_to_stop); - ret - }); - - let mut num_matched = 0; - stream_of_item - .into_iter() - .filter_map(|item| engine.match_item(item.clone()).map(|result| (item, result))) - .try_for_each(|(item, _match_result)| { - num_matched += 1; - write!(stdout, "{}{}", item.output(), bin_option.output_ending) - })?; - - Ok(if num_matched == 0 { 1 } else { 0 }) -} diff --git a/src/options.rs b/src/options.rs deleted file mode 100644 index 075e46ae..00000000 --- a/src/options.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::rc::Rc; - -use derive_builder::Builder; - -use crate::helper::item_reader::SkimItemReader; -use crate::reader::CommandCollector; -use crate::{CaseMatching, FuzzyAlgorithm, MatchEngineFactory, Selector}; -use std::cell::RefCell; - -#[derive(Builder)] -#[builder(build_fn(name = "final_build"))] -#[builder(default)] -pub struct SkimOptions<'a> { - pub bind: Vec<&'a str>, - pub multi: bool, - pub prompt: Option<&'a str>, - pub cmd_prompt: Option<&'a str>, - pub expect: Option, - pub tac: bool, - pub nosort: bool, - pub tiebreak: Option, - pub exact: bool, - pub cmd: Option<&'a str>, - pub interactive: bool, - pub query: Option<&'a str>, - pub cmd_query: Option<&'a str>, - pub regex: bool, - pub delimiter: Option<&'a str>, - pub replstr: Option<&'a str>, - pub color: Option<&'a str>, - pub margin: Option<&'a str>, - pub no_height: bool, - pub no_clear: bool, - pub no_clear_start: bool, - pub min_height: Option<&'a str>, - pub height: Option<&'a str>, - pub preview: Option<&'a str>, - pub preview_window: Option<&'a str>, - pub reverse: bool, - pub tabstop: Option<&'a str>, - pub no_hscroll: bool, - pub no_mouse: bool, - pub inline_info: bool, - pub header: Option<&'a str>, - pub header_lines: usize, - pub layout: &'a str, - pub algorithm: FuzzyAlgorithm, - pub case: CaseMatching, - pub engine_factory: Option>, - pub query_history: &'a [String], - pub cmd_history: &'a [String], - pub cmd_collector: Rc>, - pub keep_right: bool, - pub skip_to_pattern: &'a str, - pub select1: bool, - pub exit0: bool, - pub sync: bool, - pub selector: Option>, - pub no_clear_if_empty: bool, -} - -impl<'a> Default for SkimOptions<'a> { - fn default() -> Self { - Self { - bind: vec![], - multi: false, - prompt: Some("> "), - cmd_prompt: Some("c> "), - expect: None, - tac: false, - nosort: false, - tiebreak: None, - exact: false, - cmd: None, - interactive: false, - query: None, - cmd_query: None, - regex: false, - delimiter: None, - replstr: Some("{}"), - color: None, - margin: Some("0,0,0,0"), - no_height: false, - no_clear: false, - no_clear_start: false, - min_height: Some("10"), - height: Some("100%"), - preview: None, - preview_window: Some("right:50%"), - reverse: false, - tabstop: None, - no_hscroll: false, - no_mouse: false, - inline_info: false, - header: None, - header_lines: 0, - layout: "", - algorithm: FuzzyAlgorithm::default(), - case: CaseMatching::default(), - engine_factory: None, - query_history: &[], - cmd_history: &[], - cmd_collector: Rc::new(RefCell::new(SkimItemReader::new(Default::default()))), - keep_right: false, - skip_to_pattern: "", - select1: false, - exit0: false, - sync: false, - selector: None, - no_clear_if_empty: false, - } - } -} - -impl<'a> SkimOptionsBuilder<'a> { - pub fn build(&mut self) -> Result, SkimOptionsBuilderError> { - if let Some(true) = self.no_height { - self.height = Some(Some("100%")); - } - - if let Some(true) = self.reverse { - self.layout = Some("reverse"); - } - - self.final_build() - } -} diff --git a/test/test_skim.py b/test/test_skim.py index 1af56c97..f85515c8 100644 --- a/test/test_skim.py +++ b/test/test_skim.py @@ -683,17 +683,16 @@ def test_header_lines(self): def test_reserved_options(self): options = [ '--extended', - '--algo=TYPE', '--literal', '--no-mouse', '--cycle', - '--hscroll-off=COL', + '--hscroll-off=10', '--filepath-word', '--jump-labels=CHARS', '--border', '--inline-info', '--header=STR', - '--header-lines=N', + '--header-lines=1', '--no-bold', '--history-size=10', '--sync', @@ -714,7 +713,7 @@ def test_multiple_option_values_should_be_accepted(self): options = [ '--bind=ctrl-a:cancel --bind ctrl-b:cancel', '--expect=ctrl-a --expect=ctrl-v', - '--tiebreak=index --tiebreak=score', + '--tiebreak=begin --tiebreak=score', '--cmd asdf --cmd find', '--query asdf -q xyz', '--delimiter , --delimiter . -d ,', diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..9ade3a73 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.20", features = ["derive"] } +clap_complete = "4.5.37" +clap_complete_fig = "4.5.2" +clap_complete_nushell = "4.5.4" +clap_mangen = "0.2.24" +skim = { path = "../skim" } diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..e96ef99a --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,81 @@ +#![allow(clippy::pedantic, clippy::complexity)] +use std::{ + env, + path::{Path, PathBuf}, +}; + +use clap::CommandFactory; +use skim::SkimOptions; + +type DynError = Box; + +fn main() { + if let Err(e) = try_main() { + eprintln!("{}", e); + std::process::exit(-1); + } +} + +fn try_main() -> Result<(), DynError> { + let task = env::args().nth(1); + match task.as_deref() { + Some("mangen") => mangen()?, + Some("compgen") => compgen()?, + _ => print_help(), + } + Ok(()) +} + +fn print_help() { + eprintln!( + "Tasks: + +mangen generate the man page +compgen generate completions for popular shells +" + ) +} + +fn mangen() -> Result<(), DynError> { + let mut buffer: Vec = Default::default(); + clap_mangen::Man::new(SkimOptions::command()).render(&mut buffer)?; + let mandir = project_root().join("man").join("man1"); + std::fs::create_dir_all(&mandir)?; + std::fs::write(mandir.join("sk.1"), buffer)?; + + Ok(()) +} + +fn compgen() -> Result<(), DynError> { + let completions_dir = project_root().join("shell"); + std::fs::create_dir_all(&completions_dir)?; + // Bash + let mut buffer: Vec = Default::default(); + clap_complete::generate( + clap_complete::Shell::Bash, + &mut SkimOptions::command(), + "sk", + &mut buffer, + ); + std::fs::write(completions_dir.join("completion.bash"), buffer)?; + + // Zsh + let mut buffer: Vec = Default::default(); + clap_complete::generate( + clap_complete::Shell::Zsh, + &mut SkimOptions::command(), + "sk", + &mut buffer, + ); + std::fs::write(completions_dir.join("completion.zsh"), buffer)?; + + Ok(()) +} + +fn project_root() -> PathBuf { + Path::new(&env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(1) + .unwrap() + .to_path_buf() +}