feat(core): Add basic versioning for projects file
This commit is contained in:
parent
005b80fc58
commit
77378d3939
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)?,
|
||||||
|
@ -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(())
|
|
||||||
}
|
|
||||||
|
@ -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
62
src/projects/v2.rs
Normal 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)
|
||||||
|
}
|
57
src/tui.rs
57
src/tui.rs
@ -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::*;
|
||||||
|
Loading…
Reference in New Issue
Block a user