//! `atgc about`: a panel of project info floating over a field of DNA. //! //! The field is [`crate::art`]'s and is laid down across the whole frame; //! what is here is the panel, which is punched out of it and drawn on top. //! Everything is cropped by the frame rather than fitted to it, so the field //! runs off all four edges and behind the panel instead of stopping short. //! //! The frame follows the terminal instead of being fixed at one size. See //! [`frame`] for how it is chosen, and [`draw_bare`] for what happens when //! the terminal is too small to hold any of this. //! //! The art used to be the top half of this file, which is how the OAuth //! callback page came to import a *command* module for it. It is drawn, not //! stored, in both places, and now from one place. use crate::art::{self, Canvas, GUTTER, PAD, draw_bare, draw_panel, panel_lines, width_of}; use anyhow::Result; use terminal_size::{Height, Width, terminal_size}; /// Frame drawn when nothing better is known: a pipe, a redirect, or a /// terminal that will not report a size. Fixed on purpose: redirected /// output should not change shape with whatever the ambient terminal happens /// to be, the same reason [`about`] gates color on /// [`crate::term::hyperlink::stdout_escapes_wanted`]. These are the numbers /// `atgc about` drew at before it learned to look. /// /// 74 columns is a floor rather than the width: it is narrower than the /// panel plus its margins, so in practice the panel wins and the frame comes /// out 80 wide. Left as it was, so piped output is byte-for-byte unchanged. const FALLBACK_COLUMNS: i32 = 74; const FALLBACK_ROWS: i32 = 19; /// Terminal rows left unspent. One keeps the shell's next prompt off the /// last row of the art; the second absorbs a prompt that wraps to two lines, /// which is common enough (git branch, venv, exit status) to be worth a row. const RESERVED_ROWS: i32 = 2; /// Ceilings on the frame. The panel is the subject and the field is the mat /// it hangs on; past a helix or so of field on each side (`BAND_GAP` is 18 /// columns) more of it only turns the panel into a stamp on a wall of /// wallpaper. 120 columns leaves the ~68-column panel a little over a full /// helix of field either side, and 31 rows is 2.5 turns of twist, up from /// the 1.5 turns the old fixed frame showed: enough to read the pattern as /// a helix rather than as tiling. Both also bound the canvas allocation, so /// a terminal reporting something absurd cannot ask for a huge one. const MAX_COLUMNS: i32 = 120; const MAX_ROWS: i32 = 31; /// A positive integer from the environment, or nothing. Anything else: /// unset, empty, `0`, `-1`, `wide`: counts as not asked for. fn env_dimension(key: &str) -> Option { let raw = std::env::var(key).ok()?; raw.trim().parse::().ok().filter(|n| *n > 0) } /// The frame to draw into, in cells, honoring in order: an explicit /// `COLUMNS`/`LINES` in the environment, then the size the terminal reports /// for stdout, then the fixed fallback. The two resolve independently, so /// `COLUMNS=40 atgc about` can narrow the frame without also having to say /// how tall it is. /// /// `COLUMNS` and `LINES` outrank the terminal because they are the only way /// to ask for a particular frame, and because neither bash nor zsh exports /// them: one that reaches this process was put there deliberately, on this /// command line or by a script that meant it. With neither set and stdout /// not a terminal, `terminal_size` returns `None` and the fallback stands: /// so a pipe or a redirect draws the same frame every time. /// /// The result is clamped from above but not from below. It can come back /// smaller than the panel, which is [`about`]'s cue to drop to bare lines. fn frame(panel_w: i32, panel_h: i32) -> (i32, i32) { let reported = terminal_size(); let columns = env_dimension("COLUMNS").or_else(|| reported.map(|(Width(w), _)| i32::from(w))); let rows = env_dimension("LINES").or_else(|| reported.map(|(_, Height(h))| i32::from(h))); let width = match columns { Some(w) => w.min(MAX_COLUMNS), None => FALLBACK_COLUMNS.max(panel_w + 2 * (GUTTER + 4)), }; let height = match rows { Some(h) => { let spare = (h - RESERVED_ROWS).min(MAX_ROWS); let snapped = art::snap_rows(spare); // Snapping costs up to five rows, which a short terminal has not // got: below a panel with a row of field above and below it, // take the untrimmed height and let the bottom edge taper. if snapped >= panel_h + 2 { snapped } else { spare } } // No reserve subtracted here — there is no prompt to leave room for, // and this height is already a whole number of half turns. None => FALLBACK_ROWS, }; (width, height) } /// The row the panel's top edge goes on, in a frame of `height` rows: the /// slack split between the two, floored. /// /// The flooring is the point rather than an off-by-one. A block sitting /// below the arithmetic middle reads as sagging, and this is what stops it /// ever being there: the field left above the panel is never deeper than the /// field left below it. Whether the panel also gets the half row of *lift* /// an optical centre wants is a matter of parity and belongs to the panel /// rather than to this function — [`art::snap_rows`] returns none but odd /// heights, so an even panel is lifted and an odd one, which is what /// [`art::panel_lines`] builds today, lands on the exact middle row. /// /// On the short unsnapped heights the slack is a row or two and the panel /// sits wherever it fits; nudging it further there would only shove it /// against the top edge. fn top_of(height: i32, panel_h: i32) -> i32 { (height - panel_h) / 2 } pub(crate) fn about() -> Result<()> { let lines = panel_lines(); let content = lines.iter().map(width_of).max().unwrap_or(0) as i32; let panel_w = content + 2 * PAD + 2; let panel_h = lines.len() as i32 + 2; let (width, height) = frame(panel_w, panel_h); // Under the panel's own size there is no frame worth drawing: the border // would run off the edge mid-line, which reads as a bug rather than as // art. The field needs no threshold of its own — it is laid down edge to // edge and then cleared for `GUTTER` columns around the panel, so as the // frame tightens the field is squeezed out a column at a time and is // simply gone by the time the panel fills the width. let canvas = if width < panel_w || height < panel_h { draw_bare(&lines, content) } else { let mut canvas = Canvas::new(width, height); art::draw_field(&mut canvas); // Centered both ways; see [`top_of`] for which way the odd row goes. let top = top_of(height, panel_h); draw_panel(&mut canvas, (width - panel_w) / 2, top, panel_w, &lines); canvas }; let colors = crate::term::hyperlink::stdout_escapes_wanted(); print!("{}", canvas.render(colors)); Ok(()) } #[cfg(test)] mod tests { use super::{art, env_dimension}; use crate::art::panel_lines; /// What [`super::top_of`] promises: the panel never sags. At every frame /// height it can be drawn in — snapped or not — the field above it is no /// deeper than the field below. #[test] fn the_panel_never_sits_below_the_middle() { let panel_h = panel_lines().len() as i32 + 2; for rows in panel_h..=super::MAX_ROWS { let above = super::top_of(rows, panel_h); let below = rows - panel_h - above; assert!(above >= 0, "{rows} rows put the panel at {above}"); assert!(above <= below, "{rows} rows: {above} above, {below} below"); } } /// Fixed points: a height that already spans whole half turns is left /// alone, so a terminal at one of these sizes gives up no rows at all. #[test] fn snap_rows_leaves_exact_heights_alone() { for rows in [1, 7, 13, 19, 25, 31] { assert_eq!(art::snap_rows(rows), rows); } // And the fallback height is one of them, which is why the piped // frame needs no snapping. assert_eq!(art::snap_rows(super::FALLBACK_ROWS), super::FALLBACK_ROWS); } /// `COLUMNS=0`, `LINES=-1` and `COLUMNS=wide` all mean "not asked for" /// rather than a frame of that size. /// /// Only the unset case is exercised: setting an environment variable is /// `unsafe` in edition 2024, and this crate forbids `unsafe_code`, so a /// test cannot put a value there to read back. The parsing above it is /// covered by inspection and by resizing a real terminal. #[test] fn an_unset_dimension_is_not_asked_for() { assert_eq!(env_dimension("ATGC_TEST_DIMENSION_THAT_IS_NOT_SET"), None); } }