summaryrefslogtreecommitdiffstats
path: root/src/ui/components
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2019-08-31 15:37:46 +0300
committerManos Pitsidianakis <el13635@mail.ntua.gr>2019-08-31 15:37:46 +0300
commit31bf144ecdfa3c1da9e38e1f6338329aab996680 (patch)
treeb0aaaa045905bb78adc6e60af16b867e93c4e371 /src/ui/components
Initial commit
Diffstat (limited to 'src/ui/components')
-rw-r--r--src/ui/components/kernel.rs376
-rw-r--r--src/ui/components/processes.rs334
-rw-r--r--src/ui/components/utilities.rs282
-rw-r--r--src/ui/components/utilities/widgets.rs69
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(' '