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
+213
@@ -0,0 +1,213 @@
|
||||
use pulldown_cmark::{Parser, Event, Tag, TagEnd, CodeBlockKind};
|
||||
|
||||
fn as_lang_tokens(string: &str) -> impl Iterator<Item = &str> {
|
||||
// Pandoc, which Rust once used for generating documentation,
|
||||
// expects lang strings to be surrounded by `{}` and for each token
|
||||
// to be proceeded by a `.`. Since some of these lang strings are still
|
||||
// loose in the wild, we strip a pair of surrounding `{}` from the lang
|
||||
// string and a leading `.` from each token.
|
||||
|
||||
let string = string.trim();
|
||||
|
||||
let first = string.chars().next();
|
||||
let last = string.chars().last();
|
||||
|
||||
let string = if first == Some('{') && last == Some('}') {
|
||||
&string[1..string.len() - 1]
|
||||
} else {
|
||||
string
|
||||
};
|
||||
|
||||
string
|
||||
.split(|c| c == ',' || c == ' ' || c == '\t')
|
||||
.map(str::trim)
|
||||
.map(|token| token.strip_prefix('.').unwrap_or(token))
|
||||
.filter(|token| !token.is_empty())
|
||||
}
|
||||
|
||||
fn parse_lang(lang: &str) -> Option<(String, String)> {
|
||||
let our_prefix = "only_if(";
|
||||
|
||||
let mut has_our_attr = false;
|
||||
let mut key = String::new();
|
||||
let mut new_lang = String::new();
|
||||
|
||||
for token in as_lang_tokens(lang) {
|
||||
if token.starts_with(our_prefix) && token.ends_with(")") {
|
||||
has_our_attr = true;
|
||||
key = token[our_prefix.len() .. token.len()-1].to_string();
|
||||
continue;
|
||||
}
|
||||
|
||||
if !new_lang.is_empty() {
|
||||
new_lang.push_str(", ");
|
||||
}
|
||||
|
||||
new_lang.push_str(token);
|
||||
}
|
||||
|
||||
if has_our_attr {
|
||||
Some((key, new_lang))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_code(new_lang: &str, code: &str, indent: Option<&str>) -> String {
|
||||
let mut res = String::new();
|
||||
|
||||
res.push_str("\n``` ");
|
||||
res.push_str(new_lang);
|
||||
res.push_str("\n");
|
||||
res.push_str(code);
|
||||
res.push_str("```");
|
||||
|
||||
if let Some(indent) = indent {
|
||||
let mut first = true;
|
||||
res = res.lines().map(|s| {
|
||||
if first {
|
||||
first = false;
|
||||
return s.to_string()
|
||||
};
|
||||
|
||||
let mut res = String::new();
|
||||
res.push('\n');
|
||||
res.push_str(indent);
|
||||
res.push_str(s);
|
||||
|
||||
res
|
||||
}).collect();
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn get_indent_from_content(content: &str) -> Option<String> {
|
||||
let content = content.as_bytes();
|
||||
let mut len = content.len();
|
||||
|
||||
if !(content[len-1] == b'`' && content[len-2] == b'`' && content[len-3] == b'`') {
|
||||
return None;
|
||||
}
|
||||
|
||||
len -= 3;
|
||||
|
||||
let mut rres: Vec<u8> = vec![];
|
||||
|
||||
while len >= 1 {
|
||||
let b = content[len-1];
|
||||
|
||||
if b.is_ascii_whitespace() && !(b == b'\n' || b == b'\r') {
|
||||
rres.push(b);
|
||||
len -= 1;
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let res: String = rres.into_iter().rev().map(|b| b as char).collect();
|
||||
|
||||
Some(res)
|
||||
}
|
||||
|
||||
pub fn process_doctests(doc: &str, processor: impl Fn(&str, &str) -> Option<Option<String>>) -> Option<String> {
|
||||
let parser = Parser::new(doc);
|
||||
|
||||
let mut prev_offset = 0usize;
|
||||
let mut level = 0usize;
|
||||
let mut block_key = String::new();
|
||||
let mut block_new_lang = String::new();
|
||||
let mut inside_code = false;
|
||||
let mut code = String::new();
|
||||
let mut has_changes: bool = false;
|
||||
|
||||
let mut new_doc = String::new();
|
||||
|
||||
for (event, ref offset) in parser.into_offset_iter() {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(ref kind)) => {
|
||||
level += 1;
|
||||
|
||||
if level == 1 {
|
||||
match match kind {
|
||||
CodeBlockKind::Fenced(ref lang) => {
|
||||
parse_lang(lang)
|
||||
},
|
||||
CodeBlockKind::Indented => {
|
||||
None
|
||||
}
|
||||
} {
|
||||
Some((key, new_lang)) => {
|
||||
let mut new_start = offset.start;
|
||||
let mut success = false;
|
||||
while prev_offset < new_start {
|
||||
let b = doc.as_bytes()[new_start-1];
|
||||
if !b.is_ascii_whitespace() {
|
||||
break;
|
||||
}
|
||||
|
||||
if b == b'\n' {
|
||||
new_start -= 1;
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
new_start -= 1;
|
||||
}
|
||||
if !success {
|
||||
new_start = offset.start;
|
||||
}
|
||||
|
||||
new_doc.push_str(&doc[prev_offset..new_start]);
|
||||
prev_offset = new_start;
|
||||
|
||||
block_key = key;
|
||||
block_new_lang = new_lang;
|
||||
code.clear();
|
||||
inside_code = true;
|
||||
},
|
||||
None => {},
|
||||
};
|
||||
}
|
||||
},
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
if level == 1 && inside_code {
|
||||
let content = &doc[prev_offset..offset.end];
|
||||
prev_offset = offset.end;
|
||||
|
||||
|
||||
match processor(block_key.as_str(), code.as_str()) {
|
||||
Some(Some(new_code)) => {
|
||||
let indent = get_indent_from_content(content);
|
||||
let new_code = paste_code(block_new_lang.as_str(), new_code.as_str(), indent.as_ref().map(|s| s.as_str()));
|
||||
new_doc.push_str(new_code.as_str());
|
||||
has_changes = true;
|
||||
},
|
||||
Some(None) => {
|
||||
has_changes = true;
|
||||
},
|
||||
None => {
|
||||
new_doc.push_str(content);
|
||||
}
|
||||
}
|
||||
|
||||
inside_code = false;
|
||||
};
|
||||
level -= 1;
|
||||
},
|
||||
Event::Text(ref text) if inside_code => {
|
||||
code.push_str(&text);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
|
||||
if has_changes {
|
||||
if prev_offset < doc.len() {
|
||||
new_doc.push_str(&doc[prev_offset..doc.len()]);
|
||||
}
|
||||
|
||||
Some(new_doc)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user