mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
`--language CODE` sets the interface language for one run of --active, --freeze, --daemon or --about without changing config.toml. It accepts the spellings `[ui] language` does and takes precedence over both WAYSCRIBER_LANGUAGE and the config. A daemon started with it gives it to the About windows it opens, and to its overlays as an optional environment input that an older overlay ignores. The process broker strips that input from every other helper, so a configurator opened from an overlay keeps previewing its own Language row. With --daemon-toggle or an overlay action the flag is an error, since a running daemon's overlays cannot be changed from outside. `--list-languages` prints each language's code and name, the configured language, and the language in effect with the setting that decided it. Languages now carry their native and English names, and the configurator's Language row takes its labels from them. The help text, README, CONFIG.md, I18N.md and config.example.toml describe both options.
325 lines
9.7 KiB
Rust
325 lines
9.7 KiB
Rust
use anyhow::anyhow;
|
|
use log::{info, warn};
|
|
use std::collections::HashSet;
|
|
use std::env;
|
|
use std::ffi::OsString;
|
|
|
|
use crate::env_vars::PATH_ENV;
|
|
|
|
use super::launch::OverlayLaunchRequest;
|
|
use super::lifecycle::OverlayStartFailure;
|
|
|
|
use super::super::core::Daemon;
|
|
use super::super::types::OverlaySpawnCandidate;
|
|
#[cfg(feature = "tray")]
|
|
use super::super::types::OverlaySpawnErrorInfo;
|
|
|
|
impl Daemon {
|
|
pub(super) fn record_overlay_spawn_failure(&mut self, message: String) {
|
|
let backoff = self.overlay.record_spawn_failure();
|
|
warn!(
|
|
"Failed to spawn overlay process: {} (retry in {}s)",
|
|
message,
|
|
backoff.as_secs().max(1)
|
|
);
|
|
#[cfg(feature = "tray")]
|
|
self.tray_status
|
|
.set_overlay_error(Some(OverlaySpawnErrorInfo {
|
|
message,
|
|
next_retry_at: self.overlay.next_spawn_retry(),
|
|
}));
|
|
}
|
|
|
|
pub(super) fn clear_overlay_spawn_error(&mut self) {
|
|
self.overlay.clear_spawn_backoff();
|
|
#[cfg(feature = "tray")]
|
|
self.tray_status.set_overlay_error(None);
|
|
}
|
|
|
|
pub(super) fn overlay_spawn_candidates(&self) -> Vec<OverlaySpawnCandidate> {
|
|
let mut candidates = Vec::new();
|
|
let mut seen = HashSet::<OsString>::new();
|
|
|
|
if let Ok(exe) = env::current_exe() {
|
|
if exe.is_file() {
|
|
Self::push_spawn_candidate(&mut candidates, &mut seen, exe.into(), "current_exe");
|
|
} else {
|
|
warn!(
|
|
"Current executable path {} is not a file; falling back",
|
|
exe.display()
|
|
);
|
|
}
|
|
} else {
|
|
warn!("Failed to resolve current executable; falling back to argv0/{PATH_ENV}");
|
|
}
|
|
|
|
if let Some(arg0) = env::args_os().next() {
|
|
let arg0_path = std::path::Path::new(&arg0);
|
|
if arg0_path.to_string_lossy().contains('/') {
|
|
if arg0_path.is_file() {
|
|
Self::push_spawn_candidate(&mut candidates, &mut seen, arg0, "argv0");
|
|
} else {
|
|
warn!(
|
|
"argv0 path {} is not a file; falling back",
|
|
arg0_path.display()
|
|
);
|
|
}
|
|
} else {
|
|
Self::push_spawn_candidate(&mut candidates, &mut seen, arg0, "argv0");
|
|
}
|
|
}
|
|
|
|
Self::push_spawn_candidate(
|
|
&mut candidates,
|
|
&mut seen,
|
|
OsString::from("wayscriber"),
|
|
PATH_ENV,
|
|
);
|
|
|
|
candidates
|
|
}
|
|
|
|
fn push_spawn_candidate(
|
|
candidates: &mut Vec<OverlaySpawnCandidate>,
|
|
seen: &mut HashSet<OsString>,
|
|
program: OsString,
|
|
source: &'static str,
|
|
) {
|
|
if seen.insert(program.clone()) {
|
|
candidates.push(OverlaySpawnCandidate { program, source });
|
|
}
|
|
}
|
|
|
|
pub(super) fn spawn_overlay_process(
|
|
&mut self,
|
|
request: &OverlayLaunchRequest,
|
|
candidates: &[OverlaySpawnCandidate],
|
|
) -> std::result::Result<(), OverlayStartFailure> {
|
|
if candidates.is_empty() {
|
|
return Err(OverlayStartFailure::BeforeAttempt(anyhow!(
|
|
"No overlay spawn candidates available"
|
|
)));
|
|
}
|
|
|
|
let mut failures = Vec::new();
|
|
|
|
for candidate in candidates {
|
|
match self.overlay.start(
|
|
request,
|
|
candidate,
|
|
&self.session_resume_override,
|
|
self.remembered_session_file.as_deref(),
|
|
&self.instance_token,
|
|
) {
|
|
Ok(pid) => {
|
|
info!(
|
|
"Overlay process started via {} (pid {pid}, startup_activation_token={})",
|
|
candidate.source,
|
|
request.activation_token().is_some()
|
|
);
|
|
return Ok(());
|
|
}
|
|
Err(OverlayStartFailure::BeforeAttempt(error)) => {
|
|
return Err(OverlayStartFailure::BeforeAttempt(error));
|
|
}
|
|
Err(OverlayStartFailure::Attempt(error)) => {
|
|
failures.push(format!(
|
|
"{} ({}) -> {error:#}",
|
|
candidate.source,
|
|
candidate.program.to_string_lossy()
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
self.overlay.abort_start();
|
|
let failed = SpawnAttemptsFailed { attempts: failures };
|
|
warn!(
|
|
"Overlay spawn attempts failed: {}",
|
|
failed.attempts.join("; ")
|
|
);
|
|
|
|
Err(OverlayStartFailure::Attempt(anyhow::Error::new(failed)))
|
|
}
|
|
}
|
|
|
|
/// Every spawn candidate failed. It displays only the summary that callers
|
|
/// report, as `{:#}` too, since it has no source; the per-candidate failures go
|
|
/// to the log and stay on the error for inspection.
|
|
#[derive(Debug)]
|
|
pub(super) struct SpawnAttemptsFailed {
|
|
attempts: Vec<String>,
|
|
}
|
|
|
|
impl SpawnAttemptsFailed {
|
|
#[cfg(test)]
|
|
pub(super) fn attempts(&self) -> &[String] {
|
|
&self.attempts
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for SpawnAttemptsFailed {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
formatter,
|
|
"Unable to launch overlay process (tried current_exe/argv0/{PATH_ENV})"
|
|
)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for SpawnAttemptsFailed {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::super::launch::{OverlayLaunch, build_overlay_launch};
|
|
use super::*;
|
|
|
|
fn build_test_launch(daemon: &mut Daemon) -> OverlayLaunch {
|
|
let request = daemon.take_pending_launch();
|
|
build_overlay_launch(&request, daemon.session_resume_override(), None, None, None)
|
|
}
|
|
|
|
fn launch_args(launch: &OverlayLaunch) -> Vec<String> {
|
|
launch
|
|
.arguments
|
|
.iter()
|
|
.map(|arg| arg.to_string_lossy().into_owned())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn build_overlay_command_includes_freeze_when_enabled() {
|
|
let mut daemon = Daemon::new(Some("whiteboard".into()), false, None, None);
|
|
daemon.set_freeze_on_show(true);
|
|
|
|
let launch = build_test_launch(&mut daemon);
|
|
|
|
assert_eq!(
|
|
launch_args(&launch),
|
|
vec!["--active", "--freeze", "--mode", "whiteboard"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn build_overlay_command_uses_toggle_request_args() {
|
|
for (exit_after_capture, no_exit_after_capture, expected_flag) in [
|
|
(true, false, "--exit-after-capture"),
|
|
(false, true, "--no-exit-after-capture"),
|
|
] {
|
|
let mut daemon = Daemon::new(Some("whiteboard".into()), false, None, None);
|
|
daemon.queue_overlay_launch(
|
|
Some(crate::daemon::DaemonToggleRequest {
|
|
mode: Some("transparent".into()),
|
|
freeze: true,
|
|
exit_after_capture,
|
|
no_exit_after_capture,
|
|
..Default::default()
|
|
}),
|
|
None,
|
|
);
|
|
|
|
let launch = build_test_launch(&mut daemon);
|
|
|
|
assert_eq!(
|
|
launch_args(&launch),
|
|
vec![
|
|
"--active",
|
|
"--freeze",
|
|
expected_flag,
|
|
"--mode",
|
|
"transparent"
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn build_overlay_command_includes_initial_named_session_file() {
|
|
let mut daemon = Daemon::new(
|
|
Some("whiteboard".into()),
|
|
false,
|
|
None,
|
|
Some(std::path::PathBuf::from("/tmp/lecture.wayscriber-session")),
|
|
);
|
|
|
|
let launch = build_test_launch(&mut daemon);
|
|
|
|
assert_eq!(
|
|
launch_args(&launch),
|
|
vec![
|
|
"--active",
|
|
"--mode",
|
|
"whiteboard",
|
|
"--session-file",
|
|
"/tmp/lecture.wayscriber-session"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn build_overlay_command_request_session_file_overrides_initial_named_session_file() {
|
|
let mut daemon = Daemon::new(
|
|
Some("whiteboard".into()),
|
|
false,
|
|
None,
|
|
Some(std::path::PathBuf::from("/tmp/default.wayscriber-session")),
|
|
);
|
|
daemon.queue_overlay_launch(
|
|
Some(crate::daemon::DaemonToggleRequest {
|
|
session_file: Some(std::path::PathBuf::from(
|
|
"/tmp/requested.wayscriber-session",
|
|
)),
|
|
..Default::default()
|
|
}),
|
|
None,
|
|
);
|
|
|
|
let launch = build_test_launch(&mut daemon);
|
|
|
|
assert_eq!(
|
|
launch_args(&launch),
|
|
vec![
|
|
"--active",
|
|
"--mode",
|
|
"whiteboard",
|
|
"--session-file",
|
|
"/tmp/requested.wayscriber-session"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn build_overlay_command_omits_freeze_by_default() {
|
|
let mut daemon = Daemon::new(Some("whiteboard".into()), false, None, None);
|
|
|
|
let launch = build_test_launch(&mut daemon);
|
|
|
|
assert_eq!(
|
|
launch_args(&launch),
|
|
vec!["--active", "--mode", "whiteboard"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn push_spawn_candidate_deduplicates_programs() {
|
|
let mut candidates = Vec::new();
|
|
let mut seen = HashSet::<OsString>::new();
|
|
|
|
Daemon::push_spawn_candidate(
|
|
&mut candidates,
|
|
&mut seen,
|
|
OsString::from("wayscriber"),
|
|
super::PATH_ENV,
|
|
);
|
|
Daemon::push_spawn_candidate(
|
|
&mut candidates,
|
|
&mut seen,
|
|
OsString::from("wayscriber"),
|
|
"argv0",
|
|
);
|
|
|
|
assert_eq!(candidates.len(), 1);
|
|
assert_eq!(candidates[0].source, super::PATH_ENV);
|
|
}
|
|
}
|