feat(core): Add basic versioning for projects file

This commit is contained in:
Patrick Auernig 2025-01-06 22:18:27 +01:00
parent 005b80fc58
commit 77378d3939
6 changed files with 172 additions and 81 deletions

View File

@ -4,7 +4,7 @@ use anyhow::{ensure, Result};
use clap::Subcommand; use clap::Subcommand;
use crate::dirs; use crate::dirs;
use crate::projects::{write_projects_file, Project, Projects}; use crate::projects::{Project, Projects};
#[derive(Debug, Clone, clap::ValueEnum)] #[derive(Debug, Clone, clap::ValueEnum)]
@ -86,7 +86,7 @@ where
); );
projects.add(project); projects.add(project);
write_projects_file(projects)?; projects.write_file()?;
println!("Added {}", projects.list().last().unwrap().path().display()); println!("Added {}", projects.list().last().unwrap().path().display());
Ok(()) Ok(())
@ -117,7 +117,7 @@ where
if let Some(idx) = idx { if let Some(idx) = idx {
let proj = projects.remove(idx); let proj = projects.remove(idx);
write_projects_file(projects)?; projects.write_file()?;
println!("Removed {}", proj.path().display()); println!("Removed {}", proj.path().display());
} }

View File

@ -7,7 +7,7 @@ mod tui;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use cli::Command; use cli::Command;
use projects::read_projects_file; use projects::Projects;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -22,7 +22,7 @@ fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
let projects = read_projects_file()?; let projects = Projects::read_file()?;
match &args.cmd { match &args.cmd {
Some(cmd) => cmd.execute(projects)?, Some(cmd) => cmd.execute(projects)?,

View File

@ -1,10 +1,10 @@
mod v1; mod v1;
mod v2;
use std::fs; use std::fs;
use std::io::Write;
use anyhow::Result; use anyhow::Result;
pub use v1::Project; pub use v2::Project;
use crate::dirs; use crate::dirs;
@ -30,6 +30,39 @@ impl Projects {
pub fn add(&mut self, project: Project) { pub fn add(&mut self, project: Project) {
self.list.push(project); self.list.push(project);
} }
pub fn get_mut(&mut self, idx: usize) -> Option<&mut Project> {
self.list.get_mut(idx)
}
pub fn read_file() -> Result<Projects> {
let projects_file = dirs::data_path().join("projects");
if !projects_file.exists() {
return Ok(Projects::default());
}
let list = match v2::deserialize_project_list(&projects_file) {
Ok(list) => list,
Err(_) => v1::deserialize_project_list(&projects_file)?,
};
Ok(Projects { list })
}
pub fn write_file(&self) -> 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, &self.list)?;
Ok(())
}
} }
@ -41,57 +74,3 @@ impl<'a> IntoIterator for &'a Projects {
self.list.iter() 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<()> {
let cache_file = dirs::selected_project_file();
if fs::exists(cache_file)? {
fs::remove_file(cache_file)?;
}
Ok(())
}
pub fn write_selected_project_file(project: &Project) -> Result<()> {
let cache_file = dirs::selected_project_file();
let path = project.path();
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(cache_file)?;
write!(file, "{}", path.display())?;
Ok(())
}

View File

@ -1,20 +1,12 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use anyhow::Result;
use tracing::trace;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Project { pub struct Project {
path: PathBuf, pub 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 { impl From<PathBuf> for Project {
@ -22,3 +14,18 @@ impl From<PathBuf> for Project {
Self { path } Self { path }
} }
} }
#[tracing::instrument(skip_all)]
pub fn deserialize_project_list<P>(path: P) -> Result<Vec<super::Project>>
where
P: AsRef<Path>,
{
let file = std::fs::File::open(path)?;
trace!("Deserializing and migrating V1 project list");
let list = bincode::deserialize_from::<_, Vec<Project>>(file)
.map(|projects| projects.into_iter().map(From::from).collect())?;
Ok(list)
}

62
src/projects/v2.rs Normal file
View File

@ -0,0 +1,62 @@
use std::io;
use std::path::{Path, PathBuf};
use anyhow::Result;
use tracing::trace;
use super::v1;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Project {
pub path: PathBuf,
pub custom_name: Option<String>,
}
impl Project {
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn name(&self) -> String {
match &self.custom_name {
Some(name) => name.clone(),
None => {
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,
custom_name: None,
}
}
}
impl From<v1::Project> for Project {
fn from(value: v1::Project) -> Self {
Self {
path: value.path,
custom_name: None,
}
}
}
#[tracing::instrument(skip_all)]
pub fn deserialize_project_list<P>(path: P) -> Result<Vec<super::Project>>
where
P: AsRef<Path>
{
let file = std::fs::File::open(path)?;
trace!("Deserializing V2 project list");
let list = bincode::deserialize_from::<_, Vec<super::Project>>(file)?;
Ok(list)
}

View File

@ -1,4 +1,6 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fs;
use std::io::Write;
use std::sync::mpsc; use std::sync::mpsc;
use anyhow::Result; use anyhow::Result;
@ -11,9 +13,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::dirs;
clear_selected_project_file, write_selected_project_file, Project, Projects, use crate::projects::{Project, Projects};
};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -129,10 +130,14 @@ fn handle_key_event(state: &mut State, tx: &mut mpsc::Sender<Message>, event: Ke
state.mode = Mode::Search; state.mode = Mode::Search;
Message::Noop Message::Noop
} }
(Mode::Rename, KeyModifiers::NONE, KeyCode::Char(c)) => { (Mode::Rename, KeyModifiers::NONE | KeyModifiers::SHIFT, KeyCode::Char(c)) => {
state.rename.handle(InputRequest::InsertChar(c)); state.rename.handle(InputRequest::InsertChar(c));
Message::Noop Message::Noop
} }
(Mode::Rename, KeyModifiers::NONE, KeyCode::Backspace) => {
state.rename.handle(InputRequest::DeletePrevChar);
Message::Noop
}
(Mode::Rename, KeyModifiers::NONE, KeyCode::Enter) => Message::RenameEntry, (Mode::Rename, KeyModifiers::NONE, KeyCode::Enter) => Message::RenameEntry,
(_, _, KeyCode::Enter) => Message::Confirm, (_, _, KeyCode::Enter) => Message::Confirm,
@ -168,16 +173,28 @@ fn handle_messages(state: &mut State, rx: &mut mpsc::Receiver<Message>) -> Resul
} }
Message::Confirm => { Message::Confirm => {
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) = state.filtered_projects.get(selected) {
state.should_exit = true; state.should_exit = true;
state.selected_project = Some(project_path.clone()); state.selected_project = Some(project.clone());
}; };
} }
} }
Message::SearchUpdate => { Message::SearchUpdate => {
state.project_table.select_first(); state.project_table.select_first();
} }
Message::RenameEntry => {} Message::RenameEntry => {
if let Some(selected) = state.project_table.selected() {
if let Some(project) = state.filtered_projects.get(selected) {
if let Some(pos) = state.projects.iter().position(|p| p == project) {
if let Some(project) = state.projects.get_mut(pos) {
project.custom_name = Some(state.rename.value().to_string());
};
}
}
}
state.mode = Mode::Search;
state.projects.write_file()?;
}
_ => (), _ => (),
} }
@ -308,6 +325,32 @@ fn fuzzy_search(search: &str, text: &str) -> Option<Vec<usize>> {
} }
pub fn clear_selected_project_file() -> Result<()> {
let cache_file = dirs::selected_project_file();
if fs::exists(cache_file)? {
fs::remove_file(cache_file)?;
}
Ok(())
}
pub fn write_selected_project_file(project: &Project) -> Result<()> {
let cache_file = dirs::selected_project_file();
let path = project.path();
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(cache_file)?;
write!(file, "{}", path.display())?;
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;