1use std::{
2 env::current_exe,
3 ffi::OsString,
4 os::unix::process::CommandExt as _,
5 process::Command,
6 sync::mpsc::{self, SyncSender},
7 thread,
8};
9
10use color_eyre::eyre::eyre;
11use panic_message::panic_message;
12use tracing::{debug, info, trace, warn};
13
14pub use crate::args::{Action as CliAction, Scope, Search};
15pub use color_eyre::Result;
16
17use crate::{
18 action::Action,
19 args::Config,
20 entry::{Entry, Project, TmuxSession},
21 init::{Init, WindowCommand, create_config_file, edit_config_file, validate_config_file},
22 project::find_projects,
23 selection::{Selection, prompt_user},
24};
25
26mod action;
27mod args;
28mod config;
29mod entry;
30mod init;
31mod project;
32mod selection;
33mod session;
34
35pub fn run(action: CliAction) -> Result<()> {
40 match action {
41 CliAction::Shell(search_args) => run_shell(search_args)?,
42 CliAction::Search(args) => run_search(args)?,
43 CliAction::Config(Config::Init) => create_config_file()?,
44 CliAction::Config(Config::Validate { insecure }) => validate_config_file(!insecure)?,
45 CliAction::Config(Config::Edit { insecure }) => edit_config_file(!insecure)?,
46 }
47
48 Ok(())
49}
50
51fn run_shell(search_args: Vec<OsString>) -> Result<()> {
52 let bin = current_exe()?;
53 let mut search_args = Some(search_args);
54 loop {
55 let mut cmd = Command::new(bin.as_path());
56 _ = cmd.arg("--empty-exit-code=42");
57
58 if let Some(search_args) = search_args.take() {
59 _ = cmd.args(search_args);
60 }
61
62 let mut child = cmd.spawn()?;
63 let exit = child.wait()?;
64 if !exit.success() {
65 let exit = exit.code().unwrap_or(126);
66 if exit == 42 {
67 break Ok(());
68 }
69 std::process::exit(exit);
70 }
71 }
72}
73
74fn run_search(
75 Search {
76 dry_run,
77 skip_init,
78 insecure,
79 use_color,
80 empty_exit_code,
81 projects_config,
82 scope,
83 query,
84 }: Search,
85) -> Result<()> {
86 let (tx, entries) = spawn_collector();
87
88 if scope.check_projects() {
89 find_projects(projects_config, &tx)?;
90 }
91
92 let tmux_ls = (scope.check_tmux()).then(|| find_tmux_sessions(tx.clone()));
93
94 let _ = tmux_ls.map(Thread::join).transpose()?;
95
96 drop(tx);
97
98 let entries = entries.join()?;
99
100 debug!("found {} entries", entries.len());
101
102 let selection = Selection {
103 entries,
104 query,
105 color: use_color,
106 };
107
108 let command = prompt_user(selection).and_then(|e| {
109 e.map(|e| {
110 debug!(entry =? e, "selected");
111 apply_entry(e, !skip_init, !insecure)
112 })
113 .transpose()
114 })?;
115
116 let Some(mut cmd) = command else {
117 std::process::exit(empty_exit_code);
118 };
119
120 info!(?cmd);
121
122 if dry_run {
123 let cmd = shlex::try_join(
124 std::iter::once(cmd.get_program())
125 .chain(cmd.get_args())
126 .filter_map(|s| s.to_str()),
127 )?;
128
129 println!("{cmd}");
130 return Ok(());
131 }
132
133 Err(cmd.exec().into())
134}
135
136fn spawn_collector() -> (SyncSender<Entry>, Thread<Vec<Entry>>) {
137 let (tx, rx) = mpsc::sync_channel::<Entry>(16);
138 let thread = thread::spawn(move || {
139 entry::process_entries(rx.into_iter().inspect(|entry| {
140 trace!(?entry, "Entry for possible selection");
141 }))
142 });
143
144 (tx, Thread::new("collector", thread))
145}
146
147fn find_tmux_sessions(tx: SyncSender<Entry>) -> Thread<()> {
148 let thread = thread::spawn(move || session::fetch_tmux_sessions(|entry| Ok(tx.send(entry)?)));
149
150 Thread::new("tmux ls", thread)
151}
152
153fn apply_entry(entry: Entry, load_file: bool, secure: bool) -> Result<Command> {
154 let action = match entry {
155 Entry::Project(project) => {
156 let on_init = load_file
157 .then(|| init::find_action(&project.root, secure))
158 .flatten()
159 .transpose()?
160 .unwrap_or_default();
161 Action::Create {
162 name: project.name,
163 root: project.root,
164 on_init,
165 }
166 }
167 Entry::Session(session) => Action::Attach { name: session.name },
168 };
169
170 action::cmd(action)
171}
172
173struct Thread<T> {
174 name: &'static str,
175 thread: thread::JoinHandle<T>,
176}
177
178impl<T> Thread<T> {
179 const fn new(name: &'static str, thread: thread::JoinHandle<T>) -> Self {
180 Self { name, thread }
181 }
182
183 fn join(self) -> Result<T> {
184 self.thread
185 .join()
186 .map_err(|e| eyre!("{} panicked: {}", self.name, panic_message(&e)))
187 }
188}