refactor: remove lib and move some things around (#1477)

A loooooong time ago (wow, it's been 4 years apparently...), I made the brilliant (/s) decision to use both lib.rs and main.rs because I was trying to add tests in the tests folder that needed private access to some functions in the src - and at the time, the only way I could think of doing so was exposing bottom as a library and a binary.

This isn't necessarily bad, but for my use case, it's pretty unnecessary nowadays (since I've moved all my tests back into the source files) and adds complexity in looking for certain things, so it's high time we move things around and remove lib.rs. I also took the time to clean up some weird code I spotted along the way.
This commit is contained in:
Clement Tsang 2024-06-05 02:00:21 -04:00 committed by GitHub
parent 7aa379aabf
commit 8885910442
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 635 additions and 649 deletions

28
Cargo.lock generated
View File

@ -4,9 +4,9 @@ version = 3
[[package]]
name = "addr2line"
version = "0.21.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
@ -121,9 +121,9 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "backtrace"
version = "0.3.71"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
dependencies = [
"addr2line",
"cc",
@ -235,9 +235,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.95"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
[[package]]
name = "cfg-if"
@ -597,9 +597,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.28.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "hashbrown"
@ -888,9 +888,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.32.2"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
dependencies = [
"memchr",
]
@ -1114,9 +1114,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]]
name = "rustc-demangle"
version = "0.1.23"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
@ -1488,9 +1488,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.13"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [
"indexmap",
"serde",

View File

@ -40,10 +40,7 @@ rust-version = "1.74.0" # The oldest version I've tested that should still build
[[bin]]
name = "btm"
path = "src/bin/main.rs"
doc = false
[lib]
path = "src/main.rs"
test = true
doctest = true
doc = true
@ -78,7 +75,7 @@ default = ["deploy"]
[dependencies]
anyhow = "1.0.86"
backtrace = "0.3.71"
backtrace = "0.3.72"
cfg-if = "1.0.0"
clap = { version = "4.5.4", features = ["default", "cargo", "wrap_help", "derive"] }
concat-string = "1.0.1"
@ -99,7 +96,7 @@ starship-battery = { version = "0.8.3", optional = true }
sysinfo = "=0.30.12"
thiserror = "1.0.61"
time = { version = "0.3.36", features = ["formatting", "macros"] }
toml_edit = { version = "0.22.13", features = ["serde"] }
toml_edit = { version = "0.22.14", features = ["serde"] }
tui = { version = "0.26.3", package = "ratatui" }
unicode-ellipsis = "0.1.4"
unicode-segmentation = "1.11.0"

View File

@ -22,15 +22,15 @@ use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
use crate::{
canvas::components::time_chart::LegendPosition,
constants,
data_collection::temperature,
constants, convert_mem_data_points, convert_swap_data_points,
data_collection::{processes::Pid, temperature},
data_conversion::ConvertedData,
get_network_points,
utils::{
data_units::DataUnit,
error::{BottomError, Result},
},
widgets::{ProcWidgetColumn, ProcWidgetMode},
Pid,
};
#[derive(Debug, Clone, Eq, PartialEq, Default)]
@ -128,6 +128,7 @@ pub struct App {
}
impl App {
/// Create a new [`App`].
pub fn new(
app_config_fields: AppConfigFields, states: AppWidgetStates,
widget_map: HashMap<u64, BottomWidget>, current_widget: BottomWidget,
@ -159,6 +160,87 @@ impl App {
}
}
/// Update the data in the [`App`].
pub fn update_data(&mut self) {
let data_source = match &self.frozen_state {
FrozenState::NotFrozen => &self.data_collection,
FrozenState::Frozen(data) => data,
};
for proc in self.states.proc_state.widget_states.values_mut() {
if proc.force_update_data {
proc.set_table_data(data_source);
proc.force_update_data = false;
}
}
// FIXME: Make this CPU force update less terrible.
if self.states.cpu_state.force_update.is_some() {
self.converted_data.convert_cpu_data(data_source);
self.converted_data.load_avg_data = data_source.load_avg_harvest;
self.states.cpu_state.force_update = None;
}
// FIXME: This is a bit of a temp hack to move data over.
{
let data = &self.converted_data.cpu_data;
for cpu in self.states.cpu_state.widget_states.values_mut() {
cpu.update_table(data);
}
}
{
let data = &self.converted_data.temp_data;
for temp in self.states.temp_state.widget_states.values_mut() {
if temp.force_update_data {
temp.set_table_data(data);
temp.force_update_data = false;
}
}
}
{
let data = &self.converted_data.disk_data;
for disk in self.states.disk_state.widget_states.values_mut() {
if disk.force_update_data {
disk.set_table_data(data);
disk.force_update_data = false;
}
}
}
// TODO: [OPT] Prefer reassignment over new vectors?
if self.states.mem_state.force_update.is_some() {
self.converted_data.mem_data = convert_mem_data_points(data_source);
#[cfg(not(target_os = "windows"))]
{
self.converted_data.cache_data = crate::convert_cache_data_points(data_source);
}
self.converted_data.swap_data = convert_swap_data_points(data_source);
#[cfg(feature = "zfs")]
{
self.converted_data.arc_data = crate::convert_arc_data_points(data_source);
}
#[cfg(feature = "gpu")]
{
self.converted_data.gpu_data = crate::convert_gpu_data(data_source);
}
self.states.mem_state.force_update = None;
}
if self.states.net_state.force_update.is_some() {
let (rx, tx) = get_network_points(
data_source,
&self.app_config_fields.network_scale_type,
&self.app_config_fields.network_unit_type,
self.app_config_fields.network_use_binary_prefix,
);
self.converted_data.network_data_rx = rx;
self.converted_data.network_data_tx = tx;
self.states.net_state.force_update = None;
}
}
pub fn reset(&mut self) {
// Reset multi
self.reset_multi_tap_keys();

View File

@ -20,9 +20,12 @@ use hashbrown::HashMap;
#[cfg(feature = "battery")]
use crate::data_collection::batteries;
use crate::{
data_collection::{cpu, disks, memory, network, processes::ProcessHarvest, temperature, Data},
data_collection::{
cpu, disks, memory, network,
processes::{Pid, ProcessHarvest},
temperature, Data,
},
utils::data_prefixes::*,
Pid,
};
pub type TimeOffset = f64;

View File

@ -8,9 +8,9 @@ use windows::Win32::{
},
};
use crate::data_collection::processes::Pid;
#[cfg(target_family = "unix")]
use crate::utils::error::BottomError;
use crate::Pid;
/// Based from [this SO answer](https://stackoverflow.com/a/55231715).
#[cfg(target_os = "windows")]
@ -58,10 +58,11 @@ pub fn kill_process_given_pid(pid: Pid) -> crate::utils::error::Result<()> {
Ok(())
}
/// Kills a process, given a PID, for unix.
/// Kills a process, given a PID, for UNIX.
#[cfg(target_family = "unix")]
pub fn kill_process_given_pid(pid: Pid, signal: usize) -> crate::utils::error::Result<()> {
// SAFETY: the signal should be valid, and we act properly on an error (exit code not 0).
let output = unsafe { libc::kill(pid, signal as i32) };
if output != 0 {
// We had an error...

View File

@ -287,8 +287,7 @@ impl<'a> Dataset<'a> {
/// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`, `⣿`) or half-blocks
/// (`█`, `▄`, and `▀`). See [symbols::Marker] for more details.
///
/// Note [`Marker::Braille`](symbols::Marker::Braille) requires a font that supports Unicode
/// Braille Patterns.
/// Note [`Marker::Braille`] requires a font that supports Unicode Braille Patterns.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]

View File

@ -514,7 +514,6 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r#"
"#;
// Config and flags
pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml";
// TODO: Eventually deprecate this, or grab from a file.
pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. All of the settings are commented

View File

@ -17,6 +17,8 @@ use std::time::{Duration, Instant};
#[cfg(any(target_os = "linux", feature = "gpu"))]
use hashbrown::HashMap;
#[cfg(not(target_os = "windows"))]
use processes::Pid;
#[cfg(feature = "battery")]
use starship_battery::{Battery, Manager};
@ -147,7 +149,7 @@ pub struct DataCollector {
filters: DataFilters,
#[cfg(target_os = "linux")]
pid_mapping: HashMap<crate::Pid, processes::PrevProcDetails>,
pid_mapping: HashMap<Pid, processes::PrevProcDetails>,
#[cfg(target_os = "linux")]
prev_idle: f64,
#[cfg(target_os = "linux")]
@ -471,12 +473,12 @@ impl DataCollector {
}
}
/// We set a sleep duration between 10ms and 250ms, ideally sysinfo's [`System::MINIMUM_CPU_UPDATE_INTERVAL`] + 1.
/// We set a sleep duration between 10ms and 250ms, ideally sysinfo's [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] + 1.
///
/// We bound the upper end to avoid waiting too long (e.g. FreeBSD is 1s, which I'm fine with losing
/// accuracy on for the first refresh), and we bound the lower end just to avoid the off-chance that
/// refreshing too quickly causes problems. This second case should only happen on unsupported
/// systems via sysinfo, in which case [`System::MINIMUM_CPU_UPDATE_INTERVAL`] is defined as 0.
/// systems via sysinfo, in which case [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] is defined as 0.
///
/// We also do `INTERVAL + 1` for some wiggle room, just in case.
const fn get_sleep_duration() -> Duration {

View File

@ -10,64 +10,64 @@ use crate::multi_eq_ignore_ascii_case;
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
#[non_exhaustive]
pub enum FileSystem {
/// ext2 (https://en.wikipedia.org/wiki/Ext2)
/// ext2 (<https://en.wikipedia.org/wiki/Ext2>)
Ext2,
/// ext3 (https://en.wikipedia.org/wiki/Ext3)
/// ext3 (<https://en.wikipedia.org/wiki/Ext3>)
Ext3,
/// ext4 (https://en.wikipedia.org/wiki/Ext4)
/// ext4 (<https://en.wikipedia.org/wiki/Ext4>)
Ext4,
/// FAT (https://en.wikipedia.org/wiki/File_Allocation_Table)
/// FAT (<https://en.wikipedia.org/wiki/File_Allocation_Table>)
VFat,
/// exFAT (https://en.wikipedia.org/wiki/ExFAT)
/// exFAT (<https://en.wikipedia.org/wiki/ExFAT>)
ExFat,
/// F2FS (https://en.wikipedia.org/wiki/F2FS)
/// F2FS (<https://en.wikipedia.org/wiki/F2FS>)
F2fs,
/// NTFS (https://en.wikipedia.org/wiki/NTFS)
/// NTFS (<https://en.wikipedia.org/wiki/NTFS>)
Ntfs,
/// ZFS (https://en.wikipedia.org/wiki/ZFS)
/// ZFS (<https://en.wikipedia.org/wiki/ZFS>)
Zfs,
/// HFS (https://en.wikipedia.org/wiki/Hierarchical_File_System)
/// HFS (<https://en.wikipedia.org/wiki/Hierarchical_File_System>)
Hfs,
/// HFS+ (https://en.wikipedia.org/wiki/HFS_Plus)
/// HFS+ (<https://en.wikipedia.org/wiki/HFS_Plus>)
HfsPlus,
/// JFS (https://en.wikipedia.org/wiki/JFS_(file_system))
/// JFS (<https://en.wikipedia.org/wiki/JFS_(file_system)>)
Jfs,
/// ReiserFS 3 (https://en.wikipedia.org/wiki/ReiserFS)
/// ReiserFS 3 (<https://en.wikipedia.org/wiki/ReiserFS>)
Reiser3,
/// ReiserFS 4 (https://en.wikipedia.org/wiki/Reiser4)
/// ReiserFS 4 (<https://en.wikipedia.org/wiki/Reiser4>)
Reiser4,
/// Btrfs (https://en.wikipedia.org/wiki/Btrfs)
/// Btrfs (<https://en.wikipedia.org/wiki/Btrfs>)
Btrfs,
/// Bcachefs (https://en.wikipedia.org/wiki/Bcachefs)
/// Bcachefs (<https://en.wikipedia.org/wiki/Bcachefs>)
Bcachefs,
/// MINIX FS (https://en.wikipedia.org/wiki/MINIX_file_system)
/// MINIX FS (<https://en.wikipedia.org/wiki/MINIX_file_system>)
Minix,
/// NILFS (https://en.wikipedia.org/wiki/NILFS)
/// NILFS (<https://en.wikipedia.org/wiki/NILFS>)
Nilfs,
/// XFS (https://en.wikipedia.org/wiki/XFS)
/// XFS (<https://en.wikipedia.org/wiki/XFS>)
Xfs,
/// APFS (https://en.wikipedia.org/wiki/Apple_File_System)
/// APFS (<https://en.wikipedia.org/wiki/Apple_File_System>)
Apfs,
/// FUSE (https://en.wikipedia.org/wiki/Filesystem_in_Userspace)
/// FUSE (<https://en.wikipedia.org/wiki/Filesystem_in_Userspace>)
FuseBlk,
/// Some unspecified filesystem.

View File

@ -13,7 +13,7 @@ impl Usage {
u64::from(self.0.f_blocks) * u64::from(self.0.f_frsize)
}
/// Returns the available number of bytes used. Note this is not necessarily the same as [`free`].
/// Returns the available number of bytes used. Note this is not necessarily the same as [`Usage::free`].
pub fn available(&self) -> u64 {
u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize)
}
@ -25,7 +25,7 @@ impl Usage {
self.total() - avail_to_root
}
/// Returns the total number of bytes free. Note this is not necessarily the same as [`available`].
/// Returns the total number of bytes free. Note this is not necessarily the same as [`Usage::available`].
pub fn free(&self) -> u64 {
u64::from(self.0.f_bavail) * u64::from(self.0.f_frsize)
}

View File

@ -34,7 +34,17 @@ cfg_if! {
use std::{borrow::Cow, time::Duration};
use super::DataCollector;
use crate::{utils::error, Pid};
use crate::utils::error;
cfg_if! {
if #[cfg(target_family = "windows")] {
/// A Windows process ID.
pub type Pid = usize;
} else if #[cfg(target_family = "unix")] {
/// A UNIX process ID.
pub type Pid = libc::pid_t;
}
}
#[derive(Debug, Clone, Default)]
pub struct ProcessHarvest {

View File

@ -5,10 +5,7 @@ use std::{io, process::Command};
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer};
use crate::{
data_collection::{deserialize_xo, processes::UnixProcessExt},
Pid,
};
use crate::data_collection::{deserialize_xo, processes::UnixProcessExt, Pid};
#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "kebab-case")]

View File

@ -12,14 +12,13 @@ use hashbrown::HashSet;
use process::*;
use sysinfo::ProcessStatus;
use super::{ProcessHarvest, UserTable};
use super::{Pid, ProcessHarvest, UserTable};
use crate::{
data_collection::DataCollector,
utils::error::{self, BottomError},
Pid,
};
/// Maximum character length of a /proc/<PID>/stat process name.
/// Maximum character length of a `/proc/<PID>/stat`` process name.
/// If it's equal or greater, then we instead refer to the command for the name.
const MAX_STAT_NAME_LEN: usize = 15;

View File

@ -16,7 +16,7 @@ use rustix::{
path::Arg,
};
use crate::Pid;
use crate::data_collection::processes::Pid;
static PAGESIZE: OnceLock<u64> = OnceLock::new();

View File

@ -8,7 +8,7 @@ use hashbrown::HashMap;
use itertools::Itertools;
use super::UnixProcessExt;
use crate::Pid;
use crate::data_collection::Pid;
pub(crate) struct MacOSProcessExt;

View File

@ -10,7 +10,7 @@ use libc::{
};
use mach2::vm_types::user_addr_t;
use crate::Pid;
use crate::data_collection::Pid;
#[allow(non_camel_case_types)]
#[repr(C)]

View File

@ -6,7 +6,7 @@ use hashbrown::HashMap;
use sysinfo::{ProcessStatus, System};
use super::ProcessHarvest;
use crate::{data_collection::processes::UserTable, utils::error, Pid};
use crate::{data_collection::processes::UserTable, data_collection::Pid, utils::error};
pub(crate) trait UnixProcessExt {
fn sysinfo_process_data(

142
src/event.rs Normal file
View File

@ -0,0 +1,142 @@
//! Some code around handling events.
use std::sync::mpsc::Sender;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use crate::{
app::{layout_manager::WidgetDirection, App},
data_collection::Data,
};
/// Events sent to the main thread.
#[derive(Debug)]
pub enum BottomEvent {
Resize,
KeyInput(KeyEvent),
MouseInput(MouseEvent),
PasteEvent(String),
Update(Box<Data>),
Clean,
Terminate,
}
/// Events sent to the collection thread.
#[derive(Debug)]
pub enum CollectionThreadEvent {
Reset,
}
/// Handle a [`MouseEvent`].
pub fn handle_mouse_event(event: MouseEvent, app: &mut App) {
match event.kind {
MouseEventKind::ScrollUp => app.handle_scroll_up(),
MouseEventKind::ScrollDown => app.handle_scroll_down(),
MouseEventKind::Down(button) => {
let (x, y) = (event.column, event.row);
if !app.app_config_fields.disable_click {
match button {
crossterm::event::MouseButton::Left => {
// Trigger left click widget activity
app.on_left_mouse_up(x, y);
}
crossterm::event::MouseButton::Right => {}
_ => {}
}
}
}
_ => {}
};
}
/// Handle a [`KeyEvent`].
pub fn handle_key_event_or_break(
event: KeyEvent, app: &mut App, reset_sender: &Sender<CollectionThreadEvent>,
) -> bool {
// c_debug!("KeyEvent: {event:?}");
if event.modifiers.is_empty() {
// Required catch for searching - otherwise you couldn't search with q.
if event.code == KeyCode::Char('q') && !app.is_in_search_widget() {
return true;
}
match event.code {
KeyCode::End => app.skip_to_last(),
KeyCode::Home => app.skip_to_first(),
KeyCode::Up => app.on_up_key(),
KeyCode::Down => app.on_down_key(),
KeyCode::Left => app.on_left_key(),
KeyCode::Right => app.on_right_key(),
KeyCode::Char(caught_char) => app.on_char_key(caught_char),
KeyCode::Esc => app.on_esc(),
KeyCode::Enter => app.on_enter(),
KeyCode::Tab => app.on_tab(),
KeyCode::Backspace => app.on_backspace(),
KeyCode::Delete => app.on_delete(),
KeyCode::F(1) => app.toggle_ignore_case(),
KeyCode::F(2) => app.toggle_search_whole_word(),
KeyCode::F(3) => app.toggle_search_regex(),
KeyCode::F(5) => app.toggle_tree_mode(),
KeyCode::F(6) => app.toggle_sort_menu(),
KeyCode::F(9) => app.start_killing_process(),
KeyCode::PageDown => app.on_page_down(),
KeyCode::PageUp => app.on_page_up(),
_ => {}
}
} else {
// Otherwise, track the modifier as well...
if let KeyModifiers::ALT = event.modifiers {
match event.code {
KeyCode::Char('c') | KeyCode::Char('C') => app.toggle_ignore_case(),
KeyCode::Char('w') | KeyCode::Char('W') => app.toggle_search_whole_word(),
KeyCode::Char('r') | KeyCode::Char('R') => app.toggle_search_regex(),
KeyCode::Char('h') => app.on_left_key(),
KeyCode::Char('l') => app.on_right_key(),
_ => {}
}
} else if let KeyModifiers::CONTROL = event.modifiers {
if event.code == KeyCode::Char('c') {
return true;
}
match event.code {
KeyCode::Char('f') => app.on_slash(),
KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left),
KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right),
KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up),
KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),
KeyCode::Char('r') => {
if reset_sender.send(CollectionThreadEvent::Reset).is_ok() {
app.reset();
}
}
KeyCode::Char('a') => app.skip_cursor_beginning(),
KeyCode::Char('e') => app.skip_cursor_end(),
KeyCode::Char('u') if app.is_in_search_widget() => app.clear_search(),
KeyCode::Char('w') => app.clear_previous_word(),
KeyCode::Char('h') => app.on_backspace(),
KeyCode::Char('d') => app.scroll_half_page_down(),
KeyCode::Char('u') => app.scroll_half_page_up(),
// KeyCode::Char('j') => {}, // Move down
// KeyCode::Char('k') => {}, // Move up
// KeyCode::Char('h') => {}, // Move right
// KeyCode::Char('l') => {}, // Move left
// Can't do now, CTRL+BACKSPACE doesn't work and graphemes
// are hard to iter while truncating last (eloquently).
// KeyCode::Backspace => app.skip_word_backspace(),
_ => {}
}
} else if let KeyModifiers::SHIFT = event.modifiers {
match event.code {
KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left),
KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right),
KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up),
KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),
KeyCode::Char(caught_char) => app.on_char_key(caught_char),
_ => {}
}
}
}
false
}

