diff --git a/Cargo.lock b/Cargo.lock index 70bc5ea..d3bd18b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6704,6 +6704,7 @@ version = "0.0.0" dependencies = [ "chrono", "serde", + "thiserror 2.0.18", ] [[package]] diff --git a/apps/wisp-desktop/src/app.rs b/apps/wisp-desktop/src/app.rs index 58a4900..e640b87 100644 --- a/apps/wisp-desktop/src/app.rs +++ b/apps/wisp-desktop/src/app.rs @@ -11,7 +11,7 @@ use std::collections::VecDeque; use std::time::Instant; -use wisp_audiokit::{Event, SessionResult, SourceLabel}; +use wisp_audiokit::{Event, SessionError, SessionResult, SourceLabel}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionState { @@ -42,7 +42,7 @@ pub struct AppModel { pub state: SessionState, pub segments: Vec, pub recent_log: VecDeque, - pub last_error: Option, + pub last_error: Option, } impl AppModel { @@ -64,9 +64,9 @@ impl AppModel { pub fn fail( &mut self, - message: impl Into, + error: SessionError, ) { - self.last_error = Some(message.into()); + self.last_error = Some(error); self.state = SessionState::Failed; self.finalize_all_segments(); } diff --git a/apps/wisp-desktop/src/session_runner.rs b/apps/wisp-desktop/src/session_runner.rs index 49f6ca3..dacb738 100644 --- a/apps/wisp-desktop/src/session_runner.rs +++ b/apps/wisp-desktop/src/session_runner.rs @@ -10,7 +10,7 @@ use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel}; use std::thread::JoinHandle; use std::time::Duration; -use wisp_audiokit::{Event, Session}; +use wisp_audiokit::{Event, Session, SessionError}; /// Commands the UI sends to the worker. pub enum Command { @@ -28,7 +28,7 @@ pub enum Update { /// `Session::stop()` returned; the session has been torn down. Stopped, /// Lifecycle error (start/construct failed). - Error(String), + Error(SessionError), } pub struct SessionRunner { @@ -108,12 +108,12 @@ fn run_session( let session = match Session::new(output_dir, locale) { Ok(s) => s, Err(e) => { - let _ = update_tx.send(Update::Error(e.to_string())); + let _ = update_tx.send(Update::Error(e)); return; }, }; if let Err(e) = session.start() { - let _ = update_tx.send(Update::Error(e.to_string())); + let _ = update_tx.send(Update::Error(e)); return; } let _ = update_tx.send(Update::Started); diff --git a/apps/wisp-desktop/src/transcript_view.rs b/apps/wisp-desktop/src/transcript_view.rs index 17023c9..be3018f 100644 --- a/apps/wisp-desktop/src/transcript_view.rs +++ b/apps/wisp-desktop/src/transcript_view.rs @@ -14,7 +14,7 @@ use gpui::{ Context, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, Styled, Window, div, px, rgb, }; -use wisp_audiokit::SourceLabel; +use wisp_audiokit::{SessionError, SourceLabel}; use crate::app::{AppModel, Segment, SessionState}; @@ -113,7 +113,7 @@ impl Render for TranscriptView { state, segments.len(), log_count, - last_error.as_deref(), + last_error.as_ref(), )) } } @@ -320,7 +320,7 @@ fn render_status_bar( state: SessionState, segment_count: usize, log_count: usize, - last_error: Option<&str>, + last_error: Option<&SessionError>, ) -> impl IntoElement { let (dot, status_text) = match state { SessionState::Idle => (theme::record_idle(), "Idle".to_string()), diff --git a/crates/wisp-audiokit/src/error.rs b/crates/wisp-audiokit/src/error.rs new file mode 100644 index 0000000..d8cc78a --- /dev/null +++ b/crates/wisp-audiokit/src/error.rs @@ -0,0 +1,26 @@ +/// Errors surfaced by [`crate::Session`] operations. +#[derive(Debug, Clone, thiserror::Error)] +pub enum SessionError { + #[cfg(target_os = "macos")] + #[error("path contains a NUL byte or is not representable as a C string: {0:?}")] + InvalidPath(std::path::PathBuf), + + #[cfg(target_os = "macos")] + #[error("locale contains a NUL byte: {0}")] + InvalidLocale(String), + + #[cfg(target_os = "macos")] + #[error("WispAudioKit session construction failed")] + Construction, + + #[cfg(target_os = "macos")] + #[error("WispAudioKit session start failed: {0}")] + Start(String), + + #[cfg(not(target_os = "macos"))] + #[error("WispAudioKit is only available on macOS")] + UnsupportedPlatform, +} + +/// Result alias for session operations. +pub type Result = std::result::Result; diff --git a/crates/wisp-audiokit/src/lib.rs b/crates/wisp-audiokit/src/lib.rs index 5a092bb..b890167 100644 --- a/crates/wisp-audiokit/src/lib.rs +++ b/crates/wisp-audiokit/src/lib.rs @@ -3,6 +3,10 @@ //! Wraps the raw FFI from `wisp-audiokit-sys`. macOS-only; on other platforms //! everything is stubbed out so the workspace stays buildable. +mod error; + +pub use error::{Result, SessionError}; + #[cfg(target_os = "macos")] mod imp { use std::ffi::{CStr, CString}; @@ -13,6 +17,8 @@ mod imp { use wisp_audiokit_sys as sys; use wisp_core::SourceLabel; + use crate::error::{Result, SessionError}; + /// `WispAudioKit` library version (e.g. `"0.1.0"`). /// /// # Panics @@ -50,25 +56,6 @@ mod imp { Log(String), } - /// Errors surfaced by [`Session`] operations. - #[derive(Debug, thiserror::Error)] - pub enum SessionError { - #[error("path contains a NUL byte or is not representable as a C string: {0:?}")] - InvalidPath(std::path::PathBuf), - - #[error("locale contains a NUL byte: {0}")] - InvalidLocale(String), - - #[error("WispAudioKit session construction failed")] - Construction, - - #[error("WispAudioKit session start failed: {0}")] - Start(String), - } - - /// Result alias for session operations. - pub type Result = std::result::Result; - // ---- Session ------------------------------------------------------- /// Owns one running (or yet-to-be-started) capture + transcription session. @@ -267,6 +254,8 @@ mod imp { use wisp_core::SourceLabel; + use crate::error::{Result, SessionError}; + /// `WispAudioKit` library version. Always empty on non-macOS targets. #[must_use] pub fn version() -> &'static str { @@ -290,16 +279,6 @@ mod imp { Log(String), } - /// Errors surfaced by [`Session`] operations. - #[derive(Debug, thiserror::Error)] - pub enum SessionError { - #[error("WispAudioKit is only available on macOS")] - UnsupportedPlatform, - } - - /// Result alias for session operations. - pub type Result = std::result::Result; - /// Stub session — always returns [`SessionError::UnsupportedPlatform`]. pub struct Session; @@ -337,7 +316,7 @@ mod imp { } pub use imp::version; -pub use imp::{Event, Session, SessionError, SessionResult}; +pub use imp::{Event, Session, SessionResult}; pub use wisp_core::SourceLabel; #[cfg(all(test, target_os = "macos"))] diff --git a/crates/wisp-core/Cargo.toml b/crates/wisp-core/Cargo.toml index c467731..3bfcebf 100644 --- a/crates/wisp-core/Cargo.toml +++ b/crates/wisp-core/Cargo.toml @@ -12,3 +12,4 @@ workspace = true # so they live here in the shared crate. chrono = { workspace = true, features = ["serde"] } serde = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/wisp-core/src/error.rs b/crates/wisp-core/src/error.rs new file mode 100644 index 0000000..98141f7 --- /dev/null +++ b/crates/wisp-core/src/error.rs @@ -0,0 +1,4 @@ +/// Errors produced when parsing a [`crate::SourceLabel`] from its stable string form. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("unrecognized source label: {0}")] +pub struct SourceLabelError(pub String); diff --git a/crates/wisp-core/src/lib.rs b/crates/wisp-core/src/lib.rs index 3e5121c..2b797a9 100644 --- a/crates/wisp-core/src/lib.rs +++ b/crates/wisp-core/src/lib.rs @@ -4,10 +4,12 @@ //! framework wrapper (`wisp-audiokit`) into storage (`wisp-storage`) and //! the `GPUI` desktop app (`wisp-desktop`). +mod error; mod ids; mod source; mod transcript; +pub use error::SourceLabelError; pub use ids::{SegmentId, SessionId}; pub use source::SourceLabel; pub use transcript::{NewSegment, NewSession, Segment, Session}; diff --git a/crates/wisp-core/src/source.rs b/crates/wisp-core/src/source.rs index 9348c21..8c67a9a 100644 --- a/crates/wisp-core/src/source.rs +++ b/crates/wisp-core/src/source.rs @@ -32,13 +32,13 @@ impl SourceLabel { } impl std::str::FromStr for SourceLabel { - type Err = (); + type Err = crate::SourceLabelError; fn from_str(s: &str) -> std::result::Result { match s { "mic" => Ok(Self::Mic), "system" => Ok(Self::System), - _ => Err(()), + _ => Err(crate::SourceLabelError(s.to_owned())), } } } diff --git a/crates/wisp-storage/src/error.rs b/crates/wisp-storage/src/error.rs index afc29e2..f291a38 100644 --- a/crates/wisp-storage/src/error.rs +++ b/crates/wisp-storage/src/error.rs @@ -14,8 +14,8 @@ pub enum StorageError { source: rusqlite::Error, }, - #[error("unrecognized source label in database: {0}")] - UnknownSource(String), + #[error(transparent)] + SourceLabel(#[from] wisp_core::SourceLabelError), } pub type Result = std::result::Result; diff --git a/crates/wisp-storage/src/segments.rs b/crates/wisp-storage/src/segments.rs index de0d9c4..bc55b63 100644 --- a/crates/wisp-storage/src/segments.rs +++ b/crates/wisp-storage/src/segments.rs @@ -2,7 +2,7 @@ use chrono::Utc; use rusqlite::{Connection, params}; use wisp_core::{NewSegment, Segment, SegmentId, SessionId, SourceLabel}; -use crate::error::{Result, StorageError}; +use crate::error::Result; /// Read/write operations for the `segments` table. pub struct Segments<'a> { @@ -55,7 +55,7 @@ impl<'a> Segments<'a> { /// /// # Errors /// Returns [`crate::StorageError::Sqlite`] on query failure or - /// [`StorageError::UnknownSource`] if the DB holds a `source` value + /// [`StorageError::SourceLabel`] if the DB holds a `source` value /// outside `{"mic","system"}` (only possible via direct DB edits). pub fn list_by_session( &self, @@ -81,7 +81,7 @@ impl<'a> Segments<'a> { /// /// # Errors /// Returns [`crate::StorageError::Sqlite`] on query failure or - /// [`StorageError::UnknownSource`] (see [`Self::list_by_session`]). + /// [`StorageError::SourceLabel`] (see [`Self::list_by_session`]). pub fn search( &self, query: &str, @@ -109,9 +109,7 @@ impl<'a> Segments<'a> { fn row_to_segment(row: &rusqlite::Row<'_>) -> Result { let source_str: String = row.get(2)?; - let source = source_str - .parse::() - .map_err(|()| StorageError::UnknownSource(source_str))?; + let source = source_str.parse::()?; Ok(Segment { id: SegmentId::from(row.get::<_, i64>(0)?), session_id: SessionId::from(row.get::<_, i64>(1)?),