sessionizer/
lib.rs

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