Compare commits

..

2 Commits

5 changed files with 152 additions and 105 deletions

View File

@ -3,8 +3,8 @@ use std::path::{self, Path, PathBuf};
use anyhow::{ensure, Result}; use anyhow::{ensure, Result};
use clap::Subcommand; use clap::Subcommand;
use crate::state::write_projects_file; use crate::dirs;
use crate::{dirs, Project, Projects}; use crate::projects::{write_projects_file, Project, Projects};
#[derive(Debug, Clone, clap::ValueEnum)] #[derive(Debug, Clone, clap::ValueEnum)]
@ -81,13 +81,13 @@ where
let project = Project::from(path); let project = Project::from(path);
ensure!( ensure!(
!projects.list.contains(&project), !projects.list().contains(&project),
"Project path already registered" "Project path already registered"
); );
projects.list.push(project); projects.add(project);
write_projects_file(projects)?; write_projects_file(projects)?;
println!("Added {}", projects.list.last().unwrap().path().display()); println!("Added {}", projects.list().last().unwrap().path().display());
Ok(()) Ok(())
} }
@ -101,23 +101,22 @@ where
let project = Project::from(path); let project = Project::from(path);
ensure!( ensure!(
projects.list.contains(&project), projects.list().contains(&project),
"Project path not in registry" "Project path not in registry"
); );
let idx = let idx = projects.iter().enumerate().find_map(
projects.list.iter().enumerate().find_map( |(idx, elem)| {
|(idx, elem)| { if elem == &project {
if elem == &project { Some(idx)
Some(idx) } else {
} else { None
None }
} },
}, );
);
if let Some(idx) = idx { if let Some(idx) = idx {
let proj = projects.list.remove(idx); let proj = projects.remove(idx);
write_projects_file(projects)?; write_projects_file(projects)?;
println!("Removed {}", proj.path().display()); println!("Removed {}", proj.path().display());
} }
@ -127,8 +126,13 @@ 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.iter().enumerate() {
println!("{}: {} ({})", idx + 1, project.name(), project.path().display()) println!(
"{}: {} ({})",
idx + 1,
project.name(),
project.path().display()
)
} }
Ok(()) Ok(())

View File

