mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-03 01:50:12 +00:00
feat(channels): add support for multi selection (#234)
Fixes #229 <img width="1793" alt="Screenshot 2025-01-07 at 00 12 18" src="https://github.com/user-attachments/assets/f2158c70-b3ab-4c14-8856-eb8463efef8a" />
This commit is contained in:
parent
d20784891f
commit
2e5f65baef
@ -83,8 +83,12 @@ select_prev_page = "pageup"
|
|||||||
# Scrolling the preview pane
|
# Scrolling the preview pane
|
||||||
scroll_preview_half_page_down = "ctrl-d"
|
scroll_preview_half_page_down = "ctrl-d"
|
||||||
scroll_preview_half_page_up = "ctrl-u"
|
scroll_preview_half_page_up = "ctrl-u"
|
||||||
# Select an entry
|
# Add entry to selection and move to the next entry
|
||||||
select_entry = "enter"
|
toggle_selection_down = "tab"
|
||||||
|
# Add entry to selection and move to the previous entry
|
||||||
|
toggle_selection_up = "backtab"
|
||||||
|
# Confirm selection
|
||||||
|
confirm_selection = "enter"
|
||||||
# Copy the selected entry to the clipboard
|
# Copy the selected entry to the clipboard
|
||||||
copy_entry_to_clipboard = "ctrl-y"
|
copy_entry_to_clipboard = "ctrl-y"
|
||||||
# Toggle the remote control mode
|
# Toggle the remote control mode
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::entry::Entry;
|
use crate::entry::Entry;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use television_derive::{Broadcast, ToCliChannel, ToUnitChannel};
|
use television_derive::{Broadcast, ToCliChannel, ToUnitChannel};
|
||||||
@ -65,6 +67,12 @@ pub trait OnAir: Send {
|
|||||||
/// Get a specific result by its index.
|
/// Get a specific result by its index.
|
||||||
fn get_result(&self, index: u32) -> Option<Entry>;
|
fn get_result(&self, index: u32) -> Option<Entry>;
|
||||||
|
|
||||||
|
/// Get the currently selected entries.
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry>;
|
||||||
|
|
||||||
|
/// Toggles selection for the entry under the cursor.
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry);
|
||||||
|
|
||||||
/// Get the number of results currently available.
|
/// Get the number of results currently available.
|
||||||
fn result_count(&self) -> u32;
|
fn result_count(&self) -> u32;
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::channels::OnAir;
|
use crate::channels::OnAir;
|
||||||
use crate::entry::Entry;
|
use crate::entry::Entry;
|
||||||
use crate::entry::PreviewType;
|
use crate::entry::PreviewType;
|
||||||
@ -21,6 +23,7 @@ impl Alias {
|
|||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
matcher: Matcher<Alias>,
|
matcher: Matcher<Alias>,
|
||||||
file_icon: FileIcon,
|
file_icon: FileIcon,
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const NUM_THREADS: usize = 1;
|
const NUM_THREADS: usize = 1;
|
||||||
@ -52,6 +55,7 @@ impl Channel {
|
|||||||
Self {
|
Self {
|
||||||
matcher,
|
matcher,
|
||||||
file_icon: FileIcon::from(FILE_ICON_STR),
|
file_icon: FileIcon::from(FILE_ICON_STR),
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,6 +119,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ pub struct Channel {
|
|||||||
matcher: Matcher<String>,
|
matcher: Matcher<String>,
|
||||||
entries_command: String,
|
entries_command: String,
|
||||||
preview_kind: PreviewKind,
|
preview_kind: PreviewKind,
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Channel {
|
impl Default for Channel {
|
||||||
@ -93,6 +94,7 @@ impl Channel {
|
|||||||
entries_command: entries_command.to_string(),
|
entries_command: entries_command.to_string(),
|
||||||
preview_kind,
|
preview_kind,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,6 +164,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ pub struct Channel {
|
|||||||
crawl_handle: tokio::task::JoinHandle<()>,
|
crawl_handle: tokio::task::JoinHandle<()>,
|
||||||
// PERF: cache results (to make deleting characters smoother) with
|
// PERF: cache results (to make deleting characters smoother) with
|
||||||
// a shallow stack of sub-patterns as keys (e.g. "a", "ab", "abc")
|
// a shallow stack of sub-patterns as keys (e.g. "a", "ab", "abc")
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -21,6 +22,7 @@ impl Channel {
|
|||||||
Channel {
|
Channel {
|
||||||
matcher,
|
matcher,
|
||||||
crawl_handle,
|
crawl_handle,
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,6 +106,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
|
|
||||||
use super::OnAir;
|
use super::OnAir;
|
||||||
@ -15,6 +17,7 @@ struct EnvVar {
|
|||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
matcher: Matcher<EnvVar>,
|
matcher: Matcher<EnvVar>,
|
||||||
file_icon: FileIcon,
|
file_icon: FileIcon,
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const NUM_THREADS: usize = 1;
|
const NUM_THREADS: usize = 1;
|
||||||
@ -32,6 +35,7 @@ impl Channel {
|
|||||||
Channel {
|
Channel {
|
||||||
matcher,
|
matcher,
|
||||||
file_icon: FileIcon::from(FILE_ICON_STR),
|
file_icon: FileIcon::from(FILE_ICON_STR),
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,6 +99,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ pub struct Channel {
|
|||||||
crawl_handle: tokio::task::JoinHandle<()>,
|
crawl_handle: tokio::task::JoinHandle<()>,
|
||||||
// PERF: cache results (to make deleting characters smoother) with
|
// PERF: cache results (to make deleting characters smoother) with
|
||||||
// a shallow stack of sub-patterns as keys (e.g. "a", "ab", "abc")
|
// a shallow stack of sub-patterns as keys (e.g. "a", "ab", "abc")
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -21,6 +22,7 @@ impl Channel {
|
|||||||
Channel {
|
Channel {
|
||||||
matcher,
|
matcher,
|
||||||
crawl_handle,
|
crawl_handle,
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +108,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ use devicons::FileIcon;
|
|||||||
use directories::BaseDirs;
|
use directories::BaseDirs;
|
||||||
use ignore::overrides::OverrideBuilder;
|
use ignore::overrides::OverrideBuilder;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@ -15,6 +16,7 @@ pub struct Channel {
|
|||||||
matcher: Matcher<String>,
|
matcher: Matcher<String>,
|
||||||
icon: FileIcon,
|
icon: FileIcon,
|
||||||
crawl_handle: JoinHandle<()>,
|
crawl_handle: JoinHandle<()>,
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -29,6 +31,7 @@ impl Channel {
|
|||||||
matcher,
|
matcher,
|
||||||
icon: FileIcon::from("git"),
|
icon: FileIcon::from("git"),
|
||||||
crawl_handle,
|
crawl_handle,
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,6 +84,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::cable::{CableChannelPrototype, CableChannels};
|
use crate::cable::{CableChannelPrototype, CableChannels};
|
||||||
@ -13,6 +14,7 @@ use super::cable;
|
|||||||
pub struct RemoteControl {
|
pub struct RemoteControl {
|
||||||
matcher: Matcher<RCButton>,
|
matcher: Matcher<RCButton>,
|
||||||
cable_channels: Option<CableChannels>,
|
cable_channels: Option<CableChannels>,
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -59,6 +61,7 @@ impl RemoteControl {
|
|||||||
RemoteControl {
|
RemoteControl {
|
||||||
matcher,
|
matcher,
|
||||||
cable_channels,
|
cable_channels,
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +143,13 @@ impl OnAir for RemoteControl {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {}
|
||||||
|
|
||||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||||
self.matcher.get_result(index).map(|item| {
|
self.matcher.get_result(index).map(|item| {
|
||||||
let path = item.matched_string;
|
let path = item.matched_string;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
io::{stdin, BufRead},
|
io::{stdin, BufRead},
|
||||||
thread::spawn,
|
thread::spawn,
|
||||||
};
|
};
|
||||||
@ -12,6 +13,7 @@ use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher};
|
|||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
matcher: Matcher<String>,
|
matcher: Matcher<String>,
|
||||||
preview_type: PreviewType,
|
preview_type: PreviewType,
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -24,6 +26,7 @@ impl Channel {
|
|||||||
Self {
|
Self {
|
||||||
matcher,
|
matcher,
|
||||||
preview_type: preview_type.unwrap_or_default(),
|
preview_type: preview_type.unwrap_or_default(),
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,6 +93,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ use crate::entry::{Entry, PreviewType};
|
|||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
use ignore::WalkState;
|
use ignore::WalkState;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufRead, Read, Seek},
|
io::{BufRead, Read, Seek},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -36,6 +37,7 @@ impl CandidateLine {
|
|||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
matcher: Matcher<CandidateLine>,
|
matcher: Matcher<CandidateLine>,
|
||||||
crawl_handle: tokio::task::JoinHandle<()>,
|
crawl_handle: tokio::task::JoinHandle<()>,
|
||||||
|
selected_entries: HashSet<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -49,6 +51,7 @@ impl Channel {
|
|||||||
Channel {
|
Channel {
|
||||||
matcher,
|
matcher,
|
||||||
crawl_handle,
|
crawl_handle,
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +76,7 @@ impl Channel {
|
|||||||
Channel {
|
Channel {
|
||||||
matcher,
|
matcher,
|
||||||
crawl_handle,
|
crawl_handle,
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +101,7 @@ impl Channel {
|
|||||||
Channel {
|
Channel {
|
||||||
matcher,
|
matcher,
|
||||||
crawl_handle: load_handle,
|
crawl_handle: load_handle,
|
||||||
|
selected_entries: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,6 +200,18 @@ impl OnAir for Channel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
&self.selected_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
if self.selected_entries.contains(entry) {
|
||||||
|
self.selected_entries.remove(entry);
|
||||||
|
} else {
|
||||||
|
self.selected_entries.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
self.matcher.matched_item_count
|
self.matcher.matched_item_count
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
use std::{fmt::Display, path::PathBuf};
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
@ -9,7 +13,7 @@ use strum::EnumString;
|
|||||||
// channel convertible from any other that yields `EntryType`.
|
// channel convertible from any other that yields `EntryType`.
|
||||||
// This needs pondering since it does bring another level of abstraction and
|
// This needs pondering since it does bring another level of abstraction and
|
||||||
// adds a layer of complexity.
|
// adds a layer of complexity.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
/// The name of the entry.
|
/// The name of the entry.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -27,6 +31,31 @@ pub struct Entry {
|
|||||||
pub preview_type: PreviewType,
|
pub preview_type: PreviewType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hash for Entry {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.name.hash(state);
|
||||||
|
if let Some(line_number) = self.line_number {
|
||||||
|
line_number.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<Entry> for &Entry {
|
||||||
|
fn eq(&self, other: &Entry) -> bool {
|
||||||
|
self.name == other.name
|
||||||
|
&& (self.line_number.is_none() && other.line_number.is_none()
|
||||||
|
|| self.line_number == other.line_number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<Entry> for Entry {
|
||||||
|
fn eq(&self, other: &Entry) -> bool {
|
||||||
|
self.name == other.name
|
||||||
|
&& (self.line_number.is_none() && other.line_number.is_none()
|
||||||
|
|| self.line_number == other.line_number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_return)]
|
#[allow(clippy::needless_return)]
|
||||||
pub fn merge_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> {
|
pub fn merge_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> {
|
||||||
ranges.iter().fold(
|
ranges.iter().fold(
|
||||||
|
@ -213,6 +213,26 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_entries(&self) -> &HashSet<Entry> {
|
||||||
|
match self {
|
||||||
|
#(
|
||||||
|
#enum_name::#variant_names(ref channel) => {
|
||||||
|
channel.selected_entries()
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selection(&mut self, entry: &Entry) {
|
||||||
|
match self {
|
||||||
|
#(
|
||||||
|
#enum_name::#variant_names(ref mut channel) => {
|
||||||
|
channel.toggle_selection(entry)
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
fn result_count(&self) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
#(
|
#(
|
||||||
|
@ -28,6 +28,7 @@ pub struct ResultsColorscheme {
|
|||||||
pub result_preview_fg: Color,
|
pub result_preview_fg: Color,
|
||||||
pub result_line_number_fg: Color,
|
pub result_line_number_fg: Color,
|
||||||
pub result_selected_bg: Color,
|
pub result_selected_bg: Color,
|
||||||
|
pub result_selected_fg: Color,
|
||||||
pub match_foreground_color: Color,
|
pub match_foreground_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,9 @@ pub fn draw_input_box(
|
|||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
// result count
|
// result count
|
||||||
Constraint::Length(
|
Constraint::Length(
|
||||||
3 * (u16::try_from((total_count).ilog10()).unwrap() + 1) + 3,
|
3 * (u16::try_from((total_count.max(1)).ilog10()).unwrap()
|
||||||
|
+ 1)
|
||||||
|
+ 3,
|
||||||
),
|
),
|
||||||
// spinner
|
// spinner
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
|
@ -81,6 +81,7 @@ fn draw_rc_channels(
|
|||||||
let channel_list = build_results_list(
|
let channel_list = build_results_list(
|
||||||
rc_block,
|
rc_block,
|
||||||
entries,
|
entries,
|
||||||
|
None,
|
||||||
ListDirection::TopToBottom,
|
ListDirection::TopToBottom,
|
||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
icon_color_cache,
|
icon_color_cache,
|
||||||
|
@ -8,7 +8,7 @@ use ratatui::widgets::{
|
|||||||
Block, BorderType, Borders, List, ListDirection, ListState, Padding,
|
Block, BorderType, Borders, List, ListDirection, ListState, Padding,
|
||||||
};
|
};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use television_channels::entry::Entry;
|
use television_channels::entry::Entry;
|
||||||
use television_utils::strings::{
|
use television_utils::strings::{
|
||||||
@ -16,9 +16,14 @@ use television_utils::strings::{
|
|||||||
slice_at_char_boundaries,
|
slice_at_char_boundaries,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const POINTER_SYMBOL: &str = "> ";
|
||||||
|
const SELECTED_SYMBOL: &str = "● ";
|
||||||
|
const DESLECTED_SYMBOL: &str = " ";
|
||||||
|
|
||||||
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],
|
||||||
|
selected_entries: Option<&HashSet<Entry>>,
|
||||||
list_direction: ListDirection,
|
list_direction: ListDirection,
|
||||||
use_icons: bool,
|
use_icons: bool,
|
||||||
icon_color_cache: &mut HashMap<String, Color>,
|
icon_color_cache: &mut HashMap<String, Color>,
|
||||||
@ -29,6 +34,19 @@ where
|
|||||||
{
|
{
|
||||||
List::new(entries.iter().map(|entry| {
|
List::new(entries.iter().map(|entry| {
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
|
// optional selection symbol
|
||||||
|
if let Some(selected_entries) = selected_entries {
|
||||||
|
if !selected_entries.is_empty() {
|
||||||
|
spans.push(if selected_entries.contains(entry) {
|
||||||
|
Span::styled(
|
||||||
|
SELECTED_SYMBOL,
|
||||||
|
Style::default().fg(colorscheme.result_selected_fg),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Span::from(DESLECTED_SYMBOL)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// optional icon
|
// optional icon
|
||||||
if let Some(icon) = entry.icon.as_ref() {
|
if let Some(icon) = entry.icon.as_ref() {
|
||||||
if use_icons {
|
if use_icons {
|
||||||
@ -129,7 +147,7 @@ where
|
|||||||
.highlight_style(
|
.highlight_style(
|
||||||
Style::default().bg(colorscheme.result_selected_bg).bold(),
|
Style::default().bg(colorscheme.result_selected_bg).bold(),
|
||||||
)
|
)
|
||||||
.highlight_symbol("> ")
|
.highlight_symbol(POINTER_SYMBOL)
|
||||||
.block(results_block)
|
.block(results_block)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +156,7 @@ pub fn draw_results_list(
|
|||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
entries: &[Entry],
|
entries: &[Entry],
|
||||||
|
selected_entries: &HashSet<Entry>,
|
||||||
relative_picker_state: &mut ListState,
|
relative_picker_state: &mut ListState,
|
||||||
input_bar_position: InputPosition,
|
input_bar_position: InputPosition,
|
||||||
use_nerd_font_icons: bool,
|
use_nerd_font_icons: bool,
|
||||||
@ -166,6 +185,7 @@ pub fn draw_results_list(
|
|||||||
let results_list = build_results_list(
|
let results_list = build_results_list(
|
||||||
results_block,
|
results_block,
|
||||||
entries,
|
entries,
|
||||||
|
Some(selected_entries),
|
||||||
match input_bar_position {
|
match input_bar_position {
|
||||||
InputPosition::Bottom => ListDirection::BottomToTop,
|
InputPosition::Bottom => ListDirection::BottomToTop,
|
||||||
InputPosition::Top => ListDirection::TopToBottom,
|
InputPosition::Top => ListDirection::TopToBottom,
|
||||||
|
@ -39,9 +39,16 @@ pub enum Action {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
ClearScreen,
|
ClearScreen,
|
||||||
// results actions
|
// results actions
|
||||||
/// Select the entry currently under the cursor.
|
/// Add entry under cursor to the list of selected entries and move the cursor down.
|
||||||
|
#[serde(alias = "toggle_selection_down")]
|
||||||
|
ToggleSelectionDown,
|
||||||
|
/// Add entry under cursor to the list of selected entries and move the cursor up.
|
||||||
|
#[serde(alias = "toggle_selection_up")]
|
||||||
|
ToggleSelectionUp,
|
||||||
|
/// Confirm current selection (multi select or entry under cursor).
|
||||||
#[serde(alias = "select_entry")]
|
#[serde(alias = "select_entry")]
|
||||||
SelectEntry,
|
#[serde(alias = "confirm_selection")]
|
||||||
|
ConfirmSelection,
|
||||||
/// Select the entry currently under the cursor and pass the key that was pressed
|
/// Select the entry currently under the cursor and pass the key that was pressed
|
||||||
/// through to be handled the parent process.
|
/// through to be handled the parent process.
|
||||||
#[serde(alias = "select_passthrough")]
|
#[serde(alias = "select_passthrough")]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
@ -44,36 +45,36 @@ pub struct App {
|
|||||||
/// The outcome of an action.
|
/// The outcome of an action.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ActionOutcome {
|
pub enum ActionOutcome {
|
||||||
Entry(Entry),
|
Entries(HashSet<Entry>),
|
||||||
Input(String),
|
Input(String),
|
||||||
Passthrough(Entry, String),
|
Passthrough(HashSet<Entry>, String),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of the application.
|
/// The result of the application.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppOutput {
|
pub struct AppOutput {
|
||||||
pub selected_entry: Option<Entry>,
|
pub selected_entries: Option<HashSet<Entry>>,
|
||||||
pub passthrough: Option<String>,
|
pub passthrough: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ActionOutcome> for AppOutput {
|
impl From<ActionOutcome> for AppOutput {
|
||||||
fn from(outcome: ActionOutcome) -> Self {
|
fn from(outcome: ActionOutcome) -> Self {
|
||||||
match outcome {
|
match outcome {
|
||||||
ActionOutcome::Entry(entry) => Self {
|
ActionOutcome::Entries(entries) => Self {
|
||||||
selected_entry: Some(entry),
|
selected_entries: Some(entries),
|
||||||
passthrough: None,
|
passthrough: None,
|
||||||
},
|
},
|
||||||
ActionOutcome::Input(input) => Self {
|
ActionOutcome::Input(input) => Self {
|
||||||
selected_entry: None,
|
selected_entries: None,
|
||||||
passthrough: Some(input),
|
passthrough: Some(input),
|
||||||
},
|
},
|
||||||
ActionOutcome::Passthrough(entry, key) => Self {
|
ActionOutcome::Passthrough(entries, key) => Self {
|
||||||
selected_entry: Some(entry),
|
selected_entries: Some(entries),
|
||||||
passthrough: Some(key),
|
passthrough: Some(key),
|
||||||
},
|
},
|
||||||
ActionOutcome::None => Self {
|
ActionOutcome::None => Self {
|
||||||
selected_entry: None,
|
selected_entries: None,
|
||||||
passthrough: None,
|
passthrough: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -262,10 +263,13 @@ impl App {
|
|||||||
Action::SelectAndExit => {
|
Action::SelectAndExit => {
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
self.render_tx.send(RenderingTask::Quit)?;
|
self.render_tx.send(RenderingTask::Quit)?;
|
||||||
if let Some(entry) =
|
if let Some(entries) = self
|
||||||
self.television.lock().await.get_selected_entry(None)
|
.television
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get_selected_entries(Some(Mode::Channel))
|
||||||
{
|
{
|
||||||
return Ok(ActionOutcome::Entry(entry));
|
return Ok(ActionOutcome::Entries(entries));
|
||||||
}
|
}
|
||||||
return Ok(ActionOutcome::Input(
|
return Ok(ActionOutcome::Input(
|
||||||
self.television.lock().await.current_pattern.clone(),
|
self.television.lock().await.current_pattern.clone(),
|
||||||
@ -274,11 +278,14 @@ impl App {
|
|||||||
Action::SelectPassthrough(passthrough) => {
|
Action::SelectPassthrough(passthrough) => {
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
self.render_tx.send(RenderingTask::Quit)?;
|
self.render_tx.send(RenderingTask::Quit)?;
|
||||||
if let Some(entry) =
|
if let Some(entries) = self
|
||||||
self.television.lock().await.get_selected_entry(None)
|
.television
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get_selected_entries(Some(Mode::Channel))
|
||||||
{
|
{
|
||||||
return Ok(ActionOutcome::Passthrough(
|
return Ok(ActionOutcome::Passthrough(
|
||||||
entry,
|
entries,
|
||||||
passthrough,
|
passthrough,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@ pub struct Theme {
|
|||||||
pub result_line_number_fg: Color,
|
pub result_line_number_fg: Color,
|
||||||
pub result_value_fg: Color,
|
pub result_value_fg: Color,
|
||||||
pub selection_bg: Color,
|
pub selection_bg: Color,
|
||||||
|
pub selection_fg: Color,
|
||||||
pub match_fg: Color,
|
pub match_fg: Color,
|
||||||
// preview
|
// preview
|
||||||
pub preview_title_fg: Color,
|
pub preview_title_fg: Color,
|
||||||
@ -170,6 +171,9 @@ struct Inner {
|
|||||||
result_line_number_fg: String,
|
result_line_number_fg: String,
|
||||||
result_value_fg: String,
|
result_value_fg: String,
|
||||||
selection_bg: String,
|
selection_bg: String,
|
||||||
|
// this is made optional for theme backwards compatibility
|
||||||
|
// and falls back to match_fg
|
||||||
|
selection_fg: Option<String>,
|
||||||
match_fg: String,
|
match_fg: String,
|
||||||
//preview
|
//preview
|
||||||
preview_title_fg: String,
|
preview_title_fg: String,
|
||||||
@ -190,44 +194,129 @@ impl<'de> Deserialize<'de> for Theme {
|
|||||||
.background
|
.background
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
Color::from_str(&s).ok_or_else(|| {
|
Color::from_str(&s).ok_or_else(|| {
|
||||||
serde::de::Error::custom("invalid color")
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
s
|
||||||
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
border_fg: Color::from_str(&inner.border_fg)
|
border_fg: Color::from_str(&inner.border_fg).ok_or_else(|| {
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
serde::de::Error::custom(format!(
|
||||||
text_fg: Color::from_str(&inner.text_fg)
|
"invalid color {}",
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
&inner.border_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
text_fg: Color::from_str(&inner.text_fg).ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.text_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
dimmed_text_fg: Color::from_str(&inner.dimmed_text_fg)
|
dimmed_text_fg: Color::from_str(&inner.dimmed_text_fg)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
input_text_fg: Color::from_str(&inner.input_text_fg)
|
serde::de::Error::custom(format!(
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
"invalid color {}",
|
||||||
|
&inner.dimmed_text_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
input_text_fg: Color::from_str(&inner.input_text_fg).ok_or_else(
|
||||||
|
|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.input_text_fg
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?,
|
||||||
result_count_fg: Color::from_str(&inner.result_count_fg)
|
result_count_fg: Color::from_str(&inner.result_count_fg)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.result_count_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
result_name_fg: Color::from_str(&inner.result_name_fg)
|
result_name_fg: Color::from_str(&inner.result_name_fg)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.result_name_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
result_line_number_fg: Color::from_str(
|
result_line_number_fg: Color::from_str(
|
||||||
&inner.result_line_number_fg,
|
&inner.result_line_number_fg,
|
||||||
)
|
)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.result_line_number_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
result_value_fg: Color::from_str(&inner.result_value_fg)
|
result_value_fg: Color::from_str(&inner.result_value_fg)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
selection_bg: Color::from_str(&inner.selection_bg)
|
serde::de::Error::custom(format!(
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
"invalid color {}",
|
||||||
match_fg: Color::from_str(&inner.match_fg)
|
&inner.result_value_fg
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
))
|
||||||
|
})?,
|
||||||
|
selection_bg: Color::from_str(&inner.selection_bg).ok_or_else(
|
||||||
|
|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.selection_bg
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
// this is optional for theme backwards compatibility and falls back to match_fg
|
||||||
|
selection_fg: match inner.selection_fg {
|
||||||
|
Some(s) => Color::from_str(&s).ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!("invalid color {}", &s))
|
||||||
|
})?,
|
||||||
|
None => Color::from_str(&inner.match_fg).ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.match_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
},
|
||||||
|
|
||||||
|
match_fg: Color::from_str(&inner.match_fg).ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.match_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
preview_title_fg: Color::from_str(&inner.preview_title_fg)
|
preview_title_fg: Color::from_str(&inner.preview_title_fg)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.preview_title_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
channel_mode_fg: Color::from_str(&inner.channel_mode_fg)
|
channel_mode_fg: Color::from_str(&inner.channel_mode_fg)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.channel_mode_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
remote_control_mode_fg: Color::from_str(
|
remote_control_mode_fg: Color::from_str(
|
||||||
&inner.remote_control_mode_fg,
|
&inner.remote_control_mode_fg,
|
||||||
)
|
)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.remote_control_mode_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
send_to_channel_mode_fg: Color::from_str(
|
send_to_channel_mode_fg: Color::from_str(
|
||||||
&inner.send_to_channel_mode_fg,
|
&inner.send_to_channel_mode_fg,
|
||||||
)
|
)
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid color"))?,
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"invalid color {}",
|
||||||
|
&inner.send_to_channel_mode_fg
|
||||||
|
))
|
||||||
|
})?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,6 +404,7 @@ impl Into<ResultsColorscheme> for &Theme {
|
|||||||
result_preview_fg: (&self.result_value_fg).into(),
|
result_preview_fg: (&self.result_value_fg).into(),
|
||||||
result_line_number_fg: (&self.result_line_number_fg).into(),
|
result_line_number_fg: (&self.result_line_number_fg).into(),
|
||||||
result_selected_bg: (&self.selection_bg).into(),
|
result_selected_bg: (&self.selection_bg).into(),
|
||||||
|
result_selected_fg: (&self.selection_fg).into(),
|
||||||
match_foreground_color: (&self.match_fg).into(),
|
match_foreground_color: (&self.match_fg).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -371,6 +461,7 @@ mod tests {
|
|||||||
result_line_number_fg = "bright-white"
|
result_line_number_fg = "bright-white"
|
||||||
result_value_fg = "bright-white"
|
result_value_fg = "bright-white"
|
||||||
selection_bg = "bright-white"
|
selection_bg = "bright-white"
|
||||||
|
selection_fg = "bright-white"
|
||||||
match_fg = "bright-white"
|
match_fg = "bright-white"
|
||||||
preview_title_fg = "bright-white"
|
preview_title_fg = "bright-white"
|
||||||
channel_mode_fg = "bright-white"
|
channel_mode_fg = "bright-white"
|
||||||
@ -394,6 +485,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(theme.result_value_fg, Color::Ansi(ANSIColor::BrightWhite));
|
assert_eq!(theme.result_value_fg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
assert_eq!(theme.selection_bg, Color::Ansi(ANSIColor::BrightWhite));
|
assert_eq!(theme.selection_bg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
|
assert_eq!(theme.selection_fg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
assert_eq!(theme.match_fg, Color::Ansi(ANSIColor::BrightWhite));
|
assert_eq!(theme.match_fg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
theme.preview_title_fg,
|
theme.preview_title_fg,
|
||||||
@ -422,6 +514,7 @@ mod tests {
|
|||||||
result_line_number_fg = "#ffffff"
|
result_line_number_fg = "#ffffff"
|
||||||
result_value_fg = "bright-white"
|
result_value_fg = "bright-white"
|
||||||
selection_bg = "bright-white"
|
selection_bg = "bright-white"
|
||||||
|
selection_fg = "bright-white"
|
||||||
match_fg = "bright-white"
|
match_fg = "bright-white"
|
||||||
preview_title_fg = "bright-white"
|
preview_title_fg = "bright-white"
|
||||||
channel_mode_fg = "bright-white"
|
channel_mode_fg = "bright-white"
|
||||||
@ -445,6 +538,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(theme.result_value_fg, Color::Ansi(ANSIColor::BrightWhite));
|
assert_eq!(theme.result_value_fg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
assert_eq!(theme.selection_bg, Color::Ansi(ANSIColor::BrightWhite));
|
assert_eq!(theme.selection_bg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
|
assert_eq!(theme.selection_fg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
assert_eq!(theme.match_fg, Color::Ansi(ANSIColor::BrightWhite));
|
assert_eq!(theme.match_fg, Color::Ansi(ANSIColor::BrightWhite));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
theme.preview_title_fg,
|
theme.preview_title_fg,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::io::{stdout, IsTerminal, Write};
|
use std::io::{stdout, BufWriter, IsTerminal, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
@ -119,12 +119,18 @@ async fn main() -> Result<()> {
|
|||||||
stdout().flush()?;
|
stdout().flush()?;
|
||||||
let output = app.run(stdout().is_terminal()).await?;
|
let output = app.run(stdout().is_terminal()).await?;
|
||||||
info!("{:?}", output);
|
info!("{:?}", output);
|
||||||
|
// lock stdout
|
||||||
|
let stdout_handle = stdout().lock();
|
||||||
|
let mut bufwriter = BufWriter::new(stdout_handle);
|
||||||
if let Some(passthrough) = output.passthrough {
|
if let Some(passthrough) = output.passthrough {
|
||||||
writeln!(stdout(), "{passthrough}")?;
|
writeln!(bufwriter, "{passthrough}")?;
|
||||||
}
|
}
|
||||||
if let Some(entry) = output.selected_entry {
|
if let Some(entries) = output.selected_entries {
|
||||||
writeln!(stdout(), "{}", entry.stdout_repr())?;
|
for entry in &entries {
|
||||||
|
writeln!(bufwriter, "{}", entry.stdout_repr())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
bufwriter.flush()?;
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -6,7 +6,7 @@ use crate::{cable::load_cable_channels, keymap::Keymap};
|
|||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||||
use ratatui::{layout::Rect, style::Color, Frame};
|
use ratatui::{layout::Rect, style::Color, Frame};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use television_channels::channels::{
|
use television_channels::channels::{
|
||||||
remote_control::{load_builtin_channels, RemoteControl},
|
remote_control::{load_builtin_channels, RemoteControl},
|
||||||
@ -148,17 +148,40 @@ impl Television {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_selected_entry(&mut self, mode: Option<Mode>) -> Option<Entry> {
|
pub fn get_selected_entry(&mut self, mode: Option<Mode>) -> Option<Entry> {
|
||||||
match mode.unwrap_or(self.mode) {
|
match mode.unwrap_or(self.mode) {
|
||||||
Mode::Channel => self.results_picker.selected().and_then(|i| {
|
Mode::Channel => {
|
||||||
self.channel.get_result(u32::try_from(i).unwrap())
|
if let Some(i) = self.results_picker.selected() {
|
||||||
}),
|
return self.channel.get_result(i.try_into().unwrap());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
Mode::RemoteControl | Mode::SendToChannel => {
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
self.rc_picker.selected().and_then(|i| {
|
if let Some(i) = self.rc_picker.selected() {
|
||||||
self.remote_control.get_result(u32::try_from(i).unwrap())
|
return self
|
||||||
})
|
.remote_control
|
||||||
|
.get_result(i.try_into().unwrap());
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_selected_entries(
|
||||||
|
&mut self,
|
||||||
|
mode: Option<Mode>,
|
||||||
|
) -> Option<HashSet<Entry>> {
|
||||||
|
if self.channel.selected_entries().is_empty()
|
||||||
|
|| matches!(mode, Some(Mode::RemoteControl))
|
||||||
|
{
|
||||||
|
return self.get_selected_entry(mode).map(|e| {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
set.insert(e);
|
||||||
|
set
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(self.channel.selected_entries().clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_prev_entry(&mut self, step: u32) {
|
pub fn select_prev_entry(&mut self, step: u32) {
|
||||||
let (result_count, picker) = match self.mode {
|
let (result_count, picker) = match self.mode {
|
||||||
Mode::Channel => {
|
Mode::Channel => {
|
||||||
@ -334,15 +357,30 @@ impl Television {
|
|||||||
}
|
}
|
||||||
Mode::SendToChannel => {}
|
Mode::SendToChannel => {}
|
||||||
},
|
},
|
||||||
Action::SelectEntry => {
|
Action::ToggleSelectionDown | Action::ToggleSelectionUp => {
|
||||||
if let Some(entry) = self.get_selected_entry(None) {
|
if matches!(self.mode, Mode::Channel) {
|
||||||
match self.mode {
|
if let Some(entry) = self.get_selected_entry(None) {
|
||||||
Mode::Channel => self
|
self.channel.toggle_selection(&entry);
|
||||||
.action_tx
|
if matches!(action, Action::ToggleSelectionDown) {
|
||||||
|
self.select_next_entry(1);
|
||||||
|
} else {
|
||||||
|
self.select_prev_entry(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::ConfirmSelection => {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Channel => {
|
||||||
|
self.action_tx
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.send(Action::SelectAndExit)?,
|
.send(Action::SelectAndExit)?;
|
||||||
Mode::RemoteControl => {
|
}
|
||||||
|
Mode::RemoteControl => {
|
||||||
|
if let Some(entry) =
|
||||||
|
self.get_selected_entry(Some(Mode::RemoteControl))
|
||||||
|
{
|
||||||
let new_channel = self
|
let new_channel = self
|
||||||
.remote_control
|
.remote_control
|
||||||
.zap(entry.name.as_str())?;
|
.zap(entry.name.as_str())?;
|
||||||
@ -353,7 +391,11 @@ impl Television {
|
|||||||
self.mode = Mode::Channel;
|
self.mode = Mode::Channel;
|
||||||
self.change_channel(new_channel);
|
self.change_channel(new_channel);
|
||||||
}
|
}
|
||||||
Mode::SendToChannel => {
|
}
|
||||||
|
Mode::SendToChannel => {
|
||||||
|
if let Some(entry) =
|
||||||
|
self.get_selected_entry(Some(Mode::RemoteControl))
|
||||||
|
{
|
||||||
let new_channel = self.channel.transition_to(
|
let new_channel = self.channel.transition_to(
|
||||||
entry.name.as_str().try_into().unwrap(),
|
entry.name.as_str().try_into().unwrap(),
|
||||||
);
|
);
|
||||||
@ -364,18 +406,22 @@ impl Television {
|
|||||||
self.change_channel(new_channel);
|
self.change_channel(new_channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.action_tx
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.send(Action::SelectAndExit)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::CopyEntryToClipboard => {
|
Action::CopyEntryToClipboard => {
|
||||||
if self.mode == Mode::Channel {
|
if self.mode == Mode::Channel {
|
||||||
if let Some(entry) = self.get_selected_entry(None) {
|
if let Some(entries) = self.get_selected_entries(None) {
|
||||||
let mut ctx = ClipboardContext::new().unwrap();
|
let mut ctx = ClipboardContext::new().unwrap();
|
||||||
ctx.set_contents(entry.name).unwrap();
|
ctx.set_contents(
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.name.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
.to_string()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -464,6 +510,7 @@ impl Television {
|
|||||||
f,
|
f,
|
||||||
layout.results,
|
layout.results,
|
||||||
&entries,
|
&entries,
|
||||||
|
self.channel.selected_entries(),
|
||||||
&mut self.results_picker.relative_state,
|
&mut self.results_picker.relative_state,
|
||||||
self.config.ui.input_bar_position,
|
self.config.ui.input_bar_position,
|
||||||
self.config.ui.use_nerd_font_icons,
|
self.config.ui.use_nerd_font_icons,
|
||||||
@ -596,7 +643,10 @@ impl KeyBindings {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisplayableAction::SelectEntry,
|
DisplayableAction::SelectEntry,
|
||||||
serialized_keys_for_actions(self, &[Action::SelectEntry]),
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::ConfirmSelection],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisplayableAction::CopyEntryToClipboard,
|
DisplayableAction::CopyEntryToClipboard,
|
||||||
@ -637,7 +687,10 @@ impl KeyBindings {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisplayableAction::SelectEntry,
|
DisplayableAction::SelectEntry,
|
||||||
serialized_keys_for_actions(self, &[Action::SelectEntry]),
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::ConfirmSelection],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisplayableAction::ToggleRemoteControl,
|
DisplayableAction::ToggleRemoteControl,
|
||||||
@ -660,7 +713,10 @@ impl KeyBindings {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisplayableAction::SelectEntry,
|
DisplayableAction::SelectEntry,
|
||||||
serialized_keys_for_actions(self, &[Action::SelectEntry]),
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::ConfirmSelection],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisplayableAction::Cancel,
|
DisplayableAction::Cancel,
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#f38ba8'
|
|||||||
result_name_fg = '#89b4fa'
|
result_name_fg = '#89b4fa'
|
||||||
result_line_number_fg = '#f9e2af'
|
result_line_number_fg = '#f9e2af'
|
||||||
result_value_fg = '#b4befe'
|
result_value_fg = '#b4befe'
|
||||||
|
selection_fg = '#a6e3a1'
|
||||||
selection_bg = '#313244'
|
selection_bg = '#313244'
|
||||||
match_fg = '#f38ba8'
|
match_fg = '#f38ba8'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = 'bright-red'
|
|||||||
result_name_fg = 'bright-blue'
|
result_name_fg = 'bright-blue'
|
||||||
result_line_number_fg = 'bright-yellow'
|
result_line_number_fg = 'bright-yellow'
|
||||||
result_value_fg = 'white'
|
result_value_fg = 'white'
|
||||||
|
selection_fg = 'bright-green'
|
||||||
selection_bg = 'bright-black'
|
selection_bg = 'bright-black'
|
||||||
match_fg = 'bright-red'
|
match_fg = 'bright-red'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#FF5555'
|
|||||||
result_name_fg = '#BD93F9'
|
result_name_fg = '#BD93F9'
|
||||||
result_line_number_fg = '#F1FA8C'
|
result_line_number_fg = '#F1FA8C'
|
||||||
result_value_fg = '#FF79C6'
|
result_value_fg = '#FF79C6'
|
||||||
|
selection_fg = '#50FA7B'
|
||||||
selection_bg = '#44475A'
|
selection_bg = '#44475A'
|
||||||
match_fg = '#FF5555'
|
match_fg = '#FF5555'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#cc241d'
|
|||||||
result_name_fg = '#83a598'
|
result_name_fg = '#83a598'
|
||||||
result_line_number_fg = '#fabd2f'
|
result_line_number_fg = '#fabd2f'
|
||||||
result_value_fg = '#ebdbb2'
|
result_value_fg = '#ebdbb2'
|
||||||
|
selection_fg = '#b8bb26'
|
||||||
selection_bg = '#32302f'
|
selection_bg = '#32302f'
|
||||||
match_fg = '#fb4934'
|
match_fg = '#fb4934'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#af3a03'
|
|||||||
result_name_fg = '#076678'
|
result_name_fg = '#076678'
|
||||||
result_line_number_fg = '#d79921'
|
result_line_number_fg = '#d79921'
|
||||||
result_value_fg = '#665c54'
|
result_value_fg = '#665c54'
|
||||||
|
selection_fg = '#98971a'
|
||||||
selection_bg = '#ebdbb2'
|
selection_bg = '#ebdbb2'
|
||||||
match_fg = '#af3a03'
|
match_fg = '#af3a03'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#f92672'
|
|||||||
result_name_fg = '#a6e22e'
|
result_name_fg = '#a6e22e'
|
||||||
result_line_number_fg = '#e5b567'
|
result_line_number_fg = '#e5b567'
|
||||||
result_value_fg = '#d6d6d6'
|
result_value_fg = '#d6d6d6'
|
||||||
|
selection_fg = '#66d9ef'
|
||||||
selection_bg = '#494949'
|
selection_bg = '#494949'
|
||||||
match_fg = '#f92672'
|
match_fg = '#f92672'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#bf616a'
|
|||||||
result_name_fg = '#81a1c1'
|
result_name_fg = '#81a1c1'
|
||||||
result_line_number_fg = '#ebcb8b'
|
result_line_number_fg = '#ebcb8b'
|
||||||
result_value_fg = '#d8dee9'
|
result_value_fg = '#d8dee9'
|
||||||
|
selection_fg = '#a3be8c'
|
||||||
selection_bg = '#3b4252'
|
selection_bg = '#3b4252'
|
||||||
match_fg = '#bf616a'
|
match_fg = '#bf616a'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#e06c75'
|
|||||||
result_name_fg = '#61afef'
|
result_name_fg = '#61afef'
|
||||||
result_line_number_fg = '#e5c07b'
|
result_line_number_fg = '#e5c07b'
|
||||||
result_value_fg = '#abb2bf'
|
result_value_fg = '#abb2bf'
|
||||||
|
selection_fg = '#98c379'
|
||||||
selection_bg = '#3e4452'
|
selection_bg = '#3e4452'
|
||||||
match_fg = '#e06c75'
|
match_fg = '#e06c75'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#cb4b16'
|
|||||||
result_name_fg = '#268bd2'
|
result_name_fg = '#268bd2'
|
||||||
result_line_number_fg = '#b58900'
|
result_line_number_fg = '#b58900'
|
||||||
result_value_fg = '#657b83'
|
result_value_fg = '#657b83'
|
||||||
|
selection_fg = '#859900'
|
||||||
selection_bg = '#073642'
|
selection_bg = '#073642'
|
||||||
match_fg = '#cb4b16'
|
match_fg = '#cb4b16'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#cb4b16'
|
|||||||
result_name_fg = '#268bd2'
|
result_name_fg = '#268bd2'
|
||||||
result_line_number_fg = '#b58900'
|
result_line_number_fg = '#b58900'
|
||||||
result_value_fg = '#93a1a1'
|
result_value_fg = '#93a1a1'
|
||||||
|
selection_fg = '#859900'
|
||||||
selection_bg = '#eee8d5'
|
selection_bg = '#eee8d5'
|
||||||
match_fg = '#cb4b16'
|
match_fg = '#cb4b16'
|
||||||
# preview
|
# preview
|
||||||
|
@ -11,6 +11,7 @@ result_count_fg = '#d2788c'
|
|||||||
result_name_fg = '#9a9ac7'
|
result_name_fg = '#9a9ac7'
|
||||||
result_line_number_fg = '#d2a374'
|
result_line_number_fg = '#d2a374'
|
||||||
result_value_fg = '#646477'
|
result_value_fg = '#646477'
|
||||||
|
selection_fg = '#8faf77'
|
||||||
selection_bg = '#282830'
|
selection_bg = '#282830'
|
||||||
match_fg = '#d2788c'
|
match_fg = '#d2788c'
|
||||||
# preview
|
# preview
|
||||||
|
@ -10,6 +10,7 @@ result_count_fg = '#f7768e'
|
|||||||
result_name_fg = '#7aa2f7'
|
result_name_fg = '#7aa2f7'
|
||||||
result_line_number_fg = '#faba4a'
|
result_line_number_fg = '#faba4a'
|
||||||
result_value_fg = '#a9b1d6'
|
result_value_fg = '#a9b1d6'
|
||||||
|
selection_fg = '#9ece6a'
|
||||||
selection_bg = '#414868'
|
selection_bg = '#414868'
|
||||||
match_fg = '#f7768e'
|
match_fg = '#f7768e'
|
||||||
# preview
|
# preview
|
||||||
|
Loading…
x
Reference in New Issue
Block a user