mirror of
https://github.com/tareqimbasher/cargo-seek.git
synced 2026-01-09 07:52:41 +08:00
Adds install/uninstall add/remove
This commit is contained in:
parent
66131a0eca
commit
25df130d42
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)]
|
||||
|
||||
148
src/app.rs
148
src/app.rs
@ -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)?
|
||||
};
|
||||
}
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user