diff --git a/crates/gitnow/include/shell/zsh.sh b/crates/gitnow/include/shell/zsh.sh index 4eaa5c2..792e705 100644 --- a/crates/gitnow/include/shell/zsh.sh +++ b/crates/gitnow/include/shell/zsh.sh @@ -4,19 +4,30 @@ function git-now { nohup gitnow update > /dev/null 2>&1 & ) - # Find the repository of choice - choice=$(gitnow "$@" --no-shell) - if [[ $? -ne 0 ]]; then - return $? + # Create a temporary chooser file + local chooser_file + chooser_file="$(mktemp)" + + # Run gitnow with the chooser file + GITNOW_CHOOSER_FILE="$chooser_file" gitnow "$@" + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + rm -f "$chooser_file" + return $exit_code fi - # Enter local repository path - cd "$(echo "$choice" | tail --lines 1)" + # If the chooser file has content, cd to the chosen path + if [[ -s "$chooser_file" ]]; then + local target + target="$(cat "$chooser_file")" + rm -f "$chooser_file" + cd "$target" + else + rm -f "$chooser_file" + fi } function gn { git-now "$@" - if [[ $? -ne 0 ]]; then - return $? - fi } diff --git a/crates/gitnow/src/chooser.rs b/crates/gitnow/src/chooser.rs new file mode 100644 index 0000000..ec9b7ca --- /dev/null +++ b/crates/gitnow/src/chooser.rs @@ -0,0 +1,34 @@ +use std::path::{Path, PathBuf}; + +/// Manages an optional chooser file that the shell wrapper reads after gitnow +/// exits. When active, the selected directory path is written to the file +/// instead of being printed to stdout. +#[derive(Debug, Default)] +pub struct Chooser { + path: Option, +} + +impl Chooser { + pub fn new(path: PathBuf) -> Self { + Self { path: Some(path) } + } + + /// Returns `true` when a chooser file has been configured. + pub fn is_active(&self) -> bool { + self.path.is_some() + } + + /// Write `dir` to the chooser file. If no chooser file is configured the + /// path is printed to stdout (preserving the old `--no-shell` behaviour). + pub fn set(&self, dir: &Path) -> anyhow::Result<()> { + match &self.path { + Some(chooser_path) => { + std::fs::write(chooser_path, dir.display().to_string())?; + } + None => { + println!("{}", dir.display()); + } + } + Ok(()) + } +} diff --git a/crates/gitnow/src/commands/project.rs b/crates/gitnow/src/commands/project.rs index 3e0fcd7..186a752 100644 --- a/crates/gitnow/src/commands/project.rs +++ b/crates/gitnow/src/commands/project.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use crate::{ app::App, cache::load_repositories, + chooser::Chooser, custom_command::CustomCommandApp, interactive::{InteractiveApp, Searchable}, shell::ShellApp, @@ -256,16 +257,16 @@ fn select_project( // --- Command implementations --- impl ProjectCommand { - pub async fn execute(&mut self, app: &'static App) -> anyhow::Result<()> { + pub async fn execute(&mut self, app: &'static App, chooser: &Chooser) -> anyhow::Result<()> { match self.command.take() { - Some(ProjectSubcommand::Create(mut create)) => create.execute(app).await, + Some(ProjectSubcommand::Create(mut create)) => create.execute(app, chooser).await, Some(ProjectSubcommand::Add(mut add)) => add.execute(app).await, Some(ProjectSubcommand::Delete(mut delete)) => delete.execute(app).await, - None => self.open_existing(app).await, + None => self.open_existing(app, chooser).await, } } - async fn open_existing(&self, app: &'static App) -> anyhow::Result<()> { + async fn open_existing(&self, app: &'static App, chooser: &Chooser) -> anyhow::Result<()> { let projects_dir = get_projects_dir(app); let projects = list_subdirectories(&projects_dir)?; @@ -301,10 +302,10 @@ impl ProjectCommand { .ok_or(anyhow::anyhow!("no project selected"))?, }; - if !self.no_shell { + if !self.no_shell && !chooser.is_active() { app.shell().spawn_shell_at(&project.path).await?; } else { - println!("{}", project.path.display()); + chooser.set(&project.path)?; } Ok(()) @@ -312,7 +313,7 @@ impl ProjectCommand { } impl ProjectCreateCommand { - async fn execute(&mut self, app: &'static App) -> anyhow::Result<()> { + async fn execute(&mut self, app: &'static App, chooser: &Chooser) -> anyhow::Result<()> { let name = match self.name.take() { Some(n) => n, None => { @@ -399,10 +400,10 @@ impl ProjectCreateCommand { selected_repos.len() ); - if !self.no_shell { + if !self.no_shell && !chooser.is_active() { app.shell().spawn_shell_at(&project_path).await?; } else { - println!("{}", project_path.display()); + chooser.set(&project_path)?; } Ok(()) diff --git a/crates/gitnow/src/commands/root.rs b/crates/gitnow/src/commands/root.rs index 3a8a0c9..d4ab3f7 100644 --- a/crates/gitnow/src/commands/root.rs +++ b/crates/gitnow/src/commands/root.rs @@ -3,6 +3,7 @@ use std::{collections::BTreeMap, io::IsTerminal}; use crate::{ app::App, cache::{load_repositories, CacheApp}, + chooser::Chooser, components::inline_command::InlineCommand, custom_command::CustomCommandApp, fuzzy_matcher::{FuzzyMatcher, FuzzyMatcherApp}, @@ -31,6 +32,7 @@ impl RootCommand { shell: bool, force_refresh: bool, force_cache_update: bool, + chooser: &Chooser, ) -> anyhow::Result<()> { tracing::debug!("executing"); @@ -117,16 +119,7 @@ impl RootCommand { self.app.shell().spawn_shell(&repo).await?; } else { tracing::info!("skipping shell for repo: {}", &repo.to_rel_path().display()); - println!( - "{}", - self.app - .config - .settings - .projects - .directory - .join(repo.to_rel_path()) - .display() - ); + chooser.set(&self.app.config.settings.projects.directory.join(repo.to_rel_path()))?; } Ok(()) diff --git a/crates/gitnow/src/commands/worktree.rs b/crates/gitnow/src/commands/worktree.rs index bf860c5..98700c2 100644 --- a/crates/gitnow/src/commands/worktree.rs +++ b/crates/gitnow/src/commands/worktree.rs @@ -3,6 +3,7 @@ use std::io::IsTerminal; use crate::{ app::App, cache::load_repositories, + chooser::Chooser, components::inline_command::InlineCommand, fuzzy_matcher::FuzzyMatcherApp, interactive::{InteractiveApp, StringItem}, @@ -32,7 +33,7 @@ pub struct WorktreeCommand { } impl WorktreeCommand { - pub async fn execute(&mut self, app: &'static App) -> anyhow::Result<()> { + pub async fn execute(&mut self, app: &'static App, chooser: &Chooser) -> anyhow::Result<()> { // Step 1: Load repositories let repositories = load_repositories(app, !self.no_cache).await?; @@ -142,10 +143,10 @@ impl WorktreeCommand { } // Step 7: Enter shell or print path - if !self.no_shell { + if !self.no_shell && !chooser.is_active() { app.shell().spawn_shell_at(&worktree_path).await?; } else { - println!("{}", worktree_path.display()); + chooser.set(&worktree_path)?; } Ok(()) diff --git a/crates/gitnow/src/main.rs b/crates/gitnow/src/main.rs index 75ae32e..69a7c1f 100644 --- a/crates/gitnow/src/main.rs +++ b/crates/gitnow/src/main.rs @@ -13,6 +13,7 @@ use tracing_subscriber::EnvFilter; mod app; mod cache; mod cache_codec; +pub mod chooser; mod commands; mod components; mod config; @@ -48,6 +49,12 @@ struct Command { #[arg(long = "no-shell", default_value = "false")] no_shell: bool, + /// Path to a chooser file; if set, the selected directory path is written + /// to this file instead of spawning a shell or printing to stdout. + /// Can also be set via the GITNOW_CHOOSER_FILE environment variable. + #[arg(long = "chooser-file", global = true, env = "GITNOW_CHOOSER_FILE")] + chooser_file: Option, + #[arg(long = "force-refresh", default_value = "false")] force_refresh: bool, @@ -96,6 +103,16 @@ async fn main() -> anyhow::Result<()> { let app = app::App::new_static(config).await?; + // When a chooser file is provided, it implies --no-shell behaviour: + // the selected path is written to the file and no interactive shell is + // spawned. The calling shell wrapper is responsible for reading the + // file and changing directory. + let chooser = cli + .chooser_file + .map(chooser::Chooser::new) + .unwrap_or_default(); + let no_shell = cli.no_shell || chooser.is_active(); + match cli.command { Some(cmd) => match cmd { Commands::Init(mut shell) => { @@ -108,10 +125,10 @@ async fn main() -> anyhow::Result<()> { clone.execute(app).await?; } Commands::Worktree(mut wt) => { - wt.execute(app).await?; + wt.execute(app, &chooser).await?; } Commands::Project(mut project) => { - project.execute(app).await?; + project.execute(app, &chooser).await?; } }, None => { @@ -120,9 +137,10 @@ async fn main() -> anyhow::Result<()> { cli.search.as_ref(), !cli.no_cache, !cli.no_clone, - !cli.no_shell, + !no_shell, cli.force_refresh, cli.force_cache_update, + &chooser, ) .await?; }