add text input component

This commit is contained in:
Joe Ardent 2025-08-05 18:57:59 -07:00
parent 95fd86e851
commit 65a193f6e7
5 changed files with 71 additions and 20 deletions

11
Cargo.lock generated
View file

@ -1175,6 +1175,7 @@ dependencies = [
"thiserror 2.0.12",
"tokio",
"tower-http",
"tui-input",
"tui-logger",
]
@ -2377,6 +2378,16 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tui-input"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19"
dependencies = [
"ratatui",
"unicode-width 0.2.0",
]
[[package]]
name = "tui-logger"
version = "0.17.3"

View file

@ -26,4 +26,5 @@ sha256 = "1.6"
thiserror = "2"
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
tower-http = { version = "0.6", features = ["limit"] }
tui-input = "0.14.0"
tui-logger = { version = "0.17", features = ["crossterm"] }

View file

@ -11,6 +11,7 @@ use ratatui::{
};
use ratatui_explorer::FileExplorer;
use tokio::sync::mpsc::UnboundedReceiver;
use tui_input::{Input, backend::crossterm::EventHandler};
pub mod widgets;
@ -23,18 +24,19 @@ pub struct Peer {
pub struct App {
pub service: JoecalService,
pub screen: Vec<CurrentScreen>,
pub events: EventStream,
// addr -> (alias, fingerprint)
pub peers: Vec<Peer>,
pub peer_state: ListState,
pub receive_requests: BTreeMap<Julid, ReceiveRequest>,
screen: Vec<CurrentScreen>,
receiving_state: TableState,
// for getting messages back from the web server or web client about things we've done; the
// other end is held by the service
event_listener: UnboundedReceiver<TransferEvent>,
file_picker: FileExplorer,
text: Option<String>,
input: Input,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -66,6 +68,7 @@ impl App {
peer_state: Default::default(),
receive_requests: Default::default(),
receiving_state: Default::default(),
input: Default::default(),
}
}
@ -102,6 +105,13 @@ impl App {
Ok(())
}
pub fn screen(&self) -> CurrentScreen {
*self.screen.last().unwrap()
}
pub fn screen_mut(&mut self) -> &mut CurrentScreen {
self.screen.last_mut().unwrap()
}
async fn handle_key_event(&mut self, key_event: KeyEvent, event: crossterm::event::Event) {
let code = key_event.code;
let mode = self.screen.last_mut().unwrap();
@ -143,11 +153,13 @@ impl App {
KeyCode::Down => self.peer_state.select_next(),
_ => {}
},
_ => unreachable!(),
SendingScreen::Text => unreachable!(),
},
_ => {}
CurrentScreen::Main => {}
CurrentScreen::Stopping => unreachable!(),
},
},
// we only need to deal with sending text now
CurrentScreen::Sending(sending_screen) => match sending_screen {
SendingScreen::Text => match code {
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
@ -156,11 +168,21 @@ impl App {
self.text = None;
*sending_screen = SendingScreen::Files;
}
_ => { /* todo: add input widget to handle key input here */ }
_ => {
if let Some(changed) = self.input.handle_event(&event)
&& changed.value
{
if self.input.value().is_empty() {
self.text = None;
} else {
self.text = Some(self.input.to_string());
}
}
}
},
_ => unreachable!(),
// we've already handled the other sending modes
SendingScreen::Files | SendingScreen::Peers => unreachable!(),
},
CurrentScreen::Stopping => {}
}
}

View file

@ -104,6 +104,17 @@ static CONTENT_SEND_PEERS_MENU: LazyLock<Line> = LazyLock::new(|| {
])
});
static CONTENT_SEND_TEXT_MENU: LazyLock<Line> = LazyLock::new(|| {
Line::from(vec![
" Send Text ".into(),
"<ENTER>".blue().bold(),
" Peers ".into(),
"<TAB>".blue().bold(),
" Cancel ".into(),
"<ESC>".blue().bold(),
])
});
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let main_layout = Layout::vertical([Constraint::Min(5), Constraint::Min(3)]);
@ -121,12 +132,13 @@ impl Widget for &mut App {
let subscreen_margin = Margin::new(2, 4);
let current_screen = self.screen.last().unwrap();
// it's safe to call `unwrap()` here because we ensure there's always at least
// one element in `self.screen`; see the `self.pop()` method
let current_screen = self.screen();
match current_screen {
CurrentScreen::Main => {
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
outer_frame(*current_screen, &MAIN_MENU, area, buf);
outer_frame(&current_screen, &MAIN_MENU, area, buf);
logger(header_right.inner(header_margin), buf);
peers(
&self.peers,
@ -143,12 +155,12 @@ impl Widget for &mut App {
);
}
CurrentScreen::Logging => {
outer_frame(*current_screen, &LOGGING_MENU, area, buf);
outer_frame(&current_screen, &LOGGING_MENU, area, buf);
logger(area.inner(subscreen_margin), buf);
}
CurrentScreen::Receiving => {
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
outer_frame(*current_screen, &CONTENT_RECEIVE_MENU, area, buf);
outer_frame(&current_screen, &CONTENT_RECEIVE_MENU, area, buf);
receive_requests(
&rx_reqs,
&mut self.receiving_state,
@ -160,12 +172,14 @@ impl Widget for &mut App {
CurrentScreen::Sending(s) => {
match s {
SendingScreen::Files => {
outer_frame(*current_screen, &CONTENT_SEND_FILE_MENU, area, buf)
outer_frame(&current_screen, &CONTENT_SEND_FILE_MENU, area, buf)
}
SendingScreen::Peers => {
outer_frame(*current_screen, &CONTENT_SEND_PEERS_MENU, area, buf)
outer_frame(&current_screen, &CONTENT_SEND_PEERS_MENU, area, buf)
}
SendingScreen::Text => {
outer_frame(&current_screen, &CONTENT_SEND_TEXT_MENU, area, buf);
}
SendingScreen::Text => {}
}
self.file_picker
@ -179,15 +193,20 @@ impl Widget for &mut App {
bottom.inner(subscreen_margin),
buf,
);
if s == SendingScreen::Text {
let rect = centered_rect(area, 90, 80);
// TODO: display the text widget
}
}
_ => {
outer_frame(*current_screen, &MAIN_MENU, area, buf);
outer_frame(&current_screen, &MAIN_MENU, area, buf);
}
}
}
}
fn outer_frame(screen: CurrentScreen, menu: &Line, area: Rect, buf: &mut Buffer) {
fn outer_frame(screen: &CurrentScreen, menu: &Line, area: Rect, buf: &mut Buffer) {
let title = Line::from(" Joecalsend ".bold());
let block = Block::bordered()
.title(title.centered())
@ -349,7 +368,7 @@ impl Widget for NetworkInfoWidget {
}
// helpers
fn _centered_rect(area: Rect, width_pct: u16, height_pct: u16) -> Rect {
fn centered_rect(area: Rect, width_pct: u16, height_pct: u16) -> Rect {
let horizontal = Layout::horizontal([Constraint::Percentage(width_pct)]).flex(Flex::Center);
let vertical = Layout::vertical([Constraint::Percentage(height_pct)]).flex(Flex::Center);
let [area] = vertical.areas(area);

View file

@ -47,9 +47,7 @@ async fn start_and_run(
terminal.draw(|frame| app.draw(frame))?;
app.handle_events().await?;
if let Some(&top) = app.screen.last()
&& top == CurrentScreen::Stopping
{
if app.screen() == CurrentScreen::Stopping {
app.service.stop().await;
break;
}