View File

@ -1,547 +0,0 @@
//! A customizable cross-platform graphical process/system monitor for the terminal.
//! Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and htop.
//!
//! **Note:** The following documentation is primarily intended for people to refer to for development purposes rather
//! than the actual usage of the application. If you are instead looking for documentation regarding the *usage* of
//! bottom, refer to [here](https://clementtsang.github.io/bottom/stable/).
#![deny(rust_2018_idioms)]
// #![deny(missing_docs)]
#![deny(unused_extern_crates)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::missing_crate_level_docs)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::missing_safety_doc)]
pub mod app;
pub mod utils {
pub mod data_prefixes;
pub mod data_units;
pub mod error;
pub mod general;
pub mod logging;
pub mod strings;
}
pub mod canvas;
pub mod constants;
pub mod data_collection;
pub mod data_conversion;
pub mod options;
pub mod widgets;
use std::{
boxed::Box,
fs,
io::{stderr, stdout, Write},
panic::PanicInfo,
path::{Path, PathBuf},
sync::{
mpsc::{Receiver, Sender},
Arc, Condvar, Mutex,
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use app::{
frozen_state::FrozenState,
layout_manager::{UsedWidgets, WidgetDirection},
App, AppConfigFields, DataFilters,
};
use constants::*;
use crossterm::{
event::{
poll, read, DisableBracketedPaste, DisableMouseCapture, Event, KeyCode, KeyEvent,
KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind,
},
execute,
style::Print,
terminal::{disable_raw_mode, LeaveAlternateScreen},
};
use data_conversion::*;
pub use options::args;
use options::ConfigV1;
use utils::error;
#[allow(unused_imports)]
pub use utils::logging::*;
#[cfg(target_family = "windows")]
pub type Pid = usize;
#[cfg(target_family = "unix")]
pub type Pid = libc::pid_t;
/// Events sent to the main thread.
#[derive(Debug)]
pub enum BottomEvent {
Resize,
KeyInput(KeyEvent),
MouseInput(MouseEvent),
PasteEvent(String),
Update(Box<data_collection::Data>),
Clean,
Terminate,
}
/// Events sent to the collection thread.
#[derive(Debug)]
pub enum CollectionThreadEvent {
Reset,
}
pub fn handle_mouse_event(event: MouseEvent, app: &mut App) {
match event.kind {
MouseEventKind::ScrollUp => app.handle_scroll_up(),
MouseEventKind::ScrollDown => app.handle_scroll_down(),
MouseEventKind::Down(button) => {
let (x, y) = (event.column, event.row);
if !app.app_config_fields.disable_click {
match button {
crossterm::event::MouseButton::Left => {
// Trigger left click widget activity
app.on_left_mouse_up(x, y);
}
crossterm::event::MouseButton::Right => {}
_ => {}
}
}
}
_ => {}
};
}
pub fn handle_key_event_or_break(
event: KeyEvent, app: &mut App, reset_sender: &Sender<CollectionThreadEvent>,
) -> bool {
// c_debug!("KeyEvent: {event:?}");
if event.modifiers.is_empty() {
// Required catch for searching - otherwise you couldn't search with q.
if event.code == KeyCode::Char('q') && !app.is_in_search_widget() {
return true;
}
match event.code {
KeyCode::End => app.skip_to_last(),
KeyCode::Home => app.skip_to_first(),
KeyCode::Up => app.on_up_key(),
KeyCode::Down => app.on_down_key(),
KeyCode::Left => app.on_left_key(),
KeyCode::Right => app.on_right_key(),
KeyCode::Char(caught_char) => app.on_char_key(caught_char),
KeyCode::Esc => app.on_esc(),
KeyCode::Enter => app.on_enter(),
KeyCode::Tab => app.on_tab(),
KeyCode::Backspace => app.on_backspace(),
KeyCode::Delete => app.on_delete(),
KeyCode::F(1) => app.toggle_ignore_case(),
KeyCode::F(2) => app.toggle_search_whole_word(),
KeyCode::F(3) => app.toggle_search_regex(),
KeyCode::F(5) => app.toggle_tree_mode(),
KeyCode::F(6) => app.toggle_sort_menu(),
KeyCode::F(9) => app.start_killing_process(),
KeyCode::PageDown => app.on_page_down(),
KeyCode::PageUp => app.on_page_up(),
_ => {}
}
} else {
// Otherwise, track the modifier as well...
if let KeyModifiers::ALT = event.modifiers {
match event.code {
KeyCode::Char('c') | KeyCode::Char('C') => app.toggle_ignore_case(),
KeyCode::Char('w') | KeyCode::Char('W') => app.toggle_search_whole_word(),
KeyCode::Char('r') | KeyCode::Char('R') => app.toggle_search_regex(),
KeyCode::Char('h') => app.on_left_key(),
KeyCode::Char('l') => app.on_right_key(),
_ => {}
}
} else if let KeyModifiers::CONTROL = event.modifiers {
if event.code == KeyCode::Char('c') {
return true;
}
match event.code {
KeyCode::Char('f') => app.on_slash(),
KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left),
KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right),
KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up),
KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),
KeyCode::Char('r') => {
if reset_sender.send(CollectionThreadEvent::Reset).is_ok() {
app.reset();
}
}
KeyCode::Char('a') => app.skip_cursor_beginning(),
KeyCode::Char('e') => app.skip_cursor_end(),
KeyCode::Char('u') if app.is_in_search_widget() => app.clear_search(),
KeyCode::Char('w') => app.clear_previous_word(),
KeyCode::Char('h') => app.on_backspace(),
KeyCode::Char('d') => app.scroll_half_page_down(),
KeyCode::Char('u') => app.scroll_half_page_up(),
// KeyCode::Char('j') => {}, // Move down
// KeyCode::Char('k') => {}, // Move up
// KeyCode::Char('h') => {}, // Move right
// KeyCode::Char('l') => {}, // Move left
// Can't do now, CTRL+BACKSPACE doesn't work and graphemes
// are hard to iter while truncating last (eloquently).
// KeyCode::Backspace => app.skip_word_backspace(),
_ => {}
}
} else if let KeyModifiers::SHIFT = event.modifiers {
match event.code {
KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left),
KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right),
KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up),
KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),
KeyCode::Char(caught_char) => app.on_char_key(caught_char),
_ => {}
}
}
}
false
}
pub fn get_config_path(override_config_path: Option<&Path>) -> Option<PathBuf> {
if let Some(conf_loc) = override_config_path {
Some(conf_loc.to_path_buf())
} else if cfg!(target_os = "windows") {
if let Some(home_path) = dirs::config_dir() {
let mut path = home_path;
path.push(DEFAULT_CONFIG_FILE_PATH);
Some(path)
} else {
None
}
} else if let Some(home_path) = dirs::home_dir() {
let mut path = home_path;
path.push(".config/");
path.push(DEFAULT_CONFIG_FILE_PATH);
if path.exists() {
// If it already exists, use the old one.
Some(path)
} else {
// If it does not, use the new one!
if let Some(config_path) = dirs::config_dir() {
let mut path = config_path;
path.push(DEFAULT_CONFIG_FILE_PATH);
Some(path)
} else {
None
}
}
} else {
None
}
}
pub fn get_or_create_config(override_config_path: Option<&Path>) -> error::Result<ConfigV1> {
let config_path = get_config_path(override_config_path);
if let Some(path) = &config_path {
if let Ok(config_string) = fs::read_to_string(path) {
Ok(toml_edit::de::from_str(config_string.as_str())?)
} else {
if let Some(parent_path) = path.parent() {
fs::create_dir_all(parent_path)?;
}
fs::File::create(path)?.write_all(CONFIG_TEXT.as_bytes())?;
Ok(ConfigV1::default())
}
} else {
// If we somehow don't have any config path, then just assume the default config but don't write to any file.
Ok(ConfigV1::default())
}
}
pub fn try_drawing(
terminal: &mut tui::terminal::Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
app: &mut App, painter: &mut canvas::Painter,
) -> error::Result<()> {
if let Err(err) = painter.draw_data(terminal, app) {
cleanup_terminal(terminal)?;
Err(err)
} else {
Ok(())
}
}
pub fn cleanup_terminal(
terminal: &mut tui::terminal::Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
) -> error::Result<()> {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
DisableBracketedPaste,
DisableMouseCapture,
LeaveAlternateScreen
)?;
terminal.show_cursor()?;
Ok(())
}
/// Check and report to the user if the current environment is not a terminal.
pub 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().unwrap();
thread::sleep(Duration::from_secs(1));
}
}
/// A panic hook to properly restore the terminal in the case of a panic.
/// Originally based on [spotify-tui's implementation](https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs).
pub fn panic_hook(panic_info: &PanicInfo<'_>) {
let mut stdout = stdout();
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!("{:?}", backtrace::Backtrace::new());
let _ = disable_raw_mode();
let _ = execute!(
stdout,
DisableBracketedPaste,
DisableMouseCapture,
LeaveAlternateScreen
);
// Print stack trace. Must be done after!
if let Some(panic_info) = panic_info.location() {
let _ = execute!(
stdout,
Print(format!(
"thread '<unnamed>' panicked at '{msg}', {panic_info}\n\r{backtrace}",
)),
);
}
}
pub fn update_data(app: &mut App) {
let data_source = match &app.frozen_state {
FrozenState::NotFrozen => &app.data_collection,
FrozenState::Frozen(data) => data,
};
for proc in app.states.proc_state.widget_states.values_mut() {
if proc.force_update_data {
proc.set_table_data(data_source);
proc.force_update_data = false;
}
}
// FIXME: Make this CPU force update less terrible.
if app.states.cpu_state.force_update.is_some() {
app.converted_data.convert_cpu_data(data_source);
app.converted_data.load_avg_data = data_source.load_avg_harvest;
app.states.cpu_state.force_update = None;
}
// FIXME: This is a bit of a temp hack to move data over.
{
let data = &app.converted_data.cpu_data;
for cpu in app.states.cpu_state.widget_states.values_mut() {
cpu.update_table(data);
}
}
{
let data = &app.converted_data.temp_data;
for temp in app.states.temp_state.widget_states.values_mut() {
if temp.force_update_data {
temp.set_table_data(data);
temp.force_update_data = false;
}
}
}
{
let data = &app.converted_data.disk_data;
for disk in app.states.disk_state.widget_states.values_mut() {
if disk.force_update_data {
disk.set_table_data(data);
disk.force_update_data = false;
}
}
}
// TODO: [OPT] Prefer reassignment over new vectors?
if app.states.mem_state.force_update.is_some() {
app.converted_data.mem_data = convert_mem_data_points(data_source);
#[cfg(not(target_os = "windows"))]
{
app.converted_data.cache_data = convert_cache_data_points(data_source);
}
app.converted_data.swap_data = convert_swap_data_points(data_source);
#[cfg(feature = "zfs")]
{
app.converted_data.arc_data = convert_arc_data_points(data_source);
}
#[cfg(feature = "gpu")]
{
app.converted_data.gpu_data = convert_gpu_data(data_source);
}
app.states.mem_state.force_update = None;
}
if app.states.net_state.force_update.is_some() {
let (rx, tx) = get_network_points(
data_source,
&app.app_config_fields.network_scale_type,
&app.app_config_fields.network_unit_type,
app.app_config_fields.network_use_binary_prefix,
);
app.converted_data.network_data_rx = rx;
app.converted_data.network_data_tx = tx;
app.states.net_state.force_update = None;
}
}
pub fn create_input_thread(
sender: Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
) -> JoinHandle<()> {
thread::spawn(move || {
let mut mouse_timer = Instant::now();
loop {
if let Ok(is_terminated) = termination_ctrl_lock.try_lock() {
// We don't block.
if *is_terminated {
drop(is_terminated);
break;
}
}
if let Ok(poll) = poll(Duration::from_millis(20)) {
if poll {
if let Ok(event) = read() {
match event {
Event::Resize(_, _) => {
// TODO: Might want to debounce this in the future, or take into account the actual resize
// values. Maybe we want to keep the current implementation in case the resize event might
// not fire... not sure.
if sender.send(BottomEvent::Resize).is_err() {
break;
}
}
Event::Paste(paste) => {
if sender.send(BottomEvent::PasteEvent(paste)).is_err() {
break;
}
}
Event::Key(key) if key.kind == KeyEventKind::Press => {
// For now, we only care about key down events. This may change in the future.
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 => {}
}
}
}
}
}
})
}
pub fn create_collection_thread(
sender: Sender<BottomEvent>, control_receiver: Receiver<CollectionThreadEvent>,
termination_lock: Arc<Mutex<bool>>, termination_cvar: Arc<Condvar>,
app_config_fields: &AppConfigFields, filters: DataFilters, used_widget_set: UsedWidgets,
) -> JoinHandle<()> {
let temp_type = app_config_fields.temperature_type;
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_time = app_config_fields.update_rate;
thread::spawn(move || {
let mut data_state = data_collection::DataCollector::new(filters);
data_state.set_data_collection(used_widget_set);
data_state.set_temperature_type(temp_type);
data_state.set_use_current_cpu_total(use_current_cpu_total);
data_state.set_unnormalized_cpu(unnormalized_cpu);
data_state.set_show_average_cpu(show_average_cpu);
data_state.init();
loop {
// Check once at the very top... don't block though.
if let Ok(is_terminated) = termination_lock.try_lock() {
if *is_terminated {
drop(is_terminated);
break;
}
}
if let Ok(message) = control_receiver.try_recv() {
// trace!("Received message in collection thread: {message:?}");
match message {
CollectionThreadEvent::Reset => {
data_state.data.cleanup();
}
}
}
data_state.update_data();
// Yet another check to bail if needed... do not block!
if let Ok(is_terminated) = termination_lock.try_lock() {
if *is_terminated {
drop(is_terminated);
break;
}
}
let event = BottomEvent::Update(Box::from(data_state.data));
data_state.data = data_collection::Data::default();
if sender.send(event).is_err() {
break;
}
// This is actually used as a "sleep" that can be interrupted by another thread.
if let Ok((is_terminated, _)) = termination_cvar.wait_timeout(
termination_lock.lock().unwrap(),
Duration::from_millis(update_time),
) {
if *is_terminated {
drop(is_terminated);
break;
}
}
}
})
}

View File

@ -1,46 +1,287 @@
#![deny(rust_2018_idioms)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::missing_safety_doc)]
//! A customizable cross-platform graphical process/system monitor for the terminal.
//! Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and htop.
//!
//! **Note:** The following documentation is primarily intended for people to refer to for development purposes rather
//! than the actual usage of the application. If you are instead looking for documentation regarding the *usage* of
//! bottom, refer to [here](https://clementtsang.github.io/bottom/stable/).
pub mod app;
pub mod utils {
pub mod data_prefixes;
pub mod data_units;
pub mod error;
pub mod general;
pub mod logging;
pub mod strings;
}
pub mod canvas;
pub mod constants;
pub mod data_collection;
pub mod data_conversion;
pub mod event;
pub mod options;
pub mod widgets;
use std::{
boxed::Box,
io::stdout,
panic,
sync::{mpsc, Arc, Condvar, Mutex},
thread,
time::Duration,
io::{stderr, stdout, Write},
panic::{self, PanicInfo},
sync::{
mpsc::{self, Receiver, Sender},
Arc, Condvar, Mutex,
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use bottom::{
args,
canvas::{self, styling::CanvasStyling},
check_if_terminal, cleanup_terminal, create_collection_thread, create_input_thread,
data_conversion::*,
get_or_create_config, handle_key_event_or_break, handle_mouse_event,
options::{get_color_scheme, init_app},
panic_hook, try_drawing, update_data, BottomEvent,
};
use anyhow::Context;
use app::{layout_manager::UsedWidgets, App, AppConfigFields, DataFilters};
use canvas::styling::CanvasStyling;
use crossterm::{
event::{EnableBracketedPaste, EnableMouseCapture},
event::{
poll, read, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste,
EnableMouseCapture, Event, KeyEventKind, MouseEventKind,
},
execute,
terminal::{enable_raw_mode, EnterAlternateScreen},
style::Print,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use data_conversion::*;
use event::{handle_key_event_or_break, handle_mouse_event, BottomEvent, CollectionThreadEvent};
use options::args;
use options::{get_color_scheme, get_config_path, get_or_create_config, init_app};
use tui::{backend::CrosstermBackend, Terminal};
use utils::error;
#[allow(unused_imports)]
use utils::logging::*;
// Used for heap allocation debugging purposes.
// #[global_allocator]
// static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() -> Result<()> {
/// Try drawing. If not, clean up the terminal and return an error.
fn try_drawing(
terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>, app: &mut App,
painter: &mut canvas::Painter,
) -> error::Result<()> {
if let Err(err) = painter.draw_data(terminal, app) {
cleanup_terminal(terminal)?;
Err(err)
} else {
Ok(())
}
}
/// Clean up the terminal before returning it to the user.
fn cleanup_terminal(
terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
) -> error::Result<()> {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
DisableBracketedPaste,
DisableMouseCapture,
LeaveAlternateScreen
)?;
terminal.show_cursor()?;
Ok(())
}
/// Check and report to the user if the current environment is not a terminal.
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().unwrap();
thread::sleep(Duration::from_secs(1));
}
}
/// A panic hook to properly restore the terminal in the case of a panic.
/// Originally based on [spotify-tui's implementation](https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs).
fn panic_hook(panic_info: &PanicInfo<'_>) {
let mut stdout = stdout();
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!("{:?}", backtrace::Backtrace::new());
let _ = disable_raw_mode();
let _ = execute!(
stdout,
DisableBracketedPaste,
DisableMouseCapture,
LeaveAlternateScreen
);
// Print stack trace. Must be done after!
if let Some(panic_info) = panic_info.location() {
let _ = execute!(
stdout,
Print(format!(
"thread '<unnamed>' panicked at '{msg}', {panic_info}\n\r{backtrace}",
)),
);
}
}
/// Create a thread to poll for user inputs and forward them to the main thread.
fn create_input_thread(
sender: Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
) -> JoinHandle<()> {
thread::spawn(move || {
let mut mouse_timer = Instant::now();
loop {
if let Ok(is_terminated) = termination_ctrl_lock.try_lock() {
// We don't block.
if *is_terminated {
drop(is_terminated);
break;
}
}
if let Ok(poll) = poll(Duration::from_millis(20)) {
if poll {
if let Ok(event) = read() {
match event {
Event::Resize(_, _) => {
// TODO: Might want to debounce this in the future, or take into account the actual resize
// values. Maybe we want to keep the current implementation in case the resize event might
// not fire... not sure.
if sender.send(BottomEvent::Resize).is_err() {
break;
}
}
Event::Paste(paste) => {
if sender.send(BottomEvent::PasteEvent(paste)).is_err() {
break;
}
}
Event::Key(key) if key.kind == KeyEventKind::Press => {
// For now, we only care about key down events. This may change in the future.
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 => {}
}
}
}
}
}
})
}
/// Create a thread to handle data collection.
fn create_collection_thread(
sender: Sender<BottomEvent>, control_receiver: Receiver<CollectionThreadEvent>,
termination_lock: Arc<Mutex<bool>>, termination_cvar: Arc<Condvar>,
app_config_fields: &AppConfigFields, filters: DataFilters, used_widget_set: UsedWidgets,
) -> JoinHandle<()> {
let temp_type = app_config_fields.temperature_type;
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_time = app_config_fields.update_rate;
thread::spawn(move || {
let mut data_state = data_collection::DataCollector::new(filters);
data_state.set_data_collection(used_widget_set);
data_state.set_temperature_type(temp_type);
data_state.set_use_current_cpu_total(use_current_cpu_total);
data_state.set_unnormalized_cpu(unnormalized_cpu);
data_state.set_show_average_cpu(show_average_cpu);
data_state.init();
loop {
// Check once at the very top... don't block though.
if let Ok(is_terminated) = termination_lock.try_lock() {
if *is_terminated {
drop(is_terminated);
break;
}
}
if let Ok(message) = control_receiver.try_recv() {
// trace!("Received message in collection thread: {message:?}");
match message {
CollectionThreadEvent::Reset => {
data_state.data.cleanup();
}
}
}
data_state.update_data();
// Yet another check to bail if needed... do not block!
if let Ok(is_terminated) = termination_lock.try_lock() {
if *is_terminated {
drop(is_terminated);
break;
}
}
let event = BottomEvent::Update(Box::from(data_state.data));
data_state.data = data_collection::Data::default();
if sender.send(event).is_err() {
break;
}
// This is actually used as a "sleep" that can be interrupted by another thread.
if let Ok((is_terminated, _)) = termination_cvar.wait_timeout(
termination_lock.lock().unwrap(),
Duration::from_millis(update_time),
) {
if *is_terminated {
drop(is_terminated);
break;
}
}
}
})
}
fn main() -> anyhow::Result<()> {
// let _profiler = dhat::Profiler::new_heap();
let args = args::get_args();
#[cfg(feature = "logging")]
{
if let Err(err) = bottom::init_logger(
if let Err(err) = init_logger(
log::LevelFilter::Debug,
Some(std::ffi::OsStr::new("debug.log")),
) {
@ -49,8 +290,11 @@ fn main() -> Result<()> {
}
// Read from config file.
let config = get_or_create_config(args.general.config_location.as_deref())
.context("Unable to parse or create the config file.")?;
let config = {
let config_path = get_config_path(args.general.config_location.as_deref());
get_or_create_config(config_path.as_deref())
.context("Unable to parse or create the config file.")?
};
// FIXME: Should move this into build app or config
let styling = {
@ -166,17 +410,17 @@ fn main() -> Result<()> {
if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) {
break;
}
update_data(&mut app);
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::MouseInput(event) => {
handle_mouse_event(event, &mut app);
update_data(&mut app);
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::PasteEvent(paste) => {
app.handle_paste(paste);
update_data(&mut app);
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::Update(data) => {
@ -295,7 +539,7 @@ fn main() -> Result<()> {
}
}
update_data(&mut app);
app.update_data();
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
}

View File

@ -8,6 +8,9 @@ pub mod config;
use std::{
convert::TryInto,
fs,
io::Write,
path::{Path, PathBuf},
str::FromStr,
time::{Duration, Instant},
};
@ -59,6 +62,63 @@ macro_rules! is_flag_enabled {
};
}
/// Returns the config path to use. If `override_config_path` is specified, then we will use
/// that. If not, then return the "default" config path, which is:
/// - If a path already exists at `<HOME>/bottom/bottom.toml`, then use that for legacy reasons.
/// - Otherwise, use `<SYSTEM_CONFIG_FOLDER>/bottom/bottom.toml`.
///
/// For more details on this, see [dirs](https://docs.rs/dirs/latest/dirs/fn.config_dir.html)'
/// documentation.
pub fn get_config_path(override_config_path: Option<&Path>) -> Option<PathBuf> {
const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml";
if let Some(conf_loc) = override_config_path {
return Some(conf_loc.to_path_buf());
} else if let Some(home_path) = dirs::home_dir() {
let mut old_home_path = home_path;
old_home_path.push(".config/");
old_home_path.push(DEFAULT_CONFIG_FILE_PATH);
if old_home_path.exists() {
// We used to create it at `<HOME>/DEFAULT_CONFIG_FILE_PATH`, but changed it
// to be more correct later. However, for legacy reasons, if it already exists,
// use the old one.
return Some(old_home_path);
}
}
// Otherwise, return the "correct" path based on the config dir.
dirs::config_dir().map(|mut path| {
path.push(DEFAULT_CONFIG_FILE_PATH);
path
})
}
/// Get the config at `config_path`. If there is no config file at the specified path, it will
/// try to create a new file with the default settings, and return the default config. If bottom
/// fails to write a new config, it will silently just return the default config.
pub fn get_or_create_config(config_path: Option<&Path>) -> error::Result<ConfigV1> {
match &config_path {
Some(path) => {
if let Ok(config_string) = fs::read_to_string(path) {
Ok(toml_edit::de::from_str(config_string.as_str())?)
} else {
if let Some(parent_path) = path.parent() {
fs::create_dir_all(parent_path)?;
}
fs::File::create(path)?.write_all(CONFIG_TEXT.as_bytes())?;
Ok(ConfigV1::default())
}
}
None => {
// If we somehow don't have any config path, then just assume the default config but don't write to any file.
//
// TODO: Maybe make this "show" an error, but don't crash.
Ok(ConfigV1::default())
}
}
}
pub fn init_app(
args: BottomArgs, config: ConfigV1, styling: &CanvasStyling,
) -> Result<(App, BottomLayout)> {

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
/// Workaround as per https://github.com/serde-rs/serde/issues/1030
/// Workaround as per <https://github.com/serde-rs/serde/issues/1030>.
fn default_as_true() -> bool {
true
}

View File

@ -25,8 +25,7 @@ use crate::{
},
styling::CanvasStyling,
},
data_collection::processes::ProcessHarvest,
Pid,
data_collection::processes::{Pid, ProcessHarvest},
};
/// ProcessSearchState only deals with process' search's current settings and state.
@ -1012,7 +1011,7 @@ impl ProcWidgetState {
self.table.columns.iter().filter(|c| !c.is_hidden).count()
}
/// Sets the [`ProcWidget`]'s current sort index to whatever was in the sort table if possible, then closes the
/// Sets the [`ProcWidgetState`]'s current sort index to whatever was in the sort table if possible, then closes the
/// sort table.
pub(crate) fn use_sort_table_value(&mut self) {
self.table.set_sort_index(self.sort_table.current_index());

View File

@ -15,9 +15,8 @@ use crate::{
components::data_table::{DataTableColumn, DataToCell},
Painter,
},
data_collection::processes::ProcessHarvest,
data_collection::processes::{Pid, ProcessHarvest},
data_conversion::{binary_byte_string, dec_bytes_per_second_string, dec_bytes_string},
Pid,
};
#[derive(Clone, Debug)]