#![cfg_attr(
not(target_os = "macos"),
allow(dead_code, reason = "only the macOS hook tracks dictation, for now")
)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum DictationSession {
#[default]
Closed,
Open,
Ending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DictationEvent {
Started,
StartRequested { starts_reported: bool },
Ended { marked: bool },
StopRequested { marked: bool },
CompositionOver,
}
impl DictationSession {
pub(crate) fn is_dictating(self) -> bool {
!matches!(self, Self::Closed)
}
#[must_use]
pub(crate) fn after(self, event: DictationEvent) -> Self {
match event {
DictationEvent::Started => Self::Open,
DictationEvent::StartRequested { starts_reported } => {
if starts_reported {
self
} else {
Self::Open
}
}
DictationEvent::Ended { marked } | DictationEvent::StopRequested { marked } => {
if marked && self.is_dictating() {
Self::Ending
} else {
Self::Closed
}
}
DictationEvent::CompositionOver => match self {
Self::Ending => Self::Closed,
other => other,
},
}
}
}
#[cfg(test)]
mod tests {
use super::DictationEvent::{CompositionOver, Ended, StartRequested, Started, StopRequested};
use super::DictationSession::{Closed, Ending, Open};
use super::*;
use crate::text_input_hook::{CommitSignals, classify};
use teksilo_core::ImeOrigin;
fn run(events: &[DictationEvent]) -> DictationSession {
events
.iter()
.fold(DictationSession::Closed, |session, &event| {
session.after(event)
})
}
fn commit(session: DictationSession) -> ImeOrigin {
classify(CommitSignals {
had_marked_text: true,
dictation_session: session.is_dictating(),
..CommitSignals::default()
})
}
#[test]
fn every_state_after_every_event() {
let events = [
Started,
StartRequested {
starts_reported: true,
},
StartRequested {
starts_reported: false,
},
Ended { marked: false },
Ended { marked: true },
StopRequested { marked: false },
StopRequested { marked: true },
CompositionOver,
];
let table = [
(
Closed,
[Open, Closed, Open, Closed, Closed, Closed, Closed, Closed],
),
(
Open,
[Open, Open, Open, Closed, Ending, Closed, Ending, Open],
),
(
Ending,
[Open, Ending, Open, Closed, Ending, Closed, Ending, Closed],
),
];
for (state, expected) in table {
for (event, expected) in events.iter().zip(expected) {
assert_eq!(state.after(*event), expected, "{state:?} after {event:?}");
}
}
assert!(!Closed.is_dictating());
assert!(Open.is_dictating());
assert!(Ending.is_dictating());
}
#[test]
fn dictation_that_ends_on_silence_commits_then_closes() {
let session = run(&[Started]);
assert_eq!(commit(session), ImeOrigin::Dictation);
let session = session.after(CompositionOver);
assert_eq!(session, Open, "the commit alone does not end dictation");
let session = session.after(Ended { marked: false });
assert_eq!(session, Closed);
assert_eq!(
commit(session),
ImeOrigin::Composition,
"a later IME commit"
);
}
#[test]
fn dictation_the_user_stops_still_commits_its_last_sentence_as_dictation() {
let session = run(&[Started, Ended { marked: true }]);
assert_eq!(session, Ending);
assert_eq!(commit(session), ImeOrigin::Dictation, "the last sentence");
let session = session.after(CompositionOver);
assert_eq!(session, Closed, "closed once committed");
assert_eq!(
commit(session),
ImeOrigin::Composition,
"a later IME commit"
);
}
#[test]
fn a_stop_request_with_the_last_words_marked_waits_for_them_too() {
let session = run(&[Started, StopRequested { marked: true }]);
assert_eq!(commit(session), ImeOrigin::Dictation);
assert_eq!(session.after(CompositionOver), Closed);
}
#[test]
fn an_ending_session_closes_when_its_composition_goes_some_other_way() {
let session = run(&[Started, Ended { marked: true }, CompositionOver]);
assert_eq!(session, Closed);
assert_eq!(
commit(session),
ImeOrigin::Composition,
"nothing left of the dictation to commit"
);
}
#[test]
fn an_end_with_no_session_opens_nothing() {
assert_eq!(run(&[Ended { marked: true }]), Closed);
assert_eq!(run(&[StopRequested { marked: true }]), Closed);
}
#[test]
fn a_start_request_that_comes_to_nothing_leaves_no_session() {
let session = run(&[StartRequested {
starts_reported: true,
}]);
let origin = classify(CommitSignals {
dictation_session: session.is_dictating(),
..CommitSignals::default()
});
assert_eq!(origin, ImeOrigin::Insertion, "the emoji is not dictation");
}
#[test]
fn where_starts_are_not_reported_the_request_opens_the_session() {
let session = run(&[StartRequested {
starts_reported: false,
}]);
assert_eq!(commit(session), ImeOrigin::Dictation);
}
}