mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
feat(layout): allow reversing the layout and placing input bar on top (#76)
This commit is contained in:
parent
9998b9d9f8
commit
220671106e
@ -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
|
||||
# ----------------------------------------------------------------------------
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()?;
|
||||
|
@ -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 {
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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(())
|
||||
|
Loading…
x
Reference in New Issue
Block a user