1pub(crate) mod app;
11mod utils {
12 pub(crate) mod cancellation_token;
13 pub(crate) mod conversion;
14 pub(crate) mod data_units;
15 pub(crate) mod general;
16 pub(crate) mod logging;
17 pub(crate) mod process_killer;
18 pub(crate) mod strings;
19}
20pub(crate) mod canvas;
21pub(crate) mod collection;
22pub(crate) mod constants;
23pub(crate) mod event;
24pub mod options;
25pub mod widgets;
26
27use std::{
28 boxed::Box,
29 io::{Write, stderr, stdout},
30 panic::{self, PanicHookInfo},
31 sync::{
32 Arc,
33 mpsc::{self, Receiver, Sender},
34 },
35 thread::{self, JoinHandle},
36 time::{Duration, Instant},
37};
38
39use app::{App, AppConfigFields, DataFilters, layout_manager::UsedWidgets};
40use crossterm::{
41 cursor::{Hide, Show},
42 event::{
43 DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
44 Event, KeyEventKind, MouseEventKind, poll, read,
45 },
46 execute,
47 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
48};
49use event::{BottomEvent, CollectionThreadEvent, handle_key_event_or_break, handle_mouse_event};
50use options::{args, get_or_create_config, init_app};
51use tui::{Terminal, backend::CrosstermBackend};
52#[allow(unused_imports, reason = "this is needed if logging is enabled")]
53use utils::logging::*;
54use utils::{cancellation_token::CancellationToken, conversion::*};
55
56use crate::collection::Data;
57
58fn try_drawing(
64 terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>, app: &mut App,
65 painter: &mut canvas::Painter,
66) -> anyhow::Result<()> {
67 if let Err(err) = painter.draw_data(terminal, app) {
68 cleanup_terminal(terminal)?;
69 Err(err.into())
70 } else {
71 Ok(())
72 }
73}
74
75fn cleanup_terminal(
77 terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
78) -> anyhow::Result<()> {
79 disable_raw_mode()?;
80
81 execute!(
82 terminal.backend_mut(),
83 DisableMouseCapture,
84 DisableBracketedPaste,
85 LeaveAlternateScreen,
86 Show,
87 )?;
88 terminal.show_cursor()?;
89
90 Ok(())
91}
92
93fn check_if_terminal() {
95 use crossterm::tty::IsTty;
96
97 if !stdout().is_tty() {
98 eprintln!(
99 "Warning: bottom is not being output to a terminal. Things might not work properly."
100 );
101 eprintln!("If you're stuck, press 'q' or 'Ctrl-c' to quit the program.");
102 stderr().flush().unwrap();
103 thread::sleep(Duration::from_secs(1));
104 }
105}
106
107pub fn reset_stdout() {
109 let mut stdout = stdout();
110 let _ = disable_raw_mode();
111 let _ = execute!(
112 stdout,
113 DisableMouseCapture,
114 DisableBracketedPaste,
115 LeaveAlternateScreen,
116 Show,
117 );
118}
119
120fn panic_hook(panic_info: &PanicHookInfo<'_>) {
123 let msg = match panic_info.payload().downcast_ref::<&'static str>() {
124 Some(s) => *s,
125 None => match panic_info.payload().downcast_ref::<String>() {
126 Some(s) => &s[..],
127 None => "Box<Any>",
128 },
129 };
130
131 let backtrace = format!("{:?}", backtrace::Backtrace::new());
132
133 reset_stdout();
134
135 if let Some(panic_info) = panic_info.location() {
137 println!("thread '<unnamed>' panicked at '{msg}', {panic_info}\n\r{backtrace}")
138 }
139
140 std::process::exit(1);
143}
144
145fn create_input_thread(
147 sender: Sender<BottomEvent>, cancellation_token: Arc<CancellationToken>,
148 app_config_fields: &AppConfigFields,
149) -> JoinHandle<()> {
150 let keys_disabled = app_config_fields.disable_keys;
151
152 thread::spawn(move || {
153 let mut mouse_timer = Instant::now();
154
155 loop {
156 if let Some(is_terminated) = cancellation_token.try_check() {
158 if is_terminated {
159 break;
160 }
161 }
162
163 if let Ok(poll) = poll(Duration::from_millis(20)) {
164 if poll {
165 if let Ok(event) = read() {
166 match event {
167 Event::Resize(_, _) => {
168 if sender.send(BottomEvent::Resize).is_err() {
175 break;
176 }
177 }
178 Event::Paste(paste) => {
179 if sender.send(BottomEvent::PasteEvent(paste)).is_err() {
180 break;
181 }
182 }
183 Event::Key(key)
184 if !keys_disabled && key.kind == KeyEventKind::Press =>
185 {
186 if sender.send(BottomEvent::KeyInput(key)).is_err() {
189 break;
190 }
191 }
192 Event::Mouse(mouse) => match mouse.kind {
193 MouseEventKind::Moved | MouseEventKind::Drag(..) => {}
194 MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
195 if Instant::now().duration_since(mouse_timer).as_millis() >= 20
196 {
197 if sender.send(BottomEvent::MouseInput(mouse)).is_err() {
198 break;
199 }
200 mouse_timer = Instant::now();
201 }
202 }
203 _ => {
204 if sender.send(BottomEvent::MouseInput(mouse)).is_err() {
205 break;
206 }
207 }
208 },
209 Event::Key(_) => {}
210 Event::FocusGained => {}
211 Event::FocusLost => {}
212 }
213 }
214 }
215 }
216 }
217 })
218}
219
220fn create_collection_thread(
222 sender: Sender<BottomEvent>, control_receiver: Receiver<CollectionThreadEvent>,
223 cancellation_token: Arc<CancellationToken>, app_config_fields: &AppConfigFields,
224 filters: DataFilters, used_widget_set: UsedWidgets,
225) -> JoinHandle<()> {
226 let use_current_cpu_total = app_config_fields.use_current_cpu_total;
227 let unnormalized_cpu = app_config_fields.unnormalized_cpu;
228 let show_average_cpu = app_config_fields.show_average_cpu;
229 let update_sleep = app_config_fields.update_rate;
230 let get_process_threads = app_config_fields.get_process_threads;
231
232 thread::spawn(move || {
233 let mut data_collector = collection::DataCollector::new(filters);
234
235 data_collector.set_collection(used_widget_set);
236 data_collector.set_use_current_cpu_total(use_current_cpu_total);
237 data_collector.set_unnormalized_cpu(unnormalized_cpu);
238 data_collector.set_show_average_cpu(show_average_cpu);
239 data_collector.set_get_process_threads(get_process_threads);
240
241 data_collector.update_data();
242 data_collector.data = Data::default();
243
244 std::thread::sleep(Duration::from_millis(5));
246
247 loop {
248 if let Some(is_terminated) = cancellation_token.try_check() {
250 if is_terminated {
251 break;
252 }
253 }
254
255 if let Ok(message) = control_receiver.try_recv() {
256 match message {
258 CollectionThreadEvent::Reset => {
259 data_collector.data.cleanup();
260 }
261 }
262 }
263
264 data_collector.update_data();
265
266 if let Some(is_terminated) = cancellation_token.try_check() {
268 if is_terminated {
269 break;
270 }
271 }
272
273 let event = BottomEvent::Update(Box::from(data_collector.data));
274 data_collector.data = Data::default();
275
276 if sender.send(event).is_err() {
277 break;
278 }
279
280 if cancellation_token.sleep_with_cancellation(Duration::from_millis(update_sleep)) {
282 break;
283 }
284 }
285 })
286}
287
288#[inline]
290pub fn start_bottom(enable_error_hook: &mut bool) -> anyhow::Result<()> {
291 let args = args::get_args();
294
295 #[cfg(feature = "logging")]
296 {
297 if let Err(err) = init_logger(
298 log::LevelFilter::Debug,
299 Some(std::ffi::OsStr::new("debug.log")),
300 ) {
301 println!("Issue initializing logger: {err}");
302 }
303 }
304
305 let config = get_or_create_config(args.general.config_location.as_deref())?;
307
308 let (mut app, widget_layout, styling) = init_app(args, config)?;
310
311 let mut painter = canvas::Painter::init(widget_layout, styling)?;
313
314 check_if_terminal();
316
317 let cancellation_token = Arc::new(CancellationToken::default());
318 let (sender, receiver) = mpsc::channel();
319
320 let (collection_thread_ctrl_sender, collection_thread_ctrl_receiver) = mpsc::channel();
323 let _collection_thread = create_collection_thread(
324 sender.clone(),
325 collection_thread_ctrl_receiver,
326 cancellation_token.clone(),
327 &app.app_config_fields,
328 app.filters.clone(),
329 app.used_widgets,
330 );
331
332 let _input_thread = create_input_thread(
334 sender.clone(),
335 cancellation_token.clone(),
336 &app.app_config_fields,
337 );
338
339 let _cleaning_thread = {
341 let cancellation_token = cancellation_token.clone();
342 let cleaning_sender = sender.clone();
343 let offset_wait = Duration::from_millis(app.app_config_fields.retention_ms + 60000);
344 thread::spawn(move || {
345 loop {
346 if cancellation_token.sleep_with_cancellation(offset_wait) {
347 break;
348 }
349
350 if cleaning_sender.send(BottomEvent::Clean).is_err() {
351 break;
352 }
353 }
354 })
355 };
356
357 *enable_error_hook = true;
359
360 let mut stdout_val = stdout();
361 execute!(stdout_val, Hide, EnterAlternateScreen, EnableBracketedPaste)?;
362 if app.app_config_fields.disable_click {
363 execute!(stdout_val, DisableMouseCapture)?;
364 } else {
365 execute!(stdout_val, EnableMouseCapture)?;
366 }
367 enable_raw_mode()?;
368
369 let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?;
370 terminal.clear()?;
371 terminal.hide_cursor()?;
372
373 #[cfg(target_os = "freebsd")]
374 let _stderr_fd = {
375 use std::fs::OpenOptions;
378
379 use filedescriptor::{FileDescriptor, StdioDescriptor};
380
381 let path = OpenOptions::new().write(true).open("/dev/null")?;
382 FileDescriptor::redirect_stdio(&path, StdioDescriptor::Stderr)?
383 };
384
385 panic::set_hook(Box::new(panic_hook));
387
388 ctrlc::set_handler(move || {
390 let _ = sender.send(BottomEvent::Terminate);
393 })?;
394
395 let mut first_run = true;
396
397 try_drawing(&mut terminal, &mut app, &mut painter)?;
400
401 loop {
402 if let Ok(recv) = receiver.recv() {
403 match recv {
404 BottomEvent::Terminate => break,
405 BottomEvent::Resize => {
406 try_drawing(&mut terminal, &mut app, &mut painter)?;
407 }
408 BottomEvent::KeyInput(event) => {
409 if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) {
410 break;
411 }
412 app.update_data();
413 try_drawing(&mut terminal, &mut app, &mut painter)?;
414 }
415 BottomEvent::MouseInput(event) => {
416 handle_mouse_event(event, &mut app);
417 app.update_data();
418 try_drawing(&mut terminal, &mut app, &mut painter)?;
419 }
420 BottomEvent::PasteEvent(paste) => {
421 app.handle_paste(paste);
422 app.update_data();
423 try_drawing(&mut terminal, &mut app, &mut painter)?;
424 }
425 BottomEvent::Update(data) => {
426 app.data_store.eat_data(data, &app.app_config_fields);
427
428 if first_run {
431 first_run = false;
432 app.is_force_redraw = true;
433 }
434
435 if !app.data_store.is_frozen() {
436 if app.used_widgets.use_disk {
439 for disk in app.states.disk_state.widget_states.values_mut() {
440 disk.force_data_update();
441 }
442 }
443
444 if app.used_widgets.use_temp {
445 for temp in app.states.temp_state.widget_states.values_mut() {
446 temp.force_data_update();
447 }
448 }
449
450 if app.used_widgets.use_proc {
451 for proc in app.states.proc_state.widget_states.values_mut() {
452 proc.force_data_update();
453 }
454 }
455
456 if app.used_widgets.use_cpu {
457 for cpu in app.states.cpu_state.widget_states.values_mut() {
458 cpu.force_data_update();
459 }
460 }
461
462 app.update_data();
463 try_drawing(&mut terminal, &mut app, &mut painter)?;
464 }
465 }
466 BottomEvent::Clean => {
467 app.data_store
468 .clean_data(Duration::from_millis(app.app_config_fields.retention_ms));
469 }
470 }
471 }
472 }
473
474 cancellation_token.cancel();
477 cleanup_terminal(&mut terminal)?;
478
479 Ok(())
480}