Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor modals to use a global queue #19

Merged
merged 1 commit into from
Oct 27, 2023
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
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![deny(clippy::all)]
#![feature(associated_type_defaults)]
#![feature(iterator_try_collect)]
#![feature(trait_upcasting)]
#![feature(try_blocks)]

mod cli;
Expand Down
7 changes: 3 additions & 4 deletions src/tui/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl InputEngine {
InputBinding::new(KeyCode::Left, Action::Left),
InputBinding::new(KeyCode::Right, Action::Right),
InputBinding::new(KeyCode::Enter, Action::Interact),
InputBinding::new(KeyCode::Esc, Action::Close),
InputBinding::new(KeyCode::Esc, Action::Cancel),
]
.into_iter()
.map(|binding| (binding.action, binding))
Expand Down Expand Up @@ -105,9 +105,8 @@ pub enum Action {
Right,
/// Do a thing. E.g. select an item in a list
Interact,
/// Close the current modal
#[display(fmt = "Close Dialog")]
Close,
/// Close the current modal/dialog/etc.
Cancel,
}

/// A mapping from a key input sequence to an action. This can optionally have
Expand Down
8 changes: 7 additions & 1 deletion src/tui/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use crate::{
config::{ProfileId, RequestCollection, RequestRecipeId},
http::{RequestBuildError, RequestError, RequestId, RequestRecord},
template::{Prompt, Prompter},
util::ResultExt,
};
use anyhow::Context;
use derive_more::From;
use tokio::sync::mpsc::UnboundedSender;
use tracing::trace;
Expand All @@ -23,7 +25,11 @@ impl MessageSender {
pub fn send(&self, message: impl Into<Message>) {
let message: Message = message.into();
trace!(?message, "Queueing message");
self.0.send(message).expect("Message queue is closed")
let _ = self
.0
.send(message)
.context("Error enqueueing message")
.traced();
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl Tui {
while let Ok(message) = self.messages_rx.try_recv() {
// If an error occurs, store it so we can show the user
if let Err(error) = self.handle_message(message) {
self.view.set_error(error);
self.view.open_modal(error);
}
}

Expand Down Expand Up @@ -211,10 +211,10 @@ impl Tui {
}

Message::PromptStart(prompt) => {
self.view.set_prompt(prompt);
self.view.open_modal(prompt);
}

Message::Error { error } => self.view.set_error(error),
Message::Error { error } => self.view.open_modal(error),
Message::Quit => self.should_run = false,
}
Ok(())
Expand Down
168 changes: 80 additions & 88 deletions src/tui/view/component/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ use crate::{
tui::{
input::Action,
view::{
component::{Component, Draw, UpdateOutcome, ViewMessage},
component::{
modal::IntoModal, Component, Draw, Modal, UpdateOutcome,
ViewMessage,
},
state::Notification,
util::{layout, ButtonBrick, Modal, ModalContent, ToTui},
util::{layout, ButtonBrick, ToTui},
Frame, RenderContext,
},
},
};
use derive_more::From;
use itertools::Itertools;
use ratatui::{
prelude::{Alignment, Constraint, Direction, Rect},
Expand All @@ -22,46 +24,34 @@ use ratatui::{
use std::fmt::Debug;
use tui_textarea::TextArea;

/// A modal to show the user a catastrophic error
pub type ErrorModal = Modal<ErrorModalInner>;
#[derive(Debug)]
pub struct ErrorModal(anyhow::Error);

impl Modal for ErrorModal {
fn title(&self) -> &str {
"Error"
}

fn dimensions(&self) -> (Constraint, Constraint) {
(Constraint::Percentage(60), Constraint::Percentage(20))
}
}

impl Component for ErrorModal {
fn update(&mut self, message: ViewMessage) -> UpdateOutcome {
match message {
// Open the modal
ViewMessage::Error(error) => {
self.open(error.into());
UpdateOutcome::Consumed
}

// Close the modal
// Extra close action
ViewMessage::InputAction {
action: Some(Action::Interact | Action::Close),
action: Some(Action::Interact),
..
} if self.is_open() => {
self.close();
UpdateOutcome::Consumed
}
} => UpdateOutcome::Propagate(ViewMessage::CloseModal),

_ => UpdateOutcome::Propagate(message),
}
}
}

#[derive(Debug, From)]
pub struct ErrorModalInner(anyhow::Error);

impl ModalContent for ErrorModalInner {
fn title(&self) -> &str {
"Error"
}

fn dimensions(&self) -> (Constraint, Constraint) {
(Constraint::Percentage(60), Constraint::Percentage(20))
}
}

impl Draw for ErrorModalInner {
impl Draw for ErrorModal {
fn draw(
&self,
context: &RenderContext,
Expand Down Expand Up @@ -95,93 +85,87 @@ impl Draw for ErrorModalInner {
}
}

/// A modal to prompt the user for some input
pub type PromptModal = Modal<PromptModalInner>;

impl Component for PromptModal {
fn update(&mut self, message: ViewMessage) -> UpdateOutcome {
match message {
// Open the prompt
ViewMessage::Prompt(prompt) => {
// Listen for this outside the child, because it won't be in
// focus while closed
self.open(PromptModalInner::new(prompt));
UpdateOutcome::Consumed
}

// Close
ViewMessage::InputAction {
action: Some(Action::Close),
..
} if self.is_open() => {
// Dropping the prompt returner here will tell the caller
// that we're not returning anything
self.close();
UpdateOutcome::Consumed
}

// Submit
ViewMessage::InputAction {
action: Some(Action::Interact),
..
} if self.is_open() => {
// Return the user's value and close the prompt
let inner = self.close().expect("We checked is_open");
let input = inner.text_area.into_lines().join("\n");
inner.prompt.respond(input);
UpdateOutcome::Consumed
}

// All other input gets forwarded to the text editor
ViewMessage::InputAction { event, .. } if self.is_open() => {
let text_area = match self {
Modal::Closed => unreachable!("We checked is_open"),
Modal::Open {
state: PromptModalInner { text_area, .. },
..
} => text_area,
};
text_area.input(event);
UpdateOutcome::Consumed
}
impl IntoModal for anyhow::Error {
type Target = ErrorModal;

_ => UpdateOutcome::Propagate(message),
}
fn into_modal(self) -> Self::Target {
ErrorModal(self)
}
}

/// Inner state for the prompt modal
#[derive(Debug)]
pub struct PromptModalInner {
pub struct PromptModal {
// Prompt currently being shown
prompt: Prompt,
/// A queue of additional prompts to shown. If the queue is populated,
/// closing one prompt will open a the next one.
// queue: VecDeque<Prompt>,
text_area: TextArea<'static>,
/// Flag set before closing to indicate if we should submit in `on_close``
submit: bool,
}

impl PromptModalInner {
impl PromptModal {
pub fn new(prompt: Prompt) -> Self {
let mut text_area = TextArea::default();
if prompt.sensitive() {
text_area.set_mask_char('\u{2022}');
}
Self { prompt, text_area }
Self {
prompt,
text_area,
submit: false,
}
}
}

impl ModalContent for PromptModalInner {
impl Modal for PromptModal {
fn title(&self) -> &str {
self.prompt.label()
}

fn dimensions(&self) -> (Constraint, Constraint) {
(Constraint::Percentage(60), Constraint::Length(3))
}

fn on_close(self: Box<Self>) {
if self.submit {
// Return the user's value and close the prompt
let input = self.text_area.into_lines().join("\n");
self.prompt.respond(input);
}
}
}

impl Draw for PromptModalInner {
impl Component for PromptModal {
fn update(&mut self, message: ViewMessage) -> UpdateOutcome {
match message {
// Submit
ViewMessage::InputAction {
action: Some(Action::Interact),
..
} => {
// Submission is handled in on_close. The control flow here is
// ugly but it's hard with the top-down nature of modals
self.submit = true;
UpdateOutcome::Propagate(ViewMessage::CloseModal)
}

// All other input gets forwarded to the text editor (except cancel)
ViewMessage::InputAction { event, action }
if action != Some(Action::Cancel) =>
{
self.text_area.input(event);
UpdateOutcome::Consumed
}

_ => UpdateOutcome::Propagate(message),
}
}
}

impl Draw for PromptModal {
fn draw(
&self,
_context: &RenderContext,
Expand All @@ -193,6 +177,14 @@ impl Draw for PromptModalInner {
}
}

impl IntoModal for Prompt {
type Target = PromptModal;

fn into_modal(self) -> Self::Target {
PromptModal::new(self)
}
}

#[derive(Debug)]
pub struct HelpText;

Expand All @@ -209,7 +201,7 @@ impl Draw for HelpText {
Action::ReloadCollection,
Action::FocusNext,
Action::FocusPrevious,
Action::Close,
Action::Cancel,
];
let text = actions
.into_iter()
Expand Down
Loading
Loading