From 7d1085d23929c0a9931e41d62c8834bf56c35e51 Mon Sep 17 00:00:00 2001 From: erikli Date: Tue, 21 Jun 2022 20:35:53 +0200 Subject: [PATCH 1/6] issue: Run tui application on interactive operation --- Cargo.lock | 1 + issue/Cargo.toml | 1 + issue/src/lib.rs | 24 +++++++++++++++ issue/src/tui.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 issue/src/tui.rs diff --git a/Cargo.lock b/Cargo.lock index 71fbf686..d8a6045e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4838,6 +4838,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tui", ] [[package]] diff --git a/issue/Cargo.toml b/issue/Cargo.toml index c07ee877..98fa7c6d 100644 --- a/issue/Cargo.toml +++ b/issue/Cargo.toml @@ -17,3 +17,4 @@ radicle-common = { path = "../common" } serde_json = { version = "1.0" } serde_yaml = { version = "0.8" } serde = { version = "1.0" } +tui = { version = "0.18.0" } diff --git a/issue/src/lib.rs b/issue/src/lib.rs index 63f5e699..f1a8b271 100644 --- a/issue/src/lib.rs +++ b/issue/src/lib.rs @@ -9,6 +9,8 @@ use radicle_common::cobs::issue::*; use radicle_common::{cobs, keys, profile, project}; use radicle_terminal as term; +mod tui; + pub const HELP: Help = Help { name: "issue", description: env!("CARGO_PKG_DESCRIPTION"), @@ -21,6 +23,7 @@ Usage rad issue delete rad issue react [--emoji ] rad issue list + rad issue interactive Options @@ -41,6 +44,7 @@ pub enum OperationName { React, Delete, List, + Interactive, } impl Default for OperationName { @@ -67,6 +71,7 @@ pub enum Operation { reaction: cobs::Reaction, }, List, + Interactive, } /// Tool options. @@ -125,6 +130,7 @@ impl Args for Options { "d" | "delete" => op = Some(OperationName::Delete), "l" | "list" => op = Some(OperationName::List), "r" | "react" => op = Some(OperationName::React), + "i" | "interactive" => op = Some(OperationName::Interactive), unknown => anyhow::bail!("unknown operation '{}'", unknown), }, @@ -158,6 +164,7 @@ impl Args for Options { id: id.ok_or_else(|| anyhow!("an issue id to remove must be provided"))?, }, OperationName::List => Operation::List, + OperationName::Interactive => Operation::Interactive, }; Ok((Options { op }, vec![])) @@ -235,6 +242,23 @@ pub fn run(options: Options) -> anyhow::Result<()> { Operation::Delete { id } => { issues.remove(&project, &id)?; } + Operation::Interactive => { + if let Some(metadata) = project::get(&storage, &project)? { + let load = || -> anyhow::Result> { + let mut list = issues.all(&metadata.urn)?; + for (_, issue) in &mut list { + issue.resolve(&storage)?; + } + Ok(list) + }; + let mut list = load()?; + while let Some(_) = tui::run(&metadata, list.clone())? { + list = load()?; + } + } else { + anyhow::bail!("could not load project metadata"); + } + } } Ok(()) diff --git a/issue/src/tui.rs b/issue/src/tui.rs new file mode 100644 index 00000000..7e594aeb --- /dev/null +++ b/issue/src/tui.rs @@ -0,0 +1,77 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use anyhow::{Error, Result}; +use lazy_static::lazy_static; + +use radicle_common::cobs::issue::{Issue, IssueId}; +use radicle_common::project::Metadata; +use radicle_terminal as term; + +use term::tui::events::{InputEvent, Key}; +use term::tui::store::Store; +use term::tui::theme::Theme; +use term::tui::window::{EmptyWidget, PageWidget, ShortcutWidget, TitleWidget}; +use term::tui::{Application, State}; + +type IssueList = Vec<(IssueId, Issue)>; + +#[derive(Clone, Eq, PartialEq)] +pub enum InternalCall {} + +#[derive(Clone, Eq, PartialEq)] +pub enum Action { + Quit, +} + +lazy_static! { + static ref BINDINGS: HashMap = + [(Key::Char('q'), Action::Quit),].iter().cloned().collect(); +} + +pub fn run(project: &Metadata, _issues: IssueList) -> Result, Error> { + let call: Option = None; + let mut app = Application::new(&update).store(vec![ + ("app.title", Box::new(project.name.clone())), + ("app.call.internal", Box::new(call)), + ("app.shortcuts", Box::new(vec![String::from("q quit")])), + ]); + + let pages = vec![PageWidget { + title: Rc::new(TitleWidget), + widgets: vec![Rc::new(EmptyWidget)], + shortcuts: Rc::new(ShortcutWidget), + }]; + + let theme = Theme::default_dark(); + app.execute(pages, &theme)?; + + match app.store.get::>("app.call.internal") { + Ok(Some(call)) => return Ok(Some(call.clone())), + Ok(None) | Err(_) => return Ok(None), + } +} + +pub fn update(store: &mut Store, event: &InputEvent) -> Result<(), Error> { + match event { + InputEvent::Input(key) => on_action(store, *key)?, + InputEvent::Tick => {} + } + Ok(()) +} + +pub fn on_action(store: &mut Store, key: Key) -> Result<(), Error> { + if let Some(action) = BINDINGS.get(&key) { + match action { + Action::Quit => { + quit_application(store)?; + } + } + } + Ok(()) +} + +pub fn quit_application(store: &mut Store) -> Result<(), Error> { + store.set("app.state", Box::new(State::Exiting)); + Ok(()) +} From 0732f9e77bed761348c4d576f7c2a51892d3aee4 Mon Sep 17 00:00:00 2001 From: erikli Date: Wed, 22 Jun 2022 10:44:34 +0200 Subject: [PATCH 2/6] issue: List all issues in tui mode --- Cargo.lock | 242 ++++++++++++++++++++++++++++++++++++---- issue/Cargo.toml | 1 + issue/src/lib.rs | 4 +- issue/src/tui.rs | 15 ++- issue/src/tui/window.rs | 93 +++++++++++++++ 5 files changed, 326 insertions(+), 29 deletions(-) create mode 100644 issue/src/tui/window.rs diff --git a/Cargo.lock b/Cargo.lock index d8a6045e..38a69fc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,6 +276,15 @@ dependencies = [ "syn", ] +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -862,6 +871,15 @@ dependencies = [ "os_str_bytes 6.0.0", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "clru" version = "0.5.0" @@ -1168,7 +1186,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils", "lazy_static", @@ -2008,6 +2026,12 @@ dependencies = [ "libc", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -2584,7 +2608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ "ahash 0.3.8", - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -2873,7 +2897,7 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown 0.11.2", ] @@ -2945,6 +2969,16 @@ dependencies = [ "nom 7.1.1", ] +[[package]] +name = "isolang" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265ef164908329e47e753c769b14cbb27434abf0c41984dca201484022f09ce5" +dependencies = [ + "phf 0.7.24", + "phf_codegen 0.7.24", +] + [[package]] name = "itertools" version = "0.9.0" @@ -3530,7 +3564,7 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ - "autocfg", + "autocfg 1.1.0", "scopeguard", ] @@ -3598,7 +3632,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -3931,7 +3965,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -3948,7 +3982,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-traits", ] @@ -3958,7 +3992,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-traits", ] @@ -3968,7 +4002,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -3979,7 +4013,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-bigint", "num-integer", "num-traits", @@ -3991,7 +4025,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -4002,7 +4036,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -4083,7 +4117,7 @@ version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cc", "libc", "pkg-config", @@ -4250,13 +4284,32 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared 0.7.24", +] + [[package]] name = "phf" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_shared", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator 0.7.24", + "phf_shared 0.7.24", ] [[package]] @@ -4265,8 +4318,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared 0.7.24", + "rand 0.6.5", ] [[package]] @@ -4275,17 +4338,26 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" dependencies = [ - "phf_shared", + "phf_shared 0.8.0", "rand 0.7.3", ] +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher 0.2.3", +] + [[package]] name = "phf_shared" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" dependencies = [ - "siphasher", + "siphasher 0.3.10", ] [[package]] @@ -4838,6 +4910,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "timeago", "tui", ] @@ -5216,6 +5289,25 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg 0.1.2", + "rand_xorshift", + "winapi 0.3.9", +] + [[package]] name = "rand" version = "0.7.3" @@ -5226,7 +5318,7 @@ dependencies = [ "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc", + "rand_hc 0.2.0", "rand_pcg 0.2.1", ] @@ -5241,6 +5333,16 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -5261,6 +5363,21 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -5279,6 +5396,15 @@ dependencies = [ "getrandom 0.2.6", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -5288,6 +5414,50 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + [[package]] name = "rand_pcg" version = "0.2.1" @@ -5306,6 +5476,15 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -5321,7 +5500,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" dependencies = [ - "autocfg", + "autocfg 1.1.0", "crossbeam-deque", "either", "rayon-core", @@ -5339,6 +5518,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -5965,6 +6153,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + [[package]] name = "siphasher" version = "0.3.10" @@ -6167,8 +6361,8 @@ dependencies = [ "dirs", "fnv", "nom 5.1.2", - "phf", - "phf_codegen", + "phf 0.8.0", + "phf_codegen 0.8.0", ] [[package]] @@ -6235,6 +6429,10 @@ name = "timeago" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26" +dependencies = [ + "chrono", + "isolang", +] [[package]] name = "tiny-keccak" diff --git a/issue/Cargo.toml b/issue/Cargo.toml index 98fa7c6d..78376986 100644 --- a/issue/Cargo.toml +++ b/issue/Cargo.toml @@ -17,4 +17,5 @@ radicle-common = { path = "../common" } serde_json = { version = "1.0" } serde_yaml = { version = "0.8" } serde = { version = "1.0" } +timeago = { version = "0.3.1" } tui = { version = "0.18.0" } diff --git a/issue/src/lib.rs b/issue/src/lib.rs index f1a8b271..a6a2ef34 100644 --- a/issue/src/lib.rs +++ b/issue/src/lib.rs @@ -251,8 +251,8 @@ pub fn run(options: Options) -> anyhow::Result<()> { } Ok(list) }; - let mut list = load()?; - while let Some(_) = tui::run(&metadata, list.clone())? { + let mut list = load()?; + while (tui::run(&metadata, list.clone())?).is_some() { list = load()?; } } else { diff --git a/issue/src/tui.rs b/issue/src/tui.rs index 7e594aeb..ada32bb8 100644 --- a/issue/src/tui.rs +++ b/issue/src/tui.rs @@ -11,9 +11,12 @@ use radicle_terminal as term; use term::tui::events::{InputEvent, Key}; use term::tui::store::Store; use term::tui::theme::Theme; -use term::tui::window::{EmptyWidget, PageWidget, ShortcutWidget, TitleWidget}; +use term::tui::window::{PageWidget, ShortcutWidget, TitleWidget}; use term::tui::{Application, State}; +pub mod window; +use window::BrowserWidget; + type IssueList = Vec<(IssueId, Issue)>; #[derive(Clone, Eq, PartialEq)] @@ -29,17 +32,19 @@ lazy_static! { [(Key::Char('q'), Action::Quit),].iter().cloned().collect(); } -pub fn run(project: &Metadata, _issues: IssueList) -> Result, Error> { +pub fn run(project: &Metadata, issues: IssueList) -> Result, Error> { let call: Option = None; let mut app = Application::new(&update).store(vec![ ("app.title", Box::new(project.name.clone())), ("app.call.internal", Box::new(call)), ("app.shortcuts", Box::new(vec![String::from("q quit")])), + ("project.issue.list", Box::new(issues)), + ("project.issue.active", Box::new(0_usize)), ]); let pages = vec![PageWidget { title: Rc::new(TitleWidget), - widgets: vec![Rc::new(EmptyWidget)], + widgets: vec![Rc::new(BrowserWidget)], shortcuts: Rc::new(ShortcutWidget), }]; @@ -47,8 +52,8 @@ pub fn run(project: &Metadata, _issues: IssueList) -> Result>("app.call.internal") { - Ok(Some(call)) => return Ok(Some(call.clone())), - Ok(None) | Err(_) => return Ok(None), + Ok(Some(call)) => Ok(Some(call.clone())), + Ok(None) | Err(_) => Ok(None), } } diff --git a/issue/src/tui/window.rs b/issue/src/tui/window.rs new file mode 100644 index 00000000..7af9a00b --- /dev/null +++ b/issue/src/tui/window.rs @@ -0,0 +1,93 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::{Error, Result}; +use timeago; + +use tui::backend::Backend; +use tui::layout::{Alignment, Rect}; +use tui::style::{Modifier, Style}; +use tui::text::{Span, Spans}; +use tui::widgets::{ListItem}; +use tui::Frame; + +use radicle_common::cobs::issue::{Issue, IssueId}; +use radicle_terminal as term; + +use term::tui::layout::Padding; +use term::tui::store::Store; +use term::tui::strings; +use term::tui::template; +use term::tui::theme::Theme; +use term::tui::window::Widget; + +type IssueList = Vec<(IssueId, Issue)>; + +#[derive(Clone)] +pub struct BrowserWidget; + +impl BrowserWidget { + fn items(&self, _id: &IssueId, issue: &Issue, theme: &Theme) -> ListItem { + let fmt = timeago::Formatter::new(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let timeago = Duration::from_secs(now - issue.comment.timestamp.as_secs()); + + let lines = vec![ + Spans::from(Span::styled(issue.title.clone(), theme.primary)), + Spans::from(vec![ + Span::styled( + issue.author.name(), + theme.primary_dim.add_modifier(Modifier::ITALIC), + ), + Span::raw(strings::whitespaces(1)), + Span::styled( + fmt.convert(timeago), + theme.ternary_dim.add_modifier(Modifier::ITALIC), + ), + ]), + ]; + ListItem::new(lines) + } +} + +impl Widget for BrowserWidget +where + B: Backend, +{ + fn draw( + &self, + store: &Store, + frame: &mut Frame, + area: Rect, + theme: &Theme, + ) -> Result<(), Error> { + let issues = store.get::("project.issue.list")?; + let active = store.get::("project.issue.active")?; + + let (block, inner) = template::block(theme, area, Padding { top: 1, left: 2 }, true); + frame.render_widget(block, area); + + if !issues.is_empty() { + let items: Vec = issues + .iter() + .map(|(id, issue)| self.items(&id, &issue, &theme)) + .collect(); + + let (list, mut state) = template::list(items, *active, &theme); + frame.render_stateful_widget(list, inner, &mut state); + } else { + let message = String::from("No issues found"); + let message = + template::paragraph(&message, Style::default()).alignment(Alignment::Center); + frame.render_widget(message, inner); + } + + Ok(()) + } + + fn height(&self, area: Rect) -> u16 { + area.height + } +} \ No newline at end of file From 78acc5bb1d77a9da691ad9c14eb586f0b6382879 Mon Sep 17 00:00:00 2001 From: erikli Date: Wed, 22 Jun 2022 11:13:17 +0200 Subject: [PATCH 3/6] issue: Allow tui issue list browsing --- issue/src/tui.rs | 41 +++++++++++++++++++++++++++++++++++++++-- issue/src/tui/window.rs | 8 ++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/issue/src/tui.rs b/issue/src/tui.rs index ada32bb8..9782edd3 100644 --- a/issue/src/tui.rs +++ b/issue/src/tui.rs @@ -24,12 +24,20 @@ pub enum InternalCall {} #[derive(Clone, Eq, PartialEq)] pub enum Action { + Up, + Down, Quit, } lazy_static! { - static ref BINDINGS: HashMap = - [(Key::Char('q'), Action::Quit),].iter().cloned().collect(); + static ref BINDINGS: HashMap = [ + (Key::Up, Action::Up), + (Key::Down, Action::Down), + (Key::Char('q'), Action::Quit) + ] + .iter() + .cloned() + .collect(); } pub fn run(project: &Metadata, issues: IssueList) -> Result, Error> { @@ -71,6 +79,12 @@ pub fn on_action(store: &mut Store, key: Key) -> Result<(), Error> { Action::Quit => { quit_application(store)?; } + Action::Up => { + select_previous_issue(store)?; + } + Action::Down => { + select_next_issue(store)?; + } } } Ok(()) @@ -80,3 +94,26 @@ pub fn quit_application(store: &mut Store) -> Result<(), Error> { store.set("app.state", Box::new(State::Exiting)); Ok(()) } + +pub fn select_next_issue(store: &mut Store) -> Result<(), Error> { + let issues = store.get::("project.issue.list")?; + let active = store.get::("project.issue.active")?; + let active = match *active >= issues.len() - 1 { + true => issues.len() - 1, + false => active + 1, + }; + store.set("project.issue.active", Box::new(active)); + + Ok(()) +} + +pub fn select_previous_issue(store: &mut Store) -> Result<(), Error> { + let active = store.get::("project.issue.active")?; + let active = match *active == 0 { + true => 0, + false => active - 1, + }; + store.set("project.issue.active", Box::new(active)); + + Ok(()) +} diff --git a/issue/src/tui/window.rs b/issue/src/tui/window.rs index 7af9a00b..b2fa80fe 100644 --- a/issue/src/tui/window.rs +++ b/issue/src/tui/window.rs @@ -7,7 +7,7 @@ use tui::backend::Backend; use tui::layout::{Alignment, Rect}; use tui::style::{Modifier, Style}; use tui::text::{Span, Spans}; -use tui::widgets::{ListItem}; +use tui::widgets::ListItem; use tui::Frame; use radicle_common::cobs::issue::{Issue, IssueId}; @@ -72,10 +72,10 @@ where if !issues.is_empty() { let items: Vec = issues .iter() - .map(|(id, issue)| self.items(&id, &issue, &theme)) + .map(|(id, issue)| self.items(id, issue, theme)) .collect(); - let (list, mut state) = template::list(items, *active, &theme); + let (list, mut state) = template::list(items, *active, theme); frame.render_stateful_widget(list, inner, &mut state); } else { let message = String::from("No issues found"); @@ -90,4 +90,4 @@ where fn height(&self, area: Rect) -> u16 { area.height } -} \ No newline at end of file +} From 50970c3d4e6b69bc2fbabeb727679cb510b9d9dd Mon Sep 17 00:00:00 2001 From: erikli Date: Wed, 22 Jun 2022 16:22:21 +0200 Subject: [PATCH 4/6] issue: Toogle between open & closed issues in tui --- Cargo.lock | 1 + issue/Cargo.toml | 1 + issue/src/tui.rs | 70 ++++++++++++++++++++++++++----- issue/src/tui/state.rs | 35 ++++++++++++++++ issue/src/tui/window.rs | 93 ++++++++++++++++++++++++++++++++++++----- 5 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 issue/src/tui/state.rs diff --git a/Cargo.lock b/Cargo.lock index 38a69fc4..7acfd3a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4910,6 +4910,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "thiserror", "timeago", "tui", ] diff --git a/issue/Cargo.toml b/issue/Cargo.toml index 78376986..3671feb0 100644 --- a/issue/Cargo.toml +++ b/issue/Cargo.toml @@ -17,5 +17,6 @@ radicle-common = { path = "../common" } serde_json = { version = "1.0" } serde_yaml = { version = "0.8" } serde = { version = "1.0" } +thiserror = "1.0" timeago = { version = "0.3.1" } tui = { version = "0.18.0" } diff --git a/issue/src/tui.rs b/issue/src/tui.rs index 9782edd3..9da080f5 100644 --- a/issue/src/tui.rs +++ b/issue/src/tui.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use anyhow::{Error, Result}; use lazy_static::lazy_static; -use radicle_common::cobs::issue::{Issue, IssueId}; +use radicle_common::cobs::issue::{Issue, IssueId, State as IssueState}; use radicle_common::project::Metadata; use radicle_terminal as term; @@ -14,8 +14,11 @@ use term::tui::theme::Theme; use term::tui::window::{PageWidget, ShortcutWidget, TitleWidget}; use term::tui::{Application, State}; +pub mod state; pub mod window; -use window::BrowserWidget; + +use state::Tab; +use window::{BrowserWidget, TabWidget}; type IssueList = Vec<(IssueId, Issue)>; @@ -26,6 +29,7 @@ pub enum InternalCall {} pub enum Action { Up, Down, + NextTab, Quit, } @@ -33,6 +37,7 @@ lazy_static! { static ref BINDINGS: HashMap = [ (Key::Up, Action::Up), (Key::Down, Action::Down), + (Key::Tab, Action::NextTab), (Key::Char('q'), Action::Quit) ] .iter() @@ -42,17 +47,28 @@ lazy_static! { pub fn run(project: &Metadata, issues: IssueList) -> Result, Error> { let call: Option = None; + let mut open = issues.clone(); + let mut closed = issues; + + open.retain(|(_, issue)| issue.state() == IssueState::Open); + closed.retain(|(_, issue)| issue.state() != IssueState::Open); + let mut app = Application::new(&update).store(vec![ ("app.title", Box::new(project.name.clone())), ("app.call.internal", Box::new(call)), + ("app.browser.tab.active", Box::new(Tab::Open)), ("app.shortcuts", Box::new(vec![String::from("q quit")])), - ("project.issue.list", Box::new(issues)), - ("project.issue.active", Box::new(0_usize)), + ("project.issue.open.list", Box::new(open)), + ("project.issue.open.active", Box::new(0_usize)), + ("project.issue.closed.list", Box::new(closed)), + ("project.issue.closed.active", Box::new(0_usize)), ]); let pages = vec![PageWidget { title: Rc::new(TitleWidget), - widgets: vec![Rc::new(BrowserWidget)], + widgets: vec![Rc::new(BrowserWidget { + tabs: Rc::new(TabWidget), + })], shortcuts: Rc::new(ShortcutWidget), }]; @@ -85,6 +101,9 @@ pub fn on_action(store: &mut Store, key: Key) -> Result<(), Error> { Action::Down => { select_next_issue(store)?; } + Action::NextTab => { + select_next_tab(store)?; + } } } Ok(()) @@ -96,24 +115,55 @@ pub fn quit_application(store: &mut Store) -> Result<(), Error> { } pub fn select_next_issue(store: &mut Store) -> Result<(), Error> { - let issues = store.get::("project.issue.list")?; - let active = store.get::("project.issue.active")?; + let tab = store.get::("app.browser.tab.active")?; + let (issues, active) = match tab { + Tab::Open => { + let issues = store.get::("project.issue.open.list")?; + let active = store.get::("project.issue.open.active")?; + (issues, active) + } + Tab::Closed => { + let issues = store.get::("project.issue.closed.list")?; + let active = store.get::("project.issue.closed.active")?; + (issues, active) + } + }; let active = match *active >= issues.len() - 1 { true => issues.len() - 1, false => active + 1, }; - store.set("project.issue.active", Box::new(active)); + match tab { + Tab::Open => store.set("project.issue.open.active", Box::new(active)), + Tab::Closed => store.set("project.issue.closed.active", Box::new(active)), + } Ok(()) } pub fn select_previous_issue(store: &mut Store) -> Result<(), Error> { - let active = store.get::("project.issue.active")?; + let tab = store.get::("app.browser.tab.active")?; + let active = match tab { + Tab::Open => store.get::("project.issue.open.active")?, + Tab::Closed => store.get::("project.issue.closed.active")?, + }; + let active = match *active == 0 { true => 0, false => active - 1, }; - store.set("project.issue.active", Box::new(active)); + match tab { + Tab::Open => store.set("project.issue.open.active", Box::new(active)), + Tab::Closed => store.set("project.issue.closed.active", Box::new(active)), + } Ok(()) } + +pub fn select_next_tab(store: &mut Store) -> Result<(), Error> { + let tab = store.get::("app.browser.tab.active")?; + match tab { + Tab::Open => store.set("app.browser.tab.active", Box::new(Tab::Closed)), + Tab::Closed => store.set("app.browser.tab.active", Box::new(Tab::Open)), + } + Ok(()) +} diff --git a/issue/src/tui/state.rs b/issue/src/tui/state.rs new file mode 100644 index 00000000..db1e8a63 --- /dev/null +++ b/issue/src/tui/state.rs @@ -0,0 +1,35 @@ +use std::convert::{TryFrom, TryInto}; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TabError { + #[error("{0}")] + InvalidIndex(String), +} + +#[derive(Clone, Copy, Debug)] +pub enum Tab { + Open = 0, + Closed = 1, +} + +impl TryFrom for Tab { + type Error = TabError; + + fn try_from(value: usize) -> Result { + match value { + 0 => Ok(Tab::Open), + 1 => Ok(Tab::Closed), + _ => Err(TabError::InvalidIndex("Tab index not allowed!".to_owned())), + } + } +} + +impl TryInto for Tab { + type Error = TabError; + + fn try_into(self) -> Result { + Ok(self as usize) + } +} diff --git a/issue/src/tui/window.rs b/issue/src/tui/window.rs index b2fa80fe..6169ed20 100644 --- a/issue/src/tui/window.rs +++ b/issue/src/tui/window.rs @@ -1,18 +1,20 @@ +use std::rc::Rc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use anyhow::{Error, Result}; use timeago; use tui::backend::Backend; -use tui::layout::{Alignment, Rect}; +use tui::layout::{Alignment, Direction, Rect}; use tui::style::{Modifier, Style}; use tui::text::{Span, Spans}; -use tui::widgets::ListItem; +use tui::widgets::{ListItem, Tabs}; use tui::Frame; use radicle_common::cobs::issue::{Issue, IssueId}; use radicle_terminal as term; +use term::tui::layout; use term::tui::layout::Padding; use term::tui::store::Store; use term::tui::strings; @@ -20,12 +22,19 @@ use term::tui::template; use term::tui::theme::Theme; use term::tui::window::Widget; +use super::state::Tab; + type IssueList = Vec<(IssueId, Issue)>; #[derive(Clone)] -pub struct BrowserWidget; +pub struct BrowserWidget { + pub tabs: Rc>, +} -impl BrowserWidget { +impl BrowserWidget +where + B: Backend, +{ fn items(&self, _id: &IssueId, issue: &Issue, theme: &Theme) -> ListItem { let fmt = timeago::Formatter::new(); let now = SystemTime::now() @@ -52,7 +61,7 @@ impl BrowserWidget { } } -impl Widget for BrowserWidget +impl Widget for BrowserWidget where B: Backend, { @@ -63,20 +72,36 @@ where area: Rect, theme: &Theme, ) -> Result<(), Error> { - let issues = store.get::("project.issue.list")?; - let active = store.get::("project.issue.active")?; - - let (block, inner) = template::block(theme, area, Padding { top: 1, left: 2 }, true); + let open = store.get::("project.issue.open.list")?; + let closed = store.get::("project.issue.closed.list")?; + let (block, inner) = template::block(theme, area, Padding { top: 0, left: 2 }, true); frame.render_widget(block, area); - if !issues.is_empty() { + if !open.is_empty() || !closed.is_empty() { + let tab = store.get::("app.browser.tab.active")?; + let (issues, active) = match tab { + Tab::Open => { + let active = store.get::("project.issue.open.active")?; + (open, active) + } + Tab::Closed => { + let active = store.get::("project.issue.closed.active")?; + (closed, active) + } + }; let items: Vec = issues .iter() .map(|(id, issue)| self.items(id, issue, theme)) .collect(); + let tab_h = self.tabs.height(inner); + let heights = vec![tab_h, inner.height.saturating_sub(tab_h)]; + let areas = layout::split_area(inner, heights, Direction::Vertical); + + self.tabs.draw(store, frame, areas[0], theme)?; + let (list, mut state) = template::list(items, *active, theme); - frame.render_stateful_widget(list, inner, &mut state); + frame.render_stateful_widget(list, areas[1], &mut state); } else { let message = String::from("No issues found"); let message = @@ -91,3 +116,49 @@ where area.height } } + +#[derive(Clone)] +pub struct TabWidget; + +impl Widget for TabWidget +where + B: Backend, +{ + fn draw( + &self, + store: &Store, + frame: &mut Frame, + area: Rect, + theme: &Theme, + ) -> Result<(), Error> { + let open = store.get::("project.issue.open.list")?; + let closed = store.get::("project.issue.closed.list")?; + let active = store.get::("app.browser.tab.active")?; + let active = *active as usize; + + let tabs = vec![ + format!("{} open", open.len()), + format!("{} closed", closed.len()), + ]; + let divider = "|"; + + let (_, inner) = template::block(theme, area, Padding { top: 1, left: 0 }, false); + let tabs = tabs + .iter() + .map(|t| Spans::from(Span::styled(t, Style::default()))) + .collect(); + + let tabs = Tabs::new(tabs) + .style(theme.ternary_dim) + .highlight_style(theme.ternary) + .divider(divider) + .select(active); + frame.render_widget(tabs, inner); + + Ok(()) + } + + fn height(&self, _area: Rect) -> u16 { + 3_u16 + } +} From af2590504dddb875201a0d0c772a4dc92df30c80 Mon Sep 17 00:00:00 2001 From: erikli Date: Thu, 23 Jun 2022 14:13:33 +0200 Subject: [PATCH 5/6] issue: Improve tui list and tab properties --- issue/src/tui.rs | 84 +++++++++++++++++------------------------ issue/src/tui/window.rs | 53 ++++++++++++-------------- 2 files changed, 60 insertions(+), 77 deletions(-) diff --git a/issue/src/tui.rs b/issue/src/tui.rs index 9da080f5..8f559f5f 100644 --- a/issue/src/tui.rs +++ b/issue/src/tui.rs @@ -9,18 +9,20 @@ use radicle_common::project::Metadata; use radicle_terminal as term; use term::tui::events::{InputEvent, Key}; -use term::tui::store::Store; +use term::tui::store::{ListProperty, Store, TabProperty}; use term::tui::theme::Theme; use term::tui::window::{PageWidget, ShortcutWidget, TitleWidget}; use term::tui::{Application, State}; pub mod state; +pub mod store; pub mod window; -use state::Tab; +use state::{Tab}; use window::{BrowserWidget, TabWidget}; -type IssueList = Vec<(IssueId, Issue)>; +type TabList = TabProperty; +type IssueList = ListProperty<(IssueId, Issue)>; #[derive(Clone, Eq, PartialEq)] pub enum InternalCall {} @@ -45,23 +47,23 @@ lazy_static! { .collect(); } -pub fn run(project: &Metadata, issues: IssueList) -> Result, Error> { - let call: Option = None; +pub fn run( + project: &Metadata, + issues: Vec<(IssueId, Issue)>, +) -> Result, Error> { let mut open = issues.clone(); let mut closed = issues; open.retain(|(_, issue)| issue.state() == IssueState::Open); closed.retain(|(_, issue)| issue.state() != IssueState::Open); + let tabs = vec![Tab::Open, Tab::Closed]; let mut app = Application::new(&update).store(vec![ ("app.title", Box::new(project.name.clone())), - ("app.call.internal", Box::new(call)), - ("app.browser.tab.active", Box::new(Tab::Open)), + ("app.browser.tabs", Box::new(TabProperty::new(tabs))), ("app.shortcuts", Box::new(vec![String::from("q quit")])), - ("project.issue.open.list", Box::new(open)), - ("project.issue.open.active", Box::new(0_usize)), - ("project.issue.closed.list", Box::new(closed)), - ("project.issue.closed.active", Box::new(0_usize)), + ("project.issues.open", Box::new(ListProperty::new(open))), + ("project.issues.closed", Box::new(ListProperty::new(closed))), ]); let pages = vec![PageWidget { @@ -115,55 +117,39 @@ pub fn quit_application(store: &mut Store) -> Result<(), Error> { } pub fn select_next_issue(store: &mut Store) -> Result<(), Error> { - let tab = store.get::("app.browser.tab.active")?; - let (issues, active) = match tab { - Tab::Open => { - let issues = store.get::("project.issue.open.list")?; - let active = store.get::("project.issue.open.active")?; - (issues, active) + let tabs = store.get::("app.browser.tabs")?; + match tabs.items().selected() { + Some(Tab::Open) => { + let issues = store.get_mut::("project.issues.open")?; + issues.select_next(); } - Tab::Closed => { - let issues = store.get::("project.issue.closed.list")?; - let active = store.get::("project.issue.closed.active")?; - (issues, active) + Some(Tab::Closed) => { + let issues = store.get_mut::("project.issues.closed")?; + issues.select_next(); } + _ => {} }; - let active = match *active >= issues.len() - 1 { - true => issues.len() - 1, - false => active + 1, - }; - match tab { - Tab::Open => store.set("project.issue.open.active", Box::new(active)), - Tab::Closed => store.set("project.issue.closed.active", Box::new(active)), - } - Ok(()) } pub fn select_previous_issue(store: &mut Store) -> Result<(), Error> { - let tab = store.get::("app.browser.tab.active")?; - let active = match tab { - Tab::Open => store.get::("project.issue.open.active")?, - Tab::Closed => store.get::("project.issue.closed.active")?, - }; - - let active = match *active == 0 { - true => 0, - false => active - 1, + let tabs = store.get::("app.browser.tabs")?; + match tabs.items().selected() { + Some(Tab::Open) => { + let issues = store.get_mut::("project.issues.open")?; + issues.select_previous(); + } + Some(Tab::Closed) => { + let issues = store.get_mut::("project.issues.closed")?; + issues.select_previous(); + } + _ => {} }; - match tab { - Tab::Open => store.set("project.issue.open.active", Box::new(active)), - Tab::Closed => store.set("project.issue.closed.active", Box::new(active)), - } - Ok(()) } pub fn select_next_tab(store: &mut Store) -> Result<(), Error> { - let tab = store.get::("app.browser.tab.active")?; - match tab { - Tab::Open => store.set("app.browser.tab.active", Box::new(Tab::Closed)), - Tab::Closed => store.set("app.browser.tab.active", Box::new(Tab::Open)), - } + let tabs = store.get_mut::("app.browser.tabs")?; + tabs.select_next(); Ok(()) } diff --git a/issue/src/tui/window.rs b/issue/src/tui/window.rs index 6169ed20..2aec18ee 100644 --- a/issue/src/tui/window.rs +++ b/issue/src/tui/window.rs @@ -16,7 +16,7 @@ use radicle_terminal as term; use term::tui::layout; use term::tui::layout::Padding; -use term::tui::store::Store; +use term::tui::store::{ListProperty, Store, TabProperty}; use term::tui::strings; use term::tui::template; use term::tui::theme::Theme; @@ -24,7 +24,8 @@ use term::tui::window::Widget; use super::state::Tab; -type IssueList = Vec<(IssueId, Issue)>; +type IssueList = ListProperty<(IssueId, Issue)>; +type TabList = TabProperty; #[derive(Clone)] pub struct BrowserWidget { @@ -72,24 +73,21 @@ where area: Rect, theme: &Theme, ) -> Result<(), Error> { - let open = store.get::("project.issue.open.list")?; - let closed = store.get::("project.issue.closed.list")?; + let open = store.get::("project.issues.open")?; + let closed = store.get::("project.issues.closed")?; let (block, inner) = template::block(theme, area, Padding { top: 0, left: 2 }, true); frame.render_widget(block, area); - if !open.is_empty() || !closed.is_empty() { - let tab = store.get::("app.browser.tab.active")?; - let (issues, active) = match tab { - Tab::Open => { - let active = store.get::("project.issue.open.active")?; - (open, active) - } - Tab::Closed => { - let active = store.get::("project.issue.closed.active")?; - (closed, active) - } + if !open.items().is_empty() || !closed.items().is_empty() { + let tabs = store.get::("app.browser.tabs")?; + let issues = match tabs.items().selected() { + Some(Tab::Open) => open, + Some(Tab::Closed) => closed, + None => open, }; let items: Vec = issues + .items() + .all() .iter() .map(|(id, issue)| self.items(id, issue, theme)) .collect(); @@ -100,7 +98,7 @@ where self.tabs.draw(store, frame, areas[0], theme)?; - let (list, mut state) = template::list(items, *active, theme); + let (list, mut state) = template::list(items, issues.items().selected_index(), theme); frame.render_stateful_widget(list, areas[1], &mut state); } else { let message = String::from("No issues found"); @@ -131,29 +129,28 @@ where area: Rect, theme: &Theme, ) -> Result<(), Error> { - let open = store.get::("project.issue.open.list")?; - let closed = store.get::("project.issue.closed.list")?; - let active = store.get::("app.browser.tab.active")?; - let active = *active as usize; - - let tabs = vec![ - format!("{} open", open.len()), - format!("{} closed", closed.len()), + let open = store.get::("project.issues.open")?; + let closed = store.get::("project.issues.closed")?; + let tabs = store.get::("app.browser.tabs")?; + + let items = vec![ + format!("{} open", open.items().count()), + format!("{} closed", closed.items().count()), ]; let divider = "|"; let (_, inner) = template::block(theme, area, Padding { top: 1, left: 0 }, false); - let tabs = tabs + let items = items .iter() .map(|t| Spans::from(Span::styled(t, Style::default()))) .collect(); - let tabs = Tabs::new(tabs) + let items = Tabs::new(items) .style(theme.ternary_dim) .highlight_style(theme.ternary) .divider(divider) - .select(active); - frame.render_widget(tabs, inner); + .select(tabs.items().selected_index()); + frame.render_widget(items, inner); Ok(()) } From 44437e0ce5d6228f74c7f04aaf0f2a15c2f1ff76 Mon Sep 17 00:00:00 2001 From: erikli Date: Thu, 23 Jun 2022 16:12:44 +0200 Subject: [PATCH 6/6] issue: Add tui issue info --- Cargo.lock | 55 +++++++++++++++++++++++++++++ issue/src/tui.rs | 29 +++++++++++---- issue/src/tui/store.rs | 18 ++++++++++ issue/src/tui/window.rs | 78 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 issue/src/tui/store.rs diff --git a/Cargo.lock b/Cargo.lock index 7acfd3a0..78628850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -752,6 +752,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cc" version = "1.0.73" @@ -1214,6 +1220,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.8.2", + "parking_lot 0.12.0", + "signal-hook", + "signal-hook-mio", + "winapi 0.3.9", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -6129,6 +6160,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio 0.8.2", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -6588,6 +6630,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tui" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "typenum" version = "1.15.0" diff --git a/issue/src/tui.rs b/issue/src/tui.rs index 8f559f5f..11f76fb4 100644 --- a/issue/src/tui.rs +++ b/issue/src/tui.rs @@ -18,8 +18,9 @@ pub mod state; pub mod store; pub mod window; -use state::{Tab}; -use window::{BrowserWidget, TabWidget}; +use state::Tab; +use store::ToogleProperty; +use window::{BrowserWidget, InfoWidget, TabWidget}; type TabList = TabProperty; type IssueList = ListProperty<(IssueId, Issue)>; @@ -32,6 +33,7 @@ pub enum Action { Up, Down, NextTab, + ToogleInfo, Quit, } @@ -40,7 +42,8 @@ lazy_static! { (Key::Up, Action::Up), (Key::Down, Action::Down), (Key::Tab, Action::NextTab), - (Key::Char('q'), Action::Quit) + (Key::Char('q'), Action::Quit), + (Key::Char('i'), Action::ToogleInfo), ] .iter() .cloned() @@ -61,15 +64,20 @@ pub fn run( let mut app = Application::new(&update).store(vec![ ("app.title", Box::new(project.name.clone())), ("app.browser.tabs", Box::new(TabProperty::new(tabs))), - ("app.shortcuts", Box::new(vec![String::from("q quit")])), - ("project.issues.open", Box::new(ListProperty::new(open))), - ("project.issues.closed", Box::new(ListProperty::new(closed))), + ( + "app.shortcuts", + Box::new(vec![String::from("i info"), String::from("q quit")]), + ), + ("app.browser.info", Box::new(ToogleProperty::new(false))), + ("project.issues.open", Box::new(IssueList::new(open))), + ("project.issues.closed", Box::new(IssueList::new(closed))), ]); let pages = vec![PageWidget { title: Rc::new(TitleWidget), widgets: vec![Rc::new(BrowserWidget { tabs: Rc::new(TabWidget), + info: Rc::new(InfoWidget), })], shortcuts: Rc::new(ShortcutWidget), }]; @@ -106,6 +114,9 @@ pub fn on_action(store: &mut Store, key: Key) -> Result<(), Error> { Action::NextTab => { select_next_tab(store)?; } + Action::ToogleInfo => { + toogle_info(store)?; + } } } Ok(()) @@ -153,3 +164,9 @@ pub fn select_next_tab(store: &mut Store) -> Result<(), Error> { tabs.select_next(); Ok(()) } + +pub fn toogle_info(store: &mut Store) -> Result<(), Error> { + let info = store.get_mut::("app.browser.info")?; + info.toggle(); + Ok(()) +} diff --git a/issue/src/tui/store.rs b/issue/src/tui/store.rs new file mode 100644 index 00000000..7ba7f367 --- /dev/null +++ b/issue/src/tui/store.rs @@ -0,0 +1,18 @@ +#[derive(Default)] +pub struct ToogleProperty { + state: bool, +} + +impl ToogleProperty { + pub fn new(state: bool) -> Self { + Self { state } + } + + pub fn toggle(&mut self) { + self.state = !self.state + } + + pub fn is_on(&self) -> bool { + self.state + } +} diff --git a/issue/src/tui/window.rs b/issue/src/tui/window.rs index 2aec18ee..5ec3dd2b 100644 --- a/issue/src/tui/window.rs +++ b/issue/src/tui/window.rs @@ -8,7 +8,7 @@ use tui::backend::Backend; use tui::layout::{Alignment, Direction, Rect}; use tui::style::{Modifier, Style}; use tui::text::{Span, Spans}; -use tui::widgets::{ListItem, Tabs}; +use tui::widgets::{ListItem, Paragraph, Tabs}; use tui::Frame; use radicle_common::cobs::issue::{Issue, IssueId}; @@ -23,6 +23,7 @@ use term::tui::theme::Theme; use term::tui::window::Widget; use super::state::Tab; +use super::store::ToogleProperty; type IssueList = ListProperty<(IssueId, Issue)>; type TabList = TabProperty; @@ -30,6 +31,7 @@ type TabList = TabProperty; #[derive(Clone)] pub struct BrowserWidget { pub tabs: Rc>, + pub info: Rc>, } impl BrowserWidget @@ -93,13 +95,18 @@ where .collect(); let tab_h = self.tabs.height(inner); - let heights = vec![tab_h, inner.height.saturating_sub(tab_h)]; + let info_h = self.info.height(inner); + let area_h = inner.height.saturating_sub(tab_h + info_h); + let heights = vec![tab_h, area_h, info_h]; let areas = layout::split_area(inner, heights, Direction::Vertical); + // Render widgets self.tabs.draw(store, frame, areas[0], theme)?; let (list, mut state) = template::list(items, issues.items().selected_index(), theme); frame.render_stateful_widget(list, areas[1], &mut state); + + self.info.draw(store, frame, areas[2], theme)?; } else { let message = String::from("No issues found"); let message = @@ -159,3 +166,70 @@ where 3_u16 } } + +#[derive(Clone)] +pub struct InfoWidget; + +impl Widget for InfoWidget +where + B: Backend, +{ + fn draw( + &self, + store: &Store, + frame: &mut Frame, + area: Rect, + theme: &Theme, + ) -> Result<(), Error> { + let info = store.get::("app.browser.info")?; + if info.is_on() { + let title = String::from("Issue"); + let tabs = store.get::("app.browser.tabs")?; + let issues = match tabs.items().selected() { + Some(Tab::Open) => store.get::("project.issues.open")?, + Some(Tab::Closed) => store.get::("project.issues.closed")?, + None => store.get::("project.issues.open")?, + }; + + let (block, _) = template::block(theme, area, Padding { top: 0, left: 0 }, false); + frame.render_widget(block, area); + if let Some((id, issue)) = issues.items().selected() { + let author = issue.author().name(); + let id = format!(" {} ", id); + + let project_w = title.len() as u16 + 2; + let id_w = id.len() as u16; + let author_w = author.len() as u16 + 2; + let comments_w = issue.comments().len().to_string().len() as u16 + 2; + let title_w = area + .width + .checked_sub(project_w + id_w + comments_w + author_w) + .unwrap_or(0); + + let widths = vec![project_w, id_w, title_w, author_w, comments_w]; + let areas = layout::split_area(area, widths, Direction::Horizontal); + + let title = template::paragraph_styled(&title, theme.highlight_invert); + frame.render_widget(title, areas[0]); + + let id = Paragraph::new(vec![Spans::from(id)]).style(theme.bg_bright_primary); + frame.render_widget(id, areas[1]); + + let title = template::paragraph_styled(&issue.title, theme.bg_bright_ternary); + frame.render_widget(title, areas[2]); + + let author = template::paragraph_styled(&author, theme.bg_bright_primary); + frame.render_widget(author, areas[3]); + + let count = &issue.comments().len().to_string(); + let comments = template::paragraph(count, theme.bg_dark_secondary); + frame.render_widget(comments, areas[4]); + } + } + Ok(()) + } + + fn height(&self, _area: Rect) -> u16 { + 1_u16 + } +}