mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-28 13:51:41 +00:00
feat(remote): rework remote UI and add description and requirements panels
This commit is contained in:
parent
a2ebbb3557
commit
7067a2ba93
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -623,6 +623,12 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@ -1953,6 +1959,7 @@ dependencies = [
|
||||
"ureq",
|
||||
"vt100",
|
||||
"walkdir",
|
||||
"which",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
@ -2453,6 +2460,17 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
|
||||
dependencies = [
|
||||
"env_home",
|
||||
"rustix 1.0.5",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -2643,6 +2661,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
|
@ -59,6 +59,7 @@ ureq = "3.0.11"
|
||||
serde_json = "1.0.140"
|
||||
colored = "3.0.0"
|
||||
serde_with = "3.13.0"
|
||||
which = "8.0.0"
|
||||
|
||||
|
||||
# target specific dependencies
|
||||
|
@ -2,6 +2,7 @@
|
||||
[metadata]
|
||||
name = "bash-history"
|
||||
description = "A channel to select from your bash history"
|
||||
requirements = ["bash"]
|
||||
|
||||
[source]
|
||||
command = "sed '1!G;h;$!d' ${HISTFILE:-${HOME}/.bash_history}"
|
||||
|
@ -2,7 +2,7 @@
|
||||
[metadata]
|
||||
name = "docker-images"
|
||||
description = "A channel to select from Docker images"
|
||||
requirements = ["docker"]
|
||||
requirements = ["docker", "jq"]
|
||||
|
||||
[source]
|
||||
command = "docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}'"
|
||||
|
@ -2,6 +2,7 @@
|
||||
[metadata]
|
||||
name = "fish-history"
|
||||
description = "A channel to select from your fish history"
|
||||
requirements = ["fish"]
|
||||
|
||||
[source]
|
||||
command = "fish -c 'history'"
|
||||
|
@ -1,6 +1,7 @@
|
||||
[metadata]
|
||||
name = "git-branch"
|
||||
description = "A channel to select from git branches"
|
||||
requirements = ["git"]
|
||||
|
||||
[source]
|
||||
command = "git --no-pager branch --all --format=\"%(refname:short)\""
|
||||
|
@ -1,6 +1,7 @@
|
||||
[metadata]
|
||||
name = "git-diff"
|
||||
description = "A channel to select files from git diff commands"
|
||||
requirements = ["git"]
|
||||
|
||||
[source]
|
||||
command = "git diff --name-only HEAD"
|
||||
|
@ -1,6 +1,7 @@
|
||||
[metadata]
|
||||
name = "git-log"
|
||||
description = "A channel to select from git log entries"
|
||||
requirements = ["git"]
|
||||
|
||||
[source]
|
||||
command = "git log --oneline --date=short --pretty=\"format:%h %s %an %cd\" \"$@\""
|
||||
|
@ -1,6 +1,7 @@
|
||||
[metadata]
|
||||
name = "git-reflog"
|
||||
description = "A channel to select from git reflog entries"
|
||||
requirements = ["git"]
|
||||
|
||||
[source]
|
||||
command = "git reflog"
|
||||
|
@ -1,9 +1,14 @@
|
||||
[metadata]
|
||||
name = "git-repos"
|
||||
description = "A channel to select from git repositories on your local machine"
|
||||
requirements = ["fd", "git"]
|
||||
description = """
|
||||
A channel to select from git repositories on your local machine.
|
||||
|
||||
This channel uses `fd` to find directories that contain a `.git` subdirectory, and then allows you to preview the git log of the selected repository.
|
||||
"""
|
||||
|
||||
[source]
|
||||
# this is a macos version of the command. While perfectly usable on linux, you may want to tweak it a bit.
|
||||
command = "fd -g .git -HL -t d -d 10 --prune ~ -E 'Library' -E 'Application Support' --exec dirname {}"
|
||||
display = "{split:/:-1}"
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
[metadata]
|
||||
name = "zsh-history"
|
||||
description = "A channel to select from your zsh history"
|
||||
requirements = ["zsh"]
|
||||
|
||||
[source]
|
||||
command = "sed '1!G;h;$!d' ${HISTFILE:-${HOME}/.zsh_history}"
|
||||
|
@ -145,9 +145,10 @@ where
|
||||
Some(prototype)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to parse cable channel file {:?}: {}",
|
||||
p, e
|
||||
eprintln!(
|
||||
"Failed to parse cable channel file {}: {}",
|
||||
p.display(),
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use which::which;
|
||||
|
||||
use crate::config::Binding;
|
||||
use crate::{
|
||||
@ -161,6 +162,12 @@ pub struct ChannelKeyBindings {
|
||||
pub bindings: KeyBindings,
|
||||
}
|
||||
|
||||
impl ChannelKeyBindings {
|
||||
pub fn channel_shortcut(&self) -> Option<&Binding> {
|
||||
self.shortcut.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ChannelPrototype {
|
||||
pub metadata: Metadata,
|
||||
@ -247,7 +254,39 @@ pub struct Metadata {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
requirements: Vec<String>,
|
||||
pub requirements: Vec<BinaryRequirement>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct BinaryRequirement {
|
||||
pub bin_name: String,
|
||||
#[serde(skip)]
|
||||
met: bool,
|
||||
}
|
||||
|
||||
impl BinaryRequirement {
|
||||
pub fn new(bin_name: &str) -> Self {
|
||||
Self {
|
||||
bin_name: bin_name.to_string(),
|
||||
met: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the required binary is available in the system's PATH.
|
||||
///
|
||||
/// This method updates the requirement's state in place to reflect whether the binary was
|
||||
/// found.
|
||||
pub fn init(&mut self) {
|
||||
self.met = which(&self.bin_name).is_ok();
|
||||
}
|
||||
|
||||
/// Whether the requirement is available in the system's PATH.
|
||||
///
|
||||
/// This should be called after `init()`.
|
||||
pub fn is_met(&self) -> bool {
|
||||
self.met
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
|
@ -1,6 +1,9 @@
|
||||
use crate::{
|
||||
cable::Cable,
|
||||
channels::{entry::into_ranges, prototypes::ChannelPrototype},
|
||||
channels::{
|
||||
entry::into_ranges,
|
||||
prototypes::{BinaryRequirement, ChannelPrototype},
|
||||
},
|
||||
config::Binding,
|
||||
matcher::{Matcher, config::Config},
|
||||
screen::result_item::ResultItem,
|
||||
@ -12,14 +15,18 @@ pub struct CableEntry {
|
||||
pub channel_name: String,
|
||||
pub match_ranges: Option<Vec<(u32, u32)>>,
|
||||
pub shortcut: Option<Binding>,
|
||||
pub description: Option<String>,
|
||||
pub requirements: Vec<BinaryRequirement>,
|
||||
}
|
||||
|
||||
impl CableEntry {
|
||||
pub fn new(name: String, shortcut: Option<Binding>) -> Self {
|
||||
pub fn new(name: String, shortcut: Option<&Binding>) -> Self {
|
||||
CableEntry {
|
||||
channel_name: name,
|
||||
match_ranges: None,
|
||||
shortcut,
|
||||
shortcut: shortcut.cloned(),
|
||||
description: None,
|
||||
requirements: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +34,19 @@ impl CableEntry {
|
||||
self.match_ranges = Some(into_ranges(indices));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: Option<String>) -> Self {
|
||||
self.description = description;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_requirements(
|
||||
mut self,
|
||||
requirements: Vec<BinaryRequirement>,
|
||||
) -> Self {
|
||||
self.requirements = requirements;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ResultItem for CableEntry {
|
||||
@ -49,7 +69,7 @@ impl ResultItem for CableEntry {
|
||||
}
|
||||
|
||||
pub struct RemoteControl {
|
||||
matcher: Matcher<String>,
|
||||
matcher: Matcher<CableEntry>,
|
||||
pub cable_channels: Cable,
|
||||
}
|
||||
|
||||
@ -60,9 +80,30 @@ impl RemoteControl {
|
||||
let matcher =
|
||||
Matcher::new(&Config::default().n_threads(Some(NUM_THREADS)));
|
||||
let injector = matcher.injector();
|
||||
for c in cable_channels.keys() {
|
||||
let () = injector.push(c.clone(), |e, cols| {
|
||||
cols[0] = e.to_string().into();
|
||||
for (channel_name, prototype) in cable_channels.iter() {
|
||||
let channel_shortcut = prototype
|
||||
.keybindings
|
||||
.as_ref()
|
||||
.and_then(|kb| kb.channel_shortcut());
|
||||
let cable_entry =
|
||||
CableEntry::new(channel_name.to_string(), channel_shortcut)
|
||||
.with_description(prototype.metadata.description.clone())
|
||||
.with_requirements(
|
||||
// check if the prototype has binary requirements
|
||||
// and whether they are met
|
||||
prototype
|
||||
.metadata
|
||||
.requirements
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut r| {
|
||||
r.init();
|
||||
r
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let () = injector.push(cable_entry, |e, cols| {
|
||||
cols[0] = e.channel_name.clone().into();
|
||||
});
|
||||
}
|
||||
RemoteControl {
|
||||
@ -95,25 +136,13 @@ impl RemoteControl {
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
CableEntry::new(
|
||||
item.matched_string.clone(),
|
||||
self.cable_channels
|
||||
.get_channel_shortcut(&item.matched_string),
|
||||
)
|
||||
.with_match_indices(&item.match_indices)
|
||||
})
|
||||
.map(|item| item.inner.with_match_indices(&item.match_indices))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_result(&self, index: u32) -> CableEntry {
|
||||
let item = self.matcher.get_result(index).expect("Invalid index");
|
||||
CableEntry::new(
|
||||
item.matched_string.clone(),
|
||||
self.cable_channels
|
||||
.get_channel_shortcut(&item.matched_string),
|
||||
)
|
||||
.with_match_indices(&item.match_indices)
|
||||
item.inner.with_match_indices(&item.match_indices)
|
||||
}
|
||||
|
||||
pub fn result_count(&self) -> u32 {
|
||||
|
@ -270,7 +270,6 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
|
||||
ctx.config.ui.use_nerd_font_icons,
|
||||
&mut ctx.tv_state.rc_picker.state.clone(),
|
||||
&mut ctx.tv_state.rc_picker.input.clone(),
|
||||
&ctx.tv_state.mode,
|
||||
&ctx.colorscheme,
|
||||
)?;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::UiConfig;
|
||||
use crate::screen::constants::LOGO_WIDTH;
|
||||
use crate::screen::logo::REMOTE_LOGO_HEIGHT_U16;
|
||||
|
||||
pub struct Dimensions {
|
||||
pub x: u16,
|
||||
@ -110,6 +111,8 @@ pub struct Layout {
|
||||
pub remote_control: Option<Rect>,
|
||||
}
|
||||
|
||||
const REMOTE_PANEL_WIDTH_PERCENTAGE: u16 = 62;
|
||||
|
||||
impl Default for Layout {
|
||||
/// Having a default layout with a non-zero height for the results area
|
||||
/// is important for the initial rendering of the application. For the first
|
||||
@ -182,22 +185,6 @@ impl Layout {
|
||||
help_bar_layout = None;
|
||||
}
|
||||
|
||||
let remote_constraints = if show_remote {
|
||||
vec![Constraint::Fill(1), Constraint::Length(LOGO_WIDTH)]
|
||||
} else {
|
||||
vec![Constraint::Fill(1)]
|
||||
};
|
||||
let remote_chunks = layout::Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(remote_constraints)
|
||||
.split(main_rect);
|
||||
|
||||
let remote_control = if show_remote {
|
||||
Some(remote_chunks[1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Define the constraints for the results area (results list + input bar).
|
||||
// We keep this near the top so we can derive the input-bar height before
|
||||
// calculating the preview/results split.
|
||||
@ -226,7 +213,7 @@ impl Layout {
|
||||
if ui_config.orientation == Orientation::Portrait
|
||||
&& input_bar_height > 0
|
||||
{
|
||||
let total_height = remote_chunks[0].height;
|
||||
let total_height = main_rect.height;
|
||||
if total_height > input_bar_height {
|
||||
let available_height = total_height - input_bar_height;
|
||||
preview_percentage = raw_preview_percentage
|
||||
@ -266,7 +253,7 @@ impl Layout {
|
||||
Orientation::Landscape => Direction::Horizontal,
|
||||
})
|
||||
.constraints(constraints)
|
||||
.split(remote_chunks[0]);
|
||||
.split(main_rect);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Determine the rectangles for input, results list and optional preview
|
||||
@ -387,19 +374,33 @@ impl Layout {
|
||||
}
|
||||
|
||||
// Perform the split now that we have the final constraints vector
|
||||
let port_chunks = layout::Layout::default()
|
||||
let portrait_chunks = layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(portrait_constraints)
|
||||
.split(remote_chunks[0]);
|
||||
.split(main_rect);
|
||||
|
||||
let input_rect = port_chunks[input_idx];
|
||||
let results_rect = port_chunks[results_idx];
|
||||
let preview_rect = preview_idx.map(|idx| port_chunks[idx]);
|
||||
let input_rect = portrait_chunks[input_idx];
|
||||
let results_rect = portrait_chunks[results_idx];
|
||||
let preview_rect = preview_idx.map(|idx| portrait_chunks[idx]);
|
||||
|
||||
(input_rect, results_rect, preview_rect)
|
||||
}
|
||||
};
|
||||
|
||||
// the remote control is a centered popup
|
||||
let remote_control = if show_remote {
|
||||
let remote_control_rect = centered_rect_with_dimensions(
|
||||
&Dimensions::new(
|
||||
area.width * REMOTE_PANEL_WIDTH_PERCENTAGE / 100,
|
||||
REMOTE_LOGO_HEIGHT_U16,
|
||||
),
|
||||
area,
|
||||
);
|
||||
Some(remote_control_rect)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self::new(
|
||||
help_bar_layout,
|
||||
results,
|
||||
@ -412,13 +413,20 @@ impl Layout {
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let height = r.height.saturating_mul(percent_y) / 100;
|
||||
let width = r.width.saturating_mul(percent_x) / 100;
|
||||
|
||||
centered_rect_with_dimensions(&Dimensions::new(width, height), r)
|
||||
}
|
||||
|
||||
fn centered_rect_with_dimensions(dimensions: &Dimensions, r: Rect) -> Rect {
|
||||
// Cut the given rectangle into three vertical pieces
|
||||
let popup_layout = layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(dimensions.y),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(r);
|
||||
|
||||
@ -426,9 +434,9 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
layout::Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(dimensions.x),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(popup_layout[1])[1] // Return the middle chunk
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ pub fn build_logo_paragraph<'a>() -> Paragraph<'a> {
|
||||
Paragraph::new(lines)
|
||||
}
|
||||
|
||||
const REMOTE_LOGO: &str = r"
|
||||
pub const REMOTE_LOGO: &str = r"
|
||||
_____________
|
||||
/ \
|
||||
| (*) (#) |
|
||||
@ -32,18 +32,24 @@ const REMOTE_LOGO: &str = r"
|
||||
| (1) (2) (3) |
|
||||
| (4) (5) (6) |
|
||||
| (7) (8) (9) |
|
||||
| |
|
||||
| _ |
|
||||
| | | |
|
||||
| (_¯(0)¯_) |
|
||||
| | | |
|
||||
| ¯ |
|
||||
| |
|
||||
| |
|
||||
| === === === |
|
||||
| |
|
||||
| T.V |
|
||||
`-------------´";
|
||||
| TV |
|
||||
`-------------´
|
||||
";
|
||||
|
||||
pub const REMOTE_LOGO_WIDTH: usize = 15;
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub const REMOTE_LOGO_WIDTH_U16: u16 = REMOTE_LOGO_WIDTH as u16;
|
||||
pub const REMOTE_LOGO_HEIGHT: usize = 19;
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub const REMOTE_LOGO_HEIGHT_U16: u16 = REMOTE_LOGO_HEIGHT as u16;
|
||||
|
||||
pub fn build_remote_logo_paragraph<'a>() -> Paragraph<'a> {
|
||||
let lines = REMOTE_LOGO
|
||||
|
@ -1,8 +1,9 @@
|
||||
use crate::channels::prototypes::BinaryRequirement;
|
||||
use crate::channels::remote_control::CableEntry;
|
||||
use crate::screen::colors::{Colorscheme, GeneralColorscheme};
|
||||
use crate::screen::logo::build_remote_logo_paragraph;
|
||||
use crate::screen::mode::mode_color;
|
||||
use crate::television::Mode;
|
||||
use crate::screen::logo::{
|
||||
REMOTE_LOGO_WIDTH_U16, build_remote_logo_paragraph,
|
||||
};
|
||||
use crate::utils::input::Input;
|
||||
|
||||
use crate::screen::result_item;
|
||||
@ -12,7 +13,8 @@ use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||
use ratatui::prelude::{Color, Line, Span, Style};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::widgets::{
|
||||
Block, BorderType, Borders, ListDirection, ListState, Padding, Paragraph,
|
||||
Block, BorderType, Borders, Clear, ListDirection, ListState, Padding,
|
||||
Paragraph, Wrap,
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -23,20 +25,178 @@ pub fn draw_remote_control(
|
||||
use_nerd_font_icons: bool,
|
||||
picker_state: &mut ListState,
|
||||
input_state: &mut Input,
|
||||
mode: &Mode,
|
||||
colorscheme: &Colorscheme,
|
||||
) -> Result<()> {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(20),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(REMOTE_LOGO_WIDTH_U16 + 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(rect);
|
||||
|
||||
// Clear the popup area
|
||||
f.render_widget(Clear, rect);
|
||||
|
||||
let selected_entry = entries.get(picker_state.selected().unwrap_or(0));
|
||||
|
||||
draw_rc_logo(f, layout[2], &colorscheme.general);
|
||||
draw_search_panel(
|
||||
f,
|
||||
layout[0],
|
||||
entries,
|
||||
use_nerd_font_icons,
|
||||
picker_state,
|
||||
colorscheme,
|
||||
input_state,
|
||||
)?;
|
||||
draw_information_panel(f, layout[1], selected_entry, colorscheme);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_information_panel(
|
||||
f: &mut Frame,
|
||||
rect: Rect,
|
||||
selected_entry: Option<&CableEntry>,
|
||||
colorscheme: &Colorscheme,
|
||||
) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Fill(1), Constraint::Length(3)].as_ref())
|
||||
.split(rect);
|
||||
|
||||
draw_description_block(f, layout[0], selected_entry, colorscheme);
|
||||
draw_requirements_block(f, layout[1], selected_entry, colorscheme);
|
||||
}
|
||||
|
||||
fn draw_description_block(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
selected_entry: Option<&CableEntry>,
|
||||
colorscheme: &Colorscheme,
|
||||
) {
|
||||
let description_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||
.title_top(Line::from(" Description ").alignment(Alignment::Center))
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colorscheme.general.background.unwrap_or_default()),
|
||||
)
|
||||
.padding(Padding::right(1));
|
||||
|
||||
let description = if let Some(entry) = selected_entry {
|
||||
entry
|
||||
.description
|
||||
.clone()
|
||||
.unwrap_or_else(|| "No description available.".to_string())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let description_paragraph = Paragraph::new(description)
|
||||
.block(description_block)
|
||||
.style(Style::default().italic())
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
f.render_widget(description_paragraph, area);
|
||||
}
|
||||
|
||||
fn draw_requirements_block(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
selected_entry: Option<&CableEntry>,
|
||||
colorscheme: &Colorscheme,
|
||||
) {
|
||||
let mut requirements_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colorscheme.general.background.unwrap_or_default()),
|
||||
)
|
||||
.padding(Padding::right(1));
|
||||
|
||||
if selected_entry.is_none() {
|
||||
// If no entry is selected, just render an empty block
|
||||
let title = Line::from(" Requirements ")
|
||||
.alignment(Alignment::Center)
|
||||
.italic();
|
||||
f.render_widget(requirements_block.title_top(title), area);
|
||||
return;
|
||||
}
|
||||
let selected_entry = selected_entry.unwrap();
|
||||
let spans = selected_entry.requirements.iter().fold(
|
||||
Vec::new(),
|
||||
|mut acc, requirement| {
|
||||
acc.push(Span::styled(
|
||||
format!("{} ", &requirement.bin_name),
|
||||
Style::default()
|
||||
.fg(if requirement.is_met() {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
})
|
||||
.bold()
|
||||
.italic(),
|
||||
));
|
||||
acc
|
||||
},
|
||||
);
|
||||
|
||||
requirements_block = requirements_block.title_top(
|
||||
Line::from({
|
||||
let mut title = vec![Span::from(" Requirements ")];
|
||||
if spans.is_empty()
|
||||
|| selected_entry
|
||||
.requirements
|
||||
.iter()
|
||||
.all(BinaryRequirement::is_met)
|
||||
{
|
||||
title.push(Span::styled(
|
||||
"[OK] ",
|
||||
Style::default().fg(Color::Green),
|
||||
));
|
||||
} else {
|
||||
title.push(Span::styled(
|
||||
"[MISSING] ",
|
||||
Style::default().fg(Color::Red),
|
||||
));
|
||||
}
|
||||
title
|
||||
})
|
||||
.style(Style::default().italic())
|
||||
.alignment(Alignment::Center),
|
||||
);
|
||||
|
||||
let requirements_paragraph = Paragraph::new(Line::from(spans))
|
||||
.block(requirements_block)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
f.render_widget(requirements_paragraph, area);
|
||||
}
|
||||
|
||||
fn draw_search_panel(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
entries: &[CableEntry],
|
||||
use_nerd_font_icons: bool,
|
||||
picker_state: &mut ListState,
|
||||
colorscheme: &Colorscheme,
|
||||
input: &mut Input,
|
||||
) -> Result<()> {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Fill(1), Constraint::Length(3)].as_ref())
|
||||
.split(area);
|
||||
|
||||
draw_rc_channels(
|
||||
f,
|
||||
layout[0],
|
||||
@ -45,14 +205,7 @@ pub fn draw_remote_control(
|
||||
picker_state,
|
||||
colorscheme,
|
||||
);
|
||||
draw_rc_input(f, layout[1], input_state, colorscheme)?;
|
||||
draw_rc_logo(
|
||||
f,
|
||||
layout[2],
|
||||
mode_color(*mode, &colorscheme.mode),
|
||||
&colorscheme.general,
|
||||
);
|
||||
Ok(())
|
||||
draw_rc_input(f, layout[1], input, colorscheme)
|
||||
}
|
||||
|
||||
fn draw_rc_channels(
|
||||
@ -67,6 +220,11 @@ fn draw_rc_channels(
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||
.title_top(
|
||||
Line::from(" Channels ")
|
||||
.alignment(Alignment::Center)
|
||||
.italic(),
|
||||
)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colorscheme.general.background.unwrap_or_default()),
|
||||
@ -93,7 +251,9 @@ fn draw_rc_input(
|
||||
colorscheme: &Colorscheme,
|
||||
) -> Result<()> {
|
||||
let input_block = Block::default()
|
||||
.title_top(Line::from("Remote Control").alignment(Alignment::Center))
|
||||
.title_top(
|
||||
Line::from(" Search ").alignment(Alignment::Center).italic(),
|
||||
)
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||
@ -153,17 +313,10 @@ fn draw_rc_input(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_rc_logo(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
mode_color: Color,
|
||||
colorscheme: &GeneralColorscheme,
|
||||
) {
|
||||
let logo_block = Block::default().style(
|
||||
Style::default()
|
||||
.fg(mode_color)
|
||||
.bg(colorscheme.background.unwrap_or_default()),
|
||||
);
|
||||
fn draw_rc_logo(f: &mut Frame, area: Rect, colorscheme: &GeneralColorscheme) {
|
||||
let logo_block = Block::default()
|
||||
.style(Style::default().bg(colorscheme.background.unwrap_or_default()))
|
||||
.padding(Padding::horizontal(1));
|
||||
|
||||
let logo_paragraph = build_remote_logo_paragraph()
|
||||
.alignment(Alignment::Center)
|
||||
|
@ -53,7 +53,7 @@ fn no_remote() {
|
||||
|
||||
tester.send(&ctrl('t'));
|
||||
// Check that the remote control is not shown
|
||||
tester.assert_not_tui_frame_contains("──Remote Control──");
|
||||
tester.assert_not_tui_frame_contains("(1) (2) (3)");
|
||||
|
||||
// Exit the application
|
||||
tester.send(&ctrl('c'));
|
||||
@ -94,7 +94,7 @@ fn multiple_keybindings() {
|
||||
tester.assert_tui_running(&mut child);
|
||||
|
||||
tester.send(&ctrl('t'));
|
||||
tester.assert_tui_frame_contains("──Remote Control──");
|
||||
tester.assert_tui_frame_contains("(1) (2) (3)");
|
||||
|
||||
tester.send("a");
|
||||
tester.send("a");
|
||||
|
@ -11,7 +11,8 @@ fn tv_remote_control_shows() {
|
||||
// open remote control mode
|
||||
tester.send(&ctrl('t'));
|
||||
|
||||
tester.assert_tui_frame_contains("──Remote Control──");
|
||||
// FIXME: me being lazy
|
||||
tester.assert_tui_frame_contains("(1) (2) (3)");
|
||||
|
||||
// exit remote then app
|
||||
tester.send(&ctrl('c'));
|
||||
|
Loading…
x
Reference in New Issue
Block a user