diff options
author | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2019-08-31 15:37:46 +0300 |
---|---|---|
committer | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2019-08-31 15:37:46 +0300 |
commit | 31bf144ecdfa3c1da9e38e1f6338329aab996680 (patch) | |
tree | b0aaaa045905bb78adc6e60af16b867e93c4e371 /src/ui/components |
Initial commit
Diffstat (limited to 'src/ui/components')
-rw-r--r-- | src/ui/components/kernel.rs | 376 | ||||
-rw-r--r-- | src/ui/components/processes.rs | 334 | ||||
-rw-r--r-- | src/ui/components/utilities.rs | 282 | ||||
-rw-r--r-- | src/ui/components/utilities/widgets.rs | 69 |
4 files changed, 1061 insertions, 0 deletions
diff --git a/src/ui/components/kernel.rs b/src/ui/components/kernel.rs new file mode 100644 index 0000000..15d923b --- /dev/null +++ b/src/ui/components/kernel.rs @@ -0,0 +1,376 @@ +use super::*; +use std::fs::File; +use std::io::prelude::*; +use std::str::FromStr; + +/* Kernel metrics components */ +#[derive(Debug)] +pub struct KernelMetrics { + hostname: String, + kernel: String, + os_type: String, + uptime: String, + cpu_stat: Vec<Stat>, + boot_time: usize, + dirty: bool, +} + +impl fmt::Display for KernelMetrics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "kernel") + } +} + +impl KernelMetrics { + pub fn new() -> Self { + let mut file = File::open("/proc/sys/kernel/hostname").unwrap(); + let mut hostname = String::new(); + file.read_to_string(&mut hostname).unwrap(); + let mut kernel = String::new(); + file = File::open("/proc/sys/kernel/version").unwrap(); + file.read_to_string(&mut kernel).unwrap(); + let mut os_type = String::new(); + file = File::open("/proc/sys/kernel/ostype").unwrap(); + file.read_to_string(&mut os_type).unwrap(); + let mut boot_time = 0; + let cpu_stat = get_stat(&mut boot_time); + KernelMetrics { + hostname, + kernel, + os_type, + uptime: String::with_capacity(60), + cpu_stat, + boot_time, + dirty: true, + } + } +} + +impl Component for KernelMetrics { + fn draw( + &mut self, + grid: &mut CellBuffer, + area: Area, + dirty_areas: &mut VecDeque<Area>, + tick: bool, + ) { + if !is_valid_area!(area) { + return; + } + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + let total_rows = height!(area); + let total_cols = width!(area); + dirty_areas.push_back(area); + if self.dirty { + clear_area(grid, area); + let (x, y) = write_string_to_grid( + &self.hostname, + grid, + Color::Default, + Color::Default, + Attr::Bold, + area, + false, + ); + let (x, y) = write_string_to_grid( + &self.os_type, + grid, + Color::Default, + Color::Default, + Attr::Default, + ((x + 2, y), bottom_right), + false, + ); + let (x, y) = write_string_to_grid( + &self.kernel, + grid, + Color::Default, + Color::Default, + Attr::Default, + ((x + 2, y), bottom_right), + false, + ); + self.dirty = false; + } + + /* Draw uptime */ + let mut file = File::open("/proc/uptime").unwrap(); + self.uptime.clear(); + file.read_to_string(&mut self.uptime).unwrap(); + let seconds: usize = + f64::from_str(self.uptime.split(" ").next().unwrap()).unwrap() as usize; + let days = seconds / (60 * 60 * 24); + let hours = seconds / 3600 - days * 24; + let mins = seconds / 60 - hours * 60 - days * 24 * 60; + let seconds = seconds % 60; + let uptime = if days > 0 { + format!( + "uptime: {} days, {:02}:{:02}:{:02}", + days, hours, mins, seconds + ) + } else { + format!("uptime: {:02}:{:02}:{:02}", hours, mins, seconds) + }; + + write_string_to_grid( + &uptime, + grid, + Color::Default, + Color::Default, + Attr::Default, + ( + (get_x(bottom_right) - uptime.len(), get_y(upper_left)), + bottom_right, + ), + false, + ); + + let mut y_offset = 2; + + if !tick { + return; + } + /* Draw CPU usage bars */ + + let bar_max = std::dbg!((0.6 * total_cols as f32) as usize); + + let mut boot_time: usize = 0; + for (i, cpu_stat) in get_stat(&mut boot_time).into_iter().enumerate() { + let (mut x, y) = write_string_to_grid( + "CPU", + grid, + Color::Byte(250), + Color::Default, + Attr::Default, + (pos_inc(upper_left, (2, 2 + i)), bottom_right), + false, + ); + if i > 0 { + write_string_to_grid( + &i.to_string(), + grid, + Color::Default, + Color::Default, + Attr::Default, + ((x, y), bottom_right), + false, + ); + } else { + /* add padding */ + write_string_to_grid( + " ", + grid, + Color::Default, + Color::Default, + Attr::Default, + ((x, y), bottom_right), + false, + ); + } + x += 2; + + /* Calculate percentages for the cpu usage bar */ + let busy_length = (cpu_stat.user_time + cpu_stat.system_time) + - (self.cpu_stat[i].user_time + self.cpu_stat[i].system_time); + let iowait_length = cpu_stat.iowait_time - self.cpu_stat[i].iowait_time; + let bar_length: usize = std::dbg!( + (((busy_length + iowait_length) as f64 / 100.0) * bar_max as f64) as usize + ); + + let mut x_offset = 0; + while x_offset < bar_length { + write_string_to_grid( + "▁", + grid, + Color::Byte(235), + Color::Byte(240), + Attr::Default, + ((x + x_offset, y), bottom_right), + false, + ); + x_offset += 1; + } + while x_offset < bar_max { + write_string_to_grid( + "▁", + grid, + Color::Byte(236), + Color::Byte(235), + Attr::Default, + ((x + x_offset, y), bottom_right), + false, + ); + + x_offset += 1; + } + self.cpu_stat[i] = cpu_stat; + y_offset += 1; + } + self.boot_time = boot_time; + + /* Draw RAM usage bar */ + + y_offset += 1; + + let bar_max = bar_max + 5; + let (available, total) = get_mem_info(); + let available_length = ((available as f64 / total as f64) * bar_max as f64) as usize; + let mem_bar_length = bar_max - available_length; + let mem_display = format!( + "RAM {}/{}", + Bytes((total - available) * 1024).as_convenient_string(), + Bytes(total * 1024).as_convenient_string() + ); + let mem_display_padding = bar_max.saturating_sub(mem_display.len()) / 2; + + let mut x = 0; + /* Calculate spillover of mem_display string to available part of the bar in order to + * paint it differently */ + let cutoff = if mem_display_padding + mem_display.len() > mem_bar_length { + mem_bar_length - mem_display_padding + } else { + mem_display.len() + }; + + while x < mem_bar_length { + if x == mem_display_padding { + let (_x, _) = write_string_to_grid( + &mem_display[0..cutoff], + grid, + Color::White, + Color::Byte(240), + Attr::Default, + (pos_inc(upper_left, (x + 2, y_offset)), bottom_right), + false, + ); + x += cutoff; + } else { + write_string_to_grid( + "█", + grid, + Color::Byte(240), + Color::Byte(235), + Attr::Default, + (pos_inc(upper_left, (x + 2, y_offset)), bottom_right), + false, + ); + x += 1; + } + } + let x = if cutoff != mem_display.len() { + let (_x, _) = write_string_to_grid( + &mem_display[cutoff..], + grid, + Color::White, + Color::Byte(235), + Attr::Default, + (pos_inc(upper_left, (x + 2, y_offset)), bottom_right), + false, + ); + _x + } else { + x + }; + for x in x..bar_max { + write_string_to_grid( + " ", + grid, + Color::Default, + Color::Byte(235), + Attr::Default, + (pos_inc(upper_left, (x + 2, y_offset)), bottom_right), + false, + ); + } + } + + fn process_event(&mut self, event: &mut UIEvent) { + match event { + UIEvent::Resize => { + self.dirty = true; + } + _ => {} + } + } + + fn is_dirty(&self) -> bool { + true + } + + fn set_dirty(&mut self) { + self.dirty = true; + } +} + +fn get_mem_info() -> (usize, usize) { + let mut file = File::open("/proc/meminfo").unwrap(); + let mut res = String::with_capacity(2048); + file.read_to_string(&mut res).unwrap(); + let mut lines_iter = res.lines(); + let mem_total = usize::from_str( + lines_iter + .next() + .unwrap() + .split_whitespace() + .skip(1) + .next() + .unwrap(), + ) + .unwrap(); + let mem_available = usize::from_str( + lines_iter + .next() + .unwrap() + .split_whitespace() + .skip(1) + .next() + .unwrap(), + ) + .unwrap(); + (mem_available, mem_total) +} + +#[derive(Debug)] +struct Stat { + user_time: usize, + system_time: usize, + idle_time: usize, + iowait_time: usize, +} + +fn get_stat(boot_time: &mut usize) -> Vec<Stat> { + let mut file = File::open("/proc/stat").unwrap(); + let mut res = String::with_capacity(2048); + file.read_to_string(&mut res).unwrap(); + let mut lines_iter = res.lines(); + let mut ret = Vec::with_capacity(8); + let mut line; + loop { + line = lines_iter.next().unwrap(); + if !line.starts_with("cpu") { + break; + } + + let mut mut_value_iter = line.split_whitespace().skip(1); + + let user_time = usize::from_str(&mut_value_iter.next().unwrap()).unwrap(); + /* skip nice time */ + mut_value_iter.next(); + let system_time = usize::from_str(&mut_value_iter.next().unwrap()).unwrap(); + let idle_time = usize::from_str(&mut_value_iter.next().unwrap()).unwrap(); + let iowait_time = usize::from_str(&mut_value_iter.next().unwrap()).unwrap(); + ret.push(Stat { + user_time, + system_time, + idle_time, + iowait_time, + }); + } + while !line.starts_with("btime") { + line = lines_iter.next().unwrap(); + } + *boot_time = usize::from_str(&line.split_whitespace().skip(1).next().unwrap()).unwrap(); + + ret +} diff --git a/src/ui/components/processes.rs b/src/ui/components/processes.rs new file mode 100644 index 0000000..88d1fa3 --- /dev/null +++ b/src/ui/components/processes.rs @@ -0,0 +1,334 @@ +use super::*; +use std::fs::File; +use std::io::prelude::*; +use std::path::PathBuf; +use std::str::FromStr; + +/* process list components */ +#[derive(Debug)] +pub struct ProcessList { + page_movement: Option<PageMovement>, + pid_max: usize, + cursor: usize, + dirty: bool, +} + +enum State { + /* Z Zombie */ + Zombie, + /* R Running */ + Running, + /* S Sleeping in an interruptible wait */ + Sleeping, + /* D Waiting in uninterruptible disk sleep */ + Waiting, + /* T Stopped (on a signal) or (before Linux 2.6.33) trace stopped */ + Stopped, + /* t Tracing stop (Linux 2.6.33 onward) */ + Tracing, + /*W Paging (only before Linux 2.6.0) */ + Paging, + /* X Dead (from Linux 2.6.0 onward) */ + Dead, + /* K Wakekill (Linux 2.6.33 to 3.13 only) */ + Wakekill, + /* W Waking (Linux 2.6.33 to 3.13 only) */ + Waking, + /* P Parked (Linux 3.9 to 3.13 only) */ + Parked, +} + +impl From<char> for State { + fn from(val: char) -> State { + match val { + 'R' => State::Running, + 'I' => State::Sleeping, + 'S' => State::Sleeping, + 'D' => State::Waiting, + 'Z' => State::Zombie, + 'T' => State::Stopped, + 't' => State::Tracing, + 'W' => State::Paging, + 'X' => State::Dead, + 'x' => State::Dead, + 'K' => State::Wakekill, + 'W' => State::Waking, + 'P' => State::Parked, + _ => unreachable!(), + } + } +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + State::Running => 'R', + State::Sleeping => 'S', + State::Waiting => 'D', + State::Zombie => 'Z', + State::Stopped => 'T', + State::Tracing => 't', + State::Paging => 'W', + State::Dead => 'X', + State::Dead => 'x', + State::Wakekill => 'K', + State::Waking => 'W', + State::Parked => 'P', + _ => unreachable!(), + } + ) + } +} + +struct Process { + pid: usize, + ppid: usize, + vm_size: usize, + state: State, + uid: usize, + cmd_line: String, +} + +impl fmt::Display for ProcessList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "process list") + } +} + +impl ProcessList { + pub fn new() -> Self { + let mut file = File::open("/proc/sys/kernel/pid_max").unwrap(); + let mut pid_max = String::new(); + file.read_to_string(&mut pid_max).unwrap(); + ProcessList { + cursor: 0, + page_movement: None, + pid_max: usize::from_str(pid_max.trim()).unwrap(), + dirty: true, + } + } +} + +impl Component for ProcessList { + fn draw( + &mut self, + grid: &mut CellBuffer, + area: Area, + dirty_areas: &mut VecDeque<Area>, + tick: bool, + ) { + if !is_valid_area!(area) { + return; + } + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + /* Reserve first row for column headers */ + let height = height!(area) - 1; + let width = width!(area); + clear_area(grid, area); + dirty_areas.push_back(area); + + let mut processes: Vec<(String, String, String, String, String)> = Vec::with_capacity(1024); + let mut pid = 0; + let mut maxima = ("PID".len(), "PPID".len(), "VM_SIZE".len(), "STATE".len()); + for entry in std::fs::read_dir("/proc/").unwrap() { + let dir = entry.unwrap(); + if let Some(fname) = dir.file_name().to_str() { + if !fname.chars().all(|c| c.is_numeric()) { + continue; + } + } else { + continue; + } + let process = get_pid_info(dir.path()); + let strings = ( + process.pid.to_string(), + process.ppid.to_string(), + Bytes(process.vm_size * 1024).as_convenient_string(), + process.state.to_string(), + process.cmd_line, + ); + maxima.0 = std::cmp::max(maxima.0, strings.0.len()); + maxima.1 = std::cmp::max(maxima.1, strings.1.len()); + maxima.2 = std::cmp::max(maxima.2, strings.2.len()); + maxima.3 = std::cmp::max(maxima.3, strings.3.len()); + processes.push(strings); + } + + processes.sort_unstable_by(|a, b| a.4.cmp(&b.4)); + + if let Some(mvm) = self.page_movement.take() { + match mvm { + PageMovement::Home => { + self.cursor = 0; + } + PageMovement::PageUp => { + self.cursor = self.cursor.saturating_sub(height); + } + PageMovement::PageDown => { + self.cursor = + std::cmp::min(processes.len().saturating_sub(1), self.cursor + height); + } + PageMovement::End => { + self.cursor = processes.len().saturating_sub(1); + } + } + } + if self.dirty { + /* Write column headers */ + let (x, y) = write_string_to_grid( + &format!( + "{:>maxima0$} {:>maxima1$} {:>maxima2$} {:>maxima3$} {}", + "PID", + "PPID", + "VM_SIZE", + "STATE", + "CMD_LINE", + maxima0 = maxima.0, + maxima1 = maxima.1, + maxima2 = maxima.2, + maxima3 = maxima.3, + ), + grid, + Color::Black, + Color::White, + Attr::Default, + (pos_inc(upper_left, (0, 2)), bottom_right), + false, + ); + change_colors( + grid, + ((x, y), set_y(bottom_right, y)), + Color::Black, + Color::White, + ); + } + + let mut y_offset = 0; + let pages = self.cursor / height; + let p_len = processes.len(); + for (pid, ppid, vm_size, state, cmd_line) in processes.into_iter().skip(pages * height) { + let (x, y) = write_string_to_grid( + &format!( + "{:>maxima0$} {:>maxima1$} {:>maxima2$} {:>maxima3$} {}", + pid, + ppid, + vm_size, + state, + cmd_line, + maxima0 = maxima.0, + maxima1 = maxima.1, + maxima2 = maxima.2, + maxima3 = maxima.3, + ), + grid, + if pages * height + y_offset == self.cursor { + Color::Black + } else { + Color::Default + }, + if pages * height + y_offset == self.cursor { + Color::Byte(243) + } else { + Color::Default + }, + Attr::Default, + (pos_inc(upper_left, (0, y_offset + 3)), bottom_right), + false, + ); + y_offset += 1; + if y_offset >= height { + break; + } + } + } + + fn process_event(&mut self, event: &mut UIEvent) { + match event { + UIEvent::Input(Key::Up) => { + self.cursor = self.cursor.saturating_sub(1); + } + UIEvent::Input(Key::Down) => { + self.cursor += 1; + } + UIEvent::Input(Key::Home) => { + self.page_movement = Some(PageMovement::Home); + } + UIEvent::Input(Key::PageUp) => { + self.page_movement = Some(PageMovement::PageUp); + } + UIEvent::Input(Key::PageDown) => { + self.page_movement = Some(PageMovement::PageDown); + } + UIEvent::Input(Key::End) => { + self.page_movement = Some(PageMovement::End); + } + _ => {} + } + } + + fn is_dirty(&self) -> bool { + true + } + + fn set_dirty(&mut self) {} +} + +fn get_pid_info(mut path: PathBuf) -> Process { + path.push("status"); + let mut file: File = File::open(&path).unwrap(); + let mut res = String::with_capacity(2048); + file.read_to_string(&mut res).unwrap(); + let mut lines_iter = res.lines(); + let mut ret = Process { + pid: 0, + ppid: 0, + vm_size: 0, + uid: 0, + state: State::Waiting, + cmd_line: String::new(), + }; + let mut line; + + loop { + let line_opt = lines_iter.next(); + if line_opt.is_none() { + break; + } + line = line_opt.unwrap(); + let mut mut_value_iter = line.split_whitespace(); + match mut_value_iter.next().unwrap() { + "Name:" => { + ret.cmd_line = mut_value_iter.next().unwrap().to_string(); + } + "VmSize:" => { + ret.vm_size = usize::from_str(mut_value_iter.next().unwrap()).unwrap(); + } + "State:" => { + ret.state = State::from(mut_value_iter.next().unwrap().chars().next().unwrap()); + } + "Pid:" => { + ret.pid = usize::from_str(mut_value_iter.next().unwrap()).unwrap(); + } + "PPid:" => { + ret.ppid = usize::from_str(mut_value_iter.next().unwrap()).unwrap(); + } + "Uid:" => { + ret.uid = usize::from_str(mut_value_iter.next().unwrap()).unwrap(); + } + _ => {} + } + } + path.pop(); + path.push("cmdline"); + let mut file: File = File::open(&path).unwrap(); + res.clear(); + file.read_to_string(&mut res).unwrap(); + if !res.is_empty() { + ret.cmd_line = format!("{}", res.split('\0').collect::<Vec<&str>>().join(" ")); + } + ret +} diff --git a/src/ui/components/utilities.rs b/src/ui/components/utilities.rs new file mode 100644 index 0000000..274cc0b --- /dev/null +++ b/src/ui/components/utilities.rs @@ -0,0 +1,282 @@ +/* + * meli - ui crate. + * + * Copyright 2017-2018 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see <http://www.gnu.org/licenses/>. + */ + +/*! Various useful components that can be used in a generic fashion. + */ +use super::*; + +mod widgets; + +pub use self::widgets::*; + +/// A horizontally split in half container. +#[derive(Debug)] +pub struct HSplit { + top: Box<Component>, + bottom: Box<Component>, + show_divider: bool, + ratio: usize, // bottom/whole height * 100 +} + +impl fmt::Display for HSplit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + Display::fmt(&self.top, f) + } +} + +impl HSplit { + pub fn new( + top: Box<Component>, + bottom: Box<Component>, + ratio: usize, + show_divider: bool, + ) -> Self { + HSplit { + top, + bottom, + show_divider, + ratio, + } + } +} + +impl Component for HSplit { + fn draw( + &mut self, + grid: &mut CellBuffer, + area: Area, + dirty_areas: &mut VecDeque<Area>, + tick: bool, + ) { + if !is_valid_area!(area) { + return; + } + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + let total_rows = get_y(bottom_right) - get_y(upper_left); + let bottom_component_height = (self.ratio * total_rows) / 100; + let mid = get_y(upper_left) + total_rows - bottom_component_height; + + if self.show_divider { + for i in get_x(upper_left)..=get_x(bottom_right) { + grid[(i, mid)].set_ch('─'); + } + dirty_areas.push_back(((get_x(upper_left), mid), (get_x(bottom_right), mid))); + } + + self.top.draw( + grid, + ( + upper_left, + (get_x(bottom_right), get_y(upper_left) + mid - 1), + ), + dirty_areas, + tick, + ); + self.bottom.draw( + grid, + ((get_x(upper_left), get_y(upper_left) + mid), bottom_right), + dirty_areas, + tick, + ); + } + + fn process_event(&mut self, event: &mut UIEvent) { + self.top.process_event(event); + self.bottom.process_event(event); + } + + fn is_dirty(&self) -> bool { + self.top.is_dirty() || self.bottom.is_dirty() + } + + fn set_dirty(&mut self) { + self.top.set_dirty(); + self.bottom.set_dirty(); + } + + fn get_shortcuts(&self) -> ShortcutMaps { + let mut top_map = self.top.get_shortcuts(); + top_map.extend(self.bottom.get_shortcuts().into_iter()); + top_map + } +} + +/// A vertically split in half container. +#[derive(Debug)] +pub struct VSplit { + left: Box<Component>, + right: Box<Component>, + show_divider: bool, + /// This is the width of the right container to the entire width. + ratio: usize, // right/(container width) * 100 +} + +impl fmt::Display for VSplit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display focused component + Display::fmt(&self.right, f) + } +} + +impl VSplit { + pub fn new( + left: Box<Component>, + right: Box<Component>, + ratio: usize, + show_divider: bool, + ) -> Self { + VSplit { + left, + right, + show_divider, + ratio, + } + } +} + +impl Component for VSplit { + fn draw( + &mut self, + grid: &mut CellBuffer, + area: Area, + dirty_areas: &mut VecDeque<Area>, + tick: bool, + ) { + if !is_valid_area!(area) { + return; + } + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + let total_cols = get_x(bottom_right) - get_x(upper_left); + let right_component_width = (self.ratio * total_cols) / 100; + + let mid = get_x(bottom_right) - right_component_width; + + if get_y(upper_left) > 1 { + let c = grid + .get(mid, get_y(upper_left) - 1) + .map(Cell::ch) + .unwrap_or_else(|| ' '); + if let HORZ_BOUNDARY = c { + grid[(mid, get_y(upper_left) - 1)].set_ch(LIGHT_DOWN_AND_HORIZONTAL); + } + } + + if self.show_divider && mid != get_x(upper_left) { + for i in get_y(upper_left)..=get_y(bottom_right) { + grid[(mid, i)].set_ch(VERT_BOUNDARY); + grid[(mid, i)].set_fg(Color::Default); + grid[(mid, i)].set_bg(Color::Default); + } + if get_y(bottom_right) > 1 { + let c = grid + .get(mid, get_y(bottom_right) - 1) + .map(Cell::ch) + .unwrap_or_else(|| ' '); + if let HORZ_BOUNDARY = c { + grid[(mid, get_y(bottom_right) + 1)].set_ch(LIGHT_UP_AND_HORIZONTAL); + } + } + dirty_areas.push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right)))); + } + + if right_component_width == total_cols { + self.right.draw(grid, area, dirty_areas, tick); + } else if right_component_width == 0 { + self.left.draw(grid, area, dirty_areas, tick); + } else { + self.left.draw( + grid, + ( + upper_left, + ( + if self.show_divider { mid - 1 } else { mid }, + get_y(bottom_right), + ), + ), + dirty_areas, + tick, + ); + self.right.draw( + grid, + (set_x(upper_left, mid + 1), bottom_right), + dirty_areas, + tick, + ); + } + } + + fn process_event(&mut self, event: &mut UIEvent) { + self.left.process_event(event); + self.right.process_event(event); + } + + fn is_dirty(&self) -> bool { + self.left.is_dirty() || self.right.is_dirty() + } + + fn set_dirty(&mut self) { + self.left.set_dirty(); + self.right.set_dirty(); + } + + fn get_shortcuts(&self) -> ShortcutMaps { + let mut right_map = self.right.get_shortcuts(); + right_map.extend(self.left.get_shortcuts().into_iter()); + right_map + } +} + +#[derive(Debug)] +pub enum PageMovement { + Home, + PageUp, + PageDown, + End, +} + +const KILOBYTE: f64 = 1024.0; +const MEGABYTE: f64 = KILOBYTE * 1024.0; +const GIGABYTE: f64 = MEGABYTE * 1024.0; +const PETABYTE: f64 = GIGABYTE * 1024.0; + +pub struct Bytes(pub usize); + +impl Bytes { + pub fn as_convenient_string(&self) -> String { + let bytes = self.0 as f64; + if bytes == 0.0 { + "0".to_string() + } else if bytes < KILOBYTE { + format!("{:.2} bytes", bytes) + } else if bytes < MEGABYTE { + format!("{:.2} KiB", bytes / KILOBYTE) + } else if bytes < GIGABYTE { + format!("{:.2} MiB", bytes / MEGABYTE) + } else if bytes < PETABYTE { + format!("{:.2} GiB", bytes / GIGABYTE) + } else { + format!("{:.2} PiB", bytes / PETABYTE) + } + } +} diff --git a/src/ui/components/utilities/widgets.rs b/src/ui/components/utilities/widgets.rs new file mode 100644 index 0000000..ff20fda --- /dev/null +++ b/src/ui/components/utilities/widgets.rs @@ -0,0 +1,69 @@ +use super::*; + +#[derive(Default)] +pub struct ScrollBar { + show_arrows: bool, + block_character: Option<char>, +} + +impl ScrollBar { + pub fn set_show_arrows(&mut self, flag: bool) { + self.show_arrows = flag; + } + pub fn set_block_character(&mut self, val: Option<char>) { + self.block_character = val; + } + pub fn draw( + self, + grid: &mut CellBuffer, + area: Area, + pos: usize, + visible_rows: usize, + length: usize, + ) { + if length == 0 { + return; + } + let mut height = height!(area); + if height < 3 { + return; + } + if self.show_arrows { + height -= height; + } + clear_area(grid, area); + + let visible_ratio: f32 = (std::cmp::min(visible_rows, length) as f32) / (length as f32); + let scrollbar_height = std::cmp::max((visible_ratio * (height as f32)) as usize, 1); + let scrollbar_offset = { + let temp = (((pos as f32) / (length as f32)) * (height as f32)) as usize; + if temp + scrollbar_height >= height { + height - scrollbar_height + } else { + temp + } + }; + let (mut upper_left, bottom_right) = area; + + if self.show_arrows { + grid[upper_left].set_ch('▴'); + upper_left = (upper_left.0, upper_left.1 + 1); + } + + for y in get_y(upper_left)..(get_y(upper_left) + scrollbar_offset) { + grid[set_y(upper_left, y)].set_ch(' ' |