add text input component
This commit is contained in:
parent
95fd86e851
commit
65a193f6e7
5 changed files with 71 additions and 20 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -1175,6 +1175,7 @@ dependencies = [
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
"tui-input",
|
||||||
"tui-logger",
|
"tui-logger",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2377,6 +2378,16 @@ version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
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]]
|
[[package]]
|
||||||
name = "tui-logger"
|
name = "tui-logger"
|
||||||
version = "0.17.3"
|
version = "0.17.3"
|
||||||
|
|
|
@ -26,4 +26,5 @@ sha256 = "1.6"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
|
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
|
||||||
tower-http = { version = "0.6", features = ["limit"] }
|
tower-http = { version = "0.6", features = ["limit"] }
|
||||||
|
tui-input = "0.14.0"
|
||||||
tui-logger = { version = "0.17", features = ["crossterm"] }
|
tui-logger = { version = "0.17", features = ["crossterm"] }
|
||||||
|
|
|
@ -11,6 +11,7 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
use ratatui_explorer::FileExplorer;
|
use ratatui_explorer::FileExplorer;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
|
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||||
|
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
|
@ -23,18 +24,19 @@ pub struct Peer {
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub service: JoecalService,
|
pub service: JoecalService,
|
||||||
pub screen: Vec<CurrentScreen>,
|
|
||||||
pub events: EventStream,
|
pub events: EventStream,
|
||||||
// addr -> (alias, fingerprint)
|
// addr -> (alias, fingerprint)
|
||||||
pub peers: Vec<Peer>,
|
pub peers: Vec<Peer>,
|
||||||
pub peer_state: ListState,
|
pub peer_state: ListState,
|
||||||
pub receive_requests: BTreeMap<Julid, ReceiveRequest>,
|
pub receive_requests: BTreeMap<Julid, ReceiveRequest>,
|
||||||
|
screen: Vec<CurrentScreen>,
|
||||||
receiving_state: TableState,
|
receiving_state: TableState,
|
||||||
// for getting messages back from the web server or web client about things we've done; the
|
// for getting messages back from the web server or web client about things we've done; the
|
||||||
// other end is held by the service
|
// other end is held by the service
|
||||||
event_listener: UnboundedReceiver<TransferEvent>,
|
event_listener: UnboundedReceiver<TransferEvent>,
|
||||||
file_picker: FileExplorer,
|
file_picker: FileExplorer,
|
||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
|
input: Input,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
@ -66,6 +68,7 @@ impl App {
|
||||||
peer_state: Default::default(),
|
peer_state: Default::default(),
|
||||||
receive_requests: Default::default(),
|
receive_requests: Default::default(),
|
||||||
receiving_state: Default::default(),
|
receiving_state: Default::default(),
|
||||||
|
input: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +105,13 @@ impl App {
|
||||||
Ok(())
|
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) {
|
async fn handle_key_event(&mut self, key_event: KeyEvent, event: crossterm::event::Event) {
|
||||||
let code = key_event.code;
|
let code = key_event.code;
|
||||||
let mode = self.screen.last_mut().unwrap();
|
let mode = self.screen.last_mut().unwrap();
|
||||||
|
@ -143,11 +153,13 @@ impl App {
|
||||||
KeyCode::Down => self.peer_state.select_next(),
|
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 {
|
CurrentScreen::Sending(sending_screen) => match sending_screen {
|
||||||
SendingScreen::Text => match code {
|
SendingScreen::Text => match code {
|
||||||
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
|
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
|
||||||
|
@ -156,11 +168,21 @@ impl App {
|
||||||
self.text = None;
|
self.text = None;
|
||||||
*sending_screen = SendingScreen::Files;
|
*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 => {}
|
CurrentScreen::Stopping => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
impl Widget for &mut App {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let main_layout = Layout::vertical([Constraint::Min(5), Constraint::Min(3)]);
|
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 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 {
|
match current_screen {
|
||||||
CurrentScreen::Main => {
|
CurrentScreen::Main => {
|
||||||
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
|
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
|
||||||
outer_frame(*current_screen, &MAIN_MENU, area, buf);
|
outer_frame(¤t_screen, &MAIN_MENU, area, buf);
|
||||||
logger(header_right.inner(header_margin), buf);
|
logger(header_right.inner(header_margin), buf);
|
||||||
peers(
|
peers(
|
||||||
&self.peers,
|
&self.peers,
|
||||||
|
@ -143,12 +155,12 @@ impl Widget for &mut App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
CurrentScreen::Logging => {
|
CurrentScreen::Logging => {
|
||||||
outer_frame(*current_screen, &LOGGING_MENU, area, buf);
|
outer_frame(¤t_screen, &LOGGING_MENU, area, buf);
|
||||||
logger(area.inner(subscreen_margin), buf);
|
logger(area.inner(subscreen_margin), buf);
|
||||||
}
|
}
|
||||||
CurrentScreen::Receiving => {
|
CurrentScreen::Receiving => {
|
||||||
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
|
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
|
||||||
outer_frame(*current_screen, &CONTENT_RECEIVE_MENU, area, buf);
|
outer_frame(¤t_screen, &CONTENT_RECEIVE_MENU, area, buf);
|
||||||
receive_requests(
|
receive_requests(
|
||||||
&rx_reqs,
|
&rx_reqs,
|
||||||
&mut self.receiving_state,
|
&mut self.receiving_state,
|
||||||
|
@ -160,12 +172,14 @@ impl Widget for &mut App {
|
||||||
CurrentScreen::Sending(s) => {
|
CurrentScreen::Sending(s) => {
|
||||||
match s {
|
match s {
|
||||||
SendingScreen::Files => {
|
SendingScreen::Files => {
|
||||||
outer_frame(*current_screen, &CONTENT_SEND_FILE_MENU, area, buf)
|
outer_frame(¤t_screen, &CONTENT_SEND_FILE_MENU, area, buf)
|
||||||
}
|
}
|
||||||
SendingScreen::Peers => {
|
SendingScreen::Peers => {
|
||||||
outer_frame(*current_screen, &CONTENT_SEND_PEERS_MENU, area, buf)
|
outer_frame(¤t_screen, &CONTENT_SEND_PEERS_MENU, area, buf)
|
||||||
|
}
|
||||||
|
SendingScreen::Text => {
|
||||||
|
outer_frame(¤t_screen, &CONTENT_SEND_TEXT_MENU, area, buf);
|
||||||
}
|
}
|
||||||
SendingScreen::Text => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.file_picker
|
self.file_picker
|
||||||
|
@ -179,15 +193,20 @@ impl Widget for &mut App {
|
||||||
bottom.inner(subscreen_margin),
|
bottom.inner(subscreen_margin),
|
||||||
buf,
|
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(¤t_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 title = Line::from(" Joecalsend ".bold());
|
||||||
let block = Block::bordered()
|
let block = Block::bordered()
|
||||||
.title(title.centered())
|
.title(title.centered())
|
||||||
|
@ -349,7 +368,7 @@ impl Widget for NetworkInfoWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// helpers
|
// 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 horizontal = Layout::horizontal([Constraint::Percentage(width_pct)]).flex(Flex::Center);
|
||||||
let vertical = Layout::vertical([Constraint::Percentage(height_pct)]).flex(Flex::Center);
|
let vertical = Layout::vertical([Constraint::Percentage(height_pct)]).flex(Flex::Center);
|
||||||
let [area] = vertical.areas(area);
|
let [area] = vertical.areas(area);
|
||||||
|
|
|
@ -47,9 +47,7 @@ async fn start_and_run(
|
||||||
terminal.draw(|frame| app.draw(frame))?;
|
terminal.draw(|frame| app.draw(frame))?;
|
||||||
app.handle_events().await?;
|
app.handle_events().await?;
|
||||||
|
|
||||||
if let Some(&top) = app.screen.last()
|
if app.screen() == CurrentScreen::Stopping {
|
||||||
&& top == CurrentScreen::Stopping
|
|
||||||
{
|
|
||||||
app.service.stop().await;
|
app.service.stop().await;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue