Skip to content

Commit

Permalink
[V1] +comments +unit testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Ilingu committed Jul 6, 2023
1 parent 7fc5f54 commit 45096bf
Show file tree
Hide file tree
Showing 16 changed files with 197 additions and 16 deletions.
7 changes: 0 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ categories = ["command-line-interface", "filesystem"]
anyhow = "1.0.71"
chrono = "0.4.26"
crossterm = "0.26.1"
dotenv = "0.15.0"
# dotenv = "0.15.0"
fs_extra = "1.3.0"
lazy_static = "1.4.0"
number_prefix = "0.4.0"
Expand Down
51 changes: 50 additions & 1 deletion src/app/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ enum TraverseMsg {
}

impl SharableState<AppState> {
/// add item to target dirs
pub fn push_to_list(&self, target: TargetDir) {
self.mutate(|data| data.target_directories.datas.push(target));
}

/// clear all items from target dirs
pub fn clear_list(&self) {
self.mutate(|data| data.target_directories.datas.clear());
}
Expand All @@ -35,14 +37,17 @@ impl SharableState<AppState> {
self.mutate(|data| data.searching = searching);
}

/// select previous item in the list of target dirs
pub fn prev_item(&self) {
self.mutate(|data| data.target_directories.previous())
}

/// select next item in the list of target dirs
pub fn next_item(&self) {
self.mutate(|data| data.target_directories.next())
}

/// Will get the currently selected item, and call the `.delete()` method on it, deleting permanently the underlying folder
pub fn delete_current_item(&self) {
let (sender, receiver) = mpsc::channel::<bool>();
self.mutate(|data| {
Expand Down Expand Up @@ -75,6 +80,7 @@ impl SharableState<AppState> {
};
}

/// set a new ui message, as the name imply it overwrites the previous message (if there is some)
pub fn set_message(&self, message: Option<Message>) {
self.mutate(|data| data.message = message)
}
Expand All @@ -83,6 +89,7 @@ impl SharableState<AppState> {
self.mutate(|data| data.total_size = val)
}

/// will scan the specified directory to find 'target' dirs inside of it, and automatically stream the data in the app state
pub fn search(&self) {
self.set_searching(true);
let (tx, rx) = mpsc::channel::<TraverseMsg>();
Expand Down Expand Up @@ -125,8 +132,8 @@ impl SharableState<AppState> {
}
}

/// recursively search for 'target' dirs, when one is found it returns and parse then stream the data through a channel
fn find_target_dirs(path: String, tx: Sender<TraverseMsg>) {
// thread::sleep(Duration::from_millis(1));
if let Ok(entries) = fs::read_dir(&path) {
let entries = entries
.into_iter()
Expand Down Expand Up @@ -191,3 +198,45 @@ fn find_target_dirs(path: String, tx: Sender<TraverseMsg>) {
}
}
}

#[cfg(test)]
mod app_tests {
use std::{fs, process::Command, sync::mpsc, thread};

use super::{find_target_dirs, TraverseMsg};

#[test]
#[ignore]
fn test_find_target_dirs() {
let (tx, rx) = mpsc::channel::<TraverseMsg>();

assert!(Command::new("cargo")
.args(["new", "test_app"])
.current_dir("/home/ilingu/.cache/rtkill")
.output()
.is_ok());
assert!(Command::new("cargo")
.arg("build")
.current_dir("/home/ilingu/.cache/rtkill/test_app")
.output()
.is_ok());

thread::spawn(move || {
find_target_dirs("/home/ilingu/.cache/rtkill".to_string(), tx.clone());
let _ = tx.send(TraverseMsg::Exit);
});

let mut found = vec![];
for data in rx {
match data {
TraverseMsg::Data((target, _)) => found.push(target),
TraverseMsg::Exit => break,
}
}

assert_eq!(found.len(), 1);
assert_eq!(found[0].project_name, "test_app");

assert!(fs::remove_dir_all("/home/ilingu/.cache/rtkill/test_app").is_ok());
}
}
14 changes: 13 additions & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{
time::{Duration, Instant},
};

use anyhow::Result;
use anyhow::{anyhow, Result};
use crossterm::event::{self, Event, KeyCode};
use tui::{backend::Backend, Terminal};

Expand All @@ -23,23 +23,34 @@ use crate::{
utils::sharable_state::SharableState,
};

/// Desscribes a rust 'target' folder
#[derive(Default, Debug, Clone)]
pub struct TargetDir {
/// where it's located in the user disk
pub path: String,
/// rust associated project name in the Cargo.toml
pub project_name: String,
pub last_modified: String,
/// Is user deleted the target file
pub is_deleted: bool,
/// formatted size of the folder, e.g: "5 GiB", "28 KiB"...
pub size: String,
}

impl TargetDir {
/// deletes permanently the folder from the user disk
pub fn delete(&mut self) -> Result<()> {
if self.is_deleted {
return Err(anyhow!("folder already deleted"));
}

fs::remove_dir_all(self.path.clone())?;
self.is_deleted = true;
Ok(())
}
}

/// Application public variables, persist after frame rebuild
#[derive(Default)]
pub struct AppState {
pub root_dir: Option<String>,
Expand All @@ -49,6 +60,7 @@ pub struct AppState {
pub total_size: String,
}

/// launch app, and begin frame
pub fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
state: Arc<SharableState<AppState>>,
Expand Down
7 changes: 7 additions & 0 deletions src/app/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ use crate::ui::components::message::{Message, MessageAction, MessageType};
use super::AppState;

impl AppState {
/// parse and check the scope/root directory (specified or not by the user) and then initialize app state
///
/// check for user specified directory, if one is found and it's valid create app state
///
/// otherwise, get the current directory the app is spawned in
pub fn new() -> Result<Self> {
match Self::from_args() {
Ok(app) => Ok(app),
Expand Down Expand Up @@ -42,6 +47,7 @@ impl AppState {
}
}

/// create app state from the current directory
pub fn from_cd() -> Result<Self> {
let root_dir = env::current_dir()?
.to_str()
Expand All @@ -54,6 +60,7 @@ impl AppState {
})
}

/// create app state from the user specified directory
pub fn from_args() -> Result<Self> {
let args = env::args().skip(1).collect::<Vec<_>>();
if args.len() != 1 {
Expand Down
9 changes: 7 additions & 2 deletions src/ui/components/list_with_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ impl<T> ListWithState<T> {
}

impl Renderer<()> for ListWithState<TargetDir> {
/// takes a screen chunk and draw in it the target items components
fn render_and_draw_items<B: Backend>(&self, f: &mut Frame<B>, chunks: Vec<Rect>) {
if self.datas.is_empty() {
return;
}

// all items cannot be displayed on screen thus this wil choose which items to display based on where is the currently selected one, it works by 'room': it search in which interval of "n" items the currently selected one is and display this interval
let items_range = {
let (mut inf, mut sup) = (0, chunks.len());
while !(inf <= self.index + 1 && self.index < sup) {
Expand All @@ -52,6 +54,7 @@ impl Renderer<()> for ListWithState<TargetDir> {
inf..sup
};

// for each items, render it's component
let items = self.datas.iter().enumerate().collect::<Vec<_>>()[items_range].to_vec();
for (slot_id, area) in chunks.iter().enumerate() {
if slot_id >= items.len() {
Expand Down Expand Up @@ -79,6 +82,7 @@ impl Renderer<()> for ListWithState<TargetDir> {
])
.split(*area);

// "[DELETED]" if user has deleted this target folder otherwise the project name
f.render_widget(
match item_data.is_deleted {
true => Paragraph::new(Span::styled(
Expand All @@ -91,11 +95,12 @@ impl Renderer<()> for ListWithState<TargetDir> {
},
sub_chunks[0],
);
f.render_widget(Paragraph::new(item_data.path.clone()), sub_chunks[2]);
f.render_widget(Paragraph::new(item_data.path.clone()), sub_chunks[2]); // target path
f.render_widget(
Paragraph::new(item_data.last_modified.clone()),
Paragraph::new(item_data.last_modified.clone()), // last modified
sub_chunks[4],
);
// target size
f.render_widget(Paragraph::new(item_data.size.clone()), sub_chunks[6]);
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/ui/components/logo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const LOGO: [&str; 7] = [
\|_______|",
];

/// build the app logo with random colors and returns the ui component containing it
///
/// may be improved by storing/caching the "colored_text_chunks" in a const (lazy_static)
pub fn welcome_logo() -> Paragraph<'static> {
let colors_by_char_id = LOGO
.iter()
Expand All @@ -68,15 +71,15 @@ pub fn welcome_logo() -> Paragraph<'static> {
})
.collect::<Vec<_>>();

let mut colored_text: Vec<Vec<Span>> = vec![vec![Span::raw(""); 7]; 7];
let mut colored_text_chunks: Vec<Vec<Span>> = vec![vec![Span::raw(""); 7]; 7];
for (char_id, char) in LOGO.iter().enumerate() {
for (line_id, line) in char.lines().enumerate() {
colored_text[line_id][char_id] =
colored_text_chunks[line_id][char_id] =
Span::styled(line.to_string(), colors_by_char_id[char_id])
}
}

let colored_text = colored_text
let colored_text = colored_text_chunks
.into_iter()
.map(Spans::from)
.collect::<Vec<_>>();
Expand Down
3 changes: 3 additions & 0 deletions src/ui/components/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ pub enum MessageType {
Warning,
Error,
}

/// Action to take when the message is deleted
pub enum MessageAction {
Quit,
}

/// Describe an app message
pub struct Message {
msg_text: String,
msg_type: MessageType,
Expand Down
1 change: 1 addition & 0 deletions src/ui/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod logo;
pub mod message;
pub mod rainbow_text;

/// simple trait to origanize how component renders their ui
pub trait Renderer<T> {
fn render_items(&self) -> Option<T> {
None
Expand Down
9 changes: 9 additions & 0 deletions src/ui/components/rainbow_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ lazy_static! {
];
}

/// create a raibow text
///
/// it return a collection of Span (Spans), to display it use can use a widget like `Paragraph`
///
/// e.g:
///
/// ```
/// Paragraph::new(rainbow_text("rainbow!"));
/// ```
pub fn rainbow_text(text: &str) -> Spans<'static> {
let mut colored_text: Vec<Span> = vec![];
for (i, ch) in text.chars().enumerate() {
Expand Down
7 changes: 6 additions & 1 deletion src/ui/info_section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ use crate::app::AppState;

use super::components::{logo::welcome_logo, Renderer};

/// draw the ui for the inner top section of the app
pub fn draw_info_section<B: Backend>(f: &mut Frame<B>, area: Rect, state: &AppState) {
// divide the space in two chuncks of 80% (for the logo) and 20% (app infos)
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Percentage(80), Constraint::Percentage(20)])
.split(area);
f.render_widget(welcome_logo().alignment(Alignment::Center), chunks[0]);
f.render_widget(welcome_logo().alignment(Alignment::Center), chunks[0]); // render logo

// redivide the 20% for the app infos, in two equal sub-chunks for the message/app info and the controls
let sub_chunck = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);

// if message renders it in priority, otherwise render searching result if result or otherwise the searching state
if let Some(message) = &state.message {
f.render_widget(
message.render_items().unwrap().alignment(Alignment::Center),
Expand Down Expand Up @@ -59,6 +63,7 @@ pub fn draw_info_section<B: Backend>(f: &mut Frame<B>, area: Rect, state: &AppSt
);
}

// render controls
f.render_widget(
Paragraph::new(Spans::from(vec![
Span::styled(
Expand Down
1 change: 1 addition & 0 deletions src/ui/list_section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::app::AppState;

use super::components::Renderer;

/// draw ui for the list of target dirs
pub fn draw_list_section<B: Backend>(f: &mut Frame<B>, area: Rect, state: &AppState) {
let chunks = Layout::default()
.direction(Direction::Vertical)
Expand Down
1 change: 1 addition & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use tui::{
Frame,
};

/// root function to build and draw on the screen all the app ui
pub fn ui<B: Backend>(f: &mut Frame<B>, state: &AppState) {
let parent_chunk = Layout::default()
.direction(Direction::Vertical)
Expand Down
5 changes: 5 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ use anyhow::{anyhow, Result};
use number_prefix::NumberPrefix;
use rand::{thread_rng, Rng};
use tui::style::Color;
mod tests;

pub mod sharable_state;

/// generate a random **light** color
///
/// i.e: all the rgb value are between 128 and 255
pub fn generate_random_color() -> Color {
let mut rng = thread_rng();
Color::Rgb(
Expand Down Expand Up @@ -61,6 +65,7 @@ pub trait FromHex {
}

impl FromHex for Color {
/// personal helper function to convert hex value to rbg and then create a tui Color (which only accept rgb values)
fn from_hex(hex: &str) -> Result<Color> {
let safe_hex = hex.trim().trim_start_matches('#').trim_start_matches("0x");

Expand Down
Loading

0 comments on commit 45096bf

Please sign in to comment.