Add fuzzy file review selector

Signed-off-by: trivernis <trivernis@protonmail.com>
i18n
trivernis 2 years ago
parent 847f36ea40
commit 1ad98ca780
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

11
Cargo.lock generated

@ -17,6 +17,7 @@ dependencies = [
"dialoguer", "dialoguer",
"directories", "directories",
"futures", "futures",
"fuzzy-matcher",
"indicatif", "indicatif",
"lazy-regex", "lazy-regex",
"lazy_static", "lazy_static",
@ -307,6 +308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1"
dependencies = [ dependencies = [
"console", "console",
"fuzzy-matcher",
"tempfile", "tempfile",
"zeroize", "zeroize",
] ]
@ -491,6 +493,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.7" version = "0.2.7"

@ -44,11 +44,12 @@ color-eyre = { version = "0.6.2", features = ["issue-url", "url"] }
indicatif = { version = "0.17.0", features = ["tokio"] } indicatif = { version = "0.17.0", features = ["tokio"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
parking_lot = { version = "0.12.1", features = ["deadlock_detection"] } parking_lot = { version = "0.12.1", features = ["deadlock_detection"] }
dialoguer = "0.10.2" dialoguer = { version = "0.10.2", features = ["fuzzy-select"] }
lazy-regex = "2.3.0" lazy-regex = "2.3.0"
directories = "4.0.1" directories = "4.0.1"
console = "0.15.1" console = "0.15.1"
tracing-error = "0.2.0" tracing-error = "0.2.0"
fuzzy-matcher = "0.3.7"
[dependencies.tokio] [dependencies.tokio]
version = "1.21.0" version = "1.21.0"

@ -89,7 +89,7 @@ impl MakePkgBuilder {
if output.status.success() { if output.status.success() {
Ok(()) Ok(())
} else { } else {
Err(AppError::Other(output.stderr)) Err(AppError::MakePkg(output.stderr))
} }
} }

@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use crate::{ use crate::{
internal::{commands::ShellCommand, error::AppResult}, internal::{commands::ShellCommand, error::AppResult},
logging::get_logger, with_suspended_output,
}; };
#[derive(Default)] #[derive(Default)]
@ -18,10 +18,6 @@ impl PagerBuilder {
} }
pub async fn open(self) -> AppResult<()> { pub async fn open(self) -> AppResult<()> {
get_logger().suspend(); with_suspended_output!({ ShellCommand::pager().arg(self.path).wait_success().await })
ShellCommand::pager().arg(self.path).wait_success().await?;
get_logger().unsuspend();
Ok(())
} }
} }

@ -20,6 +20,13 @@ macro_rules! multi_select {
} }
} }
#[macro_export]
macro_rules! select_opt {
($items:expr, $($arg:tt)+) => {
$crate::interact::InteractOpt::interact_opt($crate::interact::AmeFuzzySelect::new(format!($($arg)+)).items($items))
};
}
#[macro_export] #[macro_export]
/// Returns a singular or plural expression depending on the given len /// Returns a singular or plural expression depending on the given len
/// Usage: /// Usage:
@ -82,16 +89,35 @@ macro_rules! normal_output {
#[macro_export] #[macro_export]
/// Suspends the output so that nothing is being written to stdout/stderr /// Suspends the output so that nothing is being written to stdout/stderr
macro_rules! supend_output { /// Returns a handle that unsuspend the output when it's dropped
macro_rules! suspend_output {
() => { () => {
$crate::loggign::get_logger().suspend(); $crate::logging::get_logger().suspend()
}; };
} }
#[macro_export] #[macro_export]
/// Unsuspends the output and writes everything buffered to stdout/stderr /// Unsuspends the output and writes everything buffered to stdout/stderr
macro_rules! unsupend_output { macro_rules! unsuspend_output {
() => {
$crate::logging::get_logger().unsuspend();
};
}
#[macro_export]
/// Suspend all output logging inside the given block
/// Note: This only works as long as the block itself doesn't unsuspend
/// the output
macro_rules! with_suspended_output {
($expr:block) => {{
let _handle = $crate::suspend_output!();
$expr
}};
}
#[macro_export]
macro_rules! newline {
() => { () => {
$crate::loggign::get_logger().unsuspend(); $crate::logging::get_logger().print_newline();
}; };
} }

@ -1,13 +1,19 @@
pub mod macros; pub mod macros;
mod multi_select; mod multi_select;
mod prompt; mod prompt;
mod select;
mod theme; mod theme;
pub use multi_select::AmeMultiSelect; pub use multi_select::AmeMultiSelect;
pub use prompt::AmePrompt; pub use prompt::AmePrompt;
pub use select::AmeFuzzySelect;
pub trait Interact { pub trait Interact {
type Result; type Result;
fn interact(&mut self) -> Self::Result; fn interact(&mut self) -> Self::Result;
} }
pub trait InteractOpt: Interact {
fn interact_opt(&mut self) -> Option<Self::Result>;
}

@ -1,6 +1,6 @@
use std::mem; use std::mem;
use crate::logging::get_logger; use crate::with_suspended_output;
use super::{theme::AmeTheme, Interact}; use super::{theme::AmeTheme, Interact};
@ -30,14 +30,12 @@ impl Interact for AmeMultiSelect {
type Result = Vec<usize>; type Result = Vec<usize>;
fn interact(&mut self) -> Self::Result { fn interact(&mut self) -> Self::Result {
get_logger().suspend(); with_suspended_output!({
let selection = dialoguer::MultiSelect::with_theme(AmeTheme::get()) dialoguer::MultiSelect::with_theme(AmeTheme::get())
.with_prompt(mem::take(&mut self.prompt)) .with_prompt(mem::take(&mut self.prompt))
.items(&self.items) .items(&self.items)
.interact() .interact()
.unwrap(); .unwrap()
get_logger().unsuspend(); })
selection
} }
} }

