Skip to content

Commit

Permalink
Use global context for static values in TUI
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasPickering committed Nov 28, 2023
1 parent 2e3642d commit c41b7f9
Show file tree
Hide file tree
Showing 16 changed files with 128 additions and 104 deletions.
20 changes: 10 additions & 10 deletions src/tui.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod context;
mod input;
mod message;
mod view;
Expand All @@ -7,7 +8,8 @@ use crate::{
http::{HttpEngine, Repository, RequestBuilder},
template::{Prompter, Template, TemplateChunk, TemplateContext},
tui::{
input::{Action, InputEngine},
context::TuiContext,
input::Action,
message::{Message, MessageSender},
view::{ModalPriority, PreviewPrompter, RequestState, View},
},
Expand Down Expand Up @@ -51,7 +53,6 @@ pub struct Tui {
messages_rx: UnboundedReceiver<Message>,
messages_tx: MessageSender,
http_engine: HttpEngine,
input_engine: InputEngine,
view: View,
collection: RequestCollection,
repository: Repository,
Expand All @@ -75,6 +76,9 @@ impl Tui {
let (messages_tx, messages_rx) = mpsc::unbounded_channel();
let messages_tx = MessageSender::new(messages_tx);

// Initialize read-only global data as early as possible
TuiContext::init(messages_tx.clone());

// If the collection fails to load, create an empty one just so we can
// move along. We'll watch the file and hopefully the user can fix it
let collection = RequestCollection::load(collection_file.clone())
Expand All @@ -84,14 +88,13 @@ impl Tui {
RequestCollection::<()>::default().with_source(collection_file)
});

let view = View::new(&collection, messages_tx.clone());
let view = View::new(&collection);
let repository = Repository::load(&collection.id).unwrap();
let app = Tui {
terminal,
messages_rx,
messages_tx,
http_engine: HttpEngine::new(repository.clone()),
input_engine: InputEngine::new(),

collection,
should_run: true,
Expand Down Expand Up @@ -128,7 +131,7 @@ impl Tui {
// Forward input to the view. Include the raw event for text
// editors and such
let event = crossterm::event::read()?;
let action = self.input_engine.action(&event);
let action = TuiContext::get().input_engine.action(&event);
if let Some(Action::ForceQuit) = action {
// Short-circuit the view/message cycle, to make sure this
// doesn't get ate
Expand All @@ -150,10 +153,7 @@ impl Tui {
}

// ===== Draw Phase =====
self.terminal.draw(|f| {
self.view
.draw(&self.input_engine, self.messages_tx.clone(), f)
})?;
self.terminal.draw(|f| self.view.draw(f))?;

// ===== Signal Phase =====
if quit_signals.pending().next().is_some() {
Expand Down Expand Up @@ -286,7 +286,7 @@ impl Tui {
self.collection = collection;

// Rebuild the whole view, because tons of things can change
self.view = View::new(&self.collection, self.messages_tx.clone());
self.view = View::new(&self.collection);
self.view.notify(format!(
"Reloaded collection from {}",
self.collection.path().to_string_lossy()
Expand Down
61 changes: 61 additions & 0 deletions src/tui/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::tui::{
input::InputEngine,
message::{Message, MessageSender},
view::Theme,
};
use std::sync::OnceLock;

/// The singleton value for the theme. Initialized once during startup, then
/// freely available *read only* everywhere.
static CONTEXT: OnceLock<TuiContext> = OnceLock::new();

/// Globally available read-only context for the TUI. This is initialized
/// once during **TUI** creation (not view creation), meaning there is only
/// one per session. Data that can change through the lifespan of the process,
/// e.g. by user input or collection reload, should *not* go in here.
///
/// The purpose of this is to make it easy for components in the view to access
/// global data without needing to drill it all down the tree. This is purely
/// for convenience. The invariants that make this work are simple andeasy to
/// enforce.
#[derive(Debug)]
pub struct TuiContext {
/// Visual theme. Colors!
pub theme: Theme,
/// Input:action bindings
pub input_engine: InputEngine,
/// Async message queue. Used to trigger async tasks and mutations from the
/// view.
pub messages_tx: MessageSender,
}

impl TuiContext {
/// Initialize global context. Should be called only once, during startup.
pub fn init(messages_tx: MessageSender) {
CONTEXT
.set(Self {
theme: Theme::default(),
input_engine: InputEngine::default(),
messages_tx,
})
.expect("Global context is already initialized");
}

#[cfg(test)]
pub fn init_test() {
use tokio::sync::mpsc;
Self::init(MessageSender::new(mpsc::unbounded_channel().0))
}

/// Get a reference to the global context
pub fn get() -> &'static Self {
// Right now the theme isn't configurable so this is fine. To make it
// configurable we'll need to populate the static value during startup
CONTEXT.get().expect("Global context is not initialized")
}

/// Send a message to trigger an async action
pub fn send_message(message: Message) {
Self::get().messages_tx.send(message);
}
}
29 changes: 7 additions & 22 deletions src/tui/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ mod util;

pub use common::modal::{IntoModal, ModalPriority};
pub use state::RequestState;
pub use theme::Theme;
pub use util::PreviewPrompter;

use crate::{
collection::{RequestCollection, RequestRecipeId},
tui::{
input::{Action, InputEngine},
message::MessageSender,
input::Action,
view::{
component::{root::Root, Component},
draw::{Draw, DrawContext},
Expand All @@ -34,18 +34,13 @@ use tracing::{error, trace, trace_span};
/// by the controller and exposed via event passing.
#[derive(Debug)]
pub struct View {
messages_tx: MessageSender,
config: ViewConfig,
root: Component<Root>,
}

impl View {
pub fn new(
collection: &RequestCollection,
messages_tx: MessageSender,
) -> Self {
pub fn new(collection: &RequestCollection) -> Self {
let mut view = Self {
messages_tx,
config: ViewConfig::default(),
root: Root::new(collection).into(),
};
Expand All @@ -56,18 +51,11 @@ impl View {

/// Draw the view to screen. This needs access to the input engine in order
/// to render input bindings as help messages to the user.
pub fn draw<'a>(
&'a self,
input_engine: &'a InputEngine,
messages_tx: MessageSender,
frame: &'a mut Frame,
) {
pub fn draw<'a>(&'a self, frame: &'a mut Frame) {
let chunk = frame.size();
self.root.draw(
&mut DrawContext {
input_engine,
config: &self.config,
messages_tx,
frame,
},
(),
Expand Down Expand Up @@ -132,11 +120,8 @@ impl View {

let span = trace_span!("View event", ?event);
span.in_scope(|| {
let mut context = UpdateContext::new(
self.messages_tx.clone(),
&mut event_queue,
&mut self.config,
);
let mut context =
UpdateContext::new(&mut event_queue, &mut self.config);

let update =
Self::update_all(self.root.as_child(), &mut context, event);
Expand Down Expand Up @@ -194,7 +179,7 @@ impl View {

/// Settings that control the behavior of the view
#[derive(Debug)]
pub struct ViewConfig {
struct ViewConfig {
/// Should templates be rendered inline in the UI, or should we show the
/// raw text?
preview_templates: bool,
Expand Down
9 changes: 7 additions & 2 deletions src/tui/view/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ pub mod text_window;
use crate::{
collection::{Profile, RequestRecipe},
http::{RequestBuildError, RequestError},
tui::view::{draw::Generate, state::Notification, theme::Theme},
tui::{
context::TuiContext,
view::{draw::Generate, state::Notification},
},
};
use chrono::{DateTime, Duration, Local, Utc};
use itertools::Itertools;
Expand All @@ -36,7 +39,9 @@ impl<'a> Generate for Block<'a> {
{
ratatui::widgets::Block::default()
.borders(Borders::ALL)
.border_style(Theme::get().pane_border_style(self.is_focused))
.border_style(
TuiContext::get().theme.pane_border_style(self.is_focused),
)
.title(self.title)
}
}
Expand Down
14 changes: 8 additions & 6 deletions src/tui/view/common/list.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::tui::view::{
common::Block,
draw::Generate,
state::select::{SelectState, SelectStateKind},
theme::Theme,
use crate::tui::{
context::TuiContext,
view::{
common::Block,
draw::Generate,
state::select::{SelectState, SelectStateKind},
},
};
use ratatui::{
text::Span,
Expand Down Expand Up @@ -38,6 +40,6 @@ where

ratatui::widgets::List::new(items)
.block(block)
.highlight_style(Theme::get().list_highlight_style)
.highlight_style(TuiContext::get().theme.list_highlight_style)
}
}
4 changes: 2 additions & 2 deletions src/tui/view/common/table.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::tui::view::{draw::Generate, theme::Theme};
use crate::tui::{context::TuiContext, view::draw::Generate};
use ratatui::{
prelude::Constraint,
widgets::{Cell, Row},
Expand Down Expand Up @@ -42,7 +42,7 @@ where
where
Self: 'this,
{
let theme = Theme::get();
let theme = &TuiContext::get().theme;
let rows = self.rows.into_iter().enumerate().map(|(i, row)| {
// Alternate row style for readability
let style = if self.alternate_row_style && i % 2 == 1 {
Expand Down
4 changes: 2 additions & 2 deletions src/tui/view/common/tabs.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::tui::{
context::TuiContext,
input::Action,
view::{
draw::{Draw, DrawContext},
event::{Event, EventHandler, Update, UpdateContext},
state::select::{Fixed, FixedSelect, SelectState},
theme::Theme,
},
};
use ratatui::prelude::Rect;
Expand Down Expand Up @@ -52,7 +52,7 @@ impl<T: FixedSelect> Draw for Tabs<T> {
T::iter().map(|e| e.to_string()).collect(),
)
.select(self.tabs.selected_index())
.highlight_style(Theme::get().tab_highlight_style),
.highlight_style(TuiContext::get().theme.tab_highlight_style),
area,
)
}
Expand Down
16 changes: 5 additions & 11 deletions src/tui/view/common/template_preview.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
use crate::{
collection::ProfileId,
template::{Template, TemplateChunk},
tui::{
message::Message,
view::{
draw::{DrawContext, Generate},
theme::Theme,
},
},
tui::{context::TuiContext, message::Message, view::draw::Generate},
};
use derive_more::Deref;
use ratatui::{
Expand Down Expand Up @@ -44,7 +38,6 @@ impl TemplatePreview {
/// render the template. Profile ID defines which profile to use for the
/// render.
pub fn new(
context: &DrawContext,
template: Template,
profile_id: Option<ProfileId>,
enabled: bool,
Expand All @@ -53,7 +46,7 @@ impl TemplatePreview {
// Tell the controller to start rendering the preview, and it'll
// store it back here when done
let lock = Arc::new(OnceLock::new());
context.messages_tx.send(Message::TemplatePreview {
TuiContext::send_message(Message::TemplatePreview {
// If this is a bottleneck we can Arc it
template: template.clone(),
profile_id,
Expand Down Expand Up @@ -120,7 +113,7 @@ impl<'a> TextStitcher<'a> {
template: &'a Template,
areas: &'a [TemplateChunk],
) -> Text<'a> {
let theme = Theme::get();
let theme = &TuiContext::get().theme;

// Each area will get its own styling, but we can't just make each
// area a Span, because one area might have multiple lines. And we
Expand Down Expand Up @@ -220,7 +213,8 @@ mod tests {
let profile = indexmap! { "user_id".into() => "🧡\n💛".into() };
let context = create!(TemplateContext, profile: profile);
let areas = template.render_chunks(&context).await;
let theme = Theme::get();
TuiContext::init_test();
let theme = &TuiContext::get().theme;

let text = TextStitcher::stitch_chunks(&template, &areas);
let rendered_style = theme.template_preview_text;
Expand Down
10 changes: 6 additions & 4 deletions src/tui/view/component/help.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::tui::{
context::TuiContext,
input::Action,
view::{
common::{modal::Modal, table::Table},
draw::{Draw, DrawContext, Generate},
event::EventHandler,
theme::Theme,
},
};
use itertools::Itertools;
Expand All @@ -25,10 +25,12 @@ impl Draw for HelpFooter {
// get granular control
let actions = [Action::OpenSettings, Action::OpenHelp, Action::Quit];

let tui_context = TuiContext::get();

let text = actions
.into_iter()
.map(|action| {
context
tui_context
.input_engine
.binding(action)
.as_ref()
Expand All @@ -41,7 +43,7 @@ impl Draw for HelpFooter {
context.frame.render_widget(
Paragraph::new(text)
.alignment(Alignment::Right)
.style(Theme::get().text_highlight),
.style(tui_context.theme.text_highlight),
area,
);
}
Expand All @@ -66,7 +68,7 @@ impl EventHandler for HelpModal {}
impl Draw for HelpModal {
fn draw(&self, context: &mut DrawContext, _: (), area: Rect) {
let table = Table {
rows: context
rows: TuiContext::get()
.input_engine
.bindings()
.values()
Expand Down
Loading

0 comments on commit c41b7f9

Please sign in to comment.