feat(tui): Wrap project path in newtype

This commit is contained in:
Patrick Auernig 2024-12-06 20:56:11 +01:00
parent 083c66aacb
commit 823d3e8f93
4 changed files with 58 additions and 32 deletions

View File

@ -4,7 +4,7 @@ use anyhow::{ensure, Result};
use clap::Subcommand; use clap::Subcommand;
use crate::state::write_projects_file; use crate::state::write_projects_file;
use crate::{dirs, Projects}; use crate::{dirs, Project, Projects};
#[derive(Debug, Clone, clap::ValueEnum)] #[derive(Debug, Clone, clap::ValueEnum)]
@ -76,16 +76,18 @@ where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path::absolute(path)?; let path = path::absolute(path)?;
ensure!(path.is_dir(), "Project path does not exists"); ensure!(path.is_dir(), "Project path does not exists");
let project = Project::from(path);
ensure!( ensure!(
!projects.list.contains(&path.to_path_buf()), !projects.list.contains(&project),
"Project path already registered" "Project path already registered"
); );
projects.list.push(path); projects.list.push(project);
write_projects_file(projects)?; write_projects_file(projects)?;
println!("Added {}", projects.list.last().unwrap().display()); println!("Added {}", projects.list.last().unwrap().path().display());
Ok(()) Ok(())
} }
@ -96,16 +98,17 @@ where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path::absolute(path)?; let path = path::absolute(path)?;
let project = Project::from(path);
ensure!( ensure!(
projects.list.contains(&path.to_path_buf()), projects.list.contains(&project),
"Project path not in registry" "Project path not in registry"
); );
let idx = let idx =
projects.list.iter().enumerate().find_map( projects.list.iter().enumerate().find_map(
|(idx, elem)| { |(idx, elem)| {
if elem == &path { if elem == &project {
Some(idx) Some(idx)
} else { } else {
None None
@ -116,7 +119,7 @@ where
if let Some(idx) = idx { if let Some(idx) = idx {
let proj = projects.list.remove(idx); let proj = projects.list.remove(idx);
write_projects_file(projects)?; write_projects_file(projects)?;
println!("Removed {}", proj.display()); println!("Removed {}", proj.path().display());
} }
Ok(()) Ok(())
@ -125,7 +128,7 @@ where
fn list_projects(projects: &Projects) -> Result<()> { fn list_projects(projects: &Projects) -> Result<()> {
for (idx, project) in projects.list.iter().enumerate() { for (idx, project) in projects.list.iter().enumerate() {
println!("{}: {}", idx + 1, project.display()) println!("{}: {} ({})", idx + 1, project.name(), project.path().display())
} }
Ok(()) Ok(())

View File

@ -21,7 +21,28 @@ struct Args {
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
struct Projects { struct Projects {
pub list: Vec<PathBuf>, pub list: Vec<Project>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
struct Project(PathBuf);
impl Project {
pub fn path(&self) -> &PathBuf {
&self.0
}
pub fn name(&self) -> String {
let name = self.0.file_name().unwrap();
name.to_string_lossy().to_string()
}
}
impl From<PathBuf> for Project {
fn from(value: PathBuf) -> Self {
Self(value)
}
} }

View File

@ -1,10 +1,9 @@
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use crate::{dirs, Projects}; use crate::{dirs, Project, Projects};
pub fn clear_selected_project_file() -> Result<()> { pub fn clear_selected_project_file() -> Result<()> {
@ -17,8 +16,9 @@ pub fn clear_selected_project_file() -> Result<()> {
Ok(()) Ok(())
} }
pub fn write_selected_project_file(path: PathBuf) -> Result<()> { pub fn write_selected_project_file(project: &Project) -> Result<()> {
let cache_file = dirs::selected_project_file(); let cache_file = dirs::selected_project_file();
let path = project.path();
let mut file = fs::OpenOptions::new() let mut file = fs::OpenOptions::new()
.write(true) .write(true)

View File

@ -1,17 +1,17 @@
use std::path::PathBuf;
use std::sync::mpsc; use std::sync::mpsc;
use anyhow::Result; use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style}; use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState}; use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState};
use ratatui::Frame; use ratatui::Frame;
use tracing::trace; use tracing::trace;
use tui_input::{Input, InputRequest}; use tui_input::{Input, InputRequest};
use crate::state::{clear_selected_project_file, write_selected_project_file}; use crate::state::{clear_selected_project_file, write_selected_project_file};
use crate::Projects; use crate::{Project, Projects};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -39,8 +39,8 @@ struct State {
mode: Mode, mode: Mode,
should_exit: bool, should_exit: bool,
project_table: TableState, project_table: TableState,
filtered_projects: Vec<String>, filtered_projects: Vec<Project>,
selected_project: Option<PathBuf>, selected_project: Option<Project>,
} }
impl State { impl State {
@ -88,11 +88,11 @@ pub fn run(projects: Projects) -> Result<()> {
if let Some(selected_project) = state.selected_project { if let Some(selected_project) = state.selected_project {
// hacky stderr abuse // hacky stderr abuse
eprintln!( eprintln!(
"{}cd:{}", "{}-cd:{}",
env!("CARGO_BIN_NAME"), env!("CARGO_BIN_NAME"),
selected_project.display() selected_project.path().display()
); );
write_selected_project_file(selected_project)? write_selected_project_file(&selected_project)?
} }
Ok(()) Ok(())
@ -152,7 +152,7 @@ fn handle_messages(state: &mut State, rx: &mut mpsc::Receiver<Message>) -> Resul
if let Some(selected) = state.project_table.selected() { if let Some(selected) = state.project_table.selected() {
if let Some(project_path) = state.filtered_projects.get(selected) { if let Some(project_path) = state.filtered_projects.get(selected) {
state.should_exit = true; state.should_exit = true;
state.selected_project = Some(PathBuf::from(project_path)); state.selected_project = Some(project_path.clone());
}; };
} }
} }
@ -213,26 +213,28 @@ fn draw_list(state: &mut State, frame: &mut Frame, area: Rect) {
.list .list
.iter() .iter()
.filter_map(|project| { .filter_map(|project| {
let path_str = project.to_str().expect("invalid path string");
let search_value = state.search.value(); let search_value = state.search.value();
if search_value.is_empty() { if search_value.is_empty() {
return Some(path_str.to_string()); return Some(project.clone());
} }
let indices = fuzzy_search(search_value, path_str)?; let indices = fuzzy_search(search_value, &project.name())?;
trace!(?search_value, ?path_str, ?indices); trace!(?search_value, ?project, ?indices);
Some(path_str.to_string()) Some(project.clone())
}) })
.collect(); .collect();
let rows = state let rows = state.filtered_projects.iter().map(|project| {
.filtered_projects let name = project.name();
.iter() let path_style = Style::new().fg(Color::DarkGray);
.map(|path| Row::new([Cell::new(path.as_str())])); let path_span = Span::styled(project.path().display().to_string(), path_style);
let name_span = Span::from(name);
let widths = [Constraint::Min(20)]; Row::new([Cell::new(name_span), Cell::new(path_span)])
});
let widths = [Constraint::Min(20), Constraint::Fill(1)];
let table_highlight = Style::default().fg(Color::Blue); let table_highlight = Style::default().fg(Color::Blue);