From 0caa9551a904a1ba675fbde70435de6fb0a176d6 Mon Sep 17 00:00:00 2001 From: Michael Gattozzi Date: Wed, 27 Nov 2019 17:57:29 -0500 Subject: Add a tui for ticket This commit sets up a basic tui for the current functionality. It's traversable by keyboard and by mouse and shows the ticket state via tab, info in a row, and the description in it's own box when selected. This is necessary for a good user experience for in repo tools. Files are fine, but interactivity is better. --- Cargo.lock | 76 ++++++++++++ ticket/Cargo.toml | 3 + ticket/src/actions.rs | 36 ++++++ ticket/src/main.rs | 42 ++++--- ticket/src/tui.rs | 336 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 477 insertions(+), 16 deletions(-) create mode 100644 ticket/src/actions.rs create mode 100644 ticket/src/tui.rs diff --git a/Cargo.lock b/Cargo.lock index e978942..219a070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,11 @@ dependencies = [ "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "cc" version = "1.0.47" @@ -255,6 +260,11 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -380,6 +390,14 @@ dependencies = [ "unicode-normalization 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "itoa" version = "0.4.4" @@ -498,6 +516,11 @@ dependencies = [ "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "openssl-probe" version = "0.1.2" @@ -694,6 +717,14 @@ name = "redox_syscall" version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "redox_users" version = "0.3.1" @@ -873,6 +904,17 @@ dependencies = [ "wincolor 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "termion" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termios" version = "0.3.1" @@ -910,7 +952,10 @@ dependencies = [ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "shared 0.1.0", "structopt 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tui 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -936,6 +981,21 @@ name = "treeline" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "tui" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cassowary 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -982,6 +1042,14 @@ name = "utf8parse" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "uuid" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vcpkg" version = "0.2.7" @@ -1063,6 +1131,7 @@ dependencies = [ "checksum blake2b_simd 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b83b7baab1e671718d78204225800d6b170e648188ac7dc992e9d6bddf87d0c0" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" +"checksum cassowary 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" "checksum cc 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)" = "aa87058dce70a3ff5621797f1506cb837edd02ac4c0ae642b4542dce802908b8" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" @@ -1077,6 +1146,7 @@ dependencies = [ "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" "checksum dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" "checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" +"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" "checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" "checksum escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19db1f7e74438642a5018cdf263bb1325b2e792f02dd0a3ca6d6c0f0d7b1d5a5" @@ -1089,6 +1159,7 @@ dependencies = [ "checksum hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "307c3c9f937f38e3534b1d6447ecf090cafcc9744e4a6360e8b037b2cf5af120" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +"checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum jobserver 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f2b1d42ef453b30b7387e113da1c83ab1605d90c5b4e0eb8e96d016ed3b8c160" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" @@ -1103,6 +1174,7 @@ dependencies = [ "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4" "checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72" +"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)" = "465d16ae7fc0e313318f7de5cecf57b2fbe7511fd213978b457e1c96ff46736f" "checksum paw 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "09c0fc9b564dbc3dc2ed7c92c0c144f4de340aa94514ce2b446065417c4084e9" @@ -1128,6 +1200,7 @@ dependencies = [ "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d" "checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" "checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" @@ -1147,12 +1220,14 @@ dependencies = [ "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" +"checksum termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330" "checksum termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "01d1404644c8b12b16bfcffa4322403a91a451584daaaa7c28d3152e6cbc98cf" "checksum treeline 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" +"checksum tui 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0606ff286997171664d4f5ace6b130dd6ba1b867e6a27433077f618807aedc3b" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b561e267b2326bb4cebfc0ef9e68355c7abe6c6f522aeac2f5bf95d56c59bdcf" "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" @@ -1160,6 +1235,7 @@ dependencies = [ "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75b414f6c464c879d7f9babf951f23bc3743fb7313c081b2e6ca719067ea9d61" "checksum utf8parse 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" +"checksum uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" "checksum vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/ticket/Cargo.toml b/ticket/Cargo.toml index 1c5c9eb..6c752b5 100644 --- a/ticket/Cargo.toml +++ b/ticket/Cargo.toml @@ -15,5 +15,8 @@ serde = { version = "1.0", features = ["derive"] } shared = { path = "../shared" } structopt = { version = "0.3", features = ["paw"] } toml = "0.5" +uuid = { version = "0.8", features = ["serde", "v1"] } log = "0.4" pretty_env_logger = "0.3" +tui = "0.7" +termion = "1.5" diff --git a/ticket/src/actions.rs b/ticket/src/actions.rs new file mode 100644 index 0000000..381c9f7 --- /dev/null +++ b/ticket/src/actions.rs @@ -0,0 +1,36 @@ +use crate::Ticket; +use anyhow::Result; +use log::*; +use shared::find_root; +use std::{ + fs, + path::PathBuf, +}; + +pub fn get_open_tickets() -> Result> { + get_tickets(ticket_root()?.join("open")) +} + +pub fn get_closed_tickets() -> Result> { + get_tickets(ticket_root()?.join("closed")) +} + +fn get_tickets(path: PathBuf) -> Result> { + let mut out = Vec::new(); + debug!("Looking for ticket."); + for entry in fs::read_dir(&path)? { + let entry = entry?; + let path = entry.path(); + trace!("Looking at entry {}.", path.display()); + if path.is_file() { + trace!("Entry is a file."); + out.push(toml::from_slice::(&fs::read(&path)?)?); + } + } + out.sort_by(|a, b| a.number.cmp(&b.number)); + Ok(out) +} + +pub fn ticket_root() -> Result { + Ok(find_root()?.join(".dev-suite").join("ticket")) +} diff --git a/ticket/src/main.rs b/ticket/src/main.rs index 7e05adf..4d5457d 100644 --- a/ticket/src/main.rs +++ b/ticket/src/main.rs @@ -1,3 +1,7 @@ +mod actions; +mod tui; + +use actions::*; use anyhow::{ bail, Result, @@ -12,17 +16,21 @@ use serde::{ Deserialize, Serialize, }; -use shared::find_root; use std::{ env, fs, - path::PathBuf, process, process::Command, }; #[derive(structopt::StructOpt)] -enum Args { +struct Args { + #[structopt(subcommand)] + cmd: Option, +} + +#[derive(structopt::StructOpt)] +enum Cmd { /// Initialize the repo to use ticket Init, New, @@ -40,19 +48,25 @@ fn main(args: Args) { env::set_var("RUST_LOG", "info"); }); pretty_env_logger::init(); - if let Err(e) = match args { - Args::Init => init(), - Args::New => new(), - Args::Show { id } => show(id), - Args::Close { id } => close(id), - } { + + if let Some(cmd) = args.cmd { + if let Err(e) = match cmd { + Cmd::Init => init(), + Cmd::New => new(), + Cmd::Show { id } => show(id), + Cmd::Close { id } => close(id), + } { + error!("{}", e); + std::process::exit(1); + } + } else if let Err(e) = tui::run() { error!("{}", e); std::process::exit(1); } } fn init() -> Result<()> { - let root = find_root()?.join(".dev-suite").join("ticket"); + let root = ticket_root()?; debug!("Creating ticket directory at {}.", root.display()); debug!("Creating open directory."); fs::create_dir_all(&root.join("open"))?; @@ -141,10 +155,6 @@ fn new() -> Result<()> { Ok(()) } -fn ticket_root() -> Result { - Ok(find_root()?.join(".dev-suite").join("ticket")) -} - fn show(id: usize) -> Result<()> { debug!("Getting ticket root."); let ticket_root = ticket_root()?; @@ -236,7 +246,7 @@ fn close(id: usize) -> Result<()> { } #[derive(Serialize, Deserialize)] -struct Ticket { +pub struct Ticket { title: String, status: Status, number: usize, @@ -245,7 +255,7 @@ struct Ticket { } #[derive(Serialize, Deserialize)] -enum Status { +pub enum Status { Open, Closed, } diff --git a/ticket/src/tui.rs b/ticket/src/tui.rs new file mode 100644 index 0000000..90dbeb6 --- /dev/null +++ b/ticket/src/tui.rs @@ -0,0 +1,336 @@ +use crate::{ + actions::{ + get_closed_tickets, + get_open_tickets, + }, + Status, + Ticket, +}; +use anyhow::Result; +use std::{ + collections::BTreeMap, + io, + sync::mpsc, + thread, + time::Duration, +}; +use termion::{ + event::Key, + input::{ + MouseTerminal, + TermRead, + }, + raw::IntoRawMode, + screen::AlternateScreen, +}; +use tui::{ + backend::TermionBackend, + layout::{ + Alignment, + Constraint, + Direction, + Layout, + }, + style::{ + Color, + Modifier, + Style, + }, + widgets::{ + Block, + Borders, + Paragraph, + Row, + Table, + Tabs, + Text, + Widget, + }, + Terminal, +}; + +pub struct TabsState<'a> { + pub titles: Vec<&'a str>, + pub index: usize, +} + +impl<'a> TabsState<'a> { + pub fn new(titles: Vec<&'a str>) -> TabsState { + TabsState { titles, index: 0 } + } + + pub fn next(&mut self) { + self.index = (self.index + 1) % self.titles.len(); + } + + pub fn previous(&mut self) { + if self.index > 0 { + self.index -= 1; + } else { + self.index = self.titles.len() - 1; + } + } +} +pub enum Event { + Input(I), + Tick, +} + +pub struct TicketState { + pub tickets: BTreeMap>, + pub index: usize, + pub status: Status, +} + +impl TicketState { + pub fn new(tickets: BTreeMap>) -> Self { + Self { + tickets, + index: 0, + status: Status::Open, + } + } + + fn len(&self) -> usize { + match self.status { + Status::Open => self.tickets.get("Open").unwrap().len(), + Status::Closed => self.tickets.get("Closed").unwrap().len(), + } + } + + pub fn next(&mut self) { + self.index = (self.index + 1) % self.len(); + } + + pub fn previous(&mut self) { + if self.index > 0 { + self.index -= 1; + } else { + self.index = self.len() - 1; + } + } +} +/// A small event handler that wrap termion input and tick events. Each event +/// type is handled in its own thread and returned to a common `Receiver` +#[allow(dead_code)] +pub struct Events { + rx: mpsc::Receiver>, + input_handle: thread::JoinHandle<()>, + tick_handle: thread::JoinHandle<()>, +} + +struct App<'a> { + tabs: TabsState<'a>, + tickets: TicketState, +} +#[derive(Debug, Clone, Copy)] +pub struct Config { + pub exit_key: Key, + pub tick_rate: Duration, +} + +impl Default for Config { + fn default() -> Config { + Config { + exit_key: Key::Char('q'), + tick_rate: Duration::from_millis(250), + } + } +} + +impl Events { + pub fn new() -> Events { + Events::with_config(Config::default()) + } + + pub fn with_config(config: Config) -> Events { + let (tx, rx) = mpsc::channel(); + let input_handle = { + let tx = tx.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + if let Ok(key) = evt { + if tx.send(Event::Input(key)).is_err() { + return; + } + if key == config.exit_key { + return; + } + } + } + }) + }; + let tick_handle = { + let tx = tx.clone(); + thread::spawn(move || { + let tx = tx.clone(); + loop { + tx.send(Event::Tick).unwrap(); + thread::sleep(config.tick_rate); + } + }) + }; + Events { + rx, + input_handle, + tick_handle, + } + } + + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } +} +pub fn run() -> Result<()> { + // Terminal initialization + let stdout = io::stdout().into_raw_mode()?; + let stdout = MouseTerminal::from(stdout); + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor()?; + + let events = Events::new(); + + // App + let mut app = App { + tabs: TabsState::new(vec!["Open", "Closed"]), + tickets: { + let mut map = BTreeMap::new(); + map.insert("Open".into(), get_open_tickets()?); + map.insert("Closed".into(), get_closed_tickets()?); + TicketState::new(map) + }, + }; + + // Main loop + loop { + terminal.draw(|mut f| { + let size = f.size(); + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(size); + let horizontal = Layout::default() + .direction(Direction::Horizontal) + .vertical_margin(3) + .constraints( + [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref(), + ) + .split(size); + + Tabs::default() + .block(Block::default().borders(Borders::ALL).title("Status")) + .titles(&app.tabs.titles) + .select(app.tabs.index) + .style(Style::default().fg(Color::Cyan)) + .highlight_style(Style::default().fg(Color::Yellow)) + .render(&mut f, vertical[0]); + + match app.tabs.index { + 0 => { + app.table("Open").render(&mut f, horizontal[0]); + + Paragraph::new(app.description("Open").iter()) + .block(Block::default().title("Description").borders(Borders::ALL)) + .alignment(Alignment::Left) + .wrap(true) + .render(&mut f, horizontal[1]); + } + 1 => { + app.table("Closed").render(&mut f, horizontal[0]); + + Paragraph::new(app.description("Closed").iter()) + .block(Block::default().title("Description").borders(Borders::ALL)) + .alignment(Alignment::Left) + .wrap(true) + .render(&mut f, horizontal[1]); + } + _ => {} + } + })?; + + match events.next()? { + Event::Input(input) => match input { + Key::Char('q') => { + break; + } + Key::Right => { + if app.tabs.index == 0 { + app.tickets.status = Status::Closed; + app.tickets.index = 0; + } + app.tabs.next(); + } + Key::Left => { + if app.tabs.index != 0 { + app.tickets.status = Status::Open; + app.tickets.index = 0; + } + app.tabs.previous(); + } + Key::Up => app.tickets.previous(), + Key::Down => app.tickets.next(), + _ => {} + }, + Event::Tick => continue, + } + } + Ok(()) +} + +impl<'a> App<'a> { + fn table(&self, tab: &'a str) -> impl Widget + '_ { + Table::new( + ["Id", "Title", "Assignee"].iter(), + self + .tickets + .tickets + .get(tab) + .unwrap() + .iter() + .enumerate() + .map(move |(idx, i)| { + let data = vec![ + i.number.to_string(), + i.title.to_string(), + i.assignee + .as_ref() + .cloned() + .unwrap_or_else(|| "None".into()), + ] + .into_iter(); + let normal_style = Style::default().fg(Color::Yellow); + let selected_style = + Style::default().fg(Color::White).modifier(Modifier::BOLD); + if idx == self.tickets.index { + Row::StyledData(data, selected_style) + } else { + Row::StyledData(data, normal_style) + } + }), + ) + .block(Block::default().title(tab).borders(Borders::ALL)) + .header_style(Style::default().fg(Color::Yellow)) + .widths(&[ + Constraint::Percentage(10), + Constraint::Percentage(70), + Constraint::Percentage(20), + ]) + .style(Style::default().fg(Color::White)) + .column_spacing(1) + } + + fn description(&self, tab: &'a str) -> Vec { + let mut description = vec![]; + for (idx, i) in self.tickets.tickets.get(tab).unwrap().iter().enumerate() { + if idx == self.tickets.index { + description = vec![Text::raw(i.description.to_owned())]; + break; + } + } + + description + } +} -- cgit v1.2.3