feat: add chooser file mechanism for shell directory switching
Some checks failed
continuous-integration/drone/push Build encountered an error
Some checks failed
continuous-integration/drone/push Build encountered an error
Replace stdout-based path capture with a temporary chooser file that the shell wrapper reads after gitnow exits. Commands that select a directory write to the file; commands that don't (e.g. project delete) leave it empty, so the shell only cd's when appropriate. The chooser file path can be set via --chooser-file flag or GITNOW_CHOOSER_FILE env var.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
34
crates/gitnow/src/chooser.rs
Normal file
34
crates/gitnow/src/chooser.rs
Normal file
@@ -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<PathBuf>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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<PathBuf>,
|
||||
|
||||
#[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?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user