Adds install/uninstall add/remove

This commit is contained in:
Tareq Imbasher 2025-03-26 18:16:29 +03:00
parent 66131a0eca
commit 25df130d42
20 changed files with 433 additions and 143 deletions

3
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -504,6 +504,7 @@ name = "crate-seek"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"better-panic",
"chrono",
"clap",

View File

@ -53,6 +53,7 @@ tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
tui-input = "0.10.1"
strum_macros = "0.26.4"
async-trait = "0.1"
[build-dependencies]
anyhow = "1.0.86"

View File

@ -26,6 +26,7 @@ pub enum Action {
UpdateStatusWithDuration(StatusLevel, StatusDuration, String),
RefreshCargoEnv,
CargoEnvRefreshed,
Search(SearchAction),
Cargo(CargoAction),
@ -62,8 +63,10 @@ pub enum SearchAction {
pub enum CargoAction {
Add(String, String),
Remove(String),
Update(String),
UpdateAll,
// Update(String),
// UpdateAll,
Install(String, String),
Uninstall(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]

View File

@ -1,13 +1,14 @@
use std::sync::Arc;
use crossterm::event::KeyEvent;
use ratatui::layout::{Constraint, Layout, Rect};
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{debug, info};
use crate::cargo::CargoEnv;
use crate::errors::AppResult;
use crate::action::CargoAction;
use crate::cargo::{add, install, remove, uninstall, CargoEnv};
use crate::components::status_bar::StatusLevel;
use crate::errors::{AppError, AppResult};
use crate::{
action::Action,
components::{
@ -19,7 +20,7 @@ use crate::{
};
pub struct App {
cargo_env: Arc<Mutex<CargoEnv>>,
cargo_env: Arc<RwLock<CargoEnv>>,
config: Config,
tick_rate: f64,
frame_rate: f64,
@ -45,7 +46,7 @@ impl App {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let root = std::env::current_dir().ok();
let cargo_env = Arc::new(Mutex::new(CargoEnv::new(root)));
let cargo_env = Arc::new(RwLock::new(CargoEnv::new(root)));
let mut components: Vec<Box<dyn Component>> = vec![
Box::new(Home::new(Arc::clone(&cargo_env), action_tx.clone())?),
@ -80,7 +81,7 @@ impl App {
.frame_rate(self.frame_rate);
tui.enter()?;
self.cargo_env.lock().await.read().ok();
self.cargo_env.write().await.read().ok();
for component in self.components.iter_mut() {
component.register_config_handler(self.config.clone())?;
@ -92,7 +93,7 @@ impl App {
let action_tx = self.action_tx.clone();
loop {
self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui)?;
self.handle_actions(&mut tui).await?;
if self.should_suspend {
tui.suspend()?;
action_tx.send(Action::Resume)?;
@ -156,7 +157,7 @@ impl App {
Ok(())
}
fn handle_actions(&mut self, tui: &mut Tui) -> AppResult<()> {
async fn handle_actions(&mut self, tui: &mut Tui) -> AppResult<()> {
while let Ok(action) = self.action_rx.try_recv() {
if action != Action::Tick && action != Action::Render {
debug!("{action:?}");
@ -181,11 +182,136 @@ impl App {
Mode::Settings
};
}
Action::Cargo(action) => {
return match action {
CargoAction::Add(crate_name, version) => {
self.action_tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("adding {} v{}", crate_name, version),
))?;
tui.exit()?;
let tx = self.action_tx.clone();
tokio::spawn(async move {
if let Err(_) = add(crate_name.clone(), Some(version.clone()), true)
{
tx.send(Action::UpdateStatus(
StatusLevel::Error,
format!("failed to add {crate_name}"),
))?;
// TODO should user full error message (in a popup maybe)
return Ok(());
}
tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("added {crate_name} v{version}"),
))?;
tx.send(Action::RefreshCargoEnv)?;
Ok::<(), AppError>(())
})
.await??;
tui.enter()?;
tui.terminal.clear()?;
Ok(())
}
CargoAction::Remove(crate_name) => {
self.action_tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("removing {}", crate_name),
))?;
let tx = self.action_tx.clone();
tokio::spawn(async move {
if let Err(_) = remove(crate_name.clone(), false) {
tx.send(Action::UpdateStatus(
StatusLevel::Error,
format!("failed to remove {crate_name}"),
))?;
// TODO should user full error message (in a popup maybe)
return Ok(());
}
tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("removed {crate_name}"),
))?;
tx.send(Action::RefreshCargoEnv)?;
Ok::<(), AppError>(())
});
Ok(())
}
// CargoAction::Update(crate_name) => {
// let _ = crate_name;
// Ok(Some(Action::RefreshCargoEnv))
// }
// CargoAction::UpdateAll => Ok(Some(Action::RefreshCargoEnv)),
CargoAction::Install(crate_name, version) => {
self.action_tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("installing {crate_name} v{version}"),
))?;
tui.exit()?;
let tx = self.action_tx.clone();
tokio::spawn(async move {
if let Err(_) = install(crate_name.clone(), Some(version.clone()), true)
{
tx.send(Action::UpdateStatus(
StatusLevel::Error,
format!("failed to install {crate_name}"),
))?;
// TODO should user full error message (in a popup maybe)
return Ok(());
}
tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("installed {crate_name} v{version}"),
))?;
tx.send(Action::RefreshCargoEnv)?;
Ok::<(), AppError>(())
})
.await??;
tui.enter()?;
tui.terminal.clear()?;
Ok(())
}
CargoAction::Uninstall(crate_name) => {
self.action_tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("uninstalling {crate_name}"),
))?;
let tx = self.action_tx.clone();
tokio::spawn(async move {
if let Err(_) = uninstall(crate_name.clone(), false) {
tx.send(Action::UpdateStatus(
StatusLevel::Error,
format!("failed to uninstall {crate_name}"),
))?;
// TODO should user full error message (in a popup maybe)
return Ok(());
}
tx.send(Action::UpdateStatus(
StatusLevel::Info,
format!("uninstalled {crate_name}"),
))?;
tx.send(Action::RefreshCargoEnv)?;
Ok::<(), AppError>(())
});
Ok(())
}
};
}
Action::RefreshCargoEnv => {
self.cargo_env.write().await.read()?;
self.action_tx.send(Action::CargoEnvRefreshed)?;
}
_ => {}
}
for component in self.components.iter_mut() {
if let Some(sub_action) = component.update(clone.clone(), tui)? {
if let Some(sub_action) = component.update(clone.clone(), tui).await? {
self.action_tx.send(sub_action)?
};
}

View File

@ -1,7 +1,10 @@
use std::path::PathBuf;
use std::process::Command;
use std::io::{BufRead, BufReader};
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use crate::errors::AppResult;
use crate::errors::{AppError, AppResult};
mod installed_binary;
mod manifest_metadata;
@ -42,7 +45,7 @@ pub fn get_installed_binaries() -> AppResult<Vec<InstalledBinary>> {
}
pub fn get_metadata(manifest_path: &PathBuf) -> AppResult<ManifestMetadata> {
let output = Command::new("cargo")
let output = cargo_cmd()
.arg("metadata")
.arg("--no-deps")
.arg("--format-version")
@ -56,6 +59,97 @@ pub fn get_metadata(manifest_path: &PathBuf) -> AppResult<ManifestMetadata> {
Ok(metadata)
}
pub fn add(mut crate_name: String, version: Option<String>, show_output: bool) -> AppResult<()> {
if let Some(version) = version {
crate_name = format!("{crate_name}@{version}");
}
if show_output {
run_cargo(vec!["add", crate_name.as_str()])?;
} else {
run_cargo_with_output(vec!["add", crate_name.as_str()])?;
}
Ok(())
}
pub fn remove(crate_name: String, show_output: bool) -> AppResult<()> {
if show_output {
run_cargo(vec!["remove", crate_name.as_str()])?;
} else {
run_cargo_with_output(vec!["remove", crate_name.as_str()])?;
}
Ok(())
}
pub fn install(
mut crate_name: String,
version: Option<String>,
show_output: bool,
) -> AppResult<()> {
if let Some(version) = version {
crate_name = format!("{crate_name}@{version}");
}
if show_output {
run_cargo(vec!["install", crate_name.as_str()])?;
} else {
run_cargo_with_output(vec!["install", crate_name.as_str()])?;
}
Ok(())
}
pub fn uninstall(crate_name: String, show_output: bool) -> AppResult<()> {
if show_output {
run_cargo(vec!["uninstall", crate_name.as_str()])?;
} else {
run_cargo_with_output(vec!["uninstall", crate_name.as_str()])?;
}
Ok(())
}
fn cargo_cmd() -> Command {
let mut cmd = Command::new("cargo");
#[cfg(target_os = "windows")]
{
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd
}
fn run_cargo(args: Vec<&str>) -> AppResult<()> {
let mut cmd = cargo_cmd();
cmd.args(args);
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let stderr = child.stderr.take().unwrap();
// Stream output
let lines = BufReader::new(stderr).lines();
for line in lines {
println!("{}", line?);
}
if !child.wait()?.success() {
return Err(AppError::Cargo("Error running cargo".into()));
}
Ok(())
}
fn run_cargo_with_output(args: Vec<&str>) -> AppResult<String> {
let output = cargo_cmd().args(args).output()?;
if !output.status.success() {
return Err(AppError::Cargo(String::from_utf8(output.stderr)?));
}
Ok(String::from_utf8(output.stderr)?)
}
// #[cfg(test)]
// mod tests {
// use super::*;

View File

@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{HashMap};
use std::path::PathBuf;
use crate::cargo::{get_installed_binaries, InstalledBinary, Project};
@ -8,7 +8,7 @@ pub struct CargoEnv {
pub root: Option<PathBuf>,
pub project: Option<Project>,
pub installed: Vec<InstalledBinary>,
installed_bin_names: HashSet<String>,
installed_map: HashMap<String, String>,
}
/// The current cargo environment (installed binaries and current project, if any)
@ -23,7 +23,7 @@ impl CargoEnv {
root,
project,
installed: Vec::new(),
installed_bin_names: HashSet::new(),
installed_map: HashMap::new(),
}
}
@ -37,8 +37,10 @@ impl CargoEnv {
self.installed = get_installed_binaries().ok().unwrap_or_default();
self.installed_bin_names =
HashSet::from_iter(self.installed.iter().map(|i| i.name.clone()));
self.installed_map = self.installed
.iter()
.map(|bin| (bin.name.clone(), bin.version.clone()))
.collect();
if let Some(project) = self.project.as_mut() {
project.read().ok();
@ -47,28 +49,7 @@ impl CargoEnv {
Ok(())
}
/// Checks if a given binary is installed.
///
/// # Arguments
///
/// * `name` - The name of the binary to check.
///
/// # Returns
///
/// Returns `true` if the binary name is found in the list of installed binaries,
/// otherwise `false`.
///
/// # Examples
///
/// ```
/// let installed = manager.is_installed("my_binary");
/// if installed {
/// println!("Binary is installed!");
/// } else {
/// println!("Binary is not installed!");
/// }
/// ```
pub fn is_installed(&self, name: &str) -> bool {
self.installed_bin_names.contains(name)
pub fn get_installed_version(&self, name: &str) -> Option<String> {
self.installed_map.get(name).cloned()
}
}

View File

@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
@ -6,12 +6,17 @@ use serde::{Deserialize, Serialize};
use crate::cargo::{get_metadata, Package};
use crate::errors::AppResult;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct DependencyInfo {
kinds: Vec<String>,
version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Project {
pub manifest_file_path: PathBuf,
pub packages: Vec<Package>,
pub dependency_kinds: HashMap<String, Vec<String>>,
dependency_names: HashSet<String>,
dependencies: HashMap<String, DependencyInfo>,
}
impl Project {
@ -50,8 +55,7 @@ impl Project {
Some(Project {
manifest_file_path,
packages: Vec::new(),
dependency_kinds: HashMap::new(),
dependency_names: HashSet::new(),
dependencies: HashMap::new(),
})
}
@ -60,27 +64,29 @@ impl Project {
let packages = metadata.packages;
let mut dependency_names = HashSet::new();
let mut dependency_kinds: HashMap<String, Vec<String>> = HashMap::new();
let mut dependencies: HashMap<String, DependencyInfo> = HashMap::new();
for package in packages.iter() {
for dependency in &package.dependencies {
dependency_names.insert(dependency.name.clone());
dependency_kinds
let info = dependencies
.entry(dependency.name.clone())
.or_default()
.push(dependency.kind.clone().unwrap_or_default());
.or_insert(DependencyInfo {
kinds: vec![],
version: String::new(),
});
info.kinds.push(dependency.kind.clone().unwrap_or_default());
info.version = dependency.req.clone();
}
}
self.dependency_names = dependency_names;
self.packages = packages;
self.dependency_kinds = dependency_kinds;
self.dependencies = dependencies;
Ok(())
}
pub fn contains_dependency(&self, package_name: &str) -> bool {
self.dependency_names.contains(package_name)
pub fn get_local_version(&self, package_name: &str) -> Option<String> {
self.dependencies.get(package_name).map(|dep| dep.version.clone())
}
}

View File

@ -1,5 +1,5 @@
use std::time::Instant;
use async_trait::async_trait;
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Style, Stylize},
@ -69,8 +69,9 @@ impl FpsCounter {
}
}
#[async_trait]
impl Component for FpsCounter {
fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
async fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
let _ = tui; // to appease clippy
match action {

View File

@ -2,15 +2,19 @@
use std::sync::Arc;
use std::{fs, io::Write, process::Command};
use crate::action::{Action, CargoAction, SearchAction};
use crate::action::{Action, SearchAction};
use crate::components::home::enums::Focusable;
use crate::components::home::Home;
use crate::components::status_bar::{StatusDuration, StatusLevel};
use crate::errors::AppResult;
use crate::search::SearchOptions;
use crate::search::{CrateSearchManager, SearchOptions};
use crate::tui::Tui;
pub fn handle_action(home: &mut Home, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
pub async fn handle_action(
home: &mut Home,
action: Action,
tui: &mut Tui,
) -> AppResult<Option<Action>> {
match action {
Action::Tick => {
// add any logic here that should run on every tick
@ -168,24 +172,12 @@ pub fn handle_action(home: &mut Home, action: Action, tui: &mut Tui) -> AppResul
}
}
},
Action::Cargo(action) => return match action {
CargoAction::Add(crate_name, version) => {
let _ = crate_name;
let _ = version;
Ok(Some(Action::RefreshCargoEnv))
Action::CargoEnvRefreshed => {
if let Some(search_results) = &mut home.search_results {
let cargo_env = home.cargo_env.read().await;
CrateSearchManager::update_results(search_results, &cargo_env);
}
CargoAction::Remove(crate_name) => {
let _ = crate_name;
Ok(Some(Action::RefreshCargoEnv))
}
CargoAction::Update(crate_name) => {
let _ = crate_name;
Ok(Some(Action::RefreshCargoEnv))
}
CargoAction::UpdateAll => {
Ok(Some(Action::RefreshCargoEnv))
}
},
}
Action::OpenReadme => {
// TODO setting if open in browser or cli
if let Some(url) = home

View File

@ -135,9 +135,9 @@ fn render_results(home: &mut Home, frame: &mut Frame, area: Rect) -> AppResult<(
.crates
.iter()
.map(|i| {
let tag = if i.is_local {
let tag = if i.local_version.is_some() {
"[local]"
} else if i.is_installed {
} else if i.installed_version.is_some() {
"[installed]"
} else {
""
@ -159,9 +159,9 @@ fn render_results(home: &mut Home, frame: &mut Frame, area: Rect) -> AppResult<(
version
);
ListItem::new(if i.is_local {
ListItem::new(if i.local_version.is_some() {
line.set_style(Style::default().fg(Color::LightCyan))
} else if i.is_installed {
} else if i.installed_version.is_some() {
line.set_style(Style::default().fg(Color::LightMagenta))
} else {
line.into()
@ -386,7 +386,23 @@ fn render_crate_details(
}]
.bold();
let text = Text::from(vec![
let mut text = Text::default();
if let Some(local_version) = &krate.local_version {
text.lines.push(Line::from(vec![
format!("{:<left_column_width$}", "Project Version:").light_cyan(),
local_version.to_string().into(),
]));
}
if let Some(installed_version) = &krate.installed_version {
text.lines.push(Line::from(vec![
format!("{:<left_column_width$}", "Installed Version:").light_magenta(),
installed_version.to_string().into(),
]));
}
text.lines.extend(vec![
Line::from(vec![
format!("{:<left_column_width$}", "Version:").set_style(prop_style),
krate.version.to_string().into(),
@ -395,6 +411,9 @@ fn render_crate_details(
format!("{:<left_column_width$}", "Latest Version:").set_style(prop_style),
krate.max_version.clone().unwrap_or_default().into(),
]),
]);
text.lines.extend(vec![
Line::from(vec![
format!("{:<left_column_width$}", "Home Page:").set_style(prop_style),
krate.homepage.clone().unwrap_or_default().into(),
@ -553,11 +572,7 @@ fn render_no_results(home: &mut Home, frame: &mut Frame, area: Rect) -> AppResul
Ok(())
}
fn center(
area: Rect,
horizontal: Constraint,
vertical: Constraint,
) -> AppResult<Rect> {
fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> AppResult<Rect> {
let [area] = Layout::horizontal([horizontal])
.flex(Flex::Center)
.areas(area);

View File

@ -44,3 +44,11 @@ impl std::fmt::Display for Sort {
write!(f, "{}", output)
}
}
pub fn is_results_or_details_focused(focused: &Focusable) -> bool {
*focused == Focusable::Results
|| *focused == Focusable::AddButton
|| *focused == Focusable::InstallButton
|| *focused == Focusable::DocsButton
|| *focused == Focusable::ReadmeButton
}

View File

@ -1,8 +1,8 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui_input::backend::crossterm::EventHandler;
use crate::action::{Action, SearchAction};
use crate::components::home::enums::Focusable;
use crate::action::{Action, CargoAction, SearchAction};
use crate::components::home::enums::{is_results_or_details_focused, Focusable};
use crate::components::home::Home;
use crate::components::Component;
use crate::errors::AppResult;
@ -103,6 +103,52 @@ pub fn handle_key(home: &mut Home, key: KeyEvent) -> AppResult<Option<Action>> {
}
_ => {}
},
KeyCode::Char('a') => {
if is_results_or_details_focused(&home.focused) {
if let Some(search_results) = home.search_results.as_ref() {
if let Some(selected) = search_results.get_selected() {
return Ok(Some(Action::Cargo(CargoAction::Add(
selected.name.clone(),
selected.version.clone(),
))));
}
}
}
}
KeyCode::Char('r') => {
if is_results_or_details_focused(&home.focused) {
if let Some(search_results) = home.search_results.as_ref() {
if let Some(selected) = search_results.get_selected() {
return Ok(Some(Action::Cargo(CargoAction::Remove(
selected.name.clone(),
))));
}
}
}
}
KeyCode::Char('i') => {
if is_results_or_details_focused(&home.focused) {
if let Some(search_results) = home.search_results.as_ref() {
if let Some(selected) = search_results.get_selected() {
return Ok(Some(Action::Cargo(CargoAction::Install(
selected.name.clone(),
selected.version.clone(),
))));
}
}
}
}
KeyCode::Char('u') => {
if is_results_or_details_focused(&home.focused) {
if let Some(search_results) = home.search_results.as_ref() {
if let Some(selected) = search_results.get_selected() {
return Ok(Some(Action::Cargo(CargoAction::Uninstall(
selected.name.clone(),
))));
}
}
}
}
_ => {}
}

View File

@ -6,11 +6,11 @@ mod key_handler;
use super::Component;
use std::sync::Arc;
use async_trait::async_trait;
use crossterm::event::KeyEvent;
use ratatui::{layout::Rect, Frame};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::Mutex;
use tokio::sync::{RwLock};
use tui_input::Input;
use crate::cargo::CargoEnv;
@ -30,7 +30,7 @@ use crate::{
};
pub struct Home {
cargo_env: Arc<Mutex<CargoEnv>>,
cargo_env: Arc<RwLock<CargoEnv>>,
input: Input,
scope_dropdown: Dropdown<Scope>,
sort_dropdown: Dropdown<Sort>,
@ -47,7 +47,7 @@ pub struct Home {
impl Home {
pub fn new(
cargo_env: Arc<Mutex<CargoEnv>>,
cargo_env: Arc<RwLock<CargoEnv>>,
action_tx: UnboundedSender<Action>,
) -> AppResult<Self> {
let tx = action_tx.clone();
@ -86,7 +86,7 @@ impl Home {
self.input.reset();
self.search_results = None;
self.action_tx
.send(Action::UpdateStatus(StatusLevel::Info, "Ready".into()))?;
.send(Action::UpdateStatus(StatusLevel::Info, "ready".into()))?;
Ok(())
}
@ -160,6 +160,7 @@ impl Home {
}
}
#[async_trait]
impl Component for Home {
fn register_config_handler(&mut self, config: Config) -> AppResult<()> {
self.sort_dropdown.register_config_handler(config.clone())?;
@ -173,8 +174,8 @@ impl Component for Home {
handle_key(self, key)
}
fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
handle_action(self, action, tui)
async fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
handle_action(self, action, tui).await
}
fn draw(&mut self, mode: &Mode, frame: &mut Frame, area: Rect) -> AppResult<()> {
@ -199,13 +200,13 @@ mod tests {
fn get_home() -> Home {
let (action_tx, _) = mpsc::unbounded_channel();
Home::new(Arc::new(Mutex::new(CargoEnv::new(None))), action_tx).unwrap()
Home::new(Arc::new(RwLock::new(CargoEnv::new(None))), action_tx).unwrap()
}
fn get_home_and_tui() -> (Home, Tui) {
let (action_tx, _) = mpsc::unbounded_channel();
(
Home::new(Arc::new(Mutex::new(CargoEnv::new(None))), action_tx).unwrap(),
Home::new(Arc::new(RwLock::new(CargoEnv::new(None))), action_tx).unwrap(),
Tui::new().unwrap(),
)
}
@ -222,7 +223,7 @@ mod tests {
let mut ac: Option<Action> = Some(action);
while ac.is_some() {
match home.update(ac.clone().unwrap(), tui) {
match home.update(ac.clone().unwrap(), tui).await {
Ok(action) => {
ac = action;
}

View File

@ -6,7 +6,7 @@ pub mod status_bar;
pub mod ux;
use std::any::Any;
use async_trait::async_trait;
use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::{layout::Rect, Frame};
@ -19,7 +19,9 @@ use crate::{action::Action, config::Config, tui::Event};
///
/// Implementors of this trait can be registered with the main application loop and will be able to
/// receive events, update state, and be rendered on the screen.
pub trait Component: Any {
#[async_trait]
pub trait Component: Any + Send + Sync {
/// Register a configuration handler that provides configuration settings if necessary.
///
/// # Arguments
@ -94,7 +96,7 @@ pub trait Component: Any {
/// # Returns
///
/// * `Result<Option<Action>>` - An action to be processed or none.
fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
async fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
let _ = action; // to appease clippy
let _ = tui;
Ok(None)

View File

@ -1,4 +1,5 @@
use crossterm::event::KeyEvent;
use async_trait::async_trait;
use crossterm::event::KeyEvent;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
@ -23,6 +24,7 @@ impl Settings {
}
}
#[async_trait]
impl Component for Settings {
fn register_config_handler(&mut self, config: Config) -> AppResult<()> {
self.config = config;
@ -34,7 +36,7 @@ impl Component for Settings {
Ok(None)
}
fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
async fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
let _ = action;
let _ = tui;

View File

@ -1,5 +1,5 @@
use std::cmp::PartialEq;
use async_trait::async_trait;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Styled, Stylize};
use ratatui::text::{Line, Text};
@ -109,7 +109,7 @@ impl StatusBar {
if cancel_rx.try_recv().is_ok() {
return;
}
tx.send(Action::UpdateStatus(Info, "Ready".into())).unwrap();
tx.send(Action::UpdateStatus(Info, "ready".into())).unwrap();
});
}
}
@ -148,6 +148,7 @@ impl StatusBar {
}
}
#[async_trait]
impl Component for StatusBar {
fn register_config_handler(&mut self, config: Config) -> AppResult<()> {
self.config = config;
@ -160,7 +161,7 @@ impl Component for StatusBar {
Ok(())
}
fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
async fn update(&mut self, action: Action, tui: &mut Tui) -> AppResult<Option<Action>> {
let _ = tui;
match action {
Action::UpdateStatus(level, message) => match level {

View File

@ -1,4 +1,5 @@
use crossterm::event::{KeyCode, KeyEvent};
use async_trait::async_trait;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::prelude::Stylize;
use ratatui::widgets::block::Title;
@ -18,11 +19,11 @@ pub struct Dropdown<T> {
config: Config,
is_focused: bool,
state: ListState,
on_enter: Box<dyn Fn(&T) + Send>,
on_enter: Box<dyn Fn(&T) + Send + Sync>,
}
impl<T: IntoEnumIterator + Default + Clone + 'static> Dropdown<T> {
pub fn new(header: String, on_enter: Box<dyn Fn(&T) + Send>) -> Self {
impl<T: IntoEnumIterator + Default + Clone> Dropdown<T> {
pub fn new(header: String, on_enter: Box<dyn Fn(&T) + Send + Sync>) -> Self {
Dropdown {
header,
config: Config::default(),
@ -46,6 +47,7 @@ impl<T: IntoEnumIterator + Default + Clone + 'static> Dropdown<T> {
}
}
#[async_trait]
impl<T: IntoEnumIterator + Default + Display + Clone + 'static> Component for Dropdown<T> {
fn register_config_handler(&mut self, config: Config) -> AppResult<()> {
self.config = config;

View File

@ -19,8 +19,14 @@ pub enum AppError {
FromUtf8(#[from] std::string::FromUtf8Error),
#[error(transparent)]
SendAction(#[from] tokio::sync::mpsc::error::SendError<Action>),
#[error(transparent)]
Join(#[from] tokio::task::JoinError),
#[error("Error: {0}")]
Unknown(String),
// Custom
#[error("{0}")]
Cargo(String)
}
/// Catch-all: if an error that implements std::error::Error occurs

View File

@ -18,8 +18,8 @@ pub struct Crate {
pub updated_at: Option<DateTime<Utc>>,
pub exact_match: bool,
#[serde(default)]
pub is_local: bool,
pub local_version: Option<String>,
#[serde(default)]
pub is_installed: bool,
pub installed_version: Option<String>,
}

View File

@ -1,7 +1,7 @@
use crates_io_api::{AsyncClient, CratesQuery};
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::{oneshot, Mutex};
use tokio::sync::{oneshot, RwLock};
use tokio::task::JoinHandle;
use crate::action::{Action, SearchAction};
@ -17,11 +17,13 @@ pub struct CrateSearchManager {
impl CrateSearchManager {
pub fn new(action_tx: UnboundedSender<Action>) -> AppResult<Self> {
let client = AsyncClient::new(
"seekr (github:tareqimbasher/seekr)",
std::time::Duration::from_millis(1000),
)?;
Ok(CrateSearchManager {
crates_io_client: Arc::new(AsyncClient::new(
"seekr (github:tareqimbasher/seekr)",
std::time::Duration::from_millis(1000),
)?),
crates_io_client: Arc::new(client),
cancel_tx: None,
action_tx,
})
@ -30,7 +32,7 @@ impl CrateSearchManager {
pub fn search(
&mut self,
options: SearchOptions,
cargo_env: Arc<Mutex<CargoEnv>>,
cargo_env: Arc<RwLock<CargoEnv>>,
) -> JoinHandle<()> {
if let Some(cancel_tx) = self.cancel_tx.take() {
let _ = cancel_tx.send(());
@ -47,7 +49,7 @@ impl CrateSearchManager {
return;
}
let cargo_env = cargo_env.lock().await;
let cargo_env = cargo_env.read().await;
let term = options.term.unwrap_or("".to_string()).to_lowercase();
let page = options.page.unwrap_or(1);
let per_page = options.per_page.unwrap_or(100);
@ -117,23 +119,23 @@ impl CrateSearchManager {
// Back-fill is_local and is_installed for search results that don't have it
// todo optimize
for cr in &mut search_results.crates {
if !cr.is_local {
if let Some(proj) = &cargo_env.project {
cr.is_local = proj.contains_dependency(&cr.name);
}
}
if !cr.is_installed {
cr.is_installed = cargo_env.is_installed(&cr.name);
}
}
Self::update_results(&mut search_results, &cargo_env);
tx.send(Action::Search(SearchAction::Render(search_results)))
.ok();
})
}
pub fn update_results(search_results: &mut SearchResults, cargo_env: &CargoEnv) {
for cr in &mut search_results.crates {
if let Some(proj) = &cargo_env.project {
cr.local_version = proj.get_local_version(&cr.name);
}
cr.installed_version = cargo_env.get_installed_version(&cr.name);
}
}
fn search_binaries(term: &str, cargo_env: &CargoEnv) -> Vec<Crate> {
let mut results: Vec<Crate> = Vec::new();
@ -155,8 +157,8 @@ impl CrateSearchManager {
created_at: None,
updated_at: None,
exact_match: name_lower == term,
is_local: false,
is_installed: true,
local_version: None,
installed_version: Some(package.version.clone()),
});
}
}
@ -187,8 +189,8 @@ impl CrateSearchManager {
created_at: None,
updated_at: None,
exact_match: name_lower == term,
is_local: true,
is_installed: false,
local_version: Some(dep.req.clone()),
installed_version: None,
});
}
}
@ -247,8 +249,8 @@ impl CrateSearchManager {
created_at: Some(c.created_at),
updated_at: Some(c.updated_at),
exact_match: c.exact_match.unwrap_or(false),
is_local: false,
is_installed: false,
local_version: None,
installed_version: None,
})
.collect();
Ok((results, sr.meta.total as usize))