lernie 0.1.56

lernie: the operator seat — the window and wire client for a yog server
Documentation
//! **The window's one list** (DESIGN §4.39): the engines this seat reaches as
//! an accordion, the open one's workspaces under it, and the AIMED workspace's
//! conversations under their own wall's row.
//!
//! It is one list and one scroll region because the middle column is gone:
//! what `crate::ui::convs` painted as a pane is a level of this one, a tree
//! under one heading rather than two panes disagreeing about what is selected.
//!
//! The grouping is the point. A seat holds one channel per workspace it
//! participates in elsewhere plus this box's own engine (§8.2), and those are
//! separate trust relationships that share nothing — not anchors, not leaves,
//! not addresses. Painting them as one flat list would say they are one thing.
//!
//! **At most one engine is open**, and it is painted first; the rest follow in
//! the order each was last opened on this seat, most recent first, the name
//! breaking a tie (`crate::ui::model::Engines`). A closed engine paints its
//! row and whatever it has to say about itself, and no walls. Opening one
//! closes the other, so the arrangement is one fact and not a set of flags.
//!
//! **And a wall that is not the aimed one paints no conversations**, not even
//! an emptiness (§4.39, §4.12): the standing read set asks about the aimed
//! wall, so this seat holds no rows for any other and no evidence that there
//! are none — aiming is the act that asks.
//!
//! **A row carries the channel it came from as a client-side stamp** and no
//! origin crosses the wire; the stamp is applied where the answer is absorbed
//! ([`crate::ui::Model::absorb`]) and read here.

use crate::reply::roster::WsRow;
use crate::ui::{Channel, Chunk, Model, theme};

/// The four ops whose subject is every channel, and the strip they hang on.
pub mod acts;
/// One engine's row, the order the rows stand in, and the pane's cursor track.
pub mod engine;
/// One wall's row, and the five per-wall controls that hang off the aimed one.
pub mod wall;

pub use acts::REFRESH;
pub use engine::{Step, track};
pub use wall::{NO_NAME_HERE, PIN, UNPIN, line};

/// **What a section says while nothing has come down its channel yet.**
///
/// The [`crate::ui::convs`] rows' doctrine one noun over: an empty list is not
/// evidence that a thing holds nothing until somebody has looked. It stands
/// from the window's first paint — before any engine is dialled, deliberately
/// — until the first roster answer lands.
pub const NOT_ANSWERED: &str = "waiting to hear from this channel";

/// What a section says for an engine that answered and holds no workspace. A
/// fact about that engine, and the one empty state that is not a wait.
pub const NO_WALLS: &str =
    "this engine holds no workspace — make one with yog on its box, then refresh";

/// The word this pane wears, and the subject the arrows act on when it is
/// focused. **It is painted by `crate::ui::shell`** — above the pane in the
/// broad shape, on the navigation bar in the narrow one (bl-dfda) — because a
/// column's name has one home and which one it is depends on the shape, and
/// the narrow bar's word follows this one for free.
///
/// **The word is *engines*, and the crate's word stays *channel*** (§4.39).
/// A channel is what this crate calls the client-side entry that reaches one
/// engine (§4.6), which is the right word for a thing in `wire/workspaces/`
/// and the wrong one over a list an operator reads: what they are looking at
/// is yogs, each by the name this box gave it.
pub const HEADING: &str = "engines";

/// Paint the roster and take a click on it. **The heading is the shell's** —
/// see [`HEADING`].
pub fn render(ui: &mut egui::Ui, model: &mut Model) {
    // **The window's own acts hang here, above the channels** — see [`acts`]
    // for which four they are and why this pane is their home.
    if !model.covered() {
        acts::render(ui, model);
    }
    // **The list scrolls, and the heading above it does not** (bl-e5d2): a
    // roster longer than its pane used to be cut off mid-glyph at the panel
    // edge, with nothing on the glass saying anything had been cut — while the
    // keyboard walked onto rows the pane had never painted. The heading stays
    // out of it because it is the one thing on this pane that is always
    // painted, and it carries the mark saying whose the arrows are.
    let reveal = model.revealing();
    egui::ScrollArea::vertical()
        .id_salt(HEADING)
        .auto_shrink(false)
        .show(ui, |ui| {
            let open = model.engine_open();
            for chunk in model.engine_rows() {
                let showing = open.as_deref() == Some(chunk.channel.name.as_str());
                section(ui, model, &chunk, showing, reveal);
            }
        });
}

