Remove `radicle-tui` crate
This crate is now in its own repository: rid:z39mP9rQAaGmERfUMPULfPUi473tY
This commit is contained in:
parent
b456d3a401
commit
1b9906db09
|
|
@ -38,17 +38,6 @@ dependencies = [
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.10",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
@ -420,12 +409,6 @@ version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cassowary"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.79"
|
version = "1.0.79"
|
||||||
|
|
@ -1053,9 +1036,6 @@ name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
dependencies = [
|
|
||||||
"ahash 0.7.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
|
|
@ -1063,7 +1043,7 @@ version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.3",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1307,15 +1287,6 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "isolang"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f80f221db1bc708b71128757b9396727c04de86968081e18e89b0575e03be071"
|
|
||||||
dependencies = [
|
|
||||||
"phf",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
|
|
@ -1360,29 +1331,6 @@ dependencies = [
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy-regex"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ff63c423c68ea6814b7da9e88ce585f793c87ddd9e78f646970891769c8235d4"
|
|
||||||
dependencies = [
|
|
||||||
"lazy-regex-proc_macros",
|
|
||||||
"once_cell",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy-regex-proc_macros"
|
|
||||||
version = "2.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"regex",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -1736,24 +1684,6 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf"
|
|
||||||
version = "0.11.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
|
||||||
dependencies = [
|
|
||||||
"phf_shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_shared"
|
|
||||||
version = "0.11.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
|
||||||
dependencies = [
|
|
||||||
"siphasher",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -1969,7 +1899,7 @@ dependencies = [
|
||||||
"similar",
|
"similar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"timeago 0.3.1",
|
"timeago",
|
||||||
"ureq",
|
"ureq",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
@ -2202,21 +2132,6 @@ dependencies = [
|
||||||
"radicle-git-ext",
|
"radicle-git-ext",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "radicle-tui"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"lexopt",
|
|
||||||
"radicle",
|
|
||||||
"radicle-cli",
|
|
||||||
"radicle-surf",
|
|
||||||
"radicle-term",
|
|
||||||
"timeago 0.4.1",
|
|
||||||
"tui-realm-stdlib",
|
|
||||||
"tuirealm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
|
@ -2634,12 +2549,6 @@ version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smawk"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snapbox"
|
name = "snapbox"
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
|
|
@ -2853,17 +2762,6 @@ dependencies = [
|
||||||
"redox_termios",
|
"redox_termios",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "textwrap"
|
|
||||||
version = "0.15.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
|
|
||||||
dependencies = [
|
|
||||||
"smawk",
|
|
||||||
"unicode-linebreak",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.43"
|
version = "1.0.43"
|
||||||
|
|
@ -2938,16 +2836,6 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26"
|
checksum = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "timeago"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5082dc942361cdfb74eab98bf995762d6015e5bb3a20bf7c5c71213778b4fcb4"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"isolang",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
@ -3134,55 +3022,6 @@ version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tui"
|
|
||||||
version = "0.19.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"cassowary",
|
|
||||||
"termion 1.5.6",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tui-realm-stdlib"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "66f252bf8b07c6fd708ddd6349b5f044ae5b488b26929c745728d9c7e2cebfa6"
|
|
||||||
dependencies = [
|
|
||||||
"textwrap",
|
|
||||||
"tuirealm",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tuirealm"
|
|
||||||
version = "1.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "265411b5606f400459af94fbc5aae6a7bc0e98094d08cb5868390c932be88e26"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"lazy-regex",
|
|
||||||
"termion 1.5.6",
|
|
||||||
"thiserror",
|
|
||||||
"tui",
|
|
||||||
"tuirealm_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tuirealm_derive"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e0adcdaf59881626555558eae08f8a53003c8a1961723b4d7a10c51599abbc81"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
|
@ -3201,16 +3040,6 @@ version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
|
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-linebreak"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown 0.12.3",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
version = "0.1.22"
|
version = "0.1.22"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ members = [
|
||||||
"radicle-remote-helper",
|
"radicle-remote-helper",
|
||||||
"radicle-ssh",
|
"radicle-ssh",
|
||||||
"radicle-tools",
|
"radicle-tools",
|
||||||
"radicle-tui",
|
|
||||||
]
|
]
|
||||||
default-members = [
|
default-members = [
|
||||||
"radicle",
|
"radicle",
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "radicle-tui"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Erik Kundt <erik@zirkular.io>"]
|
|
||||||
edition = "2021"
|
|
||||||
build = "../build.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "radicle-tui"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = { version = "1" }
|
|
||||||
lexopt = { version = "0.2" }
|
|
||||||
radicle-surf = { version = "0.14.0" }
|
|
||||||
timeago = { version = "0.4.1" }
|
|
||||||
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
|
|
||||||
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }
|
|
||||||
|
|
||||||
[dependencies.radicle]
|
|
||||||
version = "0"
|
|
||||||
path = "../radicle"
|
|
||||||
|
|
||||||
[dependencies.radicle-cli]
|
|
||||||
version = "0"
|
|
||||||
path = "../radicle-cli"
|
|
||||||
|
|
||||||
[dependencies.radicle-term]
|
|
||||||
version = "0"
|
|
||||||
path = "../radicle-term"
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
pub mod event;
|
|
||||||
pub mod page;
|
|
||||||
pub mod subscription;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use radicle::cob::issue::IssueId;
|
|
||||||
use radicle::cob::patch::PatchId;
|
|
||||||
use radicle::identity::{Id, Project};
|
|
||||||
use radicle::profile::Profile;
|
|
||||||
|
|
||||||
use tuirealm::application::PollStrategy;
|
|
||||||
use tuirealm::{Application, Frame, NoUserEvent};
|
|
||||||
|
|
||||||
use radicle_tui::ui::context::Context;
|
|
||||||
use radicle_tui::ui::theme::{self, Theme};
|
|
||||||
use radicle_tui::Tui;
|
|
||||||
use radicle_tui::{cob, ui};
|
|
||||||
|
|
||||||
use page::{HomeView, PatchView};
|
|
||||||
|
|
||||||
use self::page::{IssuePage, PageStack};
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
|
||||||
pub enum HomeCid {
|
|
||||||
Header,
|
|
||||||
Dashboard,
|
|
||||||
IssueBrowser,
|
|
||||||
PatchBrowser,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
|
||||||
pub enum PatchCid {
|
|
||||||
Header,
|
|
||||||
Activity,
|
|
||||||
Files,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
|
||||||
pub enum IssueCid {
|
|
||||||
Header,
|
|
||||||
List,
|
|
||||||
Details,
|
|
||||||
Shortcuts,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All component ids known to this application.
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
|
||||||
pub enum Cid {
|
|
||||||
Home(HomeCid),
|
|
||||||
Issue(IssueCid),
|
|
||||||
Patch(PatchCid),
|
|
||||||
GlobalListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Messages handled by this application.
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum HomeMessage {}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum IssueMessage {
|
|
||||||
Show(IssueId),
|
|
||||||
Changed(IssueId),
|
|
||||||
Focus(IssueCid),
|
|
||||||
Leave,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum PatchMessage {
|
|
||||||
Show(PatchId),
|
|
||||||
Leave,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum Message {
|
|
||||||
Home(HomeMessage),
|
|
||||||
Issue(IssueMessage),
|
|
||||||
Patch(PatchMessage),
|
|
||||||
NavigationChanged(u16),
|
|
||||||
Tick,
|
|
||||||
Quit,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct App {
|
|
||||||
context: Context,
|
|
||||||
pages: PageStack,
|
|
||||||
theme: Theme,
|
|
||||||
quit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new application using a tui-realm-application, mounts all
|
|
||||||
/// components and sets focus to a default one.
|
|
||||||
impl App {
|
|
||||||
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
|
||||||
Self {
|
|
||||||
context: Context::new(profile, id, project),
|
|
||||||
pages: PageStack::default(),
|
|
||||||
theme: theme::default_dark(),
|
|
||||||
quit: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_home(
|
|
||||||
&mut self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()> {
|
|
||||||
let home = Box::<HomeView>::default();
|
|
||||||
self.pages.push(home, app, &self.context, theme)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_patch(
|
|
||||||
&mut self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
id: PatchId,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()> {
|
|
||||||
let repo = self.context.repository();
|
|
||||||
|
|
||||||
if let Some(patch) = cob::patch::find(repo, &id)? {
|
|
||||||
let view = Box::new(PatchView::new((id, patch)));
|
|
||||||
self.pages.push(view, app, &self.context, theme)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Could not mount 'page::PatchView'. Patch not found."
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_issue(
|
|
||||||
&mut self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
id: IssueId,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()> {
|
|
||||||
let repo = self.context.repository();
|
|
||||||
|
|
||||||
if let Some(issue) = cob::issue::find(repo, &id)? {
|
|
||||||
let view = Box::new(IssuePage::new((id, issue)));
|
|
||||||
self.pages.push(view, app, &self.context, theme)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Could not mount 'page::IssueView'. Issue not found."
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tui<Cid, Message> for App {
|
|
||||||
fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
self.view_home(app, &self.theme.clone())?;
|
|
||||||
|
|
||||||
// Add global key listener and subscribe to key events
|
|
||||||
let global = ui::widget::common::global_listener().to_boxed();
|
|
||||||
app.mount(Cid::GlobalListener, global, subscription::global())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
|
||||||
if let Ok(page) = self.pages.peek_mut() {
|
|
||||||
page.view(app, frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<bool> {
|
|
||||||
match app.tick(PollStrategy::Once) {
|
|
||||||
Ok(messages) if !messages.is_empty() => {
|
|
||||||
let theme = theme::default_dark();
|
|
||||||
for message in messages {
|
|
||||||
match message {
|
|
||||||
Message::Issue(IssueMessage::Show(id)) => {
|
|
||||||
self.view_issue(app, id, &theme)?;
|
|
||||||
}
|
|
||||||
Message::Issue(IssueMessage::Leave) => {
|
|
||||||
self.pages.pop(app)?;
|
|
||||||
}
|
|
||||||
Message::Patch(PatchMessage::Show(id)) => {
|
|
||||||
self.view_patch(app, id, &theme)?;
|
|
||||||
}
|
|
||||||
Message::Patch(PatchMessage::Leave) => {
|
|
||||||
self.pages.pop(app)?;
|
|
||||||
}
|
|
||||||
Message::Quit => self.quit = true,
|
|
||||||
_ => {
|
|
||||||
self.pages
|
|
||||||
.peek_mut()?
|
|
||||||
.update(app, &self.context, &theme, message)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
_ => Ok(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn quit(&self) -> bool {
|
|
||||||
self.quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
|
|
||||||
use tuirealm::event::{Event, Key, KeyEvent};
|
|
||||||
use tuirealm::{MockComponent, NoUserEvent, State, StateValue};
|
|
||||||
|
|
||||||
use radicle_tui::ui::widget::common::container::{AppHeader, GlobalListener, LabeledContainer};
|
|
||||||
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
|
|
||||||
use radicle_tui::ui::widget::common::list::PropertyList;
|
|
||||||
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
|
|
||||||
use radicle_tui::ui::widget::{issue, patch};
|
|
||||||
|
|
||||||
use radicle_tui::ui::widget::Widget;
|
|
||||||
|
|
||||||
use super::{IssueMessage, Message, PatchMessage};
|
|
||||||
|
|
||||||
/// Since the framework does not know the type of messages that are being
|
|
||||||
/// passed around in the app, the following handlers need to be implemented for
|
|
||||||
/// each component used.
|
|
||||||
///
|
|
||||||
/// TODO: should handle `Event::WindowResize`, which is not emitted by `termion`.
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
match event {
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Char('q'),
|
|
||||||
..
|
|
||||||
}) => Some(Message::Quit),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<AppHeader> {
|
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
match event {
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
|
||||||
match self.perform(Cmd::Move(MoveDirection::Right)) {
|
|
||||||
CmdResult::Changed(State::One(StateValue::U16(index))) => {
|
|
||||||
Some(Message::NavigationChanged(index))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::LargeList> {
|
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
match event {
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
|
||||||
Some(Message::Issue(IssueMessage::Leave))
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
|
||||||
let result = self.perform(Cmd::Move(MoveDirection::Up));
|
|
||||||
match result {
|
|
||||||
CmdResult::Changed(State::One(StateValue::Usize(selected))) => {
|
|
||||||
let item = self.items().get(selected)?;
|
|
||||||
Some(Message::Issue(IssueMessage::Changed(item.id().to_owned())))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Down, ..
|
|
||||||
}) => {
|
|
||||||
let result = self.perform(Cmd::Move(MoveDirection::Down));
|
|
||||||
match result {
|
|
||||||
CmdResult::Changed(State::One(StateValue::Usize(selected))) => {
|
|
||||||
let item = self.items().get(selected)?;
|
|
||||||
Some(Message::Issue(IssueMessage::Changed(item.id().to_owned())))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::Details> {
|
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchBrowser> {
|
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
match event {
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
|
||||||
self.perform(Cmd::Move(MoveDirection::Up));
|
|
||||||
Some(Message::Tick)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Down, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Move(MoveDirection::Down));
|
|
||||||
Some(Message::Tick)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Enter, ..
|
|
||||||
}) => {
|
|
||||||
let result = self.perform(Cmd::Submit);
|
|
||||||
match result {
|
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(selected))) => {
|
|
||||||
let item = self.items().get(selected)?;
|
|
||||||
Some(Message::Patch(PatchMessage::Show(item.id().to_owned())))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<IssueBrowser> {
|
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
match event {
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
|
||||||
self.perform(Cmd::Move(MoveDirection::Up));
|
|
||||||
Some(Message::Tick)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Down, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Move(MoveDirection::Down));
|
|
||||||
Some(Message::Tick)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Enter, ..
|
|
||||||
}) => {
|
|
||||||
let result = self.perform(Cmd::Submit);
|
|
||||||
match result {
|
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(selected))) => {
|
|
||||||
let item = self.items().get(selected)?;
|
|
||||||
Some(Message::Issue(IssueMessage::Show(item.id().to_owned())))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<Dashboard> {
|
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<patch::Activity> {
|
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
match event {
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
|
||||||
Some(Message::Patch(PatchMessage::Leave))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<patch::Files> {
|
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
match event {
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
|
||||||
Some(Message::Patch(PatchMessage::Leave))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<LabeledContainer> {
|
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<PropertyList> {
|
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<ContextBar> {
|
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<Shortcuts> {
|
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,403 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use radicle::cob::issue::{Issue, IssueId};
|
|
||||||
use radicle::cob::patch::{Patch, PatchId};
|
|
||||||
|
|
||||||
use radicle_tui::cob;
|
|
||||||
use tuirealm::{Frame, NoUserEvent, Sub, SubClause};
|
|
||||||
|
|
||||||
use radicle_tui::ui::context::Context;
|
|
||||||
use radicle_tui::ui::layout;
|
|
||||||
use radicle_tui::ui::theme::Theme;
|
|
||||||
use radicle_tui::ui::widget;
|
|
||||||
|
|
||||||
use super::{subscription, Application, Cid, HomeCid, IssueCid, IssueMessage, Message, PatchCid};
|
|
||||||
|
|
||||||
/// `tuirealm`'s event and prop system is designed to work with flat component hierarchies.
|
|
||||||
/// Building deep nested component hierarchies would need a lot more additional effort to
|
|
||||||
/// properly pass events and props down these hierarchies. This makes it hard to implement
|
|
||||||
/// full app views (home, patch details etc) as components.
|
|
||||||
///
|
|
||||||
/// View pages take into account these flat component hierarchies, and provide
|
|
||||||
/// switchable sets of components.
|
|
||||||
pub trait ViewPage {
|
|
||||||
/// Will be called whenever a view page is pushed onto the page stack. Should create and mount all widgets.
|
|
||||||
fn mount(
|
|
||||||
&self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
/// Will be called whenever a view page is popped from the page stack. Should unmount all widgets.
|
|
||||||
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()>;
|
|
||||||
|
|
||||||
/// Will be called whenever a view page is on top of the stack and can be used to update its internal
|
|
||||||
/// state depending on the message passed.
|
|
||||||
fn update(
|
|
||||||
&mut self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
message: Message,
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
/// Will be called whenever a view page is on top of the page stack and needs to be rendered.
|
|
||||||
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame);
|
|
||||||
|
|
||||||
/// Will be called whenever this view page is pushed to the stack, or it is on top of the stack again
|
|
||||||
/// after another view page was popped from the stack.
|
|
||||||
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()>;
|
|
||||||
|
|
||||||
/// Will be called whenever this view page is on top of the stack and another view page is pushed
|
|
||||||
/// to the stack, or if this is popped from the stack.
|
|
||||||
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Home
|
|
||||||
///
|
|
||||||
pub struct HomeView {
|
|
||||||
active_component: Cid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for HomeView {
|
|
||||||
fn default() -> Self {
|
|
||||||
HomeView {
|
|
||||||
active_component: Cid::Home(HomeCid::Dashboard),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ViewPage for HomeView {
|
|
||||||
fn mount(
|
|
||||||
&self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()> {
|
|
||||||
let navigation = widget::home::navigation(theme);
|
|
||||||
let header = widget::common::app_header(context, theme, Some(navigation)).to_boxed();
|
|
||||||
|
|
||||||
let dashboard = widget::home::dashboard(context, theme).to_boxed();
|
|
||||||
let issue_browser = widget::home::issues(context, theme).to_boxed();
|
|
||||||
let patch_browser = widget::home::patches(context, theme).to_boxed();
|
|
||||||
|
|
||||||
app.remount(Cid::Home(HomeCid::Header), header, vec![])?;
|
|
||||||
|
|
||||||
app.remount(Cid::Home(HomeCid::Dashboard), dashboard, vec![])?;
|
|
||||||
app.remount(Cid::Home(HomeCid::IssueBrowser), issue_browser, vec![])?;
|
|
||||||
app.remount(Cid::Home(HomeCid::PatchBrowser), patch_browser, vec![])?;
|
|
||||||
|
|
||||||
app.active(&self.active_component)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
app.umount(&Cid::Home(HomeCid::Header))?;
|
|
||||||
app.umount(&Cid::Home(HomeCid::Dashboard))?;
|
|
||||||
app.umount(&Cid::Home(HomeCid::IssueBrowser))?;
|
|
||||||
app.umount(&Cid::Home(HomeCid::PatchBrowser))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(
|
|
||||||
&mut self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
_context: &Context,
|
|
||||||
_theme: &Theme,
|
|
||||||
message: Message,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Message::NavigationChanged(index) = message {
|
|
||||||
self.active_component = Cid::Home(HomeCid::from(index as usize));
|
|
||||||
app.active(&self.active_component)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
|
||||||
let area = frame.size();
|
|
||||||
let layout = layout::default_page(area);
|
|
||||||
|
|
||||||
app.view(&Cid::Home(HomeCid::Header), frame, layout[0]);
|
|
||||||
app.view(&self.active_component, frame, layout[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
app.subscribe(
|
|
||||||
&Cid::Home(HomeCid::Header),
|
|
||||||
Sub::new(subscription::navigation_clause(), SubClause::Always),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
app.unsubscribe(
|
|
||||||
&Cid::Home(HomeCid::Header),
|
|
||||||
subscription::navigation_clause(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Issue detail page
|
|
||||||
///
|
|
||||||
pub struct IssuePage {
|
|
||||||
issue: (IssueId, Issue),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IssuePage {
|
|
||||||
pub fn new(issue: (IssueId, Issue)) -> Self {
|
|
||||||
IssuePage { issue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ViewPage for IssuePage {
|
|
||||||
fn mount(
|
|
||||||
&self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (id, issue) = &self.issue;
|
|
||||||
let header = widget::common::app_header(context, theme, None).to_boxed();
|
|
||||||
let list = widget::issue::list(context, theme, (*id, issue.clone())).to_boxed();
|
|
||||||
let details = widget::issue::details(context, theme, (*id, issue.clone())).to_boxed();
|
|
||||||
let shortcuts = widget::common::shortcuts(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
widget::common::shortcut(theme, "esc", "back"),
|
|
||||||
widget::common::shortcut(theme, "q", "quit"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.to_boxed();
|
|
||||||
|
|
||||||
app.remount(Cid::Issue(IssueCid::Header), header, vec![])?;
|
|
||||||
app.remount(Cid::Issue(IssueCid::List), list, vec![])?;
|
|
||||||
app.remount(Cid::Issue(IssueCid::Details), details, vec![])?;
|
|
||||||
app.remount(Cid::Issue(IssueCid::Shortcuts), shortcuts, vec![])?;
|
|
||||||
|
|
||||||
app.active(&Cid::Issue(IssueCid::List))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
app.umount(&Cid::Issue(IssueCid::Header))?;
|
|
||||||
app.umount(&Cid::Issue(IssueCid::List))?;
|
|
||||||
app.umount(&Cid::Issue(IssueCid::Details))?;
|
|
||||||
app.umount(&Cid::Issue(IssueCid::Shortcuts))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(
|
|
||||||
&mut self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
message: Message,
|
|
||||||
) -> Result<()> {
|
|
||||||
match message {
|
|
||||||
Message::Issue(IssueMessage::Changed(id)) => {
|
|
||||||
let repo = context.repository();
|
|
||||||
if let Some(issue) = cob::issue::find(repo, &id)? {
|
|
||||||
let details = widget::issue::details(context, theme, (id, issue)).to_boxed();
|
|
||||||
app.remount(Cid::Issue(IssueCid::Details), details, vec![])?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::Issue(IssueMessage::Focus(cid)) => {
|
|
||||||
app.active(&Cid::Issue(cid))?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
|
||||||
let area = frame.size();
|
|
||||||
let shortcuts_h = 1u16;
|
|
||||||
let layout = layout::issue_preview(area, shortcuts_h);
|
|
||||||
|
|
||||||
app.view(&Cid::Issue(IssueCid::Header), frame, layout.header);
|
|
||||||
app.view(&Cid::Issue(IssueCid::List), frame, layout.list);
|
|
||||||
app.view(&Cid::Issue(IssueCid::Details), frame, layout.details);
|
|
||||||
app.view(&Cid::Issue(IssueCid::Shortcuts), frame, layout.shortcuts);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe(&self, _app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unsubscribe(&self, _app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Patch detail page
|
|
||||||
///
|
|
||||||
pub struct PatchView {
|
|
||||||
active_component: Cid,
|
|
||||||
patch: (PatchId, Patch),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PatchView {
|
|
||||||
pub fn new(patch: (PatchId, Patch)) -> Self {
|
|
||||||
PatchView {
|
|
||||||
active_component: Cid::Patch(PatchCid::Activity),
|
|
||||||
patch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ViewPage for PatchView {
|
|
||||||
fn mount(
|
|
||||||
&self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (id, patch) = &self.patch;
|
|
||||||
let navigation = widget::patch::navigation(theme);
|
|
||||||
let header = widget::common::app_header(context, theme, Some(navigation)).to_boxed();
|
|
||||||
|
|
||||||
let activity = widget::patch::activity(theme, (*id, patch), context.profile()).to_boxed();
|
|
||||||
let files = widget::patch::files(theme, (*id, patch), context.profile()).to_boxed();
|
|
||||||
|
|
||||||
app.remount(Cid::Patch(PatchCid::Header), header, vec![])?;
|
|
||||||
app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?;
|
|
||||||
app.remount(Cid::Patch(PatchCid::Files), files, vec![])?;
|
|
||||||
|
|
||||||
app.active(&self.active_component)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
app.umount(&Cid::Patch(PatchCid::Header))?;
|
|
||||||
app.umount(&Cid::Patch(PatchCid::Activity))?;
|
|
||||||
app.umount(&Cid::Patch(PatchCid::Files))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(
|
|
||||||
&mut self,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
_context: &Context,
|
|
||||||
_theme: &Theme,
|
|
||||||
message: Message,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Message::NavigationChanged(index) = message {
|
|
||||||
self.active_component = Cid::Patch(PatchCid::from(index as usize));
|
|
||||||
}
|
|
||||||
app.active(&self.active_component)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
|
||||||
let area = frame.size();
|
|
||||||
let layout = layout::default_page(area);
|
|
||||||
|
|
||||||
app.view(&Cid::Patch(PatchCid::Header), frame, layout[0]);
|
|
||||||
app.view(&self.active_component, frame, layout[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
app.subscribe(
|
|
||||||
&Cid::Patch(PatchCid::Header),
|
|
||||||
Sub::new(subscription::navigation_clause(), SubClause::Always),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
app.unsubscribe(
|
|
||||||
&Cid::Patch(PatchCid::Header),
|
|
||||||
subscription::navigation_clause(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// View pages need to preserve their state (e.g. selected navigation tab, contents
|
|
||||||
/// and the selected row of a table). Therefor they should not be (re-)created
|
|
||||||
/// each time they are displayed.
|
|
||||||
/// Instead the application can push a new page onto the page stack if it needs to
|
|
||||||
/// be displayed. Its components are then created using the internal state. If a
|
|
||||||
/// new page needs to be displayed, it will also be pushed onto the stack. Leaving
|
|
||||||
/// that page again will pop it from the stack. The application can then return to
|
|
||||||
/// the previously displayed page in the state it was left.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct PageStack {
|
|
||||||
pages: Vec<Box<dyn ViewPage>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PageStack {
|
|
||||||
pub fn push(
|
|
||||||
&mut self,
|
|
||||||
page: Box<dyn ViewPage>,
|
|
||||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(page) = self.pages.last() {
|
|
||||||
page.unsubscribe(app)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
page.mount(app, context, theme)?;
|
|
||||||
page.subscribe(app)?;
|
|
||||||
|
|
||||||
self.pages.push(page);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
||||||
self.peek_mut()?.unsubscribe(app)?;
|
|
||||||
self.peek_mut()?.unmount(app)?;
|
|
||||||
self.pages.pop();
|
|
||||||
|
|
||||||
self.peek_mut()?.subscribe(app)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn peek_mut(&mut self) -> Result<&mut Box<dyn ViewPage>> {
|
|
||||||
match self.pages.last_mut() {
|
|
||||||
Some(page) => Ok(page),
|
|
||||||
None => Err(anyhow::anyhow!(
|
|
||||||
"Could not peek active page. Page stack is empty."
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<usize> for HomeCid {
|
|
||||||
fn from(index: usize) -> Self {
|
|
||||||
match index {
|
|
||||||
0 => HomeCid::Dashboard,
|
|
||||||
1 => HomeCid::IssueBrowser,
|
|
||||||
2 => HomeCid::PatchBrowser,
|
|
||||||
_ => HomeCid::Dashboard,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<usize> for PatchCid {
|
|
||||||
fn from(index: usize) -> Self {
|
|
||||||
match index {
|
|
||||||
0 => PatchCid::Activity,
|
|
||||||
1 => PatchCid::Files,
|
|
||||||
_ => PatchCid::Activity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
use std::hash::Hash;
|
|
||||||
|
|
||||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
|
||||||
use tuirealm::{Sub, SubClause, SubEventClause};
|
|
||||||
|
|
||||||
pub fn navigation_clause<UserEvent>() -> SubEventClause<UserEvent>
|
|
||||||
where
|
|
||||||
UserEvent: Clone + Eq + PartialEq + PartialOrd,
|
|
||||||
{
|
|
||||||
SubEventClause::Keyboard(KeyEvent {
|
|
||||||
code: Key::Tab,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn global<Id, UserEvent>() -> Vec<Sub<Id, UserEvent>>
|
|
||||||
where
|
|
||||||
Id: Clone + Hash + Eq + PartialEq,
|
|
||||||
UserEvent: Clone + Eq + PartialEq + PartialOrd,
|
|
||||||
{
|
|
||||||
vec![
|
|
||||||
Sub::new(
|
|
||||||
SubEventClause::Keyboard(KeyEvent {
|
|
||||||
code: Key::Char('q'),
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
}),
|
|
||||||
SubClause::Always,
|
|
||||||
),
|
|
||||||
Sub::new(SubEventClause::WindowResize, SubClause::Always),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod issue;
|
|
||||||
pub mod patch;
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use radicle::cob::issue::{Issue, IssueId, Issues};
|
|
||||||
use radicle::storage::git::Repository;
|
|
||||||
|
|
||||||
pub fn all(repository: &Repository) -> Result<Vec<(IssueId, Issue)>> {
|
|
||||||
let patches = Issues::open(repository)?
|
|
||||||
.all()
|
|
||||||
.map(|iter| iter.flatten().collect::<Vec<_>>())?;
|
|
||||||
|
|
||||||
Ok(patches
|
|
||||||
.into_iter()
|
|
||||||
.map(|(id, issue)| (id, issue))
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find(repository: &Repository, id: &IssueId) -> Result<Option<Issue>> {
|
|
||||||
let issues = Issues::open(repository)?;
|
|
||||||
Ok(issues.get(id)?)
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use radicle::cob::patch::{Patch, PatchId, Patches};
|
|
||||||
use radicle::storage::git::Repository;
|
|
||||||
|
|
||||||
pub fn all(repository: &Repository) -> Result<Vec<(PatchId, Patch)>> {
|
|
||||||
let patches = Patches::open(repository)?
|
|
||||||
.all()
|
|
||||||
.map(|iter| iter.flatten().collect::<Vec<_>>())?;
|
|
||||||
|
|
||||||
Ok(patches
|
|
||||||
.into_iter()
|
|
||||||
.map(|(id, patch)| (id, patch))
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find(repository: &Repository, id: &PatchId) -> Result<Option<Patch>> {
|
|
||||||
let patches = Patches::open(repository)?;
|
|
||||||
Ok(patches.get(id)?)
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
use std::hash::Hash;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use tuirealm::terminal::TerminalBridge;
|
|
||||||
use tuirealm::tui::layout::Rect;
|
|
||||||
use tuirealm::Frame;
|
|
||||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
|
||||||
|
|
||||||
pub mod cob;
|
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
/// Trait that must be implemented by client applications in order to be run
|
|
||||||
/// as tui-application using tui-realm. Implementors act as models to the
|
|
||||||
/// tui-realm application that can be polled for new messages, updated
|
|
||||||
/// accordingly and rendered with new state.
|
|
||||||
///
|
|
||||||
/// Please see `examples/` for further information on how to use it.
|
|
||||||
pub trait Tui<Id, Message>
|
|
||||||
where
|
|
||||||
Id: Eq + PartialEq + Clone + Hash,
|
|
||||||
Message: Eq,
|
|
||||||
{
|
|
||||||
/// Should initialize an application by mounting and activating components.
|
|
||||||
fn init(&mut self, app: &mut Application<Id, Message, NoUserEvent>) -> Result<()>;
|
|
||||||
|
|
||||||
/// Should update the current state by handling a message from the view. Returns true
|
|
||||||
/// if view should be updated (e.g. a message was received and the current state changed).
|
|
||||||
fn update(&mut self, app: &mut Application<Id, Message, NoUserEvent>) -> Result<bool>;
|
|
||||||
|
|
||||||
/// Should draw the application to a frame.
|
|
||||||
fn view(&mut self, app: &mut Application<Id, Message, NoUserEvent>, frame: &mut Frame);
|
|
||||||
|
|
||||||
/// Should return true if the application is requested to quit.
|
|
||||||
fn quit(&self) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A tui-window using the cross-platform Terminal helper provided
|
|
||||||
/// by tui-realm.
|
|
||||||
pub struct Window {
|
|
||||||
/// Helper around `Terminal` to quickly setup and perform on terminal.
|
|
||||||
pub terminal: TerminalBridge,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Window {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides a way to create and run a new tui-application.
|
|
||||||
impl Window {
|
|
||||||
/// Creates a tui-window using the default cross-platform Terminal
|
|
||||||
/// helper and panics if its creation fails.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
terminal: TerminalBridge::new().expect("Cannot create terminal bridge"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs this tui-window with the tui-application given and performs the
|
|
||||||
/// following steps:
|
|
||||||
/// 1. Enter alternative terminal screen
|
|
||||||
/// 2. Run main loop until application should quit and with each iteration
|
|
||||||
/// - poll new events (tick or user event)
|
|
||||||
/// - update application state
|
|
||||||
/// - redraw view
|
|
||||||
/// 3. Leave alternative terminal screen
|
|
||||||
pub fn run<T, Id, Message>(&mut self, tui: &mut T, interval: u64) -> Result<()>
|
|
||||||
where
|
|
||||||
T: Tui<Id, Message>,
|
|
||||||
Id: Eq + PartialEq + Clone + Hash,
|
|
||||||
Message: Eq,
|
|
||||||
{
|
|
||||||
let mut update = true;
|
|
||||||
let mut resize = false;
|
|
||||||
let mut size = Rect::default();
|
|
||||||
let mut app = Application::init(
|
|
||||||
EventListenerCfg::default().default_input_listener(Duration::from_millis(interval)),
|
|
||||||
);
|
|
||||||
tui.init(&mut app)?;
|
|
||||||
|
|
||||||
while !tui.quit() {
|
|
||||||
if update || resize {
|
|
||||||
self.terminal
|
|
||||||
.raw_mut()
|
|
||||||
.draw(|frame| tui.view(&mut app, frame))?;
|
|
||||||
}
|
|
||||||
update = tui.update(&mut app)?;
|
|
||||||
|
|
||||||
resize = size != self.terminal.raw().size()?;
|
|
||||||
size = self.terminal.raw().size()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
use std::process;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
|
|
||||||
use radicle::storage::ReadStorage;
|
|
||||||
|
|
||||||
use radicle_cli as cli;
|
|
||||||
use radicle_term as term;
|
|
||||||
use radicle_tui::Window;
|
|
||||||
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
pub const NAME: &str = "radicle-tui";
|
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
pub const GIT_HEAD: &str = env!("GIT_HEAD");
|
|
||||||
pub const FPS: u64 = 60;
|
|
||||||
|
|
||||||
pub const HELP: &str = r#"
|
|
||||||
Usage
|
|
||||||
|
|
||||||
radicle-tui [<option>...]
|
|
||||||
|
|
||||||
Options
|
|
||||||
|
|
||||||
--version Print version
|
|
||||||
--help Print help
|
|
||||||
|
|
||||||
"#;
|
|
||||||
|
|
||||||
struct Options;
|
|
||||||
|
|
||||||
impl Options {
|
|
||||||
fn from_env() -> Result<Self, anyhow::Error> {
|
|
||||||
use lexopt::prelude::*;
|
|
||||||
|
|
||||||
let mut parser = lexopt::Parser::from_env();
|
|
||||||
|
|
||||||
while let Some(arg) = parser.next()? {
|
|
||||||
match arg {
|
|
||||||
Long("version") => {
|
|
||||||
println!("{NAME} {VERSION}+{GIT_HEAD}");
|
|
||||||
process::exit(0);
|
|
||||||
}
|
|
||||||
Long("help") | Short('h') => {
|
|
||||||
println!("{HELP}");
|
|
||||||
process::exit(0);
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!(arg.unexpected()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute() -> anyhow::Result<()> {
|
|
||||||
let _ = Options::from_env()?;
|
|
||||||
|
|
||||||
let (_, id) = radicle::rad::cwd()
|
|
||||||
.map_err(|_| anyhow!("this command must be run in the context of a project"))?;
|
|
||||||
|
|
||||||
let profile = cli::terminal::profile()?;
|
|
||||||
|
|
||||||
let signer = cli::terminal::signer(&profile)?;
|
|
||||||
let storage = &profile.storage;
|
|
||||||
|
|
||||||
let payload = storage
|
|
||||||
.get(signer.public_key(), id)?
|
|
||||||
.context("No project with such `id` exists")?;
|
|
||||||
|
|
||||||
let project = payload.project()?;
|
|
||||||
|
|
||||||
let mut window = Window::default();
|
|
||||||
window.run(&mut app::App::new(profile, id, project), 1000 / FPS)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
if let Err(err) = execute() {
|
|
||||||
term::error(format!("Error: rad-tui: {err}"));
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
pub mod cob;
|
|
||||||
pub mod context;
|
|
||||||
pub mod ext;
|
|
||||||
pub mod layout;
|
|
||||||
pub mod state;
|
|
||||||
pub mod theme;
|
|
||||||
pub mod widget;
|
|
||||||
|
|
@ -1,361 +0,0 @@
|
||||||
use radicle_surf;
|
|
||||||
|
|
||||||
use cli::terminal::format;
|
|
||||||
use radicle_cli as cli;
|
|
||||||
|
|
||||||
use radicle::prelude::Did;
|
|
||||||
use radicle::storage::git::Repository;
|
|
||||||
use radicle::storage::{Oid, ReadRepository};
|
|
||||||
use radicle::Profile;
|
|
||||||
|
|
||||||
use radicle::cob::issue::{Issue, IssueId, State as IssueState};
|
|
||||||
use radicle::cob::patch::{Patch, PatchId, State as PatchState};
|
|
||||||
use radicle::cob::{Label, Timestamp};
|
|
||||||
|
|
||||||
use tuirealm::props::{Color, Style};
|
|
||||||
use tuirealm::tui::text::{Span, Spans};
|
|
||||||
use tuirealm::tui::widgets::Cell;
|
|
||||||
|
|
||||||
use crate::ui::theme::Theme;
|
|
||||||
use crate::ui::widget::common::list::TableItem;
|
|
||||||
|
|
||||||
use super::widget::common::list::ListItem;
|
|
||||||
|
|
||||||
/// An author item that can be used in tables, list or trees.
|
|
||||||
///
|
|
||||||
/// Breaks up dependencies to [`Profile`] and [`Repository`] that
|
|
||||||
/// would be needed if [`AuthorItem`] would be used directly.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AuthorItem {
|
|
||||||
/// The author's DID.
|
|
||||||
did: Did,
|
|
||||||
/// True if the author is the current user.
|
|
||||||
is_you: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthorItem {
|
|
||||||
pub fn did(&self) -> Did {
|
|
||||||
self.did
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_you(&self) -> bool {
|
|
||||||
self.is_you
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A patch item that can be used in tables, list or trees.
|
|
||||||
///
|
|
||||||
/// Breaks up dependencies to [`Profile`] and [`Repository`] that
|
|
||||||
/// would be needed if [`Patch`] would be used directly.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct PatchItem {
|
|
||||||
/// Patch OID.
|
|
||||||
id: PatchId,
|
|
||||||
/// Patch state.
|
|
||||||
state: PatchState,
|
|
||||||
/// Patch title.
|
|
||||||
title: String,
|
|
||||||
/// Author of the latest revision.
|
|
||||||
author: AuthorItem,
|
|
||||||
/// Head of the latest revision.
|
|
||||||
head: Oid,
|
|
||||||
/// Lines added by the latest revision.
|
|
||||||
added: u16,
|
|
||||||
/// Lines removed by the latest revision.
|
|
||||||
removed: u16,
|
|
||||||
/// Time when patch was opened.
|
|
||||||
timestamp: Timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PatchItem {
|
|
||||||
pub fn id(&self) -> &PatchId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(&self) -> &PatchState {
|
|
||||||
&self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title(&self) -> &String {
|
|
||||||
&self.title
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn author(&self) -> &AuthorItem {
|
|
||||||
&self.author
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn head(&self) -> &Oid {
|
|
||||||
&self.head
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn added(&self) -> u16 {
|
|
||||||
self.added
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removed(&self) -> u16 {
|
|
||||||
self.removed
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timestamp(&self) -> &Timestamp {
|
|
||||||
&self.timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<(&Profile, &Repository, PatchId, Patch)> for PatchItem {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(value: (&Profile, &Repository, PatchId, Patch)) -> Result<Self, Self::Error> {
|
|
||||||
let (profile, repo, id, patch) = value;
|
|
||||||
let (_, rev) = patch.latest();
|
|
||||||
let repo = radicle_surf::Repository::open(repo.path())?;
|
|
||||||
let base = repo.commit(rev.base())?;
|
|
||||||
let head = repo.commit(rev.head())?;
|
|
||||||
let diff = repo.diff(base.id, head.id)?;
|
|
||||||
|
|
||||||
Ok(PatchItem {
|
|
||||||
id,
|
|
||||||
state: patch.state().clone(),
|
|
||||||
title: patch.title().into(),
|
|
||||||
author: AuthorItem {
|
|
||||||
did: patch.author().id,
|
|
||||||
is_you: *patch.author().id == *profile.did(),
|
|
||||||
},
|
|
||||||
head: rev.head(),
|
|
||||||
added: diff.stats().insertions as u16,
|
|
||||||
removed: diff.stats().deletions as u16,
|
|
||||||
timestamp: rev.timestamp(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableItem<8> for PatchItem {
|
|
||||||
fn row(&self, theme: &Theme) -> [Cell; 8] {
|
|
||||||
let (icon, color) = format_patch_state(&self.state);
|
|
||||||
let state = Cell::from(icon).style(Style::default().fg(color));
|
|
||||||
|
|
||||||
let id = Cell::from(format::cob(&self.id))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_id));
|
|
||||||
|
|
||||||
let title = Cell::from(self.title.clone())
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_title));
|
|
||||||
|
|
||||||
let author = Cell::from(format_author(&self.author.did, self.author.is_you))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_author));
|
|
||||||
|
|
||||||
let head = Cell::from(format::oid(self.head).item)
|
|
||||||
.style(Style::default().fg(theme.colors.browser_patch_list_head));
|
|
||||||
|
|
||||||
let added = Cell::from(format!("{}", self.added))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_patch_list_added));
|
|
||||||
|
|
||||||
let removed = Cell::from(format!("{}", self.removed))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_patch_list_removed));
|
|
||||||
|
|
||||||
let updated = Cell::from(format::timestamp(&self.timestamp).to_string())
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_timestamp));
|
|
||||||
|
|
||||||
[state, id, title, author, head, added, removed, updated]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An issue item that can be used in tables, list or trees.
|
|
||||||
///
|
|
||||||
/// Breaks up dependencies to [`Profile`] and [`Repository`] that
|
|
||||||
/// would be needed if [`Issue`] would be used directly.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct IssueItem {
|
|
||||||
/// Issue OID.
|
|
||||||
id: IssueId,
|
|
||||||
/// Issue state.
|
|
||||||
state: IssueState,
|
|
||||||
/// Issue title.
|
|
||||||
title: String,
|
|
||||||
/// Issue author.
|
|
||||||
author: AuthorItem,
|
|
||||||
/// Issue labels.
|
|
||||||
labels: Vec<Label>,
|
|
||||||
/// Issue assignees.
|
|
||||||
assignees: Vec<AuthorItem>,
|
|
||||||
/// Time when issue was opened.
|
|
||||||
timestamp: Timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IssueItem {
|
|
||||||
pub fn id(&self) -> &IssueId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(&self) -> &IssueState {
|
|
||||||
&self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title(&self) -> &String {
|
|
||||||
&self.title
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn author(&self) -> &AuthorItem {
|
|
||||||
&self.author
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn labels(&self) -> &Vec<Label> {
|
|
||||||
&self.labels
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assignees(&self) -> &Vec<AuthorItem> {
|
|
||||||
&self.assignees
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timestamp(&self) -> &Timestamp {
|
|
||||||
&self.timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(&Profile, &Repository, IssueId, Issue)> for IssueItem {
|
|
||||||
fn from(value: (&Profile, &Repository, IssueId, Issue)) -> Self {
|
|
||||||
let (profile, _, id, issue) = value;
|
|
||||||
|
|
||||||
IssueItem {
|
|
||||||
id,
|
|
||||||
state: *issue.state(),
|
|
||||||
title: issue.title().into(),
|
|
||||||
author: AuthorItem {
|
|
||||||
did: issue.author().id,
|
|
||||||
is_you: *issue.author().id == *profile.did(),
|
|
||||||
},
|
|
||||||
labels: issue.labels().cloned().collect(),
|
|
||||||
assignees: issue
|
|
||||||
.assigned()
|
|
||||||
.cloned()
|
|
||||||
.map(|did| AuthorItem {
|
|
||||||
did,
|
|
||||||
is_you: did == profile.did(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
timestamp: issue.timestamp(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableItem<7> for IssueItem {
|
|
||||||
fn row(&self, theme: &Theme) -> [Cell; 7] {
|
|
||||||
let (icon, color) = format_issue_state(&self.state);
|
|
||||||
let state = Cell::from(icon).style(Style::default().fg(color));
|
|
||||||
|
|
||||||
let id = Cell::from(format::cob(&self.id))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_id));
|
|
||||||
|
|
||||||
let title = Cell::from(self.title.clone())
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_title));
|
|
||||||
|
|
||||||
let author = Cell::from(format_author(&self.author.did, self.author.is_you))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_author));
|
|
||||||
|
|
||||||
let tags = Cell::from(format_labels(&self.labels))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_tags));
|
|
||||||
|
|
||||||
let assignees = self
|
|
||||||
.assignees
|
|
||||||
.iter()
|
|
||||||
.map(|author| (author.did, author.is_you))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let assignees = Cell::from(format_assignees(&assignees))
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_author));
|
|
||||||
|
|
||||||
let opened = Cell::from(format::timestamp(&self.timestamp).to_string())
|
|
||||||
.style(Style::default().fg(theme.colors.browser_list_timestamp));
|
|
||||||
|
|
||||||
[state, id, title, author, tags, assignees, opened]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListItem for IssueItem {
|
|
||||||
fn row(&self, theme: &Theme) -> tuirealm::tui::widgets::ListItem {
|
|
||||||
let (state, state_color) = format_issue_state(&self.state);
|
|
||||||
let lines = vec![
|
|
||||||
Spans::from(vec![
|
|
||||||
Span::styled(state, Style::default().fg(state_color)),
|
|
||||||
Span::styled(
|
|
||||||
self.title.clone(),
|
|
||||||
Style::default().fg(theme.colors.browser_list_title),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
Spans::from(vec![
|
|
||||||
Span::raw(String::from(" ")),
|
|
||||||
Span::styled(
|
|
||||||
format_author(&self.author.did, self.author.is_you),
|
|
||||||
Style::default().fg(theme.colors.browser_list_author),
|
|
||||||
),
|
|
||||||
Span::styled(
|
|
||||||
format!(" {} ", theme.icons.property_divider),
|
|
||||||
Style::default().fg(theme.colors.property_divider_fg),
|
|
||||||
),
|
|
||||||
Span::styled(
|
|
||||||
format::timestamp(&self.timestamp).to_string(),
|
|
||||||
Style::default().fg(theme.colors.browser_list_timestamp),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
tuirealm::tui::widgets::ListItem::new(lines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for IssueItem {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_patch_state(state: &PatchState) -> (String, Color) {
|
|
||||||
match state {
|
|
||||||
PatchState::Open { conflicts: _ } => (" ● ".into(), Color::Green),
|
|
||||||
PatchState::Archived => (" ● ".into(), Color::Yellow),
|
|
||||||
PatchState::Draft => (" ● ".into(), Color::Gray),
|
|
||||||
PatchState::Merged {
|
|
||||||
revision: _,
|
|
||||||
commit: _,
|
|
||||||
} => (" ✔ ".into(), Color::Blue),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_author(did: &Did, is_you: bool) -> String {
|
|
||||||
if is_you {
|
|
||||||
format!("{} (you)", format::did(did))
|
|
||||||
} else {
|
|
||||||
format!("{}", format::did(did))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_issue_state(state: &IssueState) -> (String, Color) {
|
|
||||||
match state {
|
|
||||||
IssueState::Open => (" ● ".into(), Color::Green),
|
|
||||||
IssueState::Closed { reason: _ } => (" ● ".into(), Color::Red),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_labels(labels: &[Label]) -> String {
|
|
||||||
let mut output = String::new();
|
|
||||||
let mut labels = labels.iter().peekable();
|
|
||||||
|
|
||||||
while let Some(tag) = labels.next() {
|
|
||||||
output.push_str(&tag.to_string());
|
|
||||||
|
|
||||||
if labels.peek().is_some() {
|
|
||||||
output.push(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_assignees(assignees: &[(Did, bool)]) -> String {
|
|
||||||
let mut output = String::new();
|
|
||||||
let mut assignees = assignees.iter().peekable();
|
|
||||||
|
|
||||||
while let Some((assignee, is_you)) = assignees.next() {
|
|
||||||
output.push_str(&format_author(assignee, *is_you));
|
|
||||||
|
|
||||||
if assignees.peek().is_some() {
|
|
||||||
output.push(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
use radicle::prelude::{Id, Project};
|
|
||||||
use radicle::Profile;
|
|
||||||
|
|
||||||
use radicle::storage::git::Repository;
|
|
||||||
use radicle::storage::ReadStorage;
|
|
||||||
pub struct Context {
|
|
||||||
profile: Profile,
|
|
||||||
id: Id,
|
|
||||||
project: Project,
|
|
||||||
repository: Repository,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Context {
|
|
||||||
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
|
||||||
let repository = profile.storage.repository(id).unwrap();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
profile,
|
|
||||||
project,
|
|
||||||
repository,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn profile(&self) -> &Profile {
|
|
||||||
&self.profile
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> &Id {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn project(&self) -> &Project {
|
|
||||||
&self.project
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn repository(&self) -> &Repository {
|
|
||||||
&self.repository
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
use tuirealm::tui::buffer::Buffer;
|
|
||||||
use tuirealm::tui::layout::Rect;
|
|
||||||
use tuirealm::tui::style::Style;
|
|
||||||
use tuirealm::tui::widgets::{BorderType, Borders, Widget};
|
|
||||||
|
|
||||||
pub struct HeaderBlock {
|
|
||||||
/// Visible borders
|
|
||||||
borders: Borders,
|
|
||||||
/// Border style
|
|
||||||
border_style: Style,
|
|
||||||
/// Type of the border. The default is plain lines but one can choose to have rounded corners
|
|
||||||
/// or doubled lines instead.
|
|
||||||
border_type: BorderType,
|
|
||||||
/// Widget style
|
|
||||||
style: Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for HeaderBlock {
|
|
||||||
fn default() -> HeaderBlock {
|
|
||||||
HeaderBlock {
|
|
||||||
borders: Borders::NONE,
|
|
||||||
border_style: Default::default(),
|
|
||||||
border_type: BorderType::Plain,
|
|
||||||
style: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HeaderBlock {
|
|
||||||
pub fn border_style(mut self, style: Style) -> HeaderBlock {
|
|
||||||
self.border_style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn style(mut self, style: Style) -> HeaderBlock {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn borders(mut self, flag: Borders) -> HeaderBlock {
|
|
||||||
self.borders = flag;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn border_type(mut self, border_type: BorderType) -> HeaderBlock {
|
|
||||||
self.border_type = border_type;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for HeaderBlock {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
if area.area() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
buf.set_style(area, self.style);
|
|
||||||
let symbols = BorderType::line_symbols(self.border_type);
|
|
||||||
|
|
||||||
// Sides
|
|
||||||
if self.borders.intersects(Borders::LEFT) {
|
|
||||||
for y in area.top()..area.bottom() {
|
|
||||||
buf.get_mut(area.left(), y)
|
|
||||||
.set_symbol(symbols.vertical)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::TOP) {
|
|
||||||
for x in area.left()..area.right() {
|
|
||||||
buf.get_mut(x, area.top())
|
|
||||||
.set_symbol(symbols.horizontal)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::RIGHT) {
|
|
||||||
let x = area.right() - 1;
|
|
||||||
for y in area.top()..area.bottom() {
|
|
||||||
buf.get_mut(x, y)
|
|
||||||
.set_symbol(symbols.vertical)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::BOTTOM) {
|
|
||||||
let y = area.bottom() - 1;
|
|
||||||
for x in area.left()..area.right() {
|
|
||||||
buf.get_mut(x, y)
|
|
||||||
.set_symbol(symbols.horizontal)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Corners
|
|
||||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
|
||||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
|
||||||
.set_symbol(symbols.vertical_left)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
|
||||||
buf.get_mut(area.right() - 1, area.top())
|
|
||||||
.set_symbol(symbols.top_right)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
|
||||||
buf.get_mut(area.left(), area.bottom() - 1)
|
|
||||||
.set_symbol(symbols.vertical_right)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
|
||||||
buf.get_mut(area.left(), area.top())
|
|
||||||
.set_symbol(symbols.top_left)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
use tuirealm::props::{AttrValue, Attribute};
|
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
|
||||||
use tuirealm::MockComponent;
|
|
||||||
|
|
||||||
pub struct AppHeader {
|
|
||||||
pub nav: Rect,
|
|
||||||
pub info: Rect,
|
|
||||||
pub line: Rect,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IssuePreview {
|
|
||||||
pub header: Rect,
|
|
||||||
pub list: Rect,
|
|
||||||
pub details: Rect,
|
|
||||||
pub discussion: Rect,
|
|
||||||
pub shortcuts: Rect,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn v_stack(
|
|
||||||
widgets: Vec<Box<dyn MockComponent>>,
|
|
||||||
area: Rect,
|
|
||||||
) -> Vec<(Box<dyn MockComponent>, Rect)> {
|
|
||||||
let constraints = widgets
|
|
||||||
.iter()
|
|
||||||
.map(|w| {
|
|
||||||
Constraint::Length(
|
|
||||||
w.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(constraints)
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
widgets.into_iter().zip(layout.into_iter()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn h_stack(
|
|
||||||
widgets: Vec<Box<dyn MockComponent>>,
|
|
||||||
area: Rect,
|
|
||||||
) -> Vec<(Box<dyn MockComponent>, Rect)> {
|
|
||||||
let constraints = widgets
|
|
||||||
.iter()
|
|
||||||
.map(|w| {
|
|
||||||
Constraint::Length(
|
|
||||||
w.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints(constraints)
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
widgets.into_iter().zip(layout.into_iter()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app_header(area: Rect, info_w: u16) -> AppHeader {
|
|
||||||
let nav_w = area.width.saturating_sub(info_w);
|
|
||||||
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(vec![
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
let top = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([Constraint::Length(nav_w), Constraint::Length(info_w)].as_ref())
|
|
||||||
.split(layout[1]);
|
|
||||||
|
|
||||||
AppHeader {
|
|
||||||
nav: top[0],
|
|
||||||
info: top[1],
|
|
||||||
line: layout[2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_page(area: Rect) -> Vec<Rect> {
|
|
||||||
let nav_h = 3u16;
|
|
||||||
let margin_h = 1u16;
|
|
||||||
let content_h = area.height.saturating_sub(nav_h.saturating_add(margin_h));
|
|
||||||
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.horizontal_margin(margin_h)
|
|
||||||
.constraints([Constraint::Length(nav_h), Constraint::Length(content_h)].as_ref())
|
|
||||||
.split(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn headerless_page(area: Rect) -> Vec<Rect> {
|
|
||||||
let margin_h = 1u16;
|
|
||||||
let content_h = area.height.saturating_sub(margin_h);
|
|
||||||
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.horizontal_margin(margin_h)
|
|
||||||
.constraints([Constraint::Length(content_h)].as_ref())
|
|
||||||
.split(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn root_component(area: Rect, shortcuts_h: u16) -> Vec<Rect> {
|
|
||||||
let content_h = area.height.saturating_sub(shortcuts_h);
|
|
||||||
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(content_h),
|
|
||||||
Constraint::Length(shortcuts_h),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn root_component_with_context(area: Rect, context_h: u16, shortcuts_h: u16) -> Vec<Rect> {
|
|
||||||
let content_h = area
|
|
||||||
.height
|
|
||||||
.saturating_sub(shortcuts_h.saturating_add(context_h));
|
|
||||||
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(content_h),
|
|
||||||
Constraint::Length(context_h),
|
|
||||||
Constraint::Length(shortcuts_h),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn centered_label(label_w: u16, area: Rect) -> Rect {
|
|
||||||
let label_h = 1u16;
|
|
||||||
let spacer_w = area.width.saturating_sub(label_w).saturating_div(2);
|
|
||||||
let spacer_h = area.height.saturating_sub(label_h).saturating_div(2);
|
|
||||||
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(spacer_h),
|
|
||||||
Constraint::Length(label_h),
|
|
||||||
Constraint::Length(spacer_h),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(spacer_w),
|
|
||||||
Constraint::Length(label_w),
|
|
||||||
Constraint::Length(spacer_w),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(layout[1])[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn issue_preview(area: Rect, shortcuts_h: u16) -> IssuePreview {
|
|
||||||
let header_h = 3u16;
|
|
||||||
let content_h = area
|
|
||||||
.height
|
|
||||||
.saturating_sub(header_h)
|
|
||||||
.saturating_sub(shortcuts_h);
|
|
||||||
|
|
||||||
let root = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.horizontal_margin(1)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(header_h),
|
|
||||||
Constraint::Length(content_h),
|
|
||||||
Constraint::Length(shortcuts_h),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
let split = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
|
||||||
.split(root[1]);
|
|
||||||
|
|
||||||
let right = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(6), Constraint::Min(0)].as_ref())
|
|
||||||
.split(split[1]);
|
|
||||||
|
|
||||||
IssuePreview {
|
|
||||||
header: root[0],
|
|
||||||
list: split[0],
|
|
||||||
details: right[0],
|
|
||||||
discussion: right[1],
|
|
||||||
shortcuts: root[2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
use tuirealm::tui::widgets::{ListState, TableState};
|
|
||||||
|
|
||||||
/// State that holds the index of a selected tab item and the count of all tab items.
|
|
||||||
/// The index can be increased and will start at 0, if length was reached.
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct TabState {
|
|
||||||
pub selected: u16,
|
|
||||||
pub len: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabState {
|
|
||||||
pub fn incr_tab_index(&mut self, rewind: bool) {
|
|
||||||
if self.selected + 1 < self.len {
|
|
||||||
self.selected += 1;
|
|
||||||
} else if rewind {
|
|
||||||
self.selected = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ItemState {
|
|
||||||
selected: Option<usize>,
|
|
||||||
len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemState {
|
|
||||||
pub fn new(selected: Option<usize>, len: usize) -> Self {
|
|
||||||
Self { selected, len }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected(&self) -> Option<usize> {
|
|
||||||
self.selected
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_previous(&mut self) -> Option<usize> {
|
|
||||||
let old_index = self.selected();
|
|
||||||
let new_index = match old_index {
|
|
||||||
Some(selected) if selected == 0 => Some(0),
|
|
||||||
Some(selected) => Some(selected.saturating_sub(1)),
|
|
||||||
None => Some(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
if old_index != new_index {
|
|
||||||
self.selected = new_index;
|
|
||||||
self.selected()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_next(&mut self) -> Option<usize> {
|
|
||||||
let old_index = self.selected();
|
|
||||||
let new_index = match old_index {
|
|
||||||
Some(selected) if selected >= self.len.saturating_sub(1) => {
|
|
||||||
Some(self.len.saturating_sub(1))
|
|
||||||
}
|
|
||||||
Some(selected) => Some(selected.saturating_add(1)),
|
|
||||||
None => Some(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
if old_index != new_index {
|
|
||||||
self.selected = new_index;
|
|
||||||
self.selected()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&ItemState> for TableState {
|
|
||||||
fn from(value: &ItemState) -> Self {
|
|
||||||
let mut state = TableState::default();
|
|
||||||
state.select(value.selected);
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&ItemState> for ListState {
|
|
||||||
fn from(value: &ItemState) -> Self {
|
|
||||||
let mut state = ListState::default();
|
|
||||||
state.select(value.selected);
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
use tuirealm::props::Color;
|
|
||||||
|
|
||||||
const COLOR_DEFAULT_FG: Color = Color::Rgb(200, 200, 200);
|
|
||||||
const COLOR_DEFAULT_DARK_FG: Color = Color::Rgb(150, 150, 150);
|
|
||||||
const COLOR_DEFAULT_DARK: Color = Color::Rgb(100, 100, 100);
|
|
||||||
const COLOR_DEFAULT_DARKER: Color = Color::Rgb(70, 70, 70);
|
|
||||||
const COLOR_DEFAULT_DARKEST: Color = Color::Rgb(40, 40, 40);
|
|
||||||
const COLOR_DEFAULT_FAINT: Color = Color::Rgb(20, 20, 20);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Colors {
|
|
||||||
pub default_fg: Color,
|
|
||||||
pub tabs_highlighted_fg: Color,
|
|
||||||
pub app_header_project_fg: Color,
|
|
||||||
pub app_header_rid_fg: Color,
|
|
||||||
pub labeled_container_bg: Color,
|
|
||||||
pub item_list_highlighted_bg: Color,
|
|
||||||
pub property_name_fg: Color,
|
|
||||||
pub property_divider_fg: Color,
|
|
||||||
pub shortcut_short_fg: Color,
|
|
||||||
pub shortcut_long_fg: Color,
|
|
||||||
pub shortcutbar_divider_fg: Color,
|
|
||||||
pub browser_list_id: Color,
|
|
||||||
pub browser_list_title: Color,
|
|
||||||
pub browser_list_description: Color,
|
|
||||||
pub browser_list_author: Color,
|
|
||||||
pub browser_list_tags: Color,
|
|
||||||
pub browser_list_comments: Color,
|
|
||||||
pub browser_list_timestamp: Color,
|
|
||||||
pub browser_patch_list_head: Color,
|
|
||||||
pub browser_patch_list_added: Color,
|
|
||||||
pub browser_patch_list_removed: Color,
|
|
||||||
pub context_bg: Color,
|
|
||||||
pub context_light_bg: Color,
|
|
||||||
pub context_badge_bg: Color,
|
|
||||||
pub context_id_fg: Color,
|
|
||||||
pub context_id_bg: Color,
|
|
||||||
pub context_id_author_fg: Color,
|
|
||||||
pub container_border_fg: Color,
|
|
||||||
pub container_border_focus_fg: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Icons {
|
|
||||||
pub property_divider: char,
|
|
||||||
pub shortcutbar_divider: char,
|
|
||||||
pub tab_divider: char,
|
|
||||||
pub tab_overline: char,
|
|
||||||
pub whitespace: char,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Tables {
|
|
||||||
pub spacing: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Radicle TUI theme. Will be defined in a JSON config file in the
|
|
||||||
/// future. e.g.:
|
|
||||||
/// {
|
|
||||||
/// "name": "Default",
|
|
||||||
/// "colors": {
|
|
||||||
/// "foreground": "#ffffff",
|
|
||||||
/// "propertyForeground": "#ffffff",
|
|
||||||
/// "highlightedBackground": "#000000",
|
|
||||||
/// },
|
|
||||||
/// "icons": {
|
|
||||||
/// "workspaces.divider": "|",
|
|
||||||
/// "shortcuts.divider: "∙",
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Theme {
|
|
||||||
pub name: String,
|
|
||||||
pub colors: Colors,
|
|
||||||
pub icons: Icons,
|
|
||||||
pub tables: Tables,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_dark() -> Theme {
|
|
||||||
Theme {
|
|
||||||
name: String::from("Default"),
|
|
||||||
colors: Colors {
|
|
||||||
default_fg: COLOR_DEFAULT_FG,
|
|
||||||
tabs_highlighted_fg: Color::Magenta,
|
|
||||||
app_header_project_fg: Color::Cyan,
|
|
||||||
app_header_rid_fg: Color::Yellow,
|
|
||||||
labeled_container_bg: COLOR_DEFAULT_FAINT,
|
|
||||||
item_list_highlighted_bg: COLOR_DEFAULT_DARKER,
|
|
||||||
property_name_fg: Color::Cyan,
|
|
||||||
property_divider_fg: COLOR_DEFAULT_DARK,
|
|
||||||
shortcut_short_fg: COLOR_DEFAULT_DARK,
|
|
||||||
shortcut_long_fg: COLOR_DEFAULT_DARKER,
|
|
||||||
shortcutbar_divider_fg: COLOR_DEFAULT_DARKER,
|
|
||||||
browser_list_id: Color::Cyan,
|
|
||||||
browser_list_title: COLOR_DEFAULT_FG,
|
|
||||||
browser_list_description: COLOR_DEFAULT_DARK,
|
|
||||||
browser_list_author: Color::Gray,
|
|
||||||
browser_list_tags: Color::LightBlue,
|
|
||||||
browser_list_comments: COLOR_DEFAULT_DARK_FG,
|
|
||||||
browser_list_timestamp: COLOR_DEFAULT_DARK,
|
|
||||||
browser_patch_list_head: Color::LightBlue,
|
|
||||||
browser_patch_list_added: Color::Green,
|
|
||||||
browser_patch_list_removed: Color::Red,
|
|
||||||
context_bg: COLOR_DEFAULT_DARKEST,
|
|
||||||
context_light_bg: Color::Gray,
|
|
||||||
context_badge_bg: Color::LightRed,
|
|
||||||
context_id_fg: Color::Cyan,
|
|
||||||
context_id_bg: COLOR_DEFAULT_DARKEST,
|
|
||||||
context_id_author_fg: Color::Gray,
|
|
||||||
container_border_fg: COLOR_DEFAULT_DARKEST,
|
|
||||||
container_border_focus_fg: COLOR_DEFAULT_DARK,
|
|
||||||
},
|
|
||||||
icons: Icons {
|
|
||||||
property_divider: '∙',
|
|
||||||
shortcutbar_divider: '∙',
|
|
||||||
tab_divider: '|',
|
|
||||||
tab_overline: '▔',
|
|
||||||
whitespace: ' ',
|
|
||||||
},
|
|
||||||
tables: Tables { spacing: 2 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
pub mod common;
|
|
||||||
pub mod home;
|
|
||||||
pub mod issue;
|
|
||||||
pub mod patch;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
|
||||||
use tuirealm::props::{AttrValue, Attribute, Color, Props};
|
|
||||||
use tuirealm::tui::layout::Rect;
|
|
||||||
use tuirealm::{Frame, MockComponent, State};
|
|
||||||
|
|
||||||
pub type BoxedWidget<T> = Box<Widget<T>>;
|
|
||||||
|
|
||||||
pub trait WidgetComponent {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect);
|
|
||||||
|
|
||||||
fn state(&self) -> State;
|
|
||||||
|
|
||||||
fn perform(&mut self, properties: &Props, cmd: Cmd) -> CmdResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Widget<T: WidgetComponent> {
|
|
||||||
component: T,
|
|
||||||
properties: Props,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: WidgetComponent> Deref for Widget<T> {
|
|
||||||
type Target = T;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.component
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: WidgetComponent> Widget<T> {
|
|
||||||
pub fn new(component: T) -> Self {
|
|
||||||
Widget {
|
|
||||||
component,
|
|
||||||
properties: Props::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground(mut self, fg: Color) -> Self {
|
|
||||||
self.attr(Attribute::Foreground, AttrValue::Color(fg));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn highlight(mut self, fg: Color) -> Self {
|
|
||||||
self.attr(Attribute::HighlightedColor, AttrValue::Color(fg));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn background(mut self, bg: Color) -> Self {
|
|
||||||
self.attr(Attribute::Background, AttrValue::Color(bg));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn height(mut self, h: u16) -> Self {
|
|
||||||
self.attr(Attribute::Height, AttrValue::Size(h));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn width(mut self, w: u16) -> Self {
|
|
||||||
self.attr(Attribute::Width, AttrValue::Size(w));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn content(mut self, content: AttrValue) -> Self {
|
|
||||||
self.attr(Attribute::Content, content);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn custom(mut self, key: &'static str, value: AttrValue) -> Self {
|
|
||||||
self.attr(Attribute::Custom(key), value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_boxed(self) -> Box<Self> {
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: WidgetComponent> MockComponent for Widget<T> {
|
|
||||||
fn view(&mut self, frame: &mut Frame, area: Rect) {
|
|
||||||
self.component.view(&self.properties, frame, area)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
|
||||||
self.properties.get(attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
|
||||||
self.properties.set(attr, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
self.component.state()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
|
||||||
self.component.perform(&self.properties, cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
pub mod container;
|
|
||||||
pub mod context;
|
|
||||||
pub mod label;
|
|
||||||
pub mod list;
|
|
||||||
|
|
||||||
use tuirealm::props::{AttrValue, Attribute};
|
|
||||||
use tuirealm::MockComponent;
|
|
||||||
|
|
||||||
use container::{GlobalListener, Header, LabeledContainer, Tabs};
|
|
||||||
use context::{Shortcut, Shortcuts};
|
|
||||||
use label::Label;
|
|
||||||
use list::{Property, PropertyList};
|
|
||||||
|
|
||||||
use self::container::{AppHeader, AppInfo, Container, VerticalLine};
|
|
||||||
use self::list::{ColumnWidth, PropertyTable};
|
|
||||||
|
|
||||||
use super::Widget;
|
|
||||||
|
|
||||||
use crate::ui::context::Context;
|
|
||||||
use crate::ui::theme::Theme;
|
|
||||||
|
|
||||||
pub fn global_listener() -> Widget<GlobalListener> {
|
|
||||||
Widget::new(GlobalListener::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(content: &str) -> Widget<Label> {
|
|
||||||
// TODO: Remove when size constraints are implemented
|
|
||||||
let width = content.chars().count() as u16;
|
|
||||||
|
|
||||||
Widget::new(Label::default())
|
|
||||||
.content(AttrValue::String(content.to_string()))
|
|
||||||
.height(1)
|
|
||||||
.width(width)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reversable_label(content: &str) -> Widget<Label> {
|
|
||||||
let content = &format!(" {content} ");
|
|
||||||
|
|
||||||
label(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn container_header(theme: &Theme, label: Widget<Label>) -> Widget<Header<1>> {
|
|
||||||
let header = Header::new([label], [ColumnWidth::Grow], theme.clone());
|
|
||||||
|
|
||||||
Widget::new(header)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn container(theme: &Theme, component: Box<dyn MockComponent>) -> Widget<Container> {
|
|
||||||
let container = Container::new(component, theme.clone());
|
|
||||||
Widget::new(container)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn labeled_container(
|
|
||||||
theme: &Theme,
|
|
||||||
title: &str,
|
|
||||||
component: Box<dyn MockComponent>,
|
|
||||||
) -> Widget<LabeledContainer> {
|
|
||||||
let header = container_header(
|
|
||||||
theme,
|
|
||||||
label(&format!(" {title} ")).foreground(theme.colors.default_fg),
|
|
||||||
);
|
|
||||||
let container = LabeledContainer::new(header, component, theme.clone());
|
|
||||||
|
|
||||||
Widget::new(container)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shortcut(theme: &Theme, short: &str, long: &str) -> Widget<Shortcut> {
|
|
||||||
let short = label(short).foreground(theme.colors.shortcut_short_fg);
|
|
||||||
let divider = label(&theme.icons.whitespace.to_string());
|
|
||||||
let long = label(long).foreground(theme.colors.shortcut_long_fg);
|
|
||||||
|
|
||||||
// TODO: Remove when size constraints are implemented
|
|
||||||
let short_w = short.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let divider_w = divider.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let long_w = long.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let width = short_w.saturating_add(divider_w).saturating_add(long_w);
|
|
||||||
|
|
||||||
let shortcut = Shortcut::new(short, divider, long);
|
|
||||||
|
|
||||||
Widget::new(shortcut).height(1).width(width)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shortcuts(theme: &Theme, shortcuts: Vec<Widget<Shortcut>>) -> Widget<Shortcuts> {
|
|
||||||
let divider = label(&format!(" {} ", theme.icons.shortcutbar_divider))
|
|
||||||
.foreground(theme.colors.shortcutbar_divider_fg);
|
|
||||||
let shortcut_bar = Shortcuts::new(shortcuts, divider);
|
|
||||||
|
|
||||||
Widget::new(shortcut_bar).height(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn property(theme: &Theme, name: &str, value: &str) -> Widget<Property> {
|
|
||||||
let name = label(name).foreground(theme.colors.property_name_fg);
|
|
||||||
let divider = label(&format!(" {} ", theme.icons.property_divider));
|
|
||||||
let value = label(value).foreground(theme.colors.default_fg);
|
|
||||||
|
|
||||||
// TODO: Remove when size constraints are implemented
|
|
||||||
let name_w = name.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let divider_w = divider.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let value_w = value.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let width = name_w.saturating_add(divider_w).saturating_add(value_w);
|
|
||||||
|
|
||||||
let property = Property::new(name, value).with_divider(divider);
|
|
||||||
|
|
||||||
Widget::new(property).height(1).width(width)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn property_list(_theme: &Theme, properties: Vec<Widget<Property>>) -> Widget<PropertyList> {
|
|
||||||
let property_list = PropertyList::new(properties);
|
|
||||||
|
|
||||||
Widget::new(property_list)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn property_table(_theme: &Theme, properties: Vec<Widget<Property>>) -> Widget<PropertyTable> {
|
|
||||||
let table = PropertyTable::new(properties);
|
|
||||||
|
|
||||||
Widget::new(table)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tabs(_theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
|
|
||||||
let tabs = Tabs::new(tabs);
|
|
||||||
|
|
||||||
Widget::new(tabs).height(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app_info(context: &Context, theme: &Theme) -> Widget<AppInfo> {
|
|
||||||
let project = label(context.project().name()).foreground(theme.colors.app_header_project_fg);
|
|
||||||
let rid = label(&format!(" ({})", context.id())).foreground(theme.colors.app_header_rid_fg);
|
|
||||||
|
|
||||||
let project_w = project
|
|
||||||
.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let rid_w = rid
|
|
||||||
.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
|
|
||||||
let info = AppInfo::new(project, rid);
|
|
||||||
Widget::new(info).width(project_w.saturating_add(rid_w))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app_header(
|
|
||||||
context: &Context,
|
|
||||||
theme: &Theme,
|
|
||||||
nav: Option<Widget<Tabs>>,
|
|
||||||
) -> Widget<AppHeader> {
|
|
||||||
let line =
|
|
||||||
label(&theme.icons.tab_overline.to_string()).foreground(theme.colors.tabs_highlighted_fg);
|
|
||||||
let line = Widget::new(VerticalLine::new(line));
|
|
||||||
let info = app_info(context, theme);
|
|
||||||
let header = AppHeader::new(nav, info, line);
|
|
||||||
|
|
||||||
Widget::new(header)
|
|
||||||
}
|
|
||||||
|
|
@ -1,458 +0,0 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
|
||||||
use tuirealm::props::{AttrValue, Attribute, BorderSides, BorderType, Props, Style, TextModifiers};
|
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
|
||||||
use tuirealm::tui::widgets::{Block, Cell, Row};
|
|
||||||
use tuirealm::{Frame, MockComponent, State, StateValue};
|
|
||||||
|
|
||||||
use crate::ui::ext::HeaderBlock;
|
|
||||||
use crate::ui::layout;
|
|
||||||
use crate::ui::state::TabState;
|
|
||||||
use crate::ui::theme::Theme;
|
|
||||||
use crate::ui::widget::{utils, Widget, WidgetComponent};
|
|
||||||
|
|
||||||
use super::label::Label;
|
|
||||||
use super::list::ColumnWidth;
|
|
||||||
|
|
||||||
/// Some user events need to be handled globally (e.g. user presses key `q` to quit
|
|
||||||
/// the application). This component can be used in conjunction with SubEventClause
|
|
||||||
/// to handle those events.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct GlobalListener {}
|
|
||||||
|
|
||||||
impl WidgetComponent for GlobalListener {
|
|
||||||
fn view(&mut self, _properties: &Props, _frame: &mut Frame, _area: Rect) {}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A vertical separator.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct VerticalLine {
|
|
||||||
line: Widget<Label>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VerticalLine {
|
|
||||||
pub fn new(line: Widget<Label>) -> Self {
|
|
||||||
Self { line }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for VerticalLine {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
// Repeat and render line.
|
|
||||||
let overlines = vec![self.line.clone(); area.width as usize];
|
|
||||||
let overlines = overlines
|
|
||||||
.iter()
|
|
||||||
.map(|l| l.clone().to_boxed() as Box<dyn MockComponent>)
|
|
||||||
.collect();
|
|
||||||
let line_layout = layout::h_stack(overlines, area);
|
|
||||||
for (mut line, area) in line_layout {
|
|
||||||
line.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// A tab header that displays all labels horizontally aligned and separated
|
|
||||||
/// by a divider. Highlights the label defined by the current tab index.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Tabs {
|
|
||||||
tabs: Vec<Widget<Label>>,
|
|
||||||
state: TabState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tabs {
|
|
||||||
pub fn new(tabs: Vec<Widget<Label>>) -> Self {
|
|
||||||
let count = &tabs.len();
|
|
||||||
Self {
|
|
||||||
tabs,
|
|
||||||
state: TabState {
|
|
||||||
selected: 0,
|
|
||||||
len: *count as u16,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Tabs {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let selected = self.state().unwrap_one().unwrap_u16();
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
// Render tabs, highlighting the selected tab.
|
|
||||||
let mut tabs = vec![];
|
|
||||||
for (index, tab) in self.tabs.iter().enumerate() {
|
|
||||||
let mut tab = tab.clone().to_boxed();
|
|
||||||
if index == selected as usize {
|
|
||||||
tab.attr(
|
|
||||||
Attribute::TextProps,
|
|
||||||
AttrValue::TextModifiers(TextModifiers::REVERSED),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
tabs.push(tab.clone().to_boxed() as Box<dyn MockComponent>);
|
|
||||||
}
|
|
||||||
tabs.push(Widget::new(Label::default()).to_boxed());
|
|
||||||
|
|
||||||
let tab_layout = layout::h_stack(tabs, area);
|
|
||||||
for (mut tab, area) in tab_layout {
|
|
||||||
tab.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::One(StateValue::U16(self.state.selected))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
use tuirealm::command::Direction;
|
|
||||||
|
|
||||||
match cmd {
|
|
||||||
Cmd::Move(Direction::Right) => {
|
|
||||||
let prev = self.state.selected;
|
|
||||||
self.state.incr_tab_index(true);
|
|
||||||
if prev != self.state.selected {
|
|
||||||
CmdResult::Changed(self.state())
|
|
||||||
} else {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => CmdResult::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An application info widget that renders project / branch information
|
|
||||||
/// and a separator line. Used in conjunction with [`Tabs`].
|
|
||||||
pub struct AppInfo {
|
|
||||||
project: Widget<Label>,
|
|
||||||
rid: Widget<Label>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppInfo {
|
|
||||||
pub fn new(project: Widget<Label>, rid: Widget<Label>) -> Self {
|
|
||||||
Self { project, rid }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for AppInfo {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let project_w = self
|
|
||||||
.project
|
|
||||||
.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(10))
|
|
||||||
.unwrap_size();
|
|
||||||
|
|
||||||
let rid_w = self
|
|
||||||
.rid
|
|
||||||
.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(10))
|
|
||||||
.unwrap_size();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints(vec![
|
|
||||||
Constraint::Length(project_w),
|
|
||||||
Constraint::Length(rid_w),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
self.project.view(frame, layout[0]);
|
|
||||||
self.rid.view(frame, layout[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A common application header that renders project / branch
|
|
||||||
/// information and an optional navigation.
|
|
||||||
pub struct AppHeader {
|
|
||||||
nav: Option<Widget<Tabs>>,
|
|
||||||
info: Widget<AppInfo>,
|
|
||||||
line: Widget<VerticalLine>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppHeader {
|
|
||||||
pub fn new(
|
|
||||||
nav: Option<Widget<Tabs>>,
|
|
||||||
info: Widget<AppInfo>,
|
|
||||||
line: Widget<VerticalLine>,
|
|
||||||
) -> Self {
|
|
||||||
Self { nav, info, line }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for AppHeader {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
let info_w = self
|
|
||||||
.info
|
|
||||||
.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(10))
|
|
||||||
.unwrap_size();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let layout = layout::app_header(area, info_w);
|
|
||||||
|
|
||||||
if let Some(nav) = self.nav.as_mut() {
|
|
||||||
nav.view(frame, layout.nav);
|
|
||||||
}
|
|
||||||
self.info.view(frame, layout.info);
|
|
||||||
self.line.view(frame, layout.line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
self.nav
|
|
||||||
.as_mut()
|
|
||||||
.map(|nav| nav.perform(cmd))
|
|
||||||
.unwrap_or(CmdResult::None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A labeled container header.
|
|
||||||
pub struct Header<const W: usize> {
|
|
||||||
header: [Widget<Label>; W],
|
|
||||||
widths: [ColumnWidth; W],
|
|
||||||
theme: Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const W: usize> Header<W> {
|
|
||||||
pub fn new(header: [Widget<Label>; W], widths: [ColumnWidth; W], theme: Theme) -> Self {
|
|
||||||
Self {
|
|
||||||
header,
|
|
||||||
widths,
|
|
||||||
theme,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const W: usize> WidgetComponent for Header<W> {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
let focus = properties
|
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let color = if focus {
|
|
||||||
self.theme.colors.container_border_focus_fg
|
|
||||||
} else {
|
|
||||||
self.theme.colors.container_border_fg
|
|
||||||
};
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let block = HeaderBlock::default()
|
|
||||||
.borders(BorderSides::all())
|
|
||||||
.border_style(Style::default().fg(color))
|
|
||||||
.border_type(BorderType::Rounded);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(vec![Constraint::Min(1)])
|
|
||||||
.vertical_margin(1)
|
|
||||||
.horizontal_margin(1)
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
let widths = utils::column_widths(area, &self.widths, self.theme.tables.spacing);
|
|
||||||
let header: [Cell; W] = self
|
|
||||||
.header
|
|
||||||
.iter()
|
|
||||||
.map(|label| {
|
|
||||||
let cell: Cell = label.into();
|
|
||||||
cell.style(Style::default().fg(self.theme.colors.default_fg))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.try_into()
|
|
||||||
.unwrap();
|
|
||||||
let header: Row<'_> = Row::new(header);
|
|
||||||
|
|
||||||
let table = tuirealm::tui::widgets::Table::new(vec![])
|
|
||||||
.column_spacing(self.theme.tables.spacing)
|
|
||||||
.header(header)
|
|
||||||
.widths(&widths);
|
|
||||||
frame.render_widget(table, layout[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Container {
|
|
||||||
component: Box<dyn MockComponent>,
|
|
||||||
theme: Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Container {
|
|
||||||
pub fn new(component: Box<dyn MockComponent>, theme: Theme) -> Self {
|
|
||||||
Self { component, theme }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Container {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
let focus = properties
|
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let color = if focus {
|
|
||||||
self.theme.colors.container_border_focus_fg
|
|
||||||
} else {
|
|
||||||
self.theme.colors.container_border_fg
|
|
||||||
};
|
|
||||||
|
|
||||||
if display {
|
|
||||||
// Make some space on the left
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.horizontal_margin(1)
|
|
||||||
.vertical_margin(1)
|
|
||||||
.constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
|
|
||||||
.split(area);
|
|
||||||
// reverse draw order: child needs to be drawn first?
|
|
||||||
self.component.view(frame, layout[1]);
|
|
||||||
|
|
||||||
let block = Block::default()
|
|
||||||
.borders(BorderSides::ALL)
|
|
||||||
.border_style(Style::default().fg(color))
|
|
||||||
.border_type(BorderType::Rounded);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
self.component.perform(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LabeledContainer {
|
|
||||||
header: Widget<Header<1>>,
|
|
||||||
component: Box<dyn MockComponent>,
|
|
||||||
theme: Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LabeledContainer {
|
|
||||||
pub fn new(header: Widget<Header<1>>, component: Box<dyn MockComponent>, theme: Theme) -> Self {
|
|
||||||
Self {
|
|
||||||
header,
|
|
||||||
component,
|
|
||||||
theme,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for LabeledContainer {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
let focus = properties
|
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let color = if focus {
|
|
||||||
self.theme.colors.container_border_focus_fg
|
|
||||||
} else {
|
|
||||||
self.theme.colors.container_border_fg
|
|
||||||
};
|
|
||||||
|
|
||||||
let header_height = self
|
|
||||||
.header
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(3))
|
|
||||||
.unwrap_size();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(header_height), Constraint::Length(0)].as_ref())
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
// Make some space on the left
|
|
||||||
let inner_layout = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.horizontal_margin(1)
|
|
||||||
.constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
|
|
||||||
.split(layout[1]);
|
|
||||||
// reverse draw order: child needs to be drawn first?
|
|
||||||
|
|
||||||
self.component
|
|
||||||
.attr(Attribute::Focus, AttrValue::Flag(focus));
|
|
||||||
self.component.view(frame, inner_layout[1]);
|
|
||||||
|
|
||||||
let block = Block::default()
|
|
||||||
.borders(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
|
||||||
.border_style(Style::default().fg(color))
|
|
||||||
.border_type(BorderType::Rounded);
|
|
||||||
frame.render_widget(block, layout[1]);
|
|
||||||
|
|
||||||
self.header.attr(Attribute::Focus, AttrValue::Flag(focus));
|
|
||||||
self.header.view(frame, layout[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
self.component.perform(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
|
||||||
use tuirealm::props::{AttrValue, Attribute, Props};
|
|
||||||
use tuirealm::tui::layout::Rect;
|
|
||||||
use tuirealm::{Frame, MockComponent, State};
|
|
||||||
|
|
||||||
use super::label::Label;
|
|
||||||
|
|
||||||
use crate::ui::layout;
|
|
||||||
use crate::ui::widget::{Widget, WidgetComponent};
|
|
||||||
|
|
||||||
/// A shortcut that consists of a label displaying the "hotkey", a label that displays
|
|
||||||
/// the action and a spacer between them.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Shortcut {
|
|
||||||
short: Widget<Label>,
|
|
||||||
divider: Widget<Label>,
|
|
||||||
long: Widget<Label>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shortcut {
|
|
||||||
pub fn new(short: Widget<Label>, divider: Widget<Label>, long: Widget<Label>) -> Self {
|
|
||||||
Self {
|
|
||||||
short,
|
|
||||||
divider,
|
|
||||||
long,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Shortcut {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let labels: Vec<Box<dyn MockComponent>> = vec![
|
|
||||||
self.short.clone().to_boxed(),
|
|
||||||
self.divider.clone().to_boxed(),
|
|
||||||
self.long.clone().to_boxed(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let layout = layout::h_stack(labels, area);
|
|
||||||
for (mut shortcut, area) in layout {
|
|
||||||
shortcut.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A shortcut bar that displays multiple shortcuts and separates them with a
|
|
||||||
/// divider.
|
|
||||||
pub struct Shortcuts {
|
|
||||||
shortcuts: Vec<Widget<Shortcut>>,
|
|
||||||
divider: Widget<Label>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shortcuts {
|
|
||||||
pub fn new(shortcuts: Vec<Widget<Shortcut>>, divider: Widget<Label>) -> Self {
|
|
||||||
Self { shortcuts, divider }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Shortcuts {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let mut widgets: Vec<Box<dyn MockComponent>> = vec![];
|
|
||||||
let mut shortcuts = self.shortcuts.iter_mut().peekable();
|
|
||||||
|
|
||||||
while let Some(shortcut) = shortcuts.next() {
|
|
||||||
if shortcuts.peek().is_some() {
|
|
||||||
widgets.push(shortcut.clone().to_boxed());
|
|
||||||
widgets.push(self.divider.clone().to_boxed())
|
|
||||||
} else {
|
|
||||||
widgets.push(shortcut.clone().to_boxed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let layout = layout::h_stack(widgets, area);
|
|
||||||
for (mut widget, area) in layout {
|
|
||||||
widget.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContextBar {
|
|
||||||
context: Widget<Label>,
|
|
||||||
id: Widget<Label>,
|
|
||||||
author: Widget<Label>,
|
|
||||||
title: Widget<Label>,
|
|
||||||
comments: Widget<Label>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextBar {
|
|
||||||
pub fn new(
|
|
||||||
context: Widget<Label>,
|
|
||||||
id: Widget<Label>,
|
|
||||||
author: Widget<Label>,
|
|
||||||
title: Widget<Label>,
|
|
||||||
comments: Widget<Label>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
context,
|
|
||||||
id,
|
|
||||||
author,
|
|
||||||
title,
|
|
||||||
comments,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for ContextBar {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let context_w = self.context.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let id_w = self.id.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let author_w = self.author.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
let count_w = self.comments.query(Attribute::Width).unwrap().unwrap_size();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let layout = layout::h_stack(
|
|
||||||
vec![
|
|
||||||
self.context.clone().to_boxed(),
|
|
||||||
self.id.clone().to_boxed(),
|
|
||||||
self.title
|
|
||||||
.clone()
|
|
||||||
.width(
|
|
||||||
area.width
|
|
||||||
.saturating_sub(context_w + id_w + author_w + count_w),
|
|
||||||
)
|
|
||||||
.to_boxed(),
|
|
||||||
self.author.clone().to_boxed(),
|
|
||||||
self.comments.clone().to_boxed(),
|
|
||||||
],
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (mut component, area) in layout {
|
|
||||||
component.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
|
||||||
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
|
|
||||||
use tuirealm::tui::layout::Rect;
|
|
||||||
use tuirealm::tui::text::{Span, Text};
|
|
||||||
use tuirealm::{Frame, MockComponent, State};
|
|
||||||
|
|
||||||
use crate::ui::widget::{Widget, WidgetComponent};
|
|
||||||
|
|
||||||
/// A label that can be styled using a foreground color and text modifiers.
|
|
||||||
/// Its height is fixed, its width depends on the length of the text it displays.
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct Label;
|
|
||||||
|
|
||||||
impl WidgetComponent for Label {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
use tui_realm_stdlib::Label;
|
|
||||||
|
|
||||||
let content = properties
|
|
||||||
.get_or(Attribute::Content, AttrValue::String(String::default()))
|
|
||||||
.unwrap_string();
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
let foreground = properties
|
|
||||||
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
|
|
||||||
.unwrap_color();
|
|
||||||
let background = properties
|
|
||||||
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
|
|
||||||
.unwrap_color();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let mut label = match properties.get(Attribute::TextProps) {
|
|
||||||
Some(modifiers) => Label::default()
|
|
||||||
.foreground(foreground)
|
|
||||||
.background(background)
|
|
||||||
.modifiers(modifiers.unwrap_text_modifiers())
|
|
||||||
.text(content),
|
|
||||||
None => Label::default()
|
|
||||||
.foreground(foreground)
|
|
||||||
.background(background)
|
|
||||||
.text(content),
|
|
||||||
};
|
|
||||||
|
|
||||||
label.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Widget<Label>> for Span<'_> {
|
|
||||||
fn from(label: &Widget<Label>) -> Self {
|
|
||||||
let content = label
|
|
||||||
.query(Attribute::Content)
|
|
||||||
.unwrap_or(AttrValue::String(String::default()))
|
|
||||||
.unwrap_string();
|
|
||||||
|
|
||||||
Span::styled(content, Style::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Widget<Label>> for Text<'_> {
|
|
||||||
fn from(label: &Widget<Label>) -> Self {
|
|
||||||
let content = label
|
|
||||||
.query(Attribute::Content)
|
|
||||||
.unwrap_or(AttrValue::String(String::default()))
|
|
||||||
.unwrap_string();
|
|
||||||
let foreground = label
|
|
||||||
.query(Attribute::Foreground)
|
|
||||||
.unwrap_or(AttrValue::Color(Color::Reset))
|
|
||||||
.unwrap_color();
|
|
||||||
|
|
||||||
Text::styled(content, Style::default().fg(foreground))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,376 +0,0 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
|
||||||
use tuirealm::props::{AttrValue, Attribute, BorderSides, BorderType, Color, Props, Style};
|
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
|
||||||
use tuirealm::tui::widgets::{Block, Cell, ListState, Row, TableState};
|
|
||||||
use tuirealm::{Frame, MockComponent, State, StateValue};
|
|
||||||
|
|
||||||
use crate::ui::layout;
|
|
||||||
use crate::ui::state::ItemState;
|
|
||||||
use crate::ui::theme::Theme;
|
|
||||||
use crate::ui::widget::{utils, Widget, WidgetComponent};
|
|
||||||
|
|
||||||
use super::container::Header;
|
|
||||||
use super::label::Label;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// A generic item that can be displayed in a table with [`const W: usize`] columns.
|
|
||||||
pub trait TableItem<const W: usize> {
|
|
||||||
/// Should return fields as table cells.
|
|
||||||
fn row(&self, theme: &Theme) -> [Cell; W];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A generic item that can be displayed in a list.
|
|
||||||
pub trait ListItem {
|
|
||||||
/// Should return fields as list item.
|
|
||||||
fn row(&self, theme: &Theme) -> tuirealm::tui::widgets::ListItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Grow behavior of a table column.
|
|
||||||
///
|
|
||||||
/// [`tuirealm::tui::widgets::Table`] does only support percental column widths.
|
|
||||||
/// A [`ColumnWidth`] is used to specify the grow behaviour of a table column
|
|
||||||
/// and a percental column width is calculated based on that.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum ColumnWidth {
|
|
||||||
/// A fixed-size column.
|
|
||||||
Fixed(u16),
|
|
||||||
/// A growable column.
|
|
||||||
Grow,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A component that displays a labeled property.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Property {
|
|
||||||
name: Widget<Label>,
|
|
||||||
divider: Widget<Label>,
|
|
||||||
value: Widget<Label>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Property {
|
|
||||||
pub fn new(name: Widget<Label>, value: Widget<Label>) -> Self {
|
|
||||||
let divider = label("");
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
divider,
|
|
||||||
value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_divider(mut self, divider: Widget<Label>) -> Self {
|
|
||||||
self.divider = divider;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &Widget<Label> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn value(&self) -> &Widget<Label> {
|
|
||||||
&self.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Property {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let labels: Vec<Box<dyn MockComponent>> = vec![
|
|
||||||
self.name.clone().to_boxed(),
|
|
||||||
self.divider.clone().to_boxed(),
|
|
||||||
self.value.clone().to_boxed(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let layout = layout::h_stack(labels, area);
|
|
||||||
for (mut label, area) in layout {
|
|
||||||
label.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A component that can display lists of labeled properties
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct PropertyList {
|
|
||||||
properties: Vec<Widget<Property>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PropertyList {
|
|
||||||
pub fn new(properties: Vec<Widget<Property>>) -> Self {
|
|
||||||
Self { properties }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for PropertyList {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let properties = self
|
|
||||||
.properties
|
|
||||||
.iter()
|
|
||||||
.map(|property| property.clone().to_boxed() as Box<dyn MockComponent>)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let layout = layout::v_stack(properties, area);
|
|
||||||
for (mut property, area) in layout {
|
|
||||||
property.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PropertyTable {
|
|
||||||
properties: Vec<Widget<Property>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PropertyTable {
|
|
||||||
pub fn new(properties: Vec<Widget<Property>>) -> Self {
|
|
||||||
Self { properties }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for PropertyTable {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
use tuirealm::tui::widgets::Table;
|
|
||||||
|
|
||||||
let display = properties
|
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
if display {
|
|
||||||
let rows = self
|
|
||||||
.properties
|
|
||||||
.iter()
|
|
||||||
.map(|p| Row::new([Cell::from(p.name()), Cell::from(p.value())]));
|
|
||||||
|
|
||||||
let table = Table::new(rows)
|
|
||||||
.widths([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref());
|
|
||||||
frame.render_widget(table, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A table component that can display a list of [`TableItem`]s.
|
|
||||||
pub struct Table<V, const W: usize>
|
|
||||||
where
|
|
||||||
V: TableItem<W> + Clone,
|
|
||||||
{
|
|
||||||
/// Items hold by this model.
|
|
||||||
items: Vec<V>,
|
|
||||||
/// The table header.
|
|
||||||
header: [Widget<Label>; W],
|
|
||||||
/// Grow behavior of table columns.
|
|
||||||
widths: [ColumnWidth; W],
|
|
||||||
/// State that keeps track of the selection.
|
|
||||||
state: ItemState,
|
|
||||||
/// The current theme.
|
|
||||||
theme: Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V, const W: usize> Table<V, W>
|
|
||||||
where
|
|
||||||
V: TableItem<W> + Clone,
|
|
||||||
{
|
|
||||||
pub fn new(
|
|
||||||
items: &[V],
|
|
||||||
header: [Widget<Label>; W],
|
|
||||||
widths: [ColumnWidth; W],
|
|
||||||
theme: Theme,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
items: items.to_vec(),
|
|
||||||
header,
|
|
||||||
widths,
|
|
||||||
state: ItemState::new(Some(0), items.len()),
|
|
||||||
theme,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V, const W: usize> WidgetComponent for Table<V, W>
|
|
||||||
where
|
|
||||||
V: TableItem<W> + Clone,
|
|
||||||
{
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let highlight = properties
|
|
||||||
.get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
|
|
||||||
.unwrap_color();
|
|
||||||
|
|
||||||
let focus = properties
|
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let color = if focus {
|
|
||||||
self.theme.colors.container_border_focus_fg
|
|
||||||
} else {
|
|
||||||
self.theme.colors.container_border_fg
|
|
||||||
};
|
|
||||||
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(vec![Constraint::Length(3), Constraint::Min(1)])
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
let widths = utils::column_widths(area, &self.widths, self.theme.tables.spacing);
|
|
||||||
let rows: Vec<Row<'_>> = self
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.map(|item| Row::new(item.row(&self.theme)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let table = tuirealm::tui::widgets::Table::new(rows)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
|
||||||
.border_style(Style::default().fg(color))
|
|
||||||
.border_type(BorderType::Rounded),
|
|
||||||
)
|
|
||||||
.highlight_style(Style::default().bg(highlight))
|
|
||||||
.column_spacing(self.theme.tables.spacing)
|
|
||||||
.widths(&widths);
|
|
||||||
|
|
||||||
let mut header = Widget::new(Header::new(
|
|
||||||
self.header.clone(),
|
|
||||||
self.widths,
|
|
||||||
self.theme.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
header.attr(Attribute::Focus, AttrValue::Flag(focus));
|
|
||||||
header.view(frame, layout[0]);
|
|
||||||
|
|
||||||
frame.render_stateful_widget(table, layout[1], &mut TableState::from(&self.state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
use tuirealm::command::Direction;
|
|
||||||
match cmd {
|
|
||||||
Cmd::Move(Direction::Up) => match self.state.select_previous() {
|
|
||||||
Some(selected) => CmdResult::Changed(State::One(StateValue::Usize(selected))),
|
|
||||||
None => CmdResult::None,
|
|
||||||
},
|
|
||||||
Cmd::Move(Direction::Down) => match self.state.select_next() {
|
|
||||||
Some(selected) => CmdResult::Changed(State::One(StateValue::Usize(selected))),
|
|
||||||
None => CmdResult::None,
|
|
||||||
},
|
|
||||||
Cmd::Submit => match self.state.selected() {
|
|
||||||
Some(selected) => CmdResult::Submit(State::One(StateValue::Usize(selected))),
|
|
||||||
None => CmdResult::None,
|
|
||||||
},
|
|
||||||
_ => CmdResult::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A list component that can display [`ListItem`]'s.
|
|
||||||
pub struct List<V>
|
|
||||||
where
|
|
||||||
V: ListItem + Clone + PartialEq,
|
|
||||||
{
|
|
||||||
/// Items held by this list.
|
|
||||||
items: Vec<V>,
|
|
||||||
/// State keeps track of the current selection.
|
|
||||||
state: ItemState,
|
|
||||||
/// The current theme.
|
|
||||||
theme: Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> List<V>
|
|
||||||
where
|
|
||||||
V: ListItem + Clone + PartialEq,
|
|
||||||
{
|
|
||||||
pub fn new(items: &[V], selected: Option<V>, theme: Theme) -> Self {
|
|
||||||
let selected = match selected {
|
|
||||||
Some(item) => items.iter().position(|i| i == &item),
|
|
||||||
None => Some(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
items: items.to_vec(),
|
|
||||||
state: ItemState::new(selected, items.len()),
|
|
||||||
theme,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> WidgetComponent for List<V>
|
|
||||||
where
|
|
||||||
V: ListItem + Clone + PartialEq,
|
|
||||||
{
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
use tuirealm::tui::widgets::{List, ListItem};
|
|
||||||
|
|
||||||
let highlight = properties
|
|
||||||
.get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
|
|
||||||
.unwrap_color();
|
|
||||||
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(vec![Constraint::Min(1), Constraint::Length(1)])
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
let rows: Vec<ListItem> = self
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.map(|item| item.row(&self.theme))
|
|
||||||
.collect();
|
|
||||||
let list = List::new(rows).highlight_style(Style::default().bg(highlight));
|
|
||||||
|
|
||||||
frame.render_stateful_widget(list, layout[0], &mut ListState::from(&self.state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
use tuirealm::command::Direction;
|
|
||||||
match cmd {
|
|
||||||
Cmd::Move(Direction::Up) => match self.state.select_previous() {
|
|
||||||
Some(selected) => CmdResult::Changed(State::One(StateValue::Usize(selected))),
|
|
||||||
None => CmdResult::None,
|
|
||||||
},
|
|
||||||
Cmd::Move(Direction::Down) => match self.state.select_next() {
|
|
||||||
Some(selected) => CmdResult::Changed(State::One(StateValue::Usize(selected))),
|
|
||||||
None => CmdResult::None,
|
|
||||||
},
|
|
||||||
Cmd::Submit => match self.state.selected() {
|
|
||||||
Some(selected) => CmdResult::Submit(State::One(StateValue::Usize(selected))),
|
|
||||||
None => CmdResult::None,
|
|
||||||
},
|
|
||||||
_ => CmdResult::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,284 +0,0 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
|
||||||
use tuirealm::tui::layout::Rect;
|
|
||||||
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};
|
|
||||||
|
|
||||||
use super::common;
|
|
||||||
use super::common::container::{LabeledContainer, Tabs};
|
|
||||||
use super::common::context::Shortcuts;
|
|
||||||
use super::common::list::{ColumnWidth, Table};
|
|
||||||
|
|
||||||
use super::{Widget, WidgetComponent};
|
|
||||||
|
|
||||||
use crate::cob;
|
|
||||||
use crate::ui::cob::{IssueItem, PatchItem};
|
|
||||||
use crate::ui::context::Context;
|
|
||||||
use crate::ui::layout;
|
|
||||||
use crate::ui::theme::Theme;
|
|
||||||
|
|
||||||
pub struct Dashboard {
|
|
||||||
about: Widget<LabeledContainer>,
|
|
||||||
shortcuts: Widget<Shortcuts>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dashboard {
|
|
||||||
pub fn new(about: Widget<LabeledContainer>, shortcuts: Widget<Shortcuts>) -> Self {
|
|
||||||
Self { about, shortcuts }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Dashboard {
|
|
||||||
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let shortcuts_h = self
|
|
||||||
.shortcuts
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let layout = layout::root_component(area, shortcuts_h);
|
|
||||||
|
|
||||||
self.about.view(frame, layout[0]);
|
|
||||||
self.shortcuts.view(frame, layout[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IssueBrowser {
|
|
||||||
items: Vec<IssueItem>,
|
|
||||||
table: Widget<Table<IssueItem, 7>>,
|
|
||||||
shortcuts: Widget<Shortcuts>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IssueBrowser {
|
|
||||||
pub fn new(context: &Context, theme: &Theme, shortcuts: Widget<Shortcuts>) -> Self {
|
|
||||||
let header = [
|
|
||||||
common::label(" ● "),
|
|
||||||
common::label("ID"),
|
|
||||||
common::label("Title"),
|
|
||||||
common::label("Author"),
|
|
||||||
common::label("Labels"),
|
|
||||||
common::label("Assignees"),
|
|
||||||
common::label("Opened"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let widths = [
|
|
||||||
ColumnWidth::Fixed(3),
|
|
||||||
ColumnWidth::Fixed(7),
|
|
||||||
ColumnWidth::Grow,
|
|
||||||
ColumnWidth::Fixed(21),
|
|
||||||
ColumnWidth::Fixed(25),
|
|
||||||
ColumnWidth::Fixed(21),
|
|
||||||
ColumnWidth::Fixed(18),
|
|
||||||
];
|
|
||||||
|
|
||||||
let repo = context.repository();
|
|
||||||
let mut items = vec![];
|
|
||||||
|
|
||||||
if let Ok(issues) = cob::issue::all(repo) {
|
|
||||||
for (id, issue) in issues {
|
|
||||||
if let Ok(item) = IssueItem::try_from((context.profile(), repo, id, issue)) {
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.sort_by(|a, b| b.timestamp().cmp(a.timestamp()));
|
|
||||||
items.sort_by(|a, b| b.state().cmp(a.state()));
|
|
||||||
|
|
||||||
let table = Widget::new(Table::new(&items, header, widths, theme.clone()))
|
|
||||||
.highlight(theme.colors.item_list_highlighted_bg);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
items,
|
|
||||||
table,
|
|
||||||
shortcuts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn items(&self) -> &Vec<IssueItem> {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for IssueBrowser {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let shortcuts_h = self
|
|
||||||
.shortcuts
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let focus = properties
|
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let layout = layout::root_component(area, shortcuts_h);
|
|
||||||
|
|
||||||
self.table.attr(Attribute::Focus, AttrValue::Flag(focus));
|
|
||||||
self.table.view(frame, layout[0]);
|
|
||||||
self.shortcuts.view(frame, layout[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
self.table.state()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
self.table.perform(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PatchBrowser {
|
|
||||||
items: Vec<PatchItem>,
|
|
||||||
table: Widget<Table<PatchItem, 8>>,
|
|
||||||
shortcuts: Widget<Shortcuts>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PatchBrowser {
|
|
||||||
pub fn new(context: &Context, theme: &Theme, shortcuts: Widget<Shortcuts>) -> Self {
|
|
||||||
let header = [
|
|
||||||
common::label(" ● "),
|
|
||||||
common::label("ID"),
|
|
||||||
common::label("Title"),
|
|
||||||
common::label("Author"),
|
|
||||||
common::label("Head"),
|
|
||||||
common::label("+"),
|
|
||||||
common::label("-"),
|
|
||||||
common::label("Updated"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let widths = [
|
|
||||||
ColumnWidth::Fixed(3),
|
|
||||||
ColumnWidth::Fixed(7),
|
|
||||||
ColumnWidth::Grow,
|
|
||||||
ColumnWidth::Fixed(21),
|
|
||||||
ColumnWidth::Fixed(7),
|
|
||||||
ColumnWidth::Fixed(4),
|
|
||||||
ColumnWidth::Fixed(4),
|
|
||||||
ColumnWidth::Fixed(18),
|
|
||||||
];
|
|
||||||
|
|
||||||
let repo = context.repository();
|
|
||||||
let mut items = vec![];
|
|
||||||
|
|
||||||
if let Ok(patches) = cob::patch::all(repo) {
|
|
||||||
for (id, patch) in patches {
|
|
||||||
if let Ok(item) = PatchItem::try_from((context.profile(), repo, id, patch)) {
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.sort_by(|a, b| b.timestamp().cmp(a.timestamp()));
|
|
||||||
items.sort_by(|a, b| a.state().cmp(b.state()));
|
|
||||||
|
|
||||||
let table = Widget::new(Table::new(&items, header, widths, theme.clone()))
|
|
||||||
.highlight(theme.colors.item_list_highlighted_bg);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
items,
|
|
||||||
table,
|
|
||||||
shortcuts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn items(&self) -> &Vec<PatchItem> {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for PatchBrowser {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let shortcuts_h = self
|
|
||||||
.shortcuts
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let focus = properties
|
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
let layout = layout::root_component(area, shortcuts_h);
|
|
||||||
|
|
||||||
self.table.attr(Attribute::Focus, AttrValue::Flag(focus));
|
|
||||||
self.table.view(frame, layout[0]);
|
|
||||||
self.shortcuts.view(frame, layout[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
self.table.state()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
self.table.perform(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn navigation(theme: &Theme) -> Widget<Tabs> {
|
|
||||||
common::tabs(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::reversable_label("dashboard").foreground(theme.colors.tabs_highlighted_fg),
|
|
||||||
common::reversable_label("issues").foreground(theme.colors.tabs_highlighted_fg),
|
|
||||||
common::reversable_label("patches").foreground(theme.colors.tabs_highlighted_fg),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dashboard(context: &Context, theme: &Theme) -> Widget<Dashboard> {
|
|
||||||
let about = common::labeled_container(
|
|
||||||
theme,
|
|
||||||
"about",
|
|
||||||
common::property_list(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::property(theme, "id", &context.id().to_string()),
|
|
||||||
common::property(theme, "name", context.project().name()),
|
|
||||||
common::property(theme, "description", context.project().description()),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.to_boxed(),
|
|
||||||
);
|
|
||||||
let shortcuts = common::shortcuts(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::shortcut(theme, "tab", "section"),
|
|
||||||
common::shortcut(theme, "q", "quit"),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
let dashboard = Dashboard::new(about, shortcuts);
|
|
||||||
|
|
||||||
Widget::new(dashboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn patches(context: &Context, theme: &Theme) -> Widget<PatchBrowser> {
|
|
||||||
let shortcuts = common::shortcuts(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::shortcut(theme, "tab", "section"),
|
|
||||||
common::shortcut(theme, "↑/↓", "navigate"),
|
|
||||||
common::shortcut(theme, "enter", "show"),
|
|
||||||
common::shortcut(theme, "q", "quit"),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget::new(PatchBrowser::new(context, theme, shortcuts))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn issues(context: &Context, theme: &Theme) -> Widget<IssueBrowser> {
|
|
||||||
let shortcuts = common::shortcuts(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::shortcut(theme, "tab", "section"),
|
|
||||||
common::shortcut(theme, "↑/↓", "navigate"),
|
|
||||||
common::shortcut(theme, "enter", "show"),
|
|
||||||
common::shortcut(theme, "q", "quit"),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget::new(IssueBrowser::new(context, theme, shortcuts))
|
|
||||||
}
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
use radicle_cli::terminal::format;
|
|
||||||
|
|
||||||
use radicle::cob::issue::Issue;
|
|
||||||
use radicle::cob::issue::IssueId;
|
|
||||||
use radicle::Profile;
|
|
||||||
use tuirealm::props::Color;
|
|
||||||
|
|
||||||
use super::common::container::Container;
|
|
||||||
use super::common::container::LabeledContainer;
|
|
||||||
use super::common::list::List;
|
|
||||||
use super::common::list::Property;
|
|
||||||
use super::Widget;
|
|
||||||
|
|
||||||
use crate::ui::cob;
|
|
||||||
use crate::ui::cob::IssueItem;
|
|
||||||
use crate::ui::context::Context;
|
|
||||||
use crate::ui::theme::Theme;
|
|
||||||
use crate::ui::widget::common::context::ContextBar;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub struct LargeList {
|
|
||||||
items: Vec<IssueItem>,
|
|
||||||
list: Widget<LabeledContainer>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LargeList {
|
|
||||||
pub fn new(context: &Context, theme: &Theme, selected: Option<(IssueId, Issue)>) -> Self {
|
|
||||||
let repo = context.repository();
|
|
||||||
let issues = crate::cob::issue::all(repo).unwrap_or_default();
|
|
||||||
let mut items = issues
|
|
||||||
.iter()
|
|
||||||
.map(|(id, issue)| IssueItem::from((context.profile(), repo, *id, issue.clone())))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
items.sort_by(|a, b| b.timestamp().cmp(a.timestamp()));
|
|
||||||
items.sort_by(|a, b| b.state().cmp(a.state()));
|
|
||||||
|
|
||||||
let selected =
|
|
||||||
selected.map(|(id, issue)| IssueItem::from((context.profile(), repo, id, issue)));
|
|
||||||
|
|
||||||
let list = Widget::new(List::new(&items, selected, theme.clone()))
|
|
||||||
.highlight(theme.colors.item_list_highlighted_bg);
|
|
||||||
|
|
||||||
let container = common::labeled_container(theme, "Issues", list.to_boxed());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
items,
|
|
||||||
list: container,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn items(&self) -> &Vec<IssueItem> {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for LargeList {
|
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let focus = properties
|
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
|
||||||
.unwrap_flag();
|
|
||||||
|
|
||||||
self.list.attr(Attribute::Focus, AttrValue::Flag(focus));
|
|
||||||
self.list.view(frame, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
|
||||||
self.list.perform(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Details {
|
|
||||||
container: Widget<Container>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Details {
|
|
||||||
pub fn new(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Self {
|
|
||||||
let repo = context.repository();
|
|
||||||
|
|
||||||
let (id, issue) = issue;
|
|
||||||
let item = IssueItem::from((context.profile(), repo, id, issue));
|
|
||||||
|
|
||||||
let title = Property::new(
|
|
||||||
common::label("Title").foreground(theme.colors.property_name_fg),
|
|
||||||
common::label(item.title()).foreground(theme.colors.browser_list_title),
|
|
||||||
);
|
|
||||||
|
|
||||||
let labels = Property::new(
|
|
||||||
common::label("Labels").foreground(theme.colors.property_name_fg),
|
|
||||||
common::label(&cob::format_labels(item.labels()))
|
|
||||||
.foreground(theme.colors.browser_list_tags),
|
|
||||||
);
|
|
||||||
|
|
||||||
let assignees = Property::new(
|
|
||||||
common::label("Assignees").foreground(theme.colors.property_name_fg),
|
|
||||||
common::label(&cob::format_assignees(
|
|
||||||
&item
|
|
||||||
.assignees()
|
|
||||||
.iter()
|
|
||||||
.map(|item| (item.did(), item.is_you()))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
))
|
|
||||||
.foreground(theme.colors.browser_list_author),
|
|
||||||
);
|
|
||||||
|
|
||||||
let state = Property::new(
|
|
||||||
common::label("Status").foreground(theme.colors.property_name_fg),
|
|
||||||
common::label(&item.state().to_string()).foreground(theme.colors.browser_list_title),
|
|
||||||
);
|
|
||||||
|
|
||||||
let table = common::property_table(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
Widget::new(title),
|
|
||||||
Widget::new(labels),
|
|
||||||
Widget::new(assignees),
|
|
||||||
Widget::new(state),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
let container = common::container(theme, table.to_boxed());
|
|
||||||
|
|
||||||
Self { container }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Details {
|
|
||||||
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
self.container.view(frame, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget<LargeList> {
|
|
||||||
let list = LargeList::new(context, theme, Some(issue));
|
|
||||||
|
|
||||||
Widget::new(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn details(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget<Details> {
|
|
||||||
let details = Details::new(context, theme, issue);
|
|
||||||
Widget::new(details)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn context(theme: &Theme, issue: (IssueId, &Issue), profile: &Profile) -> Widget<ContextBar> {
|
|
||||||
let (id, issue) = issue;
|
|
||||||
let is_you = *issue.author().id() == profile.did();
|
|
||||||
|
|
||||||
let id = format::cob(&id);
|
|
||||||
let title = issue.title();
|
|
||||||
let author = cob::format_author(issue.author().id(), is_you);
|
|
||||||
let comments = issue.comments().count();
|
|
||||||
|
|
||||||
let context = common::label(" issue ").background(theme.colors.context_badge_bg);
|
|
||||||
let id = common::label(&format!(" {id} "))
|
|
||||||
.foreground(theme.colors.context_id_fg)
|
|
||||||
.background(theme.colors.context_id_bg);
|
|
||||||
let title = common::label(&format!(" {title} "))
|
|
||||||
.foreground(theme.colors.default_fg)
|
|
||||||
.background(theme.colors.context_bg);
|
|
||||||
let author = common::label(&format!(" {author} "))
|
|
||||||
.foreground(theme.colors.context_id_author_fg)
|
|
||||||
.background(theme.colors.context_bg);
|
|
||||||
let comments = common::label(&format!(" {comments} "))
|
|
||||||
.foreground(Color::Rgb(70, 70, 70))
|
|
||||||
.background(theme.colors.context_light_bg);
|
|
||||||
|
|
||||||
let context_bar = ContextBar::new(context, id, author, title, comments);
|
|
||||||
|
|
||||||
Widget::new(context_bar).height(1)
|
|
||||||
}
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
use radicle::cob::patch::{Patch, PatchId};
|
|
||||||
use radicle::Profile;
|
|
||||||
|
|
||||||
use radicle_cli::terminal::format;
|
|
||||||
|
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
|
||||||
use tuirealm::props::Color;
|
|
||||||
use tuirealm::tui::layout::Rect;
|
|
||||||
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};
|
|
||||||
|
|
||||||
use super::{Widget, WidgetComponent};
|
|
||||||
|
|
||||||
use super::common;
|
|
||||||
use super::common::container::Tabs;
|
|
||||||
use super::common::context::{ContextBar, Shortcuts};
|
|
||||||
use super::common::label::Label;
|
|
||||||
|
|
||||||
use crate::ui::theme::Theme;
|
|
||||||
use crate::ui::{cob, layout};
|
|
||||||
|
|
||||||
pub struct Activity {
|
|
||||||
label: Widget<Label>,
|
|
||||||
context: Widget<ContextBar>,
|
|
||||||
shortcuts: Widget<Shortcuts>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Activity {
|
|
||||||
pub fn new(
|
|
||||||
label: Widget<Label>,
|
|
||||||
context: Widget<ContextBar>,
|
|
||||||
shortcuts: Widget<Shortcuts>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
label,
|
|
||||||
context,
|
|
||||||
shortcuts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Activity {
|
|
||||||
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let label_w = self
|
|
||||||
.label
|
|
||||||
.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(1))
|
|
||||||
.unwrap_size();
|
|
||||||
let context_h = self
|
|
||||||
.context
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let shortcuts_h = self
|
|
||||||
.shortcuts
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let layout = layout::root_component_with_context(area, context_h, shortcuts_h);
|
|
||||||
|
|
||||||
self.label
|
|
||||||
.view(frame, layout::centered_label(label_w, layout[0]));
|
|
||||||
self.context.view(frame, layout[1]);
|
|
||||||
self.shortcuts.view(frame, layout[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Files {
|
|
||||||
label: Widget<Label>,
|
|
||||||
context: Widget<ContextBar>,
|
|
||||||
shortcuts: Widget<Shortcuts>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Files {
|
|
||||||
pub fn new(
|
|
||||||
label: Widget<Label>,
|
|
||||||
context: Widget<ContextBar>,
|
|
||||||
shortcuts: Widget<Shortcuts>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
label,
|
|
||||||
context,
|
|
||||||
shortcuts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetComponent for Files {
|
|
||||||
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
|
||||||
let label_w = self
|
|
||||||
.label
|
|
||||||
.query(Attribute::Width)
|
|
||||||
.unwrap_or(AttrValue::Size(1))
|
|
||||||
.unwrap_size();
|
|
||||||
let context_h = self
|
|
||||||
.context
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let shortcuts_h = self
|
|
||||||
.shortcuts
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(0))
|
|
||||||
.unwrap_size();
|
|
||||||
let layout = layout::root_component_with_context(area, context_h, shortcuts_h);
|
|
||||||
|
|
||||||
self.label
|
|
||||||
.view(frame, layout::centered_label(label_w, layout[0]));
|
|
||||||
self.context.view(frame, layout[1]);
|
|
||||||
self.shortcuts.view(frame, layout[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
|
||||||
CmdResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn navigation(theme: &Theme) -> Widget<Tabs> {
|
|
||||||
common::tabs(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::reversable_label("activity").foreground(theme.colors.tabs_highlighted_fg),
|
|
||||||
common::reversable_label("files").foreground(theme.colors.tabs_highlighted_fg),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activity(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<Activity> {
|
|
||||||
let (id, patch) = patch;
|
|
||||||
let shortcuts = common::shortcuts(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::shortcut(theme, "esc", "back"),
|
|
||||||
common::shortcut(theme, "tab", "section"),
|
|
||||||
common::shortcut(theme, "q", "quit"),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
let context = context(theme, (id, patch), profile);
|
|
||||||
|
|
||||||
let not_implemented = common::label("not implemented").foreground(theme.colors.default_fg);
|
|
||||||
let activity = Activity::new(not_implemented, context, shortcuts);
|
|
||||||
|
|
||||||
Widget::new(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn files(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<Activity> {
|
|
||||||
let (id, patch) = patch;
|
|
||||||
let shortcuts = common::shortcuts(
|
|
||||||
theme,
|
|
||||||
vec![
|
|
||||||
common::shortcut(theme, "esc", "back"),
|
|
||||||
common::shortcut(theme, "tab", "section"),
|
|
||||||
common::shortcut(theme, "q", "quit"),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
let context = context(theme, (id, patch), profile);
|
|
||||||
|
|
||||||
let not_implemented = common::label("not implemented").foreground(theme.colors.default_fg);
|
|
||||||
let files = Activity::new(not_implemented, context, shortcuts);
|
|
||||||
|
|
||||||
Widget::new(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn context(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<ContextBar> {
|
|
||||||
let (id, patch) = patch;
|
|
||||||
let (_, rev) = patch.latest();
|
|
||||||
let is_you = *patch.author().id() == profile.did();
|
|
||||||
|
|
||||||
let id = format::cob(&id);
|
|
||||||
let title = patch.title();
|
|
||||||
let author = cob::format_author(patch.author().id(), is_you);
|
|
||||||
let comments = rev.discussion().len();
|
|
||||||
|
|
||||||
let context = common::label(" patch ").background(theme.colors.context_badge_bg);
|
|
||||||
let id = common::label(&format!(" {id} "))
|
|
||||||
.foreground(theme.colors.context_id_fg)
|
|
||||||
.background(theme.colors.context_id_bg);
|
|
||||||
let title = common::label(&format!(" {title} "))
|
|
||||||
.foreground(theme.colors.default_fg)
|
|
||||||
.background(theme.colors.context_bg);
|
|
||||||
let author = common::label(&format!(" {author} "))
|
|
||||||
.foreground(theme.colors.context_id_author_fg)
|
|
||||||
.background(theme.colors.context_bg);
|
|
||||||
let comments = common::label(&format!(" {comments} "))
|
|
||||||
.foreground(Color::Rgb(70, 70, 70))
|
|
||||||
.background(theme.colors.context_light_bg);
|
|
||||||
|
|
||||||
let context_bar = ContextBar::new(context, id, author, title, comments);
|
|
||||||
|
|
||||||
Widget::new(context_bar).height(1)
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
use tuirealm::tui::layout::{Constraint, Rect};
|
|
||||||
|
|
||||||
use super::common::list::ColumnWidth;
|
|
||||||
|
|
||||||
/// Calculates `Constraint::Percentage` for each fixed column width in `widths`,
|
|
||||||
/// taking into account the available width in `area` and the column spacing given by `spacing`.
|
|
||||||
pub fn column_widths(area: Rect, widths: &[ColumnWidth], spacing: u16) -> Vec<Constraint> {
|
|
||||||
let total_spacing = spacing.saturating_mul(widths.len() as u16);
|
|
||||||
let fixed_width = widths
|
|
||||||
.iter()
|
|
||||||
.fold(0u16, |total, &width| match width {
|
|
||||||
ColumnWidth::Fixed(w) => total + w,
|
|
||||||
ColumnWidth::Grow => total,
|
|
||||||
})
|
|
||||||
.saturating_add(total_spacing);
|
|
||||||
|
|
||||||
let grow_count = widths.iter().fold(0u16, |count, &w| {
|
|
||||||
if w == ColumnWidth::Grow {
|
|
||||||
count + 1
|
|
||||||
} else {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let grow_width = area
|
|
||||||
.width
|
|
||||||
.saturating_sub(fixed_width)
|
|
||||||
.checked_div(grow_count)
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
widths
|
|
||||||
.iter()
|
|
||||||
.map(|width| match width {
|
|
||||||
ColumnWidth::Fixed(w) => {
|
|
||||||
let p: f64 = *w as f64 / area.width as f64 * 100_f64;
|
|
||||||
Constraint::Percentage(p.ceil() as u16)
|
|
||||||
}
|
|
||||||
ColumnWidth::Grow => {
|
|
||||||
let p: f64 = grow_width as f64 / area.width as f64 * 100_f64;
|
|
||||||
Constraint::Percentage(p.floor() as u16)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue