Phase 2 of the rebrand cascade. Renames all 9 workspace crates from magnotia-* to lumotia-* plus the src-tauri binary crate name: - magnotia-ai-formatting -> lumotia-ai-formatting - magnotia-audio -> lumotia-audio - magnotia-cloud-providers -> lumotia-cloud-providers - magnotia-core -> lumotia-core - magnotia-hotkey -> lumotia-hotkey - magnotia-llm -> lumotia-llm - magnotia-mcp -> lumotia-mcp - magnotia-storage -> lumotia-storage - magnotia-transcription -> lumotia-transcription - magnotia -> lumotia (src-tauri binary) - magnotia_lib -> lumotia_lib (src-tauri lib target) Crate directories (crates/audio/ etc.) stay as-is; only the Cargo.toml [package] name field changes plus all consumer module imports (magnotia_core -> lumotia_core, etc.). Remaining magnotia_* references at this point are intentional and scoped to later phases: tracing targets (Phase 4), DB setting keys magnotia_preferences/magnotia_history (Phase 5). cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.7 KiB
Rust
130 lines
4.7 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
use tauri::Manager;
|
|
|
|
use crate::commands::security::ensure_main_window;
|
|
use lumotia_audio::{DeviceInfo, MicrophoneCapture};
|
|
|
|
/// Enumerate every input device available to cpal, with metadata for the
|
|
/// Settings device-picker UI. Includes a flag for likely PulseAudio /
|
|
/// PipeWire monitor sources so the UI can warn the user.
|
|
#[tauri::command]
|
|
pub async fn list_audio_devices(window: tauri::WebviewWindow) -> Result<Vec<DeviceInfo>, String> {
|
|
ensure_main_window(&window)?;
|
|
tokio::task::spawn_blocking(MicrophoneCapture::list_devices)
|
|
.await
|
|
.map_err(|e| format!("join error: {e}"))?
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Resolve the destination path for a new live-capture recording,
|
|
/// ensuring the parent directory exists. Used by
|
|
/// `start_live_transcription_session` to hand the path to the progressive
|
|
/// WAV writer before any samples arrive (brief item #19).
|
|
pub fn resolve_recording_path(
|
|
app: &tauri::AppHandle,
|
|
output_folder: Option<&str>,
|
|
) -> Result<PathBuf, String> {
|
|
let recordings_dir = match output_folder.map(str::trim).filter(|s| !s.is_empty()) {
|
|
Some(folder) => PathBuf::from(folder),
|
|
None => app
|
|
.path()
|
|
.app_local_data_dir()
|
|
.map_err(|e: tauri::Error| e.to_string())?
|
|
.join("recordings"),
|
|
};
|
|
|
|
std::fs::create_dir_all(&recordings_dir)
|
|
.map_err(|e| format!("Failed to create recordings dir: {e}"))?;
|
|
|
|
Ok(recordings_dir.join(recording_filename()))
|
|
}
|
|
|
|
/// Deterministic recording filename generator. Combines three fields
|
|
/// for absolute uniqueness across rapid calls:
|
|
///
|
|
/// - wall-clock seconds since the epoch — human-readable and
|
|
/// sortable;
|
|
/// - the sub-second nanosecond component — defeats same-second
|
|
/// collisions;
|
|
/// - a process-lifetime atomic counter — defeats even same-nanosecond
|
|
/// collisions, which `SystemTime::now()` alone cannot guarantee
|
|
/// (two calls in the same clock tick can return identical nanos).
|
|
///
|
|
/// Format: `magnotia-<secs>-<nanos_in_sec>-<counter>.wav`, e.g.
|
|
/// `magnotia-1776828000-123456789-0000.wav`.
|
|
fn recording_filename() -> String {
|
|
let duration = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default();
|
|
let secs = duration.as_secs();
|
|
let nanos = duration.subsec_nanos();
|
|
let counter = RECORDING_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
format!("magnotia-{secs}-{nanos:09}-{counter:04}.wav")
|
|
}
|
|
|
|
/// Process-lifetime monotonic counter for `recording_filename`. Starts
|
|
/// at 0 on each Magnotia launch; wall-clock secs/nanos still advance across
|
|
/// restarts, so cross-launch collisions are already impossible — the
|
|
/// counter is the last-mile guarantee against within-launch same-tick
|
|
/// collisions.
|
|
static RECORDING_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::recording_filename;
|
|
|
|
#[test]
|
|
fn recording_filenames_are_unique_across_rapid_calls() {
|
|
// Regression for the 2026-04-22 review AND the review-of-
|
|
// review MINOR: SystemTime::now() alone cannot guarantee
|
|
// uniqueness under tight loops on every OS clock resolution,
|
|
// so the filename now includes a process-lifetime atomic
|
|
// counter. With the counter, uniqueness is absolute across
|
|
// any number of in-process calls.
|
|
let mut names = std::collections::HashSet::new();
|
|
for _ in 0..1024 {
|
|
names.insert(recording_filename());
|
|
}
|
|
assert_eq!(
|
|
names.len(),
|
|
1024,
|
|
"every filename must be unique (counter-backed guarantee)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recording_filename_has_expected_shape() {
|
|
let name = recording_filename();
|
|
assert!(name.starts_with("magnotia-"));
|
|
assert!(name.ends_with(".wav"));
|
|
// Shape: magnotia-<digits>-<9 digits>-<>=4 digits>.wav
|
|
let rest = name
|
|
.strip_prefix("magnotia-")
|
|
.and_then(|s| s.strip_suffix(".wav"))
|
|
.expect("shape prefix/suffix");
|
|
let parts: Vec<&str> = rest.split('-').collect();
|
|
assert_eq!(
|
|
parts.len(),
|
|
3,
|
|
"expected three '-' separated parts, got {parts:?}"
|
|
);
|
|
assert!(
|
|
parts[0].chars().all(|c| c.is_ascii_digit()),
|
|
"secs is digits"
|
|
);
|
|
assert_eq!(
|
|
parts[1].len(),
|
|
9,
|
|
"nanos component is zero-padded to 9 digits"
|
|
);
|
|
assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
|
|
assert!(
|
|
parts[2].len() >= 4,
|
|
"counter component is zero-padded to >=4 digits"
|
|
);
|
|
assert!(parts[2].chars().all(|c| c.is_ascii_digit()));
|
|
}
|
|
}
|