1use anyhow::Result;
7use tracing;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct MidiPortInfo {
12 pub name: String,
14 pub index: usize,
16 pub direction: PortDirection,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PortDirection {
22 Input,
23 Output,
24}
25
26pub fn list_input_ports() -> Result<Vec<MidiPortInfo>> {
28 let midi_in = midir::MidiInput::new("phosphor-scan")?;
29 let ports = midi_in.ports();
30
31 let mut result = Vec::with_capacity(ports.len());
32 for (i, port) in ports.iter().enumerate() {
33 let name = midi_in
34 .port_name(port)
35 .unwrap_or_else(|_| format!("MIDI Input {i}"));
36 result.push(MidiPortInfo {
37 name,
38 index: i,
39 direction: PortDirection::Input,
40 });
41 }
42
43 tracing::debug!("Found {} MIDI input ports", result.len());
44 Ok(result)
45}
46
47pub fn list_output_ports() -> Result<Vec<MidiPortInfo>> {
49 let midi_out = midir::MidiOutput::new("phosphor-scan")?;
50 let ports = midi_out.ports();
51
52 let mut result = Vec::with_capacity(ports.len());
53 for (i, port) in ports.iter().enumerate() {
54 let name = midi_out
55 .port_name(port)
56 .unwrap_or_else(|_| format!("MIDI Output {i}"));
57 result.push(MidiPortInfo {
58 name,
59 index: i,
60 direction: PortDirection::Output,
61 });
62 }
63
64 tracing::debug!("Found {} MIDI output ports", result.len());
65 Ok(result)
66}
67
68pub fn diff_ports(old: &[MidiPortInfo], new: &[MidiPortInfo]) -> PortDiff {
70 let added: Vec<MidiPortInfo> = new
71 .iter()
72 .filter(|p| !old.iter().any(|o| o.name == p.name))
73 .cloned()
74 .collect();
75
76 let removed: Vec<MidiPortInfo> = old
77 .iter()
78 .filter(|p| !new.iter().any(|n| n.name == p.name))
79 .cloned()
80 .collect();
81
82 PortDiff { added, removed }
83}
84
85#[derive(Debug, Clone)]
87pub struct PortDiff {
88 pub added: Vec<MidiPortInfo>,
89 pub removed: Vec<MidiPortInfo>,
90}
91
92impl PortDiff {
93 pub fn has_changes(&self) -> bool {
94 !self.added.is_empty() || !self.removed.is_empty()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn diff_detects_added_port() {
104 let old = vec![];
105 let new = vec![MidiPortInfo {
106 name: "Controller A".into(),
107 index: 0,
108 direction: PortDirection::Input,
109 }];
110 let diff = diff_ports(&old, &new);
111 assert_eq!(diff.added.len(), 1);
112 assert_eq!(diff.added[0].name, "Controller A");
113 assert!(diff.removed.is_empty());
114 assert!(diff.has_changes());
115 }
116
117 #[test]
118 fn diff_detects_removed_port() {
119 let old = vec![MidiPortInfo {
120 name: "Controller A".into(),
121 index: 0,
122 direction: PortDirection::Input,
123 }];
124 let new = vec![];
125 let diff = diff_ports(&old, &new);
126 assert!(diff.added.is_empty());
127 assert_eq!(diff.removed.len(), 1);
128 assert_eq!(diff.removed[0].name, "Controller A");
129 }
130
131 #[test]
132 fn diff_no_changes() {
133 let ports = vec![MidiPortInfo {
134 name: "Controller A".into(),
135 index: 0,
136 direction: PortDirection::Input,
137 }];
138 let diff = diff_ports(&ports, &ports);
139 assert!(!diff.has_changes());
140 }
141
142 #[test]
143 fn diff_simultaneous_add_and_remove() {
144 let old = vec![MidiPortInfo {
145 name: "Old Controller".into(),
146 index: 0,
147 direction: PortDirection::Input,
148 }];
149 let new = vec![MidiPortInfo {
150 name: "New Controller".into(),
151 index: 0,
152 direction: PortDirection::Input,
153 }];
154 let diff = diff_ports(&old, &new);
155 assert_eq!(diff.added.len(), 1);
156 assert_eq!(diff.removed.len(), 1);
157 assert_eq!(diff.added[0].name, "New Controller");
158 assert_eq!(diff.removed[0].name, "Old Controller");
159 }
160
161 }