feat(layout): allow reversing the layout and placing input bar on top (#76)

This commit is contained in:
Alexandre Pasmantier 2024-11-25 21:32:41 +01:00 committed by GitHub
parent 9998b9d9f8
commit 220671106e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 117 additions and 79 deletions

View File

@ -25,6 +25,8 @@ use_nerd_font_icons = false
ui_scale = 100
# Whether to show the top help bar in the UI
show_help_bar = true
# Where to place the input bar in the UI (top or bottom)
input_bar_position = "bottom"
# Previewers settings
# ----------------------------------------------------------------------------

View File

@ -59,8 +59,6 @@ impl Keymap {
/// The main application struct that holds the state of the application.
pub struct App {
/// The configuration of the application.
config: Config,
keymap: Keymap,
// maybe move these two into config instead of passing them
// via the cli?
@ -123,13 +121,12 @@ impl App {
channel: TelevisionChannel,
tick_rate: f64,
frame_rate: f64,
passthrough_keybindings: Vec<String>,
passthrough_keybindings: &[String],
) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let (render_tx, _) = mpsc::unbounded_channel();
let (_, event_rx) = mpsc::unbounded_channel();
let (event_abort_tx, _) = mpsc::unbounded_channel();
let television = Arc::new(Mutex::new(Television::new(channel)));
let config = Config::new()?;
let keymap = Keymap::from(&config.keybindings).with_mode_mappings(
Mode::Channel,
@ -142,9 +139,10 @@ impl App {
.collect(),
)?;
debug!("{:?}", keymap);
let television =
Arc::new(Mutex::new(Television::new(channel, config.clone())));
Ok(Self {
config,
keymap,
tick_rate,
frame_rate,
@ -184,14 +182,12 @@ impl App {
let (render_tx, render_rx) = mpsc::unbounded_channel();
self.render_tx = render_tx.clone();
let action_tx_r = self.action_tx.clone();
let config_r = self.config.clone();
let television_r = self.television.clone();
let frame_rate = self.frame_rate;
let rendering_task = tokio::spawn(async move {
render(
render_rx,
action_tx_r,
config_r,
television_r,
frame_rate,
is_output_tty,

View File

@ -2,6 +2,8 @@ use config::ValueKind;
use serde::Deserialize;
use std::collections::HashMap;
use crate::ui::layout::InputPosition;
const DEFAULT_UI_SCALE: u16 = 90;
#[derive(Clone, Debug, Deserialize)]
@ -9,6 +11,7 @@ pub struct UiConfig {
pub use_nerd_font_icons: bool,
pub ui_scale: u16,
pub show_help_bar: bool,
pub input_bar_position: InputPosition,
}
impl Default for UiConfig {
@ -17,6 +20,7 @@ impl Default for UiConfig {
use_nerd_font_icons: false,
ui_scale: DEFAULT_UI_SCALE,
show_help_bar: true,
input_bar_position: InputPosition::Bottom,
}
}
}
@ -36,6 +40,10 @@ impl From<UiConfig> for ValueKind {
String::from("show_help_bar"),
ValueKind::Boolean(val.show_help_bar).into(),
);
m.insert(
String::from("input_position"),
ValueKind::String(val.input_bar_position.to_string()).into(),
);
ValueKind::Table(m)
}
}

View File

@ -45,7 +45,7 @@ async fn main() -> Result<()> {
},
args.tick_rate,
args.frame_rate,
args.passthrough_keybindings,
&args.passthrough_keybindings,
) {
Ok(mut app) => {
stdout().flush()?;

View File

@ -12,7 +12,7 @@ use tokio::{
};
use crate::television::Television;
use crate::{action::Action, config::Config, tui::Tui};
use crate::{action::Action, tui::Tui};
#[derive(Debug)]
pub enum RenderingTask {
@ -42,7 +42,6 @@ impl IoStream {
pub async fn render(
mut render_rx: mpsc::UnboundedReceiver<RenderingTask>,
action_tx: mpsc::UnboundedSender<Action>,
config: Config,
television: Arc<Mutex<Television>>,
frame_rate: f64,
is_output_tty: bool,
@ -59,15 +58,11 @@ pub async fn render(
debug!("Entering tui");
tui.enter()?;
debug!("Registering action handler and config handler");
debug!("Registering action handler");
television
.lock()
.await
.register_action_handler(action_tx.clone())?;
television
.lock()
.await
.register_config_handler(config.clone())?;
// Rendering loop
loop {

View File

@ -1,7 +1,7 @@
use crate::app::Keymap;
use crate::picker::Picker;
use crate::ui::input::actions::InputActionHandler;
use crate::ui::layout::{Dimensions, Layout};
use crate::ui::layout::{Dimensions, InputPosition, Layout};
use crate::ui::spinner::Spinner;
use crate::ui::spinner::SpinnerState;
use crate::{action::Action, config::Config};
@ -14,7 +14,6 @@ use television_channels::channels::{
remote_control::RemoteControl, OnAir, TelevisionChannel, UnitChannel,
};
use television_channels::entry::{Entry, ENTRY_PLACEHOLDER};
use television_previewers::previewers;
use television_previewers::previewers::Previewer;
use television_utils::strings::EMPTY_STRING;
use tokio::sync::mpsc::UnboundedSender;
@ -54,23 +53,30 @@ pub struct Television {
impl Television {
#[must_use]
pub fn new(mut channel: TelevisionChannel) -> Self {
pub fn new(mut channel: TelevisionChannel, config: Config) -> Self {
let results_picker = match config.ui.input_bar_position {
InputPosition::Bottom => Picker::default().inverted(),
InputPosition::Top => Picker::default(),
};
let previewer = Previewer::new(Some(config.previewers.clone().into()));
let keymap = Keymap::from(&config.keybindings);
channel.find(EMPTY_STRING);
let spinner = Spinner::default();
Self {
action_tx: None,
config: Config::default(),
keymap: Keymap::default(),
config,
keymap,
channel,
remote_control: TelevisionChannel::RemoteControl(
RemoteControl::default(),
),
mode: Mode::Channel,
current_pattern: EMPTY_STRING.to_string(),
results_picker: Picker::default().inverted(),
results_picker,
rc_picker: Picker::default(),
results_area_height: 0,
previewer: Previewer::default(),
previewer,
preview_scroll: None,
preview_pane_height: 0,
current_preview_total_lines: 0,
@ -220,24 +226,6 @@ impl Television {
Ok(())
}
/// Register a configuration handler that provides configuration settings if necessary.
///
/// # Arguments
/// * `config` - Configuration settings.
///
/// # Returns
/// * `Result<()>` - An Ok result or an error.
pub fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
self.keymap = Keymap::from(&self.config.keybindings);
let previewer_config =
std::convert::Into::<previewers::PreviewerConfig>::into(
self.config.previewers.clone(),
);
self.previewer.set_config(previewer_config);
Ok(())
}
/// Update the state of the component based on a received action.
///
/// # Arguments
@ -388,33 +376,34 @@ impl Television {
area,
!matches!(self.mode, Mode::Channel),
self.config.ui.show_help_bar,
self.config.ui.input_bar_position,
);
// help bar (metadata, keymaps, logo)
self.draw_help_bar(f, &layout)?;
self.draw_help_bar(f, &layout.help_bar)?;
self.results_area_height = u32::from(layout.results.height - 2); // 2 for the borders
self.preview_pane_height = layout.preview_window.height;
// top left block: results
self.draw_results_list(f, &layout)?;
// results list
self.draw_results_list(f, layout.results)?;
// bottom left block: input
self.draw_input_box(f, &layout)?;
// input box
self.draw_input_box(f, layout.input)?;
let selected_entry = self
.get_selected_entry(Some(Mode::Channel))
.unwrap_or(ENTRY_PLACEHOLDER);
let preview = self.previewer.preview(&selected_entry);
// top right block: preview title
// preview title
self.current_preview_total_lines = preview.total_lines();
self.draw_preview_title_block(f, &layout, &preview)?;
self.draw_preview_title_block(f, layout.preview_title, &preview)?;
// bottom right block: preview content
// preview content
self.draw_preview_content_block(
f,
&layout,
layout.preview_window,
selected_entry
.line_number
.map(|l| u16::try_from(l).unwrap_or(0)),
@ -423,7 +412,7 @@ impl Television {
// remote control
if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) {
self.draw_remote_control(f, &layout.remote_control.unwrap())?;
self.draw_remote_control(f, layout.remote_control.unwrap())?;
}
Ok(())
}

View File

@ -1,5 +1,4 @@
use crate::television::Television;
use crate::ui::layout::Layout;
use crate::ui::logo::build_logo_paragraph;
use crate::ui::mode::mode_color;
use ratatui::layout::Rect;
@ -7,6 +6,8 @@ use ratatui::prelude::{Color, Style};
use ratatui::widgets::{Block, BorderType, Borders, Padding};
use ratatui::Frame;
use super::layout::HelpBarLayout;
pub fn draw_logo_block(f: &mut Frame, area: Rect, color: Color) {
let logo_block = Block::default()
.borders(Borders::ALL)
@ -24,9 +25,9 @@ impl Television {
pub(crate) fn draw_help_bar(
&self,
f: &mut Frame,
layout: &Layout,
layout: &Option<HelpBarLayout>,
) -> color_eyre::Result<()> {
if let Some(help_bar) = layout.help_bar {
if let Some(help_bar) = layout {
self.draw_metadata_block(f, help_bar.left);
self.draw_keymaps_block(f, help_bar.middle)?;
draw_logo_block(f, help_bar.right, mode_color(self.mode));

View File

@ -1,9 +1,8 @@
use crate::television::Television;
use crate::ui::layout::Layout;
use crate::ui::BORDER_COLOR;
use color_eyre::eyre::Result;
use ratatui::layout::{
Alignment, Constraint, Direction, Layout as RatatuiLayout,
Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect,
};
use ratatui::prelude::{Span, Style};
use ratatui::style::Stylize;
@ -416,7 +415,7 @@ impl Television {
pub(crate) fn draw_input_box(
&mut self,
f: &mut Frame,
layout: &Layout,
rect: Rect,
) -> Result<()> {
let input_block = Block::default()
.title_top(Line::from(" Pattern ").alignment(Alignment::Center))
@ -425,12 +424,12 @@ impl Television {
.border_style(Style::default().fg(BORDER_COLOR))
.style(Style::default());
let input_block_inner = input_block.inner(layout.input);
let input_block_inner = input_block.inner(rect);
if input_block_inner.area() == 0 {
return Ok(());
}
f.render_widget(input_block, layout.input);
f.render_widget(input_block, rect);
// split input block into 4 parts: prompt symbol, input, result count, spinner
let total_count = self.channel.total_count();

View File

@ -1,5 +1,8 @@
use std::fmt::Display;
use ratatui::layout;
use ratatui::layout::{Constraint, Direction, Rect};
use serde::Deserialize;
pub struct Dimensions {
pub x: u16,
@ -41,6 +44,24 @@ impl HelpBarLayout {
}
}
#[derive(Debug, Clone, Copy, Deserialize, Default)]
pub enum InputPosition {
#[serde(rename = "top")]
Top,
#[serde(rename = "bottom")]
#[default]
Bottom,
}
impl Display for InputPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InputPosition::Top => write!(f, "top"),
InputPosition::Bottom => write!(f, "bottom"),
}
}
}
pub struct Layout {
pub help_bar: Option<HelpBarLayout>,
pub results: Rect,
@ -75,6 +96,7 @@ impl Layout {
area: Rect,
with_remote: bool,
with_help_bar: bool,
input_position: InputPosition,
) -> Self {
let main_block = centered_rect(dimensions.x, dimensions.y, area);
// split the main block into two vertical chunks (help bar + rest)
@ -127,23 +149,47 @@ impl Layout {
.split(main_rect);
// left block: results + input field
let results_constraints =
vec![Constraint::Min(3), Constraint::Length(3)];
let left_chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(3)])
.constraints(match input_position {
InputPosition::Top => {
results_constraints.into_iter().rev().collect()
}
InputPosition::Bottom => results_constraints,
})
.split(vt_chunks[0]);
let (input, results) = match input_position {
InputPosition::Bottom => (left_chunks[1], left_chunks[0]),
InputPosition::Top => (left_chunks[0], left_chunks[1]),
};
// right block: preview title + preview
let preview_constraints =
vec![Constraint::Length(3), Constraint::Min(3)];
let right_chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(3)])
.constraints(match input_position {
InputPosition::Top => {
preview_constraints.into_iter().rev().collect()
}
InputPosition::Bottom => preview_constraints,
})
.split(vt_chunks[1]);
let (preview_title, preview_window) = match input_position {
InputPosition::Bottom => (right_chunks[0], right_chunks[1]),
InputPosition::Top => (right_chunks[1], right_chunks[0]),
};
Self::new(
help_bar_layout,
left_chunks[0],
left_chunks[1],
right_chunks[0],
right_chunks[1],
results,
input,
preview_title,
preview_window,
if with_remote {
Some(vt_chunks[2])
} else {

View File

@ -1,5 +1,4 @@
use crate::television::Television;
use crate::ui::layout::Layout;
use crate::ui::BORDER_COLOR;
use color_eyre::eyre::Result;
use ratatui::layout::{Alignment, Rect};
@ -26,7 +25,7 @@ impl Television {
pub(crate) fn draw_preview_title_block(
&self,
f: &mut Frame,
layout: &Layout,
rect: Rect,
preview: &Arc<Preview>,
) -> Result<()> {
let mut preview_title_spans = Vec::new();
@ -44,7 +43,7 @@ impl Television {
preview_title_spans.push(Span::styled(
shrink_with_ellipsis(
&preview.title,
layout.preview_window.width.saturating_sub(4) as usize,
rect.width.saturating_sub(4) as usize,
),
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
));
@ -57,14 +56,14 @@ impl Television {
.border_style(Style::default().fg(BORDER_COLOR)),
)
.alignment(Alignment::Left);
f.render_widget(preview_title, layout.preview_title);
f.render_widget(preview_title, rect);
Ok(())
}
pub(crate) fn draw_preview_content_block(
&mut self,
f: &mut Frame,
layout: &Layout,
rect: Rect,
target_line: Option<u16>,
preview: &Arc<Preview>,
) {
@ -83,8 +82,8 @@ impl Television {
bottom: 0,
left: 1,
});
let inner = preview_outer_block.inner(layout.preview_window);
f.render_widget(preview_outer_block, layout.preview_window);
let inner = preview_outer_block.inner(rect);
f.render_widget(preview_outer_block, rect);
//if let PreviewContent::Image(img) = &preview.content {
// let image_component = StatefulImage::new(None);

View File

@ -18,7 +18,7 @@ impl Television {
pub fn draw_remote_control(
&mut self,
f: &mut Frame,
area: &Rect,
rect: Rect,
) -> Result<()> {
let layout = Layout::default()
.direction(Direction::Vertical)
@ -30,7 +30,7 @@ impl Television {
]
.as_ref(),
)
.split(*area);
.split(rect);
self.draw_rc_channels(f, &layout[0])?;
self.draw_rc_input(f, &layout[1])?;
draw_rc_logo(f, layout[2], mode_color(self.mode));

View File

@ -1,8 +1,8 @@
use crate::television::Television;
use crate::ui::layout::Layout;
use crate::ui::layout::InputPosition;
use crate::ui::BORDER_COLOR;
use color_eyre::eyre::Result;
use ratatui::layout::Alignment;
use ratatui::layout::{Alignment, Rect};
use ratatui::prelude::{Color, Line, Span, Style};
use ratatui::widgets::{
Block, BorderType, Borders, List, ListDirection, Padding,
@ -164,7 +164,7 @@ impl Television {
pub(crate) fn draw_results_list(
&mut self,
f: &mut Frame,
layout: &Layout,
rect: Rect,
) -> Result<()> {
let results_block = Block::default()
.title_top(Line::from(" Results ").alignment(Alignment::Center))
@ -181,21 +181,24 @@ impl Television {
}
let entries = self.channel.results(
layout.results.height.saturating_sub(2).into(),
rect.height.saturating_sub(2).into(),
u32::try_from(self.results_picker.offset())?,
);
let results_list = build_results_list(
results_block,
&entries,
ListDirection::BottomToTop,
match self.config.ui.input_bar_position {
InputPosition::Bottom => ListDirection::BottomToTop,
InputPosition::Top => ListDirection::TopToBottom,
},
None,
self.config.ui.use_nerd_font_icons,
);
f.render_stateful_widget(
results_list,
layout.results,
rect,
&mut self.results_picker.relative_state,
);
Ok(())