1use std::time::Duration;
2
3use clap::ArgMatches;
4use semver::Version;
5
6use crate::prelude::*;
7use service::{main::*, terminal::disable_raw_mode, terminal::reset_terminal_state};
8
9use simplelog::{TermLogger, TerminalMode};
10
11use crate::bus::MainBus;
12use message::main::{MainRecv, MainShutdown};
13
14use lifeline::dyn_bus::DynBus;
15use tab_api::{config::DaemonConfig, launch::*, log::get_level, tab::normalize_name};
16use tab_websocket::resource::connection::WebsocketResource;
17
18mod bus;
19mod env;
20mod message;
21mod prelude;
22mod service;
23mod state;
24mod utils;
25
26pub fn command_main(args: ArgMatches, tab_version: &'static str) -> anyhow::Result<i32> {
27 TermLogger::init(
28 get_level().unwrap_or(LevelFilter::Warn),
29 simplelog::ConfigBuilder::new()
30 .set_time_format_str("%H:%M:%S%.3f CMD")
31 .build(),
32 TerminalMode::Stderr,
33 )
34 .unwrap();
35
36 info!("tab-command runtime starting");
37
38 let mut runtime = tokio::runtime::Builder::new()
39 .threaded_scheduler()
40 .enable_io()
41 .enable_time()
42 .build()
43 .unwrap();
44
45 let result = runtime.block_on(async { main_async(args, tab_version).await });
46
47 info!("tab-command runtime stopped");
48 runtime.shutdown_timeout(Duration::from_millis(25));
49
50 let code = result?;
51
52 info!("tab-command runtime stopped");
53
54 Ok(code)
55}
56
57async fn main_async(matches: ArgMatches<'_>, tab_version: &'static str) -> anyhow::Result<i32> {
58 let check_workspace = matches.is_present("CHECK-WORKSPACE");
59 let close_completion = matches.is_present("AUTOCOMPLETE-CLOSE-TAB");
60 let close_tabs = matches.values_of("CLOSE-TAB");
61 let completion = matches.is_present("AUTOCOMPLETE-TAB");
62 let disconnect_tabs = matches.values_of("DISCONNECT-TAB");
63 let select_tab = matches.value_of("TAB-NAME");
64 let shutdown = matches.is_present("SHUTDOWN");
65
66 let spawn_result = spawn(tab_version).await;
67
68 if let Err(e) = spawn_result {
69 error!("Failed to initialize the tab command: {}", e);
70 return Err(e);
71 }
72
73 let (mut tx, rx_shutdown, _service) = spawn_result.unwrap();
74
75 debug!("Parsing CLI arguments...");
76 if shutdown {
77 info!("CLI Match: GlobalShutdown");
78 tx.send(MainRecv::GlobalShutdown).await?;
79 } else if completion {
80 info!("CLI Match: AutocompleteTab");
81 tx.send(MainRecv::AutocompleteTab).await?;
82 } else if close_completion {
83 info!("CLI Match: AutocompleteCloseTab");
84 tx.send(MainRecv::AutocompleteCloseTab).await?;
85 } else if check_workspace {
86 info!("CLI Match: CheckWorkspace");
87 tx.send(MainRecv::CheckWorkspace).await?;
88 } else if matches.is_present("LIST") {
89 info!("CLI Match: ListTabs");
90 tx.send(MainRecv::ListTabs).await?;
91 } else if let Some(tab) = select_tab {
92 info!("CLI Match: SelectTab({})", &tab);
93 tx.send(MainRecv::SelectTab(tab.to_string())).await?;
94 } else if let Some(tabs) = close_tabs {
95 info!("CLI Match: CloseTabs({:?})", &tabs);
96 let tabs: Vec<String> = tabs.map(normalize_name).collect();
97 tx.send(MainRecv::CloseTabs(tabs)).await?;
98 } else if let Some(tabs) = disconnect_tabs {
99 info!("CLI Match: DisconnectTabs({:?})", &tabs);
100 let tabs: Vec<String> = tabs.map(normalize_name).collect();
101 tx.send(MainRecv::DisconnectTabs(tabs)).await?;
102 } else {
103 info!("CLI Match: SelectInteractive");
104 tx.send(MainRecv::SelectInteractive).await?;
105 }
106
107 let exit = wait_for_shutdown(rx_shutdown).await;
108 disable_raw_mode();
109 reset_terminal_state();
110
111 debug!("tab-command shutdown.");
112
113 Ok(exit.0)
114}
115
116async fn spawn(
117 tab_version: &'static str,
118) -> anyhow::Result<(
119 impl Sender<MainRecv>,
120 impl Receiver<MainShutdown>,
121 MainService,
122)> {
123 let daemon_file = launch_daemon().await?;
124 validate_daemon(&daemon_file, tab_version);
125 let ws_url = format!("ws://127.0.0.1:{}/cli", daemon_file.port);
126
127 debug!("daemon is ready");
128
129 let bus = MainBus::default();
130 bus.capacity::<Request>(128)?;
131 bus.capacity::<Response>(256)?;
132
133 let websocket =
134 tab_websocket::connect_authorized(ws_url, daemon_file.auth_token.clone()).await?;
135 let websocket = WebsocketResource(websocket);
136 bus.store_resource(websocket);
137
138 info!("Launching MainService");
139 let service = MainService::spawn(&bus)?;
140
141 let tx = bus.tx::<MainRecv>()?;
142 let main_shutdown = bus.rx::<MainShutdown>()?;
143
144 debug!("Main spawn complete");
145
146 Ok((tx, main_shutdown, service))
147}
148
149fn validate_daemon(config: &DaemonConfig, tab_version: &'static str) {
150 let executable = std::env::current_exe()
151 .ok()
152 .map(|path| path.to_str().map(str::to_string))
153 .flatten();
154
155 let tab_version = Version::parse(tab_version).ok();
156 let daemon_version = config
157 .tab_version
158 .as_ref()
159 .map(String::as_str)
160 .map(Version::parse)
161 .map(Result::ok)
162 .flatten();
163
164 if let (Some(tab_version), Some(daemon_version)) = (tab_version, daemon_version) {
165 if tab_version.major != daemon_version.major || tab_version.minor != daemon_version.minor {
166 eprintln!("Warning: The tab command (v{}) has a different version than the running daemon (v{})", tab_version, daemon_version);
167 eprintln!(
168 " You should run `tab --shutdown` to terminate your tabs and relaunch the daemon."
169 );
170
171 return;
172 }
173 }
174
175 if let (Some(executable), Some(daemon_exec)) = (&executable, &config.executable) {
176 if executable != daemon_exec {
177 eprintln!(
178 "Warning: The tab command has a different executable path than the running daemon."
179 );
180 eprintln!(" You may want to run `tab --shutdown` to terminate your tabs and relaunch the daemon.");
181
182 eprintln!(" Tab command: {}", executable);
183 eprintln!(" Daemon command: {}", daemon_exec);
184 return;
185 }
186 }
187}