sessionizer/
lib.rs

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
35/// Run the sessionizer command line interface.
36///
37/// # Errors
38/// Since this is the main entry, all possible errors are returned as [`color_eyre::Result`].
39pub 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}