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