feat: add display UI with splash screen, dashboard, and RSSI sparkline
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>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
@@ -0,0 +1,35 @@
|
||||
use pulldown_cmark::{html, BrokenLink, Options, Parser};
|
||||
|
||||
fn main() {
|
||||
let input: &str = "Hello world, check out [my website][].";
|
||||
println!("Parsing the following markdown string:\n{}", input);
|
||||
|
||||
// Setup callback that sets the URL and title when it encounters
|
||||
// a reference to our home page.
|
||||
let callback = |broken_link: BrokenLink| {
|
||||
if broken_link.reference.as_ref() == "my website" {
|
||||
println!(
|
||||
"Replacing the markdown `{}` of type {:?} with a working link",
|
||||
&input[broken_link.span], broken_link.link_type,
|
||||
);
|
||||
Some(("http://example.com".into(), "my example website".into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Create a parser with our callback function for broken links.
|
||||
let parser = Parser::new_with_broken_link_callback(input, Options::empty(), Some(callback));
|
||||
|
||||
// Write to String buffer.
|
||||
let mut html_output: String = String::with_capacity(input.len() * 3 / 2);
|
||||
html::push_html(&mut html_output, parser);
|
||||
|
||||
// Check that the output is what we expected.
|
||||
let expected_html: &str =
|
||||
"<p>Hello world, check out <a href=\"http://example.com\" title=\"my example website\">my website</a>.</p>\n";
|
||||
assert_eq!(expected_html, &html_output);
|
||||
|
||||
// Write result to stdout.
|
||||
println!("\nHTML output:\n{}", &html_output);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
use std::io::Write as _;
|
||||
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, TagEnd};
|
||||
|
||||
fn main() {
|
||||
let markdown_input: &str = "This is Peter on .";
|
||||
println!("Parsing the following markdown string:\n{}", markdown_input);
|
||||
|
||||
// Set up parser. We can treat is as any other iterator. We replace Peter by John
|
||||
// and image by its alt text.
|
||||
let parser = Parser::new_ext(markdown_input, Options::empty())
|
||||
.map(|event| match event {
|
||||
Event::Text(text) => Event::Text(text.replace("Peter", "John").into()),
|
||||
_ => event,
|
||||
})
|
||||
.filter(|event| match event {
|
||||
Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image) => false,
|
||||
_ => true,
|
||||
});
|
||||
|
||||
// Write to anything implementing the `Write` trait. This could also be a file
|
||||
// or network socket.
|
||||
let stdout = std::io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
handle.write_all(b"\nHTML output:\n").unwrap();
|
||||
html::write_html_io(&mut handle, parser).unwrap();
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
use std::io::Read;
|
||||
|
||||
use pulldown_cmark::{Event, Parser};
|
||||
|
||||
/// Show all events from the text on stdin.
|
||||
fn main() {
|
||||
let mut text = String::new();
|
||||
std::io::stdin().read_to_string(&mut text).unwrap();
|
||||
|
||||
eprintln!("{text:?} -> [");
|
||||
let mut width = 0;
|
||||
for event in Parser::new(&text) {
|
||||
if let Event::End(_) = event {
|
||||
width -= 2;
|
||||
}
|
||||
eprintln!(" {:width$}{event:?}", "");
|
||||
if let Event::Start(_) = event {
|
||||
width += 2;
|
||||
}
|
||||
}
|
||||
eprintln!("]");
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write as _;
|
||||
use std::io::Write as _;
|
||||
|
||||
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag, TagEnd};
|
||||
|
||||
/// This example shows how to do footnotes as bottom-notes, in the style of GitHub.
|
||||
fn main() {
|
||||
let markdown_input: &str = "This is an [^a] footnote [^a].\n\n[^a]: footnote contents";
|
||||
println!("Parsing the following markdown string:\n{}", markdown_input);
|
||||
|
||||
// To generate this style, you have to collect the footnotes at the end, while parsing.
|
||||
// You also need to count usages.
|
||||
let mut footnotes = Vec::new();
|
||||
let mut in_footnote = Vec::new();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
// ENABLE_FOOTNOTES is used in this example, but ENABLE_OLD_FOOTNOTES would work, too.
|
||||
let parser = Parser::new_ext(markdown_input, Options::ENABLE_FOOTNOTES)
|
||||
.filter_map(|event| {
|
||||
match event {
|
||||
Event::Start(Tag::FootnoteDefinition(_)) => {
|
||||
in_footnote.push(vec![event]);
|
||||
None
|
||||
}
|
||||
Event::End(TagEnd::FootnoteDefinition) => {
|
||||
let mut f = in_footnote.pop().unwrap();
|
||||
f.push(event);
|
||||
footnotes.push(f);
|
||||
None
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
let n = footnote_numbers.len() + 1;
|
||||
let (n, nr) = footnote_numbers.entry(name.clone()).or_insert((n, 0usize));
|
||||
*nr += 1;
|
||||
let html = Event::Html(format!(r##"<sup class="footnote-reference" id="fr-{name}-{nr}"><a href="#fn-{name}">[{n}]</a></sup>"##).into());
|
||||
if in_footnote.is_empty() {
|
||||
Some(html)
|
||||
} else {
|
||||
in_footnote.last_mut().unwrap().push(html);
|
||||
None
|
||||
}
|
||||
}
|
||||
_ if !in_footnote.is_empty() => {
|
||||
in_footnote.last_mut().unwrap().push(event);
|
||||
None
|
||||
}
|
||||
_ => Some(event),
|
||||
}
|
||||
});
|
||||
|
||||
// Write to anything implementing the `Write` trait. This could also be a file
|
||||
// or network socket.
|
||||
let stdout = std::io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
handle.write_all(b"\nHTML output:\n").unwrap();
|
||||
html::write_html_io(&mut handle, parser).unwrap();
|
||||
|
||||
// To make the footnotes look right, we need to sort them by their appearance order, not by
|
||||
// the in-tree order of their actual definitions. Unused items are omitted entirely.
|
||||
//
|
||||
// For example, this code:
|
||||
//
|
||||
// test [^1] [^2]
|
||||
// [^2]: second used, first defined
|
||||
// [^1]: test
|
||||
//
|
||||
// Gets rendered like *this* if you copy it into a GitHub comment box:
|
||||
//
|
||||
// <p>test <sup>[1]</sup> <sup>[2]</sup></p>
|
||||
// <hr>
|
||||
// <ol>
|
||||
// <li>test ↩</li>
|
||||
// <li>second used, first defined ↩</li>
|
||||
// </ol>
|
||||
if !footnotes.is_empty() {
|
||||
footnotes.retain(|f| match f.first() {
|
||||
Some(Event::Start(Tag::FootnoteDefinition(name))) => {
|
||||
footnote_numbers.get(name).unwrap_or(&(0, 0)).1 != 0
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
footnotes.sort_by_cached_key(|f| match f.first() {
|
||||
Some(Event::Start(Tag::FootnoteDefinition(name))) => {
|
||||
footnote_numbers.get(name).unwrap_or(&(0, 0)).0
|
||||
}
|
||||
_ => unreachable!(),
|
||||
});
|
||||
handle
|
||||
.write_all(b"<hr><ol class=\"footnotes-list\">\n")
|
||||
.unwrap();
|
||||
html::write_html_io(
|
||||
&mut handle,
|
||||
footnotes.into_iter().flat_map(|fl| {
|
||||
// To write backrefs, the name needs kept until the end of the footnote definition.
|
||||
let mut name = CowStr::from("");
|
||||
// Backrefs are included in the final paragraph of the footnote, if it's normal text.
|
||||
// For example, this DOM can be produced:
|
||||
//
|
||||
// Markdown:
|
||||
//
|
||||
// five [^feet].
|
||||
//
|
||||
// [^feet]:
|
||||
// A foot is defined, in this case, as 0.3048 m.
|
||||
//
|
||||
// Historically, the foot has not been defined this way, corresponding to many
|
||||
// subtly different units depending on the location.
|
||||
//
|
||||
// HTML:
|
||||
//
|
||||
// <p>five <sup class="footnote-reference" id="fr-feet-1"><a href="#fn-feet">[1]</a></sup>.</p>
|
||||
//
|
||||
// <ol class="footnotes-list">
|
||||
// <li id="fn-feet">
|
||||
// <p>A foot is defined, in this case, as 0.3048 m.</p>
|
||||
// <p>Historically, the foot has not been defined this way, corresponding to many
|
||||
// subtly different units depending on the location. <a href="#fr-feet-1">↩</a></p>
|
||||
// </li>
|
||||
// </ol>
|
||||
//
|
||||
// This is mostly a visual hack, so that footnotes use less vertical space.
|
||||
//
|
||||
// If there is no final paragraph, such as a tabular, list, or image footnote, it gets
|
||||
// pushed after the last tag instead.
|
||||
let mut has_written_backrefs = false;
|
||||
let fl_len = fl.len();
|
||||
let footnote_numbers = &footnote_numbers;
|
||||
fl.into_iter().enumerate().map(move |(i, f)| match f {
|
||||
Event::Start(Tag::FootnoteDefinition(current_name)) => {
|
||||
name = current_name;
|
||||
has_written_backrefs = false;
|
||||
Event::Html(format!(r##"<li id="fn-{name}">"##).into())
|
||||
}
|
||||
Event::End(TagEnd::FootnoteDefinition) | Event::End(TagEnd::Paragraph)
|
||||
if !has_written_backrefs && i >= fl_len - 2 =>
|
||||
{
|
||||
let usage_count = footnote_numbers.get(&name).unwrap().1;
|
||||
let mut end = String::with_capacity(
|
||||
name.len() + (r##" <a href="#fr--1">↩</a></li>"##.len() * usage_count),
|
||||
);
|
||||
for usage in 1..=usage_count {
|
||||
if usage == 1 {
|
||||
write!(&mut end, r##" <a href="#fr-{name}-{usage}">↩</a>"##)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(&mut end, r##" <a href="#fr-{name}-{usage}">↩{usage}</a>"##)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
has_written_backrefs = true;
|
||||
if f == Event::End(TagEnd::FootnoteDefinition) {
|
||||
end.push_str("</li>\n");
|
||||
} else {
|
||||
end.push_str("</p>\n");
|
||||
}
|
||||
Event::Html(end.into())
|
||||
}
|
||||
Event::End(TagEnd::FootnoteDefinition) => Event::Html("</li>\n".into()),
|
||||
Event::FootnoteReference(_) => unreachable!("converted to HTML earlier"),
|
||||
f => f,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
handle.write_all(b"</ol>\n").unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use pulldown_cmark::{html, Event, Parser};
|
||||
|
||||
fn main() {
|
||||
let markdown_input = "# Example Heading\nExample paragraph with **lorem** _ipsum_ text.";
|
||||
println!(
|
||||
"\nParsing the following markdown string:\n{}\n",
|
||||
markdown_input
|
||||
);
|
||||
|
||||
// Set up the parser. We can treat is as any other iterator.
|
||||
// For each event, we print its details, such as the tag or string.
|
||||
// This filter simply returns the same event without any changes;
|
||||
// you can compare the `event-filter` example which alters the output.
|
||||
let parser = Parser::new(markdown_input).map(|event| {
|
||||
match &event {
|
||||
Event::Start(tag) => println!("Start: {:?}", tag),
|
||||
Event::End(tag) => println!("End: {:?}", tag),
|
||||
Event::Html(s) => println!("Html: {:?}", s),
|
||||
Event::InlineHtml(s) => println!("InlineHtml: {:?}", s),
|
||||
Event::Text(s) => println!("Text: {:?}", s),
|
||||
Event::Code(s) => println!("Code: {:?}", s),
|
||||
Event::DisplayMath(s) => println!("DisplayMath: {:?}", s),
|
||||
Event::InlineMath(s) => println!("Math: {:?}", s),
|
||||
Event::FootnoteReference(s) => println!("FootnoteReference: {:?}", s),
|
||||
Event::TaskListMarker(b) => println!("TaskListMarker: {:?}", b),
|
||||
Event::SoftBreak => println!("SoftBreak"),
|
||||
Event::HardBreak => println!("HardBreak"),
|
||||
Event::Rule => println!("Rule"),
|
||||
};
|
||||
event
|
||||
});
|
||||
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser);
|
||||
println!("\nHTML output:\n{}\n", &html_output);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use pulldown_cmark::{Event, Options, Parser, Tag};
|
||||
|
||||
fn main() {
|
||||
let markdown_input = concat!(
|
||||
"# My Heading\n",
|
||||
"\n",
|
||||
"My paragraph.\n",
|
||||
"\n",
|
||||
"* a\n",
|
||||
"* b\n",
|
||||
"* c\n",
|
||||
"\n",
|
||||
"1. d\n",
|
||||
"2. e\n",
|
||||
"3. f\n",
|
||||
"\n",
|
||||
"> my block quote\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"my code block\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"*emphasis*\n",
|
||||
"**strong**\n",
|
||||
"~~strikethrough~~\n",
|
||||
"[My Link](http://example.com)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"| a | b |\n",
|
||||
"| - | - |\n",
|
||||
"| c | d |\n",
|
||||
"\n",
|
||||
"hello[^1]\n",
|
||||
"[^1]: my footnote\n",
|
||||
);
|
||||
println!(
|
||||
"\nParsing the following markdown string:\n{}\n",
|
||||
markdown_input
|
||||
);
|
||||
|
||||
// Set up the parser. We can treat is as any other iterator.
|
||||
// For each event, we print its details, such as the tag or string.
|
||||
// This filter simply returns the same event without any changes;
|
||||
// you can compare the `event-filter` example which alters the output.
|
||||
let parser = Parser::new_ext(markdown_input, Options::all()).map(|event| {
|
||||
match &event {
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::HtmlBlock => println!("HtmlBlock"),
|
||||
Tag::Heading {
|
||||
level,
|
||||
id,
|
||||
classes,
|
||||
attrs,
|
||||
} => println!(
|
||||
"Heading heading_level: {} fragment identifier: {:?} classes: {:?} attrs: {:?}",
|
||||
level, id, classes, attrs
|
||||
),
|
||||
Tag::Paragraph => println!("Paragraph"),
|
||||
Tag::List(ordered_list_first_item_number) => println!(
|
||||
"List ordered_list_first_item_number: {:?}",
|
||||
ordered_list_first_item_number
|
||||
),
|
||||
Tag::Item => println!("Item (this is a list item)"),
|
||||
Tag::Emphasis => println!("Emphasis (this is a span tag)"),
|
||||
Tag::Strong => println!("Strong (this is a span tag)"),
|
||||
Tag::Strikethrough => println!("Strikethrough (this is a span tag)"),
|
||||
Tag::BlockQuote(kind) => println!("BlockQuote ({:?})", kind),
|
||||
Tag::CodeBlock(code_block_kind) => {
|
||||
println!("CodeBlock code_block_kind: {:?}", code_block_kind)
|
||||
}
|
||||
Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
} => println!(
|
||||
"Link link_type: {:?} url: {} title: {} id: {}",
|
||||
link_type, dest_url, title, id
|
||||
),
|
||||
Tag::Image {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
} => println!(
|
||||
"Image link_type: {:?} url: {} title: {} id: {}",
|
||||
link_type, dest_url, title, id
|
||||
),
|
||||
Tag::Table(column_text_alignment_list) => println!(
|
||||
"Table column_text_alignment_list: {:?}",
|
||||
column_text_alignment_list
|
||||
),
|
||||
Tag::TableHead => println!("TableHead (contains TableRow tags"),
|
||||
Tag::TableRow => println!("TableRow (contains TableCell tags)"),
|
||||
Tag::TableCell => println!("TableCell (contains inline tags)"),
|
||||
Tag::FootnoteDefinition(label) => println!("FootnoteDefinition label: {}", label),
|
||||
Tag::MetadataBlock(kind) => println!("MetadataBlock: {:?}", kind),
|
||||
},
|
||||
_ => (),
|
||||
};
|
||||
event
|
||||
});
|
||||
|
||||
let mut html_output = String::new();
|
||||
pulldown_cmark::html::push_html(&mut html_output, parser);
|
||||
println!("\nHTML output:\n{}\n", &html_output);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use pulldown_cmark::{html, Options, Parser};
|
||||
|
||||
fn main() {
|
||||
let markdown_input: &str = "Hello world, this is a ~~complicated~~ *very simple* example.";
|
||||
println!("Parsing the following markdown string:\n{}", markdown_input);
|
||||
|
||||
// Set up options and parser. Strikethroughs are not part of the CommonMark standard
|
||||
// and we therefore must enable it explicitly.
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
let parser = Parser::new_ext(markdown_input, options);
|
||||
|
||||
// Write to String buffer.
|
||||
let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
|
||||
html::push_html(&mut html_output, parser);
|
||||
|
||||
// Check that the output is what we expected.
|
||||
let expected_html: &str =
|
||||
"<p>Hello world, this is a <del>complicated</del> <em>very simple</em> example.</p>\n";
|
||||
assert_eq!(expected_html, &html_output);
|
||||
|
||||
// Write result to stdout.
|
||||
println!("\nHTML output:\n{}", &html_output);
|
||||
}
|
||||
Reference in new issue
Block a user