gate swipe type behind feature, edit dashboard

This commit is contained in:
oneshinyboi 2026-08-15 19:01:05 -05:00
parent 404b5ea659
commit ad89b44db2
No known key found for this signature in database
GPG Key ID: A494E206A095F0C0
11 changed files with 107 additions and 81 deletions

10
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -227,11 +227,12 @@
"WHISPER_MODEL": "Speech-to-text model",
"WHISPER_MODEL_HELP": "Whisper model to use.\nLarger models produce more accurate\nresults, but consume more VRAM.",
"SWIPE_TYPE": {
"NEED_TO_DOWNLOAD_MODELS": "The swipe-to-type model needs to be downloaded.\nDownload them now?",
"NEED_TO_DOWNLOAD_MODELS": "The swipe-to-type model needs to be downloaded.\nDownload it now?",
"MODELS_DOWNLOADED": "Model downloaded",
"MODELS_NOT_DOWNLOADED": "Model not downloaded",
"REMOVE_MODELS": "Remove swipe to type model",
"DOWNLOAD_MODELS": "Download swipe to type model ({} total)"
"DOWNLOAD_MODELS": "Download swipe to type model",
"DOWNLOAD_MODELS_HELP": "Swipe to type only works on qwerty layout keyboards"
},
"SWIPE_TYPE_MODELS": "Swipe-to-type models",
"SWIPE_TYPE_MODELS_HELP": "Model files used by swipe-to-type.\nAll of them are required.",

View File

