extern crate clap;
extern crate env_logger;
#[macro_use]
extern crate log;
extern crate atty;
extern crate shlex;
extern crate skim;
extern crate time;
use derive_builder::Builder;
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, 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;
const READ_BUFFER_CAPACITY: usize = 65_536;
fn main() {
env_logger::builder().format_timestamp_nanos().init();
match real_main() {
Ok(exit_code) => std::process::exit(exit_code),
Err(err) => {
if err.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(0)
}
std::process::exit(2)
}
}
}
#[rustfmt::skip]
fn real_main() -> Result<i32, std::io::Error> {
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);
}
let opts = App::new("sk")
.author("Jinzhou Zhang<lotabout@gmail.com>")
.version(crate_version!())
.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').multiple(true))
.arg(Arg::with_name("no-multi").long("no-multi").multiple(true))
.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").multiple(true))
.arg(Arg::with_name("tiebreak").long("tiebreak").short('t').multiple(true).takes_value(true).default_value("length,index"))
.arg(Arg::with_name("ansi").long("ansi").multiple(true))
.arg(Arg::with_name("exact").long("exact").short('e').multiple(true))
.arg(Arg::with_name("disabled").long("disabled").multiple(true))
.arg(Arg::with_name("cmd").long("cmd").short('c').multiple(true).takes_value(true))
.arg(Arg::with_name("interactive").long("interactive").short('i').multiple(true))
.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").multiple(true))
.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").multiple(true))
.arg(Arg::with_name("no-clear").long("no-clear").multiple(true))
.arg(Arg::with_name("no-clear-start").long("no-clear-start").multiple(true))
.arg(Arg::with_name("no-mouse").long("no-mouse").multiple(true))
.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").multiple(true))
.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").multiple(true))
.arg(Arg::with_name("cycle").long("cycle").multiple(true))
.arg(Arg::with_name("no-hscroll").long("no-hscroll").multiple(true))
.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").multiple(true))
.arg(Arg::with_name("jump-labels").long("jump-labels").multiple(true).takes_value(true).default_value("abcdefghijklmnopqrstuvwxyz"))
.arg(Arg::with_name("border").long("border").multiple(true))
.arg(Arg::with_name("inline-info").long("inline-info").multiple(true))
.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").multiple(true))
.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").multiple(true))
.arg(Arg::with_name("print-cmd").long("print-cmd").multiple(true))
.arg(Arg::with_name("print-score").long("print-score").multiple(true))
.arg(Arg::with_name("read0").long("read0").multiple(true))
.arg(Arg::with_name("print0").long("print0").multiple(true))
.arg(Arg::with_name("sync").long("sync").multiple(true))
.arg(Arg::with_name("extended").long("extended").short('x').multiple(true))
.arg(Arg::with_name("no-sort").long("no-sort").multiple(true))
.arg(Arg::with_name("select-1").long("select-1").short('1').multiple(true))
.arg(Arg::with_name("exit-0").long("exit-0").short('0').multiple(true))
.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").multiple(true))
.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").multiple(true))
.arg(Arg::with_name("show-cmd-error").long("show-cmd-error").multiple(true))
.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::<Vec<_>>().join(":"));
options.preview_window = preview_window_joined.as_deref();
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();
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;
options.bind.insert(0, "ctrl-p:previous-history,ctrl-n:next-history");
}
let pre_select_n: Option<usize> = 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<Vec<String>> = 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("");
let (rx_item, opt_ingest_handle) = if atty::isnt(atty::Stream::Stdin) {
let (rx_item, opt_ingest_handle) = cmd_collector.borrow().of_bufread(Box::new(BufReader::with_capacity(READ_BUFFER_CAPACITY, std::io::stdin())));
(Some(rx_item), opt_ingest_handle)
} else {
(None, None)
};
if opts.is_present("filter") {
return filter(&bin_options, &options, rx_item);
}
let output = Skim::run_with(&options, rx_item);
if output.is_none() { return Ok(135);
}
let output = output.unwrap();
if output.is_abort {
return Ok(130);
}
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)?;
}
if let Some(file) = fz_query_histories {
let limit = opts.values_of("history-size").and_then(|vals| vals.last())
.and_then(|size| size.parse::<usize>().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::<usize>().ok())
.unwrap_or(DEFAULT_HISTORY_SIZE);
write_history_to_file(&cmd_history, &output.cmd, limit, file)?;
}
opt_ingest_handle.map(|handle| handle.join());
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::<Vec<_>>())
.unwrap_or_default(),
)
.expect(options.values_of("expect").map(|x| x.collect::<Vec<_>>().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::<Vec<_>>().join(",")))
.tac(options.is_present("tac"))
.nosort(options.is_present("no-sort"))
.exact(options.is_present("exact"))
.disabled(options.is_present("disabled"))
.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::<usize>().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<Vec<String>, 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) {
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<SkimItemReceiver>,
) -> Result<i32, std::io::Error> {
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);
if bin_option.print_query {
write!(stdout, "{}{}", query, bin_option.output_ending)?;
}
if bin_option.print_cmd {
write!(stdout, "{}{}", cmd, bin_option.output_ending)?;
}
let engine_factory: Box<dyn MatchEngineFactory> = 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(Box::new(fuzzy_engine_factory)))
};
let engine = engine_factory.create_engine_with_case(query, options.case);
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 (stream_of_item, _control, _ingest_handle) = cmd_collector.borrow_mut().invoke(cmd, components_to_stop);
stream_of_item
});
let mut num_matched = 0;
stream_of_item
.into_iter()
.filter_map(|item| engine.match_item(item.as_ref()).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 })
}