refactor(ui): more compact general layout and make preview panel optional (#148)

Fixes #147 

<img width="2552" alt="Screenshot 2024-12-28 at 15 18 42"
src="https://github.com/user-attachments/assets/08e440c2-6878-4a0f-8734-83a8e8b84e5a"
/>
This commit is contained in:
Alex Pasmantier 2024-12-28 15:21:17 +01:00 committed by GitHub
parent ba5b0857c3
commit 499bfdb8e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 142 additions and 138 deletions

View File

@ -43,11 +43,15 @@ use_nerd_font_icons = false
ui_scale = 100 ui_scale = 100
# Whether to show the top help bar in the UI by default # Whether to show the top help bar in the UI by default
# This option can be toggled with the (default) `ctrl-g` keybinding # This option can be toggled with the (default) `ctrl-g` keybinding
show_help_bar = true show_help_bar = false
# Whether to show the preview panel in the UI by default
# This option can be toggled with the (default) `ctrl-o` keybinding
show_preview_panel = true
# Where to place the input bar in the UI (top or bottom) # Where to place the input bar in the UI (top or bottom)
input_bar_position = "bottom" input_bar_position = "top"
# DEPRECATED: title is now always displayed at the top as part of the border
# Where to place the preview title in the UI (top or bottom) # Where to place the preview title in the UI (top or bottom)
preview_title_position = "top" # preview_title_position = "top"
# The theme to use for the UI # The theme to use for the UI
# A list of builtin themes can be found in the `themes` directory of the television # A list of builtin themes can be found in the `themes` directory of the television
# repository. You may also create your own theme by creating a new file in a `themes` # repository. You may also create your own theme by creating a new file in a `themes`
@ -88,6 +92,8 @@ toggle_remote_control = "ctrl-r"
toggle_send_to_channel = "ctrl-s" toggle_send_to_channel = "ctrl-s"
# Toggle the help bar # Toggle the help bar
toggle_help = "ctrl-g" toggle_help = "ctrl-g"
# Toggle the preview panel
toggle_preview = "ctrl-o"
# Remote control mode # Remote control mode
@ -106,6 +112,8 @@ select_entry = "enter"
toggle_remote_control = "ctrl-r" toggle_remote_control = "ctrl-r"
# Toggle the help bar # Toggle the help bar
toggle_help = "ctrl-g" toggle_help = "ctrl-g"
# Toggle the preview panel
toggle_preview = "ctrl-o"
# Send to channel mode # Send to channel mode
@ -124,24 +132,27 @@ select_entry = "enter"
toggle_send_to_channel = "ctrl-s" toggle_send_to_channel = "ctrl-s"
# Toggle the help bar # Toggle the help bar
toggle_help = "ctrl-g" toggle_help = "ctrl-g"
# Toggle the preview panel
toggle_preview = "ctrl-o"
# Shell integration # Shell integration
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# #
# The shell integration feature allows you to use television as a picker for # The shell integration feature allows you to use television as a picker for
# your shell commands. E.g. typing `git checkout <TAB>` will open television # your shell commands. E.g. typing `git checkout <CTRL-T>` will open television
# with a list of branches to checkout. # with a list of branches to choose from.
[shell_integration.commands] [shell_integration.commands]
# Which commands should trigger which channels # Add your commands here. Each key is a command that will trigger tv with the
# The keys are the commands that should trigger the channels, and the values # corresponding channel as value.
# are the channels that should be triggered. # Example: say you want the following prompts to trigger the following channels
# Example: `git checkout` should trigger the `git-branches` channel # when pressing <CTRL-T>:
# `ls` should trigger the `dirs` channel # `git checkout` should trigger the `git-branches` channel
# `cat` should trigger the `files` channel # `ls` should trigger the `dirs` channel
# `cat` should trigger the `files` channel
# #
# Would be written as: # You would add the following to your configuration file:
# ``` # ```
# [shell_integration.commands] # [shell_integration.commands]
# "git checkout" = "git-branches" # "git checkout" = "git-branches"
@ -149,8 +160,6 @@ toggle_help = "ctrl-g"
# "cat" = "files" # "cat" = "files"
# ``` # ```
# The following are an example, you can remove/modify them and add your own.
# shell history (according to your shell) # shell history (according to your shell)
"" = "zsh-history" "" = "zsh-history"

View File

@ -4,7 +4,7 @@ use ratatui::{
Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect, Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect,
}, },
style::{Style, Stylize}, style::{Style, Stylize},
text::{Line, Span}, text::Span,
widgets::{Block, BorderType, Borders, ListState, Paragraph}, widgets::{Block, BorderType, Borders, ListState, Paragraph},
Frame, Frame,
}; };
@ -30,7 +30,6 @@ pub fn draw_input_box(
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
) -> Result<()> { ) -> Result<()> {
let input_block = Block::default() let input_block = Block::default()
.title_top(Line::from(" Pattern ").alignment(Alignment::Center))
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(colorscheme.general.border_fg)) .border_style(Style::default().fg(colorscheme.general.border_fg))

