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 &
|
nohup gitnow update > /dev/null 2>&1 &
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find the repository of choice
|
# Create a temporary chooser file
|
||||||
choice=$(gitnow "$@" --no-shell)
|
local chooser_file
|
||||||
if [[ $? -ne 0 ]]; then
|
chooser_file="$(mktemp)"
|
||||||
return $?
|
|
||||||
|
# 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
|
fi
|
||||||
|
|
||||||
# Enter local repository path
|
# If the chooser file has content, cd to the chosen path
|
||||||
cd "$(echo "$choice" | tail --lines 1)"
|
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 {
|
function gn {
|
||||||
git-now "$@"
|
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::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cache::load_repositories,
|
cache::load_repositories,
|
||||||
|
chooser::Chooser,
|
||||||
custom_command::CustomCommandApp,
|
custom_command::CustomCommandApp,
|
||||||
interactive::{InteractiveApp, Searchable},
|
interactive::{InteractiveApp, Searchable},
|
||||||
shell::ShellApp,
|
shell::ShellApp,
|
||||||
@@ -256,16 +257,16 @@ fn select_project(
|
|||||||
// --- Command implementations ---
|
// --- Command implementations ---
|
||||||
|
|
||||||
impl ProjectCommand {
|
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() {
|
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::Add(mut add)) => add.execute(app).await,
|
||||||
Some(ProjectSubcommand::Delete(mut delete)) => delete.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_dir = get_projects_dir(app);
|
||||||
let projects = list_subdirectories(&projects_dir)?;
|
let projects = list_subdirectories(&projects_dir)?;
|
||||||
|
|
||||||
@@ -301,10 +302,10 @@ impl ProjectCommand {
|
|||||||
.ok_or(anyhow::anyhow!("no project selected"))?,
|
.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?;
|
app.shell().spawn_shell_at(&project.path).await?;
|
||||||
} else {
|
} else {
|
||||||
println!("{}", project.path.display());
|
chooser.set(&project.path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -312,7 +313,7 @@ impl ProjectCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectCreateCommand {
|
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() {
|
let name = match self.name.take() {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => {
|
None => {
|
||||||
@@ -399,10 +400,10 @@ impl ProjectCreateCommand {
|
|||||||
selected_repos.len()
|
selected_repos.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
if !self.no_shell {
|
if !self.no_shell && !chooser.is_active() {
|
||||||
app.shell().spawn_shell_at(&project_path).await?;
|
app.shell().spawn_shell_at(&project_path).await?;
|
||||||
} else {
|
} else {
|
||||||
println!("{}", project_path.display());
|
chooser.set(&project_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::{collections::BTreeMap, io::IsTerminal};
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cache::{load_repositories, CacheApp},
|
cache::{load_repositories, CacheApp},
|
||||||
|
chooser::Chooser,
|
||||||
components::inline_command::InlineCommand,
|
components::inline_command::InlineCommand,
|
||||||
custom_command::CustomCommandApp,
|
custom_command::CustomCommandApp,
|
||||||
fuzzy_matcher::{FuzzyMatcher, FuzzyMatcherApp},
|
fuzzy_matcher::{FuzzyMatcher, FuzzyMatcherApp},
|
||||||
@@ -31,6 +32,7 @@ impl RootCommand {
|
|||||||
shell: bool,
|
shell: bool,
|
||||||
force_refresh: bool,
|
force_refresh: bool,
|
||||||
force_cache_update: bool,
|
force_cache_update: bool,
|
||||||
|
chooser: &Chooser,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
tracing::debug!("executing");
|
tracing::debug!("executing");
|
||||||
|
|
||||||
@@ -117,16 +119,7 @@ impl RootCommand {
|
|||||||
self.app.shell().spawn_shell(&repo).await?;
|
self.app.shell().spawn_shell(&repo).await?;
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("skipping shell for repo: {}", &repo.to_rel_path().display());
|
tracing::info!("skipping shell for repo: {}", &repo.to_rel_path().display());
|
||||||
println!(
|
chooser.set(&self.app.config.settings.projects.directory.join(repo.to_rel_path()))?;
|
||||||
"{}",
|
|
||||||
self.app
|
|
||||||
.config
|
|
||||||
.settings
|
|
||||||
.projects
|
|
||||||
.directory
|
|
||||||
.join(repo.to_rel_path())
|
|
||||||
.display()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::io::IsTerminal;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
cache::load_repositories,
|
cache::load_repositories,
|
||||||
|
chooser::Chooser,
|
||||||
components::inline_command::InlineCommand,
|
components::inline_command::InlineCommand,
|
||||||
fuzzy_matcher::FuzzyMatcherApp,
|
fuzzy_matcher::FuzzyMatcherApp,
|
||||||
interactive::{InteractiveApp, StringItem},
|
interactive::{InteractiveApp, StringItem},
|
||||||
@@ -32,7 +33,7 @@ pub struct WorktreeCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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
|
// Step 1: Load repositories
|
||||||
let repositories = load_repositories(app, !self.no_cache).await?;
|
let repositories = load_repositories(app, !self.no_cache).await?;
|
||||||
|
|
||||||
@@ -142,10 +143,10 @@ impl WorktreeCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 7: Enter shell or print path
|
// 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?;
|
app.shell().spawn_shell_at(&worktree_path).await?;
|
||||||
} else {
|
} else {
|
||||||
println!("{}", worktree_path.display());
|
chooser.set(&worktree_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use tracing_subscriber::EnvFilter;
|
|||||||
mod app;
|
mod app;
|
||||||
mod cache;
|
mod cache;
|
||||||
mod cache_codec;
|
mod cache_codec;
|
||||||
|
pub mod chooser;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod components;
|
mod components;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -48,6 +49,12 @@ struct Command {
|
|||||||
#[arg(long = "no-shell", default_value = "false")]
|
#[arg(long = "no-shell", default_value = "false")]
|
||||||
no_shell: bool,
|
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")]
|
#[arg(long = "force-refresh", default_value = "false")]
|
||||||
force_refresh: bool,
|
force_refresh: bool,
|
||||||
|
|
||||||
@@ -96,6 +103,16 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let app = app::App::new_static(config).await?;
|
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 {
|
match cli.command {
|
||||||
Some(cmd) => match cmd {
|
Some(cmd) => match cmd {
|
||||||
Commands::Init(mut shell) => {
|
Commands::Init(mut shell) => {
|
||||||
@@ -108,10 +125,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
clone.execute(app).await?;
|
clone.execute(app).await?;
|
||||||
}
|
}
|
||||||
Commands::Worktree(mut wt) => {
|
Commands::Worktree(mut wt) => {
|
||||||
wt.execute(app).await?;
|
wt.execute(app, &chooser).await?;
|
||||||
}
|
}
|
||||||
Commands::Project(mut project) => {
|
Commands::Project(mut project) => {
|
||||||
project.execute(app).await?;
|
project.execute(app, &chooser).await?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
@@ -120,9 +137,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
cli.search.as_ref(),
|
cli.search.as_ref(),
|
||||||
!cli.no_cache,
|
!cli.no_cache,
|
||||||
!cli.no_clone,
|
!cli.no_clone,
|
||||||
!cli.no_shell,
|
!no_shell,
|
||||||
cli.force_refresh,
|
cli.force_refresh,
|
||||||
cli.force_cache_update,
|
cli.force_cache_update,
|
||||||
|
&chooser,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user