mirror of
https://github.com/imsnif/diskonaut
synced 2024-09-30 03:33:31 +00:00
initial commit
This commit is contained in:
commit
4a0e7b5e71
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.cargo/
|
||||
target/
|
||||
vendor/
|
||||
vendor.tar
|
||||
**/*.rs.bk
|
1637
Cargo.lock
generated
Normal file
1637
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
33
Cargo.toml
Normal file
33
Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "visual-size-ls"
|
||||
version = "0.6.2"
|
||||
authors = ["Aram Drevekenin <aram@poor.dev>"]
|
||||
description = """
|
||||
"""
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
tui = "0.5"
|
||||
bitflags = "1.0"
|
||||
cassowary = "0.3"
|
||||
itertools = "0.8"
|
||||
log = "0.4"
|
||||
either = "1.5"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
termion = "1.5"
|
||||
failure = "0.1"
|
||||
async-std = "*"
|
||||
futures = "*"
|
||||
walkdir = "2"
|
||||
jwalk = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
stderrlog = "0.4"
|
||||
rand = "0.6"
|
||||
failure = "0.1"
|
||||
structopt = "0.2"
|
||||
insta = "0.12.0"
|
||||
cargo-insta = "0.11.0"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Aram Drevekenin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
5
src/display/mod.rs
Normal file
5
src/display/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod rectangle_grid;
|
||||
pub mod state;
|
||||
|
||||
pub use state::*;
|
||||
pub use rectangle_grid::*;
|
322
src/display/rectangle_grid.rs
Normal file
322
src/display/rectangle_grid.rs
Normal file
|
@ -0,0 +1,322 @@
|
|||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Style, Color};
|
||||
use tui::symbols::line;
|
||||
use tui::widgets::{Borders, Widget};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RectWithText {
|
||||
pub rect: Rect,
|
||||
pub text: String,
|
||||
pub selected: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RectangleGrid {
|
||||
rectangles: Vec<RectWithText>
|
||||
}
|
||||
|
||||
// impl<'a> Default for RectangleGrid<'a> {
|
||||
// fn default() -> RectangleGrid<'a> {
|
||||
// RectangleGrid {
|
||||
// title: None,
|
||||
// title_style: Default::default(),
|
||||
// borders: Borders::NONE,
|
||||
// border_style: Default::default(),
|
||||
// style: Default::default(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct BoundariesToUse {
|
||||
pub TOP_RIGHT: String,
|
||||
pub VERTICAL: String,
|
||||
pub HORIZONTAL: String,
|
||||
pub TOP_LEFT: String,
|
||||
pub BOTTOM_RIGHT: String,
|
||||
pub BOTTOM_LEFT: String,
|
||||
pub VERTICAL_LEFT: String,
|
||||
pub VERTICAL_RIGHT: String,
|
||||
pub HORIZONTAL_DOWN: String,
|
||||
pub HORIZONTAL_UP: String,
|
||||
pub CROSS: String,
|
||||
}
|
||||
|
||||
pub mod boundaries {
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const VERTICAL: &str = "│";
|
||||
pub const HORIZONTAL: &str = "─";
|
||||
pub const TOP_LEFT: &str = "┌";
|
||||
pub const BOTTOM_RIGHT: &str = "┘";
|
||||
pub const BOTTOM_LEFT: &str = "└";
|
||||
pub const VERTICAL_LEFT: &str = "┤";
|
||||
pub const VERTICAL_RIGHT: &str = "├";
|
||||
pub const HORIZONTAL_DOWN: &str = "┬";
|
||||
pub const HORIZONTAL_UP: &str = "┴";
|
||||
pub const CROSS: &str = "┼";
|
||||
}
|
||||
|
||||
|
||||
pub mod selected_boundaries {
|
||||
pub const TOP_RIGHT: &str = "╗";
|
||||
pub const VERTICAL: &str = "║";
|
||||
pub const HORIZONTAL: &str = "═";
|
||||
pub const TOP_LEFT: &str = "╔";
|
||||
pub const BOTTOM_RIGHT: &str = "╝";
|
||||
pub const BOTTOM_LEFT: &str = "╚";
|
||||
pub const VERTICAL_LEFT: &str = "╣";
|
||||
pub const VERTICAL_RIGHT: &str = "╠";
|
||||
pub const HORIZONTAL_DOWN: &str = "╦";
|
||||
pub const HORIZONTAL_UP: &str = "╩";
|
||||
pub const CROSS: &str = "╬";
|
||||
}
|
||||
|
||||
impl<'a> RectangleGrid {
|
||||
pub fn new (rectangles: Vec<RectWithText>) -> Self {
|
||||
RectangleGrid { rectangles }
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_middle(row: &str, max_length: u16) -> String {
|
||||
if max_length <= 6 {
|
||||
String::from(".") // TODO: make sure this never happens
|
||||
// } else if max_length == 4 {
|
||||
// String::from("[..]")
|
||||
} else if row.len() as u16 > max_length {
|
||||
let first_slice = &row[0..(max_length as usize / 2) - 2];
|
||||
let second_slice = &row[(row.len() - (max_length / 2) as usize + 2)..row.len()];
|
||||
format!("{}[..]{}", first_slice, second_slice)
|
||||
} else {
|
||||
row.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for RectangleGrid {
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return;
|
||||
}
|
||||
for rect_with_text in &self.rectangles {
|
||||
|
||||
let rect_boundary_chars = BoundariesToUse {
|
||||
TOP_RIGHT: String::from(boundaries::TOP_RIGHT),
|
||||
VERTICAL: String::from(boundaries::VERTICAL),
|
||||
HORIZONTAL: String::from(boundaries::HORIZONTAL),
|
||||
TOP_LEFT: String::from(boundaries::TOP_LEFT),
|
||||
BOTTOM_RIGHT: String::from(boundaries::BOTTOM_RIGHT),
|
||||
BOTTOM_LEFT: String::from(boundaries::BOTTOM_LEFT),
|
||||
VERTICAL_LEFT: String::from(boundaries::VERTICAL_LEFT),
|
||||
VERTICAL_RIGHT: String::from(boundaries::VERTICAL_RIGHT),
|
||||
HORIZONTAL_DOWN: String::from(boundaries::HORIZONTAL_DOWN),
|
||||
HORIZONTAL_UP: String::from(boundaries::HORIZONTAL_UP),
|
||||
CROSS: String::from(boundaries::CROSS),
|
||||
};
|
||||
let rect = rect_with_text.rect;
|
||||
let max_text_length = if rect.width > 4 { rect.width - 4 } else { 0 };
|
||||
// TODO: we should not accept a rectangle with a width of less than 8 so that the text
|
||||
// will be at least partly legible... these rectangles should be created with a small
|
||||
// height instead
|
||||
let display_text = truncate_middle(&rect_with_text.text, max_text_length);
|
||||
let text_length = display_text.len(); // TODO: better
|
||||
|
||||
let text_start_position = ((rect.width - text_length as u16) as f64 / 2.0).ceil() as u16 + rect.x;
|
||||
|
||||
|
||||
|
||||
|
||||
let text_style = if rect_with_text.selected {
|
||||
Style::default().bg(Color::White).fg(Color::Black)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
buf.set_string(text_start_position, rect.height / 2 + rect.y, display_text, text_style);
|
||||
|
||||
for x in rect.x..(rect.x + rect.width + 1) {
|
||||
if x == rect.x {
|
||||
let current_symbol_top = &buf.get(x, rect.y).symbol;
|
||||
if current_symbol_top == &rect_boundary_chars.CROSS || current_symbol_top == &rect_boundary_chars.HORIZONTAL_DOWN {
|
||||
// no-op
|
||||
} else if current_symbol_top == &rect_boundary_chars.TOP_RIGHT || current_symbol_top == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y) // TODO: do not get twice?
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_DOWN);
|
||||
} else if current_symbol_top == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_top == &rect_boundary_chars.VERTICAL || current_symbol_top == &rect_boundary_chars.VERTICAL_RIGHT {
|
||||
buf.get_mut(x, rect.y) // TODO: do not get twice?
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL_RIGHT);
|
||||
} else if current_symbol_top == &rect_boundary_chars.HORIZONTAL_UP || current_symbol_top == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_top == &rect_boundary_chars.VERTICAL_LEFT {
|
||||
buf.get_mut(x, rect.y) // TODO: do not get twice?
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.TOP_LEFT);
|
||||
// .set_style(self.border_style);
|
||||
}
|
||||
|
||||
let current_symbol_bottom = &buf.get(x, rect.y + rect.height).symbol;
|
||||
if current_symbol_bottom == &rect_boundary_chars.BOTTOM_RIGHT || current_symbol_bottom == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else if current_symbol_bottom == &rect_boundary_chars.VERTICAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL_RIGHT);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.BOTTOM_LEFT);
|
||||
}
|
||||
} else if x == rect.x + rect.width {
|
||||
let current_symbol_top = &buf.get(x, rect.y).symbol;
|
||||
if current_symbol_top == &rect_boundary_chars.CROSS {
|
||||
// no-op
|
||||
} else if current_symbol_top == &rect_boundary_chars.TOP_LEFT || current_symbol_top == &rect_boundary_chars.TOP_RIGHT || current_symbol_top == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_DOWN);
|
||||
} else if current_symbol_top == &rect_boundary_chars.HORIZONTAL_UP {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else if current_symbol_top == &rect_boundary_chars.BOTTOM_RIGHT {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL_LEFT);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.TOP_RIGHT);
|
||||
}
|
||||
let current_symbol_bottom = &buf.get(x, rect.y + rect.height).symbol;
|
||||
if current_symbol_bottom == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_bottom == &rect_boundary_chars.BOTTOM_RIGHT || current_symbol_bottom == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.BOTTOM_RIGHT);
|
||||
}
|
||||
} else {
|
||||
let current_symbol_top = &buf.get(x, rect.y).symbol;
|
||||
if current_symbol_top == &rect_boundary_chars.CROSS || current_symbol_top == &rect_boundary_chars.HORIZONTAL_UP {
|
||||
// no-op
|
||||
} else if current_symbol_top == &rect_boundary_chars.TOP_LEFT || current_symbol_top == &rect_boundary_chars.TOP_RIGHT {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_DOWN);
|
||||
} else if current_symbol_top == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_top == &rect_boundary_chars.BOTTOM_RIGHT {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else if current_symbol_top == &rect_boundary_chars.VERTICAL {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL);
|
||||
}
|
||||
let current_symbol_bottom = &buf.get(x, rect.y + rect.height).symbol;
|
||||
if current_symbol_bottom == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_bottom == &rect_boundary_chars.BOTTOM_RIGHT {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else if current_symbol_bottom == &rect_boundary_chars.VERTICAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sides
|
||||
for y in (rect.y + 1)..(rect.y + rect.height) {
|
||||
let current_symbol_left = &buf.get(rect.x, y).symbol;
|
||||
if current_symbol_left == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(rect.x, y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(rect.x, y)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL);
|
||||
}
|
||||
let current_symbol_right = &buf.get(rect.x + rect.width, y).symbol;
|
||||
if current_symbol_right == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(rect.x + rect.width, y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(rect.x + rect.width, y)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// self.background(area, buf, self.style.bg);
|
||||
//
|
||||
// // Sides
|
||||
// if self.borders.intersects(Borders::LEFT) {
|
||||
// for y in area.top()..area.bottom() {
|
||||
// buf.get_mut(area.left(), y)
|
||||
// .set_symbol(line::VERTICAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
// if self.borders.intersects(Borders::TOP) {
|
||||
// for x in area.left()..area.right() {
|
||||
// buf.get_mut(x, area.top())
|
||||
// .set_symbol(line::HORIZONTAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
// if self.borders.intersects(Borders::RIGHT) {
|
||||
// let x = area.right() - 1;
|
||||
// for y in area.top()..area.bottom() {
|
||||
// buf.get_mut(x, y)
|
||||
// .set_symbol(line::VERTICAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
// if self.borders.intersects(Borders::BOTTOM) {
|
||||
// let y = area.bottom() - 1;
|
||||
// for x in area.left()..area.right() {
|
||||
// buf.get_mut(x, y)
|
||||
// .set_symbol(line::HORIZONTAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Corners
|
||||
// if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
// buf.get_mut(area.left(), area.top())
|
||||
// .set_symbol(line::TOP_LEFT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
// buf.get_mut(area.right() - 1, area.top())
|
||||
// .set_symbol(line::TOP_RIGHT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
// buf.get_mut(area.left(), area.bottom() - 1)
|
||||
// .set_symbol(line::BOTTOM_LEFT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
// buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
// .set_symbol(line::BOTTOM_RIGHT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
//
|
||||
// if area.width > 2 {
|
||||
// if let Some(title) = self.title {
|
||||
// let lx = if self.borders.intersects(Borders::LEFT) {
|
||||
// 1
|
||||
// } else {
|
||||
// 0
|
||||
// };
|
||||
// let rx = if self.borders.intersects(Borders::RIGHT) {
|
||||
// 1
|
||||
// } else {
|
||||
// 0
|
||||
// };
|
||||
// let width = area.width - lx - rx;
|
||||
// buf.set_stringn(
|
||||
// area.left() + lx,
|
||||
// area.top(),
|
||||
// title,
|
||||
// width as usize,
|
||||
// self.title_style,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
396
src/display/state.rs
Normal file
396
src/display/state.rs
Normal file
|
@ -0,0 +1,396 @@
|
|||
use tui::layout::Rect;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::display::rectangle_grid::RectWithText;
|
||||
use ::std::fmt;
|
||||
|
||||
pub struct DisplaySize(pub f64);
|
||||
|
||||
impl fmt::Display for DisplaySize{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.0 > 999_999_999.0 {
|
||||
write!(f, "{:.1}G", self.0 / 1_000_000_000.0)
|
||||
} else if self.0 > 999_999.0 {
|
||||
write!(f, "{:.1}M", self.0 / 1_000_000.0)
|
||||
} else if self.0 > 999.0 {
|
||||
write!(f, "{:.1}K", self.0 / 1000.0)
|
||||
} else {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FilePercentage {
|
||||
pub file_name: String,
|
||||
pub percentage: f64,
|
||||
}
|
||||
|
||||
const minimum_height: u16 = 2; // TODO: consts
|
||||
const minimum_width: u16 = 2;
|
||||
const height_width_ratio: f64 = 2.5;
|
||||
|
||||
pub struct State {
|
||||
pub tiles: Vec<RectWithText>,
|
||||
pub file_percentages: Vec<FilePercentage>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tiles: Vec::new(),
|
||||
file_percentages: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn set_file_percentages (&mut self, file_percentages: Vec<FilePercentage>) {
|
||||
self.file_percentages = file_percentages;
|
||||
}
|
||||
pub fn set_tiles(&mut self, full_screen: Rect) {
|
||||
let total_space_area = full_screen.width as f64 * full_screen.height as f64;
|
||||
let mut free_spaces = vec![full_screen];
|
||||
let mut rectangles_to_render = vec![];
|
||||
|
||||
let mut has_selected = false;
|
||||
let currently_selected = self.tiles.iter().find(|&t| t.selected == true);
|
||||
for file_percentage in &self.file_percentages {
|
||||
|
||||
let total_file_area = total_space_area * file_percentage.percentage;
|
||||
let file_square_side_width = full_screen.width as f64 * (file_percentage.percentage * 2.0);
|
||||
let file_square_side_height = total_file_area / file_square_side_width;
|
||||
|
||||
let mut candidate_index = 0;
|
||||
loop {
|
||||
if candidate_index >= free_spaces.len() {
|
||||
break;
|
||||
}
|
||||
if let Some(candidate) = free_spaces.get(candidate_index) {
|
||||
let mut file_rect = None;
|
||||
if candidate.width >= file_square_side_width as u16 &&
|
||||
candidate.height >= file_square_side_height as u16
|
||||
{
|
||||
file_rect = Some(RectWithText {
|
||||
rect: Rect {
|
||||
x: candidate.x,
|
||||
y: candidate.y,
|
||||
width: file_square_side_width as u16,
|
||||
height: file_square_side_height as u16,
|
||||
},
|
||||
text: file_percentage.file_name.clone(),
|
||||
selected: false,
|
||||
});
|
||||
} else if candidate.height >= file_square_side_height as u16 &&
|
||||
candidate.width >= (total_file_area / candidate.height as f64) as u16
|
||||
{
|
||||
let height = candidate.height;
|
||||
let width = (total_file_area / height as f64) as u16;
|
||||
file_rect = Some(RectWithText {
|
||||
rect: Rect {
|
||||
x: candidate.x,
|
||||
y: candidate.y,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
text: file_percentage.file_name.clone(),
|
||||
selected: false,
|
||||
});
|
||||
} else if candidate.width >= file_square_side_width as u16 &&
|
||||
candidate.height >= (total_file_area / candidate.width as f64) as u16
|
||||
{
|
||||
let width = candidate.width;
|
||||
let height = (total_file_area / width as f64) as u16;
|
||||
file_rect = Some(RectWithText {
|
||||
rect: Rect {
|
||||
x: candidate.x,
|
||||
y: candidate.y,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
text: file_percentage.file_name.clone(),
|
||||
selected: false,
|
||||
});
|
||||
} else if candidate.height >= minimum_height &&
|
||||
candidate.width >= ((total_file_area as u16 / minimum_height) as f64 * height_width_ratio) as u16
|
||||
{
|
||||
file_rect = Some(RectWithText {
|
||||
rect: Rect {
|
||||
x: candidate.x,
|
||||
y: candidate.y,
|
||||
width: ((total_file_area as u16 / minimum_height) as f64 * height_width_ratio) as u16,
|
||||
height: minimum_height,
|
||||
},
|
||||
text: file_percentage.file_name.clone(),
|
||||
selected: false,
|
||||
});
|
||||
} else if candidate.width >= minimum_width &&
|
||||
candidate.height >= ((total_file_area as u16 / minimum_width) as f64 / height_width_ratio) as u16
|
||||
{
|
||||
file_rect = Some(RectWithText {
|
||||
rect: Rect {
|
||||
x: candidate.x,
|
||||
y: candidate.y,
|
||||
width: minimum_width,
|
||||
height: ((total_file_area as u16 / minimum_width) as f64 / height_width_ratio) as u16,
|
||||
},
|
||||
text: file_percentage.file_name.clone(),
|
||||
selected: false,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
if let Some(mut rect_with_text) = file_rect {
|
||||
// TODO: insert these at index?
|
||||
if rect_with_text.rect.width > 1 && rect_with_text.rect.height > 1 {
|
||||
let new_rectangle_right = Rect {
|
||||
x: candidate.x + rect_with_text.rect.width,
|
||||
width: (candidate.width as i16 - rect_with_text.rect.width as i16).abs() as u16,
|
||||
y: candidate.y,
|
||||
height: rect_with_text.rect.height
|
||||
};
|
||||
let mut new_rectangle_bottom = Rect {
|
||||
x: candidate.x,
|
||||
width: candidate.width,
|
||||
y: candidate.y + rect_with_text.rect.height,
|
||||
height: (candidate.height as i16 - rect_with_text.rect.height as i16).abs() as u16,
|
||||
};
|
||||
free_spaces.remove(candidate_index); // TODO: better - read api
|
||||
if new_rectangle_right.width > 1 && new_rectangle_right.height > 1 && new_rectangle_right.x + new_rectangle_right.width <= full_screen.width {
|
||||
let free_rect_above_new = free_spaces.iter_mut()
|
||||
.find(|rect| rect.y + rect.height == new_rectangle_right.y && rect.x == new_rectangle_right.x && rect.x + rect.width == new_rectangle_right.x + new_rectangle_right.width);
|
||||
|
||||
match free_rect_above_new {
|
||||
Some(free_rect_above_new) => {
|
||||
free_rect_above_new.height += new_rectangle_right.height;
|
||||
},
|
||||
None => {
|
||||
free_spaces.push(new_rectangle_right);
|
||||
}
|
||||
}
|
||||
} else if new_rectangle_right.width == 1 {
|
||||
rect_with_text.rect.width += 1;
|
||||
}
|
||||
if new_rectangle_bottom.width > 1 && new_rectangle_bottom.height > 1 && new_rectangle_bottom.y + new_rectangle_bottom.height <= full_screen.height {
|
||||
free_spaces.push(new_rectangle_bottom);
|
||||
} else if new_rectangle_bottom.height == 1 {
|
||||
rect_with_text.rect.height += 1;
|
||||
new_rectangle_bottom.x = rect_with_text.rect.x + rect_with_text.rect.width;
|
||||
new_rectangle_bottom.width -= rect_with_text.rect.width;
|
||||
|
||||
let free_rect_above_new = free_spaces.iter_mut()
|
||||
.find(|rect| rect.y + rect.height == new_rectangle_bottom.y && rect.x == new_rectangle_bottom.x && rect.x + rect.width == new_rectangle_bottom.x + new_rectangle_bottom.width);
|
||||
|
||||
match free_rect_above_new {
|
||||
Some(free_rect_above_new) => {
|
||||
free_rect_above_new.height += new_rectangle_bottom.height;
|
||||
},
|
||||
None => {
|
||||
if new_rectangle_bottom.width > 1 && new_rectangle_bottom.height > 1 && new_rectangle_bottom.y + new_rectangle_bottom.height <= full_screen.height {
|
||||
free_spaces.push(new_rectangle_bottom);
|
||||
} else {
|
||||
// TODO: ???
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
match currently_selected {
|
||||
Some(currently_selected_rect) => {
|
||||
if currently_selected_rect.text == rect_with_text.text {
|
||||
rect_with_text.selected = true;
|
||||
rectangles_to_render.push(rect_with_text);
|
||||
} else {
|
||||
rect_with_text.selected = false;
|
||||
rectangles_to_render.push(rect_with_text);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
if has_selected {
|
||||
rect_with_text.selected = false;
|
||||
rectangles_to_render.push(rect_with_text);
|
||||
} else {
|
||||
has_selected = true;
|
||||
rect_with_text.selected = true;
|
||||
rectangles_to_render.push(rect_with_text);
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
} else {
|
||||
candidate_index += 1;
|
||||
// println!("\rnot bigger than 1!!!111!!111 {:?}", rect_with_text);
|
||||
}
|
||||
} else {
|
||||
candidate_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for free_rect in free_spaces {
|
||||
// rounding errors - TODO: find a better way to do this
|
||||
// TODO: throw if larger than 2
|
||||
let occupied_rect_left = rectangles_to_render.iter_mut()
|
||||
.find(|rect_to_render| {
|
||||
let occupied_rect = rect_to_render.rect;
|
||||
occupied_rect.y == free_rect.y && occupied_rect.x + occupied_rect.width == free_rect.x
|
||||
});
|
||||
match occupied_rect_left {
|
||||
Some(occupied_rect_left) => {
|
||||
occupied_rect_left.rect.width += free_rect.width
|
||||
},
|
||||
None => {
|
||||
// TODO: ?? throw?
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
self.tiles = rectangles_to_render
|
||||
}
|
||||
pub fn clone_currently_selected(&self) -> RectWithText {
|
||||
self.tiles.iter().find(|t| t.selected == true).expect("could not find selected rect").clone()
|
||||
}
|
||||
pub fn move_selected_right (&mut self) {
|
||||
let currently_selected = self.clone_currently_selected();
|
||||
let next_to_the_right = {
|
||||
let found_next = self.tiles.iter().find(|t| { // TODO: find the rectangle with the most overlap, not just the first
|
||||
t.rect.x == currently_selected.rect.x + currently_selected.rect.width &&
|
||||
(
|
||||
t.rect.y >= currently_selected.rect.y && t.rect.y < currently_selected.rect.y + currently_selected.rect.height ||
|
||||
t.rect.y + t.rect.height >= currently_selected.rect.y && t.rect.y + t.rect.height < currently_selected.rect.y + currently_selected.rect.height ||
|
||||
t.rect.y <= currently_selected.rect.y && t.rect.y + t.rect.height >= currently_selected.rect.y
|
||||
)
|
||||
});
|
||||
match found_next {
|
||||
Some(rect) => Some(rect.clone()),
|
||||
None => None
|
||||
}
|
||||
};
|
||||
match next_to_the_right {
|
||||
Some(rect) => {
|
||||
for tile in self.tiles.iter_mut() {
|
||||
if tile.text == rect.text {
|
||||
tile.selected = true;
|
||||
} else if tile.text == currently_selected.text {
|
||||
tile.selected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
pub fn move_selected_left(&mut self) {
|
||||
let currently_selected = self.clone_currently_selected();
|
||||
let next_to_the_left = {
|
||||
let found_next = self.tiles.iter().find(|t| { // TODO: find the rectangle with the most overlap, not just the first
|
||||
t.rect.x + t.rect.width == currently_selected.rect.x &&
|
||||
(
|
||||
t.rect.y >= currently_selected.rect.y && t.rect.y < currently_selected.rect.y + currently_selected.rect.height ||
|
||||
t.rect.y + t.rect.height >= currently_selected.rect.y && t.rect.y + t.rect.height < currently_selected.rect.y + currently_selected.rect.height ||
|
||||
t.rect.y <= currently_selected.rect.y && t.rect.y + t.rect.height >= currently_selected.rect.y
|
||||
)
|
||||
});
|
||||
match found_next {
|
||||
Some(rect) => Some(rect.clone()),
|
||||
None => None
|
||||
}
|
||||
};
|
||||
match next_to_the_left {
|
||||
Some(rect) => {
|
||||
for tile in self.tiles.iter_mut() {
|
||||
if tile.text == rect.text {
|
||||
tile.selected = true;
|
||||
} else if tile.text == currently_selected.text {
|
||||
tile.selected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
pub fn move_selected_down(&mut self) {
|
||||
let currently_selected = self.clone_currently_selected();
|
||||
let next_down = {
|
||||
let found_next = self.tiles.iter().find(|t| { // TODO: find the rectangle with the most overlap, not just the first
|
||||
t.rect.y == currently_selected.rect.y + currently_selected.rect.height &&
|
||||
(
|
||||
t.rect.x >= currently_selected.rect.x && t.rect.x < currently_selected.rect.x + currently_selected.rect.width ||
|
||||
t.rect.x + t.rect.width >= currently_selected.rect.x && t.rect.x + t.rect.width < currently_selected.rect.x + currently_selected.rect.width ||
|
||||
t.rect.x <= currently_selected.rect.x && t.rect.x + t.rect.width >= currently_selected.rect.x
|
||||
)
|
||||
});
|
||||
match found_next {
|
||||
Some(rect) => Some(rect.clone()),
|
||||
None => None
|
||||
}
|
||||
};
|
||||
match next_down {
|
||||
Some(rect) => {
|
||||
for tile in self.tiles.iter_mut() {
|
||||
if tile.text == rect.text {
|
||||
tile.selected = true;
|
||||
} else if tile.text == currently_selected.text {
|
||||
tile.selected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
pub fn move_selected_up(&mut self) {
|
||||
let currently_selected = self.clone_currently_selected();
|
||||
let next_up = {
|
||||
let found_next = self.tiles.iter().find(|t| { // TODO: find the rectangle with the most overlap, not just the first
|
||||
t.rect.y + t.rect.height == currently_selected.rect.y &&
|
||||
(
|
||||
t.rect.x >= currently_selected.rect.x && t.rect.x < currently_selected.rect.x + currently_selected.rect.width ||
|
||||
t.rect.x + t.rect.width >= currently_selected.rect.x && t.rect.x + t.rect.width < currently_selected.rect.x + currently_selected.rect.width ||
|
||||
t.rect.x <= currently_selected.rect.x && t.rect.x + t.rect.width >= currently_selected.rect.x
|
||||
)
|
||||
});
|
||||
match found_next {
|
||||
Some(rect) => Some(rect.clone()),
|
||||
None => None
|
||||
}
|
||||
};
|
||||
match next_up {
|
||||
Some(rect) => {
|
||||
for tile in self.tiles.iter_mut() {
|
||||
if tile.text == rect.text {
|
||||
tile.selected = true;
|
||||
} else if tile.text == currently_selected.text {
|
||||
tile.selected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_percentages (file_sizes: HashMap<String, u64>) -> Vec<FilePercentage> {
|
||||
let mut file_percentages = Vec::new();
|
||||
let total_size = file_sizes.values().fold(0, |acc, size| acc + size);
|
||||
let mut small_files = FilePercentage {
|
||||
file_name: String::from("Small files"),
|
||||
percentage: 0.0,
|
||||
};
|
||||
for (path, size) in file_sizes.iter() {
|
||||
let percentage = *size as f64 / total_size as f64;
|
||||
let file_percentage = FilePercentage {
|
||||
file_name: format!("{} {} ({:.0}%)", path, DisplaySize(*size as f64),percentage * 100.0),
|
||||
percentage,
|
||||
};
|
||||
if file_percentage.percentage <= 0.01 { // TODO: calculate this and not hard-coded
|
||||
small_files.percentage += file_percentage.percentage;
|
||||
} else {
|
||||
file_percentages.push(file_percentage);
|
||||
}
|
||||
}
|
||||
small_files.file_name = format!("Small files ({:.0}%)", small_files.percentage * 100.0);
|
||||
file_percentages.push(small_files);
|
||||
file_percentages.sort_by(|a, b| {
|
||||
if a.percentage == b.percentage {
|
||||
a.file_name.partial_cmp(&b.file_name).unwrap()
|
||||
} else {
|
||||
b.percentage.partial_cmp(&a.percentage).unwrap()
|
||||
}
|
||||
});
|
||||
file_percentages
|
||||
}
|
3
src/filesystem/mod.rs
Normal file
3
src/filesystem/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod scan_folder;
|
||||
|
||||
pub use scan_folder::*;
|
54
src/filesystem/scan_folder.rs
Normal file
54
src/filesystem/scan_folder.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
#[allow(dead_code)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::io;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::MetadataExt; // TODO: support other OSs
|
||||
use ::std::{thread, time};
|
||||
|
||||
use ::std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use failure;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::raw::IntoRawMode;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::Rect;
|
||||
use tui::widgets::Widget;
|
||||
use tui::Terminal;
|
||||
|
||||
use std::process;
|
||||
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use ::std::fmt;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ::tui::backend::Backend;
|
||||
use ::std::sync::Arc;
|
||||
|
||||
use ::std::io::stdin;
|
||||
use ::termion::input::TermRead;
|
||||
use ::termion::event::Event;
|
||||
|
||||
pub fn scan_folder (path: PathBuf) -> HashMap<String, u64> {
|
||||
let mut file_sizes: HashMap<String, u64> = HashMap::new();
|
||||
let path_length = path.components().count() + 1;
|
||||
for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
|
||||
let entry_path = entry.path().clone();
|
||||
// all_files.push(entry_path.clone());
|
||||
match fs::metadata(entry_path.display().to_string()) {
|
||||
Ok(file_metadata) => {
|
||||
let file_size_entry_name = entry_path.iter().take(path_length).last().expect("failed to get path name"); // TODO: less hacky (also, 3 should be the length of the base dir)
|
||||
let file_size_entry = file_sizes.entry(String::from(file_size_entry_name.to_string_lossy())).or_insert(0);
|
||||
*file_size_entry += file_metadata.blocks() * 512;
|
||||
},
|
||||
Err(_e) => {
|
||||
// println!("\rerror opening {:?} {:?}", entry, e); // TODO: look into these
|
||||
}
|
||||
}
|
||||
}
|
||||
file_sizes
|
||||
}
|
154
src/main.rs
Normal file
154
src/main.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::env;
|
||||
use std::io;
|
||||
use ::std::{thread, time};
|
||||
use ::std::thread::park_timeout;
|
||||
|
||||
use ::std::io::stdin;
|
||||
use ::termion::input::TermRead;
|
||||
use ::termion::event::Event;
|
||||
use ::std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use failure;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::raw::IntoRawMode;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::widgets::Widget;
|
||||
use tui::Terminal;
|
||||
|
||||
use std::process;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ::tui::backend::Backend;
|
||||
use ::std::sync::{Arc, Mutex};
|
||||
|
||||
mod filesystem;
|
||||
mod display;
|
||||
|
||||
use filesystem::scan_folder;
|
||||
use display::state::{State, calculate_percentages};
|
||||
use display::RectangleGrid;
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = try_main() {
|
||||
eprintln!("Error: {}", err);
|
||||
process::exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_main() -> Result<(), failure::Error> {
|
||||
match io::stdout().into_raw_mode() {
|
||||
Ok(stdout) => {
|
||||
let terminal_backend = TermionBackend::new(stdout);
|
||||
let keyboard_events = KeyboardEvents {};
|
||||
start(terminal_backend, Box::new(keyboard_events), env::current_dir()?);
|
||||
}
|
||||
Err(_) => failure::bail!(
|
||||
"Failed to get stdout: if you are trying to pipe 'bandwhich' you should use the --raw flag"
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeyboardEvents;
|
||||
|
||||
impl Iterator for KeyboardEvents {
|
||||
type Item = Event;
|
||||
fn next(&mut self) -> Option<Event> {
|
||||
match stdin().events().next() {
|
||||
Some(Ok(ev)) => Some(ev),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start<B>(terminal_backend: B, keyboard_events: Box<dyn Iterator<Item = Event> + Send>, path: PathBuf)
|
||||
where
|
||||
B: Backend + Send + 'static,
|
||||
{
|
||||
let mut active_threads = vec![];
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let mut terminal = Terminal::new(terminal_backend).expect("failed to create terminal");
|
||||
terminal.clear().expect("failed to clear terminal");
|
||||
terminal.hide_cursor().expect("failed to hide cursor");
|
||||
|
||||
let state = Arc::new(Mutex::new(State::new()));
|
||||
|
||||
let display_handler = thread::Builder::new()
|
||||
.name("display_handler".to_string())
|
||||
.spawn({
|
||||
let running = running.clone();
|
||||
let state = state.clone();
|
||||
move || {
|
||||
while running.load(Ordering::Acquire) {
|
||||
terminal.draw(|mut f| {
|
||||
let mut full_screen = f.size();
|
||||
full_screen.width -= 1;
|
||||
full_screen.height -= 1;
|
||||
state.lock().unwrap().set_tiles(full_screen);
|
||||
RectangleGrid::new((*state.lock().unwrap().tiles).to_vec()).render(&mut f, full_screen);
|
||||
}).expect("failed to draw");
|
||||
park_timeout(time::Duration::from_millis(1000)); // TODO: we might not need this... we can trigger the display on events
|
||||
}
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
active_threads.push(
|
||||
thread::Builder::new()
|
||||
.name("stdin_handler".to_string())
|
||||
.spawn({
|
||||
let running = running.clone();
|
||||
let state = state.clone();
|
||||
let display_handler = display_handler.thread().clone();
|
||||
move || {
|
||||
for evt in keyboard_events {
|
||||
match evt {
|
||||
Event::Key(Key::Ctrl('c')) | Event::Key(Key::Char('q')) => {
|
||||
running.store(false, Ordering::Release);
|
||||
display_handler.unpark();
|
||||
break;
|
||||
}
|
||||
Event::Key(Key::Char('l')) => {
|
||||
state.lock().unwrap().move_selected_right();
|
||||
display_handler.unpark();
|
||||
}
|
||||
Event::Key(Key::Char('h')) => {
|
||||
state.lock().unwrap().move_selected_left();
|
||||
display_handler.unpark();
|
||||
}
|
||||
Event::Key(Key::Char('j')) => {
|
||||
state.lock().unwrap().move_selected_down();
|
||||
display_handler.unpark();
|
||||
}
|
||||
Event::Key(Key::Char('k')) => {
|
||||
state.lock().unwrap().move_selected_up();
|
||||
display_handler.unpark();
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
active_threads.push(display_handler);
|
||||
|
||||
let file_sizes = scan_folder(path);
|
||||
let file_percentages = calculate_percentages(file_sizes);
|
||||
state.lock().unwrap().set_file_percentages(file_percentages);
|
||||
for thread_handler in active_threads {
|
||||
thread_handler.join().unwrap()
|
||||
}
|
||||
|
||||
}
|
322
src/rectangle_grid.rs
Normal file
322
src/rectangle_grid.rs
Normal file
|
@ -0,0 +1,322 @@
|
|||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Style, Color};
|
||||
use tui::symbols::line;
|
||||
use tui::widgets::{Borders, Widget};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RectWithText {
|
||||
pub rect: Rect,
|
||||
pub text: String,
|
||||
pub selected: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RectangleGrid {
|
||||
rectangles: Vec<RectWithText>
|
||||
}
|
||||
|
||||
// impl<'a> Default for RectangleGrid<'a> {
|
||||
// fn default() -> RectangleGrid<'a> {
|
||||
// RectangleGrid {
|
||||
// title: None,
|
||||
// title_style: Default::default(),
|
||||
// borders: Borders::NONE,
|
||||
// border_style: Default::default(),
|
||||
// style: Default::default(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct BoundariesToUse {
|
||||
pub TOP_RIGHT: String,
|
||||
pub VERTICAL: String,
|
||||
pub HORIZONTAL: String,
|
||||
pub TOP_LEFT: String,
|
||||
pub BOTTOM_RIGHT: String,
|
||||
pub BOTTOM_LEFT: String,
|
||||
pub VERTICAL_LEFT: String,
|
||||
pub VERTICAL_RIGHT: String,
|
||||
pub HORIZONTAL_DOWN: String,
|
||||
pub HORIZONTAL_UP: String,
|
||||
pub CROSS: String,
|
||||
}
|
||||
|
||||
pub mod boundaries {
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const VERTICAL: &str = "│";
|
||||
pub const HORIZONTAL: &str = "─";
|
||||
pub const TOP_LEFT: &str = "┌";
|
||||
pub const BOTTOM_RIGHT: &str = "┘";
|
||||
pub const BOTTOM_LEFT: &str = "└";
|
||||
pub const VERTICAL_LEFT: &str = "┤";
|
||||
pub const VERTICAL_RIGHT: &str = "├";
|
||||
pub const HORIZONTAL_DOWN: &str = "┬";
|
||||
pub const HORIZONTAL_UP: &str = "┴";
|
||||
pub const CROSS: &str = "┼";
|
||||
}
|
||||
|
||||
|
||||
pub mod selected_boundaries {
|
||||
pub const TOP_RIGHT: &str = "╗";
|
||||
pub const VERTICAL: &str = "║";
|
||||
pub const HORIZONTAL: &str = "═";
|
||||
pub const TOP_LEFT: &str = "╔";
|
||||
pub const BOTTOM_RIGHT: &str = "╝";
|
||||
pub const BOTTOM_LEFT: &str = "╚";
|
||||
pub const VERTICAL_LEFT: &str = "╣";
|
||||
pub const VERTICAL_RIGHT: &str = "╠";
|
||||
pub const HORIZONTAL_DOWN: &str = "╦";
|
||||
pub const HORIZONTAL_UP: &str = "╩";
|
||||
pub const CROSS: &str = "╬";
|
||||
}
|
||||
|
||||
impl<'a> RectangleGrid {
|
||||
pub fn new (rectangles: Vec<RectWithText>) -> Self {
|
||||
RectangleGrid { rectangles }
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_middle(row: &str, max_length: u16) -> String {
|
||||
if max_length <= 6 {
|
||||
String::from(".") // TODO: make sure this never happens
|
||||
// } else if max_length == 4 {
|
||||
// String::from("[..]")
|
||||
} else if row.len() as u16 > max_length {
|
||||
let first_slice = &row[0..(max_length as usize / 2) - 2];
|
||||
let second_slice = &row[(row.len() - (max_length / 2) as usize + 2)..row.len()];
|
||||
format!("{}[..]{}", first_slice, second_slice)
|
||||
} else {
|
||||
row.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for RectangleGrid {
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return;
|
||||
}
|
||||
for rect_with_text in &self.rectangles {
|
||||
|
||||
let rect_boundary_chars = BoundariesToUse {
|
||||
TOP_RIGHT: String::from(boundaries::TOP_RIGHT),
|
||||
VERTICAL: String::from(boundaries::VERTICAL),
|
||||
HORIZONTAL: String::from(boundaries::HORIZONTAL),
|
||||
TOP_LEFT: String::from(boundaries::TOP_LEFT),
|
||||
BOTTOM_RIGHT: String::from(boundaries::BOTTOM_RIGHT),
|
||||
BOTTOM_LEFT: String::from(boundaries::BOTTOM_LEFT),
|
||||
VERTICAL_LEFT: String::from(boundaries::VERTICAL_LEFT),
|
||||
VERTICAL_RIGHT: String::from(boundaries::VERTICAL_RIGHT),
|
||||
HORIZONTAL_DOWN: String::from(boundaries::HORIZONTAL_DOWN),
|
||||
HORIZONTAL_UP: String::from(boundaries::HORIZONTAL_UP),
|
||||
CROSS: String::from(boundaries::CROSS),
|
||||
};
|
||||
let rect = rect_with_text.rect;
|
||||
let max_text_length = if rect.width > 4 { rect.width - 4 } else { 0 };
|
||||
// TODO: we should not accept a rectangle with a width of less than 8 so that the text
|
||||
// will be at least partly legible... these rectangles should be created with a small
|
||||
// height instead
|
||||
let display_text = truncate_middle(&rect_with_text.text, max_text_length);
|
||||
let text_length = display_text.len(); // TODO: better
|
||||
|
||||
let text_start_position = ((rect.width - text_length as u16) as f64 / 2.0).ceil() as u16 + rect.x;
|
||||
|
||||
|
||||
|
||||
|
||||
let text_style = if rect_with_text.selected {
|
||||
Style::default().bg(Color::White).fg(Color::Black)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
buf.set_string(text_start_position, rect.height / 2 + rect.y, display_text, text_style);
|
||||
|
||||
for x in rect.x..(rect.x + rect.width + 1) {
|
||||
if x == rect.x {
|
||||
let current_symbol_top = &buf.get(x, rect.y).symbol;
|
||||
if current_symbol_top == &rect_boundary_chars.CROSS || current_symbol_top == &rect_boundary_chars.HORIZONTAL_DOWN {
|
||||
// no-op
|
||||
} else if current_symbol_top == &rect_boundary_chars.TOP_RIGHT || current_symbol_top == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y) // TODO: do not get twice?
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_DOWN);
|
||||
} else if current_symbol_top == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_top == &rect_boundary_chars.VERTICAL || current_symbol_top == &rect_boundary_chars.VERTICAL_RIGHT {
|
||||
buf.get_mut(x, rect.y) // TODO: do not get twice?
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL_RIGHT);
|
||||
} else if current_symbol_top == &rect_boundary_chars.HORIZONTAL_UP || current_symbol_top == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_top == &rect_boundary_chars.VERTICAL_LEFT {
|
||||
buf.get_mut(x, rect.y) // TODO: do not get twice?
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.TOP_LEFT);
|
||||
// .set_style(self.border_style);
|
||||
}
|
||||
|
||||
let current_symbol_bottom = &buf.get(x, rect.y + rect.height).symbol;
|
||||
if current_symbol_bottom == &rect_boundary_chars.BOTTOM_RIGHT || current_symbol_bottom == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else if current_symbol_bottom == &rect_boundary_chars.VERTICAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL_RIGHT);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.BOTTOM_LEFT);
|
||||
}
|
||||
} else if x == rect.x + rect.width {
|
||||
let current_symbol_top = &buf.get(x, rect.y).symbol;
|
||||
if current_symbol_top == &rect_boundary_chars.CROSS {
|
||||
// no-op
|
||||
} else if current_symbol_top == &rect_boundary_chars.TOP_LEFT || current_symbol_top == &rect_boundary_chars.TOP_RIGHT || current_symbol_top == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_DOWN);
|
||||
} else if current_symbol_top == &rect_boundary_chars.HORIZONTAL_UP {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else if current_symbol_top == &rect_boundary_chars.BOTTOM_RIGHT {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL_LEFT);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.TOP_RIGHT);
|
||||
}
|
||||
let current_symbol_bottom = &buf.get(x, rect.y + rect.height).symbol;
|
||||
if current_symbol_bottom == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_bottom == &rect_boundary_chars.BOTTOM_RIGHT || current_symbol_bottom == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.BOTTOM_RIGHT);
|
||||
}
|
||||
} else {
|
||||
let current_symbol_top = &buf.get(x, rect.y).symbol;
|
||||
if current_symbol_top == &rect_boundary_chars.CROSS || current_symbol_top == &rect_boundary_chars.HORIZONTAL_UP {
|
||||
// no-op
|
||||
} else if current_symbol_top == &rect_boundary_chars.TOP_LEFT || current_symbol_top == &rect_boundary_chars.TOP_RIGHT {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_DOWN);
|
||||
} else if current_symbol_top == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_top == &rect_boundary_chars.BOTTOM_RIGHT {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else if current_symbol_top == &rect_boundary_chars.VERTICAL {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL);
|
||||
}
|
||||
let current_symbol_bottom = &buf.get(x, rect.y + rect.height).symbol;
|
||||
if current_symbol_bottom == &rect_boundary_chars.BOTTOM_LEFT || current_symbol_bottom == &rect_boundary_chars.BOTTOM_RIGHT {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL_UP);
|
||||
} else if current_symbol_bottom == &rect_boundary_chars.VERTICAL {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(x, rect.y + rect.height)
|
||||
.set_symbol(&rect_boundary_chars.HORIZONTAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sides
|
||||
for y in (rect.y + 1)..(rect.y + rect.height) {
|
||||
let current_symbol_left = &buf.get(rect.x, y).symbol;
|
||||
if current_symbol_left == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(rect.x, y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(rect.x, y)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL);
|
||||
}
|
||||
let current_symbol_right = &buf.get(rect.x + rect.width, y).symbol;
|
||||
if current_symbol_right == &rect_boundary_chars.HORIZONTAL {
|
||||
buf.get_mut(rect.x + rect.width, y)
|
||||
.set_symbol(&rect_boundary_chars.CROSS);
|
||||
} else {
|
||||
buf.get_mut(rect.x + rect.width, y)
|
||||
.set_symbol(&rect_boundary_chars.VERTICAL);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// self.background(area, buf, self.style.bg);
|
||||
//
|
||||
// // Sides
|
||||
// if self.borders.intersects(Borders::LEFT) {
|
||||
// for y in area.top()..area.bottom() {
|
||||
// buf.get_mut(area.left(), y)
|
||||
// .set_symbol(line::VERTICAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
// if self.borders.intersects(Borders::TOP) {
|
||||
// for x in area.left()..area.right() {
|
||||
// buf.get_mut(x, area.top())
|
||||
// .set_symbol(line::HORIZONTAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
// if self.borders.intersects(Borders::RIGHT) {
|
||||
// let x = area.right() - 1;
|
||||
// for y in area.top()..area.bottom() {
|
||||
// buf.get_mut(x, y)
|
||||
// .set_symbol(line::VERTICAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
// if self.borders.intersects(Borders::BOTTOM) {
|
||||
// let y = area.bottom() - 1;
|
||||
// for x in area.left()..area.right() {
|
||||
// buf.get_mut(x, y)
|
||||
// .set_symbol(line::HORIZONTAL)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Corners
|
||||
// if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
// buf.get_mut(area.left(), area.top())
|
||||
// .set_symbol(line::TOP_LEFT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
// buf.get_mut(area.right() - 1, area.top())
|
||||
// .set_symbol(line::TOP_RIGHT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
// buf.get_mut(area.left(), area.bottom() - 1)
|
||||
// .set_symbol(line::BOTTOM_LEFT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
// if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
// buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
// .set_symbol(line::BOTTOM_RIGHT)
|
||||
// .set_style(self.border_style);
|
||||
// }
|
||||
//
|
||||
// if area.width > 2 {
|
||||
// if let Some(title) = self.title {
|
||||
// let lx = if self.borders.intersects(Borders::LEFT) {
|
||||
// 1
|
||||
// } else {
|
||||
// 0
|
||||
// };
|
||||
// let rx = if self.borders.intersects(Borders::RIGHT) {
|
||||
// 1
|
||||
// } else {
|
||||
// 0
|
||||
// };
|
||||
// let width = area.width - lx - rx;
|
||||
// buf.set_stringn(
|
||||
// area.left() + lx,
|
||||
// area.top(),
|
||||
// title,
|
||||
// width as usize,
|
||||
// self.title_style,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
2
src/tests/cases/mod.rs
Normal file
2
src/tests/cases/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod test_utils;
|
||||
pub mod ui;
|
55
src/tests/cases/snapshots/ui__eleven_files.snap
Normal file
55
src/tests/cases/snapshots/ui__eleven_files.snap
Normal file
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
source: src/tests/cases/ui.rs
|
||||
expression: "&terminal_draw_events_mirror[0]"
|
||||
---
|
||||
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────┬─────┬─────┬─────────┐
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ file7 151.6K (33%) │ file10 53.2K (12%) │ . │ . │ . │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
├──────────────────────────────────────────┬──────────────────────────────────────────┬─────────────────────────────────────┴────┬─────────────────────────────────────└────┬└────┬└─────────┘
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ file11 53.2K (12%) │ file6 53.2K (12%) │ file8 53.2K (12%) │ file9 53.2K (12%) │ . │ f[..]) │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└──────────────────────────────────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┴─────┴──────────┘
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
source: src/tests/cases/ui.rs
|
||||
expression: "&terminal_draw_events_mirror[0]"
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────┐
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ file2 8.2K (40%) │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ file1 4.1K (20%) │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ file3 8.2K (40%) │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────────────────────────┘
|
||||
|
32
src/tests/cases/test_utils.rs
Normal file
32
src/tests/cases/test_utils.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::tests::fakes::{
|
||||
KeyboardEvents,
|
||||
TerminalEvent, TestBackend,
|
||||
};
|
||||
use std::iter;
|
||||
|
||||
use ::termion::event::{Event, Key};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub fn sleep_and_quit_events(sleep_num: usize) -> Box<KeyboardEvents> {
|
||||
let mut events: Vec<Option<Event>> = iter::repeat(None).take(sleep_num).collect();
|
||||
events.push(Some(Event::Key(Key::Ctrl('c'))));
|
||||
Box::new(KeyboardEvents::new(events))
|
||||
}
|
||||
|
||||
type BackendWithStreams = (
|
||||
Arc<Mutex<Vec<TerminalEvent>>>,
|
||||
Arc<Mutex<Vec<String>>>,
|
||||
TestBackend,
|
||||
);
|
||||
pub fn test_backend_factory(w: u16, h: u16) -> BackendWithStreams {
|
||||
let terminal_events: Arc<Mutex<Vec<TerminalEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let terminal_draw_events: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let backend = TestBackend::new(
|
||||
terminal_events.clone(),
|
||||
terminal_draw_events.clone(),
|
||||
Arc::new(Mutex::new(w)),
|
||||
Arc::new(Mutex::new(h)),
|
||||
);
|
||||
(terminal_events, terminal_draw_events, backend)
|
||||
}
|
130
src/tests/cases/ui.rs
Normal file
130
src/tests/cases/ui.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use crate::tests::fakes::TerminalEvent::*;
|
||||
use ::insta::assert_snapshot;
|
||||
|
||||
use crate::tests::cases::test_utils::{test_backend_factory, sleep_and_quit_events};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::start;
|
||||
|
||||
use std::env;
|
||||
use std::fs::{File, create_dir};
|
||||
use std::io::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn create_temp_dir () -> Result<PathBuf, failure::Error> {
|
||||
let mut dir = env::temp_dir();
|
||||
let temp_dir_name = Uuid::new_v4();
|
||||
dir.push(temp_dir_name.to_string());
|
||||
|
||||
create_dir(&dir)?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn create_temp_file (path: PathBuf, size: usize) -> Result<(), failure::Error> {
|
||||
let mut file = File::create(path)?;
|
||||
let mut pos = 0;
|
||||
while pos < size {
|
||||
let bytes_written = file.write(b"W")?;
|
||||
pos += bytes_written;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_large_files_one_small_file () {
|
||||
|
||||
let (terminal_events, terminal_draw_events, backend) = test_backend_factory(190, 50);
|
||||
let keyboard_events = sleep_and_quit_events(2);
|
||||
let temp_dir_path = create_temp_dir().expect("failed to create temp dir");
|
||||
|
||||
let mut file_1_path = PathBuf::from(&temp_dir_path);
|
||||
file_1_path.push("file1");
|
||||
create_temp_file(file_1_path, 4000).expect("failed to create temp file");
|
||||
|
||||
let mut file_2_path = PathBuf::from(&temp_dir_path);
|
||||
file_2_path.push("file2");
|
||||
create_temp_file(file_2_path, 5000).expect("failed to create temp file");
|
||||
|
||||
let mut file_3_path = PathBuf::from(&temp_dir_path);
|
||||
file_3_path.push("file3");
|
||||
create_temp_file(file_3_path, 5000).expect("failed to create temp file");
|
||||
|
||||
start(backend, keyboard_events, temp_dir_path.clone());
|
||||
std::fs::remove_dir_all(temp_dir_path).expect("failed to remove temporary folder");
|
||||
let terminal_draw_events_mirror = terminal_draw_events.lock().unwrap();
|
||||
|
||||
let expected_terminal_events = vec![Clear, HideCursor, Draw, Flush, Draw, Flush, Clear, ShowCursor];
|
||||
|
||||
assert_eq!(
|
||||
&terminal_events.lock().unwrap()[..],
|
||||
&expected_terminal_events[..]
|
||||
);
|
||||
|
||||
assert_eq!(terminal_draw_events_mirror.len(), 2);
|
||||
assert_snapshot!(&terminal_draw_events_mirror[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eleven_files () {
|
||||
|
||||
let (terminal_events, terminal_draw_events, backend) = test_backend_factory(190, 50);
|
||||
let keyboard_events = sleep_and_quit_events(2);
|
||||
let temp_dir_path = create_temp_dir().expect("failed to create temp dir");
|
||||
|
||||
let mut file_1_path = PathBuf::from(&temp_dir_path);
|
||||
file_1_path.push("file1");
|
||||
create_temp_file(file_1_path, 5000).expect("failed to create temp file");
|
||||
|
||||
let mut file_2_path = PathBuf::from(&temp_dir_path);
|
||||
file_2_path.push("file2");
|
||||
create_temp_file(file_2_path, 5000).expect("failed to create temp file");
|
||||
|
||||
let mut file_3_path = PathBuf::from(&temp_dir_path);
|
||||
file_3_path.push("file3");
|
||||
create_temp_file(file_3_path, 5000).expect("failed to create temp file");
|
||||
|
||||
let mut file_4_path = PathBuf::from(&temp_dir_path);
|
||||
file_4_path.push("file4");
|
||||
create_temp_file(file_4_path, 5000).expect("failed to create temp file");
|
||||
|
||||
let mut file_5_path = PathBuf::from(&temp_dir_path);
|
||||
file_5_path.push("file5");
|
||||
create_temp_file(file_5_path, 5000).expect("failed to create temp file");
|
||||
|
||||
let mut file_6_path = PathBuf::from(&temp_dir_path);
|
||||
file_6_path.push("file6");
|
||||
create_temp_file(file_6_path, 50000).expect("failed to create temp file");
|
||||
|
||||
let mut file_7_path = PathBuf::from(&temp_dir_path);
|
||||
file_7_path.push("file7");
|
||||
create_temp_file(file_7_path, 150000).expect("failed to create temp file");
|
||||
|
||||
let mut file_8_path = PathBuf::from(&temp_dir_path);
|
||||
file_8_path.push("file8");
|
||||
create_temp_file(file_8_path, 50000).expect("failed to create temp file");
|
||||
|
||||
let mut file_9_path = PathBuf::from(&temp_dir_path);
|
||||
file_9_path.push("file9");
|
||||
create_temp_file(file_9_path, 50000).expect("failed to create temp file");
|
||||
|
||||
let mut file_10_path = PathBuf::from(&temp_dir_path);
|
||||
file_10_path.push("file10");
|
||||
create_temp_file(file_10_path, 50000).expect("failed to create temp file");
|
||||
|
||||
let mut file_11_path = PathBuf::from(&temp_dir_path);
|
||||
file_11_path.push("file11");
|
||||
create_temp_file(file_11_path, 50000).expect("failed to create temp file");
|
||||
|
||||
start(backend, keyboard_events, temp_dir_path.clone());
|
||||
std::fs::remove_dir_all(temp_dir_path).expect("failed to remove temporary folder");
|
||||
let terminal_draw_events_mirror = terminal_draw_events.lock().unwrap();
|
||||
|
||||
let expected_terminal_events = vec![Clear, HideCursor, Draw, Flush, Draw, Flush, Clear, ShowCursor];
|
||||
assert_eq!(
|
||||
&terminal_events.lock().unwrap()[..],
|
||||
&expected_terminal_events[..]
|
||||
);
|
||||
|
||||
assert_eq!(terminal_draw_events_mirror.len(), 2);
|
||||
assert_snapshot!(&terminal_draw_events_mirror[1]);
|
||||
}
|
28
src/tests/fakes/fake_input.rs
Normal file
28
src/tests/fakes/fake_input.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use ::std::{thread, time};
|
||||
use ::termion::event::Event;
|
||||
|
||||
pub struct KeyboardEvents {
|
||||
pub events: Vec<Option<Event>>,
|
||||
}
|
||||
|
||||
impl KeyboardEvents {
|
||||
pub fn new(mut events: Vec<Option<Event>>) -> Self {
|
||||
events.reverse(); // this is so that we do not have to shift the array
|
||||
KeyboardEvents { events }
|
||||
}
|
||||
}
|
||||
impl Iterator for KeyboardEvents {
|
||||
type Item = Event;
|
||||
fn next(&mut self) -> Option<Event> {
|
||||
match self.events.pop() {
|
||||
Some(ev) => match ev {
|
||||
Some(ev) => Some(ev),
|
||||
None => {
|
||||
thread::sleep(time::Duration::from_millis(900));
|
||||
self.next()
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
115
src/tests/fakes/fake_output.rs
Normal file
115
src/tests/fakes/fake_output.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use ::std::collections::HashMap;
|
||||
use ::std::io;
|
||||
use ::std::sync::{Arc, Mutex};
|
||||
use ::tui::backend::Backend;
|
||||
use ::tui::buffer::Cell;
|
||||
use ::tui::layout::Rect;
|
||||
|
||||
#[derive(Hash, Debug, PartialEq)]
|
||||
pub enum TerminalEvent {
|
||||
Clear,
|
||||
HideCursor,
|
||||
ShowCursor,
|
||||
GetCursor,
|
||||
Flush,
|
||||
Draw,
|
||||
}
|
||||
|
||||
pub struct TestBackend {
|
||||
pub events: Arc<Mutex<Vec<TerminalEvent>>>,
|
||||
pub draw_events: Arc<Mutex<Vec<String>>>,
|
||||
terminal_width: Arc<Mutex<u16>>,
|
||||
terminal_height: Arc<Mutex<u16>>,
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
pub fn new(
|
||||
log: Arc<Mutex<Vec<TerminalEvent>>>,
|
||||
draw_log: Arc<Mutex<Vec<String>>>,
|
||||
terminal_width: Arc<Mutex<u16>>,
|
||||
terminal_height: Arc<Mutex<u16>>,
|
||||
) -> TestBackend {
|
||||
TestBackend {
|
||||
events: log,
|
||||
draw_events: draw_log,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq)]
|
||||
struct Point {
|
||||
x: u16,
|
||||
y: u16,
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.events.lock().unwrap().push(TerminalEvent::Clear);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.events.lock().unwrap().push(TerminalEvent::HideCursor);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.events.lock().unwrap().push(TerminalEvent::ShowCursor);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.events.lock().unwrap().push(TerminalEvent::GetCursor);
|
||||
Ok((0, 0))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, _x: u16, _y: u16) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
// use std::fmt::Write;
|
||||
self.events.lock().unwrap().push(TerminalEvent::Draw);
|
||||
let mut string = String::with_capacity(content.size_hint().0 * 3);
|
||||
let mut coordinates = HashMap::new();
|
||||
for (x, y, cell) in content {
|
||||
coordinates.insert(Point { x, y }, cell);
|
||||
}
|
||||
let terminal_height = self.terminal_height.lock().unwrap();
|
||||
let terminal_width = self.terminal_width.lock().unwrap();
|
||||
for y in 0..*terminal_height {
|
||||
for x in 0..*terminal_width {
|
||||
match coordinates.get(&Point { x, y }) {
|
||||
Some(cell) => {
|
||||
// this will contain no style information at all
|
||||
// should be good enough for testing
|
||||
string.push_str(&cell.symbol);
|
||||
}
|
||||
None => {
|
||||
string.push_str(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
string.push_str("\n");
|
||||
}
|
||||
self.draw_events.lock().unwrap().push(string);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal_height = self.terminal_height.lock().unwrap();
|
||||
let terminal_width = self.terminal_width.lock().unwrap();
|
||||
|
||||
Ok(Rect::new(0, 0, *terminal_width, *terminal_height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.events.lock().unwrap().push(TerminalEvent::Flush);
|
||||
Ok(())
|
||||
}
|
||||
}
|
5
src/tests/fakes/mod.rs
Normal file
5
src/tests/fakes/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod fake_input;
|
||||
mod fake_output;
|
||||
|
||||
pub use fake_input::*;
|
||||
pub use fake_output::*;
|
2
src/tests/mod.rs
Normal file
2
src/tests/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod cases;
|
||||
pub mod fakes;
|
83
src/util/event.rs
Normal file
83
src/util/event.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
|
||||
pub enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
/// A small event handler that wrap termion input and tick events. Each event
|
||||
/// type is handled in its own thread and returned to a common `Receiver`
|
||||
pub struct Events {
|
||||
rx: mpsc::Receiver<Event<Key>>,
|
||||
input_handle: thread::JoinHandle<()>,
|
||||
tick_handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Config {
|
||||
pub exit_key: Key,
|
||||
pub tick_rate: Duration,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
exit_key: Key::Char('q'),
|
||||
tick_rate: Duration::from_millis(250),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Events {
|
||||
pub fn new() -> Events {
|
||||
Events::with_config(Config::default())
|
||||
}
|
||||
|
||||
pub fn with_config(config: Config) -> Events {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_handle = {
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for evt in stdin.keys() {
|
||||
match evt {
|
||||
Ok(key) => {
|
||||
if let Err(_) = tx.send(Event::Input(key)) {
|
||||
return;
|
||||
}
|
||||
if key == config.exit_key {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let tick_handle = {
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let tx = tx.clone();
|
||||
loop {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(config.tick_rate);
|
||||
}
|
||||
})
|
||||
};
|
||||
Events {
|
||||
rx,
|
||||
input_handle,
|
||||
tick_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
|
||||
self.rx.recv()
|
||||
}
|
||||
}
|
77
src/util/mod.rs
Normal file
77
src/util/mod.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
#[cfg(feature = "termion")]
|
||||
pub mod event;
|
||||
|
||||
// use rand::distributions::{Distribution, Uniform};
|
||||
// use rand::rngs::ThreadRng;
|
||||
//
|
||||
// #[derive(Clone)]
|
||||
// pub struct RandomSignal {
|
||||
// distribution: Uniform<u64>,
|
||||
// rng: ThreadRng,
|
||||
// }
|
||||
//
|
||||
// impl RandomSignal {
|
||||
// pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
// RandomSignal {
|
||||
// distribution: Uniform::new(lower, upper),
|
||||
// rng: rand::thread_rng(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl Iterator for RandomSignal {
|
||||
// type Item = u64;
|
||||
// fn next(&mut self) -> Option<u64> {
|
||||
// Some(self.distribution.sample(&mut self.rng))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[derive(Clone)]
|
||||
// pub struct SinSignal {
|
||||
// x: f64,
|
||||
// interval: f64,
|
||||
// period: f64,
|
||||
// scale: f64,
|
||||
// }
|
||||
//
|
||||
// impl SinSignal {
|
||||
// pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
// SinSignal {
|
||||
// x: 0.0,
|
||||
// interval,
|
||||
// period,
|
||||
// scale,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl Iterator for SinSignal {
|
||||
// type Item = (f64, f64);
|
||||
// fn next(&mut self) -> Option<Self::Item> {
|
||||
// let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
// self.x += self.interval;
|
||||
// Some(point)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// pub struct TabsState<'a> {
|
||||
// pub titles: Vec<&'a str>,
|
||||
// pub index: usize,
|
||||
// }
|
||||
//
|
||||
// impl<'a> TabsState<'a> {
|
||||
// pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
// TabsState { titles, index: 0 }
|
||||
// }
|
||||
// pub fn next(&mut self) {
|
||||
// self.index = (self.index + 1) % self.titles.len();
|
||||
// }
|
||||
//
|
||||
// pub fn previous(&mut self) {
|
||||
// if self.index > 0 {
|
||||
// self.index -= 1;
|
||||
// } else {
|
||||
// self.index = self.titles.len() - 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
Loading…
Reference in a new issue