top menus

This commit is contained in:
alexpasmantier 2024-10-25 23:29:07 +02:00
parent 4bec64e245
commit 9eea37a5b5
9 changed files with 327 additions and 155 deletions

View File

@ -4,9 +4,7 @@ down = "SelectNextEntry"
up = "SelectPrevEntry"
ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
alt-down = "ScrollPreviewHalfPageDown"
ctrl-d = "ScrollPreviewHalfPageDown"
alt-up = "ScrollPreviewHalfPageUp"
ctrl-u = "ScrollPreviewHalfPageUp"
enter = "SelectEntry"
ctrl-enter = "SendToChannel"
@ -20,4 +18,3 @@ ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
enter = "SelectEntry"
ctrl-s = "ToggleChannelSelection"

View File

@ -8,10 +8,10 @@
_______________
|,----------. |\
|| |=| |
|| || | |
|| . _o| | |
|| | | |
|| |o| |
|`-----------' |/
~~~~~~~~~~~~~~~
`--------------'
__ __ _ _
/ /____ / /__ _ __(_)__ (_)__ ___
/ __/ -_) / -_) |/ / (_-</ / _ \/ _ \

View File

@ -14,12 +14,15 @@ use ratatui::{
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, str::FromStr};
use strum::Display;
use tokio::sync::mpsc::UnboundedSender;
use crate::ui::input::Input;
use crate::ui::layout::{Dimensions, Layout};
use crate::ui::preview::DEFAULT_PREVIEW_TITLE_FG;
use crate::ui::results::build_results_list;
use crate::ui::{
layout::{Dimensions, Layout},
logo::build_logo_paragraph,
};
use crate::utils::strings::EMPTY_STRING;
use crate::{action::Action, config::Config};
use crate::{channels::tv_guide::TvGuide, ui::get_border_style};
@ -27,13 +30,16 @@ use crate::{channels::OnAir, utils::strings::shrink_with_ellipsis};
use crate::{
channels::TelevisionChannel, ui::input::actions::InputActionHandler,
};
use crate::{channels::UnitChannel, ui::input::Input};
use crate::{
entry::{Entry, ENTRY_PLACEHOLDER},
ui::spinner::Spinner,
};
use crate::{previewers::Previewer, ui::spinner::SpinnerState};
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
#[derive(
PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize, Display,
)]
pub enum Mode {
Channel,
Guide,
@ -98,6 +104,10 @@ impl Television {
}
}
pub fn current_channel(&self) -> UnitChannel {
UnitChannel::from(&self.channel)
}
/// FIXME: this needs rework
pub fn change_channel(&mut self, channel: TelevisionChannel) {
self.reset_preview_scroll();
@ -304,19 +314,17 @@ impl Television {
Action::ScrollPreviewUp => self.scroll_preview_up(1),
Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20),
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
Action::ToggleChannelSelection => {
match self.mode {
Mode::Channel => {
self.reset_screen();
self.mode = Mode::Guide;
}
Mode::Guide => {
self.reset_screen();
self.mode = Mode::Channel;
}
Mode::SendToChannel => {}
Action::ToggleChannelSelection => match self.mode {
Mode::Channel => {
self.reset_screen();
self.mode = Mode::Guide;
}
}
Mode::Guide => {
self.reset_screen();
self.mode = Mode::Channel;
}
Mode::SendToChannel => {}
},
Action::SelectEntry => {
if let Some(entry) = self.get_selected_entry() {
match self.mode {
@ -387,19 +395,38 @@ impl Television {
},
);
let help_block = Block::default()
.borders(Borders::NONE)
let metadata_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Blue))
.padding(Padding::horizontal(1))
.style(Style::default());
let metadata_table = self.build_metadata_table().block(metadata_block);
f.render_widget(metadata_table, layout.help_bar_left);
let keymaps_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Blue))
.style(Style::default())
.padding(Padding::uniform(1));
.padding(Padding::horizontal(1));
let help_text = self
.build_help_paragraph()?
.style(Style::default().fg(Color::DarkGray).italic())
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.block(help_block);
let keymaps_table = self.build_help_table()?.block(keymaps_block);
f.render_widget(help_text, layout.help_bar);
f.render_widget(keymaps_table, layout.help_bar_middle);
let logo_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Blue))
.style(Style::default().fg(Color::Yellow))
.padding(Padding::horizontal(1));
let logo_paragraph = build_logo_paragraph().block(logo_block);
f.render_widget(logo_paragraph, layout.help_bar_right);
self.results_area_height = u32::from(layout.results.height);
if let Some(preview_window) = layout.preview_window {
@ -489,7 +516,7 @@ impl Television {
"> ",
Style::default().fg(DEFAULT_INPUT_FG).bold(),
))
.block(arrow_block);
.block(arrow_block);
f.render_widget(arrow, inner_input_chunks[0]);
let interactive_input_block = Block::default();
@ -527,8 +554,8 @@ impl Television {
),
Style::default().fg(DEFAULT_RESULTS_COUNT_FG).italic(),
))
.block(result_count_block)
.alignment(Alignment::Right);
.block(result_count_block)
.alignment(Alignment::Right);
f.render_widget(result_count_paragraph, inner_input_chunks[2]);
// Make the cursor visible and ask tui-rs to put it at the
@ -537,8 +564,8 @@ impl Television {
// Put cursor past the end of the input text
inner_input_chunks[1].x
+ u16::try_from(
self.input.visual_cursor().max(scroll) - scroll,
)?,
self.input.visual_cursor().max(scroll) - scroll,
)?,
// Move one line down, from the border to the input line
inner_input_chunks[1].y,
));

