From 1a81192cc3beaf7cbc207f6c6e5a379042592838 Mon Sep 17 00:00:00 2001
From: Nicole Tietz-Sokolskaya <me@ntietz.com>
Date: Fri, 31 Jan 2025 19:48:32 +0000
Subject: [PATCH] feat: add raw message storage and display (#2)

Reviewed-on: https://git.kittencollective.com/nicole/midi-keys/pulls/2
---
 src/midi.rs   |  14 ++++-
 src/parser.rs |   9 ++--
 src/ui.rs     | 141 ++++++++++++++++++++++++++++++--------------------
 3 files changed, 103 insertions(+), 61 deletions(-)

diff --git a/src/midi.rs b/src/midi.rs
index 1fd9a68..2ed2725 100644
--- a/src/midi.rs
+++ b/src/midi.rs
@@ -1,7 +1,19 @@
 pub mod daemon;
 
 #[derive(PartialEq, Eq, Debug, Clone)]
-pub enum Message {
+pub struct Message {
+    pub raw: Vec<u8>,
+    pub parsed: ParsedMessage,
+}
+
+impl Message {
+    pub fn new(raw: Vec<u8>, parsed: ParsedMessage) -> Self {
+        Message { raw, parsed }
+    }
+}
+
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub enum ParsedMessage {
     Voice(VoiceMessage),
     System(SystemCommon),
     Realtime(SystemRealtime),
diff --git a/src/parser.rs b/src/parser.rs
index 8d98516..3e63a22 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -3,9 +3,10 @@ use nom::{
     IResult,
 };
 
-use crate::midi::{Message, SystemCommon, SystemRealtime, VoiceCategory, VoiceMessage};
+use crate::midi::{Message, ParsedMessage, SystemCommon, SystemRealtime, VoiceCategory, VoiceMessage};
 
 pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> {
+    let raw = Vec::from(bytes);
     let (bytes, status_byte) = take(1usize)(bytes)?;
     let status_byte = status_byte[0];
 
@@ -14,13 +15,13 @@ pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> {
 
     if status_byte < 0xF0 {
         let (bytes, vm) = parse_voice_message(status_byte, bytes)?;
-        Ok((bytes, Message::Voice(vm)))
+        Ok((bytes, Message::new(raw, ParsedMessage::Voice(vm))))
     } else if status_byte < 0xf8 {
         let (bytes, sc) = parse_system_common(status_byte, bytes)?;
-        Ok((bytes, Message::System(sc)))
+        Ok((bytes, Message::new(raw, ParsedMessage::System(sc))))
     } else {
         let sr = parse_system_realtime(status_byte);
-        Ok((bytes, Message::Realtime(sr)))
+        Ok((bytes, Message::new(raw, ParsedMessage::Realtime(sr))))
     }
 }
 
diff --git a/src/ui.rs b/src/ui.rs
index 200a62e..b5bc571 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -11,7 +11,7 @@ use crossbeam::{channel::Receiver, select};
 use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, Rounding, ScrollArea, SelectableLabel};
 use midir::{MidiInput, MidiInputPort};
 
-use crate::midi::{daemon::CTM, Message, VoiceCategory, VoiceMessage};
+use crate::midi::{daemon::CTM, Message, ParsedMessage, VoiceCategory, VoiceMessage};
 
 /// State used to display the UI. It's intended to be shared between the
 /// renderer and the daemon which updates the state.
@@ -22,6 +22,7 @@ pub struct DisplayState {
     pub selected_ports: HashMap<String, bool>,
     pub max_messages: usize,
     pub only_note_on_off: Arc<AtomicBool>,
+    pub show_raw: Arc<AtomicBool>,
 }
 
 impl DisplayState {
@@ -31,7 +32,8 @@ impl DisplayState {
             midi_messages: Arc::new(Mutex::new(VecDeque::new())),
             selected_ports: HashMap::new(),
             max_messages: 10_000_usize,
-            only_note_on_off: Arc::new(AtomicBool::new(false)),
+            only_note_on_off: Arc::new(AtomicBool::new(true)),
+            show_raw: Arc::new(AtomicBool::new(true)),
         }
     }
 
@@ -159,7 +161,7 @@ impl eframe::App for MidiKeysApp {
 
                 let conn_id = port.id();
 
-                let selected = self.state.selected_ports.get(&conn_id).unwrap_or(&false);
+                let selected = self.state.selected_ports.get(&conn_id).unwrap_or(&true);
 
                 if ui
                     .add(SelectableLabel::new(*selected, &port_name))
@@ -171,73 +173,80 @@ impl eframe::App for MidiKeysApp {
         });
 
         let mut only_note_on_off = self.state.only_note_on_off.load(Ordering::Relaxed);
+        let mut show_raw = self.state.show_raw.load(Ordering::Relaxed);
 
         egui::TopBottomPanel::top("filter_panel").show(ctx, |ui| {
-            ui.checkbox(&mut only_note_on_off, "Only note on/off");
+            ui.horizontal_wrapped(|ui| {
+                ui.checkbox(&mut only_note_on_off, "Only note on/off");
+                ui.checkbox(&mut show_raw, "Display raw bytes");
+            });
         });
 
         self.state
             .only_note_on_off
             .store(only_note_on_off, Ordering::Relaxed);
+        self.state.show_raw.store(show_raw, Ordering::Relaxed);
 
         egui::CentralPanel::default().show(ctx, |ui| {
-            ScrollArea::vertical().show(ui, |ui| {
-                for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() {
-                    if only_note_on_off
-                        && !matches!(
-                            msg,
-                            Message::Voice(VoiceMessage {
-                                category: VoiceCategory::NoteOn { .. }
-                                    | VoiceCategory::NoteOff { .. },
-                                ..
-                            })
-                        )
-                    {
-                        continue;
-                    }
-                    if !self
-                        .state
-                        .selected_ports
-                        .get(conn.as_str())
-                        .unwrap_or(&false)
-                    {
-                        continue;
-                    }
-                    let mut frame = Frame::default()
-                        .inner_margin(4.0)
-                        .stroke((1.0, Color32::BLACK))
-                        .rounding(Rounding::same(2.0))
-                        .begin(ui);
+            ScrollArea::vertical()
+                .max_width(f32::INFINITY)
+                .show(ui, |ui| {
+                    ui.vertical_centered_justified(|ui| {
+                        for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() {
+                            if only_note_on_off
+                                && !matches!(
+                                    &msg.parsed,
+                                    ParsedMessage::Voice(VoiceMessage {
+                                        category: VoiceCategory::NoteOn { .. }
+                                            | VoiceCategory::NoteOff { .. },
+                                        ..
+                                    })
+                                )
+                            {
+                                continue;
+                            }
+                            if !self
+                                .state
+                                .selected_ports
+                                .get(conn.as_str())
+                                .unwrap_or(&true)
+                            {
+                                continue;
+                            }
+                            let port = match ports.iter().find(|p| &p.id() == conn) {
+                                Some(p) => p,
+                                None => continue,
+                            };
+                            ui.set_width(ui.available_width());
+                            let mut frame = Frame::default()
+                                .inner_margin(4.0)
+                                .outer_margin(4.0)
+                                .stroke((1.0, Color32::BLACK))
+                                .rounding(Rounding::same(2.0))
+                                .begin(ui);
 
-                    let port = match ports.iter().find(|p| &p.id() == conn) {
-                        Some(p) => p,
-                        None => continue,
-                    };
-                    let port_name = self
-                        .midi_in
-                        .port_name(port)
-                        .unwrap_or("(disconnected_device)".into());
+                            let port_name = self
+                                .midi_in
+                                .port_name(port)
+                                .unwrap_or("(disconnected_device)".into());
 
-                    frame.content_ui.label(RichText::new(port_name).strong());
+                            frame.content_ui.label(RichText::new(port_name).strong());
 
-                    display_midi_message(idx, msg, &mut frame.content_ui);
+                            display_midi_message(idx, msg, &mut frame.content_ui, show_raw);
+                            frame.end(ui);
+                        }
+                    });
 
-                    frame.end(ui);
-                }
-            });
-
-            ctx.request_repaint();
+                    ctx.request_repaint();
+                });
         });
     }
 }
 
-fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui) {
-    match msg {
-        Message::Voice(vm) => {
+fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) {
+    match &msg.parsed {
+        ParsedMessage::Voice(vm) => {
             Grid::new(format!("message_grid_{idx}")).show(ui, |ui| {
-                let voice_label = format!("Voice (channel={})", vm.channel);
-                ui.label(RichText::new(voice_label).italics());
-
                 let (name, fields) = match vm.category {
                     VoiceCategory::NoteOff { note, velocity } => (
                         "NoteOff",
@@ -280,16 +289,36 @@ fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui) {
                     VoiceCategory::Unknown => ("Unknown", vec![]),
                 };
 
-                ui.label(name);
-                ui.end_row();
+                let voice_label = format!("Voice:{}", name);
+
+                ui.label(RichText::new(voice_label).italics());
+                ui.label(format!("channel = {}", vm.channel));
 
                 for (name, value) in fields {
                     ui.label(format!("{name} = {value}"));
                 }
             });
         }
-        Message::System(_system_common) => {}
-        Message::Realtime(_system_realtime) => {}
+        ParsedMessage::System(_system_common) => {}
+        ParsedMessage::Realtime(_system_realtime) => {}
+    }
+
+    if raw {
+        ui.horizontal_top(|ui| {
+            ui.label("Bytes:");
+            Frame::none().show(ui, |ui| {
+                ui.spacing_mut().item_spacing = (0.0, 0.0).into();
+                for byte in &msg.raw {
+                    Frame::none()
+                        .inner_margin(2.0)
+                        .stroke((0.5, Color32::GRAY))
+                        .outer_margin(0.0)
+                        .show(ui, |ui| {
+                            ui.label(format!("{:02X}", byte));
+                        });
+                }
+            });
+        });
     }
 }