@ -1,6 +1,6 @@
use std::mem; use std::mem;
use crate::logging::get_logger; use crate::with_suspended_output;
use super::{theme::AmeTheme, Interact}; use super::{theme::AmeTheme, Interact};
@ -46,10 +46,6 @@ impl Interact for AmePrompt {
dialog dialog
.with_prompt(mem::take(&mut self.question)) .with_prompt(mem::take(&mut self.question))
.wait_for_newline(true); .wait_for_newline(true);
get_logger().suspend(); with_suspended_output!({ dialog.interact().unwrap() })
let result = dialog.interact().unwrap();
get_logger().unsuspend();
result
} }
} }

@ -0,0 +1,51 @@
use std::mem;
use crate::with_suspended_output;
use super::{theme::AmeTheme, Interact, InteractOpt};
pub struct AmeFuzzySelect {
prompt: String,
items: Vec<String>,
}
impl AmeFuzzySelect {
/// Creates a new multi select prompt
pub fn new<S: ToString>(prompt: S) -> Self {
Self {
prompt: prompt.to_string(),
items: Vec::new(),
}
}
/// Adds/replaces the items of this multi select
pub fn items<I: IntoIterator<Item = S>, S: ToString>(&mut self, items: I) -> &mut Self {
self.items = items.into_iter().map(|i| i.to_string()).collect();
self
}
fn build(&mut self) -> dialoguer::FuzzySelect {
let mut select = dialoguer::FuzzySelect::with_theme(AmeTheme::get());
select
.with_prompt(mem::take(&mut self.prompt))
.items(&self.items)
.default(0);
select
}
}
impl Interact for AmeFuzzySelect {
type Result = usize;
fn interact(&mut self) -> Self::Result {
with_suspended_output!({ self.build().interact().unwrap() })
}
}
impl InteractOpt for AmeFuzzySelect {
fn interact_opt(&mut self) -> Option<Self::Result> {
with_suspended_output!({ self.build().interact_opt().unwrap() })
}
}

