Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ impl AppTui {
}
}

let model = Model::default(context, terminal_dimensions);
let debug = std::env::var("BMM_DEBUG").unwrap_or_default().trim() == "1";
let model = Model::default(context, terminal_dimensions, debug);

Ok(Self {
terminal,
Expand Down
12 changes: 10 additions & 2 deletions src/tui/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::common::ActivePane;
use super::model::Model;
use crate::domain::{SavedBookmark, TagStats};
use crate::persistence::DBError;
use ratatui::crossterm::event::{Event, KeyCode, KeyEventKind};
use ratatui::crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use std::io::Error as IOError;

pub enum Message {
Expand All @@ -24,6 +24,7 @@ pub enum Message {
BookmarksForTagFetched(Result<Vec<SavedBookmark>, DBError>),
ContentCopiedToClipboard(Result<(), String>),
GoBackOrQuit,
QuitImmediately,
}

pub enum UrlsOpenedResult {
Expand All @@ -33,7 +34,14 @@ pub enum UrlsOpenedResult {

pub fn get_event_handling_msg(model: &Model, event: Event) -> Option<Message> {
match event {
Event::Key(key_event) => match model.terminal_too_small {
Event::Key(key_event)
if key_event.kind == KeyEventKind::Press
&& key_event.code == KeyCode::Char('c')
&& key_event.modifiers == KeyModifiers::CONTROL =>
{
Some(Message::QuitImmediately)
}
Event::Key(key_event) => match model.terminal_too_small() {
true => match key_event.kind {
KeyEventKind::Press => match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => Some(Message::GoBackOrQuit),
Expand Down
2 changes: 2 additions & 0 deletions src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod common;
mod handle;
mod message;
mod model;
#[cfg(test)]
mod tests;
mod update;
mod view;

Expand Down
37 changes: 18 additions & 19 deletions src/tui/model.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use super::{commands::Command, common::*};
use crate::{
domain::{SavedBookmark, TagStats},
persistence::SearchTerms,
};
use ratatui::{
style::Style,
text::Line,
widgets::{ListItem, ListState},
};
use super::commands::Command;
use super::common::*;
use crate::domain::{SavedBookmark, TagStats};
use crate::persistence::SearchTerms;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::widgets::{ListItem, ListState};
use tui_input::Input;

#[derive(Debug, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -168,14 +165,15 @@ pub(super) struct Model {
pub(super) search_input: Input,
pub(super) initial: bool,
pub(super) terminal_dimensions: TerminalDimensions,
pub(super) terminal_too_small: bool,
pub(super) debug: bool,
}

impl Model {
pub(crate) fn default(context: TuiContext, terminal_dimensions: TerminalDimensions) -> Self {
let debug = std::env::var("BMM_DEBUG").unwrap_or_default().trim() == "1";

pub(crate) fn default(
context: TuiContext,
terminal_dimensions: TerminalDimensions,
debug: bool,
) -> Self {
let active_pane = match context {
TuiContext::Search(_) => ActivePane::List,
TuiContext::Tags => ActivePane::TagsList,
Expand All @@ -184,9 +182,6 @@ impl Model {

let initial = matches!(context, TuiContext::Initial);

let terminal_too_small = terminal_dimensions.width < MIN_TERMINAL_WIDTH
|| terminal_dimensions.height < MIN_TERMINAL_HEIGHT;

Self {
active_pane,
running_state: RunningState::Running,
Expand All @@ -198,11 +193,15 @@ impl Model {
search_input: Input::default(),
initial,
terminal_dimensions,
terminal_too_small,
debug,
}
}

pub(super) fn terminal_too_small(&self) -> bool {
self.terminal_dimensions.width < MIN_TERMINAL_WIDTH
|| self.terminal_dimensions.height < MIN_TERMINAL_HEIGHT
}

pub(super) fn select_next_list_item(&mut self) {
match self.active_pane {
ActivePane::List => self.bookmark_items.state.select_next(),
Expand Down Expand Up @@ -261,7 +260,7 @@ impl Model {
}

pub(super) fn go_back_or_quit(&mut self) {
if self.terminal_too_small {
if self.terminal_too_small() {
self.running_state = RunningState::Done;
return;
}
Expand Down
145 changes: 145 additions & 0 deletions src/tui/tests/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use super::super::commands::Command;
use super::super::message::{Message, UrlsOpenedResult};
use super::super::model::TuiContext;
use super::super::update::update;
use super::super::view::view;
use super::helpers::{press_key, setup_test_tui_with_context};
use crate::domain::SavedBookmark;
use crate::persistence::SearchTerms;
use insta::assert_snapshot;
use ratatui::crossterm::event::KeyCode;
use std::io::Error as IOError;

#[test]
fn opening_a_bookmark_uses_the_selected_uri() {
// GIVEN
let search_terms = SearchTerms::try_from("programming").expect("search terms should be valid");
let (_, mut model) = setup_test_tui_with_context(96, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(bookmarks())));
let _ = press_key(&mut model, KeyCode::Char('j')).expect("j should be handled");

// WHEN
let commands = press_key(&mut model, KeyCode::Char('o')).expect("o should be handled");

// THEN
let [Command::OpenInBrowser(uri)] = commands.as_slice() else {
panic!("opening a bookmark should emit one open in browser command");
};
assert_eq!(uri, "https://gleam.run/");
}

#[test]
fn browser_failure_shows_an_error() {
// GIVEN
let search_terms = SearchTerms::try_from("programming").expect("search terms should be valid");
let (mut terminal, mut model) =
setup_test_tui_with_context(120, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(bookmarks())));

// WHEN
update(
&mut model,
Message::UrlsOpenedInBrowser(UrlsOpenedResult::Failure(IOError::other(
"browser unavailable",
))),
);
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
assert_snapshot!(terminal.backend());
}

#[test]
fn copying_a_bookmark_works() {
// GIVEN
let search_terms = SearchTerms::try_from("programming").expect("search terms should be valid");
let (mut terminal, mut model) =
setup_test_tui_with_context(96, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(bookmarks())));
let _ = press_key(&mut model, KeyCode::Char('j')).expect("j should be handled");

// WHEN
let commands = press_key(&mut model, KeyCode::Char('y')).expect("y should be handled");
update(&mut model, Message::ContentCopiedToClipboard(Ok(())));
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
let [Command::CopyContentToClipboard(uri)] = commands.as_slice() else {
panic!("copying a bookmark should emit one copy command");
};
assert_eq!(uri, "https://gleam.run/");
assert_snapshot!(terminal.backend());
}

#[test]
fn copying_all_bookmarks_works() {
// GIVEN
let search_terms = SearchTerms::try_from("programming").expect("search terms should be valid");
let (_, mut model) = setup_test_tui_with_context(96, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(bookmarks())));

// WHEN
let commands = press_key(&mut model, KeyCode::Char('Y')).expect("Y should be handled");

// THEN
let [Command::CopyContentToClipboard(uris)] = commands.as_slice() else {
panic!("copying all bookmarks should emit one copy command");
};
assert_eq!(uris, "https://www.rust-lang.org/\nhttps://gleam.run/");
}

#[test]
fn failed_copy_shows_an_error() {
// GIVEN
let search_terms = SearchTerms::try_from("programming").expect("search terms should be valid");
let (mut terminal, mut model) =
setup_test_tui_with_context(120, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(bookmarks())));

// WHEN
update(
&mut model,
Message::ContentCopiedToClipboard(Err("clipboard unavailable".to_string())),
);
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
assert_snapshot!(terminal.backend());
}

#[test]
fn bookmark_actions_are_ignored_when_list_is_empty() {
// GIVEN
let search_terms = SearchTerms::try_from("missing").expect("search terms should be valid");
let (_, mut model) = setup_test_tui_with_context(96, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(vec![])));

for key in [KeyCode::Char('o'), KeyCode::Char('y'), KeyCode::Char('Y')] {
// WHEN
let commands = press_key(&mut model, key).expect("bookmark action should be handled");

// THEN
assert!(commands.is_empty());
}
}

fn bookmarks() -> Vec<SavedBookmark> {
vec![
SavedBookmark {
uri: "https://www.rust-lang.org/".to_string(),
title: Some("Rust".to_string()),
tags: Some("rust,programming".to_string()),
},
SavedBookmark {
uri: "https://gleam.run/".to_string(),
title: Some("Gleam".to_string()),
tags: Some("gleam,programming".to_string()),
},
]
}
90 changes: 90 additions & 0 deletions src/tui/tests/bookmarks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use super::super::message::Message;
use super::super::model::TuiContext;
use super::super::update::update;
use super::super::view::view;
use super::helpers::{press_key, setup_test_tui_with_context};
use crate::domain::SavedBookmark;
use crate::persistence::SearchTerms;
use insta::assert_snapshot;
use ratatui::crossterm::event::KeyCode;

#[test]
fn bookmark_details_show_missing_values() {
// GIVEN
let search_terms = SearchTerms::try_from("example").expect("search terms should be valid");
let (mut terminal, mut model) =
setup_test_tui_with_context(96, 24, TuiContext::Search(search_terms));
update(
&mut model,
Message::SearchFinished(Ok(vec![SavedBookmark {
uri: "https://example.com/".to_string(),
title: None,
tags: None,
}])),
);

// WHEN
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
assert_snapshot!(terminal.backend());
}

#[test]
fn moving_to_the_next_bookmark_updates_the_selection() {
// GIVEN
let search_terms = SearchTerms::try_from("example").expect("search terms should be valid");
let (mut terminal, mut model) =
setup_test_tui_with_context(96, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(bookmarks(2))));

// WHEN
let _ = press_key(&mut model, KeyCode::Char('j')).expect("j should be handled");
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
assert_snapshot!(terminal.backend());
}

#[test]
fn jumping_between_first_and_last_bookmarks_works() {
// GIVEN
let search_terms = SearchTerms::try_from("example").expect("search terms should be valid");
let (mut terminal, mut model) =
setup_test_tui_with_context(96, 24, TuiContext::Search(search_terms));
update(&mut model, Message::SearchFinished(Ok(bookmarks(20))));

// WHEN
let _ = press_key(&mut model, KeyCode::Char('G')).expect("G should be handled");
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
assert_eq!(model.bookmark_items.state.selected(), Some(19));
assert_snapshot!(terminal.backend());

// WHEN
let _ = press_key(&mut model, KeyCode::Char('g')).expect("g should be handled");
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
assert_eq!(model.bookmark_items.state.selected(), Some(0));
assert_snapshot!(terminal.backend());
}

fn bookmarks(count: usize) -> Vec<SavedBookmark> {
(1..=count)
.map(|index| SavedBookmark {
uri: format!("https://example.com/{index:02}"),
title: Some(format!("Bookmark {index:02}")),
tags: Some("example".to_string()),
})
.collect()
}
25 changes: 25 additions & 0 deletions src/tui/tests/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use super::super::common::ActivePane;
use super::super::model::TuiContext;
use super::super::view::view;
use super::helpers::{press_key, setup_test_tui_with_context};
use crate::persistence::SearchTerms;
use insta::assert_snapshot;
use ratatui::crossterm::event::KeyCode;

#[test]
fn opening_help_displays_keybindings() {
// GIVEN
let search_terms = SearchTerms::try_from("rust").expect("search terms should be valid");
let (mut terminal, mut model) =
setup_test_tui_with_context(96, 30, TuiContext::Search(search_terms));

// WHEN
let _ = press_key(&mut model, KeyCode::Char('?')).expect("? should be handled");
terminal
.draw(|frame| view(&mut model, frame))
.expect("frame should've been drawn");

// THEN
assert_eq!(model.active_pane, ActivePane::Help);
assert_snapshot!(terminal.backend());
}
Loading
Loading