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