mod activity;
#[cfg(hex_capture)]
pub(crate) mod capture_capsule;
mod controls;
#[cfg(target_os = "macos")]
pub(crate) mod deep_link;
mod explainer;
mod found;
mod found_choice;
mod fun;
mod general;
mod history;
#[cfg_attr(target_os = "linux", path = "rune_ui/linux_host.rs")]
#[cfg_attr(target_os = "windows", path = "rune_ui/windows_host.rs")]
#[cfg_attr(target_os = "macos", path = "rune_ui/macos_host.rs")]
mod host;
mod hud_lab;
#[cfg(target_os = "macos")]
mod hud_stories;
mod icons;
mod labs;
mod list_detail;
mod lists;
mod live_notes;
pub(crate) mod live_text;
pub(crate) mod meeting_capsules;
mod meetings;
pub(crate) use self::meetings::meeting_import_failed;
mod meetings_host;
mod model_details;
mod model_import;
mod models;
mod modes;
mod onboarding;
mod onboarding_kit;
mod output;
mod paste_access;
mod picker;
mod platform;
mod platform_settings;
pub(crate) mod playground;
mod practice;
mod scenes;
#[cfg(hex_capture)]
mod scrawl;
#[cfg(hex_capture)]
pub(crate) mod screenshot_thumbnail;
mod settings;
mod shortcut_mode;
mod sidebar;
mod sidebar_update;
mod sound_sets;
mod spark;
#[cfg(target_os = "macos")]
pub(crate) mod story_link;
mod vocabulary;
use std::sync::Arc;
use std::time::{Duration, Instant};
use rune::{
Anim, AppSpec, Cx, El, Icon, ScrollSpec, TextEdit, Tx, Waker, col, el, icon, row, text,
};
use crate::app_settings::{AppSettings, HotkeyBinding, HotkeyKey, HotkeyModifiers, ModifierSide};
use crate::desktop::host::{
DesktopAction, DesktopGrantedShortcut, DesktopListenerSnapshot, DesktopShortcuts,
DesktopTranscriptionSnapshot, DesktopUpdateStatus,
};
use crate::desktop::i18n::{tr, tr_fill};
use crate::transcription::models::TranscriptionModelId;
use controls::{METRICS, Paint};
use host::{Backend, Request, ShortcutTarget};
use onboarding_kit::live::Page;
use scenes::{SCENES, Scene, fixture, mac_fixture, portal_fixture};
use settings::Capture;
use sidebar::{BuildKind, Pane, pane_header, sidebar};
const DRAFT_DELAY: Duration = Duration::from_millis(250);
const WINDOW_TICK: Duration = Duration::from_millis(250);
#[cfg(target_os = "linux")]
pub(crate) fn run_app(
host: crate::linux::app::HostHandle,
requests: std::sync::mpsc::Receiver<crate::linux::service::Control>,
first: Option<crate::linux::service::Activation>,
shutdown: &'static std::sync::atomic::AtomicBool,
) {
use crate::linux::service::{Activation, Control};
rune::set_app_id(crate::linux::portal::APP_ID);
let mut app = self::app(Some(host.clone()))
.args(window_args())
.windowless();
if !crate::linux::session::floats_windows() {
app = app.float_on_open();
}
crate::linux::rune_hud::install(&mut app);
live_text::install(&mut app, crate::rune_hud::live_text_place);
let _capture_overlays =
crate::rune_ui::capture_capsule::feed::CaptureOverlays::install(&mut app);
let meetings = crate::meeting::runtime::Meetings::install(&mut app, shutdown)
.inspect_err(|error| tracing::error!(%error, "meetings could not start"))
.ok();
let handle = app.handle();
host.set_window_visible({
let handle = handle.clone();
Arc::new(move || handle.is_visible())
});
let show = move |activation: Activation| match activation.token.or(activation.startup_id) {
Some(token) => handle.activate(token),
None => handle.open(),
};
if let Some(activation) = first {
show(activation);
}
let quit = app.handle();
let window = app.handle();
std::thread::Builder::new()
.name("hex-window-requests".into())
.spawn(move || {
while let Ok(request) = requests.recv() {
match request {
Control::Activate(activation) => show(activation),
Control::Show(pane) => {
show(Activation::default());
window.update(move |ui, tx| self::show(ui, destination(pane), tx));
}
Control::Hide => window.close(),
Control::OnboardingReset(answer) => {
show(Activation::default());
window.update(move |ui, tx| {
let _ = answer.send(restart_onboarding(ui, tx));
});
}
Control::Quit => {
self::host::close_before_quit(&window);
quit.quit();
break;
}
}
}
})
.expect("could not start the window request thread");
app.run();
shutdown.store(true, std::sync::atomic::Ordering::Release);
if let Some(meetings) = meetings {
meetings.join();
}
}
pub(crate) fn app(connect: host::Connect) -> rune::App<Ui> {
onboarding_kit::set_live_window(true);
let app = build(move || Ui::load_with(connect), PlaygroundMode::Gated);
#[cfg(target_os = "macos")]
let app = app.on_open_url(on_open_url);
app
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub(crate) fn window_args() -> Vec<String> {
vec!["--scroll-bars".into(), "always".into()]
}
fn managed_note() -> &'static str {
tr("Managed by your organization")
}
#[cfg(target_os = "macos")]
fn on_open_url(ui: &mut Ui, link: &str, tx: &mut Tx) {
if let Some(target) = deep_link::parse(link, story_link::scheme()) {
match target {
Ok(target) => deep_link::open(ui, target, tx),
Err(why) => tracing::warn!(%why, "ignored a link opened with Hex"),
}
crate::macos_rune::bring_window_forward();
return;
}
let link = match story_link::parse(link, story_link::scheme()) {
Ok(link) => link,
Err(why) => {
tracing::warn!(%why, "ignored a link opened with Hex");
return;
}
};
if let Err(why) = story_link::open(&link, tx) {
tracing::warn!(story = %link.story, %why, "a story link opened only the window");
}
crate::macos_rune::bring_window_forward();
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn render_app(rune_args: Vec<String>) -> rune::App<Ui> {
let playground = rune_args.iter().any(|arg| arg == "--playground");
let headless = rune_args
.iter()
.any(|arg| arg == "--snap" || arg == "--video" || arg == "--list-scenes");
onboarding_kit::set_live_window(playground && !headless);
let app = build(
Ui::fixture_base,
if playground && playground::enabled() {
PlaygroundMode::Attached
} else {
PlaygroundMode::None
},
)
.args(rune_args);
if playground && !headless {
app.handle().update_if(|_, _| {
crate::feedback::set_enabled(true);
crate::feedback::set_volume(0.65);
let _ = crate::feedback::preload();
false
});
}
app
}
#[derive(Default)]
pub(crate) struct WindowCompositor {
onboarding: Option<onboarding_kit::gpu_stage::OnboardingGpu>,
game: Option<onboarding_kit::game_fx::GameFx>,
sounds: sound_sets::Preview,
logged: Option<rune::compose::DeviceKey>,
}
impl rune::compose::Compositor<Ui> for WindowCompositor {
fn features(&self) -> wgpu::Features {
crate::rune_hud::Painter::FEATURES
}
fn render(&mut self, f: &mut rune::compose::Frame<'_, Ui>) {
if self.logged != Some(f.device_key()) {
self.logged = Some(f.device_key());
tracing::info!(gpu = %rune::describe_gpu(f.device), "window draws");
}
if f.rt
.marks
.iter()
.any(|&m| onboarding_kit::game_fx::is_game_mark(m))
{
self.game
.get_or_insert_with(|| onboarding_kit::game_fx::GameFx::new(f.device))
.render(f);
} else if self.onboarding.is_some()
|| f.rt
.marks
.iter()
.any(|&m| onboarding_kit::gpu_stage::is_onboard_mark(m))
{
if self
.onboarding
.as_ref()
.is_none_or(|g| !g.matches(f.format))
{
self.onboarding = Some(onboarding_kit::gpu_stage::OnboardingGpu::new(
f.device, f.format,
));
}
self.onboarding.as_mut().expect("created above").render(f);
} else {
f.draw_ui();
}
self.sounds.render(f);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PlaygroundMode {
None,
#[cfg_attr(target_os = "linux", allow(dead_code))]
Attached,
Gated,
}
fn build(init: impl FnOnce() -> Ui + 'static, playground: PlaygroundMode) -> rune::App<Ui> {
let spec = if playground == PlaygroundMode::None {
spec()
} else {
playground::spec(spec())
};
let app = rune::App::new(spec)
.init(init)
.on_close(on_close)
.compositor(WindowCompositor::default());
#[cfg(target_os = "linux")]
let app = app.modifier_keys();
let app = match playground {
PlaygroundMode::None => app,
PlaygroundMode::Attached => playground::attach(app),
PlaygroundMode::Gated => playground::attach_gated(app),
};
let window = app.handle();
window.update_if({
let window = window.clone();
move |ui, tx| {
ui.window = Some(window);
tx.wake_after(WINDOW_TICK);
false
}
});
app
}
fn on_close(ui: &mut Ui, _: &mut Tx) {
ui.flush_drafts();
settings::cancel_capture(ui);
ui.open = None;
models::close(ui);
picker::close(ui);
explainer::close(ui);
sound_sets::close(ui);
vocabulary::close(ui);
if ui.pane == Pane::HudLab {
ui.ask(platform::PlatformRequest::EndHudDemo);
ui.pane = Pane::General;
}
if let Service::Live(backend) = &mut ui.service {
backend.closed();
}
#[cfg(target_os = "macos")]
crate::macos_rune::window_closed();
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Destination {
Settings,
Models,
Meetings(Option<String>),
Modes,
History,
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn destination(pane: Option<crate::control::Pane>) -> Destination {
use crate::control::Pane as Named;
match pane {
None | Some(Named::Settings) => Destination::Settings,
Some(Named::Models) => Destination::Models,
Some(Named::Modes) => Destination::Modes,
Some(Named::History) => Destination::History,
Some(Named::Meetings) => Destination::Meetings(None),
}
}
pub(crate) fn show(ui: &mut Ui, destination: Destination, tx: &mut Tx) {
let pane = match &destination {
Destination::Settings | Destination::Models => Pane::General,
Destination::Meetings(_) => Pane::Meetings,
Destination::Modes => Pane::Output,
Destination::History => Pane::History,
};
picker::close(ui);
explainer::close(ui);
sound_sets::close(ui);
vocabulary::close(ui);
if pane.shown(ui) {
ui.select_pane(pane, tx);
}
match destination {
Destination::Models => picker::open(ui),
Destination::Meetings(Some(id)) => {
ui.meetings.view = meetings::View::Recordings;
ui.meetings.select(id);
}
_ => {}
}
}
pub(crate) fn capabilities() -> std::collections::BTreeMap<String, bool> {
platform::Capabilities::current()
.fields()
.into_iter()
.map(|(name, on)| (name.to_owned(), on))
.collect()
}
pub(crate) fn capability(name: &str) -> bool {
platform::Capabilities::current()
.fields()
.into_iter()
.any(|(field, on)| field == name && on)
}
pub(crate) fn restart_onboarding(ui: &mut Ui, tx: &mut Tx) -> Result<(), String> {
onboarding_kit::live::restart(ui, tx).inspect_err(|why| ui.error = Some(why.clone()))
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) fn current_pane(ui: &Ui) -> &'static str {
match ui.pane {
Pane::General => "settings",
pane => pane.key().trim_start_matches("nav-"),
}
}
pub(crate) fn shortcut_rule(
slot: crate::app_settings::HotkeySlot,
binding: &HotkeyBinding,
) -> Result<(), &'static str> {
use crate::app_settings::HotkeySlot;
let target = match slot {
HotkeySlot::Dictation => ShortcutTarget::Dictation,
HotkeySlot::PasteLast => ShortcutTarget::PasteLast,
HotkeySlot::RawDictation => ShortcutTarget::RawDictation,
HotkeySlot::Cancel => ShortcutTarget::Cancel,
#[cfg(hex_capture)]
HotkeySlot::Capture => ShortcutTarget::Capture,
#[cfg(all(hex_capture, target_os = "macos"))]
HotkeySlot::CaptureReplay => ShortcutTarget::CaptureReplay,
#[cfg(all(hex_capture, not(target_os = "macos")))]
HotkeySlot::CaptureReplay => ShortcutTarget::Capture,
#[cfg(target_os = "macos")]
HotkeySlot::SecondDictation => ShortcutTarget::SecondDictation,
#[cfg(target_os = "macos")]
HotkeySlot::Screenshot => ShortcutTarget::Screenshot,
#[cfg(not(target_os = "macos"))]
HotkeySlot::SecondDictation | HotkeySlot::Screenshot => ShortcutTarget::Dictation,
};
host::validate_hotkey(target, binding)
}
#[cfg(target_os = "macos")]
pub(crate) fn hotkey_key_named(name: &str) -> Result<HotkeyKey, &'static str> {
host::hotkey_key_named(name)
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn hotkey_key_named(name: &str) -> Result<HotkeyKey, &'static str> {
Ok(HotkeyKey::named(name))
}
pub(crate) fn shortcut_rules() -> crate::control::settings::ShortcutRules {
crate::control::settings::ShortcutRules {
key_named: |name| hotkey_key_named(name).map_err(str::to_owned),
rule: |slot, binding| shortcut_rule(slot, binding).map_err(str::to_owned),
}
}
pub(crate) fn meeting_provider_chosen(
ui: &mut Ui,
choice: crate::app_settings::MeetingProviderChoice,
) {
crate::app_settings::set_meeting_provider(&mut ui.settings.meetings, choice);
}
pub(crate) fn meeting_corner_moved(
ui: &mut Ui,
corner: crate::app_settings::MeetingIndicatorCorner,
) {
ui.settings.meetings.recording_indicator_corner = corner;
}
#[cfg(target_os = "macos")]
pub(crate) fn show_error(ui: &mut Ui, error: String) {
ui.error = Some(error);
}
#[cfg(target_os = "macos")]
pub(crate) fn update_check_asked(ui: &mut Ui) {
ui.update_asked = true;
}
pub(crate) fn meeting_failed(ui: &mut Ui, error: String) {
ui.meetings.start_pending = false;
ui.meetings.stop_pending = false;
ui.meetings.runtime_error = Some(error);
}
#[cfg(target_os = "linux")]
pub(crate) fn render() {
let with_playground =
std::env::args().any(|arg| arg == "--playground") && playground::enabled();
if !with_playground {
rune::run_with(spec(), WindowCompositor::default());
return;
}
let app = rune::App::new(playground::spec(spec())).compositor(WindowCompositor::default());
playground::attach(app).run();
}
const WINDOW_SIZE: (f32, f32) = (980.0, 760.0);
#[cfg(target_os = "macos")]
pub(crate) fn fixed_window() -> rune::WindowOptions {
if playground::enabled() {
return rune::WindowOptions::default();
}
rune::WindowOptions {
min_size: Some(WINDOW_SIZE),
max_size: Some(WINDOW_SIZE),
resizable: false,
..Default::default()
}
}
fn spec() -> AppSpec<Ui> {
AppSpec {
title: "Hex",
size: WINDOW_SIZE,
min_size: Some((860.0, 560.0)),
init: Ui::fixture_base,
view,
scenes: SCENES,
fonts: rune_theme_opencode::FONTS,
families: rune_theme_opencode::FAMILIES,
setup: Some(setup),
poll: Some(poll),
on_key: Some(on_key),
}
}
enum Service {
Pending(host::Connect),
Live(Box<Backend>),
Fixture(Vec<Request>),
}
struct HostState {
view: ServiceView,
settings: AppSettings,
}
#[derive(Clone, PartialEq)]
struct ServiceView {
listener: DesktopListenerSnapshot,
operation_error: Option<String>,
transcription: DesktopTranscriptionSnapshot,
update_status: DesktopUpdateStatus,
update_notes: Option<crate::desktop::host::UpdateNotes>,
update_blocked: Option<&'static str>,
editing: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Menu {
Microphone,
InputSourceModel(usize),
InputSourceLanguage(usize),
HistoryRetention,
HistoryAgain,
HistoryAgainLanguage,
Variant,
MeetingActions,
Calendars,
Transformations,
#[cfg(hex_capture)]
CaptureRetention,
Pick(&'static str),
Call(usize),
}
pub(crate) struct Ui {
settings: AppSettings,
service: Service,
window: Option<rune::AppHandle<Ui>>,
tick_at: f64,
view: Option<ServiceView>,
error: Option<String>,
connection_error: Option<String>,
service_settings: Option<AppSettings>,
pane: Pane,
open: Option<Menu>,
microphones: Result<crate::audio::InputDevices, String>,
microphones_route: Option<u64>,
microphones_listed_at: f64,
#[cfg_attr(target_os = "macos", allow(dead_code))]
input_sources_listed_at: f64,
microphones_listing:
Option<std::sync::mpsc::Receiver<Result<crate::audio::InputDevices, String>>>,
mic_level: general::MicLevel,
general_search: TextEdit,
advanced: Vec<Pane>,
labs_open: bool,
lab_store: labs::Store,
build_preview: Option<BuildKind>,
capture: Capture,
vocabulary: vocabulary::VocabularyEditor,
preparing: Option<(TranscriptionModelId, Instant)>,
draft_since: Option<Instant>,
duck_volume_drag: Option<f32>,
models: models::Models,
history: history::HistoryPane,
modes: modes::ModesPane,
output: output::OutputPane,
picker: picker::Picker,
found: found::FoundState,
explainer: explainer::Explainer,
sound_sets: sound_sets::SoundSets,
tour: Option<onboarding::Tour>,
first_run: Option<onboarding_kit::Onboard>,
platform: platform::PlatformState,
host: platform::PlatformHost,
meetings: meetings::MeetingsPane,
fun: fun::FunPane,
activity: activity::ActivityPane,
hud_copied: bool,
hud_saved: bool,
saved: usize,
quit_requested: bool,
update_asked: bool,
}
#[derive(Clone, Copy, Debug)]
struct Drafts {
modes: bool,
vocabulary: bool,
}
impl Ui {
fn fixture_base() -> Self {
Self::with(AppSettings::default(), Service::Fixture(Vec::new()))
}
fn load_with(connect: host::Connect) -> Self {
let (settings, error) = match AppSettings::load() {
Ok(settings) => (settings, None),
Err(error) => (AppSettings::default(), Some(format!("{error:#}"))),
};
crate::desktop::i18n::apply(settings.ui_language.as_deref());
let store = host::history(&connect, settings.history_retention);
#[cfg(target_os = "linux")]
let paste = connect.is_some().then(paste_access::detect).flatten();
#[cfg(target_os = "linux")]
let clipboard_unrestorable =
connect.is_some() && crate::linux::clipboard::wayland_without_data_control();
let mut ui = Self::with(settings, Service::Pending(connect));
ui.error = error;
#[cfg(target_os = "linux")]
{
ui.platform.paste = paste;
ui.platform.clipboard_unrestorable = clipboard_unrestorable;
}
if ui.settings.mouse_shortcut_notice {
ui.settings.mouse_shortcut_notice = false;
if let Err(error) = ui.settings.save() {
tracing::warn!(%error, "could not clear the mouse shortcut note");
}
ui.error.get_or_insert_with(|| {
tr("Mouse shortcuts are in Labs now, so your mouse-button shortcut went back to the default. Turn on Labs › Mouse shortcuts to use one again.").to_owned()
});
}
ui.history = history::HistoryPane::shared(store);
ui.output = output::OutputPane::shuffled();
ui.modes.load_personal_commands();
ui.platform.developer =
crate::dev_build::is_dev_build() && crate::DEVELOPER_FEATURES_ENABLED;
ui.platform.hud.tuning = crate::rune_hud::dot_scope::lab();
ui.tour = onboarding::Tour::start(&ui.settings, &ui.platform);
onboarding_kit::live::open(&mut ui);
ui.platform.set_login_item(host::launch_at_login());
ui
}
fn with(settings: AppSettings, service: Service) -> Self {
let modes = modes::ModesPane::new(&settings);
let vocabulary = vocabulary::VocabularyEditor::new(&settings.vocabulary);
Self {
settings,
service,
view: None,
error: None,
connection_error: None,
service_settings: None,
pane: Pane::General,
open: None,
microphones: Ok(Default::default()),
microphones_route: None,
microphones_listed_at: 0.0,
input_sources_listed_at: f64::NEG_INFINITY,
microphones_listing: None,
mic_level: general::MicLevel::default(),
general_search: TextEdit::default(),
advanced: Vec::new(),
labs_open: false,
lab_store: labs::Store::default(),
build_preview: None,
capture: Capture::Idle,
vocabulary,
preparing: None,
draft_since: None,
duck_volume_drag: None,
models: models::Models::default(),
history: history::HistoryPane::default(),
modes,
output: output::OutputPane::default(),
picker: picker::Picker::default(),
found: found::FoundState::default(),
explainer: explainer::Explainer::default(),
sound_sets: sound_sets::SoundSets::default(),
tour: None,
first_run: None,
platform: platform::PlatformState::new(platform::Capabilities::current()),
host: platform::PlatformHost::Local(Default::default()),
meetings: meetings::MeetingsPane::default(),
fun: fun::FunPane::default(),
activity: activity::ActivityPane::default(),
hud_copied: false,
hud_saved: false,
saved: 0,
quit_requested: false,
update_asked: false,
window: None,
tick_at: 0.0,
}
}
fn live(&self) -> bool {
matches!(self.service, Service::Live(_))
}
fn is_fixture(&self) -> bool {
matches!(self.service, Service::Fixture(_))
}
fn send(&mut self, request: Request) {
match &mut self.service {
Service::Live(client) => {
if let Err(error) = client.send(request) {
self.error = Some(format!("{error:#}"));
}
}
Service::Fixture(sent) => sent.push(request),
Service::Pending(_) => {}
}
}
fn update(&mut self, change: impl FnOnce(&mut AppSettings)) -> bool {
let mut candidate = self.drafted();
change(&mut candidate);
self.commit(candidate)
}
fn commit(&mut self, candidate: AppSettings) -> bool {
match &mut self.service {
Service::Live(backend) => {
if let Err(error) = backend.save(&candidate) {
self.error = Some(format!("{error:#}"));
return false;
}
}
Service::Fixture(_) => self.saved += 1,
Service::Pending(_) => {}
}
self.error = None;
self.settings = candidate;
self.draft_since = None;
self.settings_changed();
true
}
fn settings_changed(&mut self) {
if !meetings::available(self) {
self.meetings.view = meetings::View::Recordings;
}
}
fn drafted(&self) -> AppSettings {
let mut candidate = self.settings.clone();
self.modes.project(&mut candidate);
candidate.vocabulary = self.vocabulary.project();
candidate
}
fn drafts(&self) -> Drafts {
let settings = &self.settings;
let mut modes = settings.clone();
self.modes.project(&mut modes);
Drafts {
modes: modes.dictation_processing != settings.dictation_processing,
vocabulary: self.vocabulary.project() != settings.vocabulary,
}
}
fn save_drafts(&mut self, now: Instant) -> bool {
let candidate = self.drafted();
if candidate == self.settings {
self.draft_since = None;
return false;
}
let since = *self.draft_since.get_or_insert(now);
if now.duration_since(since) < DRAFT_DELAY {
return false;
}
self.commit(candidate)
}
fn flush_drafts(&mut self) {
let candidate = self.drafted();
if candidate != self.settings {
self.commit(candidate);
}
}
fn ask(&mut self, request: platform::PlatformRequest) {
match &request {
platform::PlatformRequest::Permission(
crate::desktop::onboarding::PermissionAction::RequestMicrophone,
) => self
.platform
.note_asked(crate::desktop::onboarding::PermissionKind::Microphone),
platform::PlatformRequest::Permission(
crate::desktop::onboarding::PermissionAction::ShowGuide(kind),
) => {
#[cfg(hex_capture)]
if *kind == crate::desktop::onboarding::PermissionKind::ScreenRecording
&& self.host.is_fixture()
&& self.platform.asked(*kind)
&& let Some(view) = self.platform.scrawl.as_mut()
{
view.screen_recording = true;
}
self.platform.clear_stale(*kind);
self.platform.note_asked(*kind);
}
platform::PlatformRequest::Permission(
crate::desktop::onboarding::PermissionAction::ResetAndPrompt(kind),
) => {
self.platform.clear_stale(*kind);
self.platform.note_asked(*kind);
}
platform::PlatformRequest::SystemAudio => {
let kind = crate::desktop::onboarding::PermissionKind::SystemAudio;
if self.host.is_fixture() && self.platform.asked(kind) {
self.platform.system_audio = true;
}
self.platform.clear_stale(kind);
self.platform.note_asked(kind);
}
#[cfg(hex_capture)]
platform::PlatformRequest::Scrawl(platform::ScrawlRequest::QuitScrawl) => {
let fixture = self.host.is_fixture();
if let Some(view) = self.platform.scrawl.as_mut() {
view.quitting_scrawl = true;
if fixture {
view.scrawl_running = false;
}
}
}
platform::PlatformRequest::SetLaunchAtLogin(enabled) => match &self.host {
platform::PlatformHost::Local(_) => {
let result = host::set_launch_at_login(*enabled);
self.platform.set_login_item(Some(result));
return;
}
platform::PlatformHost::Fixture(_) => {
self.platform.set_login_item(Some(Ok(*enabled)));
}
platform::PlatformHost::Native(_) => {
self.platform.request_login_item(*enabled);
if let Err(message) = self.host.send(request) {
let failure = crate::desktop::login_item::LoginItemFailure {
status: None,
message,
};
self.platform.login_item_answered(Err(failure), true);
}
return;
}
},
_ => {}
}
#[cfg(not(target_os = "macos"))]
if let platform::PlatformRequest::Meeting(meeting) = &request
&& matches!(self.host, platform::PlatformHost::Local(_))
{
if let Err(error) = self.send_meeting(meeting.clone()) {
self.error = Some(error);
}
return;
}
let is_relaunch = request == platform::PlatformRequest::Relaunch;
match self.host.send(request) {
Ok(()) => {
self.host.poll(&mut self.platform);
}
Err(_) if is_relaunch => self.platform.relaunch_failed = true,
Err(error) => self.error = Some(error),
}
}
fn send_meeting(&mut self, request: platform::MeetingRequest) -> Result<(), String> {
#[cfg(not(target_os = "macos"))]
if matches!(self.host, platform::PlatformHost::Local(_)) {
let waker = self.modes.waker.clone();
let wake = move || {
if let Some(waker) = &waker {
waker();
}
};
return self.meetings.local.send(request, wake);
}
self.host.send(platform::PlatformRequest::Meeting(request))
}
fn running(&self) -> bool {
self.view.as_ref().is_some_and(|view| view.listener.running)
}
fn shortcuts(&self) -> Option<&DesktopShortcuts> {
self.view.as_ref().map(|view| &view.listener.shortcuts)
}
fn records_shortcuts(&self) -> bool {
self.shortcuts() != Some(&DesktopShortcuts::Commands)
}
fn adopt(&mut self, service: AppSettings) -> bool {
if self.service_settings.as_ref() == Some(&service) {
return false;
}
let changed = service != self.settings;
if changed {
let drafts = self.drafts();
if !drafts.modes && service.dictation_processing != self.settings.dictation_processing {
self.modes.reload(&service);
}
if !drafts.vocabulary && service.vocabulary != self.settings.vocabulary {
self.vocabulary.reload(&service.vocabulary);
}
self.settings = service.clone();
self.settings_changed();
}
self.service_settings = Some(service);
changed
}
fn quit(&mut self) {
self.flush_drafts();
settings::cancel_capture(self);
match &mut self.service {
Service::Live(backend) => backend.quit(),
Service::Fixture(_) => self.quit_requested = true,
Service::Pending(_) => {}
}
}
fn select_pane(&mut self, pane: Pane, tx: &mut Tx) {
let labs = std::mem::take(&mut self.labs_open);
if self.pane == pane && !labs {
return;
}
tx.settle();
self.flush_drafts();
settings::cancel_capture(self);
self.open = None;
self.general_search.clear();
models::close(self);
picker::close(self);
explainer::close(self);
sound_sets::close(self);
vocabulary::close(self);
self.output.prompt_copied = false;
if self.pane == Pane::HudLab {
self.ask(platform::PlatformRequest::EndHudDemo);
}
self.pane = pane;
match pane {
Pane::History => self.history.reload(),
Pane::HudLab => self.ask(platform::PlatformRequest::ConfigureHud(self.platform.hud)),
#[cfg(hex_capture)]
Pane::Annotate => self.ask(platform::PlatformRequest::Scrawl(
platform::ScrawlRequest::Refresh,
)),
_ => {}
}
}
}
fn on_key(ui: &mut Ui, event: &rune::KeyEvent, tx: &mut Tx) -> bool {
if settings::on_key(ui, event, tx) {
return true;
}
if event.key == rune::Key::Command('d') && event.mods.shift && playground::enabled() {
tx.toggle_playground();
return true;
}
#[cfg(target_os = "linux")]
if event.key == rune::Key::Command('q') && !event.mods.shift && !event.mods.alt {
ui.quit();
return true;
}
if general::focus_search_key(ui, event) {
return true;
}
if let rune::Key::Command(digit @ '1'..='9') = event.key
&& !event.mods.alt
&& !ui.picker.open
&& !ui.explainer.open
&& !ui.sound_sets.open
&& !ui.vocabulary.open
&& ui.tour.is_none()
{
let index = digit as usize - '1' as usize;
let pane = labs::order(ui).get(index).copied();
if let Some(pane) = pane {
ui.select_pane(pane, tx);
}
return pane.is_some();
}
if event.key != rune::Key::Escape {
return false;
}
if ui.explainer.open {
explainer::close(ui);
true
} else if ui.sound_sets.open {
sound_sets::close(ui);
true
} else if ui.vocabulary.open {
vocabulary::close(ui);
true
} else if ui.picker.open {
picker::close(ui);
true
} else if ui.open.is_some() {
ui.open = None;
true
} else if ui.meetings.renaming.is_some() || ui.meetings.delete_armed.is_some() {
ui.meetings.renaming = None;
ui.meetings.delete_armed = None;
true
} else {
false
}
}
#[cfg(not(target_os = "macos"))]
fn local_meetings(ui: &mut Ui) -> bool {
let mut changed = false;
if ui.pane == Pane::Meetings
&& meetings::available(ui)
&& let Some(listed) = ui.meetings.local.meetings()
{
ui.meetings.adopt(listed);
changed = true;
}
match ui.meetings.local.chosen_folder() {
Some(Ok(folder)) => {
ui.platform.meeting_folder_error = None;
ui.update(|settings| settings.meetings.output_folder = Some(folder));
changed = true;
}
Some(Err(error)) => {
ui.platform.meeting_folder_error = Some(error);
changed = true;
}
None => {}
}
changed
}
fn setup(ui: &mut Ui, waker: Waker) {
ui.modes.waker = Some(waker.clone());
ui.modes.icon_cache = Some(rune::ImageCache::new(waker.clone()));
if let Service::Pending(connect) = &mut ui.service {
let connect = std::mem::take(connect);
#[cfg(target_os = "macos")]
{
let host_wake: Arc<dyn Fn() + Send + Sync> = {
let waker = waker.clone();
Arc::new(move || waker())
};
ui.host = platform::PlatformHost::Native(host::native_host(host_wake));
}
let wake: Arc<dyn Fn() + Send + Sync> = Arc::new(move || waker());
ui.service = Service::Live(Box::new(Backend::connect(connect, wake)));
}
}
fn poll(ui: &mut Ui, clock: f64, tx: &mut Tx) -> bool {
if clock >= ui.tick_at && ui.window.as_ref().is_some_and(rune::AppHandle::is_visible) {
ui.tick_at = clock + WINDOW_TICK.as_secs_f64();
tx.wake_after(WINDOW_TICK);
}
let now = Instant::now();
let mut changed = ui.save_drafts(now);
changed |= paste_access::poll(ui);
changed |= ui.models.poll(
ui.live()
&& (ui.pane == Pane::Modes || platform_settings::summary_model_shown(ui))
&& ui.window.as_ref().is_some_and(rune::AppHandle::is_visible),
);
changed |= ui.history.poll(ui.pane == Pane::History);
if ui.pane == Pane::Output && !ui.labs_open {
changed |= output::poll(ui, clock, tx);
}
if matches!(ui.pane, Pane::Modes | Pane::Output)
&& ui
.settings
.dictation_processing
.modes
.iter()
.any(|mode| !mode.applications.is_empty())
{
ui.modes.ensure_applications();
}
changed |= ui.modes.poll(ui.live());
changed |= modes::poll_chooser(ui);
changed |= model_import::poll(ui);
changed |= meetings::poll_import(ui);
changed |= found::poll(ui);
let login_unknown = ui.platform.login_item.is_none();
changed |= settings::poll_microphones(ui);
if ui.pane == Pane::General
&& ui.live()
&& ui.window.as_ref().is_some_and(rune::AppHandle::is_visible)
&& (ui.microphones_route != Some(crate::audio::input_route_generation())
|| (cfg!(target_os = "windows") && clock >= ui.microphones_listed_at + 3.0))
{
settings::refresh_microphones(ui);
ui.microphones_listed_at = clock;
}
platform_settings::update_shown_permissions(ui);
changed |= ui.host.poll(&mut ui.platform);
#[cfg(not(target_os = "macos"))]
{
changed |= settings::poll_input_sources(ui, clock);
}
if login_unknown && ui.platform.login_item.is_some() {
tx.instant();
}
if ui.pane == Pane::Meetings
&& meetings::available(ui)
&& let Some(listed) = ui.host.meetings()
{
ui.meetings.adopt(listed);
changed = true;
}
#[cfg(not(target_os = "macos"))]
if matches!(ui.host, platform::PlatformHost::Local(_)) {
changed |= local_meetings(ui);
}
changed |= ui
.activity
.poll(ui.platform.activity_log.as_ref(), ui.pane == Pane::Activity);
if let Service::Live(backend) = &mut ui.service {
backend.refresh();
let error = backend.error();
let (view, settings) = match backend.state() {
Some(state) => (Some(state.view), Some(state.settings)),
None => (None, None),
};
if error != ui.connection_error {
ui.connection_error = error;
changed = true;
}
if view != ui.view {
ui.view = view;
changed = true;
}
if let Some(settings) = settings {
changed |= ui.adopt(settings);
if !ui.pane.shown(ui) {
ui.pane = Pane::General;
ui.open = None;
changed = true;
}
}
}
let preparing = ui
.view
.as_ref()
.and_then(|view| view.transcription.preparing);
if preparing != ui.preparing.map(|(model, _)| model) {
ui.preparing = preparing.map(|model| (model, now));
changed = true;
}
changed |= onboarding::poll(ui);
changed |= onboarding_kit::live::poll(ui, tx);
changed |= general::poll_level(ui, clock, tx);
let visible = ui.window.as_ref().is_none_or(rune::AppHandle::is_visible);
changed |= explainer::poll(ui, clock, visible, tx);
changed |= sound_sets::poll(ui, clock, visible, tx);
changed | settings::advance_capture(ui, clock, tx)
}
fn view(ui: &Ui, cx: &mut Cx<Ui>) -> El {
let p = Paint::new(cx.system_dark(), cx.env().scale);
#[cfg(target_os = "windows")]
crate::windows::rune::match_title_bar(p.k.c.bg_deep);
let mut layer = Vec::new();
let (actions, body) = match ui.pane {
_ if ui.labs_open => (vec![labs::tabs(ui, p)], labs::page(ui, p)),
Pane::General => (Vec::new(), general::pane(ui, p, &mut layer)),
Pane::Output => (Vec::new(), output::pane(ui, p, cx.is_reduced_motion())),
Pane::Modes => modes::pane(ui, p, &mut layer),
Pane::History => history::pane(ui, p, &mut layer),
Pane::Meetings => meetings::pane(ui, p, &mut layer),
#[cfg(hex_capture)]
Pane::Annotate => (Vec::new(), scrawl::pane(ui, p, &mut layer)),
Pane::VoiceFilter => (Vec::new(), fun::pane(ui, p)),
Pane::HudLab => hud_lab::pane(ui, p),
Pane::Activity => activity::pane(ui, p),
};
let panel = col()
.key("panel")
.role("main")
.grow(1.0)
.shrink()
.mt(8.0)
.mr(8.0)
.mb(8.0)
.radius(p.k.radius.lg)
.fill(p.k.c.bg_base)
.border(p.hair, p.k.c.border_weak)
.padding(p.hair)
.clip_soft(0.0)
.child(pane_header(
p,
if ui.labs_open {
tr("Labs")
} else {
ui.pane.title()
},
if ui.labs_open {
Some(labs::search_field(ui, p))
} else {
(ui.pane == Pane::Meetings)
.then(|| meetings::view_switch(ui, p))
.flatten()
},
actions,
))
.child(body);
let window = row()
.key("window")
.role("none")
.fill(p.k.c.bg_deep)
.hover_motion_within(Anim::INSTANT, Anim::INSTANT)
.child(sidebar(ui, p))
.child(panel);
if let Some(modal) = picker::modal(ui, p) {
layer.extend(modal);
}
if let Some(modal) = explainer::modal(ui, cx, p) {
layer.extend(modal);
}
if let Some(modal) = sound_sets::modal(ui, p) {
layer.extend(modal);
}
if let Some(modal) = vocabulary::modal(ui, p) {
layer.extend(modal);
}
if let Some(first_run) = onboarding_kit::live::overlay(ui, p, cx.is_reduced_motion()) {
layer.push(first_run);
}
rune_kit::root(window, layer).on_window_focus(window_focused)
}
fn window_focused(ui: &mut Ui, focused: bool, tx: &mut Tx) {
explainer::window_focused(ui, focused, tx);
if focused {
ui.send(Request::Desktop(DesktopAction::WindowFocused));
}
#[cfg(hex_capture)]
if focused && ui.pane == Pane::Annotate && ui.platform.scrawl.is_some() {
ui.ask(platform::PlatformRequest::Scrawl(
platform::ScrawlRequest::Refresh,
));
}
}
const SCROLL_FADE: (f32, f32) = (4.0, 24.0);
fn scroll_body(k: &rune_kit::Kit, key: &str, content: El) -> El {
faded_body(k, key, content, SCROLL_FADE)
}
fn faded_body(k: &rune_kit::Kit, key: &str, content: El, fades: (f32, f32)) -> El {
list_detail::faded_body_sticky(k, key, content, fades, false)
}
#[cfg(all(test, target_os = "linux"))]
mod tests;
#[cfg(any(test, hex_capture))]
mod offscreen;
#[cfg(test)]
mod accessibility_tests;
#[cfg(test)]
mod snap_isolation;
#[cfg(test)]
mod keyboard_model_tests;
#[cfg(test)]
mod dmg_keycap;