more cleanup

This commit is contained in:
Tareq Imbasher 2025-03-29 02:48:15 +03:00
parent 4ae389f29e
commit 649ca8c5e8
23 changed files with 168 additions and 182 deletions

View File

@ -7,6 +7,12 @@ authors = ["Tareq Imbasher <https://github.com/tareqimbasher"]
build = "build.rs"
repository = "https://github.com/tareqimbasher/seekr"
[profile.release]
opt-level = "z" # Optimize for size.
strip = true # Automatically strip symbols from the binary.
lto = true
panic = "abort"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
@ -58,9 +64,3 @@ async-trait = "0.1"
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.2", features = ["build", "cargo"] }
[profile.release]
opt-level = "z" # Optimize for size.
strip = true # Automatically strip symbols from the binary.
lto = true
panic = "abort"

2
rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
edition = "2021"
#style_edition = "2021"

View File

@ -1,10 +1,8 @@
use crate::components::{Focusable, StatusDuration, StatusLevel};
use crate::search::{Scope, SearchResults, Sort};
use serde::{Deserialize, Serialize};
use strum::Display;
use crate::components::home::enums::Focusable;
use crate::components::status_bar::{StatusDuration, StatusLevel};
use crate::search::{Scope, SearchResults, Sort};
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum Action {
Tick,

View File

@ -5,29 +5,22 @@ use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{debug, info};
use crate::action::CargoAction;
use crate::action::{Action, CargoAction};
use crate::cargo::{add, install, remove, uninstall, CargoEnv};
use crate::components::status_bar::StatusLevel;
use crate::components::{AppId, Component, FpsCounter, Home, Settings, StatusBar, StatusLevel};
use crate::config::Config;
use crate::errors::{AppError, AppResult};
use crate::{
action::Action,
components::{
app_id::AppId, fps::FpsCounter, home::Home, settings::Settings, status_bar::StatusBar,
Component,
},
config::Config,
tui::{Event, Tui},
};
use crate::tui::{Event, Tui};
pub struct App {
cargo_env: Arc<RwLock<CargoEnv>>,
config: Config,
tick_rate: f64,
frame_rate: f64,
mode: Mode,
components: Vec<Box<dyn Component>>,
should_quit: bool,
should_suspend: bool,
mode: Mode,
last_tick_key_events: Vec<KeyEvent>,
action_tx: mpsc::UnboundedSender<Action>,
action_rx: mpsc::UnboundedReceiver<Action>,
@ -49,9 +42,9 @@ impl App {
let cargo_env = Arc::new(RwLock::new(CargoEnv::new(root)));
let mut components: Vec<Box<dyn Component>> = vec![
Box::new(AppId::new()),
Box::new(Home::new(Arc::clone(&cargo_env), action_tx.clone())?),
Box::new(Settings::new()),
Box::new(AppId::new()),
Box::new(StatusBar::new(action_tx.clone())),
];

View File

@ -1,47 +1,43 @@
use std::collections::{HashMap};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::cargo::{get_installed_binaries, InstalledBinary, Project};
use crate::errors::AppResult;
pub struct CargoEnv {
pub root: Option<PathBuf>,
pub current_dir: Option<PathBuf>,
pub project: Option<Project>,
pub installed: Vec<InstalledBinary>,
installed_map: HashMap<String, String>,
installed_versions: HashMap<String, String>,
}
/// The current cargo environment (installed binaries and current project, if any)
impl CargoEnv {
pub fn new(root: Option<PathBuf>) -> Self {
let project = match root.clone() {
Some(p) => Project::from(p),
None => None,
};
pub fn new(current_dir: Option<PathBuf>) -> Self {
Self {
root,
project,
current_dir,
project: None,
installed: Vec::new(),
installed_map: HashMap::new(),
installed_versions: HashMap::new(),
}
}
/// Reads the current Cargo environment and updates the internal state.
pub fn read(&mut self) -> AppResult<()> {
if let Some(root) = &self.root {
if self.project.is_none() {
self.project = Project::from(root.clone());
}
}
self.installed = get_installed_binaries().ok().unwrap_or_default();
self.installed_map = self.installed
self.installed_versions = self
.installed
.iter()
.map(|bin| (bin.name.clone(), bin.version.clone()))
.collect();
if self.project.is_none() {
if let Some(current_dir) = &self.current_dir {
self.project = Project::from(current_dir);
}
}
if let Some(project) = self.project.as_mut() {
project.read().ok();
}
@ -49,7 +45,8 @@ impl CargoEnv {
Ok(())
}
/// Gets the installed version of the given crate name, if any.
pub fn get_installed_version(&self, name: &str) -> Option<String> {
self.installed_map.get(name).cloned()
self.installed_versions.get(name).cloned()
}
}

View File

@ -1,7 +1,7 @@
mod cargo_env;
mod api;
mod cargo_env;
mod project;
pub use api::*;
pub use cargo_env::CargoEnv;
pub use project::*;
pub use api::*;

View File

@ -1,10 +1,12 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::fs;
use std::fs::DirEntry;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::cargo::{get_metadata, Package};
use crate::errors::AppResult;
use crate::errors::{AppError, AppResult};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct DependencyInfo {
@ -20,46 +22,30 @@ pub struct Project {
}
impl Project {
pub fn from(path: PathBuf) -> Option<Project> {
if !path.exists() || !path.is_dir() {
pub fn from(path: &Path) -> Option<Project> {
if !path.try_exists().ok().unwrap_or_default() || !path.is_dir() {
return None;
}
let files = std::fs::read_dir(path);
if files.is_err() {
return None;
if let Ok(Some(manifest_file_path)) = find_project_manifest(path) {
Some(Project {
manifest_file_path,
packages: Vec::new(),
dependencies: HashMap::new(),
})
} else {
None
}
// Iterate over files and check if we have a match. Iteration was chosen because
// checking if specific paths exists is error-prone. Ex: checking if "cargo.toml" exists
// on Windows returns true when the file's name is called "Cargo.toml", this causes an
// issue in that the cargo executable wants the exact file name.
let manifest_file = files
.unwrap()
.find(|f| {
if let Ok(file) = f {
let file_name = file.file_name().to_str().unwrap_or_default().to_string();
if file_name == "Cargo.toml" || file_name == "cargo.toml" {
return true;
}
}
false
})?
.ok();
manifest_file.as_ref()?;
let manifest_file_path = manifest_file.unwrap().path();
Some(Project {
manifest_file_path,
packages: Vec::new(),
dependencies: HashMap::new(),
})
}
/// Reads the current project and updates internal state.
pub fn read(&mut self) -> AppResult<()> {
if !self.manifest_file_path.exists() {
return Err(AppError::Unknown(
"Manifest file no longer exists".to_owned(),
));
}
let metadata = get_metadata(&self.manifest_file_path)?;
let packages = metadata.packages;
@ -86,7 +72,42 @@ impl Project {
Ok(())
}
/// Gets the local project version of the given crate name, if any.
pub fn get_local_version(&self, package_name: &str) -> Option<String> {
self.dependencies.get(package_name).map(|dep| dep.version.clone())
self.dependencies
.get(package_name)
.map(|dep| dep.version.clone())
}
}
fn find_project_manifest(starting_dir_path: &Path) -> AppResult<Option<PathBuf>> {
let mut search_path = Some(starting_dir_path);
let mut manifest_file: Option<DirEntry> = None;
while search_path.is_some() && manifest_file.is_none() {
let path = search_path.unwrap();
let found = fs::read_dir(path)?.find(|f| {
if let Ok(file) = f {
let file_name = file.file_name().to_string_lossy().to_lowercase();
if file_name == "cargo.toml" {
return true;
}
}
false
});
if let Some(found) = found {
manifest_file = Some(found?);
break;
}
search_path = path.parent();
}
if let Some(manifest_file) = manifest_file {
Ok(Some(manifest_file.path()))
} else {
Ok(None)
}
}

View File

@ -1,4 +1,3 @@
use std::time::Instant;
use async_trait::async_trait;
use ratatui::{
layout::{Constraint, Layout, Rect},
@ -7,6 +6,7 @@ use ratatui::{
widgets::Paragraph,
Frame,
};
use std::time::Instant;
use super::Component;

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use std::{fs, io::Write, process::Command};
use crate::action::{Action, SearchAction};
use crate::components::home::enums::Focusable;
use crate::components::home::focusable::Focusable;
use crate::components::home::Home;
use crate::components::status_bar::{StatusDuration, StatusLevel};
use crate::errors::AppResult;
@ -287,7 +287,9 @@ pub async fn handle_action(
.search_results
.as_ref()
.and_then(|results| results.get_selected())
.and_then(|krate| Url::parse(format!("https://crates.io/crates/{}", krate.id).as_str()).ok())
.and_then(|krate| {
Url::parse(format!("https://crates.io/crates/{}", krate.id).as_str()).ok()
})
{
open::that(url.to_string())?;
}
@ -297,7 +299,9 @@ pub async fn handle_action(
.search_results
.as_ref()
.and_then(|results| results.get_selected())
.and_then(|krate| Url::parse(format!("https://lib.rs/crates/{}", krate.id).as_str()).ok())
.and_then(|krate| {
Url::parse(format!("https://lib.rs/crates/{}", krate.id).as_str()).ok()
})
{
open::that(url.to_string())?;
}

View File

@ -1,4 +1,4 @@
use chrono::Utc;
use chrono::Utc;
use ratatui::prelude::{Color, Line, Text};
use ratatui::widgets::block::{Position, Title};
use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap};
@ -9,7 +9,7 @@ use ratatui::{
};
use crate::app::Mode;
use crate::components::home::enums::Focusable;
use crate::components::home::focusable::Focusable;
use crate::components::home::Home;
use crate::components::ux::{Button, State, GRAY, ORANGE, PURPLE, YELLOW};
use crate::components::Component;
@ -148,8 +148,11 @@ fn render_results(home: &mut Home, frame: &mut Frame, area: Rect) -> AppResult<(
let name = i.name.to_string();
let version = i.version.to_string();
let mut white_space =
area.width as i32 - name.len() as i32 - tag.len() as i32 - VERSION_PADDING as i32 - correction;
let mut white_space = area.width as i32
- name.len() as i32
- tag.len() as i32
- VERSION_PADDING as i32
- correction;
if white_space < 1 {
white_space = 1;
}
@ -169,10 +172,7 @@ fn render_results(home: &mut Home, frame: &mut Frame, area: Rect) -> AppResult<(
Style::default()
};
ListItem::new(Line::from(vec![
tag.bold(),
line.into()
]).set_style(style))
ListItem::new(Line::from(vec![tag.bold(), line.into()]).set_style(style))
})
.collect();
@ -268,7 +268,6 @@ fn render_usage(home: &mut Home, frame: &mut Frame, area: Rect) -> AppResult<()>
"installed".bold(),
]),
Line::default(),
Line::from(vec!["SEARCH".bold()]),
Line::from(vec![
format!("{:<PAD$}", "Enter:").set_style(prop_style),

View File

@ -2,8 +2,6 @@
use serde::{Deserialize, Serialize};
use std::iter::Cycle;
use crate::search::Sort;
#[derive(Default, PartialEq, Clone, Debug, Eq, Sequence, Serialize, Deserialize)]
pub enum Focusable {
Usage,
@ -32,20 +30,6 @@ impl Focusable {
}
}
impl std::fmt::Display for Sort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let output = match self {
Sort::Relevance => "Relevance",
Sort::Name => "Name",
Sort::Downloads => "Downloads",
Sort::RecentDownloads => "Recent Downloads",
Sort::RecentlyUpdated => "Recently Updated",
Sort::NewlyAdded => "Newly Added",
};
write!(f, "{}", output)
}
}
pub fn is_results_or_details_focused(focused: &Focusable) -> bool {
*focused == Focusable::Results
|| *focused == Focusable::DocsButton

View File

@ -1,8 +1,8 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui_input::backend::crossterm::EventHandler;
use crate::action::{Action, CargoAction, SearchAction};
use crate::components::home::enums::{is_results_or_details_focused, Focusable};
use crate::components::home::focusable::{is_results_or_details_focused, Focusable};
use crate::components::home::Home;
use crate::components::Component;
use crate::errors::AppResult;
@ -179,8 +179,10 @@ pub fn handle_key(home: &mut Home, key: KeyEvent) -> AppResult<Option<Action>> {
if home.focused == Focusable::Search {
match key.code {
KeyCode::Down => if home.search_results.is_some() {
return Ok(Some(Action::Focus(Focusable::Results)));
KeyCode::Down => {
if home.search_results.is_some() {
return Ok(Some(Action::Focus(Focusable::Results)));
}
}
_ => {
// Send to input box
@ -241,10 +243,8 @@ pub fn handle_key(home: &mut Home, key: KeyEvent) -> AppResult<Option<Action>> {
}
}
if is_results_or_details_focused(&home.focused) {
if ctrl && key.code == KeyCode::Char('d') {
return Ok(Some(Action::OpenDocs));
}
if is_results_or_details_focused(&home.focused) && ctrl && key.code == KeyCode::Char('d') {
return Ok(Some(Action::OpenDocs));
}
if home.focused == Focusable::Sort {

View File

@ -1,6 +1,6 @@
mod action_handler;
mod action_handler;
mod draw;
pub mod enums;
mod focusable;
mod key_handler;
use super::Component;
@ -16,7 +16,6 @@ use tui_input::Input;
use crate::cargo::CargoEnv;
use crate::components::home::action_handler::handle_action;
use crate::components::home::draw::render;
use crate::components::home::enums::Focusable;
use crate::components::home::key_handler::handle_key;
use crate::components::status_bar::StatusLevel;
use crate::components::ux::Dropdown;
@ -28,6 +27,7 @@ use crate::{
app::Mode,
config::Config,
};
pub use focusable::Focusable;
pub struct Home {
cargo_env: Arc<RwLock<CargoEnv>>,
@ -267,39 +267,6 @@ mod tests {
assert_eq!(home.focused, Focusable::Results);
}
#[tokio::test]
async fn test_focus_next_action() {
let (home, _) = execute_update(Action::FocusNext).await;
assert_eq!(home.focused, Focusable::Results);
}
#[tokio::test]
async fn test_focus_next_action_when_last_is_focused() {
let (mut home, mut tui) = execute_update(Action::Focus(Focusable::DocsButton)).await;
execute_update_with_home(&mut home, &mut tui, Action::FocusNext).await;
assert_eq!(home.focused, Focusable::Search);
}
#[tokio::test]
async fn test_focus_previous_action() {
let (mut home, mut tui) = execute_update(Action::Focus(Focusable::DocsButton)).await;
execute_update_with_home(&mut home, &mut tui, Action::FocusPrevious).await;
assert_eq!(home.focused, Focusable::ReadmeButton);
}
#[tokio::test]
async fn test_focus_previous_action_when_first_is_focused() {
let (mut home, mut tui) = execute_update(Action::Focus(Focusable::Search)).await;
execute_update_with_home(&mut home, &mut tui, Action::FocusPrevious).await;
assert_eq!(home.focused, Focusable::DocsButton);
}
#[tokio::test]
async fn test_search_clear_action() {
let (mut home, _) = get_home_and_tui();

View File

@ -1,14 +1,20 @@
pub mod app_id;
pub mod fps;
pub mod home;
pub mod settings;
pub mod status_bar;
pub mod ux;
mod app_id;
mod fps;
mod home;
mod settings;
mod status_bar;
mod ux;
use std::any::Any;
use async_trait::async_trait;
use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::{layout::Rect, Frame};
use std::any::Any;
pub use app_id::*;
pub use fps::FpsCounter;
pub use home::*;
pub use settings::Settings;
pub use status_bar::{StatusBar, StatusDuration, StatusLevel};
use crate::app::Mode;
use crate::errors::AppResult;

View File

@ -1,4 +1,3 @@
use std::cmp::PartialEq;
use async_trait::async_trait;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Styled, Stylize};
@ -6,6 +5,7 @@ use ratatui::text::{Line, Text};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use serde::{Deserialize, Serialize};
use std::cmp::PartialEq;
use strum::Display;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::oneshot;
@ -108,7 +108,8 @@ impl StatusBar {
if cancel_rx.try_recv().is_ok() {
return;
}
tx.send(Action::UpdateStatus(StatusLevel::Info, "ready".into())).unwrap();
tx.send(Action::UpdateStatus(StatusLevel::Info, "ready".into()))
.unwrap();
});
}
}
@ -213,8 +214,7 @@ impl Component for StatusBar {
}
frame.render_widget(
Paragraph::new(Text::from(Line::from(text)))
.alignment(Alignment::Right),
Paragraph::new(Text::from(Line::from(text))).alignment(Alignment::Right),
right,
);

View File

@ -1,4 +1,4 @@
use async_trait::async_trait;
use async_trait::async_trait;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::prelude::Stylize;
@ -23,7 +23,11 @@ pub struct Dropdown<T> {
}
impl<T: IntoEnumIterator + Default + Clone> Dropdown<T> {
pub fn new(header: String, selected_ix: usize, on_enter: Box<dyn Fn(&T) + Send + Sync>) -> Self {
pub fn new(
header: String,
selected_ix: usize,
on_enter: Box<dyn Fn(&T) + Send + Sync>,
) -> Self {
Dropdown {
header,
config: Config::default(),

View File

@ -23,10 +23,10 @@ pub enum AppError {
Join(#[from] tokio::task::JoinError),
#[error("Error: {0}")]
Unknown(String),
// Custom
#[error("{0}")]
Cargo(String)
Cargo(String),
}
/// Catch-all: if an error that implements std::error::Error occurs

View File

@ -2,22 +2,21 @@ mod action;
mod app;
mod cargo;
mod cli;
mod components;
mod config;
mod errors;
mod logging;
mod search;
mod tui;
mod util;
mod components;
use clap::Parser;
use cli::Cli;
use color_eyre::Result;
use crate::app::App;
#[tokio::main]
async fn main() -> Result<()> {
async fn main() -> color_eyre::Result<()> {
errors::init()?;
logging::init()?;

View File

@ -1,4 +1,4 @@
use chrono::{DateTime, Utc};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -22,4 +22,3 @@ pub struct Crate {
#[serde(default)]
pub installed_version: Option<String>,
}

View File

@ -28,7 +28,7 @@ impl CrateSearchManager {
let client = AsyncClient::with_http_client(
Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(15))
.timeout(Duration::from_secs(10))
.build()?,
Duration::from_millis(1000),
);
@ -129,7 +129,6 @@ impl CrateSearchManager {
}
// Back-fill is_local and is_installed for search results that don't have it
// todo optimize
Self::update_results(&mut search_results, &cargo_env);
tx.send(Action::Search(SearchAction::Render(search_results)))

View File

@ -1,7 +1,7 @@
mod cargo_crate;
mod cargo_crate;
mod crate_search_manager;
mod search_results;
mod search_options;
mod search_results;
pub use cargo_crate::*;
pub use crate_search_manager::*;

View File

@ -1,4 +1,4 @@
use enum_iterator::Sequence;
use enum_iterator::Sequence;
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumIter};
@ -24,6 +24,20 @@ pub enum Sort {
NewlyAdded,
}
impl std::fmt::Display for Sort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let output = match self {
Sort::Relevance => "Relevance",
Sort::Name => "Name",
Sort::Downloads => "Downloads",
Sort::RecentDownloads => "Recent Downloads",
Sort::RecentlyUpdated => "Recently Updated",
Sort::NewlyAdded => "Newly Added",
};
write!(f, "{}", output)
}
}
#[derive(Debug, Default)]
pub struct SearchOptions {
pub term: Option<String>,
@ -31,4 +45,4 @@ pub struct SearchOptions {
pub per_page: Option<usize>,
pub sort: Sort,
pub scope: Scope,
}
}

View File

@ -1,4 +1,4 @@
use ratatui::widgets::ListState;
use ratatui::widgets::ListState;
use serde::{Deserialize, Serialize};
use crate::search::Crate;
@ -87,4 +87,4 @@ impl SearchResults {
pub fn list_state(&mut self) -> &mut ListState {
&mut self.state
}
}
}