mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-16 00:10:38 +08:00
inbox: Empty impl as flux app
This commit is contained in:
@@ -148,11 +148,7 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
|
||||
);
|
||||
let output = Window::default().run(&mut app, 1000 / FPS)?;
|
||||
|
||||
let output = output
|
||||
.map(|o| serde_json::to_string(&o).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
eprint!("{output}");
|
||||
eprint!("{:?}", output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#[path = "flux/ui.rs"]
|
||||
mod ui;
|
||||
|
||||
use std::default;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use radicle::node::notifications::Notification;
|
||||
use radicle_tui as tui;
|
||||
use tui::cob::inbox::Filter;
|
||||
use tui::context::Context;
|
||||
use tui::flux::store::{State, Store};
|
||||
use tui::flux::termination::{self, Interrupted};
|
||||
use tui::flux::ui::Frontend;
|
||||
|
||||
use crate::tui_inbox::select::flux::ui::ListPage;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Id,
|
||||
#[default]
|
||||
Operation,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
context: Context,
|
||||
filter: Filter,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InboxState {
|
||||
notifications: Vec<String>,
|
||||
}
|
||||
|
||||
impl InboxState {
|
||||
pub fn notifications(&self) -> &Vec<String> {
|
||||
&self.notifications
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Action {
|
||||
Exit,
|
||||
}
|
||||
|
||||
impl State<Action> for InboxState {
|
||||
fn tick(&self) {}
|
||||
|
||||
fn handle_action(self, action: Action) -> anyhow::Result<bool> {
|
||||
match action {
|
||||
Action::Exit => Ok(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(context: Context, filter: Filter) -> Self {
|
||||
Self { context, filter }
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let (terminator, mut interrupt_rx) = termination::create_termination();
|
||||
let (store, state_rx) = Store::<Action, InboxState>::new();
|
||||
let (frontend, action_rx) = Frontend::<Action>::new();
|
||||
let state = InboxState {
|
||||
notifications: vec![],
|
||||
};
|
||||
|
||||
tokio::try_join!(
|
||||
store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
|
||||
frontend.main_loop::<InboxState, ListPage>(state_rx, interrupt_rx.resubscribe()),
|
||||
)?;
|
||||
|
||||
if let Ok(reason) = interrupt_rx.recv().await {
|
||||
match reason {
|
||||
Interrupted::UserInt => {}
|
||||
Interrupted::OsSigInt => println!("exited because of an os sig int"),
|
||||
}
|
||||
} else {
|
||||
println!("exited because of an unexpected error");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use termion::event::Key;
|
||||
|
||||
use ratatui::backend::Backend;
|
||||
use ratatui::widgets::Paragraph;
|
||||
|
||||
use radicle_tui as tui;
|
||||
use tui::flux::ui::{Render, Widget};
|
||||
|
||||
use crate::tui_inbox::select::flux::{Action, InboxState};
|
||||
|
||||
pub struct Props {
|
||||
notifications: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<&InboxState> for Props {
|
||||
fn from(state: &InboxState) -> Self {
|
||||
Props {
|
||||
notifications: state.notifications().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListPage {
|
||||
/// Action sender
|
||||
pub action_tx: UnboundedSender<Action>,
|
||||
// Mapped Props from State
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl Widget<InboxState, Action> for ListPage {
|
||||
fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
ListPage {
|
||||
action_tx: action_tx.clone(),
|
||||
props: Props::from(state),
|
||||
}
|
||||
.move_with_state(state)
|
||||
}
|
||||
|
||||
fn move_with_state(self, state: &InboxState) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
ListPage {
|
||||
props: Props::from(state),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"list-page"
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: termion::event::Key) {
|
||||
match key {
|
||||
Key::Char('q') => {
|
||||
let _ = self.action_tx.send(Action::Exit);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<()> for ListPage {
|
||||
fn render<B: Backend>(&self, frame: &mut ratatui::Frame, _props: ()) {
|
||||
let area = frame.size();
|
||||
let layout = tui::flux::ui::layout::default_page(area, 1u16, 1u16);
|
||||
|
||||
let shortcuts = Paragraph::new(String::from("q quit"));
|
||||
|
||||
frame.render_widget(shortcuts, layout.shortcuts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#[path = "realm/event.rs"]
|
||||
mod event;
|
||||
#[path = "realm/page.rs"]
|
||||
mod page;
|
||||
#[path = "realm/ui.rs"]
|
||||
mod ui;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
use tuirealm::application::PollStrategy;
|
||||
use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause};
|
||||
|
||||
use radicle_tui as tui;
|
||||
|
||||
use tui::cob::inbox::Filter;
|
||||
use tui::context::Context;
|
||||
use tui::ui::subscription;
|
||||
use tui::ui::theme::Theme;
|
||||
use tui::{Exit, PageStack, SelectionExit, Tui};
|
||||
|
||||
use page::ListView;
|
||||
|
||||
/// Wrapper around radicle's `PatchId` that serializes
|
||||
/// to a human-readable string.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PatchId(radicle::cob::patch::PatchId);
|
||||
|
||||
impl From<radicle::cob::patch::PatchId> for PatchId {
|
||||
fn from(value: radicle::cob::patch::PatchId) -> Self {
|
||||
PatchId(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PatchId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PatchId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{}", *self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// The application's subject. It tells the application
|
||||
/// which widgets to render and which output to produce.
|
||||
///
|
||||
/// Depends on CLI arguments given by the user.
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Operation,
|
||||
#[allow(dead_code)]
|
||||
Id,
|
||||
}
|
||||
|
||||
/// The selected issue operation returned by the operation
|
||||
/// selection widget.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub enum InboxOperation {
|
||||
Show,
|
||||
Clear,
|
||||
}
|
||||
|
||||
impl Display for InboxOperation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
InboxOperation::Show => {
|
||||
write!(f, "show")
|
||||
}
|
||||
InboxOperation::Clear => {
|
||||
write!(f, "clear")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum ListCid {
|
||||
NotificationBrowser,
|
||||
Context,
|
||||
Shortcuts,
|
||||
}
|
||||
|
||||
/// All component ids known to this application.
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Cid {
|
||||
List(ListCid),
|
||||
#[default]
|
||||
GlobalListener,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
pub enum Message {
|
||||
#[default]
|
||||
Tick,
|
||||
Quit(Option<SelectionExit>),
|
||||
Batch(Vec<Message>),
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
context: Context,
|
||||
pages: PageStack<Cid, Message>,
|
||||
theme: Theme,
|
||||
quit: bool,
|
||||
filter: Filter,
|
||||
output: Option<SelectionExit>,
|
||||
}
|
||||
|
||||
/// Creates a new application using a tui-realm-application, mounts all
|
||||
/// components and sets focus to a default one.
|
||||
#[allow(dead_code)]
|
||||
impl App {
|
||||
pub fn new(context: Context, filter: Filter) -> Self {
|
||||
Self {
|
||||
context,
|
||||
pages: PageStack::default(),
|
||||
theme: Theme::default(),
|
||||
quit: false,
|
||||
filter,
|
||||
output: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn view_list(
|
||||
&mut self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
theme: &Theme,
|
||||
) -> Result<()> {
|
||||
let home = Box::new(ListView::new(self.filter.clone()));
|
||||
self.pages.push(home, app, &self.context, theme)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
message: Message,
|
||||
) -> Result<Option<Message>> {
|
||||
let theme = Theme::default();
|
||||
match message {
|
||||
Message::Batch(messages) => {
|
||||
let mut results = vec![];
|
||||
for message in messages {
|
||||
if let Some(result) = self.process(app, message)? {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
match results.len() {
|
||||
0 => Ok(None),
|
||||
1 => Ok(Some(results[0].to_owned())),
|
||||
_ => Ok(Some(Message::Batch(results))),
|
||||
}
|
||||
}
|
||||
Message::Quit(output) => {
|
||||
self.quit = true;
|
||||
self.output = output;
|
||||
Ok(None)
|
||||
}
|
||||
_ => self
|
||||
.pages
|
||||
.peek_mut()?
|
||||
.update(app, &self.context, &theme, message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui<Cid, Message, SelectionExit> for App {
|
||||
fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
self.view_list(app, &self.theme.clone())?;
|
||||
|
||||
// Add global key listener and subscribe to key events
|
||||
let global = tui::ui::global_listener().to_boxed();
|
||||
app.mount(
|
||||
Cid::GlobalListener,
|
||||
global,
|
||||
vec![Sub::new(
|
||||
subscription::quit_clause(tuirealm::event::Key::Char('q')),
|
||||
SubClause::Always,
|
||||
)],
|
||||
)?;
|
||||
|
||||
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() => {
|
||||
for message in messages {
|
||||
let mut msg = Some(message);
|
||||
while msg.is_some() {
|
||||
msg = self.process(app, msg.unwrap())?;
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn exit(&self) -> Option<Exit<SelectionExit>> {
|
||||
if self.quit {
|
||||
return Some(Exit {
|
||||
value: self.output.clone(),
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ impl NotificationBrowser {
|
||||
ColumnWidth::Fixed(10),
|
||||
ColumnWidth::Fixed(18),
|
||||
];
|
||||
|
||||
|
||||
let mut items = vec![];
|
||||
for notification in context.notifications() {
|
||||
if let Ok(item) =
|
||||
Reference in New Issue
Block a user