@ -1,8 +1,8 @@
use crossterm::style::Stylize; use crossterm::style::Stylize;
use dialoguer::theme::Theme; use dialoguer::theme::Theme;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use crate::internal::utils::wrap_text; use crate::internal::utils::wrap_text;
const ERR_SYMBOL: &str = "X"; const ERR_SYMBOL: &str = "X";
const PROMPT_SYMBOL: &str = "?"; const PROMPT_SYMBOL: &str = "?";
@ -208,4 +208,54 @@ impl Theme for AmeTheme {
text text
) )
} }
fn format_fuzzy_select_prompt(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
search_term: &str,
cursor_pos: usize,
) -> std::fmt::Result {
if !prompt.is_empty() {
write!(f, "{} {} ", PROMPT_SYMBOL.magenta(), prompt.bold())?;
}
if cursor_pos < search_term.len() {
let st_head = search_term[0..cursor_pos].to_string();
let st_tail = search_term[cursor_pos..search_term.len()].to_string();
let st_cursor = "|".to_string();
write!(f, "{}{}{}", st_head, st_cursor, st_tail)
} else {
let cursor = "|".to_string();
write!(f, "{}{}", search_term, cursor)
}
}
fn format_fuzzy_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
active: bool,
highlight_matches: bool,
matcher: &SkimMatcherV2,
search_term: &str,
) -> std::fmt::Result {
write!(f, "{} ", if active { ">" } else { " " }.magenta().bold())?;
if highlight_matches {
if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) {
for (idx, c) in text.chars().into_iter().enumerate() {
if indices.contains(&idx) {
write!(f, "{}", c.bold())?;
} else {
write!(f, "{}", c)?;
}
}
return Ok(());
}
}
write!(f, "{}", text)
}
} }

@ -18,6 +18,7 @@ pub enum AppError {
BuildError { pkg_name: String }, BuildError { pkg_name: String },
UserCancellation, UserCancellation,
MissingDependencies(Vec<String>), MissingDependencies(Vec<String>),
MakePkg(String),
} }
impl Display for AppError { impl Display for AppError {
@ -33,6 +34,7 @@ impl Display for AppError {
AppError::MissingDependencies(deps) => { AppError::MissingDependencies(deps) => {
write!(f, "Missing dependencies {}", deps.join(", ")) write!(f, "Missing dependencies {}", deps.join(", "))
} }
AppError::MakePkg(msg) => write!(f, "Failed to ru makepkg {msg}"),
} }
} }
} }

