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",
"directories",
"futures",
"fuzzy-matcher",
"indicatif",
"lazy-regex",
"lazy_static",
@ -307,6 +308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1"
dependencies = [
"console",
"fuzzy-matcher",
"tempfile",
"zeroize",
]
@ -491,6 +493,15 @@ dependencies = [
"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]]
name = "getrandom"
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"] }
lazy_static = "1.4.0"
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"
directories = "4.0.1"
console = "0.15.1"
tracing-error = "0.2.0"
fuzzy-matcher = "0.3.7"
[dependencies.tokio]
version = "1.21.0"

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

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

@ -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]
/// Returns a singular or plural expression depending on the given len
/// Usage:
@ -82,16 +89,35 @@ macro_rules! normal_output {
#[macro_export]
/// 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]
/// 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;
mod multi_select;
mod prompt;
mod select;
mod theme;
pub use multi_select::AmeMultiSelect;
pub use prompt::AmePrompt;
pub use select::AmeFuzzySelect;
pub trait Interact {
type 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 crate::logging::get_logger;
use crate::with_suspended_output;
use super::{theme::AmeTheme, Interact};
@ -30,14 +30,12 @@ impl Interact for AmeMultiSelect {
type Result = Vec<usize>;
fn interact(&mut self) -> Self::Result {
get_logger().suspend();
let selection = dialoguer::MultiSelect::with_theme(AmeTheme::get())
.with_prompt(mem::take(&mut self.prompt))
.items(&self.items)
.interact()
.unwrap();
get_logger().unsuspend();
selection
with_suspended_output!({
dialoguer::MultiSelect::with_theme(AmeTheme::get())
.with_prompt(mem::take(&mut self.prompt))
.items(&self.items)
.interact()
.unwrap()
})
}
}

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

@ -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 dialoguer::theme::Theme;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use crate::internal::utils::wrap_text;
const ERR_SYMBOL: &str = "X";
const PROMPT_SYMBOL: &str = "?";
@ -208,4 +208,54 @@ impl Theme for AmeTheme {
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 },
UserCancellation,
MissingDependencies(Vec<String>),
MakePkg(String),
}
impl Display for AppError {
@ -33,6 +34,7 @@ impl Display for AppError {
AppError::MissingDependencies(deps) => {
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 textwrap::wrap;
use crate::internal::exit_code::AppExitCode;
use crate::{internal::exit_code::AppExitCode, logging::get_logger};
use lazy_static::lazy_static;
use super::error::{AppError, SilentUnwrap};
@ -31,7 +31,9 @@ macro_rules! cancelled {
/// Logs a message and exits the program with the given exit code.
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);
}

@ -3,6 +3,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
use parking_lot::{Mutex, RwLock};
use std::{
fmt::Display,
io::{self, Write},
mem,
sync::{atomic::AtomicBool, Arc},
time::Duration,
@ -10,7 +11,7 @@ use std::{
use crate::{internal::utils::wrap_text, uwu};
use super::Verbosity;
use super::{get_logger, Verbosity};
const OK_SYMBOL: &str = "❖";
const ERR_SYMBOL: &str = "X";
@ -46,6 +47,8 @@ pub enum OutputType {
},
}
pub struct SuspendHandle;
impl LogHandler {
pub fn log_error(&self, msg: String) {
if self.is_loggable(Verbosity::Error) {
@ -101,7 +104,7 @@ impl LogHandler {
}
pub fn print_newline(&self) {
self.log(String::from("\n"))
self.log(String::from(""))
}
pub fn set_verbosity(&self, level: Verbosity) {
@ -112,7 +115,8 @@ impl LogHandler {
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 old_output_type = OutputType::Stdout;
mem::swap(&mut *output_type, &mut old_output_type);
@ -120,7 +124,9 @@ impl LogHandler {
(*output_type) = OutputType::Buffer {
buffer: Arc::new(Mutex::new(Vec::new())),
suspended: Box::new(old_output_type),
}
};
SuspendHandle
}
pub fn unsuspend(&self) {
@ -194,6 +200,17 @@ impl LogHandler {
(*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 {
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::{
builder::pager::PagerBuilder,
internal::{
@ -6,7 +8,7 @@ use crate::{
structs::Options,
utils::get_cache_dir,
},
multi_select, prompt,
multi_select, newline, prompt, select_opt,
};
use super::{repo_dependency_installation::RepoDependencyInstallation, BuildContext};
@ -25,8 +27,7 @@ impl AurReview {
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)) {
let pkgbuild_path = get_cache_dir().join(pkg).join("PKGBUILD");
PagerBuilder::default().path(pkgbuild_path).open().await?;
self.review_single_package(pkg).await?;
}
if !prompt!(default yes, "Do you still want to install those packages?") {
return Err(AppError::UserCancellation);
@ -38,4 +39,39 @@ impl AurReview {
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(", ")
)
}
AppError::MakePkg(msg) => {
crash!(AppExitCode::MakePkgError, "makepgk failed {msg}")
}
_ => crash!(AppExitCode::Other, "Unknown error"),
}
}

Loading…
Cancel
Save