Improve code quality a little bit (#352)

Fixes #349
This commit is contained in:
Denis Isidoro 2020-04-25 15:30:05 -03:00 committed by GitHub
parent 818ebccdf0
commit 434fc4ac76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 261 additions and 237 deletions

2
Cargo.lock generated
View file

@ -174,7 +174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "navi"
version = "2.6.0"
version = "2.6.1"
dependencies = [
"anyhow 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -1,6 +1,6 @@
[package]
name = "navi"
version = "2.6.0"
version = "2.6.1"
authors = ["Denis Isidoro <denis_isidoro@live.com>"]
edition = "2018"
description = "An interactive cheatsheet tool for the command-line"

View file

@ -2,8 +2,8 @@
source "${HOME}/.bashrc"
if [ -n "${snippet:-}" ]; then
navi alfred suggestions
if [ -n "${snippet:-}" ]; then
navi alfred suggestions
else
navi alfred start
navi alfred start
fi

View file

@ -1,15 +1,9 @@
#!/bin/bash
_interpolate() {
local -r snippet="$1"
local -r varname="$2"
local -r value="${!varname}"
source "${HOME}/.bashrc"
echo "$snippet" | sed "s/<${varname}>/${value}/g"
}
if [ -n "${varname:-}" ]; then
echo -n "$(_interpolate "$snippet" "$varname" || echo "")"
if [ -n "${varname:-}" ]; then
echo -n "$(navi alfred transform)"
else
echo -n "$snippet"
echo -n "$snippet"
fi

View file

@ -165,6 +165,19 @@
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>triggerid</key>
<string>play</string>
</dict>
<key>type</key>
<string>alfred.workflow.trigger.external</string>
<key>uid</key>
<string>55C46852-4807-4374-95AB-CC055F4ECB7C</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
@ -311,19 +324,6 @@ fi
<key>version</key>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>triggerid</key>
<string>play</string>
</dict>
<key>type</key>
<string>alfred.workflow.trigger.external</string>
<key>uid</key>
<string>55C46852-4807-4374-95AB-CC055F4ECB7C</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>

View file

@ -17,6 +17,8 @@ cargo fmt || true
header "dot code beautify..."
find scripts -type f | xargs -I% dot code beautify % || true
dot code beautify "${NAVI_HOME}/alfred/alfred.bash" || true
dot code beautify "${NAVI_HOME}/alfred/alfred2.bash" || true
dot code beautify "${NAVI_HOME}/tests/core.bash" || true
dot code beautify "${NAVI_HOME}/tests/run" || true

View file

@ -1,117 +0,0 @@
use crate::structures::item::Item;
use crate::terminal;
use regex::Regex;
use std::cmp::max;
use termion::color;
const COMMENT_COLOR: color::LightCyan = color::LightCyan;
const TAG_COLOR: color::Blue = color::Blue;
const SNIPPET_COLOR: color::White = color::White;
const NEWLINE_ESCAPE_CHAR: char = '\x15';
pub const LINE_SEPARATOR: &str = " \x15 ";
pub const DELIMITER: &str = r" ";
lazy_static! {
pub static ref NEWLINE_REGEX: Regex = Regex::new(r"\\\s+").expect("Invalid regex");
}
pub fn get_widths() -> (usize, usize) {
let width = terminal::width();
let tag_width = max(4, width * 20 / 100);
let comment_width = max(4, width * 40 / 100);
(usize::from(tag_width), usize::from(comment_width))
}
pub fn variable_prompt(varname: &str) -> String {
format!("{}: ", varname)
}
fn fix_newlines(txt: &str) -> String {
if txt.contains(NEWLINE_ESCAPE_CHAR) {
(*NEWLINE_REGEX)
.replace_all(txt.replace(LINE_SEPARATOR, " ").as_str(), "")
.to_string()
} else {
txt.to_string()
}
}
pub fn preview(comment: &str, tags: &str, snippet: &str) {
println!(
"{comment_color}{comment} {tag_color}{tags} \n{snippet_color}{snippet}",
comment = format!("# {}", comment),
tags = format!("[{}]", tags),
snippet = fix_newlines(snippet),
comment_color = color::Fg(COMMENT_COLOR),
tag_color = color::Fg(TAG_COLOR),
snippet_color = color::Fg(SNIPPET_COLOR),
);
}
fn limit_str(text: &str, length: usize) -> String {
if text.len() > length {
format!("{}", text.chars().take(length - 1).collect::<String>())
} else {
format!("{:width$}", text, width = length)
}
}
pub trait Writer {
fn write(&mut self, item: Item) -> String;
}
pub struct FinderWriter {
pub tag_width: usize,
pub comment_width: usize,
}
pub struct AlfredWriter {
pub is_first: bool,
}
impl Writer for FinderWriter {
fn write(&mut self, item: Item) -> String {
format!(
"{tag_color}{tags_short}{delimiter}{comment_color}{comment_short}{delimiter}{snippet_color}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}\n",
tags_short = limit_str(item.tags, self.tag_width),
comment_short = limit_str(item.comment, self.comment_width),
snippet_short = fix_newlines(item.snippet),
comment_color = color::Fg(COMMENT_COLOR),
tag_color = color::Fg(TAG_COLOR),
snippet_color = color::Fg(SNIPPET_COLOR),
tags = item.tags,
comment = item.comment,
delimiter = DELIMITER,
snippet = &item.snippet)
}
}
fn escape_for_json(txt: &str) -> String {
txt.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace(NEWLINE_ESCAPE_CHAR, " ")
}
impl Writer for AlfredWriter {
fn write(&mut self, item: Item) -> String {
let prefix = if self.is_first {
self.is_first = false;
""
} else {
","
};
let tags = escape_for_json(item.tags);
let comment = escape_for_json(item.comment);
let snippet = escape_for_json(item.snippet);
format!(
r#"{prefix}{{"type":"file","title":"{comment}","match":"{comment} {tags} {snippet}","subtitle":"{tags} :: {snippet}","variables":{{"tags":"{tags}","comment":"{comment}","snippet":"{snippet}"}},"icon":{{"path":"navi.png"}}}}"#,
prefix = prefix,
tags = tags,
comment = comment,
snippet = snippet
)
}
}

76
src/display/alfred.rs Normal file
View file

@ -0,0 +1,76 @@
use crate::display::{self, Writer};
use crate::structures::item::Item;
pub struct AlfredWriter {
is_first: bool,
}
pub fn new_writer() -> AlfredWriter {
AlfredWriter { is_first: true }
}
fn escape_for_json(txt: &str) -> String {
txt.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace(display::NEWLINE_ESCAPE_CHAR, " ")
}
pub fn print_items_start(varname: Option<&str>) {
print!("{{");
if let Some(v) = varname {
print!(r#""variables": {{"varname": "{varname}"}},"#, varname = v);
}
println!(r#""items": ["#);
}
pub fn print_items_end() {
println!(r#"]}}"#);
}
impl Writer for AlfredWriter {
fn write(&mut self, item: Item) -> String {
let prefix = if self.is_first {
self.is_first = false;
""
} else {
","
};
let tags = escape_for_json(item.tags);
let comment = escape_for_json(item.comment);
let snippet = escape_for_json(item.snippet);
format!(
r#"{prefix}{{"type":"file","title":"{comment}","match":"{comment} {tags} {snippet}","subtitle":"{tags} :: {snippet}","variables":{{"tags":"{tags}","comment":"{comment}","snippet":"{snippet}"}},"icon":{{"path":"navi.png"}}}}"#,
prefix = prefix,
tags = tags,
comment = comment,
snippet = snippet
)
}
}
impl AlfredWriter {
pub fn write_suggestion(&mut self, snippet: &str, varname: &str, line: &str) {
if line.len() < 3 {
return;
}
let prefix = if self.is_first {
self.is_first = false;
""
} else {
","
};
println!(
r#"{prefix}{{"title":"{value}","subtitle":"{snippet}","variables":{{"{varname}":"{value}"}},"icon":{{"path":"navi.png"}}}}"#,
prefix = prefix,
snippet = snippet,
varname = varname,
value = line
);
}
}

28
src/display/mod.rs Normal file
View file

@ -0,0 +1,28 @@
use crate::structures::item::Item;
use regex::Regex;
pub mod alfred;
pub mod terminal;
const NEWLINE_ESCAPE_CHAR: char = '\x15';
pub const LINE_SEPARATOR: &str = " \x15 ";
pub const DELIMITER: &str = r" ";
lazy_static! {
pub static ref NEWLINE_REGEX: Regex = Regex::new(r"\\\s+").expect("Invalid regex");
pub static ref VAR_REGEX: Regex = Regex::new(r"<(\w[\w\d\-_]*)>").expect("Invalid regex");
}
pub fn fix_newlines(txt: &str) -> String {
if txt.contains(NEWLINE_ESCAPE_CHAR) {
(*NEWLINE_REGEX)
.replace_all(txt.replace(LINE_SEPARATOR, " ").as_str(), "")
.to_string()
} else {
txt.to_string()
}
}
pub trait Writer {
fn write(&mut self, item: Item) -> String;
}

62
src/display/terminal.rs Normal file
View file

@ -0,0 +1,62 @@
use crate::display::{self, Writer};
use crate::structures::item::Item;
use crate::terminal_width;
use termion::color;
const COMMENT_COLOR: color::LightCyan = color::LightCyan;
const TAG_COLOR: color::Blue = color::Blue;
const SNIPPET_COLOR: color::White = color::White;
pub fn variable_prompt(varname: &str) -> String {
format!("{}: ", varname)
}
pub fn preview(comment: &str, tags: &str, snippet: &str) {
println!(
"{comment_color}{comment} {tag_color}{tags} \n{snippet_color}{snippet}",
comment = format!("# {}", comment),
tags = format!("[{}]", tags),
snippet = display::fix_newlines(snippet),
comment_color = color::Fg(COMMENT_COLOR),
tag_color = color::Fg(TAG_COLOR),
snippet_color = color::Fg(SNIPPET_COLOR),
);
}
fn limit_str(text: &str, length: usize) -> String {
if text.len() > length {
format!("{}", text.chars().take(length - 1).collect::<String>())
} else {
format!("{:width$}", text, width = length)
}
}
pub struct TerminalWriter {
tag_width: usize,
comment_width: usize,
}
pub fn new_writer() -> TerminalWriter {
let (tag_width, comment_width) = terminal_width::get_widths();
display::terminal::TerminalWriter {
tag_width,
comment_width,
}
}
impl Writer for TerminalWriter {
fn write(&mut self, item: Item) -> String {
format!(
"{tag_color}{tags_short}{delimiter}{comment_color}{comment_short}{delimiter}{snippet_color}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}\n",
tags_short = limit_str(item.tags, self.tag_width),
comment_short = limit_str(item.comment, self.comment_width),
snippet_short = display::fix_newlines(item.snippet),
comment_color = color::Fg(COMMENT_COLOR),
tag_color = color::Fg(TAG_COLOR),
snippet_color = color::Fg(SNIPPET_COLOR),
tags = item.tags,
comment = item.comment,
delimiter = display::DELIMITER,
snippet = &item.snippet)
}
}

View file

@ -1,31 +1,27 @@
use crate::display;
use crate::parser;
use crate::structures::cheat::Suggestion;
use crate::structures::{error::command::BashSpawnError, option::Config};
use anyhow::Context;
use anyhow::Error;
use regex::Regex;
use std::env;
use std::process::{Command, Stdio};
lazy_static! {
pub static ref VAR_REGEX: Regex = Regex::new(r"<(\w[\w\d\-_]*)>").expect("Invalid regex");
}
pub fn main(config: Config) -> Result<(), Error> {
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
let stdin = child.stdin.as_mut().unwrap();
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.spawn()
.context("Unable to create child")?;
let stdin = child.stdin.as_mut().context("Unable to get stdin")?;
println!(r#"{{"items": ["#);
display::alfred::print_items_start(None);
parser::read_all(&config, stdin).context("Failed to parse variables intended for finder")?;
// make sure everything was printed to stdout before attempting to close the items vector
let _ = child.wait_with_output().context("Failed to wait for fzf")?;
println!(r#"]}}"#);
display::alfred::print_items_end();
Ok(())
}
@ -59,26 +55,22 @@ pub fn suggestions(config: Config) -> Result<(), Error> {
.stdin(Stdio::piped())
.stdout(Stdio::null())
.spawn()
.unwrap();
let stdin = child.stdin.as_mut().unwrap();
.context("Unable to create child")?;
let stdin = child.stdin.as_mut().context("Unable to get stdin")?;
let variables = parser::read_all(&config, stdin)
.context("Failed to parse variables intended for finder")?;
let tags = env::var("tags").unwrap();
let _comment = env::var("comment").unwrap();
let snippet = env::var("snippet").unwrap();
let tags = env::var("tags").context(r#"The env var "tags" isn't set"#)?;
let snippet = env::var("snippet").context(r#"The env var "snippet" isn't set"#)?;
let varname = VAR_REGEX.captures_iter(&snippet).next();
let varname = display::VAR_REGEX.captures_iter(&snippet).next();
if let Some(varname) = varname {
let varname = &varname[0];
let varname = &varname[1..varname.len() - 1];
println!(
r#"{{"variables": {{"varname": "{varname}"}}, "items": ["#,
varname = varname
);
display::alfred::print_items_start(Some(varname));
let lines = variables
.get(&tags, &varname)
@ -87,36 +79,28 @@ pub fn suggestions(config: Config) -> Result<(), Error> {
Ok(prompt_with_suggestions(&varname, &config, suggestion).unwrap())
})?;
let mut is_first = true;
let mut writer = display::alfred::new_writer();
for line in lines.split('\n') {
if line.len() < 3 {
continue;
}
let prefix = if is_first {
is_first = false;
""
} else {
","
};
println!(
r#"{prefix}{{"title":"{value}","subtitle":"{snippet}","variables":{{"{varname}":"{value}"}},"icon":{{"path":"navi.png"}}}}"#,
prefix = prefix,
snippet = snippet,
varname = varname,
value = line
);
writer.write_suggestion(&snippet, &varname, &line);
}
} else {
println!(r#"{{"items": ["#);
display::alfred::print_items_start(None);
}
println!(r#"]}}"#);
display::alfred::print_items_end();
Ok(())
}
pub fn transform(_config: Config) -> Result<(), Error> {
pub fn transform() -> Result<(), Error> {
let snippet = env::var("snippet").context(r#"The env var "snippet" isn't set"#)?;
let varname = env::var("varname").context(r#"The env var "varname" isn't set"#)?;
let value = env::var(&varname).context(format!(r#"The env var "{}" isn't set"#, &varname))?;
let bracketed_varname = format!("<{}>", varname);
let interpolated_snippet = snippet.replace(&bracketed_varname, &value);
println!("{}", interpolated_snippet);
Ok(())
}

View file

@ -10,16 +10,11 @@ use crate::structures::option;
use crate::structures::{error::command::BashSpawnError, option::Config};
use anyhow::Context;
use anyhow::Error;
use regex::Regex;
use std::env;
use std::fs;
use std::io::Write;
use std::process::{Command, Stdio};
lazy_static! {
pub static ref VAR_REGEX: Regex = Regex::new(r"<(\w[\w\d\-_]*)>").expect("Invalid regex");
}
pub enum Variant {
Core,
Filter(String),
@ -71,14 +66,6 @@ fn extract_from_selections(raw_snippet: &str, contains_key: bool) -> (&str, &str
(key, tags, snippet)
}
/* fn gen_opts_preview(snippet: &str, variable_name: &str) -> Option<String> {
Some(format!(
r#"query="{{}}"; [[ "${{#query:-}}" -lt 3 ]] && query="{{q}}"; query="${{query:1:${{#query}}-2}}"; query="$(echo "$query" | sed 's|/|\\/|g')"; echo "{}" | sed "s/<{}>/${{query}}/g" || echo 'Unable to generate command preview'"#,
snippet.replace('"', "\\\""),
variable_name
))
} */
fn prompt_with_suggestions(
variable_name: &str,
config: &Config,
@ -106,7 +93,7 @@ fn prompt_with_suggestions(
let opts = FinderOpts {
autoselect: !config.no_autoselect,
overrides: config.fzf_overrides_var.clone(),
prompt: Some(display::variable_prompt(variable_name)),
prompt: Some(display::terminal::variable_prompt(variable_name)),
..opts
};
@ -130,9 +117,8 @@ fn prompt_without_suggestions(
) -> Result<String, Error> {
let opts = FinderOpts {
autoselect: false,
prompt: Some(display::variable_prompt(variable_name)),
prompt: Some(display::terminal::variable_prompt(variable_name)),
suggestion_type: SuggestionType::Disabled,
// preview: gen_opts_preview(&snippet, &variable_name),
preview_window: Some("up:1".to_string()),
..Default::default()
};
@ -153,7 +139,7 @@ fn replace_variables_from_snippet(
) -> Result<String, Error> {
let mut interpolated_snippet = String::from(snippet);
for captures in VAR_REGEX.captures_iter(snippet) {
for captures in display::VAR_REGEX.captures_iter(snippet) {
let bracketed_variable_name = &captures[0];
let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1];
@ -226,7 +212,7 @@ pub fn main(variant: Variant, config: Config, contains_key: bool) -> Result<(),
println!("{}", interpolated_snippet);
// save to file
} else if let Some(s) = config.save {
fs::write(s, interpolated_snippet).context("Unable to save config")?;
fs::write(s, interpolated_snippet).context("Unable to save output")?;
// call navi (this prevents "failed to read /dev/tty" from finder)
} else if interpolated_snippet.starts_with("navi") {
let new_config = option::config_from_iter(interpolated_snippet.split(' ').collect());

View file

@ -12,6 +12,6 @@ fn extract_elements(argstr: &str) -> (&str, &str, &str) {
pub fn main(line: &str) -> Result<(), Error> {
let (tags, comment, snippet) = extract_elements(line);
display::preview(comment, tags, snippet);
display::terminal::preview(comment, tags, snippet);
process::exit(0)
}

View file

@ -43,7 +43,7 @@ pub fn handle_config(config: Config) -> Result<(), Error> {
Alfred { cmd } => match cmd {
AlfredCommand::Start => flows::alfred::main(config),
AlfredCommand::Suggestions => flows::alfred::suggestions(config),
AlfredCommand::Transform => flows::alfred::transform(config),
AlfredCommand::Transform => flows::alfred::transform(),
},
},
}

View file

@ -12,7 +12,7 @@ mod git;
mod handler;
mod parser;
mod structures;
mod terminal;
mod terminal_width;
mod welcome;
pub use handler::handle_config;

View file

@ -116,7 +116,7 @@ fn write_cmd(
tags: &str,
comment: &str,
snippet: &str,
writer: &mut Box<dyn Writer>,
writer: &mut dyn Writer,
stdin: &mut std::process::ChildStdin,
) -> Result<(), Error> {
if snippet.len() <= 1 {
@ -137,7 +137,7 @@ fn read_file(
path: &str,
variables: &mut VariableMap,
visited_lines: &mut HashSet<u64>,
writer: &mut Box<dyn Writer>,
writer: &mut dyn Writer,
stdin: &mut std::process::ChildStdin,
) -> Result<(), Error> {
let mut tags = String::from("");
@ -235,18 +235,14 @@ pub fn read_all(
let mut found_something = false;
let mut visited_lines = HashSet::new();
let mut writer: Box<dyn Writer> = if let Some(Alfred { .. }) = &config.cmd {
Box::new(display::AlfredWriter { is_first: true })
Box::new(display::alfred::new_writer())
} else {
let (tag_width, comment_width) = display::get_widths();
Box::new(display::FinderWriter {
tag_width,
comment_width,
})
Box::new(display::terminal::new_writer())
};
let paths = filesystem::cheat_paths(config);
if paths.is_err() {
welcome::cheatsheet(&mut writer, stdin);
welcome::cheatsheet(&mut *writer, stdin);
return Ok(variables);
}
@ -266,7 +262,7 @@ pub fn read_all(
path_str,
&mut variables,
&mut visited_lines,
&mut writer,
&mut *writer,
stdin,
)
.is_ok()
@ -280,7 +276,7 @@ pub fn read_all(
}
if !found_something {
welcome::cheatsheet(&mut writer, stdin);
welcome::cheatsheet(&mut *writer, stdin);
}
Ok(variables)
@ -314,18 +310,19 @@ mod tests {
fn test_read_file() {
let path = "tests/cheats/ssh.cheat";
let mut variables = VariableMap::new();
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.spawn()
.unwrap();
let child_stdin = child.stdin.as_mut().unwrap();
let mut visited_lines: HashSet<u64> = HashSet::new();
let mut writer: Box<dyn Writer> = Box::new(display::FinderWriter {
comment_width: 20,
tag_width: 30,
});
let mut writer: Box<dyn Writer> = Box::new(display::terminal::new_writer());
read_file(
path,
&mut variables,
&mut visited_lines,
&mut writer,
&mut *writer,
child_stdin,
)
.unwrap();

View file

@ -3,7 +3,7 @@ use thiserror::Error;
#[derive(Error, Debug)]
#[error(
"\rHey, listen! Navi encountered a problem.
"\rHey, listen! navi encountered a problem.
Do you think this is a bug? File an issue at https://github.com/denisidoro/navi."
)]
pub struct FileAnIssue {

View file

@ -11,7 +11,10 @@ fn parse_finder(src: &str) -> Result<FinderChoice, Error> {
}
#[derive(Debug, StructOpt)]
#[structopt(after_help = r#"EXAMPLES:
#[structopt(after_help = r#"MORE INFO:
Please refer to https://github.com/denisidoro/navi
EXAMPLES:
navi # default behavior
navi --print # doesn't execute the snippet
navi --path '/some/dir:/other/dir' # uses custom cheats
@ -106,7 +109,8 @@ pub enum Command {
/// bash, zsh or fish
shell: String,
},
/// Alfred
/// Helper command for Alfred integration
#[structopt(setting = AppSettings::Hidden)]
Alfred {
#[structopt(subcommand)]
cmd: AlfredCommand,
@ -126,11 +130,11 @@ pub enum RepoCommand {
#[derive(Debug, StructOpt)]
pub enum AlfredCommand {
/// Start
/// Outputs a JSON with commands
Start,
/// Suggestions
/// Outputs a JSON with variable suggestions
Suggestions,
/// Transform
/// Transforms the snippet env var with the selected value
Transform,
}

View file

@ -1,3 +1,4 @@
use std::cmp::max;
use terminal_size::{terminal_size, terminal_size_using_fd, Height, Width};
fn width_with_shell_out() -> u16 {
@ -53,7 +54,7 @@ fn width_with_fd() -> u16 {
}
}
pub fn width() -> u16 {
fn width() -> u16 {
let size = terminal_size();
if let Some((Width(w), Height(_))) = size {
w
@ -61,3 +62,10 @@ pub fn width() -> u16 {
width_with_fd()
}
}
pub fn get_widths() -> (usize, usize) {
let width = width();
let tag_width = max(4, width * 20 / 100);
let comment_width = max(4, width * 40 / 100);
(usize::from(tag_width), usize::from(comment_width))
}

View file

@ -6,7 +6,7 @@ fn add_msg(
tags: &str,
comment: &str,
snippet: &str,
writer: &mut Box<dyn Writer>,
writer: &mut dyn Writer,
stdin: &mut std::process::ChildStdin,
) {
let item = Item {
@ -19,7 +19,7 @@ fn add_msg(
.expect("Could not write to fzf's stdin");
}
pub fn cheatsheet(writer: &mut Box<dyn Writer>, stdin: &mut std::process::ChildStdin) {
pub fn cheatsheet(writer: &mut dyn Writer, stdin: &mut std::process::ChildStdin) {
add_msg(
"cheatsheets",
"Download default cheatsheets",