@ -5,7 +5,7 @@ use std::process::exit;
use directories::ProjectDirs; use directories::ProjectDirs;
use textwrap::wrap; use textwrap::wrap;
use crate::internal::exit_code::AppExitCode; use crate::{internal::exit_code::AppExitCode, logging::get_logger};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use super::error::{AppError, SilentUnwrap}; use super::error::{AppError, SilentUnwrap};
@ -31,7 +31,9 @@ macro_rules! cancelled {
/// Logs a message and exits the program with the given exit code. /// Logs a message and exits the program with the given exit code.
pub fn log_and_crash(msg: String, exit_code: AppExitCode) -> ! { pub fn log_and_crash(msg: String, exit_code: AppExitCode) -> ! {
tracing::error!(msg); get_logger().reset_output_type();
get_logger().log_error(msg);
get_logger().flush();
exit(exit_code as i32); exit(exit_code as i32);
} }

@ -3,6 +3,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use std::{ use std::{
fmt::Display, fmt::Display,
io::{self, Write},
mem, mem,
sync::{atomic::AtomicBool, Arc}, sync::{atomic::AtomicBool, Arc},
time::Duration, time::Duration,
@ -10,7 +11,7 @@ use std::{
use crate::{internal::utils::wrap_text, uwu}; use crate::{internal::utils::wrap_text, uwu};
use super::Verbosity; use super::{get_logger, Verbosity};
const OK_SYMBOL: &str = "❖"; const OK_SYMBOL: &str = "❖";
const ERR_SYMBOL: &str = "X"; const ERR_SYMBOL: &str = "X";
@ -46,6 +47,8 @@ pub enum OutputType {
}, },
} }
pub struct SuspendHandle;
impl LogHandler { impl LogHandler {
pub fn log_error(&self, msg: String) { pub fn log_error(&self, msg: String) {
if self.is_loggable(Verbosity::Error) { if self.is_loggable(Verbosity::Error) {
@ -101,7 +104,7 @@ impl LogHandler {
} }
pub fn print_newline(&self) { pub fn print_newline(&self) {
self.log(String::from("\n")) self.log(String::from(""))
} }
pub fn set_verbosity(&self, level: Verbosity) { pub fn set_verbosity(&self, level: Verbosity) {
@ -112,7 +115,8 @@ impl LogHandler {
self.set_output_type(OutputType::Stdout); self.set_output_type(OutputType::Stdout);
} }
pub fn suspend(&self) { #[must_use]
pub fn suspend(&self) -> SuspendHandle {
let mut output_type = self.output_type.write(); let mut output_type = self.output_type.write();
let mut old_output_type = OutputType::Stdout; let mut old_output_type = OutputType::Stdout;
mem::swap(&mut *output_type, &mut old_output_type); mem::swap(&mut *output_type, &mut old_output_type);
@ -120,7 +124,9 @@ impl LogHandler {
(*output_type) = OutputType::Buffer { (*output_type) = OutputType::Buffer {
buffer: Arc::new(Mutex::new(Vec::new())), buffer: Arc::new(Mutex::new(Vec::new())),
suspended: Box::new(old_output_type), suspended: Box::new(old_output_type),
} };
SuspendHandle
} }
pub fn unsuspend(&self) { pub fn unsuspend(&self) {
@ -194,6 +200,17 @@ impl LogHandler {
(*self.level.read()) >= level (*self.level.read()) >= level
} }
/// Flushes the output buffer
pub fn flush(&self) {
let output = self.output_type.read();
match &*output {
OutputType::Stdout => io::stdout().flush().unwrap(),
OutputType::Stderr => io::stderr().flush().unwrap(),
OutputType::Progress(p) => p.tick(),
_ => {}
}
}
fn preformat_msg(&self, msg: String) -> String { fn preformat_msg(&self, msg: String) -> String {
let msg = self.apply_uwu(msg); let msg = self.apply_uwu(msg);
@ -224,3 +241,9 @@ impl LogHandler {
}; };
} }
} }
impl Drop for SuspendHandle {
fn drop(&mut self) {
get_logger().unsuspend();
}
}

@ -1,3 +1,5 @@
use tokio::fs;
use crate::{ use crate::{
builder::pager::PagerBuilder, builder::pager::PagerBuilder,
internal::{ internal::{
@ -6,7 +8,7 @@ use crate::{
structs::Options, structs::Options,
utils::get_cache_dir, utils::get_cache_dir,
}, },
multi_select, prompt, multi_select, newline, prompt, select_opt,
}; };
use super::{repo_dependency_installation::RepoDependencyInstallation, BuildContext}; use super::{repo_dependency_installation::RepoDependencyInstallation, BuildContext};
@ -25,8 +27,7 @@ impl AurReview {
let to_review = multi_select!(&self.packages, "Select packages to review"); let to_review = multi_select!(&self.packages, "Select packages to review");
for pkg in to_review.into_iter().filter_map(|i| self.packages.get(i)) { for pkg in to_review.into_iter().filter_map(|i| self.packages.get(i)) {
let pkgbuild_path = get_cache_dir().join(pkg).join("PKGBUILD"); self.review_single_package(pkg).await?;
PagerBuilder::default().path(pkgbuild_path).open().await?;
} }
if !prompt!(default yes, "Do you still want to install those packages?") { if !prompt!(default yes, "Do you still want to install those packages?") {
return Err(AppError::UserCancellation); return Err(AppError::UserCancellation);
@ -38,4 +39,39 @@ impl AurReview {
contexts: self.contexts, contexts: self.contexts,
}) })
} }
async fn review_single_package(&self, pkg: &str) -> AppResult<()> {
newline!();
tracing::info!("Reviewing {pkg}");
let mut files_iter = fs::read_dir(get_cache_dir().join(pkg)).await?;
let mut files = Vec::new();
while let Some(file) = files_iter.next_entry().await? {
let path = file.path();
if path.is_file() {
files.push(file.path());
}
}
let file_names = files
.iter()
.map(|f| f.file_name().unwrap())
.map(|f| f.to_string_lossy())
.collect::<Vec<_>>();
while let Some(selection) = select_opt!(&file_names, "Select a file to review") {
if let Some(path) = files.get(selection) {
if let Err(e) = PagerBuilder::default().path(path).open().await {
tracing::debug!("Pager error {e}");
}
} else {
break;
}
}
tracing::info!("Done reviewing {pkg}");
Ok(())
}
} }

@ -114,6 +114,9 @@ pub async fn aur_install(packages: Vec<String>, options: Options) {
deps.join(", ") deps.join(", ")
) )
} }
AppError::MakePkg(msg) => {
crash!(AppExitCode::MakePkgError, "makepgk failed {msg}")
}
_ => crash!(AppExitCode::Other, "Unknown error"), _ => crash!(AppExitCode::Other, "Unknown error"),
} }
} }

Loading…
Cancel
Save