diff --git a/src/cli.rs b/src/cli.rs index c963d99..3cc1093 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -207,12 +207,12 @@ fn run_completions(shell: Shell) -> Result<(), String> { Ok(()) } -/// Boots a world for a sandbox session, prints where it lives, and plays -/// until the player quits. When `scenario` names a file, its contents open -/// the session as the player's first line. When `world` names a -/// directory, the session keeps its world and player knowledge there -/// instead of a temporary one deleted on exit, so a later run with the -/// same directory resumes it. +/// Boots a world for a sandbox session and plays until the player quits. +/// When `scenario` names a file, its contents open the session as the +/// player's first line. When `world` names a directory, the session keeps +/// its world and player knowledge there instead of a temporary one deleted +/// on exit, so a later run with the same directory resumes it. `/settings` +/// shows where the session's world and player directories live. #[cfg(not(coverage))] fn run_sandbox( overrides: &crate::config::Overrides, @@ -234,7 +234,6 @@ fn run_sandbox( None => None, }; - let kept = world.is_some(); let sandbox = match world { Some(root) => Sandbox::named(root)?, None => Sandbox::new()?, @@ -243,18 +242,6 @@ fn run_sandbox( layers.push(sandbox.world_dir()); layers.push(sandbox.player_dir()); - if kept { - println!("sandbox uses a kept world this session:"); - } else { - println!("sandbox uses a fresh world this session:"); - } - println!(" world: {}", sandbox.world_dir().display()); - println!(" player: {}", sandbox.player_dir().display()); - println!("mount stack, lowest first:"); - for layer in &layers { - println!(" {}", layer.display()); - } - crate::play::run(overrides, &layers, &sandbox.world_dir(), opening) } @@ -269,6 +256,8 @@ fn run_sandbox( /// no `world/` subdirectory inside it, and the player's own knowledge /// lives outside the world at a fixed directory shared across every /// world, with a `worlds/` overlay mounted above this one. +/// `/settings` shows the world directory, the player directory, and the +/// full mount stack once the session starts. #[cfg(not(coverage))] fn run_play( overrides: &crate::config::Overrides, @@ -318,18 +307,6 @@ fn run_play( let layers = crate::worlds::mount_stack(session_layers, &world_dir, &player_root, name); - if resumed { - println!("play resumes an existing world this session:"); - } else { - println!("play begins a new world this session:"); - } - println!(" world: {}", world_dir.display()); - println!(" player: {}", player_root.display()); - println!("mount stack, lowest first:"); - for layer in &layers { - println!(" {}", layer.display()); - } - crate::play::run(overrides, &layers, &world_dir, None) } diff --git a/src/play/mod.rs b/src/play/mod.rs index d22f4e0..246187f 100644 --- a/src/play/mod.rs +++ b/src/play/mod.rs @@ -31,28 +31,92 @@ pub mod worker; #[cfg(not(coverage))] pub use terminal::run; +use std::path::{Path, PathBuf}; + use crate::config::Config; -/// The opening line of the transcript: which model answers, and where it -/// lives. -pub fn banner(config: &Config) -> String { - format!("{} @ {}", config.model, config.api_base) +/// What `/settings` shows: the world root, the mount stack lowest first, +/// and the loaded config, one fact per line, grouped by a blank line +/// between each group. Omits the reasoning effort line when the config has +/// none. +pub fn settings(config: &Config, layers: &[PathBuf], world_root: &Path) -> String { + let mut mount_stack = vec!["mount stack, lowest first:".to_string()]; + mount_stack.extend(layers.iter().map(|layer| format!(" {}", layer.display()))); + + let mut model = vec![ + format!("model: {}", config.model), + format!("api base: {}", config.api_base), + ]; + if let Some(effort) = &config.reasoning_effort { + model.push(format!("reasoning effort: {effort}")); + } + + [ + format!("world: {}", world_root.display()), + mount_stack.join("\n"), + model.join("\n"), + ] + .join("\n\n") } #[cfg(test)] mod tests { use super::*; - #[test] - fn the_banner_names_the_model_and_the_api_base() { - let config = Config { + fn config(reasoning_effort: Option<&str>) -> Config { + Config { api_base: "https://api.example.test/v1".to_string(), api_key: "sk-test".to_string(), model: "a-model".to_string(), - reasoning_effort: None, + reasoning_effort: reasoning_effort.map(str::to_string), worlds_root: None, - }; + } + } + + #[test] + fn settings_shows_the_world_the_mount_stack_and_the_model() { + let layers = vec![PathBuf::from("rules/srd-5.2"), PathBuf::from("/world")]; + + let report = settings(&config(None), &layers, &PathBuf::from("/world")); + + assert_eq!( + report, + [ + "world: /world", + "", + "mount stack, lowest first:", + " rules/srd-5.2", + " /world", + "", + "model: a-model", + "api base: https://api.example.test/v1", + ] + .join("\n") + ); + } + + #[test] + fn settings_adds_the_reasoning_effort_line_when_the_config_has_one() { + let report = settings(&config(Some("high")), &[], &PathBuf::from("/world")); + + assert!(report.ends_with("reasoning effort: high")); + } + + #[test] + fn settings_omits_the_reasoning_effort_line_when_the_config_has_none() { + let report = settings(&config(None), &[], &PathBuf::from("/world")); + + assert!(!report.contains("reasoning effort")); + } + + #[test] + fn settings_keeps_the_layers_in_the_order_they_were_given() { + let layers = vec![PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]; + + let report = settings(&config(None), &layers, &PathBuf::from("/world")); - assert_eq!(banner(&config), "a-model @ https://api.example.test/v1"); + let position = |needle: &str| report.find(needle).unwrap(); + assert!(position(" a") < position(" b")); + assert!(position(" b") < position(" c")); } } diff --git a/src/play/screen.rs b/src/play/screen.rs index 0f4c0b5..842d5c5 100644 --- a/src/play/screen.rs +++ b/src/play/screen.rs @@ -67,7 +67,7 @@ const RULE: &str = "─"; const WORKER_GONE: &str = "the storyteller thread is gone; restart storied to continue"; /// Slash commands the player can type. Tab cycles through completions. -const SLASH_COMMANDS: &[&str] = &["/context", "/play"]; +const SLASH_COMMANDS: &[&str] = &["/context", "/play", "/settings"]; /// The input the player is typing, the reply that is still arriving, and /// how much of the screen the viewport holds while both go on. @@ -103,8 +103,8 @@ pub type Clock<'a> = &'a mut dyn FnMut() -> Option; /// Where the screen learns about the world's characters and puts one on /// stage. `/play` reads and writes through this, and the session's -/// opening reads it once to decide whether the unbound notice belongs -/// after the banner. +/// opening reads it once to decide whether the unbound notice opens the +/// session. pub trait Stage { /// Every character in the world, by slug. fn characters(&self) -> Vec; @@ -140,8 +140,9 @@ pub enum Opening { /// keys, and the worker: the prose it composes before the loop starts, /// the prompts it remembers, and where it reads the campaign clock. pub struct Session<'a> { - /// What the transcript opens with. - pub banner: &'a str, + /// What `/settings` shows: the world root, the mount stack, and the + /// loaded config, composed once at startup. + pub settings: &'a str, /// What `/context` prints: the DM's system prompt, frozen at startup. pub slash_context: &'a str, /// The prompts the player recalls with Up and Down. @@ -168,10 +169,10 @@ pub struct Session<'a> { /// /// `guard` brackets every render pass in a synchronized update, so the /// terminal paints each pass in one go instead of painting whatever has -/// landed on the wire so far. The opening banner queues before the loop -/// and paints with the first pass, inside that pass's update. An opening -/// line in `session` starts its turn right after the banner, before the -/// first key. +/// landed on the wire so far. The transcript opens empty; nothing queues +/// ahead of the loop. An opening line in `session` starts its turn before +/// the first key, so it is the first thing the transcript shows. A fresh +/// world with no opening line shows nothing until the player types one. /// /// `viewport` changes the viewport's height when the prompt needs more or /// fewer rows. Each resize sits inside the same guard as the repaint that @@ -189,7 +190,7 @@ pub struct Session<'a> { /// way through a keystroke. /// /// `session.stage` says who the world's characters are and who is on -/// stage. Right after the banner, this queries it once for the unbound +/// stage. Before the loop starts, this queries it once for the unbound /// notice: a world with several characters and nobody on stage yet gets /// a plain aside pointing at `/play`; any other world gets none. pub fn play>( @@ -209,9 +210,6 @@ pub fn play>( prefix: prompt::prefix((session.clock)()), divider: false, }; - screen - .transcript - .insert(terminal, session.banner, aside(), Kind::Aside)?; let characters = session.stage.characters(); let on_stage = session.stage.on_stage(); if let Some(notice) = unbound_notice(&characters, on_stage.as_deref()) { @@ -480,19 +478,13 @@ impl Screen { } self.input.take(); - // Slash commands stay local. /context shows the DM's system prompt. + // Slash commands stay local. /context shows the DM's system prompt, + // and /settings shows the world, the mount stack, and the model. if input == "/context" { - let line = format!("{}/context", self.prefix); - guard.begin(); - self.transcript - .insert(terminal, &line, player(), Kind::Player)?; - self.transcript - .insert(terminal, session.slash_context, aside(), Kind::Aside)?; - fit(self, terminal, viewport)?; - self.transcript.place(terminal)?; - terminal.draw(|frame| render(self, frame))?; - guard.end(); - return Ok(()); + return self.echo_local(terminal, guard, viewport, "/context", session.slash_context); + } + if input == "/settings" { + return self.echo_local(terminal, guard, viewport, "/settings", session.settings); } // /play stays local too. Alone, it lists the world's characters; @@ -538,6 +530,32 @@ impl Screen { let _ = worker.inputs.send(TurnInput { speaker, text }); } + /// Echoes `command` as a player line, then answers it with `text` as + /// an aside, the way `/context` and `/settings` both work: neither + /// reaches the worker, and neither touches `busy`, since + /// [`Self::submit`] has already refused to run this while a turn is in + /// flight. + fn echo_local>( + &mut self, + terminal: &mut Terminal, + guard: &mut G, + viewport: &mut V, + command: &str, + text: &str, + ) -> Result<(), B::Error> { + let line = format!("{}{command}", self.prefix); + guard.begin(); + self.transcript + .insert(terminal, &line, player(), Kind::Player)?; + self.transcript + .insert(terminal, text, aside(), Kind::Aside)?; + fit(self, terminal, viewport)?; + self.transcript.place(terminal)?; + terminal.draw(|frame| render(self, frame))?; + guard.end(); + Ok(()) + } + /// Handles `/play`, screen-local like `/context`: never reaches the /// worker, and never touches `busy`, since [`Self::submit`] has /// already refused to run this while a turn is in flight. diff --git a/src/play/screen_pinned_tests.rs b/src/play/screen_pinned_tests.rs index 9868436..d4eeb10 100644 --- a/src/play/screen_pinned_tests.rs +++ b/src/play/screen_pinned_tests.rs @@ -133,19 +133,7 @@ fn a_finished_table_leaves_no_blank_rows_behind_it() { let mut played = play_pinned(ROOMY, steps); - assert_eq!( - played.printed(), - [ - "a banner", - "", - TABLE_ROWS[0], - TABLE_ROWS[1], - TABLE_ROWS[2], - TABLE_ROWS[3], - TABLE_ROWS[4], - TABLE_ROWS[5] - ] - ); + assert_eq!(played.printed(), TABLE_ROWS); } #[test] @@ -170,8 +158,6 @@ fn a_table_that_finishes_while_the_player_types_leaves_no_blank_rows_behind_it() assert_eq!( played.printed(), [ - "a banner", - "", TABLE_ROWS[0], TABLE_ROWS[1], TABLE_ROWS[2], @@ -194,17 +180,7 @@ fn a_block_that_spilled_while_it_streamed_leaves_no_blank_rows_behind_it() { // already scrolling. let mut played = play_pinned(CRAMPED, steps); - assert_eq!( - played.printed(), - [ - "a banner", - "", - TALL_ROWS[0], - TALL_ROWS[1], - TALL_ROWS[2], - TALL_ROWS[3] - ] - ); + assert_eq!(played.printed(), TALL_ROWS); assert_eq!(played.viewport_bottom(), CRAMPED); } @@ -214,10 +190,7 @@ fn the_transcript_reaches_the_viewport_while_a_block_is_still_forming() { let mut played = play_pinned(CRAMPED, steps); - assert_eq!( - played.printed(), - ["a banner", "", TALL_ROWS[0], TALL_ROWS[1]] - ); + assert_eq!(played.printed(), [TALL_ROWS[0], TALL_ROWS[1]]); } #[test] @@ -232,7 +205,7 @@ fn the_row_the_input_gives_back_leaves_nothing_behind_it() { // and the row it gave back holds none of the prompt it used to. assert_eq!(played.viewport_bottom(), ROOMY - 1); assert_eq!(played.row(ROOMY - 1), (String::new(), false)); - assert_eq!(played.printed(), ["a banner"]); + assert!(played.printed().is_empty()); } #[test] @@ -244,15 +217,6 @@ fn the_line_the_player_submits_takes_back_the_row_the_prompt_gave_up() { let mut played = play_pinned(ROOMY, steps); - // The blank row is the one the transcript puts between two blocks of - // different kinds, the banner and what the player said. assert_eq!(played.viewport_bottom(), ROOMY); - assert_eq!( - played.printed(), - [ - "a banner".to_string(), - String::new(), - format!("> {}", "ab".repeat(19)), - ] - ); + assert_eq!(played.printed(), [format!("> {}", "ab".repeat(19))]); } diff --git a/src/play/screen_slash_tests.rs b/src/play/screen_slash_tests.rs index 6c81af2..e43d473 100644 --- a/src/play/screen_slash_tests.rs +++ b/src/play/screen_slash_tests.rs @@ -4,7 +4,9 @@ use std::cell::RefCell; -use super::tests::{SLASH_CONTEXT, play_script, play_script_on, play_staged, press, typing}; +use super::tests::{ + SLASH_CONTEXT, SLASH_SETTINGS, play_script, play_script_on, play_staged, press, typing, +}; use super::*; /// A stage with a fixed roster and a fixed answer for who is on it, that @@ -96,6 +98,37 @@ fn a_trailing_space_still_names_the_command() { assert!(played.nothing_submitted()); } +#[test] +fn slash_settings_puts_the_command_and_the_report_in_the_transcript() { + let mut steps = typing("/settings"); + steps.push(press(Key::Enter)); + + let played = play_script(steps); + + assert!(played.transcript().contains("> /settings")); + assert!(played.transcript().contains(SLASH_SETTINGS)); +} + +#[test] +fn slash_settings_sends_nothing_to_the_worker() { + let mut steps = typing("/settings"); + steps.push(press(Key::Enter)); + + let played = play_script(steps); + + assert!(played.nothing_submitted()); +} + +#[test] +fn slash_settings_clears_the_prompt_for_the_next_input() { + let mut steps = typing("/settings"); + steps.push(press(Key::Enter)); + + let played = play_script(steps); + + assert_eq!(played.prompt(), ">"); +} + #[test] fn tab_completes_a_slash_command_prefix() { let mut steps = typing("/c"); @@ -158,6 +191,16 @@ fn tab_completes_play() { assert_eq!(played.prompt(), "> /play"); } +#[test] +fn tab_completes_settings() { + let mut steps = typing("/se"); + steps.push(press(Key::Tab)); + + let played = play_script(steps); + + assert_eq!(played.prompt(), "> /settings"); +} + #[test] fn slash_play_lists_characters_and_marks_the_one_on_stage() { let stage = FakeStage::new(&["maren", "tomas"], Some("maren")); diff --git a/src/play/screen_sync_tests.rs b/src/play/screen_sync_tests.rs index 93446ed..2efd55b 100644 --- a/src/play/screen_sync_tests.rs +++ b/src/play/screen_sync_tests.rs @@ -227,7 +227,7 @@ fn submitting_repaints_the_shrunk_viewport_inside_its_own_guard() { let stage = EmptyStage; let mut session = Session { - banner: "", + settings: "", slash_context: "", history: &mut history, clock: &mut || None, diff --git a/src/play/screen_tests.rs b/src/play/screen_tests.rs index 6e3a3a8..27ea642 100644 --- a/src/play/screen_tests.rs +++ b/src/play/screen_tests.rs @@ -47,6 +47,10 @@ pub(in crate::play) fn press(key: Key) -> Step { /// can watch `/context` put it in the transcript. pub(in crate::play) const SLASH_CONTEXT: &str = "the DM's full system prompt"; +/// What the harness hands `play` as the settings report, so a test can +/// watch `/settings` put it in the transcript. +pub(in crate::play) const SLASH_SETTINGS: &str = "the settings report"; + /// A screen tall enough to hold a six-row block in the tail area: the /// transcript keeps five rows of it, which leaves the tail eleven. pub(in crate::play) const ROOMY: u16 = 18; @@ -451,7 +455,7 @@ fn run_script>( &mut keys, &worker, &mut Session { - banner: "a banner", + settings: SLASH_SETTINGS, slash_context: SLASH_CONTEXT, history: &mut history, clock: setup.clock, @@ -474,10 +478,10 @@ fn run_script>( } #[test] -fn the_banner_opens_the_transcript() { +fn the_transcript_opens_empty_until_something_is_said() { let played = play_script(vec![]); - assert!(played.transcript().contains("a banner")); + assert!(played.transcript().is_empty()); } #[test] diff --git a/src/play/screen_turn_tests.rs b/src/play/screen_turn_tests.rs index d989cfc..13b4589 100644 --- a/src/play/screen_turn_tests.rs +++ b/src/play/screen_turn_tests.rs @@ -177,12 +177,7 @@ fn whitespace_only_narration_flushes_without_a_blank_row() { let transcript = played.transcript(); let lines: Vec<&str> = transcript.lines().collect(); - let banner = lines.iter().position(|line| *line == "a banner").unwrap(); - let tool = lines - .iter() - .position(|line| *line == "a tool line") - .unwrap(); - assert_eq!(tool - banner, 2); + assert_eq!(lines, vec!["a tool line"]); } #[test] @@ -193,12 +188,7 @@ fn a_tool_event_with_no_narration_inserts_only_the_tool_line() { let transcript = played.transcript(); let lines: Vec<&str> = transcript.lines().collect(); - let banner = lines.iter().position(|line| *line == "a banner").unwrap(); - let tool = lines - .iter() - .position(|line| *line == "a tool line") - .unwrap(); - assert_eq!(tool - banner, 2); + assert_eq!(lines, vec!["a tool line"]); } #[test] @@ -421,12 +411,7 @@ fn a_cancelled_turn_with_no_partial_text_inserts_only_the_aside() { let transcript = played.transcript(); let lines: Vec<&str> = transcript.lines().collect(); - let banner = lines.iter().position(|line| *line == "a banner").unwrap(); - let interrupted = lines - .iter() - .position(|line| *line == "(interrupted)") - .unwrap(); - assert_eq!(interrupted - banner, 2); + assert_eq!(lines, vec!["(interrupted)"]); } #[test] diff --git a/src/play/terminal.rs b/src/play/terminal.rs index eacaa54..376854b 100644 --- a/src/play/terminal.rs +++ b/src/play/terminal.rs @@ -65,7 +65,10 @@ pub fn run( let config = config::load(overrides).map_err(|error| error.to_string())?; let mount = Arc::new(Mount::open(layers)?); let campaign = Campaign::open(world_root)?; - let banner = super::banner(&config); + // `/settings` shows this, composed once here where the config, the + // layers, and the world root are all in hand, before the config moves + // into the DM below. + let settings = super::settings(&config, layers, world_root); // The prompt reads the clock from its own handle on the campaign, // because the DM owns the one it advances and lives on the worker // thread. A read that fails leaves the prompt with the bare marker @@ -109,7 +112,7 @@ pub fn run( &mut CrosstermKeys, &worker, &mut screen::Session { - banner: &banner, + settings: &settings, slash_context: &slash_context, history: &mut history, clock: &mut clock, diff --git a/src/play/transcript.rs b/src/play/transcript.rs index 3c17a98..d6454b4 100644 --- a/src/play/transcript.rs +++ b/src/play/transcript.rs @@ -27,9 +27,10 @@ use ratatui::text::{Line, Span, Text}; use crate::markdown::MarkdownStream; use crate::wrap::{wrap, wrap_spans}; -/// Storied's own asides: the opening banner, the empty-reply line, and -/// the interrupted line. The rule above the prompt is drawn in this style -/// too, so everything that is not the game itself reads the same way. +/// Storied's own asides: the empty-reply line, the interrupted line, and +/// what `/context` and `/settings` show. The rule above the prompt is +/// drawn in this style too, so everything that is not the game itself +/// reads the same way. pub fn aside() -> Style { Style::new().add_modifier(Modifier::DIM) } @@ -65,10 +66,8 @@ pub enum Kind { Narration, /// One dispatched tool call's line. Tool, - /// Storied's own lines: the banner, the empty-reply line, the - /// interrupted line, and anything that failed. This is the kind a - /// transcript starts on, so the banner opens it with no blank row - /// ahead of it. + /// Storied's own lines: the empty-reply line, the interrupted line, + /// and anything that failed. #[default] Aside, } @@ -112,6 +111,9 @@ pub struct Transcript { pending_blank: bool, /// What the last row of the transcript holds. kind: Kind, + /// True once the transcript has put out its first row. The first row + /// never gets a blank row ahead of it, whatever kind it is. + started: bool, /// True once this turn has put a narration row or a tool line in the /// transcript. A turn that ends with this still false is the one that /// says the DM said nothing. @@ -346,7 +348,9 @@ impl Transcript { /// go out on its own: it only marks that one blank row belongs ahead /// of the next row that has something on it. A change of kind marks /// the same thing, which is what separates the blocks of the - /// transcript from each other. + /// transcript from each other. The transcript's first row gets no + /// blank row ahead of it, whatever kind it is: there is nothing above + /// it to separate it from. /// /// The test is for characters, not for columns. A row of characters /// that print in no columns at all, a combining mark that lost its @@ -356,7 +360,10 @@ impl Transcript { self.pending_blank = true; return; } - self.pending_blank |= self.kind != kind; + if self.started { + self.pending_blank |= self.kind != kind; + } + self.started = true; self.kind = kind; self.said |= matches!(kind, Kind::Narration | Kind::Tool); let blank = u16::from(self.pending_blank); diff --git a/src/play/transcript_tests.rs b/src/play/transcript_tests.rs index 60382ad..12194f7 100644 --- a/src/play/transcript_tests.rs +++ b/src/play/transcript_tests.rs @@ -139,10 +139,7 @@ fn a_finished_reply_ending_in_blank_lines_has_no_extra_blank_rows() { let transcript = played.transcript(); let lines: Vec<&str> = transcript.lines().collect(); - let banner = lines.iter().position(|line| *line == "a banner").unwrap(); - let narration = lines.iter().position(|line| *line == "You wake.").unwrap(); - assert_eq!(narration - banner, 2); - assert_eq!(lines.len(), narration + 1); + assert_eq!(lines, vec!["You wake."]); } #[test] @@ -259,8 +256,6 @@ fn a_table_reaches_the_transcript_bordered_and_aligned_when_the_turn_ends() { assert_eq!( lines, vec![ - "a banner", - "", "┌─────┬──────┐", "│ Die │ Face │", "├─────┼──────┤", @@ -312,7 +307,7 @@ fn a_block_taller_than_the_tail_area_spills_its_top_rows_while_the_turn_runs() { let transcript = played.transcript(); let lines: Vec<&str> = transcript.lines().collect(); - assert_eq!(lines, vec!["a banner", "", TALL_ROWS[0], TALL_ROWS[1]]); + assert_eq!(lines, vec![TALL_ROWS[0], TALL_ROWS[1]]); } #[test] @@ -325,14 +320,7 @@ fn every_row_of_a_spilled_block_lands_in_the_transcript_exactly_once() { let lines: Vec<&str> = transcript.lines().collect(); assert_eq!( lines, - vec![ - "a banner", - "", - TALL_ROWS[0], - TALL_ROWS[1], - TALL_ROWS[2], - TALL_ROWS[3], - ] + vec![TALL_ROWS[0], TALL_ROWS[1], TALL_ROWS[2], TALL_ROWS[3]] ); } @@ -354,8 +342,6 @@ fn two_narrated_rounds_around_a_tool_line_keep_one_blank_row_between_the_blocks( assert_eq!( lines, vec![ - "a banner", - "", "You reach for the lock.", "", "It resists.", diff --git a/src/play/transcript_width_tests.rs b/src/play/transcript_width_tests.rs index 3334ab3..dcd393f 100644 --- a/src/play/transcript_width_tests.rs +++ b/src/play/transcript_width_tests.rs @@ -81,7 +81,6 @@ fn a_round_that_widens_mid_stream_still_says_what_it_has_left() { assert_eq!( scrollback(&narrow), [ - "", "one two three four", "five six seven eight", "nine ten eleven", @@ -111,7 +110,6 @@ fn a_round_takes_the_width_it_first_has_something_to_say_at() { assert_eq!( scrollback(&narrow), [ - "", "one two three four", "five six seven eight", "nine ten eleven", @@ -131,7 +129,7 @@ fn a_round_that_narrows_mid_stream_says_nothing_a_second_time() { transcript.flush(&narrow).unwrap(); transcript.place(&mut narrow).unwrap(); - assert_eq!(scrollback(&wide), ["", PARAGRAPH]); + assert_eq!(scrollback(&wide), [PARAGRAPH]); assert_eq!(scrollback(&narrow), ["", AFTER]); } @@ -150,7 +148,7 @@ fn a_row_too_wide_for_the_terminal_it_lands_on_keeps_every_word() { // The paragraph rendered as one row at eighty columns. All of it is // here, over the four rows twenty columns takes, and the row that // separates it from the block above is one blank row of spacing. - assert_eq!(scrollback(&wide), ["", AFTER]); + assert_eq!(scrollback(&wide), [AFTER]); assert_eq!( scrollback(&narrow), [ @@ -245,7 +243,6 @@ fn a_table_too_wide_for_the_screen_clips_every_row_at_the_same_column() { assert_eq!( scrollback(&narrow), [ - "", "┌─────┬─────┬─────┬─", "│ abc │ def │ ghi │", "├─────┼─────┼─────┼─",