| |
|
| |
## 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).
|
| + |
The library portion of this crate is a framework that is the foundation for all `radicle-tui` binaries. It supports building concurrent applications with an immediate mode UI. It 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**: 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.
|
| + |
> **Note:** The framework is under heavy development and still missing some required concepts / components as well as some common low-level widgets. These will be added where needed by the `radicle-tui` binaries.
|
| |
|
| |
### Design
|
| |
|
| - |
The framework was built with a few design goals in mind:
|
| + |
The framework was built first and foremost with developer experience in mind:
|
| + |
|
| + |
- **easy-to-use**: building new or changing existing applications should be as easy as possible; ready-made widgets should come with defaults for user interactions and rendering
|
| + |
- **extensibility**: extending existing and building new widgets should be straight-forward; custom application logic components should be easy to implement
|
| + |
- **flexibility**: widgets and application logic should be easy to change and compose; it should be all about changing and composing functions and not about writing boilerplate code
|
| |
|
| - |
- **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
|
| + |
#### Components
|
| |
|
| |
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 `Frontend` drives an _immediate mode_ `Ui`. In _immediate mode_, widgets are rendered the moment they're created and events are handled right before the actual drawing happens (in _retained mode_, you'd create stateful widgets once and later modify their properties or handle their events).
|
| |
|
| - |
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.
|
| + |
> **Note:** The first versions of the framework provided a retained mode UI (rmUI) which was then replaced in favor of an immediate mode UI (imUI). The Retained mode UI is still supported, but it's recommended to use the new immediate mode UI.
|
| |
|
| |
### Example
|
| |
|
| |
|
| |
use termion::event::Key;
|
| |
|
| - |
use ratatui::text::Text;
|
| + |
use ratatui::{Frame, Viewport};
|
| |
|
| |
use radicle_tui as tui;
|
| |
|
| |
use tui::store;
|
| - |
use tui::ui::widget::text::{TextArea, TextAreaProps};
|
| - |
use tui::ui::widget::ToWidget;
|
| - |
use tui::{BoxedAny, Channel, Exit};
|
| + |
use tui::ui::im::widget::Window;
|
| + |
use tui::ui::im::Show;
|
| + |
use tui::ui::im::{Borders, Context};
|
| + |
use tui::{Channel, Exit};
|
| |
|
| - |
/// Centralized application state.
|
| |
#[derive(Clone, Debug)]
|
| - |
struct State {
|
| + |
struct App {
|
| |
hello: String,
|
| |
}
|
| |
|
| - |
/// All messages known by the application.
|
| + |
#[derive(Clone, Debug)]
|
| |
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;
|
| + |
impl store::Update<Message> for App {
|
| + |
type Return = ();
|
| |
|
| - |
fn update(&mut self, message: Self::Message) -> Option<tui::Exit<()>> {
|
| + |
fn update(&mut self, message: Message) -> Option<tui::Exit<()>> {
|
| |
match message {
|
| |
Message::Quit => Some(Exit { value: None }),
|
| |
}
|
| |
}
|
| |
}
|
| |
|
| - |
/// 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
|
| + |
impl Show<Message> for App {
|
| + |
fn show(&self, ctx: &Context<Message>, frame: &mut Frame) -> Result<()> {
|
| + |
Window::default().show(ctx, |ui| {
|
| + |
ui.text_view(frame, self.hello.clone(), &mut (0, 0), Some(Borders::None));
|
| + |
|
| + |
if ui.input_global(|key| key == Key::Char('q')) {
|
| + |
ui.send_message(Message::Quit);
|
| + |
}
|
| + |
});
|
| + |
|
| + |
Ok(())
|
| + |
}
|
| + |
}
|
| + |
|
| |
#[tokio::main]
|
| |
pub async fn main() -> Result<()> {
|
| - |
let channel = Channel::default();
|
| - |
let sender = channel.tx.clone();
|
| - |
let state = State {
|
| - |
hello: "Hey there, press (q) to quit...".to_string(),
|
| + |
let app = App {
|
| + |
hello: "Hello World!".to_string(),
|
| |
};
|
| |
|
| - |
let textarea = TextArea::default()
|
| - |
.to_widget(sender.clone())
|
| - |
.on_event(|key, _, _| match key {
|
| - |
Key::Char('q') => Some(Message::Quit),
|
| - |
_ => None,
|
| - |
})
|
| - |
.on_update(|state: &State| {
|
| - |
TextAreaProps::default()
|
| - |
.text(&Text::raw(state.hello.clone()))
|
| - |
.to_boxed_any()
|
| - |
.into()
|
| - |
});
|
| - |
|
| - |
tui::run(channel, state, textarea).await?;
|
| + |
tui::im(app, Viewport::default(), Channel::default()).await?;
|
| |
|
| |
Ok(())
|
| |
}
|
| |
|
| |
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".
|
| |
|
| + |
## License
|
| + |
|
| + |
`radicle-tui` is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
|
| + |
|
| + |
See [LICENSE-APACHE](https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z39mP9rQAaGmERfUMPULfPUi473tY/tree/LICENSE-APACHE) and [LICENSE-MIT](https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z39mP9rQAaGmERfUMPULfPUi473tY/tree/LICENSE-MIT) for details.
|
| + |
|
| |
## Contact
|
| |
|
| |
Please get in touch on [Zulip](https://radicle.zulipchat.com).
|
| |
|
| - |
## License
|
| + |
## Acknowledgments
|
| |
|
| - |
`radicle-tui` is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
|
| + |
Parts of this project rely on or were heavily inspired by some great open source projects. So we'd like to thank:
|
| |
|
| - |
See [LICENSE-APACHE](https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z39mP9rQAaGmERfUMPULfPUi473tY/tree/LICENSE-APACHE) and [LICENSE-MIT](https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z39mP9rQAaGmERfUMPULfPUi473tY/tree/LICENSE-MIT) for details.
|
| + |
- [ratatui](https://ratatui.rs)
|
| + |
- [egui](https://github.com/egui)
|
| + |
- [tui-realm](https://github.com/veeso/tui-realm)
|
| + |
- [tui-textarea](https://github.com/rhysd/tui-textarea)
|
| + |
- [tui-rs-tree-widget](https://github.com/EdJoPaTo/tui-rs-tree-widget)
|
| + |
- [rust-chat-server](https://github.com/Yengas/rust-chat-server)
|