View File

@ -150,19 +150,6 @@ fn build_keybindings_table_for_channel<'a>(
colorscheme.mode.channel, colorscheme.mode.channel,
)); ));
// MISC line (quit, help, etc.)
// Toggle help bar
let toggle_help_bar_keys = keybindings
.bindings
.get(&DisplayableAction::ToggleHelpBar)
.unwrap();
let toggle_help_bar_row = Row::new(build_cells_for_group(
"Toggle help bar",
toggle_help_bar_keys,
colorscheme.help.metadata_field_name_fg,
colorscheme.mode.channel,
));
let widths = vec![Constraint::Fill(1), Constraint::Fill(2)]; let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
Table::new( Table::new(
@ -173,7 +160,6 @@ fn build_keybindings_table_for_channel<'a>(
copy_entry_row, copy_entry_row,
send_to_channel_row, send_to_channel_row,
switch_channels_row, switch_channels_row,
toggle_help_bar_row,
], ],
widths, widths,
) )

View File

@ -84,8 +84,7 @@ pub struct Layout {
pub help_bar: Option<HelpBarLayout>, pub help_bar: Option<HelpBarLayout>,
pub results: Rect, pub results: Rect,
pub input: Rect, pub input: Rect,
pub preview_title: Rect, pub preview_window: Option<Rect>,
pub preview_window: Rect,
pub remote_control: Option<Rect>, pub remote_control: Option<Rect>,
} }
@ -95,15 +94,13 @@ impl Layout {
help_bar: Option<HelpBarLayout>, help_bar: Option<HelpBarLayout>,
results: Rect, results: Rect,
input: Rect, input: Rect,
preview_title: Rect, preview_window: Option<Rect>,
preview_window: Rect,
remote_control: Option<Rect>, remote_control: Option<Rect>,
) -> Self { ) -> Self {
Self { Self {
help_bar, help_bar,
results, results,
input, input,
preview_title,
preview_window, preview_window,
remote_control, remote_control,
} }
@ -114,8 +111,8 @@ impl Layout {
area: Rect, area: Rect,
with_remote: bool, with_remote: bool,
with_help_bar: bool, with_help_bar: bool,
with_preview: bool,
input_position: InputPosition, input_position: InputPosition,
preview_title_position: PreviewTitlePosition,
) -> Self { ) -> Self {
let main_block = centered_rect(dimensions.x, dimensions.y, area); let main_block = centered_rect(dimensions.x, dimensions.y, area);
// split the main block into two vertical chunks (help bar + rest) // split the main block into two vertical chunks (help bar + rest)
@ -152,16 +149,16 @@ impl Layout {
help_bar_layout = None; help_bar_layout = None;
} }
// split the main block into two vertical chunks // split the main block into 1, 2, or 3 vertical chunks
let constraints = if with_remote { // (results + preview + remote)
vec![ let mut constraints = vec![Constraint::Fill(1)];
Constraint::Fill(1), if with_preview {
Constraint::Fill(1), constraints.push(Constraint::Fill(1));
Constraint::Length(24), }
] if with_remote {
} else { // in order to fit with the help bar logo
vec![Constraint::Percentage(50), Constraint::Percentage(50)] constraints.push(Constraint::Length(24));
}; }
let vt_chunks = layout::Layout::default() let vt_chunks = layout::Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints(constraints) .constraints(constraints)
@ -186,34 +183,26 @@ impl Layout {
}; };
// right block: preview title + preview // right block: preview title + preview
let preview_constraints = let mut remote_idx = 1;
vec![Constraint::Length(3), Constraint::Min(3)]; let preview_window = if with_preview {
remote_idx += 1;
Some(vt_chunks[1])
} else {
None
};
let right_chunks = layout::Layout::default() let remote_control = if with_remote {
.direction(Direction::Vertical) Some(vt_chunks[remote_idx])
.constraints(match preview_title_position { } else {
PreviewTitlePosition::Bottom => { None
preview_constraints.into_iter().rev().collect()
}
PreviewTitlePosition::Top => preview_constraints,
})
.split(vt_chunks[1]);
let (preview_title, preview_window) = match preview_title_position {
PreviewTitlePosition::Top => (right_chunks[0], right_chunks[1]),
PreviewTitlePosition::Bottom => (right_chunks[1], right_chunks[0]),
}; };
Self::new( Self::new(
help_bar_layout, help_bar_layout,
results, results,
input, input,
preview_title,
preview_window, preview_window,
if with_remote { remote_control,
Some(vt_chunks[2])
} else {
None
},
) )
} }
} }

