feat(ui): add an "Are you sure you want to quit?" modal (#44)

* feat(app): show confirm modal before exit

add keyEvent handler function for exiting mode

add Exiting variant to UiMode enum | add prompt_exit method

implement ExitingMode widget rendering

fix mixed UiState issue

* resolved requested changes

* feat(app) add confirm modal before exit

fix panic in modals when terminal-width=50

listen for all possible keys when user tries to exit

exit without confirmation in ScreenTooSmall mode

fix tests to be compatible with ConfirmModal Changes

* docs(readme): add gentoo installation info (#47)

* feat(ux): make enter select largest folder if nothing is selected (#45)

* Make enter select largest folder if nothing is selected

* Rename method

* Renamed and changed method to do what it originally said

* Efficiency improvements

* Added test for the feature

* Run cargo insta review

* Fixed len for assert_eq!

* Fixed asserts at end of test

* Run cargo insta review again

* docs(changelog): enter largest folder

* docs(readme): fix error in how it works

* feat(ui): show quit shortcut ('q') in the legend (#46)

* Add <q> shortcut in the legend

* Fix typo for description

* Use <arrows> instead of <hjkl> or <arrow keys>

* Apply fmt

* Merge main

* feat(navigation): keep a stack of visited items and make go_up use it (#53)

* Keep a stack up visited items and make go_up use it

This will allow us to keep track of where our previous selections were
so can can automatically select the parents when we go up in the
hierarchy.

Closes #48.

Signed-off-by: Daniel Egger <daniel@eggers-club.de>

* Redo snapshot tests to fix failures

Signed-off-by: Daniel Egger <daniel@eggers-club.de>

* style(naming): minor naming change for clarity

Co-authored-by: Aram Drevekenin <aram@poor.dev>

* fix(formatting): prevent crashes on files with multibyte characters (#51)

* Fix crash when truncating to middle of a character

* Fix alignment of file names with wide characters

* Respect use ::formatting convention

* docs(changelog): update recent changes

* chore(release): 0.4.0

* fix enter_largest_folder_with_no_selected_tile test and its snapshot

Co-authored-by: Aram Drevekenin <aram@poor.dev>
Co-authored-by: telans <telans@protonmail.com>
Co-authored-by: redzic <48274562+redzic@users.noreply.github.com>
Co-authored-by: Oleh <45392385+olehs0@users.noreply.github.com>
Co-authored-by: Daniel Egger <daniel@eggers-club.de>
Co-authored-by: Renée Kooi <renee@kooi.me>
This commit is contained in:
Mehdi Mohseni
2020-06-27 15:18:54 +04:30
committed by GitHub
parent 41c6971f0e
commit 2a3e2b635c
42 changed files with 1615 additions and 458 deletions

View File

@@ -18,6 +18,7 @@ pub enum UiMode {
ScreenTooSmall,
DeleteFile(FileToDelete),
ErrorMessage(String),
Exiting { app_loaded: bool },
}
pub struct App<B>
@@ -119,6 +120,12 @@ where
}
};
}
pub fn prompt_exit(&mut self) {
self.ui_mode = UiMode::Exiting {
app_loaded: self.loaded,
};
self.render();
}
pub fn exit(&mut self) {
self.is_running = false;
// here we do a blocking send rather than a try_send

View File

@@ -35,7 +35,7 @@ macro_rules! key {
pub fn handle_keypress_loading_mode<B: Backend>(evt: Event, app: &mut App<B>) {
match evt {
key!(ctrl 'c') | key!(char 'q') => {
app.exit();
app.prompt_exit();
}
key!(char 'l') | key!(Right) | key!(ctrl 'f') => {
app.move_selected_right();
@@ -62,7 +62,7 @@ pub fn handle_keypress_loading_mode<B: Backend>(evt: Event, app: &mut App<B>) {
pub fn handle_keypress_normal_mode<B: Backend>(evt: Event, app: &mut App<B>) {
match evt {
key!(ctrl 'c') | key!(char 'q') => {
app.exit();
app.prompt_exit();
}
key!(ctrl 'd') => {
app.prompt_file_deletion();
@@ -122,3 +122,18 @@ pub fn handle_keypress_screen_too_small<B: Backend>(evt: Event, app: &mut App<B>
_ => (),
};
}
pub fn handle_keypress_exiting_mode<B: Backend>(evt: Event, app: &mut App<B>) {
match evt {
key!(ctrl 'c') | key!(char 'q') | key!(Esc) | key!(Backspace) | key!(char 'n') => {
app.reset_ui_mode();
// we have to manually call render here to make sure ui gets updated
// because reset_ui_mode does not call it itself
app.render();
}
key!(char 'y') => {
app.exit();
}
_ => (),
};
}

View File

@@ -116,8 +116,9 @@ pub fn start<B>(
let running = running.clone();
move || {
for evt in keyboard_events {
if let TermionEvent::Key(Key::Ctrl('c'))
| TermionEvent::Key(Key::Char('q')) = evt
if let TermionEvent::Key(Key::Char('y'))
| TermionEvent::Key(Key::Char('q'))
| TermionEvent::Key(Key::Ctrl('c')) = evt
{
// not ideal, but works in a pinch
let _ = instruction_sender.send(Instruction::Keypress(evt));

View File

@@ -5,8 +5,8 @@ use ::termion::event::Event as TermionEvent;
use ::tui::backend::Backend;
use crate::input::{
handle_keypress_delete_file_mode, handle_keypress_error_message, handle_keypress_loading_mode,
handle_keypress_normal_mode, handle_keypress_screen_too_small,
handle_keypress_delete_file_mode, handle_keypress_error_message, handle_keypress_exiting_mode,
handle_keypress_loading_mode, handle_keypress_normal_mode, handle_keypress_screen_too_small,
};
use crate::{App, UiMode};
@@ -82,6 +82,9 @@ where
UiMode::ErrorMessage(_) => {
handle_keypress_error_message(evt, app);
}
UiMode::Exiting { app_loaded: _ } => {
handle_keypress_exiting_mode(evt, app);
}
}
if !app.is_running {
break;

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[4]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[4]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[7]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[4]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[8]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[9]"
---
┌─────────────────────────────┐
│ │
│ │
│ Really quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[8]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[3]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[3]"
┌────────────────────────────────────────────┐
│ Are you sure you want to quit?
(y/n)
└────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[3]"
┌─────────────────────────────┐
│ Really quit?
(y/n)
└─────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[2]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -3,53 +3,53 @@ source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[5]"
---
file2
4.0K (33%)
subfolder1/ (+1 descendants)
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
4.0K (33%)
file3
4.0K (33%)
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────┐
│ │
│ │
│ Really quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[4]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[5]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[4]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[5]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[3]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[8]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[8]"
Permission denied (os error 13)
(Press <ESC> to dismiss)
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -22,17 +22,17 @@ expression: "&terminal_draw_events_mirror[1]"
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Are you sure you want to quit?
(y/n)
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌────────────────────────┐
│ │
│ │
│ Really quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌────────────────────────┐
│ │
│ │
│ Really quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌────────────────────────┐
│ │
│ │
│ Really quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└────────────────────────┘

View File

@@ -0,0 +1,55 @@
---
source: src/tests/cases/ui.rs
expression: "&terminal_draw_events_mirror[1]"
---
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ Are you sure you want to quit? │
│ │
│ │
│ │
│ │
│ (y/n) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -4,9 +4,13 @@ use ::termion::event::{Event, Key};
use crate::tests::fakes::{KeyboardEvents, TerminalEvent, TestBackend};
pub fn sleep_and_quit_events(sleep_num: usize) -> Box<KeyboardEvents> {
pub fn sleep_and_quit_events(sleep_num: usize, quit_after_confirm: bool) -> Box<KeyboardEvents> {
let mut events: Vec<Option<Event>> = iter::repeat(None).take(sleep_num).collect();
events.push(Some(Event::Key(Key::Ctrl('c'))));
if quit_after_confirm {
events.push(None);
events.push(Some(Event::Key(Key::Char('y'))));
}
Box::new(KeyboardEvents::new(events))
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ use crate::state::files::FileTree;
use crate::state::tiles::Board;
use crate::state::UiEffects;
use crate::ui::grid::RectangleGrid;
use crate::ui::modals::{ErrorBox, MessageBox};
use crate::ui::modals::{ConfirmBox, ErrorBox, MessageBox};
use crate::ui::title::TitleLine;
use crate::ui::{BottomLine, TermTooSmall};
use crate::UiMode;
@@ -195,6 +195,57 @@ where
);
f.render_widget(ErrorBox::new(message), full_screen);
}
UiMode::Exiting { app_loaded } => {
if *app_loaded {
// render normal ui mode
f.render_widget(
TitleLine::new(
base_path_info,
current_path_info,
file_tree.space_freed,
)
.path_error(ui_effects.current_path_is_red)
.flash_space(ui_effects.flash_space_freed)
.read_errors(file_tree.failed_to_read),
chunks[0],
);
f.render_widget(
BottomLine::new().currently_selected(board.currently_selected()),
chunks[2],
);
} else {
// render loading ui mode
f.render_widget(
TitleLine::new(
base_path_info,
current_path_info,
file_tree.space_freed,
)
.progress_indicator(ui_effects.loading_progress_indicator)
.path_error(ui_effects.current_path_is_red)
.read_errors(file_tree.failed_to_read)
.show_loading(),
chunks[0],
);
f.render_widget(
BottomLine::new()
.currently_selected(board.currently_selected())
.last_read_path(ui_effects.last_read_path.as_ref())
.hide_delete(),
chunks[2],
);
}
// render common widgets
f.render_widget(
RectangleGrid::new(
&board.tiles,
board.unrenderable_tile_coordinates,
board.selected_index,
),
chunks[1],
);
f.render_widget(ConfirmBox::new(), full_screen);
}
};
})
.expect("failed to draw");

View File

@@ -0,0 +1,92 @@
use ::tui::buffer::Buffer;
use ::tui::layout::Rect;
use ::tui::style::{Color, Modifier, Style};
use ::tui::widgets::Widget;
use crate::ui::format::truncate_middle;
use crate::ui::grid::draw_filled_rect;
fn render_confirm_prompt(buf: &mut Buffer, confirm_rect: &Rect) {
let text_style = Style::default()
.bg(Color::Black)
.fg(Color::White)
.modifier(Modifier::BOLD);
let possible_confirm_texts = [
"Are you sure you want to quit?",
"Sure you want to quit?",
"Really quit?",
"Quit?",
];
// set default value of the confirm_text
// to the longest one from possible_confirm_text array
let mut confirm_text = String::from(possible_confirm_texts[0]);
let mut confirm_text_start_position: u16 = 0;
let text_max_length = confirm_rect.width - 4;
for line in possible_confirm_texts.iter() {
// "+10" here is to make sure confirm message has always some padding
if confirm_rect.width >= (line.chars().count() as u16) + 10 {
confirm_text = truncate_middle(line, text_max_length);
confirm_text_start_position =
((confirm_rect.width - confirm_text.len() as u16) as f64 / 2.0).ceil() as u16
+ confirm_rect.x;
break;
}
}
let y_n_line = "(y/n)";
let y_n_line_start_position =
((confirm_rect.width - y_n_line.len() as u16) as f64 / 2.0).ceil() as u16 + confirm_rect.x;
buf.set_string(
confirm_text_start_position,
confirm_rect.y + confirm_rect.height / 2 - 2,
confirm_text,
text_style,
);
buf.set_string(
y_n_line_start_position,
confirm_rect.y + confirm_rect.height / 2 + 3,
y_n_line,
text_style,
);
}
pub struct ConfirmBox {}
impl ConfirmBox {
pub fn new() -> Self {
Self {}
}
}
impl<'a> Widget for ConfirmBox {
fn render(self, area: Rect, buf: &mut Buffer) {
let (width, height) = if area.width > 150 {
(150, 10)
} else if area.width >= 50 {
(area.width / 2, 10)
} else {
unreachable!("app should not be rendered if window is so small")
};
// position self in the middle of the self
let x = ((area.x + area.width) / 2) - width / 2;
let y = ((area.y + area.height) / 2) - height / 2;
let confirm_rect = Rect {
x,
y,
width,
height,
};
let fill_style = Style::default()
.bg(Color::Black)
.fg(Color::White)
.modifier(Modifier::BOLD);
draw_filled_rect(buf, fill_style, &confirm_rect);
render_confirm_prompt(buf, &confirm_rect);
}
}

View File

@@ -20,7 +20,7 @@ impl<'a> Widget for ErrorBox<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let (width, height) = if area.width > 150 {
(150, 10)
} else if area.width > 50 {
} else if area.width >= 50 {
(area.width / 2, 10)
} else {
unreachable!("app should not be rendered if window is so small")

View File

@@ -137,7 +137,7 @@ impl<'a> Widget for MessageBox<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let (width, height) = if area.width > 150 {
(150, 10)
} else if area.width > 50 {
} else if area.width >= 50 {
(area.width / 2, 10)
} else {
unreachable!("app should not be rendered if window is so small")

View File

@@ -1,5 +1,7 @@
mod confirm_box;
mod error_box;
mod message_box;
pub use confirm_box::*;
pub use error_box::*;
pub use message_box::*;