Compare commits

..

No commits in common. "005b80fc58788c4918a3caf3df2001a496427175" and "a135e2417f883ad573523b4a9debc269127735c0" have entirely different histories.

5 changed files with 105 additions and 152 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::dirs; use crate::state::write_projects_file;
use crate::projects::{write_projects_file, Project, Projects}; use crate::{dirs, 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.add(project); projects.list.push(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,11 +101,12 @@ 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 = projects.iter().enumerate().find_map( let idx =
projects.list.iter().enumerate().find_map(
|(idx, elem)| { |(idx, elem)| {
if elem == &project { if elem == &project {
Some(idx) Some(idx)
@ -116,7 +117,7 @@ where
); );
if let Some(idx) = idx { if let Some(idx) = idx {
let proj = projects.remove(idx); let proj = projects.list.remove(idx);
write_projects_file(projects)?; write_projects_file(projects)?;
println!("Removed {}", proj.path().display()); println!("Removed {}", proj.path().display());
} }
@ -126,13 +127,8 @@ where
fn list_projects(projects: &Projects) -> Result<()> { fn list_projects(projects: &Projects) -> Result<()> {
for (idx, project) in projects.iter().enumerate() { for (idx, project) in projects.list.iter().enumerate() {
println!( println!("{}: {} ({})", idx + 1, project.name(), project.path().display())
"{}: {} ({})",
idx + 1,
project.name(),
project.path().display()
)
} }
Ok(()) Ok(())

View File

@ -1,13 +1,15 @@
mod cli; mod cli;
mod dirs; mod dirs;
mod projects; mod state;
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 projects::read_projects_file; use state::read_projects_file;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -17,6 +19,33 @@ 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,24 +0,0 @@
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

@ -1,75 +1,9 @@
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; use crate::{dirs, Project, Projects};
#[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<()> {
@ -95,3 +29,31 @@ 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)
}

View File

@ -11,9 +11,8 @@ use ratatui::Frame;
use tracing::trace; use tracing::trace;
use tui_input::{Input, InputRequest}; use tui_input::{Input, InputRequest};
use crate::projects::{ use crate::state::{clear_selected_project_file, write_selected_project_file};
clear_selected_project_file, write_selected_project_file, Project, Projects, use crate::{Project, Projects};
};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -24,7 +23,6 @@ enum Message {
SelectNext, SelectNext,
Confirm, Confirm,
SearchUpdate, SearchUpdate,
RenameEntry,
} }
@ -39,7 +37,6 @@ 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,
@ -54,7 +51,6 @@ 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,
@ -121,19 +117,6 @@ 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,
@ -177,7 +160,6 @@ 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 => {}
_ => (), _ => (),
} }
@ -189,29 +171,37 @@ 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 {
Mode::Search => {
let layout = Layout::vertical([Constraint::Fill(1), Constraint::Max(2)]); let layout = Layout::vertical([Constraint::Fill(1), Constraint::Max(2)]);
let inner_area = block.inner(frame.area()); let inner_area = block.inner(frame.area());
let layout_rects = layout.split(inner_area); let layout_rects = layout.split(inner_area);
draw_list(state, frame, layout_rects[0]); draw_list(state, frame, layout_rects[0]);
draw_status(state, frame, layout_rects[1]); 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_status(state: &mut State, frame: &mut Frame, area: Rect) { fn draw_search(state: &mut State, frame: &mut Frame, area: Rect) {
let (prefix, input) = match &state.mode { const PROMPT_PREFIX: &str = "Search: ";
Mode::Search => ("Search: ", &state.search),
Mode::Rename => ("Rename: ", &state.rename),
};
let scroll_offset = (0, input.visual_scroll(area.width as usize) as u16); let search_input = &state.search;
let prompt = format!("{prefix}{}", input.value()); let scroll_offset = (0, search_input.visual_scroll(area.width as usize) as u16);
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 + (input.visual_cursor() + prefix.len()) as u16, area.x + (search_input.visual_cursor() + PROMPT_PREFIX.len()) as u16,
area.y, area.y,
); );
frame.set_cursor_position(cursor_pos) frame.set_cursor_position(cursor_pos)
@ -220,14 +210,14 @@ fn draw_status(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; let projects = &state.projects.list;
state.filtered_projects = if search_value.is_empty() { state.filtered_projects = if search_value.is_empty() {
projects.list().to_vec() projects.clone()
} else { } else {
let mut filtered = VecDeque::new(); let mut filtered = VecDeque::new();
for project in projects { for project in projects.iter() {
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);