Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
299 lines
8.0 KiB
Rust
299 lines
8.0 KiB
Rust
#![cfg(feature = "std")]
|
|
|
|
use std::io::Write;
|
|
|
|
use crate::error::ParserError;
|
|
use crate::stream::Stream;
|
|
use crate::{Parser, Result};
|
|
|
|
pub(crate) struct Trace<P, D, I, O, E>
|
|
where
|
|
P: Parser<I, O, E>,
|
|
I: Stream,
|
|
D: std::fmt::Display,
|
|
E: ParserError<I>,
|
|
{
|
|
parser: P,
|
|
name: D,
|
|
call_count: usize,
|
|
marker: core::marker::PhantomData<(I, O, E)>,
|
|
}
|
|
|
|
impl<P, D, I, O, E> Trace<P, D, I, O, E>
|
|
where
|
|
P: Parser<I, O, E>,
|
|
I: Stream,
|
|
D: std::fmt::Display,
|
|
E: ParserError<I>,
|
|
{
|
|
#[inline(always)]
|
|
pub(crate) fn new(parser: P, name: D) -> Self {
|
|
Self {
|
|
parser,
|
|
name,
|
|
call_count: 0,
|
|
marker: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<P, D, I, O, E> Parser<I, O, E> for Trace<P, D, I, O, E>
|
|
where
|
|
P: Parser<I, O, E>,
|
|
I: Stream,
|
|
D: std::fmt::Display,
|
|
E: ParserError<I>,
|
|
{
|
|
#[inline]
|
|
fn parse_next(&mut self, i: &mut I) -> Result<O, E> {
|
|
let depth = Depth::new();
|
|
let original = i.checkpoint();
|
|
start(*depth, &self.name, self.call_count, i);
|
|
|
|
let res = self.parser.parse_next(i);
|
|
|
|
let consumed = i.offset_from(&original);
|
|
let severity = Severity::with_result(&res);
|
|
end(*depth, &self.name, self.call_count, consumed, severity);
|
|
self.call_count += 1;
|
|
|
|
res
|
|
}
|
|
}
|
|
|
|
pub(crate) struct Depth {
|
|
depth: usize,
|
|
inc: bool,
|
|
}
|
|
|
|
impl Depth {
|
|
pub(crate) fn new() -> Self {
|
|
let depth = DEPTH.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
let inc = true;
|
|
Self { depth, inc }
|
|
}
|
|
|
|
pub(crate) fn existing() -> Self {
|
|
let depth = DEPTH.load(std::sync::atomic::Ordering::SeqCst);
|
|
let inc = false;
|
|
Self { depth, inc }
|
|
}
|
|
}
|
|
|
|
impl Drop for Depth {
|
|
fn drop(&mut self) {
|
|
if self.inc {
|
|
let _ = DEPTH.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AsRef<usize> for Depth {
|
|
#[inline(always)]
|
|
fn as_ref(&self) -> &usize {
|
|
&self.depth
|
|
}
|
|
}
|
|
|
|
impl core::ops::Deref for Depth {
|
|
type Target = usize;
|
|
|
|
#[inline(always)]
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.depth
|
|
}
|
|
}
|
|
|
|
static DEPTH: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
|
|
|
pub(crate) enum Severity {
|
|
Success,
|
|
Backtrack,
|
|
Cut,
|
|
Incomplete,
|
|
}
|
|
|
|
impl Severity {
|
|
pub(crate) fn with_result<T, I: Stream, E: ParserError<I>>(result: &Result<T, E>) -> Self {
|
|
match result {
|
|
Ok(_) => Self::Success,
|
|
Err(e) if e.is_backtrack() => Self::Backtrack,
|
|
Err(e) if e.is_incomplete() => Self::Incomplete,
|
|
_ => Self::Cut,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn start<I: Stream>(
|
|
depth: usize,
|
|
name: &dyn core::fmt::Display,
|
|
count: usize,
|
|
input: &I,
|
|
) {
|
|
let gutter_style = anstyle::Style::new().bold();
|
|
let input_style = anstyle::Style::new().underline();
|
|
let eof_style = anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Cyan.into()));
|
|
|
|
let (call_width, input_width) = column_widths();
|
|
|
|
let count = if 0 < count {
|
|
format!(":{count}")
|
|
} else {
|
|
"".to_owned()
|
|
};
|
|
let call_column = format!("{:depth$}> {name}{count}", "");
|
|
|
|
// The debug version of `slice` might be wider, either due to rendering one byte as two nibbles or
|
|
// escaping in strings.
|
|
let mut debug_slice = format!("{:?}", crate::util::from_fn(|f| input.trace(f)));
|
|
let (debug_slice, eof) = if let Some(debug_offset) = debug_slice
|
|
.char_indices()
|
|
.enumerate()
|
|
.find_map(|(pos, (offset, _))| (input_width <= pos).then_some(offset))
|
|
{
|
|
debug_slice.truncate(debug_offset);
|
|
let eof = "";
|
|
(debug_slice, eof)
|
|
} else {
|
|
let eof = if debug_slice.chars().count() < input_width {
|
|
"∅"
|
|
} else {
|
|
""
|
|
};
|
|
(debug_slice, eof)
|
|
};
|
|
|
|
let writer = anstream::stderr();
|
|
let mut writer = writer.lock();
|
|
let _ = writeln!(
|
|
writer,
|
|
"{call_column:call_width$} {gutter_style}|{gutter_reset} {input_style}{debug_slice}{input_reset}{eof_style}{eof}{eof_reset}",
|
|
gutter_style=gutter_style.render(),
|
|
gutter_reset=gutter_style.render_reset(),
|
|
input_style=input_style.render(),
|
|
input_reset=input_style.render_reset(),
|
|
eof_style=eof_style.render(),
|
|
eof_reset=eof_style.render_reset(),
|
|
);
|
|
}
|
|
|
|
pub(crate) fn end(
|
|
depth: usize,
|
|
name: &dyn core::fmt::Display,
|
|
count: usize,
|
|
consumed: usize,
|
|
severity: Severity,
|
|
) {
|
|
let gutter_style = anstyle::Style::new().bold();
|
|
|
|
let (call_width, _) = column_widths();
|
|
|
|
let count = if 0 < count {
|
|
format!(":{count}")
|
|
} else {
|
|
"".to_owned()
|
|
};
|
|
let call_column = format!("{:depth$}< {name}{count}", "");
|
|
|
|
let (status_style, status) = match severity {
|
|
Severity::Success => {
|
|
let style = anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Green.into()));
|
|
let status = format!("+{consumed}");
|
|
(style, status)
|
|
}
|
|
Severity::Backtrack => (
|
|
anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Yellow.into())),
|
|
"backtrack".to_owned(),
|
|
),
|
|
Severity::Cut => (
|
|
anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Red.into())),
|
|
"cut".to_owned(),
|
|
),
|
|
Severity::Incomplete => (
|
|
anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Red.into())),
|
|
"incomplete".to_owned(),
|
|
),
|
|
};
|
|
|
|
let writer = anstream::stderr();
|
|
let mut writer = writer.lock();
|
|
let _ = writeln!(
|
|
writer,
|
|
"{status_style}{call_column:call_width$}{status_reset} {gutter_style}|{gutter_reset} {status_style}{status}{status_reset}",
|
|
gutter_style=gutter_style.render(),
|
|
gutter_reset=gutter_style.render_reset(),
|
|
status_style=status_style.render(),
|
|
status_reset=status_style.render_reset(),
|
|
);
|
|
}
|
|
|
|
pub(crate) fn result(depth: usize, name: &dyn core::fmt::Display, severity: Severity) {
|
|
let gutter_style = anstyle::Style::new().bold();
|
|
|
|
let (call_width, _) = column_widths();
|
|
|
|
let call_column = format!("{:depth$}| {name}", "");
|
|
|
|
let (status_style, status) = match severity {
|
|
Severity::Success => (
|
|
anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Green.into())),
|
|
"",
|
|
),
|
|
Severity::Backtrack => (
|
|
anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Yellow.into())),
|
|
"backtrack",
|
|
),
|
|
Severity::Cut => (
|
|
anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Red.into())),
|
|
"cut",
|
|
),
|
|
Severity::Incomplete => (
|
|
anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Red.into())),
|
|
"incomplete",
|
|
),
|
|
};
|
|
|
|
let writer = anstream::stderr();
|
|
let mut writer = writer.lock();
|
|
let _ = writeln!(
|
|
writer,
|
|
"{status_style}{call_column:call_width$}{status_reset} {gutter_style}|{gutter_reset} {status_style}{status}{status_reset}",
|
|
gutter_style=gutter_style.render(),
|
|
gutter_reset=gutter_style.render_reset(),
|
|
status_style=status_style.render(),
|
|
status_reset=status_style.render_reset(),
|
|
);
|
|
}
|
|
|
|
fn column_widths() -> (usize, usize) {
|
|
let term_width = term_width();
|
|
|
|
let min_call_width = 40;
|
|
let min_input_width = 20;
|
|
let decor_width = 3;
|
|
let extra_width = term_width.saturating_sub(min_call_width + min_input_width + decor_width);
|
|
let call_width = min_call_width + 2 * extra_width / 3;
|
|
let input_width = min_input_width + extra_width / 3;
|
|
|
|
(call_width, input_width)
|
|
}
|
|
|
|
fn term_width() -> usize {
|
|
columns_env().or_else(query_width).unwrap_or(80)
|
|
}
|
|
|
|
fn query_width() -> Option<usize> {
|
|
use is_terminal_polyfill::IsTerminal;
|
|
if std::io::stderr().is_terminal() {
|
|
terminal_size::terminal_size().map(|(w, _h)| w.0.into())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn columns_env() -> Option<usize> {
|
|
std::env::var("COLUMNS")
|
|
.ok()
|
|
.and_then(|c| c.parse::<usize>().ok())
|
|
}
|