├── .gitignore ├── tests ├── bash_tests │ ├── string_test.bash │ ├── string_test.out │ ├── echo_setting_on.dt │ ├── arg_test.dt │ ├── echo_settings.bash │ ├── echo_setting_off.dt │ ├── echo_settings.out │ ├── arg_test.bash │ ├── arg_test.out │ └── string_test.dt └── bash_tests.rs ├── demo.gif ├── src ├── lib.rs ├── rt_conf.rs ├── outproxy.rs ├── main.rs ├── core.rs └── parser.rs ├── dotree.dt ├── mk-release.bash ├── Cargo.toml ├── example.dt ├── grammar.pest ├── README.md ├── Cargo.lock └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | debuglog 3 | -------------------------------------------------------------------------------- /tests/bash_tests/string_test.bash: -------------------------------------------------------------------------------- 1 | $DT -c string_test.dt e 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnorrFG/dotree/HEAD/demo.gif -------------------------------------------------------------------------------- /tests/bash_tests/string_test.out: -------------------------------------------------------------------------------- 1 | [?25l[?25hfoo=foo 2 | foo foo 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod core; 2 | pub mod outproxy; 3 | pub mod parser; 4 | pub mod rt_conf; 5 | -------------------------------------------------------------------------------- /tests/bash_tests/echo_setting_on.dt: -------------------------------------------------------------------------------- 1 | menu root { 2 | y: "echo show me" 3 | n: @"echo don\'t show me" 4 | } 5 | -------------------------------------------------------------------------------- /tests/bash_tests/arg_test.dt: -------------------------------------------------------------------------------- 1 | menu root { 2 | f: cmd { 3 | vars a, b, c="foo" 4 | "echo $a $b $c" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/bash_tests/echo_settings.bash: -------------------------------------------------------------------------------- 1 | $DT -c echo_setting_on.dt y 2 | $DT -c echo_setting_on.dt n 3 | $DT -c echo_setting_off.dt y 4 | $DT -c echo_setting_off.dt n 5 | -------------------------------------------------------------------------------- /tests/bash_tests/echo_setting_off.dt: -------------------------------------------------------------------------------- 1 | echo off 2 | 3 | menu root { 4 | n: "echo don\'t show me" 5 | y: "echo 'show me' without echoing the command" - @"echo show me" 6 | } 7 | -------------------------------------------------------------------------------- /tests/bash_tests/echo_settings.out: -------------------------------------------------------------------------------- 1 | [?25l[?25hecho show me 2 | show me 3 | [?25l[?25hdon't show me 4 | [?25l[?25hecho show me 5 | show me 6 | [?25l[?25hdon't show me 7 | -------------------------------------------------------------------------------- /tests/bash_tests/arg_test.bash: -------------------------------------------------------------------------------- 1 | $DT -c arg_test.dt f alpha beta gamma 2 | 3 | printf "lala\n" | $DT -c arg_test.dt f alpha beta 4 | 5 | printf "\n" | $DT -c arg_test.dt f alpha beta 6 | 7 | printf "\n\n\n" | $DT -c arg_test.dt f 8 | 9 | -------------------------------------------------------------------------------- /tests/bash_tests/arg_test.out: -------------------------------------------------------------------------------- 1 | [?25l[?25hecho $a $b $c 2 | alpha beta gamma 3 | [?25l[?25h echo $a $b $c 4 | alpha beta lala 5 | [?25l[?25h echo $a $b $c 6 | alpha beta foo 7 | [?25l[?25h   echo $a $b $c 8 | foo 9 | -------------------------------------------------------------------------------- /tests/bash_tests/string_test.dt: -------------------------------------------------------------------------------- 1 | echo off 2 | 3 | snippet vars = !" 4 | FOO="foo" 5 | "! 6 | 7 | snippet a_fn = " # we want a newline here 8 | append_foo() { 9 | echo $1 foo 10 | } 11 | " # and here 12 | 13 | menu root { 14 | e: $vars + "echo foo=$FOO" + $a_fn + "append_foo $FOO" 15 | } 16 | -------------------------------------------------------------------------------- /dotree.dt: -------------------------------------------------------------------------------- 1 | menu root { 2 | t: "Run tests" - !" 3 | export RUST_BACKTRACE=1 4 | cargo clippy -- -D warnings 5 | cargo test 6 | "! 7 | r: "Make release" - "./mk-release.bash" 8 | w: "build windows" - !" 9 | cargo build --release --target=x86_64-pc-windows-gnu 10 | strip target/x86_64-pc-windows-gnu/release/dt.exe 11 | "! 12 | b: "bash tests" - "cargo test --test bash_tests" 13 | k: "update k9" - "K9_UPDATE_SNAPSHOTS=1 cargo test" 14 | } 15 | -------------------------------------------------------------------------------- /mk-release.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | pkg_id="$(cargo pkgid)" 6 | tag=${pkg_id##*#} 7 | 8 | cargo clippy -- -D warnings 9 | cargo test 10 | 11 | git push 12 | 13 | cargo build --release --target=x86_64-unknown-linux-musl 14 | strip target/x86_64-unknown-linux-musl/release/dt 15 | 16 | cargo build --release --target=x86_64-pc-windows-gnu 17 | strip target/x86_64-pc-windows-gnu/release/dt.exe 18 | 19 | gh release create "$tag"\ 20 | --draft 21 | # --notes-file release_notes.md 22 | 23 | gh release upload "$tag" "target/x86_64-unknown-linux-musl/release/dt#64-bit linux musl" 24 | gh release upload "$tag" "target/x86_64-pc-windows-gnu/release/dt.exe#64-bit windows" 25 | 26 | -------------------------------------------------------------------------------- /src/rt_conf.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use once_cell::sync::OnceCell; 4 | 5 | use crate::parser::Settings; 6 | 7 | static LOCAL_CONF_DIR: OnceCell> = OnceCell::new(); 8 | static SETTINGS: OnceCell = OnceCell::new(); 9 | 10 | pub fn init(local_conf_dir: Option, settings: Settings) { 11 | LOCAL_CONF_DIR 12 | .set(local_conf_dir) 13 | .expect("initiating rt conf twice"); 14 | SETTINGS.set(settings).unwrap(); 15 | } 16 | 17 | pub fn local_conf_dir() -> Option<&'static PathBuf> { 18 | LOCAL_CONF_DIR.get().expect("missing initiation").as_ref() 19 | } 20 | 21 | pub fn settings() -> &'static Settings { 22 | SETTINGS.get().expect("missing initiation") 23 | } 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dotree" 3 | version = "0.8.2" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | name = "dotree" 10 | path = "src/lib.rs" 11 | 12 | [[bin]] 13 | name = "dt" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | anyhow = "1.0.75" 18 | clap = { version = "4.4.6", features = ["derive"] } 19 | console = "0.15.7" 20 | ctrlc = "3.4.1" 21 | dirs = "5.0.1" 22 | hashbrown = "0.14.2" 23 | log = "0.4.20" 24 | once_cell = "1.18.0" 25 | pest = "2.7.4" 26 | pest_derive = "2.7.4" 27 | pretty_env_logger = "0.5.0" 28 | rustyline = { version = "12.0.0", features = ["derive"] } 29 | 30 | [target.'cfg(not(windows))'.dependencies] 31 | exec = "0.3.1" 32 | 33 | [dev-dependencies] 34 | k9 = "0.11.6" 35 | anyhow = "1.0.75" 36 | subprocess = "0.2.9" 37 | -------------------------------------------------------------------------------- /src/outproxy.rs: -------------------------------------------------------------------------------- 1 | use std::io::{stdout, Write}; 2 | 3 | pub struct OutProxy { 4 | pub n_lines: usize, 5 | } 6 | 7 | impl Write for OutProxy { 8 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 9 | self.n_lines += count_newlines(buf); 10 | stdout().write(buf) 11 | } 12 | 13 | fn flush(&mut self) -> std::io::Result<()> { 14 | stdout().flush() 15 | } 16 | } 17 | 18 | impl OutProxy { 19 | pub fn new() -> Self { 20 | OutProxy { n_lines: 0 } 21 | } 22 | } 23 | 24 | impl Default for OutProxy { 25 | fn default() -> Self { 26 | Self::new() 27 | } 28 | } 29 | 30 | #[cfg(target_os = "windows")] 31 | fn count_newlines(buf: &[u8]) -> usize { 32 | let mut count = 0; 33 | for i in 0..(buf.len() - 1) { 34 | if buf[i] as char == '\r' && buf[i + 1] as char == '\n' { 35 | count += 1; 36 | } 37 | } 38 | count 39 | } 40 | 41 | #[cfg(not(target_os = "windows"))] 42 | fn count_newlines(buf: &[u8]) -> usize { 43 | buf.iter().filter(|x| **x as char == '\n').count() 44 | } 45 | -------------------------------------------------------------------------------- /tests/bash_tests.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::PathBuf}; 2 | 3 | use anyhow::{ensure, Result}; 4 | use subprocess::{Exec, Redirection}; 5 | 6 | #[test] 7 | fn bash_tests() -> Result<()> { 8 | ensure!( 9 | Exec::shell("cargo b").join()?.success(), 10 | "Can't compile dotree" 11 | ); 12 | 13 | let bash_files = get_bash_files()?; 14 | let bin_path = fs::canonicalize("target/debug/dt")?; 15 | env::set_var("DT", bin_path.into_os_string()); 16 | for bash_file in bash_files { 17 | let output = Exec::cmd("bash") 18 | .arg(bash_file.file_name().unwrap()) 19 | .cwd("tests/bash_tests") 20 | .stderr(Redirection::Merge) 21 | .stdout(Redirection::Pipe) 22 | .capture()? 23 | .stdout_str(); 24 | let out_file_name = bash_file.with_extension("out"); 25 | if out_file_name.exists() { 26 | let contents = fs::read_to_string(out_file_name)?; 27 | ensure!( 28 | contents == output, 29 | "test {} failed. Outputs differ. New output: \n\n{}", 30 | bash_file.to_string_lossy(), 31 | output 32 | ); 33 | } else { 34 | fs::write(out_file_name, output)?; 35 | } 36 | } 37 | Ok(()) 38 | } 39 | 40 | fn get_bash_files() -> Result> { 41 | let mut res = vec![]; 42 | for f in fs::read_dir("tests/bash_tests")? { 43 | let f = f?.path(); 44 | if f.extension().and_then(|x| x.to_str()) == Some("bash") { 45 | res.push(f); 46 | } 47 | } 48 | Ok(res) 49 | } 50 | -------------------------------------------------------------------------------- /example.dt: -------------------------------------------------------------------------------- 1 | menu root { 2 | g: git 3 | m: misc 4 | r: rust 5 | c: config 6 | } 7 | 8 | menu config { 9 | b: "bash base" - "helix $DOTFILES/bash/base_config.bash" 10 | l: "bash local" - "helix ~/.bashrc.local" 11 | i: "i3" - "nvim ~/.config/i3/config" 12 | d: "dotree" - "helix ~/.config/dotree.dt" 13 | n: "nvim" - "nvim ~/.config/nvim/init.lua" 14 | } 15 | 16 | menu rust { 17 | c: "check paged" - "cargo check --color always 2>&1 | less -r" 18 | l: "count lines" - "fd -ers -X cloc" 19 | i: "install package" - "cargo install --path ." 20 | } 21 | 22 | menu git { 23 | am: "amend staged" - "git commit --amend --no-edit" 24 | aam: "amend all" - "git commit -a --amend --no-edit" 25 | b: "git switch $(git branch | fzf)" 26 | w: git_worktree 27 | } 28 | 29 | menu git_worktree { 30 | a: cmd { 31 | vars output_dir, branch 32 | !"git worktree add -b "$branch" "$output_dir""! 33 | } 34 | l: "list" - "git worktree list" 35 | p: "prune" - "git worktree prune" 36 | } 37 | 38 | menu misc { 39 | s: "systemctl suspend" 40 | mh: cmd{ 41 | vars file 42 | "md2html" - !" 43 | echo converting $file 44 | pandoc "$file" -c ~/Sync/share/pandoc.css --toc --standalone \ 45 | --embed-resources -so "${file%.md}.html" 46 | "! 47 | } 48 | th: cmd { 49 | vars file, expected_hash 50 | "test md5 hash" - !" 51 | if [[ `md5sum $file | awk '{print $1}'` == $expected_hash ]]; then 52 | echo 'success' 53 | else 54 | echo 'fail'; exit 1 55 | fi 56 | "! 57 | } 58 | r: cmd { 59 | vars pattern, replacement 60 | "grep replace" - !" 61 | for f in $(rg "$pattern" -l); do 62 | sed "s/$pattern/$replacement/g" -i $f 63 | done 64 | "! 65 | } 66 | x: cmd { 67 | vars file 68 | "extract" - !"7z x "$file" "-o${file}.d""! 69 | } 70 | p: cmd { 71 | vars pat 72 | "ps -e | grep -i $pat" 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /grammar.pest: -------------------------------------------------------------------------------- 1 | WHITESPACE = _{ "\t" | " " } 2 | COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* ~ NEWLINE} 3 | 4 | file = { SOI ~ NEWLINE* ~ setting* ~ NEWLINE* ~ ((menu|snippet) ~ NEWLINE*)+ ~ EOI } 5 | menu = { "menu" ~ string? ~ symbol ~ NEWLINE* ~ OPENBR ~ menu_body ~ CLOSINGBR } 6 | OPENBR = _{"{"} 7 | CLOSINGBR = _{"}"} 8 | 9 | setting = { shell_def | echo_setting} 10 | 11 | shell_def = {"shell" ~ (string|word)+ } 12 | word = @{ (!("\"" | WHITESPACE | NEWLINE) ~ ANY)+ } 13 | 14 | echo_setting = {"echo" ~ echo_val} 15 | echo_val = {"on" | "off"} 16 | 17 | 18 | menu_body = { (NEWLINE* ~ entry ~ NEWLINE*)+ } 19 | entry = { keydef ~ ":" ~ (anon_command | quick_command | symbol)} 20 | keydef = @{ (!(":" | WHITESPACE | NEWLINE) ~ ANY)* } 21 | symbol = @{ (ASCII_ALPHANUMERIC | "_")+ } 22 | string = { normal_string | protected_string } 23 | 24 | // a normal string in which you can escape a " with a \ 25 | normal_string = ${ QUOTE ~ normal_content ~ QUOTE } 26 | normal_content = @{ ("\\\"" | !QUOTE ~ ANY)* } 27 | QUOTE = _{ "\"" } 28 | 29 | // a raw string, which is delimited by !""! 30 | // where you can insert any sign to make it unique e.g. 31 | // !x""x!. Multiple inserts are not mirrored, so this is valid: 32 | // !ab"content"ab!, but this is not: !ab"content"ba! 33 | protected_string = ${(sep_start ~ protected_content ~ sep_end)} 34 | protected_content = @{ (!("\"" ~ PEEK ~ "!") ~ ANY)* } 35 | 36 | quick_command = {command_name? ~ ECHO_TOGGLE_TOKEN? ~ string_expr} 37 | command_name = { (string ~ "-") } 38 | ECHO_TOGGLE_TOKEN = {"@"} 39 | 40 | sep_start = _{ EXCL ~ PUSH((!"\"" ~ ANY)*) ~ QUOTE} 41 | sep_end = _{ QUOTE ~ POP ~ EXCL } 42 | EXCL = _{ "!" } 43 | 44 | anon_command = { "cmd" ~ NEWLINE* ~ OPENBR 45 | ~ NEWLINE* ~ cmd_body ~ NEWLINE* ~ CLOSINGBR } 46 | cmd_body = { ((cmd_settings|vars_def|shell_def) ~ NEWLINE)* ~ quick_command } 47 | vars_def = { "vars" ~ var_def ~ (DEF_SEP* ~ var_def)* } 48 | DEF_SEP = _{"," ~ NEWLINE*} 49 | var_def = { symbol ~ default_var? } 50 | default_var = { "=" ~ string } 51 | cmd_settings = { "set" ~ symbol ~ (DEF_SEP* ~ symbol)* } 52 | 53 | snippet = { "snippet" ~ NEWLINE* ~ symbol ~ "=" ~ string_expr} 54 | string_expr = { string_expr_elem ~ (NEWLINE* ~ "+" ~ NEWLINE* ~ string_expr_elem)*} 55 | string_expr_elem = { string | snippet_symbol } 56 | snippet_symbol = @{"$" ~ symbol} 57 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::PathBuf, process::exit}; 2 | 3 | use anyhow::{anyhow, Context, Result}; 4 | use clap::Parser; 5 | use console::Term; 6 | use dotree::{ 7 | core::run, 8 | parser::{self, Config, Node, ShellDef}, 9 | rt_conf, 10 | }; 11 | use log::debug; 12 | 13 | fn main() -> Result<()> { 14 | pretty_env_logger::init(); 15 | let args = Args::parse(); 16 | 17 | let (conf_path, local_conf_dir) = if args.local_mode { 18 | if let Some(path) = search_local_config().context("Searching local config")? { 19 | let conf_dir = path.parent().unwrap().to_owned(); 20 | (path, Some(conf_dir)) 21 | } else { 22 | eprintln!("Couldnt find a local config"); 23 | exit(1); 24 | } 25 | } else if let Some(p) = args.conf_file { 26 | (p, None) 27 | } else { 28 | ( 29 | get_default_config_dir() 30 | .ok_or(anyhow!("Couldn't determin config dir"))? 31 | .join("dotree.dt"), 32 | None, 33 | ) 34 | }; 35 | 36 | if !conf_path.exists() { 37 | eprintln!( 38 | "Expected config file at {}, but couldn't find it. Please create one.", 39 | conf_path.display() 40 | ); 41 | exit(1); 42 | } 43 | 44 | let conf_src = fs::read_to_string(conf_path).context("loading config")?; 45 | let Config { 46 | menu, 47 | mut settings, 48 | snippet_table, 49 | } = parser::parse(&conf_src).context("Parsing Config")?; 50 | 51 | let env_shell = get_shell_from_env().context("Getting Shell from Env")?; 52 | settings.shell_def = settings.shell_def.or(env_shell); 53 | debug!("settings:\n{settings:#?}"); 54 | rt_conf::init(local_conf_dir, settings); 55 | 56 | let term = Term::stdout(); 57 | term.hide_cursor()?; 58 | let res = run(&Node::Menu(menu), &args.input, &snippet_table); 59 | if let Err(e) = term.show_cursor() { 60 | eprintln!("Warning, couldn't show cursor again:\n{e:?}"); 61 | } 62 | res 63 | } 64 | 65 | fn get_default_config_dir() -> Option { 66 | if let Ok(path) = env::var("XDG_CONFIG_HOME") { 67 | Some(path.into()) 68 | } else { 69 | dirs::config_dir() 70 | } 71 | } 72 | 73 | fn get_shell_from_env() -> Result> { 74 | Ok(if let Ok(src) = std::env::var("DT_DEFAULT_SHELL") { 75 | Some(parser::parse_shell_string(&format!("shell {src}"))?) 76 | } else { 77 | None 78 | }) 79 | } 80 | 81 | fn search_local_config() -> Result> { 82 | let cwd = std::env::current_dir().context("getting cwd")?; 83 | let mut cur_dir = cwd.as_path(); 84 | loop { 85 | let attempt = cur_dir.join("dotree.dt"); 86 | if attempt.exists() { 87 | return Ok(Some(attempt)); 88 | } 89 | if let Some(parent) = cur_dir.parent() { 90 | cur_dir = parent; 91 | } else { 92 | return Ok(None); 93 | } 94 | } 95 | } 96 | 97 | #[derive(Parser)] 98 | struct Args { 99 | /// Input that will be process character by character, as if it was entered 100 | input: Vec, 101 | 102 | /// path to config file. Defaults to $XDG_CONFIG_HOME/dotree.dt 103 | #[arg(long, short)] 104 | conf_file: Option, 105 | 106 | /// instead of reading the config file, search all directories from current 107 | /// to root for a dotree.dt file, and use this, if it is found. 108 | /// All commands are executed from the files directory 109 | #[arg(long, short)] 110 | local_mode: bool, 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dotree is a small interactive command runner. It wants to be a better home for your 2 | aliases and bash functions, especially those that you don't use that often, and an 3 | alternative to [just](https://github.com/casey/just). 4 | 5 | ![](./demo.gif) 6 | 7 | Given a config file like this: 8 | 9 | ``` 10 | menu root { 11 | g: git 12 | m: misc 13 | } 14 | 15 | menu git { 16 | am: "amend" - "git commit --amend --no-edit" 17 | aam: "all amend" - "git commit -a --amend --no-edit" 18 | ca: "git commit -a" 19 | b: "git switch $(git branch | fzf)" 20 | w: cmd { 21 | vars output_dir, branch 22 | "add worktree" - "git worktree add -b $branch $output_dir" 23 | } 24 | } 25 | 26 | menu misc { 27 | mn: "mount-netdrives" 28 | un: "unmount-netdrives" 29 | cv: "connect-vpn" 30 | } 31 | ``` 32 | 33 | It presents you with the options to execute the commands configured in the file 34 | by typing the configured key. For Example: with the given config file above, I could 35 | start dotree by typing `dt` (after it was installed), and then type `gb` while dotree is 36 | running to execute `git switch $(git branch | fzf)` in bash. 37 | 38 | Alternatively you can also do that by entering `dt gb`. If you provide an argument, its 39 | characters will be processed as if you typed them when the program is running. 40 | 41 | A command can either be declared as quick command, i.e. a string that contains bash code, 42 | optionally with another string and a `-` in front of it, to have a name displayed in place 43 | of the bash code, or as command via the `cmd` keyword, which allows for the additional 44 | definition of variables that will be queried and then passed as env vars to the bash invocation. 45 | To query the input, [rustyline](https://github.com/kkawakam/rustyline) is used, and you have 46 | path completion and a history. 47 | 48 | An alternate form of strings are protected strings: `!""!`, in which case you can use 49 | `"` freely within the string. And in case you even need `!"` in a string, you can add any 50 | characters between the `!` and the `"`. The characters are not mirrored on the closing 51 | delimiter. So `!ab""ab!` is valid, but ~`!ab""ba!`~ is not. 52 | 53 | For an example of a real world config, [click here](./example.dt) 54 | 55 | ### Command Arguments 56 | 57 | Commands can have arguments, which will be queried interactively, like this: 58 | 59 | ``` 60 | ... 61 | menu git { 62 | ... 63 | w: cmd { 64 | vars output_dir, branch 65 | "add worktree" - "git worktree add -b $branch $output_dir" 66 | } 67 | } 68 | 69 | ... 70 | ``` 71 | 72 | The values are exposed via environment variables to the callee. 73 | If you invoke dt with additional arguments, the additional arguments will be used as values 74 | for the vars. For example: `dt gw fknorr/some-feature /tmp/worktree_dir`. 75 | 76 | You can also assign default values for variables like this: 77 | 78 | ``` 79 | menu root { 80 | f: cmd { 81 | vars a, b, c="foo" 82 | "echo $a $b $c" 83 | } 84 | } 85 | ``` 86 | 87 | Vars with default values will be queried, but if you just press enter, 88 | the default value will be used. They will not be used if you pass the values 89 | via arguments. I.e., if you call the above example with `dtl f alpha beta`, it will still ask 90 | for a value for c interactively. 91 | 92 | ### Repeating Commands 93 | 94 | You can configure dotree to continue after a command was executed, so that you can trigger 95 | the command again with a single key press. This is useful for example, if you want to 96 | change screen brightness when you don't have a keyboard with appropriate keys: 97 | 98 | ``` 99 | menu root { 100 | m: brightnessctl 101 | } 102 | 103 | menu brightnessctl { 104 | +: cmd { 105 | set repeat 106 | "brightnessctl set +10%" 107 | } 108 | -: cmd { 109 | set repeat 110 | "brightnessctl set -10%" 111 | } 112 | } 113 | ``` 114 | 115 | You can also add `ignore_result` as a config option, in which case dotree won't escape 116 | when the command has a non-zero exit code, like this: 117 | 118 | ``` 119 | menu brightnessctl { 120 | ... 121 | +: cmd { 122 | set repeat, ignore_result 123 | "brightnessctl set +10%" 124 | } 125 | ... 126 | ``` 127 | 128 | ### Echoing Commands 129 | 130 | By default, dotree will echo the command it executes to stderr (this behavior change was introduced in 0.8.0). 131 | If you don't like that, you can add `echo off` to the top of the file, like this: 132 | 133 | ``` 134 | echo off 135 | 136 | menu root { 137 | n: "echo don\'t show me" 138 | y: "echo 'show me' without echoing the command" - @"echo show me" 139 | } 140 | ``` 141 | 142 | This will switch the default behavior to not echoing the executed command. Before a command, you can 143 | add an `@`, which will toggle the default behavior, i.e. if you have an `echo off` at the top of 144 | your file, then commands with `@` will be echoed. If you do not have `echo off` at the top of the file 145 | `@` will supress echoing. 146 | 147 | 148 | ### Naming Menus 149 | 150 | You can also assign a different display name to a menu, like this: 151 | 152 | ``` 153 | menu "Worktree" git_worktree { 154 | ... 155 | } 156 | ``` 157 | 158 | ### Local mode 159 | 160 | If you start dotree with -l, it will search for a dotree.dt file between the cwd and the file 161 | system root. If it finds one, it uses it instead of the normal config file, and changes the 162 | working directory before executing commands, to the containing directory. This way, you can 163 | use dotree as a more interactive version of [just](https://github.com/casey/just). I aliased 164 | `dt -l` to `dtl` 165 | 166 | ### Default Shell 167 | 168 | By default, dotree uses "bash -euo pipefail -c" as shell invocation on linux, or "cmd /c" on 169 | Windows. The shell string is always appended as last argument. You can change the default shell 170 | by setting the environment variable `DT_DEFAULT_SHELL` or on a per-file basis, by placing 171 | a shell directive as first element in the config file like this: 172 | 173 | ``` 174 | shell sh -c 175 | 176 | menu root { 177 | g: git 178 | m: misc 179 | } 180 | ... 181 | ``` 182 | 183 | It is also possible to change the shell for a command, by putting a shell directive into a 184 | command like this: 185 | 186 | ``` 187 | menu root { 188 | p: cmd { 189 | shell python -c 190 | "python hi" - !"print("hello from python")"! 191 | } 192 | } 193 | ``` 194 | 195 | ### Alternative Config Path 196 | 197 | By default, dotree looks at a file named `dotree.dt` in the XDG config dir, you can make 198 | it look somewhere else with the `-c` command line argument 199 | 200 | ### Snippets 201 | 202 | To share code between multiple commands, you can define snippets: 203 | 204 | ``` 205 | snippet shared_vars = !" 206 | a_var=a_value 207 | b_var=b_value 208 | "! 209 | 210 | menu root { 211 | e: $shared_vars + "echo $a_var" 212 | } 213 | ``` 214 | 215 | A snippet can be referenced by its name prefixed with a `$`. You use them in the definition 216 | of commands or in the definition of snippets. The can occur in any order like this: 217 | 218 | ``` 219 | snippet vars = !" 220 | FOO="foo" 221 | "! 222 | 223 | snippet a_fn = " # we want a newline here 224 | append_foo() { 225 | echo $1 foo 226 | } 227 | " # and here, since strings are simpli concatenated, and bash needs those newlines 228 | # alternatively, we could use a ; in the command 229 | 230 | menu root { 231 | e: $vars + "echo foo=$FOO" + $a_fn + "append_foo $FOO" 232 | } 233 | 234 | ``` 235 | 236 | ## Installation 237 | 238 | Download the appropriate binary for your platform (windows is untested) from the release page, 239 | or install via cargo: `cargo install --git https://github.com/knorrfg/dotree`. 240 | 241 | ### Arch Linux 242 | 243 | - [dotree](https://aur.archlinux.org/packages/dotree) 244 | - [dotree-bin](https://aur.archlinux.org/packages/dotree-bin) 245 | - [dotree-git](https://aur.archlinux.org/packages/dotree-git) 246 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, ensure, Context, Result}; 2 | use console::{pad_str, style, Alignment, Key, Term}; 3 | use log::debug; 4 | use rustyline::completion::FilenameCompleter; 5 | use rustyline::highlight::Highlighter; 6 | use rustyline::{Completer, Helper, Hinter, Validator}; 7 | use std::env; 8 | use std::io::Write; 9 | use std::path::PathBuf; 10 | use std::process::Stdio; 11 | use std::{fs, io}; 12 | 13 | use crate::outproxy::OutProxy; 14 | use crate::parser::{self, CommandSetting, Menu, Node, SnippetTable}; 15 | use crate::rt_conf; 16 | 17 | #[derive(Debug, Clone)] 18 | enum Submenus<'a> { 19 | Exact(&'a Node, usize), 20 | Incomplete(usize), 21 | None, 22 | } 23 | 24 | pub fn run(root_node: &Node, input: &[String], snippet_table: &SnippetTable) -> Result<()> { 25 | let mut input_chars = if let Some(input) = input.first() { 26 | input.chars().collect() 27 | } else { 28 | vec![] 29 | }; 30 | let arg_vals = if input.len() > 1 { &input[1..] } else { &[] }; 31 | 32 | let term = Term::stdout(); 33 | let mut out_proxy = OutProxy::new(); 34 | let (found_node, input_offset) = follow_path(root_node, &input_chars, 0); 35 | let mut input_pos = input_offset; 36 | let mut current_node = if let Some(found_node) = found_node { 37 | found_node 38 | } else { 39 | input_chars.clear(); 40 | root_node 41 | }; 42 | 43 | // we need to create a handler, because, if we don't the program will terminate abnormally 44 | // but if we do, readline will return an io::Error with kind Interrupted, when ctrl+c 45 | // is pressed 46 | ctrlc::set_handler(|| {})?; 47 | 48 | loop { 49 | match current_node { 50 | Node::Command(c) => { 51 | if c.repeat() { 52 | input_chars.pop(); 53 | } else { 54 | term.clear_last_lines(out_proxy.n_lines)?; 55 | term.show_cursor()?; 56 | } 57 | run_command(c, &term, arg_vals, snippet_table)?; 58 | } 59 | Node::Menu(m) => { 60 | term.clear_last_lines(out_proxy.n_lines)?; 61 | out_proxy.n_lines = 0; 62 | render_menu(m, &input_chars[input_pos..], &mut out_proxy)?; 63 | } 64 | } 65 | 66 | // returns true when the user pressed Esc or Ctrl+c, which means we should exit 67 | if get_input(&mut input_chars, &term)? { 68 | term.clear_last_lines(out_proxy.n_lines)?; 69 | Term::stderr().show_cursor()?; 70 | break Ok(()); 71 | }; 72 | 73 | let (found_node, input_offset_) = follow_path(root_node, &input_chars, 0); 74 | input_pos = input_offset_; 75 | current_node = if let Some(found_node) = found_node { 76 | found_node 77 | } else { 78 | input_chars.clear(); 79 | root_node 80 | }; 81 | } 82 | } 83 | 84 | type Exit = bool; 85 | fn get_input(input_chars: &mut Vec, term: &Term) -> Result { 86 | let key = match term.read_key() { 87 | Ok(k) => k, 88 | Err(e) if e.kind() == io::ErrorKind::Interrupted => { 89 | return Ok(true); 90 | } 91 | Err(e) => { 92 | bail!("Error while waiting for key: {e:?}"); 93 | } 94 | }; 95 | 96 | debug!("got char: {key:?}"); 97 | match key { 98 | Key::Char(c) => { 99 | input_chars.push(c); 100 | } 101 | Key::Backspace => { 102 | input_chars.pop(); 103 | } 104 | Key::Escape => { 105 | return Ok(true); 106 | } 107 | _ => {} 108 | } 109 | Ok(false) 110 | } 111 | 112 | fn run_command( 113 | cmd: &parser::Command, 114 | term: &Term, 115 | arg_vals: &[String], 116 | snippet_table: &SnippetTable, 117 | ) -> Result<()> { 118 | let mut history = load_hist().context("loading hist")?; 119 | debug!("Running: {cmd}"); 120 | 121 | ensure!( 122 | arg_vals.len() <= cmd.env_vars.len(), 123 | "Too many arguments for this command" 124 | ); 125 | 126 | if let Some(wd) = rt_conf::local_conf_dir() { 127 | env::set_current_dir(wd).context("Changing working directory")?; 128 | } 129 | 130 | for i in 0..cmd.env_vars.len() { 131 | let var = &cmd.env_vars[i]; 132 | let val = if let Some(val) = arg_vals.get(i) { 133 | val 134 | } else { 135 | history = query_env_var(&var.name, &var.value, history).context("querying env var")?; 136 | history.last().unwrap() 137 | }; 138 | // uppon calling exec, the env vars are kept, so just setting them here 139 | // means setting them for the callee 140 | env::set_var(&var.name, val); 141 | } 142 | term.clear_last_lines(cmd.env_vars.len() - arg_vals.len()) 143 | .context("Clearing input lines")?; 144 | store_hist(history).context("Storing history")?; 145 | 146 | let shell = cmd 147 | .shell 148 | .clone() 149 | .or_else(|| rt_conf::settings().shell_def.clone()) 150 | .unwrap_or_default(); 151 | debug!("shell: {shell:?}"); 152 | let arg = cmd 153 | .exec_str 154 | .resolve(snippet_table) 155 | .context(format!("resolving {}", cmd.exec_str))?; 156 | let args = shell.args_with(arg.as_str()); 157 | if cmd.settings.contains(&CommandSetting::Repeat) { 158 | run_subcommand( 159 | &shell.name, 160 | &args, 161 | cmd.settings.contains(&CommandSetting::IgnoreResult), 162 | ) 163 | } else { 164 | if rt_conf::settings().echo_by_default != cmd.toggle_echo_setting { 165 | eprintln!("{arg}"); 166 | } 167 | exec_cmd(&shell.name, args) 168 | } 169 | } 170 | 171 | #[cfg(not(windows))] 172 | fn exec_cmd<'a>(shell_name: &'a str, mut args: Vec<&'a str>) -> Result<()> { 173 | args.insert(0, shell_name); 174 | Err(anyhow!( 175 | "error executing command: \n{:?}", 176 | exec::execvp(shell_name, &args) 177 | )) 178 | } 179 | 180 | #[cfg(windows)] 181 | fn exec_cmd(shell_name: &str, args: Vec<&str>) -> Result<()> { 182 | // windows doesn't have an exec, let's do this instead 183 | let status = std::process::Command::new(shell_name).args(args).status()?; 184 | if !status.success() { 185 | Err(anyhow!("Process didn't exit successfully: {status:?}")) 186 | } else { 187 | std::process::exit(0); 188 | } 189 | } 190 | 191 | fn run_subcommand(prog: &str, args: &[&str], ignore_result: bool) -> Result<()> { 192 | let status = std::process::Command::new(prog) 193 | .stdout(Stdio::null()) 194 | .stderr(Stdio::null()) 195 | .args(args) 196 | .status()?; 197 | if !ignore_result && !status.success() { 198 | Err(anyhow!("Process didn't exit successfully: {status:?}")) 199 | } else { 200 | Ok(()) 201 | } 202 | } 203 | 204 | fn get_hist_path() -> Result { 205 | let dir = if let Some(sd) = dirs::state_dir() { 206 | sd 207 | } else { 208 | dirs::data_local_dir().ok_or(anyhow!("couldn't get local dir"))? 209 | }; 210 | Ok(dir.join("dthist")) 211 | } 212 | 213 | fn load_hist() -> Result> { 214 | let hist_path = get_hist_path()?; 215 | Ok(if hist_path.exists() { 216 | fs::read_to_string(hist_path) 217 | .context("reading file")? 218 | .lines() 219 | .map(|x| x.to_string()) 220 | .collect() 221 | } else { 222 | vec![] 223 | }) 224 | } 225 | 226 | fn store_hist(hist: Vec) -> Result<()> { 227 | #[cfg(windows)] 228 | let line_ending = "\r\n"; 229 | #[cfg(not(windows))] 230 | let line_ending = "\n"; 231 | let hist_path = get_hist_path()?; 232 | fs::create_dir_all(hist_path.parent().context("Getting history file dir")?) 233 | .context("creating history file dir")?; 234 | fs::write(hist_path, hist.join(line_ending))?; 235 | Ok(()) 236 | } 237 | 238 | #[derive(Helper, Completer, Hinter, Validator)] 239 | struct RlHelper { 240 | #[rustyline(Completer)] 241 | completer: FilenameCompleter, 242 | } 243 | impl Highlighter for RlHelper {} 244 | 245 | fn query_env_var( 246 | name: &str, 247 | default_val: &Option, 248 | mut hist: Vec, 249 | ) -> Result> { 250 | let mut rl = rustyline::Editor::new()?; 251 | rl.set_helper(Some(RlHelper { 252 | completer: FilenameCompleter::new(), 253 | })); 254 | for h in &hist { 255 | rl.add_history_entry(h)?; 256 | } 257 | let line = rl.readline(&format!( 258 | "Value for {name}{default}: ", 259 | default = if let Some(default_val) = default_val { 260 | format!(" ({default_val})") 261 | } else { 262 | String::new() 263 | } 264 | ))?; 265 | 266 | if line.is_empty() { 267 | if let Some(default_val) = default_val { 268 | hist.push(default_val.to_string()); 269 | return Ok(hist); 270 | } 271 | } 272 | hist.push(line); 273 | Ok(hist) 274 | } 275 | 276 | fn render_menu( 277 | current_menu: &Menu, 278 | remaining_path: &[char], 279 | out_proxy: &mut OutProxy, 280 | ) -> Result<()> { 281 | let remaining_path = String::from_iter(remaining_path); 282 | let keysection_len = current_menu 283 | .entries 284 | .keys() 285 | .map(|keys| keys.len()) 286 | .max() 287 | .expect("empty menu") 288 | + 1; 289 | for (keys, node) in ¤t_menu.entries { 290 | let keys = String::from_iter(keys); 291 | let keys = if let Some(rest) = keys.strip_prefix(&remaining_path) { 292 | format!( 293 | "{}{}:", 294 | style(&remaining_path).green().bright().bold(), 295 | rest 296 | ) 297 | } else { 298 | format!("{keys}:") 299 | }; 300 | let keys = pad_str(&keys, keysection_len, Alignment::Left, None); 301 | writeln!(out_proxy, "{keys} {node}")?; 302 | } 303 | Ok(()) 304 | } 305 | 306 | fn follow_path<'a>(node: &'a Node, input_chars: &[char], pos: usize) -> (Option<&'a Node>, usize) { 307 | match node { 308 | Node::Menu(this) => match find_submenus_for(this, input_chars, pos) { 309 | Submenus::Exact(next_node, new_pos) => follow_path(next_node, input_chars, new_pos), 310 | Submenus::Incomplete(new_pos) => (Some(node), new_pos), 311 | Submenus::None => (None, 0), 312 | }, 313 | Node::Command(_) => (Some(node), pos), 314 | } 315 | } 316 | 317 | fn find_submenus_for<'a>(menu: &'a Menu, input_chars: &[char], pos: usize) -> Submenus<'a> { 318 | // The base idea here is to compare the path with valid entries character wise. 319 | // A vec of options of chars is used, so it can be set to none, if it doesn't match any more 320 | // If it matches, the first char is removed. If after the removal of the char, the slice is 321 | // empty, we have an exact match and return it. 322 | // If we don't have any options left after checking the complete path, that means the path was 323 | // invalid, otherwise it's not yet complete 324 | let mut entries: Vec<_> = menu 325 | .entries 326 | .iter() 327 | .map(|(chars, nodes)| (Some(chars.as_slice()), nodes)) 328 | .collect(); 329 | for (i, c) in input_chars[pos..].iter().enumerate() { 330 | for (chars_opt, node) in &mut entries { 331 | if let Some(chars) = chars_opt { 332 | // this could panic, but empty menu entries aren't allowed and won't happen. 333 | // and, since it is immediately checked whether an entry is empty uppon removal, 334 | // we won't produce that state either 335 | if chars[0] == *c { 336 | *chars = &chars[1..]; 337 | if chars.is_empty() { 338 | return Submenus::Exact(node, pos + i + 1); 339 | } 340 | } else { 341 | *chars_opt = None; 342 | } 343 | } 344 | } 345 | } 346 | 347 | if entries.iter().all(|(chars, _)| chars.is_none()) { 348 | Submenus::None 349 | } else { 350 | Submenus::Incomplete(pos) 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use hashbrown::HashMap; 2 | use log::debug; 3 | use std::collections::VecDeque; 4 | 5 | use pest::{ 6 | iterators::{Pair, Pairs}, 7 | Parser, 8 | }; 9 | use pest_derive::Parser; 10 | 11 | use anyhow::{anyhow, ensure, Context, Result}; 12 | 13 | #[derive(Parser)] 14 | #[grammar = "grammar.pest"] 15 | struct ConfigParser; 16 | 17 | #[derive(Debug, Clone)] 18 | pub enum Node { 19 | Menu(Menu), 20 | Command(Command), 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct Menu { 25 | pub name: String, 26 | pub display_name: Option, 27 | pub entries: HashMap, Node>, 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct Command { 32 | pub exec_str: StringExpr, 33 | pub settings: Vec, 34 | pub name: Option, 35 | pub shell: Option, 36 | pub env_vars: Vec, 37 | pub toggle_echo_setting: bool, 38 | } 39 | 40 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 41 | pub enum CommandSetting { 42 | Repeat, 43 | IgnoreResult, 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | pub struct ShellDef { 48 | pub name: String, 49 | pub args: Vec, 50 | } 51 | 52 | #[derive(Debug, Clone)] 53 | pub struct VarDef { 54 | pub name: String, 55 | pub value: Option, 56 | } 57 | 58 | #[derive(Debug, Clone)] 59 | struct RawMenu<'a> { 60 | display_name: Option, 61 | body: Pairs<'a, Rule>, 62 | } 63 | 64 | #[derive(Debug, Clone)] 65 | pub enum StringExprElem { 66 | Symbol(String), 67 | String(String), 68 | } 69 | 70 | #[derive(Debug, Clone)] 71 | pub struct Settings { 72 | pub shell_def: Option, 73 | pub echo_by_default: bool, 74 | } 75 | 76 | #[derive(Debug, Clone)] 77 | pub struct Config { 78 | pub menu: Menu, 79 | pub settings: Settings, 80 | pub snippet_table: SnippetTable, 81 | } 82 | 83 | #[derive(Debug, Clone)] 84 | pub struct StringExpr(Vec); 85 | 86 | pub type SnippetTable = HashMap; 87 | 88 | trait INext: Sized { 89 | fn inext(self) -> Self; 90 | fn nnext(mut self, n: usize) -> Self { 91 | for _ in 0..n { 92 | self = self.inext(); 93 | } 94 | self 95 | } 96 | } 97 | 98 | impl INext for Pair<'_, Rule> { 99 | fn inext(self) -> Self { 100 | self.into_inner().next().unwrap() 101 | } 102 | } 103 | 104 | impl Default for Settings { 105 | fn default() -> Self { 106 | Settings { 107 | shell_def: None, 108 | echo_by_default: true, 109 | } 110 | } 111 | } 112 | 113 | fn from_string(p: Pair<'_, Rule>) -> String { 114 | p.nnext(2).as_str().to_string() 115 | } 116 | 117 | pub fn parse(src: &str) -> Result { 118 | let mut pairs = ConfigParser::parse(Rule::file, src).context("Parsing source")?; 119 | let file = pairs.next().unwrap(); 120 | assert!(file.as_rule() == Rule::file); 121 | 122 | let (settings, entries) = parse_settings(file.into_inner()); 123 | 124 | let menus = get_menu_table(entries.clone()); 125 | let snippet_table = get_snippet_table(entries); 126 | let menu = parse_menu("root", &menus)?; 127 | 128 | Ok(Config { 129 | menu, 130 | settings, 131 | snippet_table, 132 | }) 133 | } 134 | 135 | fn parse_settings(mut entries: Pairs) -> (Settings, Pairs) { 136 | let mut res = Settings::default(); 137 | debug!("Parsing settings: \n{entries:?}"); 138 | while let Some(first_entry) = entries.peek() { 139 | if first_entry.as_rule() != Rule::setting { 140 | break; 141 | } 142 | let first_entry = first_entry.inext(); 143 | match first_entry.as_rule() { 144 | Rule::shell_def => { 145 | res.shell_def = Some(parse_shell_def(first_entry)); 146 | debug!("parsing shell_def result: {:?}", res.shell_def); 147 | } 148 | Rule::echo_setting => { 149 | res.echo_by_default = parse_echo_setting(first_entry); 150 | debug!("parsing echo_setting result: {:?}", res.echo_by_default); 151 | } 152 | _ => { 153 | panic!("unexpected rule:\n{first_entry:#?}"); 154 | } 155 | } 156 | _ = entries.next(); 157 | } 158 | (res, entries) 159 | } 160 | 161 | fn get_snippet_table(entries: Pairs<'_, Rule>) -> HashMap { 162 | let mut res = HashMap::new(); 163 | for e in entries { 164 | if e.as_rule() == Rule::snippet { 165 | let mut e = e.into_inner(); 166 | res.insert( 167 | e.next().unwrap().as_str().to_string(), 168 | parse_string_expr(e.next().unwrap()), 169 | ); 170 | } 171 | } 172 | res 173 | } 174 | 175 | pub fn parse_shell_string(src: &str) -> Result { 176 | let mut pairs = ConfigParser::parse(Rule::shell_def, src).context("Parsing shell def")?; 177 | Ok(parse_shell_def(pairs.next().unwrap())) 178 | } 179 | 180 | fn parse_shell_def(p: Pair<'_, Rule>) -> ShellDef { 181 | let mut elems = VecDeque::new(); 182 | for p in p.into_inner() { 183 | match p.as_rule() { 184 | Rule::word => elems.push_back(p.as_str().to_string()), 185 | 186 | Rule::string => elems.push_back(from_string(p)), 187 | _ => panic!("unexpected rule: {p:?}"), 188 | } 189 | } 190 | ShellDef { 191 | name: elems.pop_front().unwrap(), 192 | args: elems.into_iter().collect(), 193 | } 194 | } 195 | 196 | fn get_menu_table(pairs: Pairs<'_, Rule>) -> HashMap<&str, RawMenu<'_>> { 197 | pairs 198 | .into_iter() 199 | .filter(|x| x.as_rule() == Rule::menu) 200 | .map(|menu| { 201 | let mut menu_elems = menu.into_inner(); 202 | let first_child = menu_elems.next().unwrap(); 203 | let (display_name, menu_name) = if first_child.as_rule() == Rule::string { 204 | (Some(from_string(first_child)), menu_elems.next().unwrap()) 205 | } else { 206 | (None, first_child) 207 | }; 208 | ( 209 | menu_name.as_str(), 210 | RawMenu { 211 | display_name, 212 | body: menu_elems.next().unwrap().into_inner(), 213 | }, 214 | ) 215 | }) 216 | .collect() 217 | } 218 | 219 | fn parse_menu(name: &str, menus: &HashMap<&str, RawMenu<'_>>) -> Result { 220 | let mut entries = HashMap::new(); 221 | let RawMenu { display_name, body } = menus 222 | .get(name) 223 | .ok_or(anyhow!("Undefined symbol: {name}"))? 224 | .clone(); 225 | for entry in body { 226 | let mut children = entry.into_inner(); 227 | let keys = children.next().unwrap().as_str().chars().collect(); 228 | let child_pair = children.next().unwrap(); 229 | let next_node = match child_pair.as_rule() { 230 | Rule::symbol => { 231 | let submenu_name = child_pair.as_str(); 232 | Node::Menu( 233 | parse_menu(submenu_name, menus) 234 | .context(format!("Parsing submenu: {submenu_name}"))?, 235 | ) 236 | } 237 | Rule::quick_command => { 238 | let (display_name, toggle_echo_setting, exec_str) = parse_quick_command(child_pair); 239 | Node::Command(Command { 240 | exec_str, 241 | name: display_name, 242 | settings: vec![], 243 | env_vars: vec![], 244 | shell: None, 245 | toggle_echo_setting, 246 | }) 247 | } 248 | Rule::anon_command => Node::Command(parse_anon_command(child_pair)), 249 | _ => { 250 | panic!("unexpected rule: {child_pair:?}") 251 | } 252 | }; 253 | entries.insert(keys, next_node); 254 | } 255 | Ok(Menu { 256 | name: name.to_string(), 257 | display_name, 258 | entries, 259 | }) 260 | } 261 | 262 | fn parse_anon_command(p: Pair<'_, Rule>) -> Command { 263 | let body = p.inext(); 264 | let mut elems = body.into_inner(); 265 | let mut parser = CmdBodyParser::default(); 266 | loop { 267 | let p = elems.next().unwrap(); 268 | if let Some(cmd) = parser.parse(p) { 269 | break cmd; 270 | } 271 | } 272 | } 273 | 274 | #[derive(Default)] 275 | struct CmdBodyParser { 276 | settings: Option>, 277 | vars: Option>, 278 | shell_def: Option, 279 | } 280 | 281 | impl CmdBodyParser { 282 | fn parse(&mut self, p: Pair<'_, Rule>) -> Option { 283 | match p.as_rule() { 284 | Rule::cmd_settings => { 285 | self.settings = Some(parse_cmd_settings(p)); 286 | None 287 | } 288 | Rule::vars_def => { 289 | self.vars = Some(parse_vars_def(p)); 290 | None 291 | } 292 | Rule::shell_def => { 293 | self.shell_def = Some(parse_shell_def(p)); 294 | None 295 | } 296 | Rule::quick_command => { 297 | let (display_name, toggle_echo_setting, exec_str) = parse_quick_command(p); 298 | Some(Command { 299 | exec_str, 300 | settings: self.settings.take().unwrap_or_default(), 301 | name: display_name, 302 | env_vars: self.vars.take().unwrap_or_default(), 303 | shell: self.shell_def.take(), 304 | toggle_echo_setting, 305 | }) 306 | } 307 | _ => panic!("unexpected rule: {p:#?}"), 308 | } 309 | } 310 | } 311 | 312 | fn parse_cmd_settings(p: Pair<'_, Rule>) -> Vec { 313 | let mut res = vec![]; 314 | for pair in p.into_inner() { 315 | assert!(pair.as_rule() == Rule::symbol); 316 | res.push(match pair.as_str() { 317 | "repeat" => CommandSetting::Repeat, 318 | "ignore_result" => CommandSetting::IgnoreResult, 319 | other => panic!("invalid command setting: {other}"), 320 | }) 321 | } 322 | res 323 | } 324 | 325 | fn parse_vars_def(p: Pair<'_, Rule>) -> Vec { 326 | fn parse_var_def(p: Pair<'_, Rule>) -> VarDef { 327 | assert!(p.as_rule() == Rule::var_def, "unexpected rule: {p:#?}"); 328 | let mut p = p.into_inner(); 329 | let name_def = p.next().unwrap(); 330 | let value_def = p.next(); 331 | 332 | let name = name_def.as_str().to_string(); 333 | let value = value_def.map(|v| { 334 | assert!(v.as_rule() == Rule::default_var, "unexpected rule: {p:#?}"); 335 | from_string(v.inext()) 336 | }); 337 | 338 | VarDef { name, value } 339 | } 340 | 341 | assert!(p.as_rule() == Rule::vars_def); 342 | p.into_inner().map(parse_var_def).collect() 343 | } 344 | 345 | fn parse_quick_command(pair: Pair<'_, Rule>) -> (Option, bool, StringExpr) { 346 | assert!(pair.as_rule() == Rule::quick_command); 347 | let mut name = None; 348 | let mut toggle_echo = false; 349 | let mut str_expr = None; 350 | 351 | for elem in pair.into_inner() { 352 | match elem.as_rule() { 353 | Rule::command_name => name = Some(from_string(elem.inext())), 354 | Rule::ECHO_TOGGLE_TOKEN => toggle_echo = true, 355 | Rule::string_expr => str_expr = Some(parse_string_expr(elem)), 356 | _ => panic!("unexpected pair: {elem:#?}"), 357 | } 358 | } 359 | (name, toggle_echo, str_expr.unwrap()) 360 | } 361 | 362 | fn parse_string_expr(p: Pair<'_, Rule>) -> StringExpr { 363 | let mut res = vec![]; 364 | for e in p.into_inner() { 365 | assert!(e.as_rule() == Rule::string_expr_elem); 366 | let actual_elem = e.inext(); 367 | match actual_elem.as_rule() { 368 | Rule::string => res.push(StringExprElem::String(from_string(actual_elem))), 369 | Rule::snippet_symbol => res.push(StringExprElem::Symbol( 370 | actual_elem.as_str()[1..].to_string(), 371 | )), 372 | _ => panic!("unexpected symbol"), 373 | } 374 | } 375 | StringExpr(res) 376 | } 377 | 378 | fn parse_echo_setting(p: Pair<'_, Rule>) -> bool { 379 | assert!(p.as_rule() == Rule::echo_setting); 380 | p.inext().as_str() == "on" 381 | } 382 | 383 | impl std::fmt::Display for Node { 384 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 385 | match self { 386 | Self::Menu(m) => write!(f, "{}", m.display_name.as_ref().unwrap_or(&m.name)), 387 | Self::Command(c) => write!(f, "{c}"), 388 | } 389 | } 390 | } 391 | 392 | impl std::fmt::Display for StringExpr { 393 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 394 | let elems: Vec<_> = self 395 | .0 396 | .iter() 397 | .map(|x| match x { 398 | StringExprElem::Symbol(s) => s.clone(), 399 | StringExprElem::String(s) => format!("{s:?}"), 400 | }) 401 | .collect(); 402 | write!(f, "{}", elems.join(" + ")) 403 | } 404 | } 405 | 406 | impl std::fmt::Display for Command { 407 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 408 | if let Some(name) = &self.name { 409 | write!(f, "{name}") 410 | } else { 411 | write!(f, "{}", self.exec_str) 412 | } 413 | } 414 | } 415 | 416 | impl Command { 417 | pub fn repeat(&self) -> bool { 418 | self.settings.contains(&CommandSetting::Repeat) 419 | } 420 | } 421 | 422 | impl Default for ShellDef { 423 | fn default() -> Self { 424 | #[cfg(not(windows))] 425 | let res = ShellDef { 426 | name: "bash".into(), 427 | args: vec!["-euo".into(), "pipefail".into(), "-c".into()], 428 | }; 429 | 430 | #[cfg(windows)] 431 | let res = ShellDef { 432 | name: "cmd".into(), 433 | args: vec!["/c".into()], 434 | }; 435 | 436 | res 437 | } 438 | } 439 | 440 | impl ShellDef { 441 | pub fn args_with<'a>(&'a self, additional_arg: &'a str) -> Vec<&'a str> { 442 | self.args 443 | .iter() 444 | .map(String::as_str) 445 | .chain(std::iter::once(additional_arg)) 446 | .collect() 447 | } 448 | } 449 | 450 | impl StringExpr { 451 | pub fn resolve(&self, snippet_table: &SnippetTable) -> Result { 452 | self.inner_resolve(snippet_table, vec![]) 453 | } 454 | 455 | fn inner_resolve(&self, snippet_table: &SnippetTable, parents: Vec) -> Result { 456 | let elems: Vec<_> = self 457 | .0 458 | .iter() 459 | .map(|x| match x { 460 | StringExprElem::Symbol(s) => { 461 | let snip = snippet_table 462 | .get(s) 463 | .ok_or(anyhow!("Undefined snippet: {s}"))?; 464 | let mut parents = parents.clone(); 465 | ensure!( 466 | !parents.contains(s), 467 | "Detected cycle while resolving String Expression: {parents:?}" 468 | ); 469 | parents.push(s.clone()); 470 | snip.inner_resolve(snippet_table, parents) 471 | } 472 | StringExprElem::String(s) => Ok(s.clone()), 473 | }) 474 | .collect::>>()?; 475 | Ok(elems.join("")) 476 | } 477 | } 478 | 479 | #[cfg(test)] 480 | mod tests { 481 | use super::*; 482 | 483 | const CONF: &str = r#" 484 | menu root { 485 | c: custom_commands 486 | f: !xa"echo "!"xa! 487 | } 488 | 489 | menu custom_commands { 490 | h: "print hi" - !"echo hi"! 491 | c: @"echo ciao" 492 | } 493 | "#; 494 | 495 | const ANON_CMD: &str = r#" 496 | menu root { 497 | c: cmd { 498 | "echo foo" 499 | } 500 | } 501 | "#; 502 | 503 | const ANON_CMD2: &str = r#" 504 | menu root { 505 | c: cmd { 506 | vars foo, 507 | bar 508 | "echo $foo $bar" 509 | } 510 | } 511 | "#; 512 | 513 | const NAMED_MENU: &str = r#" 514 | menu root { 515 | m: menu2 516 | } 517 | 518 | menu "2nd menu" menu2 { 519 | f: "echo foo" 520 | } 521 | "#; 522 | 523 | const MISSING_IDENT: &str = r#" 524 | menu root { 525 | s: missing 526 | } 527 | "#; 528 | 529 | const NO_ROOT: &str = r#" 530 | menu no_root { 531 | a: "echo a" 532 | } 533 | "#; 534 | 535 | const WITH_SETTING: &str = r#" 536 | menu root { 537 | a: cmd { 538 | set repeat 539 | "touch foo" 540 | } 541 | } 542 | "#; 543 | 544 | const WITH_SETTING_2: &str = r#" 545 | menu root { 546 | a: cmd { 547 | set repeat, ignore_result 548 | "touch foo" 549 | } 550 | } 551 | "#; 552 | 553 | #[test] 554 | fn test_parsing() -> Result<()> { 555 | let root = parse(CONF)?; 556 | k9::snapshot!( 557 | root, 558 | r#" 559 | Config { 560 | menu: Menu { 561 | name: "root", 562 | display_name: None, 563 | entries: { 564 | [ 565 | 'c', 566 | ]: Menu( 567 | Menu { 568 | name: "custom_commands", 569 | display_name: None, 570 | entries: { 571 | [ 572 | 'c', 573 | ]: Command( 574 | Command { 575 | exec_str: StringExpr( 576 | [ 577 | String( 578 | "echo ciao", 579 | ), 580 | ], 581 | ), 582 | settings: [], 583 | name: None, 584 | shell: None, 585 | env_vars: [], 586 | toggle_echo_setting: true, 587 | }, 588 | ), 589 | [ 590 | 'h', 591 | ]: Command( 592 | Command { 593 | exec_str: StringExpr( 594 | [ 595 | String( 596 | "echo hi", 597 | ), 598 | ], 599 | ), 600 | settings: [], 601 | name: Some( 602 | "print hi", 603 | ), 604 | shell: None, 605 | env_vars: [], 606 | toggle_echo_setting: false, 607 | }, 608 | ), 609 | }, 610 | }, 611 | ), 612 | [ 613 | 'f', 614 | ]: Command( 615 | Command { 616 | exec_str: StringExpr( 617 | [ 618 | String( 619 | "echo "!", 620 | ), 621 | ], 622 | ), 623 | settings: [], 624 | name: None, 625 | shell: None, 626 | env_vars: [], 627 | toggle_echo_setting: false, 628 | }, 629 | ), 630 | }, 631 | }, 632 | settings: Settings { 633 | shell_def: None, 634 | echo_by_default: true, 635 | }, 636 | snippet_table: {}, 637 | } 638 | "# 639 | ); 640 | Ok(()) 641 | } 642 | 643 | #[test] 644 | fn test_missing_ident() -> Result<()> { 645 | let root = parse(MISSING_IDENT); 646 | k9::snapshot!( 647 | root, 648 | r#" 649 | Err( 650 | Error { 651 | context: "Parsing submenu: missing", 652 | source: "Undefined symbol: missing", 653 | }, 654 | ) 655 | "# 656 | ); 657 | Ok(()) 658 | } 659 | 660 | #[test] 661 | fn test_no_root() -> Result<()> { 662 | let root = parse(NO_ROOT); 663 | k9::snapshot!( 664 | root, 665 | r#" 666 | Err( 667 | "Undefined symbol: root", 668 | ) 669 | "# 670 | ); 671 | Ok(()) 672 | } 673 | 674 | #[test] 675 | fn anon_cmd() -> Result<()> { 676 | let root = parse(ANON_CMD); 677 | k9::snapshot!( 678 | root, 679 | r#" 680 | Ok( 681 | Config { 682 | menu: Menu { 683 | name: "root", 684 | display_name: None, 685 | entries: { 686 | [ 687 | 'c', 688 | ]: Command( 689 | Command { 690 | exec_str: StringExpr( 691 | [ 692 | String( 693 | "echo foo", 694 | ), 695 | ], 696 | ), 697 | settings: [], 698 | name: None, 699 | shell: None, 700 | env_vars: [], 701 | toggle_echo_setting: false, 702 | }, 703 | ), 704 | }, 705 | }, 706 | settings: Settings { 707 | shell_def: None, 708 | echo_by_default: true, 709 | }, 710 | snippet_table: {}, 711 | }, 712 | ) 713 | "# 714 | ); 715 | Ok(()) 716 | } 717 | 718 | #[test] 719 | fn anon_cmd_2_args() -> Result<()> { 720 | let root = parse(ANON_CMD2)?; 721 | k9::snapshot!( 722 | root, 723 | r#" 724 | Config { 725 | menu: Menu { 726 | name: "root", 727 | display_name: None, 728 | entries: { 729 | [ 730 | 'c', 731 | ]: Command( 732 | Command { 733 | exec_str: StringExpr( 734 | [ 735 | String( 736 | "echo $foo $bar", 737 | ), 738 | ], 739 | ), 740 | settings: [], 741 | name: None, 742 | shell: None, 743 | env_vars: [ 744 | VarDef { 745 | name: "foo", 746 | value: None, 747 | }, 748 | VarDef { 749 | name: "bar", 750 | value: None, 751 | }, 752 | ], 753 | toggle_echo_setting: false, 754 | }, 755 | ), 756 | }, 757 | }, 758 | settings: Settings { 759 | shell_def: None, 760 | echo_by_default: true, 761 | }, 762 | snippet_table: {}, 763 | } 764 | "# 765 | ); 766 | Ok(()) 767 | } 768 | 769 | #[test] 770 | fn named_menu() -> Result<()> { 771 | let root = parse(NAMED_MENU)?; 772 | k9::snapshot!( 773 | root, 774 | r#" 775 | Config { 776 | menu: Menu { 777 | name: "root", 778 | display_name: None, 779 | entries: { 780 | [ 781 | 'm', 782 | ]: Menu( 783 | Menu { 784 | name: "menu2", 785 | display_name: Some( 786 | "2nd menu", 787 | ), 788 | entries: { 789 | [ 790 | 'f', 791 | ]: Command( 792 | Command { 793 | exec_str: StringExpr( 794 | [ 795 | String( 796 | "echo foo", 797 | ), 798 | ], 799 | ), 800 | settings: [], 801 | name: None, 802 | shell: None, 803 | env_vars: [], 804 | toggle_echo_setting: false, 805 | }, 806 | ), 807 | }, 808 | }, 809 | ), 810 | }, 811 | }, 812 | settings: Settings { 813 | shell_def: None, 814 | echo_by_default: true, 815 | }, 816 | snippet_table: {}, 817 | } 818 | "# 819 | ); 820 | Ok(()) 821 | } 822 | 823 | #[test] 824 | fn with_setting() -> Result<()> { 825 | let root = parse(WITH_SETTING)?; 826 | k9::snapshot!( 827 | root, 828 | r#" 829 | Config { 830 | menu: Menu { 831 | name: "root", 832 | display_name: None, 833 | entries: { 834 | [ 835 | 'a', 836 | ]: Command( 837 | Command { 838 | exec_str: StringExpr( 839 | [ 840 | String( 841 | "touch foo", 842 | ), 843 | ], 844 | ), 845 | settings: [ 846 | Repeat, 847 | ], 848 | name: None, 849 | shell: None, 850 | env_vars: [], 851 | toggle_echo_setting: false, 852 | }, 853 | ), 854 | }, 855 | }, 856 | settings: Settings { 857 | shell_def: None, 858 | echo_by_default: true, 859 | }, 860 | snippet_table: {}, 861 | } 862 | "# 863 | ); 864 | Ok(()) 865 | } 866 | 867 | #[test] 868 | fn with_setting2() -> Result<()> { 869 | let root = parse(WITH_SETTING_2)?; 870 | k9::snapshot!( 871 | root, 872 | r#" 873 | Config { 874 | menu: Menu { 875 | name: "root", 876 | display_name: None, 877 | entries: { 878 | [ 879 | 'a', 880 | ]: Command( 881 | Command { 882 | exec_str: StringExpr( 883 | [ 884 | String( 885 | "touch foo", 886 | ), 887 | ], 888 | ), 889 | settings: [ 890 | Repeat, 891 | IgnoreResult, 892 | ], 893 | name: None, 894 | shell: None, 895 | env_vars: [], 896 | toggle_echo_setting: false, 897 | }, 898 | ), 899 | }, 900 | }, 901 | settings: Settings { 902 | shell_def: None, 903 | echo_by_default: true, 904 | }, 905 | snippet_table: {}, 906 | } 907 | "# 908 | ); 909 | Ok(()) 910 | } 911 | 912 | #[test] 913 | fn test_shell_parsing() { 914 | k9::snapshot!( 915 | parse_shell_string("shell bash -euo pipefail -c"), 916 | r#" 917 | Ok( 918 | ShellDef { 919 | name: "bash", 920 | args: [ 921 | "-euo", 922 | "pipefail", 923 | "-c", 924 | ], 925 | }, 926 | ) 927 | "# 928 | ); 929 | } 930 | 931 | #[test] 932 | fn test_snippet_parsing() -> Result<()> { 933 | k9::snapshot!( 934 | parse_string_expr( 935 | ConfigParser::parse(Rule::string_expr, r#"$a + "b" + $c + "d""#,)? 936 | .next() 937 | .unwrap() 938 | ), 939 | r#" 940 | StringExpr( 941 | [ 942 | Symbol( 943 | "a", 944 | ), 945 | String( 946 | "b", 947 | ), 948 | Symbol( 949 | "c", 950 | ), 951 | String( 952 | "d", 953 | ), 954 | ], 955 | ) 956 | "# 957 | ); 958 | Ok(()) 959 | } 960 | 961 | #[test] 962 | fn test_echo_rule() -> Result<()> { 963 | k9::snapshot!( 964 | parse_echo_setting( 965 | ConfigParser::parse(Rule::echo_setting, r#"echo on"#)? 966 | .next() 967 | .unwrap() 968 | ), 969 | "true" 970 | ); 971 | k9::snapshot!( 972 | parse_echo_setting( 973 | ConfigParser::parse(Rule::echo_setting, r#"echo off"#)? 974 | .next() 975 | .unwrap() 976 | ), 977 | "false" 978 | ); 979 | k9::snapshot!( 980 | ConfigParser::parse(Rule::echo_setting, r#"echo foo"#).is_err(), 981 | "true" 982 | ); 983 | Ok(()) 984 | } 985 | } 986 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "allocator-api2" 28 | version = "0.2.16" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 31 | 32 | [[package]] 33 | name = "anstream" 34 | version = "0.6.4" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 37 | dependencies = [ 38 | "anstyle", 39 | "anstyle-parse", 40 | "anstyle-query", 41 | "anstyle-wincon", 42 | "colorchoice", 43 | "utf8parse", 44 | ] 45 | 46 | [[package]] 47 | name = "anstyle" 48 | version = "1.0.4" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 51 | 52 | [[package]] 53 | name = "anstyle-parse" 54 | version = "0.2.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 57 | dependencies = [ 58 | "utf8parse", 59 | ] 60 | 61 | [[package]] 62 | name = "anstyle-query" 63 | version = "1.0.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 66 | dependencies = [ 67 | "windows-sys 0.48.0", 68 | ] 69 | 70 | [[package]] 71 | name = "anstyle-wincon" 72 | version = "3.0.1" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 75 | dependencies = [ 76 | "anstyle", 77 | "windows-sys 0.48.0", 78 | ] 79 | 80 | [[package]] 81 | name = "anyhow" 82 | version = "1.0.75" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 85 | 86 | [[package]] 87 | name = "bitflags" 88 | version = "1.3.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 91 | 92 | [[package]] 93 | name = "bitflags" 94 | version = "2.4.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 97 | 98 | [[package]] 99 | name = "block-buffer" 100 | version = "0.10.4" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 103 | dependencies = [ 104 | "generic-array", 105 | ] 106 | 107 | [[package]] 108 | name = "cc" 109 | version = "1.0.83" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 112 | dependencies = [ 113 | "libc", 114 | ] 115 | 116 | [[package]] 117 | name = "cfg-if" 118 | version = "1.0.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 121 | 122 | [[package]] 123 | name = "clap" 124 | version = "4.4.6" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" 127 | dependencies = [ 128 | "clap_builder", 129 | "clap_derive", 130 | ] 131 | 132 | [[package]] 133 | name = "clap_builder" 134 | version = "4.4.6" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" 137 | dependencies = [ 138 | "anstream", 139 | "anstyle", 140 | "clap_lex", 141 | "strsim", 142 | ] 143 | 144 | [[package]] 145 | name = "clap_derive" 146 | version = "4.4.2" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 149 | dependencies = [ 150 | "heck", 151 | "proc-macro2", 152 | "quote", 153 | "syn 2.0.38", 154 | ] 155 | 156 | [[package]] 157 | name = "clap_lex" 158 | version = "0.5.1" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 161 | 162 | [[package]] 163 | name = "clipboard-win" 164 | version = "4.5.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" 167 | dependencies = [ 168 | "error-code", 169 | "str-buf", 170 | "winapi", 171 | ] 172 | 173 | [[package]] 174 | name = "colorchoice" 175 | version = "1.0.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 178 | 179 | [[package]] 180 | name = "colored" 181 | version = "1.9.4" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" 184 | dependencies = [ 185 | "is-terminal", 186 | "lazy_static", 187 | "winapi", 188 | ] 189 | 190 | [[package]] 191 | name = "console" 192 | version = "0.15.7" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" 195 | dependencies = [ 196 | "encode_unicode", 197 | "lazy_static", 198 | "libc", 199 | "unicode-width", 200 | "windows-sys 0.45.0", 201 | ] 202 | 203 | [[package]] 204 | name = "cpufeatures" 205 | version = "0.2.10" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" 208 | dependencies = [ 209 | "libc", 210 | ] 211 | 212 | [[package]] 213 | name = "crypto-common" 214 | version = "0.1.6" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 217 | dependencies = [ 218 | "generic-array", 219 | "typenum", 220 | ] 221 | 222 | [[package]] 223 | name = "ctrlc" 224 | version = "3.4.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" 227 | dependencies = [ 228 | "nix 0.27.1", 229 | "windows-sys 0.48.0", 230 | ] 231 | 232 | [[package]] 233 | name = "diff" 234 | version = "0.1.13" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 237 | 238 | [[package]] 239 | name = "digest" 240 | version = "0.10.7" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 243 | dependencies = [ 244 | "block-buffer", 245 | "crypto-common", 246 | ] 247 | 248 | [[package]] 249 | name = "dirs" 250 | version = "5.0.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 253 | dependencies = [ 254 | "dirs-sys", 255 | ] 256 | 257 | [[package]] 258 | name = "dirs-sys" 259 | version = "0.4.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 262 | dependencies = [ 263 | "libc", 264 | "option-ext", 265 | "redox_users", 266 | "windows-sys 0.48.0", 267 | ] 268 | 269 | [[package]] 270 | name = "dotree" 271 | version = "0.8.2" 272 | dependencies = [ 273 | "anyhow", 274 | "clap", 275 | "console", 276 | "ctrlc", 277 | "dirs", 278 | "exec", 279 | "hashbrown", 280 | "k9", 281 | "log", 282 | "once_cell", 283 | "pest", 284 | "pest_derive", 285 | "pretty_env_logger", 286 | "rustyline", 287 | "subprocess", 288 | ] 289 | 290 | [[package]] 291 | name = "encode_unicode" 292 | version = "0.3.6" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 295 | 296 | [[package]] 297 | name = "endian-type" 298 | version = "0.1.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 301 | 302 | [[package]] 303 | name = "env_logger" 304 | version = "0.10.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 307 | dependencies = [ 308 | "humantime", 309 | "is-terminal", 310 | "log", 311 | "regex", 312 | "termcolor", 313 | ] 314 | 315 | [[package]] 316 | name = "errno" 317 | version = "0.2.8" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 320 | dependencies = [ 321 | "errno-dragonfly", 322 | "libc", 323 | "winapi", 324 | ] 325 | 326 | [[package]] 327 | name = "errno" 328 | version = "0.3.5" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" 331 | dependencies = [ 332 | "libc", 333 | "windows-sys 0.48.0", 334 | ] 335 | 336 | [[package]] 337 | name = "errno-dragonfly" 338 | version = "0.1.2" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 341 | dependencies = [ 342 | "cc", 343 | "libc", 344 | ] 345 | 346 | [[package]] 347 | name = "error-code" 348 | version = "2.3.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" 351 | dependencies = [ 352 | "libc", 353 | "str-buf", 354 | ] 355 | 356 | [[package]] 357 | name = "exec" 358 | version = "0.3.1" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615" 361 | dependencies = [ 362 | "errno 0.2.8", 363 | "libc", 364 | ] 365 | 366 | [[package]] 367 | name = "fd-lock" 368 | version = "3.0.13" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" 371 | dependencies = [ 372 | "cfg-if", 373 | "rustix", 374 | "windows-sys 0.48.0", 375 | ] 376 | 377 | [[package]] 378 | name = "generic-array" 379 | version = "0.14.7" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 382 | dependencies = [ 383 | "typenum", 384 | "version_check", 385 | ] 386 | 387 | [[package]] 388 | name = "getrandom" 389 | version = "0.2.10" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 392 | dependencies = [ 393 | "cfg-if", 394 | "libc", 395 | "wasi", 396 | ] 397 | 398 | [[package]] 399 | name = "hashbrown" 400 | version = "0.14.2" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" 403 | dependencies = [ 404 | "ahash", 405 | "allocator-api2", 406 | ] 407 | 408 | [[package]] 409 | name = "heck" 410 | version = "0.4.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 413 | 414 | [[package]] 415 | name = "hermit-abi" 416 | version = "0.3.3" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 419 | 420 | [[package]] 421 | name = "home" 422 | version = "0.5.5" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 425 | dependencies = [ 426 | "windows-sys 0.48.0", 427 | ] 428 | 429 | [[package]] 430 | name = "humantime" 431 | version = "2.1.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 434 | 435 | [[package]] 436 | name = "is-terminal" 437 | version = "0.4.9" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 440 | dependencies = [ 441 | "hermit-abi", 442 | "rustix", 443 | "windows-sys 0.48.0", 444 | ] 445 | 446 | [[package]] 447 | name = "k9" 448 | version = "0.11.6" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "32ddb58b0079a063218472916af599f2753ccb40942cdaba9d1f3fefccef17a9" 451 | dependencies = [ 452 | "anyhow", 453 | "colored", 454 | "diff", 455 | "lazy_static", 456 | "libc", 457 | "proc-macro2", 458 | "regex", 459 | "syn 1.0.109", 460 | "term_size", 461 | ] 462 | 463 | [[package]] 464 | name = "lazy_static" 465 | version = "1.4.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 468 | 469 | [[package]] 470 | name = "libc" 471 | version = "0.2.149" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" 474 | 475 | [[package]] 476 | name = "linux-raw-sys" 477 | version = "0.4.10" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" 480 | 481 | [[package]] 482 | name = "log" 483 | version = "0.4.20" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 486 | 487 | [[package]] 488 | name = "memchr" 489 | version = "2.6.4" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 492 | 493 | [[package]] 494 | name = "nibble_vec" 495 | version = "0.1.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 498 | dependencies = [ 499 | "smallvec", 500 | ] 501 | 502 | [[package]] 503 | name = "nix" 504 | version = "0.26.4" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 507 | dependencies = [ 508 | "bitflags 1.3.2", 509 | "cfg-if", 510 | "libc", 511 | ] 512 | 513 | [[package]] 514 | name = "nix" 515 | version = "0.27.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 518 | dependencies = [ 519 | "bitflags 2.4.1", 520 | "cfg-if", 521 | "libc", 522 | ] 523 | 524 | [[package]] 525 | name = "once_cell" 526 | version = "1.18.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 529 | 530 | [[package]] 531 | name = "option-ext" 532 | version = "0.2.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 535 | 536 | [[package]] 537 | name = "pest" 538 | version = "2.7.4" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" 541 | dependencies = [ 542 | "memchr", 543 | "thiserror", 544 | "ucd-trie", 545 | ] 546 | 547 | [[package]] 548 | name = "pest_derive" 549 | version = "2.7.4" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" 552 | dependencies = [ 553 | "pest", 554 | "pest_generator", 555 | ] 556 | 557 | [[package]] 558 | name = "pest_generator" 559 | version = "2.7.4" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" 562 | dependencies = [ 563 | "pest", 564 | "pest_meta", 565 | "proc-macro2", 566 | "quote", 567 | "syn 2.0.38", 568 | ] 569 | 570 | [[package]] 571 | name = "pest_meta" 572 | version = "2.7.4" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" 575 | dependencies = [ 576 | "once_cell", 577 | "pest", 578 | "sha2", 579 | ] 580 | 581 | [[package]] 582 | name = "pretty_env_logger" 583 | version = "0.5.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" 586 | dependencies = [ 587 | "env_logger", 588 | "log", 589 | ] 590 | 591 | [[package]] 592 | name = "proc-macro2" 593 | version = "1.0.69" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 596 | dependencies = [ 597 | "unicode-ident", 598 | ] 599 | 600 | [[package]] 601 | name = "quote" 602 | version = "1.0.33" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 605 | dependencies = [ 606 | "proc-macro2", 607 | ] 608 | 609 | [[package]] 610 | name = "radix_trie" 611 | version = "0.2.1" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 614 | dependencies = [ 615 | "endian-type", 616 | "nibble_vec", 617 | ] 618 | 619 | [[package]] 620 | name = "redox_syscall" 621 | version = "0.2.16" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 624 | dependencies = [ 625 | "bitflags 1.3.2", 626 | ] 627 | 628 | [[package]] 629 | name = "redox_users" 630 | version = "0.4.3" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 633 | dependencies = [ 634 | "getrandom", 635 | "redox_syscall", 636 | "thiserror", 637 | ] 638 | 639 | [[package]] 640 | name = "regex" 641 | version = "1.10.2" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 644 | dependencies = [ 645 | "aho-corasick", 646 | "memchr", 647 | "regex-automata", 648 | "regex-syntax", 649 | ] 650 | 651 | [[package]] 652 | name = "regex-automata" 653 | version = "0.4.3" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 656 | dependencies = [ 657 | "aho-corasick", 658 | "memchr", 659 | "regex-syntax", 660 | ] 661 | 662 | [[package]] 663 | name = "regex-syntax" 664 | version = "0.8.2" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 667 | 668 | [[package]] 669 | name = "rustix" 670 | version = "0.38.21" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" 673 | dependencies = [ 674 | "bitflags 2.4.1", 675 | "errno 0.3.5", 676 | "libc", 677 | "linux-raw-sys", 678 | "windows-sys 0.48.0", 679 | ] 680 | 681 | [[package]] 682 | name = "rustyline" 683 | version = "12.0.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" 686 | dependencies = [ 687 | "bitflags 2.4.1", 688 | "cfg-if", 689 | "clipboard-win", 690 | "fd-lock", 691 | "home", 692 | "libc", 693 | "log", 694 | "memchr", 695 | "nix 0.26.4", 696 | "radix_trie", 697 | "rustyline-derive", 698 | "scopeguard", 699 | "unicode-segmentation", 700 | "unicode-width", 701 | "utf8parse", 702 | "winapi", 703 | ] 704 | 705 | [[package]] 706 | name = "rustyline-derive" 707 | version = "0.9.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "5a32af5427251d2e4be14fc151eabe18abb4a7aad5efee7044da9f096c906a43" 710 | dependencies = [ 711 | "proc-macro2", 712 | "quote", 713 | "syn 2.0.38", 714 | ] 715 | 716 | [[package]] 717 | name = "scopeguard" 718 | version = "1.2.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 721 | 722 | [[package]] 723 | name = "sha2" 724 | version = "0.10.8" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 727 | dependencies = [ 728 | "cfg-if", 729 | "cpufeatures", 730 | "digest", 731 | ] 732 | 733 | [[package]] 734 | name = "smallvec" 735 | version = "1.11.1" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 738 | 739 | [[package]] 740 | name = "str-buf" 741 | version = "1.0.6" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" 744 | 745 | [[package]] 746 | name = "strsim" 747 | version = "0.10.0" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 750 | 751 | [[package]] 752 | name = "subprocess" 753 | version = "0.2.9" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" 756 | dependencies = [ 757 | "libc", 758 | "winapi", 759 | ] 760 | 761 | [[package]] 762 | name = "syn" 763 | version = "1.0.109" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 766 | dependencies = [ 767 | "proc-macro2", 768 | "quote", 769 | "unicode-ident", 770 | ] 771 | 772 | [[package]] 773 | name = "syn" 774 | version = "2.0.38" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" 777 | dependencies = [ 778 | "proc-macro2", 779 | "quote", 780 | "unicode-ident", 781 | ] 782 | 783 | [[package]] 784 | name = "term_size" 785 | version = "0.3.2" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" 788 | dependencies = [ 789 | "libc", 790 | "winapi", 791 | ] 792 | 793 | [[package]] 794 | name = "termcolor" 795 | version = "1.3.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" 798 | dependencies = [ 799 | "winapi-util", 800 | ] 801 | 802 | [[package]] 803 | name = "thiserror" 804 | version = "1.0.50" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 807 | dependencies = [ 808 | "thiserror-impl", 809 | ] 810 | 811 | [[package]] 812 | name = "thiserror-impl" 813 | version = "1.0.50" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 816 | dependencies = [ 817 | "proc-macro2", 818 | "quote", 819 | "syn 2.0.38", 820 | ] 821 | 822 | [[package]] 823 | name = "typenum" 824 | version = "1.17.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 827 | 828 | [[package]] 829 | name = "ucd-trie" 830 | version = "0.1.6" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 833 | 834 | [[package]] 835 | name = "unicode-ident" 836 | version = "1.0.12" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 839 | 840 | [[package]] 841 | name = "unicode-segmentation" 842 | version = "1.10.1" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 845 | 846 | [[package]] 847 | name = "unicode-width" 848 | version = "0.1.11" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 851 | 852 | [[package]] 853 | name = "utf8parse" 854 | version = "0.2.1" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 857 | 858 | [[package]] 859 | name = "version_check" 860 | version = "0.9.4" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 863 | 864 | [[package]] 865 | name = "wasi" 866 | version = "0.11.0+wasi-snapshot-preview1" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 869 | 870 | [[package]] 871 | name = "winapi" 872 | version = "0.3.9" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 875 | dependencies = [ 876 | "winapi-i686-pc-windows-gnu", 877 | "winapi-x86_64-pc-windows-gnu", 878 | ] 879 | 880 | [[package]] 881 | name = "winapi-i686-pc-windows-gnu" 882 | version = "0.4.0" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 885 | 886 | [[package]] 887 | name = "winapi-util" 888 | version = "0.1.6" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 891 | dependencies = [ 892 | "winapi", 893 | ] 894 | 895 | [[package]] 896 | name = "winapi-x86_64-pc-windows-gnu" 897 | version = "0.4.0" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 900 | 901 | [[package]] 902 | name = "windows-sys" 903 | version = "0.45.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 906 | dependencies = [ 907 | "windows-targets 0.42.2", 908 | ] 909 | 910 | [[package]] 911 | name = "windows-sys" 912 | version = "0.48.0" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 915 | dependencies = [ 916 | "windows-targets 0.48.5", 917 | ] 918 | 919 | [[package]] 920 | name = "windows-targets" 921 | version = "0.42.2" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 924 | dependencies = [ 925 | "windows_aarch64_gnullvm 0.42.2", 926 | "windows_aarch64_msvc 0.42.2", 927 | "windows_i686_gnu 0.42.2", 928 | "windows_i686_msvc 0.42.2", 929 | "windows_x86_64_gnu 0.42.2", 930 | "windows_x86_64_gnullvm 0.42.2", 931 | "windows_x86_64_msvc 0.42.2", 932 | ] 933 | 934 | [[package]] 935 | name = "windows-targets" 936 | version = "0.48.5" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 939 | dependencies = [ 940 | "windows_aarch64_gnullvm 0.48.5", 941 | "windows_aarch64_msvc 0.48.5", 942 | "windows_i686_gnu 0.48.5", 943 | "windows_i686_msvc 0.48.5", 944 | "windows_x86_64_gnu 0.48.5", 945 | "windows_x86_64_gnullvm 0.48.5", 946 | "windows_x86_64_msvc 0.48.5", 947 | ] 948 | 949 | [[package]] 950 | name = "windows_aarch64_gnullvm" 951 | version = "0.42.2" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 954 | 955 | [[package]] 956 | name = "windows_aarch64_gnullvm" 957 | version = "0.48.5" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 960 | 961 | [[package]] 962 | name = "windows_aarch64_msvc" 963 | version = "0.42.2" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 966 | 967 | [[package]] 968 | name = "windows_aarch64_msvc" 969 | version = "0.48.5" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 972 | 973 | [[package]] 974 | name = "windows_i686_gnu" 975 | version = "0.42.2" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 978 | 979 | [[package]] 980 | name = "windows_i686_gnu" 981 | version = "0.48.5" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 984 | 985 | [[package]] 986 | name = "windows_i686_msvc" 987 | version = "0.42.2" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 990 | 991 | [[package]] 992 | name = "windows_i686_msvc" 993 | version = "0.48.5" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 996 | 997 | [[package]] 998 | name = "windows_x86_64_gnu" 999 | version = "0.42.2" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1002 | 1003 | [[package]] 1004 | name = "windows_x86_64_gnu" 1005 | version = "0.48.5" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1008 | 1009 | [[package]] 1010 | name = "windows_x86_64_gnullvm" 1011 | version = "0.42.2" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1014 | 1015 | [[package]] 1016 | name = "windows_x86_64_gnullvm" 1017 | version = "0.48.5" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1020 | 1021 | [[package]] 1022 | name = "windows_x86_64_msvc" 1023 | version = "0.42.2" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1026 | 1027 | [[package]] 1028 | name = "windows_x86_64_msvc" 1029 | version = "0.48.5" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1032 | 1033 | [[package]] 1034 | name = "zerocopy" 1035 | version = "0.7.25" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" 1038 | dependencies = [ 1039 | "zerocopy-derive", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "zerocopy-derive" 1044 | version = "0.7.25" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" 1047 | dependencies = [ 1048 | "proc-macro2", 1049 | "quote", 1050 | "syn 2.0.38", 1051 | ] 1052 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------