mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 03:25:23 +00:00
great progress
This commit is contained in:
parent
9eea37a5b5
commit
76d32a01c2
@ -8,13 +8,13 @@ ctrl-d = "ScrollPreviewHalfPageDown"
|
|||||||
ctrl-u = "ScrollPreviewHalfPageUp"
|
ctrl-u = "ScrollPreviewHalfPageUp"
|
||||||
enter = "SelectEntry"
|
enter = "SelectEntry"
|
||||||
ctrl-enter = "SendToChannel"
|
ctrl-enter = "SendToChannel"
|
||||||
ctrl-s = "ToggleChannelSelection"
|
ctrl-s = "ToggleRemoteControl"
|
||||||
|
|
||||||
[keybindings.Guide]
|
[keybindings.RemoteControl]
|
||||||
esc = "Quit"
|
esc = "Quit"
|
||||||
down = "SelectNextEntry"
|
down = "SelectNextEntry"
|
||||||
up = "SelectPrevEntry"
|
up = "SelectPrevEntry"
|
||||||
ctrl-n = "SelectNextEntry"
|
ctrl-n = "SelectNextEntry"
|
||||||
ctrl-p = "SelectPrevEntry"
|
ctrl-p = "SelectPrevEntry"
|
||||||
enter = "SelectEntry"
|
enter = "SelectEntry"
|
||||||
ctrl-s = "ToggleChannelSelection"
|
ctrl-s = "ToggleRemoteControl"
|
||||||
|
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -1541,6 +1541,12 @@ dependencies = [
|
|||||||
"hashbrown 0.15.0",
|
"hashbrown 0.15.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "infer"
|
name = "infer"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@ -2038,24 +2044,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ratatui"
|
name = "ratatui"
|
||||||
version = "0.28.1"
|
version = "0.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d"
|
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cassowary",
|
"cassowary",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"indoc",
|
||||||
"instability",
|
"instability",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lru",
|
"lru",
|
||||||
"paste",
|
"paste",
|
||||||
"serde",
|
"serde",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-truncate",
|
"unicode-truncate",
|
||||||
"unicode-width 0.1.14",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
22
Cargo.toml
22
Cargo.toml
@ -9,10 +9,10 @@ build = "build.rs"
|
|||||||
repository = "https://github.com/alexpasmantier/television"
|
repository = "https://github.com/alexpasmantier/television"
|
||||||
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
|
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
|
||||||
categories = [
|
categories = [
|
||||||
"command-line-utilities",
|
"command-line-utilities",
|
||||||
"command-line-interface",
|
"command-line-interface",
|
||||||
"concurrency",
|
"concurrency",
|
||||||
"development-tools",
|
"development-tools",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -28,12 +28,12 @@ members = ["crates/television_derive"]
|
|||||||
television-derive = { version = "0.1.0", path = "crates/television_derive" }
|
television-derive = { version = "0.1.0", path = "crates/television_derive" }
|
||||||
better-panic = "0.3.0"
|
better-panic = "0.3.0"
|
||||||
clap = { version = "4.4.5", features = [
|
clap = { version = "4.4.5", features = [
|
||||||
"derive",
|
"derive",
|
||||||
"cargo",
|
"cargo",
|
||||||
"wrap_help",
|
"wrap_help",
|
||||||
"unicode",
|
"unicode",
|
||||||
"string",
|
"string",
|
||||||
"unstable-styles",
|
"unstable-styles",
|
||||||
] }
|
] }
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
config = "0.14.0"
|
config = "0.14.0"
|
||||||
@ -50,7 +50,7 @@ libc = "0.2.158"
|
|||||||
nucleo = "0.5.0"
|
nucleo = "0.5.0"
|
||||||
nucleo-matcher = "0.3.1"
|
nucleo-matcher = "0.3.1"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
ratatui = { version = "0.28.1", features = ["serde", "macros"] }
|
ratatui = { version = "0.29.0", features = ["serde", "macros"] }
|
||||||
regex = "1.10.6"
|
regex = "1.10.6"
|
||||||
serde = { version = "1.0.208", features = ["derive"] }
|
serde = { version = "1.0.208", features = ["derive"] }
|
||||||
serde_json = "1.0.125"
|
serde_json = "1.0.125"
|
||||||
|
@ -42,6 +42,6 @@ pub enum Action {
|
|||||||
Error(String),
|
Error(String),
|
||||||
NoOp,
|
NoOp,
|
||||||
// channel actions
|
// channel actions
|
||||||
ToggleChannelSelection,
|
ToggleRemoteControl,
|
||||||
SendToChannel,
|
SendToChannel,
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,9 @@ mod alias;
|
|||||||
mod env;
|
mod env;
|
||||||
mod files;
|
mod files;
|
||||||
mod git_repos;
|
mod git_repos;
|
||||||
|
pub mod remote_control;
|
||||||
pub mod stdin;
|
pub mod stdin;
|
||||||
mod text;
|
mod text;
|
||||||
pub mod tv_guide;
|
|
||||||
|
|
||||||
/// The interface that all television channels must implement.
|
/// The interface that all television channels must implement.
|
||||||
///
|
///
|
||||||
@ -94,13 +94,34 @@ pub trait OnAir: Send {
|
|||||||
#[allow(dead_code, clippy::module_name_repetitions)]
|
#[allow(dead_code, clippy::module_name_repetitions)]
|
||||||
#[derive(UnitChannel, CliChannel, Broadcast)]
|
#[derive(UnitChannel, CliChannel, Broadcast)]
|
||||||
pub enum TelevisionChannel {
|
pub enum TelevisionChannel {
|
||||||
|
/// The environment variables channel.
|
||||||
|
///
|
||||||
|
/// This channel allows to search through environment variables.
|
||||||
Env(env::Channel),
|
Env(env::Channel),
|
||||||
|
/// The files channel.
|
||||||
|
///
|
||||||
|
/// This channel allows to search through files.
|
||||||
Files(files::Channel),
|
Files(files::Channel),
|
||||||
|
/// The git repositories channel.
|
||||||
|
///
|
||||||
|
/// This channel allows to search through git repositories.
|
||||||
GitRepos(git_repos::Channel),
|
GitRepos(git_repos::Channel),
|
||||||
|
/// The text channel.
|
||||||
|
///
|
||||||
|
/// This channel allows to search through the contents of text files.
|
||||||
Text(text::Channel),
|
Text(text::Channel),
|
||||||
|
/// The standard input channel.
|
||||||
|
///
|
||||||
|
/// This channel allows to search through whatever is passed through stdin.
|
||||||
Stdin(stdin::Channel),
|
Stdin(stdin::Channel),
|
||||||
|
/// The alias channel.
|
||||||
|
///
|
||||||
|
/// This channel allows to search through aliases.
|
||||||
Alias(alias::Channel),
|
Alias(alias::Channel),
|
||||||
TvGuide(tv_guide::TvGuide),
|
/// The remote control channel.
|
||||||
|
///
|
||||||
|
/// This channel allows to switch between different channels.
|
||||||
|
RemoteControl(remote_control::RemoteControl),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: this could be generated by a derive macro
|
/// NOTE: this could be generated by a derive macro
|
||||||
|
@ -52,7 +52,7 @@ fn get_raw_aliases(shell: &str) -> Vec<String> {
|
|||||||
let aliases = String::from_utf8(output.stdout).unwrap();
|
let aliases = String::from_utf8(output.stdout).unwrap();
|
||||||
aliases
|
aliases
|
||||||
.lines()
|
.lines()
|
||||||
.map(std::string::ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
"zsh" => {
|
"zsh" => {
|
||||||
@ -65,7 +65,7 @@ fn get_raw_aliases(shell: &str) -> Vec<String> {
|
|||||||
let aliases = String::from_utf8(output.stdout).unwrap();
|
let aliases = String::from_utf8(output.stdout).unwrap();
|
||||||
aliases
|
aliases
|
||||||
.lines()
|
.lines()
|
||||||
.map(std::string::ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
// TODO: add more shells
|
// TODO: add more shells
|
||||||
@ -179,7 +179,7 @@ impl OnAir for Channel {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_result(&self, index: u32) -> Option<super::Entry> {
|
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||||
let snapshot = self.matcher.snapshot();
|
let snapshot = self.matcher.snapshot();
|
||||||
snapshot.get_matched_item(index).map(|item| {
|
snapshot.get_matched_item(index).map(|item| {
|
||||||
Entry::new(item.data.name.clone(), PreviewType::EnvVar)
|
Entry::new(item.data.name.clone(), PreviewType::EnvVar)
|
||||||
|
@ -76,14 +76,6 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
|
||||||
self.result_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn total_count(&self) -> u32 {
|
|
||||||
self.total_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||||
let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT);
|
let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT);
|
||||||
let snapshot = self.matcher.snapshot();
|
let snapshot = self.matcher.snapshot();
|
||||||
@ -157,6 +149,14 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn result_count(&self) -> u32 {
|
||||||
|
self.result_count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_count(&self) -> u32 {
|
||||||
|
self.total_count
|
||||||
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.running
|
self.running
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ impl OnAir for Channel {
|
|||||||
.matched_items(
|
.matched_items(
|
||||||
offset
|
offset
|
||||||
..(num_entries + offset)
|
..(num_entries + offset)
|
||||||
.min(snapshot.matched_item_count()),
|
.min(snapshot.matched_item_count()),
|
||||||
)
|
)
|
||||||
.map(move |item| {
|
.map(move |item| {
|
||||||
snapshot.pattern().column_pattern(0).indices(
|
snapshot.pattern().column_pattern(0).indices(
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use color_eyre::owo_colors::OwoColorize;
|
|
||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
use ignore::{overrides::OverrideBuilder, DirEntry};
|
use ignore::{overrides::OverrideBuilder, DirEntry};
|
||||||
use nucleo::{
|
use nucleo::{
|
||||||
@ -105,7 +104,7 @@ impl OnAir for Channel {
|
|||||||
.matched_items(
|
.matched_items(
|
||||||
offset
|
offset
|
||||||
..(num_entries + offset)
|
..(num_entries + offset)
|
||||||
.min(snapshot.matched_item_count()),
|
.min(snapshot.matched_item_count()),
|
||||||
)
|
)
|
||||||
.map(move |item| {
|
.map(move |item| {
|
||||||
snapshot.pattern().column_pattern(0).indices(
|
snapshot.pattern().column_pattern(0).indices(
|
||||||
@ -201,7 +200,7 @@ fn get_ignored_paths() -> Vec<PathBuf> {
|
|||||||
}
|
}
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::unused_async)]
|
||||||
async fn crawl_for_repos(
|
async fn crawl_for_repos(
|
||||||
starting_point: std::path::PathBuf,
|
starting_point: PathBuf,
|
||||||
injector: nucleo::Injector<DirEntry>,
|
injector: nucleo::Injector<DirEntry>,
|
||||||
entry_cache: Arc<Mutex<HashSet<String>>>,
|
entry_cache: Arc<Mutex<HashSet<String>>>,
|
||||||
cache_valid: Arc<Mutex<bool>>,
|
cache_valid: Arc<Mutex<bool>>,
|
||||||
@ -214,7 +213,7 @@ async fn crawl_for_repos(
|
|||||||
Some(walker_overrides_builder.build().unwrap()),
|
Some(walker_overrides_builder.build().unwrap()),
|
||||||
Some(get_ignored_paths()),
|
Some(get_ignored_paths()),
|
||||||
)
|
)
|
||||||
.build_parallel();
|
.build_parallel();
|
||||||
|
|
||||||
walker.run(|| {
|
walker.run(|| {
|
||||||
let injector = injector.clone();
|
let injector = injector.clone();
|
||||||
|
@ -14,7 +14,7 @@ use crate::{
|
|||||||
previewers::PreviewType,
|
previewers::PreviewType,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TvGuide {
|
pub struct RemoteControl {
|
||||||
matcher: Nucleo<CliTvChannel>,
|
matcher: Nucleo<CliTvChannel>,
|
||||||
last_pattern: String,
|
last_pattern: String,
|
||||||
result_count: u32,
|
result_count: u32,
|
||||||
@ -24,7 +24,7 @@ pub struct TvGuide {
|
|||||||
|
|
||||||
const NUM_THREADS: usize = 1;
|
const NUM_THREADS: usize = 1;
|
||||||
|
|
||||||
impl TvGuide {
|
impl RemoteControl {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let matcher = Nucleo::new(
|
let matcher = Nucleo::new(
|
||||||
Config::DEFAULT,
|
Config::DEFAULT,
|
||||||
@ -38,7 +38,7 @@ impl TvGuide {
|
|||||||
cols[0] = (*e).to_string().into();
|
cols[0] = (*e).to_string().into();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
TvGuide {
|
RemoteControl {
|
||||||
matcher,
|
matcher,
|
||||||
last_pattern: String::new(),
|
last_pattern: String::new(),
|
||||||
result_count: 0,
|
result_count: 0,
|
||||||
@ -50,7 +50,7 @@ impl TvGuide {
|
|||||||
const MATCHER_TICK_TIMEOUT: u64 = 2;
|
const MATCHER_TICK_TIMEOUT: u64 = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TvGuide {
|
impl Default for RemoteControl {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ const TV_ICON: FileIcon = FileIcon {
|
|||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
};
|
};
|
||||||
|
|
||||||
impl OnAir for TvGuide {
|
impl OnAir for RemoteControl {
|
||||||
fn find(&mut self, pattern: &str) {
|
fn find(&mut self, pattern: &str) {
|
||||||
if pattern != self.last_pattern {
|
if pattern != self.last_pattern {
|
||||||
self.matcher.pattern.reparse(
|
self.matcher.pattern.reparse(
|
||||||
@ -75,14 +75,6 @@ impl OnAir for TvGuide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
|
||||||
self.result_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn total_count(&self) -> u32 {
|
|
||||||
self.total_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||||
let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT);
|
let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT);
|
||||||
let snapshot = self.matcher.snapshot();
|
let snapshot = self.matcher.snapshot();
|
||||||
@ -130,6 +122,14 @@ impl OnAir for TvGuide {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn result_count(&self) -> u32 {
|
||||||
|
self.result_count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_count(&self) -> u32 {
|
||||||
|
self.total_count
|
||||||
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.running
|
self.running
|
||||||
}
|
}
|
@ -90,7 +90,7 @@ impl OnAir for Channel {
|
|||||||
.matched_items(
|
.matched_items(
|
||||||
offset
|
offset
|
||||||
..(num_entries + offset)
|
..(num_entries + offset)
|
||||||
.min(snapshot.matched_item_count()),
|
.min(snapshot.matched_item_count()),
|
||||||
)
|
)
|
||||||
.map(move |item| {
|
.map(move |item| {
|
||||||
snapshot.pattern().column_pattern(0).indices(
|
snapshot.pattern().column_pattern(0).indices(
|
||||||
@ -118,7 +118,7 @@ impl OnAir for Channel {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_result(&self, index: u32) -> Option<super::Entry> {
|
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||||
let snapshot = self.matcher.snapshot();
|
let snapshot = self.matcher.snapshot();
|
||||||
snapshot.get_matched_item(index).map(|item| {
|
snapshot.get_matched_item(index).map(|item| {
|
||||||
let content = item.matcher_columns[0].to_string();
|
let content = item.matcher_columns[0].to_string();
|
||||||
|
@ -91,14 +91,6 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
|
||||||
self.result_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn total_count(&self) -> u32 {
|
|
||||||
self.total_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||||
let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT);
|
let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT);
|
||||||
let snapshot = self.matcher.snapshot();
|
let snapshot = self.matcher.snapshot();
|
||||||
@ -157,6 +149,14 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn result_count(&self) -> u32 {
|
||||||
|
self.result_count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_count(&self) -> u32 {
|
||||||
|
self.total_count
|
||||||
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.running
|
self.running
|
||||||
}
|
}
|
||||||
@ -219,9 +219,9 @@ async fn load_candidates(path: PathBuf, injector: Injector<CandidateLine>) {
|
|||||||
Ok(bytes_read) => {
|
Ok(bytes_read) => {
|
||||||
if (bytes_read == 0)
|
if (bytes_read == 0)
|
||||||
|| is_not_text(&buffer)
|
|| is_not_text(&buffer)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|| proportion_of_printable_ascii_characters(&buffer)
|
|| proportion_of_printable_ascii_characters(&buffer)
|
||||||
< PRINTABLE_ASCII_THRESHOLD
|
< PRINTABLE_ASCII_THRESHOLD
|
||||||
{
|
{
|
||||||
return ignore::WalkState::Continue;
|
return ignore::WalkState::Continue;
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,6 @@ lazy_static! {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Result<Self, config::ConfigError> {
|
pub fn new() -> Result<Self, config::ConfigError> {
|
||||||
//let default_config: Config = json5::from_str(CONFIG).unwrap();
|
|
||||||
let default_config: Config = toml::from_str(CONFIG).unwrap();
|
let default_config: Config = toml::from_str(CONFIG).unwrap();
|
||||||
let data_dir = get_data_dir();
|
let data_dir = get_data_dir();
|
||||||
let config_dir = get_config_dir();
|
let config_dir = get_config_dir();
|
||||||
|
@ -154,7 +154,7 @@ impl EventLoop {
|
|||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
let tx_c = tx.clone();
|
let tx_c = tx.clone();
|
||||||
let tick_interval =
|
let tick_interval =
|
||||||
tokio::time::Duration::from_secs_f64(1.0 / tick_rate);
|
Duration::from_secs_f64(1.0 / tick_rate);
|
||||||
|
|
||||||
let (abort, mut abort_recv) = mpsc::unbounded_channel();
|
let (abort, mut abort_recv) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ mod errors;
|
|||||||
mod event;
|
mod event;
|
||||||
mod fuzzy;
|
mod fuzzy;
|
||||||
mod logging;
|
mod logging;
|
||||||
|
mod picker;
|
||||||
mod previewers;
|
mod previewers;
|
||||||
mod render;
|
mod render;
|
||||||
mod television;
|
mod television;
|
||||||
@ -28,8 +29,8 @@ mod utils;
|
|||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
crate::errors::init()?;
|
errors::init()?;
|
||||||
crate::logging::init()?;
|
logging::init()?;
|
||||||
|
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
|
|
||||||
|
114
crates/television/picker.rs
Normal file
114
crates/television/picker.rs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
use crate::ui::input::Input;
|
||||||
|
use crate::utils::strings::EMPTY_STRING;
|
||||||
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Picker {
|
||||||
|
pub(crate) state: ListState,
|
||||||
|
pub(crate) relative_state: ListState,
|
||||||
|
pub(crate) view_offset: usize,
|
||||||
|
_inverted: bool,
|
||||||
|
pub(crate) input: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Picker {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Picker {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: ListState::default(),
|
||||||
|
relative_state: ListState::default(),
|
||||||
|
view_offset: 0,
|
||||||
|
_inverted: false,
|
||||||
|
input: Input::new(EMPTY_STRING.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inverted(mut self) -> Self {
|
||||||
|
self._inverted = !self._inverted;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset_selection(&mut self) {
|
||||||
|
self.state.select(Some(0));
|
||||||
|
self.relative_state.select(Some(0));
|
||||||
|
self.view_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset_input(&mut self) {
|
||||||
|
self.input.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn selected(&self) -> Option<usize> {
|
||||||
|
self.state.selected()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select(&mut self, index: Option<usize>) {
|
||||||
|
self.state.select(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_selected(&self) -> Option<usize> {
|
||||||
|
self.relative_state.selected()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn relative_select(&mut self, index: Option<usize>) {
|
||||||
|
self.relative_state.select(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_next(&mut self, total_items: usize, height: usize) {
|
||||||
|
if self._inverted {
|
||||||
|
self._select_prev(total_items, height);
|
||||||
|
} else {
|
||||||
|
self._select_next(total_items, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_prev(&mut self, total_items: usize, height: usize) {
|
||||||
|
if self._inverted {
|
||||||
|
self._select_next(total_items, height);
|
||||||
|
} else {
|
||||||
|
self._select_prev(total_items, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _select_next(&mut self, total_items: usize, height: usize) {
|
||||||
|
let selected = self.selected().unwrap_or(0);
|
||||||
|
let relative_selected = self.relative_selected().unwrap_or(0);
|
||||||
|
if selected > 0 {
|
||||||
|
self.select(Some(selected - 1));
|
||||||
|
self.relative_select(Some(relative_selected.saturating_sub(1)));
|
||||||
|
if relative_selected == 0 {
|
||||||
|
self.view_offset = self.view_offset.saturating_sub(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.view_offset = total_items.saturating_sub(height - 2);
|
||||||
|
self.select(Some((total_items).saturating_sub(1)));
|
||||||
|
self.relative_select(Some(height - 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _select_prev(&mut self, total_items: usize, height: usize) {
|
||||||
|
let new_index = (self.selected().unwrap_or(0) + 1) % total_items;
|
||||||
|
self.select(Some(new_index));
|
||||||
|
if new_index == 0 {
|
||||||
|
self.view_offset = 0;
|
||||||
|
self.relative_select(Some(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.relative_selected().unwrap_or(0) == height - 3 {
|
||||||
|
self.view_offset += 1;
|
||||||
|
self.relative_select(Some(
|
||||||
|
self.selected().unwrap_or(0).min(height - 3),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.relative_select(Some(
|
||||||
|
(self.relative_selected().unwrap_or(0) + 1)
|
||||||
|
.min(self.selected().unwrap_or(0)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -85,7 +85,7 @@ fn tree<P: AsRef<Path>>(
|
|||||||
.build();
|
.build();
|
||||||
let mut level_entry_count: u8 = 0;
|
let mut level_entry_count: u8 = 0;
|
||||||
|
|
||||||
for path in w.skip(1).filter_map(std::result::Result::ok) {
|
for path in w.skip(1).filter_map(Result::ok) {
|
||||||
let m = path.metadata().unwrap();
|
let m = path.metadata().unwrap();
|
||||||
if m.is_dir() && max_depth > 1 {
|
if m.is_dir() && max_depth > 1 {
|
||||||
root.push(tree(
|
root.push(tree(
|
||||||
|
@ -5,11 +5,10 @@ use std::fs::File;
|
|||||||
use std::io::{BufRead, BufReader, Read, Seek};
|
use std::io::{BufRead, BufReader, Read, Seek};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use syntect::easy::HighlightLines;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use syntect::{
|
use syntect::{
|
||||||
highlighting::{Style, Theme, ThemeSet},
|
highlighting::{Theme, ThemeSet},
|
||||||
parsing::SyntaxSet,
|
parsing::SyntaxSet,
|
||||||
};
|
};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@ -82,7 +81,7 @@ impl FilePreviewer {
|
|||||||
entry.name.clone(),
|
entry.name.clone(),
|
||||||
preview.clone(),
|
preview.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// compute the highlighted version in the background
|
// compute the highlighted version in the background
|
||||||
let mut reader =
|
let mut reader =
|
||||||
@ -226,8 +225,8 @@ impl FilePreviewer {
|
|||||||
if let Ok(bytes_read) = f.read(&mut buffer) {
|
if let Ok(bytes_read) = f.read(&mut buffer) {
|
||||||
if bytes_read > 0
|
if bytes_read > 0
|
||||||
&& proportion_of_printable_ascii_characters(
|
&& proportion_of_printable_ascii_characters(
|
||||||
&buffer[..bytes_read],
|
&buffer[..bytes_read],
|
||||||
) > PRINTABLE_ASCII_THRESHOLD
|
) > PRINTABLE_ASCII_THRESHOLD
|
||||||
{
|
{
|
||||||
file_type = FileType::Text;
|
file_type = FileType::Text;
|
||||||
}
|
}
|
||||||
|
@ -1,62 +1,44 @@
|
|||||||
use color_eyre::Result;
|
use crate::channels::remote_control::RemoteControl;
|
||||||
use futures::executor::block_on;
|
use crate::channels::OnAir;
|
||||||
use ratatui::{
|
use crate::channels::UnitChannel;
|
||||||
layout::{
|
use crate::picker::Picker;
|
||||||
Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect,
|
use crate::ui::layout::{Dimensions, Layout};
|
||||||
},
|
|
||||||
style::{Color, Style, Stylize},
|
|
||||||
text::{Line, Span},
|
|
||||||
widgets::{
|
|
||||||
block::{Position, Title},
|
|
||||||
Block, BorderType, Borders, ListState, Padding, Paragraph, Wrap,
|
|
||||||
},
|
|
||||||
Frame,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{collections::HashMap, str::FromStr};
|
|
||||||
use strum::Display;
|
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
|
|
||||||
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::utils::strings::EMPTY_STRING;
|
||||||
use crate::{action::Action, config::Config};
|
use crate::{action::Action, config::Config};
|
||||||
use crate::{channels::tv_guide::TvGuide, ui::get_border_style};
|
|
||||||
use crate::{channels::OnAir, utils::strings::shrink_with_ellipsis};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
channels::TelevisionChannel, ui::input::actions::InputActionHandler,
|
channels::TelevisionChannel, ui::input::actions::InputActionHandler,
|
||||||
};
|
};
|
||||||
use crate::{channels::UnitChannel, ui::input::Input};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
entry::{Entry, ENTRY_PLACEHOLDER},
|
entry::{Entry, ENTRY_PLACEHOLDER},
|
||||||
ui::spinner::Spinner,
|
ui::spinner::Spinner,
|
||||||
};
|
};
|
||||||
use crate::{previewers::Previewer, ui::spinner::SpinnerState};
|
use crate::{previewers::Previewer, ui::spinner::SpinnerState};
|
||||||
|
use color_eyre::Result;
|
||||||
|
use futures::executor::block_on;
|
||||||
|
use ratatui::{layout::Rect, style::Color, widgets::Paragraph, Frame};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use strum::Display;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize, Display,
|
PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize, Display,
|
||||||
)]
|
)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Channel,
|
Channel,
|
||||||
Guide,
|
RemoteControl,
|
||||||
SendToChannel,
|
SendToChannel,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Television {
|
pub struct Television {
|
||||||
action_tx: Option<UnboundedSender<Action>>,
|
action_tx: Option<UnboundedSender<Action>>,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
channel: TelevisionChannel,
|
pub(crate) channel: TelevisionChannel,
|
||||||
guide: TelevisionChannel,
|
pub(crate) remote_control: TelevisionChannel,
|
||||||
current_pattern: String,
|
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
input: Input,
|
current_pattern: String,
|
||||||
picker_state: ListState,
|
pub(crate) results_picker: Picker,
|
||||||
relative_picker_state: ListState,
|
pub(crate) rc_picker: Picker,
|
||||||
picker_view_offset: usize,
|
|
||||||
results_area_height: u32,
|
results_area_height: u32,
|
||||||
pub previewer: Previewer,
|
pub previewer: Previewer,
|
||||||
pub preview_scroll: Option<u16>,
|
pub preview_scroll: Option<u16>,
|
||||||
@ -69,30 +51,26 @@ pub struct Television {
|
|||||||
/// are rendered correctly even when resizing the terminal while still
|
/// are rendered correctly even when resizing the terminal while still
|
||||||
/// benefiting from a cache mechanism.
|
/// benefiting from a cache mechanism.
|
||||||
pub meta_paragraph_cache: HashMap<(String, u16, u16), Paragraph<'static>>,
|
pub meta_paragraph_cache: HashMap<(String, u16, u16), Paragraph<'static>>,
|
||||||
spinner: Spinner,
|
pub(crate) spinner: Spinner,
|
||||||
spinner_state: SpinnerState,
|
pub(crate) spinner_state: SpinnerState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(mut channel: TelevisionChannel) -> Self {
|
pub fn new(mut channel: TelevisionChannel) -> Self {
|
||||||
channel.find(EMPTY_STRING);
|
channel.find(EMPTY_STRING);
|
||||||
let guide = TelevisionChannel::TvGuide(TvGuide::new());
|
|
||||||
|
|
||||||
let spinner = Spinner::default();
|
let spinner = Spinner::default();
|
||||||
let spinner_state = SpinnerState::from(&spinner);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
action_tx: None,
|
action_tx: None,
|
||||||
config: Config::default(),
|
config: Config::default(),
|
||||||
channel,
|
channel,
|
||||||
guide,
|
remote_control: TelevisionChannel::RemoteControl(
|
||||||
current_pattern: EMPTY_STRING.to_string(),
|
RemoteControl::new(),
|
||||||
|
),
|
||||||
mode: Mode::Channel,
|
mode: Mode::Channel,
|
||||||
input: Input::new(EMPTY_STRING.to_string()),
|
current_pattern: EMPTY_STRING.to_string(),
|
||||||
picker_state: ListState::default(),
|
results_picker: Picker::default(),
|
||||||
relative_picker_state: ListState::default(),
|
rc_picker: Picker::default().inverted(),
|
||||||
picker_view_offset: 0,
|
|
||||||
results_area_height: 0,
|
results_area_height: 0,
|
||||||
previewer: Previewer::new(),
|
previewer: Previewer::new(),
|
||||||
preview_scroll: None,
|
preview_scroll: None,
|
||||||
@ -100,7 +78,7 @@ impl Television {
|
|||||||
current_preview_total_lines: 0,
|
current_preview_total_lines: 0,
|
||||||
meta_paragraph_cache: HashMap::new(),
|
meta_paragraph_cache: HashMap::new(),
|
||||||
spinner,
|
spinner,
|
||||||
spinner_state,
|
spinner_state: SpinnerState::from(&spinner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,9 +89,9 @@ impl Television {
|
|||||||
/// FIXME: this needs rework
|
/// FIXME: this needs rework
|
||||||
pub fn change_channel(&mut self, channel: TelevisionChannel) {
|
pub fn change_channel(&mut self, channel: TelevisionChannel) {
|
||||||
self.reset_preview_scroll();
|
self.reset_preview_scroll();
|
||||||
self.reset_results_selection();
|
self.reset_picker_selection();
|
||||||
|
self.reset_picker_input();
|
||||||
self.current_pattern = EMPTY_STRING.to_string();
|
self.current_pattern = EMPTY_STRING.to_string();
|
||||||
self.input.reset();
|
|
||||||
self.channel.shutdown();
|
self.channel.shutdown();
|
||||||
self.channel = channel;
|
self.channel = channel;
|
||||||
}
|
}
|
||||||
@ -123,92 +101,84 @@ impl Television {
|
|||||||
Mode::Channel => {
|
Mode::Channel => {
|
||||||
self.channel.find(pattern);
|
self.channel.find(pattern);
|
||||||
}
|
}
|
||||||
Mode::Guide | Mode::SendToChannel => {
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
self.guide.find(pattern);
|
self.remote_control.find(pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_selected_entry(&mut self) -> Option<Entry> {
|
pub fn get_selected_entry(&mut self) -> Option<Entry> {
|
||||||
self.picker_state.selected().and_then(|i| match self.mode {
|
match self.mode {
|
||||||
Mode::Channel => {
|
Mode::Channel => self.results_picker.selected().and_then(|i| {
|
||||||
self.channel.get_result(u32::try_from(i).unwrap())
|
self.channel.get_result(u32::try_from(i).unwrap())
|
||||||
|
}),
|
||||||
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
|
self.rc_picker.selected().and_then(|i| {
|
||||||
|
self.remote_control.get_result(u32::try_from(i).unwrap())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Mode::Guide | Mode::SendToChannel => {
|
}
|
||||||
self.guide.get_result(u32::try_from(i).unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_prev_entry(&mut self) {
|
pub fn select_prev_entry(&mut self) {
|
||||||
let result_count = match self.mode {
|
let (result_count, picker) = match self.mode {
|
||||||
Mode::Channel => self.channel.result_count(),
|
Mode::Channel => {
|
||||||
Mode::Guide | Mode::SendToChannel => self.guide.total_count(),
|
(self.channel.result_count(), &mut self.results_picker)
|
||||||
|
}
|
||||||
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
|
(self.remote_control.total_count(), &mut self.rc_picker)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if result_count == 0 {
|
if result_count == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let new_index = (self.picker_state.selected().unwrap_or(0) + 1)
|
picker.select_prev(
|
||||||
% result_count as usize;
|
result_count as usize,
|
||||||
self.picker_state.select(Some(new_index));
|
self.results_area_height as usize,
|
||||||
if new_index == 0 {
|
);
|
||||||
self.picker_view_offset = 0;
|
|
||||||
self.relative_picker_state.select(Some(0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if self.relative_picker_state.selected().unwrap_or(0)
|
|
||||||
== self.results_area_height as usize - 3
|
|
||||||
{
|
|
||||||
self.picker_view_offset += 1;
|
|
||||||
self.relative_picker_state.select(Some(
|
|
||||||
self.picker_state
|
|
||||||
.selected()
|
|
||||||
.unwrap_or(0)
|
|
||||||
.min(self.results_area_height as usize - 3),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
self.relative_picker_state.select(Some(
|
|
||||||
(self.relative_picker_state.selected().unwrap_or(0) + 1)
|
|
||||||
.min(self.picker_state.selected().unwrap_or(0)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_next_entry(&mut self) {
|
pub fn select_next_entry(&mut self) {
|
||||||
let result_count = match self.mode {
|
let (result_count, picker) = match self.mode {
|
||||||
Mode::Channel => self.channel.result_count(),
|
Mode::Channel => {
|
||||||
Mode::Guide | Mode::SendToChannel => self.guide.total_count(),
|
(self.channel.result_count(), &mut self.results_picker)
|
||||||
|
}
|
||||||
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
|
(self.remote_control.total_count(), &mut self.rc_picker)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if result_count == 0 {
|
if result_count == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let selected = self.picker_state.selected().unwrap_or(0);
|
picker.select_next(
|
||||||
let relative_selected =
|
result_count as usize,
|
||||||
self.relative_picker_state.selected().unwrap_or(0);
|
self.results_area_height as usize,
|
||||||
if selected > 0 {
|
);
|
||||||
self.picker_state.select(Some(selected - 1));
|
|
||||||
self.relative_picker_state
|
|
||||||
.select(Some(relative_selected.saturating_sub(1)));
|
|
||||||
if relative_selected == 0 {
|
|
||||||
self.picker_view_offset =
|
|
||||||
self.picker_view_offset.saturating_sub(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.picker_view_offset = result_count
|
|
||||||
.saturating_sub(self.results_area_height - 2)
|
|
||||||
as usize;
|
|
||||||
self.picker_state
|
|
||||||
.select(Some((result_count as usize).saturating_sub(1)));
|
|
||||||
self.relative_picker_state
|
|
||||||
.select(Some(self.results_area_height as usize - 3));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_preview_scroll(&mut self) {
|
fn reset_preview_scroll(&mut self) {
|
||||||
self.preview_scroll = None;
|
self.preview_scroll = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset_picker_selection(&mut self) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Channel => self.results_picker.reset_selection(),
|
||||||
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
|
self.rc_picker.reset_selection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_picker_input(&mut self) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Channel => self.results_picker.reset_input(),
|
||||||
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
|
self.rc_picker.reset_input()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_preview_down(&mut self, offset: u16) {
|
pub fn scroll_preview_down(&mut self, offset: u16) {
|
||||||
if self.preview_scroll.is_none() {
|
if self.preview_scroll.is_none() {
|
||||||
self.preview_scroll = Some(0);
|
self.preview_scroll = Some(0);
|
||||||
@ -228,18 +198,12 @@ impl Television {
|
|||||||
self.preview_scroll = Some(scroll.saturating_sub(offset));
|
self.preview_scroll = Some(scroll.saturating_sub(offset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_results_selection(&mut self) {
|
|
||||||
self.picker_state.select(Some(0));
|
|
||||||
self.relative_picker_state.select(Some(0));
|
|
||||||
self.picker_view_offset = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
// input
|
// input
|
||||||
const DEFAULT_INPUT_FG: Color = Color::LightRed;
|
pub(crate) const DEFAULT_INPUT_FG: Color = Color::LightRed;
|
||||||
const DEFAULT_RESULTS_COUNT_FG: Color = Color::LightRed;
|
pub(crate) const DEFAULT_RESULTS_COUNT_FG: Color = Color::LightRed;
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
/// Register an action handler that can send actions for processing if necessary.
|
/// Register an action handler that can send actions for processing if necessary.
|
||||||
@ -286,17 +250,23 @@ impl Television {
|
|||||||
| Action::GoToInputStart
|
| Action::GoToInputStart
|
||||||
| Action::GoToNextChar
|
| Action::GoToNextChar
|
||||||
| Action::GoToPrevChar => {
|
| Action::GoToPrevChar => {
|
||||||
self.input.handle_action(&action);
|
let input = match self.mode {
|
||||||
|
Mode::Channel => &mut self.results_picker.input,
|
||||||
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
|
&mut self.rc_picker.input
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.handle_action(&action);
|
||||||
match action {
|
match action {
|
||||||
Action::AddInputChar(_)
|
Action::AddInputChar(_)
|
||||||
| Action::DeletePrevChar
|
| Action::DeletePrevChar
|
||||||
| Action::DeleteNextChar => {
|
| Action::DeleteNextChar => {
|
||||||
let new_pattern = self.input.value().to_string();
|
let new_pattern = input.value().to_string();
|
||||||
if new_pattern != self.current_pattern {
|
if new_pattern != self.current_pattern {
|
||||||
self.current_pattern.clone_from(&new_pattern);
|
self.current_pattern.clone_from(&new_pattern);
|
||||||
self.find(&new_pattern);
|
self.find(&new_pattern);
|
||||||
self.reset_preview_scroll();
|
self.reset_preview_scroll();
|
||||||
self.reset_results_selection();
|
self.reset_picker_selection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -314,13 +284,15 @@ impl Television {
|
|||||||
Action::ScrollPreviewUp => self.scroll_preview_up(1),
|
Action::ScrollPreviewUp => self.scroll_preview_up(1),
|
||||||
Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20),
|
Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20),
|
||||||
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
|
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
|
||||||
Action::ToggleChannelSelection => match self.mode {
|
Action::ToggleRemoteControl => match self.mode {
|
||||||
Mode::Channel => {
|
Mode::Channel => {
|
||||||
self.reset_screen();
|
self.mode = Mode::RemoteControl;
|
||||||
self.mode = Mode::Guide;
|
|
||||||
}
|
}
|
||||||
Mode::Guide => {
|
Mode::RemoteControl => {
|
||||||
self.reset_screen();
|
// this resets the RC picker
|
||||||
|
self.reset_picker_input();
|
||||||
|
self.remote_control.find(EMPTY_STRING);
|
||||||
|
self.reset_picker_selection();
|
||||||
self.mode = Mode::Channel;
|
self.mode = Mode::Channel;
|
||||||
}
|
}
|
||||||
Mode::SendToChannel => {}
|
Mode::SendToChannel => {}
|
||||||
@ -333,10 +305,14 @@ impl Television {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.send(Action::SelectAndExit)?,
|
.send(Action::SelectAndExit)?,
|
||||||
Mode::Guide => {
|
Mode::RemoteControl => {
|
||||||
if let Ok(new_channel) =
|
if let Ok(new_channel) =
|
||||||
TelevisionChannel::try_from(&entry)
|
TelevisionChannel::try_from(&entry)
|
||||||
{
|
{
|
||||||
|
// this resets the RC picker
|
||||||
|
self.reset_picker_selection();
|
||||||
|
self.reset_picker_input();
|
||||||
|
self.remote_control.find(EMPTY_STRING);
|
||||||
self.mode = Mode::Channel;
|
self.mode = Mode::Channel;
|
||||||
self.change_channel(new_channel);
|
self.change_channel(new_channel);
|
||||||
}
|
}
|
||||||
@ -356,7 +332,8 @@ impl Television {
|
|||||||
Action::SendToChannel => {
|
Action::SendToChannel => {
|
||||||
self.mode = Mode::SendToChannel;
|
self.mode = Mode::SendToChannel;
|
||||||
// TODO: build new guide from current channel based on which are pipeable into
|
// TODO: build new guide from current channel based on which are pipeable into
|
||||||
self.guide = TelevisionChannel::TvGuide(TvGuide::new());
|
self.remote_control =
|
||||||
|
TelevisionChannel::RemoteControl(RemoteControl::new());
|
||||||
self.reset_screen();
|
self.reset_screen();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -366,11 +343,11 @@ impl Television {
|
|||||||
|
|
||||||
fn reset_screen(&mut self) {
|
fn reset_screen(&mut self) {
|
||||||
self.reset_preview_scroll();
|
self.reset_preview_scroll();
|
||||||
self.reset_results_selection();
|
self.reset_picker_selection();
|
||||||
|
self.reset_picker_input();
|
||||||
self.current_pattern = EMPTY_STRING.to_string();
|
self.current_pattern = EMPTY_STRING.to_string();
|
||||||
self.input.reset();
|
|
||||||
self.channel.find(EMPTY_STRING);
|
self.channel.find(EMPTY_STRING);
|
||||||
self.guide.find(EMPTY_STRING);
|
self.remote_control.find(EMPTY_STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the television on the screen.
|
/// Render the television on the screen.
|
||||||
@ -382,278 +359,43 @@ impl Television {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Result<()>` - An Ok result or an error.
|
/// * `Result<()>` - An Ok result or an error.
|
||||||
pub fn draw(&mut self, f: &mut Frame, area: Rect) -> Result<()> {
|
pub fn draw(&mut self, f: &mut Frame, area: Rect) -> Result<()> {
|
||||||
let dimensions = match self.mode {
|
|
||||||
Mode::Channel => &Dimensions::default(),
|
|
||||||
Mode::Guide | Mode::SendToChannel => &Dimensions::new(30, 70),
|
|
||||||
};
|
|
||||||
let layout = Layout::build(
|
let layout = Layout::build(
|
||||||
dimensions,
|
&Dimensions::default(),
|
||||||
area,
|
area,
|
||||||
match self.mode {
|
!matches!(self.mode, Mode::Channel),
|
||||||
Mode::Channel => true,
|
|
||||||
Mode::Guide | Mode::SendToChannel => false,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let metadata_block = Block::default()
|
// help bar (metadata, keymaps, logo)
|
||||||
.borders(Borders::ALL)
|
self.draw_help_bar(f, &layout)?;
|
||||||
.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::horizontal(1));
|
|
||||||
|
|
||||||
let keymaps_table = self.build_help_table()?.block(keymaps_block);
|
|
||||||
|
|
||||||
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);
|
self.results_area_height = u32::from(layout.results.height);
|
||||||
if let Some(preview_window) = layout.preview_window {
|
self.preview_pane_height = layout.preview_window.height;
|
||||||
self.preview_pane_height = preview_window.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// top left block: results
|
// top left block: results
|
||||||
let results_block = Block::default()
|
self.draw_results_list(f, &layout)?;
|
||||||
.title(
|
|
||||||
Title::from(" Results ")
|
|
||||||
.position(Position::Top)
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(get_border_style(false))
|
|
||||||
.style(Style::default())
|
|
||||||
.padding(Padding::right(1));
|
|
||||||
|
|
||||||
let result_count = match self.mode {
|
|
||||||
Mode::Channel => self.channel.result_count(),
|
|
||||||
Mode::Guide | Mode::SendToChannel => self.guide.total_count(),
|
|
||||||
};
|
|
||||||
if result_count > 0 && self.picker_state.selected().is_none() {
|
|
||||||
self.picker_state.select(Some(0));
|
|
||||||
self.relative_picker_state.select(Some(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries = match self.mode {
|
|
||||||
Mode::Channel => self.channel.results(
|
|
||||||
layout.results.height.saturating_sub(2).into(),
|
|
||||||
u32::try_from(self.picker_view_offset)?,
|
|
||||||
),
|
|
||||||
Mode::Guide | Mode::SendToChannel => self.guide.results(
|
|
||||||
layout.results.height.saturating_sub(2).into(),
|
|
||||||
u32::try_from(self.picker_view_offset)?,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let results_list = build_results_list(results_block, &entries);
|
|
||||||
|
|
||||||
f.render_stateful_widget(
|
|
||||||
results_list,
|
|
||||||
layout.results,
|
|
||||||
&mut self.relative_picker_state,
|
|
||||||
);
|
|
||||||
|
|
||||||
// bottom left block: input
|
// bottom left block: input
|
||||||
let input_block = Block::default()
|
self.draw_input_box(f, &layout)?;
|
||||||
.title(
|
|
||||||
Title::from(" Pattern ")
|
|
||||||
.position(Position::Top)
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(get_border_style(false))
|
|
||||||
.style(Style::default());
|
|
||||||
|
|
||||||
let input_block_inner = input_block.inner(layout.input);
|
let selected_entry =
|
||||||
|
self.get_selected_entry().unwrap_or(ENTRY_PLACEHOLDER);
|
||||||
|
let preview = block_on(self.previewer.preview(&selected_entry));
|
||||||
|
|
||||||
f.render_widget(input_block, layout.input);
|
// top right block: preview title
|
||||||
|
self.current_preview_total_lines = preview.total_lines();
|
||||||
|
self.draw_preview_title_block(f, &layout, &selected_entry, &preview)?;
|
||||||
|
|
||||||
// split input block into 3 parts: prompt symbol, input, result count
|
// bottom right block: preview content
|
||||||
let total_count = match self.mode {
|
self.draw_preview_content_block(
|
||||||
Mode::Channel => self.channel.total_count(),
|
f,
|
||||||
Mode::Guide | Mode::SendToChannel => self.guide.total_count(),
|
&layout,
|
||||||
};
|
&selected_entry,
|
||||||
let inner_input_chunks = RatatuiLayout::default()
|
&preview,
|
||||||
.direction(Direction::Horizontal)
|
)?;
|
||||||
.constraints([
|
|
||||||
// prompt symbol
|
|
||||||
Constraint::Length(2),
|
|
||||||
// input field
|
|
||||||
Constraint::Fill(1),
|
|
||||||
// result count
|
|
||||||
Constraint::Length(
|
|
||||||
3 * ((total_count as f32).log10().ceil() as u16 + 1) + 3,
|
|
||||||
),
|
|
||||||
// spinner
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
|
||||||
.split(input_block_inner);
|
|
||||||
|
|
||||||
let arrow_block = Block::default();
|
// remote control
|
||||||
let arrow = Paragraph::new(Span::styled(
|
if matches!(self.mode, Mode::RemoteControl) {
|
||||||
"> ",
|
self.draw_remote_control(f, &layout.remote_control.unwrap())?;
|
||||||
Style::default().fg(DEFAULT_INPUT_FG).bold(),
|
|
||||||
))
|
|
||||||
.block(arrow_block);
|
|
||||||
f.render_widget(arrow, inner_input_chunks[0]);
|
|
||||||
|
|
||||||
let interactive_input_block = Block::default();
|
|
||||||
// keep 2 for borders and 1 for cursor
|
|
||||||
let width = inner_input_chunks[1].width.max(3) - 3;
|
|
||||||
let scroll = self.input.visual_scroll(width as usize);
|
|
||||||
let input = Paragraph::new(self.input.value())
|
|
||||||
.scroll((0, u16::try_from(scroll)?))
|
|
||||||
.block(interactive_input_block)
|
|
||||||
.style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic())
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
f.render_widget(input, inner_input_chunks[1]);
|
|
||||||
|
|
||||||
if match self.mode {
|
|
||||||
Mode::Channel => self.channel.running(),
|
|
||||||
Mode::Guide | Mode::SendToChannel => self.guide.running(),
|
|
||||||
} {
|
|
||||||
f.render_stateful_widget(
|
|
||||||
self.spinner,
|
|
||||||
inner_input_chunks[3],
|
|
||||||
&mut self.spinner_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result_count_block = Block::default();
|
|
||||||
let result_count_paragraph = Paragraph::new(Span::styled(
|
|
||||||
format!(
|
|
||||||
" {} / {} ",
|
|
||||||
if result_count == 0 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
self.picker_state.selected().unwrap_or(0) + 1
|
|
||||||
},
|
|
||||||
result_count,
|
|
||||||
),
|
|
||||||
Style::default().fg(DEFAULT_RESULTS_COUNT_FG).italic(),
|
|
||||||
))
|
|
||||||
.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
|
|
||||||
// specified coordinates after rendering
|
|
||||||
f.set_cursor_position((
|
|
||||||
// Put cursor past the end of the input text
|
|
||||||
inner_input_chunks[1].x
|
|
||||||
+ u16::try_from(
|
|
||||||
self.input.visual_cursor().max(scroll) - scroll,
|
|
||||||
)?,
|
|
||||||
// Move one line down, from the border to the input line
|
|
||||||
inner_input_chunks[1].y,
|
|
||||||
));
|
|
||||||
|
|
||||||
if layout.preview_title.is_some() || layout.preview_window.is_some() {
|
|
||||||
let selected_entry =
|
|
||||||
self.get_selected_entry().unwrap_or(ENTRY_PLACEHOLDER);
|
|
||||||
let preview = block_on(self.previewer.preview(&selected_entry));
|
|
||||||
|
|
||||||
if let Some(preview_title_area) = layout.preview_title {
|
|
||||||
// top right block: preview title
|
|
||||||
self.current_preview_total_lines = preview.total_lines();
|
|
||||||
|
|
||||||
let mut preview_title_spans = Vec::new();
|
|
||||||
if let Some(icon) = &selected_entry.icon {
|
|
||||||
preview_title_spans.push(Span::styled(
|
|
||||||
{
|
|
||||||
let mut icon_str = String::from(" ");
|
|
||||||
icon_str.push(icon.icon);
|
|
||||||
icon_str.push(' ');
|
|
||||||
icon_str
|
|
||||||
},
|
|
||||||
Style::default().fg(Color::from_str(icon.color)?),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
preview_title_spans.push(Span::styled(
|
|
||||||
shrink_with_ellipsis(
|
|
||||||
&preview.title,
|
|
||||||
preview_title_area.width.saturating_sub(4) as usize,
|
|
||||||
),
|
|
||||||
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
|
|
||||||
));
|
|
||||||
let preview_title =
|
|
||||||
Paragraph::new(Line::from(preview_title_spans))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(get_border_style(false)),
|
|
||||||
)
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
f.render_widget(preview_title, preview_title_area);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(preview_area) = layout.preview_window {
|
|
||||||
// file preview
|
|
||||||
let preview_outer_block = Block::default()
|
|
||||||
.title(
|
|
||||||
Title::from(" Preview ")
|
|
||||||
.position(Position::Top)
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(get_border_style(false))
|
|
||||||
.style(Style::default())
|
|
||||||
.padding(Padding::right(1));
|
|
||||||
|
|
||||||
let preview_inner_block = Block::default()
|
|
||||||
.style(Style::default())
|
|
||||||
.padding(Padding {
|
|
||||||
top: 0,
|
|
||||||
right: 1,
|
|
||||||
bottom: 0,
|
|
||||||
left: 1,
|
|
||||||
});
|
|
||||||
let inner = preview_outer_block.inner(preview_area);
|
|
||||||
f.render_widget(preview_outer_block, preview_area);
|
|
||||||
|
|
||||||
//if let PreviewContent::Image(img) = &preview.content {
|
|
||||||
// let image_component = StatefulImage::new(None);
|
|
||||||
// frame.render_stateful_widget(
|
|
||||||
// image_component,
|
|
||||||
// inner,
|
|
||||||
// &mut img.clone(),
|
|
||||||
// );
|
|
||||||
//} else {
|
|
||||||
let preview_block = self.build_preview_paragraph(
|
|
||||||
preview_inner_block,
|
|
||||||
inner,
|
|
||||||
&preview,
|
|
||||||
selected_entry
|
|
||||||
.line_number
|
|
||||||
.map(|l| u16::try_from(l).unwrap_or(0)),
|
|
||||||
);
|
|
||||||
f.render_widget(preview_block, inner);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
use ratatui::style::{Color, Style, Stylize};
|
use ratatui::style::{Color, Style};
|
||||||
|
|
||||||
pub mod help;
|
pub(crate) mod help;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
pub mod keymap;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod logo;
|
pub mod logo;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
|
mod remote_control;
|
||||||
pub mod results;
|
pub mod results;
|
||||||
pub mod spinner;
|
pub mod spinner;
|
||||||
|
|
||||||
// input
|
// input
|
||||||
//const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200);
|
//const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200);
|
||||||
//const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150);
|
//const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150);
|
||||||
|
@ -1,237 +1,64 @@
|
|||||||
use color_eyre::eyre::{OptionExt, Result};
|
use crate::television::Television;
|
||||||
use ratatui::{
|
use crate::ui::layout::Layout;
|
||||||
layout::Constraint,
|
use crate::ui::logo::build_logo_paragraph;
|
||||||
style::{Color, Style},
|
use ratatui::layout::Rect;
|
||||||
text::{Line, Span},
|
use ratatui::prelude::{Color, Style};
|
||||||
widgets::{Cell, Row, Table},
|
use ratatui::widgets::{Block, BorderType, Borders, Padding};
|
||||||
};
|
use ratatui::Frame;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::{
|
pub fn draw_logo_block(f: &mut Frame, area: Rect) {
|
||||||
action::Action,
|
let logo_block = Block::default()
|
||||||
event::Key,
|
.borders(Borders::ALL)
|
||||||
television::{Mode, Television},
|
.border_type(BorderType::Rounded)
|
||||||
};
|
.border_style(Style::default().fg(Color::Blue))
|
||||||
|
.style(Style::default().fg(Color::Yellow))
|
||||||
|
.padding(Padding::horizontal(1));
|
||||||
|
|
||||||
const ACTION_COLOR: Color = Color::DarkGray;
|
let logo_paragraph = build_logo_paragraph().block(logo_block);
|
||||||
const KEY_COLOR: Color = Color::LightYellow;
|
|
||||||
|
f.render_widget(logo_paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
pub fn build_help_table<'a>(&self) -> Result<Table<'a>> {
|
pub(crate) fn draw_help_bar(
|
||||||
match self.mode {
|
&self,
|
||||||
Mode::Channel => self.build_help_table_for_channel(),
|
f: &mut Frame,
|
||||||
Mode::Guide => self.build_help_table_for_channel_selection(),
|
layout: &Layout,
|
||||||
Mode::SendToChannel => self.build_help_table_for_channel(),
|
) -> color_eyre::Result<()> {
|
||||||
}
|
self.draw_metadata_block(f, layout.help_bar_left);
|
||||||
|
self.draw_keymaps_block(f, layout.help_bar_middle)?;
|
||||||
|
draw_logo_block(f, layout.help_bar_right);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_help_table_for_channel<'a>(&self) -> Result<Table<'a>> {
|
fn draw_metadata_block(&self, f: &mut Frame, area: Rect) {
|
||||||
let keymap = self.keymap_for_mode()?;
|
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());
|
||||||
|
|
||||||
// Results navigation
|
let metadata_table = self.build_metadata_table().block(metadata_block);
|
||||||
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
|
f.render_widget(metadata_table, area);
|
||||||
let up_keys =
|
|
||||||
keys_for_action(keymap, &Action::ScrollPreviewHalfPageUp);
|
|
||||||
let down_keys =
|
|
||||||
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_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"✓ Select entry",
|
|
||||||
vec![select_entry_keys],
|
|
||||||
));
|
|
||||||
|
|
||||||
// Send to channel
|
|
||||||
let send_to_channel_keys =
|
|
||||||
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_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"⨀ Switch channels",
|
|
||||||
vec![switch_channels_keys],
|
|
||||||
));
|
|
||||||
|
|
||||||
// MISC line (quit, help, etc.)
|
|
||||||
// Quit ⏼
|
|
||||||
let quit_keys = keys_for_action(keymap, &Action::Quit);
|
|
||||||
let quit_row =
|
|
||||||
Row::new(build_cells_for_key_groups("⏼ Quit", vec![quit_keys]));
|
|
||||||
|
|
||||||
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_table_for_channel_selection<'a>(&self) -> Result<Table<'a>> {
|
fn draw_keymaps_block(
|
||||||
let keymap = self.keymap_for_mode()?;
|
&self,
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
) -> color_eyre::Result<()> {
|
||||||
|
let keymaps_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(Color::Blue))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::horizontal(1));
|
||||||
|
|
||||||
// Results navigation
|
let keymaps_table = self.build_keymap_table()?.block(keymaps_block);
|
||||||
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
|
f.render_widget(keymaps_table, area);
|
||||||
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
|
Ok(())
|
||||||
let select_entry_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Select entry",
|
|
||||||
vec![select_entry_keys],
|
|
||||||
));
|
|
||||||
|
|
||||||
// Switch channels
|
|
||||||
let switch_channels_keys =
|
|
||||||
keys_for_action(keymap, &Action::ToggleChannelSelection);
|
|
||||||
let switch_channels_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Switch channels",
|
|
||||||
vec![switch_channels_keys],
|
|
||||||
));
|
|
||||||
|
|
||||||
// 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(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.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A reference to the keymap for the current mode.
|
|
||||||
fn keymap_for_mode(&self) -> Result<&HashMap<Key, Action>> {
|
|
||||||
let keymap = self
|
|
||||||
.config
|
|
||||||
.keybindings
|
|
||||||
.get(&self.mode)
|
|
||||||
.ok_or_eyre("No keybindings found for the current Mode")?;
|
|
||||||
Ok(keymap)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the corresponding spans for a group of keys.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// - `group_name`: The name of the group.
|
|
||||||
/// - `key_groups`: A vector of vectors of strings representing the keys for each group.
|
|
||||||
/// Each vector of strings represents a group of alternate keys for a given `Action`.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A vector of `Span`s representing the key groups.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use ratatui::text::Span;
|
|
||||||
/// use television::ui::help::build_spans_for_key_groups;
|
|
||||||
///
|
|
||||||
/// let key_groups = vec![
|
|
||||||
/// // alternate keys for the `SelectNextEntry` action
|
|
||||||
/// vec!["j".to_string(), "n".to_string()],
|
|
||||||
/// // alternate keys for the `SelectPrevEntry` action
|
|
||||||
/// vec!["k".to_string(), "p".to_string()],
|
|
||||||
/// ];
|
|
||||||
/// let spans = build_spans_for_key_groups("↕ Results", key_groups);
|
|
||||||
///
|
|
||||||
/// assert_eq!(spans.len(), 5);
|
|
||||||
/// ```
|
|
||||||
fn build_cells_for_key_groups(
|
|
||||||
group_name: &str,
|
|
||||||
key_groups: Vec<Vec<String>>,
|
|
||||||
) -> 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 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(", ");
|
|
||||||
Span::styled(key_group, Style::default().fg(KEY_COLOR))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
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)));
|
|
||||||
cells.push(Cell::from(Line::from(spans)));
|
|
||||||
|
|
||||||
cells
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the keys for a given action.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// - `keymap`: A hashmap of keybindings.
|
|
||||||
/// - `action`: The action to get the keys for.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A vector of strings representing the keys for the given action.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use std::collections::HashMap;
|
|
||||||
/// use television::action::Action;
|
|
||||||
/// use television::ui::help::keys_for_action;
|
|
||||||
///
|
|
||||||
/// let mut keymap = HashMap::new();
|
|
||||||
/// keymap.insert('j', Action::SelectNextEntry);
|
|
||||||
/// keymap.insert('k', Action::SelectPrevEntry);
|
|
||||||
///
|
|
||||||
/// let keys = keys_for_action(&keymap, Action::SelectNextEntry);
|
|
||||||
///
|
|
||||||
/// assert_eq!(keys, vec!["j"]);
|
|
||||||
/// ```
|
|
||||||
fn keys_for_action(
|
|
||||||
keymap: &HashMap<Key, Action>,
|
|
||||||
action: &Action,
|
|
||||||
) -> Vec<String> {
|
|
||||||
keymap
|
|
||||||
.iter()
|
|
||||||
.filter(|(_key, act)| *act == action)
|
|
||||||
.map(|(key, _act)| format!("{key}"))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
|
use crate::channels::OnAir;
|
||||||
|
use crate::television::Television;
|
||||||
|
use crate::ui::get_border_style;
|
||||||
|
use crate::ui::layout::Layout;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use ratatui::layout::{
|
||||||
|
Alignment, Constraint, Direction, Layout as RatatuiLayout,
|
||||||
|
};
|
||||||
|
use ratatui::prelude::{Span, Style};
|
||||||
|
use ratatui::style::Stylize;
|
||||||
|
use ratatui::text::Line;
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
pub mod backend;
|
pub mod backend;
|
||||||
|
|
||||||
@ -332,12 +346,12 @@ impl Input {
|
|||||||
self.value.as_str()
|
self.value.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the currect cursor placement.
|
/// Get the correct cursor placement.
|
||||||
pub fn cursor(&self) -> usize {
|
pub fn cursor(&self) -> usize {
|
||||||
self.cursor
|
self.cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current cursor position with account for multispace characters.
|
/// Get the current cursor position with account for multi space characters.
|
||||||
pub fn visual_cursor(&self) -> usize {
|
pub fn visual_cursor(&self) -> usize {
|
||||||
if self.cursor == 0 {
|
if self.cursor == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
@ -355,7 +369,7 @@ impl Input {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the scroll position with account for multispace characters.
|
/// Get the scroll position with account for multi space characters.
|
||||||
pub fn visual_scroll(&self, width: usize) -> usize {
|
pub fn visual_scroll(&self, width: usize) -> usize {
|
||||||
let scroll = self.visual_cursor().max(width) - width;
|
let scroll = self.visual_cursor().max(width) - width;
|
||||||
let mut uscroll = 0;
|
let mut uscroll = 0;
|
||||||
@ -398,9 +412,113 @@ impl std::fmt::Display for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Television {
|
||||||
|
pub(crate) fn draw_input_box(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame,
|
||||||
|
layout: &Layout,
|
||||||
|
) -> Result<()> {
|
||||||
|
let input_block = Block::default()
|
||||||
|
.title_top(Line::from(" Pattern ").alignment(Alignment::Center))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(get_border_style(false))
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
let input_block_inner = input_block.inner(layout.input);
|
||||||
|
|
||||||
|
f.render_widget(input_block, layout.input);
|
||||||
|
|
||||||
|
// split input block into 4 parts: prompt symbol, input, result count, spinner
|
||||||
|
let total_count = self.channel.total_count();
|
||||||
|
let inner_input_chunks = RatatuiLayout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
// prompt symbol
|
||||||
|
Constraint::Length(2),
|
||||||
|
// input field
|
||||||
|
Constraint::Fill(1),
|
||||||
|
// result count
|
||||||
|
Constraint::Length(
|
||||||
|
3 * ((total_count as f32).log10().ceil() as u16 + 1) + 3,
|
||||||
|
),
|
||||||
|
// spinner
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.split(input_block_inner);
|
||||||
|
|
||||||
|
let arrow_block = Block::default();
|
||||||
|
let arrow = Paragraph::new(Span::styled(
|
||||||
|
"> ",
|
||||||
|
Style::default()
|
||||||
|
.fg(crate::television::DEFAULT_INPUT_FG)
|
||||||
|
.bold(),
|
||||||
|
))
|
||||||
|
.block(arrow_block);
|
||||||
|
f.render_widget(arrow, inner_input_chunks[0]);
|
||||||
|
|
||||||
|
let interactive_input_block = Block::default();
|
||||||
|
// keep 2 for borders and 1 for cursor
|
||||||
|
let width = inner_input_chunks[1].width.max(3) - 3;
|
||||||
|
let scroll = self.results_picker.input.visual_scroll(width as usize);
|
||||||
|
let input = Paragraph::new(self.results_picker.input.value())
|
||||||
|
.scroll((0, u16::try_from(scroll)?))
|
||||||
|
.block(interactive_input_block)
|
||||||
|
.style(
|
||||||
|
Style::default()
|
||||||
|
.fg(crate::television::DEFAULT_INPUT_FG)
|
||||||
|
.bold()
|
||||||
|
.italic(),
|
||||||
|
)
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(input, inner_input_chunks[1]);
|
||||||
|
|
||||||
|
if self.channel.running() {
|
||||||
|
f.render_stateful_widget(
|
||||||
|
self.spinner,
|
||||||
|
inner_input_chunks[3],
|
||||||
|
&mut self.spinner_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result_count = self.channel.result_count();
|
||||||
|
let result_count_block = Block::default();
|
||||||
|
let result_count_paragraph = Paragraph::new(Span::styled(
|
||||||
|
format!(
|
||||||
|
" {} / {} ",
|
||||||
|
if result_count == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.results_picker.selected().unwrap_or(0) + 1
|
||||||
|
},
|
||||||
|
result_count,
|
||||||
|
),
|
||||||
|
Style::default()
|
||||||
|
.fg(crate::television::DEFAULT_RESULTS_COUNT_FG)
|
||||||
|
.italic(),
|
||||||
|
))
|
||||||
|
.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
|
||||||
|
// specified coordinates after rendering
|
||||||
|
f.set_cursor_position((
|
||||||
|
// Put cursor past the end of the input text
|
||||||
|
inner_input_chunks[1].x
|
||||||
|
+ u16::try_from(
|
||||||
|
self.results_picker.input.visual_cursor().max(scroll)
|
||||||
|
- scroll,
|
||||||
|
)?,
|
||||||
|
// Move one line down, from the border to the input line
|
||||||
|
inner_input_chunks[1].y,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
const TEXT: &str = "first second, third.";
|
const TEXT: &str = "first second, third.";
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
241
crates/television/ui/keymap.rs
Normal file
241
crates/television/ui/keymap.rs
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
use color_eyre::eyre::{OptionExt, Result};
|
||||||
|
use ratatui::{
|
||||||
|
layout::Constraint,
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Cell, Row, Table},
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
action::Action,
|
||||||
|
event::Key,
|
||||||
|
television::{Mode, Television},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACTION_COLOR: Color = Color::DarkGray;
|
||||||
|
const KEY_COLOR: Color = Color::LightYellow;
|
||||||
|
|
||||||
|
impl Television {
|
||||||
|
pub fn build_keymap_table<'a>(&self) -> Result<Table<'a>> {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Channel => self.build_keymap_table_for_channel(),
|
||||||
|
Mode::RemoteControl => {
|
||||||
|
self.build_keymap_table_for_channel_selection()
|
||||||
|
}
|
||||||
|
Mode::SendToChannel => self.build_keymap_table_for_channel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_keymap_table_for_channel<'a>(&self) -> Result<Table<'a>> {
|
||||||
|
let keymap = self.keymap_for_mode()?;
|
||||||
|
|
||||||
|
// Results navigation
|
||||||
|
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 down_keys =
|
||||||
|
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_row = Row::new(build_cells_for_key_groups(
|
||||||
|
"✓ Select entry",
|
||||||
|
vec![select_entry_keys],
|
||||||
|
));
|
||||||
|
|
||||||
|
// Send to channel
|
||||||
|
let send_to_channel_keys =
|
||||||
|
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::ToggleRemoteControl);
|
||||||
|
let switch_channels_row = Row::new(build_cells_for_key_groups(
|
||||||
|
"⨀ Remote control",
|
||||||
|
vec![switch_channels_keys],
|
||||||
|
));
|
||||||
|
|
||||||
|
// MISC line (quit, help, etc.)
|
||||||
|
// Quit ⏼
|
||||||
|
let quit_keys = keys_for_action(keymap, &Action::Quit);
|
||||||
|
let quit_row =
|
||||||
|
Row::new(build_cells_for_key_groups("⏼ Quit", vec![quit_keys]));
|
||||||
|
|
||||||
|
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_keymap_table_for_channel_selection<'a>(
|
||||||
|
&self,
|
||||||
|
) -> Result<Table<'a>> {
|
||||||
|
let keymap = self.keymap_for_mode()?;
|
||||||
|
|
||||||
|
// Results navigation
|
||||||
|
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_row = Row::new(build_cells_for_key_groups(
|
||||||
|
"Select entry",
|
||||||
|
vec![select_entry_keys],
|
||||||
|
));
|
||||||
|
|
||||||
|
// Switch channels
|
||||||
|
let switch_channels_keys =
|
||||||
|
keys_for_action(keymap, &Action::ToggleRemoteControl);
|
||||||
|
let switch_channels_row = Row::new(build_cells_for_key_groups(
|
||||||
|
"Switch channels",
|
||||||
|
vec![switch_channels_keys],
|
||||||
|
));
|
||||||
|
|
||||||
|
// 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(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.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A reference to the keymap for the current mode.
|
||||||
|
fn keymap_for_mode(&self) -> Result<&HashMap<Key, Action>> {
|
||||||
|
let keymap = self
|
||||||
|
.config
|
||||||
|
.keybindings
|
||||||
|
.get(&self.mode)
|
||||||
|
.ok_or_eyre("No keybindings found for the current Mode")?;
|
||||||
|
Ok(keymap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the corresponding spans for a group of keys.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `group_name`: The name of the group.
|
||||||
|
/// - `key_groups`: A vector of vectors of strings representing the keys for each group.
|
||||||
|
/// Each vector of strings represents a group of alternate keys for a given `Action`.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A vector of `Span`s representing the key groups.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use ratatui::text::Span;
|
||||||
|
/// use television::ui::help::build_spans_for_key_groups;
|
||||||
|
///
|
||||||
|
/// let key_groups = vec![
|
||||||
|
/// // alternate keys for the `SelectNextEntry` action
|
||||||
|
/// vec!["j".to_string(), "n".to_string()],
|
||||||
|
/// // alternate keys for the `SelectPrevEntry` action
|
||||||
|
/// vec!["k".to_string(), "p".to_string()],
|
||||||
|
/// ];
|
||||||
|
/// let spans = build_spans_for_key_groups("↕ Results", key_groups);
|
||||||
|
///
|
||||||
|
/// assert_eq!(spans.len(), 5);
|
||||||
|
/// ```
|
||||||
|
fn build_cells_for_key_groups(
|
||||||
|
group_name: &str,
|
||||||
|
key_groups: Vec<Vec<String>>,
|
||||||
|
) -> Vec<Cell> {
|
||||||
|
if key_groups.is_empty() || key_groups.iter().all(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 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(", ");
|
||||||
|
Span::styled(key_group, Style::default().fg(KEY_COLOR))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
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)));
|
||||||
|
cells.push(Cell::from(Line::from(spans)));
|
||||||
|
|
||||||
|
cells
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the keys for a given action.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `keymap`: A hashmap of keybindings.
|
||||||
|
/// - `action`: The action to get the keys for.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A vector of strings representing the keys for the given action.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use std::collections::HashMap;
|
||||||
|
/// use television::action::Action;
|
||||||
|
/// use television::ui::help::keys_for_action;
|
||||||
|
///
|
||||||
|
/// let mut keymap = HashMap::new();
|
||||||
|
/// keymap.insert('j', Action::SelectNextEntry);
|
||||||
|
/// keymap.insert('k', Action::SelectPrevEntry);
|
||||||
|
///
|
||||||
|
/// let keys = keys_for_action(&keymap, Action::SelectNextEntry);
|
||||||
|
///
|
||||||
|
/// assert_eq!(keys, vec!["j"]);
|
||||||
|
/// ```
|
||||||
|
fn keys_for_action(
|
||||||
|
keymap: &HashMap<Key, Action>,
|
||||||
|
action: &Action,
|
||||||
|
) -> Vec<String> {
|
||||||
|
keymap
|
||||||
|
.iter()
|
||||||
|
.filter(|(_key, act)| *act == action)
|
||||||
|
.map(|(key, _act)| format!("{key}"))
|
||||||
|
.collect()
|
||||||
|
}
|
@ -24,8 +24,9 @@ pub struct Layout {
|
|||||||
pub help_bar_right: Rect,
|
pub help_bar_right: Rect,
|
||||||
pub results: Rect,
|
pub results: Rect,
|
||||||
pub input: Rect,
|
pub input: Rect,
|
||||||
pub preview_title: Option<Rect>,
|
pub preview_title: Rect,
|
||||||
pub preview_window: Option<Rect>,
|
pub preview_window: Rect,
|
||||||
|
pub remote_control: Option<Rect>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
@ -35,8 +36,9 @@ impl Layout {
|
|||||||
help_bar_right: Rect,
|
help_bar_right: Rect,
|
||||||
results: Rect,
|
results: Rect,
|
||||||
input: Rect,
|
input: Rect,
|
||||||
preview_title: Option<Rect>,
|
preview_title: Rect,
|
||||||
preview_window: Option<Rect>,
|
preview_window: Rect,
|
||||||
|
remote_control: Option<Rect>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_bar_left,
|
help_bar_left,
|
||||||
@ -46,13 +48,14 @@ impl Layout {
|
|||||||
input,
|
input,
|
||||||
preview_title,
|
preview_title,
|
||||||
preview_window,
|
preview_window,
|
||||||
|
remote_control,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(
|
pub fn build(
|
||||||
dimensions: &Dimensions,
|
dimensions: &Dimensions,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
with_preview: bool,
|
with_remote: bool,
|
||||||
) -> 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)
|
||||||
@ -65,60 +68,56 @@ impl Layout {
|
|||||||
let help_bar_chunks = layout::Layout::default()
|
let help_bar_chunks = layout::Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([
|
.constraints([
|
||||||
|
// metadata
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
|
// keymaps
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
|
// logo
|
||||||
Constraint::Length(24),
|
Constraint::Length(24),
|
||||||
])
|
])
|
||||||
.split(hz_chunks[0]);
|
.split(hz_chunks[0]);
|
||||||
|
|
||||||
if with_preview {
|
// split the main block into two vertical chunks
|
||||||
// split the main block into two vertical chunks
|
let constraints = if with_remote {
|
||||||
let vt_chunks = layout::Layout::default()
|
vec![
|
||||||
.direction(Direction::Horizontal)
|
Constraint::Fill(1),
|
||||||
.constraints([
|
Constraint::Fill(1),
|
||||||
Constraint::Percentage(50),
|
Constraint::Length(24),
|
||||||
Constraint::Percentage(50),
|
]
|
||||||
])
|
|
||||||
.split(hz_chunks[1]);
|
|
||||||
|
|
||||||
// left block: results + input field
|
|
||||||
let left_chunks = layout::Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(10), Constraint::Length(3)])
|
|
||||||
.split(vt_chunks[0]);
|
|
||||||
|
|
||||||
// right block: preview title + preview
|
|
||||||
let right_chunks = layout::Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(10)])
|
|
||||||
.split(vt_chunks[1]);
|
|
||||||
|
|
||||||
Self::new(
|
|
||||||
help_bar_chunks[0],
|
|
||||||
help_bar_chunks[1],
|
|
||||||
help_bar_chunks[2],
|
|
||||||
left_chunks[0],
|
|
||||||
left_chunks[1],
|
|
||||||
Some(right_chunks[0]),
|
|
||||||
Some(right_chunks[1]),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// split the main block into two vertical chunks
|
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||||
let chunks = layout::Layout::default()
|
};
|
||||||
.direction(Direction::Vertical)
|
let vt_chunks = layout::Layout::default()
|
||||||
.constraints([Constraint::Min(10), Constraint::Length(3)])
|
.direction(Direction::Horizontal)
|
||||||
.split(hz_chunks[1]);
|
.constraints(constraints)
|
||||||
|
.split(hz_chunks[1]);
|
||||||
|
|
||||||
Self::new(
|
// left block: results + input field
|
||||||
help_bar_chunks[0],
|
let left_chunks = layout::Layout::default()
|
||||||
help_bar_chunks[1],
|
.direction(Direction::Vertical)
|
||||||
help_bar_chunks[2],
|
.constraints([Constraint::Min(10), Constraint::Length(3)])
|
||||||
chunks[0],
|
.split(vt_chunks[0]);
|
||||||
chunks[1],
|
|
||||||
None,
|
// right block: preview title + preview
|
||||||
None,
|
let right_chunks = layout::Layout::default()
|
||||||
)
|
.direction(Direction::Vertical)
|
||||||
}
|
.constraints([Constraint::Length(3), Constraint::Min(10)])
|
||||||
|
.split(vt_chunks[1]);
|
||||||
|
|
||||||
|
Self::new(
|
||||||
|
help_bar_chunks[0],
|
||||||
|
help_bar_chunks[1],
|
||||||
|
help_bar_chunks[2],
|
||||||
|
left_chunks[0],
|
||||||
|
left_chunks[1],
|
||||||
|
right_chunks[0],
|
||||||
|
right_chunks[1],
|
||||||
|
if with_remote {
|
||||||
|
Some(vt_chunks[2])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,3 +24,33 @@ pub fn build_logo_paragraph<'a>() -> Paragraph<'a> {
|
|||||||
let logo_paragraph = Paragraph::new(lines);
|
let logo_paragraph = Paragraph::new(lines);
|
||||||
logo_paragraph
|
logo_paragraph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REMOTE_LOGO: &str = r"
|
||||||
|
_____________
|
||||||
|
/ \
|
||||||
|
| (*) (#) |
|
||||||
|
| |
|
||||||
|
| (1) (2) (3) |
|
||||||
|
| (4) (5) (6) |
|
||||||
|
| (7) (8) (9) |
|
||||||
|
| |
|
||||||
|
| _ |
|
||||||
|
| | | |
|
||||||
|
| (_¯(0)¯_) |
|
||||||
|
| | | |
|
||||||
|
| ¯ |
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| === === === |
|
||||||
|
| |
|
||||||
|
| T.V |
|
||||||
|
`-------------´";
|
||||||
|
|
||||||
|
pub fn build_remote_logo_paragraph<'a>() -> Paragraph<'a> {
|
||||||
|
let lines = REMOTE_LOGO
|
||||||
|
.lines()
|
||||||
|
.map(std::convert::Into::into)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let logo_paragraph = Paragraph::new(lines);
|
||||||
|
logo_paragraph
|
||||||
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
|
use crate::entry::Entry;
|
||||||
use crate::previewers::{
|
use crate::previewers::{
|
||||||
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG,
|
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG,
|
||||||
};
|
};
|
||||||
use crate::television::Television;
|
use crate::television::Television;
|
||||||
use crate::utils::strings::EMPTY_STRING;
|
use crate::ui::get_border_style;
|
||||||
|
use crate::ui::layout::Layout;
|
||||||
|
use crate::utils::strings::{shrink_with_ellipsis, EMPTY_STRING};
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
use ratatui::layout::{Alignment, Rect};
|
use ratatui::layout::{Alignment, Rect};
|
||||||
use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text};
|
use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text};
|
||||||
use ratatui::widgets::{Block, Paragraph, Wrap};
|
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
||||||
|
use ratatui::Frame;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use syntect::highlighting::Color as SyntectColor;
|
use syntect::highlighting::Color as SyntectColor;
|
||||||
|
|
||||||
@ -17,6 +23,91 @@ const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70);
|
|||||||
const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150);
|
const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150);
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
|
pub(crate) fn draw_preview_title_block(
|
||||||
|
&self,
|
||||||
|
f: &mut Frame,
|
||||||
|
layout: &Layout,
|
||||||
|
selected_entry: &Entry,
|
||||||
|
preview: &Arc<Preview>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut preview_title_spans = Vec::new();
|
||||||
|
if let Some(icon) = &selected_entry.icon {
|
||||||
|
preview_title_spans.push(Span::styled(
|
||||||
|
{
|
||||||
|
// FIXME: this should be done using padding on the parent block
|
||||||
|
let mut icon_str = String::from(" ");
|
||||||
|
icon_str.push(icon.icon);
|
||||||
|
icon_str.push(' ');
|
||||||
|
icon_str
|
||||||
|
},
|
||||||
|
Style::default().fg(Color::from_str(icon.color)?),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
preview_title_spans.push(Span::styled(
|
||||||
|
shrink_with_ellipsis(
|
||||||
|
&preview.title,
|
||||||
|
layout.preview_window.width.saturating_sub(4) as usize,
|
||||||
|
),
|
||||||
|
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
|
||||||
|
));
|
||||||
|
let preview_title = Paragraph::new(Line::from(preview_title_spans))
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(get_border_style(false)),
|
||||||
|
)
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(preview_title, layout.preview_title);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn draw_preview_content_block(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame,
|
||||||
|
layout: &Layout,
|
||||||
|
selected_entry: &Entry,
|
||||||
|
preview: &Arc<Preview>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let preview_outer_block = Block::default()
|
||||||
|
.title_top(Line::from(" Preview ").alignment(Alignment::Center))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(get_border_style(false))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::right(1));
|
||||||
|
|
||||||
|
let preview_inner_block =
|
||||||
|
Block::default().style(Style::default()).padding(Padding {
|
||||||
|
top: 0,
|
||||||
|
right: 1,
|
||||||
|
bottom: 0,
|
||||||
|
left: 1,
|
||||||
|
});
|
||||||
|
let inner = preview_outer_block.inner(layout.preview_window);
|
||||||
|
f.render_widget(preview_outer_block, layout.preview_window);
|
||||||
|
|
||||||
|
//if let PreviewContent::Image(img) = &preview.content {
|
||||||
|
// let image_component = StatefulImage::new(None);
|
||||||
|
// frame.render_stateful_widget(
|
||||||
|
// image_component,
|
||||||
|
// inner,
|
||||||
|
// &mut img.clone(),
|
||||||
|
// );
|
||||||
|
//} else {
|
||||||
|
let preview_block = self.build_preview_paragraph(
|
||||||
|
preview_inner_block,
|
||||||
|
inner,
|
||||||
|
&preview,
|
||||||
|
selected_entry
|
||||||
|
.line_number
|
||||||
|
.map(|l| u16::try_from(l).unwrap_or(0)),
|
||||||
|
);
|
||||||
|
f.render_widget(preview_block, inner);
|
||||||
|
//}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
const FILL_CHAR_SLANTED: char = '╱';
|
const FILL_CHAR_SLANTED: char = '╱';
|
||||||
const FILL_CHAR_EMPTY: char = ' ';
|
const FILL_CHAR_EMPTY: char = ' ';
|
||||||
|
|
||||||
@ -45,7 +136,7 @@ impl Television {
|
|||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
Span::styled(" │ ",
|
Span::styled(" │ ",
|
||||||
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()),
|
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
line.to_string(),
|
line.to_string(),
|
||||||
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg(
|
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg(
|
||||||
@ -83,9 +174,9 @@ impl Television {
|
|||||||
self.preview_scroll.unwrap_or(0),
|
self.preview_scroll.unwrap_or(0),
|
||||||
self.preview_pane_height,
|
self.preview_pane_height,
|
||||||
)
|
)
|
||||||
.block(preview_block)
|
.block(preview_block)
|
||||||
.alignment(Alignment::Left)
|
.alignment(Alignment::Left)
|
||||||
.scroll((self.preview_scroll.unwrap_or(0), 0))
|
.scroll((self.preview_scroll.unwrap_or(0), 0))
|
||||||
}
|
}
|
||||||
// meta
|
// meta
|
||||||
PreviewContent::Loading => self
|
PreviewContent::Loading => self
|
||||||
@ -277,6 +368,6 @@ pub fn convert_syn_region_to_span<'a>(
|
|||||||
|
|
||||||
fn convert_syn_color_to_ratatui_color(
|
fn convert_syn_color_to_ratatui_color(
|
||||||
color: syntect::highlighting::Color,
|
color: syntect::highlighting::Color,
|
||||||
) -> ratatui::style::Color {
|
) -> Color {
|
||||||
ratatui::style::Color::Rgb(color.r, color.g, color.b)
|
Color::Rgb(color.r, color.g, color.b)
|
||||||
}
|
}
|
||||||
|
147
crates/television/ui/remote_control.rs
Normal file
147
crates/television/ui/remote_control.rs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
use crate::channels::OnAir;
|
||||||
|
use crate::television::Television;
|
||||||
|
use crate::ui::get_border_style;
|
||||||
|
use crate::ui::logo::build_remote_logo_paragraph;
|
||||||
|
use crate::ui::results::build_results_list;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
|
use ratatui::prelude::Style;
|
||||||
|
use ratatui::style::{Color, Stylize};
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::{
|
||||||
|
Block, BorderType, Borders, ListDirection, Padding, Paragraph,
|
||||||
|
};
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
impl Television {
|
||||||
|
pub fn draw_remote_control(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame,
|
||||||
|
area: &Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(20),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(*area);
|
||||||
|
self.draw_rc_channels(f, &layout[0])?;
|
||||||
|
self.draw_rc_input(f, &layout[1])?;
|
||||||
|
draw_rc_logo(f, layout[2]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_rc_channels(&mut self, f: &mut Frame, area: &Rect) -> Result<()> {
|
||||||
|
let rc_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(get_border_style(false))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::right(1));
|
||||||
|
|
||||||
|
let result_count = self.remote_control.result_count();
|
||||||
|
if result_count > 0 && self.rc_picker.selected().is_none() {
|
||||||
|
self.rc_picker.select(Some(0));
|
||||||
|
self.rc_picker.relative_select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = self.remote_control.results(
|
||||||
|
area.height.saturating_sub(2).into(),
|
||||||
|
u32::try_from(self.rc_picker.view_offset)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let channel_list =
|
||||||
|
build_results_list(rc_block, &entries, ListDirection::TopToBottom);
|
||||||
|
|
||||||
|
f.render_stateful_widget(
|
||||||
|
channel_list,
|
||||||
|
*area,
|
||||||
|
&mut self.rc_picker.state,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_rc_input(&mut self, f: &mut Frame, area: &Rect) -> Result<()> {
|
||||||
|
let input_block = Block::default()
|
||||||
|
.title_top(
|
||||||
|
Line::from("Remote Control").alignment(Alignment::Center),
|
||||||
|
)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(get_border_style(false))
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
let input_block_inner = input_block.inner(*area);
|
||||||
|
|
||||||
|
f.render_widget(input_block, *area);
|
||||||
|
|
||||||
|
// split input block into 2 parts: prompt symbol, input
|
||||||
|
let inner_input_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
// prompt symbol
|
||||||
|
Constraint::Length(2),
|
||||||
|
// input field
|
||||||
|
Constraint::Fill(1),
|
||||||
|
])
|
||||||
|
.split(input_block_inner);
|
||||||
|
|
||||||
|
let prompt_symbol_block = Block::default();
|
||||||
|
let arrow = Paragraph::new(Span::styled(
|
||||||
|
"> ",
|
||||||
|
Style::default()
|
||||||
|
.fg(crate::television::DEFAULT_INPUT_FG)
|
||||||
|
.bold(),
|
||||||
|
))
|
||||||
|
.block(prompt_symbol_block);
|
||||||
|
f.render_widget(arrow, inner_input_chunks[0]);
|
||||||
|
|
||||||
|
let interactive_input_block = Block::default();
|
||||||
|
// keep 2 for borders and 1 for cursor
|
||||||
|
let width = inner_input_chunks[1].width.max(3) - 3;
|
||||||
|
let scroll = self.rc_picker.input.visual_scroll(width as usize);
|
||||||
|
let input = Paragraph::new(self.rc_picker.input.value())
|
||||||
|
.scroll((0, u16::try_from(scroll)?))
|
||||||
|
.block(interactive_input_block)
|
||||||
|
.style(
|
||||||
|
Style::default()
|
||||||
|
.fg(crate::television::DEFAULT_INPUT_FG)
|
||||||
|
.bold()
|
||||||
|
.italic(),
|
||||||
|
)
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(input, inner_input_chunks[1]);
|
||||||
|
|
||||||
|
// Make the cursor visible and ask tui-rs to put it at the
|
||||||
|
// specified coordinates after rendering
|
||||||
|
f.set_cursor_position((
|
||||||
|
// Put cursor past the end of the input text
|
||||||
|
inner_input_chunks[1].x
|
||||||
|
+ u16::try_from(
|
||||||
|
self.rc_picker.input.visual_cursor().max(scroll) - scroll,
|
||||||
|
)?,
|
||||||
|
// Move one line down, from the border to the input line
|
||||||
|
inner_input_chunks[1].y,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_rc_logo(f: &mut Frame, area: Rect) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
let logo_paragraph = build_remote_logo_paragraph()
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(logo_block);
|
||||||
|
|
||||||
|
f.render_widget(logo_paragraph, area);
|
||||||
|
}
|
@ -1,7 +1,16 @@
|
|||||||
|
use crate::channels::OnAir;
|
||||||
use crate::entry::Entry;
|
use crate::entry::Entry;
|
||||||
|
use crate::television::Television;
|
||||||
|
use crate::ui::get_border_style;
|
||||||
|
use crate::ui::layout::Layout;
|
||||||
use crate::utils::strings::{next_char_boundary, slice_at_char_boundaries};
|
use crate::utils::strings::{next_char_boundary, slice_at_char_boundaries};
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use ratatui::layout::Alignment;
|
||||||
use ratatui::prelude::{Color, Line, Span, Style, Stylize};
|
use ratatui::prelude::{Color, Line, Span, Style, Stylize};
|
||||||
use ratatui::widgets::{Block, List, ListDirection};
|
use ratatui::widgets::{
|
||||||
|
Block, BorderType, Borders, List, ListDirection, Padding,
|
||||||
|
};
|
||||||
|
use ratatui::Frame;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
@ -13,6 +22,7 @@ const DEFAULT_RESULT_SELECTED_BG: Color = Color::Rgb(50, 50, 50);
|
|||||||
pub fn build_results_list<'a, 'b>(
|
pub fn build_results_list<'a, 'b>(
|
||||||
results_block: Block<'b>,
|
results_block: Block<'b>,
|
||||||
entries: &'a [Entry],
|
entries: &'a [Entry],
|
||||||
|
list_direction: ListDirection,
|
||||||
) -> List<'a>
|
) -> List<'a>
|
||||||
where
|
where
|
||||||
'b: 'a,
|
'b: 'a,
|
||||||
@ -107,8 +117,48 @@ where
|
|||||||
}
|
}
|
||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
}))
|
}))
|
||||||
.direction(ListDirection::BottomToTop)
|
.direction(list_direction)
|
||||||
.highlight_style(Style::default().bg(DEFAULT_RESULT_SELECTED_BG))
|
.highlight_style(Style::default().bg(DEFAULT_RESULT_SELECTED_BG))
|
||||||
.highlight_symbol("> ")
|
.highlight_symbol("> ")
|
||||||
.block(results_block)
|
.block(results_block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Television {
|
||||||
|
pub(crate) fn draw_results_list(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame,
|
||||||
|
layout: &Layout,
|
||||||
|
) -> Result<()> {
|
||||||
|
let results_block = Block::default()
|
||||||
|
.title_top(Line::from(" Results ").alignment(Alignment::Center))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(get_border_style(false))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::right(1));
|
||||||
|
|
||||||
|
let result_count = self.channel.result_count();
|
||||||
|
if result_count > 0 && self.results_picker.selected().is_none() {
|
||||||
|
self.results_picker.select(Some(0));
|
||||||
|
self.results_picker.relative_select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = self.channel.results(
|
||||||
|
layout.results.height.saturating_sub(2).into(),
|
||||||
|
u32::try_from(self.results_picker.view_offset)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let results_list = build_results_list(
|
||||||
|
results_block,
|
||||||
|
&entries,
|
||||||
|
ListDirection::BottomToTop,
|
||||||
|
);
|
||||||
|
|
||||||
|
f.render_stateful_widget(
|
||||||
|
results_list,
|
||||||
|
layout.results,
|
||||||
|
&mut self.results_picker.relative_state,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
|
/// This macro generates a `CliChannel` enum and the necessary glue code
|
||||||
|
/// to convert into a `TelevisionChannel` member:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use crate::channels::{TelevisionChannel, OnAir};
|
||||||
|
/// use television_derive::CliChannel;
|
||||||
|
/// use crate::channels::{files, text};
|
||||||
|
///
|
||||||
|
/// #[derive(CliChannel)]
|
||||||
|
/// enum TelevisionChannel {
|
||||||
|
/// Files(files::Channel),
|
||||||
|
/// Text(text::Channel),
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let television_channel: TelevisionChannel = CliTvChannel::Files.to_channel();
|
||||||
|
///
|
||||||
|
/// assert!(matches!(television_channel, TelevisionChannel::Files(_)));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The `CliChannel` enum is used to select channels from the command line.
|
||||||
#[proc_macro_derive(CliChannel)]
|
#[proc_macro_derive(CliChannel)]
|
||||||
pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
|
pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
|
||||||
// Construct a representation of Rust code as a syntax tree
|
// Construct a representation of Rust code as a syntax tree
|
||||||
@ -11,7 +32,8 @@ pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
|
|||||||
impl_cli_channel(&ast)
|
impl_cli_channel(&ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
const VARIANT_BLACKLIST: [&str; 2] = ["Stdin", "TvGuide"];
|
/// List of variant names that should be ignored when generating the CliTvChannel enum.
|
||||||
|
const VARIANT_BLACKLIST: [&str; 2] = ["Stdin", "RemoteControl"];
|
||||||
|
|
||||||
fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
|
fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
// check that the struct is an enum
|
// check that the struct is an enum
|
||||||
@ -88,8 +110,34 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
|
|||||||
gen.into()
|
gen.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This macro generates the `OnAir` trait implementation for the
|
/// This macro generates the `OnAir` trait implementation for the given enum.
|
||||||
/// given enum.
|
///
|
||||||
|
/// The `OnAir` trait is used to interact with the different television channels
|
||||||
|
/// and forwards the method calls to the corresponding channel variants.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```rust
|
||||||
|
/// use television_derive::Broadcast;
|
||||||
|
/// use crate::channels::{TelevisionChannel, OnAir};
|
||||||
|
/// use crate::channels::{files, text};
|
||||||
|
///
|
||||||
|
/// #[derive(Broadcast)]
|
||||||
|
/// enum TelevisionChannel {
|
||||||
|
/// Files(files::Channel),
|
||||||
|
/// Text(text::Channel),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let mut channel = TelevisionChannel::Files(files::Channel::default());
|
||||||
|
///
|
||||||
|
/// // Use the `OnAir` trait methods directly on TelevisionChannel
|
||||||
|
/// channel.find("pattern");
|
||||||
|
/// let results = channel.results(10, 0);
|
||||||
|
/// let result = channel.get_result(0);
|
||||||
|
/// let result_count = channel.result_count();
|
||||||
|
/// let total_count = channel.total_count();
|
||||||
|
/// let running = channel.running();
|
||||||
|
/// channel.shutdown();
|
||||||
|
/// ```
|
||||||
#[proc_macro_derive(Broadcast)]
|
#[proc_macro_derive(Broadcast)]
|
||||||
pub fn tv_channel_derive(input: TokenStream) -> TokenStream {
|
pub fn tv_channel_derive(input: TokenStream) -> TokenStream {
|
||||||
// Construct a representation of Rust code as a syntax tree
|
// Construct a representation of Rust code as a syntax tree
|
||||||
@ -196,6 +244,11 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream {
|
|||||||
trait_impl.into()
|
trait_impl.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This macro generates a `UnitChannel` enum and the necessary glue code
|
||||||
|
/// to convert from and to a `TelevisionChannel` member.
|
||||||
|
///
|
||||||
|
/// The `UnitChannel` enum is used as a unit variant of the `TelevisionChannel`
|
||||||
|
/// enum.
|
||||||
#[proc_macro_derive(UnitChannel)]
|
#[proc_macro_derive(UnitChannel)]
|
||||||
pub fn unit_channel_derive(input: TokenStream) -> TokenStream {
|
pub fn unit_channel_derive(input: TokenStream) -> TokenStream {
|
||||||
// Construct a representation of Rust code as a syntax tree
|
// Construct a representation of Rust code as a syntax tree
|
||||||
|
Loading…
x
Reference in New Issue
Block a user