pub(crate) mod app;
pub(crate) mod atspi;
pub(crate) mod audio_control;
pub(crate) mod background;
pub(crate) mod clipboard;
pub(crate) mod context;
pub(crate) mod dictation;
pub(crate) mod gnome_shortcuts;
pub(crate) mod gsettings;
pub(crate) mod icons;
#[path = "linux/input.rs"]
pub(crate) mod linux_input;
pub(crate) use linux_input as input;
pub(crate) mod input_route;
pub(crate) mod login_item;
pub(crate) mod meeting_capsules;
pub(crate) mod paste;
pub(crate) mod portal;
pub(crate) mod remote_desktop;
pub(crate) mod rune_hud;
pub(crate) mod service;
pub(crate) mod session;
pub(crate) mod tray;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use clap::{Parser, Subcommand};
use color_eyre::Result;
use crate::control::cli::ControlCli;
use crate::linux::service::{Activation, Launch};
#[derive(Parser)]
#[command(
name = "hex",
version = crate::VERSION,
about = "Local, observable voice recognition (Linux beta)",
long_about = "Local, observable voice recognition (Linux beta).\n\n\
Control commands (status, dictation, settings, model, window, …) drive the one running \
Hex over its owner-only socket and never launch it, except `start` and `restart`. Every \
one takes --json for a versioned reply (docs/cli.md). Exit codes: 0 done or nothing to \
change, 1 refused or failed, 2 usage, 3 Hex isn't running.",
after_help = "Run `hex <command> --help` for details, or `hex settings list` for every setting."
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
#[command(flatten)]
Control(ControlCli),
App {
#[arg(long)]
hidden: bool,
#[arg(last = true)]
rune_args: Vec<String>,
},
Service {
#[arg(long, hide = true)]
embedded: bool,
},
#[command(hide = true)]
Stop,
Dictate {
#[arg(long)]
device: Option<String>,
},
Devices,
}
pub(crate) fn run(shutdown: &'static AtomicBool) -> Result<()> {
let activation = crate::linux::service::Activation::take_from_environment();
crate::linux::service::refresh_x_authority();
color_eyre::install()?;
let command = Cli::parse().command.unwrap_or(Command::App {
hidden: false,
rune_args: Vec::new(),
});
let command = match command {
Command::Control(command) => crate::control::cli::finish(crate::control::cli::run(command)),
Command::Stop => crate::control::cli::finish(crate::control::cli::send(
crate::control::ControlCommand::Quit { force: false },
Default::default(),
)),
command => command,
};
let adopted = matches!(command, Command::Service { embedded: false })
.then(crate::linux::service::adopt_wayland_display)
.flatten();
if let Command::App { rune_args, .. } = &command
&& !rune_args.is_empty()
{
crate::rune_ui::render();
return Ok(());
}
let log_dir = crate::app_paths::init_process_logging(shutdown)?;
if let Some(wayland_display) = adopted {
tracing::info!(%wayland_display, "adopted the session's Wayland display");
}
if matches!(
command,
Command::App { .. } | Command::Service { embedded: false }
) && crate::updater::legacy::take_over()
{
return Ok(());
}
let event_path = log_dir.join("live.ndjson");
match command {
Command::App { hidden, .. } => launch(&activation, !hidden, event_path, shutdown),
Command::Service { embedded: true } => {
crate::local_api::run_service(&log_dir, event_path, true, shutdown)
}
Command::Service { embedded: false } => {
match crate::linux::app::run(event_path, shutdown, None) {
Err(_) if crate::linux::service::another_instance_answers() => Ok(()),
result => result,
}
}
Command::Dictate { device } => {
let _instance = crate::desktop::instance::acquire("listener")?;
crate::labs::log_launch();
let settings = crate::app_settings::AppSettings::load()?;
crate::linux::dictation::initialize_feedback(&settings);
crate::linux::dictation::run(
&event_path,
device.as_deref(),
shutdown,
&settings,
crate::linux::dictation::DictationOutput::from_settings(
&settings,
crate::history::History::open_default(settings.history_retention).ok(),
),
None,
Default::default(),
std::sync::mpsc::channel().1.into(),
Default::default(),
std::sync::mpsc::channel().1,
)
}
Command::Devices => {
for device in crate::audio::input_device_names()? {
println!("{device}");
}
Ok(())
}
Command::Control(_) | Command::Stop => unreachable!("handled before logging"),
}
}
fn launch(
activation: &Activation,
show: bool,
event_path: PathBuf,
shutdown: &'static AtomicBool,
) -> Result<()> {
loop {
match crate::linux::service::launch(activation, show)? {
Launch::Forwarded => return Ok(()),
Launch::BecomeInstance => {}
}
match crate::linux::app::run(
event_path.clone(),
shutdown,
show.then(|| activation.clone()),
) {
Err(_) if crate::linux::service::another_instance_answers() => continue,
result => return result,
}
}
}
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser};
use super::{Cli, Command, ControlCli};
#[test]
fn the_shared_control_commands_parse_beside_linuxs_own() {
Cli::command().debug_assert();
let parse = |arguments: &[&str]| Cli::try_parse_from(arguments).map(|cli| cli.command);
for arguments in [
&["hex", "status", "--json"][..],
&["hex", "dictation", "stop", "--print"],
&["hex", "paste-last"],
&["hex", "settings", "set", "sound_effects", "off"],
&["hex", "model", "use", "parakeet_v3", "--language", "pl"],
&["hex", "model", "install"],
&["hex", "model", "status"],
&["hex", "window", "show", "history"],
&["hex", "vocabulary", "add", "Rune"],
&["hex", "history", "clear", "--yes"],
&["hex", "check-for-updates"],
&["hex", "launch-at-login", "on"],
&["hex", "logs", "-n", "5"],
&["hex", "meetings", "list"],
&["hex", "mcp"],
&["hex", "commands", "check"],
&["hex", "onboarding", "reset"],
&["hex", "annotate", "status"],
&["hex", "capture", "draw"],
&["hex", "labs"],
&["hex", "restart", "--force"],
&["hex", "start"],
] {
assert!(
matches!(parse(arguments), Ok(Some(Command::Control(_)))),
"{arguments:?}"
);
}
assert!(matches!(
parse(&["hex", "quit", "--force"]),
Ok(Some(Command::Control(ControlCli::Quit { force: true, .. })))
));
assert!(matches!(parse(&["hex", "stop"]), Ok(Some(Command::Stop))));
assert!(matches!(
parse(&["hex", "app", "--hidden"]),
Ok(Some(Command::App { hidden: true, .. }))
));
assert!(matches!(parse(&["hex"]), Ok(None)));
}
}