@ -1,15 +1,13 @@
mod cli; mod cli;
mod dirs; mod dirs;
mod state; mod projects;
mod tui; mod tui;
use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use cli::Command; use cli::Command;
use state::read_projects_file; use projects::read_projects_file;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -19,33 +17,6 @@ struct Args {
} }
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
struct Projects {
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)
}
}
fn main() -> Result<()> { fn main() -> Result<()> {
init_tracing()?; init_tracing()?;

View File

@ -1,9 +1,75 @@
mod v1;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use anyhow::Result; use anyhow::Result;
pub use v1::Project;
use crate::{dirs, Project, Projects}; use crate::dirs;
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Projects {
list: Vec<Project>,
}
impl Projects {
pub fn iter(&self) -> std::slice::Iter<'_, Project> {
self.list.iter()
}
pub fn list(&self) -> &[Project] {
&self.list
}
pub fn remove(&mut self, idx: usize) -> Project {
self.list.remove(idx)
}
pub fn add(&mut self, project: Project) {
self.list.push(project);
}
}
impl<'a> IntoIterator for &'a Projects {
type IntoIter = std::slice::Iter<'a, Project>;
type Item = &'a Project;
fn into_iter(self) -> Self::IntoIter {
self.list.iter()
}
}
pub fn read_projects_file() -> Result<Projects> {
let projects_file = dirs::data_path().join("projects");
if !projects_file.exists() {
return Ok(Projects::default());
}
let file = std::fs::File::open(projects_file)?;
let projects = bincode::deserialize_from(file)?;
Ok(projects)
}
pub fn write_projects_file(projects: &Projects) -> Result<()> {
let projects_file = dirs::data_path().join("projects");
let file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(projects_file)?;
bincode::serialize_into(file, projects)?;
Ok(())
}
pub fn clear_selected_project_file() -> Result<()> { pub fn clear_selected_project_file() -> Result<()> {
@ -29,31 +95,3 @@ pub fn write_selected_project_file(project: &Project) -> Result<()> {
Ok(()) Ok(())
} }
pub fn write_projects_file(projects: &Projects) -> Result<()> {
let projects_file = dirs::data_path().join("projects");
let file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(projects_file)?;
bincode::serialize_into(file, projects)?;
Ok(())
}
pub fn read_projects_file() -> Result<Projects> {
let projects_file = dirs::data_path().join("projects");
if !projects_file.exists() {
return Ok(Projects::default());
}
let file = std::fs::File::open(projects_file)?;
let projects = bincode::deserialize_from(file)?;
Ok(projects)
}

24
src/projects/v1.rs Normal file
View File

@ -0,0 +1,24 @@
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Project {
path: PathBuf,
}
impl Project {
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn name(&self) -> String {
let name = self.path.file_name().unwrap();
name.to_string_lossy().to_string()
}
}
impl From<PathBuf> for Project {
fn from(path: PathBuf) -> Self {
Self { path }
}
}

View File

@ -11,8 +11,9 @@ 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::projects::{
use crate::{Project, Projects}; clear_selected_project_file, write_selected_project_file, Project, Projects,
};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -23,6 +24,7 @@ enum Message {
SelectNext, SelectNext,
Confirm, Confirm,
SearchUpdate, SearchUpdate,
RenameEntry,
} }
@ -37,6 +39,7 @@ enum Mode {
struct State { struct State {
projects: Projects, projects: Projects,
search: Input, search: Input,
rename: Input,
mode: Mode, mode: Mode,
should_exit: bool, should_exit: bool,
project_table: TableState, project_table: TableState,
@ -51,6 +54,7 @@ impl State {
Self { Self {
projects, projects,
search: Input::default(), search: Input::default(),
rename: Input::default(),
mode: Mode::default(), mode: Mode::default(),
should_exit: false, should_exit: false,
project_table, project_table,
@ -117,6 +121,19 @@ fn handle_key_event(state: &mut State, tx: &mut mpsc::Sender<Message>, event: Ke
state.search.handle(InputRequest::DeletePrevChar); state.search.handle(InputRequest::DeletePrevChar);
Message::SearchUpdate Message::SearchUpdate
} }
(Mode::Search, KeyModifiers::CONTROL, KeyCode::Char('r')) => {
state.mode = Mode::Rename;
Message::Noop
}
(Mode::Rename, KeyModifiers::NONE, KeyCode::Esc) => {
state.mode = Mode::Search;
Message::Noop
}
(Mode::Rename, KeyModifiers::NONE, KeyCode::Char(c)) => {
state.rename.handle(InputRequest::InsertChar(c));
Message::Noop
}
(Mode::Rename, KeyModifiers::NONE, KeyCode::Enter) => Message::RenameEntry,
(_, _, KeyCode::Enter) => Message::Confirm, (_, _, KeyCode::Enter) => Message::Confirm,
_ => Message::Noop, _ => Message::Noop,
@ -160,6 +177,7 @@ fn handle_messages(state: &mut State, rx: &mut mpsc::Receiver<Message>) -> Resul
Message::SearchUpdate => { Message::SearchUpdate => {
state.project_table.select_first(); state.project_table.select_first();
} }
Message::RenameEntry => {}
_ => (), _ => (),
} }
@ -171,37 +189,29 @@ fn draw(state: &mut State, frame: &mut Frame) {
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
frame.render_widget(&block, frame.area()); frame.render_widget(&block, frame.area());
match state.mode { let layout = Layout::vertical([Constraint::Fill(1), Constraint::Max(2)]);
Mode::Search => { let inner_area = block.inner(frame.area());
let layout = Layout::vertical([Constraint::Fill(1), Constraint::Max(2)]); let layout_rects = layout.split(inner_area);
let inner_area = block.inner(frame.area()); draw_list(state, frame, layout_rects[0]);
let layout_rects = layout.split(inner_area); draw_status(state, frame, layout_rects[1]);
draw_list(state, frame, layout_rects[0]);
draw_search(state, frame, layout_rects[1]);
}
Mode::Rename => {
let layout = Layout::vertical([Constraint::Fill(1)]);
let inner_area = block.inner(frame.area());
let layout_rects = layout.split(inner_area);
draw_list(state, frame, layout_rects[0]);
}
}
} }
fn draw_search(state: &mut State, frame: &mut Frame, area: Rect) { fn draw_status(state: &mut State, frame: &mut Frame, area: Rect) {
const PROMPT_PREFIX: &str = "Search: "; let (prefix, input) = match &state.mode {
Mode::Search => ("Search: ", &state.search),
Mode::Rename => ("Rename: ", &state.rename),
};
let search_input = &state.search; let scroll_offset = (0, input.visual_scroll(area.width as usize) as u16);
let scroll_offset = (0, search_input.visual_scroll(area.width as usize) as u16); let prompt = format!("{prefix}{}", input.value());
let prompt = format!("{PROMPT_PREFIX}{}", search_input.value());
let search = Paragraph::new(prompt).scroll(scroll_offset); let search = Paragraph::new(prompt).scroll(scroll_offset);
frame.render_widget(search, area); frame.render_widget(search, area);
let cursor_pos = ( let cursor_pos = (
area.x + (search_input.visual_cursor() + PROMPT_PREFIX.len()) as u16, area.x + (input.visual_cursor() + prefix.len()) as u16,
area.y, area.y,
); );
frame.set_cursor_position(cursor_pos) frame.set_cursor_position(cursor_pos)
@ -210,14 +220,14 @@ fn draw_search(state: &mut State, frame: &mut Frame, area: Rect) {
fn draw_list(state: &mut State, frame: &mut Frame, area: Rect) { fn draw_list(state: &mut State, frame: &mut Frame, area: Rect) {
let search_value = state.search.value(); let search_value = state.search.value();
let projects = &state.projects.list; let projects = &state.projects;
state.filtered_projects = if search_value.is_empty() { state.filtered_projects = if search_value.is_empty() {
projects.clone() projects.list().to_vec()
} else { } else {
let mut filtered = VecDeque::new(); let mut filtered = VecDeque::new();
for project in projects.iter() { for project in projects {
match fuzzy_search(search_value, &project.name()) { match fuzzy_search(search_value, &project.name()) {
Some(indices) => { Some(indices) => {
trace!(?search_value, ?project, ?indices); trace!(?search_value, ?project, ?indices);