Add a global ToggleDictation keybinding
This commit is contained in:
Generated
+2
@@ -1460,6 +1460,7 @@ dependencies = [
|
|||||||
"assets",
|
"assets",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"chat",
|
"chat",
|
||||||
|
"chat_ui",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
"gpui-kit",
|
"gpui-kit",
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
@@ -1488,6 +1489,7 @@ version = "2.0.0-alpha2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"assets",
|
"assets",
|
||||||
"chat",
|
"chat",
|
||||||
|
"chat_ui",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use gpui::Action;
|
use gpui::{Action, actions};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::SignerKind;
|
use settings::SignerKind;
|
||||||
@@ -15,3 +15,6 @@ pub enum Command {
|
|||||||
Njump(PublicKey),
|
Njump(PublicKey),
|
||||||
Trace(EventId),
|
Trace(EventId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start or stop dictation in the focused composer
|
||||||
|
actions!(chat, [ToggleDictation]);
|
||||||
|
|||||||
@@ -882,6 +882,17 @@ impl ChatPanel {
|
|||||||
self.directory.read(cx).person(public_key, cx)
|
self.directory.read(cx).person(public_key, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_dictation(
|
||||||
|
&mut self,
|
||||||
|
_: &ToggleDictation,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.speech.update(cx, |speech, cx| {
|
||||||
|
speech.toggle(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match command {
|
match command {
|
||||||
Command::Insert(content) => {
|
Command::Insert(content) => {
|
||||||
@@ -1871,6 +1882,7 @@ impl Render for ChatPanel {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.image_cache(retain_all(self.id.clone()))
|
.image_cache(retain_all(self.id.clone()))
|
||||||
.on_action(cx.listener(Self::on_command))
|
.on_action(cx.listener(Self::on_command))
|
||||||
|
.on_action(cx.listener(Self::toggle_dictation))
|
||||||
.size_full()
|
.size_full()
|
||||||
.when(*self.subject_bar.read(cx), |this| {
|
.when(*self.subject_bar.read(cx), |this| {
|
||||||
this.child(
|
this.child(
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ use std::collections::BTreeMap;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use chat_ui::ToggleDictation;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
FollowMode, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, Render,
|
FollowMode, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState,
|
||||||
SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list, px,
|
ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list,
|
||||||
|
px,
|
||||||
};
|
};
|
||||||
use gpui_kit::assets::IconName;
|
use gpui_kit::assets::IconName;
|
||||||
use gpui_kit::base::StyledExt as _;
|
use gpui_kit::base::StyledExt as _;
|
||||||
@@ -1063,6 +1065,17 @@ impl GroupPanel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_dictation(
|
||||||
|
&mut self,
|
||||||
|
_ev: &ToggleDictation,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.speech.update(cx, |speech, cx| {
|
||||||
|
speech.toggle(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn render_composer(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_composer(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let notice = self.group.upgrade().and_then(|group| {
|
let notice = self.group.upgrade().and_then(|group| {
|
||||||
let group = group.read(cx);
|
let group = group.read(cx);
|
||||||
@@ -1341,6 +1354,7 @@ impl Render for GroupPanel {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
|
.on_action(cx.listener(Self::toggle_dictation))
|
||||||
.child(self.render_encryption_notice(cx))
|
.child(self.render_encryption_notice(cx))
|
||||||
.child(self.render_banner(cx))
|
.child(self.render_banner(cx))
|
||||||
.child(self.render_pins(cx))
|
.child(self.render_pins(cx))
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chat_ui::ToggleDictation;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use gpui::PathPromptOptions;
|
use gpui::PathPromptOptions;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use gpui::Task;
|
||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled,
|
AnyElement, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled,
|
||||||
Subscription, Task, Window, div,
|
Subscription, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_kit::assets::IconName;
|
use gpui_kit::assets::IconName;
|
||||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||||
use gpui_kit::component::input::{Input, InputEvent, InputState, NumberInput};
|
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::menu::{DropdownMenu, PopupMenuItem};
|
||||||
use gpui_kit::component::notification::Notification;
|
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::separator::Separator;
|
||||||
use gpui_kit::component::switch::Switch;
|
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};
|
use settings::{AppSettings, AppearanceMode};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use speech::model::{self as speech_model};
|
use speech::model::{self as speech_model};
|
||||||
@@ -236,66 +242,90 @@ impl Preferences {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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() {
|
let (description, control) = match &status {
|
||||||
speech_model::Status::NotInstalled => {
|
speech_model::Status::NotInstalled => (
|
||||||
let size = speech_model::total_size() / 1024 / 1024;
|
format!("{size} MB download"),
|
||||||
|
Button::new("download-model")
|
||||||
h_flex().gap_1().child(
|
.label("Download")
|
||||||
Button::new("download-model")
|
.secondary()
|
||||||
.label(format!("Download model ({size} MB)"))
|
.small()
|
||||||
.small()
|
.on_click(|_, _window, cx| {
|
||||||
.on_click(|_, _window, cx| {
|
speech_model::start_download();
|
||||||
speech_model::start_download();
|
cx.refresh_windows();
|
||||||
cx.refresh_windows();
|
})
|
||||||
}),
|
.into_any_element(),
|
||||||
)
|
),
|
||||||
}
|
speech_model::Status::Downloading { downloaded, total } => (
|
||||||
speech_model::Status::Downloading { downloaded, total } => {
|
format!(
|
||||||
let percent = downloaded * 100 / total.max(1);
|
"Downloading · {} of {} MB",
|
||||||
let downloaded = downloaded / 1024 / 1024;
|
downloaded / 1_000_000,
|
||||||
let total = total / 1024 / 1024;
|
total / 1_000_000
|
||||||
|
|
||||||
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();
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
|
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 {
|
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 SCREENING: &str = "Show an screening dialog to verify the unknown sender.";
|
||||||
const WOT: &str = "Treat people followed by your contacts as known senders.";
|
const WOT: &str = "Treat people followed by your contacts as known senders.";
|
||||||
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
||||||
@@ -478,6 +508,9 @@ impl Render for Preferences {
|
|||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(Separator::horizontal())
|
.child(Separator::horizontal())
|
||||||
|
.child(self.render_speech(cx))
|
||||||
|
.child(self.render_dictation_shortcut(window, cx))
|
||||||
|
.child(Separator::horizontal())
|
||||||
.child(settings_block(
|
.child(settings_block(
|
||||||
"Media Upload Service",
|
"Media Upload Service",
|
||||||
MEDIA,
|
MEDIA,
|
||||||
@@ -495,6 +528,5 @@ impl Render for Preferences {
|
|||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(self.render_speech(cx))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ ui = { path = "../crates/ui" }
|
|||||||
state = { path = "../crates/state" }
|
state = { path = "../crates/state" }
|
||||||
nip4e = { path = "../crates/nip4e" }
|
nip4e = { path = "../crates/nip4e" }
|
||||||
chat = { path = "../crates/chat" }
|
chat = { path = "../crates/chat" }
|
||||||
|
chat_ui = { path = "../crates/chat_ui" }
|
||||||
nip29 = { path = "../crates/nip29" }
|
nip29 = { path = "../crates/nip29" }
|
||||||
settings = { path = "../crates/settings" }
|
settings = { path = "../crates/settings" }
|
||||||
auto_update = { path = "../crates/auto_update" }
|
auto_update = { path = "../crates/auto_update" }
|
||||||
|
|||||||
+11
-4
@@ -1,5 +1,6 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use chat_ui::ToggleDictation;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Bounds, Global, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
|
App, Bounds, Global, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
|
||||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||||
@@ -45,13 +46,19 @@ fn main() {
|
|||||||
// Register the `quit` function
|
// Register the `quit` function
|
||||||
cx.on_action(quit);
|
cx.on_action(quit);
|
||||||
|
|
||||||
// Register the `quit` function with CMD+Q (macOS)
|
// Register the `quit` function with CMD+Q (macOS) and dictation with CMD+D
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
cx.bind_keys([
|
||||||
|
KeyBinding::new("cmd-q", Quit, None),
|
||||||
|
KeyBinding::new("cmd-d", ToggleDictation, None),
|
||||||
|
]);
|
||||||
|
|
||||||
// Register the `quit` function with Super+Q (others)
|
// Register the `quit` function with Super+Q (others) and dictation with Ctrl+D
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
cx.bind_keys([KeyBinding::new("super-q", Quit, None)]);
|
cx.bind_keys([
|
||||||
|
KeyBinding::new("super-q", Quit, None),
|
||||||
|
KeyBinding::new("ctrl-d", ToggleDictation, None),
|
||||||
|
]);
|
||||||
|
|
||||||
// Set menu items
|
// Set menu items
|
||||||
cx.set_menus(vec![Menu {
|
cx.set_menus(vec![Menu {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ ui = { path = "../crates/ui" }
|
|||||||
state = { path = "../crates/state" }
|
state = { path = "../crates/state" }
|
||||||
nip4e = { path = "../crates/nip4e" }
|
nip4e = { path = "../crates/nip4e" }
|
||||||
chat = { path = "../crates/chat" }
|
chat = { path = "../crates/chat" }
|
||||||
|
chat_ui = { path = "../crates/chat_ui" }
|
||||||
nip29 = { path = "../crates/nip29" }
|
nip29 = { path = "../crates/nip29" }
|
||||||
settings = { path = "../crates/settings" }
|
settings = { path = "../crates/settings" }
|
||||||
person = { path = "../crates/person" }
|
person = { path = "../crates/person" }
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ pub fn run() -> Result<(), JsValue> {
|
|||||||
|
|
||||||
APPLICATION.with(|application| {
|
APPLICATION.with(|application| {
|
||||||
*application.borrow_mut() = Some(app.run_embedded(|cx| {
|
*application.borrow_mut() = Some(app.run_embedded(|cx| {
|
||||||
|
cx.bind_keys([
|
||||||
|
KeyBinding::new("cmd-d", chat_ui::ToggleDictation, None),
|
||||||
|
KeyBinding::new("ctrl-d", chat_ui::ToggleDictation, None),
|
||||||
|
]);
|
||||||
|
|
||||||
gpui_kit::init(cx);
|
gpui_kit::init(cx);
|
||||||
|
|
||||||
load_embedded_fonts(cx);
|
load_embedded_fonts(cx);
|
||||||
|
|||||||
Reference in New Issue
Block a user