mirror of
https://github.com/denisidoro/navi.git
synced 2026-01-23 02:14:19 +00:00
rustfmt: max-width 150 (#388)
This commit is contained in:
parent
4ba0373790
commit
2dd918d1e0
23 changed files with 113 additions and 374 deletions
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
max_width = 150
|
||||
|
|
@ -24,22 +24,10 @@ fn lines(query: &str, markdown: &str) -> impl Iterator<Item = Result<String, Err
|
|||
.into_iter()
|
||||
}
|
||||
|
||||
fn read_all(
|
||||
query: &str,
|
||||
cheat: &str,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
fn read_all(query: &str, cheat: &str, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result<Option<VariableMap>, Error> {
|
||||
let mut variables = VariableMap::new();
|
||||
let mut visited_lines = HashSet::new();
|
||||
parser::read_lines(
|
||||
lines(query, cheat),
|
||||
"cheatsh",
|
||||
&mut variables,
|
||||
&mut visited_lines,
|
||||
writer,
|
||||
stdin,
|
||||
)?;
|
||||
parser::read_lines(lines(query, cheat), "cheatsh", &mut variables, &mut visited_lines, writer, stdin)?;
|
||||
Ok(Some(variables))
|
||||
}
|
||||
|
||||
|
|
@ -58,10 +46,7 @@ pub fn fetch(query: &str) -> Result<String, Error> {
|
|||
}
|
||||
};
|
||||
|
||||
let stdout = child
|
||||
.wait_with_output()
|
||||
.context("Failed to wait for curl")?
|
||||
.stdout;
|
||||
let stdout = child.wait_with_output().context("Failed to wait for curl")?.stdout;
|
||||
|
||||
let plain_bytes = strip_ansi_escapes::strip(&stdout)?;
|
||||
|
||||
|
|
@ -79,11 +64,7 @@ impl Foo {
|
|||
}
|
||||
|
||||
impl Fetcher for Foo {
|
||||
fn fetch(
|
||||
&self,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result<Option<VariableMap>, Error> {
|
||||
eprintln!("TODO!!!!");
|
||||
let cheat = fetch(&self.query)?;
|
||||
dbg!(&cheat);
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ use std::env;
|
|||
use std::process::{Command, Stdio};
|
||||
|
||||
pub fn main(_config: Config) -> Result<(), Error> {
|
||||
let mut child = Command::new("cat")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Unable to create child")?;
|
||||
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")?;
|
||||
let _writer = display::alfred::Writer::new();
|
||||
|
||||
|
|
@ -37,13 +34,8 @@ fn prompt_with_suggestions(suggestion: &Suggestion) -> Result<String, Error> {
|
|||
.spawn()
|
||||
.map_err(|e| BashSpawnError::new(suggestion_command, e))?;
|
||||
|
||||
let suggestions = String::from_utf8(
|
||||
child
|
||||
.wait_with_output()
|
||||
.context("Failed to wait and collect output from bash")?
|
||||
.stdout,
|
||||
)
|
||||
.context("Suggestions are invalid utf8")?;
|
||||
let suggestions = String::from_utf8(child.wait_with_output().context("Failed to wait and collect output from bash")?.stdout)
|
||||
.context("Suggestions are invalid utf8")?;
|
||||
|
||||
Ok(suggestions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,40 +29,23 @@ fn gen_core_finder_opts(config: &Config) -> Result<FinderOpts, Error> {
|
|||
autoselect: !config.get_no_autoselect(),
|
||||
overrides: config.fzf_overrides.clone(),
|
||||
suggestion_type: SuggestionType::SnippetSelection,
|
||||
query: if config.get_single() {
|
||||
None
|
||||
} else {
|
||||
config.get_query()
|
||||
},
|
||||
filter: if config.get_single() {
|
||||
config.get_query()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
query: if config.get_single() { None } else { config.get_query() },
|
||||
filter: if config.get_single() { config.get_query() } else { None },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(opts)
|
||||
}
|
||||
|
||||
fn extract_from_selections(
|
||||
raw_snippet: &str,
|
||||
is_single: bool,
|
||||
) -> Result<(&str, &str, &str), Error> {
|
||||
fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, &str, &str), Error> {
|
||||
let mut lines = raw_snippet.split('\n');
|
||||
let key = if !is_single {
|
||||
lines
|
||||
.next()
|
||||
.context("Key was promised but not present in `selections`")?
|
||||
lines.next().context("Key was promised but not present in `selections`")?
|
||||
} else {
|
||||
"enter"
|
||||
};
|
||||
|
||||
let mut parts = lines
|
||||
.next()
|
||||
.context("No more parts in `selections`")?
|
||||
.split(display::DELIMITER)
|
||||
.skip(3);
|
||||
let mut parts = lines.next().context("No more parts in `selections`")?.split(display::DELIMITER).skip(3);
|
||||
|
||||
let tags = parts.next().unwrap_or("");
|
||||
parts.next();
|
||||
|
|
@ -71,12 +54,7 @@ fn extract_from_selections(
|
|||
Ok((key, tags, snippet))
|
||||
}
|
||||
|
||||
fn prompt_with_suggestions(
|
||||
variable_name: &str,
|
||||
config: &Config,
|
||||
suggestion: &Suggestion,
|
||||
_snippet: String,
|
||||
) -> Result<String, Error> {
|
||||
fn prompt_with_suggestions(variable_name: &str, config: &Config, suggestion: &Suggestion, _snippet: String) -> Result<String, Error> {
|
||||
let (suggestion_command, suggestion_opts) = suggestion;
|
||||
|
||||
let child = Command::new("bash")
|
||||
|
|
@ -86,13 +64,8 @@ fn prompt_with_suggestions(
|
|||
.spawn()
|
||||
.map_err(|e| BashSpawnError::new(suggestion_command, e))?;
|
||||
|
||||
let suggestions = String::from_utf8(
|
||||
child
|
||||
.wait_with_output()
|
||||
.context("Failed to wait and collect output from bash")?
|
||||
.stdout,
|
||||
)
|
||||
.context("Suggestions are invalid utf8")?;
|
||||
let suggestions = String::from_utf8(child.wait_with_output().context("Failed to wait and collect output from bash")?.stdout)
|
||||
.context("Suggestions are invalid utf8")?;
|
||||
|
||||
let opts = suggestion_opts.clone().unwrap_or_default();
|
||||
let opts = FinderOpts {
|
||||
|
|
@ -105,9 +78,7 @@ fn prompt_with_suggestions(
|
|||
let (output, _) = config
|
||||
.finder
|
||||
.call(opts, |stdin| {
|
||||
stdin
|
||||
.write_all(suggestions.as_bytes())
|
||||
.context("Could not write to finder's stdin")?;
|
||||
stdin.write_all(suggestions.as_bytes()).context("Could not write to finder's stdin")?;
|
||||
Ok(None)
|
||||
})
|
||||
.context("finder was unable to prompt with suggestions")?;
|
||||
|
|
@ -132,12 +103,7 @@ fn prompt_without_suggestions(variable_name: &str, config: &Config) -> Result<St
|
|||
Ok(output)
|
||||
}
|
||||
|
||||
fn replace_variables_from_snippet(
|
||||
snippet: &str,
|
||||
tags: &str,
|
||||
variables: VariableMap,
|
||||
config: &Config,
|
||||
) -> Result<String, Error> {
|
||||
fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: VariableMap, config: &Config) -> Result<String, Error> {
|
||||
let mut interpolated_snippet = String::from(snippet);
|
||||
|
||||
for captures in display::VAR_REGEX.captures_iter(snippet) {
|
||||
|
|
@ -154,19 +120,9 @@ fn replace_variables_from_snippet(
|
|||
.ok_or_else(|| anyhow!("No suggestions"))
|
||||
.and_then(|suggestion| {
|
||||
let mut new_suggestion = suggestion.clone();
|
||||
new_suggestion.0 = replace_variables_from_snippet(
|
||||
&new_suggestion.0,
|
||||
tags,
|
||||
variables.clone(),
|
||||
config,
|
||||
)?;
|
||||
new_suggestion.0 = replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone(), config)?;
|
||||
|
||||
prompt_with_suggestions(
|
||||
variable_name,
|
||||
&config,
|
||||
&new_suggestion,
|
||||
interpolated_snippet.clone(),
|
||||
)
|
||||
prompt_with_suggestions(variable_name, &config, &new_suggestion, interpolated_snippet.clone())
|
||||
})
|
||||
.or_else(|_| prompt_without_suggestions(variable_name, &config))?
|
||||
};
|
||||
|
|
@ -213,13 +169,8 @@ pub fn main(config: Config) -> Result<(), Error> {
|
|||
let (key, tags, snippet) = extract_from_selections(&raw_selection[..], config.get_single())?;
|
||||
|
||||
let interpolated_snippet = display::with_new_lines(
|
||||
replace_variables_from_snippet(
|
||||
snippet,
|
||||
tags,
|
||||
variables.expect("No variables received from finder"),
|
||||
&config,
|
||||
)
|
||||
.context("Failed to replace variables from snippet")?,
|
||||
replace_variables_from_snippet(snippet, tags, variables.expect("No variables received from finder"), &config)
|
||||
.context("Failed to replace variables from snippet")?,
|
||||
);
|
||||
|
||||
match config.action() {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ pub fn main(func: String, args: Vec<String>) -> Result<(), Error> {
|
|||
match func.as_str() {
|
||||
"url::open" => url::open(args),
|
||||
|
||||
"welcome" => handler::handle_config(config::config_from_iter(
|
||||
"navi --path /tmp/navi/irrelevant".split(' ').collect(),
|
||||
)),
|
||||
"welcome" => handler::handle_config(config::config_from_iter("navi --path /tmp/navi/irrelevant".split(' ').collect())),
|
||||
|
||||
_ => Err(anyhow!("Unrecognized function")),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ pub fn browse(finder: &FinderChoice) -> Result<(), Error> {
|
|||
filesystem::create_dir(&repo_path_str)?;
|
||||
|
||||
let (repo_url, _, _) = git::meta("denisidoro/cheats");
|
||||
git::shallow_clone(repo_url.as_str(), &repo_path_str)
|
||||
.with_context(|| format!("Failed to clone `{}`", repo_url))?;
|
||||
git::shallow_clone(repo_url.as_str(), &repo_path_str).with_context(|| format!("Failed to clone `{}`", repo_url))?;
|
||||
|
||||
let repos = fs::read_to_string(format!("{}/featured_repos.txt", &repo_path_str))
|
||||
.context("Unable to fetch featured repositories")?;
|
||||
let repos = fs::read_to_string(format!("{}/featured_repos.txt", &repo_path_str)).context("Unable to fetch featured repositories")?;
|
||||
|
||||
let opts = FinderOpts {
|
||||
column: Some(1),
|
||||
|
|
@ -28,9 +26,7 @@ pub fn browse(finder: &FinderChoice) -> Result<(), Error> {
|
|||
|
||||
let (repo, _) = finder
|
||||
.call(opts, |stdin| {
|
||||
stdin
|
||||
.write_all(repos.as_bytes())
|
||||
.context("Unable to prompt featured repositories")?;
|
||||
stdin.write_all(repos.as_bytes()).context("Unable to prompt featured repositories")?;
|
||||
Ok(None)
|
||||
})
|
||||
.context("Failed to get repo URL from finder")?;
|
||||
|
|
@ -52,8 +48,7 @@ pub fn add(uri: String, finder: &FinderChoice) -> Result<(), Error> {
|
|||
|
||||
eprintln!("Cloning {} into {}...\n", &actual_uri, &tmp_path_str);
|
||||
|
||||
git::shallow_clone(actual_uri.as_str(), &tmp_path_str)
|
||||
.with_context(|| format!("Failed to clone `{}`", actual_uri))?;
|
||||
git::shallow_clone(actual_uri.as_str(), &tmp_path_str).with_context(|| format!("Failed to clone `{}`", actual_uri))?;
|
||||
|
||||
let all_files = WalkDir::new(&tmp_path_str)
|
||||
.into_iter()
|
||||
|
|
@ -67,18 +62,14 @@ pub fn add(uri: String, finder: &FinderChoice) -> Result<(), Error> {
|
|||
let opts = FinderOpts {
|
||||
suggestion_type: SuggestionType::MultipleSelections,
|
||||
preview: Some(format!("cat '{}/{{}}'", tmp_path_str)),
|
||||
header: Some(
|
||||
"Select the cheatsheets you want to import with <TAB> then hit <Enter>".to_string(),
|
||||
),
|
||||
header: Some("Select the cheatsheets you want to import with <TAB> then hit <Enter>".to_string()),
|
||||
preview_window: Some("right:30%".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (files, _) = finder
|
||||
.call(opts, |stdin| {
|
||||
stdin
|
||||
.write_all(all_files.as_bytes())
|
||||
.context("Unable to prompt cheats to import")?;
|
||||
stdin.write_all(all_files.as_bytes()).context("Unable to prompt cheats to import")?;
|
||||
Ok(None)
|
||||
})
|
||||
.context("Failed to get cheatsheet files from finder")?;
|
||||
|
|
@ -94,7 +85,10 @@ pub fn add(uri: String, finder: &FinderChoice) -> Result<(), Error> {
|
|||
|
||||
filesystem::remove_dir(&tmp_path_str)?;
|
||||
|
||||
eprintln!("The following .cheat files were imported successfully:\n{}\n\nThey are now located at {}\n\nPlease run navi again to check the results.", files, cheat_path_str);
|
||||
eprintln!(
|
||||
"The following .cheat files were imported successfully:\n{}\n\nThey are now located at {}\n\nPlease run navi again to check the results.",
|
||||
files, cheat_path_str
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ impl FileAnIssue {
|
|||
where
|
||||
SourceError: Into<anyhow::Error>,
|
||||
{
|
||||
FileAnIssue {
|
||||
source: source.into(),
|
||||
}
|
||||
FileAnIssue { source: source.into() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,7 @@ where
|
|||
P: AsRef<Path> + Display + Copy,
|
||||
{
|
||||
let file = File::open(filename).with_context(|| format!("Failed to open file {}", filename))?;
|
||||
Ok(io::BufReader::new(file)
|
||||
.lines()
|
||||
.map(|line| line.map_err(Error::from)))
|
||||
Ok(io::BufReader::new(file).lines().map(|line| line.map_err(Error::from)))
|
||||
}
|
||||
|
||||
pub fn pathbuf_to_string(pathbuf: PathBuf) -> Result<String, Error> {
|
||||
|
|
@ -52,18 +50,10 @@ pub fn pathbuf_to_string(pathbuf: PathBuf) -> Result<String, Error> {
|
|||
fn follow_symlink(pathbuf: PathBuf) -> Result<PathBuf, Error> {
|
||||
fs::read_link(pathbuf.clone())
|
||||
.map(|o| {
|
||||
let o_str = o
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or_else(|| InvalidPath(o.to_path_buf()))?;
|
||||
let o_str = o.as_os_str().to_str().ok_or_else(|| InvalidPath(o.to_path_buf()))?;
|
||||
if o_str.starts_with('.') {
|
||||
let parent = pathbuf
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("`{}` has no parent", pathbuf.display()))?;
|
||||
let parent_str = parent
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or_else(|| InvalidPath(parent.to_path_buf()))?;
|
||||
let parent = pathbuf.parent().ok_or_else(|| anyhow!("`{}` has no parent", pathbuf.display()))?;
|
||||
let parent_str = parent.as_os_str().to_str().ok_or_else(|| InvalidPath(parent.to_path_buf()))?;
|
||||
let path_str = format!("{}/{}", parent_str, o_str);
|
||||
let p = PathBuf::from(path_str);
|
||||
follow_symlink(p)
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ fn width_with_shell_out() -> u16 {
|
|||
let stdout = String::from_utf8(output.stdout).expect("Invalid utf8 output from stty");
|
||||
let mut data = stdout.split_whitespace();
|
||||
data.next();
|
||||
u16::from_str_radix(data.next().expect("Not enough data"), 10)
|
||||
.expect("Invalid base-10 number")
|
||||
u16::from_str_radix(data.next().expect("Not enough data"), 10).expect("Invalid base-10 number")
|
||||
}
|
||||
_ => FALLBACK_WIDTH,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ use anyhow::Error;
|
|||
use std::process::Command;
|
||||
|
||||
pub fn open(args: Vec<String>) -> Result<(), Error> {
|
||||
let url = args
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("No URL specified"))?;
|
||||
let url = args.into_iter().next().ok_or_else(|| anyhow!("No URL specified"))?;
|
||||
let code = r#"
|
||||
exst() {
|
||||
type "$1" &>/dev/null
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ pub struct Writer {
|
|||
}
|
||||
|
||||
fn escape_for_json(txt: &str) -> String {
|
||||
txt.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace(display::NEWLINE_ESCAPE_CHAR, " ")
|
||||
txt.replace('\\', "\\\\").replace('"', "\\\"").replace(display::NEWLINE_ESCAPE_CHAR, " ")
|
||||
}
|
||||
|
||||
pub fn print_items_start(varname: Option<&str>) {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ pub fn with_new_lines(txt: String) -> String {
|
|||
|
||||
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()
|
||||
(*NEWLINE_REGEX).replace_all(txt.replace(LINE_SEPARATOR, " ").as_str(), "").to_string()
|
||||
} else {
|
||||
txt.to_string()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,15 +22,11 @@ fn parse_env_var_u16(varname: &str) -> Option<u16> {
|
|||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TAG_COLOR: color::AnsiValue =
|
||||
color::AnsiValue(parse_env_var_u8("NAVI_TAG_COLOR").unwrap_or(14));
|
||||
pub static ref COMMENT_COLOR: color::AnsiValue =
|
||||
color::AnsiValue(parse_env_var_u8("NAVI_COMMENT_COLOR").unwrap_or(4));
|
||||
pub static ref SNIPPET_COLOR: color::AnsiValue =
|
||||
color::AnsiValue(parse_env_var_u8("NAVI_SNIPPET_COLOR").unwrap_or(7));
|
||||
pub static ref TAG_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var_u8("NAVI_TAG_COLOR").unwrap_or(14));
|
||||
pub static ref COMMENT_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var_u8("NAVI_COMMENT_COLOR").unwrap_or(4));
|
||||
pub static ref SNIPPET_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var_u8("NAVI_SNIPPET_COLOR").unwrap_or(7));
|
||||
pub static ref TAG_WIDTH_PERCENTAGE: u16 = parse_env_var_u16("NAVI_TAG_WIDTH").unwrap_or(20);
|
||||
pub static ref COMMENT_WIDTH_PERCENTAGE: u16 =
|
||||
parse_env_var_u16("NAVI_COMMENT_WIDTH").unwrap_or(40);
|
||||
pub static ref COMMENT_WIDTH_PERCENTAGE: u16 = parse_env_var_u16("NAVI_COMMENT_WIDTH").unwrap_or(40);
|
||||
}
|
||||
|
||||
pub fn variable_prompt(varname: &str) -> String {
|
||||
|
|
@ -72,10 +68,7 @@ pub struct Writer {
|
|||
impl Writer {
|
||||
pub fn new() -> Writer {
|
||||
let (tag_width, comment_width) = get_widths();
|
||||
display::terminal::Writer {
|
||||
tag_width,
|
||||
comment_width,
|
||||
}
|
||||
display::terminal::Writer { tag_width, comment_width }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,12 +45,7 @@ fn cheat_paths_from_config_dir() -> Result<String, Error> {
|
|||
let mut paths_str = String::from("");
|
||||
for entry in dir_entries {
|
||||
let path = entry.map_err(|e| UnreadableDir::new(path.clone(), e))?;
|
||||
paths_str.push_str(
|
||||
path.path()
|
||||
.into_os_string()
|
||||
.to_str()
|
||||
.ok_or_else(|| InvalidPath(path.path()))?,
|
||||
);
|
||||
paths_str.push_str(path.path().into_os_string().to_str().ok_or_else(|| InvalidPath(path.path()))?);
|
||||
paths_str.push_str(":");
|
||||
}
|
||||
Ok(paths_str)
|
||||
|
|
@ -58,16 +53,11 @@ fn cheat_paths_from_config_dir() -> Result<String, Error> {
|
|||
}
|
||||
|
||||
pub fn cheat_paths(path: Option<String>) -> Result<String, Error> {
|
||||
path.ok_or_else(|| anyhow!("No cheat paths")).or_else(|_| {
|
||||
cheat_paths_from_config_dir().context("No directory for cheats in user data directory")
|
||||
})
|
||||
path.ok_or_else(|| anyhow!("No cheat paths"))
|
||||
.or_else(|_| cheat_paths_from_config_dir().context("No directory for cheats in user data directory"))
|
||||
}
|
||||
|
||||
pub fn read_all(
|
||||
path: Option<String>,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
pub fn read_all(path: Option<String>, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result<Option<VariableMap>, Error> {
|
||||
let mut variables = VariableMap::new();
|
||||
let mut found_something = false;
|
||||
let mut visited_lines = HashSet::new();
|
||||
|
|
@ -89,12 +79,9 @@ pub fn read_all(
|
|||
for entry in dir_entries {
|
||||
if entry.is_ok() {
|
||||
let path = entry.expect("Impossible to read an invalid entry").path();
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.ok_or_else(|| InvalidPath(path.to_path_buf()))?;
|
||||
let path_str = path.to_str().ok_or_else(|| InvalidPath(path.to_path_buf()))?;
|
||||
if path_str.ends_with(".cheat")
|
||||
&& read_file(path_str, &mut variables, &mut visited_lines, writer, stdin)
|
||||
.is_ok()
|
||||
&& read_file(path_str, &mut variables, &mut visited_lines, writer, stdin).is_ok()
|
||||
&& !found_something
|
||||
{
|
||||
found_something = true;
|
||||
|
|
@ -122,22 +109,11 @@ 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())
|
||||
.stdout(Stdio::null())
|
||||
.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::terminal::Writer::new());
|
||||
read_file(
|
||||
path,
|
||||
&mut variables,
|
||||
&mut visited_lines,
|
||||
&mut *writer,
|
||||
child_stdin,
|
||||
)
|
||||
.unwrap();
|
||||
read_file(path, &mut variables, &mut visited_lines, &mut *writer, child_stdin).unwrap();
|
||||
let expected_suggestion = (
|
||||
r#" echo -e "$(whoami)\nroot" "#.to_string(),
|
||||
Some(FinderOpts {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,5 @@ use crate::structures::cheat::VariableMap;
|
|||
use anyhow::Error;
|
||||
|
||||
pub trait Fetcher {
|
||||
fn fetch(
|
||||
self: &Self,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error>;
|
||||
fn fetch(self: &Self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result<Option<VariableMap>, Error>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
pub use crate::common::filesystem::{
|
||||
create_dir, exe_string, pathbuf_to_string, remove_dir, InvalidPath, UnreadableDir,
|
||||
};
|
||||
pub use crate::common::filesystem::{create_dir, exe_string, pathbuf_to_string, remove_dir, InvalidPath, UnreadableDir};
|
||||
use crate::display::Writer;
|
||||
pub use crate::fetcher::filesystem::{cheat_pathbuf, read_all};
|
||||
use crate::fetcher::Fetcher;
|
||||
|
|
@ -23,11 +21,7 @@ impl Foo {
|
|||
}
|
||||
|
||||
impl Fetcher for Foo {
|
||||
fn fetch(
|
||||
&self,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result<Option<VariableMap>, Error> {
|
||||
read_all(self.path.clone(), stdin, writer)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ use std::process::{self, Command, Stdio};
|
|||
pub struct FzfFinder;
|
||||
|
||||
impl Finder for FzfFinder {
|
||||
fn call<F>(
|
||||
&self,
|
||||
finder_opts: Opts,
|
||||
stdin_fn: F,
|
||||
) -> Result<(String, Option<VariableMap>), Error>
|
||||
fn call<F>(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, Option<VariableMap>), Error>
|
||||
where
|
||||
F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>,
|
||||
{
|
||||
|
|
@ -83,13 +79,9 @@ impl Finder for FzfFinder {
|
|||
}
|
||||
|
||||
if let Some(o) = opts.overrides {
|
||||
o.as_str()
|
||||
.split(' ')
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.for_each(|s| {
|
||||
command.arg(s);
|
||||
});
|
||||
o.as_str().split(' ').map(|s| s.to_string()).filter(|s| !s.is_empty()).for_each(|s| {
|
||||
command.arg(s);
|
||||
});
|
||||
}
|
||||
|
||||
let child = command.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn();
|
||||
|
|
@ -102,10 +94,7 @@ impl Finder for FzfFinder {
|
|||
}
|
||||
};
|
||||
|
||||
let stdin = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("Unable to acquire stdin of fzf"))?;
|
||||
let stdin = child.stdin.as_mut().ok_or_else(|| anyhow!("Unable to acquire stdin of fzf"))?;
|
||||
let result_map = stdin_fn(stdin).context("Failed to pass data to fzf")?;
|
||||
|
||||
let out = child.wait_with_output().context("Failed to wait for fzf")?;
|
||||
|
|
|
|||
|
|
@ -73,14 +73,8 @@ fn get_column(text: String, column: Option<u8>, delimiter: Option<&str>) -> Stri
|
|||
|
||||
fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> Result<String, Error> {
|
||||
Ok(match suggestion_type {
|
||||
SuggestionType::SingleSelection => text
|
||||
.lines()
|
||||
.next()
|
||||
.context("Not sufficient data for single selection")?
|
||||
.to_string(),
|
||||
SuggestionType::MultipleSelections
|
||||
| SuggestionType::Disabled
|
||||
| SuggestionType::SnippetSelection => {
|
||||
SuggestionType::SingleSelection => text.lines().next().context("Not sufficient data for single selection")?.to_string(),
|
||||
SuggestionType::MultipleSelections | SuggestionType::Disabled | SuggestionType::SnippetSelection => {
|
||||
let len = text.len();
|
||||
if len > 1 {
|
||||
text.truncate(len - 1);
|
||||
|
|
@ -91,20 +85,14 @@ fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> Res
|
|||
let lines: Vec<&str> = text.lines().collect();
|
||||
|
||||
match (lines.get(0), lines.get(1), lines.get(2)) {
|
||||
(Some(one), Some(termination), Some(two))
|
||||
if *termination == "enter" || termination.is_empty() =>
|
||||
{
|
||||
(Some(one), Some(termination), Some(two)) if *termination == "enter" || termination.is_empty() => {
|
||||
if two.is_empty() {
|
||||
(*one).to_string()
|
||||
} else {
|
||||
(*two).to_string()
|
||||
}
|
||||
}
|
||||
(Some(one), Some(termination), None)
|
||||
if *termination == "enter" || termination.is_empty() =>
|
||||
{
|
||||
(*one).to_string()
|
||||
}
|
||||
(Some(one), Some(termination), None) if *termination == "enter" || termination.is_empty() => (*one).to_string(),
|
||||
(Some(one), Some(termination), _) if *termination == "tab" => (*one).to_string(),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
|
|
@ -114,13 +102,10 @@ fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> Res
|
|||
|
||||
fn parse(out: Output, opts: Opts) -> Result<String, Error> {
|
||||
let text = match out.status.code() {
|
||||
Some(0) | Some(1) | Some(2) => {
|
||||
String::from_utf8(out.stdout).context("Invalid utf8 received from finder")?
|
||||
}
|
||||
Some(0) | Some(1) | Some(2) => String::from_utf8(out.stdout).context("Invalid utf8 received from finder")?,
|
||||
Some(130) => process::exit(130),
|
||||
_ => {
|
||||
let err = String::from_utf8(out.stderr)
|
||||
.unwrap_or_else(|_| "<stderr contains invalid UTF-8>".to_owned());
|
||||
let err = String::from_utf8(out.stderr).unwrap_or_else(|_| "<stderr contains invalid UTF-8>".to_owned());
|
||||
panic!("External command failed:\n {}", err)
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ use std::process::{self, Command, Stdio};
|
|||
pub struct SkimFinder;
|
||||
|
||||
impl Finder for SkimFinder {
|
||||
fn call<F>(
|
||||
&self,
|
||||
finder_opts: Opts,
|
||||
stdin_fn: F,
|
||||
) -> Result<(String, Option<VariableMap>), Error>
|
||||
fn call<F>(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, Option<VariableMap>), Error>
|
||||
where
|
||||
F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>,
|
||||
{
|
||||
|
|
@ -114,15 +110,10 @@ impl Finder for SkimFinder {
|
|||
}
|
||||
};
|
||||
|
||||
let stdin = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("Unable to acquire stdin of skim"))?;
|
||||
let stdin = child.stdin.as_mut().ok_or_else(|| anyhow!("Unable to acquire stdin of skim"))?;
|
||||
let result_map = stdin_fn(stdin).context("Failed to pass data to skim")?;
|
||||
|
||||
let out = child
|
||||
.wait_with_output()
|
||||
.context("Failed to wait for skim")?;
|
||||
let out = child.wait_with_output().context("Failed to wait for skim")?;
|
||||
|
||||
let output = parse(out, finder_opts).context("Unable to get output")?;
|
||||
Ok((output, result_map))
|
||||
|
|
|
|||
|
|
@ -8,37 +8,28 @@ pub fn handle_config(config: Config) -> Result<(), Error> {
|
|||
match config.cmd.as_ref() {
|
||||
None => cmds::core::main(config),
|
||||
|
||||
Some(c) => {
|
||||
match c {
|
||||
Preview { line } => cmds::preview::main(&line[..]),
|
||||
Some(c) => match c {
|
||||
Preview { line } => cmds::preview::main(&line[..]),
|
||||
|
||||
Widget { shell } => {
|
||||
cmds::shell::main(&shell).context("Failed to print shell widget code")
|
||||
Widget { shell } => cmds::shell::main(&shell).context("Failed to print shell widget code"),
|
||||
|
||||
Fn { func, args } => cmds::func::main(func.clone(), args.to_vec()).with_context(|| format!("Failed to execute function `{}`", func)),
|
||||
|
||||
Repo { cmd } => match cmd {
|
||||
RepoCommand::Add { uri } => {
|
||||
cmds::repo::add(uri.clone(), &config.finder).with_context(|| format!("Failed to import cheatsheets from `{}`", uri))
|
||||
}
|
||||
RepoCommand::Browse => cmds::repo::browse(&config.finder).context("Failed to browse featured cheatsheets"),
|
||||
},
|
||||
|
||||
Fn { func, args } => cmds::func::main(func.clone(), args.to_vec())
|
||||
.with_context(|| format!("Failed to execute function `{}`", func)),
|
||||
Alfred { cmd } => match cmd {
|
||||
AlfredCommand::Start => cmds::alfred::main(config).context("Failed to call Alfred starting function"),
|
||||
AlfredCommand::Suggestions => cmds::alfred::suggestions(config, false).context("Failed to call Alfred suggestion function"),
|
||||
AlfredCommand::Check => cmds::alfred::suggestions(config, true).context("Failed to call Alfred check function"),
|
||||
AlfredCommand::Transform => cmds::alfred::transform().context("Failed to call Alfred transform function"),
|
||||
},
|
||||
|
||||
Repo { cmd } => match cmd {
|
||||
RepoCommand::Add { uri } => cmds::repo::add(uri.clone(), &config.finder)
|
||||
.with_context(|| format!("Failed to import cheatsheets from `{}`", uri)),
|
||||
RepoCommand::Browse => cmds::repo::browse(&config.finder)
|
||||
.context("Failed to browse featured cheatsheets"),
|
||||
},
|
||||
|
||||
Alfred { cmd } => match cmd {
|
||||
AlfredCommand::Start => cmds::alfred::main(config)
|
||||
.context("Failed to call Alfred starting function"),
|
||||
AlfredCommand::Suggestions => cmds::alfred::suggestions(config, false)
|
||||
.context("Failed to call Alfred suggestion function"),
|
||||
AlfredCommand::Check => cmds::alfred::suggestions(config, true)
|
||||
.context("Failed to call Alfred check function"),
|
||||
AlfredCommand::Transform => cmds::alfred::transform()
|
||||
.context("Failed to call Alfred transform function"),
|
||||
},
|
||||
|
||||
_ => cmds::core::main(config),
|
||||
}
|
||||
}
|
||||
_ => cmds::core::main(config),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ use std::collections::HashSet;
|
|||
use std::io::Write;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref VAR_LINE_REGEX: Regex =
|
||||
Regex::new(r"^\$\s*([^:]+):(.*)").expect("Invalid regex");
|
||||
pub static ref VAR_LINE_REGEX: Regex = Regex::new(r"^\$\s*([^:]+):(.*)").expect("Invalid regex");
|
||||
}
|
||||
|
||||
fn parse_opts(text: &str) -> Result<FinderOpts, Error> {
|
||||
|
|
@ -19,8 +18,7 @@ fn parse_opts(text: &str) -> Result<FinderOpts, Error> {
|
|||
let mut is_global = false;
|
||||
let mut opts = FinderOpts::default();
|
||||
|
||||
let parts = shellwords::split(text)
|
||||
.map_err(|_| anyhow!("Given options are missing a closing quote"))?;
|
||||
let parts = shellwords::split(text).map_err(|_| anyhow!("Given options are missing a closing quote"))?;
|
||||
|
||||
parts
|
||||
.into_iter()
|
||||
|
|
@ -47,18 +45,8 @@ fn parse_opts(text: &str) -> Result<FinderOpts, Error> {
|
|||
.map(|flag_and_value| {
|
||||
if let [flag, value] = flag_and_value {
|
||||
match flag.as_str() {
|
||||
"--headers" | "--header-lines" => {
|
||||
opts.header_lines = value
|
||||
.parse::<u8>()
|
||||
.context("Value for `--headers` is invalid u8")?
|
||||
}
|
||||
"--column" => {
|
||||
opts.column = Some(
|
||||
value
|
||||
.parse::<u8>()
|
||||
.context("Value for `--column` is invalid u8")?,
|
||||
)
|
||||
}
|
||||
"--headers" | "--header-lines" => opts.header_lines = value.parse::<u8>().context("Value for `--headers` is invalid u8")?,
|
||||
"--column" => opts.column = Some(value.parse::<u8>().context("Value for `--column` is invalid u8")?),
|
||||
"--map" => opts.map = Some(value.to_string()),
|
||||
"--delimiter" => opts.delimiter = Some(value.to_string()),
|
||||
"--query" => opts.query = Some(value.to_string()),
|
||||
|
|
@ -91,12 +79,9 @@ fn parse_opts(text: &str) -> Result<FinderOpts, Error> {
|
|||
}
|
||||
|
||||
fn parse_variable_line(line: &str) -> Result<(&str, &str, Option<FinderOpts>), Error> {
|
||||
let caps = VAR_LINE_REGEX.captures(line).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"No variables, command, and options found in the line `{}`",
|
||||
line
|
||||
)
|
||||
})?;
|
||||
let caps = VAR_LINE_REGEX
|
||||
.captures(line)
|
||||
.ok_or_else(|| anyhow!("No variables, command, and options found in the line `{}`", line))?;
|
||||
let variable = caps
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow!("No variable captured in the line `{}`", line))?
|
||||
|
|
@ -114,13 +99,7 @@ fn parse_variable_line(line: &str) -> Result<(&str, &str, Option<FinderOpts>), E
|
|||
Ok((variable, command, command_options))
|
||||
}
|
||||
|
||||
fn write_cmd(
|
||||
tags: &str,
|
||||
comment: &str,
|
||||
snippet: &str,
|
||||
writer: &mut dyn Writer,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
) -> Result<(), Error> {
|
||||
fn write_cmd(tags: &str, comment: &str, snippet: &str, writer: &mut dyn Writer, stdin: &mut std::process::ChildStdin) -> Result<(), Error> {
|
||||
if snippet.len() <= 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -149,8 +128,7 @@ pub fn read_lines(
|
|||
let mut should_break = false;
|
||||
|
||||
for (line_nr, line_result) in lines.enumerate() {
|
||||
let line = line_result
|
||||
.with_context(|| format!("Failed to read line nr.{} from `{}`", line_nr, id))?;
|
||||
let line = line_result.with_context(|| format!("Failed to read line nr.{} from `{}`", line_nr, id))?;
|
||||
|
||||
if should_break {
|
||||
break;
|
||||
|
|
@ -167,19 +145,11 @@ pub fn read_lines(
|
|||
should_break = true
|
||||
}
|
||||
snippet = String::from("");
|
||||
tags = if line.len() > 2 {
|
||||
String::from(&line[2..])
|
||||
} else {
|
||||
String::from("")
|
||||
};
|
||||
tags = if line.len() > 2 { String::from(&line[2..]) } else { String::from("") };
|
||||
}
|
||||
// dependency
|
||||
else if line.starts_with('@') {
|
||||
let tags_dependency = if line.len() > 2 {
|
||||
String::from(&line[2..])
|
||||
} else {
|
||||
String::from("")
|
||||
};
|
||||
let tags_dependency = if line.len() > 2 { String::from(&line[2..]) } else { String::from("") };
|
||||
variables.insert_dependency(&tags, &tags_dependency);
|
||||
}
|
||||
// metacomment
|
||||
|
|
@ -191,11 +161,7 @@ pub fn read_lines(
|
|||
should_break = true
|
||||
}
|
||||
snippet = String::from("");
|
||||
comment = if line.len() > 2 {
|
||||
String::from(&line[2..])
|
||||
} else {
|
||||
String::from("")
|
||||
};
|
||||
comment = if line.len() > 2 { String::from(&line[2..]) } else { String::from("") };
|
||||
}
|
||||
// variable
|
||||
else if line.starts_with('$') {
|
||||
|
|
@ -203,13 +169,8 @@ pub fn read_lines(
|
|||
should_break = true
|
||||
}
|
||||
snippet = String::from("");
|
||||
let (variable, command, opts) = parse_variable_line(&line).with_context(|| {
|
||||
format!(
|
||||
"Failed to parse variable line. See line number {} in cheatsheet `{}`",
|
||||
line_nr + 1,
|
||||
id
|
||||
)
|
||||
})?;
|
||||
let (variable, command, opts) = parse_variable_line(&line)
|
||||
.with_context(|| format!("Failed to parse variable line. See line number {} in cheatsheet `{}`", line_nr + 1, id))?;
|
||||
variables.insert_suggestion(&tags, &variable, (String::from(command), opts));
|
||||
}
|
||||
// snippet
|
||||
|
|
@ -240,9 +201,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parse_variable_line() {
|
||||
let (variable, command, command_options) =
|
||||
parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --prevent-extra")
|
||||
.unwrap();
|
||||
let (variable, command, command_options) = parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --prevent-extra").unwrap();
|
||||
assert_eq!(command, " echo -e \"$(whoami)\\nroot\" ");
|
||||
assert_eq!(variable, "user");
|
||||
assert_eq!(
|
||||
|
|
|
|||
18
src/tldr.rs
18
src/tldr.rs
|
|
@ -57,12 +57,7 @@ fn markdown_lines(query: &str, markdown: &str) -> impl Iterator<Item = Result<St
|
|||
.into_iter()
|
||||
}
|
||||
|
||||
fn read_all(
|
||||
query: &str,
|
||||
markdown: &str,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
fn read_all(query: &str, markdown: &str, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result<Option<VariableMap>, Error> {
|
||||
let mut variables = VariableMap::new();
|
||||
let mut visited_lines = HashSet::new();
|
||||
parser::read_lines(
|
||||
|
|
@ -91,10 +86,7 @@ pub fn fetch(query: &str) -> Result<String, Error> {
|
|||
}
|
||||
};
|
||||
|
||||
let stdout = child
|
||||
.wait_with_output()
|
||||
.context("Failed to wait for tldr")?
|
||||
.stdout;
|
||||
let stdout = child.wait_with_output().context("Failed to wait for tldr")?.stdout;
|
||||
|
||||
String::from_utf8(stdout).context("Suggestions are invalid utf8")
|
||||
}
|
||||
|
|
@ -110,11 +102,7 @@ impl Foo {
|
|||
}
|
||||
|
||||
impl Fetcher for Foo {
|
||||
fn fetch(
|
||||
&self,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result<Option<VariableMap>, Error> {
|
||||
eprintln!("TODO!!!!");
|
||||
let markdown = fetch(&self.query)?;
|
||||
read_all(&self.query, &markdown, stdin, writer)
|
||||
|
|
|
|||
|
|
@ -2,21 +2,13 @@ use crate::display::Writer;
|
|||
use crate::structures::item::Item;
|
||||
use std::io::Write;
|
||||
|
||||
fn add_msg(
|
||||
tags: &str,
|
||||
comment: &str,
|
||||
snippet: &str,
|
||||
writer: &mut dyn Writer,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
) {
|
||||
fn add_msg(tags: &str, comment: &str, snippet: &str, writer: &mut dyn Writer, stdin: &mut std::process::ChildStdin) {
|
||||
let item = Item {
|
||||
tags: &tags,
|
||||
comment: &comment,
|
||||
snippet: &snippet,
|
||||
};
|
||||
stdin
|
||||
.write_all(writer.write(item).as_bytes())
|
||||
.expect("Could not write to fzf's stdin");
|
||||
stdin.write_all(writer.write(item).as_bytes()).expect("Could not write to fzf's stdin");
|
||||
}
|
||||
|
||||
pub fn populate_cheatsheet(writer: &mut dyn Writer, stdin: &mut std::process::ChildStdin) {
|
||||
|
|
@ -27,18 +19,6 @@ pub fn populate_cheatsheet(writer: &mut dyn Writer, stdin: &mut std::process::Ch
|
|||
writer,
|
||||
stdin,
|
||||
);
|
||||
add_msg(
|
||||
"cheatsheets",
|
||||
"Browse for cheatsheet repos",
|
||||
"navi repo browse",
|
||||
writer,
|
||||
stdin,
|
||||
);
|
||||
add_msg(
|
||||
"more info",
|
||||
"Read --help message",
|
||||
"navi --help",
|
||||
writer,
|
||||
stdin,
|
||||
);
|
||||
add_msg("cheatsheets", "Browse for cheatsheet repos", "navi repo browse", writer, stdin);
|
||||
add_msg("more info", "Read --help message", "navi --help", writer, stdin);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue