Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Prepare RC2
Merged did:key:z6MkswQE...2C1V opened 1 year ago
8 files changed +153 -27 3f96409e 1f6c169e
modified README.md
@@ -2,13 +2,33 @@

![alt text](demo.gif "Demo")

-
`radicle-tui` provides various terminal user interfaces for interacting with the [Radicle](https://radicle.xyz) code forge and exposes the application framework they were built with.
+
`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.

-
## Binaries
+
# Table of Contents
+
1. [Getting Started](#getting-started)
+
    - [Installation](#installation)
+
    - [Usage](#usage)
+
2. [Application framework](#application-framework)
+
    - [Design](#design)
+
    - [Example](#example)
+
3. [Roadmap](#roadmap)
+
4. [Contributing](#contributing)
+
5. [Contact](#contact)
+
6. [License](#license)

-
This crate provides a single binary called `rad-tui` which contains all user interfaces. Specific interfaces can be run by the appropriate command, e.g. `rad-tui patch select` shows a patch selector.
+
## Getting started

-
The interfaces are designed to be modular and to integrate well with the existing Radicle CLI. Right now, they are meant to be called from other programs that will collect and process their output.
+
This crate provides a binary called `rad-tui` which contains all user interfaces. Specific interfaces can be run by the appropriate command, e.g. `rad-tui patch select` shows a patch selector.
+

+
The interfaces are designed to be modular and to integrate well with the existing Radicle CLI. Right now, the binary is meant to be called from `rad`, which will collect and process its output, e.g.
+

+
```
+
rad patch show
+
```
+

+
will show a patch selector and pass on the id of the selected patch.
+

+
> **Note:** The integration into the Radicle CLI is not fully done, yet. Please refer to the [Usage](#usage) section for information on how to use `rad-tui` already.

### Installation

@@ -20,10 +40,9 @@ The interfaces are designed to be modular and to integrate well with the existin

#### From source

-
> Requires the Rust toolchain.
+
> **Note**: Requires the Rust toolchain.

-
You can install the binary from source, by running the following
-
commands from inside this repository:
+
You can install the binary from source, by running the following commands from inside this repository:

```
cargo install --path . --force --locked
@@ -35,10 +54,25 @@ Or directly from our seed node:
cargo install --force --locked --git https://seed.radicle.xyz/z39mP9rQAaGmERfUMPULfPUi473tY.git
```

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

### Usage

+
Soon, `rad-tui` will be integrated into [`heartwood`](https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5). Until then, you can use the `rad` proxy script that is provided. It's considered to be a drop-in replacement for `rad` and can be used for testing and prototyping purposes. It should reflect the current behavior, as if `rad-tui` would have been integrated, e.g.
+

+
```sh
+
# show an interface that let's you select a patch
+
./scripts/rad.sh patch show
+
```
+
```sh
+
# show an interface that let's you select a patch and an operation
+
./scripts/rad.sh patch --tui
+
```
+

+
Both commands will call into `rad-tui`, process its output and call `rad` accordingly.
+

+
#### Interfaces
+

Select a patch, an issue or a notification and an operation:

```
@@ -56,17 +90,37 @@ Select a patch, an issue or a notification only and return its id:
$ rad-tui <patch | issue | inbox> select --mode id
```

-
### Output
+
#### Output

-
All interfaces return a JSON object that reflects the choices made by the user, e.g.: 
+
All interfaces return a common JSON object that reflects the choices made by the user, e.g.: 

```
{ "operation": "show", "ids": ["546443226b300484a97a2b2d7c7000af6e8169ba"], args:[] }
```

-
## Framework
+
## Application framework
+

+
The library portion of this crate is a framework that is the foundation for all `radicle-tui` binaries. The framework is built on top of [ratatui](https://ratatui.rs) and mostly follows the Flux application pattern. It took some ideas from [tui-realm](https://github.com/veeso/tui-realm) and [cursive](https://github.com/gyscos/cursive). The concurrency model was mostly inspired by [rust-chat-server](https://github.com/Yengas/rust-chat-server).
+

+
> **Note**: Existing core functionalities are considered to be stable, but the API may still change at any point. New features like configurations, used-defined keybindings, themes etc. will be added soon though.
+

+
The framework comes with a widget library that provides low-level widgets such as lists, text fields etc. as well as higher-level application widgets such as windows, pages and various other containers.
+

+
> **Note:** The widget library is under heavy development and still missing most low-level widgets. These will be added where needed by the `radicle-tui` binaries.
+

+
### Design

-
The library portion of this crate is a framework that is the foundation for the interfaces mentioned above. The framework is built on top of [ratatui](https://ratatui.rs) and was inspired by the Flux application pattern. It took some ideas from [cursive](https://github.com/gyscos/cursive) and [rust-chat-server](https://github.com/Yengas/rust-chat-server).
+
The framework was built with a few design goals in mind:
+

+
- **async**: state updates and IO should be asynchronous and not block the UI
+
- **declarative**: developers should rather think about the *What* then the *How*
+
- **widget library**: custom widgets should be easy to build; ready-made widgets should come with defaults for user interactions and rendering
+

+
The central pieces of the framework are the `Store`, the `Frontend` and a message passing system that let both communicate with each other. The `Store` handles the centralized application state and sends updates to the `Frontend`, whereas the `Frontend` handles user-interactions and sends messages to the `Store`, which updates the state accordingly.
+

+
On top of this, an extensible widget library was built. A widget is defined by an implementation of the `View` trait and a `Widget` it is wrapped in. A `View` handles user-interactions, updates itself whenever the application state changed and renders itself frequently. A `Widget` adds additional support for properties and event, update and render callbacks. Properties define the data, configuration etc. of a widget. They are updated by the framework taking the properties built by the `on_update` callback. The `on_event` callback is used to emit application messages whenever a widget receives an event.
+

+
The main idea is to build widgets that handle their specific events already, and that are updated with the properties built by the `on_update` callback. Custom logic is added by setting the `on_event` callback. E.g. the `Table` widget handles item selection already; items are set via the `on_update` callback and application messages are emitted via the `on_event` callback.

### Example

@@ -84,15 +138,20 @@ use tui::ui::widget::text::{TextArea, TextAreaProps};
use tui::ui::widget::ToWidget;
use tui::{BoxedAny, Channel, Exit};

+
/// Centralized application state.
#[derive(Clone, Debug)]
struct State {
    hello: String,
}

+
/// All messages known by the application.
enum Message {
    Quit,
}

+
/// Implementation of the app-state trait. It's updated whenever a message was received.
+
/// Applications quit whenever an `Exit` is returned by the `update` function. The `Exit`
+
/// type also provides and optional return value. 
impl store::State<()> for State {
    type Message = Message;

@@ -101,10 +160,13 @@ impl store::State<()> for State {
            Message::Quit => Some(Exit { value: None }),
        }
    }
-

-
    fn tick(&mut self) {}
}

+
/// 1. Initializes the communication channel between frontend and state store
+
/// 2. Initializes the application state
+
/// 3. Builds a textarea widget which renders a welcome message and quits the 
+
///    application when (q) is pressed
+
/// 4. Runs the TUI application
#[tokio::main]
pub async fn main() -> Result<()> {
    let channel = Channel::default();
@@ -113,7 +175,7 @@ pub async fn main() -> Result<()> {
        hello: "Hey there, press (q) to quit...".to_string(),
    };

-
    let scene = TextArea::default()
+
    let textarea = TextArea::default()
        .to_widget(sender.clone())
        .on_event(|key, _, _| match key {
            Key::Char('q') => Some(Message::Quit),
@@ -126,12 +188,43 @@ pub async fn main() -> Result<()> {
                .into()
        });

-
    tui::run(channel, state, scene).await?;
+
    tui::run(channel, state, textarea).await?;

    Ok(())
}
```

+
## ROADMAP
+

+
The project roadmap is largely defined by the requirements of the [Radicle](https://radicle.xyz) team. If you're missing something or have any suggestions that would make this better, please feel free to [get in touch](#contact).
+

+
### Now
+

+
- [ ] Patch and issue preview in selection interfaces
+
- [ ] Basic `radicle-cli` integration
+

+
### Next
+

+
- [ ] Support for multiple selected list and tree items
+
- [ ] Read configuration from file
+
- [ ] Support user-defined keybindings
+
- [ ] Patch review
+

+
### Later   
+

+
- [ ] Streamline CLI integration w/ config and flags for `rad` commands (e.g. `rad patch edit --tui`)`
+
- [ ] Read COBs from JSON input
+
- [ ] Add support for custom themes
+

+
## Contributing
+

+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
+

+
If you have any suggestions that would make this better, please clone the repo and open a patch. You can also simply open an issue with the label "enhancement".
+

+
## Contact
+

+
Please get in touch on [Zulip](https://radicle.zulipchat.com).

## License

modified bin/commands/inbox/select.rs
@@ -221,8 +221,6 @@ pub enum Message {
impl store::State<Selection> for State {
    type Message = Message;

-
    fn tick(&mut self) {}
-

    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
modified bin/commands/issue/select.rs
@@ -198,8 +198,6 @@ impl store::State<Selection> for State {
            }
        }
    }
-

-
    fn tick(&mut self) {}
}

impl App {
modified bin/commands/patch/select.rs
@@ -196,8 +196,6 @@ impl store::State<Selection> for State {
            }
        }
    }
-

-
    fn tick(&mut self) {}
}

impl App {
modified examples/basic.rs
@@ -45,8 +45,6 @@ impl store::State<()> for State {
            }
        }
    }
-

-
    fn tick(&mut self) {}
}

#[tokio::main]
modified examples/hello.rs
@@ -44,8 +44,6 @@ impl store::State<()> for State {
            Message::Quit => Some(Exit { value: None }),
        }
    }
-

-
    fn tick(&mut self) {}
}

#[tokio::main]
added scripts/rad.sh
@@ -0,0 +1,43 @@
+
#!/bin/bash
+
extract_operation() {
+
    local op=$(echo $1 | jq '.operation')
+
    op=${op//\"/""}
+
    
+
    echo "$op"
+
}
+

+
extract_id() {
+
    local ids=$(echo $1 | jq '.ids')
+
    local id=$(echo $ids | jq '.[0]')
+
    id=${id//\"/""}
+
    
+
    echo "$id"
+
}
+

+
if [[ "$1" == "patch" ]] || [[ "$1" == "issue" ]] || [[ "$1" == "inbox" ]]; then
+
    if [[ -n "$2" ]]; then
+
        if [[ "$2" == "--tui" ]]; then
+
            # Run TUI
+
            { out=$(rad-tui $1 select 2>&1 >&3 3>&-); } 3>&1
+
            if [[ "$out" == "" ]]; then
+
                exit 1
+
            fi
+
            
+
            op=$(extract_operation $out)
+
            id=$(extract_id $out)
+
            
+
            rad $1 $op $id
+
        else
+
            # Run TUI
+
            args="--mode id"
+
            { out=$(rad-tui $1 select $args 2>&1 >&3 3>&-); } 3>&1
+
            id=$(extract_id $out)
+
            
+
            rad $1 $2 $id
+
        fi
+
    else
+
        rad $@
+
    fi
+
else
+
    rad $@
+
fi

\ No newline at end of file
modified src/store.rs
@@ -24,7 +24,7 @@ where
    fn update(&mut self, message: Self::Message) -> Option<Exit<P>>;

    /// Handle recurring tick.
-
    fn tick(&mut self);
+
    fn tick(&mut self) {}
}

/// The `Store` updates the applications' state concurrently. It handles