From d0d453fe9748c42b7d81d7a2bfbad6fe0d966c84 Mon Sep 17 00:00:00 2001 From: alexpasmantier Date: Tue, 29 Oct 2024 21:53:01 +0100 Subject: [PATCH] feat: send to channel --- .config/config.toml | 12 +- Cargo.lock | 374 ++++++++++++++++++- Cargo.toml | 3 +- crates/television/action.rs | 3 +- crates/television/app.rs | 3 +- crates/television/channels.rs | 120 +++++- crates/television/channels/files.rs | 28 +- crates/television/channels/remote_control.rs | 26 +- crates/television/channels/text.rs | 269 ++++++++----- crates/television/entry.rs | 13 +- crates/television/event.rs | 34 +- crates/television/previewers/directory.rs | 2 +- crates/television/television.rs | 55 ++- crates/television/ui/keymap.rs | 84 ++++- crates/television/ui/mode.rs | 6 +- crates/television/ui/remote_control.rs | 10 +- crates/television/utils/strings.rs | 18 +- crates/television_derive/src/lib.rs | 68 +++- 18 files changed, 926 insertions(+), 202 deletions(-) diff --git a/.config/config.toml b/.config/config.toml index c49e675..026c891 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -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" diff --git a/Cargo.lock b/Cargo.lock index e2776f0..e1ac786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index ecabf21..960c54d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/crates/television/action.rs b/crates/television/action.rs index e9de727..29312d5 100644 --- a/crates/television/action.rs +++ b/crates/television/action.rs @@ -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, } diff --git a/crates/television/app.rs b/crates/television/app.rs index 0f0256d..c9e1aa5 100644 --- a/crates/television/app.rs +++ b/crates/television/app.rs @@ -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? { diff --git a/crates/television/channels.rs b/crates/television/channels.rs index aabe860..070641f 100644 --- a/crates/television/channels.rs +++ b/crates/television/channels.rs @@ -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 { +/// 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 { + 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( + ::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; diff --git a/crates/television/channels/files.rs b/crates/television/channels/files.rs index b7da0d9..914e7e3 100644 --- a/crates/television/channels/files.rs +++ b/crates/television/channels/files.rs @@ -52,19 +52,19 @@ impl Default for Channel { } } -impl From 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, injector: Injector) { if let Ok(entry) = result { if entry.file_type().unwrap().is_file() { let file_path = preprocess_line( - &*entry + &entry .path() .strip_prefix(¤t_dir) - .unwrap() + .unwrap_or(entry.path()) .to_string_lossy(), ); let _ = injector.push(file_path, |e, cols| { diff --git a/crates/television/channels/remote_control.rs b/crates/television/channels/remote_control.rs index 9a1d3aa..7416e6a 100644 --- a/crates/television/channels/remote_control.rs +++ b/crates/television/channels/remote_control.rs @@ -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, + matcher: Nucleo, 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) -> 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(), ) diff --git a/crates/television/channels/text.rs b/crates/television/channels/text.rs index 7996047..8559c94 100644 --- a/crates/television/channels/text.rs +++ b/crates/television/channels/text.rs @@ -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) -> 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) -> 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(), ¤t_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) { +async fn crawl_for_candidates( + directories: Vec, + injector: Injector, +) { + 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(¤t_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(), + ¤t_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, + current_dir: &PathBuf, + path: &Path, +) -> Option { + 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(¤t_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 + } + } +} diff --git a/crates/television/entry.rs b/crates/television/entry.rs index 448179a..771ec9d 100644 --- a/crates/television/entry.rs +++ b/crates/television/entry.rs @@ -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` 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 } diff --git a/crates/television/event.rs b/crates/television/event.rs index 07fb005..e350fad 100644 --- a/crates/television/event.rs +++ b/crates/television/event.rs @@ -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"), diff --git a/crates/television/previewers/directory.rs b/crates/television/previewers/directory.rs index bbc000d..fddb421 100644 --- a/crates/television/previewers/directory.rs +++ b/crates/television/previewers/directory.rs @@ -61,7 +61,7 @@ fn build_tree_preview(entry: &Entry) -> Preview { fn label>(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()) } diff --git a/crates/television/television.rs b/crates/television/television.rs index 2504d62..ef58afd 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -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(()) diff --git a/crates/television/ui/keymap.rs b/crates/television/ui/keymap.rs index df1f945..fb42153 100644 --- a/crates/television/ui/keymap.rs +++ b/crates/television/ui/keymap.rs @@ -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> { + 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 diff --git a/crates/television/ui/mode.rs b/crates/television/ui/mode.rs index 47b3ba5..a898b5f 100644 --- a/crates/television/ui/mode.rs +++ b/crates/television/ui/mode.rs @@ -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 { diff --git a/crates/television/ui/remote_control.rs b/crates/television/ui/remote_control.rs index 7a66cf6..8772926 100644 --- a/crates/television/ui/remote_control.rs +++ b/crates/television/ui/remote_control.rs @@ -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, )); diff --git a/crates/television/utils/strings.rs b/crates/television/utils/strings.rs index b710196..b4199f4 100644 --- a/crates/television/utils/strings.rs +++ b/crates/television/utils/strings.rs @@ -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, ) } diff --git a/crates/television_derive/src/lib.rs b/crates/television_derive/src/lib.rs index 8e4e789..5e9520a 100644 --- a/crates/television_derive/src/lib.rs +++ b/crates/television_derive/src/lib.rs @@ -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()