From 83d45c64143847b21ca2c68295e0a9c076a83bfb Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 14 Aug 2025 19:22:02 +0300 Subject: [PATCH] list: if we're in a TTY, output data in a TUI Signed-off-by: NotAShelf Change-Id: I6a6a696493f2e7ca911b6c1e2692b67f357a4b6b --- Cargo.lock | 326 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 +- src/commands/list.rs | 284 ++++++++++++++++++++++++++++++++++++- src/main.rs | 46 +++--- 4 files changed, 635 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4810791..13fd399 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.20" @@ -186,6 +192,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -229,6 +246,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.32" @@ -300,6 +332,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -309,6 +355,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -324,13 +379,47 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "mio 1.0.4", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio 1.0.4", + "parking_lot", + "rustix 1.0.8", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -340,6 +429,62 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dirs" version = "6.0.0" @@ -361,6 +506,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -373,6 +527,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "env_filter" version = "0.1.3" @@ -457,6 +617,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -526,6 +692,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -544,12 +712,27 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "imagesize" version = "0.14.0" @@ -566,6 +749,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "inquire" version = "0.7.5" @@ -573,13 +762,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ "bitflags 2.9.1", - "crossterm", + "crossterm 0.25.0", "dyn-clone", "fxhash", "newline-converter", "once_cell", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", +] + +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -588,6 +790,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -657,6 +868,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "lock_api" version = "0.4.13" @@ -673,6 +890,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.5" @@ -697,6 +923,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "newline-converter" version = "0.3.0" @@ -829,7 +1067,7 @@ checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix 1.0.8", "windows-sys 0.60.2", @@ -883,6 +1121,27 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -994,6 +1253,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.20" @@ -1061,7 +1326,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.4", "signal-hook", ] @@ -1107,14 +1373,17 @@ dependencies = [ name = "stash" version = "0.2.4" dependencies = [ + "atty", "base64", "clap", "clap-verbosity-flag", + "crossterm 0.29.0", "dirs", "env_logger", "imagesize", "inquire", "log", + "ratatui", "regex", "rmp-serde", "rusqlite", @@ -1122,15 +1391,45 @@ dependencies = [ "serde_json", "smol", "thiserror", + "unicode-segmentation", + "unicode-width 0.2.0", "wl-clipboard-rs", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.105" @@ -1199,12 +1498,29 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 248e525..09abd5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,11 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" base64 = "0.22.1" regex = "1.11.1" - +ratatui = "0.29.0" +atty = "0.2.14" +crossterm = "0.29.0" +unicode-segmentation = "1.12.0" +unicode-width = "0.2.0" [profile.release] lto = true diff --git a/src/commands/list.rs b/src/commands/list.rs index f79f407..0dd1ac1 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,14 +1,292 @@ -use crate::db::{ClipboardDb, SqliteClipboardDb}; use std::io::Write; +use crate::db::{ClipboardDb, SqliteClipboardDb, StashError}; +use unicode_width::UnicodeWidthStr; + pub trait ListCommand { - fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError>; + fn list(&self, out: impl Write, preview_width: u32) -> Result<(), StashError>; } impl ListCommand for SqliteClipboardDb { - fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError> { + fn list(&self, out: impl Write, preview_width: u32) -> Result<(), StashError> { self.list_entries(out, preview_width)?; log::info!("Listed clipboard entries"); Ok(()) } } + +impl SqliteClipboardDb { + /// Public TUI listing function for use in main.rs + #[allow(clippy::too_many_lines)] + pub fn list_tui(&self, preview_width: u32) -> Result<(), StashError> { + use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{ + EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, + }, + }; + use ratatui::{ + Terminal, + backend::CrosstermBackend, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState}, + }; + use std::io::stdout; + + // Query entries from DB + let mut stmt = self + .conn + .prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC") + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mut rows = stmt + .query([]) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + + struct EntryRow { + id: u64, + preview: String, + mime: String, + } + + let mut entries: Vec = Vec::new(); + let mut max_id_width = 2; + let mut max_mime_width = 8; + while let Some(row) = rows + .next() + .map_err(|e| StashError::ListDecode(e.to_string()))? + { + let id: u64 = row + .get(0) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mime: Option = row + .get(2) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let preview = crate::db::preview_entry(&contents, mime.as_deref(), preview_width); + let mime_str = mime.as_deref().unwrap_or("").to_string(); + let id_str = id.to_string(); + max_id_width = max_id_width.max(id_str.width()); + max_mime_width = max_mime_width.max(mime_str.width()); + entries.push(EntryRow { + id, + preview, + mime: mime_str, + }); + } + + enable_raw_mode().map_err(|e| StashError::ListDecode(e.to_string()))?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = + Terminal::new(backend).map_err(|e| StashError::ListDecode(e.to_string()))?; + + let mut state = ListState::default(); + if !entries.is_empty() { + state.select(Some(0)); + } + + let res = (|| -> Result<(), StashError> { + loop { + terminal + .draw(|f| { + let area = f.area(); + let block = Block::default() + .title("Clipboard Entries (j/k/↑/↓ to move, q/ESC to quit)") + .borders(Borders::ALL); + + use unicode_segmentation::UnicodeSegmentation; + use unicode_width::UnicodeWidthStr; + + let border_width = 2; + let highlight_symbol = ">"; + let highlight_width = 1; + let content_width = area.width as usize - border_width; + + // Minimum widths for columns + let min_id_width = 2; + let min_mime_width = 6; + let min_preview_width = 4; + let spaces = 3; // [id][ ][preview][ ][mime] + + // Dynamically allocate widths + let mut id_col = max_id_width.max(min_id_width); + let mut mime_col = max_mime_width.max(min_mime_width); + let mut preview_col = content_width + .saturating_sub(highlight_width) + .saturating_sub(id_col) + .saturating_sub(mime_col) + .saturating_sub(spaces); + + // If not enough space, shrink columns + if preview_col < min_preview_width { + let needed = min_preview_width - preview_col; + if mime_col > min_mime_width { + let reduce = mime_col - min_mime_width; + let take = reduce.min(needed); + mime_col -= take; + preview_col += take; + } + } + if preview_col < min_preview_width { + let needed = min_preview_width - preview_col; + if id_col > min_id_width { + let reduce = id_col - min_id_width; + let take = reduce.min(needed); + id_col -= take; + preview_col += take; + } + } + if preview_col < min_preview_width { + preview_col = min_preview_width; + } + + let selected = state.selected(); + + let list_items: Vec = entries + .iter() + .enumerate() + .map(|(i, entry)| { + // Truncate preview by grapheme clusters and display width + let mut preview = String::new(); + let mut width = 0; + for g in entry.preview.graphemes(true) { + let g_width = UnicodeWidthStr::width(g); + if width + g_width > preview_col { + preview.push('…'); + break; + } + preview.push_str(g); + width += g_width; + } + // Truncate and pad mimetype + let mut mime = String::new(); + let mut mwidth = 0; + for g in entry.mime.graphemes(true) { + let g_width = UnicodeWidthStr::width(g); + if mwidth + g_width > mime_col { + mime.push('…'); + break; + } + mime.push_str(g); + mwidth += g_width; + } + + let preview_str = format!("{preview:mime_col$}"); + + // Compose the row as highlight + id + space + preview + space + mimetype + let mut spans = Vec::new(); + if Some(i) == selected { + spans.push(Span::styled( + highlight_symbol, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); + spans.push(Span::styled( + format!("{:>width$}", entry.id, width = id_col), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); + spans.push(Span::raw(" ")); + spans.push(Span::styled( + preview_str, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); + spans.push(Span::raw(" ")); + spans.push(Span::styled( + mime_str, + Style::default().fg(Color::Green), + )); + } else { + spans.push(Span::raw(" ")); + spans.push(Span::raw(format!( + "{:>width$}", + entry.id, + width = id_col + ))); + spans.push(Span::raw(" ")); + spans.push(Span::raw(preview_str)); + spans.push(Span::raw(" ")); + spans.push(Span::raw(mime_str)); + } + ListItem::new(Line::from(spans)) + }) + .collect(); + + let list = List::new(list_items) + .block(block) + .highlight_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(""); // handled manually + + f.render_stateful_widget(list, area, &mut state); + }) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + + if event::poll(std::time::Duration::from_millis(250)) + .map_err(|e| StashError::ListDecode(e.to_string()))? + { + if let Event::Key(key) = + event::read().map_err(|e| StashError::ListDecode(e.to_string()))? + { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => break, + KeyCode::Down | KeyCode::Char('j') => { + let i = match state.selected() { + Some(i) => { + if i >= entries.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + state.select(Some(i)); + } + KeyCode::Up | KeyCode::Char('k') => { + let i = match state.selected() { + Some(i) => { + if i == 0 { + entries.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + state.select(Some(i)); + } + _ => {} + } + } + } + } + Ok(()) + })(); + + disable_raw_mode().ok(); + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ) + .ok(); + terminal.show_cursor().ok(); + + res + } +} diff --git a/src/main.rs b/src/main.rs index d4a1255..334afef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ use std::{ process, }; +use atty::Stream; + use clap::{CommandFactory, Parser, Subcommand}; use inquire::Confirm; @@ -153,30 +155,38 @@ fn main() { "Failed to store entry", ); } - Some(Command::List { format }) => { - let format = format.as_deref().unwrap_or("tsv"); - match format { - "tsv" => { + Some(Command::List { format }) => match format.as_deref() { + Some("tsv") => { + report_error( + db.list(io::stdout(), cli.preview_width), + "Failed to list entries", + ); + } + Some("json") => match db.list_json() { + Ok(json) => { + println!("{json}"); + } + Err(e) => { + log::error!("Failed to list entries as JSON: {e}"); + } + }, + Some(other) => { + log::error!("Unsupported format: {other}"); + } + None => { + if atty::is(Stream::Stdout) { + report_error( + db.list_tui(cli.preview_width), + "Failed to list entries in TUI", + ); + } else { report_error( db.list(io::stdout(), cli.preview_width), "Failed to list entries", ); } - - "json" => match db.list_json() { - Ok(json) => { - println!("{json}"); - } - Err(e) => { - log::error!("Failed to list entries as JSON: {e}"); - } - }, - - _ => { - log::error!("Unsupported format: {format}"); - } } - } + }, Some(Command::Decode { input }) => { report_error( db.decode(io::stdin(), io::stdout(), input),