@ -353,6 +353,7 @@ pub(crate) enum SettingType {
InvertScrollDirectionY,
KeyboardMiddleClick,
KeyboardSoundEnabled,
KeyboardSwipeToTypeEnabled,
Language,
LeftHandedMouse,
LongPressDuration,
@ -412,6 +413,7 @@ impl SettingType {
Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y,
Self::KeyboardSoundEnabled => &mut config.keyboard_sound_enabled,
Self::KeyboardSwipeToTypeEnabled => &mut config.keyboard_swipe_to_type_enabled,
Self::LeftHandedMouse => &mut config.left_handed_mouse,
Self::NotificationsEnabled => &mut config.notifications_enabled,
Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
@ -550,6 +552,7 @@ impl SettingType {
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
Self::KeyboardSwipeToTypeEnabled => Ok("APP_SETTINGS.KEYBOARD_SWIPE_TYPE_ENABLED"),
Self::Language => Ok("APP_SETTINGS.LANGUAGE"),
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),

View File

@ -1,3 +1,4 @@
use std::path::PathBuf;
use std::rc::Rc;
use wgui::{
@ -22,22 +23,23 @@ use crate::{
util::{
popup_manager::PopupHolder,
swipe_type::{
SWIPE_TYPE_MODELS, SwipeTypeModel, swipe_type_all_models_downloaded,
SWIPE_TYPE_MODEL, swwipe_type_model_downloaded,
swipe_type_delete_all_models, swipe_type_model_path,
},
whisper::{
WHISPER_MODELS, WhisperModel, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name,
WHISPER_MODELS, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name,
whisper_model_path,
},
},
views::{self, ViewUpdateParams},
};
use crate::util::downloadable_file::DownloadableFile;
#[derive(Clone)]
enum Task {
WhisperDownloadClosed,
WhisperRemoveUnused,
WhisperDownload(&'static WhisperModel),
WhisperDownload(&'static DownloadableFile),
WhisperDownloadDone,
SwipeTypeDownloadClosed,
SwipeTypeRemoveAll,
@ -54,8 +56,8 @@ pub struct State {
globals: WguiGlobals,
tasks: Tasks<Task>,
parent_tasks: Tasks<ParentTask>,
pending_whisper_download: Option<&'static WhisperModel>,
pending_swipe_download: Option<&'static [SwipeTypeModel]>,
pending_whisper_download: Option<&'static DownloadableFile>,
pending_swipe_download: Option<&'static DownloadableFile>,
}
impl SettingsTab for State {
@ -81,7 +83,7 @@ impl SettingsTab for State {
}
Task::WhisperDownload(model) => {
self.pending_whisper_download = Some(model);
self.show_whisper_download_dialog(model, par.executor.clone());
self.show_download_dialogue(model, par.executor.clone(), whisper_model_path(model.file_name), Task::WhisperDownloadClosed, Task::WhisperDownloadDone);
}
Task::WhisperDownloadDone => {
if let Some(model) = self.pending_whisper_download.take() {
@ -94,22 +96,22 @@ impl SettingsTab for State {
}
Task::SwipeTypeDownloadClosed => {
self.pending_swipe_download = None;
if !swipe_type_all_models_downloaded().unwrap_or_default() {
par.general_config.swipe_type_models_downloaded = false;
if !swwipe_type_model_downloaded().unwrap_or_default() {
par.general_config.keyboard_swipe_to_type_enabled = false;
par.config_change_kind.replace(ConfigChangeKind::Other);
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
}
}
Task::SwipeTypeRemoveAll => {
let _ = swipe_type_delete_all_models().log_err("could not remove swipe type models");
par.general_config.keyboard_swipe_to_type_enabled = false;
}
Task::SwipeTypeDownloadAll => {
self.pending_swipe_download = Some(SWIPE_TYPE_MODELS);
self.show_swipe_type_download_dialog(SWIPE_TYPE_MODELS, par.executor.clone());
self.pending_swipe_download = Some(&SWIPE_TYPE_MODEL);
self.show_download_dialogue(&SWIPE_TYPE_MODEL, par.executor.clone(), swipe_type_model_path(SWIPE_TYPE_MODEL.file_name), Task::SwipeTypeDownloadClosed, Task::SwipeTypeDownloadDone);
}
Task::SwipeTypeDownloadDone => {
if let Some(_) = self.pending_swipe_download.take() {
par.general_config.swipe_type_models_downloaded = true;
par.config_change_kind.replace(ConfigChangeKind::Other);
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
}
@ -204,7 +206,7 @@ impl State {
if par.feats.swipe_to_type {
swipe_type_models_button(par.mp, c)?;
}
options_checkbox(par.mp, c, SettingType::KeyboardSwipeToTypeEnabled)?;
options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?;
options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?;
@ -240,27 +242,22 @@ impl State {
})
}
fn show_whisper_download_dialog(&mut self, model: &WhisperModel, executor: AsyncExecutor) {
fn show_download_dialogue(&mut self, file: &DownloadableFile, executor: AsyncExecutor, target_path: PathBuf, on_closed: Task, on_downloaded: Task) {
views::download_file::mount_popup(
self.popup_download.clone(),
self.frontend_tasks.clone(),
self.tasks.make_callback_box(Task::WhisperDownloadClosed),
self.tasks.make_callback_box(on_closed),
views::download_file::Params {
globals: self.globals.clone(),
executor,
target_path: whisper_model_path(model.file_name),
url: model.url.into(),
on_downloaded: self.tasks.make_callback_box(Task::WhisperDownloadDone),
target_path,
url: file.url.into(),
on_downloaded: self.tasks.make_callback_box(on_downloaded),
},
);
}
fn show_swipe_type_download_dialog(&mut self, models: &[SwipeTypeModel], executor: AsyncExecutor) {
//TODO: create views::download_files and use that
unimplemented!()
}
fn show_whisper_model_dialog_box_download(&mut self, model: &'static WhisperModel) -> anyhow::Result<()> {
fn show_whisper_model_dialog_box_download(&mut self, model: &'static DownloadableFile) -> anyhow::Result<()> {
const ACTION_DOWNLOAD: &str = "download";
const ACTION_CANCEL: &str = "cancel";
@ -464,7 +461,7 @@ fn swipe_type_models_button(mp: &mut MacroParams, parent: WidgetID) -> anyhow::R
let id_cell = horiz_cell(mp.layout, parent)?;
let all_downloaded = swipe_type_all_models_downloaded().unwrap_or_default();
let all_downloaded = swwipe_type_model_downloaded().unwrap_or_default();
let (translation, icon, action) = if all_downloaded {
(
"APP_SETTINGS.SWIPE_TYPE.REMOVE_MODELS",
@ -473,7 +470,7 @@ fn swipe_type_models_button(mp: &mut MacroParams, parent: WidgetID) -> anyhow::R
)
} else {
(
"APP_SETTINGS.SWIPE_TYPE.DOWNLOAD_MODELS;5.0 MiB",
"APP_SETTINGS.SWIPE_TYPE.DOWNLOAD_MODELS",
"dashboard/download.svg",
"swipe_type_download",
)

View File

@ -0,0 +1,5 @@
pub struct DownloadableFile {
pub file_name: &'static str,
pub display_name: &'static str,
pub url: &'static str,
}

View File

@ -8,3 +8,4 @@ pub mod toast_manager;
pub mod wgui_simple;
pub mod whisper;
pub mod swipe_type;
pub mod downloadable_file;

View File

@ -1,33 +1,13 @@
use std::{fs, io, path::PathBuf};
use wlx_common::data_dir;
use crate::util::downloadable_file::DownloadableFile;
/// The set of model files required by the swipe-to-type engine.
/// These correspond to the assets that `super-swipe-type` currently
/// downloads via `cached_path` in `SwipeOrchestrator::new()`.
pub struct SwipeTypeModel {
pub file_name: &'static str,
pub url: &'static str,
}
pub const SWIPE_TYPE_MODELS: &[SwipeTypeModel] = &[
SwipeTypeModel {
file_name: "swipe_encoder_android.onnx",
url: "https://wayvr.org/files/swipe_type/swipe_encoder_android.onnx",
},
SwipeTypeModel {
file_name: "swipe_decoder_android.onnx",
url: "https://wayvr.org/files/swipe_type/swipe_decoder_android.onnx",
},
SwipeTypeModel {
file_name: "en_wordlist.fst",
url: "https://wayvr.org/files/swipe_type/en_wordlist.fst",
},
SwipeTypeModel {
file_name: "en_bigrams.fst",
url: "https://wayvr.org/files/swipe_type/en_bigrams.fst",
},
];
pub const SWIPE_TYPE_MODEL: DownloadableFile = DownloadableFile {
file_name: "en.tar",
display_name: "English Qwerty (15 MiB)",
url: "https://github.com/oneshinyboi/super-swipe-type/raw/refs/tags/v0.4.2/crates/super-swipe-type/assets/en.tar",
};
pub fn swipe_type_model_folder() -> PathBuf {
data_dir::get_path("swipe_type")
@ -37,16 +17,13 @@ pub fn swipe_type_model_path(file_name: &str) -> PathBuf {
swipe_type_model_folder().join(file_name)
}
/// Returns true when every required model file is present on disk.
pub fn swipe_type_all_models_downloaded() -> io::Result<bool> {
pub fn swwipe_type_model_downloaded() -> io::Result<bool> {
let path = swipe_type_model_folder();
if !path.is_dir() {
return Ok(false);
}
for model in SWIPE_TYPE_MODELS {
if !path.join(model.file_name).exists() {
return Ok(false);
}
if !path.join(SWIPE_TYPE_MODEL.file_name).exists() {
return Ok(false);
}
Ok(true)
}

View File

@ -1,42 +1,37 @@
use std::{fs, io, path::PathBuf};
use wlx_common::data_dir;
use crate::util::downloadable_file::DownloadableFile;
pub struct WhisperModel {
pub file_name: &'static str,
pub display_name: &'static str,
pub url: &'static str,
}
pub const WHISPER_MODELS: &[WhisperModel] = &[
WhisperModel {
pub const WHISPER_MODELS: &[DownloadableFile] = &[
DownloadableFile {
file_name: "ggml-base-q8_0.bin",
display_name: "Base Q8 (78MiB)",
url: "https://wayvr.org/files/whisper/ggml-base-q8_0.bin",
},
WhisperModel {
DownloadableFile {
file_name: "ggml-small-q8_0.bin",
display_name: "Small Q8 (252MiB)",
url: "https://wayvr.org/files/whisper/ggml-small-q8_0.bin",
},
WhisperModel {
DownloadableFile {
file_name: "ggml-large-v3-turbo-q5_0.bin",
display_name: "Turbo Q5 (574MiB)",
url: "https://wayvr.org/files/whisper/ggml-large-v3-turbo-q5_0.bin",
},
WhisperModel {
DownloadableFile {
file_name: "ggml-large-v3-turbo-q8_0.bin",
display_name: "Turbo Q8 (874MiB)",
url: "https://wayvr.org/files/whisper/ggml-large-v3-turbo-q8_0.bin",
},
WhisperModel {
DownloadableFile {
file_name: "ggml-large-v3-turbo.bin",
display_name: "Turbo (1.5GiB)",
url: "https://wayvr.org/files/whisper/ggml-large-v3-turbo.bin",
},
];
pub fn whisper_model_from_name(file_name: &str) -> Option<&'static WhisperModel> {
pub fn whisper_model_from_name(file_name: &str) -> Option<&'static DownloadableFile> {
WHISPER_MODELS.iter().find(|x| x.file_name == file_name)
}

View File

@ -34,7 +34,11 @@ use wgui::{
use wgui::event::StyleSetRequest;
use wgui::layout::LayoutTask;
use wgui::taffy::Display;
use super::{KeyButtonData, KeyState, KeyboardState, handle_press, handle_release, layout::{self, KeyCapType}, handle_mouse_motion, init_swipe_type_manager};
#[cfg(feature = "swipe-to-type")]
use wlx_common::data_dir;
use super::{KeyButtonData, KeyState, KeyboardState, handle_press, handle_release, layout::{self, KeyCapType}, handle_mouse_motion};
#[cfg(feature = "swipe-to-type")]
use super::init_swipe_type_manager;
const PIXELS_PER_UNIT: f32 = 60.;
@ -478,7 +482,8 @@ pub(super) fn create_keyboard_panel(
}
}
if app.session.config.keyboard_swipe_to_type_enabled && panel.state.swipe_typing_manager.is_none() {
init_swipe_type_manager(&mut panel.state);
#[cfg(feature = "swipe-to-type")]
init_swipe_type_manager(&mut panel.state, data_dir::get_path("swipe_type"));
let predictions_root = panel.parser_state
.get_widget_id("swipe_predictions_root")

View File

@ -45,6 +45,8 @@ use wlx_common::{
config::AltModifier,
overlays::{BackendAttrib, BackendAttribValue},
};
#[cfg(feature = "swipe-to-type")]
use wlx_common::data_dir;
use crate::overlays::keyboard::builder::update_swipe_prediction_bar;
use crate::overlays::keyboard::layout::KeyCapType;
use crate::overlays::keyboard::swipe_type::SwipeTypingManager;
@ -52,9 +54,32 @@ use crate::overlays::keyboard::swipe_type::SwipeTypingManager;
pub mod builder;
mod layout;
#[cfg(feature = "swipe-to-type ")]
#[cfg(feature = "swipe-to-type")]
mod swipe_type;
#[cfg(not(feature = "swipe-to-type"))]
mod swipe_type {
use std::sync::mpsc::Receiver;
use wgui::event::{DeviceBitmask, MouseButtonIndex};
use glam::Vec2;
pub struct SwipeTypingManager;
impl SwipeTypingManager {
pub fn new(_model_folder: std::path::PathBuf) -> anyhow::Result<(Self, Receiver<Option<Vec<String>>>)> {
Ok((Self, std::sync::mpsc::sync_channel(1).1))
}
pub fn add_swipe(&mut self, _within_key_pos_normalized: &Vec2, _key_label: char, _device: DeviceBitmask, _index: Option<MouseButtonIndex>) {}
pub fn predict(&mut self) -> anyhow::Result<()> { Ok(()) }
pub fn reset(&mut self) {}
pub fn did_swipe_leave_first_key(&self) -> bool { false }
pub fn is_current_swipe_empty(&self) -> bool { true }
pub fn current_swipe_mouse_button_index(&self) -> Option<MouseButtonIndex> { None }
pub fn select_word(&mut self, _word: &String, _app: &mut crate::state::AppState, _original_keyboard_mods: crate::subsystem::hid::KeyModifier) {}
pub fn select_alternate_prediction(&mut self, _word: &String, _app: &mut crate::state::AppState, _original_keyboard_mods: crate::subsystem::hid::KeyModifier) {}
}
}
pub const KEYBOARD_NAME: &str = "kbd";
const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, ALTGR];
const SYSTEM_LAYOUT_ALIASES: [&str; 5] = ["mozc", "pinyin", "hangul", "sayura", "unikey"];
@ -132,8 +157,9 @@ pub fn create_keyboard(app: &mut AppState, wayland: bool) -> anyhow::Result<Over
..OverlayWindowConfig::from_backend(Box::new(backend))
})
}
pub(self) fn init_swipe_type_manager(state: &mut KeyboardState) {
match SwipeTypingManager::new() {
#[cfg(feature = "swipe-to-type")]
pub(self) fn init_swipe_type_manager(state: &mut KeyboardState, model_folder: std::path::PathBuf) {
match SwipeTypingManager::new(model_folder) {
Ok((engine, receiver)) => {
state.swipe_typing_manager = Some(engine);
state.swipe_candidate_receiver = Some(receiver);
@ -143,6 +169,7 @@ pub(self) fn init_swipe_type_manager(state: &mut KeyboardState) {
}
};
}
#[cfg(feature = "swipe-to-type")]
pub(self) fn hide_swipe_type_manager(panel: &mut GuiPanel<KeyboardState>) {
let predictions_root = panel.parser_state
.get_widget_id("swipe_predictions_root")
@ -188,7 +215,8 @@ impl KeyboardBackend {
let mut state = self.default_state.take();
if app.session.config.keyboard_swipe_to_type_enabled {
init_swipe_type_manager(&mut state);
#[cfg(feature = "swipe-to-type")]
init_swipe_type_manager(&mut state, data_dir::get_path("swipe_type"));
log::info!("swipe engine created");
}
@ -196,6 +224,7 @@ impl KeyboardBackend {
create_keyboard_panel(app, keymap, state, &self.wlx_layout)?;
if !app.session.config.keyboard_swipe_to_type_enabled {
#[cfg(feature = "swipe-to-type")]
hide_swipe_type_manager(&mut panel);
}
@ -243,7 +272,8 @@ impl KeyboardBackend {
.take();
if app.session.config.keyboard_swipe_to_type_enabled {
init_swipe_type_manager(&mut state_from);
#[cfg(feature = "swipe-to-type")]
init_swipe_type_manager(&mut state_from, data_dir::get_path("swipe_type"));
}
self.active_layout = new_key;
@ -254,6 +284,7 @@ impl KeyboardBackend {
.state = state_from;
if !app.session.config.keyboard_swipe_to_type_enabled {
#[cfg(feature = "swipe-to-type")]
hide_swipe_type_manager(self.layout_panels
.get_mut(self.active_layout)
.unwrap()

View File

@ -3,6 +3,7 @@ use crate::subsystem::hid::{KeyModifier, VirtualKey, CTRL};
use anyhow::{bail};
use glam::Vec2;
use std::mem;
use std::path::PathBuf;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender, channel, Sender};
use std::thread::{self, JoinHandle};
use std::time::Instant;
@ -87,14 +88,14 @@ impl SwipeTypingManager {
app.hid_provider
.set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods);
}
pub fn new() -> anyhow::Result<(SwipeTypingManager, Receiver<Option<Vec<String>>>)> {
pub fn new(model_folder: PathBuf) -> anyhow::Result<(SwipeTypingManager, Receiver<Option<Vec<String>>>)> {
let (candidate_sender, candidate_receiver) = sync_channel(1);
let (task_sender, task_receiver) = channel::<PredictionTask>();
// Spawn persistent worker thread
let worker_candidate_sender = candidate_sender.clone();
let worker_thread = thread::spawn(move || {
let mut swipe_engine = match SwipeOrchestrator::new() {
let mut swipe_engine = match SwipeOrchestrator::new_with_paths(&model_folder) {
Ok(engine) => engine,
Err(e) => {
log::error!("Failed to initialize SwipeOrchestrator: {}", e);