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