| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
|
| 22 |
|
| 23 |
|
| 24 |
|
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
|
| 30 |
|
| 31 |
|
| 32 |
|
| 33 |
|
| 34 |
|
| 35 |
|
| 36 |
|
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
|
| 41 |
|
| 42 |
|
| 43 |
use makeover_layout as layout; |
| 44 |
use quasi_router::{ |
| 45 |
Action, Chrome, Message, Method, Node, Outcome, Params, Request, Response, Screen, |
| 46 |
}; |
| 47 |
use ratatui::buffer::Buffer; |
| 48 |
use ratatui::layout::Rect; |
| 49 |
|
| 50 |
use crate::focus::{Reach, Spot}; |
| 51 |
use crate::{Tui, View}; |
| 52 |
|
| 53 |
|
| 54 |
|
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
|
| 59 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 60 |
pub enum Key { |
| 61 |
|
| 62 |
Char(char), |
| 63 |
|
| 64 |
Enter, |
| 65 |
|
| 66 |
Tab, |
| 67 |
|
| 68 |
BackTab, |
| 69 |
|
| 70 |
Backspace, |
| 71 |
|
| 72 |
Escape, |
| 73 |
|
| 74 |
Up, |
| 75 |
|
| 76 |
Down, |
| 77 |
|
| 78 |
PageUp, |
| 79 |
|
| 80 |
PageDown, |
| 81 |
} |
| 82 |
|
| 83 |
|
| 84 |
#[derive(Debug, Clone, PartialEq, Eq)] |
| 85 |
pub enum Step { |
| 86 |
|
| 87 |
Idle, |
| 88 |
|
| 89 |
Call(Request), |
| 90 |
|
| 91 |
|
| 92 |
Ask(String), |
| 93 |
|
| 94 |
Open(String), |
| 95 |
} |
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
|
| 100 |
|
| 101 |
|
| 102 |
|
| 103 |
|
| 104 |
#[derive(Debug, Clone)] |
| 105 |
struct Layer { |
| 106 |
screen: Screen, |
| 107 |
view: View, |
| 108 |
} |
| 109 |
|
| 110 |
|
| 111 |
#[derive(Debug, Clone)] |
| 112 |
pub struct Runtime { |
| 113 |
screen: Screen, |
| 114 |
view: View, |
| 115 |
|
| 116 |
|
| 117 |
chrome: Chrome, |
| 118 |
|
| 119 |
|
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
|
| 125 |
|
| 126 |
|
| 127 |
under: Vec<Layer>, |
| 128 |
|
| 129 |
|
| 130 |
|
| 131 |
|
| 132 |
|
| 133 |
|
| 134 |
history: Vec<Request>, |
| 135 |
|
| 136 |
here: Option<Request>, |
| 137 |
|
| 138 |
asked: Option<(Action, Params)>, |
| 139 |
|
| 140 |
saying: Option<Message>, |
| 141 |
} |
| 142 |
|
| 143 |
impl Runtime { |
| 144 |
|
| 145 |
#[must_use] |
| 146 |
pub fn new(screen: Screen) -> Self { |
| 147 |
let mut runtime = Self { |
| 148 |
screen, |
| 149 |
view: View::new(), |
| 150 |
chrome: Chrome::new(), |
| 151 |
under: Vec::new(), |
| 152 |
history: Vec::new(), |
| 153 |
here: None, |
| 154 |
asked: None, |
| 155 |
saying: None, |
| 156 |
}; |
| 157 |
runtime.view.seed(&runtime.screen); |
| 158 |
runtime |
| 159 |
} |
| 160 |
|
| 161 |
|
| 162 |
|
| 163 |
|
| 164 |
|
| 165 |
#[must_use] |
| 166 |
pub fn with_chrome(mut self, chrome: Chrome) -> Self { |
| 167 |
self.chrome = chrome; |
| 168 |
self |
| 169 |
} |
| 170 |
|
| 171 |
|
| 172 |
#[must_use] |
| 173 |
pub const fn overlaid(&self) -> bool { |
| 174 |
!self.under.is_empty() |
| 175 |
} |
| 176 |
|
| 177 |
|
| 178 |
|
| 179 |
|
| 180 |
|
| 181 |
#[must_use] |
| 182 |
pub const fn screen(&self) -> &Screen { |
| 183 |
&self.screen |
| 184 |
} |
| 185 |
|
| 186 |
|
| 187 |
#[must_use] |
| 188 |
pub const fn view(&self) -> &View { |
| 189 |
&self.view |
| 190 |
} |
| 191 |
|
| 192 |
|
| 193 |
#[must_use] |
| 194 |
pub fn reaches(&self) -> Vec<Reach> { |
| 195 |
crate::focus::reaches(&self.screen) |
| 196 |
} |
| 197 |
|
| 198 |
|
| 199 |
|
| 200 |
|
| 201 |
|
| 202 |
|
| 203 |
|
| 204 |
#[must_use] |
| 205 |
pub fn editing(&self) -> bool { |
| 206 |
self.focused().is_some_and(|spot| spot.field().is_some()) |
| 207 |
} |
| 208 |
|
| 209 |
|
| 210 |
#[must_use] |
| 211 |
pub const fn asking(&self) -> bool { |
| 212 |
self.asked.is_some() |
| 213 |
} |
| 214 |
|
| 215 |
|
| 216 |
pub fn draw(&self, tui: &Tui, area: Rect, buf: &mut Buffer) { |
| 217 |
|
| 218 |
|
| 219 |
|
| 220 |
for layer in &self.under { |
| 221 |
tui.screen(&layer.screen, &layer.view, area, buf); |
| 222 |
} |
| 223 |
let area = if self.under.is_empty() { |
| 224 |
area |
| 225 |
} else { |
| 226 |
let inset = Self::overlay_area(area); |
| 227 |
|
| 228 |
|
| 229 |
for y in inset.top()..inset.bottom() { |
| 230 |
for x in inset.left()..inset.right() { |
| 231 |
buf[(x, y)].reset(); |
| 232 |
} |
| 233 |
} |
| 234 |
inset |
| 235 |
}; |
| 236 |
tui.screen(&self.screen, &self.view, area, buf); |
| 237 |
} |
| 238 |
|
| 239 |
|
| 240 |
|
| 241 |
|
| 242 |
|
| 243 |
|
| 244 |
|
| 245 |
fn overlay_area(area: Rect) -> Rect { |
| 246 |
let pad_x = (area.width / 8) |
| 247 |
.max(1) |
| 248 |
.min(area.width.saturating_sub(2) / 2); |
| 249 |
let pad_y = (area.height / 8) |
| 250 |
.max(1) |
| 251 |
.min(area.height.saturating_sub(2) / 2); |
| 252 |
Rect { |
| 253 |
x: area.x + pad_x, |
| 254 |
y: area.y + pad_y, |
| 255 |
width: area.width.saturating_sub(pad_x * 2), |
| 256 |
height: area.height.saturating_sub(pad_y * 2), |
| 257 |
} |
| 258 |
} |
| 259 |
|
| 260 |
|
| 261 |
|
| 262 |
|
| 263 |
|
| 264 |
|
| 265 |
|
| 266 |
pub fn say(&mut self, text: impl Into<String>) { |
| 267 |
self.screen.notices.push(Node::Notice { |
| 268 |
kind: layout::Notice::Banner, |
| 269 |
tone: layout::Tone::Danger, |
| 270 |
text: text.into(), |
| 271 |
}); |
| 272 |
} |
| 273 |
|
| 274 |
|
| 275 |
#[must_use] |
| 276 |
pub fn focused(&self) -> Option<Spot> { |
| 277 |
let mut reaches = crate::focus::reaches(&self.screen); |
| 278 |
if self.view.focus() >= reaches.len() { |
| 279 |
return None; |
| 280 |
} |
| 281 |
Some(reaches.swap_remove(self.view.focus()).spot) |
| 282 |
} |
| 283 |
|
| 284 |
|
| 285 |
pub fn key(&mut self, key: Key) -> Step { |
| 286 |
|
| 287 |
|
| 288 |
|
| 289 |
if let Some((action, payload)) = self.asked.take() { |
| 290 |
return match key { |
| 291 |
|
| 292 |
|
| 293 |
|
| 294 |
|
| 295 |
Key::Char('y' | 'Y') | Key::Enter => Self::send(&action, payload), |
| 296 |
_ => Step::Idle, |
| 297 |
}; |
| 298 |
} |
| 299 |
|
| 300 |
|
| 301 |
|
| 302 |
|
| 303 |
|
| 304 |
|
| 305 |
|
| 306 |
|
| 307 |
if !self.editing() || !matches!(key, Key::Char(_)) { |
| 308 |
let pressed = Self::key_name(key); |
| 309 |
if let Some(binding) = pressed.as_deref().and_then(|name| self.chrome.bound(name)) { |
| 310 |
return Self::call(&binding.action.clone()); |
| 311 |
} |
| 312 |
} |
| 313 |
|
| 314 |
let reaches = crate::focus::reaches(&self.screen); |
| 315 |
let count = reaches.len(); |
| 316 |
let here = reaches |
| 317 |
.get(self.view.focus()) |
| 318 |
.map(|reach| reach.spot.clone()); |
| 319 |
|
| 320 |
match key { |
| 321 |
Key::Tab | Key::Down => { |
| 322 |
self.view.advance(1, count); |
| 323 |
Step::Idle |
| 324 |
} |
| 325 |
Key::BackTab | Key::Up => { |
| 326 |
self.view.advance(-1, count); |
| 327 |
Step::Idle |
| 328 |
} |
| 329 |
|
| 330 |
Key::PageDown | Key::PageUp => { |
| 331 |
|
| 332 |
|
| 333 |
|
| 334 |
|
| 335 |
if let Some(reach) = reaches.get(self.view.focus()) { |
| 336 |
let rows = if matches!(key, Key::PageDown) { |
| 337 |
10 |
| 338 |
} else { |
| 339 |
-10 |
| 340 |
}; |
| 341 |
self.view.scroll_by(&reach.region, rows); |
| 342 |
} |
| 343 |
Step::Idle |
| 344 |
} |
| 345 |
|
| 346 |
|
| 347 |
|
| 348 |
Key::Escape => { |
| 349 |
if self.dismiss() { |
| 350 |
Step::Idle |
| 351 |
} else { |
| 352 |
self.back() |
| 353 |
} |
| 354 |
} |
| 355 |
|
| 356 |
Key::Backspace => { |
| 357 |
if let Some(field) = here.as_ref().and_then(Spot::field) { |
| 358 |
self.view.backspace(field); |
| 359 |
} |
| 360 |
Step::Idle |
| 361 |
} |
| 362 |
|
| 363 |
Key::Enter => match here { |
| 364 |
Some(Spot::Act { |
| 365 |
action, |
| 366 |
confirm, |
| 367 |
over, |
| 368 |
.. |
| 369 |
}) => { |
| 370 |
let payload = self.gathering(over.as_deref()); |
| 371 |
match confirm { |
| 372 |
Some(prompt) => { |
| 373 |
self.asked = Some((action, payload)); |
| 374 |
Step::Ask(prompt) |
| 375 |
} |
| 376 |
None => Self::send(&action, payload), |
| 377 |
} |
| 378 |
} |
| 379 |
Some(Spot::Submit { action, names }) => { |
| 380 |
let payload = self.view.submission( |
| 381 |
&names, |
| 382 |
&reaches |
| 383 |
.iter() |
| 384 |
.map(|reach| reach.spot.clone()) |
| 385 |
.collect::<Vec<_>>(), |
| 386 |
); |
| 387 |
Self::send(&action, payload) |
| 388 |
} |
| 389 |
|
| 390 |
|
| 391 |
|
| 392 |
|
| 393 |
Some(Spot::Field(_)) | None => Step::Idle, |
| 394 |
Some(other) => match other.enters() { |
| 395 |
Some(action) => Self::call(&action.clone()), |
| 396 |
None => Step::Idle, |
| 397 |
}, |
| 398 |
}, |
| 399 |
|
| 400 |
Key::Char(' ') if !self.editing() => match here { |
| 401 |
|
| 402 |
|
| 403 |
|
| 404 |
|
| 405 |
Some(Spot::Row { |
| 406 |
toggle: Some(action), |
| 407 |
.. |
| 408 |
}) => Self::call(&action), |
| 409 |
|
| 410 |
|
| 411 |
|
| 412 |
|
| 413 |
|
| 414 |
|
| 415 |
|
| 416 |
|
| 417 |
|
| 418 |
|
| 419 |
Some(Spot::Row { |
| 420 |
ticked: Some(_), |
| 421 |
value: Some(value), |
| 422 |
.. |
| 423 |
}) if self.screen.selection.is_some() => { |
| 424 |
self.view.tick(&value); |
| 425 |
Step::Idle |
| 426 |
} |
| 427 |
_ => Step::Idle, |
| 428 |
}, |
| 429 |
|
| 430 |
Key::Char(ch) => { |
| 431 |
if let Some(field) = here.as_ref().and_then(Spot::field).cloned() { |
| 432 |
self.type_into(&field, ch); |
| 433 |
return self.after_typing(here.as_ref()); |
| 434 |
} |
| 435 |
|
| 436 |
|
| 437 |
|
| 438 |
|
| 439 |
|
| 440 |
let pressed = ch.to_string(); |
| 441 |
let claimed = reaches.iter().find_map(|reach| match &reach.spot { |
| 442 |
Spot::Act { |
| 443 |
action, |
| 444 |
key: Some(key), |
| 445 |
over, |
| 446 |
.. |
| 447 |
} if *key == pressed => Some((action.clone(), over.clone())), |
| 448 |
_ => None, |
| 449 |
}); |
| 450 |
match claimed { |
| 451 |
Some((action, over)) => { |
| 452 |
let payload = self.gathering(over.as_deref()); |
| 453 |
Self::send(&action, payload) |
| 454 |
} |
| 455 |
None => Step::Idle, |
| 456 |
} |
| 457 |
} |
| 458 |
} |
| 459 |
} |
| 460 |
|
| 461 |
|
| 462 |
|
| 463 |
|
| 464 |
|
| 465 |
|
| 466 |
|
| 467 |
|
| 468 |
pub fn apply(&mut self, request: &Request, response: Response) -> Option<Request> { |
| 469 |
let Response { |
| 470 |
outcome, |
| 471 |
notice, |
| 472 |
address, |
| 473 |
invalidates, |
| 474 |
} = response; |
| 475 |
self.saying = notice.or(self.saying.take()); |
| 476 |
|
| 477 |
match outcome { |
| 478 |
|
| 479 |
|
| 480 |
|
| 481 |
|
| 482 |
|
| 483 |
|
| 484 |
|
| 485 |
|
| 486 |
|
| 487 |
|
| 488 |
|
| 489 |
Outcome::Over(screen) => { |
| 490 |
let under = Layer { |
| 491 |
screen: std::mem::replace(&mut self.screen, screen), |
| 492 |
view: std::mem::replace(&mut self.view, View::new()), |
| 493 |
}; |
| 494 |
self.under.push(under); |
| 495 |
self.view.seed(&self.screen); |
| 496 |
self.announce(); |
| 497 |
None |
| 498 |
} |
| 499 |
|
| 500 |
|
| 501 |
|
| 502 |
|
| 503 |
Outcome::Screen(screen) => { |
| 504 |
self.under.clear(); |
| 505 |
self.remember(request, address.as_ref()); |
| 506 |
self.screen = screen; |
| 507 |
self.view.reset(); |
| 508 |
|
| 509 |
|
| 510 |
|
| 511 |
self.view.seed(&self.screen); |
| 512 |
self.announce(); |
| 513 |
None |
| 514 |
} |
| 515 |
Outcome::Fragment { region, node } => { |
| 516 |
|
| 517 |
|
| 518 |
|
| 519 |
|
| 520 |
|
| 521 |
|
| 522 |
|
| 523 |
|
| 524 |
|
| 525 |
|
| 526 |
let mut missing: Vec<String> = Vec::new(); |
| 527 |
if !self.screen.replace(®ion, node) { |
| 528 |
missing.push(region); |
| 529 |
} |
| 530 |
for stale in invalidates { |
| 531 |
if !self.screen.replace(&stale.region, stale.node) { |
| 532 |
missing.push(stale.region); |
| 533 |
} |
| 534 |
} |
| 535 |
if !missing.is_empty() { |
| 536 |
|
| 537 |
|
| 538 |
let named = missing |
| 539 |
.iter() |
| 540 |
.map(|region| format!("`{region}`")) |
| 541 |
.collect::<Vec<_>>() |
| 542 |
.join(", "); |
| 543 |
let subject = if missing.len() == 1 { "is" } else { "are" }; |
| 544 |
self.saying = Some(Message { |
| 545 |
kind: layout::Notice::Banner, |
| 546 |
tone: layout::Tone::Danger, |
| 547 |
text: format!("nothing on this screen {subject} called {named}"), |
| 548 |
undo: None, |
| 549 |
}); |
| 550 |
} |
| 551 |
self.view.prune(&self.screen); |
| 552 |
self.announce(); |
| 553 |
None |
| 554 |
} |
| 555 |
Outcome::Goto(action) => match Self::call(&action) { |
| 556 |
Step::Call(request) => Some(request), |
| 557 |
|
| 558 |
|
| 559 |
_ => None, |
| 560 |
}, |
| 561 |
} |
| 562 |
} |
| 563 |
|
| 564 |
|
| 565 |
|
| 566 |
|
| 567 |
|
| 568 |
|
| 569 |
|
| 570 |
|
| 571 |
fn key_name(key: Key) -> Option<String> { |
| 572 |
Some(match key { |
| 573 |
Key::Char(ch) => ch.to_string(), |
| 574 |
Key::Enter => "enter".into(), |
| 575 |
Key::Escape => "escape".into(), |
| 576 |
Key::Tab => "tab".into(), |
| 577 |
Key::BackTab => "backtab".into(), |
| 578 |
Key::Up => "up".into(), |
| 579 |
Key::Down => "down".into(), |
| 580 |
Key::PageUp => "pageup".into(), |
| 581 |
Key::PageDown => "pagedown".into(), |
| 582 |
Key::Backspace => "backspace".into(), |
| 583 |
}) |
| 584 |
} |
| 585 |
|
| 586 |
|
| 587 |
|
| 588 |
|
| 589 |
|
| 590 |
|
| 591 |
|
| 592 |
fn dismiss(&mut self) -> bool { |
| 593 |
match self.under.pop() { |
| 594 |
Some(layer) => { |
| 595 |
self.screen = layer.screen; |
| 596 |
self.view = layer.view; |
| 597 |
true |
| 598 |
} |
| 599 |
None => false, |
| 600 |
} |
| 601 |
} |
| 602 |
|
| 603 |
fn back(&mut self) -> Step { |
| 604 |
match self.history.pop() { |
| 605 |
Some(request) => { |
| 606 |
self.here = Some(request.clone()); |
| 607 |
Step::Call(request) |
| 608 |
} |
| 609 |
None => Step::Idle, |
| 610 |
} |
| 611 |
} |
| 612 |
|
| 613 |
|
| 614 |
|
| 615 |
|
| 616 |
|
| 617 |
|
| 618 |
|
| 619 |
fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) { |
| 620 |
let place = match address { |
| 621 |
Some(quasi_router::Address::Enters(_)) => true, |
| 622 |
Some(quasi_router::Address::Unchanged) => false, |
| 623 |
Some(quasi_router::Address::Replaces(_)) => { |
| 624 |
self.here = Some(request.clone()); |
| 625 |
return; |
| 626 |
} |
| 627 |
None => request.method == Method::Get, |
| 628 |
}; |
| 629 |
if place && let Some(previous) = self.here.replace(request.clone()) { |
| 630 |
self.history.push(previous); |
| 631 |
} |
| 632 |
} |
| 633 |
|
| 634 |
|
| 635 |
fn announce(&mut self) { |
| 636 |
|
| 637 |
|
| 638 |
|
| 639 |
if let Some(Message { |
| 640 |
kind, tone, text, .. |
| 641 |
}) = self.saying.take() |
| 642 |
{ |
| 643 |
self.screen.notices.push(Node::Notice { kind, tone, text }); |
| 644 |
} |
| 645 |
} |
| 646 |
|
| 647 |
|
| 648 |
fn type_into(&mut self, field: &crate::FieldSpot, ch: char) { |
| 649 |
if matches!(field.kind, layout::FieldKind::Checkbox) { |
| 650 |
|
| 651 |
|
| 652 |
|
| 653 |
let ticked = self.view.typed(field) == Node::SELECTED; |
| 654 |
let next = if ticked { |
| 655 |
String::new() |
| 656 |
} else { |
| 657 |
Node::SELECTED.to_string() |
| 658 |
}; |
| 659 |
self.view.set(&field.name, next); |
| 660 |
return; |
| 661 |
} |
| 662 |
|
| 663 |
|
| 664 |
|
| 665 |
|
| 666 |
if let Some(limit) = field.max_length |
| 667 |
&& self.view.typed(field).chars().count() >= limit as usize |
| 668 |
{ |
| 669 |
return; |
| 670 |
} |
| 671 |
self.view.push(field, ch); |
| 672 |
} |
| 673 |
|
| 674 |
|
| 675 |
fn after_typing(&mut self, here: Option<&Spot>) -> Step { |
| 676 |
match here.and_then(Spot::field).and_then(|field| { |
| 677 |
field |
| 678 |
.changes |
| 679 |
.clone() |
| 680 |
.map(|action| (action, field.name.clone())) |
| 681 |
}) { |
| 682 |
|
| 683 |
|
| 684 |
|
| 685 |
|
| 686 |
|
| 687 |
Some((action, name)) => { |
| 688 |
let value = self.view.edit(&name).unwrap_or_default().to_string(); |
| 689 |
let payload = Params::new().with(name, value); |
| 690 |
Self::send(&action, payload) |
| 691 |
} |
| 692 |
None => Step::Idle, |
| 693 |
} |
| 694 |
} |
| 695 |
|
| 696 |
|
| 697 |
|
| 698 |
|
| 699 |
|
| 700 |
|
| 701 |
|
| 702 |
|
| 703 |
|
| 704 |
|
| 705 |
|
| 706 |
|
| 707 |
|
| 708 |
|
| 709 |
fn gathering(&self, over: Option<&str>) -> Params { |
| 710 |
let mut payload = Params::new(); |
| 711 |
if over.is_some() { |
| 712 |
for value in self.view.ticks() { |
| 713 |
payload.insert(Node::TICKED.to_owned(), value.to_owned()); |
| 714 |
} |
| 715 |
} |
| 716 |
payload |
| 717 |
} |
| 718 |
|
| 719 |
|
| 720 |
fn call(action: &Action) -> Step { |
| 721 |
Self::send(action, Params::new()) |
| 722 |
} |
| 723 |
|
| 724 |
|
| 725 |
fn send(action: &Action, extra: Params) -> Step { |
| 726 |
let Some(path) = action.destination.route() else { |
| 727 |
return Step::Open(action.destination.as_str().to_string()); |
| 728 |
}; |
| 729 |
let mut payload = extra; |
| 730 |
payload.absorb(action.params.clone()); |
| 731 |
Step::Call(Request { |
| 732 |
method: action.method, |
| 733 |
path: path.to_string(), |
| 734 |
captures: Params::new(), |
| 735 |
payload, |
| 736 |
carried: action.carried.clone(), |
| 737 |
}) |
| 738 |
} |
| 739 |
} |
| 740 |
|