aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Gattozzi <mgattozzi@gmail.com>2019-11-27 17:57:29 -0500
committerMichael Gattozzi <mgattozzi@gmail.com>2019-12-02 15:06:03 -0500
commit0caa9551a904a1ba675fbde70435de6fb0a176d6 (patch)
tree1b6224f031e13e20358f00081ef1ec91f8b118a9
parentfc072f0656ceb99994f1217325aa11f932881d55 (diff)
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.
-rw-r--r--Cargo.lock76
-rw-r--r--ticket/Cargo.toml3
-rw-r--r--ticket/src/actions.rs36
-rw-r--r--ticket/src/main.rs42
-rw-r--r--ticket/src/tui.rs336
5 files changed, 477 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e978942..219a070 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -113,6 +113,11 @@ dependencies = [
]
[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "cc"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -256,6 +261,11 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -381,6 +391,14 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -499,6 +517,11 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -695,6 +718,14 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -874,6 +905,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -937,6 +982,21 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -983,6 +1043,14 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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<Vec<Ticket>> {
+ get_tickets(ticket_root()?.join("open"))
+}
+
+pub fn get_closed_tickets() -> Result<Vec<Ticket>> {
+ get_tickets(ticket_root()?.join("closed"))
+}
+
+fn get_tickets(path: PathBuf) -> Result<Vec<Ticket>> {
+ 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::<Ticket>(&fs::read(&path)?)?);
+ }
+ }
+ out.sort_by(|a, b| a.number.cmp(&b.number));
+ Ok(out)
+}
+
+pub fn ticket_root() -> Result<PathBuf> {
+ 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<Cmd>,
+}
+
+#[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<PathBuf> {
- 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<I> {
+ Input(I),
+ Tick,
+}
+
+pub struct TicketState {
+ pub tickets: BTreeMap<String, Vec<Ticket>>,
+ pub index: usize,
+ pub status: Status,
+}
+
+impl TicketState {
+ pub fn new(tickets: BTreeMap<String, Vec<Ticket>>) -> 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<Event<Key>>,
+ 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<Event<Key>, 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<Text> {
+ 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
+ }
+}