From f2d171f0058ca322f1250c528bee8eae0c3c1351 Mon Sep 17 00:00:00 2001 From: Prometheus1400 Date: Thu, 4 Dec 2025 19:15:46 -0800 Subject: [PATCH 1/5] wip --- cli/src/actors/client.rs | 384 ++++++++++++------------- cli/src/actors/mod.rs | 6 +- cli/src/actors/ui.rs | 81 +++--- cli/src/actors/ui2.rs | 33 +++ cli/src/app.rs | 181 ++++++++++++ cli/src/error.rs | 81 +++--- cli/src/input.rs | 44 +++ cli/src/input_parser/parser.rs | 6 +- cli/src/main.rs | 54 ++-- cli/src/widgets/mod.rs | 2 + cli/src/widgets/status_line.rs | 1 + cli/src/widgets/terminal.rs | 226 +++++++++++++++ cli/src/widgets/traits.rs | 9 +- core/src/events.rs | 2 + daemon/src/actors/client_connection.rs | 4 + 15 files changed, 799 insertions(+), 315 deletions(-) create mode 100644 cli/src/actors/ui2.rs create mode 100644 cli/src/app.rs create mode 100644 cli/src/input.rs create mode 100644 cli/src/widgets/status_line.rs create mode 100644 cli/src/widgets/terminal.rs diff --git a/cli/src/actors/client.rs b/cli/src/actors/client.rs index 045021b..fe52ef4 100644 --- a/cli/src/actors/client.rs +++ b/cli/src/actors/client.rs @@ -1,192 +1,192 @@ -use std::time::Duration; - -use bytes::Bytes; -use handle_macro::Handle; -use remux_core::{ - comm, - events::{CliEvent, DaemonEvent}, - states::DaemonState, -}; -use tokio::{io::AsyncReadExt, net::UnixStream, sync::mpsc, time::interval}; -use tracing::{Instrument, debug}; - -use crate::{ - actors::ui::{UI, UIHandle}, - input_parser::{Action, InputParser, ParsedEvent}, - prelude::*, - utils::DisplayableVec, -}; - -#[derive(Handle)] -pub enum ClientEvent { - Selected(Option), // index of the selected item -} -use ClientEvent::*; - -#[derive(Debug)] -enum ClientState { - Normal, // running normally with stdin parsed into events and sent to daemon - SelectingSession, // means that the ui is currently busy selecting redirects stdin to ui selector -} - -#[derive(Debug)] -pub struct Client { - _handle: ClientHandle, // handle used to send the client events - stream: UnixStream, // the client owns the stream - rx: mpsc::Receiver, // receiver for client events - daemon_state: DaemonState, // determines if currently accepting events from daemon - sync_daemon_state: bool, // if the state is dirty only then do we need to sync to the ui - ui_stdin_tx: mpsc::Sender, // this is for popup actor to connect to stdin - ui_handle: UIHandle, // how the client sends messages to ui - input_parser: InputParser, // converts streams of bytes into actionable events - client_state: ClientState, // the current state of the client -} -impl Client { - #[instrument(skip(stream))] - pub fn spawn(stream: UnixStream, daemon_state: DaemonState) -> Result { - Client::new(stream, daemon_state)?.run() - } - - #[instrument(skip(stream))] - fn new(stream: UnixStream, daemon_state: DaemonState) -> Result { - let (tx, rx) = mpsc::channel(100); - let (ui_stdin_tx, ui_stdin_rx) = mpsc::channel(100); - let handle = ClientHandle { tx }; - let ui_handle = UI::spawn(handle.clone(), ui_stdin_rx)?; - Ok(Self { - _handle: handle, - stream, - rx, - ui_stdin_tx, - ui_handle, - daemon_state, - sync_daemon_state: true, - input_parser: InputParser::new(), - client_state: ClientState::Normal, - }) - } - - #[instrument(skip(self), fields(client_state = ?self.client_state))] - fn run(mut self) -> Result { - let task: CliTask = tokio::spawn({ - let span = tracing::Span::current(); - let mut stdin = tokio::io::stdin(); - let mut stdin_buf = [0u8; 1024]; - async move { - let mut ticker = interval(Duration::from_millis(1000)); - loop { - tokio::select! { - Some(event) = self.rx.recv() => { - match event { - Selected(index) => { - debug!("Selected: {index:?}"); - match self.client_state { - ClientState::Normal => { - error!("should not receive selected event in normal state"); - }, - ClientState::SelectingSession => { - if let Some(index) = index { - let selected_session = self.daemon_state.session_ids[index]; - debug!("sending session selection: {selected_session}"); - comm::send_event(&mut self.stream, CliEvent::SwitchSession(selected_session)).await.unwrap(); - } - debug!("Returning to normal state"); - self.client_state = ClientState::Normal; - } - } - } - } - }, - res = comm::recv_daemon_event(&mut self.stream) => { - match res { - Ok(event) => { - match event { - DaemonEvent::Raw(bytes) => { - trace!("DaemonEvent(Raw({bytes:?}))"); - self.ui_handle.output(bytes).await?; - } - DaemonEvent::Disconnected => { - debug!("DaemonEvent(Disconnected)"); - self.ui_handle.kill().await.unwrap(); - break; - } - DaemonEvent::CurrentSessions(session_ids) => { - debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); - self.daemon_state.set_sessions(session_ids); - self.sync_daemon_state = true; - } - DaemonEvent::ActiveSession(session_id) => { - debug!("DaemonEvent(ActiveSession({session_id}))"); - self.daemon_state.set_active_session(session_id); - self.sync_daemon_state = true; - } - DaemonEvent::NewSession(session_id) => { - debug!("DaemonEvent(NewSession({session_id}))"); - self.daemon_state.add_session(session_id); - self.sync_daemon_state = true; - } - DaemonEvent::DeletedSession(_session_id) => { - todo!("implement delete session"); - } - } - } - Err(e) => { - error!("Error receiving daemon event: {e}"); - break; - } - } - } - stdin_res = stdin.read(&mut stdin_buf) => { - match stdin_res { - Ok(n) if n > 0 => { - match self.client_state { - ClientState::Normal => { - trace!("Sending {n} bytes to Daemon"); - for event in self.input_parser.process(&stdin_buf[..n]) { - match event { - ParsedEvent::DaemonAction(cli_event) => { - comm::send_event(&mut self.stream, cli_event).await?; - }, - ParsedEvent::LocalAction(local_action) => { - match local_action { - Action::SwitchSession => { - self.client_state = ClientState::SelectingSession; - let items = DisplayableVec::new(self.daemon_state.session_ids.clone()); - self.ui_handle.select_fuzzy(items, "Select Session".to_owned()).await.unwrap(); - }, - } - }, - } - } - - }, - ClientState::SelectingSession => { - trace!("Sending {n} bytes to ui"); - self.ui_stdin_tx.send(Bytes::copy_from_slice(&stdin_buf[..n])).await?; - }, - } - } - Ok(_) => { - break; - } - Err(e) => { - error!("Error receiving stdin: {e}"); - continue; - } - } - }, - _ = ticker.tick(), if self.sync_daemon_state => { - debug!("syncing daemon_state"); - self.ui_handle.sync_daemon_state(self.daemon_state.clone()).await?; - self.sync_daemon_state = false; - } - } - } - debug!("Client stopped"); - Ok(()) - }.instrument(span) - }); - - Ok(task) - } -} +// use std::time::Duration; +// +// use bytes::Bytes; +// use handle_macro::Handle; +// use remux_core::{ +// comm, +// events::{CliEvent, DaemonEvent}, +// states::DaemonState, +// }; +// use tokio::{io::AsyncReadExt, net::UnixStream, sync::mpsc, time::interval}; +// use tracing::{Instrument, debug}; +// +// use crate::{ +// actors::ui::{UI, UIHandle}, +// input_parser::{Action, InputParser, ParsedEvent}, +// prelude::*, +// utils::DisplayableVec, +// }; +// +// #[derive(Handle)] +// pub enum ClientEvent { +// Selected(Option), // index of the selected item +// } +// use ClientEvent::*; +// +// #[derive(Debug)] +// enum ClientState { +// Normal, // running normally with stdin parsed into events and sent to daemon +// SelectingSession, // means that the ui is currently busy selecting redirects stdin to ui selector +// } +// +// #[derive(Debug)] +// pub struct Client { +// _handle: ClientHandle, // handle used to send the client events +// stream: UnixStream, // the client owns the stream +// rx: mpsc::Receiver, // receiver for client events +// daemon_state: DaemonState, // determines if currently accepting events from daemon +// sync_daemon_state: bool, // if the state is dirty only then do we need to sync to the ui +// ui_stdin_tx: mpsc::Sender, // this is for popup actor to connect to stdin +// ui_handle: UIHandle, // how the client sends messages to ui +// input_parser: InputParser, // converts streams of bytes into actionable events +// client_state: ClientState, // the current state of the client +// } +// impl Client { +// #[instrument(skip(stream))] +// pub fn spawn(stream: UnixStream, daemon_state: DaemonState) -> Result { +// Client::new(stream, daemon_state)?.run() +// } +// +// #[instrument(skip(stream))] +// fn new(stream: UnixStream, daemon_state: DaemonState) -> Result { +// let (tx, rx) = mpsc::channel(100); +// let (ui_stdin_tx, ui_stdin_rx) = mpsc::channel(100); +// let handle = ClientHandle { tx }; +// let ui_handle = UI::spawn(handle.clone(), ui_stdin_rx)?; +// Ok(Self { +// _handle: handle, +// stream, +// rx, +// ui_stdin_tx, +// ui_handle, +// daemon_state, +// sync_daemon_state: true, +// input_parser: InputParser::new(), +// client_state: ClientState::Normal, +// }) +// } +// +// #[instrument(skip(self), fields(client_state = ?self.client_state))] +// fn run(mut self) -> Result { +// let task: CliTask = tokio::spawn({ +// let span = tracing::Span::current(); +// let mut stdin = tokio::io::stdin(); +// let mut stdin_buf = [0u8; 1024]; +// async move { +// let mut ticker = interval(Duration::from_millis(1000)); +// loop { +// tokio::select! { +// Some(event) = self.rx.recv() => { +// match event { +// Selected(index) => { +// debug!("Selected: {index:?}"); +// match self.client_state { +// ClientState::Normal => { +// error!("should not receive selected event in normal state"); +// }, +// ClientState::SelectingSession => { +// if let Some(index) = index { +// let selected_session = self.daemon_state.session_ids[index]; +// debug!("sending session selection: {selected_session}"); +// comm::send_event(&mut self.stream, CliEvent::SwitchSession(selected_session)).await.unwrap(); +// } +// debug!("Returning to normal state"); +// self.client_state = ClientState::Normal; +// } +// } +// } +// } +// }, +// res = comm::recv_daemon_event(&mut self.stream) => { +// match res { +// Ok(event) => { +// match event { +// DaemonEvent::Raw(bytes) => { +// trace!("DaemonEvent(Raw({bytes:?}))"); +// self.ui_handle.output(bytes).await?; +// } +// DaemonEvent::Disconnected => { +// debug!("DaemonEvent(Disconnected)"); +// self.ui_handle.kill().await.unwrap(); +// break; +// } +// DaemonEvent::CurrentSessions(session_ids) => { +// debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); +// self.daemon_state.set_sessions(session_ids); +// self.sync_daemon_state = true; +// } +// DaemonEvent::ActiveSession(session_id) => { +// debug!("DaemonEvent(ActiveSession({session_id}))"); +// self.daemon_state.set_active_session(session_id); +// self.sync_daemon_state = true; +// } +// DaemonEvent::NewSession(session_id) => { +// debug!("DaemonEvent(NewSession({session_id}))"); +// self.daemon_state.add_session(session_id); +// self.sync_daemon_state = true; +// } +// DaemonEvent::DeletedSession(_session_id) => { +// todo!("implement delete session"); +// } +// } +// } +// Err(e) => { +// error!("Error receiving daemon event: {e}"); +// break; +// } +// } +// } +// stdin_res = stdin.read(&mut stdin_buf) => { +// match stdin_res { +// Ok(n) if n > 0 => { +// match self.client_state { +// ClientState::Normal => { +// trace!("Sending {n} bytes to Daemon"); +// for event in self.input_parser.process(&stdin_buf[..n]) { +// match event { +// ParsedEvent::DaemonAction(cli_event) => { +// comm::send_event(&mut self.stream, cli_event).await?; +// }, +// ParsedEvent::LocalAction(local_action) => { +// match local_action { +// Action::SwitchSession => { +// self.client_state = ClientState::SelectingSession; +// let items = DisplayableVec::new(self.daemon_state.session_ids.clone()); +// self.ui_handle.select_fuzzy(items, "Select Session".to_owned()).await.unwrap(); +// }, +// } +// }, +// } +// } +// +// }, +// ClientState::SelectingSession => { +// trace!("Sending {n} bytes to ui"); +// self.ui_stdin_tx.send(Bytes::copy_from_slice(&stdin_buf[..n])).await?; +// }, +// } +// } +// Ok(_) => { +// break; +// } +// Err(e) => { +// error!("Error receiving stdin: {e}"); +// continue; +// } +// } +// }, +// _ = ticker.tick(), if self.sync_daemon_state => { +// debug!("syncing daemon_state"); +// self.ui_handle.sync_daemon_state(self.daemon_state.clone()).await?; +// self.sync_daemon_state = false; +// } +// } +// } +// debug!("Client stopped"); +// Ok(()) +// }.instrument(span) +// }); +// +// Ok(task) +// } +// } diff --git a/cli/src/actors/mod.rs b/cli/src/actors/mod.rs index 8563029..9afd6f1 100644 --- a/cli/src/actors/mod.rs +++ b/cli/src/actors/mod.rs @@ -1,5 +1 @@ -pub mod client; -pub mod lua; -pub mod ui; - -pub use client::Client; +pub mod ui2; diff --git a/cli/src/actors/ui.rs b/cli/src/actors/ui.rs index daf2706..cf2417d 100644 --- a/cli/src/actors/ui.rs +++ b/cli/src/actors/ui.rs @@ -21,24 +21,25 @@ use tui_term::widget::PseudoTerminal; use vt100::Parser; use crate::{ - actors::{ - client::ClientHandle, - lua::{Lua, LuaHandle}, - }, prelude::*, states::status_line_state::StatusLineState, utils::DisplayableVec, - widgets::{BasicSelector, FuzzySelector, Selector}, + widgets::{BasicSelector, FuzzySelector, Selector, TerminalWidget}, }; +use crate::actors::{ + client::ClientHandle, lua::{Lua, LuaHandle} + }; #[derive(Handle)] pub enum UIEvent { Output(Bytes), + Stdin(Bytes), Kill, SyncDaemonState(DaemonState), SyncStatusLineState(StatusLineState), SelectBasic { items: DisplayableVec, title: String }, SelectFuzzy { items: DisplayableVec, title: String }, + SelectedIndex(Option), } use UIEvent::*; @@ -51,7 +52,7 @@ enum UIState { pub struct UI { // for communication - handle: UIHandle, + _handle: UIHandle, rx: mpsc::Receiver, // state for rendering daemon_state: DaemonState, @@ -64,25 +65,28 @@ pub struct UI { fuzzy_selector: Arc>, selector_rx: mpsc::Receiver>, stdin_rx: mpsc::Receiver, + + terminal_widget: TerminalWidget } impl UI { - #[instrument(skip(client_handle, stdin_rx))] - pub fn spawn(client_handle: ClientHandle, stdin_rx: mpsc::Receiver) -> Result { - Self::new(client_handle, stdin_rx)?.run() - } - #[instrument(skip(client_handle, stdin_rx))] - fn new(client_handle: ClientHandle, stdin_rx: mpsc::Receiver) -> Result { + #[instrument(skip())] + pub fn new() -> Result { let (tx, rx) = mpsc::channel(100); - let handle = UIHandle { tx }; + let _handle = UIHandle { tx }; + + let client_handle = Client::spawn(, daemon_state); + let parser = vt100::Parser::default(); let lua_handle = Lua::spawn(handle.clone())?; let (selector_tx, selector_rx) = mpsc::channel(100); + Ok(Self { + _handle, + rx, + daemon_state: DaemonState::default(), status_line_state: StatusLineState::default(), - rx, - handle, parser, lua_handle, client_handle, @@ -91,21 +95,20 @@ impl UI { fuzzy_selector: FuzzySelector::new(selector_tx.clone()), selector_rx, stdin_rx, + terminal_widget: TerminalWidget::default(), }) } #[instrument(skip(self), fields(ui_state = ?self.ui_state))] - pub fn run(mut self) -> Result { + pub async fn run(mut self) -> Result<()> { let span = tracing::Span::current(); - - let handle_clone = self.handle.clone(); let mut term = Terminal::new(CrosstermBackend::new(stdout())).unwrap(); let (selector_tx, _) = broadcast::channel(10000); - tokio::spawn({ - let selector_tx = selector_tx.clone(); + let selector_tx = selector_tx.clone(); + let mut ticker = interval(Duration::from_millis(16)); + execute!(stdout(), EnterAlternateScreen)?; + term.clear()?; + let task = tokio::spawn({ async move { - let mut ticker = interval(Duration::from_millis(16)); - execute!(stdout(), EnterAlternateScreen)?; - term.clear()?; loop { tokio::select! { Some(event) = self.rx.recv() => { @@ -113,6 +116,12 @@ impl UI { Output(bytes) => { self.parser.process(&bytes); } + Stdin(bytes) if matches!(self.ui_state, UIState::SelectingFuzzy | UIState::SelectingBasic) => { + selector_tx.send(bytes).unwrap(); + } + Stdin(_) => { + error!("Shouldn't recieve stdin in current state"); + } SyncDaemonState(daemon_state) => { self.lua_handle.sync_daemon_state(daemon_state.clone()).unwrap(); self.daemon_state = daemon_state; @@ -128,20 +137,16 @@ impl UI { self.ui_state = UIState::SelectingFuzzy; FuzzySelector::run(&self.fuzzy_selector, selector_tx.subscribe(), items, title).unwrap(); } + SelectedIndex(index) => { + self.client_handle.selected(index).await.unwrap(); + self.ui_state = UIState::Normal; + } Kill => { self.lua_handle.kill().unwrap(); break; } } } - Some(bytes) = self.stdin_rx.recv(), if matches!(self.ui_state, UIState::SelectingFuzzy | UIState::SelectingBasic) => { - selector_tx.send(bytes).unwrap(); - } - Some(index) = self.selector_rx.recv() => { - debug!("selected index({index:?})"); - self.client_handle.selected(index).await.unwrap(); - self.ui_state = UIState::Normal; - } _ = ticker.tick() => { let screen = self.parser.screen(); term.draw(|f| { @@ -176,13 +181,11 @@ impl UI { } } } - debug!("ui stopped"); - execute!(stdout(), LeaveAlternateScreen)?; - Ok::<(), Error>(()) - } - .instrument(span) - }); - - Ok(handle_clone) + debug!("ui stopped"); + execute!(stdout(), LeaveAlternateScreen)?; + Ok::<(), Error>(()) + }.instrument(span) + }).await; + Ok(()) } } diff --git a/cli/src/actors/ui2.rs b/cli/src/actors/ui2.rs new file mode 100644 index 0000000..698151a --- /dev/null +++ b/cli/src/actors/ui2.rs @@ -0,0 +1,33 @@ +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout}, +}; +use tui_term::widget::PseudoTerminal; + +use crate::app::AppState; + +pub fn draw(f: &mut Frame, state: &mut AppState) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(1), // pseudo terminal takes everything else + Constraint::Length(1), // bottom status bar + ]) + .split(f.area()); + + // render the normal terminal output + let term_ui = PseudoTerminal::new(state.terminal.emulator.screen()); + f.render_widget(term_ui, chunks[0]); + + // render the status bar + // let status_line = StatusLine::new(app.status_line); + // f.render_widget(); + + // render selector if active + // if self.ui_state == UIState::SelectingBasic { + // BasicSelector::render(&self.basic_selector, f); + // } + // if self.ui_state == UIState::SelectingFuzzy { + // FuzzySelector::render(&self.fuzzy_selector, f); + // } +} diff --git a/cli/src/app.rs b/cli/src/app.rs new file mode 100644 index 0000000..b0e494e --- /dev/null +++ b/cli/src/app.rs @@ -0,0 +1,181 @@ +use std::{io::Stdout, time::Duration}; + +use bytes::Bytes; +use crossterm::event::Event; +use ratatui::{Terminal, prelude::CrosstermBackend}; +use remux_core::{ + comm, + events::{CliEvent, DaemonEvent}, + states::DaemonState, +}; +use tokio::{net::UnixStream, sync::mpsc, time::interval}; +use vt100::Parser; + +use crate::{ + actors::ui2, + input::{self, Input}, + input_parser::{self, InputParser}, + prelude::*, +}; + +struct Ui {} + +pub struct StatusLineState {} + +pub enum UiState { + Normal, + Selecting, +} + +pub struct TerminalState { + pub emulator: Parser, + pub size: (u16, u16), + pub needs_resize: bool, +} + +pub struct AppState { + pub terminal: TerminalState, + pub daemon: DaemonState, + pub ui: UiState, +} + +pub struct App { + pub state: AppState, + input_parser: InputParser, + ui: Ui, + stream: UnixStream, +} + +impl App { + pub fn new(stream: UnixStream) -> Self { + Self { + stream, + input_parser: InputParser::default(), + state: AppState { + terminal: TerminalState { + emulator: Parser::default(), + size: (0, 0), + needs_resize: true, + }, + daemon: DaemonState::default(), + ui: UiState::Normal, + }, + ui: Ui {}, + } + } + + pub async fn run(&mut self) -> Result<()> { + let (tx, mut rx) = mpsc::channel::(100); + input::start_input_listener(tx); + let mut ticker = interval(Duration::from_millis(50)); + let mut term = ratatui::init(); + loop { + // need an initial render since ui updates app state to convey terminal size information + term.draw(|f| ui2::draw(f, &mut self.state)).unwrap(); + if self.state.terminal.needs_resize { + self.state + .terminal + .emulator + .set_size(self.state.terminal.size.0, self.state.terminal.size.1); + self.state.terminal.needs_resize = false; + } + tokio::select! { + Some(input) = rx.recv() => { + use Input::{Stdin, Crossterm}; + match input { + Stdin(bytes) => { + self.dispatch_stdin(bytes).await; + } + Crossterm(event) => { + self.dispatch_crossterm(event, &mut term).await; + } + } + } + res = comm::recv_daemon_event(&mut self.stream) => { + match res { + Ok(event) => { + match event { + DaemonEvent::Raw(bytes) => { + trace!("DaemonEvent(Raw({bytes:?}))"); + self.state.terminal.emulator.process(&bytes); + } + _ => { + todo!(); + } + // DaemonEvent::Disconnected => { + // debug!("DaemonEvent(Disconnected)"); + // self.ui_handle.kill().await.unwrap(); + // break; + // } + // DaemonEvent::CurrentSessions(session_ids) => { + // debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); + // self.daemon_state.set_sessions(session_ids); + // self.sync_daemon_state = true; + // } + // DaemonEvent::ActiveSession(session_id) => { + // debug!("DaemonEvent(ActiveSession({session_id}))"); + // self.daemon_state.set_active_session(session_id); + // self.sync_daemon_state = true; + // } + // DaemonEvent::NewSession(session_id) => { + // debug!("DaemonEvent(NewSession({session_id}))"); + // self.daemon_state.add_session(session_id); + // self.sync_daemon_state = true; + // } + // DaemonEvent::DeletedSession(_session_id) => { + // todo!("implement delete session"); + // } + } + } + Err(e) => { + error!("Error receiving daemon event: {e}"); + break; + } + } + } + _ = ticker.tick() => { + term.draw(|f| ui2::draw(f, &mut self.state)).unwrap(); + } + } + } + Ok(()) + } + + async fn dispatch_stdin(&mut self, bytes: Bytes) { + for parsed_event in self.input_parser.process(&bytes) { + match parsed_event { + input_parser::ParsedEvent::LocalAction(_action) => { + todo!("update the application state") + } + input_parser::ParsedEvent::DaemonAction(cli_event) => { + comm::send_event(&mut self.stream, cli_event).await; + } + } + } + } + + async fn dispatch_crossterm(&mut self, event: Event, term: &mut Terminal>) { + match event { + Event::FocusGained => todo!(), + Event::FocusLost => todo!(), + Event::Key(key_event) => match key_event { + _ => todo!(), + }, + Event::Mouse(mouse_event) => match mouse_event { + _ => todo!(), + }, + Event::Paste(_) => todo!(), + Event::Resize(_, _) => { + self.state.terminal.needs_resize = true; + term.draw(|f| { + ui2::draw(f, &mut self.state); + }) + .unwrap(); + let (rows, cols) = self.state.terminal.size; + comm::send_event(&mut self.stream, CliEvent::TerminalResize { rows, cols }) + .await + .unwrap(); + } + } + } +} diff --git a/cli/src/error.rs b/cli/src/error.rs index 200d2b6..76657ec 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -2,7 +2,7 @@ use bytes::Bytes; use thiserror::Error; use tokio::sync::mpsc::error::SendError; -use crate::actors::{client::ClientEvent, lua::LuaEvent, ui::UIEvent}; +// use crate::actors::{client::ClientEvent, lua::LuaEvent, ui::UIEvent}; pub type Result = std::result::Result; @@ -37,12 +37,11 @@ pub enum Error { #[error("Lua error: {0}")] Lua(String), + // #[error("Event Send Error: {0}")] + // EventSend(EventSendError), - #[error("Event Send Error: {0}")] - EventSend(EventSendError), - - #[error("Sync Send Error: {0}")] - SyncSend(#[from] std::sync::mpsc::SendError), + // #[error("Sync Send Error: {0}")] + // SyncSend(#[from] std::sync::mpsc::SendError), } impl From for Error { @@ -51,42 +50,42 @@ impl From for Error { } } -#[derive(Error, Debug)] -pub enum EventSendError { - #[error("IO send error: {0}")] - IO(SendError), - #[error("UI send error: {0}")] - UI(SendError), - #[error("Bytes send error: {0}")] - Bytes(SendError), - // #[error("WidgetRunner send error: {0}")] - // WidgetRunner(SendError), - // #[error("LuaActor send error: {0}")] - // LuaActor(SendError), -} - -impl From> for Error { - fn from(e: SendError) -> Self { - Self::EventSend(EventSendError::IO(e)) - } -} -impl From> for Error { - fn from(e: SendError) -> Self { - Self::EventSend(EventSendError::UI(e)) - } -} -impl From> for Error { - fn from(e: SendError) -> Self { - Self::EventSend(EventSendError::Bytes(e)) - } -} -// impl From> for Error { -// fn from(e: SendError) -> Self { -// Self::EventSend(EventSendError::WidgetRunner(e)) +// #[derive(Error, Debug)] +// pub enum EventSendError { +// #[error("IO send error: {0}")] +// IO(SendError), +// #[error("UI send error: {0}")] +// UI(SendError), +// #[error("Bytes send error: {0}")] +// Bytes(SendError), +// // #[error("WidgetRunner send error: {0}")] +// // WidgetRunner(SendError), +// // #[error("LuaActor send error: {0}")] +// // LuaActor(SendError), +// } +// +// impl From> for Error { +// fn from(e: SendError) -> Self { +// Self::EventSend(EventSendError::IO(e)) +// } +// } +// impl From> for Error { +// fn from(e: SendError) -> Self { +// Self::EventSend(EventSendError::UI(e)) // } // } -// impl From> for Error { -// fn from(e: SendError) -> Self { -// Self::EventSend(EventSendError::LuaActor(e)) +// impl From> for Error { +// fn from(e: SendError) -> Self { +// Self::EventSend(EventSendError::Bytes(e)) // } // } +// // impl From> for Error { +// // fn from(e: SendError) -> Self { +// // Self::EventSend(EventSendError::WidgetRunner(e)) +// // } +// // } +// // impl From> for Error { +// // fn from(e: SendError) -> Self { +// // Self::EventSend(EventSendError::LuaActor(e)) +// // } +// // } diff --git a/cli/src/input.rs b/cli/src/input.rs new file mode 100644 index 0000000..d9c8b8b --- /dev/null +++ b/cli/src/input.rs @@ -0,0 +1,44 @@ +use bytes::Bytes; +use crossterm::event::{self, Event}; +use tokio::{io::AsyncReadExt, sync::mpsc}; + +use crate::prelude::*; + +#[derive(Debug)] +pub enum Input { + Stdin(Bytes), + Crossterm(Event), +} + +pub fn start_input_listener(tx: mpsc::Sender) { + tokio::spawn({ + let tx = tx.clone(); + async move { + let mut stdin = tokio::io::stdin(); + let mut buf = [0u8; 1024]; + loop { + match stdin.read(&mut buf).await { + Ok(n) if n > 0 => { + tx.send(Input::Stdin(Bytes::copy_from_slice(&buf))).await.unwrap(); + } + Ok(_) => { + break; + } + Err(e) => { + error!("Error receiving stdin: {e}"); + continue; + } + } + } + } + }); + + tokio::spawn(async move { + loop { + if event::poll(std::time::Duration::from_millis(100)).unwrap() { + let ev = event::read().unwrap(); + tx.send(Input::Crossterm(ev)).await.ok(); + } + } + }); +} diff --git a/cli/src/input_parser/parser.rs b/cli/src/input_parser/parser.rs index 54a994c..37241cc 100644 --- a/cli/src/input_parser/parser.rs +++ b/cli/src/input_parser/parser.rs @@ -17,16 +17,12 @@ const S: u8 = 0x73; const X: u8 = 0x78; const D: u8 = 0x64; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct InputParser { buf: Vec, } impl InputParser { - pub fn new() -> Self { - Self { buf: vec![] } - } - pub fn process(&mut self, input: &[u8]) -> Vec { use ParsedEvent::{DaemonAction, LocalAction}; self.buf.extend(input); diff --git a/cli/src/main.rs b/cli/src/main.rs index 93ad54d..1eb9ab1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,6 +1,8 @@ mod actors; +pub mod app; mod args; mod error; +mod input; mod input_parser; mod prelude; mod states; @@ -20,7 +22,7 @@ use remux_core::{ use tokio::net::UnixStream; use crate::{ - actors::Client, + app::App, args::{Args, Commands}, error::{Error, Result}, prelude::*, @@ -74,20 +76,6 @@ async fn connect() -> Result { }) } -// #[instrument(skip(stream))] -// async fn handle_session_command(mut stream: UnixStream, command: SessionCommands) -> Result<()> { -// let req: Request = command.into(); -// let res: Response = comm::send_and_recv(&mut stream, &req).await?; -// assert_eq!(res.status, ResponseStatus::Ok); -// match res.body { -// ResponseBody::SessionsList { sessions } => { -// println!("{sessions:?}"); -// } -// _ => {} -// } -// Ok(()) -// } - #[instrument] async fn run(command: Commands) -> Result<()> { let stream = connect().await?; @@ -105,7 +93,6 @@ async fn run(command: Commands) -> Result<()> { ) .await } - // Commands::Session { action } => handle_session_command(stream, action).await, _ => todo!(), } } @@ -117,20 +104,23 @@ async fn attach(mut stream: UnixStream, attach_request: CliRequestMessage { - error!("Error joining client task: {e}"); - } - Err(e) => { - error!("Error joining client task: {e}"); - } - _ => {} - } - } - disable_raw_mode()?; - debug!("Disabled raw mode"); - Ok(()) + let mut app = App::new(stream); + app.run().await + + // enable_raw_mode()?; + // debug!("Enabled raw mode"); + // if let Ok(task) = Client::spawn(stream, res.initial_daemon_state) { + // match task.await { + // Ok(Err(e)) => { + // error!("Error joining client task: {e}"); + // } + // Err(e) => { + // error!("Error joining client task: {e}"); + // } + // _ => {} + // } + // } + // disable_raw_mode()?; + // debug!("Disabled raw mode"); + // Ok(()) } diff --git a/cli/src/widgets/mod.rs b/cli/src/widgets/mod.rs index 8cbcba9..45c0957 100644 --- a/cli/src/widgets/mod.rs +++ b/cli/src/widgets/mod.rs @@ -1,5 +1,7 @@ mod basic_selector; mod fuzzy_selector; +mod status_line; +mod terminal; mod traits; pub use basic_selector::BasicSelector; diff --git a/cli/src/widgets/status_line.rs b/cli/src/widgets/status_line.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cli/src/widgets/status_line.rs @@ -0,0 +1 @@ + diff --git a/cli/src/widgets/terminal.rs b/cli/src/widgets/terminal.rs new file mode 100644 index 0000000..586fc18 --- /dev/null +++ b/cli/src/widgets/terminal.rs @@ -0,0 +1,226 @@ +// // use bytes::Bytes; +// // use ratatui::layout::Rect; +// // use tui_term::widget::PseudoTerminal; +// // use vt100::Parser; +// // +// // use crate::widgets::traits::Renderable; +// // +// // #[derive(Default)] +// // pub struct TerminalWidget { +// // emulator: Parser, +// // } +// // +// // impl TerminalWidget { +// // pub fn write(&mut self, bytes: Bytes) { +// // self.emulator.process(&bytes); +// // } +// // +// // pub fn size(&self) -> (u16, u16) { +// // self.emulator.screen().size() +// // } +// // +// // pub fn set_size(&mut self, rows: u16, cols: u16) { +// // self.emulator.set_size(rows, cols); +// // } +// // +// // } +// // +// // impl Renderable for TerminalWidget { +// // fn render(&self, f: &mut ratatui::Frame, rect: Rect) { +// // f.render_widget(PseudoTerminal::new(self.emulator.screen()), rect); +// // } +// // } +// +// use std::time::Duration; +// +// use bytes::Bytes; +// use handle_macro::Handle; +// use remux_core::{ +// comm, +// events::{CliEvent, DaemonEvent}, +// states::DaemonState, +// }; +// use tokio::{io::AsyncReadExt, net::UnixStream, sync::mpsc, time::interval}; +// use tracing::{Instrument, debug}; +// +// use crate::{ +// input_parser::{Action, InputParser, ParsedEvent}, +// prelude::*, +// utils::DisplayableVec, +// }; +// +// #[derive(Handle)] +// pub enum TerminalWidgetEvent { +// Selected(Option), // index of the selected item +// } +// use TerminalWidgetEvent::*; +// +// #[derive(Debug)] +// enum ClientWidgetEvent { +// Normal, // running normally with stdin parsed into events and sent to daemon +// SelectingSession, // means that the ui is currently busy selecting redirects stdin to ui selector +// } +// +// #[derive(Debug)] +// pub struct Client { +// _handle: ClientHandle, // handle used to send the client events +// stream: UnixStream, // the client owns the stream +// rx: mpsc::Receiver, // receiver for client events +// daemon_state: DaemonState, // determines if currently accepting events from daemon +// sync_daemon_state: bool, // if the state is dirty only then do we need to sync to the ui +// ui_stdin_tx: mpsc::Sender, // this is for popup actor to connect to stdin +// ui_handle: UIHandle, // how the client sends messages to ui +// input_parser: InputParser, // converts streams of bytes into actionable events +// client_state: ClientWidgetEvent, // the current state of the client +// } +// impl Client { +// #[instrument(skip(stream))] +// pub fn spawn(stream: UnixStream, daemon_state: DaemonState) -> Result { +// Client::new(stream, daemon_state)?.run() +// } +// +// #[instrument(skip(stream))] +// fn new(stream: UnixStream, daemon_state: DaemonState) -> Result { +// let (tx, rx) = mpsc::channel(100); +// let (ui_stdin_tx, ui_stdin_rx) = mpsc::channel(100); +// let handle = ClientHandle { tx }; +// let ui_handle = UI::spawn(handle.clone(), ui_stdin_rx)?; +// Ok(Self { +// _handle: handle, +// stream, +// rx, +// ui_stdin_tx, +// ui_handle, +// daemon_state, +// sync_daemon_state: true, +// input_parser: InputParser::new(), +// client_state: ClientWidgetEvent::Normal, +// }) +// } +// +// #[instrument(skip(self), fields(client_state = ?self.client_state))] +// fn run(mut self) -> Result { +// let task: CliTask = tokio::spawn({ +// let span = tracing::Span::current(); +// let mut stdin = tokio::io::stdin(); +// let mut stdin_buf = [0u8; 1024]; +// async move { +// let mut ticker = interval(Duration::from_millis(1000)); +// loop { +// tokio::select! { +// Some(event) = self.rx.recv() => { +// match event { +// Selected(index) => { +// debug!("Selected: {index:?}"); +// match self.client_state { +// ClientWidgetEvent::Normal => { +// error!("should not receive selected event in normal state"); +// }, +// ClientWidgetEvent::SelectingSession => { +// if let Some(index) = index { +// let selected_session = self.daemon_state.session_ids[index]; +// debug!("sending session selection: {selected_session}"); +// comm::send_event(&mut self.stream, CliEvent::SwitchSession(selected_session)).await.unwrap(); +// } +// debug!("Returning to normal state"); +// self.client_state = ClientWidgetEvent::Normal; +// } +// } +// } +// } +// }, +// res = comm::recv_daemon_event(&mut self.stream) => { +// match res { +// Ok(event) => { +// match event { +// DaemonEvent::Raw(bytes) => { +// trace!("DaemonEvent(Raw({bytes:?}))"); +// self.ui_handle.output(bytes).await?; +// } +// DaemonEvent::Disconnected => { +// debug!("DaemonEvent(Disconnected)"); +// self.ui_handle.kill().await.unwrap(); +// break; +// } +// DaemonEvent::CurrentSessions(session_ids) => { +// debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); +// self.daemon_state.set_sessions(session_ids); +// self.sync_daemon_state = true; +// } +// DaemonEvent::ActiveSession(session_id) => { +// debug!("DaemonEvent(ActiveSession({session_id}))"); +// self.daemon_state.set_active_session(session_id); +// self.sync_daemon_state = true; +// } +// DaemonEvent::NewSession(session_id) => { +// debug!("DaemonEvent(NewSession({session_id}))"); +// self.daemon_state.add_session(session_id); +// self.sync_daemon_state = true; +// } +// DaemonEvent::DeletedSession(_session_id) => { +// todo!("implement delete session"); +// } +// } +// } +// Err(e) => { +// error!("Error receiving daemon event: {e}"); +// break; +// } +// } +// } +// stdin_res = stdin.read(&mut stdin_buf) => { +// match stdin_res { +// Ok(n) if n > 0 => { +// match self.client_state { +// ClientWidgetEvent::Normal => { +// trace!("Sending {n} bytes to Daemon"); +// for event in self.input_parser.process(&stdin_buf[..n]) { +// match event { +// ParsedEvent::DaemonAction(cli_event) => { +// comm::send_event(&mut self.stream, cli_event).await?; +// }, +// ParsedEvent::LocalAction(local_action) => { +// match local_action { +// Action::SwitchSession => { +// self.client_state = ClientWidgetEvent::SelectingSession; +// let items = DisplayableVec::new(self.daemon_state.session_ids.clone()); +// self.ui_handle.select_fuzzy(items, "Select Session".to_owned()).await.unwrap(); +// }, +// } +// }, +// } +// } +// +// }, +// ClientWidgetEvent::SelectingSession => { +// trace!("Sending {n} bytes to ui"); +// self.ui_stdin_tx.send(Bytes::copy_from_slice(&stdin_buf[..n])).await?; +// }, +// } +// } +// Ok(_) => { +// break; +// } +// Err(e) => { +// error!("Error receiving stdin: {e}"); +// continue; +// } +// } +// }, +// _ = ticker.tick(), if self.sync_daemon_state => { +// debug!("syncing daemon_state"); +// self.ui_handle.sync_daemon_state(self.daemon_state.clone()).await?; +// self.sync_daemon_state = false; +// } +// } +// } +// debug!("Client stopped"); +// Ok(()) +// }.instrument(span) +// }); +// +// Ok(task) +// } +// } +// +// diff --git a/cli/src/widgets/traits.rs b/cli/src/widgets/traits.rs index 6b07c5b..45b586f 100644 --- a/cli/src/widgets/traits.rs +++ b/cli/src/widgets/traits.rs @@ -1,7 +1,8 @@ use std::sync::{Arc, RwLock}; use bytes::Bytes; -use ratatui::Frame; +use crossterm::event::Event; +use ratatui::{Frame, layout::Rect}; use tokio::sync::broadcast; use crate::{prelude::*, utils::DisplayableVec}; @@ -16,3 +17,9 @@ pub trait Selector { fn render(selector: &Arc>, f: &mut Frame); } + +pub trait Component { + fn handle_event(&mut self, event: Option); + + fn render(&mut self, f: &mut Frame, rect: Rect); +} diff --git a/core/src/events.rs b/core/src/events.rs index 7cb408a..e21791d 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -14,6 +14,8 @@ pub enum CliEvent { SwitchSession(u32), // switch session - does nothing if session does not exist + TerminalResize { rows: u16, cols: u16 }, + Detach, } diff --git a/daemon/src/actors/client_connection.rs b/daemon/src/actors/client_connection.rs index 3b08d2c..40426d5 100644 --- a/daemon/src/actors/client_connection.rs +++ b/daemon/src/actors/client_connection.rs @@ -144,6 +144,10 @@ impl ClientConnection { trace!("Client Event Input: raw({bytes:?})"); self.session_manager_handle.user_input(self.id, bytes).await.unwrap(); }, + CliEvent::TerminalResize{rows, cols} => { + trace!("Client Event Input: terminal resize(rows={rows}, cols={cols})"); + todo!() + }, CliEvent::Detach => { trace!("Client Event Input: detach"); self.session_manager_handle.client_disconnect(self.id).await.unwrap(); From ab6456beeaab5cf2d953212b9c6758f2fd1a83fc Mon Sep 17 00:00:00 2001 From: Prometheus1400 Date: Thu, 4 Dec 2025 20:22:40 -0800 Subject: [PATCH 2/5] wip --- Cargo.lock | 14 +++++++++++ Cargo.toml | 3 ++- cli/Cargo.toml | 1 + cli/src/actors/ui2.rs | 4 ++- cli/src/app.rs | 58 +++++++++++++++++-------------------------- cli/src/input.rs | 18 ++++++++------ 6 files changed, 53 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbe0739..49b2503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,6 +271,7 @@ dependencies = [ "crossterm_winapi", "derive_more", "document-features", + "futures-core", "mio", "parking_lot", "rustix 1.1.2", @@ -447,6 +448,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-task" version = "0.3.31" @@ -460,6 +472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "pin-utils", @@ -952,6 +965,7 @@ dependencies = [ "bytes", "clap", "crossterm 0.29.0", + "futures-util", "fuzzy-matcher", "handle-macro", "mlua", diff --git a/Cargo.toml b/Cargo.toml index 1b36b51..e6601af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,10 @@ edition = "2024" [workspace.dependencies] bytes = { version = "1.11.0", features = ["serde"] } -crossterm = "0.29.0" +crossterm = { version = "0.29.0", features = ["event-stream"] } derive_more = { version = "2", features = ["display"] } itertools = { version = "0.14" } +futures-util = { version = "0.3" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.145" } thiserror = { version = "2.0.17" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 9f59668..09992d2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -10,6 +10,7 @@ remux-core = { path = "../core" } bytes.workspace = true crossterm.workspace = true +futures-util.workspace = true serde.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/cli/src/actors/ui2.rs b/cli/src/actors/ui2.rs index 698151a..6d45d76 100644 --- a/cli/src/actors/ui2.rs +++ b/cli/src/actors/ui2.rs @@ -16,8 +16,10 @@ pub fn draw(f: &mut Frame, state: &mut AppState) { .split(f.area()); // render the normal terminal output + let term_area = chunks[0]; + state.terminal.size = (term_area.height, term_area.width); let term_ui = PseudoTerminal::new(state.terminal.emulator.screen()); - f.render_widget(term_ui, chunks[0]); + f.render_widget(term_ui, term_area); // render the status bar // let status_line = StatusLine::new(app.status_line); diff --git a/cli/src/app.rs b/cli/src/app.rs index b0e494e..22b0399 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -1,7 +1,10 @@ -use std::{io::Stdout, time::Duration}; +use std::{ + io::{Stdout, stdin, stdout}, + time::Duration, +}; use bytes::Bytes; -use crossterm::event::Event; +use crossterm::{event::Event, execute}; use ratatui::{Terminal, prelude::CrosstermBackend}; use remux_core::{ comm, @@ -65,6 +68,7 @@ impl App { } pub async fn run(&mut self) -> Result<()> { + debug!("starting app"); let (tx, mut rx) = mpsc::channel::(100); input::start_input_listener(tx); let mut ticker = interval(Duration::from_millis(50)); @@ -73,21 +77,30 @@ impl App { // need an initial render since ui updates app state to convey terminal size information term.draw(|f| ui2::draw(f, &mut self.state)).unwrap(); if self.state.terminal.needs_resize { - self.state - .terminal - .emulator - .set_size(self.state.terminal.size.0, self.state.terminal.size.1); + let (rows, cols) = self.state.terminal.size; + debug!("setting terminal emulator size (rows={rows}, cols={cols})"); + self.state.terminal.emulator.set_size(rows, cols); self.state.terminal.needs_resize = false; } tokio::select! { Some(input) = rx.recv() => { - use Input::{Stdin, Crossterm}; + use Input::{Stdin, Resize}; match input { Stdin(bytes) => { + trace!("stdin({bytes:?}"); self.dispatch_stdin(bytes).await; } - Crossterm(event) => { - self.dispatch_crossterm(event, &mut term).await; + Resize => { + debug!("resize"); + self.state.terminal.needs_resize = true; + term.draw(|f| { + ui2::draw(f, &mut self.state); + }) + .unwrap(); + let (rows, cols) = self.state.terminal.size; + comm::send_event(&mut self.stream, CliEvent::TerminalResize { rows, cols }) + .await + .unwrap(); } } } @@ -100,7 +113,7 @@ impl App { self.state.terminal.emulator.process(&bytes); } _ => { - todo!(); + // todo!(); } // DaemonEvent::Disconnected => { // debug!("DaemonEvent(Disconnected)"); @@ -153,29 +166,4 @@ impl App { } } } - - async fn dispatch_crossterm(&mut self, event: Event, term: &mut Terminal>) { - match event { - Event::FocusGained => todo!(), - Event::FocusLost => todo!(), - Event::Key(key_event) => match key_event { - _ => todo!(), - }, - Event::Mouse(mouse_event) => match mouse_event { - _ => todo!(), - }, - Event::Paste(_) => todo!(), - Event::Resize(_, _) => { - self.state.terminal.needs_resize = true; - term.draw(|f| { - ui2::draw(f, &mut self.state); - }) - .unwrap(); - let (rows, cols) = self.state.terminal.size; - comm::send_event(&mut self.stream, CliEvent::TerminalResize { rows, cols }) - .await - .unwrap(); - } - } - } } diff --git a/cli/src/input.rs b/cli/src/input.rs index d9c8b8b..5ae3420 100644 --- a/cli/src/input.rs +++ b/cli/src/input.rs @@ -1,13 +1,16 @@ use bytes::Bytes; -use crossterm::event::{self, Event}; -use tokio::{io::AsyncReadExt, sync::mpsc}; +use tokio::{ + io::AsyncReadExt, + signal::unix::{SignalKind, signal}, + sync::mpsc, +}; use crate::prelude::*; #[derive(Debug)] pub enum Input { Stdin(Bytes), - Crossterm(Event), + Resize, } pub fn start_input_listener(tx: mpsc::Sender) { @@ -19,6 +22,7 @@ pub fn start_input_listener(tx: mpsc::Sender) { loop { match stdin.read(&mut buf).await { Ok(n) if n > 0 => { + trace!("read {} bytes from stdin", n); tx.send(Input::Stdin(Bytes::copy_from_slice(&buf))).await.unwrap(); } Ok(_) => { @@ -34,11 +38,9 @@ pub fn start_input_listener(tx: mpsc::Sender) { }); tokio::spawn(async move { - loop { - if event::poll(std::time::Duration::from_millis(100)).unwrap() { - let ev = event::read().unwrap(); - tx.send(Input::Crossterm(ev)).await.ok(); - } + let mut sigwinch = signal(SignalKind::window_change()).unwrap(); + while sigwinch.recv().await.is_some() { + tx.send(Input::Resize).await.unwrap(); } }); } From fa80d0c70a7deeab05bdd37a6fdbf52c1e00a5f8 Mon Sep 17 00:00:00 2001 From: Prometheus1400 Date: Thu, 4 Dec 2025 21:21:21 -0800 Subject: [PATCH 3/5] fix bug --- Cargo.lock | 51 +++++++++++++++++++------- cli/Cargo.toml | 1 + cli/src/actors/ui2.rs | 4 +- cli/src/app.rs | 28 +++++++++++--- cli/src/input.rs | 2 +- cli/src/main.rs | 2 +- daemon/src/actors/client_connection.rs | 2 +- 7 files changed, 67 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49b2503..4fc1d69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,7 +192,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -310,7 +310,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.111", ] [[package]] @@ -321,7 +321,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -333,6 +333,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -351,7 +362,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "unicode-xid", ] @@ -456,7 +467,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -507,7 +518,7 @@ dependencies = [ "bytes", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "tokio", ] @@ -553,7 +564,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -965,6 +976,7 @@ dependencies = [ "bytes", "clap", "crossterm 0.29.0", + "derivative", "futures-util", "fuzzy-matcher", "handle-macro", @@ -1104,7 +1116,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1218,7 +1230,18 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -1267,7 +1290,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1278,7 +1301,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1346,7 +1369,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1380,7 +1403,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1787,5 +1810,5 @@ checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 09992d2..89cfe4d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,3 +25,4 @@ ratatui = { version = "0.29", features = ["crossterm"] } terminput = "0.5.11" tui-term = "0.2" fuzzy-matcher = "0.3.7" +derivative = "2.2.0" diff --git a/cli/src/actors/ui2.rs b/cli/src/actors/ui2.rs index 6d45d76..e90a589 100644 --- a/cli/src/actors/ui2.rs +++ b/cli/src/actors/ui2.rs @@ -4,8 +4,9 @@ use ratatui::{ }; use tui_term::widget::PseudoTerminal; -use crate::app::AppState; +use crate::{app::AppState, prelude::*}; +#[instrument(skip(f))] pub fn draw(f: &mut Frame, state: &mut AppState) { let chunks = Layout::default() .direction(Direction::Vertical) @@ -18,6 +19,7 @@ pub fn draw(f: &mut Frame, state: &mut AppState) { // render the normal terminal output let term_area = chunks[0]; state.terminal.size = (term_area.height, term_area.width); + trace!("rendering terminal into rect: {term_area}"); let term_ui = PseudoTerminal::new(state.terminal.emulator.screen()); f.render_widget(term_ui, term_area); diff --git a/cli/src/app.rs b/cli/src/app.rs index 22b0399..38eeda5 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -1,10 +1,14 @@ +use core::panic; use std::{ + fmt::Debug, io::{Stdout, stdin, stdout}, + panic::set_hook, time::Duration, }; use bytes::Bytes; use crossterm::{event::Event, execute}; +use derivative::Derivative; use ratatui::{Terminal, prelude::CrosstermBackend}; use remux_core::{ comm, @@ -21,21 +25,28 @@ use crate::{ prelude::*, }; +#[derive(Debug)] struct Ui {} +#[derive(Debug)] pub struct StatusLineState {} +#[derive(Debug)] pub enum UiState { Normal, Selecting, } +#[derive(Derivative)] +#[derivative(Debug)] pub struct TerminalState { + #[derivative(Debug = "ignore")] pub emulator: Parser, pub size: (u16, u16), pub needs_resize: bool, } +#[derive(Debug)] pub struct AppState { pub terminal: TerminalState, pub daemon: DaemonState, @@ -50,7 +61,7 @@ pub struct App { } impl App { - pub fn new(stream: UnixStream) -> Self { + pub fn new(stream: UnixStream, daemon_state: DaemonState) -> Self { Self { stream, input_parser: InputParser::default(), @@ -60,22 +71,27 @@ impl App { size: (0, 0), needs_resize: true, }, - daemon: DaemonState::default(), + daemon: daemon_state, ui: UiState::Normal, }, ui: Ui {}, } } + #[instrument(skip(self))] pub async fn run(&mut self) -> Result<()> { debug!("starting app"); let (tx, mut rx) = mpsc::channel::(100); input::start_input_listener(tx); let mut ticker = interval(Duration::from_millis(50)); let mut term = ratatui::init(); + set_hook(Box::new(|info| { + ratatui::restore(); + eprintln!("Application crashed: {info}"); + })); + // need an initial render since ui updates app state to convey terminal size information + term.draw(|f| ui2::draw(f, &mut self.state)).unwrap(); loop { - // need an initial render since ui updates app state to convey terminal size information - term.draw(|f| ui2::draw(f, &mut self.state)).unwrap(); if self.state.terminal.needs_resize { let (rows, cols) = self.state.terminal.size; debug!("setting terminal emulator size (rows={rows}, cols={cols})"); @@ -154,6 +170,7 @@ impl App { Ok(()) } + #[instrument(skip(self, bytes))] async fn dispatch_stdin(&mut self, bytes: Bytes) { for parsed_event in self.input_parser.process(&bytes) { match parsed_event { @@ -161,7 +178,8 @@ impl App { todo!("update the application state") } input_parser::ParsedEvent::DaemonAction(cli_event) => { - comm::send_event(&mut self.stream, cli_event).await; + debug!("sending cli event: {cli_event:?}"); + comm::send_event(&mut self.stream, cli_event).await.unwrap(); } } } diff --git a/cli/src/input.rs b/cli/src/input.rs index 5ae3420..914ca2d 100644 --- a/cli/src/input.rs +++ b/cli/src/input.rs @@ -23,7 +23,7 @@ pub fn start_input_listener(tx: mpsc::Sender) { match stdin.read(&mut buf).await { Ok(n) if n > 0 => { trace!("read {} bytes from stdin", n); - tx.send(Input::Stdin(Bytes::copy_from_slice(&buf))).await.unwrap(); + tx.send(Input::Stdin(Bytes::copy_from_slice(&buf[..n]))).await.unwrap(); } Ok(_) => { break; diff --git a/cli/src/main.rs b/cli/src/main.rs index 1eb9ab1..a4f9dfd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -104,7 +104,7 @@ async fn attach(mut stream: UnixStream, attach_request: CliRequestMessage { trace!("Client Event Input: terminal resize(rows={rows}, cols={cols})"); - todo!() + // todo!() }, CliEvent::Detach => { trace!("Client Event Input: detach"); From 1e7cde99a420086c43042ba3f20bae5020ce8289 Mon Sep 17 00:00:00 2001 From: Prometheus1400 Date: Fri, 5 Dec 2025 08:03:34 -0800 Subject: [PATCH 4/5] wip --- Cargo.lock | 1 - Cargo.toml | 2 +- cli/src/actors/client.rs | 192 ------------------------- cli/src/actors/mod.rs | 2 +- cli/src/actors/ui.rs | 191 ------------------------ cli/src/app.rs | 91 +++++++----- cli/src/input.rs | 10 +- cli/src/main.rs | 36 ++--- cli/src/ui/mod.rs | 3 + cli/src/{actors/ui2.rs => ui/ui.rs} | 0 daemon/src/actors/client_connection.rs | 4 +- 11 files changed, 83 insertions(+), 449 deletions(-) delete mode 100644 cli/src/actors/client.rs delete mode 100644 cli/src/actors/ui.rs create mode 100644 cli/src/ui/mod.rs rename cli/src/{actors/ui2.rs => ui/ui.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 4fc1d69..fd381e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,7 +271,6 @@ dependencies = [ "crossterm_winapi", "derive_more", "document-features", - "futures-core", "mio", "parking_lot", "rustix 1.1.2", diff --git a/Cargo.toml b/Cargo.toml index e6601af..eb3c191 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" [workspace.dependencies] bytes = { version = "1.11.0", features = ["serde"] } -crossterm = { version = "0.29.0", features = ["event-stream"] } +crossterm = { version = "0.29.0" } derive_more = { version = "2", features = ["display"] } itertools = { version = "0.14" } futures-util = { version = "0.3" } diff --git a/cli/src/actors/client.rs b/cli/src/actors/client.rs deleted file mode 100644 index fe52ef4..0000000 --- a/cli/src/actors/client.rs +++ /dev/null @@ -1,192 +0,0 @@ -// use std::time::Duration; -// -// use bytes::Bytes; -// use handle_macro::Handle; -// use remux_core::{ -// comm, -// events::{CliEvent, DaemonEvent}, -// states::DaemonState, -// }; -// use tokio::{io::AsyncReadExt, net::UnixStream, sync::mpsc, time::interval}; -// use tracing::{Instrument, debug}; -// -// use crate::{ -// actors::ui::{UI, UIHandle}, -// input_parser::{Action, InputParser, ParsedEvent}, -// prelude::*, -// utils::DisplayableVec, -// }; -// -// #[derive(Handle)] -// pub enum ClientEvent { -// Selected(Option), // index of the selected item -// } -// use ClientEvent::*; -// -// #[derive(Debug)] -// enum ClientState { -// Normal, // running normally with stdin parsed into events and sent to daemon -// SelectingSession, // means that the ui is currently busy selecting redirects stdin to ui selector -// } -// -// #[derive(Debug)] -// pub struct Client { -// _handle: ClientHandle, // handle used to send the client events -// stream: UnixStream, // the client owns the stream -// rx: mpsc::Receiver, // receiver for client events -// daemon_state: DaemonState, // determines if currently accepting events from daemon -// sync_daemon_state: bool, // if the state is dirty only then do we need to sync to the ui -// ui_stdin_tx: mpsc::Sender, // this is for popup actor to connect to stdin -// ui_handle: UIHandle, // how the client sends messages to ui -// input_parser: InputParser, // converts streams of bytes into actionable events -// client_state: ClientState, // the current state of the client -// } -// impl Client { -// #[instrument(skip(stream))] -// pub fn spawn(stream: UnixStream, daemon_state: DaemonState) -> Result { -// Client::new(stream, daemon_state)?.run() -// } -// -// #[instrument(skip(stream))] -// fn new(stream: UnixStream, daemon_state: DaemonState) -> Result { -// let (tx, rx) = mpsc::channel(100); -// let (ui_stdin_tx, ui_stdin_rx) = mpsc::channel(100); -// let handle = ClientHandle { tx }; -// let ui_handle = UI::spawn(handle.clone(), ui_stdin_rx)?; -// Ok(Self { -// _handle: handle, -// stream, -// rx, -// ui_stdin_tx, -// ui_handle, -// daemon_state, -// sync_daemon_state: true, -// input_parser: InputParser::new(), -// client_state: ClientState::Normal, -// }) -// } -// -// #[instrument(skip(self), fields(client_state = ?self.client_state))] -// fn run(mut self) -> Result { -// let task: CliTask = tokio::spawn({ -// let span = tracing::Span::current(); -// let mut stdin = tokio::io::stdin(); -// let mut stdin_buf = [0u8; 1024]; -// async move { -// let mut ticker = interval(Duration::from_millis(1000)); -// loop { -// tokio::select! { -// Some(event) = self.rx.recv() => { -// match event { -// Selected(index) => { -// debug!("Selected: {index:?}"); -// match self.client_state { -// ClientState::Normal => { -// error!("should not receive selected event in normal state"); -// }, -// ClientState::SelectingSession => { -// if let Some(index) = index { -// let selected_session = self.daemon_state.session_ids[index]; -// debug!("sending session selection: {selected_session}"); -// comm::send_event(&mut self.stream, CliEvent::SwitchSession(selected_session)).await.unwrap(); -// } -// debug!("Returning to normal state"); -// self.client_state = ClientState::Normal; -// } -// } -// } -// } -// }, -// res = comm::recv_daemon_event(&mut self.stream) => { -// match res { -// Ok(event) => { -// match event { -// DaemonEvent::Raw(bytes) => { -// trace!("DaemonEvent(Raw({bytes:?}))"); -// self.ui_handle.output(bytes).await?; -// } -// DaemonEvent::Disconnected => { -// debug!("DaemonEvent(Disconnected)"); -// self.ui_handle.kill().await.unwrap(); -// break; -// } -// DaemonEvent::CurrentSessions(session_ids) => { -// debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); -// self.daemon_state.set_sessions(session_ids); -// self.sync_daemon_state = true; -// } -// DaemonEvent::ActiveSession(session_id) => { -// debug!("DaemonEvent(ActiveSession({session_id}))"); -// self.daemon_state.set_active_session(session_id); -// self.sync_daemon_state = true; -// } -// DaemonEvent::NewSession(session_id) => { -// debug!("DaemonEvent(NewSession({session_id}))"); -// self.daemon_state.add_session(session_id); -// self.sync_daemon_state = true; -// } -// DaemonEvent::DeletedSession(_session_id) => { -// todo!("implement delete session"); -// } -// } -// } -// Err(e) => { -// error!("Error receiving daemon event: {e}"); -// break; -// } -// } -// } -// stdin_res = stdin.read(&mut stdin_buf) => { -// match stdin_res { -// Ok(n) if n > 0 => { -// match self.client_state { -// ClientState::Normal => { -// trace!("Sending {n} bytes to Daemon"); -// for event in self.input_parser.process(&stdin_buf[..n]) { -// match event { -// ParsedEvent::DaemonAction(cli_event) => { -// comm::send_event(&mut self.stream, cli_event).await?; -// }, -// ParsedEvent::LocalAction(local_action) => { -// match local_action { -// Action::SwitchSession => { -// self.client_state = ClientState::SelectingSession; -// let items = DisplayableVec::new(self.daemon_state.session_ids.clone()); -// self.ui_handle.select_fuzzy(items, "Select Session".to_owned()).await.unwrap(); -// }, -// } -// }, -// } -// } -// -// }, -// ClientState::SelectingSession => { -// trace!("Sending {n} bytes to ui"); -// self.ui_stdin_tx.send(Bytes::copy_from_slice(&stdin_buf[..n])).await?; -// }, -// } -// } -// Ok(_) => { -// break; -// } -// Err(e) => { -// error!("Error receiving stdin: {e}"); -// continue; -// } -// } -// }, -// _ = ticker.tick(), if self.sync_daemon_state => { -// debug!("syncing daemon_state"); -// self.ui_handle.sync_daemon_state(self.daemon_state.clone()).await?; -// self.sync_daemon_state = false; -// } -// } -// } -// debug!("Client stopped"); -// Ok(()) -// }.instrument(span) -// }); -// -// Ok(task) -// } -// } diff --git a/cli/src/actors/mod.rs b/cli/src/actors/mod.rs index 9afd6f1..8b13789 100644 --- a/cli/src/actors/mod.rs +++ b/cli/src/actors/mod.rs @@ -1 +1 @@ -pub mod ui2; + diff --git a/cli/src/actors/ui.rs b/cli/src/actors/ui.rs deleted file mode 100644 index cf2417d..0000000 --- a/cli/src/actors/ui.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{ - io::stdout, - sync::{Arc, RwLock}, - time::Duration, -}; - -use bytes::Bytes; -use crossterm::{ - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen}, -}; -use handle_macro::Handle; -use ratatui::{Terminal, prelude::CrosstermBackend}; -use remux_core::states::DaemonState; -use tokio::{ - sync::{broadcast, mpsc}, - time::interval, -}; -use tracing::Instrument; -use tui_term::widget::PseudoTerminal; -use vt100::Parser; - -use crate::{ - prelude::*, - states::status_line_state::StatusLineState, - utils::DisplayableVec, - widgets::{BasicSelector, FuzzySelector, Selector, TerminalWidget}, -}; -use crate::actors::{ - client::ClientHandle, lua::{Lua, LuaHandle} - }; - -#[derive(Handle)] -pub enum UIEvent { - Output(Bytes), - Stdin(Bytes), - Kill, - SyncDaemonState(DaemonState), - SyncStatusLineState(StatusLineState), - SelectBasic { items: DisplayableVec, title: String }, - SelectFuzzy { items: DisplayableVec, title: String }, - SelectedIndex(Option), -} -use UIEvent::*; - -#[derive(Debug, PartialEq)] -enum UIState { - Normal, - SelectingBasic, - SelectingFuzzy, -} - -pub struct UI { - // for communication - _handle: UIHandle, - rx: mpsc::Receiver, - // state for rendering - daemon_state: DaemonState, - status_line_state: StatusLineState, - parser: Parser, - lua_handle: LuaHandle, - client_handle: ClientHandle, - ui_state: UIState, - basic_selector: Arc>, - fuzzy_selector: Arc>, - selector_rx: mpsc::Receiver>, - stdin_rx: mpsc::Receiver, - - terminal_widget: TerminalWidget -} - -impl UI { - #[instrument(skip())] - pub fn new() -> Result { - let (tx, rx) = mpsc::channel(100); - let _handle = UIHandle { tx }; - - let client_handle = Client::spawn(, daemon_state); - - let parser = vt100::Parser::default(); - let lua_handle = Lua::spawn(handle.clone())?; - let (selector_tx, selector_rx) = mpsc::channel(100); - - Ok(Self { - _handle, - rx, - - daemon_state: DaemonState::default(), - status_line_state: StatusLineState::default(), - parser, - lua_handle, - client_handle, - ui_state: UIState::Normal, - basic_selector: BasicSelector::new(selector_tx.clone()), - fuzzy_selector: FuzzySelector::new(selector_tx.clone()), - selector_rx, - stdin_rx, - terminal_widget: TerminalWidget::default(), - }) - } - #[instrument(skip(self), fields(ui_state = ?self.ui_state))] - pub async fn run(mut self) -> Result<()> { - let span = tracing::Span::current(); - let mut term = Terminal::new(CrosstermBackend::new(stdout())).unwrap(); - let (selector_tx, _) = broadcast::channel(10000); - let selector_tx = selector_tx.clone(); - let mut ticker = interval(Duration::from_millis(16)); - execute!(stdout(), EnterAlternateScreen)?; - term.clear()?; - let task = tokio::spawn({ - async move { - loop { - tokio::select! { - Some(event) = self.rx.recv() => { - match event { - Output(bytes) => { - self.parser.process(&bytes); - } - Stdin(bytes) if matches!(self.ui_state, UIState::SelectingFuzzy | UIState::SelectingBasic) => { - selector_tx.send(bytes).unwrap(); - } - Stdin(_) => { - error!("Shouldn't recieve stdin in current state"); - } - SyncDaemonState(daemon_state) => { - self.lua_handle.sync_daemon_state(daemon_state.clone()).unwrap(); - self.daemon_state = daemon_state; - } - SyncStatusLineState(status_line_state) => { - self.status_line_state = status_line_state; - } - SelectBasic{items, title} => { - self.ui_state = UIState::SelectingBasic; - BasicSelector::run(&self.basic_selector, selector_tx.subscribe(), items, title).unwrap(); - } - SelectFuzzy{items, title} => { - self.ui_state = UIState::SelectingFuzzy; - FuzzySelector::run(&self.fuzzy_selector, selector_tx.subscribe(), items, title).unwrap(); - } - SelectedIndex(index) => { - self.client_handle.selected(index).await.unwrap(); - self.ui_state = UIState::Normal; - } - Kill => { - self.lua_handle.kill().unwrap(); - break; - } - } - } - _ = ticker.tick() => { - let screen = self.parser.screen(); - term.draw(|f| { - use ratatui::{ - layout::Constraint, layout::Direction, layout::Layout, - }; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Min(1), // pseudo terminal takes everything else - Constraint::Length(1), // bottom status bar - ]) - .split(f.area()); - - // render the normal terminal output - let term_ui = PseudoTerminal::new(screen); - f.render_widget(term_ui, chunks[0]); - - // render the status bar - self.status_line_state.render(f, chunks[1]); - - // render selector if active - if self.ui_state == UIState::SelectingBasic { - BasicSelector::render(&self.basic_selector, f); - } - if self.ui_state == UIState::SelectingFuzzy { - FuzzySelector::render(&self.fuzzy_selector, f); - } - - }).unwrap(); - } - } - } - debug!("ui stopped"); - execute!(stdout(), LeaveAlternateScreen)?; - Ok::<(), Error>(()) - }.instrument(span) - }).await; - Ok(()) - } -} diff --git a/cli/src/app.rs b/cli/src/app.rs index 38eeda5..ca245c1 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -1,33 +1,36 @@ -use core::panic; use std::{ fmt::Debug, - io::{Stdout, stdin, stdout}, - panic::set_hook, + io::{Stdout, stdout}, time::Duration, }; use bytes::Bytes; -use crossterm::{event::Event, execute}; +use crossterm::{ + cursor::Show, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode}, +}; use derivative::Derivative; -use ratatui::{Terminal, prelude::CrosstermBackend}; +use ratatui::{Terminal, crossterm::terminal::disable_raw_mode, prelude::CrosstermBackend, restore}; use remux_core::{ comm, events::{CliEvent, DaemonEvent}, states::DaemonState, }; -use tokio::{net::UnixStream, sync::mpsc, time::interval}; +use tokio::{ + net::UnixStream, + sync::{mpsc, watch}, + time::interval, +}; use vt100::Parser; use crate::{ - actors::ui2, input::{self, Input}, input_parser::{self, InputParser}, prelude::*, + ui, }; -#[derive(Debug)] -struct Ui {} - #[derive(Debug)] pub struct StatusLineState {} @@ -56,8 +59,8 @@ pub struct AppState { pub struct App { pub state: AppState, input_parser: InputParser, - ui: Ui, stream: UnixStream, + bg_tasks: Vec, } impl App { @@ -74,7 +77,7 @@ impl App { daemon: daemon_state, ui: UiState::Normal, }, - ui: Ui {}, + bg_tasks: Vec::new(), } } @@ -82,21 +85,27 @@ impl App { pub async fn run(&mut self) -> Result<()> { debug!("starting app"); let (tx, mut rx) = mpsc::channel::(100); - input::start_input_listener(tx); + self.bg_tasks.extend(input::start_input_listeners(tx)); let mut ticker = interval(Duration::from_millis(50)); - let mut term = ratatui::init(); - set_hook(Box::new(|info| { - ratatui::restore(); - eprintln!("Application crashed: {info}"); - })); + let mut term = ratatui::Terminal::new(CrosstermBackend::new(stdout())).unwrap(); + install_panic_hook(); + enable_raw_mode()?; + debug!("Enabled raw mode"); + execute!(stdout(), EnterAlternateScreen)?; + debug!("Entered alternate screen"); + // need an initial render since ui updates app state to convey terminal size information - term.draw(|f| ui2::draw(f, &mut self.state)).unwrap(); + term.draw(|f| ui::draw(f, &mut self.state)).unwrap(); loop { if self.state.terminal.needs_resize { let (rows, cols) = self.state.terminal.size; debug!("setting terminal emulator size (rows={rows}, cols={cols})"); self.state.terminal.emulator.set_size(rows, cols); self.state.terminal.needs_resize = false; + let (rows, cols) = self.state.terminal.size; + comm::send_event(&mut self.stream, CliEvent::TerminalResize { rows, cols }) + .await + .unwrap(); } tokio::select! { Some(input) = rx.recv() => { @@ -108,15 +117,7 @@ impl App { } Resize => { debug!("resize"); - self.state.terminal.needs_resize = true; - term.draw(|f| { - ui2::draw(f, &mut self.state); - }) - .unwrap(); - let (rows, cols) = self.state.terminal.size; - comm::send_event(&mut self.stream, CliEvent::TerminalResize { rows, cols }) - .await - .unwrap(); + self.handle_resize(&mut term).await; } } } @@ -128,14 +129,13 @@ impl App { trace!("DaemonEvent(Raw({bytes:?}))"); self.state.terminal.emulator.process(&bytes); } + DaemonEvent::Disconnected => { + debug!("DaemonEvent(Disconnected)"); + break; + } _ => { // todo!(); } - // DaemonEvent::Disconnected => { - // debug!("DaemonEvent(Disconnected)"); - // self.ui_handle.kill().await.unwrap(); - // break; - // } // DaemonEvent::CurrentSessions(session_ids) => { // debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); // self.daemon_state.set_sessions(session_ids); @@ -163,10 +163,17 @@ impl App { } } _ = ticker.tick() => { - term.draw(|f| ui2::draw(f, &mut self.state)).unwrap(); + term.draw(|f| ui::draw(f, &mut self.state)).unwrap(); } } } + for task in self.bg_tasks.drain(..) { + task.abort(); + let _ = task.await; + } + drop(term); + restore(); + debug!("Restoring terminal"); Ok(()) } @@ -184,4 +191,20 @@ impl App { } } } + + #[instrument(skip(self, term))] + async fn handle_resize(&mut self, term: &mut Terminal>) { + self.state.terminal.needs_resize = true; + term.draw(|f| { + ui::draw(f, &mut self.state); + }) + .unwrap(); + } +} +fn install_panic_hook() { + std::panic::set_hook(Box::new(|info| { + let _ = execute!(stdout(), LeaveAlternateScreen, Show); + let _ = disable_raw_mode(); + eprintln!("{}", info); + })); } diff --git a/cli/src/input.rs b/cli/src/input.rs index 914ca2d..f3deb0e 100644 --- a/cli/src/input.rs +++ b/cli/src/input.rs @@ -13,8 +13,8 @@ pub enum Input { Resize, } -pub fn start_input_listener(tx: mpsc::Sender) { - tokio::spawn({ +pub fn start_input_listeners(tx: mpsc::Sender) -> Vec { + let task1: CliTask = tokio::spawn({ let tx = tx.clone(); async move { let mut stdin = tokio::io::stdin(); @@ -34,13 +34,17 @@ pub fn start_input_listener(tx: mpsc::Sender) { } } } + Ok(()) } }); - tokio::spawn(async move { + let task2: CliTask = tokio::spawn(async move { let mut sigwinch = signal(SignalKind::window_change()).unwrap(); while sigwinch.recv().await.is_some() { tx.send(Input::Resize).await.unwrap(); } + Ok(()) }); + + vec![task1, task2] } diff --git a/cli/src/main.rs b/cli/src/main.rs index a4f9dfd..2a49f31 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,11 +6,14 @@ mod input; mod input_parser; mod prelude; mod states; +mod ui; mod utils; mod widgets; +use std::fs::File; + use clap::Parser; -use ratatui::crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use ratatui::crossterm::terminal::disable_raw_mode; use remux_core::{ comm, daemon_utils::get_sock_path, @@ -20,6 +23,7 @@ use remux_core::{ }, }; use tokio::net::UnixStream; +use tracing::field::debug; use crate::{ app::App, @@ -51,14 +55,12 @@ async fn main() { } fn setup_logging() -> Result { - use tracing_appender::{non_blocking, rolling}; + use tracing_appender::non_blocking; use tracing_subscriber::{EnvFilter, fmt}; - let file_appender = rolling::daily("logs", "remux-cli.log"); - let (non_blocking, guard) = non_blocking(file_appender); - + let file = File::create("./logs/remux-cli.log").unwrap(); + let (non_blocking, guard) = non_blocking(file); let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")); - let subscriber = fmt().with_writer(non_blocking).with_env_filter(env_filter).finish(); tracing::subscriber::set_global_default(subscriber)?; Ok(guard) @@ -104,23 +106,9 @@ async fn attach(mut stream: UnixStream, attach_request: CliRequestMessage { - // error!("Error joining client task: {e}"); - // } - // Err(e) => { - // error!("Error joining client task: {e}"); - // } - // _ => {} - // } - // } - // disable_raw_mode()?; - // debug!("Disabled raw mode"); - // Ok(()) + app.run().await?; + debug!("App terminated"); + Ok(()) } diff --git a/cli/src/ui/mod.rs b/cli/src/ui/mod.rs new file mode 100644 index 0000000..c1a9c8d --- /dev/null +++ b/cli/src/ui/mod.rs @@ -0,0 +1,3 @@ +mod ui; + +pub use ui::draw; diff --git a/cli/src/actors/ui2.rs b/cli/src/ui/ui.rs similarity index 100% rename from cli/src/actors/ui2.rs rename to cli/src/ui/ui.rs diff --git a/daemon/src/actors/client_connection.rs b/daemon/src/actors/client_connection.rs index 19d724c..9640866 100644 --- a/daemon/src/actors/client_connection.rs +++ b/daemon/src/actors/client_connection.rs @@ -145,11 +145,11 @@ impl ClientConnection { self.session_manager_handle.user_input(self.id, bytes).await.unwrap(); }, CliEvent::TerminalResize{rows, cols} => { - trace!("Client Event Input: terminal resize(rows={rows}, cols={cols})"); + debug!("Client Event Input: terminal resize(rows={rows}, cols={cols})"); // todo!() }, CliEvent::Detach => { - trace!("Client Event Input: detach"); + debug!("Client Event Input: detach"); self.session_manager_handle.client_disconnect(self.id).await.unwrap(); }, CliEvent::KillPane => { From 1ee8d2a831cefd54b3d2e6fce4d0c667383a210a Mon Sep 17 00:00:00 2001 From: Prometheus1400 Date: Sat, 6 Dec 2025 20:31:29 -0800 Subject: [PATCH 5/5] refactor --- cli/Cargo.toml | 2 +- cli/src/actors/lua.rs | 183 --------------------- cli/src/actors/mod.rs | 1 - cli/src/app.rs | 202 ++++++++++++++++------- cli/src/main.rs | 9 +- cli/src/states/status_line_state.rs | 41 ++--- cli/src/{ => tasks}/input.rs | 0 cli/src/tasks/lua.rs | 92 +++++++++++ cli/src/tasks/mod.rs | 2 + cli/src/ui/basic_selector_widget.rs | 88 ++++++++++ cli/src/ui/fuzzy_selector_widget.rs | 157 ++++++++++++++++++ cli/src/ui/mod.rs | 4 + cli/src/ui/status_line_widget.rs | 48 ++++++ cli/src/ui/traits.rs | 11 ++ cli/src/ui/ui.rs | 32 ++-- cli/src/utils.rs | 17 -- cli/src/widgets/basic_selector.rs | 156 ------------------ cli/src/widgets/fuzzy_selector.rs | 243 ---------------------------- cli/src/widgets/mod.rs | 9 -- cli/src/widgets/status_line.rs | 1 - cli/src/widgets/terminal.rs | 226 -------------------------- cli/src/widgets/traits.rs | 25 --- defaults/statusbar.lua | 2 +- 23 files changed, 585 insertions(+), 966 deletions(-) delete mode 100644 cli/src/actors/lua.rs delete mode 100644 cli/src/actors/mod.rs rename cli/src/{ => tasks}/input.rs (100%) create mode 100644 cli/src/tasks/lua.rs create mode 100644 cli/src/tasks/mod.rs create mode 100644 cli/src/ui/basic_selector_widget.rs create mode 100644 cli/src/ui/fuzzy_selector_widget.rs create mode 100644 cli/src/ui/status_line_widget.rs create mode 100644 cli/src/ui/traits.rs delete mode 100644 cli/src/utils.rs delete mode 100644 cli/src/widgets/basic_selector.rs delete mode 100644 cli/src/widgets/fuzzy_selector.rs delete mode 100644 cli/src/widgets/mod.rs delete mode 100644 cli/src/widgets/status_line.rs delete mode 100644 cli/src/widgets/terminal.rs delete mode 100644 cli/src/widgets/traits.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 89cfe4d..e6b083e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,7 +20,7 @@ tracing.workspace = true vt100.workspace = true clap = { version = "4.5.51", features = ["derive"] } -mlua = { version = "0.11.4", features = ["lua54", "serde", "async", "vendored"] } +mlua = { version = "0.11.4", features = ["lua54", "serde", "async", "vendored", "send"] } ratatui = { version = "0.29", features = ["crossterm"] } terminput = "0.5.11" tui-term = "0.2" diff --git a/cli/src/actors/lua.rs b/cli/src/actors/lua.rs deleted file mode 100644 index 5d30f68..0000000 --- a/cli/src/actors/lua.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::{ - fs, - sync::{ - Arc, RwLock, - mpsc::{self, RecvTimeoutError}, - }, - time::Duration, -}; - -use mlua::Lua as MLua; -use remux_core::states::DaemonState; -use tokio::runtime::Handle; - -use crate::{actors::ui::UIHandle, prelude::*, states::status_line_state::StatusLineState}; - -pub enum LuaEvent { - Kill, - SyncDaemonState(DaemonState), -} -use LuaEvent::*; - -#[derive(Debug, Clone)] -pub struct LuaHandle { - tx: mpsc::Sender, -} -impl LuaHandle { - pub fn kill(&mut self) -> Result<()> { - Ok(self.tx.send(LuaEvent::Kill)?) - } - pub fn sync_daemon_state(&mut self, daemon_state: DaemonState) -> Result<()> { - Ok(self.tx.send(LuaEvent::SyncDaemonState(daemon_state))?) - } -} - -#[derive(Debug)] -pub struct Lua { - pub _handle: LuaHandle, - pub rx: mpsc::Receiver, - pub lua: MLua, - pub ui_handle: UIHandle, - pub daemon_state: Arc>, -} - -impl Lua { - fn new(ui_handle: UIHandle, handle: LuaHandle, rx: mpsc::Receiver) -> Self { - Self { - _handle: handle, - rx, - lua: MLua::new(), - ui_handle, - daemon_state: Arc::new(RwLock::new(DaemonState::default())), - } - } - - fn initialize_lua_state(&mut self) -> Result<()> { - // ui configurations - let sections_table = self.lua.create_table()?; - let section_a = self.lua.create_table()?; - let section_b = self.lua.create_table()?; - let section_c = self.lua.create_table()?; - sections_table.set("a", section_a)?; - sections_table.set("b", section_b)?; - sections_table.set("c", section_c)?; - let status_line_table = self.lua.create_table()?; - status_line_table.set("sections", sections_table)?; - status_line_table.set("enabled", true)?; - // 6. Create the parent 'ui' table (if it doesn't exist) - let ui_table = self.lua.create_table()?; - ui_table.set("status_line", status_line_table)?; - - let daemon_state_clone = self.daemon_state.clone(); - let get_active_session = self.lua.create_function(move |lua, ()| { - if let Ok(guard) = daemon_state_clone.read() { - if let Some(session_id) = guard.active_session { - Ok(mlua::Value::String(lua.create_string(session_id.to_string())?)) - } else { - Ok(mlua::Value::Nil) - } - } else { - Ok(mlua::Value::Nil) - } - })?; - - self.lua.globals().set("ui", ui_table)?; - self.lua.globals().set("get_active_session", get_active_session)?; - - Ok(()) - } - - fn resolve_status_line_state(&mut self) -> Result { - let ui_table: mlua::Table = self.lua.globals().get("ui")?; - let status_line_config: mlua::Table = ui_table.get("status_line")?; - let sections_config: mlua::Table = status_line_config.get("sections")?; - let enabled: mlua::Value = status_line_config.get("enabled")?; - - if let mlua::Value::Boolean(false) = enabled { - return Ok(StatusLineState::disabled()); - } - - let mut status_line_state = StatusLineState::default(); - for pair in sections_config.pairs::() { - let (key, val) = pair?; - for pair in val.pairs::() { - let (_, val) = pair?; - let resolved_string = match val { - mlua::Value::String(s) => { - // Item is a direct string - s.to_str()?.to_owned() - } - mlua::Value::Function(func) => { - // Item is a Lua function, execute it and get the string result - func.call::(())? - } - mlua::Value::Nil => { - // Ignore nil values which can happen in sparse tables - continue; - } - _ => { - warn!("Ignoring lua type"); - continue; - } - }; - - match key.as_str() { - "a" => { - status_line_state.a.push(resolved_string); - } - "b" => { - status_line_state.b.push(resolved_string); - } - "c" => { - status_line_state.c.push(resolved_string); - } - _ => {} - } - } - } - - Ok(status_line_state) - } - - pub fn spawn(ui_handle: UIHandle) -> Result { - let (tx, rx) = mpsc::channel(); - let handle = LuaHandle { tx }; - let handle_clone = handle.clone(); - tokio::task::spawn_blocking(|| { - let mut actor = Self::new(ui_handle, handle, rx); - actor.initialize_lua_state().unwrap(); - let code = fs::read_to_string("defaults/statusbar.lua").unwrap(); - let runtime = Handle::current(); - loop { - match actor.rx.recv_timeout(Duration::from_millis(500)) { - Ok(event) => match event { - Kill => { - debug!("killed"); - break; - } - SyncDaemonState(daemon_state) => { - if let Ok(mut guard) = actor.daemon_state.write() { - *guard = daemon_state; - } - } - }, - Err(RecvTimeoutError::Disconnected) => { - warn!("disconnected"); - break; - } - Err(RecvTimeoutError::Timeout) => {} - } - - actor.lua.load(&code).exec().unwrap(); - let status_line_state = actor.resolve_status_line_state().unwrap(); - let ui_handle_clone = actor.ui_handle.clone(); - let status_line_state_clone = status_line_state.clone(); - runtime - .block_on(async move { ui_handle_clone.sync_status_line_state(status_line_state_clone).await })?; - } - Ok::<(), Error>(()) - }); - - Ok(handle_clone) - } -} diff --git a/cli/src/actors/mod.rs b/cli/src/actors/mod.rs deleted file mode 100644 index 8b13789..0000000 --- a/cli/src/actors/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/cli/src/app.rs b/cli/src/app.rs index ca245c1..4bf5b1f 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -1,45 +1,35 @@ -use std::{ - fmt::Debug, - io::{Stdout, stdout}, - time::Duration, -}; +use std::{fmt::Debug, io::Stdout, time::Duration}; use bytes::Bytes; -use crossterm::{ - cursor::Show, - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode}, -}; use derivative::Derivative; -use ratatui::{Terminal, crossterm::terminal::disable_raw_mode, prelude::CrosstermBackend, restore}; +use ratatui::{Terminal, prelude::CrosstermBackend, restore, widgets::ListState}; use remux_core::{ comm, events::{CliEvent, DaemonEvent}, states::DaemonState, }; +use terminput::Event; use tokio::{ net::UnixStream, - sync::{mpsc, watch}, + sync::{broadcast, mpsc}, time::interval, }; use vt100::Parser; use crate::{ - input::{self, Input}, input_parser::{self, InputParser}, prelude::*, - ui, + states::status_line_state::StatusLineState, + tasks::{ + input::{self, Input}, + lua, + }, + ui::{ + self, basic_selector_widget::BasicSelectorWidget, fuzzy_selector_widget::FuzzySelectorWidget, + traits::SelectorStatefulWidget, + }, }; -#[derive(Debug)] -pub struct StatusLineState {} - -#[derive(Debug)] -pub enum UiState { - Normal, - Selecting, -} - #[derive(Derivative)] #[derivative(Debug)] pub struct TerminalState { @@ -49,11 +39,53 @@ pub struct TerminalState { pub needs_resize: bool, } +#[derive(Debug)] +pub struct UiState { + pub selector: SelectorState, + pub status_line: StatusLineState, +} + +#[derive(Debug)] +pub enum SelectorType { + Basic, + Fuzzy, +} + +#[derive(Debug, Clone)] +pub struct IndexedItem { + pub index: usize, + pub item: String, +} + +impl IndexedItem { + pub fn new(index: usize, item: String) -> Self { + Self { index, item } + } +} + +#[derive(Debug)] +pub struct SelectorState { + pub selector_type: SelectorType, + pub list_state: ListState, + pub list: Vec, + pub query: String, + // selector might filter out some items - so we need to + // maintain it's original index to be able to return it + pub displaying_list: Vec, +} + +#[derive(Debug)] +pub enum AppMode { + Normal, + SelectingSession, +} + #[derive(Debug)] pub struct AppState { pub terminal: TerminalState, pub daemon: DaemonState, pub ui: UiState, + pub mode: AppMode, } pub struct App { @@ -69,13 +101,23 @@ impl App { stream, input_parser: InputParser::default(), state: AppState { + mode: AppMode::Normal, terminal: TerminalState { emulator: Parser::default(), size: (0, 0), needs_resize: true, }, daemon: daemon_state, - ui: UiState::Normal, + ui: UiState { + selector: SelectorState { + list_state: ListState::default(), + list: Vec::new(), + selector_type: SelectorType::Basic, + query: String::new(), + displaying_list: Vec::new(), + }, + status_line: StatusLineState::default(), + }, }, bg_tasks: Vec::new(), } @@ -83,15 +125,15 @@ impl App { #[instrument(skip(self))] pub async fn run(&mut self) -> Result<()> { + let mut term = ratatui::init(); debug!("starting app"); - let (tx, mut rx) = mpsc::channel::(100); - self.bg_tasks.extend(input::start_input_listeners(tx)); + let (input_tx, mut input_rx) = mpsc::channel::(100); + let (lua_tx, mut lua_rx) = broadcast::channel(100); + self.bg_tasks.extend(input::start_input_listeners(input_tx)); + self.bg_tasks.push(lua::start_status_line_task(lua_tx)); let mut ticker = interval(Duration::from_millis(50)); - let mut term = ratatui::Terminal::new(CrosstermBackend::new(stdout())).unwrap(); - install_panic_hook(); - enable_raw_mode()?; debug!("Enabled raw mode"); - execute!(stdout(), EnterAlternateScreen)?; + // execute!(stdout(), EnterAlternateScreen)?; debug!("Entered alternate screen"); // need an initial render since ui updates app state to convey terminal size information @@ -108,7 +150,7 @@ impl App { .unwrap(); } tokio::select! { - Some(input) = rx.recv() => { + Some(input) = input_rx.recv() => { use Input::{Stdin, Resize}; match input { Stdin(bytes) => { @@ -121,6 +163,11 @@ impl App { } } } + Ok(mut status_line_state) = lua_rx.recv() => { + debug!("received status line state"); + status_line_state.apply_built_ins(&self.state); + self.state.ui.status_line = status_line_state; + } res = comm::recv_daemon_event(&mut self.stream) => { match res { Ok(event) => { @@ -133,24 +180,17 @@ impl App { debug!("DaemonEvent(Disconnected)"); break; } + DaemonEvent::ActiveSession(session_id) => { + debug!("DaemonEvent(ActiveSession({session_id}))"); + self.state.daemon.set_active_session(session_id); + } + DaemonEvent::NewSession(session_id) => { + debug!("DaemonEvent(NewSession({session_id}))"); + self.state.daemon.add_session(session_id); + } _ => { - // todo!(); + todo!(); } - // DaemonEvent::CurrentSessions(session_ids) => { - // debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); - // self.daemon_state.set_sessions(session_ids); - // self.sync_daemon_state = true; - // } - // DaemonEvent::ActiveSession(session_id) => { - // debug!("DaemonEvent(ActiveSession({session_id}))"); - // self.daemon_state.set_active_session(session_id); - // self.sync_daemon_state = true; - // } - // DaemonEvent::NewSession(session_id) => { - // debug!("DaemonEvent(NewSession({session_id}))"); - // self.daemon_state.add_session(session_id); - // self.sync_daemon_state = true; - // } // DaemonEvent::DeletedSession(_session_id) => { // todo!("implement delete session"); // } @@ -179,10 +219,42 @@ impl App { #[instrument(skip(self, bytes))] async fn dispatch_stdin(&mut self, bytes: Bytes) { + match self.state.mode { + AppMode::Normal => self.handle_stdin_for_normal_mode(bytes).await, + AppMode::SelectingSession => self.handle_stdin_for_selecting_mode(bytes).await, + } + } + + async fn handle_stdin_for_selecting_mode(&mut self, bytes: Bytes) { + let event = Event::parse_from(&bytes).unwrap().unwrap(); + let selection_opt = match self.state.ui.selector.selector_type { + SelectorType::Basic => BasicSelectorWidget::input(event, &mut self.state.ui.selector), + SelectorType::Fuzzy => FuzzySelectorWidget::input(event, &mut self.state.ui.selector), + }; + if let Some(selection) = selection_opt { + match selection { + ui::traits::Selection::Index(i) => match self.state.mode { + AppMode::SelectingSession => { + let session = self.state.ui.selector.list[i].parse::().unwrap(); + comm::send_event(&mut self.stream, CliEvent::SwitchSession(session)) + .await + .unwrap(); + } + AppMode::Normal => {} + }, + ui::traits::Selection::Cancelled => {} + } + self.state.mode = AppMode::Normal; + self.state.ui.selector.list_state.select(Some(0)); + self.state.ui.selector.list.clear(); + } + } + + async fn handle_stdin_for_normal_mode(&mut self, bytes: Bytes) { for parsed_event in self.input_parser.process(&bytes) { match parsed_event { - input_parser::ParsedEvent::LocalAction(_action) => { - todo!("update the application state") + input_parser::ParsedEvent::LocalAction(action) => { + self.dispatch_action(action).await; } input_parser::ParsedEvent::DaemonAction(cli_event) => { debug!("sending cli event: {cli_event:?}"); @@ -192,6 +264,31 @@ impl App { } } + async fn dispatch_action(&mut self, action: input_parser::Action) { + match action { + input_parser::Action::SwitchSession => { + self.state.mode = AppMode::SelectingSession; + self.state.ui.selector.query.clear(); + self.state.ui.selector.list_state.select(Some(0)); + self.state.ui.selector.selector_type = SelectorType::Fuzzy; + self.state + .ui + .selector + .list + .extend(self.state.daemon.session_ids.iter().map(|x| x.to_string())); + self.state.ui.selector.displaying_list = self + .state + .ui + .selector + .list + .iter() + .enumerate() + .map(|(i, x)| IndexedItem::new(i, x.clone())) + .collect(); + } + } + } + #[instrument(skip(self, term))] async fn handle_resize(&mut self, term: &mut Terminal>) { self.state.terminal.needs_resize = true; @@ -201,10 +298,3 @@ impl App { .unwrap(); } } -fn install_panic_hook() { - std::panic::set_hook(Box::new(|info| { - let _ = execute!(stdout(), LeaveAlternateScreen, Show); - let _ = disable_raw_mode(); - eprintln!("{}", info); - })); -} diff --git a/cli/src/main.rs b/cli/src/main.rs index 2a49f31..2d85c22 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,14 +1,11 @@ -mod actors; -pub mod app; +mod app; mod args; mod error; -mod input; mod input_parser; mod prelude; mod states; +mod tasks; mod ui; -mod utils; -mod widgets; use std::fs::File; @@ -23,7 +20,6 @@ use remux_core::{ }, }; use tokio::net::UnixStream; -use tracing::field::debug; use crate::{ app::App, @@ -110,5 +106,6 @@ async fn attach(mut stream: UnixStream, attach_request: CliRequestMessage Result<()> { + info!("Initializing lua state"); + let sections_table = lua.create_table()?; + let section_a = lua.create_table()?; + let section_b = lua.create_table()?; + let section_c = lua.create_table()?; + sections_table.set("a", section_a)?; + sections_table.set("b", section_b)?; + sections_table.set("c", section_c)?; + let status_line_table = lua.create_table()?; + status_line_table.set("sections", sections_table)?; + status_line_table.set("enabled", true)?; + let ui_table = lua.create_table()?; + ui_table.set("status_line", status_line_table)?; + lua.globals().set("ui", ui_table)?; + Ok(()) +} + +pub fn start_status_line_task(tx: broadcast::Sender) -> CliTask { + let mut lua = Lua::default(); + initialize_lua_state(&mut lua).unwrap(); + let code = fs::read_to_string("defaults/statusbar.lua").unwrap(); + + info!("Starting lua status line task"); + let task: CliTask = tokio::spawn({ + async move { + loop { + tokio::time::sleep(Duration::from_millis(500)).await; + lua.load(&code).exec().unwrap(); + let ui_table: mlua::Table = lua.globals().get("ui")?; + let status_line_config: mlua::Table = ui_table.get("status_line")?; + let sections_config: mlua::Table = status_line_config.get("sections")?; + let enabled: mlua::Value = status_line_config.get("enabled")?; + + if let mlua::Value::Boolean(false) = enabled { + let _ = tx.send(StatusLineState::disabled()); + continue; + } + + let mut status_line_state = StatusLineState::default(); + for pair in sections_config.pairs::() { + let (key, val) = pair?; + for pair in val.pairs::() { + let (_, val) = pair?; + let resolved_string = match val { + mlua::Value::String(s) => { + // Item is a direct string + s.to_str()?.to_owned() + } + mlua::Value::Function(func) => { + // Item is a Lua function, execute it and get the string result + func.call::(())? + } + mlua::Value::Nil => { + // Ignore nil values which can happen in sparse tables + continue; + } + _ => { + warn!("Ignoring lua type"); + continue; + } + }; + + match key.as_str() { + "a" => { + status_line_state.a.push(resolved_string); + } + "b" => { + status_line_state.b.push(resolved_string); + } + "c" => { + status_line_state.c.push(resolved_string); + } + _ => {} + } + } + } + let _ = tx.send(status_line_state); + } + // Ok::<(), Error>(()) + } + }); + + task +} diff --git a/cli/src/tasks/mod.rs b/cli/src/tasks/mod.rs new file mode 100644 index 0000000..c21f31f --- /dev/null +++ b/cli/src/tasks/mod.rs @@ -0,0 +1,2 @@ +pub mod input; +pub mod lua; diff --git a/cli/src/ui/basic_selector_widget.rs b/cli/src/ui/basic_selector_widget.rs new file mode 100644 index 0000000..5f9e15e --- /dev/null +++ b/cli/src/ui/basic_selector_widget.rs @@ -0,0 +1,88 @@ +use ratatui::widgets::StatefulWidget; + +use crate::{ + app::SelectorState, + prelude::*, + ui::traits::{Selection, SelectorStatefulWidget}, +}; + +#[derive(Debug, Default)] +pub struct BasicSelectorWidget {} + +impl StatefulWidget for BasicSelectorWidget { + type State = SelectorState; + + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) { + use ratatui::{ + layout::Rect, + prelude::Stylize, + style::{Modifier, Style}, + widgets::{Block, Borders, List}, + }; + let list_state = &mut state.list_state; + let list = &state.list; + + // Calculate popup size (width and height) + let width = (area.width / 2).min(50); // max width 50 + let height = (list.len() as u16 + 2).min(area.height / 2); // +2 for padding/border + let x = (area.width.saturating_sub(width)) / 2; + let y = (area.height.saturating_sub(height)) / 2; + let rect = Rect::new(x, y, width, height); + let list = List::new(list.clone()) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().bold()) + .title("Select") + .title_alignment(ratatui::layout::Alignment::Center), + ) + .highlight_symbol(">> ") + .highlight_style( + Style::default() + .fg(ratatui::style::Color::Green) + .add_modifier(Modifier::BOLD), + ); + + list.render(rect, buf, list_state); + } +} + +impl SelectorStatefulWidget for BasicSelectorWidget { + fn input(event: terminput::Event, state: &mut Self::State) -> Option { + use terminput::KeyCode::*; + + let list_state = &mut state.list_state; + let list = &state.list; + + match event { + terminput::Event::Key(key_event) => match key_event.code { + Up | Char('k') => { + let i = match list_state.selected() { + Some(i) if i > 0 => i - 1, + _ => 0, + }; + list_state.select(Some(i)); + None + } + Down | Char('j') => { + let i = match list_state.selected() { + Some(i) if i < list.len() - 1 => i + 1, + _ => list.len() - 1, + }; + list_state.select(Some(i)); + None + } + Enter => { + debug!("enter pressed"); + list_state.selected().map(Selection::Index) + } + Esc | Char('q') => { + debug!("esc or q pressed"); + Some(Selection::Cancelled) + } + _ => None, + }, + _ => None, + } + } +} diff --git a/cli/src/ui/fuzzy_selector_widget.rs b/cli/src/ui/fuzzy_selector_widget.rs new file mode 100644 index 0000000..e1831aa --- /dev/null +++ b/cli/src/ui/fuzzy_selector_widget.rs @@ -0,0 +1,157 @@ +use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; +use ratatui::widgets::{Padding, StatefulWidget}; +use terminput::KeyCode; + +use crate::{ + app::{IndexedItem, SelectorState}, + prelude::*, + ui::traits::{Selection, SelectorStatefulWidget}, +}; + +#[derive(Debug, Default)] +pub struct FuzzySelectorWidget {} + +impl StatefulWidget for FuzzySelectorWidget { + type State = SelectorState; + + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) { + use ratatui::{ + prelude::*, + style::{Modifier, Style}, + widgets::{Block, Borders, List, Paragraph}, + }; + + let mut list_state = &mut state.list_state; + let list = &state.list; + let filtered_items = state + .displaying_list + .iter() + .map(|pair| pair.item.clone()) + .collect::>(); + let query = &state.query; + + let width = area.width / 2; + let height = area.height / 2; + let x = (area.width.saturating_sub(width)) / 2; + let y = (area.height.saturating_sub(height)) / 2; + let rect = Rect::new(x, y, width, height); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)]) + .split(rect); + + let max_height = chunks[0].height; + let items_height = filtered_items.len() as u16 + 2; + let subchunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(max_height - items_height), + Constraint::Length(items_height), + ]) + .split(chunks[0]); + + let list = List::new(filtered_items) + .block(Block::default().padding(Padding::uniform(1))) + .highlight_symbol(">> ") + .highlight_style( + Style::default() + .fg(ratatui::style::Color::Green) + .add_modifier(Modifier::BOLD), + ); + + let display_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().bold()) + .title("Selecting") + .title_alignment(ratatui::layout::Alignment::Center); + + let display_query = Paragraph::new(query.clone()).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().bold()) + .padding(Padding::horizontal(1)), + ); + + display_block.render(chunks[0], buf); + StatefulWidget::render(&list, subchunks[1], buf, &mut list_state); + display_query.render(chunks[1], buf); + } +} + +impl SelectorStatefulWidget for FuzzySelectorWidget { + fn input(event: terminput::Event, state: &mut Self::State) -> Option { + use terminput::KeyCode::*; + + let list_state = &mut state.list_state; + let list = state + .list + .iter() + .enumerate() + .map(|(i, x)| IndexedItem::new(i, x.clone())) + .collect::>(); + let filtered_list = &mut state.displaying_list; + let query = &mut state.query; + let matcher = SkimMatcherV2::default(); + + match event { + terminput::Event::Key(key_event) => match key_event.code { + Enter => { + debug!("enter pressed"); + let selected_index = list_state.selected(); + // Map the current filtered index to the original index + let original_index = selected_index + .and_then(|i| filtered_list.get(i)) + .map(|item| item.index) + .unwrap(); + + Some(Selection::Index(original_index)) + } + Esc => { + debug!("esc pressed"); + Some(Selection::Cancelled) + } + Up => { + let current_index = list_state.selected().unwrap_or(0); + let i = current_index.saturating_sub(1); + list_state.select(Some(i)); + None + } + Down => { + let max_index = filtered_list.len().saturating_sub(1); + let current_index = list_state.selected().unwrap_or(0); + let i = (current_index + 1).min(max_index); + list_state.select(Some(i)); + None + } + Backspace => { + query.pop(); + *filtered_list = filter_items(&matcher, query, &list); + let opt = Some(filtered_list.len().saturating_sub(1)); + list_state.select(opt); + None + } + Char(c) => { + query.push(c); + *filtered_list = filter_items(&matcher, query, &list); + let opt = Some(filtered_list.len().saturating_sub(1)); + list_state.select(opt); + None + } + _ => None, + }, + _ => None, + } + } +} + +fn filter_items(matcher: &SkimMatcherV2, query: &str, items: &[IndexedItem]) -> Vec { + let mut filtered_items = items + .iter() + .filter_map(|x| matcher.fuzzy_match(&x.item, query).map(|score| (score, x.clone()))) + .collect::>(); + + filtered_items.sort_by(|a, b| b.0.cmp(&a.0)); + filtered_items.reverse(); + filtered_items.into_iter().map(|x| x.1).collect::>() +} diff --git a/cli/src/ui/mod.rs b/cli/src/ui/mod.rs index c1a9c8d..d0e7d96 100644 --- a/cli/src/ui/mod.rs +++ b/cli/src/ui/mod.rs @@ -1,3 +1,7 @@ +pub mod basic_selector_widget; +pub mod fuzzy_selector_widget; +mod status_line_widget; +pub mod traits; mod ui; pub use ui::draw; diff --git a/cli/src/ui/status_line_widget.rs b/cli/src/ui/status_line_widget.rs new file mode 100644 index 0000000..02bc282 --- /dev/null +++ b/cli/src/ui/status_line_widget.rs @@ -0,0 +1,48 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout}, + widgets::{Paragraph, Widget}, +}; +use tracing::info; + +use crate::states::status_line_state::StatusLineState; + +pub struct StatusLineWidget { + state: StatusLineState, +} + +impl StatusLineWidget { + pub fn new(state: StatusLineState) -> Self { + Self { state } + } +} + +impl Widget for StatusLineWidget { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { + if !self.state.enabled || area.height < 1 { + return; + } + + info!("rendering status line widget"); + + // 1. Create 3 horizontal constraints for Left, Center, Right sections + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(area.width / 3), // Fixed or Length for A + Constraint::Min(0), // Remaining space for B (Center) + Constraint::Length(area.width / 3), // Fixed or Length for C + ]) + .split(area); + + let separator = " | "; + let text_a = self.state.a.join(separator); + let text_b = self.state.b.join(separator); + let text_c = self.state.c.join(separator); + let paragraph_a = Paragraph::new(text_a); + paragraph_a.render(chunks[0], buf); + let paragraph_b = Paragraph::new(text_b).alignment(ratatui::layout::Alignment::Center); + paragraph_b.render(chunks[1], buf); + let paragraph_c = Paragraph::new(text_c).alignment(ratatui::layout::Alignment::Right); + paragraph_c.render(chunks[2], buf); + } +} diff --git a/cli/src/ui/traits.rs b/cli/src/ui/traits.rs new file mode 100644 index 0000000..171794f --- /dev/null +++ b/cli/src/ui/traits.rs @@ -0,0 +1,11 @@ +use ratatui::widgets::StatefulWidget; +use terminput::Event; + +pub enum Selection { + Index(usize), + Cancelled, +} + +pub trait SelectorStatefulWidget: StatefulWidget { + fn input(event: Event, state: &mut Self::State) -> Option; +} diff --git a/cli/src/ui/ui.rs b/cli/src/ui/ui.rs index e90a589..fc39a49 100644 --- a/cli/src/ui/ui.rs +++ b/cli/src/ui/ui.rs @@ -4,7 +4,14 @@ use ratatui::{ }; use tui_term::widget::PseudoTerminal; -use crate::{app::AppState, prelude::*}; +use crate::{ + app::{AppMode, AppState}, + prelude::*, + ui::{ + basic_selector_widget::BasicSelectorWidget, fuzzy_selector_widget::FuzzySelectorWidget, + status_line_widget::StatusLineWidget, + }, +}; #[instrument(skip(f))] pub fn draw(f: &mut Frame, state: &mut AppState) { @@ -24,14 +31,19 @@ pub fn draw(f: &mut Frame, state: &mut AppState) { f.render_widget(term_ui, term_area); // render the status bar - // let status_line = StatusLine::new(app.status_line); - // f.render_widget(); + let status_line = StatusLineWidget::new(state.ui.status_line.clone()); + f.render_widget(status_line, chunks[1]); - // render selector if active - // if self.ui_state == UIState::SelectingBasic { - // BasicSelector::render(&self.basic_selector, f); - // } - // if self.ui_state == UIState::SelectingFuzzy { - // FuzzySelector::render(&self.fuzzy_selector, f); - // } + if let AppMode::SelectingSession = state.mode { + match state.ui.selector.selector_type { + crate::app::SelectorType::Basic => { + let popup = BasicSelectorWidget::default(); + f.render_stateful_widget(popup, f.area(), &mut state.ui.selector); + } + crate::app::SelectorType::Fuzzy => { + let popup = FuzzySelectorWidget::default(); + f.render_stateful_widget(popup, f.area(), &mut state.ui.selector); + } + } + } } diff --git a/cli/src/utils.rs b/cli/src/utils.rs deleted file mode 100644 index 9cfa004..0000000 --- a/cli/src/utils.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::sync::Arc; - -pub struct DisplayableVec { - inner: Arc Vec + Send + Sync>, -} - -impl DisplayableVec { - pub fn new(vec: Vec) -> Self { - Self { - inner: Arc::new(move || vec.iter().map(|x| x.to_string()).collect()), - } - } - - pub fn to_strings(&self) -> Vec { - (self.inner)() - } -} diff --git a/cli/src/widgets/basic_selector.rs b/cli/src/widgets/basic_selector.rs deleted file mode 100644 index 39e563a..0000000 --- a/cli/src/widgets/basic_selector.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use bytes::Bytes; -use ratatui::{Frame, widgets::ListState}; -use terminput::Event; -use tokio::sync::{broadcast, mpsc}; - -use crate::{prelude::*, utils::DisplayableVec, widgets::traits::Selector}; - -#[derive(Debug, Clone)] -pub struct BasicSelector { - pub select_state: ListState, - pub title: Option, - pub items: Vec, - pub tx: mpsc::Sender>, - pub is_running: bool, -} -impl BasicSelector { - pub fn new(tx: mpsc::Sender>) -> Arc> { - Arc::new(RwLock::new(Self { - select_state: ListState::default().with_selected(Some(0)), - title: None, - items: Vec::new(), - tx, - is_running: false, - })) - } -} -impl Selector for BasicSelector { - fn run>( - selector: &Arc>, - mut rx: broadcast::Receiver, - items: DisplayableVec, - title: T, - ) -> Result<()> { - { - let mut guard = selector.write().unwrap(); - if guard.is_running { - return Err(Error::Custom("duplicate task".to_owned())); - } - guard.items = items.to_strings(); - guard.title = Some(title.into()); - } - tokio::spawn({ - let selector = Arc::clone(selector); - { - selector.write().unwrap().is_running = true; - } - async move { - loop { - debug!("Kaleb"); - let key_event = { - if let Ok(bytes) = rx.recv().await { - debug!("bytes gotten in basic selector"); - match Event::parse_from(&bytes) { - Ok(None) => { - warn!("Couldn't fully parse bytes to terminal event"); - None - } - Err(e) => { - error!("Couldn't parse bytes to terminal event: {e}"); - None - } - Ok(Some(Event::Key(key_event))) => Some(key_event), - _ => None, - } - } else { - None - } - }; - let tx = { selector.read().unwrap().tx.clone() }; - let selection = { - if let Some(key_event) = key_event { - debug!("key pressed"); - use terminput::KeyCode::*; - let mut guard = selector.write().unwrap(); - match key_event.code { - Up | Char('k') => { - let i = match guard.select_state.selected() { - Some(i) if i > 0 => i - 1, - _ => 0, - }; - guard.select_state.select(Some(i)); - None - } - Down | Char('j') => { - let i = match guard.select_state.selected() { - Some(i) if i < guard.items.len() - 1 => i + 1, - _ => guard.items.len() - 1, - }; - guard.select_state.select(Some(i)); - None - } - Enter => { - debug!("enter pressed"); - Some(guard.select_state.selected()) - } - Esc | Char('q') => { - debug!("esc or q pressed"); - Some(None) - } - _ => None, - } - } else { - None - } - }; - - if let Some(selection) = selection { - tx.send(selection).await.unwrap(); - break; - } - } - { - selector.write().unwrap().is_running = false; - } - Ok::<(), Error>(()) - } - }); - Ok(()) - } - - fn render(selector: &Arc>, f: &mut Frame) { - use ratatui::{ - layout::Rect, - prelude::*, - style::{Modifier, Style}, - widgets::{Block, Borders, List}, - }; - let mut guard = selector.write().unwrap(); - let size = f.area(); - - // Calculate popup size (width and height) - let width = (size.width / 2).min(50); // max width 50 - let height = (guard.items.len() as u16 + 2).min(size.height / 2); // +2 for padding/border - let x = (size.width.saturating_sub(width)) / 2; - let y = (size.height.saturating_sub(height)) / 2; - let rect = Rect::new(x, y, width, height); - let list = List::new(guard.items.clone()) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().bold()) - .title(guard.title.as_ref().unwrap().clone()) - .title_alignment(ratatui::layout::Alignment::Center), - ) - .highlight_symbol(">> ") - .highlight_style( - Style::default() - .fg(ratatui::style::Color::Green) - .add_modifier(Modifier::BOLD), - ); - - f.render_stateful_widget(list, rect, &mut guard.select_state); - } -} diff --git a/cli/src/widgets/fuzzy_selector.rs b/cli/src/widgets/fuzzy_selector.rs deleted file mode 100644 index ee00c10..0000000 --- a/cli/src/widgets/fuzzy_selector.rs +++ /dev/null @@ -1,243 +0,0 @@ -use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; - -use bytes::Bytes; -use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; -use ratatui::{ - Frame, - layout::Rect, - widgets::{ListState, Padding}, -}; -use terminput::{Event, KeyCode}; -use tokio::sync::{broadcast, mpsc}; - -use crate::{prelude::*, utils::DisplayableVec, widgets::traits::Selector}; - -#[derive(Debug, Clone)] -pub struct IndexedItem { - pub index: usize, - pub item: String, -} -impl IndexedItem { - pub fn new(index: usize, item: String) -> Self { - Self { index, item } - } -} - -pub struct FuzzySelector { - pub select_state: ListState, - pub title: Option, - pub items: Vec, - pub filtered_items: Vec, - pub tx: mpsc::Sender>, - pub is_running: bool, - pub query: String, -} - -impl FuzzySelector { - pub fn new(tx: mpsc::Sender>) -> Arc> { - Arc::new(RwLock::new(Self { - select_state: ListState::default().with_selected(Some(0)), - title: None, - items: Vec::new(), - filtered_items: Vec::new(), - tx, - is_running: false, - query: "".to_owned(), - })) - } -} - -impl Selector for FuzzySelector { - fn run>( - selector: &Arc>, - mut rx: broadcast::Receiver, - items: DisplayableVec, - title: T, - ) -> Result<()> { - let matcher = SkimMatcherV2::default(); - { - let mut guard = selector.write().unwrap(); - if guard.is_running { - return Err(Error::Custom("duplicate task".to_owned())); - } - guard.is_running = true; - guard.items = items - .to_strings() - .into_iter() - .enumerate() - .map(|pair| IndexedItem::new(pair.0, pair.1)) - .collect(); - guard.filtered_items = filter_items(&matcher, &guard.query, &guard.items); - let opt = Some(guard.filtered_items.len().saturating_sub(1)); - guard.select_state.select(opt); // Select first item if exists - guard.title = Some(title.into()); - guard.query = String::new(); - } - - let selector_clone = Arc::clone(selector); - tokio::spawn(async move { - loop { - let final_selection: Option> = { - if let Ok(bytes) = rx.recv().await { - match Event::parse_from(&bytes) { - Ok(Some(Event::Key(key_code))) => { - let mut guard = selector_clone.write().unwrap(); - let mut result: Option> = None; - - match key_code.code { - KeyCode::Enter => { - debug!("enter pressed"); - let selected_index = guard.select_state.selected(); - // Map the current filtered index to the original index - let original_index = selected_index - .and_then(|i| guard.filtered_items.get(i)) - .map(|item| item.index); - - result = Some(original_index); - } - KeyCode::Esc => { - debug!("esc pressed"); - result = Some(None); - } - KeyCode::Up => { - let current_index = guard.select_state.selected().unwrap_or(0); - let i = current_index.saturating_sub(1); - guard.select_state.select(Some(i)); - } - KeyCode::Down => { - let max_index = guard.filtered_items.len().saturating_sub(1); - let current_index = guard.select_state.selected().unwrap_or(0); - let i = (current_index + 1).min(max_index); - guard.select_state.select(Some(i)); - } - KeyCode::Backspace => { - guard.query.pop(); - guard.filtered_items = filter_items(&matcher, &guard.query, &guard.items); - let opt = Some(guard.filtered_items.len().saturating_sub(1)); - guard.select_state.select(opt); - } - KeyCode::Char(c) => { - guard.query.push(c); - guard.filtered_items = filter_items(&matcher, &guard.query, &guard.items); - let opt = Some(guard.filtered_items.len().saturating_sub(1)); - guard.select_state.select(opt); - } - _ => {} - } - - result - } - Ok(None) => { - warn!("Couldn't fully parse bytes to terminal event"); - None - } - Err(e) => { - error!("Couldn_t parse bytes to terminal event: {e}"); - None - } - _ => None, - } - } else { - break; - } - }; - - // If a final selection was determined (Enter/Esc), send the result and break. - if let Some(selection) = final_selection { - let tx_clone = selector_clone.read().unwrap().tx.clone(); - tx_clone.send(selection).await.unwrap(); - break; - } - } - - { - selector_clone.write().unwrap().is_running = false; - } - Ok::<(), Error>(()) - }); - Ok(()) - } - - fn render(selector: &Arc>, f: &mut Frame) { - use ratatui::{ - prelude::*, - style::{Modifier, Style}, - widgets::{Block, Borders, List, Paragraph}, - }; - let (title, query, items, mut select_state) = { - let guard = selector.read().unwrap(); - ( - guard.title.as_ref().unwrap_or(&"Select Item".to_owned()).clone(), - guard.query.clone(), - guard - .filtered_items - .iter() - .map(|pair| pair.item.clone()) - .collect::>(), - guard.select_state.clone(), - ) - }; - let rect = get_rect(f); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(3)]) - .split(rect); - - let max_height = chunks[0].height; - let items_height = items.len() as u16 + 2; - let subchunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(max_height - items_height), - Constraint::Length(items_height), - ]) - .split(chunks[0]); - - let list = List::new(items) - .block(Block::default().padding(Padding::uniform(1))) - .highlight_symbol(">> ") - .highlight_style( - Style::default() - .fg(ratatui::style::Color::Green) - .add_modifier(Modifier::BOLD), - ); - - let display_block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().bold()) - .title(title) - .title_alignment(ratatui::layout::Alignment::Center); - - let display_query = Paragraph::new(query).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().bold()) - .padding(Padding::horizontal(1)), - ); - - f.render_widget(display_block, chunks[0]); - f.render_stateful_widget(list, subchunks[1], &mut select_state); - f.render_widget(display_query, chunks[1]); - } -} - -fn filter_items(matcher: &SkimMatcherV2, query: &str, items: &[IndexedItem]) -> Vec { - let mut filtered_items = items - .iter() - .filter_map(|x| matcher.fuzzy_match(&x.item, query).map(|score| (score, x.clone()))) - .collect::>(); - - filtered_items.sort_by(|a, b| b.0.cmp(&a.0)); - filtered_items.reverse(); - filtered_items.into_iter().map(|x| x.1).collect::>() -} - -fn get_rect(f: &Frame) -> Rect { - let size = f.area(); - let width = size.width / 2; - let height = size.height / 2; - let x = (size.width.saturating_sub(width)) / 2; - let y = (size.height.saturating_sub(height)) / 2; - Rect::new(x, y, width, height) -} diff --git a/cli/src/widgets/mod.rs b/cli/src/widgets/mod.rs deleted file mode 100644 index 45c0957..0000000 --- a/cli/src/widgets/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod basic_selector; -mod fuzzy_selector; -mod status_line; -mod terminal; -mod traits; - -pub use basic_selector::BasicSelector; -pub use fuzzy_selector::FuzzySelector; -pub use traits::Selector; diff --git a/cli/src/widgets/status_line.rs b/cli/src/widgets/status_line.rs deleted file mode 100644 index 8b13789..0000000 --- a/cli/src/widgets/status_line.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/cli/src/widgets/terminal.rs b/cli/src/widgets/terminal.rs deleted file mode 100644 index 586fc18..0000000 --- a/cli/src/widgets/terminal.rs +++ /dev/null @@ -1,226 +0,0 @@ -// // use bytes::Bytes; -// // use ratatui::layout::Rect; -// // use tui_term::widget::PseudoTerminal; -// // use vt100::Parser; -// // -// // use crate::widgets::traits::Renderable; -// // -// // #[derive(Default)] -// // pub struct TerminalWidget { -// // emulator: Parser, -// // } -// // -// // impl TerminalWidget { -// // pub fn write(&mut self, bytes: Bytes) { -// // self.emulator.process(&bytes); -// // } -// // -// // pub fn size(&self) -> (u16, u16) { -// // self.emulator.screen().size() -// // } -// // -// // pub fn set_size(&mut self, rows: u16, cols: u16) { -// // self.emulator.set_size(rows, cols); -// // } -// // -// // } -// // -// // impl Renderable for TerminalWidget { -// // fn render(&self, f: &mut ratatui::Frame, rect: Rect) { -// // f.render_widget(PseudoTerminal::new(self.emulator.screen()), rect); -// // } -// // } -// -// use std::time::Duration; -// -// use bytes::Bytes; -// use handle_macro::Handle; -// use remux_core::{ -// comm, -// events::{CliEvent, DaemonEvent}, -// states::DaemonState, -// }; -// use tokio::{io::AsyncReadExt, net::UnixStream, sync::mpsc, time::interval}; -// use tracing::{Instrument, debug}; -// -// use crate::{ -// input_parser::{Action, InputParser, ParsedEvent}, -// prelude::*, -// utils::DisplayableVec, -// }; -// -// #[derive(Handle)] -// pub enum TerminalWidgetEvent { -// Selected(Option), // index of the selected item -// } -// use TerminalWidgetEvent::*; -// -// #[derive(Debug)] -// enum ClientWidgetEvent { -// Normal, // running normally with stdin parsed into events and sent to daemon -// SelectingSession, // means that the ui is currently busy selecting redirects stdin to ui selector -// } -// -// #[derive(Debug)] -// pub struct Client { -// _handle: ClientHandle, // handle used to send the client events -// stream: UnixStream, // the client owns the stream -// rx: mpsc::Receiver, // receiver for client events -// daemon_state: DaemonState, // determines if currently accepting events from daemon -// sync_daemon_state: bool, // if the state is dirty only then do we need to sync to the ui -// ui_stdin_tx: mpsc::Sender, // this is for popup actor to connect to stdin -// ui_handle: UIHandle, // how the client sends messages to ui -// input_parser: InputParser, // converts streams of bytes into actionable events -// client_state: ClientWidgetEvent, // the current state of the client -// } -// impl Client { -// #[instrument(skip(stream))] -// pub fn spawn(stream: UnixStream, daemon_state: DaemonState) -> Result { -// Client::new(stream, daemon_state)?.run() -// } -// -// #[instrument(skip(stream))] -// fn new(stream: UnixStream, daemon_state: DaemonState) -> Result { -// let (tx, rx) = mpsc::channel(100); -// let (ui_stdin_tx, ui_stdin_rx) = mpsc::channel(100); -// let handle = ClientHandle { tx }; -// let ui_handle = UI::spawn(handle.clone(), ui_stdin_rx)?; -// Ok(Self { -// _handle: handle, -// stream, -// rx, -// ui_stdin_tx, -// ui_handle, -// daemon_state, -// sync_daemon_state: true, -// input_parser: InputParser::new(), -// client_state: ClientWidgetEvent::Normal, -// }) -// } -// -// #[instrument(skip(self), fields(client_state = ?self.client_state))] -// fn run(mut self) -> Result { -// let task: CliTask = tokio::spawn({ -// let span = tracing::Span::current(); -// let mut stdin = tokio::io::stdin(); -// let mut stdin_buf = [0u8; 1024]; -// async move { -// let mut ticker = interval(Duration::from_millis(1000)); -// loop { -// tokio::select! { -// Some(event) = self.rx.recv() => { -// match event { -// Selected(index) => { -// debug!("Selected: {index:?}"); -// match self.client_state { -// ClientWidgetEvent::Normal => { -// error!("should not receive selected event in normal state"); -// }, -// ClientWidgetEvent::SelectingSession => { -// if let Some(index) = index { -// let selected_session = self.daemon_state.session_ids[index]; -// debug!("sending session selection: {selected_session}"); -// comm::send_event(&mut self.stream, CliEvent::SwitchSession(selected_session)).await.unwrap(); -// } -// debug!("Returning to normal state"); -// self.client_state = ClientWidgetEvent::Normal; -// } -// } -// } -// } -// }, -// res = comm::recv_daemon_event(&mut self.stream) => { -// match res { -// Ok(event) => { -// match event { -// DaemonEvent::Raw(bytes) => { -// trace!("DaemonEvent(Raw({bytes:?}))"); -// self.ui_handle.output(bytes).await?; -// } -// DaemonEvent::Disconnected => { -// debug!("DaemonEvent(Disconnected)"); -// self.ui_handle.kill().await.unwrap(); -// break; -// } -// DaemonEvent::CurrentSessions(session_ids) => { -// debug!("DaemonEvent(CurrentSessions({session_ids:?}))"); -// self.daemon_state.set_sessions(session_ids); -// self.sync_daemon_state = true; -// } -// DaemonEvent::ActiveSession(session_id) => { -// debug!("DaemonEvent(ActiveSession({session_id}))"); -// self.daemon_state.set_active_session(session_id); -// self.sync_daemon_state = true; -// } -// DaemonEvent::NewSession(session_id) => { -// debug!("DaemonEvent(NewSession({session_id}))"); -// self.daemon_state.add_session(session_id); -// self.sync_daemon_state = true; -// } -// DaemonEvent::DeletedSession(_session_id) => { -// todo!("implement delete session"); -// } -// } -// } -// Err(e) => { -// error!("Error receiving daemon event: {e}"); -// break; -// } -// } -// } -// stdin_res = stdin.read(&mut stdin_buf) => { -// match stdin_res { -// Ok(n) if n > 0 => { -// match self.client_state { -// ClientWidgetEvent::Normal => { -// trace!("Sending {n} bytes to Daemon"); -// for event in self.input_parser.process(&stdin_buf[..n]) { -// match event { -// ParsedEvent::DaemonAction(cli_event) => { -// comm::send_event(&mut self.stream, cli_event).await?; -// }, -// ParsedEvent::LocalAction(local_action) => { -// match local_action { -// Action::SwitchSession => { -// self.client_state = ClientWidgetEvent::SelectingSession; -// let items = DisplayableVec::new(self.daemon_state.session_ids.clone()); -// self.ui_handle.select_fuzzy(items, "Select Session".to_owned()).await.unwrap(); -// }, -// } -// }, -// } -// } -// -// }, -// ClientWidgetEvent::SelectingSession => { -// trace!("Sending {n} bytes to ui"); -// self.ui_stdin_tx.send(Bytes::copy_from_slice(&stdin_buf[..n])).await?; -// }, -// } -// } -// Ok(_) => { -// break; -// } -// Err(e) => { -// error!("Error receiving stdin: {e}"); -// continue; -// } -// } -// }, -// _ = ticker.tick(), if self.sync_daemon_state => { -// debug!("syncing daemon_state"); -// self.ui_handle.sync_daemon_state(self.daemon_state.clone()).await?; -// self.sync_daemon_state = false; -// } -// } -// } -// debug!("Client stopped"); -// Ok(()) -// }.instrument(span) -// }); -// -// Ok(task) -// } -// } -// -// diff --git a/cli/src/widgets/traits.rs b/cli/src/widgets/traits.rs deleted file mode 100644 index 45b586f..0000000 --- a/cli/src/widgets/traits.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use bytes::Bytes; -use crossterm::event::Event; -use ratatui::{Frame, layout::Rect}; -use tokio::sync::broadcast; - -use crate::{prelude::*, utils::DisplayableVec}; - -pub trait Selector { - fn run>( - selector: &Arc>, - rx: broadcast::Receiver, - items: DisplayableVec, - title: T, - ) -> Result<()>; - - fn render(selector: &Arc>, f: &mut Frame); -} - -pub trait Component { - fn handle_event(&mut self, event: Option); - - fn render(&mut self, f: &mut Frame, rect: Rect); -} diff --git a/defaults/statusbar.lua b/defaults/statusbar.lua index 14f6f83..3c71f8d 100644 --- a/defaults/statusbar.lua +++ b/defaults/statusbar.lua @@ -1,7 +1,7 @@ ui.status_line = { sections = { a = { - get_active_session(), + "active-session" }, b = { function()