feat: send to channel

This commit is contained in:
alexpasmantier 2024-10-29 21:53:01 +01:00
parent ddedbc11da
commit d0d453fe97
18 changed files with 926 additions and 202 deletions

View File

@ -7,8 +7,9 @@ ctrl-p = "SelectPrevEntry"
ctrl-d = "ScrollPreviewHalfPageDown"
ctrl-u = "ScrollPreviewHalfPageUp"
enter = "SelectEntry"
ctrl-enter = "SendToChannel"
ctrl-y = "CopyEntryToClipboard"
ctrl-s = "ToggleRemoteControl"
alt-s = "ToggleSendToChannel"
[keybindings.RemoteControl]
esc = "Quit"
@ -18,3 +19,12 @@ ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
enter = "SelectEntry"
ctrl-s = "ToggleRemoteControl"
[keybindings.SendToChannel]
esc = "Quit"
down = "SelectNextEntry"
up = "SelectPrevEntry"
ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
enter = "SelectEntry"
alt-s = "ToggleSendToChannel"

374
Cargo.lock generated
View File

@ -195,6 +195,12 @@ dependencies = [
"serde",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -227,6 +233,32 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "calloop"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
"bitflags 2.6.0",
"log",
"polling",
"rustix",
"slab",
"thiserror",
]
[[package]]
name = "calloop-wayland-source"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
"calloop",
"rustix",
"wayland-backend",
"wayland-client",
]
[[package]]
name = "camino"
version = "1.1.9"
@ -343,6 +375,16 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "clipboard-win"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
dependencies = [
"lazy-bytes-cast",
"winapi",
]
[[package]]
name = "clru"
version = "0.6.2"
@ -397,6 +439,15 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.14.1"
@ -457,6 +508,20 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "copypasta"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858"
dependencies = [
"clipboard-win",
"objc",
"objc-foundation",
"objc_id",
"smithay-clipboard",
"x11-clipboard",
]
[[package]]
name = "cpufeatures"
version = "0.2.14"
@ -543,6 +608,12 @@ dependencies = [
"typenum",
]
[[package]]
name = "cursor-icon"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "darling"
version = "0.20.10"
@ -675,6 +746,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
@ -684,6 +764,12 @@ dependencies = [
"const-random",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dunce"
version = "1.0.5"
@ -902,6 +988,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
dependencies = [
"libc",
"windows-targets 0.48.5",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@ -1468,6 +1564,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "home"
version = "0.5.9"
@ -1623,6 +1725,12 @@ dependencies = [
"serde",
]
[[package]]
name = "lazy-bytes-cast"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -1635,6 +1743,16 @@ version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "libloading"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
]
[[package]]
name = "libredox"
version = "0.1.3"
@ -1683,6 +1801,15 @@ dependencies = [
"hashbrown 0.15.0",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.1.0"
@ -1737,7 +1864,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.9",
"libc",
"log",
"wasi",
@ -1800,6 +1927,35 @@ dependencies = [
"libc",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.32.2"
@ -1988,11 +2144,26 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap",
"quick-xml",
"quick-xml 0.32.0",
"serde",
"time",
]
[[package]]
name = "polling"
version = "3.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -2033,6 +2204,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.37"
@ -2187,9 +2367,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.37"
version = "0.38.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
dependencies = [
"bitflags 2.6.0",
"errno",
@ -2219,6 +2399,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -2236,18 +2422,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.213"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.213"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
@ -2352,6 +2538,42 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smithay-client-toolkit"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.6.0",
"calloop",
"calloop-wayland-source",
"cursor-icon",
"libc",
"log",
"memmap2",
"rustix",
"thiserror",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-wlr",
"wayland-scanner",
"xkeysym",
]
[[package]]
name = "smithay-clipboard"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846"
dependencies = [
"libc",
"smithay-client-toolkit",
"wayland-backend",
]
[[package]]
name = "socket2"
version = "0.5.7"
@ -2458,6 +2680,7 @@ dependencies = [
"clap",
"color-eyre",
"config",
"copypasta",
"crossterm",
"derive_deref",
"devicons",
@ -2942,6 +3165,102 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wayland-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"scoped-tls",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
dependencies = [
"bitflags 2.6.0",
"rustix",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-csd-frame"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
dependencies = [
"bitflags 2.6.0",
"cursor-icon",
"wayland-backend",
]
[[package]]
name = "wayland-cursor"
version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c"
dependencies = [
"rustix",
"wayland-client",
"xcursor",
]
[[package]]
name = "wayland-protocols"
version = "0.32.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e"
dependencies = [
"bitflags 2.6.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022"
dependencies = [
"bitflags 2.6.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3"
dependencies = [
"proc-macro2",
"quick-xml 0.36.2",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09"
dependencies = [
"dlib",
"log",
"once_cell",
"pkg-config",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -3130,6 +3449,45 @@ dependencies = [
"memchr",
]
[[package]]
name = "x11-clipboard"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3"
dependencies = [
"libc",
"x11rb",
]
[[package]]
name = "x11rb"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
dependencies = [
"gethostname",
"rustix",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
[[package]]
name = "xcursor"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
[[package]]
name = "xkeysym"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "yaml-rust"
version = "0.4.5"

View File

@ -67,6 +67,7 @@ unicode-width = "0.2.0"
human-panic = "2.0.2"
pretty_assertions = "1.4.1"
termtree = "0.5.1"
copypasta = "0.10.1"
[build-dependencies]
@ -86,7 +87,7 @@ debug = true
[profile.release]
lto = "thin"
lto = "fat"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.28.1", features = ["serde", "use-dev-tty"] }

View File

@ -20,6 +20,7 @@ pub enum Action {
SelectAndExit,
SelectNextEntry,
SelectPrevEntry,
CopyEntryToClipboard,
// navigation actions
GoToPaneUp,
GoToPaneDown,
@ -43,5 +44,5 @@ pub enum Action {
NoOp,
// channel actions
ToggleRemoteControl,
SendToChannel,
ToggleSendToChannel,
}

View File

@ -56,7 +56,7 @@ use color_eyre::Result;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, info};
use crate::channels::{CliTvChannel, TelevisionChannel};
use crate::channels::TelevisionChannel;
use crate::television::{Mode, Television};
use crate::{
action::Action,
@ -234,6 +234,7 @@ impl App {
}
_ => {}
}
// forward action to the television handler
if let Some(action) =
self.television.lock().await.update(action.clone()).await?
{

View File

@ -1,6 +1,6 @@
use crate::entry::Entry;
use color_eyre::eyre::Result;
use television_derive::{Broadcast, CliChannel, UnitChannel};
use television_derive::{Broadcast, ToCliChannel, ToUnitChannel};
mod alias;
mod env;
@ -104,7 +104,7 @@ pub trait OnAir: Send {
/// of carrying the actual channel instances around. It also generates the necessary
/// glue code to automatically create a channel instance from the selected enum variant.
#[allow(dead_code, clippy::module_name_repetitions)]
#[derive(UnitChannel, CliChannel, Broadcast)]
#[derive(ToUnitChannel, ToCliChannel, Broadcast)]
pub enum TelevisionChannel {
/// The environment variables channel.
///
@ -134,11 +134,125 @@ pub enum TelevisionChannel {
/// The remote control channel.
///
/// This channel allows to switch between different channels.
#[exclude_from_unit]
#[exclude_from_cli]
RemoteControl(remote_control::RemoteControl),
}
/// NOTE: this could be generated by a derive macro
macro_rules! variant_to_module {
(Files) => {
files::Channel
};
(Text) => {
text::Channel
};
(GitRepos) => {
git_repos::Channel
};
(Env) => {
env::Channel
};
(Stdin) => {
stdin::Channel
};
(Alias) => {
alias::Channel
};
(RemoteControl) => {
remote_control::RemoteControl
};
}
/// A macro that generates two methods for the `TelevisionChannel` enum based on
/// the transitions defined in the macro call.
///
/// The first method `available_transitions` returns a list of possible transitions
/// from the current channel.
///
/// The second method `transition_to` transitions from the current channel to the
/// target channel.
///
/// # Example
/// The following example defines transitions from the `Files` channel to the `Text`
/// channel and from the `GitRepos` channel to the `Files` and `Text` channels.
/// ```rust
/// define_transitions! {
/// // The `Files` channel can transition to the `Text` channel.
/// Files => [Text],
/// // The `GitRepos` channel can transition to the `Files` and `Text` channels.
/// GitRepos => [Files, Text],
/// }
/// ```
/// This will generate the following methods for the `TelevisionChannel` enum:
/// ```rust
/// impl TelevisionChannel {
/// pub fn available_transitions(&self) -> Vec<UnitChannel> {
/// match self {
/// TelevisionChannel::Files(_) => vec![UnitChannel::Text],
/// TelevisionChannel::GitRepos(_) => vec![UnitChannel::Files, UnitChannel::Text],
/// _ => Vec::new(),
/// }
/// }
///
/// pub fn transition_to(self, target: UnitChannel) -> TelevisionChannel {
/// match (self, target) {
/// (tv_channel @ TelevisionChannel::Files(_), UnitChannel::Text) => {
/// TelevisionChannel::Text(text::Channel::from(tv_channel))
/// },
/// (tv_channel @ TelevisionChannel::GitRepos(_), UnitChannel::Files) => {
/// TelevisionChannel::Files(files::Channel::from(tv_channel))
/// },
/// (tv_channel @ TelevisionChannel::GitRepos(_), UnitChannel::Text) => {
/// TelevisionChannel::Text(text::Channel::from(tv_channel))
/// },
/// _ => unreachable!(),
/// }
/// }
/// }
///
///
macro_rules! define_transitions {
(
$(
$from_variant:ident => [ $($to_variant:ident),* $(,)? ],
)*
) => {
impl TelevisionChannel {
pub fn available_transitions(&self) -> Vec<UnitChannel> {
match self {
$(
TelevisionChannel::$from_variant(_) => vec![
$( UnitChannel::$to_variant ),*
],
)*
_ => Vec::new(),
}
}
pub fn transition_to(&mut self, target: UnitChannel) -> TelevisionChannel {
match (self, target) {
$(
$(
(tv_channel @ TelevisionChannel::$from_variant(_), UnitChannel::$to_variant) => {
TelevisionChannel::$to_variant(
<variant_to_module!($to_variant)>::from(tv_channel)
)
},
)*
)*
_ => unreachable!(),
}
}
}
}
}
define_transitions! {
Files => [Text],
GitRepos => [Files, Text],
}
/// NOTE: this could/should be generated by a macro
impl TryFrom<&Entry> for TelevisionChannel {
type Error = String;

View File

@ -52,19 +52,19 @@ impl Default for Channel {
}
}
impl From<TelevisionChannel> for Channel {
fn from(channel: TelevisionChannel) -> Self {
match channel {
TelevisionChannel::Files(channel) => channel,
TelevisionChannel::GitRepos(mut channel) => {
let git_project_paths = channel
.results(channel.result_count(), 0)
.iter()
.map(|entry| PathBuf::from(entry.name.clone()))
.collect();
Self::new(git_project_paths)
impl From<&mut TelevisionChannel> for Channel {
fn from(value: &mut TelevisionChannel) -> Self {
match value {
c @ TelevisionChannel::GitRepos(_) => {
let entries = c.results(c.result_count(), 0);
Self::new(
entries
.iter()
.map(|entry| PathBuf::from(entry.name.clone()))
.collect(),
)
}
_ => Channel::default(),
_ => unreachable!(),
}
}
}
@ -166,10 +166,10 @@ async fn load_files(paths: Vec<PathBuf>, injector: Injector<String>) {
if let Ok(entry) = result {
if entry.file_type().unwrap().is_file() {
let file_path = preprocess_line(
&*entry
&entry
.path()
.strip_prefix(&current_dir)
.unwrap()
.unwrap_or(entry.path())
.to_string_lossy(),
);
let _ = injector.push(file_path, |e, cols| {

View File

@ -7,6 +7,7 @@ use nucleo::{
Config, Nucleo,
};
use crate::channels::{TelevisionChannel, UnitChannel};
use crate::{
channels::{CliTvChannel, OnAir},
entry::Entry,
@ -15,7 +16,7 @@ use crate::{
};
pub struct RemoteControl {
matcher: Nucleo<CliTvChannel>,
matcher: Nucleo<String>,
last_pattern: String,
result_count: u32,
total_count: u32,
@ -25,7 +26,7 @@ pub struct RemoteControl {
const NUM_THREADS: usize = 1;
impl RemoteControl {
pub fn new() -> Self {
pub fn new(channels: Vec<UnitChannel>) -> Self {
let matcher = Nucleo::new(
Config::DEFAULT,
Arc::new(|| {}),
@ -33,9 +34,9 @@ impl RemoteControl {
1,
);
let injector = matcher.injector();
for variant in CliTvChannel::value_variants() {
let _ = injector.push(*variant, |e, cols| {
cols[0] = (*e).to_string().into();
for channel in channels {
let _ = injector.push(channel.to_string(), |e, cols| {
cols[0] = e.clone().into();
});
}
RemoteControl {
@ -47,12 +48,23 @@ impl RemoteControl {
}
}
pub fn with_transitions_from(
television_channel: &TelevisionChannel,
) -> Self {
Self::new(television_channel.available_transitions())
}
const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for RemoteControl {
fn default() -> Self {
Self::new()
Self::new(
CliTvChannel::value_variants()
.iter()
.map(|v| v.to_string().as_str().into())
.collect(),
)
}
}
@ -103,7 +115,7 @@ impl OnAir for RemoteControl {
let indices = indices.drain(..);
let name = item.matcher_columns[0].to_string();
Entry::new(name.clone(), PreviewType::Basic)
Entry::new(name, PreviewType::Basic)
.with_name_match_ranges(
indices.map(|i| (i, i + 1)).collect(),
)

View File

@ -1,4 +1,5 @@
use devicons::FileIcon;
use ignore::WalkState;
use nucleo::{
pattern::{CaseMatching, Normalization},
Config, Injector, Nucleo,
@ -8,11 +9,11 @@ use std::{
io::{BufRead, Read, Seek},
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
u32,
};
use tracing::{debug, warn};
use tracing::{debug, info};
use super::OnAir;
use super::{OnAir, TelevisionChannel};
use crate::previewers::PreviewType;
use crate::utils::{
files::{is_not_text, walk_builder, DEFAULT_NUM_THREADS},
@ -51,11 +52,11 @@ pub struct Channel {
}
impl Channel {
pub fn new(working_dir: &Path) -> Self {
pub fn new(directories: Vec<PathBuf>) -> Self {
let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1);
// start loading files in the background
let crawl_handle = tokio::spawn(load_candidates(
working_dir.to_path_buf(),
let crawl_handle = tokio::spawn(crawl_for_candidates(
directories,
matcher.injector(),
));
Channel {
@ -68,12 +69,88 @@ impl Channel {
}
}
fn from_file_paths(file_paths: Vec<PathBuf>) -> Self {
let matcher = Nucleo::new(
Config::DEFAULT.match_paths(),
Arc::new(|| {}),
None,
1,
);
let injector = matcher.injector();
let current_dir = std::env::current_dir().unwrap();
let crawl_handle = tokio::spawn(async move {
let mut lines_in_mem = 0;
for path in file_paths {
if lines_in_mem > MAX_LINES_IN_MEM {
break;
}
if let Some(injected_lines) =
try_inject_lines(injector.clone(), &current_dir, &path)
{
lines_in_mem += injected_lines;
}
}
});
Channel {
matcher,
last_pattern: String::new(),
result_count: 0,
total_count: 0,
running: false,
crawl_handle,
}
}
const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for Channel {
fn default() -> Self {
Self::new(&std::env::current_dir().unwrap())
Self::new(vec![std::env::current_dir().unwrap()])
}
}
/// Since we're limiting the number of lines in memory, it makes sense to also limit the number of files
/// we're willing to search in when piping from the `Files` channel.
/// This prevents blocking the UI for too long when piping from a channel with a lot of files.
///
/// This should be calculated based on the number of lines we're willing to keep in memory:
/// `MAX_LINES_IN_MEM / 100` (assuming 100 lines per file on average).
const MAX_PIPED_FILES: usize = MAX_LINES_IN_MEM / 200;
impl From<&mut TelevisionChannel> for Channel {
fn from(value: &mut TelevisionChannel) -> Self {
match value {
c @ TelevisionChannel::Files(_) => {
let entries = c.results(
c.result_count().min(
u32::try_from(MAX_PIPED_FILES).unwrap_or(u32::MAX),
),
0,
);
Self::from_file_paths(
entries
.iter()
.flat_map(|entry| {
PathBuf::from(entry.name.clone()).canonicalize()
})
.collect(),
)
}
c @ TelevisionChannel::GitRepos(_) => {
let entries = c.results(c.result_count(), 0);
Self::new(
entries
.iter()
.flat_map(|entry| {
PathBuf::from(entry.name.clone()).canonicalize()
})
.collect(),
)
}
_ => unreachable!(),
}
}
}
@ -106,7 +183,7 @@ impl OnAir for Channel {
.matched_items(
offset
..(num_entries + offset)
.min(snapshot.matched_item_count()),
.min(snapshot.matched_item_count()),
)
.map(move |item| {
snapshot.pattern().column_pattern(0).indices(
@ -125,11 +202,11 @@ impl OnAir for Channel {
display_path.clone() + &item.data.line_number.to_string(),
PreviewType::Files,
)
.with_display_name(display_path)
.with_value(line)
.with_value_match_ranges(indices.map(|i| (i, i + 1)).collect())
.with_icon(FileIcon::from(item.data.path.as_path()))
.with_line_number(item.data.line_number)
.with_display_name(display_path)
.with_value(line)
.with_value_match_ranges(indices.map(|i| (i, i + 1)).collect())
.with_icon(FileIcon::from(item.data.path.as_path()))
.with_line_number(item.data.line_number)
})
.collect()
}
@ -184,98 +261,120 @@ const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
///
/// A typical line should take somewhere around 100 bytes in memory (for utf8 english text),
/// so this should take around 100 x `5_000_000` = 500MB of memory.
const MAX_IN_MEMORY_LINES: usize = 5_000_000;
const MAX_LINES_IN_MEM: usize = 5_000_000;
#[allow(clippy::unused_async)]
async fn load_candidates(path: PathBuf, injector: Injector<CandidateLine>) {
async fn crawl_for_candidates(
directories: Vec<PathBuf>,
injector: Injector<CandidateLine>,
) {
if directories.is_empty() {
return;
}
let current_dir = std::env::current_dir().unwrap();
let walker =
walk_builder(&path, *DEFAULT_NUM_THREADS, None, None).build_parallel();
let mut walker =
walk_builder(&directories[0], *DEFAULT_NUM_THREADS, None, None);
for path in directories[1..].iter() {
walker.add(path);
}
let lines_in_mem = Arc::new(AtomicUsize::new(0));
walker.run(|| {
walker.build_parallel().run(|| {
let injector = injector.clone();
let current_dir = current_dir.clone();
let lines_in_mem = lines_in_mem.clone();
Box::new(move |result| {
if lines_in_mem.load(std::sync::atomic::Ordering::Relaxed) > MAX_IN_MEMORY_LINES {
return ignore::WalkState::Quit;
if lines_in_mem.load(std::sync::atomic::Ordering::Relaxed)
> MAX_LINES_IN_MEM
{
return WalkState::Quit;
}
if let Ok(entry) = result {
if entry.file_type().unwrap().is_file() {
if let Ok(m) = entry.metadata() {
if m.len() > MAX_FILE_SIZE {
return ignore::WalkState::Continue;
return WalkState::Continue;
}
}
// iterate over the lines of the file
match File::open(entry.path()) {
Ok(file) => {
// is the file a text-based file?
let mut reader = std::io::BufReader::new(&file);
let mut buffer = [0u8; 128];
match reader.read(&mut buffer) {
Ok(bytes_read) => {
if (bytes_read == 0)
|| is_not_text(&buffer)
.unwrap_or(false)
|| proportion_of_printable_ascii_characters(&buffer)
< PRINTABLE_ASCII_THRESHOLD
{
return ignore::WalkState::Continue;
}
reader
.seek(std::io::SeekFrom::Start(0))
.unwrap();
}
Err(_) => {
return ignore::WalkState::Continue;
}
}
// read the lines of the file
let mut line_number = 0;
for maybe_line in reader.lines() {
match maybe_line {
Ok(l) => {
line_number += 1;
let line = preprocess_line(&l);
if line.is_empty() {
debug!("Empty line");
continue;
}
let candidate = CandidateLine::new(
entry
.path()
.strip_prefix(&current_dir)
.unwrap()
.to_path_buf(),
line,
line_number,
);
let _ = injector.push(
candidate,
|c, cols| {
cols[0] =
c.line.clone().into();
},
);
lines_in_mem.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
Err(e) => {
info!("Error reading line: {:?}", e);
break;
}
}
}
}
Err(e) => {
info!("Error opening file: {:?}", e);
}
// try to inject the lines of the file
if let Some(injected_lines) = try_inject_lines(
injector.clone(),
&current_dir,
entry.path(),
) {
lines_in_mem.fetch_add(
injected_lines,
std::sync::atomic::Ordering::Relaxed,
);
}
}
}
ignore::WalkState::Continue
WalkState::Continue
})
});
}
fn try_inject_lines(
injector: Injector<CandidateLine>,
current_dir: &PathBuf,
path: &Path,
) -> Option<usize> {
match File::open(path) {
Ok(file) => {
// is the file a text-based file?
let mut reader = std::io::BufReader::new(&file);
let mut buffer = [0u8; 128];
match reader.read(&mut buffer) {
Ok(bytes_read) => {
if (bytes_read == 0)
|| is_not_text(&buffer).unwrap_or(false)
|| proportion_of_printable_ascii_characters(&buffer)
< PRINTABLE_ASCII_THRESHOLD
{
return None;
}
reader.seek(std::io::SeekFrom::Start(0)).unwrap();
}
Err(_) => {
return None;
}
}
// read the lines of the file
let mut line_number = 0;
let mut injected_lines = 0;
for maybe_line in reader.lines() {
match maybe_line {
Ok(l) => {
line_number += 1;
let line = preprocess_line(&l);
if line.is_empty() {
debug!("Empty line");
continue;
}
let candidate = CandidateLine::new(
path.strip_prefix(&current_dir)
.unwrap_or(path)
.to_path_buf(),
line,
line_number,
);
let _ = injector.push(candidate, |c, cols| {
cols[0] = c.line.clone().into();
});
injected_lines += 1;
}
Err(e) => {
warn!("Error reading line: {:?}", e);
break;
}
}
}
Some(injected_lines)
}
Err(e) => {
warn!("Error opening file {:?}: {:?}", path, e);
None
}
}
}

View File

@ -1,8 +1,13 @@
use devicons::FileIcon;
use crate::previewers::PreviewType;
use crate::utils::strings::preprocess_line;
/// NOTE: having an enum for entry types would be nice since it would allow
/// having a nicer implementation for transitions between channels. This would
/// permit implementing `From<EntryType>` for channels which would make the
/// 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)]
pub struct Entry {
pub name: String,
@ -18,7 +23,7 @@ pub struct Entry {
impl Entry {
pub fn new(name: String, preview_type: PreviewType) -> Self {
Self {
name: preprocess_line(&name),
name,
display_name: None,
value: None,
name_match_ranges: None,
@ -30,12 +35,12 @@ impl Entry {
}
pub fn with_display_name(mut self, display_name: String) -> Self {
self.display_name = Some(preprocess_line(&display_name));
self.display_name = Some(display_name);
self
}
pub fn with_value(mut self, value: String) -> Self {
self.value = Some(preprocess_line(&value));
self.value = Some(value);
self
}

View File

@ -74,26 +74,26 @@ impl Display for Key {
match self {
Key::Backspace => write!(f, "Backspace"),
Key::Enter => write!(f, "Enter"),
Key::Left => write!(f, ""),
Key::Right => write!(f, ""),
Key::Up => write!(f, ""),
Key::Down => write!(f, ""),
Key::Left => write!(f, "Left"),
Key::Right => write!(f, "Right"),
Key::Up => write!(f, "Up"),
Key::Down => write!(f, "Down"),
Key::CtrlSpace => write!(f, "Ctrl-Space"),
Key::CtrlBackspace => write!(f, "Ctrl-Backspace"),
Key::CtrlEnter => write!(f, "Ctrl-Enter"),
Key::CtrlLeft => write!(f, "Ctrl-"),
Key::CtrlRight => write!(f, "Ctrl-"),
Key::CtrlUp => write!(f, "Ctrl-"),
Key::CtrlDown => write!(f, "Ctrl-"),
Key::CtrlLeft => write!(f, "Ctrl-Left"),
Key::CtrlRight => write!(f, "Ctrl-Right"),
Key::CtrlUp => write!(f, "Ctrl-Up"),
Key::CtrlDown => write!(f, "Ctrl-Down"),
Key::CtrlDelete => write!(f, "Ctrl-Del"),
Key::AltSpace => write!(f, "Alt+Space"),
Key::AltEnter => write!(f, "Alt+Enter"),
Key::AltBackspace => write!(f, "Alt+Backspace"),
Key::AltDelete => write!(f, "Alt+Delete"),
Key::AltUp => write!(f, "Alt"),
Key::AltDown => write!(f, "Alt"),
Key::AltLeft => write!(f, "Alt"),
Key::AltRight => write!(f, "Alt"),
Key::AltSpace => write!(f, "Alt-Space"),
Key::AltEnter => write!(f, "Alt-Enter"),
Key::AltBackspace => write!(f, "Alt-Backspace"),
Key::AltDelete => write!(f, "Alt-Delete"),
Key::AltUp => write!(f, "Alt-Up"),
Key::AltDown => write!(f, "Alt-Down"),
Key::AltLeft => write!(f, "Alt-Left"),
Key::AltRight => write!(f, "Alt-Right"),
Key::Home => write!(f, "Home"),
Key::End => write!(f, "End"),
Key::PageUp => write!(f, "PageUp"),
@ -103,7 +103,7 @@ impl Display for Key {
Key::Insert => write!(f, "Insert"),
Key::F(k) => write!(f, "F{k}"),
Key::Char(c) => write!(f, "{c}"),
Key::Alt(c) => write!(f, "Alt+{c}"),
Key::Alt(c) => write!(f, "Alt-{c}"),
Key::Ctrl(c) => write!(f, "Ctrl-{c}"),
Key::Null => write!(f, "Null"),
Key::Esc => write!(f, "Esc"),

View File

@ -61,7 +61,7 @@ fn build_tree_preview(entry: &Entry) -> Preview {
fn label<P: AsRef<Path>>(p: P, strip: &str) -> String {
let icon = FileIcon::from(&p);
let path = p.as_ref().strip_prefix(strip).unwrap();
let path = p.as_ref().strip_prefix(strip).unwrap_or(p.as_ref());
format!("{} {}", icon, path.display())
}

View File

@ -1,6 +1,5 @@
use crate::channels::remote_control::RemoteControl;
use crate::channels::OnAir;
use crate::channels::UnitChannel;
use crate::channels::{OnAir, UnitChannel};
use crate::picker::Picker;
use crate::ui::layout::{Dimensions, Layout};
use crate::utils::strings::EMPTY_STRING;
@ -14,6 +13,7 @@ use crate::{
};
use crate::{previewers::Previewer, ui::spinner::SpinnerState};
use color_eyre::Result;
use copypasta::{ClipboardContext, ClipboardProvider};
use futures::executor::block_on;
use ratatui::{layout::Rect, style::Color, widgets::Paragraph, Frame};
use serde::{Deserialize, Serialize};
@ -65,7 +65,7 @@ impl Television {
config: Config::default(),
channel,
remote_control: TelevisionChannel::RemoteControl(
RemoteControl::new(),
RemoteControl::default(),
),
mode: Mode::Channel,
current_pattern: EMPTY_STRING.to_string(),
@ -286,6 +286,9 @@ impl Television {
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
Action::ToggleRemoteControl => match self.mode {
Mode::Channel => {
self.remote_control = TelevisionChannel::RemoteControl(
RemoteControl::default(),
);
self.mode = Mode::RemoteControl;
}
Mode::RemoteControl => {
@ -307,6 +310,7 @@ impl Television {
.send(Action::SelectAndExit)?,
Mode::RemoteControl => {
if let Ok(new_channel) =
// FIXME: this is kind of shitty
TelevisionChannel::try_from(&entry)
{
// this resets the RC picker
@ -318,24 +322,41 @@ impl Television {
}
}
Mode::SendToChannel => {
// if let Ok(new_channel) =
// UnitChannel::try_from(&entry)
// {
// }
self.reset_screen();
let new_channel = self
.channel
.transition_to(entry.name.as_str().into());
self.reset_picker_selection();
self.reset_picker_input();
self.remote_control.find(EMPTY_STRING);
self.mode = Mode::Channel;
// TODO: spawn new channel with selected entries
self.change_channel(new_channel);
}
}
}
}
Action::SendToChannel => {
self.mode = Mode::SendToChannel;
// TODO: build new guide from current channel based on which are pipeable into
self.remote_control =
TelevisionChannel::RemoteControl(RemoteControl::new());
self.reset_screen();
}
Action::CopyEntryToClipboard => match self.mode {
Mode::Channel => {
if let Some(entry) = self.get_selected_entry(None) {
let mut ctx = ClipboardContext::new().unwrap();
ctx.set_contents(entry.name).unwrap();
}
}
_ => {}
},
Action::ToggleSendToChannel => match self.mode {
Mode::Channel | Mode::RemoteControl => {
self.mode = Mode::SendToChannel;
self.remote_control = TelevisionChannel::RemoteControl(
RemoteControl::with_transitions_from(&self.channel),
);
}
Mode::SendToChannel => {
self.reset_picker_input();
self.remote_control.find(EMPTY_STRING);
self.reset_picker_selection();
self.mode = Mode::Channel;
}
},
_ => {}
}
Ok(None)
@ -395,7 +416,7 @@ impl Television {
)?;
// remote control
if matches!(self.mode, Mode::RemoteControl) {
if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) {
self.draw_remote_control(f, &layout.remote_control.unwrap())?;
}
Ok(())

View File

@ -23,7 +23,9 @@ impl Television {
Mode::RemoteControl => {
self.build_keymap_table_for_channel_selection()
}
Mode::SendToChannel => self.build_keymap_table_for_channel(),
Mode::SendToChannel => {
self.build_keymap_table_for_channel_transitions()
}
}
}
@ -35,7 +37,7 @@ impl Television {
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",
"Results navigation",
vec![prev, next],
key_color,
));
@ -46,7 +48,7 @@ impl Television {
let down_keys =
keys_for_action(keymap, &Action::ScrollPreviewHalfPageDown);
let preview_row = Row::new(build_cells_for_key_groups(
"Preview navigation",
"Preview navigation",
vec![up_keys, down_keys],
key_color,
));
@ -54,16 +56,25 @@ impl Television {
// 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",
"Select entry",
vec![select_entry_keys],
key_color,
));
// Copy entry to clipboard
let copy_entry_keys =
keys_for_action(keymap, &Action::CopyEntryToClipboard);
let copy_entry_row = Row::new(build_cells_for_key_groups(
"Copy entry to clipboard",
vec![copy_entry_keys],
key_color,
));
// Send to channel
let send_to_channel_keys =
keys_for_action(keymap, &Action::SendToChannel);
keys_for_action(keymap, &Action::ToggleSendToChannel);
let send_to_channel_row = Row::new(build_cells_for_key_groups(
"⇉ Send results to",
"Send results to",
vec![send_to_channel_keys],
key_color,
));
@ -72,7 +83,7 @@ impl Television {
let switch_channels_keys =
keys_for_action(keymap, &Action::ToggleRemoteControl);
let switch_channels_row = Row::new(build_cells_for_key_groups(
"Toggle Remote control",
"Toggle Remote control",
vec![switch_channels_keys],
key_color,
));
@ -81,7 +92,7 @@ impl Television {
// Quit ⏼
let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row = Row::new(build_cells_for_key_groups(
"Quit",
"Quit",
vec![quit_keys],
key_color,
));
@ -93,6 +104,7 @@ impl Television {
results_row,
preview_row,
select_entry_row,
copy_entry_row,
send_to_channel_row,
switch_channels_row,
quit_row,
@ -111,7 +123,7 @@ impl Television {
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(
"Browse channels",
"Browse channels",
vec![prev, next],
key_color,
));
@ -119,16 +131,16 @@ impl Television {
// Select entry
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
let select_entry_row = Row::new(build_cells_for_key_groups(
"Select channel",
"Select channel",
vec![select_entry_keys],
key_color,
));
// Switch channels
// Remote control
let switch_channels_keys =
keys_for_action(keymap, &Action::ToggleRemoteControl);
let switch_channels_row = Row::new(build_cells_for_key_groups(
"Toggle Remote control",
"Toggle Remote control",
vec![switch_channels_keys],
key_color,
));
@ -136,7 +148,7 @@ impl Television {
// Quit
let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row = Row::new(build_cells_for_key_groups(
"Quit",
"Quit",
vec![quit_keys],
key_color,
));
@ -147,6 +159,52 @@ impl Television {
))
}
fn build_keymap_table_for_channel_transitions<'a>(
&self,
) -> Result<Table<'a>> {
let keymap = self.keymap_for_mode()?;
let key_color = mode_color(self.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(
"Browse channels",
vec![prev, next],
key_color,
));
// Select entry
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
let select_entry_row = Row::new(build_cells_for_key_groups(
"Send to channel",
vec![select_entry_keys],
key_color,
));
// Cancel
let cancel_keys =
keys_for_action(keymap, &Action::ToggleSendToChannel);
let cancel_row = Row::new(build_cells_for_key_groups(
"Cancel",
vec![cancel_keys],
key_color,
));
// Quit
let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row = Row::new(build_cells_for_key_groups(
"Quit",
vec![quit_keys],
key_color,
));
Ok(Table::new(
vec![results_row, select_entry_row, cancel_row, quit_row],
vec![Constraint::Fill(1), Constraint::Fill(2)],
))
}
/// Get the keymap for the current mode.
///
/// # Returns

View File

@ -1,9 +1,9 @@
use crate::television::Mode;
use ratatui::style::Color;
const CHANNEL_COLOR: Color = Color::LightYellow;
const REMOTE_CONTROL_COLOR: Color = Color::LightMagenta;
const SEND_TO_CHANNEL_COLOR: Color = Color::LightCyan;
const CHANNEL_COLOR: Color = Color::Indexed(222);
const REMOTE_CONTROL_COLOR: Color = Color::Indexed(1);
const SEND_TO_CHANNEL_COLOR: Color = Color::Indexed(105);
pub fn mode_color(mode: Mode) -> Color {
match mode {

View File

@ -24,11 +24,11 @@ impl Television {
.direction(Direction::Vertical)
.constraints(
[
Constraint::Fill(1),
Constraint::Min(3),
Constraint::Length(3),
Constraint::Length(20),
]
.as_ref(),
.as_ref(),
)
.split(*area);
self.draw_rc_channels(f, &layout[0])?;
@ -106,7 +106,7 @@ impl Television {
.fg(crate::television::DEFAULT_INPUT_FG)
.bold(),
))
.block(prompt_symbol_block);
.block(prompt_symbol_block);
f.render_widget(arrow, inner_input_chunks[0]);
let interactive_input_block = Block::default();
@ -131,8 +131,8 @@ impl Television {
// 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,
)?,
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,
));

View File

@ -1,10 +1,10 @@
use lazy_static::lazy_static;
use std::fmt::Write;
use tracing::debug;
pub fn next_char_boundary(s: &str, start: usize) -> usize {
let mut i = start;
while !s.is_char_boundary(i) {
let len = s.len();
while !s.is_char_boundary(i) && i < len - 1 {
i += 1;
}
i
@ -12,7 +12,7 @@ pub fn next_char_boundary(s: &str, start: usize) -> usize {
pub fn prev_char_boundary(s: &str, start: usize) -> usize {
let mut i = start;
while !s.is_char_boundary(i) {
while !s.is_char_boundary(i) && i > 0 {
i -= 1;
}
i
@ -23,6 +23,9 @@ pub fn slice_at_char_boundaries(
start_byte_index: usize,
end_byte_index: usize,
) -> &str {
if start_byte_index > end_byte_index || start_byte_index > s.len() || end_byte_index > s.len() {
return EMPTY_STRING;
}
&s[prev_char_boundary(s, start_byte_index)
..next_char_boundary(s, end_byte_index)]
}
@ -97,10 +100,7 @@ pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String {
output.push(*NULL_SYMBOL);
}
// everything else
c => {
debug!("char: {:?}", c);
output.push(c)
}
c => output.push(c),
}
} else {
write!(output, "\\x{:02X}", input[idx]).ok();
@ -138,8 +138,8 @@ pub fn preprocess_line(line: &str) -> String {
line
}
}
.trim_end_matches(['\r', '\n', '\0'])
.as_bytes(),
.trim_end_matches(['\r', '\n', '\0'])
.as_bytes(),
TAB_WIDTH,
)
}

View File

@ -6,10 +6,10 @@ use quote::quote;
///
/// ```rust
/// use crate::channels::{TelevisionChannel, OnAir};
/// use television_derive::CliChannel;
/// use television_derive::ToCliChannel;
/// use crate::channels::{files, text};
///
/// #[derive(CliChannel)]
/// #[derive(ToCliChannel)]
/// enum TelevisionChannel {
/// Files(files::Channel),
/// Text(text::Channel),
@ -25,7 +25,7 @@ use quote::quote;
///
/// Any variant that should not be included in the CLI should be annotated with
/// `#[exclude_from_cli]`.
#[proc_macro_derive(CliChannel, attributes(exclude_from_cli))]
#[proc_macro_derive(ToCliChannel, attributes(exclude_from_cli))]
pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
@ -35,12 +35,12 @@ pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
impl_cli_channel(&ast)
}
fn has_exclude_attr(attrs: &[syn::Attribute]) -> bool {
attrs
.iter()
.any(|attr| attr.path().is_ident("exclude_from_cli"))
fn has_attribute(attrs: &[syn::Attribute], attribute: &str) -> bool {
attrs.iter().any(|attr| attr.path().is_ident(attribute))
}
const EXCLUDE_FROM_CLI: &str = "exclude_from_cli";
fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// check that the struct is an enum
let variants = if let syn::Data::Enum(data_enum) = &ast.data {
@ -58,7 +58,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// create the CliTvChannel enum
let cli_enum_variants = variants
.iter()
.filter(|variant| !has_exclude_attr(&variant.attrs))
.filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI))
.map(|variant| {
let variant_name = &variant.ident;
quote! {
@ -80,7 +80,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// Generate the match arms for the `to_channel` method
let arms = variants.iter().filter(
|variant| !has_exclude_attr(&variant.attrs)
|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI),
).map(|variant| {
let variant_name = &variant.ident;
@ -255,7 +255,7 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream {
///
/// The `UnitChannel` enum is used as a unit variant of the `TelevisionChannel`
/// enum.
#[proc_macro_derive(UnitChannel)]
#[proc_macro_derive(ToUnitChannel, attributes(exclude_from_unit))]
pub fn unit_channel_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
@ -265,6 +265,8 @@ pub fn unit_channel_derive(input: TokenStream) -> TokenStream {
impl_unit_channel(&ast)
}
const EXCLUDE_FROM_UNIT: &str = "exclude_from_unit";
fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
// Ensure the struct is an enum
let variants = if let syn::Data::Enum(data_enum) = &ast.data {
@ -279,7 +281,17 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
"#[derive(UnitChannel)] requires at least one variant"
);
let variant_names: Vec<_> = variants.iter().map(|v| &v.ident).collect();
let variant_names: Vec<_> = variants
.iter()
.filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_UNIT))
.map(|v| &v.ident)
.collect();
let excluded_variants: Vec<_> = variants
.iter()
.filter(|variant| has_attribute(&variant.attrs, EXCLUDE_FROM_UNIT))
.map(|v| &v.ident)
.collect();
// Generate a unit enum from the given enum
let unit_enum = quote! {
@ -310,7 +322,37 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
fn from(channel: &TelevisionChannel) -> Self {
match channel {
#(
TelevisionChannel::#variant_names(_) => UnitChannel::#variant_names,
TelevisionChannel::#variant_names(_) => Self::#variant_names,
)*
#(
TelevisionChannel::#excluded_variants(_) => panic!("Cannot convert excluded variant to unit channel."),
)*
}
}
}
};
// Generate From<&str> implementation
let from_str_impl = quote! {
impl From<&str> for UnitChannel {
fn from(channel: &str) -> Self {
match channel {
#(
stringify!(#variant_names) => Self::#variant_names,
)*
_ => panic!("Invalid unit channel name."),
}
}
}
};
// Generate Into<&str> implementation
let into_str_impl = quote! {
impl Into<&str> for UnitChannel {
fn into(self) -> &'static str {
match self {
#(
UnitChannel::#variant_names => stringify!(#variant_names),
)*
}
}
@ -321,6 +363,8 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
#unit_enum
#into_impl
#from_impl
#from_str_impl
#into_str_impl
};
gen.into()