From 845cad7f7417573d6f8d7daa132dc03ab87a6a96 Mon Sep 17 00:00:00 2001 From: Simonas Kareiva Date: Thu, 21 May 2026 18:31:29 +0300 Subject: [PATCH] Update TUI to scroll from the bottom --- src/tui.rs | 596 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 371 insertions(+), 225 deletions(-) diff --git a/src/tui.rs b/src/tui.rs index 8afda32..9c3832a 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -18,18 +18,9 @@ use crate::similarity; enum Screen { Welcome, - TierSelect { - cursor: u8, - }, - Level, - Result { - score: f64, - passed: bool, - level_name: String, - }, - InvalidYaml { - error: String, - }, + TierSelect { cursor: u8 }, + /// Active play: left column = HistoryLog, right column = Editor. + Game, ResetConfirm, Completed, } @@ -40,12 +31,72 @@ enum Focus { Editor, } -/// Multi-line YAML editor backing the right column. Byte-indexed; assumes -/// ASCII content (YAML is conventionally ASCII). Splitting at a multi-byte -/// boundary would panic β€” that's flagged as risk R6. +enum LogEntry { + /// Game rules + the full controls listing. Appended once on + /// Welcome β†’ TierSelect so the player can scroll back to it. + Intro, + /// The full tier table with a marker on the chosen one. + TierChoice { + chosen: u8, // 0=Easy, 1=Medium, 2=Hard + }, + LevelPrompt { + level_id: u8, + level_name: String, + tier_name: &'static str, + flavor: String, + description: String, + }, + ResultPass { + level_name: String, + score: f64, + threshold: f64, + }, + ResultFail { + level_name: String, + score: f64, + threshold: f64, + }, + InvalidYaml { + error: String, + }, + Completed, +} + +/// Append-only chronological log of game events. Rendered bottom-anchored +/// in the left column. `scroll` is the number of lines offset from the +/// bottom; 0 means pinned to the latest entry. +struct HistoryLog { + entries: Vec, + scroll: usize, +} + +impl HistoryLog { + fn new() -> Self { + Self { + entries: vec![], + scroll: 0, + } + } + + fn push(&mut self, entry: LogEntry) { + self.entries.push(entry); + // Always snap back to the bottom on a new event. + self.scroll = 0; + } + + fn scroll_up(&mut self, n: usize) { + self.scroll = self.scroll.saturating_add(n); + } + + fn scroll_down(&mut self, n: usize) { + self.scroll = self.scroll.saturating_sub(n); + } +} + +/// Multi-line YAML editor. Byte-indexed; ASCII-only (see risk R6). struct Editor { buffer: Vec, - cursor: (usize, usize), // (row, col) + cursor: (usize, usize), } impl Editor { @@ -166,34 +217,61 @@ fn leave() -> Result<()> { Ok(()) } -fn initial_screen(prog: &Progress, registry_len: usize) -> Screen { +fn tier_index(t: Difficulty) -> u8 { + match t { + Difficulty::Easy => 0, + Difficulty::Medium => 1, + Difficulty::Hard => 2, + } +} + +fn initial_state(prog: &Progress, registry: &[Box]) -> (Screen, HistoryLog) { + let mut log = HistoryLog::new(); + // The Intro entry is always the bottom of the log on startup so that + // Welcome and TierSelect can render it inline (the player can scroll + // back to it any time). + log.push(LogEntry::Intro); + match (prog.tier, prog.current_level) { - (None, 0) => Screen::Welcome, - (None, _) => Screen::TierSelect { cursor: 0 }, - (Some(_), 0) => Screen::TierSelect { cursor: 0 }, - (Some(_), n) if (n as usize) > registry_len => Screen::Completed, - (Some(_), _) => Screen::Level, + (None, 0) => (Screen::Welcome, log), + (None, _) => (Screen::TierSelect { cursor: 0 }, log), + (Some(tier), 0) => (Screen::TierSelect { cursor: tier_index(tier) }, log), + (Some(_), n) if (n as usize) > registry.len() => { + log.push(LogEntry::Completed); + (Screen::Completed, log) + } + (Some(tier), _) => { + log.push(LogEntry::TierChoice { chosen: tier_index(tier) }); + log.push(level_prompt_entry(prog, registry, tier)); + (Screen::Game, log) + } + } +} + +fn level_prompt_entry( + prog: &Progress, + registry: &[Box], + tier: Difficulty, +) -> LogEntry { + let idx = (prog.current_level - 1) as usize; + let level = ®istry[idx]; + let g = level.generate(prog.current_seed); + LogEntry::LevelPrompt { + level_id: level.id(), + level_name: level.name().to_string(), + tier_name: tier.name(), + flavor: g.flavor, + description: g.description, } } fn default_focus(screen: &Screen) -> Focus { match screen { - Screen::Level | Screen::InvalidYaml { .. } => Focus::Editor, + Screen::Game => Focus::Editor, _ => Focus::Game, } } -fn editor_active(screen: &Screen) -> bool { - matches!( - screen, - Screen::Level | Screen::Result { .. } | Screen::InvalidYaml { .. } - ) -} - -fn editor_interactive(screen: &Screen) -> bool { - matches!(screen, Screen::Level | Screen::InvalidYaml { .. }) -} - // -- Main loop -------------------------------------------------------------- fn main_loop( @@ -201,12 +279,14 @@ fn main_loop( prog: &mut Progress, ) -> Result<()> { let registry = levels::registry(); - let mut screen = initial_screen(prog, registry.len()); + let (mut screen, mut log) = initial_state(prog, ®istry); let mut focus = default_focus(&screen); let mut editor = Editor::new(); loop { - terminal.draw(|frame| render(frame, &screen, prog, ®istry, focus, &editor))?; + terminal.draw(|frame| { + render(frame, &screen, focus, &editor, &log); + })?; let Event::Key(key) = event::read()? else { continue; }; @@ -214,38 +294,49 @@ fn main_loop( continue; } - if step(&mut screen, &mut focus, &mut editor, key, prog, ®istry)? { + if step( + &mut screen, + &mut focus, + &mut editor, + &mut log, + key, + prog, + ®istry, + )? { return Ok(()); } } } -/// Returns `true` when the user wants to quit. +/// Returns `true` to quit the loop. fn step( screen: &mut Screen, focus: &mut Focus, editor: &mut Editor, + log: &mut HistoryLog, key: KeyEvent, prog: &mut Progress, registry: &[Box], ) -> Result { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - // Global: Ctrl-Q quits from anywhere. + // Global: Ctrl-Q quits. if ctrl && key.code == KeyCode::Char('q') { return Ok(true); } - // Global: Ctrl-S grades when the editor is in play. - if ctrl && key.code == KeyCode::Char('s') && editor_interactive(screen) { - let next = grade(editor.text(), prog, registry)?; - *focus = default_focus(&next); - *screen = next; + // Global: Ctrl-S grades while in Game. + if ctrl && key.code == KeyCode::Char('s') && matches!(screen, Screen::Game) { + grade(editor.text(), prog, registry, log)?; + if matches!(log.entries.last(), Some(LogEntry::Completed)) { + *screen = Screen::Completed; + *focus = default_focus(screen); + } return Ok(false); } - // Tab toggles focus while the editor is interactive. - if key.code == KeyCode::Tab && editor_interactive(screen) { + // Tab toggles focus while in Game. + if key.code == KeyCode::Tab && matches!(screen, Screen::Game) { *focus = match *focus { Focus::Game => Focus::Editor, Focus::Editor => Focus::Game, @@ -253,10 +344,8 @@ fn step( return Ok(false); } - // Editor keys (only when Editor is focused and screen is interactive). - if *focus == Focus::Editor && editor_interactive(screen) { - let was_invalid = matches!(screen, Screen::InvalidYaml { .. }); - let mut consumed = true; + // Editor focus inside Game: dispatch editing keys. + if *focus == Focus::Editor && matches!(screen, Screen::Game) { match key.code { KeyCode::Char(c) if !ctrl => editor.insert_char(c), KeyCode::Backspace => editor.backspace(), @@ -267,32 +356,40 @@ fn step( KeyCode::Down => editor.down(), KeyCode::Home => editor.home(), KeyCode::End => editor.end(), - KeyCode::Esc => { - // Esc dismisses InvalidYaml (without retry) or returns focus - // to the Game column on Level. - if was_invalid { - *screen = Screen::Level; - *focus = default_focus(screen); - } else { - *focus = Focus::Game; - } - } - _ => consumed = false, - } - if consumed && was_invalid { - // Any edit implicitly dismisses the error overlay. - *screen = Screen::Level; + KeyCode::Esc => *focus = Focus::Game, + _ => {} } return Ok(false); } - // Game-focus keys. + // Game focus inside Game: scroll the log + shortcuts. + if matches!(screen, Screen::Game) && *focus == Focus::Game { + match key.code { + KeyCode::Up => log.scroll_up(1), + KeyCode::Down => log.scroll_down(1), + KeyCode::PageUp => log.scroll_up(10), + KeyCode::PageDown => log.scroll_down(10), + KeyCode::Char('r') => { + *screen = Screen::ResetConfirm; + *focus = default_focus(screen); + } + KeyCode::Char('q') => return Ok(true), + _ => {} + } + return Ok(false); + } + + // Other screens (Welcome, TierSelect, ResetConfirm, Completed). match (&*screen, key.code) { (Screen::Welcome, KeyCode::Enter) => { *screen = Screen::TierSelect { cursor: 0 }; *focus = default_focus(screen); } (Screen::Welcome, KeyCode::Char('q')) => return Ok(true), + (Screen::Welcome, KeyCode::Up) => log.scroll_up(1), + (Screen::Welcome, KeyCode::Down) => log.scroll_down(1), + (Screen::Welcome, KeyCode::PageUp) => log.scroll_up(10), + (Screen::Welcome, KeyCode::PageDown) => log.scroll_down(10), (Screen::TierSelect { cursor }, KeyCode::Up) => { let c = cursor.saturating_sub(1); @@ -303,7 +400,8 @@ fn step( *screen = Screen::TierSelect { cursor: c }; } (Screen::TierSelect { cursor }, KeyCode::Enter) => { - let tier = match cursor { + let chosen = *cursor; + let tier = match chosen { 0 => Difficulty::Easy, 1 => Difficulty::Medium, _ => Difficulty::Hard, @@ -315,63 +413,38 @@ fn step( } progress::save(prog)?; editor.clear(); - *screen = Screen::Level; + log.push(LogEntry::TierChoice { chosen }); + log.push(level_prompt_entry(prog, registry, tier)); + *screen = Screen::Game; *focus = default_focus(screen); } (Screen::TierSelect { .. }, KeyCode::Char('q')) => return Ok(true), - - (Screen::Level, KeyCode::Char('r')) => { - *screen = Screen::ResetConfirm; - *focus = default_focus(screen); - } - (Screen::Level, KeyCode::Char('q')) => return Ok(true), - - (Screen::Result { passed, .. }, KeyCode::Enter) => { - let pass = *passed; - if pass { - editor.clear(); - if (prog.current_level as usize) > registry.len() { - *screen = Screen::Completed; - } else { - *screen = Screen::Level; - } - } else { - *screen = Screen::Level; - } - *focus = default_focus(screen); - } - - (Screen::InvalidYaml { .. }, KeyCode::Enter) => { - *screen = Screen::Level; - *focus = default_focus(screen); - } - (Screen::InvalidYaml { .. }, KeyCode::Esc) => { - *screen = Screen::Level; - *focus = default_focus(screen); - } - (Screen::InvalidYaml { .. }, KeyCode::Char('r')) => { - *screen = Screen::ResetConfirm; - *focus = default_focus(screen); - } - (Screen::InvalidYaml { .. }, KeyCode::Char('q')) => return Ok(true), + (Screen::TierSelect { .. }, KeyCode::PageUp) => log.scroll_up(10), + (Screen::TierSelect { .. }, KeyCode::PageDown) => log.scroll_down(10), (Screen::ResetConfirm, KeyCode::Char('y')) => { let _ = std::fs::remove_file(progress::save_path()); *prog = Progress::default(); editor.clear(); + *log = HistoryLog::new(); + log.push(LogEntry::Intro); *screen = Screen::Welcome; *focus = default_focus(screen); } (Screen::ResetConfirm, _) => { - *screen = Screen::Level; + *screen = Screen::Game; *focus = default_focus(screen); } - (Screen::Completed, KeyCode::Char('q')) => return Ok(true), + (Screen::Completed, KeyCode::Up) => log.scroll_up(1), + (Screen::Completed, KeyCode::Down) => log.scroll_down(1), + (Screen::Completed, KeyCode::PageUp) => log.scroll_up(10), + (Screen::Completed, KeyCode::PageDown) => log.scroll_down(10), (Screen::Completed, KeyCode::Char('r')) => { *screen = Screen::ResetConfirm; *focus = default_focus(screen); } + (Screen::Completed, KeyCode::Char('q')) => return Ok(true), _ => {} } @@ -383,19 +456,22 @@ fn grade( candidate: String, prog: &mut Progress, registry: &[Box], -) -> Result { + log: &mut HistoryLog, +) -> Result<()> { + // Parse-first guard. if let Err(e) = serde_yaml::from_str::(&candidate) { - return Ok(Screen::InvalidYaml { error: e.to_string() }); + log.push(LogEntry::InvalidYaml { + error: e.to_string(), + }); + return Ok(()); } let idx = (prog.current_level - 1) as usize; let level = ®istry[idx]; let g = level.generate(prog.current_seed); let score = similarity::semantic_or_textual(&g.target_yaml, &candidate); - let threshold = prog - .tier - .expect("tier set before reaching the Level screen") - .threshold(); + let tier = prog.tier.expect("tier set before grading"); + let threshold = tier.threshold(); let passed = score >= threshold; let level_name = level.name().to_string(); let level_id = level.id(); @@ -404,16 +480,31 @@ fn grade( prog.completed.push(level_id); prog.current_level += 1; prog.current_seed = rand::random(); + progress::save(prog)?; + log.push(LogEntry::ResultPass { + level_name, + score, + threshold, + }); + + // Append the next LevelPrompt, or Completed if we ran out. + let next_idx = (prog.current_level - 1) as usize; + if next_idx >= registry.len() { + log.push(LogEntry::Completed); + } else { + log.push(level_prompt_entry(prog, registry, tier)); + } } else { prog.attempts += 1; + progress::save(prog)?; + log.push(LogEntry::ResultFail { + level_name, + score, + threshold, + }); } - progress::save(prog)?; - Ok(Screen::Result { - score, - passed, - level_name, - }) + Ok(()) } // -- Rendering -------------------------------------------------------------- @@ -421,10 +512,9 @@ fn grade( fn render( frame: &mut Frame, screen: &Screen, - prog: &Progress, - registry: &[Box], focus: Focus, editor: &Editor, + log: &HistoryLog, ) { let area = frame.size(); let chunks = Layout::default() @@ -434,22 +524,16 @@ fn render( let (left, right) = (chunks[0], chunks[1]); match screen { - Screen::Welcome => render_welcome(frame, left), - Screen::TierSelect { cursor } => render_tier_select(frame, left, *cursor), - Screen::Level => render_level(frame, left, prog, registry), - Screen::Result { - score, - passed, - level_name, - } => render_result(frame, left, *score, *passed, level_name, prog), - Screen::InvalidYaml { error } => render_invalid_yaml(frame, left, error), + Screen::Welcome => render_log(frame, left, log, Some(&welcome_prompt_lines())), + Screen::TierSelect { cursor } => { + render_log(frame, left, log, Some(&tier_picker_lines(*cursor))) + } + Screen::Game | Screen::Completed => render_log(frame, left, log, None), Screen::ResetConfirm => render_reset_confirm(frame, left), - Screen::Completed => render_completed(frame, left), } - if editor_active(screen) { - let editor_focused = focus == Focus::Editor && editor_interactive(screen); - render_editor(frame, right, editor, editor_focused); + if matches!(screen, Screen::Game) { + render_editor(frame, right, editor, focus == Focus::Editor); } else { render_editor_inactive(frame, right); } @@ -461,113 +545,172 @@ fn screen_widget<'a>(title: String, body: String) -> Paragraph<'a> { .wrap(Wrap { trim: false }) } -fn render_welcome(frame: &mut Frame, area: Rect) { - let body = "\n\ - You stand at the entrance to the YAML labyrinth.\n\ - Within, broken syntax festers and dictionaries\n\ - sprawl like vines.\n\ - \n\ - [Enter] begin Β· [q] flee" - .to_string(); - frame.render_widget(screen_widget(" YAMLabyrinth ".to_string(), body), area); +fn welcome_prompt_lines() -> Vec { + vec![ + "🏰 You stand at the entrance.".to_string(), + " [Enter] begin Β· [q] flee Β· [PgUp/PgDn] scroll the log".to_string(), + ] } -fn render_tier_select(frame: &mut Frame, area: Rect, cursor: u8) { +fn tier_picker_lines(cursor: u8) -> Vec { let rows = [ - ("Easy", "70 %", "forgive small slips"), - ("Medium", "80 %", "most details must match"), - ("Hard", "95 %", "only near-perfect passes"), + ("πŸ₯‰", "Easy", "70 %", "forgive small slips"), + ("πŸ₯ˆ", "Medium", "80 %", "most details must match"), + ("πŸ₯‡", "Hard", "95 %", "only near-perfect passes"), ]; - let mut body = String::from("\n"); - for (i, (name, pct, hint)) in rows.iter().enumerate() { - let marker = if i == cursor as usize { ">" } else { " " }; - body.push_str(&format!(" {marker} {name:<7} ({pct}) {hint}\n")); + let mut v = vec!["🎚 Choose your tier:".to_string()]; + for (i, (emoji, name, pct, hint)) in rows.iter().enumerate() { + let marker = if i == cursor as usize { "β€Ί" } else { " " }; + v.push(format!(" {marker} {emoji} {name:<7} ({pct}) {hint}")); } - body.push_str("\n↑/↓ choose Β· [Enter] confirm Β· [q] quit"); - frame.render_widget(screen_widget(" Choose your tier ".to_string(), body), area); -} - -fn render_level( - frame: &mut Frame, - area: Rect, - prog: &Progress, - registry: &[Box], -) { - let idx = (prog.current_level - 1) as usize; - let level = ®istry[idx]; - let g = level.generate(prog.current_seed); - let tier_label = prog.tier.map(|t| t.label()).unwrap_or("?"); - let title = format!( - " Level {}: {} Β· Tier: {} ", - level.id(), - level.name(), - tier_label - ); - let body = format!( - "\n{}\n\n{}\n\n[Tab] swap focus Β· [Ctrl-S] grade Β· [r] reset Β· [q] quit", - g.flavor, g.description - ); - frame.render_widget(screen_widget(title, body), area); -} - -fn render_result( - frame: &mut Frame, - area: Rect, - score: f64, - passed: bool, - level_name: &str, - prog: &Progress, -) { - let threshold = prog.tier.map(|t| t.threshold()).unwrap_or(0.0); - let (title, narration) = if passed { - ( - format!(" {level_name} cleared "), - "The door creaks open. You step deeper into the labyrinth.", - ) - } else { - ( - " Not yet ".to_string(), - "The labyrinth resists. Refine your YAML and try again.", - ) - }; - let body = format!( - "\nScore {:.0} % Threshold {:.0} %\n\n{}\n\n[Enter] continue", - score * 100.0, - threshold * 100.0, - narration, - ); - frame.render_widget(screen_widget(title, body), area); -} - -fn render_invalid_yaml(frame: &mut Frame, area: Rect, error: &str) { - let body = format!( - "\n{error}\n\nFix the YAML in the editor (it's still there) and press\n\ - [Ctrl-S] to retry. Attempts counter is not bumped.\n\n\ - [Enter] dismiss Β· [r] reset Β· [q] quit" - ); - frame.render_widget( - screen_widget(" Couldn't parse your YAML ".to_string(), body), - area, - ); + v.push(String::new()); + v.push(" ↑/↓ choose Β· [Enter] confirm Β· [PgUp/PgDn] scroll Β· [q] quit".to_string()); + v } fn render_reset_confirm(frame: &mut Frame, area: Rect) { - let body = - "\nWipe progress and start over?\n\n[y] yes, wipe Β· any other key cancels".to_string(); + let body = "\nπŸ’€ Wipe progress and start over?\n\n[y] yes, wipe Β· any other key cancels" + .to_string(); frame.render_widget(screen_widget(" Reset? ".to_string(), body), area); } -fn render_completed(frame: &mut Frame, area: Rect) { - let body = - "\nYou cleared the YAML labyrinth.\n\n[r] reset and replay Β· [q] quit".to_string(); - frame.render_widget(screen_widget(" Labyrinth complete ".to_string(), body), area); +fn render_log( + frame: &mut Frame, + area: Rect, + log: &HistoryLog, + trailing_prompt: Option<&[String]>, +) { + // Flatten log entries into a single line stream, blank line between each. + let mut all_lines: Vec = Vec::new(); + for (i, entry) in log.entries.iter().enumerate() { + if i > 0 { + all_lines.push(String::new()); + } + all_lines.extend(entry_lines(entry)); + } + // Append the transient prompt (Welcome / TierSelect) as the bottom block. + // It's part of the bottom-anchored view but not part of the log itself. + if let Some(prompt) = trailing_prompt { + if !all_lines.is_empty() { + all_lines.push(String::new()); + } + all_lines.extend_from_slice(prompt); + } + + // Pick a visible slice anchored to the bottom, offset by `log.scroll`. + let visible_h = (area.height as usize).saturating_sub(2); // borders + let total = all_lines.len(); + let max_scroll = total.saturating_sub(visible_h); + let scroll = log.scroll.min(max_scroll); + let end = total.saturating_sub(scroll); + let start = end.saturating_sub(visible_h); + let slice = &all_lines[start..end]; + + // Pad the top so the slice sits flush with the bottom of the column. + let pad = visible_h.saturating_sub(slice.len()); + let mut body_lines: Vec = vec![String::new(); pad]; + body_lines.extend_from_slice(slice); + let body = body_lines.join("\n"); + + let title = if log.scroll > 0 { + format!(" πŸ“œ Labyrinth log Β· scrolled {}/{} ", scroll, max_scroll) + } else { + " πŸ“œ Labyrinth log ".to_string() + }; + frame.render_widget(screen_widget(title, body), area); +} + +fn entry_lines(entry: &LogEntry) -> Vec { + match entry { + LogEntry::Intro => vec![ + "πŸŒ€ YAMLabyrinth β€” learn YAML by writing your way through.".to_string(), + String::new(), + " Each chamber reveals a target. Match it with YAML in the editor".to_string(), + " on the right, then press Ctrl-S to grade your attempt.".to_string(), + String::new(), + " ⌨ Controls".to_string(), + " [Tab] swap focus between log and editor".to_string(), + " [Ctrl-S] grade your YAML".to_string(), + " [↑/↓] scroll the log (when log is focused)".to_string(), + " [PgUp/PgDn] scroll faster".to_string(), + " [r] reset progress".to_string(), + " [q] / [Ctrl-Q] quit".to_string(), + ], + LogEntry::TierChoice { chosen } => { + let rows = [ + ("πŸ₯‰", "Easy", "70 %", "forgive small slips"), + ("πŸ₯ˆ", "Medium", "80 %", "most details must match"), + ("πŸ₯‡", "Hard", "95 %", "only near-perfect passes"), + ]; + let mut v = vec!["🎚 Difficulty".to_string(), String::new()]; + for (i, (emoji, name, pct, hint)) in rows.iter().enumerate() { + let mark = if i == *chosen as usize { " ← chosen" } else { "" }; + v.push(format!(" {emoji} {name:<7} ({pct}) {hint}{mark}")); + } + v + } + LogEntry::LevelPrompt { + level_id, + level_name, + tier_name, + flavor, + description, + } => { + let mut v = vec![ + format!("πŸ—Ί Level {} β€” {} ({})", level_id, level_name, tier_name), + flavor.clone(), + String::new(), + ]; + for line in description.lines() { + v.push(line.to_string()); + } + v.push(String::new()); + v.push( + " ⌨ [Ctrl-S] grade Β· [Tab] swap Β· [↑/↓] scroll Β· [r] reset Β· [q] quit" + .to_string(), + ); + v + } + LogEntry::ResultPass { + level_name, + score, + threshold, + } => vec![format!( + " πŸ— {} cleared at {:.0} % (threshold {:.0} %) ✨", + level_name, + score * 100.0, + threshold * 100.0 + )], + LogEntry::ResultFail { + level_name, + score, + threshold, + } => vec![ + format!( + " πŸ•Έ {} not yet β€” {:.0} % (threshold {:.0} %)", + level_name, + score * 100.0, + threshold * 100.0 + ), + " Refine your YAML and press Ctrl-S to retry.".to_string(), + ], + LogEntry::InvalidYaml { error } => vec![ + " ⚠ Couldn't parse YAML".to_string(), + format!(" {error}"), + " Fix it in the editor and press Ctrl-S to retry.".to_string(), + ], + LogEntry::Completed => vec![ + " πŸ† Labyrinth complete!".to_string(), + " [r] reset and replay Β· [q] quit".to_string(), + ], + } } fn render_editor(frame: &mut Frame, area: Rect, editor: &Editor, focused: bool) { let title = if focused { - " Your YAML * ".to_string() + " πŸ“ Your YAML * ".to_string() } else { - " Your YAML ".to_string() + " πŸ“ Your YAML ".to_string() }; let body = if focused { let (cr, cc) = editor.cursor; @@ -592,6 +735,9 @@ fn render_editor(frame: &mut Frame, area: Rect, editor: &Editor, focused: bool) } fn render_editor_inactive(frame: &mut Frame, area: Rect) { - let body = "\n The editor opens when you start a level.".to_string(); - frame.render_widget(screen_widget(" Editor (inactive) ".to_string(), body), area); + let body = "\n πŸͺΆ The quill rests. Pick a tier first…".to_string(); + frame.render_widget( + screen_widget(" πŸ“ Editor (inactive) ".to_string(), body), + area, + ); }