//! The half a webview host never writes. //! //! `quasi-axum` and `quasi-tauri` both answer a request with markup and stop. //! Everything between one request and the next — which control is under the //! caret, what the user has typed into it, what a key means, where the back //! button goes — is the browser's, and neither adapter contains a line of it. //! A terminal has no browser under it, so this is that half, written out. //! //! # It does not own the router //! //! [`Runtime`] turns keys into [`Request`]s and applies [`Response`]s, and it //! never calls a handler. The host holds the router and the state and does the //! calling, which keeps this free of the state type and makes every binding //! below testable without standing up an app. //! //! ```text //! key ──► Runtime::key ──► Step::Call(request) //! │ //! host: router.handle(&state, request) //! │ //! Runtime::apply ◄── Response //! ``` //! //! # The bindings are this renderer's, and the description reaches two of them //! //! Nothing in a description says what Tab does, so the table below is policy. //! The two exceptions are the two the vocabulary already carries: [`Act::key`] //! names the key that reaches a control, and [`Act::confirm`] names the question //! to ask before doing it. Both were drawn and declined by the drawing half, //! and this is where they are honoured. //! //! | Key | What it does | //! |---|---| //! | Tab, Down | the next reachable thing | //! | `BackTab`, Up | the previous one | //! | Enter | call what is under the caret | //! | Space | tick the row under the caret | //! | `PageUp`, `PageDown` | scroll the region the caret is in | //! | Backspace | take a character back out of a field | //! | printable | type into a field, or reach the control that named the key | //! | Escape | step out of a table row, dismiss the overlay, or go back | //! | Left, Right | step into the table row under the caret and cycle its controls | //! //! # Right steps into a table row //! //! A table row is one stop with the controls in its cells inside it rather //! than beside it, which [`crate::focus`]'s header explains; this is the key //! that gets at them. Right steps in and cycles forward, Left cycles back, //! Escape steps out, and Enter presses whatever the caret has stepped onto //! instead of opening the row. //! //! Right and Left rather than Enter, because Enter is taken: `Row::activate` //! already means "this row opens", and every measured table with a control in a //! cell also opens its rows. They are the keys a grid is walked with everywhere //! else, and the reader is already holding them for the same shape. //! //! They mean this before they mean the other thing they mean. A region showing //! one child at a time is moved with the same two keys, and the caret standing //! on a row with controls in it wins, because the thing under the caret beats //! the screen-wide search that arm falls back to. A screen with a table inside a //! carousel therefore steps between frames from any stop but that one, which is //! the cost, and it is the smaller of the two: the frames are reachable from //! anywhere else on the screen and the controls in the row are reachable from //! nowhere else. use makeover_layout as layout; use quasi_router::{ Accepted, Action, Choosing, Chrome, Consult, Frame, Locating, Message, Method, Node, Outcome, Params, Request, Response, Screen, Slot, safe_file_name, }; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use crate::focus::{FieldSpot, Reach, Spot}; use crate::{Tui, View}; /// A key, named the way this crate wants to talk about one. /// /// Not crossterm's, deliberately. A host maps its own events onto this in a /// dozen lines, and in exchange the bindings below are testable without a /// terminal and this crate does not make every consumer take a backend it might /// not be using. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Key { /// A character the user typed. Char(char), /// Confirm, follow, submit. Enter, /// Forward through the reachable things. Tab, /// Backward through them. BackTab, /// Take a character back. Backspace, /// Out, back, never mind. Escape, /// Up one reachable thing. Up, /// Down one reachable thing. Down, /// A screen's worth backwards. PageUp, /// A screen's worth forwards. PageDown, /// Back one child, in a region showing one at a time. Left, /// On one child, in a region showing one at a time. Right, } /// How a press was meant, as the payload it travels in. /// /// A terminal has no modifiers on this crate's `Key`, so the two readings it /// can offer are the two keys it binds: Enter is the plain one and Space is /// "and also this one". `Choosing::Through` -- a run from the app's pointer to /// here -- has no key yet and is not invented one: shift-Enter is not /// expressible while `Key` carries no modifiers, and widening `Key` is a /// change to every host that drives this renderer. fn meant(choosing: Choosing) -> Params { Params::new().with(Node::CHOOSING.to_owned(), choosing.as_str().to_owned()) } /// What the host should do about a key. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Step { /// Nothing left to do but redraw. Idle, /// Ask the router this, then hand the answer to [`Runtime::apply`]. Call(Request), /// Ask this question. The next key answers it: `y` or Enter does the thing, /// anything else does not. Ask(String), /// Somewhere outside the app. The host opens it, and nothing comes back. Open(String), /// This value goes on the clipboard. The host puts it there, and nothing /// comes back. /// /// [`Act::copies`] says a press copies something, and this is the /// terminal's half. [`Open`](Self::Open)'s shape and for its reason: this /// crate turns keys into requests and owns no I/O, so writing OSC 52 or /// reaching a system clipboard is the host's, exactly as opening a URL is. /// /// A copying act's action is local -- [`Act::copying`] makes it so -- and /// this is what the press produces, so nothing is asked and nothing is /// lost. An act that copies *and* names a route is not describable through /// this runtime in one step: the copy is what comes back and the call does /// not happen. No measured site is one. /// /// [`Act::copies`]: quasi_router::Act::copies /// [`Act::copying`]: quasi_router::Act::copying Copy(String), /// Ask the router these, each not until its own `after` has passed with no /// further key. A later one of these replaces an outstanding one whole /// rather than queueing behind it. /// /// [`Field::consults`], and the waits are the description's own. Until it /// existed this runtime had no way to say "later" and the comment on /// [`Runtime::after_typing`] said why it would not invent one: a delay /// picked here is a delay the webview renderer disagrees with, and the two /// hosts then debounce the same field differently. /// /// The clock stays with the host, exactly as the router does. This crate /// turns keys into requests and owns no I/O, so "wait 500ms, and drop this /// if another key arrives" is the host's timer to run — the same division /// that keeps [`Call`](Self::Call) from calling anything. /// /// # Several, and still no scheduler here /// /// `N8`. One box can raise more than one question: MNW's discover search /// asks a suggestion route after 200ms and a results route after 150. The /// runtime said it did not want a queue and still does not have one — this /// is one keystroke producing one `Step`, and what varies is how many /// questions that keystroke posed. The host runs a timer per entry, which /// is the timer it was already running, and replacement stays wholesale: /// every question here is about the same value, so a newer keystroke /// invalidates all of them together. /// /// [`Field::consults`]: quasi_router::Field::consults CallAfter { /// The questions this keystroke posed, in the order the description /// asked them. asks: Vec, }, } /// One question and how long the value must stand still before it is asked. /// /// [`Step::CallAfter`]'s member. A pair rather than two parallel lists, so a /// host cannot pick up a request and the wrong wait. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Delayed { /// What to ask. pub request: Request, /// How long the value must stand still first. pub after: std::time::Duration, } /// A file a route answered with, for the host to put somewhere. /// /// [`Outcome::File`] says what the file is and never where it goes, so this /// runtime does not write it: this crate turns keys into requests and owns no /// I/O, the same division that keeps [`Step::Call`] from calling anything and /// [`Step::Open`] from opening anything. /// /// The host drains it with [`Runtime::handed`] after [`Runtime::apply`]. On a /// terminal the ruling says what to do with it — write it into the working /// directory under [`name`](Self::name) — and a host with somewhere better to /// put it is free to ask. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Handed { /// The suggested file name, already through [`safe_file_name`]. /// /// Sanitised here rather than by each host, because the host that forgets /// is the one that writes `../../.ssh/authorized_keys` beside the process. pub name: String, /// What kind of file it is. pub kind: Accepted, /// The file. pub bytes: Vec, } /// How the layer on top is drawn over the one under it. /// /// The two outcomes that layer a screen want different boxes, and this is the /// whole of the difference between them here: everything else -- the stack, /// the dedupe, the dismissal, the keys -- is one code path. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Laid { /// Centred and inset on all four sides. `Outcome::Over`: a palette, a help /// overlay, an app-modal dialog, all of which are about the app rather than /// about anything on the screen. Over, /// A compact box in the half of the screen the subject is not in. /// `Outcome::Anchored`. /// /// # Why a half and not a position /// /// This renderer has no coordinates to anchor to. `makeover_tui::table` /// decides its own column widths and answers none back, and /// `crate::focus`' header says why per-cell rects are not being added: Max /// ruled against them. So a terminal cannot draw a menu *at* the control /// the way egui can, and inventing a cell to point at would be this /// renderer guessing. /// /// What it can promise is that the menu does not cover what it was opened /// on. The reach order is the one ordering this renderer does have, so a /// subject in the first half of it puts the menu low and one in the second /// half puts it high. That is proximity said in the only currency a /// terminal has, and it is honest about being a half rather than a point. Anchored { /// Whether the menu sits in the lower half. low: bool, }, } /// A screen and what the user has done to it. /// /// The pair is the unit an overlay needs: an overlay has its own reach, its own /// focus, its own edits and its own scroll, so it holds a [`View`] of its own /// rather than borrowing the one underneath. Sharing it is the bug the overlay /// tests exist to catch — dismissing a palette would take the user's typing and /// scroll position with it. #[derive(Debug, Clone)] struct Layer { screen: Screen, view: View, /// The request that opened the overlay this layer was displaced by. /// /// Saved on the way down and restored on the way up, so a nested overlay /// dismissing back to an outer one restores the *outer* one's identity /// rather than losing it. See [`Runtime::over`]. over: Option, /// How the layer this one was displaced by was drawn. Saved and restored /// with `over`, and for its reason. See [`Runtime::laid`]. laid: Option, } /// A screen, what the user has done to it, and how they got here. #[derive(Debug, Clone)] pub struct Runtime { screen: Screen, view: View, /// What the app offers from every screen. Matched before this runtime's own /// key table, so a screen cannot capture the key that opens the palette. chrome: Chrome, /// The layers this one is drawn over, outermost first. /// /// Empty on an ordinary screen. `screen` and `view` above are always the /// ACTIVE layer, so every key, every gather and every draw works on the /// overlay once one is open, with no second code path. /// /// Separate from `history` on purpose: an overlay is not a place. Opening /// one pushes here and leaves history alone, and dismissing one pops here /// and reveals the screen the user never left. under: Vec, /// The places behind this one, most recent last. /// /// Requests rather than addresses, because going back means asking again /// and a request is what asking takes. [`Address`](quasi_router::Address) /// carries a string for a browser's address bar, which a terminal does not /// have. history: Vec, /// The request that produced the screen currently showing. here: Option, /// The request that produced the overlay currently on top, if one is. /// /// Five presses of a help key were five Escapes. /// /// Nothing in the description had to grow for it. The runtime is handed the /// request it fired, so the identity the dedupe needs is already in hand -- /// this is `here`'s shape for a layer that is not a place, which is why /// `remember` is not called for one. /// /// A webview never had the bug: `Outcome::Over` lands in one overlay /// container and replaces what is in it. Two of three hosts stacking was /// one description behaving two ways, which is the drift this stack exists /// to end. over: Option, /// Where the layer on top is drawn, when one is. /// /// An `Outcome::Over` is a palette and takes the inset box in the middle; /// an `Outcome::Anchored` is a menu and takes a compact box out of the /// subject's way. Kept beside `over` rather than on `Layer`, because it is /// a fact about the layer on top and `Layer` holds the ones underneath. /// /// `None` whenever `over` is, and the two move together. pub(crate) laid: Option, /// A control waiting on its own question being answered. asked: Option<(Action, Params)>, /// The awaiting call this runtime has dispatched and not yet been answered /// about, with the request that went out. /// /// Only an action carrying [`Action::awaiting`] lands here: the /// description is what says which calls are worth locking a control for, /// so this renderer does not decide that a route is slow. /// /// The pair rather than the action alone, because the refusal is against the /// request that is outstanding: pressing the same control again is the /// double submit, and pressing a different one is a different call this has /// no business refusing. outstanding: Option<(Action, Request)>, /// Something to say once the screen it belongs to has arrived. saying: Option, /// What this mount puts around whatever screen is showing. /// /// Held beside the screen rather than arriving with one, which is what /// makes it the mount's: it survives every answer that replaces the screen /// inside it, the way `chrome` above survives every navigation. The /// difference between the two is lifetime — chrome is the app's and this /// is one place the app puts a screen up. frame: Frame, /// When this last handed out [`Runtime::refreshes`], so the cadence is kept /// here rather than by every host that draws a live screen. /// /// `None` until the first call, which makes the first ask immediate: a /// region that waited out a whole period before its first answer would be a /// slower screen than the one liveness replaces. refreshed: Option, /// When each toast on the screen was raised, in the order the toasts sit in /// `screen.notices`. /// /// The description says a toast goes away on its own and never says when, /// so the when is kept here: one instant per transient notice, and /// [`expires_at`](Runtime::expires_at) takes away the ones whose time is /// up. Banners have no entry, because nothing about a banner is on a /// clock. /// /// Positional rather than keyed, because a notice has no identity to key /// on and does not need one: a toast joins the screen at the end of the /// list ([`announce`](Runtime::announce)) or arrives inside a whole screen, /// and both are handled where they happen rather than guessed at here. raised: Vec, /// A file a route answered with and the host has not taken yet. /// /// Drained by [`handed`](Runtime::handed) rather than returned from /// [`apply`](Runtime::apply), because a file and a follow-up request are /// not alternatives: an answer is one or the other, and widening `apply`'s /// return type to say so would change every host for the case that cannot /// happen. /// /// One at a time, and a second overwrites the first. Two files from one /// answer is not a thing the vocabulary can say. handed: Option, /// A place a route asked for and the host has not gone looking for yet. /// /// [`handed`](Self::handed)'s twin, drained by /// [`locating`](Runtime::locating) and here for the same reason: this crate /// turns keys into requests and owns no I/O, and asking a person where to /// write is I/O of the most blocking kind. /// /// One at a time, and a second replaces the first. A picker is modal /// wherever there is one, and on a terminal there is nothing to open at /// all — see [`locating`](Runtime::locating) for what this host does with /// one instead. locating: Option, } impl Runtime { /// Start on this screen, with nothing typed and nothing behind it. #[must_use] pub fn new(screen: Screen) -> Self { let mut runtime = Self { screen, view: View::new(), chrome: Chrome::new(), under: Vec::new(), laid: None, history: Vec::new(), here: None, over: None, asked: None, outstanding: None, saying: None, frame: Frame::new(), refreshed: None, raised: Vec::new(), handed: None, locating: None, }; runtime.view.seed(&runtime.screen); runtime.open_caret(); runtime.reraise(std::time::Instant::now()); runtime } /// Declare what the app offers from every screen. /// /// Held beside the screen rather than arriving with one, which is what /// makes it chrome: the bindings outlive every answer this runtime applies. #[must_use] pub fn with_chrome(mut self, chrome: Chrome) -> Self { self.chrome = chrome; self } /// Declare what this mount puts around the screen. /// /// A mount with two ways of showing one screen builds two runtimes /// carrying two frames, which is what it already does for everything else /// it holds across frames. The screen inside them says nothing about /// either. #[must_use] pub fn with_frame(mut self, frame: Frame) -> Self { self.frame = frame; self } /// What the app offers from every screen. /// /// The panel is what a caller reads off this: an answer aimed at it lands /// here rather than on the screen, so this is where its current contents /// are. #[must_use] pub const fn chrome(&self) -> &Chrome { &self.chrome } /// What this mount puts around the screen. #[must_use] pub const fn frame(&self) -> &Frame { &self.frame } /// Whether an overlay is open over the screen. #[must_use] pub const fn overlaid(&self) -> bool { !self.under.is_empty() } /// The screen being shown. /// /// The overlay's, when one is open. That is what "being shown" means, and /// it is what every key this runtime handles is working on. #[must_use] pub const fn screen(&self) -> &Screen { &self.screen } /// What the user has done to it. #[must_use] pub const fn view(&self) -> &View { &self.view } /// The request that produced the screen showing now. /// /// `None` before the first navigation: a runtime is built from a screen /// rather than from an address, so the opening screen has no request behind /// it until the host performs one. #[must_use] pub const fn here(&self) -> Option<&Request> { self.here.as_ref() } /// Ask for this screen again. /// /// **What a described screen has no other way to say: the thing it is about /// changed, and nothing the user did to this screen changed it.** A route /// answers a screen built from the state at the moment it was asked, and /// that answer is kept until something fires. So a host whose state moves /// underneath a screen — a background job reporting progress, a write the /// host applies after the frame — has a screen describing a past it can /// neither notice nor correct. /// /// This is the host saying so. It is deliberately not a description member: /// nothing in [`Screen`] claims a refresh rate, because how often a fact /// goes stale is a property of the app holding it rather than of the screen /// showing it. /// /// [`Step::Idle`] when there is nothing to ask for, which is the opening /// screen before any navigation. History is untouched: asking for the screen /// you are on again is not going anywhere. #[must_use] pub fn reload(&self) -> Step { match &self.here { Some(request) => Step::Call(request.clone()), None => Step::Idle, } } /// The calls this screen's regions are waiting on, for the host to perform. /// /// A webview gets this for free: every region fed by a call carries a /// trigger and the browser asks as soon as the element exists. A terminal /// has nobody to do that, so the host asks for these after putting a screen /// up, hands each answer back to [`apply`](Self::apply), and the region /// fills where its stand-in was. /// /// Ask again after applying an answer rather than keeping the list: a /// fragment landing clears the region's feed, so what comes back is what is /// still outstanding. #[must_use] pub fn feeds(&self) -> Vec { self.screen .feeds() .into_iter() .filter_map(Self::request_for) .collect() } /// The calls this screen's live regions re-ask, when it is time to ask. /// /// [`feeds`](Self::feeds)' counterpart and never overlapping it: a feed /// arrives once and a refresh never stops. Empty until /// [`CADENCE`](crate::CADENCE) has passed since the last time this handed /// anything back, so a host may call it as often as it likes and the rate /// stays this crate's. /// /// The pacing is here rather than in each host for the reason the number /// is: a host that timed its own polling would be a host the other /// renderers disagree with, and every app would rebuild the same timer. /// /// # Two shapes of answer /// /// A live region naming a call comes back as that call, and its answer is a /// fragment for that region. A live region naming none comes back as the /// screen's own address, because re-reading state the host already holds /// means building the description again — and that is a whole screen, not a /// fragment. Both are requests, and a host performs them the same way. #[must_use] pub fn refreshes(&mut self) -> Vec { self.refreshes_at(std::time::Instant::now()) } /// [`refreshes`](Self::refreshes) against a clock the caller holds. /// /// The seam a test needs, and the one an event loop that already knows what /// time it is should reach for rather than asking again. #[must_use] pub fn refreshes_at(&mut self, now: std::time::Instant) -> Vec { let due = self .refreshed .is_none_or(|last| now.duration_since(last) >= crate::CADENCE); if !due { return Vec::new(); } let mut out: Vec = self .screen .refreshes() .into_iter() .filter_map(Self::request_for) .collect(); // A live region that names no call is re-read by asking the screen's own // address again, which is what re-reading means for a host that retains // a description rather than a document. The audiofiles sync panel is // that case: its state is the app's own and moves when an OAuth callback // lands in another process, so there is no fragment to fetch and the // whole screen is rebuilt from what is true now. // // Only when nothing else answered. A screen with a live region that does // name a call has already been given the narrower ask, and adding the // address to it would rebuild the screen the fragment was about to land // in. if out.is_empty() && self.screen.is_live() && let Some(here) = &self.here { out.push(here.clone()); } // Stamped even when the screen has nothing live, so a still screen is // not re-walked on every frame an egui host draws. self.refreshed = Some(now); out } /// Whether anything on this screen changes without the user. /// /// True for a live region whether or not it names a call, which is the /// difference from [`refreshes`](Self::refreshes): a region reading state /// the host already holds has nothing to ask for and still has to be /// redrawn. #[must_use] pub fn is_live(&self) -> bool { self.screen.is_live() } /// How long a host may wait before this screen has to be drawn again for /// its own sake, if it holds anything that goes stale on its own. /// /// A terminal redraws on an event, and a readout derived from the current /// time is the case with no event to redraw on. What comes back is the /// finest cadence the screen's readouts demand, so a host blocking on /// input gives this as its timeout and every readout on the screen moves /// on one tick. /// /// A toast counts too, and its answer is what is left of its /// [`LINGER`](crate::LINGER) rather than a cadence: it is drawn again once, /// to not be drawn at all. /// /// `None` for a screen holding none, which is nearly all of them, and the /// host may then block for as long as it likes. Independent of /// [`refreshes`](Self::refreshes): that one asks something over a network /// on [`CADENCE`](crate::CADENCE), and this is arithmetic the terminal does /// itself. A screen with both takes the shorter answer for its timeout and /// the two stay separate facts. #[must_use] pub fn tick_in(&self) -> Option { self.tick_in_at(std::time::Instant::now()) } /// [`tick_in`](Self::tick_in) against a clock the caller holds. /// /// The seam a test needs, and the one an event loop that already knows what /// time it is should reach for rather than asking again. #[must_use] pub fn tick_in_at(&self, now: std::time::Instant) -> Option { let clocks = self.screen.clocks().into_iter().map(crate::cadence).min(); let toasts = self .raised .iter() .map(|at| crate::LINGER.saturating_sub(now.duration_since(*at))) .min(); clocks.into_iter().chain(toasts).min() } /// Take away every toast whose time is up, and say whether one went. /// /// `Notice::Toast` says the message goes away on its own, and this is the /// terminal keeping that promise: a browser has a timer per element and a /// terminal has an event loop, so the loop asks this once a turn the way /// it already asks [`refreshes`](Self::refreshes), and /// [`tick_in`](Self::tick_in) is what wakes a loop that is only blocking on /// a key. /// /// A banner is never touched. It goes when the condition it reports is /// fixed, which is a route's business and not a clock's. pub fn expires(&mut self) -> bool { self.expires_at(std::time::Instant::now()) } /// [`expires`](Self::expires) against a clock the caller holds. pub fn expires_at(&mut self, now: std::time::Instant) -> bool { if self.raised.is_empty() { return false; } let raised = std::mem::take(&mut self.raised); let mut ages = raised.into_iter(); let mut kept = Vec::new(); let before = self.screen.notices.len(); self.screen.notices.retain(|node| { let Node::Notice { kind, .. } = node else { return true; }; // `eea7ba88`. One call rather than a transience check and a // constant: `None` is a notice with no lifetime, which is a banner, // and it is kept for the same reason the check used to keep it. let Some(lifetime) = makeover_timing::notice_lifetime(kind.transient()) else { return true; }; // A toast with no instant beside it is one this runtime never saw // raised, which nothing in the crate produces. It is given now // rather than dropped: an unexplained toast on the screen is a // smaller wrong than a message the user never got to read. let at = ages.next().unwrap_or(now); let up = now.duration_since(at) >= lifetime; if !up { kept.push(at); } !up }); self.raised = kept; self.screen.notices.len() != before } /// Start every toast on the screen lingering from now. /// /// A whole screen arriving brings whatever notices it was described with, /// and a screen coming back out from under an overlay is in front of the /// user again. Both are the moment the reading starts, so both reset the /// clock rather than trying to remember one from before. fn reraise(&mut self, now: std::time::Instant) { let toasts = self .screen .notices .iter() .filter(|node| matches!(node, Node::Notice { kind, .. } if kind.transient())) .count(); self.raised = vec![now; toasts]; } /// What the control that was pressed is waiting on, if one is. /// /// The amount rides on it when the description measured one, which is what /// a host drawing its own progress reads. Nothing here turns it into a time: /// a bar shows what is done over what there is and how long it has taken, /// and predicts nothing. #[must_use] pub fn awaiting(&self) -> Option { self.outstanding .as_ref() .and_then(|(action, _)| action.awaiting) } /// Everything reachable on it, in focus order. /// /// The regions that do not apply right now are left out, which is what the /// drawing does with them: the caret stops on what is on the screen. #[must_use] pub fn reaches(&self) -> Vec { let hidden = crate::reveal::hidden(&self.screen, &self.chrome, &self.view); crate::focus::reaches_chromed( &self.screen, &self.frame, &self.chrome, &crate::Local::of(&hidden, &self.view), ) } /// Put the caret where the screen said it starts. /// /// Arrival behaviour and nothing else, which is the whole of what /// [`Screen::opens_at`] is: a screen cannot pull the caret back on a /// redraw, so this runs where [`View::seed`] runs and nowhere else. A /// screen that names nothing leaves the caret at the first stop, which is /// where a new screen has always started. /// /// A name no field on the screen carries moves nothing. Not an error, for /// the reason the member states: the screen is the app's and so is the /// name. fn open_caret(&mut self) { let Some(name) = self.screen.opens_at.clone() else { return; }; let reaches = self.reaches(); let Some(at) = reaches .iter() .position(|reach| reach.spot.field().is_some_and(|field| field.name == name)) else { return; }; self.view.focus_on(at, reaches.len()); } /// Everything the screen describes, whether or not it is out right now. /// /// What a submit reads.: a region that does not apply is left off the /// screen here and its boxes still hold what was typed into them, exactly /// as a browser keeps the value of a hidden input and sends it. The caret /// walk and this one differ only in that, and each is used where it is the /// honest answer. fn described(&self) -> Vec { crate::focus::reaches_chromed( &self.screen, &self.frame, &self.chrome, &crate::Local::of(&crate::Hidden::none(), &self.view), ) } /// Whether the caret is in a field, which is what decides whether a /// printable key is a shortcut or a character. /// /// A host wanting `q` to quit asks this first. Quitting is the host's and /// not a binding here, because a key that closes the app is a fact about /// the app rather than about the screen. #[must_use] pub fn editing(&self) -> bool { self.focused().is_some_and(|spot| spot.field().is_some()) } /// Whether a question is waiting to be answered. #[must_use] pub const fn asking(&self) -> bool { self.asked.is_some() } /// Draw it. pub fn draw(&self, tui: &Tui, area: Rect, buf: &mut Buffer) { // What is under it first, outermost first, then this one over the top. // An overlay that painted only itself would be a screen swap wearing // another name. for layer in &self.under { tui.screen(&layer.screen, &layer.view, area, buf); } let area = if self.under.is_empty() { area } else { // `ae8e8836`. A palette takes the middle and a menu takes a corner // out of the subject's way. `Laid::Over` is the fallback for a // stack that predates the member as well as for an anchor that // named nothing, which is the same box this always drew. let inset = match self.laid { Some(Laid::Anchored { low }) => Self::anchored_area(area, low), Some(Laid::Over) | None => Self::overlay_area(area), }; // Clear what is under it inside its own bounds, so the overlay // reads as being over the screen rather than mixed into it. for y in inset.top()..inset.bottom() { for x in inset.left()..inset.right() { buf[(x, y)].reset(); } } inset }; tui.chromed( &self.screen, &self.frame, &self.chrome, &self.view, area, buf, ); } /// Where an overlay sits inside the screen it is over. /// /// Inset on all four sides so the screen underneath stays visible around /// it, which is the whole visual claim an overlay makes. Proportional /// rather than fixed: a palette 4 rows from the edge of an 80x24 terminal /// is a different thing from one 4 rows from the edge of a 200x60. fn overlay_area(area: Rect) -> Rect { let pad_x = (area.width / 8) .max(1) .min(area.width.saturating_sub(2) / 2); let pad_y = (area.height / 8) .max(1) .min(area.height.saturating_sub(2) / 2); Rect { x: area.x + pad_x, y: area.y + pad_y, width: area.width.saturating_sub(pad_x * 2), height: area.height.saturating_sub(pad_y * 2), } } /// Put a screen over the one showing, keeping that one whole underneath. /// /// The shared half of `Outcome::Over` and `Outcome::Anchored`, which differ /// only in the box they take. Everything else about a layer -- the stack, /// the dedupe, the seeding, the dismissal -- is one path, and the two /// outcomes drifting apart here is what would make one description behave /// two ways. /// /// # The dedupe /// /// A binding that opens an overlay is asked at the top of every frame and /// an open overlay does not suppress it, so this pushed a second copy of /// the same help over the first and it took an Escape per press to get /// back. The guard is the request rather than the screen, because every /// press is a fresh route call and the two screen values are equal by /// accident rather than by identity. /// /// The top layer only. A screen raised from within another one is a /// different request and still stacks, which is what a confirm over a /// palette is. fn layer(&mut self, request: &Request, screen: Screen, laid: Laid) { if self.over.as_ref() == Some(request) { return; } let under = Layer { screen: std::mem::replace(&mut self.screen, screen), view: std::mem::replace(&mut self.view, View::new()), over: self.over.replace(request.clone()), laid: self.laid.replace(laid), }; self.under.push(under); self.view.seed(&self.screen); self.open_caret(); self.reraise(std::time::Instant::now()); self.announce(); } /// Where an anchored menu sits inside the screen it is over. /// /// A compact box against one edge rather than the centred inset an overlay /// takes, because a menu is a short list of verbs and a box the size of a /// palette around six of them reads as a screen. Half the width and half /// the height, in the half `low` names, so the subject stays visible in the /// other one. See [`Laid::Anchored`] for why a half is the honest unit. fn anchored_area(area: Rect, low: bool) -> Rect { let width = (area.width / 2).max(1); let height = (area.height / 2).max(1); // Against the left edge, inset by one, so a menu and the screen under // it never share a column of glyphs. The vertical half is the half the // subject is not in. let x = area.x + u16::from(area.width > 2); let y = if low { area.y + area.height.saturating_sub(height) } else { area.y }; Rect { x, y, width: width.min(area.width.saturating_sub(x - area.x)), height, } } /// Whether the thing the user is on sits in the first half of the reach /// order, which is what puts an anchored menu low. /// /// The reach order is the only ordering this renderer has; see /// [`Laid::Anchored`]. A screen with nothing reachable answers `false` and /// the menu goes high, which is where a menu with no subject to avoid may /// as well be. fn subject_is_high(&self) -> bool { let reach = self.reaches().len(); let at = self.view.focus(); reach > 0 && at < reach && at * 2 < reach } /// Put a message on the screen, from the host rather than from a route. /// /// The host has things to say that no handler knows about: a route that /// failed, an address it will not open, a device that is not there. Without /// this they would go to stderr, which on a terminal app is underneath the /// alternate screen and therefore nowhere. pub fn say(&mut self, text: impl Into) { self.screen.notices.push(Node::Notice { kind: layout::Notice::Banner, tone: layout::Tone::Danger, text: text.into(), // Nothing to do about it. What the host says here is a report -- // a route that failed, an address it will not open -- and there is // no route it could offer that would undo any of them. act: None, }); } /// What is under the caret. #[must_use] pub fn focused(&self) -> Option { let mut reaches = self.reaches(); if self.view.focus() >= reaches.len() { return None; } Some(reaches.swap_remove(self.view.focus()).spot) } /// Take a key, and say what the host should do about it. pub fn key(&mut self, key: Key) -> Step { // A question owns the keyboard until it is answered. Anything that is // not yes is no, which is the safe way round for a prompt that is only // ever raised by something destructive. if let Some((action, payload)) = self.asked.take() { return match key { // The selection was gathered when the question was raised, not // now. Nothing can tick while a prompt owns the keyboard, so // the two are the same set -- and reading it here would mean // the answer depended on state the user could not see. Key::Char('y' | 'Y') | Key::Enter => self.send(&action, payload), _ => Step::Idle, }; } // The app's own keys, before this runtime's table and before any // screen's `Act::key`. An affordance available everywhere is not // available everywhere if a screen can capture its key. // // Not while typing: a field has the keyboard, and a binding on a // printable key would otherwise be unreachable as a character. A // binding naming a key no field can consume still lands. if !self.editing() || !matches!(key, Key::Char(_)) { let pressed = Self::key_name(key); if let Some(binding) = pressed.as_deref().and_then(|name| self.chrome.bound(name)) { return self.call(&binding.action.clone()); } } let reaches = self.reaches(); let count = reaches.len(); let here = reaches .get(self.view.focus()) .map(|reach| reach.spot.clone()); // What the caret is actually on, which is the row until it has stepped // into one. Every arm below that acts on the caret takes this, so // pressing Enter inside a row fires the control rather than opening the // row; the arms that act on the row itself take `here` and say so. let caret = match (&here, self.view.inside()) { (Some(Spot::Row { inside, .. }), Some(at)) => { inside.get(at).cloned().or_else(|| here.clone()) } _ => here.clone(), }; // A field with an open suggestion list owns four keys while it is // open, and gives them all back the moment it closes. Arrows move the // highlight rather than the focus, Enter takes the highlighted // candidate, and Escape puts the list away before it means anything // else -- which is the innermost-thing-first rule Escape already // follows below, one level further in. // // Enter with nothing highlighted is deliberately not captured: a list // arrives with no highlight, so a form stays submittable from a box the // user has typed into and paused in. if let Some(field) = caret.as_ref().and_then(Spot::field) && self.view.suggesting(&field.name).is_some() { match key { Key::Down => { self.view.highlight(1); return Step::Idle; } Key::Up => { self.view.highlight(-1); return Step::Idle; } Key::Escape => { self.view.unsuggest(); return Step::Idle; } Key::Enter => { if let Some(candidate) = self.view.pick() { // A candidate that says what picking it does has that // performed, and nothing is written into the box. // `ed1fa86f`: local is the default, not the definition. // // Performed exactly as a control's action is, with no // payload rule invented for this one place -- an action // already carries its own params and the view it was // offered under. MNW's search box is the case that // decides it: its candidates are `Action::get(url)` to // a project, item or creator page, and appending the // typed value to that address would be a description // saying something the route never asked for. if let Some(action) = candidate.picks { return self.send(&action, Params::new()); } let value = candidate.value; let name = field.name.clone(); self.view.set(&name, value.clone()); // What a pick costs is what a settled value costs: the // webview sends `change` for the same reason, and a // field that writes when it changes has just changed. // What it does not do is re-ask the questions a field // asks *while it is being typed into*, which is why // `after_typing` is not called here. if let Some(action) = field.writes.clone() { let mut payload = Params::new(); payload.insert(name, value); return self.send(&action, payload); } return Step::Idle; } } _ => {} } } match key { Key::Tab | Key::Down => { let wrote = self.leaving(here.as_ref()); self.view.advance(1, count); wrote.unwrap_or(Step::Idle) } Key::BackTab | Key::Up => { let wrote = self.leaving(here.as_ref()); self.view.advance(-1, count); wrote.unwrap_or(Step::Idle) } Key::PageDown | Key::PageUp => { // The region the caret is in, because it is the one the user is // working in. A screen with focus nowhere scrolls nothing, // which is honest: there is no "the pane" on a screen with // several. if let Some(reach) = reaches.get(self.view.focus()) { let rows = if matches!(key, Key::PageDown) { 10 } else { -10 }; self.view.scroll_by(&reach.region, rows); } Step::Idle } Key::Left | Key::Right => { // A branch under the caret first. `ccaa7e4b`: these are the // keys every tree in a terminal already answers, and a reader // who has walked onto a folder presses them expecting it to // open. Right opens and Left shuts, so a press that changes // nothing is one that says the branch was already that way. // // Ahead of the two readings below because a row that is a // branch is a tree first: stepping into its cells is what Tab // and Enter are for, and a carousel elsewhere on the screen is // not what the caret is on. if let Some(Spot::Row { branch: Some(branch), open, .. }) = &here && *open != matches!(key, Key::Right) { let branch = branch.clone(); self.view.fold(&branch, *open); // The rows under it arrived or went away, so the stops // moved under the caret. Held to what is reachable now, for // the reason `prune` clamps it. let stops = self.reaches().len(); self.view .focus_on(self.view.focus().min(stops.saturating_sub(1)), stops); return Step::Idle; } // The table row under the caret next, when it has controls in // it. `27f2331e`: the row is one stop and these are the keys // that get inside it. Ahead of the region below because the // thing under the caret beats a search of the whole screen -- // see this module's header for what that costs. if let Some(Spot::Row { inside, .. }) = &here && !inside.is_empty() { let steps = if matches!(key, Key::Right) { 1 } else { -1 }; self.view.step_inside(steps, inside.len()); return Step::Idle; } // The region the caret is in, when that region shows one child // at a time, and otherwise the first one on the screen that // does. The fallback is not a convenience: a carousel's frames // are pictures, so there is nothing reachable inside one and // focus can never be in it. Without this the one widget that // asked for these keys could not be reached by them. if let Some(slot) = self.moving(&reaches) { let steps = if matches!(key, Key::Right) { 1 } else { -1 }; let slot = slot.clone(); self.view.show_by(&slot, steps); // `dfbc88ce`. A tab's panel is a route, and the strip is // what calls it: the browser puts the address on the button // and htmx fires on the press, and this is the same press. // Only when the panel is empty, so pressing back to a tab // already read is free, which is what the reader means by // going back to it. if let Some(action) = self .view .shown(&slot) .and_then(|at| slot.body.get(at)) .and_then(|placed| match &placed.node { Node::Region(panel) => panel.asked_for(), _ => None, }) && let Some(request) = Self::request_for(action) { return Step::Call(request); } } Step::Idle } // The innermost thing first, all the way out: Escape steps out of // a table row, then closes what is on top, then goes back. Which is // what Escape means everywhere else it is bound. The short circuit // is what makes it one at a time: a press that left a row does not // also dismiss the overlay the row is on. Key::Escape => { if self.view.leave() || self.dismiss() { Step::Idle } else { self.back() } } Key::Backspace => { if let Some(field) = caret.as_ref().and_then(Spot::field) { self.view.backspace(field); // A delete asks nothing -- see `after_typing`, which a // delete does not reach -- and it can still take the // candidates away: they are about a value that has just // stopped earning them. self.unearned(field); } Step::Idle } Key::Enter => match caret { Some(Spot::Act { action, confirm, over, asks, fills, copies, .. }) => { // A control over an empty selection is drawn disabled // (`commit_line`), and this is the other half of saying so: // pressing it does nothing rather than calling a route that // will act on nothing and answer "0 tasks completed". The // focus ring still stops on it, deliberately -- the reach // walk reads the description and the ticks are the view's, // so skipping it here would leave the two disagreeing about // how many stops a screen has. if over.is_some() && self.view.ticks().next().is_none() { return Step::Idle; } let mut payload = self.gathering(over.as_deref()); payload.absorb(self.asked(&asks)); self.deposit(fills.as_ref()); // Before the call, matching `deposit` above and the other // two renderers: the local half of a press happens whether // or not anything is asked, and a copying act asks nothing. if let Some(value) = copies { return Step::Copy(value); } match confirm { Some(prompt) => { self.asked = Some((action, payload)); Step::Ask(prompt) } None => self.send(&action, payload), } } Some(Spot::Submit { action, names }) => { let payload = self.view.submission( &names, // Every described box, including one in a region that // does not apply right now: a browser sends the value // of a hidden input, and one description submitted on // two hosts has to send the same form. `079a011e`. &self .described() .into_iter() .map(|reach| reach.spot) .collect::>(), ); self.send(&action, payload) } // The controls of a question answered N times. `60d1753c`: // pressing one changes how many boxes there are and asks // nothing of any route, which is the third of the three things // the member is for. The caret stays where it is, so a reader // adding three slots presses add three times without walking // back to it. Some(Spot::Repeat { field, at }) => { match at { Some(at) => self.view.remove_slot(&field, at), None => self.view.add_slot(&field), } // The stops moved under the caret: a slot arrived above the // add control, or one went away above it. Held to what is // reachable now, for the reason `prune` clamps it. let stops = self.reaches().len(); self.view .focus_on(self.view.focus().min(stops.saturating_sub(1)), stops); Step::Idle } // A field takes Enter and does nothing with it. A browser // submits the form around it, and doing that here would fire a // write from the first box the user finished typing in; the // submit is one Tab away and says what it does. Some(Spot::Field(_)) | None => Step::Idle, // A row of a live selection says how the press was meant, which // for Enter is always the plain reading: this row and nothing // else. Space below is the other one a terminal has. `1894e95d`. Some(Spot::Row { activate: Some(action), chosen: Some(_), .. }) => { let action = action.clone(); self.send(&action, meant(Choosing::Only)) } Some(other) => match other.enters() { Some(action) => self.call(&action.clone()), None => Step::Idle, }, }, // The row's own key, so it takes `here` and not `caret`: a tick // belongs to the row and there is nothing in a cell that could hold // one. A caret that has stepped into a row therefore ticks nothing, // and Escape is one press away from the row that does. Key::Char(' ') if !self.editing() => match here { // A tick is a write when the description says it is, and // staged selection when it does not. `toggle` first, because a // row carrying one has said the tick *is* the write and that // claim beats the screen's set. Some(Spot::Row { toggle: Some(action), .. }) => self.call(&action), // Otherwise it joins or leaves the set the screen names. The // hole `5f2b8753` was filed for was here: this used to be // `Step::Idle`, so the box was drawn, the key was bound, and // pressing it did nothing. // // Still idle when a row names no value or the screen holds no // set, which is the same description bug one step earlier. A // key bound to nothing is what this stopped doing, so it does // not start doing it again by accepting a tick that cannot be // read back. Some(Spot::Row { ticked: Some(_), value: Some(value), .. }) if self.screen.selection.is_some() => { self.view.tick(&value); Step::Idle } // The terminal's ctrl-click. A live selection is the app's, so // unlike a tick this does not write into the view: it calls the // row's own address and says the press meant "and also this // one", and the app answers with a screen where it is chosen. // `1894e95d`. // // After the tick arms, deliberately. A row carrying both has // said two contradictory things and the staged reading is the // older one; nothing in the tree does, and the order is what // decides it if anything ever does. Some(Spot::Row { activate: Some(action), chosen: Some(_), .. }) => self.send(&action, meant(Choosing::Also)), _ => Step::Idle, }, Key::Char(ch) => { if let Some(field) = caret.as_ref().and_then(Spot::field).cloned() { self.type_into(&field, ch); return self.after_typing(caret.as_ref()); } // Not in a field, so the key is a shortcut if any control on // the screen claimed it. `Act::key` is text rather than a // modelled chord, so this is a string comparison against what // the description wrote, and a name this renderer does not // understand simply never matches. let pressed = ch.to_string(); let claimed = reaches.iter().find_map(|reach| match &reach.spot { // A key reaching a commit control over an empty selection // is refused for the same reason a press on it is. Spot::Act { action, key: Some(key), over, asks, fills, copies, .. } if *key == pressed && (over.is_none() || self.view.ticks().next().is_some()) => { Some(( action.clone(), over.clone(), asks.clone(), fills.clone(), copies.clone(), )) } _ => None, }); match claimed { Some((action, over, asks, fills, copies)) => { let mut payload = self.gathering(over.as_deref()); payload.absorb(self.asked(&asks)); self.deposit(fills.as_ref()); // The bound key and Enter are one press said two ways, // so a copy reaches the host from either. if let Some(value) = copies { return Step::Copy(value); } self.send(&action, payload) } None => Step::Idle, } } } } /// Put fresh contents wherever this names, and say whether anywhere did. /// /// The panel is not on the screen and is addressable all the same, so an /// answer aimed at it lands in the chrome. Asked in that order rather than /// the other way round because the panel's id is the app's and a screen /// could carry a region with the same name, and the app's panel is the one /// that outlives the screen. fn land(&mut self, region: &str, node: Node) -> bool { if self.chrome.panel(region).is_some() { return self.chrome.replace(region, node); } self.screen.replace(region, node) } /// Put what the router answered onto the screen. /// /// Answers with a follow-up request when the response says to go somewhere /// else, which the host performs the same way it performed the first one. /// `request` is what was asked, because whether an answer is a place is /// derived from it: a read that answered a whole screen is somewhere you /// can come back to, and a write is not. pub fn apply(&mut self, request: &Request, response: Response) -> Option { let Response { outcome, notice, address, invalidates, } = response; self.saying = notice.or(self.saying.take()); // Whatever was outstanding has been answered. Only the request that went // out clears it: an answer to something else arriving first leaves the // control locked, which is what it means for that control to still be // waiting. if self .outstanding .as_ref() .is_some_and(|(_, sent)| sent == request) { self.outstanding = None; self.view.awaiting(None); } match outcome { // Invalidations are not applied to a whole screen, matching what an // HTTP host does with them and for the same reason: every region is // being replaced already, so naming one of them again says nothing // the new screen does not. // Over what is already there. The layer underneath is put away // whole -- its screen and the view holding everything the user did // to it -- and comes back untouched when the overlay is dismissed. // // `remember` is deliberately not called: an overlay is not a place, // so history is left exactly as it was and Escape from the overlay // reveals rather than navigates. // The candidates for the box being typed into. Not a region and // not a screen: the list belongs to a control, so it lands on the // view beside what has been typed rather than in the description. Outcome::Suggestions { field, options } => { self.view.suggested(field, options); None } Outcome::Over(screen) => { self.layer(request, screen, Laid::Over); None } // Over what is already there, at something on it. The same layering, // plus one decision: where the box goes. // // The anchor is resolved against the screen being covered rather // than against the one arriving, because that is the screen it // names -- a menu is anchored to what was showing when it opened. // `Screen::anchors` is where the walk lives, so all three renderers // answer the question the same way. // // An anchor naming nothing on that screen falls back to the overlay // box. That is a description bug and gets a description bug's // treatment everywhere else in this crate: the screen still draws, // and the loss is the placement rather than the menu. Outcome::Anchored { screen, anchor } => { let laid = if self.screen.anchors(&anchor) { Laid::Anchored { low: self.subject_is_high(), } } else { Laid::Over }; self.layer(request, screen, laid); None } // A whole screen replaces everything, including any overlay open // over it. A route that answers with a screen is a navigation, and // navigating with a palette still floating over the destination is // the state nobody asked for. Outcome::Screen(screen) => { // Arriving where you already are is a refresh rather than a // navigation, and the difference is the whole of what the user // has done to the screen. `reset` and `seed` below are both // *arrival* behaviour, so a refresh runs neither: otherwise a // reload clears the field being typed into and puts back an // untick the moment anything redraws. let refreshed = self.here.as_ref() == Some(request); self.under.clear(); self.over = None; self.laid = None; self.remember(request, address.as_ref()); self.screen = screen; self.reraise(std::time::Instant::now()); if !refreshed { self.view.reset(); // The rows a new screen says are already ticked. After this // the user's ticks are the truth, which is why it is applied // once on arrival rather than read on every draw. self.view.seed(&self.screen); // And where the caret starts, for the same reason and at the // same moment: a refresh runs neither, or a reload would // snatch the caret back out of whatever is being typed into. self.open_caret(); } self.announce(); None } Outcome::Fragment { region, node } => { // A region that is not there is the description bug // `Screen::replace` describes, and a terminal can say so // rather than swallowing it: the region it named is gone, and // drawing nothing would look like a control that does nothing. // // The slots the answer invalidated go in the same way. On a // terminal that is the whole of what invalidation means: the // next frame redraws everything, so putting the new contents // on the screen is putting them in front of the user. What a // webview needs an out-of-band swap for, this gets for free. let mut missing: Vec = Vec::new(); if !self.land(®ion, node) { missing.push(region); } for stale in invalidates { if !self.land(&stale.region, stale.node) { missing.push(stale.region); } } if !missing.is_empty() { // One message naming all of them, rather than a banner per // region where only the last would survive. let named = missing .iter() .map(|region| format!("`{region}`")) .collect::>() .join(", "); let subject = if missing.len() == 1 { "is" } else { "are" }; self.saying = Some(Message { kind: layout::Notice::Banner, tone: layout::Tone::Danger, text: format!("nothing on this screen {subject} called {named}"), undo: None, }); } self.view.prune(&self.screen, &self.frame, &self.chrome); self.announce(); None } // The work was handed off and the region is waiting on it. A // retained-screen host needs no markup for this: the slot's // readiness is the axis, `region::draw` already writes "Loading" // from it, and the next thing the region is told puts content back. // // A region that is not there is the same description bug a fragment // naming one is, and gets the same banner. Said once here rather // than folded into the arm above: that one collects misses across // the answer and its invalidations, and there is only ever one // region here. // // The message goes in as a stand-in and this renderer will not draw // it, because a pending region returns before it reaches the body. // Applied anyway rather than dropped: what is on the screen is the // description, and a host that later draws a pending body — or a // test asking what the region holds — should find what the route // said rather than what was there before. Outcome::Started { region, message } => { if !self.screen.started(®ion, message) { self.saying = Some(Message { kind: layout::Notice::Banner, tone: layout::Tone::Danger, text: format!("nothing on this screen is called `{region}`"), undo: None, }); } self.view.prune(&self.screen, &self.frame, &self.chrome); self.announce(); None } Outcome::Goto(action) => match self.call(&action) { Step::Call(request) => Some(request), // An external destination is the host's to open, and there is // nothing to come back for. _ => None, }, // A file for the host to put somewhere. Not a screen, not a // region, and not a place: what is showing stays showing, and the // answer leaves by `handed` rather than by the return value. Outcome::File { name, kind, bytes } => { self.handed = Some(Handed { name: safe_file_name(&name), kind, bytes, }); None } // A place for the host to go and find. Not a screen, not a region // and not a place in the other sense either: what is showing stays // showing, and the ask leaves by `locating`. Outcome::Locate(asking) => { self.locating = Some(asking); None } } } /// Take the file the last answer handed over, if it handed one over. /// /// Called after [`apply`](Self::apply), the way /// [`refreshes`](Self::refreshes) is called after a draw: this drains, so a /// host that calls it twice gets the file once. A host that never calls it /// silently drops every download, which is why the file arrives here rather /// than being written where the runtime cannot be tested. /// /// What to do with it is the ruling's: write it into the working directory /// under [`Handed::name`]. See [`Handed`]. pub fn handed(&mut self) -> Option { self.handed.take() } /// Take the place the last answer asked for, if it asked for one. /// /// [`handed`](Self::handed)'s twin: drained after /// [`apply`](Self::apply), once per ask, and dropped silently by a host /// that never calls it. Build the follow-up call with /// [`Locating::answered`] once the place is known and hand the answer back /// to [`apply`](Self::apply); a reader who backs out is not an answer and /// nothing goes to the router. /// /// # A terminal has no picker, and that is not a gap /// /// The other renderers open a native dialog. A terminal has none, and the /// honest reading is the one [`Handed`] already takes on this host: the /// place is the working directory unless the app has somewhere better, and /// a host that wants to ask can read [`Locating::prompt`] and ask however /// it asks — a prompt line, a picker of its own, an argument it was started /// with. What it must not do is guess and stay quiet, because the handle it /// answers with is where a long export is about to write hundreds of files. /// /// # The save shape, on a host with no save dialog /// /// [`Sought::Save`](quasi_router::Sought::Save) carries the name the /// dialog would have opened with, which is exactly what a terminal needs /// to answer without one: the working directory joined to /// [`safe_file_name`] of that name, said out loud so the reader can see /// where it went. The suggestion is frequently built from something they /// typed earlier, so it goes through the sanitiser here for the reason it /// does on a download: this host writes beside the process, and /// `../../.ssh/authorized_keys` is a name a description can hold. A host /// that would rather ask reads the name as the default in its own prompt. /// /// # One call, whatever was picked /// /// [`Locating::answered`] takes every /// [`Picked`](quasi_router::Picked) at once and builds one request, so an /// ask for several files is answered once with all of them. A host that /// calls it per file makes N requests out of one ask and turns a batched /// import into N imports. pub fn locating(&mut self) -> Option { self.locating.take() } /// Go back, if there is anywhere to go. /// A key as the text a [`Chrome`] binding names it by. /// /// The same string comparison `Act::key` gets, for the same reason: the /// vocabulary of keys is the host's, and this host's names are these. A /// modifier this renderer cannot receive is a name that never matches, /// which is what a binding for another host should do here. fn key_name(key: Key) -> Option { Some(match key { Key::Char(ch) => ch.to_string(), Key::Enter => "enter".into(), Key::Escape => "escape".into(), Key::Tab => "tab".into(), Key::BackTab => "backtab".into(), Key::Up => "up".into(), Key::Down => "down".into(), Key::PageUp => "pageup".into(), Key::PageDown => "pagedown".into(), Key::Backspace => "backspace".into(), Key::Left => "left".into(), Key::Right => "right".into(), }) } /// The region the arrow keys move, if the screen has one. /// /// The one the caret is in when that region shows one child at a time, and /// otherwise the first such region in draw order. Two rules rather than one /// because focus is not always a usable answer here: a carousel holds /// pictures, nothing in it is reachable, and a rule that only ever asked /// where the caret was would leave the widget that wanted these keys unable /// to be reached by them. /// /// A screen with two of these and no focus in either moves the first, which /// is arbitrary and is said out loud rather than hidden. Nothing in the tree /// has two yet; the screen that does is the one that will want a reachable /// control instead, and that is a `Spot` rather than a rule here. fn moving<'a>(&'a self, reaches: &[crate::focus::Reach]) -> Option<&'a Slot> { let here = reaches .get(self.view.focus()) .and_then(|reach| self.find(&reach.region)) .filter(|slot| slot.showing().selective()); here.or_else(|| self.screen.slots.iter().find_map(Self::selective)) } /// This slot or the first under it that shows one child at a time. fn selective(slot: &Slot) -> Option<&Slot> { if slot.showing().selective() { return Some(slot); } slot.body.iter().find_map(|placed| match &placed.node { Node::Region(inner) => Self::selective(inner), _ => None, }) } /// The slot under this address, anywhere on the screen. fn find(&self, region: &str) -> Option<&Slot> { self.screen.slots.iter().find_map(|slot| slot.find(region)) } /// Close the overlay on top, if there is one. /// /// The layer underneath comes back exactly as it was left: its own focus, /// its own edits, its own scroll. That is the whole reason a layer carries /// its own [`View`], and history is not touched because an overlay was /// never a place. fn dismiss(&mut self) -> bool { match self.under.pop() { Some(layer) => { self.screen = layer.screen; self.view = layer.view; // The outer overlay's identity, or `None` back on the base // screen. Restored rather than cleared, so dismissing a confirm // raised over a palette leaves the palette still refusing to // stack itself. self.over = layer.over; self.laid = layer.laid; self.reraise(std::time::Instant::now()); true } None => false, } } /// Go back, if there is anywhere to go. /// /// What [`Destination::Back`](quasi_router::Destination::Back) is answered /// with, and what this runtime's own Escape reaches once there is no /// overlay left to dismiss. `Step::Idle` at the bottom of the stack: the /// first screen is not somewhere you arrived at. fn back(&mut self) -> Step { match self.history.pop() { Some(request) => { self.here = Some(request.clone()); Step::Call(request) } None => Step::Idle, } } /// Note where we were, before we leave it. /// /// The derivation the response's own documentation describes: a read that /// answered a screen is a place, everything else is not, and /// [`Address`](quasi_router::Address) is the override for the two cases the /// derivation cannot reach. /// /// Arriving where you already are is not leaving anywhere, which is what the /// first guard is for: [`reload`](Self::reload) answers the request that /// produced the screen showing, so without it every refresh would push a /// duplicate of the current place and `back` would walk through a stack of /// the screen it is already on. fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) { if self.here.as_ref() == Some(request) { return; } let place = match address { Some(quasi_router::Address::Enters(_)) => true, Some(quasi_router::Address::Unchanged) => false, Some(quasi_router::Address::Replaces(_)) => { self.here = Some(request.clone()); return; } None => request.method == Method::Get, }; if place && let Some(previous) = self.here.replace(request.clone()) { self.history.push(previous); } } /// Put whatever the response wanted said onto the screen it belongs to. fn announce(&mut self) { let Some(message) = self.saying.take() else { return; }; // The way back the response offered, as the control it becomes here. // `bde35298`: this used to be dropped, because `Node::Notice` had // nowhere to hang a control and the comment saying so was the whole of // the answer. The member exists now. let act = message.undo_act(); let Message { kind, tone, text, .. } = message; if kind.transient() { self.raised.push(std::time::Instant::now()); } self.screen.notices.push(Node::Notice { kind, tone, text, act, }); } /// Type into a field, honouring what the description says it will take. fn type_into(&mut self, field: &crate::FieldSpot, ch: char) { if matches!(field.kind, layout::FieldKind::Checkbox) { // A checkbox holds one of two values, so a key does not type into // it: any key flips it, which is what space does to one in a // browser and is the only sentence a box with two states can hear. let ticked = self.view.typed(field) == Node::SELECTED; let next = if ticked { String::new() } else { Node::SELECTED.to_string() }; self.view.set(&field.name, next); return; } // `Field::max_length` is a rule the description carries and every // renderer emits in its host's idiom. A browser stops accepting // characters, and so does this. if let Some(limit) = field.max_length && self.view.typed(field).chars().count() >= limit as usize { return; } self.view.push(field, ch); } /// What a keystroke in a field costs: a write when the field writes as it /// changes, an ask when it consults something about what is being typed. /// /// A write beats a consult when a field carries both, because the write is /// what the keystroke *means* and the ask is about it. One keystroke /// produces one [`Step`], so the consult goes unasked in that case; no /// measured site carries both, and inventing a queue for a shape nothing /// has asked for is how this crate would grow a scheduler. /// /// The regions holding the box ask under the same rule and lose it the same /// way, which is worth stating because it is the less obvious half: a dial /// that writes on its own inside a panel that recomputes writes, and the /// panel is left to the next keystroke. The write cannot join the delayed /// set instead -- [`Step::CallAfter`] says a later one replaces an /// outstanding one whole, so a write parked in there is a write a keystroke /// can cancel. /// The write a field owes when the caret leaves it, if it owes one. /// /// `Field::writes` means the change is complete, and on a terminal a value /// built up character by character is complete when the reader walks off /// it. The same moment a browser raises `change`, which is what `quasi- /// webview` emits and what the pick site above already calls "what a /// settled value costs". /// /// `None` when there is nothing to say: not a field, no `changes`, or a /// value nobody altered. Walking through a form must not write every box it /// passes. fn leaving(&mut self, here: Option<&Spot>) -> Option { let field = here.and_then(Spot::field)?; if field.kind.offers_options() || field.kind.takes_files() || field.kind.offers_themes() { // Already written when it changed, for the reason `after_typing` // gives: one interaction, one whole answer. return None; } let action = field.writes.clone()?; let name = field.name.clone(); // Seeded from the description when the box is untouched, which is what // is showing in it. `unwrap_or_default` here read an untouched box as // empty, so walking through a form wrote every prefilled field it // passed -- caught by `walking_through_a_box_nobody_altered_writes_nothing`. let described = field.value.as_deref().unwrap_or_default(); let value = self.view.edit(&name).unwrap_or(described).to_string(); if !self.view.unwritten(&name, &value, field.value.as_deref()) { return None; } self.view.wrote(&name, &value); let mut payload = Params::new(); payload.insert(name, value); Some(self.send(&action, payload)) } fn after_typing(&mut self, here: Option<&Spot>) -> Step { let Some(field) = here.and_then(Spot::field) else { return Step::Idle; }; let name = field.name.clone(); // `8032fe61`. A keystroke is not a write. `Field::writes` means the // change is complete, which is what `quasi-webview` has always emitted // it as -- `hx-trigger="change"`, raised on blur or Enter for a text // control -- and this fired on every character, so a search box was one // request per letter and a bounded number posted every value on the way // to the one being typed. // // What is left of the old shape is exactly the controls a browser fires // `change` for at once: a value chosen in one go was complete when it // changed. Everything the reader builds up waits for `leaving`. // A theme is picked in one gesture, so it belongs with the options // and not with the values a reader builds up character by character. let built_up = !(field.kind.offers_options() || field.kind.takes_files() || field.kind.offers_themes()); if let Some(action) = field.writes.clone().filter(|_| !built_up) { let value = self.view.edit(&name).unwrap_or_default().to_string(); let mut payload = Params::new(); payload.insert(name, value); return self.send(&action, payload); } // Asking about the value carries the value and nothing else. No ticks: // a consult is a question about this box, not a control over a set. // The question the field owns is asked alongside the ones it merely // asks, and it is asked the same way: the answer differs, not the // asking. Dropping under its floor closes the list rather than leaving // candidates for a value that no longer earns them. let questions: Vec<&Consult> = field.suggests.iter().chain(field.consults.iter()).collect(); self.unearned(field); let value = self.view.edit(&name).unwrap_or_default().to_string(); let mut asks = Vec::new(); for consult in questions { // Too little to ask about yet, and asked per question rather than // for the field: the two questions about one box carry their own // floors, and MNW's do differ from each other in wait even where // the floors agree. if !consult.asks_about(&value) { continue; } // What else rides along, which is the half that made discover's // results route unsayable: it answers about the current filters, // and a question asked without them answers about a screen the // user is not looking at. let mut payload = self.contributed(&consult.sends); payload.insert(name.clone(), value.clone()); match self.send(&consult.action, payload) { Step::Call(request) => asks.push(Delayed { request, after: consult.after, }), // A consult that names somewhere outside the app is not a // question this runtime can wait on. `Self::send` already // answers for it, and delaying it would be a delay before // opening a browser. other => return other, } } // The regions this box sits in, each asking about the whole set of // dials it contains rather than about this one value. `cb62a9dc`: on a // browser the question is one element's and the values are gathered by // containment; here the containment is walked instead, and the two // hosts gather the same set because `Slot::questions` is the only walk // either of them reads. match self.region_asks(&name, &value) { Ok(region) => asks.extend(region), Err(other) => return other, } // `Idle` and not an empty delayed call: nothing is outstanding here // — the wait lives with the host, which is what keeps a clock out // of this crate — so a keystroke under every floor costs nothing // and deleting back under them asks nothing. if asks.is_empty() { return Step::Idle; } Step::CallAfter { asks } } /// What the regions holding this box ask when it moves. /// /// [`Slot::consults`](quasi_router::Slot::consults). Every region on the /// screen that both carries a question of its own and contains this field, /// at any depth, which is what makes a panel nested three groups deep /// recompute from a dial in one of them. /// /// The floor is read against the value that moved, never against the /// gathered set, which is [`Consult::asks_about`]'s own rule and the same /// thing the browser's `event.target.value` filter says. /// /// `Err` carries a step that is not a question this runtime can wait on -- /// an address outside the app -- exactly as the field's own consults hand /// one back. fn region_asks(&mut self, moved: &str, value: &str) -> Result, Step> { // Read whole before anything is sent, because sending takes the // runtime mutably and the questions are read out of the screen it // holds. What is kept is the two small things a question needs -- the // consult and the values -- rather than the regions themselves, so a // keystroke does not clone a subtree. let mut asking: Vec<(Consult, Params)> = Vec::new(); for slot in self.screen.consulting() { let inside = slot.questions(); if !inside.iter().any(|field| field.name == moved) { continue; } // The dials, read the way a submit reads a form: what the reader // typed, falling back to what the description offered, so an // untouched box still sends what it is showing. let mut held = Params::new(); for field in inside { let value = self .view .edit(&field.name) .map(str::to_owned) .or_else(|| field.value.clone()); if let Some(value) = value { held = held.with(field.name.clone(), value); } } for consult in &slot.consults { if !consult.asks_about(value) { continue; } asking.push((consult.clone(), held.clone())); } } let mut asks = Vec::new(); for (consult, held) in asking { // What rides along from outside the region goes in first, so a dial // inside it wins where a name sits on both sides: the region's own // contents are what the question is about. let mut payload = self.contributed(&consult.sends); for (name, value) in held.iter() { payload = payload.with(name.to_owned(), value.to_owned()); } match self.send(&consult.action, payload) { Step::Call(request) => asks.push(Delayed { request, after: consult.after, }), other => return Err(other), } } Ok(asks) } /// Close an open list whose value no longer clears the floor. /// /// The floor is the description's answer to "is there enough here to be /// worth asking about", and candidates left over a value that no longer /// clears it are an answer to a question the field would not ask now. fn unearned(&mut self, field: &FieldSpot) { let Some(owned) = field.suggests.as_ref() else { return; }; let value = self.view.edit(&field.name).unwrap_or_default().to_string(); if !owned.asks_about(&value) { self.view.unsuggest(); } } /// The values other controls contribute to a question, by name. /// /// [`Consult::sends`](quasi_router::Consult::sends). What the user has /// typed, falling back to what the description offered, which is the same /// order a submit reads them in — an untouched filter still sends what it /// is showing. /// /// A name nothing on the screen carries contributes nothing rather than an /// empty value, so a route can tell "not on this screen" from "on it and /// blank". That is the miss `Consult::sends` documents, seen from the one /// host where it is cheap to notice. fn contributed(&self, names: &[String]) -> Params { let mut params = Params::new(); if names.is_empty() { return params; } let spots = self.described(); for name in names { let described = spots.iter().find_map(|reach| match &reach.spot { Spot::Field(field) if &field.name == name => Some(field), _ => None, }); let Some(field) = described else { continue }; let value = self .view .edit(name) .map(str::to_owned) .or_else(|| field.value.clone()); if let Some(value) = value { params = params.with(name.clone(), value); } } params } /// The ticks a control acting over a selection sends with its call. /// /// Empty for a control that names no selection. A control that names one /// sends the whole set, whatever it called it: see [`Act::over`] for why /// the name is not matched against the screen's, which is that a webview /// rendering a fragment has no screen to match it against and the two /// hosts would then disagree about a typo. /// /// Empty is also what a control over a set nobody ticked sends, and the two /// are deliberately the same. A handler receives a bulk action over /// nothing, which is a case it has to handle regardless. /// /// [`Act::over`]: quasi_router::Act::over fn gathering(&self, over: Option<&str>) -> Params { let mut payload = Params::new(); if over.is_some() { for value in self.view.ticks() { payload.insert(Node::TICKED.to_owned(), value.to_owned()); } } payload } /// Put the act's value into the box it named. /// /// [`Act::fills`] names a field on this screen and the renderer decides /// where in it the value lands; here that is the end of what is already /// there. A terminal has no caret inside a field to insert at — `d52884b0` /// is why the view holds none — and the vocabulary calls appending correct /// rather than a fallback, because a description names a destination and /// never a position. /// /// After the payload is gathered, deliberately. A webview's htmx listener /// sits on the control and the fill script on the document, so there the /// press sends what the boxes held before it; doing it in the other order /// here would make one description send two different things on two hosts. /// /// From what the box is showing rather than from nothing: what has been /// typed if anything has, and otherwise what the description offered. A /// deposit that started from an empty string would silently discard the /// draft it was meant to add to, which is the exact defect the member was /// filed to stop the server-side workaround causing. /// /// A name no field on the screen carries writes an edit nothing reads, /// which is what a webview does with the same description: nothing is /// deposited and the act's own action still runs. A description naming a /// box that is not there is a description bug rather than a renderer's. /// /// [`Act::fills`]: quasi_router::Act::fills fn deposit(&mut self, fill: Option<&quasi_router::Prefill>) { let Some(fill) = fill else { return; }; let described = self.described(); let offered = described.iter().find_map(|reach| match &reach.spot { Spot::Field(field) if field.name == fill.field => field.value.as_deref(), _ => None, }); let mut value = self.view.showing(&fill.field, offered).to_owned(); value.push_str(&fill.value); self.view.set(&fill.field, value); } /// The values a control asked for before it fired, by name. /// /// [`Act::asks`]. Read exactly as a submit reads a form's boxes, through /// the same view: an untouched box sends what the description offered, and /// an unticked checkbox sends nothing. A control that asked for nothing /// sends nothing, which is nearly all of them. /// /// [`Act::asks`]: quasi_router::Act::asks fn asked(&self, names: &[String]) -> Params { if names.is_empty() { return Params::new(); } let spots: Vec = self .described() .into_iter() .map(|reach| reach.spot) .collect(); self.view.submission(names, &spots) } /// An action as something the host can ask. fn call(&mut self, action: &Action) -> Step { self.send(action, Params::new()) } /// The request an action makes, or nothing when it names somewhere outside /// the app. /// /// Carries the action's own payload and the view it was offered under. What /// a control gathers on top of that is `send`'s, because only a press /// happens next to a selection. fn request_for(action: &Action) -> Option { let path = action.destination.route()?; Some(Request { method: action.method, path: path.to_string(), captures: Params::new(), payload: action.params.clone(), carried: action.carried.clone(), }) } /// An action, plus values the control is sending that are not on it. fn send(&mut self, action: &Action, extra: Params) -> Step { // Nothing to ask and nowhere to send anyone. This renderer is // `Renderer::Client`: it holds what it draws and redraws it from // memory, so whatever the local action names is something it already // does natively, and the mark tells it nothing it did not know. // // Handled rather than left to the guard below, which reads `route()` as // "not a route, therefore somewhere outside" and would hand the host an // empty address to open. `210574ca`. if action.destination.is_local() { return Step::Idle; } // Wherever the reader came from, which is this runtime's history and // not anything the description could have named. `33c27e81`. Before the // guard below for `Local`'s reason: `route()` is `None` here too, and // the guard would read that as "outside the app" and hand the host an // empty address to open. // // `back` dismisses nothing. An overlay is not a place -- `dismiss` says // so and keeps its own stack -- so a host that wants one key to do both // tries `dismiss` first, which is what this runtime's own Escape does. if action.destination.is_back() { return self.back(); } if action.destination.route().is_none() { return Step::Open(action.destination.as_str().to_string()); } // `a81384d4`. The description says this call is the host's, and this // host does not know the sequence behind it -- the case it was ruled on // is an upload, three requests to a browser and no file picker here to // start one. Calling the address anyway would ask a signing endpoint for // a screen and swap the JSON it answered with. // // Said rather than swallowed, the same as a fragment naming a region // that is not there: a control that is drawn, is reachable, and does // nothing when pressed is the hardest kind of bug to see from what is on // the screen. if action.by_host { self.saying = Some(Message { kind: layout::Notice::Banner, tone: layout::Tone::Danger, text: "this app cannot do that here".to_string(), undo: None, }); // Announced here rather than left for the next navigation, because // no navigation is coming: this key produced no request, so nothing // downstream would ever move the message onto the screen. self.announce(); return Step::Idle; } // The whole screen is being replaced, so this is an arrival rather than // a swap: whatever is open over the screen is put away before the call // leaves, and what comes back stands where the screen stood. That is // what "pushes a screen" means on a terminal, and it is the same // sentence the webview says by emitting the anchor and no verb. // `00ee7af5`, ruled 2026-08-25. // // Before the request is built, and not on the answer: `Outcome::Screen` // clears the overlay stack already, and a navigating call that answers // with anything else would otherwise land under a palette still // floating over the place it left. if action.navigates { self.under.clear(); self.over = None; self.laid = None; } // `Action::elsewhere` gets no branch here, and that is the answer // rather than the gap. A mount of its own in a terminal would be a // split or a tab, which is this renderer's furniture and not the // description's: a screen asking for one would be asking for a layout. // So the call falls through and is performed where it stands, which is // the degradation the member's docs promise. goingson `3fb2526a`. let mut payload = extra; payload.absorb(action.params.clone()); let Some(request) = Self::request_for(action).map(|request| Request { payload, ..request }) else { return Step::Idle; }; // An awaiting call locks the control that made it until the answer // arrives. A terminal has no browser to do this for it, and the second // press is the one that buys the same thing twice. // // The refusal is against the outstanding request rather than against // being busy at all: the rest of the screen keeps working, which is what // "this control is doing something" means as opposed to "the app is". if action.awaits() { if self .outstanding .as_ref() .is_some_and(|(_, sent)| *sent == request) { return Step::Idle; } self.outstanding = Some((action.clone(), request.clone())); self.view.awaiting(Some(action.clone())); } Step::Call(request) } }