Skip to content

Commit 36d4509

Browse files
committed
More code cleanup
1 parent f225c06 commit 36d4509

8 files changed

Lines changed: 523 additions & 164 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Documentation
9393

9494
- User guide: `docs/USER_GUIDE.md`
9595
- Plugin developer guide (API v1): `docs/PLUGIN_DEVELOPER_GUIDE.md`
96+
- Architecture guardrails: `docs/ARCHITECTURE_GUARDRAILS.md`
9697

9798
Plugin examples
9899

docs/ARCHITECTURE_GUARDRAILS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# MIDIMaster Architecture Guardrails
2+
3+
This document defines module ownership and cleanup rules so boundaries stay clear after refactors.
4+
5+
## Frontend Ownership
6+
7+
- `src/main.js` is the composition root only.
8+
- Runtime plugin lifecycle and integration target normalization belong in `src/app/plugin_runtime.js`.
9+
- Tauri API binding/retry and shared invocation helpers belong in `src/app/bootstrap.js`.
10+
- Theme and persisted MIDI preference storage behavior belongs in `src/app/preferences.js`.
11+
- Session/device polling behavior belongs in `src/app/session_refresh.js`.
12+
- Plugin UI (installed/store tabs and plugin actions) belongs in `src/features/plugins/`.
13+
14+
## Backend Ownership
15+
16+
- `src-tauri/src/main.rs` should focus on app assembly: state wiring, plugin registration, invoke handler wiring, runtime startup.
17+
- Monitor discovery and monitor selection logic belong in `src-tauri/src/monitors.rs`.
18+
- Learn classification and media-key helper logic belong in `src-tauri/src/runtime_helpers.rs`.
19+
- Command handlers stay in `src-tauri/src/commands/*` and should not duplicate domain logic already owned by helper modules.
20+
21+
## Compatibility Rules
22+
23+
- Plugin API v1 contract is stable: no breaking changes to plugin context (`ctx`) or manifest schema in cleanup-only passes.
24+
- Tauri command names are stable unless an explicit migration plan is approved.
25+
- Frontend command payload shape should use canonical camelCase arguments only.
26+
- Do not add duplicate camelCase/snake_case aliases unless there is a proven compatibility requirement.
27+
28+
## Readability and Redundancy Rules
29+
30+
- Remove silent fallback branches when they hide actionable failures without adding compatibility value.
31+
- Keep one source of truth per concern (plugin state, profile persistence, refresh loops, and monitor resolution).
32+
- Prefer small focused modules over adding more cross-feature logic in composition files.
33+
- If a function requires many unrelated dependencies, move it to the owning subsystem rather than passing globals around.

src-tauri/src/main.rs

Lines changed: 6 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ mod commands;
88
mod device_target;
99
mod midi;
1010
mod model;
11+
mod monitors;
1112
mod plugin_api;
1213
mod profile_store;
14+
mod runtime_helpers;
1315
mod store_api;
1416
mod windows_autostart;
1517
mod windows_display;
@@ -23,45 +25,9 @@ use commands::*;
2325
use device_target::{parse_device_target, DeviceTargetKind};
2426
use midi::MidiManager;
2527
use model::{LearnedControl, MidiEvent, OsdSettings, Profile};
28+
use monitors::resolve_monitor_for_osd;
29+
use runtime_helpers::{classify_learned_control, send_media_key, LearnCandidate};
2630
use windows_autostart::set_windows_autostart;
27-
use windows_display::{display_device_id, monitor_display_name};
28-
29-
fn send_media_key(vk: u16) {
30-
use windows::Win32::UI::Input::KeyboardAndMouse::{
31-
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYBD_EVENT_FLAGS, KEYEVENTF_KEYUP,
32-
VIRTUAL_KEY,
33-
};
34-
35-
let key_down = INPUT {
36-
r#type: INPUT_KEYBOARD,
37-
Anonymous: INPUT_0 {
38-
ki: KEYBDINPUT {
39-
wVk: VIRTUAL_KEY(vk),
40-
wScan: 0,
41-
dwFlags: KEYBD_EVENT_FLAGS(0),
42-
time: 0,
43-
dwExtraInfo: 0,
44-
},
45-
},
46-
};
47-
48-
let key_up = INPUT {
49-
r#type: INPUT_KEYBOARD,
50-
Anonymous: INPUT_0 {
51-
ki: KEYBDINPUT {
52-
wVk: VIRTUAL_KEY(vk),
53-
wScan: 0,
54-
dwFlags: KEYEVENTF_KEYUP,
55-
time: 0,
56-
dwExtraInfo: 0,
57-
},
58-
},
59-
};
60-
61-
unsafe {
62-
SendInput(&[key_down, key_up], std::mem::size_of::<INPUT>() as i32);
63-
}
64-
}
6531

