Add a global ToggleDictation keybinding

This commit is contained in:
2026-10-27 13:24:25 +07:00
parent f486075099
commit 47feaa1e23
9 changed files with 138 additions and 61 deletions
+86 -54
View File
@@ -1,21 +1,27 @@
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
use chat_ui::ToggleDictation;
#[cfg(not(target_arch = "wasm32"))]
use gpui::PathPromptOptions;
#[cfg(not(target_arch = "wasm32"))]
use gpui::Task;
use gpui::http_client::Url;
use gpui::{
AnyElement, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled,
Subscription, Task, Window, div,
Subscription, Window, div,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::{Input, InputEvent, InputState, NumberInput};
use gpui_kit::component::kbd::Kbd;
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
use gpui_kit::component::notification::Notification;
#[cfg(not(target_arch = "wasm32"))]
use gpui_kit::component::progress::Progress;
use gpui_kit::component::separator::Separator;
use gpui_kit::component::switch::Switch;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use gpui_kit::component::{Disableable, Sizable, WindowExt, h_flex, v_flex};
use settings::{AppSettings, AppearanceMode};
#[cfg(not(target_arch = "wasm32"))]
use speech::model::{self as speech_model};
@@ -236,66 +242,90 @@ impl Preferences {
#[cfg(not(target_arch = "wasm32"))]
{
const SPEECH: &str = "On-device speech model used by the speech to text feature. Audio never leaves the device.";
let size = speech_model::total_size() / 1_000_000;
let status = speech_model::status();
let control = match speech_model::status() {
speech_model::Status::NotInstalled => {
let size = speech_model::total_size() / 1024 / 1024;
h_flex().gap_1().child(
Button::new("download-model")
.label(format!("Download model ({size} MB)"))
.small()
.on_click(|_, _window, cx| {
speech_model::start_download();
cx.refresh_windows();
}),
)
}
speech_model::Status::Downloading { downloaded, total } => {
let percent = downloaded * 100 / total.max(1);
let downloaded = downloaded / 1024 / 1024;
let total = total / 1024 / 1024;
h_flex()
.gap_2()
.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child(format!("Downloading {percent}% · {downloaded}/{total} MB")),
)
.child(
Button::new("cancel-download")
.label("Cancel")
.small()
.on_click(|_, _window, cx| {
speech_model::cancel_download();
cx.refresh_windows();
}),
)
}
speech_model::Status::Installed => h_flex()
.gap_2()
.child(div().text_sm().child("Installed"))
.child(
Button::new("delete-model")
.label("Delete")
.small()
.on_click(|_, _window, cx| {
speech_model::delete();
cx.refresh_windows();
}),
let (description, control) = match &status {
speech_model::Status::NotInstalled => (
format!("{size} MB download"),
Button::new("download-model")
.label("Download")
.secondary()
.small()
.on_click(|_, _window, cx| {
speech_model::start_download();
cx.refresh_windows();
})
.into_any_element(),
),
speech_model::Status::Downloading { downloaded, total } => (
format!(
"Downloading · {} of {} MB",
downloaded / 1_000_000,
total / 1_000_000
),
Button::new("cancel-download")
.label("Cancel")
.ghost()
.small()
.on_click(|_, _window, cx| {
speech_model::cancel_download();
cx.refresh_windows();
})
.into_any_element(),
),
speech_model::Status::Installed => (
format!("Parakeet v3 · {size} MB"),
Button::new("remove-model")
.label("Remove")
.secondary()
.small()
.on_click(|_, _window, cx| {
speech_model::delete();
cx.refresh_windows();
})
.into_any_element(),
),
};
settings_block("Speech Recognition", SPEECH, control, cx).into_any_element()
let mut block = v_flex().w_full().gap_3().child(settings_row(
"Speech model",
description,
control,
cx,
));
if let speech_model::Status::Downloading { downloaded, total } = &status {
let (downloaded, total) = (*downloaded, *total);
block = block.child(
Progress::new("speech-model-download")
.value(downloaded as f32 / total.max(1) as f32 * 100.)
.small(),
);
}
block.into_any_element()
}
}
/// Shows the keystroke actually bound to the dictation action at startup
fn render_dictation_shortcut(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let control = match Kbd::global_binding_for_action(&ToggleDictation, window) {
Some(kbd) => kbd.into_any_element(),
None => div().into_any_element(),
};
settings_row("Shortcut", "Press to start or stop dictation", control, cx)
}
}
impl Render for Preferences {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
const SCREENING: &str = "Show an screening dialog to verify the unknown sender.";
const WOT: &str = "Treat people followed by your contacts as known senders.";
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
@@ -478,6 +508,9 @@ impl Render for Preferences {
cx,
))
.child(Separator::horizontal())
.child(self.render_speech(cx))
.child(self.render_dictation_shortcut(window, cx))
.child(Separator::horizontal())
.child(settings_block(
"Media Upload Service",
MEDIA,
@@ -495,6 +528,5 @@ impl Render for Preferences {
),
cx,
))
.child(self.render_speech(cx))
}
}