View File

@ -239,14 +239,18 @@ pub fn build_meta_preview_paragraph<'a>(
Paragraph::new(Text::from(lines)) Paragraph::new(Text::from(lines))
} }
pub fn draw_preview_title_block( #[allow(clippy::too_many_arguments)]
pub fn draw_preview_content_block(
f: &mut Frame, f: &mut Frame,
rect: Rect, rect: Rect,
entry: &Entry,
preview: &Arc<Preview>, preview: &Arc<Preview>,
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
preview_scroll: u16,
use_nerd_font_icons: bool, use_nerd_font_icons: bool,
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
) -> Result<()> { ) -> Result<()> {
let mut preview_title_spans = Vec::new(); let mut preview_title_spans = vec![Span::from(" ")];
if preview.icon.is_some() && use_nerd_font_icons { if preview.icon.is_some() && use_nerd_font_icons {
let icon = preview.icon.as_ref().unwrap(); let icon = preview.icon.as_ref().unwrap();
preview_title_spans.push(Span::styled( preview_title_spans.push(Span::styled(
@ -269,38 +273,18 @@ pub fn draw_preview_title_block(
), ),
Style::default().fg(colorscheme.preview.title_fg).bold(), Style::default().fg(colorscheme.preview.title_fg).bold(),
)); ));
let preview_title = Paragraph::new(Line::from(preview_title_spans)) preview_title_spans.push(Span::from(" "));
.block(
Block::default()
.padding(Padding::horizontal(1))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(
Style::default().fg(colorscheme.general.border_fg),
),
)
.alignment(Alignment::Left)
.style(Style::default().bg(colorscheme.general.background));
f.render_widget(preview_title, rect);
Ok(())
}
pub fn draw_preview_content_block(
f: &mut Frame,
rect: Rect,
entry: &Entry,
preview: &Arc<Preview>,
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
preview_scroll: u16,
colorscheme: &Colorscheme,
) {
let preview_outer_block = Block::default() let preview_outer_block = Block::default()
.title_top(Line::from(" Preview ").alignment(Alignment::Center)) .title_top(
Line::from(preview_title_spans)
.alignment(Alignment::Center)
.style(Style::default().fg(colorscheme.preview.title_fg)),
)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(colorscheme.general.border_fg)) .border_style(Style::default().fg(colorscheme.general.border_fg))
.style(Style::default().bg(colorscheme.general.background)) .style(Style::default().bg(colorscheme.general.background))
.padding(Padding::right(1)); .padding(Padding::new(0, 1, 1, 0));
let preview_inner_block = let preview_inner_block =
Block::default().style(Style::default()).padding(Padding { Block::default().style(Style::default()).padding(Padding {
@ -321,7 +305,7 @@ pub fn draw_preview_content_block(
{ {
let p = preview_paragraph.as_ref().clone(); let p = preview_paragraph.as_ref().clone();
f.render_widget(p.scroll((preview_scroll, 0)), inner); f.render_widget(p.scroll((preview_scroll, 0)), inner);
return; return Ok(());
} }
// If not, render the preview content and cache it if not empty // If not, render the preview content and cache it if not empty
let c_scheme = colorscheme.clone(); let c_scheme = colorscheme.clone();
@ -343,6 +327,7 @@ pub fn draw_preview_content_block(
Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)), Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)),
inner, inner,
); );
Ok(())
} }
fn build_line_number_span<'a>(line_number: usize) -> Span<'a> { fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {

View File

@ -143,9 +143,17 @@ pub fn draw_results_list(
use_nerd_font_icons: bool, use_nerd_font_icons: bool,
icon_color_cache: &mut HashMap<String, Color>, icon_color_cache: &mut HashMap<String, Color>,
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
help_keybinding: &str,
preview_keybinding: &str,
) -> Result<()> { ) -> Result<()> {
let results_block = Block::default() let results_block = Block::default()
.title_top(Line::from(" Results ").alignment(Alignment::Center)) .title_top(Line::from(" Results ").alignment(Alignment::Center))
.title_bottom(
Line::from(format!(
" help: <{help_keybinding}> preview: <{preview_keybinding}> "
))
.alignment(Alignment::Center),
)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(colorscheme.general.border_fg)) .border_style(Style::default().fg(colorscheme.general.border_fg))

View File

@ -93,6 +93,9 @@ pub enum Action {
/// Toggle the help bar. /// Toggle the help bar.
#[serde(alias = "toggle_help")] #[serde(alias = "toggle_help")]
ToggleHelp, ToggleHelp,
/// Toggle the preview panel.
#[serde(alias = "toggle_preview")]
TogglePreview,
/// Signal an error with the given message. /// Signal an error with the given message.
#[serde(skip)] #[serde(skip)]
Error(String), Error(String),

View File

@ -6,16 +6,17 @@ use television_screen::layout::{InputPosition, PreviewTitlePosition};
use super::themes::DEFAULT_THEME; use super::themes::DEFAULT_THEME;
const DEFAULT_UI_SCALE: u16 = 90; const DEFAULT_UI_SCALE: u16 = 100;
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct UiConfig { pub struct UiConfig {
pub use_nerd_font_icons: bool, pub use_nerd_font_icons: bool,
pub ui_scale: u16, pub ui_scale: u16,
pub show_help_bar: bool, pub show_help_bar: bool,
pub show_preview_panel: bool,
#[serde(default)] #[serde(default)]
pub input_bar_position: InputPosition, pub input_bar_position: InputPosition,
pub preview_title_position: PreviewTitlePosition, pub preview_title_position: Option<PreviewTitlePosition>,
pub theme: String, pub theme: String,
} }
@ -24,9 +25,10 @@ impl Default for UiConfig {
Self { Self {
use_nerd_font_icons: false, use_nerd_font_icons: false,
ui_scale: DEFAULT_UI_SCALE, ui_scale: DEFAULT_UI_SCALE,
show_help_bar: true, show_help_bar: false,
input_bar_position: InputPosition::Bottom, show_preview_panel: true,
preview_title_position: PreviewTitlePosition::Top, input_bar_position: InputPosition::Top,
preview_title_position: None,
theme: String::from(DEFAULT_THEME), theme: String::from(DEFAULT_THEME),
} }
} }
@ -47,13 +49,21 @@ impl From<UiConfig> for ValueKind {
String::from("show_help_bar"), String::from("show_help_bar"),
ValueKind::Boolean(val.show_help_bar).into(), ValueKind::Boolean(val.show_help_bar).into(),
); );
m.insert(
String::from("show_preview_panel"),
ValueKind::Boolean(val.show_preview_panel).into(),
);
m.insert( m.insert(
String::from("input_position"), String::from("input_position"),
ValueKind::String(val.input_bar_position.to_string()).into(), ValueKind::String(val.input_bar_position.to_string()).into(),
); );
m.insert( m.insert(
String::from("preview_title_position"), String::from("preview_title_position"),
ValueKind::String(val.preview_title_position.to_string()).into(), match val.preview_title_position {
Some(pos) => ValueKind::String(pos.to_string()),
None => ValueKind::Nil,
}
.into(),
); );
m.insert(String::from("theme"), ValueKind::String(val.theme).into()); m.insert(String::from("theme"), ValueKind::String(val.theme).into());
ValueKind::Table(m) ValueKind::Table(m)

View File

@ -23,9 +23,7 @@ use television_screen::keybindings::{
}; };
use television_screen::layout::{Dimensions, InputPosition, Layout}; use television_screen::layout::{Dimensions, InputPosition, Layout};
use television_screen::mode::Mode; use television_screen::mode::Mode;
use television_screen::preview::{ use television_screen::preview::draw_preview_content_block;
draw_preview_content_block, draw_preview_title_block,
};
use television_screen::remote_control::draw_remote_control; use television_screen::remote_control::draw_remote_control;
use television_screen::results::draw_results_list; use television_screen::results::draw_results_list;
use television_screen::spinner::{Spinner, SpinnerState}; use television_screen::spinner::{Spinner, SpinnerState};
@ -386,6 +384,10 @@ impl Television {
Action::ToggleHelp => { Action::ToggleHelp => {
self.config.ui.show_help_bar = !self.config.ui.show_help_bar; self.config.ui.show_help_bar = !self.config.ui.show_help_bar;
} }
Action::TogglePreview => {
self.config.ui.show_preview_panel =
!self.config.ui.show_preview_panel;
}
_ => {} _ => {}
} }
Ok(None) Ok(None)
@ -405,8 +407,8 @@ impl Television {
area, area,
!matches!(self.mode, Mode::Channel), !matches!(self.mode, Mode::Channel),
self.config.ui.show_help_bar, self.config.ui.show_help_bar,
self.config.ui.show_preview_panel,
self.config.ui.input_bar_position, self.config.ui.input_bar_position,
self.config.ui.preview_title_position,
); );
// help bar (metadata, keymaps, logo) // help bar (metadata, keymaps, logo)
@ -426,7 +428,10 @@ impl Television {
self.results_area_height = self.results_area_height =
u32::from(layout.results.height.saturating_sub(2)); // 2 for the borders u32::from(layout.results.height.saturating_sub(2)); // 2 for the borders
self.preview_pane_height = layout.preview_window.height; self.preview_pane_height = match layout.preview_window {
Some(preview) => preview.height,
None => 0,
};
// results list // results list
let result_count = self.channel.result_count(); let result_count = self.channel.result_count();
@ -447,6 +452,22 @@ impl Television {
self.config.ui.use_nerd_font_icons, self.config.ui.use_nerd_font_icons,
&mut self.icon_color_cache, &mut self.icon_color_cache,
&self.colorscheme, &self.colorscheme,
&self
.config
.keybindings
.get(&self.mode)
.unwrap()
.get(&Action::ToggleHelp)
.unwrap()
.to_string(),
&self
.config
.keybindings
.get(&self.mode)
.unwrap()
.get(&Action::TogglePreview)
.unwrap()
.to_string(),
)?; )?;
// input box // input box
@ -463,38 +484,32 @@ impl Television {
&self.colorscheme, &self.colorscheme,
)?; )?;
let selected_entry = self if self.config.ui.show_preview_panel {
.get_selected_entry(Some(Mode::Channel)) let selected_entry = self
.unwrap_or(ENTRY_PLACEHOLDER); .get_selected_entry(Some(Mode::Channel))
let preview = self.previewer.preview(&selected_entry); .unwrap_or(ENTRY_PLACEHOLDER);
// preview title // preview content
self.current_preview_total_lines = preview.total_lines(); let preview = self.previewer.preview(&selected_entry);
draw_preview_title_block( self.current_preview_total_lines = preview.total_lines();
f, // initialize preview scroll
layout.preview_title, self.maybe_init_preview_scroll(
&preview, selected_entry
self.config.ui.use_nerd_font_icons, .line_number
&self.colorscheme, .map(|l| u16::try_from(l).unwrap_or(0)),
)?; layout.preview_window.unwrap().height,
);
// preview content draw_preview_content_block(
// initialize preview scroll f,
self.maybe_init_preview_scroll( layout.preview_window.unwrap(),
selected_entry &selected_entry,
.line_number &preview,
.map(|l| u16::try_from(l).unwrap_or(0)), &self.rendered_preview_cache,
layout.preview_window.height, self.preview_scroll.unwrap_or(0),
); self.config.ui.use_nerd_font_icons,
draw_preview_content_block( &self.colorscheme,
f, )?;
layout.preview_window, }
&selected_entry,
&preview,
&self.rendered_preview_cache,
self.preview_scroll.unwrap_or(0),
&self.colorscheme,
);
// remote control // remote control
if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) { if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) {