Compare commits
2 Commits
a135e2417f
...
005b80fc58
Author | SHA1 | Date | |
---|---|---|---|
005b80fc58 | |||
4ea4644b90 |
26
src/cli.rs
26
src/cli.rs
@ -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,12 +101,11 @@ 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)
|
||||||
@ -117,7 +116,7 @@ where
|
|||||||
);
|
);
|
||||||
|
|
||||||
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(())
|
||||||
|
33
src/main.rs
33
src/main.rs
@ -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()?;
|
||||||
|
|
||||||
|
@ -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
24
src/projects/v1.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
54
src/tui.rs
54
src/tui.rs
@ -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 {
|
|
||||||
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_search(state, frame, layout_rects[1]);
|
draw_status(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);
|
||||||
|
Loading…
Reference in New Issue
Block a user