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",
|
||||
"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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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 => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¤t_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(¤t_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(¤t_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(¤t_screen, &CONTENT_SEND_FILE_MENU, area, buf)
|
||||
}
|
||||
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
|
||||
|
@ -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(¤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 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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue