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-d = "ScrollPreviewHalfPageDown"
ctrl-u = "ScrollPreviewHalfPageUp" ctrl-u = "ScrollPreviewHalfPageUp"
enter = "SelectEntry" enter = "SelectEntry"
ctrl-enter = "SendToChannel" ctrl-y = "CopyEntryToClipboard"
ctrl-s = "ToggleRemoteControl" ctrl-s = "ToggleRemoteControl"
alt-s = "ToggleSendToChannel"
[keybindings.RemoteControl] [keybindings.RemoteControl]
esc = "Quit" esc = "Quit"
@ -18,3 +19,12 @@ ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry" ctrl-p = "SelectPrevEntry"
enter = "SelectEntry" enter = "SelectEntry"
ctrl-s = "ToggleRemoteControl" 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", "serde",
] ]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -227,6 +233,32 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" 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]] [[package]]
name = "camino" name = "camino"
version = "1.1.9" version = "1.1.9"
@ -343,6 +375,16 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 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]] [[package]]
name = "clru" name = "clru"
version = "0.6.2" version = "0.6.2"
@ -397,6 +439,15 @@ dependencies = [
"static_assertions", "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]] [[package]]
name = "config" name = "config"
version = "0.14.1" version = "0.14.1"
@ -457,6 +508,20 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.14" version = "0.2.14"
@ -543,6 +608,12 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "cursor-icon"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.10" version = "0.20.10"
@ -675,6 +746,15 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "dlv-list" name = "dlv-list"
version = "0.5.2" version = "0.5.2"
@ -684,6 +764,12 @@ dependencies = [
"const-random", "const-random",
] ]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.5" version = "1.0.5"
@ -902,6 +988,16 @@ dependencies = [
"version_check", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@ -1468,6 +1564,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.9" version = "0.5.9"
@ -1623,6 +1725,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "lazy-bytes-cast"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1635,6 +1743,16 @@ version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 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]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.3" version = "0.1.3"
@ -1683,6 +1801,15 @@ dependencies = [
"hashbrown 0.15.0", "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]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -1737,7 +1864,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.3.9",
"libc", "libc",
"log", "log",
"wasi", "wasi",
@ -1800,6 +1927,35 @@ dependencies = [
"libc", "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]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@ -1988,11 +2144,26 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"indexmap", "indexmap",
"quick-xml", "quick-xml 0.32.0",
"serde", "serde",
"time", "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]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -2033,6 +2204,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quick-xml"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.37" version = "1.0.37"
@ -2187,9 +2367,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.37" version = "0.38.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
@ -2219,6 +2399,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -2236,18 +2422,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.213" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.213" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2352,6 +2538,42 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 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]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.7" version = "0.5.7"
@ -2458,6 +2680,7 @@ dependencies = [
"clap", "clap",
"color-eyre", "color-eyre",
"config", "config",
"copypasta",
"crossterm", "crossterm",
"derive_deref", "derive_deref",
"devicons", "devicons",
@ -2942,6 +3165,102 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -3130,6 +3449,45 @@ dependencies = [
"memchr", "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]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
use crate::entry::Entry; use crate::entry::Entry;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use television_derive::{Broadcast, CliChannel, UnitChannel}; use television_derive::{Broadcast, ToCliChannel, ToUnitChannel};
mod alias; mod alias;
mod env; mod env;
@ -104,7 +104,7 @@ pub trait OnAir: Send {
/// of carrying the actual channel instances around. It also generates the necessary /// 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. /// glue code to automatically create a channel instance from the selected enum variant.
#[allow(dead_code, clippy::module_name_repetitions)] #[allow(dead_code, clippy::module_name_repetitions)]
#[derive(UnitChannel, CliChannel, Broadcast)] #[derive(ToUnitChannel, ToCliChannel, Broadcast)]
pub enum TelevisionChannel { pub enum TelevisionChannel {
/// The environment variables channel. /// The environment variables channel.
/// ///
@ -134,11 +134,125 @@ pub enum TelevisionChannel {
/// The remote control channel. /// The remote control channel.
/// ///
/// This channel allows to switch between different channels. /// This channel allows to switch between different channels.
#[exclude_from_unit]
#[exclude_from_cli] #[exclude_from_cli]
RemoteControl(remote_control::RemoteControl), 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 { impl TryFrom<&Entry> for TelevisionChannel {
type Error = String; type Error = String;

View File

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

View File

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

View File

@ -1,4 +1,5 @@
use devicons::FileIcon; use devicons::FileIcon;
use ignore::WalkState;
use nucleo::{ use nucleo::{
pattern::{CaseMatching, Normalization}, pattern::{CaseMatching, Normalization},
Config, Injector, Nucleo, Config, Injector, Nucleo,
@ -8,11 +9,11 @@ use std::{
io::{BufRead, Read, Seek}, io::{BufRead, Read, Seek},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc}, sync::{atomic::AtomicUsize, Arc},
u32,
}; };
use tracing::{debug, warn};
use tracing::{debug, info}; use super::{OnAir, TelevisionChannel};
use super::OnAir;
use crate::previewers::PreviewType; use crate::previewers::PreviewType;
use crate::utils::{ use crate::utils::{
files::{is_not_text, walk_builder, DEFAULT_NUM_THREADS}, files::{is_not_text, walk_builder, DEFAULT_NUM_THREADS},
@ -51,11 +52,11 @@ pub struct Channel {
} }
impl 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); let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1);
// start loading files in the background // start loading files in the background
let crawl_handle = tokio::spawn(load_candidates( let crawl_handle = tokio::spawn(crawl_for_candidates(
working_dir.to_path_buf(), directories,
matcher.injector(), matcher.injector(),
)); ));
Channel { 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; const MATCHER_TICK_TIMEOUT: u64 = 2;
} }
impl Default for Channel { impl Default for Channel {
fn default() -> Self { 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( .matched_items(
offset offset
..(num_entries + offset) ..(num_entries + offset)
.min(snapshot.matched_item_count()), .min(snapshot.matched_item_count()),
) )
.map(move |item| { .map(move |item| {
snapshot.pattern().column_pattern(0).indices( snapshot.pattern().column_pattern(0).indices(
@ -125,11 +202,11 @@ impl OnAir for Channel {
display_path.clone() + &item.data.line_number.to_string(), display_path.clone() + &item.data.line_number.to_string(),
PreviewType::Files, PreviewType::Files,
) )
.with_display_name(display_path) .with_display_name(display_path)
.with_value(line) .with_value(line)
.with_value_match_ranges(indices.map(|i| (i, i + 1)).collect()) .with_value_match_ranges(indices.map(|i| (i, i + 1)).collect())
.with_icon(FileIcon::from(item.data.path.as_path())) .with_icon(FileIcon::from(item.data.path.as_path()))
.with_line_number(item.data.line_number) .with_line_number(item.data.line_number)
}) })
.collect() .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), /// 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. /// 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)] #[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 current_dir = std::env::current_dir().unwrap();
let walker = let mut walker =
walk_builder(&path, *DEFAULT_NUM_THREADS, None, None).build_parallel(); 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)); let lines_in_mem = Arc::new(AtomicUsize::new(0));
walker.run(|| { walker.build_parallel().run(|| {
let injector = injector.clone(); let injector = injector.clone();
let current_dir = current_dir.clone(); let current_dir = current_dir.clone();
let lines_in_mem = lines_in_mem.clone(); let lines_in_mem = lines_in_mem.clone();
Box::new(move |result| { Box::new(move |result| {
if lines_in_mem.load(std::sync::atomic::Ordering::Relaxed) > MAX_IN_MEMORY_LINES { if lines_in_mem.load(std::sync::atomic::Ordering::Relaxed)
return ignore::WalkState::Quit; > MAX_LINES_IN_MEM
{
return WalkState::Quit;
} }
if let Ok(entry) = result { if let Ok(entry) = result {
if entry.file_type().unwrap().is_file() { if entry.file_type().unwrap().is_file() {
if let Ok(m) = entry.metadata() { if let Ok(m) = entry.metadata() {
if m.len() > MAX_FILE_SIZE { if m.len() > MAX_FILE_SIZE {
return ignore::WalkState::Continue; return WalkState::Continue;
} }
} }
// iterate over the lines of the file // try to inject the lines of the file
match File::open(entry.path()) { if let Some(injected_lines) = try_inject_lines(
Ok(file) => { injector.clone(),
// is the file a text-based file? &current_dir,
let mut reader = std::io::BufReader::new(&file); entry.path(),
let mut buffer = [0u8; 128]; ) {
match reader.read(&mut buffer) { lines_in_mem.fetch_add(
Ok(bytes_read) => { injected_lines,
if (bytes_read == 0) std::sync::atomic::Ordering::Relaxed,
|| 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);
}
} }
} }
} }
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 devicons::FileIcon;
use crate::previewers::PreviewType; 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)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Entry { pub struct Entry {
pub name: String, pub name: String,
@ -18,7 +23,7 @@ pub struct Entry {
impl Entry { impl Entry {
pub fn new(name: String, preview_type: PreviewType) -> Self { pub fn new(name: String, preview_type: PreviewType) -> Self {
Self { Self {
name: preprocess_line(&name), name,
display_name: None, display_name: None,
value: None, value: None,
name_match_ranges: None, name_match_ranges: None,
@ -30,12 +35,12 @@ impl Entry {
} }
pub fn with_display_name(mut self, display_name: String) -> Self { 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 self
} }
pub fn with_value(mut self, value: String) -> Self { pub fn with_value(mut self, value: String) -> Self {
self.value = Some(preprocess_line(&value)); self.value = Some(value);
self self
} }

View File

@ -74,26 +74,26 @@ impl Display for Key {
match self { match self {
Key::Backspace => write!(f, "Backspace"), Key::Backspace => write!(f, "Backspace"),
Key::Enter => write!(f, "Enter"), Key::Enter => write!(f, "Enter"),
Key::Left => write!(f, ""), Key::Left => write!(f, "Left"),
Key::Right => write!(f, ""), Key::Right => write!(f, "Right"),
Key::Up => write!(f, ""), Key::Up => write!(f, "Up"),
Key::Down => write!(f, ""), Key::Down => write!(f, "Down"),
Key::CtrlSpace => write!(f, "Ctrl-Space"), Key::CtrlSpace => write!(f, "Ctrl-Space"),
Key::CtrlBackspace => write!(f, "Ctrl-Backspace"), Key::CtrlBackspace => write!(f, "Ctrl-Backspace"),
Key::CtrlEnter => write!(f, "Ctrl-Enter"), Key::CtrlEnter => write!(f, "Ctrl-Enter"),
Key::CtrlLeft => write!(f, "Ctrl-"), Key::CtrlLeft => write!(f, "Ctrl-Left"),
Key::CtrlRight => write!(f, "Ctrl-"), Key::CtrlRight => write!(f, "Ctrl-Right"),
Key::CtrlUp => write!(f, "Ctrl-"), Key::CtrlUp => write!(f, "Ctrl-Up"),
Key::CtrlDown => write!(f, "Ctrl-"), Key::CtrlDown => write!(f, "Ctrl-Down"),
Key::CtrlDelete => write!(f, "Ctrl-Del"), Key::CtrlDelete => write!(f, "Ctrl-Del"),
Key::AltSpace => write!(f, "Alt+Space"), Key::AltSpace => write!(f, "Alt-Space"),
Key::AltEnter => write!(f, "Alt+Enter"), Key::AltEnter => write!(f, "Alt-Enter"),
Key::AltBackspace => write!(f, "Alt+Backspace"), Key::AltBackspace => write!(f, "Alt-Backspace"),
Key::AltDelete => write!(f, "Alt+Delete"), Key::AltDelete => write!(f, "Alt-Delete"),
Key::AltUp => write!(f, "Alt"), Key::AltUp => write!(f, "Alt-Up"),
Key::AltDown => write!(f, "Alt"), Key::AltDown => write!(f, "Alt-Down"),
Key::AltLeft => write!(f, "Alt"), Key::AltLeft => write!(f, "Alt-Left"),
Key::AltRight => write!(f, "Alt"), Key::AltRight => write!(f, "Alt-Right"),
Key::Home => write!(f, "Home"), Key::Home => write!(f, "Home"),
Key::End => write!(f, "End"), Key::End => write!(f, "End"),
Key::PageUp => write!(f, "PageUp"), Key::PageUp => write!(f, "PageUp"),
@ -103,7 +103,7 @@ impl Display for Key {
Key::Insert => write!(f, "Insert"), Key::Insert => write!(f, "Insert"),
Key::F(k) => write!(f, "F{k}"), Key::F(k) => write!(f, "F{k}"),
Key::Char(c) => write!(f, "{c}"), 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::Ctrl(c) => write!(f, "Ctrl-{c}"),
Key::Null => write!(f, "Null"), Key::Null => write!(f, "Null"),
Key::Esc => write!(f, "Esc"), 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 { fn label<P: AsRef<Path>>(p: P, strip: &str) -> String {
let icon = FileIcon::from(&p); 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()) format!("{} {}", icon, path.display())
} }

View File

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

View File

@ -23,7 +23,9 @@ impl Television {
Mode::RemoteControl => { Mode::RemoteControl => {
self.build_keymap_table_for_channel_selection() 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 prev = keys_for_action(keymap, &Action::SelectPrevEntry);
let next = keys_for_action(keymap, &Action::SelectNextEntry); let next = keys_for_action(keymap, &Action::SelectNextEntry);
let results_row = Row::new(build_cells_for_key_groups( let results_row = Row::new(build_cells_for_key_groups(
"Results navigation", "Results navigation",
vec![prev, next], vec![prev, next],
key_color, key_color,
)); ));
@ -46,7 +48,7 @@ impl Television {
let down_keys = let down_keys =
keys_for_action(keymap, &Action::ScrollPreviewHalfPageDown); keys_for_action(keymap, &Action::ScrollPreviewHalfPageDown);
let preview_row = Row::new(build_cells_for_key_groups( let preview_row = Row::new(build_cells_for_key_groups(
"Preview navigation", "Preview navigation",
vec![up_keys, down_keys], vec![up_keys, down_keys],
key_color, key_color,
)); ));
@ -54,16 +56,25 @@ impl Television {
// Select entry // Select entry
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry); let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
let select_entry_row = Row::new(build_cells_for_key_groups( let select_entry_row = Row::new(build_cells_for_key_groups(
"Select entry", "Select entry",
vec![select_entry_keys], vec![select_entry_keys],
key_color, 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 // Send to channel
let send_to_channel_keys = 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( let send_to_channel_row = Row::new(build_cells_for_key_groups(
"⇉ Send results to", "Send results to",
vec![send_to_channel_keys], vec![send_to_channel_keys],
key_color, key_color,
)); ));
@ -72,7 +83,7 @@ impl Television {
let switch_channels_keys = let switch_channels_keys =
keys_for_action(keymap, &Action::ToggleRemoteControl); keys_for_action(keymap, &Action::ToggleRemoteControl);
let switch_channels_row = Row::new(build_cells_for_key_groups( let switch_channels_row = Row::new(build_cells_for_key_groups(
"Toggle Remote control", "Toggle Remote control",
vec![switch_channels_keys], vec![switch_channels_keys],
key_color, key_color,
)); ));
@ -81,7 +92,7 @@ impl Television {
// Quit ⏼ // Quit ⏼
let quit_keys = keys_for_action(keymap, &Action::Quit); let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row = Row::new(build_cells_for_key_groups( let quit_row = Row::new(build_cells_for_key_groups(
"Quit", "Quit",
vec![quit_keys], vec![quit_keys],
key_color, key_color,
)); ));
@ -93,6 +104,7 @@ impl Television {
results_row, results_row,
preview_row, preview_row,
select_entry_row, select_entry_row,
copy_entry_row,
send_to_channel_row, send_to_channel_row,
switch_channels_row, switch_channels_row,
quit_row, quit_row,
@ -111,7 +123,7 @@ impl Television {
let prev = keys_for_action(keymap, &Action::SelectPrevEntry); let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
let next = keys_for_action(keymap, &Action::SelectNextEntry); let next = keys_for_action(keymap, &Action::SelectNextEntry);
let results_row = Row::new(build_cells_for_key_groups( let results_row = Row::new(build_cells_for_key_groups(
"Browse channels", "Browse channels",
vec![prev, next], vec![prev, next],
key_color, key_color,
)); ));
@ -119,16 +131,16 @@ impl Television {
// Select entry // Select entry
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry); let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
let select_entry_row = Row::new(build_cells_for_key_groups( let select_entry_row = Row::new(build_cells_for_key_groups(
"Select channel", "Select channel",
vec![select_entry_keys], vec![select_entry_keys],
key_color, key_color,
)); ));
// Switch channels // Remote control
let switch_channels_keys = let switch_channels_keys =
keys_for_action(keymap, &Action::ToggleRemoteControl); keys_for_action(keymap, &Action::ToggleRemoteControl);
let switch_channels_row = Row::new(build_cells_for_key_groups( let switch_channels_row = Row::new(build_cells_for_key_groups(
"Toggle Remote control", "Toggle Remote control",
vec![switch_channels_keys], vec![switch_channels_keys],
key_color, key_color,
)); ));
@ -136,7 +148,7 @@ impl Television {
// Quit // Quit
let quit_keys = keys_for_action(keymap, &Action::Quit); let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row = Row::new(build_cells_for_key_groups( let quit_row = Row::new(build_cells_for_key_groups(
"Quit", "Quit",
vec![quit_keys], vec![quit_keys],
key_color, 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. /// Get the keymap for the current mode.
/// ///
/// # Returns /// # Returns

View File

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

View File

@ -24,11 +24,11 @@ impl Television {
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(
[ [
Constraint::Fill(1), Constraint::Min(3),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(20), Constraint::Length(20),
] ]
.as_ref(), .as_ref(),
) )
.split(*area); .split(*area);
self.draw_rc_channels(f, &layout[0])?; self.draw_rc_channels(f, &layout[0])?;
@ -106,7 +106,7 @@ impl Television {
.fg(crate::television::DEFAULT_INPUT_FG) .fg(crate::television::DEFAULT_INPUT_FG)
.bold(), .bold(),
)) ))
.block(prompt_symbol_block); .block(prompt_symbol_block);
f.render_widget(arrow, inner_input_chunks[0]); f.render_widget(arrow, inner_input_chunks[0]);
let interactive_input_block = Block::default(); let interactive_input_block = Block::default();
@ -131,8 +131,8 @@ impl Television {
// Put cursor past the end of the input text // Put cursor past the end of the input text
inner_input_chunks[1].x inner_input_chunks[1].x
+ u16::try_from( + 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 // Move one line down, from the border to the input line
inner_input_chunks[1].y, inner_input_chunks[1].y,
)); ));

View File

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

View File

@ -6,10 +6,10 @@ use quote::quote;
/// ///
/// ```rust /// ```rust
/// use crate::channels::{TelevisionChannel, OnAir}; /// use crate::channels::{TelevisionChannel, OnAir};
/// use television_derive::CliChannel; /// use television_derive::ToCliChannel;
/// use crate::channels::{files, text}; /// use crate::channels::{files, text};
/// ///
/// #[derive(CliChannel)] /// #[derive(ToCliChannel)]
/// enum TelevisionChannel { /// enum TelevisionChannel {
/// Files(files::Channel), /// Files(files::Channel),
/// Text(text::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 /// Any variant that should not be included in the CLI should be annotated with
/// `#[exclude_from_cli]`. /// `#[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 { pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree // Construct a representation of Rust code as a syntax tree
// that we can manipulate // that we can manipulate
@ -35,12 +35,12 @@ pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
impl_cli_channel(&ast) impl_cli_channel(&ast)
} }
fn has_exclude_attr(attrs: &[syn::Attribute]) -> bool { fn has_attribute(attrs: &[syn::Attribute], attribute: &str) -> bool {
attrs attrs.iter().any(|attr| attr.path().is_ident(attribute))
.iter()
.any(|attr| attr.path().is_ident("exclude_from_cli"))
} }
const EXCLUDE_FROM_CLI: &str = "exclude_from_cli";
fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// check that the struct is an enum // check that the struct is an enum
let variants = if let syn::Data::Enum(data_enum) = &ast.data { 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 // create the CliTvChannel enum
let cli_enum_variants = variants let cli_enum_variants = variants
.iter() .iter()
.filter(|variant| !has_exclude_attr(&variant.attrs)) .filter(|variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI))
.map(|variant| { .map(|variant| {
let variant_name = &variant.ident; let variant_name = &variant.ident;
quote! { quote! {
@ -80,7 +80,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// Generate the match arms for the `to_channel` method // Generate the match arms for the `to_channel` method
let arms = variants.iter().filter( let arms = variants.iter().filter(
|variant| !has_exclude_attr(&variant.attrs) |variant| !has_attribute(&variant.attrs, EXCLUDE_FROM_CLI),
).map(|variant| { ).map(|variant| {
let variant_name = &variant.ident; 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` /// The `UnitChannel` enum is used as a unit variant of the `TelevisionChannel`
/// enum. /// enum.
#[proc_macro_derive(UnitChannel)] #[proc_macro_derive(ToUnitChannel, attributes(exclude_from_unit))]
pub fn unit_channel_derive(input: TokenStream) -> TokenStream { pub fn unit_channel_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree // Construct a representation of Rust code as a syntax tree
// that we can manipulate // that we can manipulate
@ -265,6 +265,8 @@ pub fn unit_channel_derive(input: TokenStream) -> TokenStream {
impl_unit_channel(&ast) impl_unit_channel(&ast)
} }
const EXCLUDE_FROM_UNIT: &str = "exclude_from_unit";
fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream { fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
// Ensure the struct is an enum // Ensure the struct is an enum
let variants = if let syn::Data::Enum(data_enum) = &ast.data { 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" "#[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 // Generate a unit enum from the given enum
let unit_enum = quote! { let unit_enum = quote! {
@ -310,7 +322,37 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
fn from(channel: &TelevisionChannel) -> Self { fn from(channel: &TelevisionChannel) -> Self {
match channel { 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 #unit_enum
#into_impl #into_impl
#from_impl #from_impl
#from_str_impl
#into_str_impl
}; };
gen.into() gen.into()