/// One engine's section: its row, what it has to say about itself, and — while
/// it is the open one — its walls.
///
/// **What it says about itself is painted open or closed** (bl-e620): the bar
/// holds one sentence and the last writer wins, so a seat with two unreachable
/// engines could discover only one of them from the glass. A channel that
/// cannot be dialled says so under its own row whether or not anybody has
/// opened it. What the accordion folds away is the WALLS.
fn section(ui: &mut egui::Ui, model: &mut Model, chunk: &Chunk, open: bool, reveal: bool) {
    // **Air and a weak word, never a line** (`docs/STYLE.md` §1, rule 2): the
    // separator that used to stand here was a stroke, and the one stroke this
    // window spends is the brand ring on the field holding the caret. What
    // divides two engines is the space between them.
    ui.add_space(theme::space::S);
    engine::render(ui, model, chunk, open, reveal);
    // **A channel that cannot be reached says so HERE**, under its own header
    // and beside whatever it last answered — never in the shell-wide bar, which
    // is for what an engine said about a gesture (bl-e620). It stands above the
    // walls rather than in place of them: the rows are the last thing that
    // channel did say, and they are worth keeping while it is down.
    if let crate::ui::Held::Unheld(why) = &chunk.held {
        // **A channel that will not dial is an error and not a note** (§1:
        // red is an error that will not mend itself). It stood in the
        // annotation orange, which is the colour of a thing worth reading;
        // this is a thing that is broken until somebody mends it.
        ui.colored_label(theme::accent(theme::State::Error), why);
    }
    for note in [chunk.stale.as_ref(), chunk.growth.as_ref()]
        .into_iter()
        .flatten()
    {
        ui.colored_label(theme::tone_ink(&crate::reply::convs::Tone::Weak), note);
    }
    if !open {
        return;
    }
    if chunk.walls.is_empty() {
        // **An empty section says which emptiness it is** (bl-08b6). The pane
        // used to carry one sentence, for an empty ROSTER — which is
        // unreachable, because every box holds its own engine's slot whether or
        // not anything is provisioned in it (`crate::seat::channels`). So the
        // box the sentence was written for got a section header over a blank,
        // on the first run of a seat, which is the whole of what it has. The
        // unheld case is already said above, in its own words.
        match &chunk.held {
            crate::ui::Held::Unheard => {
                theme::paint::empty(ui, NOT_ANSWERED);
            }
            crate::ui::Held::Heard => {
                theme::paint::empty(ui, NO_WALLS);
            }
            // Already said above, in its own words.
            crate::ui::Held::Unheld(_) => {}
        }
        return;
    }
    for row in ordered(&chunk.walls) {
        wall::render(ui, model, chunk, &row, reveal);
    }
}

/// The section header: what this box calls the channel, what its host calls the
/// workspace when the two differ, and **the address it dials**.
///
/// It takes the channel and not the chunk because the decision queue groups its
/// rows by channel too (`crate::ui::queue`), and two spellings of a section
/// header would be two things an operator has to reconcile.
///
/// The rename is here because a local rename is the remedy for a name
/// collision, and an operator has to be able to see one. The address is here
/// because the pane used to drop the one fact that explains a duplicate
/// (bl-77df): an entry whose `address` file holds what this box's own engine
/// listens on paints every workspace of that engine twice, under two headers,
/// with nothing on either saying they are the same server. `lernie entries`
/// prints the address under every row and the window did not.
pub fn header(channel: &Channel) -> String {
    let named = match &channel.named_there {
        Some(there) if *there != channel.name => {
            format!("{} (named {:?} on its host)", channel.name, there)
        }
        _ => channel.name.clone(),
    };
    match &channel.dials {
        Some(at) => format!("{named} — {at}"),
        None => named,
    }
}

/// **Pinned first, in pin order**, then the rest by name.
///
/// The rank is what makes this a sort rather than a filter: a seat given only a
/// flag would have to read the pin list back to order them, which is the seat
/// joining an answer against a document only the engine holds.
pub fn ordered(walls: &[WsRow]) -> Vec<WsRow> {
    let mut rows = walls.to_vec();
    rows.sort_by(|a, b| {
        (a.pinned.unwrap_or(u64::MAX), &a.workspace)
            .cmp(&(b.pinned.unwrap_or(u64::MAX), &b.workspace))
    });
    rows
}

#[cfg(test)]
mod tests;