diff --git a/Cargo.lock b/Cargo.lock index 14e37da..00ca414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli", + "gimli 0.31.1", ] [[package]] @@ -32,6 +32,17 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alterable_logger" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a2c81a0e8d57d88d11554612d5e0afe5f942cecbcc239b10a394fd7ce404b" +dependencies = [ + "arc-swap", + "log", + "once_cell", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -319,6 +330,23 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbor-edn" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b43829b1f353168aa8593c2e4ef6e71a81d6749fe09edfc33849f890c69278" +dependencies = [ + "chrono", + "data-encoding", + "data-encoding-macro", + "encoding_rs", + "hex", + "hexfloat2", + "num-bigint", + "num-traits", + "peg", +] + [[package]] name = "cc" version = "1.2.21" @@ -390,6 +418,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "compact_str" version = "0.8.1" @@ -684,6 +722,73 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn 2.0.101", +] + +[[package]] +name = "defmt-decoder" +version = "1.0.0" +source = "git+https://github.com/knurling-rs/defmt?rev=d52b9908c175497d46fc527f4f8dfd6278744f09#d52b9908c175497d46fc527f4f8dfd6278744f09" +dependencies = [ + "alterable_logger", + "anyhow", + "byteorder", + "cbor-edn", + "colored", + "defmt-json-schema", + "defmt-parser", + "dissimilar", + "gimli 0.29.0", + "log", + "nom", + "object", + "regex", + "ryu", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "defmt-json-schema" +version = "0.1.0" +source = "git+https://github.com/knurling-rs/defmt?rev=d52b9908c175497d46fc527f4f8dfd6278744f09#d52b9908c175497d46fc527f4f8dfd6278744f09" +dependencies = [ + "log", + "serde", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "git+https://github.com/knurling-rs/defmt?rev=d52b9908c175497d46fc527f4f8dfd6278744f09#d52b9908c175497d46fc527f4f8dfd6278744f09" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "deku" version = "0.16.0" @@ -852,6 +957,24 @@ dependencies = [ "objc2", ] +[[package]] +name = "dissimilar" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "either" version = "1.15.0" @@ -873,6 +996,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "enum-rotate" version = "0.1.1" @@ -913,7 +1065,7 @@ dependencies = [ "csv", "deku", "heapless", - "md5", + "md5 0.7.0", "parse_int", "regex", "serde", @@ -956,6 +1108,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fdeflate" version = "0.3.7" @@ -965,6 +1123,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" version = "1.1.1" @@ -1008,6 +1178,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "funty" version = "2.0.0" @@ -1057,6 +1236,16 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1112,6 +1301,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexfloat2" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befe65164a090041cdf6e0d21a0ec3198d856fbfe2b76e324a073e790bb49f8c" + [[package]] name = "human-panic" version = "2.0.2" @@ -1200,6 +1395,26 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instability" version = "0.3.7" @@ -1281,6 +1496,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1301,6 +1536,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.0", "libc", + "redox_syscall", ] [[package]] @@ -1344,6 +1580,9 @@ name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "serde", +] [[package]] name = "lru" @@ -1388,6 +1627,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + [[package]] name = "memchr" version = "2.7.4" @@ -1471,6 +1716,32 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" +dependencies = [ + "bitflags 2.9.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.59.0", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1481,6 +1752,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1660,6 +1941,33 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "peg" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1833,6 +2141,16 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "ratatui-explorer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5135951c1bf2ef7ce25b46c53eacf45f1a3e6b283669229008d6744a9ca1332" +dependencies = [ + "educe", + "ratatui", +] + [[package]] name = "ratatui-macros" version = "0.6.0" @@ -1946,6 +2264,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2646,6 +2973,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2741,6 +3078,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3080,6 +3426,8 @@ dependencies = [ "copy_to_output", "crokey", "crossbeam", + "defmt-decoder", + "defmt-parser", "derivative", "directories", "enable-ansi-support", @@ -3092,9 +3440,13 @@ dependencies = [ "int-enum", "itertools 0.14.0", "libc", + "md5 0.8.0", "memchr", + "nom", + "notify", "num-integer", "ratatui", + "ratatui-explorer", "ratatui-macros", "regex", "rolling-file", diff --git a/Cargo.toml b/Cargo.toml index 2649e64..4a4db59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,18 @@ version = "0.1.0" edition = "2024" [features] -default = ["macros","logging","espflash"] +default = ["macros", "espflash", "defmt_watch"] # Allow flashing ESP32 targets with binary and ELF files espflash = ["dep:espflash"] logging = [] -# defmt = [] TODO +defmt = [ + "dep:defmt-decoder", + "dep:defmt-parser", + "dep:ratatui-explorer", + "dep:md5", + "dep:notify", +] +defmt_watch = ["defmt", "dep:notify"] macros = [] portable = [] @@ -26,6 +33,9 @@ color-eyre = "0.6.3" compact_str = { version = "0.9.0", features = ["serde"] } crokey = { version = "1.1.2", features = ["serde"] } crossbeam = "0.8.4" +# defmt-decoder = { version = "1.0.0", optional = true } +defmt-decoder = { git = "https://github.com/knurling-rs/defmt", package = "defmt-decoder", optional = true, rev = "d52b9908c175497d46fc527f4f8dfd6278744f09" } +defmt-parser = { git = "https://github.com/knurling-rs/defmt", package = "defmt-parser", optional = true, rev = "d52b9908c175497d46fc527f4f8dfd6278744f09" } # crokey = { path = "../crokey", features = ["serde"] } derivative = "2.2.0" directories = "6.0.0" @@ -33,7 +43,9 @@ enum-rotate = "0.1.1" # enum_rotate = { path = "../enum-rotate" } # espflash = { version = "3.3.0", optional = true, features = ["serialport"] } # espflash = { path = "../espflash/espflash", optional = true, features = ["serialport"] } -espflash = { git = "https://github.com/nullstalgia/espflash", branch = "public_verify_and_skip", optional = true, default-features = false, features = ["serialport"] } +espflash = { git = "https://github.com/nullstalgia/espflash", branch = "public_verify_and_skip", optional = true, default-features = false, features = [ + "serialport", +] } fs-err = "3.1.0" hex = "0.4.3" human-panic = "2.0.2" @@ -42,11 +54,17 @@ indexmap = { version = "2.9.0", features = ["serde"] } int-enum = "1.2.0" itertools = "0.14.0" libc = "0.2.169" +md5 = { version = "0.8.0", optional = true } memchr = "2.7.4" +nom = "7.1" +notify = { version = "8.0.0", features = [ + "crossbeam-channel", +], optional = true } num-integer = "0.1.46" # log = "0.4.25" # num_enum = "0.7.3" ratatui = { version = "0.29.0", features = ["unstable-rendered-line-info"] } +ratatui-explorer = { version = "0.2.1", optional = true } ratatui-macros = "0.6.0" regex = "1.11.1" # regex-lite = "0.1.6" diff --git a/src/app.rs b/src/app.rs index 19e3c99..157e9bd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,8 @@ use std::{ use arboard::Clipboard; use bstr::ByteVec; +#[cfg(feature = "defmt")] +use camino::Utf8Path; use color_eyre::{eyre::Result, owo_colors::OwoColorize}; use compact_str::{CompactString, ToCompactString}; use crokey::{KeyCombination, key}; @@ -26,6 +28,8 @@ use ratatui::{ ScrollbarState, Table, TableState, Widget, Wrap, }, }; +#[cfg(feature = "defmt")] +use ratatui_explorer::FileExplorer; use ratatui_macros::{horizontal, line, span, vertical}; use serialport::{SerialPortInfo, SerialPortType}; use struct_table::{ArrowKey, StructTable}; @@ -37,8 +41,13 @@ use tui_big_text::{BigText, PixelSize}; use tui_input::{Input, StateChanged, backend::crossterm::EventHandler}; use unicode_width::UnicodeWidthStr; +#[cfg(feature = "defmt")] use crate::{ - buffer::Buffer, + buffer::defmt::elf_watcher::ElfWatchEvent, keybinds::ShowDefmtSelect, settings::Defmt, + tui::defmt::DefmtMeow, +}; +use crate::{ + buffer::{Buffer, defmt::DefmtTableError}, event_carousel::{self, CarouselHandle}, history::{History, UserInput}, keybinds::{Action, AppAction, BaseAction, Keybinds, PortAction, ShowPopupAction}, @@ -103,9 +112,11 @@ pub enum Event { Crossterm(CrosstermEvent), Serial(SerialEvent), RxBuffer(Vec), + Tick(Tick), #[cfg(feature = "logging")] Logging(LoggingEvent), - Tick(Tick), + #[cfg(feature = "defmt_watch")] + DefmtElfWatch(ElfWatchEvent), Quit, } @@ -189,6 +200,9 @@ pub enum PopupMenu { EspFlash, #[cfg(feature = "macros")] Macros, + #[cfg(feature = "defmt")] + #[strum(serialize = "defmt")] + Defmt, } #[derive(Debug, PartialEq, Eq)] @@ -196,6 +210,10 @@ enum Popup { PopupMenu(PopupMenu), CurrentKeybinds, ErrorMessage(String), + #[cfg(feature = "defmt")] + DefmtNewElf(FileExplorer), + #[cfg(feature = "defmt")] + DefmtRecentElf, } impl From for Popup { @@ -268,6 +286,9 @@ pub struct App { #[cfg(feature = "espflash")] espflash: EspFlashState, + + #[cfg(feature = "defmt")] + defmt_meow: DefmtMeow, // TODO // error_message: Option, } @@ -333,13 +354,51 @@ impl App { Duration::from_secs(1), ); let line_ending = settings.last_port_settings.rx_line_ending.as_bytes(); - let buffer = Buffer::new( + + #[cfg(feature = "defmt")] + let mut defmt_meow = DefmtMeow::build( + #[cfg(feature = "defmt_watch")] + tx.clone(), + ) + .unwrap(); + + let mut buffer = Buffer::new( line_ending, settings.rendering.clone(), #[cfg(feature = "logging")] settings.logging.clone(), + #[cfg(feature = "logging")] tx.clone(), + settings.defmt.clone(), ); + + if let Some(last_elf) = defmt_meow.recent_elfs.last() + && last_elf.is_file() + { + match try_load_defmt_elf( + &last_elf.to_owned(), + &mut buffer.defmt_decoder, + &mut defmt_meow.recent_elfs, + #[cfg(feature = "defmt_watch")] + &mut defmt_meow.watcher_handle, + ) { + Ok(()) => (), + Err(e) => { + let text = format!("defmt ELF reload failed! {e}"); + error!("{text}"); + // self.notifs.notify_str(text, Color::Green); + } + } + } + + if let Some(last_path) = defmt_meow.recent_elfs.last() + && last_path.exists() + && last_path.is_file() + && let Ok(decoder) = crate::buffer::defmt::DefmtDecoder::from_elf_bytes(last_path) + { + let _ = buffer.defmt_decoder.insert(decoder); + } + // debug!("{buffer:#?}"); Self { state: RunningState::Running, @@ -381,6 +440,9 @@ impl App { #[cfg(feature = "espflash")] espflash: EspFlashState::new(), + #[cfg(feature = "defmt")] + defmt_meow, + user_broke_connection: false, } } @@ -391,7 +453,8 @@ impl App { // Get initial size of buffer. self.buffer.update_terminal_size(&mut terminal)?; let mut max_draw = Duration::default(); - let mut max_handle = Duration::default(); + let mut max_rx_handle = Duration::default(); + let mut max_event_handle = Duration::default(); while self.is_running() { let start = Instant::now(); // TODO performance widget? @@ -422,17 +485,21 @@ impl App { Err(crossbeam::channel::TryRecvError::Disconnected) => todo!(), } + let end2 = start2.elapsed(); + let start3 = Instant::now(); + match self.rx.try_recv() { Ok(event) => self.handle_event(event, &mut terminal)?, Err(crossbeam::channel::TryRecvError::Empty) => (), Err(crossbeam::channel::TryRecvError::Disconnected) => todo!(), } - let end2 = start2.elapsed(); - max_handle = max_handle.max(end2); + let end3 = start3.elapsed(); + max_rx_handle = max_rx_handle.max(end2); + max_event_handle = max_event_handle.max(end3); debug!( - "Frame took {:?} to draw (max: {max_draw:?}), {:?} to handle (max: {max_handle:?}) ", - end1, end2 + "Frame took {:?} to draw (max: {max_draw:?}), {:?} to handle RX (max: {max_rx_handle:?}), {:?} to handle event (max: {max_event_handle:?}) ", + end1, end2, end3 ); // debug!("{msg:?}"); @@ -463,7 +530,16 @@ impl App { match event { Event::Quit => self.shutdown(), + Event::RxBuffer(mut data) => { + self.buffer.fresh_rx_bytes(&mut data); + self.buffer.scroll_by(0); + + self.repeating_line_flip.flip(); + } + + // TODO force re-draw every minute or so? Event::Crossterm(CrosstermEvent::Resize) => { + terminal.autoresize()?; self.buffer.update_terminal_size(terminal)?; } Event::Crossterm(CrosstermEvent::KeyPress(key)) => self.handle_key_press(key), @@ -569,12 +645,6 @@ impl App { } } } - Event::RxBuffer(mut data) => { - self.buffer.append_rx_bytes(&mut data); - self.buffer.scroll_by(0); - - self.repeating_line_flip.flip(); - } Event::Serial(SerialEvent::Ports(ports)) => { self.ports = ports; if let Menu::PortSelection(PortSelectionElement::Ports) = &self.menu { @@ -684,6 +754,34 @@ impl App { Event::Tick(Tick::Tx) => { self.repeating_line_flip.flip(); } + #[cfg(feature = "defmt_watch")] + Event::DefmtElfWatch(ElfWatchEvent::ElfUpdated(elf_path)) => { + if self.settings.defmt.watch_elf_for_changes { + info!("ELF File Watch triggered, reloading ELF at {elf_path}"); + + match try_load_defmt_elf( + &elf_path, + &mut self.buffer.defmt_decoder, + &mut self.defmt_meow.recent_elfs, + #[cfg(feature = "defmt_watch")] + &mut self.defmt_meow.watcher_handle, + ) { + Ok(()) => { + self.notifs + .notify_str("defmt ELF reloaded due to file update!", Color::Green); + } + Err(e) => { + let text = format!("defmt ELF reload failed! {e}"); + error!("{text}"); + self.notifs.notify_str(text, Color::Red); + } + } + } + } + #[cfg(feature = "defmt")] + Event::DefmtElfWatch(ElfWatchEvent::Error(err)) => { + self.notifs.notify_str(err, Color::Red); + } } Ok(()) } @@ -816,6 +914,66 @@ impl App { } _ => (), } + + #[cfg(feature = "defmt")] + if let Some(Popup::DefmtNewElf(file_explorer)) = &mut self.popup { + let input = match key.code { + KeyCode::Left | KeyCode::Char('h') => ratatui_explorer::Input::Left, + KeyCode::Down | KeyCode::Char('j') => ratatui_explorer::Input::Down, + KeyCode::Up | KeyCode::Char('k') => ratatui_explorer::Input::Up, + KeyCode::Right | KeyCode::Char('l') => ratatui_explorer::Input::Right, + KeyCode::Enter => ratatui_explorer::Input::Right, + KeyCode::Backspace | KeyCode::BackTab => ratatui_explorer::Input::Left, + KeyCode::PageUp => ratatui_explorer::Input::PageUp, + KeyCode::PageDown => ratatui_explorer::Input::PageDown, + KeyCode::Home => ratatui_explorer::Input::Home, + KeyCode::End => ratatui_explorer::Input::End, + + _ => ratatui_explorer::Input::None, + }; + if let Err(e) = file_explorer.handle(input) { + error!("File Explorer Error: {e}"); + self.notifs + .notify_str(format!("Explorer Error: {e}"), Color::Red); + self.dismiss_popup(); + return; + }; + match input { + ratatui_explorer::Input::None => (), + ratatui_explorer::Input::Right => { + let current = file_explorer.current(); + let is_file = current.is_file(); + if is_file { + use camino::Utf8PathBuf; + + let elf_path: Utf8PathBuf = current.path().to_owned().try_into().unwrap(); + + match try_load_defmt_elf( + &elf_path, + &mut self.buffer.defmt_decoder, + &mut self.defmt_meow.recent_elfs, + #[cfg(feature = "defmt_watch")] + &mut self.defmt_meow.watcher_handle, + ) { + Ok(()) => { + self.notifs + .notify_str("defmt ELF loaded successfully!", Color::Green); + } + Err(e) => { + let text = format!("defmt ELF load failed! {e}"); + error!("{text}"); + self.notifs.notify_str(text, Color::Red); + } + } + + self.dismiss_popup(); + } + return; + } + _ => return, + } + } + match self.menu { Menu::Terminal(TerminalPrompt::None) if self.popup.is_none() => { at_terminal = true; @@ -1119,12 +1277,41 @@ impl App { } #[cfg(feature = "espflash")] // TODO show name of flashing profile - Action::EspFlashProfile(profile) => self - .serial - .esp_flash_profile(self.espflash.profile_from_name(&profile).unwrap())?, + Action::EspFlashProfile(profile) => { + let profile = self.espflash.profile_from_name(&profile).unwrap(); + self.esp_flash_profile(profile)?; + } } Ok(None) } + #[cfg(feature = "espflash")] + fn esp_flash_profile(&mut self, profile: esp::EspProfile) -> Result<()> { + #[cfg(feature = "defmt")] + let profile_defmt_path = profile.defmt_elf_path(); + + self.serial.esp_flash_profile(profile)?; + + #[cfg(feature = "defmt")] + if let Some(elf_path) = profile_defmt_path { + match try_load_defmt_elf( + &elf_path, + &mut self.buffer.defmt_decoder, + &mut self.defmt_meow.recent_elfs, + #[cfg(feature = "defmt_watch")] + &mut self.defmt_meow.watcher_handle, + ) { + Ok(()) => { + self.notifs.notify_str("defmt ELF loaded!", Color::Green); + } + Err(e) => { + let text = format!("defmt ELF load failed! {e}"); + error!("{text}"); + self.notifs.notify_str(text, Color::Red); + } + } + } + Ok(()) + } fn run_method_from_action(&mut self, action: AppAction) -> Result<()> { let pretty_bool = |b: bool| { if b { "On" } else { "Off" } @@ -1290,6 +1477,16 @@ impl App { self.notifs .notify_str("Reloaded espflash profiles!", Color::Green); self.espflash.reload().unwrap(); + } + #[cfg(feature = "defmt")] + A::ShowDefmtSelect(ShowDefmtSelect::SelectRecent) => { + self.show_popup(Popup::DefmtRecentElf) + } + A::ShowDefmtSelect(ShowDefmtSelect::SelectTui) => { + self.show_popup(Popup::DefmtNewElf(create_file_explorer()?)) + } + A::ShowDefmtSelect(ShowDefmtSelect::SelectSystem) => { + todo!("need a system file picker"); } // unknown => { // warn!("Unknown keybind action: {unknown}"); // self.notifs.notify_str( @@ -1302,6 +1499,7 @@ impl App { } // fn tab_pressed(&mut self) {} fn esc_pressed(&mut self) { + #[cfg(feature = "defmt")] match self.popup { None => (), Some(_) => { @@ -1349,9 +1547,19 @@ impl App { self.popup_hint_scroll = -2; match self.popup_menu_scroll { 0 => self.popup_menu_scroll = self.current_popup_item_count(), - _ => self.popup_menu_scroll = self.popup_menu_scroll - 1, + _ => self.popup_menu_scroll -= 1, } } + #[cfg(feature = "defmt")] + Some(Popup::DefmtNewElf(_)) => (), + #[cfg(feature = "defmt")] + Some(Popup::DefmtRecentElf) => match self.popup_menu_scroll { + 0 => { + self.popup_menu_scroll = + self.defmt_meow.recent_elfs.recents_len().saturating_sub(1) + } + _ => self.popup_menu_scroll -= 1, + }, } if self.popup.is_some() { @@ -1394,9 +1602,20 @@ impl App { _ if self.popup_menu_scroll == self.current_popup_item_count() => { self.popup_menu_scroll = 0 } - _ => self.popup_menu_scroll = self.popup_menu_scroll + 1, + _ => self.popup_menu_scroll += 1, } } + #[cfg(feature = "defmt")] + Some(Popup::DefmtNewElf(_)) => (), + #[cfg(feature = "defmt")] + Some(Popup::DefmtRecentElf) => match self.popup_menu_scroll { + _ if self.popup_menu_scroll + == self.defmt_meow.recent_elfs.recents_len().saturating_sub(1) => + { + self.popup_menu_scroll = 0 + } + _ => self.popup_menu_scroll += 1, + }, } if self.popup.is_some() { @@ -1497,6 +1716,22 @@ impl App { .handle_input(ArrowKey::Left, self.get_corrected_popup_item().unwrap()) .unwrap(); } + #[cfg(feature = "defmt")] + Some(Popup::PopupMenu(PopupMenu::Defmt)) => { + if self.popup_menu_scroll <= 2 { + return; + } + + let result = self + .scratch + .defmt + .handle_input(ArrowKey::Left, self.get_corrected_popup_item().unwrap()) + .unwrap(); + } + #[cfg(feature = "defmt")] + Some(Popup::DefmtNewElf(_)) => (), + #[cfg(feature = "defmt")] + Some(Popup::DefmtRecentElf) => (), } if self.popup.is_some() { return; @@ -1569,6 +1804,22 @@ impl App { .handle_input(ArrowKey::Right, self.get_corrected_popup_item().unwrap()) .unwrap(); } + #[cfg(feature = "defmt")] + Some(Popup::PopupMenu(PopupMenu::Defmt)) => { + if self.popup_menu_scroll <= 2 { + return; + } + + let result = self + .scratch + .defmt + .handle_input(ArrowKey::Right, self.get_corrected_popup_item().unwrap()) + .unwrap(); + } + #[cfg(feature = "defmt")] + Some(Popup::DefmtNewElf(_)) => (), + #[cfg(feature = "defmt")] + Some(Popup::DefmtRecentElf) => (), } if self.popup.is_some() { return; @@ -1689,8 +1940,7 @@ impl App { "shouldn't have selected a non-existant flash profile" ); - self.serial - .esp_flash_profile(self.espflash.profile_from_index(selected).unwrap()) + self.esp_flash_profile(self.espflash.profile_from_index(selected).unwrap()) .unwrap(); } else { match selected { @@ -1759,6 +2009,64 @@ impl App { self.notifs .notify_str("Logging settings saved!", Color::Green); } + #[cfg(feature = "defmt")] + Some(Popup::PopupMenu(PopupMenu::Defmt)) => { + if self.popup_menu_scroll == 0 { + return; + } else if self.popup_menu_scroll == 1 { + // open file selector + + let file_explorer = create_file_explorer().unwrap(); + + self.show_popup(Popup::DefmtNewElf(file_explorer)); + + return; + } else if self.popup_menu_scroll == 2 { + // open recent selector + self.show_popup(Popup::DefmtRecentElf); + return; + } + // Otherwise, save settings. + + self.settings.defmt = self.scratch.defmt.clone(); + + self.buffer + .update_defmt_settings(self.settings.defmt.clone()); + + self.settings.save().unwrap(); + self.dismiss_popup(); + self.notifs + .notify_str("defmt settings saved!", Color::Green); + } + Some(Popup::DefmtNewElf(_)) => (), + Some(Popup::DefmtRecentElf) => { + if let Some(selected) = self.popup_table_state.selected() { + let elf_path = self + .defmt_meow + .recent_elfs + .nth_path(selected) + .unwrap() + .to_owned(); + match try_load_defmt_elf( + &elf_path, + &mut self.buffer.defmt_decoder, + &mut self.defmt_meow.recent_elfs, + #[cfg(feature = "defmt_watch")] + &mut self.defmt_meow.watcher_handle, + ) { + Ok(()) => { + self.notifs + .notify_str("defmt ELF loaded successfully!", Color::Green); + } + Err(e) => { + let text = format!("defmt ELF load failed! {e}"); + error!("{text}"); + self.notifs.notify_str(text, Color::Red); + } + } + self.dismiss_popup(); + } + } } if self.popup.is_some() || popup_was_some { return; @@ -2023,6 +2331,12 @@ impl App { 1 + // Start/Stop Logging button Logging::VISIBLE_FIELDS } + + #[cfg(feature = "defmt")] + PopupMenu::Defmt => { + 2 + // Select New/Recent ELF buttons + Defmt::VISIBLE_FIELDS + } } } /// Gets corrected index of selected element. @@ -2064,13 +2378,20 @@ impl App { (PopupMenu::Logging, _) => Some(raw_scroll - 2), #[cfg(feature = "espflash")] - // espflash pre-set action buttons + // espflash user profiles (PopupMenu::EspFlash, _) if raw_scroll >= esp::ESPFLASH_BUTTON_COUNT + 1 => { Some(raw_scroll - (esp::ESPFLASH_BUTTON_COUNT + 1)) } #[cfg(feature = "espflash")] - // espflash user profiles + // espflash pre-set action buttons (PopupMenu::EspFlash, _) => Some(raw_scroll - 1), + + #[cfg(feature = "defmt")] + // espflash user profiles + (PopupMenu::Defmt, _) if raw_scroll >= 2 + 1 => Some(raw_scroll - (2 + 1)), + #[cfg(feature = "defmt")] + // espflash pre-set action buttons + (PopupMenu::Defmt, _) => Some(raw_scroll - 1), } } pub fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { @@ -2138,6 +2459,49 @@ impl App { self.popup_menu_scroll = scroll as usize; } Some(Popup::ErrorMessage(_)) => todo!(), + #[cfg(feature = "defmt")] + Some(Popup::DefmtNewElf(file_explorer)) => { + let area = centered_rect_size( + Size { + width: 70, + height: 20, + }, + area, + ); + frame.render_widget(Clear, area); + frame.render_widget(&file_explorer.widget(), area); + } + Some(Popup::DefmtRecentElf) => { + let area = centered_rect_size( + Size { + width: 80, + height: 15, + }, + area, + ); + + let title = Line::raw(" Select from recently used ELFs: ") + .centered() + .reset(); + + let block = Block::bordered() + .border_style(Style::new().light_red()) + .title_top(title); + + let inner = block.inner(area); + + self.popup_table_state.select(Some(self.popup_menu_scroll)); + + debug!("{:?}, {:?}", self.popup_menu_scroll, self.popup_table_state); + + frame.render_widget(Clear, area); + frame.render_widget(block, area); + frame.render_stateful_widget( + self.defmt_meow.recent_elfs.as_table(), + inner, + &mut self.popup_table_state, + ); + } } } fn render_popup_menus(&mut self, frame: &mut Frame, area: Rect) { @@ -2155,6 +2519,8 @@ impl App { PopupMenu::EspFlash => Color::Magenta, #[cfg(feature = "logging")] PopupMenu::Logging => Color::Yellow, + #[cfg(feature = "defmt")] + PopupMenu::Defmt => Color::LightRed, }; let mut menu_selector_state = SingleLineSelectorState::new(); @@ -2721,7 +3087,7 @@ impl App { .borders(Borders::TOP) .border_style(Style::from(popup_color)); frame.render_widget( - Line::raw("Powered by esp-rs/espflash!") + Line::raw("Powered by esp-rs/espflash v3.3.0!") .all_spans_styled(Color::DarkGray.into()) .centered(), line_area, @@ -2825,6 +3191,154 @@ impl App { new_seperator, ); } + #[cfg(feature = "defmt")] + PopupMenu::Defmt => { + let new_seperator = { + let mut area = center_inner.clone(); + area.y = area.top().saturating_add(4); + area.height = 1; + area + }; + let defmt_settings_area = { + let mut area = center_inner.clone(); + area.y = area.top().saturating_add(5); + area.height = area.height.saturating_sub(7); + area + }; + let line_block = Block::new() + .borders(Borders::TOP) + .border_style(Style::from(popup_color)); + frame.render_widget( + Line::raw("Powered by knurling-rs/defmt v1.0.0!") + .all_spans_styled(Color::DarkGray.into()) + .centered(), + line_area, + ); + + let [ + elf_title, + current_elf, + select_new_elf, + select_recent_elf, + _rest, + ] = vertical![==1,==1,==1,==1,*=1].areas(settings_area); + + frame.render_widget( + Line::raw("Esc: Close | Enter: Select/Save") + .all_spans_styled(Color::DarkGray.into()) + .centered(), + hint_text_area, + ); + + let settings_selected = self.popup_menu_scroll >= 2 + 1; + + // frame.render_widget(esp::espflash_buttons(), settings_area); + frame.render_widget(&line_block, new_seperator); + + let current_elf_str = if let Some(decoder) = &self.buffer.defmt_decoder { + decoder.elf_path.as_str() + } else { + "None" + }; + + let select_style = if self.popup_menu_scroll == 1 { + Style::new().reversed() + } else { + Style::new() + }; + let recent_style = if self.popup_menu_scroll == 2 { + Style::new().reversed() + } else { + Style::new() + }; + + let current_elf_text = if let Some(decoder) = &self.buffer.defmt_decoder { + Cow::Owned(format!( + "Current ELF MD5: {}", + &decoder.elf_md5.as_str()[..8] + )) + } else { + Cow::Borrowed("Current ELF:") + }; + + frame.render_widget( + Line::raw(current_elf_text).centered().dark_gray(), + elf_title, + ); + frame.render_widget(Line::raw(current_elf_str).centered(), current_elf); + frame.render_widget( + Line::raw("[Select ELF File]") + .centered() + .all_spans_styled(select_style), + select_new_elf, + ); + frame.render_widget( + Line::raw("[Select Recent ELF]") + .centered() + .all_spans_styled(recent_style), + select_recent_elf, + ); + + // if self.popup_menu_item == + if settings_selected { + let selected = self.get_corrected_popup_item(); + self.popup_table_state.select(selected); + self.popup_table_state.select_first_column(); + + use crate::settings::Defmt; + + frame.render_stateful_widget( + self.scratch.defmt.as_table(), + defmt_settings_area, + &mut self.popup_table_state, + ); + let text: &str = self + .popup_table_state + .selected() + .map(|i| Defmt::DOCSTRINGS[i]) + .unwrap_or(&""); + render_scrolling_line( + text, + frame, + scrolling_text_area, + &mut self.popup_hint_scroll, + ); + } else { + self.popup_table_state.select(Some(0)); + self.popup_table_state.select_first_column(); + let corrected_index = self.get_corrected_popup_item(); + + // frame.render_stateful_widget( + // esp::espflash_buttons(), + // settings_area, + // &mut TableState::new() + // .with_selected_column(0) + // .with_selected(corrected_index), + // ); + frame.render_widget(self.settings.defmt.as_table(), defmt_settings_area); + + let hints = [ + "Select an ELF file to decode defmt packets with. Shift/Ctrl to use native system file picker.", + "Select from a list of recently used ELFs.", + ]; + if let Some(idx) = corrected_index { + if let Some(&hint_text) = hints.get(idx) { + render_scrolling_line( + hint_text, + frame, + scrolling_text_area, + &mut self.popup_hint_scroll, + ); + } + } + } + frame.render_widget( + Line::raw("Settings:") + .all_spans_styled(Color::DarkGray.into()) + .centered(), + new_seperator, + ); + } } // TODO // shrink scrollbar and change content length based on if its for a submenu or not @@ -3303,6 +3817,47 @@ impl App { } } +#[derive(Debug, thiserror::Error)] +enum TryLoadDefmtError { + #[error("failed serializing recents: {0}")] + RecentSer(#[from] toml::ser::Error), + #[error("failed saving recent elfs: {0}")] + RecentSave(#[from] std::io::Error), + #[error("failed parsing defmt from elf: {0}")] + DefmtParse(#[from] DefmtTableError), +} + +#[cfg(feature = "defmt")] +fn try_load_defmt_elf( + path: &Utf8Path, + decoder_opt: &mut Option, + recent_elfs: &mut crate::tui::defmt::DefmtRecentElfs, + #[cfg(feature = "defmt_watch")] + watcher_handle: &mut crate::buffer::defmt::elf_watcher::ElfWatchHandle, +) -> Result<(), TryLoadDefmtError> { + use camino::Utf8PathBuf; + + use crate::buffer::defmt::DefmtDecoder; + + let new_decoder = DefmtDecoder::from_elf_bytes(path); + match new_decoder { + Ok(new_decoder) => { + let _ = decoder_opt.insert(new_decoder); + // self.notifs + // .notify_str("defmt data parsed from ELF!", Color::Green); + recent_elfs.elf_loaded(path)?; + #[cfg(feature = "defmt_watch")] + watcher_handle.begin_watch(path); + } + Err(e) => { + // self.notifs + // .notify_str(format!("defmt Error: {e}"), Color::Red); + } + } + + Ok(()) +} + pub fn repeating_pattern_widget( frame: &mut Frame, area: Rect, @@ -3428,3 +3983,22 @@ pub fn render_scrolling_line<'a, T: Into>>( }), ); } + +fn create_file_explorer() -> Result { + use ratatui_explorer::FileExplorer; + + let explorer_theme = ratatui_explorer::Theme::default() + .with_scroll_padding(1) + .add_default_title(); + + let root_path = std::path::PathBuf::from("/"); + + let base_dirs_opt = directories::BaseDirs::new(); + + let starting_dir = base_dirs_opt + .as_ref() + .map(|base_dirs| base_dirs.home_dir()) + .unwrap_or(&root_path); + + FileExplorer::with_theme(explorer_theme).and_then(|mut e| e.set_cwd(starting_dir).map(|_| e)) +} diff --git a/src/buffer/buf_line.rs b/src/buffer/buf_line.rs index bad9eae..ae13b54 100644 --- a/src/buffer/buf_line.rs +++ b/src/buffer/buf_line.rs @@ -11,8 +11,13 @@ use ratatui::{ use ratatui_macros::{line, span}; use tracing::debug; +#[cfg(feature = "defmt")] +use defmt_parser::Level; + +#[cfg(feature = "defmt")] +use crate::settings::Defmt; use crate::{ - buffer::LineEnding, + buffer::{LineEnding, RangeSlice}, settings::Rendering, traits::{ByteSuffixCheck, FirstChars, LineHelpers}, }; @@ -32,6 +37,15 @@ pub struct BufLine { pub raw_buffer_index: usize, pub line_type: LineType, + // #[cfg(feature = "defmt")] + // defmt_level: Option, +} + +#[derive(Clone, Copy)] +pub struct RenderSettings<'a> { + pub rendering: &'a Rendering, + #[cfg(feature = "defmt")] + pub defmt: &'a Defmt, } // impl PartialEq for BufLine { @@ -68,8 +82,36 @@ pub(super) enum LineType { User { is_bytes: bool, is_macro: bool, + escaped_line_ending: Option, reloggable_raw: Vec, }, + #[cfg(feature = "defmt")] + PortDefmt { + level: Option, + location: Option, + device_timestamp: Option, + // /// Includes any potential prefix or terminator + // total_frame_len: usize, + }, +} + +#[cfg(feature = "defmt")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FrameLocation { + // Original type is u64 but I'm not storing that. + line: u32, + module: CompactString, + file: CompactString, +} + +impl From<&defmt_decoder::Location> for FrameLocation { + fn from(value: &defmt_decoder::Location) -> Self { + Self { + line: value.line.try_into().expect("line larger than u32::MAX??"), + module: value.module.to_compact_string(), + file: value.file.display().to_compact_string(), + } + } } impl LineType { @@ -88,78 +130,114 @@ impl LineType { } } +pub struct BufLineKit<'a> { + pub full_range_slice: RangeSlice<'a>, + pub area_width: u16, + pub render: RenderSettings<'a>, + pub timestamp: DateTime, +} + // Many changes needed, esp. in regards to current app-state things (index, width, color, showing timestamp) impl BufLine { - pub fn new_with_line( - mut line: Line<'static>, - raw_value: &[u8], - raw_buffer_index: usize, - area_width: u16, - rendering: &Rendering, - line_ending: &LineEnding, - now: DateTime, - line_type: LineType, - ) -> Self { + fn new(mut line: Line<'static>, kit: BufLineKit, line_type: LineType) -> Self { let time_format = "[%H:%M:%S%.3f] "; line.remove_unsavory_chars(); - // if !line.is_styled() && !line.is_empty() { - // assert!(line.spans.len() <= 1); - // determine_color(&mut line, &[]); - // } + let index_info = make_index_info(&kit.full_range_slice); - let index_info = make_index_info(raw_value, raw_buffer_index, &line_type); + let timestamp = kit.timestamp; let mut bufline = Self { - timestamp_str: now.format(time_format).to_compact_string(), - timestamp: now, + timestamp_str: timestamp.format(time_format).to_compact_string(), + timestamp, index_info, value: line, - raw_buffer_index, + raw_buffer_index: kit.full_range_slice.range.start, rendered_line_height: 0, line_type, }; - bufline.populate_line_ending(raw_value, line_ending); - bufline.update_line_height(area_width, rendering); + // bufline.populate_line_ending(raw_value, line_ending); + bufline.update_line_height(kit.area_width, kit.render); bufline } - pub fn populate_line_ending(&mut self, full_line_slice: &[u8], line_ending: &LineEnding) { - match &mut self.line_type { - LineType::Port { - escaped_line_ending, - } => { - if escaped_line_ending.is_some() { - unreachable!(); - } - if full_line_slice.has_line_ending(line_ending) { - _ = escaped_line_ending - .insert(line_ending.as_bytes().escape_bytes().to_compact_string()); - } - } - // TODO? - LineType::User { .. } => (), - } + pub fn port_text_line(line: Line<'static>, kit: BufLineKit, line_ending: &LineEnding) -> Self { + let line_type = LineType::Port { + escaped_line_ending: line_ending.escaped_from(kit.full_range_slice.slice), + }; + + Self::new( + line, kit, // line_ending, + line_type, + ) } - pub fn update_line( - &mut self, + #[cfg(feature = "defmt")] + pub fn port_defmt_line( line: Line<'static>, - full_line_slice: &[u8], - area_width: u16, - rendering: &Rendering, + kit: BufLineKit, + level: Option, + device_timestamp: Option<&dyn std::fmt::Display>, + location: Option, + ) -> Self { + let line_type = LineType::PortDefmt { + level, + device_timestamp: device_timestamp.map(|ts| format_compact!("[{ts}] ")), + location, + }; + + Self::new( + line, kit, // line_ending, + line_type, + ) + } + pub fn user_line( + line: Line<'static>, + kit: BufLineKit, line_ending: &LineEnding, - ) { - self.index_info = make_index_info(full_line_slice, self.raw_buffer_index, &self.line_type); + is_bytes: bool, + is_macro: bool, + reloggable_raw: &[u8], + ) -> Self { + let line_type = LineType::User { + is_bytes, + is_macro, + reloggable_raw: reloggable_raw.to_vec(), + escaped_line_ending: line_ending.escaped_from(reloggable_raw), + }; + + Self::new(line, kit, line_type) + } + + pub fn update_line(&mut self, line: Line<'static>, kit: BufLineKit, line_ending: &LineEnding) { + assert_eq!( + self.line_type, + LineType::Port { + escaped_line_ending: None + } + ); + + self.index_info = make_index_info(&kit.full_range_slice); self.value = line; self.value.remove_unsavory_chars(); - self.populate_line_ending(full_line_slice, line_ending); + let LineType::Port { + escaped_line_ending, + } = &mut self.line_type + else { + unreachable!(); + }; + + if let Some(escaped) = line_ending.escaped_from(&kit.full_range_slice.slice) { + _ = escaped_line_ending.insert(escaped); + }; + + // self.populate_line_ending(full_line_slice, line_ending); - self.update_line_height(area_width, rendering); + self.update_line_height(kit.area_width, kit.render); } - pub fn update_line_height(&mut self, area_width: u16, rendering: &Rendering) -> usize { + pub fn update_line_height(&mut self, area_width: u16, rendering: RenderSettings) -> usize { let para = Paragraph::new(self.as_line(rendering)).wrap(Wrap { trim: false }); // TODO make the sub 1 for margin/scrollbar more sane/clear // Paragraph::line_count comes from an unstable ratatui feature (unstable-rendered-line-info) @@ -174,39 +252,160 @@ impl BufLine { } /// Returns an owned `Line` that borrows from the current line's spans. - pub fn as_line(&self, rendering: &Rendering) -> Line { + pub fn as_line(&self, rendering: RenderSettings) -> Line { let borrowed_spans = self.value.borrowed_spans_iter(); + let dark_gray = Style::new().dark_gray(); + let indices_and_len = std::iter::once(Span::styled( Cow::Borrowed(self.index_info.as_ref()), - Style::new().dark_gray(), + dark_gray, )) - .filter(|_| rendering.show_indices); + .filter(|_| rendering.rendering.show_indices); let timestamp = std::iter::once(Span::styled( Cow::Borrowed(self.timestamp_str.as_ref()), - Style::new().dark_gray(), + dark_gray, )) - .filter(|_| rendering.timestamps); + .filter(|_| rendering.rendering.timestamps); + + #[cfg(feature = "defmt")] + let defmt_device_timestamp = std::iter::once(&self.line_type).filter_map(|lt| match lt { + _ if !rendering.defmt.device_timestamp => None, + LineType::PortDefmt { + device_timestamp: Some(device_timestamp), + .. + } => Some(Span::styled(device_timestamp, dark_gray)), + _ => None, + }); - let line_ending = std::iter::once(&self.line_type).filter_map(|lt| match lt { - _ if !rendering.show_line_ending => None, - LineType::Port { - escaped_line_ending: Some(line_ending), - } => Some(Span::styled( - Cow::Borrowed(line_ending.as_str()), - Style::new().dark_gray(), - )), - LineType::Port { - escaped_line_ending: None, - } => None, - LineType::User { .. } => None, + #[cfg(feature = "defmt")] + let defmt_level = std::iter::once(&self.line_type) + .filter_map(|lt| match lt { + LineType::PortDefmt { level, .. } => { + Some(super::tui::defmt::defmt_level_bracketed(*level)) + } + _ => None, + }) + .flatten(); + + // #[cfg(feature = "defmt")] + // let defmt_location = std::iter::once(&self.line_type).filter_map(|lt| match lt { + // LineType::PortDefmt { + // location: Some(FrameLocation { line, module, file }), + // .. + // } => Some(Span::styled( + // format!(" {module} @ {file}:{line}"), + // Style::new().dark_gray(), + // )), + // _ => None, + // }); + + fn shorten_module_path(full_module_path: &str) -> &str { + full_module_path + .split("::") + .last() + .unwrap_or(full_module_path) + } + + fn shorten_file_path(full_file_path: &str) -> &str { + full_file_path + .split(&['/', '\\']) + .last() + .unwrap_or(full_file_path) + } + + #[cfg(feature = "defmt")] + let defmt_location = std::iter::once(&self.line_type).filter_map(|lt| match lt { + LineType::PortDefmt { + location: + Some(FrameLocation { + line: defmt_line_num, + module: defmt_module, + file: defmt_file, + }), + .. + } => { + use crate::settings::DefmtLocation; + + let RenderSettings { rendering, defmt } = rendering; + + let module = &defmt.show_module; + let file = &defmt.show_file; + let line_num = defmt.show_line_number; + + match (module, file, line_num) { + (DefmtLocation::Hidden, DefmtLocation::Hidden, false) => return None, + _ => (), + }; + + let module_file_seperator = + if !module.is_hidden() && (!file.is_hidden() || line_num) { + " @ " + } else { + "" + }; + let file_line_seperator = if !file.is_hidden() && line_num { + ":" + } else { + "" + }; + + let module = match module { + DefmtLocation::Hidden => "", + DefmtLocation::Shortened => shorten_module_path(defmt_module), + DefmtLocation::Full => defmt_module, + }; + let file = match file { + DefmtLocation::Hidden => "", + DefmtLocation::Shortened => shorten_file_path(defmt_file), + DefmtLocation::Full => defmt_file, + }; + let line_num = if line_num { + Cow::Owned(defmt_line_num.to_string()) + } else { + Cow::Borrowed("") + }; + + Some(Span::styled( + format!( + " {module}{module_file_seperator}{file}{file_line_seperator}{line_num}" + ), + Style::new().dark_gray(), + )) + // todo!() + } + _ => None, }); - let spans = timestamp - .chain(indices_and_len) - .chain(borrowed_spans) - .chain(line_ending); + // let line_ending = std::iter::once(&self.line_type).filter_map(|lt| match lt { + // _ if !rendering.show_line_ending => None, + // LineType::Port { + // escaped_line_ending: Some(line_ending), + // } => Some(Span::styled(Cow::Borrowed(line_ending.as_str()), dark_gray)), + // LineType::Port { + // escaped_line_ending: None, + // } => None, + // LineType::User { .. } => None, + // LineType::PortDefmt { .. } => None, + // }); + + let spans = timestamp; + + #[cfg(feature = "defmt")] + let spans = spans.chain(defmt_device_timestamp); + + let spans = spans.chain(indices_and_len); + + #[cfg(feature = "defmt")] + let spans = spans.chain(defmt_level); + + let spans= spans.chain(borrowed_spans) + // .chain(line_ending) + ; + + #[cfg(feature = "defmt")] + let spans = spans.chain(defmt_location); Line::from_iter(spans) } @@ -217,23 +416,21 @@ impl BufLine { } fn make_index_info( - full_line_slice: &[u8], - start_index: usize, - line_type: &LineType, + range: &RangeSlice, + // line_type: &LineType, ) -> CompactString { - if let LineType::User { .. } = line_type { - format_compact!( - "({start:06}->{end:06}, {len:3}) ", - start = start_index, - end = start_index + full_line_slice.len(), - len = full_line_slice.len(), - ) - } else { - format_compact!( - "({start:06}..{end:06}, {len:3}) ", - start = start_index, - end = start_index + full_line_slice.len(), - len = full_line_slice.len(), - ) - } + // if let LineType::User { .. } = line_type { + // format_compact!( + // "({start:06}->{end:06}, {len:3}) ", + // start = start_index, + // end = start_index + full_line_slice.len(), + // len = full_line_slice.len(), + // ) + // } else { + let start = range.range.start; + let end = range.range.end; + let len = range.slice.len(); + debug_assert_eq!(end - start, len); + format_compact!("({start:06}..{end:06}, {len:3}) ") + // } } diff --git a/src/buffer/defmt/elf_watcher.rs b/src/buffer/defmt/elf_watcher.rs new file mode 100644 index 0000000..c143f71 --- /dev/null +++ b/src/buffer/defmt/elf_watcher.rs @@ -0,0 +1,229 @@ +use std::{ + path::Path, + thread::JoinHandle, + time::{Duration, Instant}, +}; + +use camino::{Utf8Path, Utf8PathBuf}; +use crossbeam::channel::{Receiver, RecvTimeoutError, Sender, TryRecvError, bounded}; +use notify::{ + EventKind, RecommendedWatcher, Watcher, + event::{ModifyKind, RenameMode}, +}; +use tracing::{debug, error, info, trace}; + +use crate::app::Event; + +#[derive(Debug)] +pub enum ElfWatchEvent { + ElfUpdated(Utf8PathBuf), + Error(String), +} + +enum ElfWatchCommand { + BeginWatch(Utf8PathBuf), + EndWatch, + Shutdown(Sender<()>), +} + +pub struct ElfWatchHandle { + command_tx: Sender, +} + +// TODO make a bespoke error type to use around the work loop + +impl ElfWatchHandle { + pub fn build(event_tx: Sender) -> Result<(Self, JoinHandle<()>), notify::Error> { + let (command_tx, command_rx) = bounded(1); + let (watcher_tx, watcher_rx) = bounded(10); + + let watcher = notify::recommended_watcher(watcher_tx)?; + + let mut worker = ElfWatchWorker { + command_rx, + event_tx, + watcher_rx, + watcher, + file_under_watch: None, + load_debounce_instant: Instant::now(), + }; + + let worker = std::thread::spawn(move || { + worker + .work_loop() + .expect("ELF Watcher encountered a fatal error"); + }); + + Ok((Self { command_tx }, worker)) + } + + pub fn begin_watch(&self, elf_path: &Utf8Path) { + let path = elf_path.to_owned(); + self.command_tx + .send(ElfWatchCommand::BeginWatch(path)) + .unwrap(); + } + + pub fn end_watch(&self) { + self.command_tx.send(ElfWatchCommand::EndWatch).unwrap(); + } + + pub fn shutdown(&self) -> Result<(), ()> { + let (shutdown_tx, shutdown_rx) = bounded(0); + if self + .command_tx + .send(ElfWatchCommand::Shutdown(shutdown_tx)) + .is_ok() + { + if shutdown_rx.recv_timeout(Duration::from_secs(3)).is_ok() { + Ok(()) + } else { + error!("ELF watcher thread didn't react to shutdown request."); + Err(()) + } + } else { + error!("Couldn't send ELF watcher shutdown."); + Err(()) + } + } +} + +const DEBOUNCE_DURATION: Duration = Duration::from_secs(2); + +struct ElfWatchWorker { + command_rx: Receiver, + event_tx: Sender, + watcher: RecommendedWatcher, + watcher_rx: Receiver>, + file_under_watch: Option, + load_debounce_instant: Instant, +} + +impl ElfWatchWorker { + pub fn work_loop(&mut self) -> Result<(), std::io::Error> { + loop { + // if let Ok(watcher_event_res) = + + let mut channel_notifier = crossbeam::channel::Select::new(); + channel_notifier.recv(&self.watcher_rx); + channel_notifier.recv(&self.command_rx); + // Waiting... + let _ready_index = channel_notifier.ready(); + + match self.watcher_rx.try_recv() { + Ok(watcher_event_res) => { + match watcher_event_res { + Ok(watcher_event) if self.event_matches_watched_file(&watcher_event) => { + // trace!("File watcher event: {watcher_event:?}"); + + if self.load_debounce_instant.elapsed() < DEBOUNCE_DURATION { + info!("Ignoring ELF file update, too soon since last one."); + } else { + let owned_watched_path = + self.file_under_watch.as_ref().unwrap().to_owned(); + + if let Err(e) = self.event_tx.send(Event::DefmtElfWatch( + ElfWatchEvent::ElfUpdated(owned_watched_path), + )) { + error!("Error sending file watch event, stopping thread: {e}"); + break; + } + self.load_debounce_instant = Instant::now(); + debug!("ELF Watcher sent reload request."); + } + } + Ok(_) => (), + Err(e) => error!("File watcher error: {e}"), + } + } + Err(TryRecvError::Empty) => (), + Err(TryRecvError::Disconnected) => break, + } + + match self.command_rx.try_recv() { + Ok(ElfWatchCommand::Shutdown(shutdown_tx)) => { + shutdown_tx + .send(()) + .expect("Failed to reply to shutdown request"); + break; + } + Ok(command) => { + self.handle_command(command).unwrap(); + } + Err(TryRecvError::Empty) => (), + Err(TryRecvError::Disconnected) => break, + } + } + + Ok(()) + } + fn event_matches_watched_file(&self, event: ¬ify::Event) -> bool { + if let Some(watched_path) = &self.file_under_watch + && event.paths.iter().any(|p| p == watched_path) + { + // guh. + match event.kind { + EventKind::Create(_) => true, + EventKind::Modify(modify_kind) => match modify_kind { + ModifyKind::Data(_) => true, + ModifyKind::Any => true, + ModifyKind::Other => true, + ModifyKind::Metadata(_) => false, + ModifyKind::Name(rename_mode) => match rename_mode { + RenameMode::To => true, + RenameMode::From => false, + + RenameMode::Both if event.paths[1] == *watched_path => true, + RenameMode::Both => false, + + RenameMode::Any | RenameMode::Other => true, + }, + }, + EventKind::Any => true, + EventKind::Other => true, + EventKind::Access(_) => false, + EventKind::Remove(_) => false, + } + } else { + false + } + } + fn handle_command(&mut self, command: ElfWatchCommand) -> Result<(), std::io::Error> { + match command { + ElfWatchCommand::BeginWatch(new_file) => { + info!("Asked to watch for updates to: {new_file}"); + // Check if we're already watching it + if let Some(current_path) = &self.file_under_watch + && *current_path == new_file + { + info!("Already watching! Not acting further."); + return Ok(()); + } + + let new_file_parent = new_file.parent().ok_or("file has no parent").unwrap(); + + self.handle_command(ElfWatchCommand::EndWatch)?; + + if let Err(e) = self.watcher.watch( + new_file_parent.as_ref(), + notify::RecursiveMode::NonRecursive, + ) { + self.event_tx + .send(Event::DefmtElfWatch(ElfWatchEvent::Error(e.to_string()))) + .unwrap(); + } + _ = self.file_under_watch.insert(new_file); + } + ElfWatchCommand::EndWatch => { + if let Some(old_path) = self.file_under_watch.take() { + let old_path_parent = old_path.parent().unwrap(); + if let Err(e) = self.watcher.unwatch(old_path_parent.as_ref()) { + error!("Error unwatching file: {e}") + } + } + } + ElfWatchCommand::Shutdown(_) => unreachable!(), + } + Ok(()) + } +} diff --git a/src/buffer/defmt/frame_delimiting.rs b/src/buffer/defmt/frame_delimiting.rs new file mode 100644 index 0000000..5f9de2d --- /dev/null +++ b/src/buffer/defmt/frame_delimiting.rs @@ -0,0 +1,180 @@ +use nom::{ + IResult, + branch::alt, + bytes::{ + complete, + streaming::{self, tag}, + }, + combinator::{cut, map}, + sequence::{preceded, terminated}, +}; + +use crate::buffer::{DelimitedSlice, RangeSlice}; + +#[inline(always)] +pub fn esp_println_delimited(input: &[u8]) -> IResult<&[u8], DelimitedSlice<'_>> { + const FRAME_START: &[u8] = &[0xFF, 0x00]; + const FRAME_END: &[u8] = &[0x00]; + + const { + assert!(FRAME_END.len() == 1); + } + + // Try a framed packet first: 0xFF 0x00 [potentially leading 0x00's] ...content... 0x00 + let packet = map( + preceded( + tag(FRAME_START), // header + cut(terminated( + preceded( + // skip any leading 0x00 bytes after FRAME_START before content + complete::take_till(|b| b != FRAME_END[0]), + // payload, never empty, must not contain 0x00 mid-data + streaming::take_till(|b| b == FRAME_END[0]), + ), + tag(FRAME_END), // terminator, 0x00, rzcobs frame end + )), + ), + |inner: &[u8]| { + let range_slice = unsafe { RangeSlice::from_parent_and_child(input, inner) }; + // Add length of terminating tag, + let raw_end = range_slice.range.end.wrapping_add(FRAME_END.len()); + // and start from the beginning of the input for the rest. + DelimitedSlice::DefmtRzcobs { + raw: &input[..raw_end], + inner, + } + }, + ); + + const FRAME_INIT_BYTE: u8 = FRAME_START[0]; + + // If no frame was found (incomplete or otherwise), spit out raw bytes + // up to (but not including) the next thing that could be the start of a frame. + let raw_run = map( + complete::take_till(|b| b == FRAME_INIT_BYTE), + DelimitedSlice::Raw, + ); + + // But if we run into a "packet" that matched part of the FRAME_START but not entirely, + // just return what we have up until the next potential frame. + let non_defmt_packet = map( + preceded( + tag(&[FRAME_INIT_BYTE]), + complete::take_till(|b| b == FRAME_INIT_BYTE), + ), + |raw: &[u8]| { + let raw_with_ff = &input[0..raw.len() + 1]; // including the 0xFF byte in the result + DelimitedSlice::Raw(raw_with_ff) + }, + ); + + alt((packet, raw_run, non_defmt_packet))(input) +} + +#[test] +fn esp_defmt_delimit_test() { + let packet = &[0xFF, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00]; + + let (rest, delimited) = esp_println_delimited(packet).unwrap(); + + assert!(rest.is_empty()); + assert_eq!( + delimited, + DelimitedSlice::DefmtRzcobs { + raw: packet, + inner: &packet[4..8] + } + ); + + let packet = &[0xFF, 0x00]; + let result = esp_println_delimited(packet); + assert!(result.is_err()); + + let packet = &[0xDE, 0xAD, 0xBE, 0xEF]; + let (rest, delimited) = esp_println_delimited(packet).unwrap(); + assert!(rest.is_empty()); + assert_eq!(delimited, DelimitedSlice::Raw(packet)); + + let packet = &[0xDE, 0xAD, 0xBE, 0xEF, 0xFF]; + let (rest, delimited) = esp_println_delimited(packet).unwrap(); + assert!(rest.len() == 1); + assert_eq!(delimited, DelimitedSlice::Raw(&packet[..4])); +} + +#[inline(always)] +pub fn zero_delimited(input: &[u8]) -> IResult<&[u8], DelimitedSlice<'_>> { + map( + terminated( + preceded( + // skip any leading 0x00 bytes after FRAME_START before content + complete::take_till(|b| b != 0x00), + // payload, never empty, must not contain 0x00 mid-data + streaming::take_till(|b| b == 0x00), + ), + tag(&[0x00]), + ), + |inner: &[u8]| { + let range_slice = unsafe { RangeSlice::from_parent_and_child(input, inner) }; + // Add length of terminating tag, + let raw_end = range_slice.range.end.wrapping_add(1); + // and start from the beginning of the input for the rest. + DelimitedSlice::DefmtRzcobs { + raw: &input[..raw_end], + inner, + } + }, + )(input) +} + +#[test] +fn zero_delimit_test() { + let packet = &[0xFF, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00]; + // 1......... ----------- 2........................... + // Packet 1 Ignored Packet 2 + + let (rest, delimited) = zero_delimited(packet).unwrap(); + println!("{rest:#?}"); + assert!( + !rest.is_empty(), + "ignored + packet 2 should be still present" + ); + assert_eq!( + delimited, + DelimitedSlice::DefmtRzcobs { + raw: &packet[..2], + inner: &packet[..1] + } + ); + + let (rest, delimited) = zero_delimited(rest).unwrap(); + println!("{rest:#?}"); + assert!(rest.is_empty()); + assert_eq!( + delimited, + DelimitedSlice::DefmtRzcobs { + raw: &packet[2..], + inner: &packet[4..8] + } + ); + + //// ----------- + + let packet = &[0xFF, 0x00]; + let (rest, delimited) = zero_delimited(packet).unwrap(); + assert!(rest.is_empty()); + assert_eq!( + delimited, + DelimitedSlice::DefmtRzcobs { + raw: packet, + inner: &packet[..1] + } + ); + + let packet = &[0x00, 0xFF]; + let res = zero_delimited(packet); + assert!(res.is_err()); + + let packet = &[0xDE, 0xAD, 0xBE, 0xEF]; + let res = zero_delimited(packet); + assert!(res.is_err()); +} diff --git a/src/buffer/defmt/mod.rs b/src/buffer/defmt/mod.rs new file mode 100644 index 0000000..9e83350 --- /dev/null +++ b/src/buffer/defmt/mod.rs @@ -0,0 +1,227 @@ +use camino::{Utf8Path, Utf8PathBuf}; +use chrono::{DateTime, Local}; +use defmt_decoder::{DecodeError, Frame, Location, Locations, StreamDecoder, Table}; +use defmt_parser::Level; +use fs_err as fs; +use ratatui::{ + style::{Color, Style}, + text::Span, +}; +use tracing::{debug, warn}; + +use crate::buffer::{buf_line::BufLine, tui::defmt::defmt_level_bracketed}; + +// #[ouroboros::self_referencing] +pub struct DefmtDecoder { + elf: Vec, + pub elf_md5: String, + pub elf_path: Utf8PathBuf, + pub table: Table, + // #[borrows(table)] + // #[covariant] + // pub decoder: Box, + pub locations: Option, +} + +#[cfg(feature = "defmt_watch")] +pub mod elf_watcher; + +pub mod frame_delimiting; + +#[derive(Debug, thiserror::Error)] +pub enum DefmtPacketError { + #[error("no defmt table loaded")] + NoDecoder, + #[error("rzcobs decompress failed")] + RzcobsDecompress, + #[error("packet decode failed")] + DefmtDecode, +} + +#[derive(Debug, thiserror::Error)] +pub enum DefmtTableError { + #[error("defmt data missing")] + DataMissing, + #[error("locations get failed")] + Locations, + #[error("elf parse failed")] + ParseFail(String), +} + +// Much taken from defmt-print +// https://github.com/knurling-rs/defmt/blob/d52b9908c175497d46fc527f4f8dfd6278744f09/print/src/main.rs#L183 + +// include_bytes!( +// "/home/tony/git/yap/defmt-meow-no-wire-debug" +// ) + +impl DefmtDecoder { + // pub fn from_elf_bytes(bytes: &[u8]) -> Result { + pub fn from_elf_bytes>(path: P) -> Result { + let path = path.as_ref().to_owned(); + let bytes = fs::read(&path).unwrap(); + + let table = Table::parse(&bytes) + .map_err(|e| DefmtTableError::ParseFail(e.to_string()))? + .ok_or_else(|| DefmtTableError::DataMissing)?; + + let locs = table + .get_locations(&bytes) + .map_err(|_| DefmtTableError::Locations)?; + + // TODO notify in UI + let locations = if !table.is_empty() && locs.is_empty() { + warn!( + "Insufficient DWARF info; compile your program with `debug = 2` to enable location info." + ); + None + } else if table.indices().all(|idx| locs.contains_key(&(idx as u64))) { + Some(locs) + } else { + warn!("(BUG) location info is incomplete; it will be omitted from the output"); + None + }; + + let elf = bytes.to_owned(); + + // let decoder = DefmtDecoder::new(elf, table, Table::new_stream_decoder, locations); + + let decoder = DefmtDecoder { + elf_md5: format!("{:X}", md5::compute(&elf)), + elf, + locations, + table, + elf_path: Utf8PathBuf::from_path_buf(path).unwrap(), + }; + + Ok(decoder) + } +} + +// Shamelessly stolen from +// https://github.com/esp-rs/espflash/blob/2c56b23fdf046be5019f22e4621d215ae01cfdc1/espflash/src/cli/monitor/parser/esp_defmt.rs +// +// I don't intend on keeping this exactly like they have it forever, it's just a good starting-off point. + +// #[derive(Debug)] +// pub struct FrameDelimiter { +// buffer: Vec, +// in_frame: bool, +// } + +// Framing info added by esp-println + +// impl FrameDelimiter { +// pub fn new() -> Self { +// Self { +// buffer: Vec::new(), +// in_frame: false, +// } +// } + +// pub fn search(haystack: &[u8], look_for_end: bool) -> Option<(&[u8], usize)> { +// let needle = if look_for_end { FRAME_END } else { FRAME_START }; +// let start = if look_for_end { +// // skip leading zeros +// haystack.iter().position(|&b| b != 0)? +// } else { +// 0 +// }; + +// let end = haystack[start..] +// .windows(needle.len()) +// .position(|window| window == needle)?; + +// let end_extra = if look_for_end { needle.len() } else { 0 }; + +// Some(( +// &haystack[start..][..end + end_extra], +// start + end + needle.len(), +// )) +// } + +// /// Feeds data into the parser, extracting and processing framed or raw +// /// data. +// pub fn feed(&mut self, buffer: &[u8], mut process: impl FnMut(DefmtDelimitedSlice<'_>)) { +// self.buffer.extend_from_slice(buffer); +// debug!("feeding {} bytes", buffer.len()); +// debug!("{buffer:?}"); +// while let Some((frame, consumed)) = Self::search(&self.buffer, self.in_frame) { +// debug!( +// "in_frame: {} | frame len: {} | consumed: {}", +// self.in_frame, +// frame.len(), +// consumed +// ); +// if self.in_frame { +// process(DefmtDelimitedSlice::DefmtRzcobs { +// raw: &self.buffer[..consumed], +// inner: frame, +// }); +// } else if !frame.is_empty() { +// process(DefmtDelimitedSlice::Raw(frame)); +// } +// self.in_frame = !self.in_frame; + +// self.buffer.drain(..consumed); +// } + +// if !self.in_frame { +// // If we have a 0xFF byte at the end, we should assume it's the start of a new +// // frame. +// let consume = if self.buffer.ends_with(&[0xFF]) { +// &self.buffer[..self.buffer.len() - 1] +// } else { +// self.buffer.as_slice() +// }; + +// if !consume.is_empty() { +// process(DefmtDelimitedSlice::Raw(consume)); +// self.buffer.drain(..consume.len()); +// } +// } +// } +// } + +// pub struct ProcessedFrame<'a> { +// level: Option, +// location: Option<&'a Location>, +// } + +/// Decode a full message. +/// +/// `data` must be a full rzCOBS encoded message. Decoding partial +/// messages is not possible. `data` must NOT include any `0x00` separator byte. +pub fn rzcobs_decode(data: &[u8]) -> Result, DecodeError> { + let mut res = vec![]; + let mut data = data.iter().rev().cloned(); + while let Some(x) = data.next() { + match x { + 0 => return Err(DecodeError::Malformed), + 0x01..=0x7f => { + for i in 0..7 { + if x & (1 << (6 - i)) == 0 { + res.push(data.next().ok_or(DecodeError::Malformed)?); + } else { + res.push(0); + } + } + } + 0x80..=0xfe => { + let n = (x & 0x7f) + 7; + res.push(0); + for _ in 0..n { + res.push(data.next().ok_or(DecodeError::Malformed)?); + } + } + 0xff => { + for _ in 0..134 { + res.push(data.next().ok_or(DecodeError::Malformed)?); + } + } + } + } + + res.reverse(); + Ok(res) +} diff --git a/src/buffer/logging.rs b/src/buffer/logging.rs index e5857aa..eca665c 100644 --- a/src/buffer/logging.rs +++ b/src/buffer/logging.rs @@ -52,6 +52,7 @@ pub enum LoggingCommand { RequestStop, // RequestToggle(DateTime, SerialPortInfo), RequestClearFiles, + // InvalidateAndResetClearIndices, RxBytes(DateTime, Vec), TxBytes { timestamp: DateTime, diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index 26ba964..55b410e 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, thread::JoinHandle}; +use std::{cmp::Ordering, ops::Range, thread::JoinHandle}; use ansi_to_tui::{IntoText, LossyFlavor}; use bstr::{ByteSlice, ByteVec}; @@ -20,12 +20,21 @@ use serialport::SerialPortInfo; use takeable::Takeable; use tracing::{debug, error, info, warn}; +#[cfg(feature = "defmt")] +use crate::buffer::defmt::DefmtDecoder; + +#[cfg(feature = "defmt")] +use crate::settings::Defmt; use crate::{ app::Event, - buffer::tui::COLOR_RULES_PATH, + buffer::{ + buf_line::{BufLineKit, FrameLocation, RenderSettings}, + defmt::{DefmtPacketError, rzcobs_decode}, + tui::COLOR_RULES_PATH, + }, changed, errors::YapResult, - settings::{LoggingType, Rendering}, + settings::{DefmtSupport, LoggingType, Rendering}, traits::{ByteSuffixCheck, LineHelpers, interleave_by}, tui::color_rules::ColorRules, }; @@ -43,6 +52,10 @@ mod logging; use logging::LoggingHandle; #[cfg(feature = "logging")] pub use logging::{DEFAULT_TIMESTAMP_FORMAT, LoggingEvent}; +#[cfg(feature = "defmt")] +pub mod defmt; +// #[cfg(feature = "defmt")] +// pub mod ; #[cfg(test)] mod tests; @@ -59,6 +72,71 @@ pub struct BufferState { hex_section_width: u16, } +#[derive(Debug, Clone)] +struct RangeSlice<'a> { + range: Range, + slice: &'a [u8], +} + +impl<'a> AsRef<[u8]> for RangeSlice<'a> { + fn as_ref(&self) -> &[u8] { + self.slice + } +} + +impl<'a> RangeSlice<'a> { + /// Create a [RangeSlice] from a parent buffer (likely a `Vec`) + /// and a child slice (`&[u8]`) from within the parent buffer, + /// populating the `range` field with the child slice's start and end indices. + /// + /// # Safety + /// `child` **must** be a subslice of `parent`, i.e. both slices come from the same + /// allocation and `child` lies **entirely** within `parent`. + /// + /// # Panics + /// + /// Debug Builds: Panics if the child slice is empty, larger than the parent, + /// or if the child's pointers do not lie within the parent's pointer bounds. + /// + /// Release Builds: Assertions skipped. + pub unsafe fn from_parent_and_child(parent: &'a [u8], child: &'a [u8]) -> Self { + // Fail-fast checks + debug_assert!(!parent.is_empty()); + debug_assert!(!child.is_empty()); + // TODO make debug_assert? + debug_assert!( + child.len() <= parent.len(), + "child can't be larger than parent" + ); + let parent_range = parent.as_ptr_range(); + let child_range = child.as_ptr_range(); + + // Ensure child pointers lies within parent pointer bounds + // TODO make debug_assert? + debug_assert!( + child_range.start >= parent_range.start, + "child_range.start must be >= parent_range.start", + ); + debug_assert!( + child_range.end <= parent_range.end, + "child_range.end must be <= parent_range.end", + ); + + // Getting difference between pointers in T-sized chunks. + // + // SAFETY: + // Trivial to ensure safety, as long as documented + // precondition of ensuring `child` is always a + // subslice of `parent`. + let offset = unsafe { child_range.start.offset_from(parent_range.start) } as usize; + + Self { + range: offset..offset + child.len(), + slice: child, + } + } +} + impl UserEcho { /// Determines whether a given `BufLine` should be displayed based on the current `UserEcho` setting. /// @@ -70,15 +148,21 @@ impl UserEcho { /// - `NoBytes`: Display all user lines except those marked as bytes (if they have any unprintable bytes when escaped). /// - `NoMacros`: Display all user lines except those marked as macros. /// - `NoMacrosOrBytes`: Display only user lines that are neither bytes nor macros. - fn filter_user_line(&self, buf_line: &BufLine) -> bool { + /// + /// # Panics + /// + /// Panics if line_type isn't a user line. + fn filter_user_line(&self, line_type: &LineType) -> bool { + assert!( + matches!(line_type, LineType::User { .. }), + "port lines not allowed" + ); match self { UserEcho::None => false, UserEcho::All => true, - UserEcho::NoBytes => !buf_line.line_type.is_bytes(), - UserEcho::NoMacros => !buf_line.line_type.is_macro(), - UserEcho::NoMacrosOrBytes => { - !buf_line.line_type.is_bytes() && !buf_line.line_type.is_macro() - } + UserEcho::NoBytes => !line_type.is_bytes(), + UserEcho::NoMacros => !line_type.is_macro(), + UserEcho::NoMacrosOrBytes => !line_type.is_bytes() && !line_type.is_macro(), } } } @@ -98,6 +182,14 @@ impl LineEnding { LineEnding::MultiByte(finder) => finder.needle(), } } + + fn escaped_from(&self, buffer: &[u8]) -> Option { + if buffer.has_line_ending(self) { + Some(self.as_bytes().escape_bytes().to_compact_string()) + } else { + None + } + } } impl PartialEq for LineEnding { @@ -208,14 +300,319 @@ struct RawBuffer { inner: Vec, /// Time-tagged indexes into `raw_buffer`, from each input from the port. buffer_timestamps: Vec<(usize, DateTime)>, + consumed_up_to: usize, +} + +impl RawBuffer { + fn with_capacities(raw: usize, timestamps: usize) -> Self { + Self { + buffer_timestamps: Vec::with_capacity(timestamps), + inner: Vec::with_capacity(raw), + consumed_up_to: 0, + } + } + fn reset(&mut self) { + self.inner.clear(); + self.inner.shrink_to(1024); + self.buffer_timestamps.clear(); + self.buffer_timestamps.shrink_to(1024); + self.consumed_up_to = 0; + } + fn feed(&mut self, new: &[u8], timestamp: DateTime) { + // warn!("fed {} bytes", new.len()); + self.buffer_timestamps.push((self.inner.len(), timestamp)); + self.inner.extend(new); + } + fn consumed(&mut self, amount: usize) { + self.consumed_up_to += amount; + } + fn range(&self, range: Range) -> Option<&[u8]> { + let len = self.inner.len(); + if range.end <= len { + Some(&self.inner[range]) + } else { + None + } + } + fn next_slice( + &self, + defmt_support: DefmtSupport, + defmt_raw_consume_fail_raw_buffer_len: &Option, + ) -> Option<(usize, DelimitedSlice)> { + match defmt_support { + DefmtSupport::FramedRzcobs => self.next_slice_defmt_rzcobs(true), + DefmtSupport::UnframedRzcobs => self.next_slice_defmt_rzcobs(false), + DefmtSupport::Raw => match defmt_raw_consume_fail_raw_buffer_len { + None => self.next_slice_defmt_raw(), + Some(raw_len_on_fail) if *raw_len_on_fail == self.inner.len() => None, + Some(raw_len_on_fail) if *raw_len_on_fail == usize::MAX => None, + Some(_) => self.next_slice_defmt_raw(), + }, + DefmtSupport::Disabled => self.next_slice_raw(), + } + } + fn next_slice_raw(&self) -> Option<(usize, DelimitedSlice)> { + let start_index = self.consumed_up_to; + let newest = &self.inner[start_index..]; + if newest.is_empty() { + None + } else { + Some((start_index, DelimitedSlice::Raw(newest))) + } + } + fn next_slice_defmt_raw(&self) -> Option<(usize, DelimitedSlice)> { + let start_index = self.consumed_up_to; + let newest = &self.inner[start_index..]; + if newest.is_empty() { + None + } else { + Some((start_index, DelimitedSlice::DefmtRaw(newest))) + } + } + /// Returns (index_in_buffer, raw/defmt slice) + /// + /// Returns None if either a defmt frame is incomplete, or there is no new data to give. + fn next_slice_defmt_rzcobs(&self, esp_println_framed: bool) -> Option<(usize, DelimitedSlice)> { + let start_index = self.consumed_up_to; + let newest = &self.inner[start_index..]; + if newest.is_empty() { + return None; + } + + let slice_fn = if esp_println_framed { + defmt::frame_delimiting::esp_println_delimited + } else { + defmt::frame_delimiting::zero_delimited + }; + + let slice = match slice_fn(newest) { + // `rest` unused, up to caller to call Self::consumed() with, + // the length of `slice` to avoid reconsumption. + Ok((_rest, slice)) => slice, + // Only returned if there's an incomplete frame. + Err(_) => { + return None; + } + }; + + Some((start_index, slice)) + } +} + +#[derive(Debug, PartialEq)] +pub enum DelimitedSlice<'a> { + #[cfg(feature = "defmt")] + /// Used by framed inputs, such as from esp-println. + DefmtRzcobs { + /// Complete original slice, containing prefix and terminator. + raw: &'a [u8], + /// (Supposedly) rzCOBS packet, stripped of prefix and terminator. + inner: &'a [u8], + }, + #[cfg(feature = "defmt")] + /// Use if ELF has raw encoding enabled (no rzCOBS compression). + DefmtRaw(&'a [u8]), + /// Non-defmt input, either junk data or plain ASCII/UTF-8 logs. + Raw(&'a [u8]), +} + +impl DelimitedSlice<'_> { + pub fn raw_len(&self) -> usize { + match self { + #[cfg(feature = "defmt")] + DelimitedSlice::DefmtRzcobs { raw, .. } => raw.len(), + #[cfg(feature = "defmt")] + DelimitedSlice::DefmtRaw(raw) => raw.len(), + DelimitedSlice::Raw(raw) => raw.len(), + } + } } struct StyledLines { rx: Vec, - last_rx_completed: bool, + last_rx_was_complete: bool, tx: Vec, } +impl StyledLines { + fn failed_decode( + &mut self, + delimited_slice: DelimitedSlice, + reason: DefmtPacketError, + kit: BufLineKit, + line_ending: &LineEnding, + ) { + let DelimitedSlice::DefmtRzcobs { raw, inner } = delimited_slice else { + unreachable!() + }; + + let mut text = format!("Couldn't decode defmt rzcobs packet ({reason}): "); + text.extend(inner.iter().map(|b| format!("{b:02X}"))); + + self.rx + .push(BufLine::port_text_line(Line::raw(text), kit, line_ending)); + } + fn consume_as_text( + &mut self, + raw_buffer: &RawBuffer, + color_rules: &ColorRules, + index_in_buffer: usize, + delimited_slice: DelimitedSlice, + kit: BufLineKit, + line_ending: &LineEnding, + ) { + let mut can_append_to_line = !self.last_rx_was_complete; + + let DelimitedSlice::Raw(slice) = delimited_slice else { + unreachable!() + }; + + for (trunc, orig, indices) in line_ending_iter(slice, &line_ending) { + // index = self.raw.inner.len(); + + // if let Some(index) = self.index_of_incomplete_line.take() { + if can_append_to_line { + can_append_to_line = false; + let last_line = self.rx.last_mut().expect("can't append to nothing"); + let last_index = last_line.index_in_buffer(); + // assert_eq!(last_index, index); + + // let start = range_slice.range.start; + let trunc = last_index..index_in_buffer + trunc.len(); + let trunc = raw_buffer.range(trunc).unwrap(); + let orig = last_index..index_in_buffer + orig.len(); + let orig = raw_buffer.range(orig).unwrap(); + // debug!("Appendo from {last_index}! trunc: {trunc:#?} orig: {orig:#?}"); + + // info!("AAAFG: {:?}", slice); + let lossy_flavor = if kit.render.rendering.escape_invalid_bytes { + LossyFlavor::escaped_bytes_styled(Style::new().dark_gray()) + } else { + LossyFlavor::replacement_char() + }; + let mut line = match trunc.into_line_lossy(Style::new(), lossy_flavor) { + Ok(line) => line, + Err(_) => { + error!("ansi-to-tui failed to parse input! Using unstyled text."); + Line::from(String::from_utf8_lossy(trunc).to_string()) + } + }; + + let line_opt = color_rules.apply_onto(trunc, line); + + // let render_settings = RenderSettings { + // rendering: &self.rendering, + // defmt: &self.defmt_settings, + // }; + + if let Some(line) = line_opt { + let kit = BufLineKit { + full_range_slice: unsafe { + RangeSlice::from_parent_and_child(&raw_buffer.inner, orig) + }, + ..kit + }; + + last_line.update_line(line, kit, &line_ending); + } else { + _ = self.rx.pop(); + self.last_rx_was_complete = true; + // last_line.clear_line(); + } + self.last_rx_was_complete = orig.has_line_ending(&line_ending); + continue; + } + + let kit = BufLineKit { + full_range_slice: unsafe { + RangeSlice::from_parent_and_child(&raw_buffer.inner, orig) + }, + ..kit + }; + + if let Some(new_bufline) = slice_as_port_text(kit, color_rules, line_ending) { + self.rx.push(new_bufline); + } + self.last_rx_was_complete = orig.has_line_ending(&line_ending); + } + } + fn consume_frame( + &mut self, + kit: BufLineKit, + decoder: &DefmtDecoder, + frame: &defmt_decoder::Frame<'_>, + color_rules: &ColorRules, + ) { + // choosing to skip pushing instead of filtering + // to keep consistent with other logic that expects + // invisible lines to not be pushed, + // and to skip further handling for those lines. + let commit_frame = match frame.level() { + None => true, + Some(level) => kit.render.defmt.max_log_level <= crate::settings::Level::from(level), + }; + if !commit_frame { + return; + } + + // let meow = std::time::Instant::now(); + // error!("{:?}", meow.elapsed()); + // debug!("{frame:#?}"); + let loc_opt = decoder + .locations + .as_ref() + .and_then(|locs| locs.get(&frame.index())) + .map(FrameLocation::from); + + let message = frame.display_message().to_string(); + let message_lines = message.lines(); + + let device_timestamp = frame.display_timestamp(); + let device_timestamp_ref = device_timestamp + .as_ref() + .map(|ts| ts as &dyn std::fmt::Display); + + for line in message_lines { + let mut message_line = Line::default(); + message_line.push_span(Span::raw(line)); + + if let Some(line) = color_rules.apply_onto(line.as_bytes(), message_line) { + let owned_spans: Vec> = line + .into_iter() + .map(|s| match s.content { + std::borrow::Cow::Owned(owned) => Span { + content: std::borrow::Cow::Owned(owned), + ..s + }, + std::borrow::Cow::Borrowed(borrowed) => Span { + content: std::borrow::Cow::Owned(borrowed.to_string()), + ..s + }, + }) + .collect(); + + let owned_line = Line { + spans: owned_spans, + ..Default::default() + }; + + let kit = BufLineKit { + full_range_slice: kit.full_range_slice.clone(), + ..kit + }; + + self.rx.push(BufLine::port_defmt_line( + owned_line, + kit, + frame.level(), + device_timestamp_ref, + loc_opt.clone(), + )); + } + } + } +} + pub struct Buffer { raw: RawBuffer, styled_lines: StyledLines, @@ -237,6 +634,14 @@ pub struct Buffer { log_thread: Takeable>, #[cfg(feature = "logging")] log_settings: Logging, + #[cfg(feature = "defmt")] + pub defmt_decoder: Option, + #[cfg(feature = "defmt")] + defmt_settings: Defmt, + #[cfg(feature = "defmt")] + defmt_raw_consume_fail_raw_buffer_len: Option, + // #[cfg(feature = "defmt")] + // frame_delimiter: FrameDelimiter, } #[cfg(feature = "logging")] @@ -290,7 +695,8 @@ impl Buffer { line_ending: &[u8], rendering: Rendering, #[cfg(feature = "logging")] logging: Logging, - event_tx: Sender, + #[cfg(feature = "logging")] event_tx: Sender, + #[cfg(feature = "defmt")] defmt: Defmt, ) -> Self { let line_ending: LineEnding = line_ending.into(); #[cfg(feature = "logging")] @@ -300,14 +706,15 @@ impl Buffer { Self { raw: RawBuffer { inner: Vec::with_capacity(1024), - buffer_timestamps: Vec::with_capacity(1024), + consumed_up_to: 0, }, styled_lines: StyledLines { rx: Vec::with_capacity(1024), - last_rx_completed: true, + last_rx_was_complete: true, tx: Vec::with_capacity(1024), }, + last_terminal_size: Size::default(), state: BufferState { vert_scroll: 0, @@ -325,6 +732,12 @@ impl Buffer { log_thread: Takeable::new(log_thread), #[cfg(feature = "logging")] log_settings: logging, + #[cfg(feature = "defmt")] + defmt_decoder: None, + #[cfg(feature = "defmt")] + defmt_settings: defmt, + #[cfg(feature = "defmt")] + defmt_raw_consume_fail_raw_buffer_len: None, } } // pub fn append_str(&mut self, str: &str) { @@ -338,7 +751,6 @@ impl Buffer { sensitive: bool, ) { let now = Local::now(); - let user_span = span!(Color::DarkGray; "BYTE> "); let text = if !sensitive { @@ -357,30 +769,42 @@ impl Buffer { let line = Line::from(vec![user_span, text]); - let combined = bytes.iter().chain(line_ending.iter()).map(|b| *b).collect(); + let combined: Vec<_> = bytes.iter().chain(line_ending.iter()).map(|b| *b).collect(); + let line_ending: LineEnding = line_ending.into(); // line.spans.insert(0, user_span.clone()); // line.style_all_spans(Color::DarkGray.into()); - let user_buf_line = BufLine::new_with_line( - line, - bytes, - self.raw.inner.len(), // .max(1) - self.last_terminal_size.width, - &self.rendering, - &self.line_ending, - now, - LineType::User { - is_bytes: true, - is_macro, - reloggable_raw: combined, + // let user_buf_line = BufLine::new_with_line( + // line, + // bytes, + // self.raw.inner.len(), // .max(1) + // self.last_terminal_size.width, + // &self.rendering, + // &self.line_ending, + // now, + // LineType::User { + // is_bytes: true, + // is_macro, + // reloggable_raw: combined, + // }, + // ); + let kit = BufLineKit { + timestamp: now, + area_width: self.last_terminal_size.width, + render: self.line_render_settings(), + full_range_slice: RangeSlice { + range: self.raw.inner.len()..self.raw.inner.len(), + slice: &[], }, - ); + }; + let user_buf_line = BufLine::user_line(line, kit, &line_ending, true, is_macro, &combined); - self.styled_lines.last_rx_completed = self + self.styled_lines.last_rx_was_complete = self .rendering .echo_user_input - .filter_user_line(&user_buf_line) + .filter_user_line(&user_buf_line.line_type) || (self.raw.inner.is_empty() || self.raw.inner.has_line_ending(&self.line_ending)); + #[cfg(feature = "logging")] if self.log_handle.logging_active() { match self.log_settings.log_file_type { @@ -402,18 +826,20 @@ impl Buffer { sensitive: bool, ) { let now = Local::now(); - let escaped_line_ending = line_ending.escape_bytes().to_string(); - let escaped_chained: Vec = text - .as_bytes() - .iter() - .chain(escaped_line_ending.as_bytes().iter()) - .map(|i| *i) - .collect(); + // let escaped_line_ending = line_ending.escape_bytes().to_string(); + // let escaped_chained: Vec = text + // .as_bytes() + // .iter() + // .chain(escaped_line_ending.as_bytes().iter()) + // .map(|i| *i) + // .collect(); + + let line_ending: LineEnding = line_ending.into(); let user_span = span!(Color::DarkGray;"USER> "); // let Text { lines, .. } = text; // TODO HANDLE MULTI-LINE USER INPUT AAAA - for (trunc, orig, _indices) in line_ending_iter(&escaped_chained, &self.line_ending) { + for (trunc, orig, _indices) in line_ending_iter(text.as_bytes(), &line_ending) { // not sure if i want to ansi-style user text? // let mut line = match trunc.into_line_lossy(Style::new()) { // Ok(line) => line, @@ -434,26 +860,37 @@ impl Buffer { line }; - let user_buf_line = BufLine::new_with_line( - line, - orig, - self.raw.inner.len(), // .max(1) - self.last_terminal_size.width, - &self.rendering, - &self.line_ending, - now, - LineType::User { - is_bytes: false, - is_macro, - reloggable_raw: orig.to_owned(), + // let user_buf_line = BufLine::new_with_line( + // line, + // orig, + // self.raw.inner.len(), // .max(1) + // self.last_terminal_size.width, + // &self.rendering, + // &self.line_ending, + // now, + // LineType::User { + // is_bytes: false, + // is_macro, + // reloggable_raw: orig.to_owned(), + // }, + // ); + let kit = BufLineKit { + timestamp: now, + area_width: self.last_terminal_size.width, + render: self.line_render_settings(), + full_range_slice: RangeSlice { + range: self.raw.inner.len()..self.raw.inner.len(), + slice: &[], }, - ); + }; + let user_buf_line = BufLine::user_line(line, kit, &line_ending, false, is_macro, orig); // Used to be out of the for loop. - self.styled_lines.last_rx_completed = self + self.styled_lines.last_rx_was_complete = self .rendering .echo_user_input - .filter_user_line(&user_buf_line) + .filter_user_line(&user_buf_line.line_type) || (self.raw.inner.is_empty() || self.raw.inner.has_line_ending(&self.line_ending)); + #[cfg(feature = "logging")] if self.log_handle.logging_active() { match self.log_settings.log_file_type { @@ -468,127 +905,180 @@ impl Buffer { } } - /// Consumes **post**-split by line endings slices, - /// either creating a new line or appending to the last one. - fn consume_port_bytes<'a>( - &mut self, - trunc: &'a [u8], - orig: &'a [u8], - start_index: usize, - known_time: Option>, - ) { - // debug!("{trunc:?}, {orig:?}"); - assert!( - trunc.len() <= orig.len(), - "truncated buffer can't be larger than original" - ); - let append_to_last = !self.styled_lines.last_rx_completed; - if orig.is_empty() { - return; - } + // Forced to use Vec for now + pub fn fresh_rx_bytes(&mut self, bytes: &mut Vec) { + let now = Local::now(); + // debug!("{lines:?}"); + // debug!("{:#?}", self.lines); + #[cfg(feature = "logging")] + self.log_handle.log_rx_bytes(now, bytes.clone()).unwrap(); - let this_rx_completed = self.raw.inner.has_line_ending(&self.line_ending); + self.raw.feed(&bytes, now); - if append_to_last { - let last_line = self - .styled_lines - .rx - .last_mut() - .expect("can't append to nothing"); - let last_index = last_line.index_in_buffer(); + // let meow = std::time::Instant::now(); + self.consume_latest_bytes(now); + // error!("{:?}", meow.elapsed()); - let trunc = &self.raw.inner[last_index..start_index + trunc.len()]; - let orig = &self.raw.inner[last_index..start_index + orig.len()]; - // info!("AAAFG: {:?}", slice); - let lossy_flavor = if self.rendering.escape_invalid_bytes { - LossyFlavor::escaped_bytes_styled(Style::new().dark_gray()) - } else { - LossyFlavor::replacement_char() + // self.raw.inner.extend(bytes.iter()); + // while self.consume_latest_bytes(Some(now)) { + // (); + // } + } + + fn consume_latest_bytes(&mut self, timestamp: DateTime) { + #[cfg(not(feature = "defmt"))] + while let Some((index_in_buffer, delimited_slice)) = self.raw.next_slice_raw() { + let DelimitedSlice::Raw(slice) = delimited_slice else { + unreachable!(); }; - let mut line = match trunc.into_line_lossy(Style::new(), lossy_flavor) { - Ok(line) => line, - Err(_) => { - error!("ansi-to-tui failed to parse input! Using unstyled text."); - Line::from(String::from_utf8_lossy(trunc).to_string()) - } + let kit = BufLineKit { + timestamp, + area_width: self.last_terminal_size.width, + render: RenderSettings { + rendering: &self.rendering, + defmt: &self.defmt_settings, + }, + full_range_slice: unsafe { + RangeSlice::from_parent_and_child(&self.raw.inner, slice) + }, }; - // debug!( - // "buf_index: {last_index}, update: {line}", - // line = line - // .spans - // .iter() - // .map(|s| s.content.as_ref()) - // .join("") - // .escape_default() - // ); - // if line.width() >= 5 { - // line.style_slice(1..3, Style::new().red().italic()); - // } + self.styled_lines.consume_as_text( + &self.raw, + &self.color_rules, + index_in_buffer, + delimited_slice, + kit, + &self.line_ending, + ); + self.raw.consumed(slice.len()); + } - let line_opt = self.color_rules.apply_onto(trunc, line); + #[cfg(feature = "defmt")] + while let Some((index_in_buffer, delimited_slice)) = self.raw.next_slice( + self.defmt_settings.defmt_parsing.clone(), + &self.defmt_raw_consume_fail_raw_buffer_len, + ) { + match delimited_slice { + DelimitedSlice::Raw(slice) => { + let kit = BufLineKit { + timestamp, + area_width: self.last_terminal_size.width, + render: RenderSettings { + rendering: &self.rendering, + defmt: &self.defmt_settings, + }, + full_range_slice: unsafe { + RangeSlice::from_parent_and_child(&self.raw.inner, slice) + }, + }; - if let Some(line) = line_opt { - last_line.update_line( - line, - orig, - self.last_terminal_size.width, - &self.rendering, - &self.line_ending, - ); - } else { - _ = self.styled_lines.rx.pop(); - self.styled_lines.last_rx_completed = true; - // last_line.clear_line(); - } - } else { - let lossy_flavor = if self.rendering.escape_invalid_bytes { - LossyFlavor::escaped_bytes_styled(Style::new().dark_gray()) - } else { - LossyFlavor::replacement_char() - }; - let mut line = match trunc.into_line_lossy(Style::new(), lossy_flavor) { - Ok(line) => line, - Err(_) => { - error!("ansi-to-tui failed to parse input! Using unstyled text."); - Line::from(String::from_utf8_lossy(trunc).to_string()) + self.styled_lines.consume_as_text( + &self.raw, + &self.color_rules, + index_in_buffer, + delimited_slice, + kit, + &self.line_ending, + ); + self.raw.consumed(slice.len()); + } + DelimitedSlice::DefmtRaw(raw_uncompressed) => { + if let Some(decoder) = &self.defmt_decoder { + let kit = BufLineKit { + timestamp, + area_width: self.last_terminal_size.width, + render: RenderSettings { + rendering: &self.rendering, + defmt: &self.defmt_settings, + }, + full_range_slice: unsafe { + RangeSlice::from_parent_and_child(&self.raw.inner, raw_uncompressed) + }, + }; + + match decoder.table.decode(&raw_uncompressed) { + Ok((frame, consumed)) => { + self.defmt_raw_consume_fail_raw_buffer_len = None; + self.styled_lines.consume_frame( + kit, + decoder, + &frame, + &self.color_rules, + ); + self.styled_lines.last_rx_was_complete = true; + + self.raw.consumed(consumed); + } + Err(defmt_decoder::DecodeError::UnexpectedEof) => { + self.defmt_raw_consume_fail_raw_buffer_len = + Some(self.raw.inner.len()); + } + Err(defmt_decoder::DecodeError::Malformed) => { + self.defmt_raw_consume_fail_raw_buffer_len = Some(usize::MAX); + let line = + Line::raw("defmt raw parse error, ceasing further attempts."); + self.styled_lines.rx.push(BufLine::port_text_line( + line, + kit, + &LineEnding::None, + )); + } + } + } + } + DelimitedSlice::DefmtRzcobs { raw, inner } => { + let kit = BufLineKit { + timestamp, + area_width: self.last_terminal_size.width, + render: RenderSettings { + rendering: &self.rendering, + defmt: &self.defmt_settings, + }, + full_range_slice: unsafe { + RangeSlice::from_parent_and_child(&self.raw.inner, raw) + }, + }; + let raw_slice_len = raw.len(); + + if let Some(decoder) = &self.defmt_decoder { + if let Ok(uncompressed) = rzcobs_decode(inner) { + if let Ok((frame, _consumed)) = decoder.table.decode(&uncompressed) { + self.styled_lines.consume_frame( + kit, + decoder, + &frame, + &self.color_rules, + ); + } else { + self.styled_lines.failed_decode( + delimited_slice, + DefmtPacketError::DefmtDecode, + kit, + &self.line_ending, + ); + } + } else { + self.styled_lines.failed_decode( + delimited_slice, + DefmtPacketError::RzcobsDecompress, + kit, + &self.line_ending, + ); + } + } else { + self.styled_lines.failed_decode( + delimited_slice, + DefmtPacketError::NoDecoder, + kit, + &self.line_ending, + ); + } + + self.styled_lines.last_rx_was_complete = true; + self.raw.consumed(raw_slice_len); } - }; - - let line_opt = self.color_rules.apply_onto(trunc, line); - - if let Some(line) = line_opt { - self.styled_lines.rx.push(BufLine::new_with_line( - line, - orig, - start_index, - self.last_terminal_size.width, - &self.rendering, - &self.line_ending, - known_time.unwrap_or_else(Local::now), - LineType::Port { - escaped_line_ending: None, - }, - )); } - }; - - self.styled_lines.last_rx_completed = this_rx_completed; - } - - // Forced to use Vec for now - pub fn append_rx_bytes(&mut self, bytes: &mut Vec) { - let now = Local::now(); - let mut index = self.raw.inner.len(); - self.raw.buffer_timestamps.push((index, now)); - // debug!("{lines:?}"); - // debug!("{:#?}", self.lines); - #[cfg(feature = "logging")] - self.log_handle.log_rx_bytes(now, bytes.clone()).unwrap(); - for (trunc, orig, indices) in line_ending_iter(bytes, &self.line_ending.clone()) { - index = self.raw.inner.len(); - self.raw.inner.extend(orig); - self.consume_port_bytes(trunc, orig, index, Some(now)); } } @@ -606,30 +1096,61 @@ impl Buffer { // Taking these variables out of `self` temporarily to allow running &mut self methods while holding // references to these. - let timestamps = std::mem::take(&mut self.raw.buffer_timestamps); - let user_lines = std::mem::take(&mut self.styled_lines.tx); + let user_timestamps: Vec<_> = self + .styled_lines + .tx + .iter() + .map(|b| { + let LineType::User { + is_bytes, is_macro, .. + } = &b.line_type + else { + unreachable!(); + }; + + ( + LineType::User { + is_bytes: *is_bytes, + is_macro: *is_macro, + reloggable_raw: Vec::new(), + escaped_line_ending: None, + }, + b.raw_buffer_index, + b.timestamp, + ) + }) + .collect(); let orig_buf_len = self.raw.inner.len(); - let buffer = std::mem::replace(&mut self.raw.inner, Vec::with_capacity(orig_buf_len)); - let line_ending = self.line_ending.clone(); + let timestamps_len = self.raw.buffer_timestamps.len(); + let rx_buffer = std::mem::replace( + &mut self.raw, + RawBuffer::with_capacities(orig_buf_len, timestamps_len), + ); let user_echo = self.rendering.echo_user_input.clone(); // No lines to append to. - self.styled_lines.last_rx_completed = true; + self.styled_lines.last_rx_was_complete = true; + + #[cfg(feature = "defmt")] + { + self.defmt_raw_consume_fail_raw_buffer_len = None; + } // Getting all time-tagged indices in the buffer where either // 1. Data came in through the port // 2. The user sent data let interleaved_points = interleave_by( - timestamps + rx_buffer + .buffer_timestamps .iter() .map(|(index, timestamp)| (*index, *timestamp, false)) // Add a "finale" element to capture any remaining buffer, always placed at the end. .chain(std::iter::once((orig_buf_len, Local::now(), false))), - user_lines - .iter() + user_timestamps + .into_iter() // If a user line isn't visible, ignore it when taking external new-lines into account. - .filter(|b| user_echo.filter_user_line(b)) - .map(|b| (b.raw_buffer_index, b.timestamp, true)), + .filter(|(line_type, _, _)| user_echo.filter_user_line(line_type)) + .map(|(_, index, timestamp)| (index, timestamp, true)), // Interleaving by sorting in order of raw_buffer_index, if they're equal, then whichever has a sooner timestamp. |port, user| match port.0.cmp(&user.0) { Ordering::Equal => port.1 <= user.1, @@ -638,7 +1159,7 @@ impl Buffer { }, ); - let mut new_index = 0; + let mut new_length = 0; debug!("total len: {orig_buf_len}"); let buffer_slices = interleaved_points @@ -651,7 +1172,7 @@ impl Buffer { .map( |((start_index, timestamp, was_user_line), (end_index, _, _))| { ( - &buffer[start_index..end_index], + &rx_buffer.inner[start_index..end_index], timestamp, was_user_line, (start_index, end_index), @@ -668,28 +1189,31 @@ impl Buffer { // If this was where a user line we allow to render is, // then we'll finish this line early if it's not already finished. if was_user_line { - self.styled_lines.last_rx_completed = true; + self.styled_lines.last_rx_was_complete = true; continue; } - + self.raw.feed(slice, timestamp); + self.consume_latest_bytes(timestamp); // info!( // "Getting {le} slices from [{slice_start}..{slice_end}], {timestamp}, {was_user_line}", // le = line_ending.escape_debug() // ); - for (trunc, orig, (orig_start, orig_end)) in line_ending_iter(slice, &line_ending) { - // info!( - // "trunc: {trunc_len}, orig: {orig_len}. [{start}..{end}]", - // trunc_len = trunc.len(), - // orig_len = orig.len(), - // start = orig_start + slice_start, - // end = orig_end + slice_start, - // ); - self.raw.inner.extend(orig); - self.consume_port_bytes(trunc, orig, new_index, Some(timestamp)); - new_index += orig.len(); - } + // for (trunc, orig, (orig_start, orig_end)) in line_ending_iter(slice, &line_ending) { + // info!( + // "trunc: {trunc_len}, orig: {orig_len}. [{start}..{end}]", + // trunc_len = trunc.len(), + // orig_len = orig.len(), + // start = orig_start + slice_start, + // end = orig_end + slice_start, + // ); + + // while self.consume_latest_bytes(Some(timestamp)) {} + new_length += slice.len(); + // } } + // defmt_delimit(meow); + // Asserting that our work seems correct. assert_eq!( orig_buf_len, @@ -697,9 +1221,13 @@ impl Buffer { "Buffer size should not have changed during reconsumption." ); assert_eq!( - new_index, orig_buf_len, + new_length, orig_buf_len, "Iterator's slices should have same total length as raw buffer." ); + assert_eq!( + self.raw.buffer_timestamps, rx_buffer.buffer_timestamps, + "RawBuffers should have identical buffer_timestamps." + ); self.styled_lines.rx.windows(2).for_each(|lines| { assert!( lines[0].raw_buffer_index < lines[1].raw_buffer_index, @@ -709,8 +1237,8 @@ impl Buffer { // Returning variables we stole back to self. // (excluding the raw buffer since that got reconsumed gradually back into self) - self.raw.buffer_timestamps = timestamps; - self.styled_lines.tx = user_lines; + // self.raw.buffer_timestamps = rx_timestamps; + // self.styled_lines.tx = user_lines; self.scroll_by(0); } @@ -730,12 +1258,9 @@ impl Buffer { pub fn update_render_settings(&mut self, rendering: Rendering) { let old = std::mem::replace(&mut self.rendering, rendering); let new = &self.rendering; - let should_reconsume = - changed!(old, new, echo_user_input) || changed!(old, new, escape_invalid_bytes); + let should_reconsume = changed!(old, new, echo_user_input, escape_invalid_bytes); - let should_rewrap_lines = changed!(old, new, timestamps) - || changed!(old, new, show_indices) - || changed!(old, new, show_line_ending); + let should_rewrap_lines = changed!(old, new, timestamps, show_indices, show_line_ending); if changed!(old, new, bytes_per_line) { self.determine_bytes_per_line(new.bytes_per_line.into()); @@ -752,6 +1277,29 @@ impl Buffer { self.scroll_by(0); } + #[cfg(feature = "defmt")] + pub fn update_defmt_settings(&mut self, defmt: Defmt) { + let old = std::mem::replace(&mut self.defmt_settings, defmt); + let new = &self.defmt_settings; + let should_reconsume = changed!(old, new, defmt_parsing, max_log_level); + + let should_rewrap_lines = changed!( + old, + new, + device_timestamp, + show_file, + show_module, + show_line_number + ); + + if should_reconsume { + self.reconsume_raw_buffer(); + } else if should_rewrap_lines { + self.update_wrapped_line_heights(); + } + + self.scroll_by(0); + } #[cfg(feature = "logging")] fn clear_and_relog_buffers(&mut self, with_user_input: bool) { self.log_handle.clear_current_logs().unwrap(); @@ -852,16 +1400,51 @@ impl Buffer { self.styled_lines.rx.clear(); self.styled_lines.rx.shrink_to(1024); - self.raw.buffer_timestamps.clear(); - self.raw.buffer_timestamps.shrink_to(1024); - self.styled_lines.tx.clear(); self.styled_lines.tx.shrink_to(1024); - self.raw.inner.clear(); - self.raw.inner.shrink_to(1024); + self.styled_lines.last_rx_was_complete = true; + + self.raw.reset(); + } +} +// Returns None if the slice would be fully hidden by the color rules. +fn slice_as_port_text( + kit: BufLineKit, + color_rules: &ColorRules, + line_ending: &LineEnding, +) -> Option { + let lossy_flavor = if kit.render.rendering.escape_invalid_bytes { + LossyFlavor::escaped_bytes_styled(Style::new().dark_gray()) + } else { + LossyFlavor::replacement_char() + }; + + let RangeSlice { + range, + slice: original, + } = &kit.full_range_slice; + + let truncated = if original.has_line_ending(&line_ending) { + &original[..original.len() - line_ending.as_bytes().len()] + } else { + original + }; + + let line = match truncated.into_line_lossy(Style::new(), lossy_flavor) { + Ok(line) => line, + Err(_) => { + error!("ansi-to-tui failed to parse input! Using unstyled text."); + Line::from(String::from_utf8_lossy(truncated).to_string()) + } + }; + + let line_opt = color_rules.apply_onto(truncated, line); - self.styled_lines.last_rx_completed = true; + if let Some(line) = line_opt { + Some(BufLine::port_text_line(line, kit, &line_ending)) + } else { + None } } @@ -872,6 +1455,7 @@ impl Buffer { /// `usize` tuple has the inclusive indices into the given slice. /// /// If no line ending was found, emits the whole slice once. +#[inline(always)] pub fn line_ending_iter<'a>( bytes: &'a [u8], line_ending: &'a LineEnding, diff --git a/src/buffer/tui.rs b/src/buffer/tui.rs index 4680862..8c84211 100644 --- a/src/buffer/tui.rs +++ b/src/buffer/tui.rs @@ -8,6 +8,7 @@ use ratatui::{ use ratatui_macros::{horizontal, vertical}; use crate::{ + buffer::buf_line::RenderSettings, errors::YapResult, settings::HexHighlightStyle, traits::{ToggleBool, interleave_by}, @@ -22,25 +23,47 @@ impl Buffer { /// Updates each BufLine's render height with the new terminal width, returning the sum total at the end pub fn update_wrapped_line_heights(&mut self) -> usize { self.styled_lines.rx.iter_mut().fold(0, |total, l| { - let new_height = l.update_line_height(self.last_terminal_size.width, &self.rendering); + let render_settings = RenderSettings { + rendering: &self.rendering, + defmt: &self.defmt_settings, + }; + let new_height = l.update_line_height(self.last_terminal_size.width, render_settings); total + new_height }) + self.styled_lines.tx.iter_mut().fold(0, |total, l| { - let new_height = l.update_line_height(self.last_terminal_size.width, &self.rendering); + let render_settings = RenderSettings { + rendering: &self.rendering, + defmt: &self.defmt_settings, + }; + let new_height = l.update_line_height(self.last_terminal_size.width, render_settings); total + new_height }) } + // #[cfg(feature = "defmt")] + // fn rx_lines_iter(&self) -> impl Iterator { + // self.styled_lines.rx.iter().filter(|b| match b.line_type { + // super::LineType::PortDefmt { + // level: Some(level), .. + // } => self.defmt_settings.max_log_level <= crate::settings::Level::from(level), + // _ => true, + // }) + // } + // #[cfg(not(feature = "defmt"))] + // fn rx_lines_iter(&self) -> impl Iterator { + // self.styled_lines.rx.iter() + // } fn buflines_iter(&self) -> impl Iterator { if self.rendering.echo_user_input == UserEcho::None { Either::Left(self.styled_lines.rx.iter()) } else { Either::Right(interleave_by( self.styled_lines.rx.iter(), - self.styled_lines - .tx - .iter() - .filter(|l| self.rendering.echo_user_input.filter_user_line(l)), + self.styled_lines.tx.iter().filter(|l| { + self.rendering + .echo_user_input + .filter_user_line(&l.line_type) + }), |port, user| match port.raw_buffer_index.cmp(&user.raw_buffer_index) { Ordering::Equal => port.timestamp <= user.timestamp, Ordering::Less => true, @@ -51,7 +74,10 @@ impl Buffer { } pub fn lines_iter(&self) -> (impl Iterator, u16) { let (buflines, wrapped_scroll) = self.visible_buflines_iter(); - (buflines.map(|l| l.as_line(&self.rendering)), wrapped_scroll) + ( + buflines.map(|l| l.as_line(self.line_render_settings())), + wrapped_scroll, + ) } fn visible_buflines_iter(&self) -> (impl Iterator, u16) { @@ -249,7 +275,11 @@ impl Buffer { .styled_lines .tx .iter() - .filter(|l| self.rendering.echo_user_input.filter_user_line(l)) + .filter(|l| { + self.rendering + .echo_user_input + .filter_user_line(&l.line_type) + }) .count() } } @@ -335,6 +365,13 @@ impl Buffer { } } } + pub fn line_render_settings(&self) -> RenderSettings { + RenderSettings { + rendering: &self.rendering, + #[cfg(feature = "defmt")] + defmt: &self.defmt_settings, + } + } } impl Buffer { @@ -904,3 +941,44 @@ impl Widget for &mut Buffer { scrollbar.render(area, buf, &mut self.state.scrollbar_state); } } + +pub mod defmt { + use defmt_parser::Level; + use ratatui::prelude::*; + + pub fn defmt_level_color(level: Option) -> Color { + match level { + Some(Level::Error) => Color::Red, + Some(Level::Warn) => Color::Yellow, + Some(Level::Info) => Color::Green, + Some(Level::Debug) => Color::Blue, + Some(Level::Trace) => Color::Magenta, + None => Color::Gray, + } + } + + fn defmt_level_span(level: Option) -> Span<'static> { + match level { + Some(Level::Error) => Span::styled("ERROR", defmt_level_color(level)), + Some(Level::Warn) => Span::styled("WARN", defmt_level_color(level)), + Some(Level::Info) => Span::styled("INFO", defmt_level_color(level)), + Some(Level::Debug) => Span::styled("DEBUG", defmt_level_color(level)), + Some(Level::Trace) => Span::styled("TRACE", defmt_level_color(level)), + None => Span::styled("???", defmt_level_color(level)), + } + } + + pub fn defmt_level_bracketed(level: Option) -> Vec> { + let end_bracket = match level { + None => "] ", + Some(Level::Info) | Some(Level::Warn) => "] ", + Some(_) => "] ", + }; + let dark_gray = Style::new().dark_gray(); + vec![ + Span::styled("[", dark_gray), + defmt_level_span(level), + Span::styled(end_bracket, dark_gray), + ] + } +} diff --git a/src/keybinds.rs b/src/keybinds.rs index 498ee43..61e4f15 100644 --- a/src/keybinds.rs +++ b/src/keybinds.rs @@ -13,7 +13,6 @@ use crate::app::PopupMenu; #[cfg(feature = "macros")] use crate::macros::MacroNameTag; -// Maybe combine with app::PopupMenu instead of being its own type? #[derive( Debug, PartialEq, Eq, PartialOrd, Ord, strum::EnumString, strum::Display, strum::AsRefStr, )] @@ -34,6 +33,8 @@ pub enum ShowPopupAction { ShowEspFlash, #[cfg(feature = "logging")] ShowLogging, + #[cfg(feature = "defmt")] + ShowDefmt, } impl From for PopupMenu { @@ -48,6 +49,8 @@ impl From for PopupMenu { ShowPopupAction::ShowEspFlash => Self::EspFlash, #[cfg(feature = "macros")] ShowPopupAction::ShowMacros => Self::Macros, + #[cfg(feature = "macros")] + ShowPopupAction::ShowDefmt => Self::Defmt, } } } @@ -126,7 +129,22 @@ pub enum LoggingAction { Toggle, } -// TODO make logging-file-start, etc +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, strum::EnumString, strum::Display, strum::AsRefStr, +)] +#[strum(serialize_all = "kebab-case")] +#[strum(ascii_case_insensitive)] +#[repr(u8)] +// #[strum(prefix = "show-")] +// nevermind, doesn't work with FromStr :( +pub enum ShowDefmtSelect { + #[strum(serialize = "defmt-select-tui")] + SelectTui, + #[strum(serialize = "defmt-select-system")] + SelectSystem, + #[strum(serialize = "defmt-select-recent")] + SelectRecent, +} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, strum::AsRefStr)] pub enum AppAction { @@ -139,6 +157,8 @@ pub enum AppAction { Esp(EspAction), #[cfg(feature = "logging")] Logging(LoggingAction), + #[cfg(feature = "defmt")] + ShowDefmtSelect(ShowDefmtSelect), } // // TODO replace this @@ -170,6 +190,8 @@ impl fmt::Display for AppAction { AppAction::Esp(action) => write!(f, "{action}"), #[cfg(feature = "logging")] AppAction::Logging(action) => write!(f, "{action}"), + #[cfg(feature = "defmt")] + AppAction::ShowDefmtSelect(action) => write!(f, "{action}"), } } } @@ -207,6 +229,13 @@ impl From for AppAction { } } +#[cfg(feature = "defmt")] +impl From for AppAction { + fn from(action: ShowDefmtSelect) -> Self { + AppAction::ShowDefmtSelect(action) + } +} + impl FromStr for AppAction { type Err = String; @@ -233,6 +262,10 @@ impl FromStr for AppAction { if let Ok(esp) = s.parse::() { return Ok(AppAction::Esp(esp)); } + #[cfg(feature = "defmt")] + if let Ok(defmt_select) = s.parse::() { + return Ok(AppAction::ShowDefmtSelect(defmt_select)); + } Err(format!("Unrecognized AppAction variant for string: {}", s)) } diff --git a/src/lib.rs b/src/lib.rs index 52dc8df..f1ac0ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,7 +151,7 @@ pub fn initialize_logging(max_level: Level) -> color_eyre::Result { // let (fmt_layer, reload_handle) = tracing_subscriber::reload::Layer::new(fmt_layer); // Allow everything through but limit lnk to just info, since it spits out a bit too much when reading shortcuts - let env_filter = tracing_subscriber::EnvFilter::new("trace,espflash=info"); + let env_filter = tracing_subscriber::EnvFilter::new("trace,espflash=info,notify=debug"); let registry = tracing_subscriber::registry() // .with(console) .with(env_filter) @@ -227,11 +227,20 @@ fn get_user_dir() -> Option { } #[macro_export] -/// Macro to check if a field has changed between two objects. +/// Macro to check if any field has changed between two objects. /// -/// Usage: `changed!(old, new, field_name)` +/// Usage: `changed!(old, new, field_name1, field_name2, ...)` macro_rules! changed { + // I technically don't need to keep the single field version, + // but rust-analyzer doesn't suggest field names on the multi-field version, + // and I wanted to keep the ergonomics of using LSP-suggested names + // instead of going to the type myself. ($a:expr, $b:expr, $field:ident) => { ($a.$field != $b.$field) }; + ($a:expr, $b:expr, $($field:ident),+) => { + { + false $(|| $a.$field != $b.$field)+ + } + }; } diff --git a/src/serial/esp.rs b/src/serial/esp.rs index d8eac54..7d13506 100644 --- a/src/serial/esp.rs +++ b/src/serial/esp.rs @@ -125,12 +125,6 @@ impl ProgressPropagator { } impl ProgressCallbacks for ProgressPropagator { fn init(&mut self, addr: u32, total: usize) { - // assert!( - // self.filenames.len() <= u8::MAX as usize, - // "Not supporting more than 255 files per profile." - // ); - // self.current_index += 1; - _ = self.tx.send( FlashProgress::Init { chip: self.chip.clone(), diff --git a/src/serial/worker.rs b/src/serial/worker.rs index 613c294..e38f4fd 100644 --- a/src/serial/worker.rs +++ b/src/serial/worker.rs @@ -150,6 +150,9 @@ impl SerialWorker { // TODO Fuzz testing with this + buffer match self.command_rx.recv_timeout(sleep_time) { Ok(SerialWorkerCommand::Shutdown(shutdown_tx)) => { + if let Some(port) = self.port.as_mut_port() { + _ = port.flush(); + } self.port.drop(); self.shared_status .store(Arc::new(PortStatus::new_idle(&PortSettings::default()))); @@ -621,7 +624,7 @@ impl SerialWorker { .port .as_mut_port() .expect("port just populated, should be present"); - + port.set_timeout(Duration::from_millis(100))?; port.write_request_to_send(port_status.signals.rts)?; // let port = serialport::new(port, 115200).open()?; diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 898d047..6b9b8b0 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -51,6 +51,9 @@ pub struct Settings { pub misc: Misc, #[serde(default)] pub last_port_settings: PortSettings, + #[cfg(feature = "defmt")] + #[serde(default)] + pub defmt: Defmt, #[cfg(feature = "logging")] #[serde(default)] pub logging: Logging, @@ -324,6 +327,137 @@ pub struct Behavior { pub fuzzy_macro_match: bool, } +#[cfg(feature = "defmt")] +#[derive( + Debug, + Default, + Clone, + PartialEq, + Serialize, + Deserialize, + strum::VariantArray, + strum::EnumString, + strum::Display, +)] +pub enum DefmtSupport { + FramedRzcobs, + UnframedRzcobs, + Raw, + #[default] + Disabled, +} + +#[cfg(feature = "defmt")] +#[derive( + Debug, + Default, + Clone, + PartialEq, + Serialize, + Deserialize, + strum::VariantArray, + strum::EnumString, + strum::Display, + strum::EnumIs, +)] +pub enum DefmtLocation { + #[default] + Full, + Shortened, + Hidden, +} + +#[derive( + Debug, + Default, + Clone, + PartialEq, + PartialOrd, + Serialize, + Deserialize, + strum::VariantArray, + strum::EnumString, + strum::Display, +)] +#[strum(serialize_all = "title_case")] +#[strum(ascii_case_insensitive)] +pub enum Level { + #[default] + Trace, + Debug, + Info, + Warn, + Error, +} + +#[cfg(feature = "defmt")] +impl From for defmt_parser::Level { + fn from(value: Level) -> Self { + match value { + Level::Trace => defmt_parser::Level::Trace, + Level::Debug => defmt_parser::Level::Debug, + Level::Info => defmt_parser::Level::Info, + Level::Warn => defmt_parser::Level::Warn, + Level::Error => defmt_parser::Level::Error, + } + } +} +#[cfg(feature = "defmt")] +impl From for Level { + fn from(value: defmt_parser::Level) -> Self { + match value { + defmt_parser::Level::Trace => Level::Trace, + defmt_parser::Level::Debug => Level::Debug, + defmt_parser::Level::Info => Level::Info, + defmt_parser::Level::Warn => Level::Warn, + defmt_parser::Level::Error => Level::Error, + } + } +} + +#[cfg(feature = "defmt")] +#[serde_inline_default] +#[derive(Debug, Clone, Serialize, Deserialize, StructTable, Derivative)] +#[derivative(Default)] +pub struct Defmt { + #[serde(default)] + #[table(values = DefmtSupport::VARIANTS)] + /// Enable parsing RX'd serial data as defmt packets. + pub defmt_parsing: DefmtSupport, + + #[cfg(feature = "defmt_watch")] + #[table(rename = "Watch ELF for Changes")] + /// Reload defmt data from ELF when file is updated. + pub watch_elf_for_changes: bool, + + #[serde_inline_default(Level::Trace)] + #[derivative(Default(value = "Level::Trace"))] + #[table(display = Debug)] + #[table(values = Level::VARIANTS)] + /// Maximum log level to display. Items without a level are always shown. + pub max_log_level: Level, + + #[serde_inline_default(true)] + #[derivative(Default(value = "true"))] + /// Show device-derived timestamps, if available. + pub device_timestamp: bool, + + #[serde(default)] + #[table(values = DefmtLocation::VARIANTS)] + /// Show module where log originated from, if available. + pub show_module: DefmtLocation, + + #[serde(default)] + #[table(values = DefmtLocation::VARIANTS)] + /// Show file where log originated from, if available. + pub show_file: DefmtLocation, + + #[serde_inline_default(true)] + #[derivative(Default(value = "true"))] + /// Show line number in file where log originated from, if available. + pub show_line_number: bool, +} + #[serde_inline_default] #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, StructTable)] pub struct PortSettings { @@ -332,6 +466,7 @@ pub struct PortSettings { #[table(immutable)] #[serde_inline_default(DEFAULT_BAUD)] pub baud_rate: u32, + /// Number of bits per character. #[table(values = [DataBits::Five, DataBits::Six, DataBits::Seven, DataBits::Eight])] #[serde_inline_default(DataBits::Eight)] @@ -340,14 +475,17 @@ pub struct PortSettings { deserialize_with = "deserialize_from_u8" )] pub data_bits: DataBits, + /// Flow control modes. #[table(values = [FlowControl::None, FlowControl::Software, FlowControl::Hardware])] #[serde_inline_default(FlowControl::None)] pub flow_control: FlowControl, + /// Parity bit modes. #[table(values = [Parity::None, Parity::Odd, Parity::Even])] #[serde_inline_default(Parity::None)] pub parity_bits: Parity, + /// Number of stop bits. #[table(values = [StopBits::One, StopBits::Two])] #[serde_inline_default(StopBits::One)] @@ -356,10 +494,12 @@ pub struct PortSettings { deserialize_with = "deserialize_from_u8" )] pub stop_bits: StopBits, + /// Assert DTR to this state on port connect (and reconnect). #[table(rename = "DTR on Connect")] #[serde_inline_default(true)] pub dtr_on_connect: bool, + /// Enable reconnections. Strict checks USB PID+VID+Serial#. Loose checks for any similar USB device/COM port. #[table(values = Reconnections::VARIANTS)] #[serde_inline_default(Reconnections::LooseChecks)] @@ -376,6 +516,7 @@ pub struct PortSettings { )] #[serde_inline_default(RxLineEnding::Preset("\\n", &[b'\n']))] pub rx_line_ending: RxLineEnding, + /// Line endings for TX'd data. #[table(display = ["Inherit RX", "\\n", "\\r", "\\r\\n", "None"])] #[table(rename = "TX Line Ending")] diff --git a/src/traits.rs b/src/traits.rs index ba52fe2..95e1a69 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -231,7 +231,8 @@ pub trait LineMutator { impl LineMutator for Line<'_> { /// ## Panics if range intersects char boundaries or goes out of bounds! fn style_slice(&mut self, range: Range, style: Style) { - debug!("Styling {range:?} with {style:?}"); + // #[cfg(debug_assertions)] + // debug!("Styling {range:?} with {style:?}"); let spans = &mut self.spans; let mut current = 0; for (index, span) in spans.iter_mut().enumerate() { @@ -282,7 +283,8 @@ impl LineMutator for Line<'_> { } } fn censor_slice(&mut self, range: Range, style: Option