ui: validity indicator rendering + warning theme colour (ADR-0027 step D)
Adds the `[ERR]` / `[WRN]` validity indicator to the input row. `App` gains `input_indicator: Option<Severity>` (the runtime owns its timing — step E) and a pure `input_validity_verdict()` query that runs `input_verdict` in simple mode only (advanced mode is raw SQL, ADR-0027 §7). `render_input_panel` reserves the rightmost six columns of the input row unconditionally (ADR-0027 §4) — a five-column label plus a one-column gap — so the typed command never shifts sideways when the indicator appears or hides. The label renders only when `input_indicator` is set: `[ERR]` in `theme.error`, `[WRN]` in the new amber `theme.warning` (defined for both light and dark themes). The indicator is not yet wired live — `input_indicator` stays `None` until the debounce lands (step E). Covered by a render test and the theme contrast test; the input-panel snapshot is updated for the six-column reservation.
This commit is contained in:
+23
@@ -71,6 +71,13 @@ pub struct App {
|
|||||||
pub input_cursor: usize,
|
pub input_cursor: usize,
|
||||||
pub output: VecDeque<OutputLine>,
|
pub output: VecDeque<OutputLine>,
|
||||||
pub hint: Option<String>,
|
pub hint: Option<String>,
|
||||||
|
/// The validity indicator's currently-visible verdict
|
||||||
|
/// (ADR-0027). `None` means the indicator shows nothing —
|
||||||
|
/// the input is clean, or it is hidden mid-typing while the
|
||||||
|
/// debounce settles. The runtime owns the timing: it clears
|
||||||
|
/// this on a keystroke and sets it from
|
||||||
|
/// [`App::input_validity_verdict`] once typing pauses.
|
||||||
|
pub input_indicator: Option<crate::dsl::walker::Severity>,
|
||||||
pub tables: Vec<String>,
|
pub tables: Vec<String>,
|
||||||
/// Last successfully described table, shown in the output
|
/// Last successfully described table, shown in the output
|
||||||
/// pane until the next DDL operation.
|
/// pane until the next DDL operation.
|
||||||
@@ -235,6 +242,7 @@ impl App {
|
|||||||
input_cursor: 0,
|
input_cursor: 0,
|
||||||
output: VecDeque::with_capacity(OUTPUT_CAPACITY),
|
output: VecDeque::with_capacity(OUTPUT_CAPACITY),
|
||||||
hint: None,
|
hint: None,
|
||||||
|
input_indicator: None,
|
||||||
tables: Vec::new(),
|
tables: Vec::new(),
|
||||||
current_table: None,
|
current_table: None,
|
||||||
history: Vec::new(),
|
history: Vec::new(),
|
||||||
@@ -304,6 +312,21 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The validity-indicator verdict for the current input
|
||||||
|
/// (ADR-0027 §3). `None` when the input would run clean.
|
||||||
|
///
|
||||||
|
/// Computed only in simple mode — advanced mode is raw SQL,
|
||||||
|
/// which the DSL walker does not parse (ADR-0027 §7). A
|
||||||
|
/// pure query the runtime calls once the typing debounce
|
||||||
|
/// settles; the result is stored in `input_indicator`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn input_validity_verdict(&self) -> Option<crate::dsl::walker::Severity> {
|
||||||
|
if !matches!(self.effective_mode(), EffectiveMode::Simple) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
crate::dsl::walker::input_verdict(&self.input, Some(&self.schema_cache))
|
||||||
|
}
|
||||||
|
|
||||||
/// Process one event from the runtime, mutating state and
|
/// Process one event from the runtime, mutating state and
|
||||||
/// returning any actions for the runtime to enact.
|
/// returning any actions for the runtime to enact.
|
||||||
pub fn update(&mut self, event: AppEvent) -> Vec<Action> {
|
pub fn update(&mut self, event: AppEvent) -> Vec<Action> {
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ expression: snapshot
|
|||||||
│ ││ │
|
│ ││ │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||||
│ ││insert into T values (1, 'hi', null) --all-rows $ │
|
│ ││insert into T values (1, 'hi', null) --all-r │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ Hint ────────────────────────────────────────────╮
|
│ │╭ Hint ────────────────────────────────────────────╮
|
||||||
│ ││after `insert into T values (1, 'hi', null)`, │
|
│ ││after `insert into T values (1, 'hi', null)`, │
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ pub struct Theme {
|
|||||||
pub mode_advanced: Color,
|
pub mode_advanced: Color,
|
||||||
pub system: Color,
|
pub system: Color,
|
||||||
pub error: Color,
|
pub error: Color,
|
||||||
|
/// Validity-indicator WARNING colour (ADR-0027 §4) — an
|
||||||
|
/// amber distinct from `error`'s red. Drives the `[WRN]`
|
||||||
|
/// label; `[ERR]` reuses `error`.
|
||||||
|
pub warning: Color,
|
||||||
// ---- Per-token-class colours (ADR-0022 §3) -------------------
|
// ---- Per-token-class colours (ADR-0022 §3) -------------------
|
||||||
pub tok_keyword: Color,
|
pub tok_keyword: Color,
|
||||||
pub tok_identifier: Color,
|
pub tok_identifier: Color,
|
||||||
@@ -62,6 +66,8 @@ impl Theme {
|
|||||||
mode_advanced: Color::Rgb(0xFF, 0x9E, 0x6B),
|
mode_advanced: Color::Rgb(0xFF, 0x9E, 0x6B),
|
||||||
system: Color::Rgb(0x9F, 0xD8, 0x91),
|
system: Color::Rgb(0x9F, 0xD8, 0x91),
|
||||||
error: Color::Rgb(0xFF, 0x6B, 0x6B),
|
error: Color::Rgb(0xFF, 0x6B, 0x6B),
|
||||||
|
warning: Color::Rgb(0xF5, 0xA9, 0x4B), // amber
|
||||||
|
|
||||||
// Token classes — distinct enough to tell apart at a
|
// Token classes — distinct enough to tell apart at a
|
||||||
// glance, quiet enough that 80-char lines don't read
|
// glance, quiet enough that 80-char lines don't read
|
||||||
// like a Christmas tree. Identifier and punct sit
|
// like a Christmas tree. Identifier and punct sit
|
||||||
@@ -92,6 +98,8 @@ impl Theme {
|
|||||||
mode_advanced: Color::Rgb(0xB0, 0x4A, 0x12),
|
mode_advanced: Color::Rgb(0xB0, 0x4A, 0x12),
|
||||||
system: Color::Rgb(0x2E, 0x7C, 0x3C),
|
system: Color::Rgb(0x2E, 0x7C, 0x3C),
|
||||||
error: Color::Rgb(0xC0, 0x39, 0x2B),
|
error: Color::Rgb(0xC0, 0x39, 0x2B),
|
||||||
|
warning: Color::Rgb(0xA6, 0x5A, 0x00), // burnt amber
|
||||||
|
|
||||||
// Light-theme token palette: same intent as dark —
|
// Light-theme token palette: same intent as dark —
|
||||||
// identifier/punct close to fg/muted; warm tones for
|
// identifier/punct close to fg/muted; warm tones for
|
||||||
// literals + flags; cool accent for keyword.
|
// literals + flags; cool accent for keyword.
|
||||||
@@ -144,6 +152,7 @@ mod tests {
|
|||||||
("tok_string", t.tok_string),
|
("tok_string", t.tok_string),
|
||||||
("tok_flag", t.tok_flag),
|
("tok_flag", t.tok_flag),
|
||||||
("tok_error", t.tok_error),
|
("tok_error", t.tok_error),
|
||||||
|
("warning", t.warning),
|
||||||
] {
|
] {
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
c, t.bg,
|
c, t.bg,
|
||||||
@@ -161,6 +170,7 @@ mod tests {
|
|||||||
("tok_string", t.tok_string),
|
("tok_string", t.tok_string),
|
||||||
("tok_flag", t.tok_flag),
|
("tok_flag", t.tok_flag),
|
||||||
("tok_error", t.tok_error),
|
("tok_error", t.tok_error),
|
||||||
|
("warning", t.warning),
|
||||||
] {
|
] {
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
c, t.bg,
|
c, t.bg,
|
||||||
|
|||||||
@@ -643,8 +643,37 @@ fn render_input_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rec
|
|||||||
plain_input_spans(&app.input, cursor, theme)
|
plain_input_spans(&app.input, cursor, theme)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let paragraph = Paragraph::new(Line::from(spans)).block(block);
|
// ADR-0027 §4: the rightmost six columns of the input row
|
||||||
frame.render_widget(paragraph, area);
|
// (a five-column label plus a one-column gap) are reserved
|
||||||
|
// unconditionally, so the text area is always
|
||||||
|
// `inner.width - 6` and the typed command never shifts
|
||||||
|
// sideways when the validity indicator appears or hides.
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
let text_area = Rect {
|
||||||
|
width: inner.width.saturating_sub(6),
|
||||||
|
..inner
|
||||||
|
};
|
||||||
|
frame.render_widget(Paragraph::new(Line::from(spans)), text_area);
|
||||||
|
|
||||||
|
if let Some(severity) = app.input_indicator {
|
||||||
|
let (indicator_label, color) = match severity {
|
||||||
|
crate::dsl::walker::Severity::Error => ("[ERR]", theme.error),
|
||||||
|
crate::dsl::walker::Severity::Warning => ("[WRN]", theme.warning),
|
||||||
|
};
|
||||||
|
let label_area = Rect {
|
||||||
|
x: inner.x + inner.width.saturating_sub(5),
|
||||||
|
width: 5.min(inner.width),
|
||||||
|
..inner
|
||||||
|
};
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Line::from(Span::styled(
|
||||||
|
indicator_label,
|
||||||
|
Style::default().fg(color).add_modifier(Modifier::BOLD),
|
||||||
|
))),
|
||||||
|
label_area,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert `StyledRun`s into ratatui `Span`s borrowed from
|
/// Convert `StyledRun`s into ratatui `Span`s borrowed from
|
||||||
|
|||||||
@@ -595,3 +595,23 @@ fn dsl_failure_shows_friendly_error_in_output() {
|
|||||||
"error should include the friendly message:\n{rendered}"
|
"error should include the friendly message:\n{rendered}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validity_indicator_renders_err_and_wrn_labels() {
|
||||||
|
// ADR-0027 §4: the input row shows a `[ERR]` / `[WRN]`
|
||||||
|
// label at its right edge, or nothing when clean.
|
||||||
|
use rdbms_playground::dsl::walker::Severity;
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
let clean = rendered_text(&mut app, &Theme::dark(), 80, 24);
|
||||||
|
assert!(!clean.contains("[ERR]"), "clean input shows no label:\n{clean}");
|
||||||
|
assert!(!clean.contains("[WRN]"), "clean input shows no label:\n{clean}");
|
||||||
|
|
||||||
|
app.input_indicator = Some(Severity::Error);
|
||||||
|
let err = rendered_text(&mut app, &Theme::dark(), 80, 24);
|
||||||
|
assert!(err.contains("[ERR]"), "ERROR verdict shows [ERR]:\n{err}");
|
||||||
|
|
||||||
|
app.input_indicator = Some(Severity::Warning);
|
||||||
|
let wrn = rendered_text(&mut app, &Theme::dark(), 80, 24);
|
||||||
|
assert!(wrn.contains("[WRN]"), "WARNING verdict shows [WRN]:\n{wrn}");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user