From 456111acae2366f2b5467a7c73821e554d5d1c5d Mon Sep 17 00:00:00 2001
From: Patrick Auernig <patrick.auernig@mykolab.com>
Date: Wed, 27 Nov 2024 16:29:36 +0100
Subject: [PATCH] refactor: Split CLI and state into submodules

---
 src/cli.rs   | 132 +++++++++++++++++++++++++++++++
 src/main.rs  | 215 ++++-----------------------------------------------
 src/state.rs |  59 ++++++++++++++
 src/tui.rs   |  11 ++-
 4 files changed, 213 insertions(+), 204 deletions(-)
 create mode 100644 src/cli.rs
 create mode 100644 src/state.rs

diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..31aa6ae
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,132 @@
+use std::path::{self, Path, PathBuf};
+
+use anyhow::{ensure, Result};
+use clap::Subcommand;
+
+use crate::state::write_projects_file;
+use crate::{dirs, Projects};
+
+
+#[derive(Debug, Clone, clap::ValueEnum)]
+pub enum ShellType {
+    Nu,
+}
+
+impl ShellType {
+    pub fn integration(&self) -> String {
+        let template = match self {
+            Self::Nu => include_str!("./integrations/nushell.nu"),
+        };
+
+        let file_path = dirs::selected_project_file().to_str().unwrap();
+        let current_exe = std::env::current_exe().unwrap();
+        let current_exe = current_exe.to_str().unwrap();
+
+        template
+            .replace("{%SELECTED_PROJECT_FILE%}", file_path)
+            .replace("{%PROJ_EXE%}", current_exe)
+    }
+}
+
+
+#[derive(Debug, Subcommand)]
+pub enum Command {
+    /// Add a project
+    Add { path: PathBuf },
+
+    /// Remove a project
+    Remove { path: PathBuf },
+
+    /// List existing projects
+    List,
+
+    /// Initialize shell integration
+    Init { shell: ShellType },
+}
+
+impl Command {
+    pub fn execute(&self, mut projects: Projects) -> Result<()> {
+        match self {
+            Command::Add { path } => {
+                add_project(&mut projects, path)?;
+            }
+            Command::Remove { path } => {
+                remove_project(&mut projects, path)?;
+            }
+            Command::List => {
+                list_projects(&projects)?;
+            }
+            Command::Init { shell } => {
+                print_shell_integration(shell);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+
+fn print_shell_integration(shell: &ShellType) {
+    println!("{}", shell.integration());
+}
+
+
+fn add_project<P>(projects: &mut Projects, path: P) -> Result<()>
+where
+    P: AsRef<Path>,
+{
+    let path = path::absolute(path)?;
+
+    ensure!(path.is_dir(), "Project path does not exists");
+    ensure!(
+        !projects.list.contains(&path.to_path_buf()),
+        "Project path already registered"
+    );
+
+    projects.list.push(path);
+    write_projects_file(projects)?;
+    println!("Added {}", projects.list.last().unwrap().display());
+
+    Ok(())
+}
+
+
+fn remove_project<P>(projects: &mut Projects, path: P) -> Result<()>
+where
+    P: AsRef<Path>,
+{
+    let path = path::absolute(path)?;
+
+    ensure!(
+        projects.list.contains(&path.to_path_buf()),
+        "Project path not in registry"
+    );
+
+    let idx =
+        projects.list.iter().enumerate().find_map(
+            |(idx, elem)| {
+                if elem == &path {
+                    Some(idx)
+                } else {
+                    None
+                }
+            },
+        );
+
+    if let Some(idx) = idx {
+        let proj = projects.list.remove(idx);
+        write_projects_file(projects)?;
+        println!("Removed {}", proj.display());
+    }
+
+    Ok(())
+}
+
+
+fn list_projects(projects: &Projects) -> Result<()> {
+    for project in &projects.list {
+        println!("{project:?}")
+    }
+
+    Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
index 50dbaa2..9bd7e99 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,62 +1,27 @@
+mod cli;
 mod dirs;
+mod state;
 mod tui;
 
 
-use std::io::Write;
-use std::path::{Path, PathBuf};
-use std::{fs, path};
+use std::path::PathBuf;
 
-use anyhow::{ensure, Result};
-use clap::{Parser, Subcommand};
-
-
-#[derive(Debug, Clone, clap::ValueEnum)]
-enum ShellType {
-    Nu,
-}
-
-impl ShellType {
-    pub fn integration(&self) -> String {
-        let template = match self {
-            Self::Nu => include_str!("./integrations/nushell.nu"),
-        };
-
-        let file_path = dbg!(dirs::selected_project_file().to_str().unwrap());
-        let current_exe = std::env::current_exe().unwrap();
-        let current_exe = current_exe.to_str().unwrap();
-
-        template
-            .replace("{%SELECTED_PROJECT_FILE%}", file_path)
-            .replace("{%PROJ_EXE%}", current_exe)
-    }
-}
+use anyhow::Result;
+use clap::Parser;
+use cli::Command;
+use state::read_projects_file;
 
 
 #[derive(Debug, Parser)]
-pub struct Args {
+struct Args {
     #[command(subcommand)]
-    cmd: Option<Command>,
-}
-
-#[derive(Debug, Subcommand)]
-enum Command {
-    /// Add a project
-    Add { path: PathBuf },
-
-    /// Remove a project
-    Remove { path: PathBuf },
-
-    /// List existing projects
-    List,
-
-    /// Initialize shell integration
-    Init { shell: ShellType },
+    pub cmd: Option<Command>,
 }
 
 
 #[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
 struct Projects {
-    list: Vec<PathBuf>,
+    pub list: Vec<PathBuf>,
 }
 
 
@@ -65,164 +30,10 @@ fn main() -> Result<()> {
 
     let projects = read_projects_file()?;
 
-    let result = match &args.cmd {
-        Some(cmd) => handle_subcommands(projects, cmd),
-        None => run_tui(projects),
+    match &args.cmd {
+        Some(cmd) => cmd.execute(projects)?,
+        None => tui::run(projects)?,
     };
 
-    if let Err(err) = result {
-        eprintln!("{err}");
-        std::process::exit(1);
-    }
-
     Ok(())
 }
-
-
-fn run_tui(projects: Projects) -> Result<()> {
-    clear_selected_project_file()?;
-    let selected_project = tui::run(projects)?;
-
-    if let Some(selected_project) = selected_project {
-        write_selected_project_file(selected_project)?
-    }
-
-    Ok(())
-}
-
-
-fn handle_subcommands(mut projects: Projects, cmd: &Command) -> Result<()> {
-    match cmd {
-        Command::Add { path } => {
-            add_project(&mut projects, path)?;
-        }
-        Command::Remove { path } => {
-            remove_project(&mut projects, path)?;
-        }
-        Command::List => {
-            list_projects(&projects)?;
-        }
-        Command::Init { shell } => {
-            print_shell_integration(shell);
-        }
-    }
-
-    Ok(())
-}
-
-
-fn print_shell_integration(shell: &ShellType) {
-    println!("{}", shell.integration());
-}
-
-
-fn add_project<P>(projects: &mut Projects, path: P) -> Result<()>
-where
-    P: AsRef<Path>,
-{
-    let path = path::absolute(path)?;
-
-    ensure!(path.is_dir(), "Project path does not exists");
-    ensure!(
-        !projects.list.contains(&path.to_path_buf()),
-        "Project path already registered"
-    );
-
-    projects.list.push(path);
-    write_projects_file(projects)?;
-    println!("Added {}", projects.list.last().unwrap().display());
-
-    Ok(())
-}
-
-
-fn remove_project<P>(projects: &mut Projects, path: P) -> Result<()>
-where
-    P: AsRef<Path>,
-{
-    let path = path::absolute(path)?;
-
-    ensure!(
-        projects.list.contains(&path.to_path_buf()),
-        "Project path not in registry"
-    );
-
-    let idx =
-        projects.list.iter().enumerate().find_map(
-            |(idx, elem)| {
-                if elem == &path {
-                    Some(idx)
-                } else {
-                    None
-                }
-            },
-        );
-
-    if let Some(idx) = idx {
-        let proj = projects.list.remove(idx);
-        write_projects_file(projects)?;
-        println!("Removed {}", proj.display());
-    }
-
-    Ok(())
-}
-
-
-fn list_projects(projects: &Projects) -> Result<()> {
-    for project in &projects.list {
-        println!("{project:?}")
-    }
-
-    Ok(())
-}
-
-fn clear_selected_project_file() -> Result<()> {
-    let cache_file = dirs::selected_project_file();
-
-    if fs::exists(cache_file)? {
-        fs::remove_file(cache_file)?;
-    }
-
-    Ok(())
-}
-
-fn write_selected_project_file(path: PathBuf) -> Result<()> {
-    let cache_file = dirs::selected_project_file();
-
-    let mut file = fs::OpenOptions::new()
-        .write(true)
-        .create(true)
-        .truncate(true)
-        .open(cache_file)?;
-    write!(file, "{}", path.display())?;
-
-    Ok(())
-}
-
-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(())
-}
-
-
-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)
-}
diff --git a/src/state.rs b/src/state.rs
new file mode 100644
index 0000000..7826943
--- /dev/null
+++ b/src/state.rs
@@ -0,0 +1,59 @@
+use std::fs;
+use std::io::Write;
+use std::path::PathBuf;
+
+use anyhow::Result;
+
+use crate::{dirs, Projects};
+
+
+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(path: PathBuf) -> Result<()> {
+    let cache_file = dirs::selected_project_file();
+
+    let mut file = fs::OpenOptions::new()
+        .write(true)
+        .create(true)
+        .truncate(true)
+        .open(cache_file)?;
+    write!(file, "{}", path.display())?;
+
+    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)
+}
diff --git a/src/tui.rs b/src/tui.rs
index f2ea3ce..f2b0a50 100644
--- a/src/tui.rs
+++ b/src/tui.rs
@@ -9,6 +9,7 @@ use ratatui::style::{Color, Style};
 use ratatui::widgets::{Block, Borders, Cell, Row, Table, TableState};
 use ratatui::Frame;
 
+use crate::state::{clear_selected_project_file, write_selected_project_file};
 use crate::Projects;
 
 
@@ -43,7 +44,9 @@ impl State {
 }
 
 
-pub fn run(projects: Projects) -> Result<Option<PathBuf>> {
+pub fn run(projects: Projects) -> Result<()> {
+    clear_selected_project_file()?;
+
     let mut term = ratatui::init();
     let mut state = State::new(projects);
 
@@ -69,7 +72,11 @@ pub fn run(projects: Projects) -> Result<Option<PathBuf>> {
 
     ratatui::restore();
 
-    Ok(state.selected_project)
+    if let Some(selected_project) = state.selected_project {
+        write_selected_project_file(selected_project)?
+    }
+
+    Ok(())
 }