pub(crate) mod app;
mod utils {
pub(crate) mod cancellation_token;
pub(crate) mod conversion;
pub(crate) mod data_units;
pub(crate) mod general;
pub(crate) mod logging;
pub(crate) mod process_killer;
pub(crate) mod strings;
}
pub(crate) mod canvas;
pub(crate) mod collection;
pub(crate) mod constants;
pub(crate) mod event;
pub mod options;
pub mod widgets;
use std::{
boxed::Box,
io::{Write, stderr, stdout},
panic::{self, PanicHookInfo},
sync::{
Arc,
mpsc::{self, Receiver, Sender},
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use app::{App, AppConfigFields, DataFilters, layout_manager::UsedWidgets};
use crossterm::{
cursor::{Hide, Show},
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event, KeyEventKind, MouseEventKind, poll, read,
},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use event::{BottomEvent, CollectionThreadEvent, handle_key_event_or_break, handle_mouse_event};
use options::{args, get_or_create_config, init_app};
use tui::{Terminal, backend::CrosstermBackend};
#[allow(unused_imports, reason = "this is needed if logging is enabled")]
use utils::logging::*;
use utils::{cancellation_token::CancellationToken, conversion::*};
use crate::collection::Data;
fn try_drawing(
terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>, app: &mut App,
painter: &mut canvas::Painter,
) -> anyhow::Result<()> {
if let Err(err) = painter.draw_data(terminal, app) {
cleanup_terminal(terminal)?;
Err(err.into())
} else {
Ok(())
}
}
fn cleanup_terminal(
terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
) -> anyhow::Result<()> {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
DisableMouseCapture,
DisableBracketedPaste,
LeaveAlternateScreen,
Show,
)?;
terminal.show_cursor()?;
Ok(())
}
fn check_if_terminal() {
use crossterm::tty::IsTty;
if !stdout().is_tty() {
eprintln!(
"Warning: bottom is not being output to a terminal. Things might not work properly."
);
eprintln!("If you're stuck, press 'q' or 'Ctrl-c' to quit the program.");
stderr().flush().expect("should succeed in flushing stderr");
thread::sleep(Duration::from_secs(1));
}
}
pub fn reset_stdout() {
let mut stdout = stdout();
let _ = disable_raw_mode();
let _ = execute!(
stdout,
DisableMouseCapture,
DisableBracketedPaste,
LeaveAlternateScreen,
Show,
);
}
fn panic_hook(panic_info: &PanicHookInfo<'_>) {
let msg = match panic_info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match panic_info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<Any>",
},
};
let backtrace = format!("{:?}", std::backtrace::Backtrace::capture());
reset_stdout();
if let Some(panic_info) = panic_info.location() {
println!("thread '<unnamed>' panicked at '{msg}', {panic_info}\n\r{backtrace}")
}
std::process::exit(1);
}
fn create_input_thread(
sender: Sender<BottomEvent>, cancellation_token: Arc<CancellationToken>,
app_config_fields: &AppConfigFields,
) -> JoinHandle<()> {
let keys_disabled = app_config_fields.disable_keys;
thread::spawn(move || {
let mut mouse_timer = Instant::now();
loop {
if let Some(is_terminated) = cancellation_token.try_check() {
if is_terminated {
break;
}
}
if let Ok(poll) = poll(Duration::from_millis(20)) {
if poll {
if let Ok(event) = read() {
match event {
Event::Resize(_, _) => {
if sender.send(BottomEvent::Resize).is_err() {
break;
}
}
Event::Paste(paste) => {
if sender.send(BottomEvent::PasteEvent(paste)).is_err() {
break;
}
}
Event::Key(key)
if !keys_disabled && key.kind == KeyEventKind::Press =>
{
if sender.send(BottomEvent::KeyInput(key)).is_err() {
break;
}
}
Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::Moved | MouseEventKind::Drag(..) => {}
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
if Instant::now().duration_since(mouse_timer).as_millis() >= 20
{
if sender.send(BottomEvent::MouseInput(mouse)).is_err() {
break;
}
mouse_timer = Instant::now();
}
}
_ => {
if sender.send(BottomEvent::MouseInput(mouse)).is_err() {
break;
}
}
},
Event::Key(_) => {}
Event::FocusGained => {}
Event::FocusLost => {}
}
}
}
}
}
})
}
fn create_collection_thread(
sender: Sender<BottomEvent>, control_receiver: Receiver<CollectionThreadEvent>,
cancellation_token: Arc<CancellationToken>, app_config_fields: &AppConfigFields,
filters: DataFilters, used_widget_set: UsedWidgets,
) -> JoinHandle<()> {
let use_current_cpu_total = app_config_fields.use_current_cpu_total;
let unnormalized_cpu = app_config_fields.unnormalized_cpu;
let show_average_cpu = app_config_fields.show_average_cpu;
let update_sleep = app_config_fields.update_rate;
let get_process_threads = app_config_fields.get_process_threads;
#[cfg(feature = "zfs")]
let get_arc_free = app_config_fields.free_arc;
thread::spawn(move || {
let mut data_collector = collection::DataCollector::new(filters);
data_collector.set_collection(used_widget_set);
data_collector.set_use_current_cpu_total(use_current_cpu_total);
data_collector.set_unnormalized_cpu(unnormalized_cpu);
data_collector.set_show_average_cpu(show_average_cpu);
data_collector.set_get_process_threads(get_process_threads);
#[cfg(feature = "zfs")]
data_collector.set_free_arc_mem(get_arc_free);
data_collector.update_data();
data_collector.data = Data::default();
std::thread::sleep(Duration::from_millis(5));
loop {
if let Some(is_terminated) = cancellation_token.try_check() {
if is_terminated {
break;
}
}
if let Ok(message) = control_receiver.try_recv() {
match message {
CollectionThreadEvent::Reset => {
data_collector.data.cleanup();
}
}
}
data_collector.update_data();
if let Some(is_terminated) = cancellation_token.try_check() {
if is_terminated {
break;
}
}
let event = BottomEvent::Update(Box::from(data_collector.data));
data_collector.data = Data::default();
if sender.send(event).is_err() {
break;
}
if cancellation_token.sleep_with_cancellation(Duration::from_millis(update_sleep)) {
break;
}
}
})
}
#[inline]
pub fn start_bottom(enable_error_hook: &mut bool) -> anyhow::Result<()> {
let args = args::get_args();
#[cfg(feature = "logging")]
{
if let Err(err) = init_logger(
log::LevelFilter::Debug,
Some(std::ffi::OsStr::new("debug.log")),
) {
println!("Issue initializing logger: {err}");
}
}
let config = get_or_create_config(args.general.config_location.as_deref())?;
let (mut app, widget_layout, styling) = init_app(args, config)?;
let mut painter = canvas::Painter::init(widget_layout, styling)?;
check_if_terminal();
let cancellation_token = Arc::new(CancellationToken::default());
let (sender, receiver) = mpsc::channel();
let (collection_thread_ctrl_sender, collection_thread_ctrl_receiver) = mpsc::channel();
let _collection_thread = create_collection_thread(
sender.clone(),
collection_thread_ctrl_receiver,
cancellation_token.clone(),
&app.app_config_fields,
app.filters.clone(),
app.used_widgets,
);
let _input_thread = create_input_thread(
sender.clone(),
cancellation_token.clone(),
&app.app_config_fields,
);
let _cleaning_thread = {
let cancellation_token = cancellation_token.clone();
let cleaning_sender = sender.clone();
let offset_wait = Duration::from_millis(app.app_config_fields.retention_ms + 60000);
thread::spawn(move || {
loop {
if cancellation_token.sleep_with_cancellation(offset_wait) {
break;
}
if cleaning_sender.send(BottomEvent::Clean).is_err() {
break;
}
}
})
};
*enable_error_hook = true;
let mut stdout_val = stdout();
execute!(stdout_val, Hide, EnterAlternateScreen, EnableBracketedPaste)?;
if app.app_config_fields.disable_click {
execute!(stdout_val, DisableMouseCapture)?;
} else {
execute!(stdout_val, EnableMouseCapture)?;
}
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?;
terminal.clear()?;
terminal.hide_cursor()?;
#[cfg(target_os = "freebsd")]
let _stderr_fd = {
use std::fs::OpenOptions;
use filedescriptor::{FileDescriptor, StdioDescriptor};
let path = OpenOptions::new().write(true).open("/dev/null")?;
FileDescriptor::redirect_stdio(&path, StdioDescriptor::Stderr)?
};
panic::set_hook(Box::new(panic_hook));
ctrlc::set_handler(move || {
let _ = sender.send(BottomEvent::Terminate);
})?;
let mut first_run = true;
try_drawing(&mut terminal, &mut app, &mut painter)?;
loop {
if let Ok(recv) = receiver.recv() {
match recv {
BottomEvent::Terminate => break,
BottomEvent::Resize => {
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::KeyInput(event) => {
if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) {
break;
}
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::MouseInput(event) => {
handle_mouse_event(event, &mut app);
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::PasteEvent(paste) => {
app.handle_paste(paste);
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::Update(data) => {
app.data_store.eat_data(data, &app.app_config_fields);
if first_run {
first_run = false;
app.is_force_redraw = true;
}
if !app.data_store.is_frozen() {
if app.used_widgets.use_disk {
for disk in app.states.disk_state.widget_states.values_mut() {
disk.force_data_update();
}
}
if app.used_widgets.use_temp {
for temp in app.states.temp_state.widget_states.values_mut() {
temp.force_data_update();
}
}
if app.used_widgets.use_proc {
for proc in app.states.proc_state.widget_states.values_mut() {
proc.force_data_update();
}
}
if app.used_widgets.use_cpu {
for cpu in app.states.cpu_state.widget_states.values_mut() {
cpu.force_data_update();
}
}
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
}
BottomEvent::Clean => {
app.data_store
.clean_data(Duration::from_millis(app.app_config_fields.retention_ms));
}
}
}
}
cancellation_token.cancel();
cleanup_terminal(&mut terminal)?;
Ok(())
}