crates/veilvoice-gui/src/main.rs
what this file is for · veilvoice-gui · 231 lines · the same file on GitHub
The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.
// SPDX-License-Identifier: GPL-3.0-or-later
//! Entry point for the desktop application: open a window, hand it to
//! [`veilvoice_gui::VeilVoiceApp`], and get out of the way.
//!
//! Everything of substance is in the library beside this file. That split is
//! the point: a binary crate cannot be unit tested, so the whole user interface
//! lives in `lib.rs` and its modules where tests can reach it, and this file
//! holds only what genuinely needs a window to exist.
//!
//! Three decisions are made here and nowhere else.
//!
//! **No console window on Windows, in release only.** A release build sets
//! `windows_subsystem = "windows"`, so double-clicking the application does not
//! flash up a terminal behind it. A debug build deliberately keeps the console,
//! because that is where panics and `eprintln!` go and losing them while
//! developing costs far more than the flash of a window is worth.
//!
//! **The icon is raw RGBA, not a PNG.** `assets/generate.py` writes
//! `icon-32.rgba` beside the PNG it generates from the same pixels, so the
//! application can set its own title-bar icon without linking an image decoder.
//! A decoder is a parser, a parser is an attack surface, and this one would
//! exist solely to draw a 32x32 square. The length is checked before use, and a
//! mismatch means the window simply opens without an icon rather than panicking
//! at startup.
//!
//! **The window has a minimum size, and it is about width.** Every tab is
//! inside one scroll area, so anything taller than the window can be reached by
//! scrolling to it and a short window loses nothing. Width is different: the
//! layout is monospace and column-based, and below roughly 720 across, columns
//! start overlapping rather than reflowing. So the floor is enforced here
//! rather than left to produce an unreadable window on somebody else's machine.
//!
//! It opens at 1100 by 720, which is large enough to read without resizing and
//! still fits a 1366 by 768 laptop with its taskbar. Anything bigger opens
//! partly off the bottom of a common screen, which looks like a broken
//! application rather than a generous one.
//!
//! # In plain words
//!
//! Opens the window and hands over to the rest of the application.
//!
//! Almost nothing happens here. Everything of substance lives beside it in code
//! that can be tested without a screen, and this file holds only the few things
//! that genuinely need a window to exist: its size, its icon, and making sure a
//! failure to open leaves a message behind.
// No console window on Windows for a release build; a debug build keeps it so
// panics and `eprintln!` stay visible while developing.
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#![forbid(unsafe_code)]
/// The window icon, as raw 32x32 RGBA produced by `assets/generate.py`.
///
/// Raw rather than PNG so the application needs no image decoder just to draw
/// its own title bar.
const ICON_RGBA: &[u8] = include_bytes!("../../../assets/icon-32.rgba");
const ICON_SIZE: u32 = 32;
/// What `--help` prints, on the platforms where printing works.
///
/// Kept beside the argument parsing rather than in a file somewhere, because a
/// second description of an interface drifts from the first: `tools/release/
/// manpage.py` turns this text into the installed manual page at package build
/// time, so the page and the program cannot disagree.
///
/// Declared only where it is read. A Windows release build has no console to
/// print to, so the reader below is Unix only, and a constant nothing reads is
/// dead code: the Windows job failed on it under `-D warnings` while every
/// other platform passed. The test that checks this text against the tab names
/// reads the file rather than the constant, so it still runs everywhere.
#[cfg(unix)]
const USAGE: &str = "\
veilvoice-gui - the VeilVoice desktop application
Usage:
veilvoice-gui [--tab <NAME>] [--size <W>x<H>]
Options:
--tab <NAME> Open on a named tab rather than the last one used.
Names are the ones the tabs carry: file, group, studio,
browser, monitor, lock, verify, settings, install,
about. `live` is accepted and opens the Studio, which is
where live scramble went.
--size <W>x<H> Open at this size in logical pixels rather than the
default 1100x720. Both are clamped to the window's
minimum of 720x520.
--tabs List the tab names, one per line, and exit. What the
screenshot scripts read so they need no copy of the list.
--typeface Say which face the window would draw with, and exit.
`JetBrains Mono` and its path, or `built-in monospace`.
The screenshot scripts check this before capturing.
-h, --help Print this message.
-V, --version Print the version.
The command line is `veilvoice`, and `veilvoice gui` opens this window too.
Everything this window does, that command can do without one.
";
/// Answer `--help` and `--version` before a window is opened.
///
/// Unix only, and the restriction is the honest part rather than an oversight.
/// A release build on Windows declares `windows_subsystem = "windows"` and has
/// no console attached, so `println!` there writes to nothing: the program
/// would appear to do nothing at all, which is worse than the window it
/// currently opens. Where a console is guaranteed, this answers; where it is
/// not, behaviour is unchanged.
///
/// It exists because `veilvoice-gui --help` used to try to open a window and,
/// on a machine with no display, failed with a winit error naming
/// `WAYLAND_DISPLAY`. That is the reply to a reasonable question, and it is
/// also what `lintian` was pointing at with `no-manual-page`: a binary with no
/// help text has no page to derive.
#[cfg(unix)]
fn answered_without_a_window() -> bool {
for arg in std::env::args().skip(1) {
if arg == "-h" || arg == "--help" {
print!("{USAGE}");
return true;
}
if arg == "-V" || arg == "--version" {
println!("veilvoice-gui {}", env!("CARGO_PKG_VERSION"));
return true;
}
// The tab names, one per line, read from the window's own list.
//
// For `tools/shots/gui.sh` and `gui.ps1`, which photograph every tab
// and used to carry a hand-written copy of this list. A copy goes
// stale the first time a tab is added, and the way it goes stale is
// silent: the run succeeds, and the new tab simply has no picture.
if arg == "--tabs" {
for tab in veilvoice_gui::tabs() {
println!("{tab}");
}
return true;
}
// Which face the window would draw with, answered without opening one.
//
// The About tab shows this, but a screenshot script cannot read a tab
// it has not photographed yet, and a capture taken with the built-in
// face looks subtly unlike every other one while nothing in the run
// says so. `tools/shots/gui.sh` asks this first and refuses to
// photograph anything if the answer is the fallback.
if arg == "--typeface" {
match veilvoice_gui::jetbrains_mono_path() {
Some(path) => println!("JetBrains Mono\t{}", path.display()),
None => println!("built-in monospace"),
}
return true;
}
}
false
}
#[cfg(not(unix))]
fn answered_without_a_window() -> bool {
false
}
fn main() -> eframe::Result<()> {
if answered_without_a_window() {
return Ok(());
}
// First, before anything that can fail.
//
// This binary has no console (`windows_subsystem = "windows"`) and the
// workspace aborts on panic, so without this every failure produces
// literally nothing: no message, no dialog, no log, just a window that
// never appears. A user has nothing to report but "it crashed", which is
// exactly the report that arrived against v0.1.10.
veilvoice_gui::crashlog::install();
let mut viewport = egui::ViewportBuilder::default()
// Opens large enough to read without resizing, and still fits a
// 1366x768 laptop with its taskbar. Bigger than this and the window
// would open partly off the bottom of a common screen, which looks
// like a broken application rather than a generous one.
//
// `--size` overrides it. That exists because the screenshot harness
// needs a window bigger than the default -- at 1100x720 the longest
// tab does not fit and the picture shows a panel cut off partway --
// and the only way to get one used to be resizing the window after it
// opened, through `SetWindowPos`, which is Windows and nothing else.
// Asking for the size up front works on every platform, and it is a
// reasonable thing for a person with a large display to want too.
.with_inner_size(veilvoice_gui::window::opening_size(
None,
veilvoice_gui::window::requested_size(),
))
// The floor. Everything below it is reachable by scrolling -- every
// tab is inside one scroller now -- so the only thing this has to
// protect is the horizontal layout, which is monospace and
// column-based and starts overlapping rather than reflowing.
.with_min_inner_size(veilvoice_gui::window::MINIMUM)
.with_title("VeilVoice");
if ICON_RGBA.len() == (ICON_SIZE * ICON_SIZE * 4) as usize {
viewport = viewport.with_icon(std::sync::Arc::new(egui::IconData {
rgba: ICON_RGBA.to_vec(),
width: ICON_SIZE,
height: ICON_SIZE,
}));
}
// Read before the window is made, because it decides how the window is
// made. The settings file is the one place this is turned off, and a
// machine whose driver accepts the request and then draws badly is the case
// it exists for: from inside the process that looks like success.
let acceleration = veilvoice_gui::prefs::default_path()
.map(|path| veilvoice_gui::prefs::Prefs::load(&path).acceleration)
.unwrap_or(true);
let result = eframe::run_native(
"VeilVoice",
veilvoice_gui::graphics::options(viewport, acceleration),
Box::new(|cc| Ok(Box::new(veilvoice_gui::VeilVoiceApp::new(cc)))),
);
// `eframe` reports a failed start by returning, not by panicking, so the
// hook above never sees it -- and the `Err` a `main` returns is printed to
// a stderr that does not exist here. Creating the window is also the most
// likely thing to fail on somebody else's machine: this renders through
// glow, which is OpenGL, and a virtual machine, a remote desktop session or
// hybrid graphics handing over the wrong adapter can all refuse a context.
if let Err(error) = &result {
veilvoice_gui::crashlog::record_startup_failure(&format!("{error}"));
}
result
}