Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Release preparations
Merged did:key:z6MkgFq6...nBGz opened 3 months ago
  • README: Better cover image
  • lib: Allow to focus a container section with numpad
  • lib: Improve default theming
9 files changed +121 -116 1f2ecd79 46c931c4
modified README.md
@@ -1,6 +1,6 @@
-
# radicle-tui
+
![Screenshot](./demo.jpg "Screenshot of radicle-tui")

-
![Screenshot](https://seed.radicle.xyz/raw/rad:z39mP9rQAaGmERfUMPULfPUi473tY/4e1233bcf87f3bf2d98f2d5c3c996dff3be390fe/demo.png "Screenshot of radicle-tui")
+
# radicle-tui

`radicle-tui` provides various terminal user interfaces for interacting with the [Radicle](https://radicle.xyz) code forge. It also exposes the application framework they were built with.

@@ -43,46 +43,6 @@ cargo install --force --locked --git https://seed.radicle.xyz/z39mP9rQAaGmERfUMP

This will install `rad-tui`. All available commands can be shown by running `rad-tui --help`.

-
#### Nix
-

-
There is a `flake.nix` present in the repository. This means that for
-
development, it should be as simple as using [`direnv`](https://direnv.net/) and
-
having the following `.envrc` file:
-

-
```
-
# .envrc
-
use flake
-
```
-

-
For using the binary in a NixOS, in your `flake.nix` you can add one of the
-
following to the `inputs` set:
-

-
```nix
-
inputs = {
-
    # Replace <Tag> with the specific tag to build
-
    radicle-tui = {
-
        url = "git+https://seed.radicle.xyz/z39mP9rQAaGmERfUMPULfPUi473tY.git?tag=<Tag>";
-
    }
-
}
-
```
-

-
```nix
-
inputs = {
-
    # Replace <Commit SHA> with the specific commit to build
-
    rad-tui = {
-
        url = "git+https://seed.radicle.xyz/z39mP9rQAaGmERfUMPULfPUi473tY.git?rev=<Commit SHA>";
-
    }
-
}
-
```
-

-
Then in your `home.nix` you can add:
-

-
```
-
home.packages = [
-
  inputs.radicle-tui.packages.${system}.default
-
];
-
```
-

### Usage

This crate provides a binary called `rad-tui` which can be used as a drop-in replacement for `rad`. It maps known commands and operations to internal ones, running the corresponding app, e.g.
@@ -126,6 +86,48 @@ runs the patch list app and return a JSON object specifying the operation and id
{ "operation": "show", "ids": ["546443226b300484a97a2b2d7c7000af6e8169ba"], args:[] }
```

+
#### Other installation methods
+

+
##### Nix
+

+
There is a `flake.nix` present in the repository. This means that for
+
development, it should be as simple as using [`direnv`](https://direnv.net/) and
+
having the following `.envrc` file:
+

+
```
+
# .envrc
+
use flake
+
```
+

+
For using the binary in a NixOS, in your `flake.nix` you can add one of the
+
following to the `inputs` set:
+

+
```nix
+
inputs = {
+
    # Replace <Tag> with the specific tag to build
+
    radicle-tui = {
+
        url = "git+https://seed.radicle.xyz/z39mP9rQAaGmERfUMPULfPUi473tY.git?tag=<Tag>";
+
    }
+
}
+
```
+

+
```nix
+
inputs = {
+
    # Replace <Commit SHA> with the specific commit to build
+
    rad-tui = {
+
        url = "git+https://seed.radicle.xyz/z39mP9rQAaGmERfUMPULfPUi473tY.git?rev=<Commit SHA>";
+
    }
+
}
+
```
+

+
Then in your `home.nix` you can add:
+

+
```
+
home.packages = [
+
  inputs.radicle-tui.packages.${system}.default
+
];
+
```
+

## Build with `radicle-tui`

The library portion of this crate is a framework that is the foundation for all `radicle-tui` binaries. Although it evolved from the work on Radicle-specific applications and is far from being complete, it can serve as a general purpose framework to build applications on top already.
modified bin/apps/inbox/list.rs
@@ -65,6 +65,9 @@ const HELP: &str = r#"# Generic keybindings
`PageDown`: move cursor one page down
`Home`:     move cursor to the first line
`End`:      move cursor to the last line
+
`1-9`:      focus section
+
`Tab`:      focus next section
+
`BackTab`:  focus previous section
`Esc`:      Cancel
`q`:        Quit

@@ -512,8 +515,8 @@ impl App {
                let unseen = format!(" {} ", seen_counts.1);
                [
                    Column::new(
-
                        Span::raw(" Search ".to_string()).cyan().dim().reversed(),
-
                        Constraint::Length(8),
+
                        Span::raw(" Inbox ".to_string()).cyan().dim().reversed(),
+
                        Constraint::Length(7),
                    ),
                    Column::new(
                        Span::raw(format!(" {search} "))
@@ -564,7 +567,7 @@ impl App {
            } else {
                [
                    Column::new(
-
                        Span::raw(" Search ".to_string()).cyan().dim().reversed(),
+
                        Span::raw(" Inbox ".to_string()).cyan().dim().reversed(),
                        Constraint::Length(8),
                    ),
                    Column::new(
modified bin/apps/issue/list.rs
@@ -108,6 +108,7 @@ const HELP: &str = r#"# Generic keybindings
`PageDown`: move cursor one page down
`Home`:     move cursor to the first line
`End`:      move cursor to the last line
+
`1-9`:      focus section
`Tab`:      focus next section
`BackTab`:  focus previous section
`Esc`:      Cancel
@@ -797,7 +798,7 @@ impl App {
            if !self.state.filter.has_state() {
                [
                    Column::new(
-
                        Span::raw(" Search ".to_string()).cyan().dim().reversed(),
+
                        Span::raw(" Issues ".to_string()).cyan().dim().reversed(),
                        Constraint::Length(8),
                    ),
                    Column::new(
@@ -855,7 +856,7 @@ impl App {
            } else {
                [
                    Column::new(
-
                        Span::raw(" Search ".to_string()).cyan().dim().reversed(),
+
                        Span::raw(" Issues ".to_string()).cyan().dim().reversed(),
                        Constraint::Length(8),
                    ),
                    Column::new(
modified bin/apps/patch/list.rs
@@ -44,6 +44,9 @@ const HELP: &str = r#"# Generic keybindings
`PageDown`: move cursor one page down
`Home`:     move cursor to the first line
`End`:      move cursor to the last line
+
`1-9`:      focus section
+
`Tab`:      focus next section
+
`BackTab`:  focus previous section
`Esc`:      Cancel
`q`:        Quit

@@ -611,8 +614,8 @@ impl App {
                let merged = format!(" {} ", state_counts.3);
                [
                    Column::new(
-
                        Span::raw(" Search ".to_string()).cyan().dim().reversed(),
-
                        Constraint::Length(8),
+
                        Span::raw(" Patches ").cyan().dim().reversed(),
+
                        Constraint::Length(9),
                    ),
                    Column::new(
                        Span::raw(format!(" {search} "))
@@ -690,8 +693,8 @@ impl App {
            } else {
                [
                    Column::new(
-
                        Span::raw(" Search ".to_string()).cyan().dim().reversed(),
-
                        Constraint::Length(8),
+
                        Span::raw(" Patches ".to_string()).cyan().dim().reversed(),
+
                        Constraint::Length(9),
                    ),
                    Column::new(
                        Span::raw(format!(" {search} "))
added demo.jpg
deleted demo.png
modified src/ui.rs
@@ -248,7 +248,7 @@ impl Layout {
                    ])
                    .areas(area);
                    let [right_top, right_bottom] =
-
                        Layout::vertical([Constraint::Percentage(65), Constraint::Percentage(35)])
+
                        Layout::vertical([Constraint::Percentage(60), Constraint::Percentage(40)])
                            .areas(right);

                    [left, right_top, right_bottom].into()
modified src/ui/theme.rs
@@ -39,9 +39,9 @@ impl Theme {
    pub fn default_dark() -> Self {
        Self {
            border_style: Style::default().fg(Color::Indexed(240)),
-
            focus_border_style: Style::default().cyan(),
+
            focus_border_style: Style::default().fg(Color::Indexed(249)),
            scroll_style: Style::default().fg(Color::Indexed(240)),
-
            focus_scroll_style: style::cyan(),
+
            focus_scroll_style: Style::default().fg(Color::Indexed(249)),
            shortcuts_keys_style: style::yellow().dim(),
            shortcuts_action_style: style::gray().dim(),
            textview_style: style::reset(),
@@ -49,10 +49,18 @@ impl Theme {
            dim_no_focus: false,
        }
    }
+

+
    pub fn highlight(&self, focus: bool) -> Style {
+
        if focus {
+
            style::cyan().not_dim().reversed()
+
        } else {
+
            style::cyan().dim().reversed()
+
        }
+
    }
}

pub mod style {
-
    use ratatui::style::{Color, Style, Stylize};
+
    use ratatui::style::{Color, Style};

    pub fn reset() -> Style {
        Style::default().fg(Color::Reset)
@@ -93,12 +101,4 @@ pub mod style {
    pub fn darkgray() -> Style {
        Style::default().fg(Color::DarkGray)
    }
-

-
    pub fn highlight(focus: bool) -> Style {
-
        if focus {
-
            cyan().not_dim().reversed()
-
        } else {
-
            cyan().dim().reversed()
-
        }
-
    }
}
modified src/ui/widget.rs
@@ -186,14 +186,42 @@ impl ContainerState {
        self.len == 0
    }

-
    pub fn focus_next(&mut self) {
-
        self.focus = self
+
    pub fn focus_next(&mut self) -> bool {
+
        let focus = self
            .focus
-
            .map(|focus| cmp::min(focus.saturating_add(1), self.len.saturating_sub(1)))
+
            .map(|focus| cmp::min(focus.saturating_add(1), self.len.saturating_sub(1)));
+
        let changed = focus != self.focus;
+
        if changed {
+
            self.focus = focus;
+
        }
+
        changed
+
    }
+

+
    pub fn focus_prev(&mut self) -> bool {
+
        let focus = self.focus.map(|f| f.saturating_sub(1));
+
        let changed = focus != self.focus;
+
        if changed {
+
            self.focus = focus;
+
        }
+
        changed
+
    }
+

+
    pub fn focus_index(&mut self, focus: usize) -> bool {
+
        let focus = (focus < self.len).then_some(focus);
+
        let changed = focus.is_some() && focus != self.focus;
+
        if changed {
+
            self.focus = focus;
+
        }
+
        changed
    }
+
}

-
    pub fn focus_prev(&mut self) {
-
        self.focus = self.focus.map(|focus| focus.saturating_sub(1))
+
impl<'a> From<&Container<'a>> for ContainerState {
+
    fn from(container: &Container<'a>) -> Self {
+
        Self {
+
            len: container.len,
+
            focus: *container.focus,
+
        }
    }
}

@@ -227,54 +255,22 @@ impl<'a> Container<'a> {
        M: Clone,
    {
        let mut response = Response::default();
-

-
        let mut state = ContainerState {
-
            focus: *self.focus,
-
            len: self.len,
-
        };
-

-
        if ui.has_global_input(|key| key == Key::Tab) {
-
            state.focus_next();
-
            response.changed = true;
-
        }
-
        if ui.has_global_input(|key| key == Key::BackTab) {
-
            state.focus_prev();
-
            response.changed = true;
+
        let mut state = ContainerState::from(&self);
+

+
        response.changed |= ui.has_global_input(|key| key == Key::Tab) && state.focus_next();
+
        response.changed |= ui.has_global_input(|key| key == Key::BackTab) && state.focus_prev();
+
        for index in 1..=self.len {
+
            if let Some(c) = char::from_digit(index as u32, 10) {
+
                response.changed |=
+
                    ui.has_global_input(|key| key == Key::Char(c)) && state.focus_index(index - 1);
+
            }
        }
        *self.focus = state.focus;

-
        let mut ui = Ui {
-
            focus_area: state.focus,
-
            ..ui.clone()
-
        };
-

-
        let inner = add_contents(&mut ui);
-

-
        InnerResponse::new(inner, response)
-
    }
-
}
-

-
#[derive(Clone, Debug, Serialize, Deserialize)]
-
pub struct CompositeState {
-
    len: usize,
-
    focus: usize,
-
}
-

-
impl CompositeState {
-
    pub fn new(len: usize, focus: usize) -> Self {
-
        Self { len, focus }
-
    }
-

-
    pub fn focus(&self) -> usize {
-
        self.focus
-
    }
-

-
    pub fn len(&self) -> usize {
-
        self.len
-
    }
-

-
    pub fn is_empty(&self) -> bool {
-
        self.len == 0
+
        InnerResponse::new(
+
            add_contents(&mut ui.clone().with_area_focus(state.focus)),
+
            response,
+
        )
    }
}

@@ -555,7 +551,7 @@ where
                .rows(rows)
                .widths(widths)
                .column_spacing(self.spacing.into())
-
                .row_highlight_style(style::highlight(ui.has_focus));
+
                .row_highlight_style(ui.theme.highlight(ui.has_focus));

            let table = if !area_focus && self.dim {
                table.dim()
@@ -746,13 +742,13 @@ where
                            ui.theme.scroll_style
                        }),
                ))
-
                .highlight_style(style::highlight(ui.has_focus))
+
                .highlight_style(ui.theme.highlight(ui.has_focus))
                .style(tree_style)
        } else {
            tui_tree_widget::Tree::new(&items)
                .expect("all item identifiers are unique")
                .style(tree_style)
-
                .highlight_style(style::highlight(ui.has_focus))
+
                .highlight_style(ui.theme.highlight(ui.has_focus))
        };

        frame.render_stateful_widget(tree, area, &mut state.internal);