add Jon's feature.
This commit is contained in:
parent
b5eb5b0431
commit
cff0969b36
11 changed files with 134 additions and 54 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,11 +1,11 @@
|
|||
use crate::managers::audio::AudioRecordingManager;
|
||||
use crate::managers::transcription::TranscriptionManager;
|
||||
use crate::utils;
|
||||
use crate::utils::change_tray_icon;
|
||||
use crate::utils::TrayIconState;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::image::Image;
|
||||
use tauri::tray::TrayIcon;
|
||||
use tauri::AppHandle;
|
||||
use tauri::Manager;
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ use tauri::Manager;
|
|||
pub type PressAction = fn(app: &AppHandle, shortcut_str: &str);
|
||||
pub type ReleaseAction = fn(app: &AppHandle, shortcut_str: &str);
|
||||
|
||||
// TODO refactor to start and stop
|
||||
pub struct ActionSet {
|
||||
pub press: PressAction,
|
||||
pub release: ReleaseAction,
|
||||
|
|
@ -20,45 +21,22 @@ pub struct ActionSet {
|
|||
|
||||
// Handler Functions
|
||||
fn transcribe_pressed_action(app: &AppHandle, _shortcut_str: &str) {
|
||||
let tray = app.state::<TrayIcon>();
|
||||
tray.set_icon(Some(
|
||||
Image::from_path(
|
||||
app.path()
|
||||
.resolve(
|
||||
"resources/tray_recording.png",
|
||||
tauri::path::BaseDirectory::Resource,
|
||||
)
|
||||
.expect("failed to resolve"),
|
||||
)
|
||||
.expect("failed to set icon"),
|
||||
));
|
||||
change_tray_icon(app, TrayIconState::Recording);
|
||||
|
||||
let rm = app.state::<Arc<AudioRecordingManager>>();
|
||||
rm.try_start_recording("transcribe");
|
||||
}
|
||||
|
||||
fn transcribe_released_action(app: &AppHandle, _shortcut_str: &str) {
|
||||
let tray = app.state::<TrayIcon>();
|
||||
tray.set_icon(Some(
|
||||
Image::from_path(
|
||||
app.path()
|
||||
.resolve(
|
||||
"resources/tray_64x64.png",
|
||||
tauri::path::BaseDirectory::Resource,
|
||||
)
|
||||
.expect("failed to resolve"),
|
||||
)
|
||||
.expect("failed to set icon"),
|
||||
));
|
||||
|
||||
let ah = app.clone();
|
||||
let rm = Arc::clone(&app.state::<Arc<AudioRecordingManager>>());
|
||||
let tm = Arc::clone(&app.state::<Arc<TranscriptionManager>>());
|
||||
|
||||
change_tray_icon(app, TrayIconState::Idle);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Some(samples) = rm.stop_recording("transcribe") {
|
||||
match tm.transcribe(samples) {
|
||||
// Not .await, as transcribe is synchronous
|
||||
Ok(transcription) => {
|
||||
println!("Global Shortcut Transcription: {}", transcription);
|
||||
utils::paste(transcription, ah);
|
||||
|
|
|
|||
|
|
@ -6,13 +6,22 @@ mod utils;
|
|||
|
||||
use managers::audio::AudioRecordingManager;
|
||||
use managers::transcription::TranscriptionManager;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::image::Image;
|
||||
use tauri::menu::{Menu, MenuItem};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShortcutToggleStates {
|
||||
// Map: shortcut_binding_id -> is_active
|
||||
active_toggles: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
type ManagedToggleState = Mutex<ShortcutToggleStates>;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
env_logger::init();
|
||||
|
|
@ -27,13 +36,14 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.manage(Mutex::new(ShortcutToggleStates::default()))
|
||||
.setup(move |app| {
|
||||
let settings_i = MenuItem::with_id(app, "settings", "Settings", true, None::<&str>)?;
|
||||
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&settings_i, &quit_i])?;
|
||||
let tray = TrayIconBuilder::new()
|
||||
.icon(Image::from_path(app.path().resolve(
|
||||
"resources/tray_64x64.png",
|
||||
"resources/tray_idle.png",
|
||||
tauri::path::BaseDirectory::Resource,
|
||||
)?)?)
|
||||
.menu(&menu)
|
||||
|
|
@ -86,7 +96,8 @@ pub fn run() {
|
|||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
shortcut::change_binding,
|
||||
shortcut::reset_binding
|
||||
shortcut::reset_binding,
|
||||
shortcut::change_ptt_setting
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ impl TranscriptionManager {
|
|||
params.set_print_progress(false);
|
||||
params.set_print_realtime(false);
|
||||
params.set_print_timestamps(false);
|
||||
params.set_suppress_blank(true);
|
||||
params.set_suppress_non_speech_tokens(true);
|
||||
|
||||
state
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{App, AppHandle, Runtime};
|
||||
use tauri_plugin_store::{Store, StoreExt};
|
||||
use std::collections::HashMap;
|
||||
use tauri::{App, AppHandle};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ShortcutBinding {
|
||||
|
|
@ -16,6 +16,7 @@ pub struct ShortcutBinding {
|
|||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct AppSettings {
|
||||
pub bindings: HashMap<String, ShortcutBinding>,
|
||||
pub push_to_talk: bool,
|
||||
}
|
||||
|
||||
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
||||
|
|
@ -32,18 +33,21 @@ pub fn get_default_settings() -> AppSettings {
|
|||
current_binding: "alt+space".to_string(),
|
||||
},
|
||||
);
|
||||
bindings.insert(
|
||||
"test".to_string(),
|
||||
ShortcutBinding {
|
||||
id: "test".to_string(),
|
||||
name: "Test".to_string(),
|
||||
description: "This is a test binding.".to_string(),
|
||||
default_binding: "ctrl+d".to_string(),
|
||||
current_binding: "ctrl+d".to_string(),
|
||||
},
|
||||
);
|
||||
// bindings.insert(
|
||||
// "test".to_string(),
|
||||
// ShortcutBinding {
|
||||
// id: "test".to_string(),
|
||||
// name: "Test".to_string(),
|
||||
// description: "This is a test binding.".to_string(),
|
||||
// default_binding: "ctrl+d".to_string(),
|
||||
// current_binding: "ctrl+d".to_string(),
|
||||
// },
|
||||
// );
|
||||
|
||||
AppSettings { bindings }
|
||||
AppSettings {
|
||||
bindings,
|
||||
push_to_talk: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_or_create_app_settings(app: &App) -> AppSettings {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use serde::Serialize;
|
||||
use tauri::{App, AppHandle};
|
||||
use tauri::{App, AppHandle, Manager};
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||
use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
||||
|
||||
use crate::actions::ACTION_MAP;
|
||||
use crate::settings;
|
||||
use crate::settings::ShortcutBinding;
|
||||
use crate::settings::{self, get_settings};
|
||||
use crate::ManagedToggleState;
|
||||
|
||||
pub fn init_shortcuts(app: &App) {
|
||||
let settings = settings::load_or_create_app_settings(app);
|
||||
|
|
@ -89,6 +90,19 @@ pub fn reset_binding(app: AppHandle, id: String) -> Result<BindingResponse, Stri
|
|||
return change_binding(app, id, binding.default_binding);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
|
||||
// TODO if the setting is currently false, we probably want to
|
||||
// cancel any ongoing recordings or actions
|
||||
settings.push_to_talk = enabled;
|
||||
|
||||
settings::write_settings(&app, settings);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
|
||||
// Parse shortcut and return error if it fails
|
||||
let shortcut = match binding.current_binding.parse::<Shortcut>() {
|
||||
|
|
@ -100,15 +114,34 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S
|
|||
let binding_id_for_closure = binding.id.clone();
|
||||
|
||||
app.global_shortcut()
|
||||
.on_shortcut(shortcut, move |handler_app, scut, event| {
|
||||
.on_shortcut(shortcut, move |ah, scut, event| {
|
||||
if scut == &shortcut {
|
||||
let shortcut_string = scut.into_string();
|
||||
let settings = get_settings(ah);
|
||||
|
||||
if let Some(action_set) = ACTION_MAP.get(&binding_id_for_closure) {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
(action_set.press)(handler_app, &shortcut_string);
|
||||
} else if event.state == ShortcutState::Released {
|
||||
(action_set.release)(handler_app, &shortcut_string);
|
||||
if settings.push_to_talk {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
(action_set.press)(ah, &shortcut_string);
|
||||
} else if event.state == ShortcutState::Released {
|
||||
(action_set.release)(ah, &shortcut_string);
|
||||
}
|
||||
} else {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
let toggle_state_manager = ah.state::<ManagedToggleState>();
|
||||
|
||||
let mut states = toggle_state_manager.lock().expect("Failed to lock toggle state manager");
|
||||
|
||||
let is_currently_active = states.active_toggles.entry(binding_id_for_closure.clone()).or_insert(false);
|
||||
|
||||
if *is_currently_active {
|
||||
(action_set.release)(ah, &shortcut_string);
|
||||
*is_currently_active = false; // Update state to inactive
|
||||
} else {
|
||||
(action_set.press)(ah, &shortcut_string);
|
||||
*is_currently_active = true; // Update state to active
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
use rdev::{simulate, EventType, Key, SimulateError};
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use tauri::image::Image;
|
||||
use tauri::tray::TrayIcon;
|
||||
use tauri::AppHandle;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
fn try_send_event(event: &EventType) {
|
||||
|
|
@ -43,3 +46,26 @@ pub fn paste(text: String, app_handle: AppHandle) {
|
|||
// restore the clipboard
|
||||
clipboard.write_text(&clipboard_content).unwrap();
|
||||
}
|
||||
|
||||
pub enum TrayIconState {
|
||||
Idle,
|
||||
Recording,
|
||||
}
|
||||
|
||||
pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) {
|
||||
let tray = app.state::<TrayIcon>();
|
||||
|
||||
let icon_path = match icon {
|
||||
TrayIconState::Idle => "resources/tray_idle.png",
|
||||
TrayIconState::Recording => "resources/tray_recording.png",
|
||||
};
|
||||
|
||||
let _ = tray.set_icon(Some(
|
||||
Image::from_path(
|
||||
app.path()
|
||||
.resolve(icon_path, tauri::path::BaseDirectory::Resource)
|
||||
.expect("failed to resolve"),
|
||||
)
|
||||
.expect("failed to set icon"),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import ResetIcon from "../icons/ResetIcon";
|
|||
|
||||
export const KeyboardShortcuts: React.FC = () => {
|
||||
const [bindings, setBindings] = React.useState<ShortcutBindingsMap>({});
|
||||
const [pttEnabled, setPttEnabled] = React.useState<boolean>(false);
|
||||
const [keyPressed, setKeyPressed] = useState<string[]>([]);
|
||||
const [recordedKeys, setRecordedKeys] = useState<string[]>([]);
|
||||
const [editingShortcutId, setEditingShortcutId] = useState<string | null>(
|
||||
|
|
@ -27,6 +28,7 @@ export const KeyboardShortcuts: React.FC = () => {
|
|||
r.get("settings").then((s) => {
|
||||
const settings = SettingsSchema.parse(s);
|
||||
setBindings(settings.bindings);
|
||||
setPttEnabled(settings.push_to_talk);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
|
@ -146,6 +148,30 @@ export const KeyboardShortcuts: React.FC = () => {
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-mid-gray/20 ">
|
||||
<div className="max-w-2/3">
|
||||
<h3 className="text-sm font-medium ">Push To Talk</h3>
|
||||
<p className="text-sm">Hold to record, release to stop</p>
|
||||
</div>
|
||||
<label className="inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
className="sr-only peer"
|
||||
checked={pttEnabled}
|
||||
onChange={(e) => {
|
||||
console.log("change ptt setting", e.target.checked);
|
||||
const newValue = e.target.checked;
|
||||
setPttEnabled(newValue);
|
||||
|
||||
invoke("change_ptt_setting", {
|
||||
enabled: newValue,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="relative w-11 h-6 bg-mid-gray/20 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-logo-primary rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-logo-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
{Object.entries(bindings).map(([id, binding]) => (
|
||||
<div
|
||||
key={id}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { KeyboardShortcuts } from "./KeyboardShortcuts";
|
|||
export const Settings: React.FC = () => {
|
||||
return (
|
||||
<div className="max-w-3xl w-full mx-auto space-y-4">
|
||||
<div className="">
|
||||
<h2 className="text-xl font-semibold mb-2">Keyboard Shortcuts</h2>
|
||||
<div className="mt-8">
|
||||
{/* <h2 className="text-xl font-semibold mb-2">Keyboard Shortcuts</h2> */}
|
||||
<KeyboardShortcuts />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const ShortcutBindingsMapSchema = z.record(
|
|||
|
||||
export const SettingsSchema = z.object({
|
||||
bindings: ShortcutBindingsMapSchema,
|
||||
push_to_talk: z.boolean(),
|
||||
});
|
||||
|
||||
export const BindingResponseSchema = z.object({
|
||||
|
|
|
|||
Loading…
Reference in a new issue