use std::collections::HashMap;
use std::io::{ErrorKind, Write};
use std::path::Path;
use std::{fs, io, process};
use color_eyre::Help;
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, Select};
use structopt::clap::Shell;
use structopt::StructOpt;
use crate::configuration::{ConfigCommand, TheWayConfig};
use crate::errors::LostTheWay;
use crate::language::{CodeHighlight, Language};
use crate::the_way::{
cli::{TheWayCLI, ThemeCommand},
filter::Filters,
snippet::Snippet,
};
use crate::utils;
pub mod cli;
mod database;
mod filter;
mod gist;
mod search;
pub mod snippet;
pub struct TheWay {
config: TheWayConfig,
db: sled::Db,
languages: HashMap<String, Language>,
highlighter: CodeHighlight,
}
impl TheWay {
pub fn start(cli: TheWayCLI, languages: HashMap<String, Language>) -> color_eyre::Result<()> {
if let TheWayCLI::Config {
cmd: ConfigCommand::Default { file },
} = &cli
{
TheWayConfig::default_config(file.as_deref())?;
return Ok(());
}
let config = TheWayConfig::load()?;
let mut the_way = Self {
db: Self::get_db(&config.db_dir)?,
languages,
highlighter: CodeHighlight::new(&config.theme, config.themes_dir.clone())?,
config,
};
the_way.set_merge()?;
the_way.run(cli)?;
Ok(())
}
fn run(&mut self, cli: TheWayCLI) -> color_eyre::Result<()> {
match cli {
TheWayCLI::New => self.the_way(),
TheWayCLI::Cmd { code } => self.the_way_cmd(code),
TheWayCLI::Search {
filters,
stdout,
exact,
} => self.search(&filters, stdout, exact),
TheWayCLI::Cp { index, stdout } => self.copy(index, stdout),
TheWayCLI::Edit { index } => self.edit(index),
TheWayCLI::Del { index, force } => self.delete(index, force),
TheWayCLI::View { index } => self.view(index),
TheWayCLI::List { filters } => self.list(&filters),
TheWayCLI::Import { file, gist_url } => self.import(file.as_deref(), gist_url),
TheWayCLI::Export { filters, file } => self.export(&filters, file.as_deref()),
TheWayCLI::Complete { shell } => {
Self::complete(shell);
Ok(())
}
TheWayCLI::Themes { cmd } => self.themes(cmd),
TheWayCLI::Clear { force } => self.clear(force),
TheWayCLI::Config { cmd } => match cmd {
ConfigCommand::Default { file } => TheWayConfig::default_config(file.as_deref()), ConfigCommand::Get => TheWayConfig::print_config_location(),
},
TheWayCLI::Sync => self.sync(),
}
}
fn the_way(&mut self) -> color_eyre::Result<()> {
let snippet =
Snippet::from_user(self.get_current_snippet_index()? + 1, &self.languages, None)?;
let index = self.add_snippet(&snippet)?;
println!(
"{}",
self.highlight_string(&format!("Snippet #{} added", index))
);
self.increment_snippet_index()?;
Ok(())
}
fn the_way_cmd(&mut self, code: Option<String>) -> color_eyre::Result<()> {
let snippet =
Snippet::cmd_from_user(self.get_current_snippet_index()? + 1, code.as_deref())?;
let index = self.add_snippet(&snippet)?;
println!(
"{}",
self.highlight_string(&format!("Snippet #{} added", index))
);
self.increment_snippet_index()?;
Ok(())
}
fn delete(&mut self, index: usize, force: bool) -> color_eyre::Result<()> {
if force
|| Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(&format!("Delete snippet #{}?", index))
.default(false)
.interact()?
{
self.delete_snippet(index)?;
println!(
"{}",
self.highlight_string(&format!("Snippet #{} deleted", index))
);
Ok(())
} else {
let error: color_eyre::Result<()> = Err(LostTheWay::DoingNothing.into());
error.suggestion("Press Y next time!")
}
}
fn edit(&mut self, index: usize) -> color_eyre::Result<()> {
let old_snippet = self.get_snippet(index)?;
let new_snippet = Snippet::from_user(index, &self.languages, Some(&old_snippet))?;
self.delete_snippet(index)?;
self.add_snippet(&new_snippet)?;
println!(
"{}",
self.highlight_string(&format!("Snippet #{} changed", index))
);
Ok(())
}
fn view(&self, index: usize) -> color_eyre::Result<()> {
let snippet = self.get_snippet(index)?;
print!(
"{}",
utils::highlight_strings(
&snippet.pretty_print(
&self.highlighter,
self.languages
.get(&snippet.language)
.unwrap_or(&Language::default()),
),
false
)
);
Ok(())
}
fn copy(&self, index: usize, to_stdout: bool) -> color_eyre::Result<()> {
let snippet = self.get_snippet(index)?;
let code = snippet.fill_snippet(self.highlighter.selection_style)?;
if to_stdout {
let mut stdout = std::io::stdout();
if let Err(e) = writeln!(stdout, "{}", code) {
if e.kind() != ErrorKind::BrokenPipe {
eprintln!("{}", e);
process::exit(1);
}
}
} else {
utils::copy_to_clipboard(&code)?;
eprintln!(
"{}",
self.highlight_string(&format!("Snippet #{} copied to clipboard", index))
);
}
Ok(())
}
fn import(&mut self, file: Option<&Path>, gist_url: Option<String>) -> color_eyre::Result<()> {
let mut num = 0;
if let Some(gist_url) = gist_url {
let snippets = self.import_gist(&gist_url)?;
num = snippets.len();
} else {
for mut snippet in self.import_file(file)? {
snippet.index = self.get_current_snippet_index()? + 1;
self.add_snippet(&snippet)?;
self.increment_snippet_index()?;
num += 1;
}
}
println!(
"{}",
self.highlight_string(&format!("Imported {} snippets", num))
);
Ok(())
}
fn import_file(&self, file: Option<&Path>) -> color_eyre::Result<Vec<Snippet>> {
let reader: Box<dyn io::Read> = match file {
Some(file) => Box::new(fs::File::open(file)?),
None => Box::new(io::stdin()),
};
let mut buffered = io::BufReader::new(reader);
let mut snippets = Snippet::read(&mut buffered).collect::<Result<Vec<_>, _>>()?;
for snippet in &mut snippets {
snippet.set_extension(&snippet.language.to_owned(), &self.languages);
}
Ok(snippets)
}
fn export(&self, filters: &Filters, file: Option<&Path>) -> color_eyre::Result<()> {
let writer: Box<dyn io::Write> = match file {
Some(file) => Box::new(fs::File::create(file)?),
None => Box::new(io::stdout()),
};
let mut buffered = io::BufWriter::new(writer);
for snippet in self.filter_snippets(filters)? {
snippet.to_json(&mut buffered)?;
buffered.write_all(b"\n")?;
}
Ok(())
}
fn show_snippets(&self, snippets: &[Snippet]) {
let mut colorized = Vec::new();
let default_language = Language::default();
for snippet in snippets {
colorized.extend_from_slice(
&snippet.pretty_print(
&self.highlighter,
self.languages
.get(&snippet.language)
.unwrap_or(&default_language),
),
);
}
print!("{}", utils::highlight_strings(&colorized, false));
}
fn list(&self, filters: &Filters) -> color_eyre::Result<()> {
let mut snippets = self.filter_snippets(filters)?;
snippets.sort_by(|a, b| a.index.cmp(&b.index));
self.show_snippets(&snippets);
Ok(())
}
fn search(&mut self, filters: &Filters, stdout: bool, exact: bool) -> color_eyre::Result<()> {
let mut snippets = self.filter_snippets(filters)?;
snippets.sort_by(|a, b| a.index.cmp(&b.index));
self.make_search(
snippets,
self.highlighter.skim_theme.to_owned(),
self.highlighter.selection_style,
stdout,
exact,
)?;
Ok(())
}
fn complete(shell: Shell) {
TheWayCLI::clap().gen_completions_to(utils::NAME, shell, &mut io::stdout());
}
fn clear(&self, force: bool) -> color_eyre::Result<()> {
if force
|| Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Clear all data?")
.default(false)
.interact()?
{
for path in fs::read_dir(&self.config.db_dir)? {
let path = path?.path();
if path.is_dir() {
fs::remove_dir_all(path)?;
} else {
fs::remove_file(path)?;
}
}
self.reset_index()?;
println!("{}", self.highlight_string("Data cleared."));
Ok(())
} else {
let error: color_eyre::Result<()> = Err(LostTheWay::DoingNothing.into());
error.suggestion("Press Y next time!")
}
}
fn sync(&mut self) -> color_eyre::Result<()> {
self.config.github_access_token = std::env::var("THE_WAY_GITHUB_TOKEN")
.ok()
.or_else(|| self.config.github_access_token.clone());
if self.config.github_access_token.is_none() {
println!(
"{}",
self.highlight_string("Get a GitHub access token from https://github.com/settings/tokens/new (add the \"gist\" scope)\n",
)
);
self.config.github_access_token = Some(
dialoguer::Password::with_theme(&ColorfulTheme::default())
.with_prompt("GitHub access token")
.interact()?,
);
}
if self.config.gist_id.is_some() {
self.sync_gist()?;
} else {
self.config.gist_id =
Some(self.make_gist(self.config.github_access_token.as_ref().unwrap())?);
}
self.config.store()?;
Ok(())
}
fn themes(&mut self, cmd: ThemeCommand) -> color_eyre::Result<()> {
match cmd {
ThemeCommand::Set { theme } => {
let theme = match theme {
Some(theme) => theme,
None => {
let themes = self.highlighter.get_themes();
let theme_index =
Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
.with_prompt("Choose a syntax highlighting theme:")
.items(&themes[..])
.interact()?;
themes[theme_index].to_owned()
}
};
self.highlighter.set_theme(theme.to_owned())?;
println!(
"{}",
self.highlight_string(&format!("Theme changed to {}", theme))
);
self.config.theme = theme;
self.config.store()?;
Ok(())
}
ThemeCommand::Add { file } => {
let theme = self.highlighter.add_theme(&file)?;
println!(
"{}",
self.highlight_string(&format!("Added theme {}", theme))
);
Ok(())
}
ThemeCommand::Language { file } => {
let language = self.highlighter.add_syntax(&file)?;
println!(
"{}",
self.highlight_string(&format!("Added {} syntax", language))
);
Ok(())
}
ThemeCommand::Get => {
println!(
"{}",
self.highlight_string(&format!(
"Current theme: {}",
self.highlighter.get_theme_name()
))
);
Ok(())
}
}
}
pub(crate) fn highlight_string(&self, input: &str) -> String {
utils::highlight_string(input, self.highlighter.main_style)
}
}