Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Remove `radicle-tui` crate
cloudhead committed 2 years ago
commit 1b9906db099c8edd0781058994ecab90d1d0a3c7
parent b456d3a401abf5314598cbede49c4fb2864bdf6f
29 files changed +2 -4266
modified Cargo.lock
@@ -40,17 +40,6 @@ dependencies = [

[[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]]
-
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
@@ -421,12 +410,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"

[[package]]
-
name = "cassowary"
-
version = "0.3.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
-

-
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1053,9 +1036,6 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-
dependencies = [
-
 "ahash 0.7.6",
-
]

[[package]]
name = "hashbrown"
@@ -1063,7 +1043,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
-
 "ahash 0.8.3",
+
 "ahash",
]

[[package]]
@@ -1308,15 +1288,6 @@ dependencies = [
]

[[package]]
-
name = "isolang"
-
version = "2.3.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "f80f221db1bc708b71128757b9396727c04de86968081e18e89b0575e03be071"
-
dependencies = [
-
 "phf",
-
]
-

-
[[package]]
name = "itoa"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1361,29 +1332,6 @@ dependencies = [
]

[[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]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1737,24 +1685,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "pin-project"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1969,7 +1899,7 @@ dependencies = [
 "similar",
 "tempfile",
 "thiserror",
-
 "timeago 0.3.1",
+
 "timeago",
 "ureq",
 "zeroize",
]
@@ -2203,21 +2133,6 @@ dependencies = [
]

[[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]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2635,12 +2550,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"

[[package]]
-
name = "smawk"
-
version = "0.3.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
-

-
[[package]]
name = "snapbox"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2854,17 +2763,6 @@ dependencies = [
]

[[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]]
name = "thiserror"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2939,16 +2837,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3135,55 +3023,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3202,16 +3041,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified Cargo.toml
@@ -12,7 +12,6 @@ members = [
  "radicle-remote-helper",
  "radicle-ssh",
  "radicle-tools",
-
  "radicle-tui",
]
default-members = [
  "radicle",
deleted radicle-tui/Cargo.toml
@@ -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"
deleted radicle-tui/src/app.rs
@@ -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
-
    }
-
}
deleted radicle-tui/src/app/event.rs
@@ -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
-
    }
-
}
deleted radicle-tui/src/app/page.rs
@@ -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,
-
        }
-
    }
-
}
deleted radicle-tui/src/app/subscription.rs
@@ -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),
-
    ]
-
}
deleted radicle-tui/src/cob.rs
@@ -1,2 +0,0 @@
-
pub mod issue;
-
pub mod patch;
deleted radicle-tui/src/cob/issue.rs
@@ -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)?)
-
}
deleted radicle-tui/src/cob/patch.rs
@@ -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)?)
-
}
deleted radicle-tui/src/lib.rs
@@ -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(())
-
    }
-
}
deleted radicle-tui/src/main.rs
@@ -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);
-
    }
-
}
deleted radicle-tui/src/ui.rs
@@ -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;
deleted radicle-tui/src/ui/cob.rs
@@ -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
-
}
deleted radicle-tui/src/ui/context.rs
@@ -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
-
    }
-
}
deleted radicle-tui/src/ui/ext.rs
@@ -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);
-
        }
-
    }
-
}
deleted radicle-tui/src/ui/layout.rs
@@ -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],
-
    }
-
}
deleted radicle-tui/src/ui/state.rs
@@ -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
-
    }
-
}
deleted radicle-tui/src/ui/theme.rs
@@ -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 },
-
    }
-
}
deleted radicle-tui/src/ui/widget.rs
@@ -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)
-
    }
-
}
deleted radicle-tui/src/ui/widget/common.rs
@@ -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)
-
}
deleted radicle-tui/src/ui/widget/common/container.rs
@@ -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)
-
    }
-
}
deleted radicle-tui/src/ui/widget/common/context.rs
@@ -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
-
    }
-
}
deleted radicle-tui/src/ui/widget/common/label.rs
@@ -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))
-
    }
-
}
deleted radicle-tui/src/ui/widget/common/list.rs
@@ -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,
-
        }
-
    }
-
}
deleted radicle-tui/src/ui/widget/home.rs
@@ -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))
-
}
deleted radicle-tui/src/ui/widget/issue.rs
@@ -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)
-
}
deleted radicle-tui/src/ui/widget/patch.rs
@@ -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)
-
}
deleted radicle-tui/src/ui/widget/utils.rs
@@ -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()
-
}