6632
use profile_store::ProfileStore;
6733
use std::collections::HashMap;
@@ -76,13 +42,12 @@ use tauri::{
7642
};
7743
use tokio::time::sleep;
7844

45+
pub(crate) use monitors::collect_monitor_descriptors;
7946
use plugin_api::{
8047
ensure_builtin_plugin, get_plugins_dir, hue_api_get, hue_api_put, hue_discover_bridges,
8148
hue_pair_bridge, install_plugin_package, list_plugins, read_plugin_base64, read_plugin_text,
8249
set_plugin_enabled, uninstall_plugin,
8350
};
84-
use std::thread::sleep as thread_sleep;
85-
use std::time::Duration as StdDuration;
8651
use store_api::{fetch_store_catalog, install_store_plugin};
8752
use ws_bridge::{get_wavelink_ws_port, ws_close, ws_open, ws_send, WsHub};
8853

@@ -92,34 +57,6 @@ use audio::windows::WindowsAudioBackend;
9257
#[cfg(not(target_os = "windows"))]
9358
use audio::unsupported::UnsupportedAudioBackend;
9459

95-
#[derive(Debug, Clone)]
96-
struct LearnCandidate {
97-
control: LearnedControl,
98-
last_seen_at: Instant,
99-
saw_zero: bool,
100-
saw_max: bool,
101-
}
102-
103-
fn classify_cc_candidate(saw_zero: bool, saw_max: bool) -> model::BindingControlKind {
104-
if saw_zero && saw_max {
105-
model::BindingControlKind::Button
106-
} else {
107-
model::BindingControlKind::Continuous
108-
}
109-
}
110-
111-
fn classify_learned_control(candidate: &LearnCandidate) -> LearnedControl {
112-
let mut learned = candidate.control.clone();
113-
learned.control_kind = match learned.msg_type {
114-
model::MidiMessageType::Note => model::BindingControlKind::Button,
115-
model::MidiMessageType::ControlChange => {
116-
classify_cc_candidate(candidate.saw_zero, candidate.saw_max)
117-
}
118-
model::MidiMessageType::PitchBend => model::BindingControlKind::Continuous,
119-
};
120-
learned
121-
}
122-
12360
struct AppState {
12461
audio: Box<dyn AudioBackend>,
12562
midi: Arc<Mutex<MidiManager>>,
@@ -138,101 +75,6 @@ struct AppState {
13875
app_settings: Mutex<AppSettings>,
13976
}
14077

141-
#[derive(Clone)]
142-
pub(crate) struct MonitorDescriptor {
143-
pub index: usize,
144-
pub friendly_name: String,
145-
pub stable_id: String,
146-
pub is_primary: bool,
147-
pub monitor: tauri::Monitor,
148-
}
149-
150-
pub(crate) fn collect_monitor_descriptors(
151-
app: &AppHandle,
152-
) -> Result<Vec<MonitorDescriptor>, String> {
153-
let monitors = app
154-
.available_monitors()
155-
.map_err(|_| "Failed to load monitors".to_string())?;
156-
let primary = app.primary_monitor().ok().flatten();
157-
158-
Ok(monitors
159-
.iter()
160-
.enumerate()
161-
.map(|(index, monitor)| {
162-
let raw_name = monitor
163-
.name()
164-
.cloned()
165-
.unwrap_or_else(|| format!("Monitor {}", index + 1));
166-
let stable_id = display_device_id(&raw_name).unwrap_or_else(|| raw_name.clone());
167-
let friendly_name = monitor_display_name(&raw_name).unwrap_or_else(|| raw_name.clone());
168-
let is_primary = primary
169-
.as_ref()
170-
.map(|p| {
171-
p.name() == monitor.name()
172-
&& p.size() == monitor.size()
173-
&& p.position() == monitor.position()
174-
})
175-
.unwrap_or(false);
176-
177-
MonitorDescriptor {
178-
index,
179-
friendly_name,
180-
stable_id,
181-
is_primary,
182-
monitor: monitor.clone(),
183-
}
184-
})
185-
.collect())
186-
}
187-
188-
#[derive(Clone)]
189-
struct SelectedMonitor {
190-
monitor: tauri::Monitor,
191-
}
192-
193-
fn resolve_monitor_for_osd(app: &AppHandle, settings: &OsdSettings) -> Option<SelectedMonitor> {
194-
let requested_id = settings.monitor_id.as_ref().and_then(|id| {
195-
let trimmed = id.trim();
196-
if trimmed.is_empty() {
197-
None
198-
} else {
199-
Some(trimmed.to_string())
200-
}
201-
});
202-
203-
// Retry only when an explicit monitor ID was requested but not yet enumerated.
204-
let max_attempts = if requested_id.is_some() { 7 } else { 1 };
205-
206-
for attempt in 0..max_attempts {
207-
let descriptors = collect_monitor_descriptors(app).ok()?;
208-
209-
if let Some(ref id) = requested_id {
210-
if let Some(found) = descriptors.iter().find(|m| m.stable_id == *id) {
211-
return Some(SelectedMonitor {
212-
monitor: found.monitor.clone(),
213-
});
214-
}
215-
216-
if attempt + 1 < max_attempts {
217-
thread_sleep(StdDuration::from_millis(250));
218-
continue;
219-
}
220-
}
221-
222-
if let Some(primary) = descriptors
223-
.iter()
224-
.find(|m| m.is_primary)
225-
.or_else(|| descriptors.first())
226-
{
227-
return Some(SelectedMonitor {
228-
monitor: primary.monitor.clone(),
229-
});
230-
}
231-
}
232-
233-
None
234-
}
235-
23678
impl AppState {
23779
fn apply_osd_settings(app: &AppHandle, settings: &OsdSettings) {
23880
let Some(osd_window) = app.get_webview_window("osd") else {
@@ -271,7 +113,7 @@ impl AppState {
271113

272114
let selected = resolve_monitor_for_osd(app, settings);
273115
if let Some(selected) = selected {
274-
let monitor = selected.monitor;
116+
let monitor = selected;
275117
let scale_factor = monitor.scale_factor();
276118
let size = monitor.size();
277119
let position = monitor.position();

src-tauri/src/monitors.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use crate::model::OsdSettings;
2+
use crate::windows_display::{display_device_id, monitor_display_name};
3+
use std::thread::sleep as thread_sleep;
4+
use std::time::Duration as StdDuration;
5+
use tauri::AppHandle;
6+
7+
#[derive(Clone)]
8+
pub(crate) struct MonitorDescriptor {
9+
pub index: usize,
10+
pub friendly_name: String,
11+
pub stable_id: String,
12+
pub is_primary: bool,
13+
pub monitor: tauri::Monitor,
14+
}
15+
16+
pub(crate) fn collect_monitor_descriptors(
17+
app: &AppHandle,
18+
) -> Result<Vec<MonitorDescriptor>, String> {
19+
let monitors = app
20+
.available_monitors()
21+
.map_err(|_| "Failed to load monitors".to_string())?;
22+
let primary = app.primary_monitor().ok().flatten();
23+
24+
Ok(monitors
25+
.iter()
26+
.enumerate()
27+
.map(|(index, monitor)| {
28+
let raw_name = monitor
29+
.name()
30+
.cloned()
31+
.unwrap_or_else(|| format!("Monitor {}", index + 1));
32+
let stable_id = display_device_id(&raw_name).unwrap_or_else(|| raw_name.clone());
33+
let friendly_name = monitor_display_name(&raw_name).unwrap_or_else(|| raw_name.clone());
34+
let is_primary = primary
35+
.as_ref()
36+
.map(|p| {
37+
p.name() == monitor.name()
38+
&& p.size() == monitor.size()
39+
&& p.position() == monitor.position()
40+
})
41+
.unwrap_or(false);
42+
43+
MonitorDescriptor {
44+
index,
45+
friendly_name,
46+
stable_id,
47+
is_primary,
48+
monitor: monitor.clone(),
49+
}
50+
})
51+
.collect())
52+
}
53+
54+
pub(crate) fn resolve_monitor_for_osd(
55+
app: &AppHandle,
56+
settings: &OsdSettings,
57+
) -> Option<tauri::Monitor> {
58+
let requested_id = settings.monitor_id.as_ref().and_then(|id| {
59+
let trimmed = id.trim();
60+
if trimmed.is_empty() {
61+
None
62+
} else {
63+
Some(trimmed.to_string())
64+
}
65+
});
66+
67+
let max_attempts = if requested_id.is_some() { 7 } else { 1 };
68+
69+
for attempt in 0..max_attempts {
70+
let descriptors = collect_monitor_descriptors(app).ok()?;
71+
72+
if let Some(ref id) = requested_id {
73+
if let Some(found) = descriptors.iter().find(|m| m.stable_id == *id) {
74+
return Some(found.monitor.clone());
75+
}
76+
77+
if attempt + 1 < max_attempts {
78+
thread_sleep(StdDuration::from_millis(250));
79+
continue;
80+
}
81+
}
82+
83+
if let Some(primary) = descriptors
84+
.iter()
85+
.find(|m| m.is_primary)
86+
.or_else(|| descriptors.first())
87+
{
88+
return Some(primary.monitor.clone());
89+
}
90+
}
91+
92+
None
93+
}

0 commit comments

Comments
 (0)