From 16e187f5c6f8ba7353b848d4e827f7c072f035eb Mon Sep 17 00:00:00 2001 From: Vitalii Lukyanov Date: Mon, 24 Feb 2025 14:32:23 +0100 Subject: [PATCH 1/3] update rust toolchain hash (#365) Hey again, just realized there was an update with toolchain change 1.81 -> 1.83, so updating the hash for it :) Co-authored-by: Vitalii Lukyanov --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 4746e2b..a8e48d8 100644 --- a/flake.nix +++ b/flake.nix @@ -33,7 +33,7 @@ pkgs.rustChannelOf { rustToolchain = ./rust-toolchain.toml; - sha256 = "VZZnlyP69+Y3crrLHQyJirqlHrTtGTsyiSnZB8jEvVo="; + sha256 = "s1RPtyvDGJaX/BisLT+ifVfuhDT1nZkZ1NcK8sbwELM="; } ) .rust; From e2a0fb204724c5b6fc12554a35355a6a419ad198 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:50:53 +0100 Subject: [PATCH 2/3] fix(clipboard): fix issue where clipboard wouldn't work on X11-based environments (#364) fixes #282 tested with the following setup: ``` -` alex@alex-mbp .o+` ------------- `ooo/ OS: Arch Linux x86_64 `+oooo: Host: MacBookPro10,2 1.0 `+oooooo: Kernel: 6.13.3-arch1-1 -+oooooo+: Uptime: 1 hour, 8 mins `/:-:++oooo+: Packages: 559 (pacman) `/++++/+++++++: Shell: zsh 5.9 `/++++++++++++++: Resolution: 2048x1280 `/+++ooooooooooooo/` WM: awesome ./ooosssso++osssssso+` Theme: Adwaita [GTK3] .oossssso-````/ossssss+` Icons: Adwaita [GTK3] -osssssso. :ssssssso. Terminal: tmux :osssssss/ osssso+++. CPU: Intel i5-3230M (4) @ 3.200GHz /ossssssss/ +ssssooo/- GPU: Intel 3rd Gen Core processor Graphics Controller `/ossssso+/:- -:/+osssso+- Memory: 4180MiB / 7815MiB `+sso+:-` `.-/+oso: `++:. `-/+/ .` `/ ``` --- Cargo.lock | 464 ++-------------------------------- Cargo.toml | 3 +- television/main.rs | 3 + television/television.rs | 24 +- television/utils/clipboard.rs | 177 +++++++++++++ television/utils/mod.rs | 2 + television/utils/rocell.rs | 99 ++++++++ 7 files changed, 318 insertions(+), 454 deletions(-) create mode 100644 television/utils/clipboard.rs create mode 100644 television/utils/rocell.rs diff --git a/Cargo.lock b/Cargo.lock index fdab5f6..4d6b777 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,7 +121,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -201,12 +201,6 @@ dependencies = [ "serde", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "bstr" version = "1.11.3" @@ -241,32 +235,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.8.0", - "log", - "polling", - "rustix", - "slab", - "thiserror 1.0.69", -] - -[[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 = "cassowary" version = "0.3.0" @@ -372,12 +340,11 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" -version = "3.1.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ - "lazy-bytes-cast", - "winapi", + "error-code", ] [[package]] @@ -411,15 +378,6 @@ 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 = "console" version = "0.15.10" @@ -442,20 +400,6 @@ dependencies = [ "memchr", ] -[[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 = "crc32fast" version = "1.4.2" @@ -561,12 +505,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" -[[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" @@ -641,21 +579,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "either" version = "1.13.0" @@ -693,6 +616,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + [[package]] name = "fastrand" version = "2.3.0" @@ -803,16 +732,6 @@ dependencies = [ "tempfile", ] -[[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" @@ -833,7 +752,7 @@ dependencies = [ "cfg-if", "libc", "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1016,12 +935,6 @@ dependencies = [ "wasm-bindgen", ] -[[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" @@ -1034,16 +947,6 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libloading" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" -dependencies = [ - "cfg-if", - "windows-targets 0.52.6", -] - [[package]] name = "libredox" version = "0.1.3" @@ -1085,15 +988,6 @@ dependencies = [ "hashbrown", ] -[[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" @@ -1109,15 +1003,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1210,35 +1095,6 @@ dependencies = [ "autocfg", ] -[[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.36.7" @@ -1325,7 +1181,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1369,7 +1225,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64", "indexmap", - "quick-xml 0.32.0", + "quick-xml", "serde", "time", ] @@ -1402,21 +1258,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "polling" -version = "3.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -1441,15 +1282,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick-xml" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.38" @@ -1620,12 +1452,6 @@ 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" @@ -1766,57 +1592,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" 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.8.0", - "calloop", - "calloop-wayland-source", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix", - "thiserror 1.0.69", - "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.8" @@ -1903,10 +1684,11 @@ name = "television" version = "0.10.6" dependencies = [ "anyhow", + "base64", "bat", "better-panic", "clap", - "copypasta", + "clipboard-win", "criterion", "crossterm", "devicons", @@ -2348,102 +2130,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wayland-backend" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" -dependencies = [ - "cc", - "downcast-rs", - "rustix", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" -dependencies = [ - "bitflags 2.8.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.8.0", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" -dependencies = [ - "rustix", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" -dependencies = [ - "bitflags 2.8.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" -dependencies = [ - "bitflags 2.8.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" -dependencies = [ - "proc-macro2", - "quick-xml 0.37.2", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - [[package]] name = "web-sys" version = "0.3.77" @@ -2492,7 +2178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ "windows-core", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2504,7 +2190,7 @@ dependencies = [ "windows-implement", "windows-interface", "windows-result", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2535,7 +2221,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2544,7 +2230,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2553,22 +2239,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -2577,46 +2248,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2629,48 +2282,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2695,45 +2324,6 @@ dependencies = [ "bitflags 2.8.0", ] -[[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 = "xterm-color" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 4f46516..c31c645 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ path = "television/lib.rs" television-derive = { path = "television-derive", version = "0.0.25" } anyhow = "1.0" +base64 = "0.22.1" directories = "6.0" devicons = "0.6" lazy_static = "1.5" @@ -46,7 +47,6 @@ ratatui = { version = "0.29", features = ["serde", "macros"] } better-panic = "0.3" signal-hook = "0.3" human-panic = "2.0" -copypasta = "0.10" ignore = "0.4" strum = { version = "0.26", features = ["derive"] } regex = "1.11" @@ -62,6 +62,7 @@ toml = "0.8" [target.'cfg(windows)'.dependencies] winapi-util = "0.1.9" +clipboard-win = "5.4.0" [dev-dependencies] criterion = { version = "0.5", features = ["async_tokio"] } diff --git a/television/main.rs b/television/main.rs index 9eed959..d60b447 100644 --- a/television/main.rs +++ b/television/main.rs @@ -5,6 +5,7 @@ use std::process::exit; use anyhow::Result; use clap::Parser; +use television::utils::clipboard::CLIPBOARD; use tracing::{debug, error, info}; use television::app::App; @@ -77,6 +78,8 @@ async fn main() -> Result<()> { env::set_current_dir(path)?; } + CLIPBOARD.with(<_>::default); + match App::new( { if is_readable_stdin() { diff --git a/television/television.rs b/television/television.rs index c5acc44..980d33b 100644 --- a/television/television.rs +++ b/television/television.rs @@ -13,15 +13,14 @@ use crate::preview::{PreviewState, Previewer}; use crate::screen::colors::Colorscheme; use crate::screen::layout::InputPosition; use crate::screen::spinner::{Spinner, SpinnerState}; +use crate::utils::clipboard::CLIPBOARD; use crate::utils::metadata::AppMetadata; use crate::utils::strings::EMPTY_STRING; use anyhow::Result; -use copypasta::{ClipboardContext, ClipboardProvider}; use rustc_hash::{FxBuildHasher, FxHashSet}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use tokio::sync::mpsc::{Receiver, Sender, UnboundedSender}; -use tracing::error; #[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)] pub enum Mode { @@ -480,20 +479,13 @@ impl Television { pub fn handle_copy_entry_to_clipboard(&mut self) { if self.mode == Mode::Channel { if let Some(entries) = self.get_selected_entries(None) { - if let Ok(mut ctx) = ClipboardContext::new() { - ctx.set_contents( - entries - .iter() - .map(|e| e.name.clone()) - .collect::>() - .join(" "), - ) - .unwrap_or_else(|_| { - error!("Could not copy to clipboard"); - }); - } else { - error!("Could not copy to clipboard"); - } + let copied_string = entries + .iter() + .map(|e| e.name.clone()) + .collect::>() + .join(" "); + + tokio::spawn(CLIPBOARD.set(copied_string)); } } } diff --git a/television/utils/clipboard.rs b/television/utils/clipboard.rs new file mode 100644 index 0000000..86c58d2 --- /dev/null +++ b/television/utils/clipboard.rs @@ -0,0 +1,177 @@ +/* +MIT License + +Copyright (c) 2023 - sxyazi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +use std::ffi::OsString; + +use crate::utils::rocell::RoCell; +use parking_lot::Mutex; + +pub static CLIPBOARD: RoCell = RoCell::new(); + +#[derive(Default)] +pub struct Clipboard { + content: Mutex, +} + +impl Clipboard { + #[cfg(unix)] + pub async fn get(&self) -> OsString { + use std::os::unix::prelude::OsStringExt; + + use tokio::process::Command; + + let all = [ + ("pbpaste", &[][..]), + ("termux-clipboard-get", &[]), + ("wl-paste", &[]), + ("xclip", &["-o", "-selection", "clipboard"]), + ("xsel", &["-ob"]), + ]; + + for (bin, args) in all { + let Ok(output) = Command::new(bin) + .args(args) + .kill_on_drop(true) + .output() + .await + else { + continue; + }; + if output.status.success() { + return OsString::from_vec(output.stdout); + } + } + self.content.lock().clone() + } + + #[cfg(windows)] + pub async fn get(&self) -> OsString { + use clipboard_win::{formats, get_clipboard}; + + let result = tokio::task::spawn_blocking(|| { + get_clipboard::(formats::Unicode) + }); + if let Ok(Ok(s)) = result.await { + return s.into(); + } + + self.content.lock().clone() + } + + #[cfg(unix)] + pub async fn set(&self, s: impl AsRef) { + use std::{ + io::{stderr, BufWriter}, + process::Stdio, + }; + + use crossterm::execute; + use tokio::{io::AsyncWriteExt, process::Command}; + + s.as_ref().clone_into(&mut self.content.lock()); + execute!( + BufWriter::new(stderr()), + osc52::SetClipboard::new(s.as_ref()) + ) + .ok(); + + let all = [ + ("pbcopy", &[][..]), + ("termux-clipboard-set", &[]), + ("wl-copy", &[]), + ("xclip", &["-selection", "clipboard"]), + ("xsel", &["-ib"]), + ]; + + for (bin, args) in all { + let cmd = Command::new(bin) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .spawn(); + + let Ok(mut child) = cmd else { continue }; + + let mut stdin = child.stdin.take().unwrap(); + if stdin + .write_all(s.as_ref().as_encoded_bytes()) + .await + .is_err() + { + continue; + } + drop(stdin); + + if child.wait().await.is_ok_and(|s| s.success()) { + break; + } + } + } + + #[cfg(windows)] + pub async fn set(&self, s: impl AsRef) { + use clipboard_win::{formats, set_clipboard}; + + let s = s.as_ref().to_owned(); + *self.content.lock() = s.clone(); + + tokio::task::spawn_blocking(move || { + set_clipboard(formats::Unicode, s.to_string_lossy()) + }) + .await + .ok(); + } +} + +#[cfg(unix)] +mod osc52 { + use std::ffi::OsStr; + + use base64::{engine::general_purpose, Engine}; + + #[derive(Debug)] + pub struct SetClipboard { + content: String, + } + + impl SetClipboard { + pub fn new(content: &OsStr) -> Self { + Self { + content: general_purpose::STANDARD + .encode(content.as_encoded_bytes()), + } + } + } + + impl crossterm::Command for SetClipboard { + fn write_ansi( + &self, + f: &mut impl std::fmt::Write, + ) -> std::fmt::Result { + write!(f, "\x1b]52;c;{}\x1b\\", self.content) + } + } +} diff --git a/television/utils/mod.rs b/television/utils/mod.rs index 80fa27a..0b88158 100644 --- a/television/utils/mod.rs +++ b/television/utils/mod.rs @@ -1,10 +1,12 @@ pub mod cache; +pub mod clipboard; pub mod command; pub mod files; pub mod hashmaps; pub mod indices; pub mod input; pub mod metadata; +pub mod rocell; pub mod shell; pub mod stdin; pub mod strings; diff --git a/television/utils/rocell.rs b/television/utils/rocell.rs new file mode 100644 index 0000000..d3758e6 --- /dev/null +++ b/television/utils/rocell.rs @@ -0,0 +1,99 @@ +/* +MIT License + +Copyright (c) 2023 - sxyazi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +use std::{ + cell::UnsafeCell, + fmt::{self, Display}, + mem, + ops::Deref, +}; + +// Read-only cell. It's safe to use this in a static variable, but it's not safe +// to mutate it. This is useful for storing static data that is expensive to +// initialize, but is immutable once. +pub struct RoCell(UnsafeCell>); + +unsafe impl Sync for RoCell {} + +impl RoCell { + #[inline] + pub const fn new() -> Self { + Self(UnsafeCell::new(None)) + } + + #[inline] + pub const fn new_const(value: T) -> Self { + Self(UnsafeCell::new(Some(value))) + } + + #[inline] + pub fn init(&self, value: T) { + debug_assert!(!self.initialized()); + unsafe { + *self.0.get() = Some(value); + } + } + + #[inline] + pub fn with(&self, f: F) + where + F: FnOnce() -> T, + { + self.init(f()); + } + + #[inline] + pub fn drop(&self) -> T { + debug_assert!(self.initialized()); + unsafe { mem::take(&mut *self.0.get()).unwrap_unchecked() } + } + + #[inline] + fn initialized(&self) -> bool { + unsafe { (*self.0.get()).is_some() } + } +} + +impl Default for RoCell { + fn default() -> Self { + Self::new() + } +} + +impl Deref for RoCell { + type Target = T; + + fn deref(&self) -> &Self::Target { + debug_assert!(self.initialized()); + unsafe { (*self.0.get()).as_ref().unwrap_unchecked() } + } +} + +impl Display for RoCell +where + T: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.deref().fmt(f) + } +} From 63cb9760272067ab8787085b37690255e20ecbb9 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Wed, 5 Mar 2025 01:11:24 +0100 Subject: [PATCH 3/3] refactor(ui): communicate ui state to tv using channels (#369) The state of the UI is now synchronized with the `Television` struct using a dedicated channel and is available at `Television.ui_state`. This removes quite a bit of complexity from the existing code and should allow for nicer implementations of features that need the UI state to compute things in the background (typically knowing the target size of an image you wish to construct in the background, as in #363) The `UiState` currently only holds the UI layout: ```rs pub struct UiState { pub layout: Layout, } ``` --- Cargo.lock | 1 - Cargo.toml | 1 - television/app.rs | 17 +- television/channels/cable.rs | 8 +- television/channels/dirs.rs | 4 +- television/channels/files.rs | 4 +- television/channels/git_repos.rs | 40 +- television/channels/text.rs | 4 +- television/config/mod.rs | 46 +- television/config/themes.rs | 8 +- television/config/themes/builtin.rs | 76 ++- television/draw.rs | 24 +- television/logging.rs | 6 +- television/preview/previewers/command.rs | 55 +- television/render.rs | 35 +- television/screen/layout.rs | 14 +- television/television.rs | 70 +-- television/utils/files.rs | 681 ++++++++++++----------- television/utils/strings.rs | 15 +- television/utils/syntax.rs | 11 +- television/utils/threads.rs | 3 +- 21 files changed, 591 insertions(+), 532 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d6b777..89d0adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1696,7 +1696,6 @@ dependencies = [ "gag", "human-panic", "ignore", - "lazy_static", "nom", "nucleo", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index c31c645..fe3112a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ anyhow = "1.0" base64 = "0.22.1" directories = "6.0" devicons = "0.6" -lazy_static = "1.5" tokio = { version = "1.43", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/television/app.rs b/television/app.rs index 65ae958..f1a7c16 100644 --- a/television/app.rs +++ b/television/app.rs @@ -8,6 +8,7 @@ use crate::channels::entry::Entry; use crate::channels::TelevisionChannel; use crate::config::{parse_key, Config}; use crate::keymap::Keymap; +use crate::render::UiState; use crate::television::{Mode, Television}; use crate::{ action::Action, @@ -37,6 +38,9 @@ pub struct App { event_abort_tx: mpsc::UnboundedSender<()>, /// A sender channel for rendering tasks. render_tx: mpsc::UnboundedSender, + /// A channel that listens to UI updates. + ui_state_rx: mpsc::UnboundedReceiver, + ui_state_tx: mpsc::UnboundedSender, } /// The outcome of an action. @@ -104,6 +108,7 @@ impl App { .collect(), )?; debug!("{:?}", keymap); + let (ui_state_tx, ui_state_rx) = mpsc::unbounded_channel(); let television = Television::new(action_tx.clone(), channel, config, input); @@ -118,6 +123,8 @@ impl App { event_rx, event_abort_tx, render_tx, + ui_state_rx, + ui_state_tx, }) } @@ -145,9 +152,10 @@ impl App { debug!("Starting rendering loop"); let (render_tx, render_rx) = mpsc::unbounded_channel(); self.render_tx = render_tx.clone(); + let ui_state_tx = self.ui_state_tx.clone(); let action_tx_r = self.action_tx.clone(); let rendering_task = tokio::spawn(async move { - render(render_rx, action_tx_r, is_output_tty).await + render(render_rx, action_tx_r, ui_state_tx, is_output_tty).await }); self.action_tx.send(Action::Render)?; @@ -298,9 +306,14 @@ impl App { self.render_tx.send(RenderingTask::Resize(w, h))?; } Action::Render => { + // forward to the rendering task self.render_tx.send(RenderingTask::Render( - self.television.dump_context(), + Box::new(self.television.dump_context()), ))?; + // update the television UI state with the previous frame + if let Ok(ui_state) = self.ui_state_rx.try_recv() { + self.television.update_ui_state(ui_state); + } } _ => {} } diff --git a/television/channels/cable.rs b/television/channels/cable.rs index 14c7162..4546c9d 100644 --- a/television/channels/cable.rs +++ b/television/channels/cable.rs @@ -9,7 +9,6 @@ use std::io::{BufRead, BufReader}; use std::process::Stdio; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use rustc_hash::{FxBuildHasher, FxHashSet}; use tracing::debug; @@ -64,13 +63,10 @@ impl From for Channel { } } -lazy_static! { - static ref BUILTIN_PREVIEW_RE: Regex = Regex::new(r"^:(\w+):$").unwrap(); -} - fn parse_preview_kind(command: &PreviewCommand) -> Result { debug!("Parsing preview kind for command: {:?}", command); - if let Some(captures) = BUILTIN_PREVIEW_RE.captures(&command.command) { + let re = Regex::new(r"^\:(\w+)\:$").unwrap(); + if let Some(captures) = re.captures(&command.command) { let preview_type = PreviewType::try_from(&captures[1])?; Ok(PreviewKind::Builtin(preview_type)) } else { diff --git a/television/channels/dirs.rs b/television/channels/dirs.rs index 26b62dd..16bd4ff 100644 --- a/television/channels/dirs.rs +++ b/television/channels/dirs.rs @@ -1,7 +1,7 @@ use crate::channels::entry::{Entry, PreviewCommand, PreviewType}; use crate::channels::{OnAir, TelevisionChannel}; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; use devicons::FileIcon; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::collections::HashSet; @@ -151,7 +151,7 @@ async fn load_dirs(paths: Vec, injector: Injector) { } let current_dir = std::env::current_dir().unwrap(); let mut builder = - walk_builder(&paths[0], *DEFAULT_NUM_THREADS, None, None); + walk_builder(&paths[0], get_default_num_threads(), None, None); paths[1..].iter().for_each(|path| { builder.add(path); }); diff --git a/television/channels/files.rs b/television/channels/files.rs index ef09b07..6e7a2ff 100644 --- a/television/channels/files.rs +++ b/television/channels/files.rs @@ -1,7 +1,7 @@ use crate::channels::entry::{Entry, PreviewType}; use crate::channels::{OnAir, TelevisionChannel}; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; use devicons::FileIcon; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::collections::HashSet; @@ -157,7 +157,7 @@ async fn load_files(paths: Vec, injector: Injector) { } let current_dir = std::env::current_dir().unwrap(); let mut builder = - walk_builder(&paths[0], *DEFAULT_NUM_THREADS, None, None); + walk_builder(&paths[0], get_default_num_threads(), None, None); paths[1..].iter().for_each(|path| { builder.add(path); }); diff --git a/television/channels/git_repos.rs b/television/channels/git_repos.rs index fec62f0..f8cebf5 100644 --- a/television/channels/git_repos.rs +++ b/television/channels/git_repos.rs @@ -1,7 +1,6 @@ use devicons::FileIcon; use directories::BaseDirs; use ignore::overrides::OverrideBuilder; -use lazy_static::lazy_static; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::collections::HashSet; use std::path::PathBuf; @@ -11,13 +10,14 @@ use tracing::debug; use crate::channels::entry::{Entry, PreviewCommand, PreviewType}; use crate::channels::OnAir; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; pub struct Channel { matcher: Matcher, icon: FileIcon, crawl_handle: JoinHandle<()>, selected_entries: FxHashSet, + preview_command: PreviewCommand, } impl Channel { @@ -28,11 +28,20 @@ impl Channel { base_dirs.home_dir().to_path_buf(), matcher.injector(), )); + + let preview_command = PreviewCommand { + command: String::from( + "cd {} && git log -n 200 --pretty=medium --all --graph --color", + ), + delimiter: ":".to_string(), + }; + Channel { matcher, icon: FileIcon::from("git"), crawl_handle, selected_entries: HashSet::with_hasher(FxBuildHasher), + preview_command, } } } @@ -43,15 +52,6 @@ impl Default for Channel { } } -lazy_static! { - static ref PREVIEW_COMMAND: PreviewCommand = PreviewCommand { - command: String::from( - "cd {} && git log -n 200 --pretty=medium --all --graph --color", - ), - delimiter: ":".to_string(), - }; -} - impl OnAir for Channel { fn find(&mut self, pattern: &str) { self.matcher.find(pattern); @@ -64,9 +64,12 @@ impl OnAir for Channel { .into_iter() .map(|item| { let path = item.matched_string; - Entry::new(path, PreviewType::Command(PREVIEW_COMMAND.clone())) - .with_name_match_ranges(&item.match_indices) - .with_icon(self.icon) + Entry::new( + path, + PreviewType::Command(self.preview_command.clone()), + ) + .with_name_match_ranges(&item.match_indices) + .with_icon(self.icon) }) .collect() } @@ -74,8 +77,11 @@ impl OnAir for Channel { fn get_result(&self, index: u32) -> Option { self.matcher.get_result(index).map(|item| { let path = item.matched_string; - Entry::new(path, PreviewType::Command(PREVIEW_COMMAND.clone())) - .with_icon(self.icon) + Entry::new( + path, + PreviewType::Command(self.preview_command.clone()), + ) + .with_icon(self.icon) }) } @@ -162,7 +168,7 @@ async fn crawl_for_repos(starting_point: PathBuf, injector: Injector) { walker_overrides_builder.add(".git").unwrap(); let walker = walk_builder( &starting_point, - *DEFAULT_NUM_THREADS, + get_default_num_threads(), Some(walker_overrides_builder.build().unwrap()), Some(get_ignored_paths()), ) diff --git a/television/channels/text.rs b/television/channels/text.rs index 97024df..f5dbe87 100644 --- a/television/channels/text.rs +++ b/television/channels/text.rs @@ -1,7 +1,7 @@ use super::{OnAir, TelevisionChannel}; use crate::channels::entry::{Entry, PreviewType}; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; use crate::utils::strings::{ proportion_of_printable_ascii_characters, PRINTABLE_ASCII_THRESHOLD, }; @@ -277,7 +277,7 @@ async fn crawl_for_candidates( } let current_dir = std::env::current_dir().unwrap(); let mut walker = - walk_builder(&directories[0], *DEFAULT_NUM_THREADS, None, None); + walk_builder(&directories[0], get_default_num_threads(), None, None); directories[1..].iter().for_each(|path| { walker.add(path); }); diff --git a/television/config/mod.rs b/television/config/mod.rs index f3bdeac..451695d 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -9,7 +9,6 @@ use anyhow::{Context, Result}; use directories::ProjectDirs; use keybindings::merge_keybindings; pub use keybindings::{parse_key, Binding, KeyBindings}; -use lazy_static::lazy_static; use previewers::PreviewersConfig; use serde::Deserialize; use shell_integration::ShellIntegrationConfig; @@ -70,23 +69,7 @@ pub struct Config { pub shell_integration: ShellIntegrationConfig, } -lazy_static! { - pub static ref PROJECT_NAME: String = String::from("television"); - pub static ref PROJECT_NAME_UPPER: String = PROJECT_NAME.to_uppercase(); - pub static ref DATA_FOLDER: Option = - // if `TELEVISION_DATA` is set, use that as the data directory - env::var_os(format!("{}_DATA", PROJECT_NAME_UPPER.clone())).map(PathBuf::from).or_else(|| { - // otherwise, use the XDG data directory - env::var_os("XDG_DATA_HOME").map(PathBuf::from).map(|p| p.join(PROJECT_NAME.as_str())).filter(|p| p.is_absolute()) - }); - pub static ref CONFIG_FOLDER: Option = - // if `TELEVISION_CONFIG` is set, use that as the television config directory - env::var_os(format!("{}_CONFIG", PROJECT_NAME_UPPER.clone())).map(PathBuf::from).or_else(|| { - // otherwise, use the XDG config directory + 'television' - env::var_os("XDG_CONFIG_HOME").map(PathBuf::from).map(|p| p.join(PROJECT_NAME.as_str())).filter(|p| p.is_absolute()) - }); -} - +const PROJECT_NAME: &str = "television"; const CONFIG_FILE_NAME: &str = "config.toml"; pub struct ConfigEnv { @@ -184,7 +167,19 @@ impl Config { } pub fn get_data_dir() -> PathBuf { - let directory = if let Some(s) = DATA_FOLDER.clone() { + // if `TELEVISION_DATA` is set, use that as the data directory + let data_folder = + env::var_os(format!("{}_DATA", PROJECT_NAME.to_uppercase())) + .map(PathBuf::from) + .or_else(|| { + // otherwise, use the XDG data directory + env::var_os("XDG_DATA_HOME") + .map(PathBuf::from) + .map(|p| p.join(PROJECT_NAME)) + .filter(|p| p.is_absolute()) + }); + + let directory = if let Some(s) = data_folder { debug!("Using data directory: {:?}", s); s } else if let Some(proj_dirs) = project_directory() { @@ -197,7 +192,18 @@ pub fn get_data_dir() -> PathBuf { } pub fn get_config_dir() -> PathBuf { - let directory = if let Some(s) = CONFIG_FOLDER.clone() { + // if `TELEVISION_CONFIG` is set, use that as the television config directory + let config_dir = + env::var_os(format!("{}_CONFIG", PROJECT_NAME.to_uppercase())) + .map(PathBuf::from) + .or_else(|| { + // otherwise, use the XDG config directory + 'television' + env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .map(|p| p.join(PROJECT_NAME)) + .filter(|p| p.is_absolute()) + }); + let directory = if let Some(s) = config_dir { debug!("Config directory: {:?}", s); s } else if cfg!(unix) { diff --git a/television/config/themes.rs b/television/config/themes.rs index 2708278..8f64558 100644 --- a/television/config/themes.rs +++ b/television/config/themes.rs @@ -128,10 +128,10 @@ impl Theme { pub fn from_builtin( name: &str, ) -> Result> { - let theme_content: &str = builtin::BUILTIN_THEMES.get(name).map_or( - builtin::BUILTIN_THEMES.get(DEFAULT_THEME).unwrap(), - |t| *t, - ); + let builtin_themes = builtin::builtin_themes(); + let theme_content: &str = builtin_themes + .get(name) + .map_or(builtin_themes.get(DEFAULT_THEME).unwrap(), |t| *t); let theme = toml::from_str(theme_content)?; Ok(theme) } diff --git a/television/config/themes/builtin.rs b/television/config/themes/builtin.rs index bb6826d..081d481 100644 --- a/television/config/themes/builtin.rs +++ b/television/config/themes/builtin.rs @@ -1,43 +1,39 @@ use rustc_hash::FxHashMap; -use lazy_static::lazy_static; - -lazy_static! { - pub static ref BUILTIN_THEMES: FxHashMap<&'static str, &'static str> = { - let mut m = FxHashMap::default(); - m.insert("default", include_str!("../../../themes/default.toml")); - m.insert( - "television", - include_str!("../../../themes/television.toml"), - ); - m.insert( - "gruvbox-dark", - include_str!("../../../themes/gruvbox-dark.toml"), - ); - m.insert( - "gruvbox-light", - include_str!("../../../themes/gruvbox-light.toml"), - ); - m.insert( - "catppuccin", - include_str!("../../../themes/catppuccin.toml"), - ); - m.insert("nord-dark", include_str!("../../../themes/nord-dark.toml")); - m.insert( - "solarized-dark", - include_str!("../../../themes/solarized-dark.toml"), - ); - m.insert( - "solarized-light", - include_str!("../../../themes/solarized-light.toml"), - ); - m.insert("dracula", include_str!("../../../themes/dracula.toml")); - m.insert("monokai", include_str!("../../../themes/monokai.toml")); - m.insert("onedark", include_str!("../../../themes/onedark.toml")); - m.insert( - "tokyonight", - include_str!("../../../themes/tokyonight.toml"), - ); - m - }; +pub fn builtin_themes() -> FxHashMap<&'static str, &'static str> { + let mut m = FxHashMap::default(); + m.insert("default", include_str!("../../../themes/default.toml")); + m.insert( + "television", + include_str!("../../../themes/television.toml"), + ); + m.insert( + "gruvbox-dark", + include_str!("../../../themes/gruvbox-dark.toml"), + ); + m.insert( + "gruvbox-light", + include_str!("../../../themes/gruvbox-light.toml"), + ); + m.insert( + "catppuccin", + include_str!("../../../themes/catppuccin.toml"), + ); + m.insert("nord-dark", include_str!("../../../themes/nord-dark.toml")); + m.insert( + "solarized-dark", + include_str!("../../../themes/solarized-dark.toml"), + ); + m.insert( + "solarized-light", + include_str!("../../../themes/solarized-light.toml"), + ); + m.insert("dracula", include_str!("../../../themes/dracula.toml")); + m.insert("monokai", include_str!("../../../themes/monokai.toml")); + m.insert("onedark", include_str!("../../../themes/onedark.toml")); + m.insert( + "tokyonight", + include_str!("../../../themes/tokyonight.toml"), + ); + m } diff --git a/television/draw.rs b/television/draw.rs index 3846791..5c93690 100644 --- a/television/draw.rs +++ b/television/draw.rs @@ -3,7 +3,6 @@ use std::{hash::Hash, time::Instant}; use anyhow::Result; use ratatui::{layout::Rect, Frame}; use rustc_hash::FxHashSet; -use tokio::sync::mpsc::Sender; use crate::{ action::Action, @@ -18,7 +17,7 @@ use crate::{ remote_control::draw_remote_control, results::draw_results_list, spinner::Spinner, }, - television::{Message, Mode}, + television::Mode, utils::metadata::AppMetadata, }; @@ -61,7 +60,6 @@ impl Hash for ChannelState { pub struct TvState { pub mode: Mode, pub selected_entry: Option, - pub results_area_height: u16, pub results_picker: Picker, pub rc_picker: Picker, pub channel_state: ChannelState, @@ -74,7 +72,6 @@ impl TvState { pub fn new( mode: Mode, selected_entry: Option, - results_area_height: u16, results_picker: Picker, rc_picker: Picker, channel_state: ChannelState, @@ -84,7 +81,6 @@ impl TvState { Self { mode, selected_entry, - results_area_height, results_picker, rc_picker, channel_state, @@ -100,8 +96,8 @@ pub struct Ctx { pub config: Config, pub colorscheme: Colorscheme, pub app_metadata: AppMetadata, - pub tv_tx_handle: Sender, pub instant: Instant, + pub layout: Layout, } impl Ctx { @@ -110,16 +106,16 @@ impl Ctx { config: Config, colorscheme: Colorscheme, app_metadata: AppMetadata, - tv_tx_handle: Sender, instant: Instant, + layout: Layout, ) -> Self { Self { tv_state, config, colorscheme, app_metadata, - tv_tx_handle, instant, + layout, } } } @@ -156,7 +152,7 @@ impl Ord for Ctx { } } -pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> { +pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result { let selected_entry = ctx .tv_state .selected_entry @@ -185,14 +181,6 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> { &ctx.colorscheme, ); - if layout.results.height.saturating_sub(2) - != ctx.tv_state.results_area_height - { - ctx.tv_tx_handle.try_send(Message::ResultListHeightChanged( - layout.results.height.saturating_sub(2), - ))?; - } - // results list draw_results_list( f, @@ -259,5 +247,5 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> { )?; } - Ok(()) + Ok(layout) } diff --git a/television/logging.rs b/television/logging.rs index 8913fcc..1ed37fe 100644 --- a/television/logging.rs +++ b/television/logging.rs @@ -3,14 +3,10 @@ use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use crate::config::get_data_dir; -lazy_static::lazy_static! { - pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); -} - pub fn init() -> Result<()> { let directory = get_data_dir(); std::fs::create_dir_all(directory.clone())?; - let log_path = directory.join(LOG_FILE.clone()); + let log_path = directory.join(format!("{}.log", env!("CARGO_PKG_NAME"))); let log_file = std::fs::File::create(log_path)?; let file_subscriber = fmt::layer() .with_file(true) diff --git a/television/preview/previewers/command.rs b/television/preview/previewers/command.rs index 122c08a..b239fa7 100644 --- a/television/preview/previewers/command.rs +++ b/television/preview/previewers/command.rs @@ -2,7 +2,6 @@ use crate::channels::entry::{Entry, PreviewCommand}; use crate::preview::cache::PreviewCache; use crate::preview::{Preview, PreviewContent}; use crate::utils::command::shell_command; -use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; use rustc_hash::FxHashSet; @@ -11,12 +10,19 @@ use std::sync::Arc; use tracing::debug; #[allow(dead_code)] -#[derive(Debug, Default)] +#[derive(Debug)] pub struct CommandPreviewer { cache: Arc>, config: CommandPreviewerConfig, concurrent_preview_tasks: Arc, in_flight_previews: Arc>>, + command_re: Regex, +} + +impl Default for CommandPreviewer { + fn default() -> Self { + CommandPreviewer::new(None) + } } #[allow(dead_code)] @@ -53,6 +59,7 @@ impl CommandPreviewer { config, concurrent_preview_tasks: Arc::new(AtomicU8::new(0)), in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())), + command_re: Regex::new(r"\{(\d+)\}").unwrap(), } } @@ -96,6 +103,7 @@ impl CommandPreviewer { let concurrent_tasks = self.concurrent_preview_tasks.clone(); let command = command.clone(); let in_flight_previews = self.in_flight_previews.clone(); + let command_re = self.command_re.clone(); tokio::spawn(async move { try_preview( &command, @@ -103,6 +111,7 @@ impl CommandPreviewer { &cache, &concurrent_tasks, &in_flight_previews, + &command_re, ); }); } else { @@ -114,11 +123,6 @@ impl CommandPreviewer { } } -lazy_static! { - static ref COMMAND_PLACEHOLDER_REGEX: Regex = - Regex::new(r"\{(\d+)\}").unwrap(); -} - /// Format the command with the entry name and provided placeholders /// /// # Example @@ -131,11 +135,15 @@ lazy_static! { /// delimiter: ":".to_string(), /// }; /// let entry = Entry::new("a:given:entry:to:preview".to_string(), PreviewType::Command(command.clone())); -/// let formatted_command = format_command(&command, &entry); +/// let formatted_command = format_command(&command, &entry, ®ex::Regex::new(r"\{(\d+)\}").unwrap()); /// /// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'"); /// ``` -pub fn format_command(command: &PreviewCommand, entry: &Entry) -> String { +pub fn format_command( + command: &PreviewCommand, + entry: &Entry, + command_re: &Regex, +) -> String { let parts = entry.name.split(&command.delimiter).collect::>(); debug!("Parts: {:?}", parts); @@ -143,7 +151,7 @@ pub fn format_command(command: &PreviewCommand, entry: &Entry) -> String { .command .replace("{}", format!("'{}'", entry.name).as_str()); - formatted_command = COMMAND_PLACEHOLDER_REGEX + formatted_command = command_re .replace_all(&formatted_command, |caps: ®ex::Captures| { let index = caps.get(1).unwrap().as_str().parse::().unwrap(); @@ -160,9 +168,10 @@ pub fn try_preview( cache: &Arc>, concurrent_tasks: &Arc, in_flight_previews: &Arc>>, + command_re: &Regex, ) { debug!("Computing preview for {:?}", entry.name); - let command = format_command(command, entry); + let command = format_command(command, entry, command_re); debug!("Formatted preview command: {:?}", command); let child = shell_command() @@ -212,7 +221,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!( formatted_command, @@ -230,7 +243,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!(formatted_command, "something"); } @@ -245,7 +262,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!(formatted_command, "something 'an:entry:to:preview'"); } @@ -260,7 +281,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!(formatted_command, "something 'an' -t 'to'"); } diff --git a/television/render.rs b/television/render.rs index 036175e..fa6dd9f 100644 --- a/television/render.rs +++ b/television/render.rs @@ -8,12 +8,13 @@ use tracing::{debug, warn}; use tokio::sync::mpsc; use crate::draw::Ctx; +use crate::screen::layout::Layout; use crate::{action::Action, draw::draw, tui::Tui}; #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] pub enum RenderingTask { ClearScreen, - Render(Ctx), + Render(Box), Resize(u16, u16), Resume, Suspend, @@ -35,9 +36,21 @@ impl IoStream { } } +#[derive(Default)] +pub struct UiState { + pub layout: Layout, +} + +impl UiState { + pub fn new(layout: Layout) -> Self { + Self { layout } + } +} + pub async fn render( mut render_rx: mpsc::UnboundedReceiver, action_tx: mpsc::UnboundedSender, + ui_state_tx: mpsc::UnboundedSender, is_output_tty: bool, ) -> Result<()> { let stream = if is_output_tty { @@ -73,13 +86,19 @@ pub async fn render( if size.width.checked_mul(size.height).is_some() { queue!(stderr(), BeginSynchronizedUpdate).ok(); tui.terminal.draw(|frame| { - if let Err(err) = - draw(&context, frame, frame.area()) - { - warn!("Failed to draw: {:?}", err); - let _ = action_tx.send(Action::Error( - format!("Failed to draw: {err:?}"), - )); + match draw(&context, frame, frame.area()) { + Ok(layout) => { + if layout != context.layout { + let _ = ui_state_tx + .send(UiState::new(layout)); + } + } + Err(err) => { + warn!("Failed to draw: {:?}", err); + let _ = action_tx.send(Action::Error( + format!("Failed to draw: {err:?}"), + )); + } } })?; execute!(stderr(), EndSynchronizedUpdate).ok(); diff --git a/television/screen/layout.rs b/television/screen/layout.rs index 7049317..9197df0 100644 --- a/television/screen/layout.rs +++ b/television/screen/layout.rs @@ -29,7 +29,7 @@ impl Default for Dimensions { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct HelpBarLayout { pub left: Rect, pub middle: Rect, @@ -82,6 +82,7 @@ impl Display for PreviewTitlePosition { } } +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Layout { pub help_bar: Option, pub results: Rect, @@ -90,6 +91,17 @@ pub struct Layout { pub remote_control: Option, } +impl Default for Layout { + /// Having a default layout with a non-zero height for the results area + /// is important for the initial rendering of the application. For the first + /// frame, this avoids not rendering any results at all since the picker's contents + /// depend on the height of the results area which is not known until the first + /// frame is rendered. + fn default() -> Self { + Self::new(None, Rect::new(0, 0, 0, 100), Rect::default(), None, None) + } +} + impl Layout { #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/television/television.rs b/television/television.rs index 980d33b..07161e6 100644 --- a/television/television.rs +++ b/television/television.rs @@ -10,6 +10,7 @@ use crate::draw::{ChannelState, Ctx, TvState}; use crate::input::convert_action_to_input_request; use crate::picker::Picker; use crate::preview::{PreviewState, Previewer}; +use crate::render::UiState; use crate::screen::colors::Colorscheme; use crate::screen::layout::InputPosition; use crate::screen::spinner::{Spinner, SpinnerState}; @@ -20,7 +21,7 @@ use anyhow::Result; use rustc_hash::{FxBuildHasher, FxHashSet}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use tokio::sync::mpsc::{Receiver, Sender, UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; #[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)] pub enum Mode { @@ -38,7 +39,6 @@ pub struct Television { pub current_pattern: String, pub results_picker: Picker, pub rc_picker: Picker, - results_area_height: u16, pub previewer: Previewer, pub preview_state: PreviewState, pub spinner: Spinner, @@ -46,16 +46,7 @@ pub struct Television { pub app_metadata: AppMetadata, pub colorscheme: Colorscheme, pub ticks: u64, - // these are really here as a means to communicate between the render thread - // and the main thread to update `Television`'s state without needing to pass - // a mutable reference to `draw` - pub inner_rx: Receiver, - pub inner_tx: Sender, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Message { - ResultListHeightChanged(u16), + pub ui_state: UiState, } impl Television { @@ -87,8 +78,6 @@ impl Television { channel.find(&input.unwrap_or(EMPTY_STRING.to_string())); let spinner = Spinner::default(); - // capacity is quite arbitrary here, we can adjust it later - let (inner_tx, inner_rx) = tokio::sync::mpsc::channel(10); Self { action_tx, @@ -101,7 +90,6 @@ impl Television { current_pattern: EMPTY_STRING.to_string(), results_picker, rc_picker: Picker::default(), - results_area_height: 0, previewer, preview_state: PreviewState::default(), spinner, @@ -109,11 +97,14 @@ impl Television { app_metadata, colorscheme, ticks: 0, - inner_rx, - inner_tx, + ui_state: UiState::default(), } } + pub fn update_ui_state(&mut self, ui_state: UiState) { + self.ui_state = ui_state; + } + pub fn init_remote_control(&mut self) { let cable_channels = load_cable_channels().unwrap_or_default(); let builtin_channels = load_builtin_channels(Some( @@ -134,7 +125,6 @@ impl Television { let tv_state = TvState::new( self.mode, self.get_selected_entry(Some(Mode::Channel)), - self.results_area_height, self.results_picker.clone(), self.rc_picker.clone(), channel_state, @@ -147,9 +137,9 @@ impl Television { self.config.clone(), self.colorscheme.clone(), self.app_metadata.clone(), - self.inner_tx.clone(), // now timestamp std::time::Instant::now(), + self.ui_state.layout, ) } @@ -229,7 +219,7 @@ impl Television { picker.select_prev( step, result_count as usize, - self.results_area_height as usize, + self.ui_state.layout.results.height.saturating_sub(2) as usize, ); } @@ -248,7 +238,7 @@ impl Television { picker.select_next( step, result_count as usize, - self.results_area_height as usize, + self.ui_state.layout.results.height.saturating_sub(2) as usize, ); } @@ -315,6 +305,7 @@ impl Television { { // preview content if let Some(preview) = self.previewer.preview(selected_entry) { + // only update if the preview content has changed if self.preview_state.preview.title != preview.title { self.preview_state.update( preview, @@ -323,7 +314,13 @@ impl Television { .line_number .unwrap_or(0) .saturating_sub( - (self.results_area_height / 2).into(), + (self + .ui_state + .layout + .preview_window + .map_or(0, |w| w.height) + / 2) + .into(), ) .try_into()?, selected_entry @@ -348,7 +345,7 @@ impl Television { } self.results_picker.entries = self.channel.results( - self.results_area_height.into(), + self.ui_state.layout.results.height.into(), u32::try_from(self.results_picker.offset()).unwrap(), ); self.results_picker.total_items = self.channel.result_count(); @@ -364,7 +361,7 @@ impl Television { self.rc_picker.entries = self.remote_control.results( // this'll be more than the actual rc height but it's fine - self.results_area_height.into(), + self.ui_state.layout.results.height.into(), u32::try_from(self.rc_picker.offset()).unwrap(), ); self.rc_picker.total_items = self.remote_control.total_count(); @@ -513,11 +510,25 @@ impl Television { } Action::SelectNextPage => { self.preview_state.reset(); - self.select_next_entry(self.results_area_height.into()); + self.select_next_entry( + self.ui_state + .layout + .results + .height + .saturating_sub(2) + .into(), + ); } Action::SelectPrevPage => { self.preview_state.reset(); - self.select_prev_entry(self.results_area_height.into()); + self.select_prev_entry( + self.ui_state + .layout + .results + .height + .saturating_sub(2) + .into(), + ); } Action::ScrollPreviewDown => self.preview_state.scroll_down(1), Action::ScrollPreviewUp => self.preview_state.scroll_up(1), @@ -559,13 +570,6 @@ impl Television { /// /// This function may return an Action that'll be processed by the parent `App`. pub fn update(&mut self, action: &Action) -> Result> { - if let Ok(Message::ResultListHeightChanged(height)) = - self.inner_rx.try_recv() - { - self.results_area_height = height; - self.action_tx.send(Action::Render)?; - } - self.handle_action(action)?; self.update_results_picker_state(); diff --git a/television/utils/files.rs b/television/utils/files.rs index fb81fc4..f21d613 100644 --- a/television/utils/files.rs +++ b/television/utils/files.rs @@ -6,9 +6,9 @@ use std::io::BufReader; use std::io::Read; use std::path::Path; use std::path::PathBuf; +use std::sync::OnceLock; use ignore::{overrides::Override, types::TypesBuilder, WalkBuilder}; -use lazy_static::lazy_static; use tracing::{debug, warn}; use crate::utils::strings::{ @@ -61,8 +61,10 @@ where } } -lazy_static::lazy_static! { - pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into(); +pub static DEFAULT_NUM_THREADS: OnceLock = OnceLock::new(); + +pub fn get_default_num_threads() -> usize { + *DEFAULT_NUM_THREADS.get_or_init(default_num_threads) } pub fn walk_builder( @@ -143,340 +145,345 @@ where path.as_ref() .extension() .and_then(|ext| ext.to_str()) - .is_some_and(|ext| KNOWN_TEXT_FILE_EXTENSIONS.contains(ext)) + .is_some_and(|ext| get_known_text_file_extensions().contains(ext)) } -lazy_static! { - static ref KNOWN_TEXT_FILE_EXTENSIONS: FxHashSet<&'static str> = [ - "ada", - "adb", - "ads", - "applescript", - "as", - "asc", - "ascii", - "ascx", - "asm", - "asmx", - "asp", - "aspx", - "atom", - "au3", - "awk", - "bas", - "bash", - "bashrc", - "bat", - "bbcolors", - "bcp", - "bdsgroup", - "bdsproj", - "bib", - "bowerrc", - "c", - "cbl", - "cc", - "cfc", - "cfg", - "cfm", - "cfml", - "cgi", - "cjs", - "clj", - "cljs", - "cls", - "cmake", - "cmd", - "cnf", - "cob", - "code-snippets", - "coffee", - "coffeekup", - "conf", - "cp", - "cpp", - "cpt", - "cpy", - "crt", - "cs", - "csh", - "cson", - "csproj", - "csr", - "css", - "csslintrc", - "csv", - "ctl", - "curlrc", - "cxx", - "d", - "dart", - "dfm", - "diff", - "dof", - "dpk", - "dpr", - "dproj", - "dtd", - "eco", - "editorconfig", - "ejs", - "el", - "elm", - "emacs", - "eml", - "ent", - "erb", - "erl", - "eslintignore", - "eslintrc", - "ex", - "exs", - "f", - "f03", - "f77", - "f90", - "f95", - "fish", - "for", - "fpp", - "frm", - "fs", - "fsproj", - "fsx", - "ftn", - "gemrc", - "gemspec", - "gitattributes", - "gitconfig", - "gitignore", - "gitkeep", - "gitmodules", - "go", - "gpp", - "gradle", - "graphql", - "groovy", - "groupproj", - "grunit", - "gtmpl", - "gvimrc", - "h", - "haml", - "hbs", - "hgignore", - "hh", - "hpp", - "hrl", - "hs", - "hta", - "htaccess", - "htc", - "htm", - "html", - "htpasswd", - "hxx", - "iced", - "iml", - "inc", - "inf", - "info", - "ini", - "ino", - "int", - "irbrc", - "itcl", - "itermcolors", - "itk", - "jade", - "java", - "jhtm", - "jhtml", - "js", - "jscsrc", - "jshintignore", - "jshintrc", - "json", - "json5", - "jsonld", - "jsp", - "jspx", - "jsx", - "ksh", - "less", - "lhs", - "lisp", - "log", - "ls", - "lsp", - "lua", - "m", - "m4", - "mak", - "map", - "markdown", - "master", - "md", - "mdown", - "mdwn", - "mdx", - "metadata", - "mht", - "mhtml", - "mjs", - "mk", - "mkd", - "mkdn", - "mkdown", - "ml", - "mli", - "mm", - "mxml", - "nfm", - "nfo", - "noon", - "npmignore", - "npmrc", - "nuspec", - "nvmrc", - "ops", - "pas", - "pasm", - "patch", - "pbxproj", - "pch", - "pem", - "pg", - "php", - "php3", - "php4", - "php5", - "phpt", - "phtml", - "pir", - "pl", - "pm", - "pmc", - "pod", - "pot", - "prettierrc", - "properties", - "props", - "pt", - "pug", - "purs", - "py", - "pyx", - "r", - "rake", - "rb", - "rbw", - "rc", - "rdoc", - "rdoc_options", - "resx", - "rexx", - "rhtml", - "rjs", - "rlib", - "ron", - "rs", - "rss", - "rst", - "rtf", - "rvmrc", - "rxml", - "s", - "sass", - "scala", - "scm", - "scss", - "seestyle", - "sh", - "shtml", - "sln", - "sls", - "spec", - "sql", - "sqlite", - "sqlproj", - "srt", - "ss", - "sss", - "st", - "strings", - "sty", - "styl", - "stylus", - "sub", - "sublime-build", - "sublime-commands", - "sublime-completions", - "sublime-keymap", - "sublime-macro", - "sublime-menu", - "sublime-project", - "sublime-settings", - "sublime-workspace", - "sv", - "svc", - "svg", - "swift", - "t", - "tcl", - "tcsh", - "terminal", - "tex", - "text", - "textile", - "tg", - "tk", - "tmLanguage", - "tmpl", - "tmTheme", - "toml", - "tpl", - "ts", - "tsv", - "tsx", - "tt", - "tt2", - "ttml", - "twig", - "txt", - "v", - "vb", - "vbproj", - "vbs", - "vcproj", - "vcxproj", - "vh", - "vhd", - "vhdl", - "vim", - "viminfo", - "vimrc", - "vm", - "vue", - "webapp", - "webmanifest", - "wsc", - "x-php", - "xaml", - "xht", - "xhtml", - "xml", - "xs", - "xsd", - "xsl", - "xslt", - "y", - "yaml", - "yml", - "zsh", - "zshrc", - ] - .iter() - .copied() - .collect(); +pub static KNOWN_TEXT_FILE_EXTENSIONS: OnceLock> = + OnceLock::new(); + +pub fn get_known_text_file_extensions() -> &'static FxHashSet<&'static str> { + KNOWN_TEXT_FILE_EXTENSIONS.get_or_init(|| { + [ + "ada", + "adb", + "ads", + "applescript", + "as", + "asc", + "ascii", + "ascx", + "asm", + "asmx", + "asp", + "aspx", + "atom", + "au3", + "awk", + "bas", + "bash", + "bashrc", + "bat", + "bbcolors", + "bcp", + "bdsgroup", + "bdsproj", + "bib", + "bowerrc", + "c", + "cbl", + "cc", + "cfc", + "cfg", + "cfm", + "cfml", + "cgi", + "cjs", + "clj", + "cljs", + "cls", + "cmake", + "cmd", + "cnf", + "cob", + "code-snippets", + "coffee", + "coffeekup", + "conf", + "cp", + "cpp", + "cpt", + "cpy", + "crt", + "cs", + "csh", + "cson", + "csproj", + "csr", + "css", + "csslintrc", + "csv", + "ctl", + "curlrc", + "cxx", + "d", + "dart", + "dfm", + "diff", + "dof", + "dpk", + "dpr", + "dproj", + "dtd", + "eco", + "editorconfig", + "ejs", + "el", + "elm", + "emacs", + "eml", + "ent", + "erb", + "erl", + "eslintignore", + "eslintrc", + "ex", + "exs", + "f", + "f03", + "f77", + "f90", + "f95", + "fish", + "for", + "fpp", + "frm", + "fs", + "fsproj", + "fsx", + "ftn", + "gemrc", + "gemspec", + "gitattributes", + "gitconfig", + "gitignore", + "gitkeep", + "gitmodules", + "go", + "gpp", + "gradle", + "graphql", + "groovy", + "groupproj", + "grunit", + "gtmpl", + "gvimrc", + "h", + "haml", + "hbs", + "hgignore", + "hh", + "hpp", + "hrl", + "hs", + "hta", + "htaccess", + "htc", + "htm", + "html", + "htpasswd", + "hxx", + "iced", + "iml", + "inc", + "inf", + "info", + "ini", + "ino", + "int", + "irbrc", + "itcl", + "itermcolors", + "itk", + "jade", + "java", + "jhtm", + "jhtml", + "js", + "jscsrc", + "jshintignore", + "jshintrc", + "json", + "json5", + "jsonld", + "jsp", + "jspx", + "jsx", + "ksh", + "less", + "lhs", + "lisp", + "log", + "ls", + "lsp", + "lua", + "m", + "m4", + "mak", + "map", + "markdown", + "master", + "md", + "mdown", + "mdwn", + "mdx", + "metadata", + "mht", + "mhtml", + "mjs", + "mk", + "mkd", + "mkdn", + "mkdown", + "ml", + "mli", + "mm", + "mxml", + "nfm", + "nfo", + "noon", + "npmignore", + "npmrc", + "nuspec", + "nvmrc", + "ops", + "pas", + "pasm", + "patch", + "pbxproj", + "pch", + "pem", + "pg", + "php", + "php3", + "php4", + "php5", + "phpt", + "phtml", + "pir", + "pl", + "pm", + "pmc", + "pod", + "pot", + "prettierrc", + "properties", + "props", + "pt", + "pug", + "purs", + "py", + "pyx", + "r", + "rake", + "rb", + "rbw", + "rc", + "rdoc", + "rdoc_options", + "resx", + "rexx", + "rhtml", + "rjs", + "rlib", + "ron", + "rs", + "rss", + "rst", + "rtf", + "rvmrc", + "rxml", + "s", + "sass", + "scala", + "scm", + "scss", + "seestyle", + "sh", + "shtml", + "sln", + "sls", + "spec", + "sql", + "sqlite", + "sqlproj", + "srt", + "ss", + "sss", + "st", + "strings", + "sty", + "styl", + "stylus", + "sub", + "sublime-build", + "sublime-commands", + "sublime-completions", + "sublime-keymap", + "sublime-macro", + "sublime-menu", + "sublime-project", + "sublime-settings", + "sublime-workspace", + "sv", + "svc", + "svg", + "swift", + "t", + "tcl", + "tcsh", + "terminal", + "tex", + "text", + "textile", + "tg", + "tk", + "tmLanguage", + "tmpl", + "tmTheme", + "toml", + "tpl", + "ts", + "tsv", + "tsx", + "tt", + "tt2", + "ttml", + "twig", + "txt", + "v", + "vb", + "vbproj", + "vbs", + "vcproj", + "vcxproj", + "vh", + "vhd", + "vhdl", + "vim", + "viminfo", + "vimrc", + "vm", + "vue", + "webapp", + "webmanifest", + "wsc", + "x-php", + "xaml", + "xht", + "xhtml", + "xml", + "xs", + "xsd", + "xsl", + "xslt", + "y", + "yaml", + "yml", + "zsh", + "zshrc", + ] + .iter() + .copied() + .collect() + }) } diff --git a/television/utils/strings.rs b/television/utils/strings.rs index be63aa2..1610f1e 100644 --- a/television/utils/strings.rs +++ b/television/utils/strings.rs @@ -1,5 +1,3 @@ -use lazy_static::lazy_static; - /// Returns the index of the next character boundary in the given string. /// /// If the given index is already a character boundary, it is returned as is. @@ -151,14 +149,11 @@ pub fn try_parse_utf8_char(input: &[u8]) -> Option<(char, usize)> { decoded.map(|(seq, n)| (seq.chars().next().unwrap(), n)) } -lazy_static! { - /// The Unicode symbol to use for non-printable characters. - static ref NULL_SYMBOL: char = char::from_u32(0x2400).unwrap(); -} - pub const EMPTY_STRING: &str = ""; pub const TAB_WIDTH: usize = 4; +/// The Unicode symbol to use for non-printable characters. +const NULL_SYMBOL: char = '\u{2400}'; const TAB_CHARACTER: char = '\t'; const LINE_FEED_CHARACTER: char = '\x0A'; const DELETE_CHARACTER: char = '\x7F'; @@ -304,7 +299,7 @@ pub fn replace_non_printable( | BOM_CHARACTER if config.replace_control_characters => { - output.push(*NULL_SYMBOL); + output.push(NULL_SYMBOL); } // CJK Unified Ideographs // ex: 解 @@ -337,13 +332,13 @@ pub fn replace_non_printable( } // Unicode characters above 0x0700 seem unstable with ratatui c if c > '\u{0700}' => { - output.push(*NULL_SYMBOL); + output.push(NULL_SYMBOL); } // everything else c => output.push(c), } } else { - output.push(*NULL_SYMBOL); + output.push(NULL_SYMBOL); idx += 1; } } diff --git a/television/utils/syntax.rs b/television/utils/syntax.rs index 5ca69f5..6b2f5c1 100644 --- a/television/utils/syntax.rs +++ b/television/utils/syntax.rs @@ -224,7 +224,6 @@ pub fn compute_highlights_for_line<'a>( // SOFTWARE. use directories::BaseDirs; -use lazy_static::lazy_static; #[cfg(target_os = "macos")] use std::env; @@ -258,13 +257,11 @@ impl BatProjectDirs { } } -lazy_static! { - pub static ref PROJECT_DIRS: BatProjectDirs = BatProjectDirs::new() - .unwrap_or_else(|| panic!("Could not get home directory")); -} - pub fn load_highlighting_assets() -> HighlightingAssets { - HighlightingAssets::from_cache(PROJECT_DIRS.cache_dir()) + let project_dirs = BatProjectDirs::new() + .unwrap_or_else(|| panic!("Could not get home directory")); + + HighlightingAssets::from_cache(project_dirs.cache_dir()) .unwrap_or_else(|_| HighlightingAssets::from_binary()) } diff --git a/television/utils/threads.rs b/television/utils/threads.rs index 64d2c09..160b0da 100644 --- a/television/utils/threads.rs +++ b/television/utils/threads.rs @@ -5,7 +5,7 @@ use std::num::NonZeroUsize; /// This will use the number of available threads if possible, but will default to 1 if the number /// of available threads cannot be determined. It will also never use more than 32 threads to avoid /// startup overhead. -pub fn default_num_threads() -> NonZeroUsize { +pub fn default_num_threads() -> usize { // default to 1 thread if we can't determine the number of available threads let default = NonZeroUsize::MIN; // never use more than 32 threads to avoid startup overhead @@ -14,4 +14,5 @@ pub fn default_num_threads() -> NonZeroUsize { std::thread::available_parallelism() .unwrap_or(default) .min(limit) + .get() }