View File

@ -3,6 +3,8 @@ use ratatui::style::{Color, Style, Stylize};
pub mod help;
pub mod input;
pub mod layout;
pub mod logo;
pub mod metadata;
pub mod preview;
pub mod results;
pub mod spinner;

View File

@ -1,8 +1,9 @@
use color_eyre::eyre::{OptionExt, Result};
use ratatui::{
layout::Constraint,
style::{Color, Style},
text::{Line, Span},
widgets::Paragraph,
widgets::{Cell, Row, Table},
};
use std::collections::HashMap;
@ -12,142 +13,118 @@ use crate::{
television::{Mode, Television},
};
const SEPARATOR: &str = " ";
const ACTION_COLOR: Color = Color::DarkGray;
const KEY_COLOR: Color = Color::LightYellow;
impl Television {
pub fn build_help_paragraph<'a>(&self) -> Result<Paragraph<'a>> {
pub fn build_help_table<'a>(&self) -> Result<Table<'a>> {
match self.mode {
Mode::Channel => self.build_help_paragraph_for_channel(),
Mode::Guide => self.build_help_paragraph_for_channel_selection(),
Mode::SendToChannel => self.build_help_paragraph_for_channel(),
Mode::Channel => self.build_help_table_for_channel(),
Mode::Guide => self.build_help_table_for_channel_selection(),
Mode::SendToChannel => self.build_help_table_for_channel(),
}
}
fn build_help_paragraph_for_channel<'a>(&self) -> Result<Paragraph<'a>> {
fn build_help_table_for_channel<'a>(&self) -> Result<Table<'a>> {
let keymap = self.keymap_for_mode()?;
let mut lines = Vec::new();
// NAVIGATION and SELECTION line
let mut ns_line = Line::default();
// Results navigation
let prev = keys_for_action(keymap, Action::SelectPrevEntry);
let next = keys_for_action(keymap, Action::SelectNextEntry);
let results_spans =
build_spans_for_key_groups("↕ Results", vec![prev, next]);
ns_line.extend(results_spans);
ns_line.push_span(Span::styled(SEPARATOR, Style::default()));
let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
let next = keys_for_action(keymap, &Action::SelectNextEntry);
let results_row = Row::new(build_cells_for_key_groups(
"↕ Results navigation",
vec![prev, next],
));
// Preview navigation
let up_keys = keys_for_action(keymap, Action::ScrollPreviewHalfPageUp);
let up_keys =
keys_for_action(keymap, &Action::ScrollPreviewHalfPageUp);
let down_keys =
keys_for_action(keymap, Action::ScrollPreviewHalfPageDown);
let preview_spans =
build_spans_for_key_groups("↕ Preview", vec![up_keys, down_keys]);
ns_line.extend(preview_spans);
ns_line.push_span(Span::styled(SEPARATOR, Style::default()));
keys_for_action(keymap, &Action::ScrollPreviewHalfPageDown);
let preview_row = Row::new(build_cells_for_key_groups(
"↕ Preview navigation",
vec![up_keys, down_keys],
));
// Select entry
let select_entry_keys = keys_for_action(keymap, Action::SelectEntry);
let select_entry_spans = build_spans_for_key_groups(
"Select entry",
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
let select_entry_row = Row::new(build_cells_for_key_groups(
"Select entry",
vec![select_entry_keys],
);
ns_line.extend(select_entry_spans);
ns_line.push_span(Span::styled(SEPARATOR, Style::default()));
));
// Send to channel
let send_to_channel_keys =
keys_for_action(keymap, Action::SendToChannel);
// TODO: add send icon
let send_to_channel_spans =
build_spans_for_key_groups("Send to", vec![send_to_channel_keys]);
ns_line.extend(send_to_channel_spans);
ns_line.push_span(Span::styled(SEPARATOR, Style::default()));
keys_for_action(keymap, &Action::SendToChannel);
let send_to_channel_row = Row::new(build_cells_for_key_groups(
"⇉ Send results to",
vec![send_to_channel_keys],
));
// Switch channels
let switch_channels_keys =
keys_for_action(keymap, Action::ToggleChannelSelection);
let switch_channels_spans = build_spans_for_key_groups(
"Switch channels",
keys_for_action(keymap, &Action::ToggleChannelSelection);
let switch_channels_row = Row::new(build_cells_for_key_groups(
"Switch channels",
vec![switch_channels_keys],
);
ns_line.extend(switch_channels_spans);
lines.push(ns_line);
));
// MISC line (quit, help, etc.)
// let mut misc_line = Line::default();
//
// // Quit
// let quit_keys = keys_for_action(keymap, Action::Quit);
// let quit_spans = build_spans_for_key_groups("Quit", vec![quit_keys]);
//
// misc_line.extend(quit_spans);
//
// lines.push(misc_line);
// Quit ⏼
let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row =
Row::new(build_cells_for_key_groups("⏼ Quit", vec![quit_keys]));
Ok(Paragraph::new(lines))
let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
Ok(Table::new(
vec![
results_row,
preview_row,
select_entry_row,
send_to_channel_row,
switch_channels_row,
quit_row,
],
widths,
))
}
fn build_help_paragraph_for_channel_selection<'a>(
&self,
) -> Result<Paragraph<'a>> {
fn build_help_table_for_channel_selection<'a>(&self) -> Result<Table<'a>> {
let keymap = self.keymap_for_mode()?;
let mut lines = Vec::new();
// NAVIGATION + SELECTION line
let mut ns_line = Line::default();
// Results navigation
let prev = keys_for_action(keymap, Action::SelectPrevEntry);
let next = keys_for_action(keymap, Action::SelectNextEntry);
let results_spans =
build_spans_for_key_groups("↕ Results", vec![prev, next]);
ns_line.extend(results_spans);
ns_line.push_span(Span::styled(SEPARATOR, Style::default()));
let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
let next = keys_for_action(keymap, &Action::SelectNextEntry);
let results_row = Row::new(build_cells_for_key_groups(
"↕ Results",
vec![prev, next],
));
// Select entry
let select_entry_keys = keys_for_action(keymap, Action::SelectEntry);
let select_entry_spans = build_spans_for_key_groups(
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
let select_entry_row = Row::new(build_cells_for_key_groups(
"Select entry",
vec![select_entry_keys],
);
ns_line.extend(select_entry_spans);
ns_line.push_span(Span::styled(SEPARATOR, Style::default()));
));
// Switch channels
let switch_channels_keys =
keys_for_action(keymap, Action::ToggleChannelSelection);
let switch_channels_spans = build_spans_for_key_groups(
keys_for_action(keymap, &Action::ToggleChannelSelection);
let switch_channels_row = Row::new(build_cells_for_key_groups(
"Switch channels",
vec![switch_channels_keys],
);
ns_line.extend(switch_channels_spans);
lines.push(ns_line);
// MISC line (quit, help, etc.)
// let mut misc_line = Line::default();
));
// Quit
// let quit_keys = keys_for_action(keymap, Action::Quit);
// let quit_spans = build_spans_for_key_groups("Quit", vec![quit_keys]);
let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row =
Row::new(build_cells_for_key_groups("Quit", vec![quit_keys]));
// misc_line.extend(quit_spans);
// lines.push(misc_line);
Ok(Paragraph::new(lines))
Ok(Table::new(
vec![results_row, select_entry_row, switch_channels_row, quit_row],
vec![Constraint::Fill(1), Constraint::Fill(2)],
))
}
/// Get the keymap for the current mode.
@ -189,21 +166,23 @@ impl Television {
///
/// assert_eq!(spans.len(), 5);
/// ```
fn build_spans_for_key_groups(
fn build_cells_for_key_groups(
group_name: &str,
key_groups: Vec<Vec<String>>,
) -> Vec<Span> {
if key_groups.is_empty() || key_groups.iter().all(|keys| keys.is_empty()) {
return vec![];
) -> Vec<Cell> {
if key_groups.is_empty() || key_groups.iter().all(std::vec::Vec::is_empty)
{
return vec![group_name.into(), "No keybindings".into()];
}
let non_empty_groups = key_groups.iter().filter(|keys| !keys.is_empty());
let mut spans = vec![
Span::styled(
group_name.to_owned() + ": ",
Style::default().fg(ACTION_COLOR),
),
Span::styled("[", Style::default().fg(KEY_COLOR)),
];
let mut cells = vec![Cell::from(Span::styled(
group_name.to_owned() + ": ",
Style::default().fg(ACTION_COLOR),
))];
let mut spans = Vec::new();
//spans.push(Span::styled("[", Style::default().fg(KEY_COLOR)));
let key_group_spans: Vec<Span> = non_empty_groups
.map(|keys| {
let key_group = keys.join(", ");
@ -213,12 +192,14 @@ fn build_spans_for_key_groups(
key_group_spans.iter().enumerate().for_each(|(i, span)| {
spans.push(span.clone());
if i < key_group_spans.len() - 1 {
spans.push(Span::styled(" | ", Style::default().fg(KEY_COLOR)));
spans.push(Span::styled(" / ", Style::default().fg(KEY_COLOR)));
}
});
spans.push(Span::styled("]", Style::default().fg(KEY_COLOR)));
spans
//spans.push(Span::styled("]", Style::default().fg(KEY_COLOR)));
cells.push(Cell::from(Line::from(spans)));
cells
}
/// Get the keys for a given action.
@ -246,11 +227,11 @@ fn build_spans_for_key_groups(
/// ```
fn keys_for_action(
keymap: &HashMap<Key, Action>,
action: Action,
action: &Action,
) -> Vec<String> {
keymap
.iter()
.filter(|(_key, act)| **act == action)
.filter(|(_key, act)| *act == action)
.map(|(key, _act)| format!("{key}"))
.collect()
}

View File

@ -19,7 +19,9 @@ impl Default for Dimensions {
}
pub struct Layout {
pub help_bar: Rect,
pub help_bar_left: Rect,
pub help_bar_middle: Rect,
pub help_bar_right: Rect,
pub results: Rect,
pub input: Rect,
pub preview_title: Option<Rect>,
@ -28,14 +30,18 @@ pub struct Layout {
impl Layout {
pub fn new(
help_bar: Rect,
help_bar_left: Rect,
help_bar_middle: Rect,
help_bar_right: Rect,
results: Rect,
input: Rect,
preview_title: Option<Rect>,
preview_window: Option<Rect>,
) -> Self {
Self {
help_bar,
help_bar_left,
help_bar_middle,
help_bar_right,
results,
input,
preview_title,
@ -52,9 +58,19 @@ impl Layout {
// split the main block into two vertical chunks (help bar + rest)
let hz_chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(1), Constraint::Length(5)])
.constraints([Constraint::Length(9), Constraint::Fill(1)])
.split(main_block);
// split the help bar into three horizontal chunks (left + center + right)
let help_bar_chunks = layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Length(24),
])
.split(hz_chunks[0]);
if with_preview {
// split the main block into two vertical chunks
let vt_chunks = layout::Layout::default()
@ -63,7 +79,7 @@ impl Layout {
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(hz_chunks[0]);
.split(hz_chunks[1]);
// left block: results + input field
let left_chunks = layout::Layout::default()
@ -78,7 +94,9 @@ impl Layout {
.split(vt_chunks[1]);
Self::new(
hz_chunks[1],
help_bar_chunks[0],
help_bar_chunks[1],
help_bar_chunks[2],
left_chunks[0],
left_chunks[1],
Some(right_chunks[0]),
@ -89,9 +107,17 @@ impl Layout {
let chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(3)])
.split(hz_chunks[0]);
.split(hz_chunks[1]);
Self::new(hz_chunks[1], chunks[0], chunks[1], None, None)
Self::new(
help_bar_chunks[0],
help_bar_chunks[1],
help_bar_chunks[2],
chunks[0],
chunks[1],
None,
None,
)
}
}
}

View File

@ -0,0 +1,26 @@
use ratatui::widgets::Paragraph;
//const LOGO: &str = r" _______________
// __ __ _ _ |,----------. |\
// / /____ / /__ _ __(_)__ (_)__ ___ || |=| |
// / __/ -_) / -_) |/ / (_-</ / _ \/ _ \ || | | |
// \__/\__/_/\__/|___/_/___/_/\___/_//_/ || |o| |
// |`-----------' |/
// `--------------'";
const LOGO: &str = r" _______________
|,----------. |\
|| |=| |
|| | | |
|| |o| |
|`-----------' |/
`--------------'";
pub fn build_logo_paragraph<'a>() -> Paragraph<'a> {
let lines = LOGO
.lines()
.map(std::convert::Into::into)
.collect::<Vec<_>>();
let logo_paragraph = Paragraph::new(lines);
logo_paragraph
}

View File

@ -0,0 +1,113 @@
use ratatui::{
layout::Constraint,
style::{Color, Style},
text::Span,
widgets::{Cell, Row, Table},
};
use crate::television::Television;
// television 0.1.6
// target triple: aarch64-apple-darwin
// build: 1.82.0 (2024-10-24)
// current_channel: git_repos
// current_mode: channel
impl Television {
pub fn build_metadata_table<'a>(&self) -> Table<'a> {
let version_row = Row::new(vec![
Cell::from(Span::styled(
"version: ",
Style::default().fg(Color::DarkGray),
)),
Cell::from(Span::styled(
env!("CARGO_PKG_VERSION"),
Style::default().fg(Color::LightYellow),
)),
]);
let target_triple_row = Row::new(vec![
Cell::from(Span::styled(
"target triple: ",
Style::default().fg(Color::DarkGray),
)),
Cell::from(Span::styled(
env!("VERGEN_CARGO_TARGET_TRIPLE"),
Style::default().fg(Color::LightYellow),
)),
]);
let build_row = Row::new(vec![
Cell::from(Span::styled(
"build: ",
Style::default().fg(Color::DarkGray),
)),
Cell::from(Span::styled(
env!("VERGEN_RUSTC_SEMVER"),
Style::default().fg(Color::LightYellow),
)),
Cell::from(Span::styled(
" (",
Style::default().fg(Color::DarkGray),
)),
Cell::from(Span::styled(
env!("VERGEN_BUILD_DATE"),
Style::default().fg(Color::LightYellow),
)),
Cell::from(Span::styled(
")",
Style::default().fg(Color::DarkGray),
)),
]);
let current_dir_row = Row::new(vec![
Cell::from(Span::styled(
"current directory: ",
Style::default().fg(Color::DarkGray),
)),
Cell::from(Span::styled(
std::env::current_dir()
.expect("Could not get current directory")
.display()
.to_string(),
Style::default().fg(Color::LightYellow),
)),
]);
let current_channel_row = Row::new(vec![
Cell::from(Span::styled(
"current channel: ",
Style::default().fg(Color::DarkGray),
)),
Cell::from(Span::styled(
self.current_channel().to_string(),
Style::default().fg(Color::LightYellow),
)),
]);
let current_mode_row = Row::new(vec![
Cell::from(Span::styled(
"current mode: ",
Style::default().fg(Color::DarkGray),
)),
Cell::from(Span::styled(
self.mode.to_string(),
Style::default().fg(Color::LightYellow),
)),
]);
let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
Table::new(
vec![
version_row,
target_triple_row,
build_row,
current_dir_row,
current_channel_row,
current_mode_row,
],
widths,
)
}
}

View File

@ -224,7 +224,7 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
// Generate a unit enum from the given enum
let unit_enum = quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum UnitChannel {
#(
#variant_names,