Tier 0: render what the services already decided, honestly
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -78,17 +78,74 @@ SettingsPage {
|
||||
// runs exactly as the hotkey runs it, and the words arrive here. The helper
|
||||
// is addressed through the path the Dictation service already publishes
|
||||
// rather than one spelled again in this file.
|
||||
|
||||
// True only once the helper has confirmed a recording actually started.
|
||||
//
|
||||
// It used to be set on the press, which made it a claim rather than a fact:
|
||||
// `panama-dictate start` refuses with {"ok":false,"error":"already-recording"}
|
||||
// when the hotkey is already holding the microphone, and the field said
|
||||
// "Listening…" over that refusal. Worse, the button then read "Stop and
|
||||
// type it" -- so the next press stopped the hotkey's recording and typed
|
||||
// somebody else's words into this test field.
|
||||
property bool listening: false
|
||||
|
||||
// Why the last press did not do what it said, in this page's words.
|
||||
property string testError: ""
|
||||
|
||||
// The helper answers with a code, not a sentence, so the words live here.
|
||||
// Anything unrecognised is shown as itself rather than swallowed.
|
||||
readonly property var dictateReasons: ({
|
||||
"already-recording": "Something is already listening — the dictation hotkey, most likely. Let that finish first.",
|
||||
"not-recording": "Nothing was listening, so there was nothing to type.",
|
||||
"not-downloaded": "The speech model has not been downloaded yet.",
|
||||
"no-image": "The speech server is not installed yet.",
|
||||
"server-unavailable": "The speech server did not answer.",
|
||||
"transcribe-failed": "That could not be transcribed.",
|
||||
"too-short": "That was too short to transcribe.",
|
||||
"no-speech": "Nothing was said.",
|
||||
"deliver-failed": "The words could not be typed into the field.",
|
||||
"unknown-command": "The dictation helper did not understand that."
|
||||
})
|
||||
|
||||
Process {
|
||||
id: dictateRun
|
||||
|
||||
// Which action this run was, so its reply can be read against it.
|
||||
property string action: ""
|
||||
|
||||
stdout: StdioCollector { onStreamFinished: root.absorbDictate(this.text) }
|
||||
onExited: (exitCode, exitStatus) => Dictation.refresh()
|
||||
}
|
||||
|
||||
// The helper's reply decides what happened. A start that was refused leaves
|
||||
// this page exactly as it was, with the refusal on screen.
|
||||
function absorbDictate(text: string): void {
|
||||
let reply = null;
|
||||
try {
|
||||
reply = JSON.parse(text);
|
||||
} catch (error) {
|
||||
reply = null;
|
||||
}
|
||||
const ok = reply?.ok === true;
|
||||
root.listening = dictateRun.action === "start" && ok;
|
||||
if (ok) {
|
||||
root.testError = "";
|
||||
return;
|
||||
}
|
||||
const code = String(reply?.error ?? "");
|
||||
if (code === "")
|
||||
root.testError = "The dictation helper did not answer.";
|
||||
else
|
||||
root.testError = root.dictateReasons[code] !== undefined
|
||||
? String(root.dictateReasons[code])
|
||||
: code;
|
||||
}
|
||||
|
||||
function runDictate(action: string): void {
|
||||
if (dictateRun.running)
|
||||
return;
|
||||
root.testError = "";
|
||||
dictateRun.action = action;
|
||||
dictateRun.command = [Dictation.helper, action];
|
||||
dictateRun.running = true;
|
||||
}
|
||||
@@ -181,7 +238,11 @@ SettingsPage {
|
||||
|
||||
SettingRow {
|
||||
label: "Try it"
|
||||
detail: "Speak a sentence and it is typed into the field here, rather than into whatever you were working on"
|
||||
// The refusal takes the detail's place while there is one, so a
|
||||
// press that did nothing says so where the press happened.
|
||||
detail: root.testError !== ""
|
||||
? root.testError
|
||||
: "Speak a sentence and it is typed into the field here, rather than into whatever you were working on"
|
||||
controlWidth: 340
|
||||
|
||||
Row {
|
||||
@@ -226,19 +287,27 @@ SettingsPage {
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.listening ? "Stop and type it" : "Test dictation"
|
||||
// Four states, because stopping is not instant: the helper
|
||||
// transcribes before it types, and a button still reading
|
||||
// "Stop and type it" through those seconds invites a second
|
||||
// press for a stop that has already been asked for.
|
||||
text: {
|
||||
if (dictateRun.running)
|
||||
return root.listening ? "Transcribing…" : "Starting…";
|
||||
return root.listening ? "Stop and type it" : "Test dictation";
|
||||
}
|
||||
tone: root.listening ? "accent" : "normal"
|
||||
enabled: !dictateRun.running
|
||||
onClicked: {
|
||||
// Focus first, and keep it: the helper types with wtype
|
||||
// into whatever holds keyboard focus when it finishes.
|
||||
heard.forceActiveFocus();
|
||||
if (root.listening) {
|
||||
root.listening = false;
|
||||
root.runDictate("stop");
|
||||
return;
|
||||
}
|
||||
heard.text = "";
|
||||
root.listening = true;
|
||||
// `listening` is set by the reply, not by this press.
|
||||
root.runDictate("start");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user