together_rs/
lib.rs

1use std::sync::{Arc, Mutex};
2
3use config::StartTogetherOptions;
4use errors::TogetherResult;
5use manager::ProcessAction;
6use terminal_ext::TerminalExt;
7
8pub mod config;
9pub mod errors;
10pub mod kb;
11pub mod manager;
12pub mod process;
13pub mod terminal;
14pub mod terminal_ext;
15
16pub fn start(options: StartTogetherOptions) -> TogetherResult<()> {
17    let StartTogetherOptions {
18        config,
19        working_directory,
20        ..
21    } = &options;
22
23    let manager = manager::ProcessManager::new()
24        .with_raw_mode(config.start_options.raw)
25        .with_exit_on_error(config.start_options.exit_on_error)
26        .with_quit_on_completion(config.start_options.quit_on_completion)
27        .with_working_directory(working_directory.to_owned())
28        .start();
29
30    let sender = manager.subscribe();
31    handle_ctrl_signal(sender);
32
33    let selected_commands = collect_together_commands(&manager, &options)?;
34
35    if config.start_options.no_init {
36        log!("Skipping startup commands...");
37    } else {
38        execute_startup_commands(&manager, &config)?;
39    }
40
41    if config.start_options.init_only {
42        log!("Finished running startup commands, waiting for user input... (press '?' for help)");
43    } else {
44        execute_together_commands(&manager, selected_commands)?;
45    }
46
47    let sender = manager.subscribe();
48    kb::block_for_user_input(&options, sender)?;
49
50    std::mem::drop(manager);
51    Ok(())
52}
53
54pub fn handle_ctrl_signal(sender: manager::ProcessManagerHandle) {
55    let state = Arc::new(Mutex::new(false));
56    let handler = ctrlc::set_handler(move || {
57        {
58            let mut state = state.lock().unwrap();
59            if *state {
60                log!("Ctrl-C pressed again, exiting immediately...");
61                std::process::exit(1);
62            }
63            *state = true;
64        }
65
66        log!("Ctrl-C pressed, stopping all processes...");
67        sender
68            .send(ProcessAction::KillAll)
69            .expect("Could not send signal on channel.");
70    });
71    handler.expect("Error setting Ctrl-C handler");
72}
73
74fn collect_together_commands(
75    manager: &manager::ProcessManagerHandle,
76    options: &StartTogetherOptions,
77) -> TogetherResult<Vec<String>> {
78    if let Some(recipes) = &options.active_recipes {
79        log!("Running commands from recipes...");
80        let config_opts = &options.config.start_options;
81
82        let selected_commands = config::collect_commands_by_recipes(&config_opts, recipes);
83
84        log!("Commands selected by recipes:");
85        for command in &selected_commands {
86            log!("  - {}", command);
87        }
88
89        return Ok(selected_commands);
90    }
91
92    let config = &options.config;
93    let selected_commands = match &config.running_commands() {
94        Some(commands) => {
95            log!("Running commands from configuration...");
96            commands.into_iter().map(|c| c.to_string()).collect()
97        }
98        None if config.start_options.all => {
99            log!("Running all commands...");
100            config.start_options.as_commands()
101        }
102        None => {
103            let all_commands = config.start_options.as_commands();
104            let sender = manager.subscribe();
105            let commands = terminal::Terminal::select_multiple_commands(
106                "Select commands to run together",
107                &sender,
108                &all_commands,
109            )?;
110            commands.into_iter().cloned().collect()
111        }
112    };
113    Ok(selected_commands)
114}
115
116fn execute_startup_commands(
117    manager: &manager::ProcessManagerHandle,
118    config: &config::TogetherConfigFile,
119) -> TogetherResult<()> {
120    let Some(startup) = &config.startup else {
121        return Ok(());
122    };
123
124    log!("Running startup commands...");
125    let sender = manager.subscribe();
126
127    let commands = startup
128        .iter()
129        .flat_map(|index| index.retrieve(&config.start_options.commands))
130        .map(|c| c.as_str().to_string())
131        .collect::<Vec<_>>();
132
133    let opts = if config.start_options.quiet_startup {
134        manager::CreateOptions::default().with_stderr_only()
135    } else {
136        manager::CreateOptions::default()
137    };
138
139    for command in commands {
140        let id = sender.spawn_advanced(&command, &opts)?;
141        sender.wait(id)?;
142        log!("Startup command '{}' completed", command);
143    }
144
145    Ok(())
146}
147
148fn execute_together_commands(
149    manager: &manager::ProcessManagerHandle,
150    selected_commands: Vec<String>,
151) -> TogetherResult<()> {
152    let sender = manager.subscribe();
153    for command in selected_commands {
154        sender.send(ProcessAction::Create(command.clone()))?;
155    }
156    Ok(())
157}