initial commit

This commit is contained in:
Aram Drevekenin 2020-02-22 21:23:28 +01:00
commit 4a0e7b5e71
23 changed files with 3538 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.cargo/
target/
vendor/
vendor.tar
**/*.rs.bk

1637
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

33
Cargo.toml Normal file
View 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
View 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.

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# visual-size-ls
wip

5
src/display/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod rectangle_grid;
pub mod state;
pub use state::*;
pub use rectangle_grid::*;

View 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
View 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
View file

@ -0,0 +1,3 @@
mod scan_folder;
pub use scan_folder::*;

View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
pub mod test_utils;
pub mod ui;

View 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[..]) │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└──────────────────────────────────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┴─────┴──────────┘

View file

@ -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%) │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────────────────────────┘

View 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
View 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]);
}

View 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,
}
}
}

View 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
View 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
View file

@ -0,0 +1,2 @@
pub mod cases;
pub mod fakes;

83
src/util/event.rs Normal file
View 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
View 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;
// }
// }
// }