refactor: standardize Rust style across the workspace

This commit is contained in:
devmobasa committed 2026-10-22 09:26:12 +02:00
1 parent c6e89d0072
commit 87e1d057f7
1262 files changed
+18853 -14839

No files matched your search

+12 -1
View File
@@ -30,13 +30,24 @@
- Review changed functions after `cargo fmt`. Formatter success verifies mechanical layout; also verify visually that logical chunks are clearly separated and easy to scan. - Review changed functions after `cargo fmt`. Formatter success verifies mechanical layout; also verify visually that logical chunks are clearly separated and easy to scan.
- Apply these rules to new and materially changed code. Do not reformat unrelated existing code solely to adopt them. - Apply these rules to new and materially changed code. Do not reformat unrelated existing code solely to adopt them.
## Rust Style
- Rust code follows the [Google Rust style guide](https://google.github.io/styleguide/rustguide.html) as application code. Its rules that Clippy can check are workspace lints in `Cargo.toml`, configured in `clippy.toml`. Fix a warning rather than suppressing it. When a site truly cannot comply, use `#[expect(lint, reason = "...")]` with the concrete constraint, never `#[allow]`.
- Each `use` statement imports from one module: `use foo::bar::{Bar, Baz};`, not `use foo::{Foo, bar::Bar};`. `tests/repository_guards/use_statements.rs` checks this because rustfmt's `imports_granularity` is unstable.
- Do not use glob imports, except `use super::*` in test modules and third-party preludes.
- Import type and trait names and use them unqualified; call functions through their module (`fs::read`). Keep ambiguous names such as `io::Error` and `fmt::Result` module-qualified. A gated import carries the same `#[cfg(...)]` as the code that uses it.
- Borrow instead of cloning or taking ownership (`&str`, `&[T]`, `Option<&T>`). Replace more than three bool parameters with enums or an options struct.
- Use `expect` with a reason instead of `unwrap` in production code. Give assertions messages that explain the invariant, and document panics in `# Panics`.
- Never reach inside a poisoned lock. Use `expect` when the critical section cannot panic, or a fallback that leaves the data alone. Tests that serialize on process-wide state use `test_env::lock()` or a `test_env::Lease`.
- `catch_unwind` is reserved for the reviewed worker-isolation sites that carry an `#[expect]` reason. Tests observe panics through a joined scoped thread.
- Getters are `foo()`/`foo_mut()`, a fallible constructor is `try_new`, and type lifetimes have descriptive names. Every `unsafe` block has a `// SAFETY:` comment that says why it is sound.
## Coupled Changes ## Coupled Changes
- Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests. - Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests.
- Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests. - Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests.
- Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`. - Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`.
## Validation ## Validation
- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles. - Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, use statements, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles.
- That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests. - That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests.
- Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well. - Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well.
- For docs-only `AGENTS.md` edits, make new files visible to Git before whitespace checks, for example `rg --files --hidden -g AGENTS.md -0 | xargs -0 git add -N --` followed by `git diff --check`. - For docs-only `AGENTS.md` edits, make new files visible to Git before whitespace checks, for example `rg --files --hidden -g AGENTS.md -0 | xargs -0 git add -N --` followed by `git diff --check`.
+18
View File
@@ -14,6 +14,24 @@ default-members = ["."]
[workspace.lints.clippy] [workspace.lints.clippy]
cognitive_complexity = "warn" cognitive_complexity = "warn"
# Rules from the Google Rust style guide that Clippy can check. CI promotes
# these warnings to errors; clippy.toml holds their settings.
absolute_paths = "warn"
enum_glob_use = "warn"
fn_params_excessive_bools = "warn"
missing_assert_message = "warn"
missing_panics_doc = "warn"
must_use_candidate = "warn"
needless_for_each = "warn"
needless_pass_by_value = "warn"
redundant_clone = "warn"
ref_option = "warn"
return_self_not_must_use = "warn"
string_add = "warn"
undocumented_unsafe_blocks = "warn"
unnecessary_safety_comment = "warn"
unwrap_used = "warn"
wildcard_imports = "warn"
[lints] [lints]
workspace = true workspace = true
+1 -2
View File
@@ -1,7 +1,6 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::{env, fs};
fn main() { fn main() {
println!("cargo:rerun-if-env-changed=WAYSCRIBER_RELEASE_VERSION"); println!("cargo:rerun-if-env-changed=WAYSCRIBER_RELEASE_VERSION");
+13
View File
@@ -1,3 +1,16 @@
# Keep functions within the agreed cognitive-complexity budget. Clippy emits a # Keep functions within the agreed cognitive-complexity budget. Clippy emits a
# warning above this value, and CI promotes warnings to errors. # warning above this value, and CI promotes warnings to errors.
cognitive-complexity-threshold = 20 cognitive-complexity-threshold = 20
# Tests may panic for assertions, so `unwrap_used` applies to production code.
allow-unwrap-in-tests = true
# Never reach inside a poisoned lock, and do not rely on catching panics.
disallowed-methods = [
{ path = "std::panic::catch_unwind", reason = "panics are bugs; recover only as defense in depth with a reviewed `#[expect]`" },
{ path = "std::sync::PoisonError::get_mut", reason = "a poisoned lock may hold broken invariants" },
{ path = "std::sync::PoisonError::get_ref", reason = "a poisoned lock may hold broken invariants" },
{ path = "std::sync::PoisonError::into_inner", reason = "a poisoned lock may hold broken invariants" },
{ path = "std::sync::Mutex::clear_poison", reason = "a poisoned lock may hold broken invariants" },
{ path = "std::sync::RwLock::clear_poison", reason = "a poisoned lock may hold broken invariants" },
]
+15 -14
View File
@@ -1,9 +1,9 @@
use std::fmt; use std::fmt;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use tokio::task;
const PRODUCTION_BLOCKING_JOB_LIMIT: usize = 2; const PRODUCTION_BLOCKING_JOB_LIMIT: usize = 2;
const SLOW_JOB_THRESHOLD: Duration = Duration::from_millis(250); const SLOW_JOB_THRESHOLD: Duration = Duration::from_millis(250);
@@ -65,21 +65,21 @@ impl BlockingJobRunner {
.map_err(|_| format!("{kind} blocking job queue is unavailable"))?; .map_err(|_| format!("{kind} blocking job queue is unavailable"))?;
report_slow_phase(kind, "queue wait", queued_at.elapsed()); report_slow_phase(kind, "queue wait", queued_at.elapsed());
let joined = tokio::task::spawn_blocking(move || { let joined = task::spawn_blocking(move || {
let _permit = permit; let _permit = permit;
let started_at = Instant::now(); let started_at = Instant::now();
let outcome = catch_unwind(AssertUnwindSafe(job)); let result = job();
report_slow_phase(kind, "execution", started_at.elapsed()); report_slow_phase(kind, "execution", started_at.elapsed());
outcome result
}) })
.await; .await;
let outcome = // A panicking job unwinds its blocking thread, which releases the
joined.map_err(|err| format!("{kind} blocking job did not complete: {err}"))?; // permit; tokio reports the panic through the join error.
match joined {
match outcome {
Ok(result) => result, Ok(result) => result,
Err(_) => Err(format!("{kind} blocking job panicked")), Err(err) if err.is_panic() => Err(format!("{kind} blocking job panicked")),
Err(err) => Err(format!("{kind} blocking job did not complete: {err}")),
} }
} }
} }
@@ -106,6 +106,7 @@ mod tests {
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use tokio::sync::oneshot;
use tokio::time::{Duration, timeout}; use tokio::time::{Duration, timeout};
use super::*; use super::*;
@@ -134,7 +135,7 @@ mod tests {
async fn blocking_work_is_offloaded_while_executor_work_progresses() { async fn blocking_work_is_offloaded_while_executor_work_progresses() {
let runner = BlockingJobRunner::new(1); let runner = BlockingJobRunner::new(1);
let caller_thread = thread::current().id(); let caller_thread = thread::current().id();
let (started_tx, started_rx) = tokio::sync::oneshot::channel(); let (started_tx, started_rx) = oneshot::channel();
let (release_tx, release_rx) = mpsc::channel(); let (release_tx, release_rx) = mpsc::channel();
let job = tokio::spawn(async move { let job = tokio::spawn(async move {
@@ -169,7 +170,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn one_permit_prevents_a_second_closure_from_starting() { async fn one_permit_prevents_a_second_closure_from_starting() {
let runner = BlockingJobRunner::new(1); let runner = BlockingJobRunner::new(1);
let (first_started_tx, first_started_rx) = tokio::sync::oneshot::channel(); let (first_started_tx, first_started_rx) = oneshot::channel();
let (first_release_tx, first_release_rx) = mpsc::channel(); let (first_release_tx, first_release_rx) = mpsc::channel();
let first_runner = runner.clone(); let first_runner = runner.clone();
let first = tokio::spawn(async move { let first = tokio::spawn(async move {
@@ -186,7 +187,7 @@ mod tests {
.expect("first closure should start") .expect("first closure should start")
.expect("first closure should report startup"); .expect("first closure should report startup");
let (second_started_tx, mut second_started_rx) = tokio::sync::oneshot::channel(); let (second_started_tx, mut second_started_rx) = oneshot::channel();
let second = tokio::spawn(async move { let second = tokio::spawn(async move {
runner runner
.run(BlockingJobKind::SessionCatalogLoad, move || { .run(BlockingJobKind::SessionCatalogLoad, move || {
@@ -238,7 +239,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dropping_the_waiter_does_not_release_a_running_jobs_permit() { async fn dropping_the_waiter_does_not_release_a_running_jobs_permit() {
let runner = BlockingJobRunner::new(1); let runner = BlockingJobRunner::new(1);
let (first_started_tx, first_started_rx) = tokio::sync::oneshot::channel(); let (first_started_tx, first_started_rx) = oneshot::channel();
let (first_release_tx, first_release_rx) = mpsc::channel(); let (first_release_tx, first_release_rx) = mpsc::channel();
let first_runner = runner.clone(); let first_runner = runner.clone();
let first = tokio::spawn(async move { let first = tokio::spawn(async move {
@@ -268,7 +269,7 @@ mod tests {
"the detached blocking closure must retain its permit" "the detached blocking closure must retain its permit"
); );
let (second_started_tx, mut second_started_rx) = tokio::sync::oneshot::channel(); let (second_started_tx, mut second_started_rx) = oneshot::channel();
let second = tokio::spawn(async move { let second = tokio::spawn(async move {
runner runner
.run(BlockingJobKind::SessionCatalogLoad, move || { .run(BlockingJobKind::SessionCatalogLoad, move || {
+6 -3
View File
@@ -31,7 +31,7 @@ fn load_app_stylesheet() {
// overlay crate's `v4_12` and then deprecates this method. // overlay crate's `v4_12` and then deprecates this method.
#[allow(deprecated)] #[allow(deprecated)]
provider.load_from_data(include_str!("style.css")); provider.load_from_data(include_str!("style.css"));
if let Some(display) = gtk::gdk::Display::default() { if let Some(display) = Display::default() {
gtk::style_context_add_provider_for_display( gtk::style_context_add_provider_for_display(
&display, &display,
&provider, &provider,
@@ -46,6 +46,9 @@ fn load_app_stylesheet() {
const APP_ID: &str = "org.wayscriber.Configurator"; const APP_ID: &str = "org.wayscriber.Configurator";
use effects::spawn_effect; use effects::spawn_effect;
use gtk::gdk::Display;
use gtk::gio::ApplicationFlags;
use wayscriber::domain::Language;
pub(crate) fn run(startup: StartupRequest) { pub(crate) fn run(startup: StartupRequest) {
// Every launch is its own window, as it was under Iced: the overlay and // Every launch is its own window, as it was under Iced: the overlay and
@@ -56,7 +59,7 @@ pub(crate) fn run(startup: StartupRequest) {
// safe: the guarded `ConfigDocument` save revision arbitrates writes. // safe: the guarded `ConfigDocument` save revision arbitrates writes.
let application = adw::Application::builder() let application = adw::Application::builder()
.application_id(APP_ID) .application_id(APP_ID)
.flags(gtk::gio::ApplicationFlags::NON_UNIQUE) .flags(ApplicationFlags::NON_UNIQUE)
.build(); .build();
let app = RelmApp::from_app(application); let app = RelmApp::from_app(application);
// GTK must not parse our argv: `--open <destination>` is ours and was // GTK must not parse our argv: `--open <destination>` is ours and was
@@ -92,7 +95,7 @@ pub(crate) struct AppWidgets {
bindings: Vec<Binding>, bindings: Vec<Binding>,
/// The language the pages were built in. When the active language moves /// The language the pages were built in. When the active language moves
/// away from it, the next refresh rebuilds them. /// away from it, the next refresh rebuilds them.
language: wayscriber::domain::Language, language: Language,
chrome: shell::Chrome, chrome: shell::Chrome,
} }
+2 -1
View File
@@ -1,4 +1,5 @@
use relm4::ComponentSender; use relm4::ComponentSender;
use relm4::adw::ApplicationWindow;
use crate::messages::CommandMessage; use crate::messages::CommandMessage;
@@ -11,7 +12,7 @@ use super::super::{daemon_setup, io, session_catalog};
pub(super) fn spawn_effect( pub(super) fn spawn_effect(
effect: Effect, effect: Effect,
sender: &ComponentSender<ConfiguratorApp>, sender: &ComponentSender<ConfiguratorApp>,
root: &relm4::adw::ApplicationWindow, root: &ApplicationWindow,
) { ) {
use relm4::gtk::prelude::*; use relm4::gtk::prelude::*;
match effect { match effect {
+10 -6
View File
@@ -10,10 +10,14 @@
//! it, and how far the visible page is scrolled. This module carries those //! it, and how far the visible page is scrolled. This module carries those
//! across. //! across.
use std::iter;
use relm4::{ComponentSender, gtk}; use relm4::{ComponentSender, gtk};
use gtk::glib; use gtk::glib;
use gtk::prelude::*; use gtk::glib::GString;
use gtk::prelude::{GtkWindowExt, *};
use wayscriber::i18n;
use super::super::pages; use super::super::pages;
use super::super::state::ConfiguratorApp; use super::super::state::ConfiguratorApp;
@@ -56,7 +60,7 @@ impl WidgetPath {
} }
fn children(widget: &gtk::Widget) -> impl Iterator<Item = gtk::Widget> { fn children(widget: &gtk::Widget) -> impl Iterator<Item = gtk::Widget> {
std::iter::successors(widget.first_child(), gtk::Widget::next_sibling) iter::successors(widget.first_child(), gtk::Widget::next_sibling)
} }
/// The caret and selection of an editable control, in characters. /// The caret and selection of an editable control, in characters.
@@ -69,7 +73,7 @@ struct Caret {
/// View state from the pages being replaced, applied to the new ones. /// View state from the pages being replaced, applied to the new ones.
pub(super) struct ViewState { pub(super) struct ViewState {
/// The stack name of the page the state was read from. /// The stack name of the page the state was read from.
page: Option<gtk::glib::GString>, page: Option<GString>,
/// Set when focus was inside the visible page; focus elsewhere sits in /// Set when focus was inside the visible page; focus elsewhere sits in
/// persistent chrome, which the rebuild leaves alone. /// persistent chrome, which the rebuild leaves alone.
focus: Option<(Option<WidgetPath>, Option<Caret>)>, focus: Option<(Option<WidgetPath>, Option<Caret>)>,
@@ -84,7 +88,7 @@ pub(super) fn rebuild_if_language_changed(
widgets: &mut AppWidgets, widgets: &mut AppWidgets,
sender: &ComponentSender<ConfiguratorApp>, sender: &ComponentSender<ConfiguratorApp>,
) -> Option<ViewState> { ) -> Option<ViewState> {
let language = wayscriber::i18n::language(); let language = i18n::language();
if widgets.language == language { if widgets.language == language {
return None; return None;
} }
@@ -123,7 +127,7 @@ fn capture(widgets: &AppWidgets) -> ViewState {
}; };
}; };
let focus = gtk::prelude::GtkWindowExt::focus(&widgets.root) let focus = GtkWindowExt::focus(&widgets.root)
.filter(|focused| focused.is_ancestor(&page)) .filter(|focused| focused.is_ancestor(&page))
.map(|focused| { .map(|focused| {
let caret = focused let caret = focused
@@ -157,7 +161,7 @@ fn capture(widgets: &AppWidgets) -> ViewState {
fn descendants(widget: &gtk::Widget) -> impl Iterator<Item = gtk::Widget> { fn descendants(widget: &gtk::Widget) -> impl Iterator<Item = gtk::Widget> {
let mut pending = vec![widget.clone()]; let mut pending = vec![widget.clone()];
std::iter::from_fn(move || { iter::from_fn(move || {
let next = pending.pop()?; let next = pending.pop()?;
pending.extend(children(&next)); pending.extend(children(&next));
Some(next) Some(next)
+11 -13
View File
@@ -2,7 +2,10 @@ use relm4::{ComponentSender, adw, gtk};
use adw::prelude::*; use adw::prelude::*;
use gtk::gdk::{Key, ModifierType};
use gtk::glib::Propagation;
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use wayscriber::{build_info, i18n};
use crate::messages::Message; use crate::messages::Message;
use crate::models::TabId; use crate::models::TabId;
@@ -39,7 +42,7 @@ pub(super) fn build(
let sender = sender.clone(); let sender = sender.clone();
root.connect_close_request(move |_| { root.connect_close_request(move |_| {
sender.input(Message::CloseRequested); sender.input(Message::CloseRequested);
gtk::glib::Propagation::Stop Propagation::Stop
}); });
} }
// ---- Sidebar ---------------------------------------------------- // ---- Sidebar ----------------------------------------------------
@@ -267,13 +270,11 @@ pub(super) fn build(
// lets widgets such as SearchEntry perform their own Escape behavior. // lets widgets such as SearchEntry perform their own Escape behavior.
controller.set_propagation_phase(gtk::PropagationPhase::Capture); controller.set_propagation_phase(gtk::PropagationPhase::Capture);
controller.connect_key_pressed(move |_, key, _, modifiers| { controller.connect_key_pressed(move |_, key, _, modifiers| {
if modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK) if modifiers.contains(ModifierType::CONTROL_MASK) && matches!(key, Key::f | Key::F) {
&& matches!(key, gtk::gdk::Key::f | gtk::gdk::Key::F)
{
sender.input(Message::SearchFocusRequested); sender.input(Message::SearchFocusRequested);
return gtk::glib::Propagation::Stop; return Propagation::Stop;
} }
if key == gtk::gdk::Key::Escape { if key == Key::Escape {
// The recorder owns Escape while it is open so a recorded // The recorder owns Escape while it is open so a recorded
// Escape becomes a binding. Confirmation cancel is the // Escape becomes a binding. Confirmation cancel is the
// fallback when nothing is recording. // fallback when nothing is recording.
@@ -281,10 +282,10 @@ pub(super) fn build(
} }
// Tab is the user moving focus deliberately; a still-pending // Tab is the user moving focus deliberately; a still-pending
// startup search focus must not steal it back later. // startup search focus must not steal it back later.
if matches!(key, gtk::gdk::Key::Tab | gtk::gdk::Key::ISO_Left_Tab) { if matches!(key, Key::Tab | Key::ISO_Left_Tab) {
sender.input(Message::StartupInteractionObserved); sender.input(Message::StartupInteractionObserved);
} }
gtk::glib::Propagation::Proceed Propagation::Proceed
}); });
root.add_controller(controller); root.add_controller(controller);
} }
@@ -325,7 +326,7 @@ pub(super) fn build(
search_entry, search_entry,
seen_focus_serial: 0, seen_focus_serial: 0,
bindings, bindings,
language: wayscriber::i18n::language(), language: i18n::language(),
chrome: Chrome { chrome: Chrome {
build_identity, build_identity,
content_page, content_page,
@@ -385,10 +386,7 @@ pub(super) fn retitle(widgets: &AppWidgets) {
} }
chrome.build_identity.set_label(&trf( chrome.build_identity.set_label(&trf(
"Version {} · commit {}", "Version {} · commit {}",
&[ &[&build_info::version(), &build_info::commit_hash()],
&wayscriber::build_info::version(),
&wayscriber::build_info::commit_hash(),
],
)); ));
widgets widgets
.window_title .window_title
+27 -22
View File
@@ -17,17 +17,23 @@
//! every supported language, whichever language is active. //! every supported language, whichever language is active.
use std::path::Path; use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{env, fs, iter, thread};
use relm4::prelude::*; use relm4::prelude::*;
use relm4::{adw, gtk}; use relm4::{adw, gtk};
use adw::prelude::*; use adw::prelude::*;
use gtk::glib::MainContext;
use gtk::prelude::GtkWindowExt;
use wayscriber::domain::Language; use wayscriber::domain::Language;
use wayscriber::{env_vars, i18n};
use crate::messages::{CommandMessage, Message}; use crate::messages::{CommandMessage, Message};
use crate::models::{KeybindingField, LanguageOption, StartupRequest, TabId, UiTabId}; use crate::models::{KeybindingField, LanguageOption, StartupRequest, TabId, UiTabId};
use crate::test_temp;
use super::super::state::ConfiguratorApp; use super::super::state::ConfiguratorApp;
@@ -37,14 +43,14 @@ const CHILD_ENV: &str = "WAYSCRIBER_CONFIGURATOR_GTK_CHILD";
#[test] #[test]
fn language_changes_rebuild_the_window_in_place() { fn language_changes_rebuild_the_window_in_place() {
// A window is allowed only on the dedicated test compositor. // A window is allowed only on the dedicated test compositor.
if std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() if env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none()
|| std::env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests") || env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests")
{ {
eprintln!("skipping configurator GTK test: not on the private test compositor"); eprintln!("skipping configurator GTK test: not on the private test compositor");
return; return;
} }
if std::env::var_os(CHILD_ENV).is_none() { if env::var_os(CHILD_ENV).is_none() {
run_in_child(); run_in_child();
return; return;
} }
@@ -57,8 +63,8 @@ fn language_changes_rebuild_the_window_in_place() {
/// homes, so nothing reads or writes the user's files, no setup tool runs, /// homes, so nothing reads or writes the user's files, no setup tool runs,
/// and the catalogs load from the embedded copies. /// and the catalogs load from the embedded copies.
fn run_in_child() { fn run_in_child() {
let homes = crate::test_temp::tempdir().expect("child homes"); let homes = test_temp::tempdir().expect("child homes");
let mut command = std::process::Command::new(std::env::current_exe().expect("test binary")); let mut command = Command::new(env::current_exe().expect("test binary"));
command command
.args([ .args([
concat!( concat!(
@@ -72,21 +78,21 @@ fn run_in_child() {
"--nocapture", "--nocapture",
]) ])
.env(CHILD_ENV, "1") .env(CHILD_ENV, "1")
.env_remove(wayscriber::env_vars::LANGUAGE_OVERRIDE_ENV); .env_remove(env_vars::LANGUAGE_OVERRIDE_ENV);
for (key, directory) in [ for (key, directory) in [
("XDG_CONFIG_HOME", "config"), ("XDG_CONFIG_HOME", "config"),
("XDG_DATA_HOME", "data"), ("XDG_DATA_HOME", "data"),
("XDG_STATE_HOME", "state"), ("XDG_STATE_HOME", "state"),
("XDG_CACHE_HOME", "cache"), ("XDG_CACHE_HOME", "cache"),
(wayscriber::env_vars::LOCALE_DIR_ENV, "locales"), (env_vars::LOCALE_DIR_ENV, "locales"),
// No setup tool (systemctl, gsettings) is reachable from here. // No setup tool (systemctl, gsettings) is reachable from here.
("PATH", "bin"), ("PATH", "bin"),
] { ] {
let path = homes.path().join(directory); let path = homes.path().join(directory);
std::fs::create_dir_all(&path).expect("child home"); fs::create_dir_all(&path).expect("child home");
command.env(key, path); command.env(key, path);
} }
for key in wayscriber::env_vars::LOCALE_ENV_KEYS { for key in env_vars::LOCALE_ENV_KEYS {
command.env_remove(key); command.env_remove(key);
} }
write_config(&homes.path().join("config"), "[ui]\nlanguage = \"en\"\n"); write_config(&homes.path().join("config"), "[ui]\nlanguage = \"en\"\n");
@@ -97,8 +103,8 @@ fn run_in_child() {
fn write_config(config_home: &Path, contents: &str) { fn write_config(config_home: &Path, contents: &str) {
let directory = config_home.join("wayscriber"); let directory = config_home.join("wayscriber");
std::fs::create_dir_all(&directory).expect("config directory"); fs::create_dir_all(&directory).expect("config directory");
std::fs::write(directory.join("config.toml"), contents).expect("write config"); fs::write(directory.join("config.toml"), contents).expect("write config");
} }
fn run_scenario() { fn run_scenario() {
@@ -111,7 +117,7 @@ fn run_scenario() {
!app.model().document.is_loading() !app.model().document.is_loading()
}); });
settle(); settle();
assert_eq!(wayscriber::i18n::language(), Language::English); assert_eq!(i18n::language(), Language::English);
assert_eq!(sidebar_label(&app, TabId::Drawing), "Drawing"); assert_eq!(sidebar_label(&app, TabId::Drawing), "Drawing");
let pages = app.widgets().stack.observe_children().n_items(); let pages = app.widgets().stack.observe_children().n_items();
let bindings = app.widgets().bindings.len(); let bindings = app.widgets().bindings.len();
@@ -199,7 +205,7 @@ fn service_buttons_describe_service_actions_in_each_language(app: &Controller<Co
let buttons: Vec<_> = visible_page_descendants(app) let buttons: Vec<_> = visible_page_descendants(app)
.into_iter() .into_iter()
.filter(|widget| { .filter(|widget| {
std::iter::successors(Some(widget.clone()), gtk::Widget::parent) iter::successors(Some(widget.clone()), gtk::Widget::parent)
.take_while(|ancestor| *ancestor != page) .take_while(|ancestor| *ancestor != page)
.all(|ancestor| ancestor.is_visible()) .all(|ancestor| ancestor.is_visible())
}) })
@@ -430,7 +436,7 @@ fn a_load_that_changes_the_language_keeps_its_resets(app: &Controller<Configurat
app.emit(Message::ShortcutRecordingStarted(KeybindingField::Undo)); app.emit(Message::ShortcutRecordingStarted(KeybindingField::Undo));
settle(); settle();
write_config( write_config(
Path::new(&std::env::var_os("XDG_CONFIG_HOME").expect("child config home")), Path::new(&env::var_os("XDG_CONFIG_HOME").expect("child config home")),
"[ui]\nlanguage = \"zh-CN\"\n", "[ui]\nlanguage = \"zh-CN\"\n",
); );
@@ -440,8 +446,7 @@ fn a_load_that_changes_the_language_keeps_its_resets(app: &Controller<Configurat
settle(); settle();
app.emit(Message::LeaveDiscardRequested); app.emit(Message::LeaveDiscardRequested);
pump_until("the reload", || { pump_until("the reload", || {
!app.model().document.is_loading() !app.model().document.is_loading() && i18n::language() == Language::SimplifiedChinese
&& wayscriber::i18n::language() == Language::SimplifiedChinese
}); });
settle(); settle();
@@ -492,7 +497,7 @@ fn assert_caret(
} }
fn focus(app: &Controller<ConfiguratorApp>) -> Option<gtk::Widget> { fn focus(app: &Controller<ConfiguratorApp>) -> Option<gtk::Widget> {
gtk::prelude::GtkWindowExt::focus(app.widget()) GtkWindowExt::focus(app.widget())
} }
fn visible_page_descendants(app: &Controller<ConfiguratorApp>) -> Vec<gtk::Widget> { fn visible_page_descendants(app: &Controller<ConfiguratorApp>) -> Vec<gtk::Widget> {
@@ -504,7 +509,7 @@ fn descendants(root: gtk::Widget) -> Vec<gtk::Widget> {
let mut found = Vec::new(); let mut found = Vec::new();
let mut pending = vec![root]; let mut pending = vec![root];
while let Some(widget) = pending.pop() { while let Some(widget) = pending.pop() {
pending.extend(std::iter::successors( pending.extend(iter::successors(
widget.first_child(), widget.first_child(),
gtk::Widget::next_sibling, gtk::Widget::next_sibling,
)); ));
@@ -542,25 +547,25 @@ fn scrollable(app: &Controller<ConfiguratorApp>) -> Option<gtk::ScrolledWindow>
/// Runs the main loop until `done`, failing after a generous timeout. /// Runs the main loop until `done`, failing after a generous timeout.
fn pump_until(what: &str, mut done: impl FnMut() -> bool) { fn pump_until(what: &str, mut done: impl FnMut() -> bool) {
let context = gtk::glib::MainContext::default(); let context = MainContext::default();
let deadline = Instant::now() + Duration::from_secs(30); let deadline = Instant::now() + Duration::from_secs(30);
while !done() { while !done() {
assert!(Instant::now() < deadline, "timed out waiting for {what}"); assert!(Instant::now() < deadline, "timed out waiting for {what}");
while context.pending() { while context.pending() {
context.iteration(false); context.iteration(false);
} }
std::thread::sleep(Duration::from_millis(5)); thread::sleep(Duration::from_millis(5));
} }
} }
/// Lets queued messages, refreshes and a few frames run. /// Lets queued messages, refreshes and a few frames run.
fn settle() { fn settle() {
let context = gtk::glib::MainContext::default(); let context = MainContext::default();
let until = Instant::now() + Duration::from_millis(300); let until = Instant::now() + Duration::from_millis(300);
while Instant::now() < until { while Instant::now() < until {
while context.pending() { while context.pending() {
context.iteration(false); context.iteration(false);
} }
std::thread::sleep(Duration::from_millis(5)); thread::sleep(Duration::from_millis(5));
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@ use relm4::{ComponentSender, adw, gtk};
use adw::prelude::*; use adw::prelude::*;
use gtk::prelude::GtkWindowExt;
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use super::super::pages; use super::super::pages;
@@ -78,7 +79,7 @@ fn write_widgets(app: &ConfiguratorApp, widgets: &mut AppWidgets) {
let leave_pending = app.pending_leave().is_some(); let leave_pending = app.pending_leave().is_some();
let leave_was_pending = widgets.leave_actions.get_visible(); let leave_was_pending = widgets.leave_actions.get_visible();
if leave_pending && !leave_was_pending { if leave_pending && !leave_was_pending {
widgets.leave_previous_focus = gtk::prelude::GtkWindowExt::focus(&widgets.root); widgets.leave_previous_focus = GtkWindowExt::focus(&widgets.root);
} }
set_visible(&widgets.leave_actions, leave_pending); set_visible(&widgets.leave_actions, leave_pending);
if leave_pending && !leave_was_pending { if leave_pending && !leave_was_pending {
@@ -1,4 +1,5 @@
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use wayscriber::{durable_io, paths};
use std::fs; use std::fs;
use std::io::ErrorKind; use std::io::ErrorKind;
@@ -85,7 +86,7 @@ impl HyprlandLightControlsInstallResult {
} }
pub(super) fn read_light_controls_status() -> HyprlandLightControlsStatus { pub(super) fn read_light_controls_status() -> HyprlandLightControlsStatus {
let Some(config_root) = wayscriber::paths::config_dir() else { let Some(config_root) = paths::config_dir() else {
return HyprlandLightControlsStatus { return HyprlandLightControlsStatus {
include_path: None, include_path: None,
include_exists: false, include_exists: false,
@@ -111,7 +112,7 @@ fn read_light_controls_status_from_config_root(config_root: &Path) -> HyprlandLi
} }
pub(super) fn install_light_controls() -> Result<HyprlandLightControlsInstallResult, String> { pub(super) fn install_light_controls() -> Result<HyprlandLightControlsInstallResult, String> {
let config_root = wayscriber::paths::config_dir().ok_or_else(|| { let config_root = paths::config_dir().ok_or_else(|| {
tr("Cannot resolve config directory; failed to determine Hyprland config path.").to_string() tr("Cannot resolve config directory; failed to determine Hyprland config path.").to_string()
})?; })?;
let binary_path = resolve_wayscriber_binary_path()?; let binary_path = resolve_wayscriber_binary_path()?;
@@ -151,7 +152,7 @@ fn write_light_controls(
})?; })?;
let include_path = light_controls_include_path(config_root); let include_path = light_controls_include_path(config_root);
wayscriber::durable_io::write_text_atomic( durable_io::write_text_atomic(
&include_path, &include_path,
&render_light_controls(binary_path), &render_light_controls(binary_path),
AtomicWriteOptions::user_config_file(), AtomicWriteOptions::user_config_file(),
@@ -174,7 +175,7 @@ fn write_light_controls(
source_configured = true; source_configured = true;
source_updated = changed; source_updated = changed;
if changed { if changed {
wayscriber::durable_io::write_text_atomic( durable_io::write_text_atomic(
&main_config_path, &main_config_path,
&updated_content, &updated_content,
AtomicWriteOptions::user_config_file(), AtomicWriteOptions::user_config_file(),
@@ -326,7 +327,7 @@ fn strip_matching_quotes(value: &str) -> &str {
fn normalize_source_target(target: &str) -> PathBuf { fn normalize_source_target(target: &str) -> PathBuf {
let expanded = if let Some(stripped) = target.strip_prefix("~/") { let expanded = if let Some(stripped) = target.strip_prefix("~/") {
wayscriber::paths::home_dir() paths::home_dir()
.map(|home| home.join(stripped)) .map(|home| home.join(stripped))
.unwrap_or_else(|| PathBuf::from(target)) .unwrap_or_else(|| PathBuf::from(target))
} else { } else {
@@ -1,3 +1,6 @@
use crate::test_env::EnvGuard;
use crate::test_temp;
use super::*; use super::*;
use wayscriber::env_vars::HOME_ENV; use wayscriber::env_vars::HOME_ENV;
@@ -76,9 +79,9 @@ fn has_source_line_matches_quoted_and_inline_commented_targets() {
#[test] #[test]
fn has_source_line_matches_tilde_target() { fn has_source_line_matches_tilde_target() {
let tmp = crate::test_temp::tempdir().unwrap(); let tmp = test_temp::tempdir().unwrap();
let home = tmp.path(); let home = tmp.path();
let _env = crate::test_env::EnvGuard::set(&[(HOME_ENV, home.as_os_str())]); let _env = EnvGuard::set(&[(HOME_ENV, home.as_os_str())]);
let absolute = home let absolute = home
.join(".config") .join(".config")
@@ -93,7 +96,7 @@ fn has_source_line_matches_tilde_target() {
#[test] #[test]
fn write_light_controls_writes_include_and_sources_existing_main() { fn write_light_controls_writes_include_and_sources_existing_main() {
let tmp = crate::test_temp::tempdir().unwrap(); let tmp = test_temp::tempdir().unwrap();
let hypr_dir = tmp.path().join(HYPRLAND_DIR); let hypr_dir = tmp.path().join(HYPRLAND_DIR);
fs::create_dir_all(&hypr_dir).unwrap(); fs::create_dir_all(&hypr_dir).unwrap();
let main = hypr_dir.join(MAIN_CONFIG); let main = hypr_dir.join(MAIN_CONFIG);
@@ -113,7 +116,7 @@ fn write_light_controls_writes_include_and_sources_existing_main() {
#[test] #[test]
fn write_light_controls_is_idempotent_for_existing_source() { fn write_light_controls_is_idempotent_for_existing_source() {
let tmp = crate::test_temp::tempdir().unwrap(); let tmp = test_temp::tempdir().unwrap();
let hypr_dir = tmp.path().join(HYPRLAND_DIR); let hypr_dir = tmp.path().join(HYPRLAND_DIR);
fs::create_dir_all(&hypr_dir).unwrap(); fs::create_dir_all(&hypr_dir).unwrap();
let main = hypr_dir.join(MAIN_CONFIG); let main = hypr_dir.join(MAIN_CONFIG);
@@ -131,7 +134,7 @@ fn write_light_controls_is_idempotent_for_existing_source() {
#[test] #[test]
fn write_light_controls_handles_missing_main_config() { fn write_light_controls_handles_missing_main_config() {
let tmp = crate::test_temp::tempdir().unwrap(); let tmp = test_temp::tempdir().unwrap();
let result = write_light_controls(tmp.path(), Path::new("/tmp/wayscriber")).unwrap(); let result = write_light_controls(tmp.path(), Path::new("/tmp/wayscriber")).unwrap();
+2 -2
View File
@@ -7,7 +7,7 @@ use wayscriber::i18n::{tr, trf};
use crate::models::{ use crate::models::{
DaemonAction, DaemonActionResult, DaemonRuntimeStatus, DesktopEnvironment, DaemonAction, DaemonActionResult, DaemonRuntimeStatus, DesktopEnvironment,
LightShortcutApplyCapability, ShortcutBackend, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend,
}; };
use command::command_available; use command::command_available;
@@ -139,7 +139,7 @@ pub(super) fn load_daemon_runtime_status_sync() -> Result<DaemonRuntimeStatus, S
read_portal_shortcut_dropin_state(), read_portal_shortcut_dropin_state(),
portal_runtime_supported, portal_runtime_supported,
); );
let shortcut_apply_capability = crate::models::ShortcutApplyCapability::from_environment( let shortcut_apply_capability = ShortcutApplyCapability::from_environment(
desktop, desktop,
gsettings_available, gsettings_available,
systemctl_available, systemctl_available,
+3 -3
View File
@@ -1,9 +1,9 @@
use wayscriber::durable_io;
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use std::env;
use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::{env, fs};
use crate::models::DesktopEnvironment; use crate::models::DesktopEnvironment;
use wayscriber::durable_io::AtomicWriteOptions; use wayscriber::durable_io::AtomicWriteOptions;
@@ -115,7 +115,7 @@ pub(super) fn install_or_update_user_service() -> Result<PathBuf, String> {
})?; })?;
let contents = render_user_service_file(&binary_path); let contents = render_user_service_file(&binary_path);
wayscriber::durable_io::write_text_atomic( durable_io::write_text_atomic(
&service_path, &service_path,
&contents, &contents,
AtomicWriteOptions::user_config_file(), AtomicWriteOptions::user_config_file(),
@@ -1,6 +1,8 @@
use wayscriber::durable_io;
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use std::fs; use std::fs;
use std::path::PathBuf;
use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend}; use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend};
use wayscriber::durable_io::AtomicWriteOptions; use wayscriber::durable_io::AtomicWriteOptions;
@@ -176,7 +178,7 @@ fn run_gsettings_command(args: &[&str]) -> Result<(), String> {
Ok(()) Ok(())
} }
fn write_portal_shortcut_dropin(shortcut: &str) -> Result<std::path::PathBuf, String> { fn write_portal_shortcut_dropin(shortcut: &str) -> Result<PathBuf, String> {
let dropin_path = portal_shortcut_dropin_path().ok_or_else(|| { let dropin_path = portal_shortcut_dropin_path().ok_or_else(|| {
tr("Cannot resolve home directory; failed to determine systemd drop-in path.").to_string() tr("Cannot resolve home directory; failed to determine systemd drop-in path.").to_string()
})?; })?;
@@ -193,7 +195,7 @@ fn write_portal_shortcut_dropin(shortcut: &str) -> Result<std::path::PathBuf, St
let escaped_shortcut = escape_systemd_env_value(shortcut); let escaped_shortcut = escape_systemd_env_value(shortcut);
let escaped_app_id = escape_systemd_env_value(PORTAL_APP_ID); let escaped_app_id = escape_systemd_env_value(PORTAL_APP_ID);
let contents = render_portal_shortcut_dropin(&escaped_shortcut, &escaped_app_id); let contents = render_portal_shortcut_dropin(&escaped_shortcut, &escaped_app_id);
wayscriber::durable_io::write_text_atomic( durable_io::write_text_atomic(
&dropin_path, &dropin_path,
&contents, &contents,
AtomicWriteOptions::user_config_file(), AtomicWriteOptions::user_config_file(),
+6 -2
View File
@@ -240,6 +240,10 @@ fn action_pending_message(action: DaemonAction) -> &'static str {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use wayscriber::i18n;
use crate::test_env;
use super::*; use super::*;
use wayscriber::domain::Language; use wayscriber::domain::Language;
@@ -298,7 +302,7 @@ mod tests {
#[test] #[test]
fn feedback_renders_in_the_language_active_when_it_is_drawn() { fn feedback_renders_in_the_language_active_when_it_is_drawn() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::feedback_renders_in_the_language_active_when_it_is_drawn" "::feedback_renders_in_the_language_active_when_it_is_drawn"
@@ -313,7 +317,7 @@ mod tests {
let detecting = DaemonFeedback::Status(StatusFeedback::Detecting); let detecting = DaemonFeedback::Status(StatusFeedback::Detecting);
let english = (failed.text(), detecting.text()); let english = (failed.text(), detecting.text());
wayscriber::i18n::set_language(Language::SimplifiedChinese); i18n::set_language(Language::SimplifiedChinese);
assert_eq!( assert_eq!(
english.0, english.0,
+2 -2
View File
@@ -1,4 +1,5 @@
//! Owns document transfer to effects and mutually exclusive load/save phases. //! Owns document transfer to effects and mutually exclusive load/save phases.
use std::mem;
use std::path::PathBuf; use std::path::PathBuf;
use wayscriber::config::{ConfigDocument, ConfigValidationReport}; use wayscriber::config::{ConfigDocument, ConfigValidationReport};
@@ -78,8 +79,7 @@ impl DocumentWorkflow {
if !self.is_saving() { if !self.is_saving() {
return None; return None;
} }
let DocumentPhase::Saving(context) = let DocumentPhase::Saving(context) = mem::replace(&mut self.phase, DocumentPhase::Idle)
std::mem::replace(&mut self.phase, DocumentPhase::Idle)
else { else {
return None; return None;
}; };
+2 -1
View File
@@ -1,4 +1,5 @@
use wayscriber::config::{Config, ConfigDocument}; use wayscriber::config::{Config, ConfigDocument};
use wayscriber::i18n;
use crate::messages::ConfigSaveResult; use crate::messages::ConfigSaveResult;
@@ -10,7 +11,7 @@ pub(super) async fn load_config_from_disk() -> Result<(Box<ConfigDocument>, Opti
// Every catalog, whatever language the file names: the Language row // Every catalog, whatever language the file names: the Language row
// can preview any of them later, and reading a user override is file // can preview any of them later, and reading a user override is file
// work that must not happen on the GTK thread. // work that must not happen on the GTK thread.
wayscriber::i18n::prepare_catalogs(); i18n::prepare_catalogs();
ConfigDocument::load_for_editing() ConfigDocument::load_for_editing()
.map(|(document, warning)| (Box::new(document), warning)) .map(|(document, warning)| (Box::new(document), warning))
.map_err(|err| format!("{err:#}")) .map_err(|err| format!("{err:#}"))
+2 -2
View File
@@ -45,10 +45,10 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
"Shape of the next arrow drawn. Every arrow keeps its own style, so \ "Shape of the next arrow drawn. Every arrow keeps its own style, so \
changing this never restyles existing drawings.", changing this never restyles existing drawings.",
ArrowStyleOption::list(), ArrowStyleOption::list(),
ArrowStyleOption::list() &ArrowStyleOption::list()
.iter() .iter()
.map(|option| option.label().to_string()) .map(|option| option.label().to_string())
.collect(), .collect::<Vec<_>>(),
|app| app.draft.arrow_style, |app| app.draft.arrow_style,
Message::ArrowStyleChanged, Message::ArrowStyleChanged,
); );
+11 -10
View File
@@ -34,6 +34,7 @@ use wayscriber::i18n::tr;
use relm4::prelude::*; use relm4::prelude::*;
use relm4::{adw, gtk}; use relm4::{adw, gtk};
use wayscriber::domain::BoardGridKind;
use crate::messages::Message; use crate::messages::Message;
use crate::models::{ use crate::models::{
@@ -238,7 +239,7 @@ struct SectionLayout {
visible: bool, visible: bool,
expanded: bool, expanded: bool,
background_kind: BoardBackgroundOption, background_kind: BoardBackgroundOption,
grid_kind: wayscriber::domain::BoardGridKind, grid_kind: BoardGridKind,
pen_enabled: bool, pen_enabled: bool,
auto_adjust: bool, auto_adjust: bool,
persist: bool, persist: bool,
@@ -271,18 +272,18 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec<Sec
} }
/// One board row's values: everything the layout deliberately left out. /// One board row's values: everything the layout deliberately left out.
struct BoardValues<'a> { struct BoardValues<'draft> {
id: &'a str, id: &'draft str,
name: &'a str, name: &'draft str,
grid_spacing: &'a str, grid_spacing: &'draft str,
background: ColorValues<'a>, background: ColorValues<'draft>,
pen: ColorValues<'a>, pen: ColorValues<'draft>,
} }
/// One color field's values: the picker's editing hex, and the draft's own /// One color field's values: the picker's editing hex, and the draft's own
/// triplet, so the swatch parses the components by the same rules the draft /// triplet, so the swatch parses the components by the same rules the draft
/// does. /// does.
struct ColorValues<'a> { struct ColorValues<'draft> {
hex: &'a str, hex: &'draft str,
color: &'a ColorTripletInput, color: &'draft ColorTripletInput,
} }
+5 -2
View File
@@ -1,5 +1,8 @@
use std::array;
use relm4::{ComponentSender, gtk}; use relm4::{ComponentSender, gtk};
use gtk::gdk::RGBA;
use gtk::glib::SignalHandlerId; use gtk::glib::SignalHandlerId;
use gtk::prelude::*; use gtk::prelude::*;
@@ -44,7 +47,7 @@ impl ColorRow {
} }
let rgb = parse_triplet_values(&values.color.components); let rgb = parse_triplet_values(&values.color.components);
let rgba = gtk::gdk::RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0); let rgba = RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0);
set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba); set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba);
} }
} }
@@ -88,7 +91,7 @@ pub(super) fn build_color_row(
}; };
controls.append(&swatch); controls.append(&swatch);
let components = std::array::from_fn(|component| { let components = array::from_fn(|component| {
let entry = gtk::Entry::builder() let entry = gtk::Entry::builder()
.placeholder_text(COMPONENT_PLACEHOLDERS.get(component).copied().unwrap_or("")) .placeholder_text(COMPONENT_PLACEHOLDERS.get(component).copied().unwrap_or(""))
.width_chars(6) .width_chars(6)
+2 -4
View File
@@ -1,9 +1,7 @@
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use super::{ use super::SectionLayout;
SectionLayout, use super::rows::{TextRow, build_text_row};
rows::{TextRow, build_text_row},
};
use crate::app::state::ConfiguratorApp; use crate::app::state::ConfiguratorApp;
use crate::messages::Message; use crate::messages::Message;
use crate::models::{BoardBackgroundOption, BoardItemTextField}; use crate::models::{BoardBackgroundOption, BoardItemTextField};
+7 -7
View File
@@ -88,7 +88,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Selection frontend"), tr("Selection frontend"),
tr("Native draws the picker over a frozen desktop image. Slurp uses the external selector."), tr("Native draws the picker over a frozen desktop image. Slurp uses the external selector."),
RegionPickerOption::list(), RegionPickerOption::list(),
labels(RegionPickerOption::list(), RegionPickerOption::label), &labels(RegionPickerOption::list(), RegionPickerOption::label),
|app| app.draft.capture.region_picker, |app| app.draft.capture.region_picker,
Message::CaptureRegionPickerChanged, Message::CaptureRegionPickerChanged,
) )
@@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Page size"), tr("Page size"),
"", "",
PdfPageSizeOption::list(), PdfPageSizeOption::list(),
labels(PdfPageSizeOption::list(), PdfPageSizeOption::label), &labels(PdfPageSizeOption::list(), PdfPageSizeOption::label),
|app| app.draft.export_pdf_page_size, |app| app.draft.export_pdf_page_size,
Message::ExportPdfPageSizeChanged, Message::ExportPdfPageSizeChanged,
) )
@@ -134,7 +134,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Orientation"), tr("Orientation"),
"", "",
PdfOrientationOption::list(), PdfOrientationOption::list(),
labels(PdfOrientationOption::list(), PdfOrientationOption::label), &labels(PdfOrientationOption::list(), PdfOrientationOption::label),
|app| app.draft.export_pdf_orientation, |app| app.draft.export_pdf_orientation,
Message::ExportPdfOrientationChanged, Message::ExportPdfOrientationChanged,
) )
@@ -142,7 +142,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Fit"), tr("Fit"),
"", "",
PdfFitModeOption::list(), PdfFitModeOption::list(),
labels(PdfFitModeOption::list(), PdfFitModeOption::label), &labels(PdfFitModeOption::list(), PdfFitModeOption::label),
|app| app.draft.export_pdf_fit, |app| app.draft.export_pdf_fit,
Message::ExportPdfFitChanged, Message::ExportPdfFitChanged,
) )
@@ -150,7 +150,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Transparent page background"), tr("Transparent page background"),
"", "",
PdfTransparentBackgroundOption::list(), PdfTransparentBackgroundOption::list(),
labels( &labels(
PdfTransparentBackgroundOption::list(), PdfTransparentBackgroundOption::list(),
PdfTransparentBackgroundOption::label, PdfTransparentBackgroundOption::label,
), ),
@@ -185,7 +185,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Label position"), tr("Label position"),
"", "",
PdfLabelPositionOption::list(), PdfLabelPositionOption::list(),
labels( &labels(
PdfLabelPositionOption::list(), PdfLabelPositionOption::list(),
PdfLabelPositionOption::label, PdfLabelPositionOption::label,
), ),
@@ -196,7 +196,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Label content"), tr("Label content"),
"", "",
PdfLabelContentModeOption::list(), PdfLabelContentModeOption::list(),
labels( &labels(
PdfLabelContentModeOption::list(), PdfLabelContentModeOption::list(),
PdfLabelContentModeOption::label, PdfLabelContentModeOption::label,
), ),
+6 -5
View File
@@ -16,6 +16,7 @@
use relm4::{adw, gtk}; use relm4::{adw, gtk};
use adw::prelude::*; use adw::prelude::*;
use gtk::gdk::RGBA;
use gtk::glib::SignalHandlerId; use gtk::glib::SignalHandlerId;
use crate::messages::Message; use crate::messages::Message;
@@ -30,7 +31,7 @@ pub(crate) type ResolvedColor = Option<(f64, f64, f64, f64)>;
/// Formats a dialog selection the way the hex handler expects: `#rrggbb`, /// Formats a dialog selection the way the hex handler expects: `#rrggbb`,
/// with an alpha byte only when the color is actually translucent. /// with an alpha byte only when the color is actually translucent.
pub(crate) fn dialog_hex(rgba: &gtk::gdk::RGBA) -> String { pub(crate) fn dialog_hex(rgba: &RGBA) -> String {
let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8; let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
let (r, g, b, a) = ( let (r, g, b, a) = (
channel(rgba.red()), channel(rgba.red()),
@@ -47,7 +48,7 @@ pub(crate) fn dialog_hex(rgba: &gtk::gdk::RGBA) -> String {
/// Formats a selection for a four-component field, retaining `FF` so choosing /// Formats a selection for a four-component field, retaining `FF` so choosing
/// fully opaque replaces any translucent alpha already in the draft. /// fully opaque replaces any translucent alpha already in the draft.
fn dialog_hex_with_alpha(rgba: &gtk::gdk::RGBA) -> String { fn dialog_hex_with_alpha(rgba: &RGBA) -> String {
let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8; let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
let (r, g, b, a) = ( let (r, g, b, a) = (
channel(rgba.red()), channel(rgba.red()),
@@ -86,7 +87,7 @@ pub(crate) fn mark_hex_error(widget: &impl IsA<gtk::Widget>, text: &str) {
pub(crate) fn set_swatch_blocked( pub(crate) fn set_swatch_blocked(
button: &gtk::ColorDialogButton, button: &gtk::ColorDialogButton,
handler: &SignalHandlerId, handler: &SignalHandlerId,
rgba: &gtk::gdk::RGBA, rgba: &RGBA,
) { ) {
if dialog_hex(&button.rgba()) == dialog_hex(rgba) { if dialog_hex(&button.rgba()) == dialog_hex(rgba) {
return; return;
@@ -140,7 +141,7 @@ pub(crate) fn color_row(
set_text_blocked(&row, &row_handler, &text); set_text_blocked(&row, &row_handler, &text);
mark_hex_error(&row, &text); mark_hex_error(&row, &text);
if let Some((r, g, b, a)) = resolved(app) { if let Some((r, g, b, a)) = resolved(app) {
let rgba = gtk::gdk::RGBA::new(r as f32, g as f32, b as f32, a as f32); let rgba = RGBA::new(r as f32, g as f32, b as f32, a as f32);
set_swatch_blocked(&dialog_button, &button_handler, &rgba); set_swatch_blocked(&dialog_button, &button_handler, &rgba);
} }
}); });
@@ -153,7 +154,7 @@ mod tests {
#[test] #[test]
fn alpha_dialog_hex_keeps_an_explicit_opaque_byte() { fn alpha_dialog_hex_keeps_an_explicit_opaque_byte() {
let rgba = gtk::gdk::RGBA::new(18.0 / 255.0, 52.0 / 255.0, 86.0 / 255.0, 1.0); let rgba = RGBA::new(18.0 / 255.0, 52.0 / 255.0, 86.0 / 255.0, 1.0);
assert_eq!(dialog_hex_with_alpha(&rgba), "#123456FF"); assert_eq!(dialog_hex_with_alpha(&rgba), "#123456FF");
assert_eq!(dialog_hex(&rgba), "#123456"); assert_eq!(dialog_hex(&rgba), "#123456");
+7 -2
View File
@@ -1,3 +1,8 @@
use wayscriber::domain::Language;
use wayscriber::i18n;
use crate::test_env;
use super::*; use super::*;
use crate::models::{ use crate::models::{
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability,
@@ -122,14 +127,14 @@ fn overall_status_follows_service_state() {
#[test] #[test]
fn feedback_tone_follows_the_outcome_in_every_language() { fn feedback_tone_follows_the_outcome_in_every_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::feedback_tone_follows_the_outcome_in_every_language" "::feedback_tone_follows_the_outcome_in_every_language"
), ),
|| { || {
use crate::app::daemon_workflow::{ActionFeedback, DaemonFeedback, StatusFeedback}; use crate::app::daemon_workflow::{ActionFeedback, DaemonFeedback, StatusFeedback};
wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); i18n::set_language(Language::SimplifiedChinese);
let failed = DaemonFeedback::Action(ActionFeedback::Failed("权限不足".into())); let failed = DaemonFeedback::Action(ActionFeedback::Failed("权限不足".into()));
let loaded = DaemonFeedback::Status(StatusFeedback::Loaded); let loaded = DaemonFeedback::Status(StatusFeedback::Loaded);
+4 -4
View File
@@ -92,13 +92,13 @@ fn section_combo_row<O>(
list: &gtk::ListBox, list: &gtk::ListBox,
title: &str, title: &str,
values: Vec<O>, values: Vec<O>,
labels: Vec<String>, labels: &[String],
get: impl Fn(&ConfiguratorApp) -> O + 'static, get: impl Fn(&ConfiguratorApp) -> O + 'static,
to_message: impl Fn(O) -> Message + 'static, to_message: impl Fn(O) -> Message + 'static,
) where ) where
O: Copy + PartialEq + 'static, O: Copy + PartialEq + 'static,
{ {
let row = combo_row_widget(title, &labels); let row = combo_row_widget(title, labels);
let handler = connect_combo(&row, page.sender(), values.clone(), to_message); let handler = connect_combo(&row, page.sender(), values.clone(), to_message);
list.append(&row); list.append(&row);
page.bind(move |app, _summary| { page.bind(move |app, _summary| {
@@ -191,8 +191,8 @@ fn set_visible_if_changed(widget: &impl IsA<gtk::Widget>, visible: bool) {
} }
} }
fn select_if_changed<O: PartialEq>(row: &adw::ComboRow, values: &[O], current: O) { fn select_if_changed<O: PartialEq>(row: &adw::ComboRow, values: &[O], current: &O) {
if let Some(index) = values.iter().position(|value| *value == current) { if let Some(index) = values.iter().position(|value| value == current) {
let index = index as u32; let index = index as u32;
if row.selected() != index { if row.selected() != index {
row.set_selected(index); row.set_selected(index);
@@ -18,7 +18,7 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Color mode"), tr("Color mode"),
tr("A palette name or hex string, or explicit RGB components."), tr("A palette name or hex string, or explicit RGB components."),
COLOR_MODES.to_vec(), COLOR_MODES.to_vec(),
vec![tr("Named color").to_string(), tr("RGB color").to_string()], &[tr("Named color").to_string(), tr("RGB color").to_string()],
|app| app.draft.drawing_color.mode, |app| app.draft.drawing_color.mode,
Message::ColorModeChanged, Message::ColorModeChanged,
); );
@@ -29,7 +29,7 @@ pub(super) fn build(page: &mut PageBuilder) {
&named, &named,
tr("Named color"), tr("Named color"),
NamedColorOption::list(), NamedColorOption::list(),
named_color_labels(), &named_color_labels(),
|app| app.draft.drawing_color.selected_named, |app| app.draft.drawing_color.selected_named,
Message::NamedColorSelected, Message::NamedColorSelected,
); );
@@ -1,3 +1,4 @@
use wayscriber::config;
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use crate::messages::Message; use crate::messages::Message;
@@ -37,10 +38,10 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Eraser mode"), tr("Eraser mode"),
"", "",
EraserModeOption::list(), EraserModeOption::list(),
EraserModeOption::list() &EraserModeOption::list()
.iter() .iter()
.map(|option| option.label().to_string()) .map(|option| option.label().to_string())
.collect(), .collect::<Vec<_>>(),
|app| app.draft.drawing_default_eraser_mode, |app| app.draft.drawing_default_eraser_mode,
Message::EraserModeChanged, Message::EraserModeChanged,
) )
@@ -64,7 +65,7 @@ pub(super) fn build(page: &mut PageBuilder) {
validate_usize_range( validate_usize_range(
&app.draft.drawing_shape_recognition_sensitivity, &app.draft.drawing_shape_recognition_sensitivity,
0, 0,
usize::from(wayscriber::config::MAX_SHAPE_RECOGNITION_SENSITIVITY), usize::from(config::MAX_SHAPE_RECOGNITION_SENSITIVITY),
) )
}, },
) )
@@ -86,7 +86,7 @@ fn build_drag_button_section(page: &mut PageBuilder, button: DragMouseButton) {
&list, &list,
field.label(), field.label(),
tools.clone(), tools.clone(),
tool_labels.clone(), &tool_labels,
move |app| drag_tool_for_field(drag_button_config(app, button), field), move |app| drag_tool_for_field(drag_button_config(app, button), field),
move |option| Message::DrawingMouseDragToolChanged(button, field, option), move |option| Message::DrawingMouseDragToolChanged(button, field, option),
); );
@@ -95,7 +95,7 @@ fn build_drag_button_section(page: &mut PageBuilder, button: DragMouseButton) {
&list, &list,
&trf("{} color", &[&field.label()]), &trf("{} color", &[&field.label()]),
colors.clone(), colors.clone(),
color_labels.clone(), &color_labels,
move |app| drag_color_for_field(drag_button_config(app, button), field), move |app| drag_color_for_field(drag_button_config(app, button), field),
move |option| Message::DrawingMouseDragColorChanged(button, field, option), move |option| Message::DrawingMouseDragColorChanged(button, field, option),
); );
+18 -14
View File
@@ -5,6 +5,7 @@ use std::rc::Rc;
use relm4::{ComponentSender, adw, gtk}; use relm4::{ComponentSender, adw, gtk};
use adw::prelude::*; use adw::prelude::*;
use gtk::pango::{AttrFontDesc, AttrList, FontDescription};
use crate::messages::Message; use crate::messages::Message;
use crate::models::{FontStyleOption, FontWeightOption, TextField}; use crate::models::{FontStyleOption, FontWeightOption, TextField};
@@ -27,10 +28,10 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Font weight"), tr("Font weight"),
"", "",
FontWeightOption::list(), FontWeightOption::list(),
FontWeightOption::list() &FontWeightOption::list()
.iter() .iter()
.map(|option| option.label().to_string()) .map(|option| option.label().to_string())
.collect(), .collect::<Vec<_>>(),
|app| app.draft.drawing_font_weight_option, |app| app.draft.drawing_font_weight_option,
Message::FontWeightOptionSelected, Message::FontWeightOptionSelected,
) )
@@ -43,10 +44,10 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Font style"), tr("Font style"),
"", "",
FontStyleOption::list(), FontStyleOption::list(),
FontStyleOption::list() &FontStyleOption::list()
.iter() .iter()
.map(|option| option.label().to_string()) .map(|option| option.label().to_string())
.collect(), .collect::<Vec<_>>(),
|app| app.draft.drawing_font_style_option, |app| app.draft.drawing_font_style_option,
Message::FontStyleOptionSelected, Message::FontStyleOptionSelected,
); );
@@ -130,7 +131,7 @@ fn build_font_cycle(page: &mut PageBuilder) {
.map(|row| row.layout) .map(|row| row.layout)
.eq(layouts.iter().copied()) .eq(layouts.iter().copied())
{ {
rows = rebuild_font_cycle(&list, &families, Rc::clone(&installed), &layouts, &sender); rows = rebuild_font_cycle(&list, &families, &installed, &layouts, &sender);
} }
for (family, row) in app for (family, row) in app
.draft .draft
@@ -177,7 +178,7 @@ struct BoundFontRow {
fn rebuild_font_cycle( fn rebuild_font_cycle(
list: &gtk::ListBox, list: &gtk::ListBox,
families: &gtk::StringList, families: &gtk::StringList,
family_names: Rc<[String]>, family_names: &Rc<[String]>,
layouts: &[FontCycleLayout], layouts: &[FontCycleLayout],
sender: &ComponentSender<ConfiguratorApp>, sender: &ComponentSender<ConfiguratorApp>,
) -> Vec<BoundFontRow> { ) -> Vec<BoundFontRow> {
@@ -189,7 +190,7 @@ fn rebuild_font_cycle(
.iter() .iter()
.map(|layout| { .map(|layout| {
let (row, refresh) = let (row, refresh) =
build_font_cycle_row(*layout, families, Rc::clone(&family_names), sender); build_font_cycle_row(*layout, families, Rc::clone(family_names), sender);
list.append(&row); list.append(&row);
BoundFontRow { BoundFontRow {
layout: *layout, layout: *layout,
@@ -292,8 +293,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory {
return; return;
}; };
label.set_label(&family); label.set_label(&family);
let attributes = gtk::pango::AttrList::new(); let attributes = AttrList::new();
attributes.insert(gtk::pango::AttrFontDesc::new(&family_preview_font(&family))); attributes.insert(AttrFontDesc::new(&family_preview_font(&family)));
label.set_attributes(Some(&attributes)); label.set_attributes(Some(&attributes));
}); });
factory factory
@@ -304,8 +305,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory {
/// Built field by field: `FontDescription::from_string` reads trailing style, /// Built field by field: `FontDescription::from_string` reads trailing style,
/// weight, and stretch words off the family, so "Roboto Condensed" would preview /// weight, and stretch words off the family, so "Roboto Condensed" would preview
/// in Roboto and "Archivo Black" in Archivo. /// in Roboto and "Archivo Black" in Archivo.
fn family_preview_font(family: &str) -> gtk::pango::FontDescription { fn family_preview_font(family: &str) -> FontDescription {
let mut description = gtk::pango::FontDescription::new(); let mut description = FontDescription::new();
description.set_family(family); description.set_family(family);
description description
} }
@@ -361,10 +362,13 @@ fn missing_family_note(family: &str) -> Option<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use gtk::pango::{Stretch, Weight};
use wayscriber::draw;
use super::*; use super::*;
fn installed() -> String { fn installed() -> String {
wayscriber::draw::system_font_families() draw::system_font_families()
.first() .first()
.expect("at least one family") .expect("at least one family")
.clone() .clone()
@@ -403,8 +407,8 @@ mod tests {
let description = family_preview_font(family); let description = family_preview_font(family);
assert_eq!(description.family().as_deref(), Some(family)); assert_eq!(description.family().as_deref(), Some(family));
assert_eq!(description.weight(), gtk::pango::Weight::Normal); assert_eq!(description.weight(), Weight::Normal);
assert_eq!(description.stretch(), gtk::pango::Stretch::Normal); assert_eq!(description.stretch(), Stretch::Normal);
} }
} }
@@ -3,6 +3,7 @@ use wayscriber::i18n::{tr, trf};
use relm4::{ComponentSender, adw, gtk}; use relm4::{ComponentSender, adw, gtk};
use adw::prelude::*; use adw::prelude::*;
use gtk::gdk::RGBA;
use wayscriber::config::{QUICK_COLOR_RENDER_LIMIT, QuickColorSlot}; use wayscriber::config::{QUICK_COLOR_RENDER_LIMIT, QuickColorSlot};
@@ -120,10 +121,10 @@ fn quick_color_layouts(app: &ConfiguratorApp) -> Vec<QuickColorLayout> {
} }
/// One quick color row's values: everything the layout deliberately left out. /// One quick color row's values: everything the layout deliberately left out.
struct QuickColorValues<'a> { struct QuickColorValues<'draft> {
label: &'a str, label: &'draft str,
name: &'a str, name: &'draft str,
hex: &'a str, hex: &'draft str,
named: NamedColorOption, named: NamedColorOption,
/// The color the entry resolves to, `None` when it resolves to nothing — /// The color the entry resolves to, `None` when it resolves to nothing —
/// which is also what makes the name field an error. /// which is also what makes the name field an error.
@@ -224,7 +225,7 @@ fn build_quick_color_row(
tr("Color mode"), tr("Color mode"),
&[tr("Named or hex").to_string(), "RGB".to_string()], &[tr("Named or hex").to_string(), "RGB".to_string()],
); );
select_if_changed(&mode, &COLOR_MODES, layout.mode); select_if_changed(&mode, &COLOR_MODES, &layout.mode);
connect_combo(&mode, sender.clone(), COLOR_MODES.to_vec(), move |value| { connect_combo(&mode, sender.clone(), COLOR_MODES.to_vec(), move |value| {
Message::QuickColorModeChanged(index, value) Message::QuickColorModeChanged(index, value)
}); });
@@ -307,7 +308,7 @@ fn build_quick_color_row(
} }
if let Some((r, g, b, a)) = values.preview { if let Some((r, g, b, a)) = values.preview {
let rgba = gtk::gdk::RGBA::new(r as f32, g as f32, b as f32, a as f32); let rgba = RGBA::new(r as f32, g as f32, b as f32, a as f32);
set_swatch_blocked(&swatch, &swatch_handler, &rgba); set_swatch_blocked(&swatch, &swatch_handler, &rgba);
} }
}); });
@@ -8,7 +8,7 @@ mod widgets;
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use std::cell::RefCell; use std::cell::{Cell, RefCell};
use std::rc::Rc; use std::rc::Rc;
use relm4::prelude::*; use relm4::prelude::*;
@@ -47,11 +47,11 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
"Record a shortcut or sequence, reset one action, or edit the comma-separated list as text. Filter and sort to review many actions at once.", "Record a shortcut or sequence, reset one action, or edit the comma-separated list as text. Filter and sort to review many actions at once.",
)) ))
.build(); .build();
for field in fields let tab_fields = fields
.iter() .iter()
.copied() .copied()
.filter(|field| keybinding_tab(*field) == tab) .filter(|field| keybinding_tab(*field) == tab);
{ for field in tab_fields {
let row = binding_row(&group, field, sender, &mut bindings, refresh.clone()); let row = binding_row(&group, field, sender, &mut bindings, refresh.clone());
slots.push((field, row, group.clone())); slots.push((field, row, group.clone()));
} }
@@ -173,7 +173,7 @@ fn conflict_banner(
.child(&row) .child(&row)
.build(); .build();
let jump_field = std::rc::Rc::new(std::cell::Cell::new(None::<KeybindingField>)); let jump_field = Rc::new(Cell::new(None::<KeybindingField>));
{ {
let sender = sender.clone(); let sender = sender.clone();
let jump_field = jump_field.clone(); let jump_field = jump_field.clone();
@@ -6,6 +6,8 @@ use std::rc::Rc;
use relm4::{ComponentSender, gtk}; use relm4::{ComponentSender, gtk};
use gtk::gdk::{ButtonEvent, EventType, InputSource, Key, ModifierType};
use gtk::glib::Propagation;
use gtk::glib::object::IsA; use gtk::glib::object::IsA;
use gtk::prelude::*; use gtk::prelude::*;
@@ -151,25 +153,24 @@ fn attach_key_controller(popover: &gtk::Popover, sender: &ComponentSender<Config
let controller = gtk::EventControllerKey::new(); let controller = gtk::EventControllerKey::new();
controller.set_propagation_phase(gtk::PropagationPhase::Capture); controller.set_propagation_phase(gtk::PropagationPhase::Capture);
{ {
let sender = sender.clone();
let pressed = pressed.clone(); let pressed = pressed.clone();
controller.connect_key_pressed(move |_, key, keycode, modifiers| { controller.connect_key_pressed(move |_, key, keycode, modifiers| {
// GTK repeats key-pressed for auto-repeat. Sequence recording // GTK repeats key-pressed for auto-repeat. Sequence recording
// would otherwise append Ctrl+K > Ctrl+K > Ctrl+K from a hold. // would otherwise append Ctrl+K > Ctrl+K > Ctrl+K from a hold.
if !pressed.borrow_mut().insert(keycode) { if !pressed.borrow_mut().insert(keycode) {
return gtk::glib::Propagation::Stop; return Propagation::Stop;
} }
let keyval = gdk_keyval(key); let keyval = gdk_keyval(key);
let recorded = KeyboardModifiers { let recorded = KeyboardModifiers {
ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK), ctrl: modifiers.contains(ModifierType::CONTROL_MASK),
shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK), shift: modifiers.contains(ModifierType::SHIFT_MASK),
alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK), alt: modifiers.contains(ModifierType::ALT_MASK),
super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK) super_held: modifiers.contains(ModifierType::SUPER_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::META_MASK) || modifiers.contains(ModifierType::META_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK), || modifiers.contains(ModifierType::HYPER_MASK),
}; };
sender.input(Message::ShortcutRecorderKey(keyval, recorded)); sender.input(Message::ShortcutRecorderKey(keyval, recorded));
gtk::glib::Propagation::Stop Propagation::Stop
}); });
} }
{ {
@@ -215,30 +216,30 @@ fn attach_button_controller(popover: &gtk::Popover, sender: &ComponentSender<Con
let controller = gtk::EventControllerLegacy::new(); let controller = gtk::EventControllerLegacy::new();
controller.set_propagation_phase(gtk::PropagationPhase::Capture); controller.set_propagation_phase(gtk::PropagationPhase::Capture);
controller.connect_event(move |_, event| { controller.connect_event(move |_, event| {
if event.event_type() != gtk::gdk::EventType::ButtonPress { if event.event_type() != EventType::ButtonPress {
return gtk::glib::Propagation::Proceed; return Propagation::Proceed;
} }
let Some(button_event) = event.downcast_ref::<gtk::gdk::ButtonEvent>() else { let Some(button_event) = event.downcast_ref::<ButtonEvent>() else {
return gtk::glib::Propagation::Proceed; return Propagation::Proceed;
}; };
let kind = event let kind = event
.device() .device()
.map(|device| match device.source() { .map(|device| match device.source() {
gtk::gdk::InputSource::Mouse InputSource::Mouse | InputSource::Touchpad | InputSource::Trackpoint => {
| gtk::gdk::InputSource::Touchpad RecorderDeviceKind::Mouse
| gtk::gdk::InputSource::Trackpoint => RecorderDeviceKind::Mouse, }
gtk::gdk::InputSource::Pen => RecorderDeviceKind::Pen, InputSource::Pen => RecorderDeviceKind::Pen,
_ => RecorderDeviceKind::Other, _ => RecorderDeviceKind::Other,
}) })
.unwrap_or(RecorderDeviceKind::Other); .unwrap_or(RecorderDeviceKind::Other);
let modifiers = event.modifier_state(); let modifiers = event.modifier_state();
let recorded = KeyboardModifiers { let recorded = KeyboardModifiers {
ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK), ctrl: modifiers.contains(ModifierType::CONTROL_MASK),
shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK), shift: modifiers.contains(ModifierType::SHIFT_MASK),
alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK), alt: modifiers.contains(ModifierType::ALT_MASK),
super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK) super_held: modifiers.contains(ModifierType::SUPER_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::META_MASK) || modifiers.contains(ModifierType::META_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK), || modifiers.contains(ModifierType::HYPER_MASK),
}; };
// Primary clicks must reach Finish / Remove Last Step / Cancel. Stop // Primary clicks must reach Finish / Remove Last Step / Cancel. Stop
// only bindable auxiliary mouse or stylus barrel events. // only bindable auxiliary mouse or stylus barrel events.
@@ -249,15 +250,15 @@ fn attach_button_controller(popover: &gtk::Popover, sender: &ComponentSender<Con
kind, kind,
recorded, recorded,
)); ));
gtk::glib::Propagation::Stop Propagation::Stop
} }
RecordedDevice::Unsupported { .. } => gtk::glib::Propagation::Proceed, RecordedDevice::Unsupported { .. } => Propagation::Proceed,
} }
}); });
popover.add_controller(controller); popover.add_controller(controller);
} }
fn gdk_keyval(key: gtk::gdk::Key) -> u32 { fn gdk_keyval(key: Key) -> u32 {
use gtk::glib::translate::IntoGlib; use gtk::glib::translate::IntoGlib;
key.into_glib() key.into_glib()
} }
+7 -11
View File
@@ -1,12 +1,13 @@
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use std::cell::RefCell; use std::cell::{Cell, RefCell};
use std::rc::Rc; use std::rc::Rc;
use relm4::prelude::*; use relm4::prelude::*;
use relm4::{adw, gtk}; use relm4::{adw, gtk};
use adw::prelude::*; use adw::prelude::*;
use gtk::pango::EllipsizeMode;
use wayscriber::config::Shortcut; use wayscriber::config::Shortcut;
use crate::messages::Message; use crate::messages::Message;
@@ -82,7 +83,7 @@ pub(super) fn binding_row(
let compact_summary = gtk::Label::builder() let compact_summary = gtk::Label::builder()
.css_classes(["dim-label"]) .css_classes(["dim-label"])
.ellipsize(gtk::pango::EllipsizeMode::End) .ellipsize(EllipsizeMode::End)
.max_width_chars(24) .max_width_chars(24)
.xalign(1.0) .xalign(1.0)
.build(); .build();
@@ -162,16 +163,11 @@ pub(super) fn binding_row(
let recorder = RecorderPopover::build(&row, field, sender); let recorder = RecorderPopover::build(&row, field, sender);
let text_editor = TextEditorPopover::build(&row, field, sender); let text_editor = TextEditorPopover::build(&row, field, sender);
let seen_chips = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new())); let seen_chips = Rc::new(RefCell::new(Vec::<String>::new()));
let seen_badges = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new())); let seen_badges = Rc::new(RefCell::new(Vec::<String>::new()));
let compact_mode = std::rc::Rc::new(std::cell::Cell::new(false)); let compact_mode = Rc::new(Cell::new(false));
let last_focus_serial = std::rc::Rc::new(std::cell::Cell::new(0)); let last_focus_serial = Rc::new(Cell::new(0));
{ {
let compact = compact.clone();
let inline_host = inline_host.clone();
let compact_mode = compact_mode.clone();
let editor_popover = editor_popover.clone();
let editor = editor.clone();
watch_compact(&row, move |is_compact| { watch_compact(&row, move |is_compact| {
compact_mode.set(is_compact); compact_mode.set(is_compact);
set_visible(&compact, is_compact); set_visible(&compact, is_compact);
@@ -2,6 +2,7 @@ use wayscriber::i18n::tr;
use relm4::{ComponentSender, gtk}; use relm4::{ComponentSender, gtk};
use gtk::glib::SignalHandlerId;
use gtk::glib::object::IsA; use gtk::glib::object::IsA;
use gtk::prelude::*; use gtk::prelude::*;
@@ -15,7 +16,7 @@ use super::widgets::{field_canceled, set_accessible_label, set_label, unparent_o
pub(super) struct TextEditorPopover { pub(super) struct TextEditorPopover {
pub popover: gtk::Popover, pub popover: gtk::Popover,
entry: gtk::Entry, entry: gtk::Entry,
entry_handler: gtk::glib::SignalHandlerId, entry_handler: SignalHandlerId,
error: gtk::Label, error: gtk::Label,
} }
@@ -2,7 +2,8 @@
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use relm4::{gtk, prelude::*}; use relm4::gtk;
use relm4::prelude::*;
use gtk::prelude::*; use gtk::prelude::*;
@@ -138,7 +139,7 @@ fn actions_bar(
row.append(&reset_all); row.append(&reset_all);
row.append(&review); row.append(&review);
let sort_for_bind = sort.clone(); let sort_for_bind = sort;
bindings.push(Box::new(move |app, _summary| { bindings.push(Box::new(move |app, _summary| {
let selected = ShortcutManagerSort::ALL let selected = ShortcutManagerSort::ALL
.iter() .iter()
@@ -1,7 +1,8 @@
use relm4::{ComponentSender, gtk}; use relm4::{ComponentSender, gtk};
use gtk::accessible::Property;
use gtk::glib::object::IsA; use gtk::glib::object::IsA;
use gtk::prelude::*; use gtk::prelude::{AccessibleExt, *};
use crate::messages::Message; use crate::messages::Message;
use crate::models::KeybindingField; use crate::models::KeybindingField;
@@ -20,8 +21,8 @@ pub(super) fn icon_button(icon: &str, tooltip: &str) -> gtk::Button {
button button
} }
pub(super) fn set_accessible_label(widget: &impl gtk::prelude::AccessibleExt, label: &str) { pub(super) fn set_accessible_label(widget: &impl AccessibleExt, label: &str) {
widget.update_property(&[gtk::accessible::Property::Label(label)]); widget.update_property(&[Property::Label(label)]);
} }
/// Writes the widget's own visibility flag, never `is_visible`: a widget /// Writes the widget's own visibility flag, never `is_visible`: a widget
+7 -5
View File
@@ -24,6 +24,8 @@ mod session;
mod tablet; mod tablet;
mod ui; mod ui;
use std::cell::Cell;
use relm4::prelude::*; use relm4::prelude::*;
use relm4::{adw, gtk}; use relm4::{adw, gtk};
@@ -49,7 +51,7 @@ use super::state::ConfiguratorApp;
pub(crate) type Binding = Box<dyn FnMut(&ConfiguratorApp, &AppSearchSummary)>; pub(crate) type Binding = Box<dyn FnMut(&ConfiguratorApp, &AppSearchSummary)>;
thread_local! { thread_local! {
static REPLACING_PAGES: std::cell::Cell<bool> = const { std::cell::Cell::new(false) }; static REPLACING_PAGES: Cell<bool> = const { Cell::new(false) };
} }
/// Marks the pages as being replaced for as long as it lives. /// Marks the pages as being replaced for as long as it lives.
@@ -76,7 +78,7 @@ impl Drop for ReplacingPages {
/// Whether the pages are being replaced right now; see [`ReplacingPages`]. /// Whether the pages are being replaced right now; see [`ReplacingPages`].
pub(crate) fn replacing_pages() -> bool { pub(crate) fn replacing_pages() -> bool {
REPLACING_PAGES.with(std::cell::Cell::get) REPLACING_PAGES.with(Cell::get)
} }
/// Writes editable text the model owns without reporting it as a user edit. /// Writes editable text the model owns without reporting it as a user edit.
@@ -349,7 +351,7 @@ impl PageBuilder {
title: &str, title: &str,
subtitle: &str, subtitle: &str,
values: Vec<O>, values: Vec<O>,
labels: Vec<String>, labels: &[String],
get: impl Fn(&ConfiguratorApp) -> O + 'static, get: impl Fn(&ConfiguratorApp) -> O + 'static,
to_message: impl Fn(O) -> Message + 'static, to_message: impl Fn(O) -> Message + 'static,
) -> &mut Self ) -> &mut Self
@@ -376,7 +378,7 @@ impl PageBuilder {
where where
O: Copy + PartialEq + 'static, O: Copy + PartialEq + 'static,
{ {
let row = self.add_combo_row(title, subtitle, values, labels, get, to_message); let row = self.add_combo_row(title, subtitle, values, &labels, get, to_message);
row.add_prefix(&gtk::Image::from_icon_name(icon_name)); row.add_prefix(&gtk::Image::from_icon_name(icon_name));
self self
} }
@@ -386,7 +388,7 @@ impl PageBuilder {
title: &str, title: &str,
subtitle: &str, subtitle: &str,
values: Vec<O>, values: Vec<O>,
labels: Vec<String>, labels: &[String],
get: impl Fn(&ConfiguratorApp) -> O + 'static, get: impl Fn(&ConfiguratorApp) -> O + 'static,
to_message: impl Fn(O) -> Message + 'static, to_message: impl Fn(O) -> Message + 'static,
) -> adw::ComboRow ) -> adw::ComboRow
+1 -1
View File
@@ -41,7 +41,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
buffer_metadata.label, buffer_metadata.label,
buffer_metadata.help, buffer_metadata.help,
buffer_counts, buffer_counts,
buffer_labels, &buffer_labels,
|app| app.draft.performance_buffer_count, |app| app.draft.performance_buffer_count,
Message::BufferCountChanged, Message::BufferCountChanged,
) )
+4 -4
View File
@@ -42,7 +42,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Visible slots"), tr("Visible slots"),
tr("How many preset slots the toolbar offers."), tr("How many preset slots the toolbar offers."),
counts, counts,
count_labels, &count_labels,
|app| app.draft.presets.slot_count, |app| app.draft.presets.slot_count,
Message::PresetSlotCountChanged, Message::PresetSlotCountChanged,
); );
@@ -187,7 +187,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
rows.combo_row( rows.combo_row(
tr("Tool"), tr("Tool"),
tools, tools,
tool_labels, &tool_labels,
move |app| app.draft.presets.slot(slot).map(|draft| draft.tool), move |app| app.draft.presets.slot(slot).map(|draft| draft.tool),
Message::PresetToolChanged, Message::PresetToolChanged,
); );
@@ -215,7 +215,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
rows.combo_row( rows.combo_row(
tr("Eraser kind"), tr("Eraser kind"),
kinds, kinds,
kind_labels, &kind_labels,
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_kind), move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_kind),
Message::PresetEraserKindChanged, Message::PresetEraserKindChanged,
); );
@@ -224,7 +224,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
rows.combo_row( rows.combo_row(
tr("Eraser mode"), tr("Eraser mode"),
modes, modes,
mode_labels, &mode_labels,
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_mode), move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_mode),
Message::PresetEraserModeChanged, Message::PresetEraserModeChanged,
); );
+2 -2
View File
@@ -14,7 +14,7 @@ pub(super) fn build_color_rows(rows: &mut SlotBuilder<'_>, slot: usize) {
let mode_row = rows.combo_row( let mode_row = rows.combo_row(
tr("Color mode"), tr("Color mode"),
vec![ColorMode::Named, ColorMode::Rgb], vec![ColorMode::Named, ColorMode::Rgb],
vec![tr("Named").to_string(), "RGB".to_string()], &[tr("Named").to_string(), "RGB".to_string()],
move |app| app.draft.presets.slot(slot).map(|draft| draft.color.mode), move |app| app.draft.presets.slot(slot).map(|draft| draft.color.mode),
Message::PresetColorModeChanged, Message::PresetColorModeChanged,
); );
@@ -52,7 +52,7 @@ pub(super) fn build_color_rows(rows: &mut SlotBuilder<'_>, slot: usize) {
let named_row = rows.combo_row( let named_row = rows.combo_row(
tr("Named color"), tr("Named color"),
named, named,
named_labels, &named_labels,
move |app| { move |app| {
app.draft app.draft
.presets .presets
+4 -4
View File
@@ -14,8 +14,8 @@ use super::super::{PageBuilder, set_selected_blocked, set_text_blocked};
/// Mirrors the [`PageBuilder`] row helpers, but adds rows to the slot's /// Mirrors the [`PageBuilder`] row helpers, but adds rows to the slot's
/// expander and reads the slot out of the model, so a slot the draft does /// expander and reads the slot out of the model, so a slot the draft does
/// not hold simply leaves its rows alone. /// not hold simply leaves its rows alone.
pub(super) struct SlotBuilder<'a> { pub(super) struct SlotBuilder<'page> {
pub(super) page: &'a mut PageBuilder, pub(super) page: &'page mut PageBuilder,
pub(super) expander: adw::ExpanderRow, pub(super) expander: adw::ExpanderRow,
pub(super) slot: usize, pub(super) slot: usize,
} }
@@ -69,7 +69,7 @@ impl SlotBuilder<'_> {
&mut self, &mut self,
title: &str, title: &str,
values: Vec<O>, values: Vec<O>,
labels: Vec<String>, labels: &[String],
get: impl Fn(&ConfiguratorApp) -> Option<O> + 'static, get: impl Fn(&ConfiguratorApp) -> Option<O> + 'static,
to_message: impl Fn(usize, O) -> Message + 'static, to_message: impl Fn(usize, O) -> Message + 'static,
) -> adw::ComboRow ) -> adw::ComboRow
@@ -117,7 +117,7 @@ impl SlotBuilder<'_> {
) -> adw::ComboRow { ) -> adw::ComboRow {
let values = OverrideOption::list(); let values = OverrideOption::list();
let labels = labels_of(&values, OverrideOption::label); let labels = labels_of(&values, OverrideOption::label);
self.combo_row(title, values, labels, get, move |slot, value| { self.combo_row(title, values, &labels, get, move |slot, value| {
Message::PresetToggleOptionChanged(slot, field, value) Message::PresetToggleOptionChanged(slot, field, value)
}) })
} }
@@ -79,7 +79,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Canvas export profile"), tr("Canvas export profile"),
"", "",
export_options, export_options,
export_labels, &export_labels,
|app| app.draft.render_profiles.export, |app| app.draft.render_profiles.export,
Message::RenderProfileExportChanged, Message::RenderProfileExportChanged,
); );
@@ -279,15 +279,15 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec<Sec
} }
/// One profile row's values: everything the layout deliberately left out. /// One profile row's values: everything the layout deliberately left out.
struct ProfileValues<'a> { struct ProfileValues<'draft> {
id: &'a str, id: &'draft str,
name: &'a str, name: &'draft str,
/// One entry per mapping row, in mapping order. /// One entry per mapping row, in mapping order.
mappings: Vec<MappingValues<'a>>, mappings: Vec<MappingValues<'draft>>,
} }
/// One mapping row's two editing hex fields. /// One mapping row's two editing hex fields.
struct MappingValues<'a> { struct MappingValues<'draft> {
from: &'a str, from: &'draft str,
to: &'a str, to: &'draft str,
} }
@@ -3,6 +3,7 @@ use wayscriber::i18n::tr;
use super::rows::{connect_button, plain_row, row_content_box, side_label}; use super::rows::{connect_button, plain_row, row_content_box, side_label};
use relm4::{ComponentSender, gtk}; use relm4::{ComponentSender, gtk};
use gtk::gdk::RGBA;
use gtk::glib::SignalHandlerId; use gtk::glib::SignalHandlerId;
use gtk::prelude::*; use gtk::prelude::*;
@@ -35,7 +36,7 @@ impl ColorField {
// rather than flashing it to black on every keystroke. // rather than flashing it to black on every keystroke.
return; return;
}; };
let rgba = gtk::gdk::RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0); let rgba = RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0);
set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba); set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba);
} }
} }
@@ -208,12 +208,12 @@ fn item_card(
let clear_was_armed = confirm.get_visible(); let clear_was_armed = confirm.get_visible();
let answer_has_focus = confirm_button.has_focus() || cancel_button.has_focus(); let answer_has_focus = confirm_button.has_focus() || cancel_button.has_focus();
let focus_after_refresh = clear_focus_after_refresh( let focus_after_refresh = clear_focus_after_refresh(ClearConfirmTransition {
clear_was_armed, was_armed: clear_was_armed,
values.clear_armed, is_armed: values.clear_armed,
values.clear_enabled, clear_enabled: values.clear_enabled,
answer_has_focus, answer_has_focus,
); });
if focus_after_refresh == ClearFocusTarget::Catalog { if focus_after_refresh == ClearFocusTarget::Catalog {
// Confirm enters the busy state and disables the row's actions. // Confirm enters the busy state and disables the row's actions.
// Temporarily make the stable page target focusable and move there // Temporarily make the stable page target focusable and move there
@@ -242,10 +242,10 @@ fn item_card(
CatalogRow { card, refresh } CatalogRow { card, refresh }
} }
struct InputRow<'a> { struct InputRow<'text> {
id: &'a str, id: &'text str,
placeholder: &'a str, placeholder: &'text str,
button_label: &'a str, button_label: &'text str,
on_input: fn(String, String) -> Message, on_input: fn(String, String) -> Message,
request: Message, request: Message,
} }
@@ -293,12 +293,27 @@ enum ClearFocusTarget {
Catalog, Catalog,
} }
fn clear_focus_after_refresh( /// The clear confirmation's state across one refresh.
#[derive(Debug, Clone, Copy, Default)]
struct ClearConfirmTransition {
/// The confirmation was showing before this refresh.
was_armed: bool, was_armed: bool,
/// The confirmation shows after this refresh.
is_armed: bool, is_armed: bool,
/// The clear action stays enabled after this refresh.
clear_enabled: bool, clear_enabled: bool,
/// Keyboard focus is on the confirm or cancel answer.
answer_has_focus: bool, answer_has_focus: bool,
) -> ClearFocusTarget { }
fn clear_focus_after_refresh(transition: ClearConfirmTransition) -> ClearFocusTarget {
let ClearConfirmTransition {
was_armed,
is_armed,
clear_enabled,
answer_has_focus,
} = transition;
if is_armed && !was_armed { if is_armed && !was_armed {
return ClearFocusTarget::Confirm; return ClearFocusTarget::Confirm;
} }
@@ -319,22 +334,38 @@ mod tests {
#[test] #[test]
fn clear_confirmation_focus_has_enabled_targets_for_every_transition() { fn clear_confirmation_focus_has_enabled_targets_for_every_transition() {
assert_eq!( assert_eq!(
clear_focus_after_refresh(false, true, true, false), clear_focus_after_refresh(ClearConfirmTransition {
is_armed: true,
clear_enabled: true,
..ClearConfirmTransition::default()
}),
ClearFocusTarget::Confirm, ClearFocusTarget::Confirm,
"arming focuses the newly revealed confirmation" "arming focuses the newly revealed confirmation"
); );
assert_eq!( assert_eq!(
clear_focus_after_refresh(true, false, true, true), clear_focus_after_refresh(ClearConfirmTransition {
was_armed: true,
clear_enabled: true,
answer_has_focus: true,
..ClearConfirmTransition::default()
}),
ClearFocusTarget::ClearAction, ClearFocusTarget::ClearAction,
"cancel returns to the restored clear action" "cancel returns to the restored clear action"
); );
assert_eq!( assert_eq!(
clear_focus_after_refresh(true, false, false, true), clear_focus_after_refresh(ClearConfirmTransition {
was_armed: true,
answer_has_focus: true,
..ClearConfirmTransition::default()
}),
ClearFocusTarget::Catalog, ClearFocusTarget::Catalog,
"confirm moves away from the row actions disabled by busy state" "confirm moves away from the row actions disabled by busy state"
); );
assert_eq!( assert_eq!(
clear_focus_after_refresh(true, false, false, false), clear_focus_after_refresh(ClearConfirmTransition {
was_armed: true,
..ClearConfirmTransition::default()
}),
ClearFocusTarget::Unchanged, ClearFocusTarget::Unchanged,
"a pointer-triggered transition does not steal keyboard focus" "a pointer-triggered transition does not steal keyboard focus"
); );
@@ -1,5 +1,9 @@
use std::path::PathBuf; use std::path::PathBuf;
use wayscriber::i18n;
use crate::test_env;
use super::*; use super::*;
use crate::app::state::PendingConfirmation; use crate::app::state::PendingConfirmation;
use crate::models::session::SessionArtifactSummary; use crate::models::session::SessionArtifactSummary;
@@ -182,7 +186,7 @@ fn catalog_focus_fallback_leaves_the_tab_chain_when_busy_work_finishes() {
/// chosen when the layout is drawn, and a new language rebuilds the card. /// chosen when the layout is drawn, and a new language rebuilds the card.
#[test] #[test]
fn card_labels_follow_the_language_the_list_is_drawn_in() { fn card_labels_follow_the_language_the_list_is_drawn_in() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::card_labels_follow_the_language_the_list_is_drawn_in" "::card_labels_follow_the_language_the_list_is_drawn_in"
@@ -193,7 +197,7 @@ fn card_labels_follow_the_language_the_list_is_drawn_in() {
let english = catalog_layout(&app); let english = catalog_layout(&app);
assert_eq!(english.items[0].last_opened_label, "Never"); assert_eq!(english.items[0].last_opened_label, "Never");
wayscriber::i18n::set_language(Language::SimplifiedChinese); i18n::set_language(Language::SimplifiedChinese);
let chinese = catalog_layout(&app); let chinese = catalog_layout(&app);
assert_eq!(chinese.items[0].last_opened_label, "从不"); assert_eq!(chinese.items[0].last_opened_label, "从不");
@@ -56,7 +56,7 @@ pub(super) fn add(page: &mut PageBuilder) {
tr("Storage mode"), tr("Storage mode"),
"", "",
SessionStorageModeOption::list(), SessionStorageModeOption::list(),
option_labels(SessionStorageModeOption::list(), |option| option.label()), &option_labels(&SessionStorageModeOption::list(), |option| option.label()),
|app| app.draft.session_storage_mode, |app| app.draft.session_storage_mode,
Message::SessionStorageModeChanged, Message::SessionStorageModeChanged,
); );
@@ -65,7 +65,7 @@ pub(super) fn add(page: &mut PageBuilder) {
tr("Compression"), tr("Compression"),
"", "",
SessionCompressionOption::list(), SessionCompressionOption::list(),
option_labels(SessionCompressionOption::list(), |option| option.label()), &option_labels(&SessionCompressionOption::list(), |option| option.label()),
|app| app.draft.session_compression, |app| app.draft.session_compression,
Message::SessionCompressionChanged, Message::SessionCompressionChanged,
) )
@@ -131,7 +131,7 @@ pub(super) fn add(page: &mut PageBuilder) {
); );
} }
fn option_labels<O: Copy>(options: Vec<O>, label: impl Fn(&O) -> &'static str) -> Vec<String> { fn option_labels<O: Copy>(options: &[O], label: impl Fn(&O) -> &'static str) -> Vec<String> {
options options
.iter() .iter()
.map(|option| label(option).to_string()) .map(|option| label(option).to_string())
+4 -4
View File
@@ -20,12 +20,12 @@ use super::{BuiltPage, PageBuilder};
pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage { pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
let edit_modes = PressureThicknessEditModeOption::list(); let edit_modes = PressureThicknessEditModeOption::list();
let edit_mode_labels = edit_modes let edit_mode_labels: Vec<String> = edit_modes
.iter() .iter()
.map(|mode| mode.label().to_string()) .map(|mode| mode.label().to_string())
.collect(); .collect();
let entry_modes = PressureThicknessEntryModeOption::list(); let entry_modes = PressureThicknessEntryModeOption::list();
let entry_mode_labels = entry_modes let entry_mode_labels: Vec<String> = entry_modes
.iter() .iter()
.map(|mode| mode.label().to_string()) .map(|mode| mode.label().to_string())
.collect(); .collect();
@@ -82,7 +82,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Pressure thickness edit mode"), tr("Pressure thickness edit mode"),
"", "",
edit_modes, edit_modes,
edit_mode_labels, &edit_mode_labels,
|app| app.draft.tablet_pressure_thickness_edit_mode, |app| app.draft.tablet_pressure_thickness_edit_mode,
Message::TabletPressureEditModeChanged, Message::TabletPressureEditModeChanged,
) )
@@ -90,7 +90,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Pressure thickness entry mode"), tr("Pressure thickness entry mode"),
"", "",
entry_modes, entry_modes,
entry_mode_labels, &entry_mode_labels,
|app| app.draft.tablet_pressure_thickness_entry_mode, |app| app.draft.tablet_pressure_thickness_entry_mode,
Message::TabletPressureEntryModeChanged, Message::TabletPressureEntryModeChanged,
); );
+5 -2
View File
@@ -1,4 +1,7 @@
use wayscriber::domain::Language; use wayscriber::domain::Language;
use wayscriber::i18n;
use crate::test_env;
use super::validate_u32_range; use super::validate_u32_range;
@@ -22,10 +25,10 @@ fn an_invalid_whole_number_names_the_actual_bounds() {
#[test] #[test]
fn the_bounds_survive_translation() { fn the_bounds_survive_translation() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!(module_path!(), "::the_bounds_survive_translation"), concat!(module_path!(), "::the_bounds_survive_translation"),
|| { || {
wayscriber::i18n::set_language(Language::SimplifiedChinese); i18n::set_language(Language::SimplifiedChinese);
assert_eq!( assert_eq!(
validate_u32_range("256", 0, 255).as_deref(), validate_u32_range("256", 0, 255).as_deref(),
Some("请输入 0 到 255 之间的整数。") Some("请输入 0 到 255 之间的整数。")
+2 -2
View File
@@ -32,7 +32,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Theme"), tr("Theme"),
tr("\"Auto\" uses dark. \"Light\" supports the command palette, including shortcut editing and tooltips; other popups still use dark."), tr("\"Auto\" uses dark. \"Light\" supports the command palette, including shortcut editing and tooltips; other popups still use dark."),
themes, themes,
theme_labels, &theme_labels,
|app| app.draft.ui_theme, |app| app.draft.ui_theme,
Message::UiThemeChanged, Message::UiThemeChanged,
) )
@@ -40,7 +40,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Reduced motion"), tr("Reduced motion"),
tr("\"On\" disables UI animations. \"Auto\" follows the system preference in a future release and keeps full motion for now."), tr("\"On\" disables UI animations. \"Auto\" follows the system preference in a future release and keeps full motion for now."),
motions, motions,
motion_labels, &motion_labels,
|app| app.draft.ui_reduced_motion, |app| app.draft.ui_reduced_motion,
Message::UiReducedMotionChanged, Message::UiReducedMotionChanged,
) )
+2 -2
View File
@@ -30,7 +30,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Input source"), tr("Input source"),
tr("\"Overlay only\" shows what Wayscriber itself receives. \"System-wide\" also shows input that goes to other apps; it needs a build with the input-monitor feature and read access to /dev/input (usually `input` group membership), and it sees every keystroke on the seat - including passwords typed elsewhere."), tr("\"Overlay only\" shows what Wayscriber itself receives. \"System-wide\" also shows input that goes to other apps; it needs a build with the input-monitor feature and read access to /dev/input (usually `input` group membership), and it sees every keystroke on the seat - including passwords typed elsewhere."),
modes, modes,
mode_labels, &mode_labels,
|app| app.draft.input_hud_mode, |app| app.draft.input_hud_mode,
Message::InputHudModeChanged, Message::InputHudModeChanged,
) )
@@ -38,7 +38,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Screen position"), tr("Screen position"),
"", "",
positions, positions,
position_labels, &position_labels,
|app| app.draft.input_hud_position, |app| app.draft.input_hud_position,
Message::InputHudPositionChanged, Message::InputHudPositionChanged,
) )
@@ -37,7 +37,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Top toolbar while presenting"), tr("Top toolbar while presenting"),
"", "",
toolbar_modes, toolbar_modes,
toolbar_mode_labels, &toolbar_mode_labels,
|app| app.draft.presenter_toolbar_mode, |app| app.draft.presenter_toolbar_mode,
Message::PresenterToolbarModeChanged, Message::PresenterToolbarModeChanged,
) )
@@ -69,7 +69,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Tool behavior"), tr("Tool behavior"),
"", "",
behaviors, behaviors,
behavior_labels, &behavior_labels,
|app| app.draft.presenter_tool_behavior, |app| app.draft.presenter_tool_behavior,
Message::PresenterToolBehaviorChanged, Message::PresenterToolBehaviorChanged,
) )
+1 -1
View File
@@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Status bar position"), tr("Status bar position"),
"", "",
positions, positions,
position_labels, &position_labels,
|app| app.draft.ui_status_position, |app| app.draft.ui_status_position,
Message::StatusPositionChanged, Message::StatusPositionChanged,
); );
+6 -6
View File
@@ -36,7 +36,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Layout mode"), tr("Layout mode"),
tr("Simple: core pens. Regular: adds Shape Pen, Line, Arrow, presets. Advanced: shapes inline, multi-step undo."), tr("Simple: core pens. Regular: adds Shape Pen, Line, Arrow, presets. Advanced: shapes inline, multi-step undo."),
layout_modes, layout_modes,
layout_labels, &layout_labels,
|app| app.draft.ui_toolbar_layout_mode, |app| app.draft.ui_toolbar_layout_mode,
Message::ToolbarLayoutModeChanged, Message::ToolbarLayoutModeChanged,
) )
@@ -44,7 +44,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Zoom chip"), tr("Zoom chip"),
"", "",
zoom_chips, zoom_chips,
zoom_chip_labels, &zoom_chip_labels,
|app| app.draft.ui_toolbar_zoom_chip_display, |app| app.draft.ui_toolbar_zoom_chip_display,
Message::ToolbarZoomChipDisplayChanged, Message::ToolbarZoomChipDisplayChanged,
) )
@@ -58,7 +58,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Shortcut edit click"), tr("Shortcut edit click"),
"", "",
rebinds, rebinds,
rebind_labels, &rebind_labels,
|app| app.draft.ui_toolbar_rebind_modifier, |app| app.draft.ui_toolbar_rebind_modifier,
Message::ToolbarRebindModifierChanged, Message::ToolbarRebindModifierChanged,
) )
@@ -66,7 +66,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Stroke controls"), tr("Stroke controls"),
tr("How the style pill shows pen smoothing and Shape Pen sensitivity. Panel: one Pen feel button opening meters with a live smoothing preview. Meters or Steppers: inline controls."), tr("How the style pill shows pen smoothing and Shape Pen sensitivity. Panel: one Pen feel button opening meters with a live smoothing preview. Meters or Steppers: inline controls."),
stroke_styles, stroke_styles,
stroke_style_labels, &stroke_style_labels,
|app| app.draft.ui_toolbar_stroke_controls, |app| app.draft.ui_toolbar_stroke_controls,
Message::ToolbarStrokeControlsChanged, Message::ToolbarStrokeControlsChanged,
) )
@@ -179,7 +179,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Edit mode"), tr("Edit mode"),
tr("Overrides below apply to the mode selected here; \"Default\" keeps the mode preset."), tr("Overrides below apply to the mode selected here; \"Default\" keeps the mode preset."),
override_modes, override_modes,
override_mode_labels, &override_mode_labels,
|app| app.override_mode, |app| app.override_mode,
Message::ToolbarOverrideModeChanged, Message::ToolbarOverrideModeChanged,
); );
@@ -198,7 +198,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
field.label(), field.label(),
"", "",
values, values,
labels, &labels,
move |app| toolbar_override(app, field), move |app| toolbar_override(app, field),
move |value| Message::ToolbarOverrideChanged(field, value), move |value| Message::ToolbarOverrideChanged(field, value),
); );
+20 -6
View File
@@ -1,10 +1,24 @@
use std::iter;
use wayscriber::domain::Language;
use crate::app::state::ConfiguratorApp; use crate::app::state::ConfiguratorApp;
use crate::models::{KeybindingsTabId, SearchQuery, TabId, UiTabId, keybinding_tab}; use crate::models::{KeybindingsTabId, SearchQuery, TabId, UiTabId, keybinding_tab};
use wayscriber::config::{ use wayscriber::config::{
PERFORMANCE_FIELD_METADATA, PerformanceFieldGroup, action_meta, toolbar_item_definitions, PERFORMANCE_FIELD_METADATA, PerformanceFieldGroup, action_meta, toolbar_item_definitions,
}; };
use super::terms::*; #[cfg(feature = "tablet-input")]
use super::terms::TABLET_TERMS;
use super::terms::{
ARROW_TERMS, BOARD_GENERAL_TERMS, CAPTURE_FILE_TERMS, CAPTURE_PDF_IDENTITY_TERMS,
CAPTURE_PDF_TERMS, CAPTURE_REGION_TERMS, DAEMON_LIGHT_TERMS, DAEMON_SERVICE_TERMS,
DAEMON_SHORTCUT_TERMS, DAEMON_STATUS_TERMS, DRAWING_COLOR_TERMS, DRAWING_DEFAULT_TERMS,
DRAWING_DRAG_TERMS, DRAWING_FONT_TERMS, DRAWING_LASER_TERMS, HISTORY_CUSTOM_TERMS,
HISTORY_MAIN_TERMS, PRESET_CONTROL_TERMS, RENDER_PROFILE_GENERAL_TERMS, SESSION_CATALOG_TERMS,
SESSION_PERSISTENCE_TERMS, UI_GENERAL_TERMS, tab_aliases, tab_scope_aliases, ui_tab_aliases,
ui_tab_terms,
};
use super::types::{AppSearchSummary, SearchArea, TabSearchSummary}; use super::types::{AppSearchSummary, SearchArea, TabSearchSummary};
pub(super) fn build_search_summary(app: &ConfiguratorApp) -> AppSearchSummary { pub(super) fn build_search_summary(app: &ConfiguratorApp) -> AppSearchSummary {
@@ -167,10 +181,10 @@ fn performance_matches(query: &SearchQuery, summary: &mut TabSearchSummary) {
] { ] {
let mut parts = vec![identity]; let mut parts = vec![identity];
parts.extend(area.section_titles()); parts.extend(area.section_titles());
for metadata in PERFORMANCE_FIELD_METADATA let group_fields = PERFORMANCE_FIELD_METADATA
.iter() .iter()
.filter(|metadata| metadata.group == group) .filter(|metadata| metadata.group == group);
{ for metadata in group_fields {
parts.extend([metadata.path, metadata.label, metadata.help]); parts.extend([metadata.path, metadata.label, metadata.help]);
parts.extend_from_slice(metadata.search_terms); parts.extend_from_slice(metadata.search_terms);
} }
@@ -187,7 +201,7 @@ fn ui_matches(query: &SearchQuery, summary: &mut TabSearchSummary) {
.iter() .iter()
.copied() .copied()
.chain( .chain(
wayscriber::domain::Language::ALL Language::ALL
.into_iter() .into_iter()
.flat_map(|language| [language.native_name(), language.word_for_language()]) .flat_map(|language| [language.native_name(), language.word_for_language()])
.flatten(), .flatten(),
@@ -468,7 +482,7 @@ impl ScopedSearchQuery for SearchQuery {
.chain(parts.iter().copied()), .chain(parts.iter().copied()),
) || tab_scope_aliases(tab) ) || tab_scope_aliases(tab)
.iter() .iter()
.any(|alias| self.matches_parts(std::iter::once(*alias).chain(parts.iter().copied()))) .any(|alias| self.matches_parts(iter::once(*alias).chain(parts.iter().copied())))
} }
fn matches_any_raw_text(&self, values: &[&str]) -> bool { fn matches_any_raw_text(&self, values: &[&str]) -> bool {
+9 -8
View File
@@ -5,6 +5,9 @@
//! that every control label a page shows is indexed by the search terms — //! that every control label a page shows is indexed by the search terms —
//! return with the GTK page registry, once every page is ported. //! return with the GTK page registry, once every page is ported.
use wayscriber::domain::Language;
use wayscriber::i18n;
use super::*; use super::*;
use std::path::PathBuf; use std::path::PathBuf;
@@ -12,6 +15,7 @@ use crate::models::session::SessionArtifactSummary;
use crate::models::{ use crate::models::{
KeybindingField, KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId, KeybindingField, KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId,
}; };
use crate::test_env;
use wayscriber::config::toolbar_item_ids as ids; use wayscriber::config::toolbar_item_ids as ids;
#[test] #[test]
@@ -856,13 +860,13 @@ fn catalog_item(id: &str) -> SessionCatalogItem {
/// found, and titles as displayed find their tab, sub-tab and section. /// found, and titles as displayed find their tab, sub-tab and section.
#[test] #[test]
fn chinese_ui_search_matches_english_terms_and_displayed_titles() { fn chinese_ui_search_matches_english_terms_and_displayed_titles() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::chinese_ui_search_matches_english_terms_and_displayed_titles" "::chinese_ui_search_matches_english_terms_and_displayed_titles"
), ),
|| { || {
wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); i18n::set_language(Language::SimplifiedChinese);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
let tabs_for = |app: &mut ConfiguratorApp, query: &str| { let tabs_for = |app: &mut ConfiguratorApp, query: &str| {
app.search_query = SearchQuery::new(query); app.search_query = SearchQuery::new(query);
@@ -889,17 +893,14 @@ fn chinese_ui_search_matches_english_terms_and_displayed_titles() {
#[test] #[test]
fn language_search_reveals_general_ui_in_either_language() { fn language_search_reveals_general_ui_in_either_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::language_search_reveals_general_ui_in_either_language" "::language_search_reveals_general_ui_in_either_language"
), ),
|| { || {
for language in [ for language in [Language::English, Language::SimplifiedChinese] {
wayscriber::domain::Language::English, i18n::set_language(language);
wayscriber::domain::Language::SimplifiedChinese,
] {
wayscriber::i18n::set_language(language);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
for query in ["language", "locale", "English", "简体中文", "中文", "语言"] { for query in ["language", "locale", "English", "简体中文", "中文", "语言"] {
app.search_query = SearchQuery::new(query); app.search_query = SearchQuery::new(query);
+22 -26
View File
@@ -1,3 +1,4 @@
use std::fs;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -17,6 +18,9 @@ mod move_file;
pub(super) use duplicate::duplicate_session_catalog_entry; pub(super) use duplicate::duplicate_session_catalog_entry;
pub(super) use move_file::move_session_catalog_entry; pub(super) use move_file::move_session_catalog_entry;
use wayscriber::config::{Config, LoadedConfig};
use wayscriber::session::{ClearToolStateOutcome, SessionOptions, catalog};
use wayscriber::{paths, session};
pub(super) async fn load_session_catalog() -> Result<Vec<SessionCatalogItem>, String> { pub(super) async fn load_session_catalog() -> Result<Vec<SessionCatalogItem>, String> {
run_blocking( run_blocking(
@@ -30,8 +34,7 @@ pub(super) async fn forget_session_catalog_entry(
id: String, id: String,
) -> Result<SessionCatalogActionResult, String> { ) -> Result<SessionCatalogActionResult, String> {
run_blocking(BlockingJobKind::SessionCatalogMutation, move || { run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
let removed = wayscriber::session::catalog::forget_session_by_id(&id) let removed = catalog::forget_session_by_id(&id).map_err(|err| err.to_string())?;
.map_err(|err| err.to_string())?;
let items = load_session_catalog_sync()?; let items = load_session_catalog_sync()?;
Ok(SessionCatalogActionResult::success( Ok(SessionCatalogActionResult::success(
if removed { if removed {
@@ -50,11 +53,8 @@ pub(super) async fn rename_session_catalog_entry(
display_name: String, display_name: String,
) -> Result<SessionCatalogActionResult, String> { ) -> Result<SessionCatalogActionResult, String> {
run_blocking(BlockingJobKind::SessionCatalogMutation, move || { run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
let renamed = wayscriber::session::catalog::rename_session_display_name_by_id( let renamed = catalog::rename_session_display_name_by_id(&id, display_name.trim())
&id, .map_err(|err| err.to_string())?;
display_name.trim(),
)
.map_err(|err| err.to_string())?;
let items = load_session_catalog_sync()?; let items = load_session_catalog_sync()?;
Ok(SessionCatalogActionResult::success( Ok(SessionCatalogActionResult::success(
if let Some(entry) = renamed { if let Some(entry) = renamed {
@@ -95,7 +95,7 @@ pub(super) async fn clear_session_catalog_entry(
return Err(blocker); return Err(blocker);
} }
let outcome = wayscriber::session::clear_named_session_non_lock_artifacts(&item.path) let outcome = session::clear_named_session_non_lock_artifacts(&item.path)
.map_err(|err| err.to_string())?; .map_err(|err| err.to_string())?;
let items = load_session_catalog_sync()?; let items = load_session_catalog_sync()?;
Ok(SessionCatalogActionResult::success( Ok(SessionCatalogActionResult::success(
@@ -125,8 +125,7 @@ pub(super) async fn clear_session_catalog_tool_state_entry(
} }
let options = named_session_options_for_catalog_item(&item)?; let options = named_session_options_for_catalog_item(&item)?;
let outcome = let outcome = session::clear_tool_state(&options).map_err(|err| err.to_string())?;
wayscriber::session::clear_tool_state(&options).map_err(|err| err.to_string())?;
let items = load_session_catalog_sync()?; let items = load_session_catalog_sync()?;
Ok(SessionCatalogActionResult::success( Ok(SessionCatalogActionResult::success(
clear_tool_state_catalog_message(&item.display_name, outcome), clear_tool_state_catalog_message(&item.display_name, outcome),
@@ -144,7 +143,7 @@ fn service_status_blocker(
} }
fn load_session_catalog_sync() -> Result<Vec<SessionCatalogItem>, String> { fn load_session_catalog_sync() -> Result<Vec<SessionCatalogItem>, String> {
wayscriber::session::catalog::recent_sessions() catalog::recent_sessions()
.map_err(|err| err.to_string())? .map_err(|err| err.to_string())?
.into_iter() .into_iter()
.map(SessionCatalogItem::from_entry) .map(SessionCatalogItem::from_entry)
@@ -160,10 +159,10 @@ fn find_session_catalog_item(id: &str) -> Result<SessionCatalogItem, String> {
fn named_session_options_for_catalog_item( fn named_session_options_for_catalog_item(
item: &SessionCatalogItem, item: &SessionCatalogItem,
) -> Result<wayscriber::session::SessionOptions, String> { ) -> Result<SessionOptions, String> {
let loaded = wayscriber::config::Config::load().map_err(|err| err.to_string())?; let loaded = Config::load().map_err(|err| err.to_string())?;
ensure_session_config_available_for_destructive_action(&loaded)?; ensure_session_config_available_for_destructive_action(&loaded)?;
let mut options = wayscriber::session::options_from_config_for_named_file( let mut options = session::options_from_config_for_named_file(
&loaded.config.session, &loaded.config.session,
item.path.clone(), item.path.clone(),
None, None,
@@ -173,7 +172,7 @@ fn named_session_options_for_catalog_item(
} }
fn ensure_session_config_available_for_destructive_action( fn ensure_session_config_available_for_destructive_action(
loaded: &wayscriber::config::LoadedConfig, loaded: &LoadedConfig,
) -> Result<(), String> { ) -> Result<(), String> {
if loaded.section_failed("session") { if loaded.section_failed("session") {
return Err( return Err(
@@ -184,24 +183,21 @@ fn ensure_session_config_available_for_destructive_action(
Ok(()) Ok(())
} }
fn clear_tool_state_catalog_message( fn clear_tool_state_catalog_message(display_name: &str, outcome: ClearToolStateOutcome) -> String {
display_name: &str,
outcome: wayscriber::session::ClearToolStateOutcome,
) -> String {
match outcome { match outcome {
wayscriber::session::ClearToolStateOutcome::NoSession => { ClearToolStateOutcome::NoSession => {
trf("No saved session file found for {}.", &[&display_name]) trf("No saved session file found for {}.", &[&display_name])
} }
wayscriber::session::ClearToolStateOutcome::NoToolState => { ClearToolStateOutcome::NoToolState => {
trf("No saved tool state found for {}.", &[&display_name]) trf("No saved tool state found for {}.", &[&display_name])
} }
wayscriber::session::ClearToolStateOutcome::Cleared { ClearToolStateOutcome::Cleared {
preserved_board_data: true, preserved_board_data: true,
} => trf( } => trf(
tr("Cleared saved tool state for {}. Boards and history were preserved."), tr("Cleared saved tool state for {}. Boards and history were preserved."),
&[&display_name], &[&display_name],
), ),
wayscriber::session::ClearToolStateOutcome::Cleared { ClearToolStateOutcome::Cleared {
preserved_board_data: false, preserved_board_data: false,
} => trf( } => trf(
"Cleared saved tool state for {}. No board data was present.", "Cleared saved tool state for {}. No board data was present.",
@@ -238,8 +234,8 @@ enum RuntimeLockKind {
impl RuntimeLockKind { impl RuntimeLockKind {
fn path(self) -> PathBuf { fn path(self) -> PathBuf {
match self { match self {
Self::Daemon => wayscriber::paths::daemon_lock_file(), Self::Daemon => paths::daemon_lock_file(),
Self::Overlay => wayscriber::paths::overlay_lock_file(), Self::Overlay => paths::overlay_lock_file(),
} }
} }
@@ -320,7 +316,7 @@ fn acquire_runtime_lock_for_inactive_operation(
) -> Result<File, String> { ) -> Result<File, String> {
let path = kind.path(); let path = kind.path();
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|err| { fs::create_dir_all(parent).map_err(|err| {
format!( format!(
"{} is disabled because the {} lock directory could not be created: {} ({err})", "{} is disabled because the {} lock directory could not be created: {} ({err})",
operation.label(), operation.label(),
@@ -1,4 +1,7 @@
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use wayscriber::session;
use wayscriber::session::catalog;
use wayscriber::session::catalog::CatalogEvent;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -39,11 +42,11 @@ fn duplicate_session_catalog_entry_sync(
return Err(blocker); return Err(blocker);
} }
let outcome = wayscriber::session::duplicate_named_session_primary(&item.path, target) let outcome = session::duplicate_named_session_primary(&item.path, target)
.map_err(|err| err.to_string())?; .map_err(|err| err.to_string())?;
let entry = match wayscriber::session::catalog::upsert_session_event_with_display_name( let entry = match catalog::upsert_session_event_with_display_name(
&outcome.target, &outcome.target,
wayscriber::session::catalog::CatalogEvent::Saved, CatalogEvent::Saved,
&item.display_name, &item.display_name,
) { ) {
Ok(entry) => entry, Ok(entry) => entry,
@@ -81,11 +84,14 @@ fn duplicate_session_catalog_entry_sync(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::ffi::OsString; use std::ffi::OsString;
use std::fs;
use crate::models::{ use crate::models::{
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
ShortcutApplyCapability, ShortcutBackend, ShortcutApplyCapability, ShortcutBackend,
}; };
use crate::test_env::EnvGuard;
use crate::test_temp;
use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV}; use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV};
use super::*; use super::*;
@@ -110,22 +116,22 @@ mod tests {
#[test] #[test]
fn duplicate_session_catalog_entry_copies_primary_and_catalogs_new_entry() { fn duplicate_session_catalog_entry_copies_primary_and_catalogs_new_entry() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[ let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]); ]);
let source = temp.path().join("lecture.wayscriber-session"); let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("lecture-copy.wayscriber-session"); let target = temp.path().join("lecture-copy.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); let source_artifacts = session::named_session_artifact_paths(&source);
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target); let target_artifacts = session::named_session_artifact_paths(&target);
std::fs::write(&source_artifacts.primary, b"primary").unwrap(); fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::write(&source_artifacts.backup, b"backup").unwrap(); fs::write(&source_artifacts.backup, b"backup").unwrap();
std::fs::write(&source_artifacts.lock, b"lock").unwrap(); fs::write(&source_artifacts.lock, b"lock").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( let source_entry = catalog::upsert_session_event_with_display_name(
&source, &source,
wayscriber::session::catalog::CatalogEvent::Saved, CatalogEvent::Saved,
"Lecture", "Lecture",
) )
.unwrap(); .unwrap();
@@ -134,14 +140,11 @@ mod tests {
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.unwrap(); .unwrap();
assert_eq!( assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary");
std::fs::read(&target_artifacts.primary).unwrap(),
b"primary"
);
assert!(!target_artifacts.backup.exists()); assert!(!target_artifacts.backup.exists());
assert!(!target_artifacts.lock.exists()); assert!(!target_artifacts.lock.exists());
assert!(result.message.contains("Duplicated Lecture")); assert!(result.message.contains("Duplicated Lecture"));
let recents = wayscriber::session::catalog::recent_sessions().unwrap(); let recents = catalog::recent_sessions().unwrap();
assert_eq!(recents.len(), 2); assert_eq!(recents.len(), 2);
assert_eq!( assert_eq!(
recents recents
@@ -155,52 +158,47 @@ mod tests {
#[test] #[test]
fn duplicate_session_catalog_entry_warns_when_catalog_update_fails_after_copy() { fn duplicate_session_catalog_entry_warns_when_catalog_update_fails_after_copy() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[ let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]); ]);
let source = temp.path().join("lecture.wayscriber-session"); let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("lecture-copy.wayscriber-session"); let target = temp.path().join("lecture-copy.wayscriber-session");
std::fs::write(&source, b"primary").unwrap(); fs::write(&source, b"primary").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( let source_entry = catalog::upsert_session_event_with_display_name(
&source, &source,
wayscriber::session::catalog::CatalogEvent::Saved, CatalogEvent::Saved,
"Lecture", "Lecture",
) )
.unwrap(); .unwrap();
let lock_path = catalog_lock_path(); let lock_path = catalog_lock_path();
std::fs::remove_file(&lock_path).unwrap(); fs::remove_file(&lock_path).unwrap();
std::fs::create_dir(&lock_path).unwrap(); fs::create_dir(&lock_path).unwrap();
let result = let result =
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.unwrap(); .unwrap();
std::fs::remove_dir(&lock_path).unwrap(); fs::remove_dir(&lock_path).unwrap();
assert!(result.warning); assert!(result.warning);
assert!( assert!(
result result
.message .message
.contains("failed to update the session catalog") .contains("failed to update the session catalog")
); );
assert_eq!(std::fs::read(&target).unwrap(), b"primary"); assert_eq!(fs::read(&target).unwrap(), b"primary");
assert_eq!( assert_eq!(
result.items.len(), result.items.len(),
1, 1,
"warning should keep the pre-copy catalog rows visible" "warning should keep the pre-copy catalog rows visible"
); );
assert_eq!( assert_eq!(catalog::recent_sessions().unwrap().len(), 1);
wayscriber::session::catalog::recent_sessions()
.unwrap()
.len(),
1
);
} }
fn catalog_lock_path() -> std::path::PathBuf { fn catalog_lock_path() -> PathBuf {
let catalog_path = wayscriber::session::catalog::catalog_path(); let catalog_path = catalog::catalog_path();
let mut raw = OsString::from(catalog_path.as_os_str()); let mut raw = OsString::from(catalog_path.as_os_str());
raw.push(".lock"); raw.push(".lock");
raw.into() raw.into()
@@ -1,4 +1,6 @@
use wayscriber::i18n::{tr, trf}; use wayscriber::i18n::{tr, trf};
use wayscriber::session;
use wayscriber::session::{NamedSessionMoveOutcome, catalog};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -41,29 +43,27 @@ fn move_session_catalog_entry_sync(
return Err(blocker); return Err(blocker);
} }
let outcome = wayscriber::session::move_named_session_non_lock_artifacts(&item.path, target) let outcome = session::move_named_session_non_lock_artifacts(&item.path, target)
.map_err(|err| err.to_string())?; .map_err(|err| err.to_string())?;
let entry = let entry = match catalog::move_session_path_by_id(&item.id, &outcome.target) {
match wayscriber::session::catalog::move_session_path_by_id(&item.id, &outcome.target) { Ok(Some(entry)) => entry,
Ok(Some(entry)) => entry, Ok(None) => {
Ok(None) => { return rollback_moved_artifacts_after_catalog_failure(
return rollback_moved_artifacts_after_catalog_failure( &item,
&item, &outcome,
&outcome, initial_items,
initial_items, tr("the original catalog entry disappeared before metadata could be updated"),
tr("the original catalog entry disappeared before metadata could be updated") );
.to_string(), }
); Err(err) => {
} return rollback_moved_artifacts_after_catalog_failure(
Err(err) => { &item,
return rollback_moved_artifacts_after_catalog_failure( &outcome,
&item, initial_items,
&outcome, &format!("failed to update the session catalog: {err}"),
initial_items, );
format!("failed to update the session catalog: {err}"), }
); };
}
};
let items = match load_session_catalog_sync() { let items = match load_session_catalog_sync() {
Ok(items) => items, Ok(items) => items,
Err(err) => { Err(err) => {
@@ -88,11 +88,11 @@ fn move_session_catalog_entry_sync(
fn rollback_moved_artifacts_after_catalog_failure( fn rollback_moved_artifacts_after_catalog_failure(
item: &SessionCatalogItem, item: &SessionCatalogItem,
outcome: &wayscriber::session::NamedSessionMoveOutcome, outcome: &NamedSessionMoveOutcome,
initial_items: Vec<SessionCatalogItem>, initial_items: Vec<SessionCatalogItem>,
reason: String, reason: &str,
) -> Result<SessionCatalogActionResult, String> { ) -> Result<SessionCatalogActionResult, String> {
match wayscriber::session::rollback_named_session_non_lock_artifacts_move(outcome) { match session::rollback_named_session_non_lock_artifacts_move(outcome) {
Ok(_) => Err(trf( Ok(_) => Err(trf(
"Move Session failed because {}; rolled back moved files for {}.", "Move Session failed because {}; rolled back moved files for {}.",
&[&reason, &item.display_name], &[&reason, &item.display_name],
@@ -118,10 +118,10 @@ fn reject_catalog_target_collision(
source_id: &str, source_id: &str,
target: &Path, target: &Path,
) -> Result<(), String> { ) -> Result<(), String> {
if let Some(existing) = items.iter().find(|item| { if let Some(existing) = items
item.id != source_id .iter()
&& wayscriber::session::catalog::session_paths_match(&item.path, target) .find(|item| item.id != source_id && catalog::session_paths_match(&item.path, target))
}) { {
return Err(trf( return Err(trf(
"Move Session target is already in the catalog as {}.", "Move Session target is already in the catalog as {}.",
&[&existing.display_name], &[&existing.display_name],
@@ -133,11 +133,16 @@ fn reject_catalog_target_collision(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::ffi::OsString; use std::ffi::OsString;
use std::fs;
use wayscriber::session::catalog::CatalogEvent;
use crate::models::{ use crate::models::{
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
ShortcutApplyCapability, ShortcutBackend, ShortcutApplyCapability, ShortcutBackend,
}; };
use crate::test_env::EnvGuard;
use crate::test_temp;
use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV}; use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV};
use super::*; use super::*;
@@ -162,22 +167,22 @@ mod tests {
#[test] #[test]
fn move_session_catalog_entry_moves_artifacts_and_preserves_catalog_id() { fn move_session_catalog_entry_moves_artifacts_and_preserves_catalog_id() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[ let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]); ]);
let source = temp.path().join("lecture.wayscriber-session"); let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); let source_artifacts = session::named_session_artifact_paths(&source);
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target); let target_artifacts = session::named_session_artifact_paths(&target);
std::fs::write(&source_artifacts.primary, b"primary").unwrap(); fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::write(&source_artifacts.backup, b"backup").unwrap(); fs::write(&source_artifacts.backup, b"backup").unwrap();
std::fs::write(&source_artifacts.lock, b"source lock").unwrap(); fs::write(&source_artifacts.lock, b"source lock").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( let source_entry = catalog::upsert_session_event_with_display_name(
&source, &source,
wayscriber::session::catalog::CatalogEvent::Saved, CatalogEvent::Saved,
"Lecture", "Lecture",
) )
.unwrap(); .unwrap();
@@ -186,18 +191,12 @@ mod tests {
move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()).unwrap(); move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()).unwrap();
assert!(!result.warning); assert!(!result.warning);
assert_eq!( assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary");
std::fs::read(&target_artifacts.primary).unwrap(), assert_eq!(fs::read(&target_artifacts.backup).unwrap(), b"backup");
b"primary" assert_eq!(fs::read(&source_artifacts.lock).unwrap(), b"source lock");
);
assert_eq!(std::fs::read(&target_artifacts.backup).unwrap(), b"backup");
assert_eq!(
std::fs::read(&source_artifacts.lock).unwrap(),
b"source lock"
);
assert!(!source_artifacts.primary.exists()); assert!(!source_artifacts.primary.exists());
assert!(!source_artifacts.backup.exists()); assert!(!source_artifacts.backup.exists());
let recents = wayscriber::session::catalog::recent_sessions().unwrap(); let recents = catalog::recent_sessions().unwrap();
assert_eq!(recents.len(), 1); assert_eq!(recents.len(), 1);
assert_eq!(recents[0].id, source_entry.id); assert_eq!(recents[0].id, source_entry.id);
assert_eq!(Path::new(&recents[0].path), target.as_path()); assert_eq!(Path::new(&recents[0].path), target.as_path());
@@ -205,22 +204,22 @@ mod tests {
#[test] #[test]
fn move_session_catalog_entry_failure_keeps_catalog_and_source_artifacts() { fn move_session_catalog_entry_failure_keeps_catalog_and_source_artifacts() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[ let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]); ]);
let source = temp.path().join("lecture.wayscriber-session"); let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); let source_artifacts = session::named_session_artifact_paths(&source);
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target); let target_artifacts = session::named_session_artifact_paths(&target);
std::fs::write(&source_artifacts.primary, b"primary").unwrap(); fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::write(&source_artifacts.backup, b"backup").unwrap(); fs::write(&source_artifacts.backup, b"backup").unwrap();
std::fs::write(&target_artifacts.clear_marker, b"cleared").unwrap(); fs::write(&target_artifacts.clear_marker, b"cleared").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( let source_entry = catalog::upsert_session_event_with_display_name(
&source, &source,
wayscriber::session::catalog::CatalogEvent::Saved, CatalogEvent::Saved,
"Lecture", "Lecture",
) )
.unwrap(); .unwrap();
@@ -229,16 +228,13 @@ mod tests {
.expect_err("target artifact should block move"); .expect_err("target artifact should block move");
assert!(err.contains("already has session artifacts")); assert!(err.contains("already has session artifacts"));
assert_eq!(fs::read(&source_artifacts.primary).unwrap(), b"primary");
assert_eq!(fs::read(&source_artifacts.backup).unwrap(), b"backup");
assert_eq!( assert_eq!(
std::fs::read(&source_artifacts.primary).unwrap(), fs::read(&target_artifacts.clear_marker).unwrap(),
b"primary"
);
assert_eq!(std::fs::read(&source_artifacts.backup).unwrap(), b"backup");
assert_eq!(
std::fs::read(&target_artifacts.clear_marker).unwrap(),
b"cleared" b"cleared"
); );
let recents = wayscriber::session::catalog::recent_sessions().unwrap(); let recents = catalog::recent_sessions().unwrap();
assert_eq!(recents.len(), 1); assert_eq!(recents.len(), 1);
assert_eq!(recents[0].id, source_entry.id); assert_eq!(recents[0].id, source_entry.id);
assert_eq!(Path::new(&recents[0].path), source.as_path()); assert_eq!(Path::new(&recents[0].path), source.as_path());
@@ -246,75 +242,71 @@ mod tests {
#[test] #[test]
fn move_session_catalog_entry_rejects_catalog_target_collision_before_disk_move() { fn move_session_catalog_entry_rejects_catalog_target_collision_before_disk_move() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[ let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]); ]);
let source = temp.path().join("lecture.wayscriber-session"); let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session");
std::fs::write(&source, b"primary").unwrap(); fs::write(&source, b"primary").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( let source_entry = catalog::upsert_session_event_with_display_name(
&source, &source,
wayscriber::session::catalog::CatalogEvent::Saved, CatalogEvent::Saved,
"Lecture", "Lecture",
) )
.unwrap(); .unwrap();
wayscriber::session::catalog::upsert_session_event_with_display_name( catalog::upsert_session_event_with_display_name(&target, CatalogEvent::Saved, "Archive")
&target, .unwrap();
wayscriber::session::catalog::CatalogEvent::Saved,
"Archive",
)
.unwrap();
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.expect_err("target catalog entry should block move"); .expect_err("target catalog entry should block move");
assert!(err.contains("already in the catalog")); assert!(err.contains("already in the catalog"));
assert_eq!(std::fs::read(&source).unwrap(), b"primary"); assert_eq!(fs::read(&source).unwrap(), b"primary");
assert!(!target.exists()); assert!(!target.exists());
} }
#[test] #[test]
fn move_session_catalog_entry_rolls_back_when_catalog_update_fails_after_move() { fn move_session_catalog_entry_rolls_back_when_catalog_update_fails_after_move() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[ let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]); ]);
let source = temp.path().join("lecture.wayscriber-session"); let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); let source_artifacts = session::named_session_artifact_paths(&source);
std::fs::write(&source_artifacts.primary, b"primary").unwrap(); fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::create_dir(&source_artifacts.backup).unwrap(); fs::create_dir(&source_artifacts.backup).unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( let source_entry = catalog::upsert_session_event_with_display_name(
&source, &source,
wayscriber::session::catalog::CatalogEvent::Saved, CatalogEvent::Saved,
"Lecture", "Lecture",
) )
.unwrap(); .unwrap();
let lock_path = catalog_lock_path(); let lock_path = catalog_lock_path();
std::fs::remove_file(&lock_path).unwrap(); fs::remove_file(&lock_path).unwrap();
std::fs::create_dir(&lock_path).unwrap(); fs::create_dir(&lock_path).unwrap();
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.expect_err("catalog update failure should roll back disk move"); .expect_err("catalog update failure should roll back disk move");
std::fs::remove_dir(&lock_path).unwrap(); fs::remove_dir(&lock_path).unwrap();
assert!(err.contains("failed to update the session catalog")); assert!(err.contains("failed to update the session catalog"));
assert!(err.contains("rolled back moved files")); assert!(err.contains("rolled back moved files"));
assert_eq!(std::fs::read(&source).unwrap(), b"primary"); assert_eq!(fs::read(&source).unwrap(), b"primary");
assert!(source_artifacts.backup.is_dir()); assert!(source_artifacts.backup.is_dir());
assert!(!target.exists()); assert!(!target.exists());
let recents = wayscriber::session::catalog::recent_sessions().unwrap(); let recents = catalog::recent_sessions().unwrap();
assert_eq!(recents.len(), 1); assert_eq!(recents.len(), 1);
assert_eq!(Path::new(&recents[0].path), source.as_path()); assert_eq!(Path::new(&recents[0].path), source.as_path());
} }
fn catalog_lock_path() -> PathBuf { fn catalog_lock_path() -> PathBuf {
let catalog_path = wayscriber::session::catalog::catalog_path(); let catalog_path = catalog::catalog_path();
let mut raw = OsString::from(catalog_path.as_os_str()); let mut raw = OsString::from(catalog_path.as_os_str());
raw.push(".lock"); raw.push(".lock");
raw.into() raw.into()
+33 -28
View File
@@ -1,9 +1,14 @@
use wayscriber::config::{ConfigSectionError, ConfigSource, ConfigValidationReport};
use super::*; use super::*;
use std::path::PathBuf; use std::path::PathBuf;
use crate::models::session::SessionArtifactSummary;
use crate::models::{ use crate::models::{
DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend,
}; };
use crate::test_env::EnvGuard;
use crate::test_temp;
use wayscriber::env_vars::XDG_RUNTIME_DIR_ENV; use wayscriber::env_vars::XDG_RUNTIME_DIR_ENV;
fn daemon_status(active: bool) -> DaemonRuntimeStatus { fn daemon_status(active: bool) -> DaemonRuntimeStatus {
@@ -26,8 +31,8 @@ fn daemon_status(active: bool) -> DaemonRuntimeStatus {
#[test] #[test]
fn clear_policy_blocks_running_daemon() { fn clear_policy_blocks_running_daemon() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let status = daemon_status(true); let status = daemon_status(true);
let blocker = SessionCatalogOperation::Clear let blocker = SessionCatalogOperation::Clear
@@ -39,8 +44,8 @@ fn clear_policy_blocks_running_daemon() {
#[test] #[test]
fn clear_policy_allows_inactive_daemon() { fn clear_policy_allows_inactive_daemon() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let status = daemon_status(false); let status = daemon_status(false);
let blocker = SessionCatalogOperation::Clear.cached_status_blocker(Some(&status)); let blocker = SessionCatalogOperation::Clear.cached_status_blocker(Some(&status));
@@ -50,8 +55,8 @@ fn clear_policy_allows_inactive_daemon() {
#[test] #[test]
fn duplicate_policy_uses_duplicate_status_message() { fn duplicate_policy_uses_duplicate_status_message() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::Duplicate let blocker = SessionCatalogOperation::Duplicate
.cached_status_blocker(None) .cached_status_blocker(None)
.expect("unknown status should block duplicate"); .expect("unknown status should block duplicate");
@@ -61,8 +66,8 @@ fn duplicate_policy_uses_duplicate_status_message() {
#[test] #[test]
fn move_policy_uses_move_status_message() { fn move_policy_uses_move_status_message() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::Move let blocker = SessionCatalogOperation::Move
.cached_status_blocker(None) .cached_status_blocker(None)
.expect("unknown status should block move"); .expect("unknown status should block move");
@@ -72,8 +77,8 @@ fn move_policy_uses_move_status_message() {
#[test] #[test]
fn clear_policy_blocks_unknown_daemon_status() { fn clear_policy_blocks_unknown_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::Clear let blocker = SessionCatalogOperation::Clear
.cached_status_blocker(None) .cached_status_blocker(None)
.expect("unknown status should block clear"); .expect("unknown status should block clear");
@@ -83,8 +88,8 @@ fn clear_policy_blocks_unknown_daemon_status() {
#[test] #[test]
fn clear_tool_state_policy_uses_tool_state_status_message() { fn clear_tool_state_policy_uses_tool_state_status_message() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::ClearToolState let blocker = SessionCatalogOperation::ClearToolState
.cached_status_blocker(None) .cached_status_blocker(None)
.expect("unknown status should block tool reset"); .expect("unknown status should block tool reset");
@@ -95,8 +100,8 @@ fn clear_tool_state_policy_uses_tool_state_status_message() {
#[test] #[test]
fn session_clear_transaction_guard_blocks_manual_daemon_lock() { fn session_clear_transaction_guard_blocks_manual_daemon_lock() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
let error = acquire_runtime_lock_for_inactive_operation( let error = acquire_runtime_lock_for_inactive_operation(
RuntimeLockKind::Daemon, RuntimeLockKind::Daemon,
@@ -109,8 +114,8 @@ fn session_clear_transaction_guard_blocks_manual_daemon_lock() {
#[test] #[test]
fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock() { fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let reservation = reserve_inactive_session_operation(SessionCatalogOperation::Clear).unwrap(); let reservation = reserve_inactive_session_operation(SessionCatalogOperation::Clear).unwrap();
@@ -130,8 +135,8 @@ fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock()
#[test] #[test]
fn inactive_session_reservation_refuses_a_running_daemon() { fn inactive_session_reservation_refuses_a_running_daemon() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
let error = match reserve_inactive_session_operation(SessionCatalogOperation::Move) { let error = match reserve_inactive_session_operation(SessionCatalogOperation::Move) {
@@ -148,8 +153,8 @@ fn inactive_session_reservation_refuses_a_running_daemon() {
#[test] #[test]
fn cached_status_blocker_does_not_probe_runtime_locks() { fn cached_status_blocker_does_not_probe_runtime_locks() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap(); let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap();
let status = daemon_status(false); let status = daemon_status(false);
@@ -164,8 +169,8 @@ fn cached_status_blocker_does_not_probe_runtime_locks() {
#[test] #[test]
fn clear_runtime_guards_hold_daemon_and_overlay_locks() { fn clear_runtime_guards_hold_daemon_and_overlay_locks() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap(); let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap();
@@ -190,7 +195,7 @@ fn session_artifact_status_reports_size_when_present() {
created_at_millis: 1_767_261_600_000, created_at_millis: 1_767_261_600_000,
last_opened_at_millis: None, last_opened_at_millis: None,
last_saved_at_millis: None, last_saved_at_millis: None,
artifacts: crate::models::session::SessionArtifactSummary { artifacts: SessionArtifactSummary {
primary_exists: true, primary_exists: true,
backup_exists: false, backup_exists: false,
recovery_exists: false, recovery_exists: false,
@@ -206,11 +211,11 @@ fn session_artifact_status_reports_size_when_present() {
#[test] #[test]
fn clear_tool_state_rejects_defaulted_session_config() { fn clear_tool_state_rejects_defaulted_session_config() {
let loaded = wayscriber::config::LoadedConfig { let loaded = LoadedConfig {
config: wayscriber::config::Config::default(), config: Config::default(),
source: wayscriber::config::ConfigSource::Primary, source: ConfigSource::Primary,
validation: wayscriber::config::ConfigValidationReport::default(), validation: ConfigValidationReport::default(),
section_errors: vec![wayscriber::config::ConfigSectionError { section_errors: vec![ConfigSectionError {
section: "session".to_string(), section: "session".to_string(),
error: "invalid value".to_string(), error: "invalid value".to_string(),
}], }],
+4 -2
View File
@@ -1,4 +1,6 @@
//! Shortcut edit modes are mutually exclusive and clear as one workflow. //! Shortcut edit modes are mutually exclusive and clear as one workflow.
use std::mem;
use crate::models::{ use crate::models::{
KeybindingField, PendingShortcutConflict, ShortcutRecorderState, ShortcutTextEditor, KeybindingField, PendingShortcutConflict, ShortcutRecorderState, ShortcutTextEditor,
}; };
@@ -59,7 +61,7 @@ impl ShortcutWorkflow {
} }
pub(crate) fn take_recorder(&mut self) -> Option<ShortcutRecorderState> { pub(crate) fn take_recorder(&mut self) -> Option<ShortcutRecorderState> {
self.recorder()?; self.recorder()?;
match std::mem::take(&mut self.phase) { match mem::take(&mut self.phase) {
ShortcutPhase::Recording(value) => Some(value), ShortcutPhase::Recording(value) => Some(value),
_ => None, _ => None,
} }
@@ -98,7 +100,7 @@ impl ShortcutWorkflow {
} }
pub(crate) fn take_conflict(&mut self) -> Option<PendingShortcutConflict> { pub(crate) fn take_conflict(&mut self) -> Option<PendingShortcutConflict> {
self.conflict()?; self.conflict()?;
match std::mem::take(&mut self.phase) { match mem::take(&mut self.phase) {
ShortcutPhase::Conflict(value) => Some(value), ShortcutPhase::Conflict(value) => Some(value),
_ => None, _ => None,
} }
+24 -23
View File
@@ -1,6 +1,8 @@
use std::mem;
use wayscriber::configurator_destination::ConfiguratorScreen; use wayscriber::configurator_destination::ConfiguratorScreen;
use crate::models::{SearchQuery, TabId, UiTabId}; use crate::models::{SearchQuery, TabId, UiTabId, keybindings};
use super::effects::Effect; use super::effects::Effect;
use super::state::ConfiguratorApp; use super::state::ConfiguratorApp;
@@ -12,7 +14,7 @@ impl ConfiguratorApp {
/// Reload is the user asking for the file again, not a fresh launch, and /// Reload is the user asking for the file again, not a fresh launch, and
/// snapping the tabs back would undo wherever they navigated in between. /// snapping the tabs back would undo wherever they navigated in between.
pub(crate) fn apply_startup_request(&mut self) -> Vec<Effect> { pub(crate) fn apply_startup_request(&mut self) -> Vec<Effect> {
let request = std::mem::take(&mut self.startup_request); let request = mem::take(&mut self.startup_request);
if let Some(problem) = request.problem() { if let Some(problem) = request.problem() {
self.status = self.status.clone().with_note(&problem.message()); self.status = self.status.clone().with_note(&problem.message());
@@ -40,8 +42,7 @@ impl ConfiguratorApp {
// nothing to show lands where the matches actually are. // nothing to show lands where the matches actually are.
self.align_active_tabs_for_search(); self.align_active_tabs_for_search();
if self.active_tab == TabId::Keybindings if self.active_tab == TabId::Keybindings
&& let Some(field) = && let Some(field) = keybindings::field_matching_search_term(&term)
crate::models::keybindings::field_matching_search_term(&term)
{ {
self.select_keybinding_field(field); self.select_keybinding_field(field);
} }
@@ -102,11 +103,18 @@ impl ConfiguratorApp {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path; use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::{fs, iter};
use wayscriber::config::ConfigDocument; use wayscriber::config::ConfigDocument;
use wayscriber::configurator_destination;
use wayscriber::configurator_destination::{ConfiguratorDestination, KeybindingsSection}; use wayscriber::configurator_destination::{ConfiguratorDestination, KeybindingsSection};
use crate::app::search::SearchArea;
use crate::models::KeybindingField;
use crate::test_env;
use super::*; use super::*;
use crate::app::state::StatusMessage; use crate::app::state::StatusMessage;
use crate::messages::CommandMessage; use crate::messages::CommandMessage;
@@ -117,18 +125,18 @@ mod tests {
status.text().unwrap_or_default().to_string() status.text().unwrap_or_default().to_string()
} }
fn args(values: &[&str]) -> Vec<std::ffi::OsString> { fn args(values: &[&str]) -> Vec<OsString> {
std::iter::once("wayscriber-configurator") iter::once("wayscriber-configurator")
.chain(values.iter().copied()) .chain(values.iter().copied())
.map(std::ffi::OsString::from) .map(OsString::from)
.collect() .collect()
} }
/// A config file of its own, so the load path under test is the real one. /// A config file of its own, so the load path under test is the real one.
fn config_file() -> (TempDir, std::path::PathBuf) { fn config_file() -> (TempDir, PathBuf) {
let dir = tempdir().expect("temporary test directory"); let dir = tempdir().expect("temporary test directory");
let path = dir.path().join("config.toml"); let path = dir.path().join("config.toml");
std::fs::write(&path, "").expect("write test config"); fs::write(&path, "").expect("write test config");
(dir, path) (dir, path)
} }
@@ -140,8 +148,8 @@ mod tests {
} }
/// The app as the launcher would start it, after its first config load. /// The app as the launcher would start it, after its first config load.
fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, std::path::PathBuf) { fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, PathBuf) {
crate::test_env::sanitize_language_environment(); test_env::sanitize_language_environment();
let (dir, path) = config_file(); let (dir, path) = config_file();
let (mut app, _effects) = let (mut app, _effects) =
ConfiguratorApp::new_app_with_startup(StartupRequest::from_args(args(values))); ConfiguratorApp::new_app_with_startup(StartupRequest::from_args(args(values)));
@@ -237,8 +245,7 @@ mod tests {
#[test] #[test]
fn the_onboarding_hint_destination_lands_on_the_general_ui_setting() { fn the_onboarding_hint_destination_lands_on_the_general_ui_setting() {
let launched = let launched = configurator_destination::onboarding_hints_destination().as_arg();
wayscriber::configurator_destination::onboarding_hints_destination().as_arg();
let (app, _dir, _path) = app_launched_with(&["--open", &launched]); let (app, _dir, _path) = app_launched_with(&["--open", &launched]);
assert_eq!(app.active_tab, TabId::Ui); assert_eq!(app.active_tab, TabId::Ui);
@@ -247,7 +254,7 @@ mod tests {
let ui = summary let ui = summary
.tab(TabId::Ui) .tab(TabId::Ui)
.expect("General UI destination match"); .expect("General UI destination match");
assert!(ui.area_matches(crate::app::search::SearchArea::UiGeneral)); assert!(ui.area_matches(SearchArea::UiGeneral));
} }
/// The shortcut-action row: subtab plus action search. /// The shortcut-action row: subtab plus action search.
@@ -259,10 +266,7 @@ mod tests {
assert_eq!(app.active_tab, TabId::Keybindings); assert_eq!(app.active_tab, TabId::Keybindings);
assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing); assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing);
assert_eq!(app.search_query.raw(), "Clear Canvas"); assert_eq!(app.search_query.raw(), "Clear Canvas");
assert_eq!( assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas));
app.selected_keybinding,
Some(crate::models::KeybindingField::ClearCanvas)
);
assert!(app.search_summary().tab(TabId::Keybindings).is_some()); assert!(app.search_summary().tab(TabId::Keybindings).is_some());
} }
@@ -275,10 +279,7 @@ mod tests {
assert_eq!(app.active_tab, TabId::Keybindings); assert_eq!(app.active_tab, TabId::Keybindings);
assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing); assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing);
assert_eq!( assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas));
app.selected_keybinding,
Some(crate::models::KeybindingField::ClearCanvas)
);
} }
/// A term with no match on the requested screen is the one case where /// A term with no match on the requested screen is the one case where
+5 -2
View File
@@ -3,12 +3,15 @@ use wayscriber::i18n::tr;
use std::collections::HashMap; use std::collections::HashMap;
use wayscriber::config::{Config, MigrationPreview, PRESET_SLOTS_MAX}; use wayscriber::config::{Config, MigrationPreview, PRESET_SLOTS_MAX};
use wayscriber::i18n;
use crate::models::{ use crate::models::{
ColorPickerId, ConfigDraft, DesktopEnvironment, DragMouseButton, KeybindingField, ColorPickerId, ConfigDraft, DesktopEnvironment, DragMouseButton, KeybindingField,
KeybindingsTabId, SearchQuery, SessionCatalogState, ShortcutManagerFilter, ShortcutManagerSort, KeybindingsTabId, SearchQuery, SessionCatalogState, ShortcutManagerFilter, ShortcutManagerSort,
StartupRequest, TabId, ToolbarLayoutModeOption, UiTabId, StartupRequest, TabId, ToolbarLayoutModeOption, UiTabId,
}; };
#[cfg(test)]
use crate::test_env;
use super::effects::Effect; use super::effects::Effect;
@@ -222,7 +225,7 @@ impl ConfiguratorApp {
/// about to draw. A transition that fails keeps the language it found. /// about to draw. A transition that fails keeps the language it found.
/// The shell notices the change and rebuilds its pages in that language. /// The shell notices the change and rebuilds its pages in that language.
pub(crate) fn apply_draft_language(&self) { pub(crate) fn apply_draft_language(&self) {
wayscriber::i18n::set_language(self.draft.ui_language.to_language()); i18n::set_language(self.draft.ui_language.to_language());
} }
/// The app as a launch with no destination. /// The app as a launch with no destination.
@@ -231,7 +234,7 @@ impl ConfiguratorApp {
/// when it is the empty one. /// when it is the empty one.
#[cfg(test)] #[cfg(test)]
pub(crate) fn new_app() -> (Self, Vec<Effect>) { pub(crate) fn new_app() -> (Self, Vec<Effect>) {
crate::test_env::sanitize_language_environment(); test_env::sanitize_language_environment();
Self::new_app_with_startup(StartupRequest::default()) Self::new_app_with_startup(StartupRequest::default())
} }
+3 -1
View File
@@ -1,3 +1,5 @@
use wayscriber::domain::BoardGridKind;
use crate::models::{ use crate::models::{
BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorPickerId, BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorPickerId,
}; };
@@ -9,7 +11,7 @@ impl ConfiguratorApp {
pub(super) fn handle_boards_grid_kind_changed( pub(super) fn handle_boards_grid_kind_changed(
&mut self, &mut self,
index: usize, index: usize,
value: wayscriber::domain::BoardGridKind, value: BoardGridKind,
) -> Vec<Effect> { ) -> Vec<Effect> {
if let Some(item) = self.draft.boards.items.get_mut(index) { if let Some(item) = self.draft.boards.items.get_mut(index) {
item.grid_kind = value; item.grid_kind = value;
+14 -13
View File
@@ -15,9 +15,10 @@ impl ConfiguratorApp {
value: String, value: String,
) -> Vec<Effect> { ) -> Vec<Effect> {
self.status = StatusMessage::idle(); self.status = StatusMessage::idle();
self.color_picker_hex.insert(id, value.clone()); let parsed = parse_hex(&value);
self.color_picker_hex.insert(id, value);
if let Some((rgb, alpha)) = parse_hex(&value) { if let Some((rgb, alpha)) = parsed {
let alpha = if id.uses_alpha() { let alpha = if id.uses_alpha() {
alpha.or_else(|| self.current_alpha_for_id(id)) alpha.or_else(|| self.current_alpha_for_id(id))
} else { } else {
@@ -169,41 +170,41 @@ impl ConfiguratorApp {
} }
} }
ColorPickerId::StatusBarBg => { ColorPickerId::StatusBarBg => {
self.apply_quad_rgb(QuadField::StatusBarBg, values, alpha); self.apply_quad_rgb(QuadField::StatusBarBg, &values, alpha);
} }
ColorPickerId::StatusBarText => { ColorPickerId::StatusBarText => {
self.apply_quad_rgb(QuadField::StatusBarText, values, alpha); self.apply_quad_rgb(QuadField::StatusBarText, &values, alpha);
} }
ColorPickerId::HighlightFill => { ColorPickerId::HighlightFill => {
self.apply_quad_rgb(QuadField::HighlightFill, values, alpha); self.apply_quad_rgb(QuadField::HighlightFill, &values, alpha);
} }
ColorPickerId::HighlightOutline => { ColorPickerId::HighlightOutline => {
self.apply_quad_rgb(QuadField::HighlightOutline, values, alpha); self.apply_quad_rgb(QuadField::HighlightOutline, &values, alpha);
} }
ColorPickerId::LaserColor => { ColorPickerId::LaserColor => {
self.apply_quad_rgb(QuadField::LaserColor, values, alpha); self.apply_quad_rgb(QuadField::LaserColor, &values, alpha);
} }
ColorPickerId::HelpBg => { ColorPickerId::HelpBg => {
self.apply_quad_rgb(QuadField::HelpBg, values, alpha); self.apply_quad_rgb(QuadField::HelpBg, &values, alpha);
} }
ColorPickerId::HelpBorder => { ColorPickerId::HelpBorder => {
self.apply_quad_rgb(QuadField::HelpBorder, values, alpha); self.apply_quad_rgb(QuadField::HelpBorder, &values, alpha);
} }
ColorPickerId::HelpText => { ColorPickerId::HelpText => {
self.apply_quad_rgb(QuadField::HelpText, values, alpha); self.apply_quad_rgb(QuadField::HelpText, &values, alpha);
} }
ColorPickerId::ExportPdfLabelText => { ColorPickerId::ExportPdfLabelText => {
self.apply_quad_rgb(QuadField::ExportPdfLabelText, values, alpha); self.apply_quad_rgb(QuadField::ExportPdfLabelText, &values, alpha);
} }
ColorPickerId::ExportPdfLabelBackground => { ColorPickerId::ExportPdfLabelBackground => {
self.apply_quad_rgb(QuadField::ExportPdfLabelBackground, values, alpha); self.apply_quad_rgb(QuadField::ExportPdfLabelBackground, &values, alpha);
} }
} }
self.sync_color_picker_hex_for_id(id); self.sync_color_picker_hex_for_id(id);
} }
fn apply_quad_rgb(&mut self, field: QuadField, values: [String; 3], alpha: Option<f64>) { fn apply_quad_rgb(&mut self, field: QuadField, values: &[String; 3], alpha: Option<f64>) {
for (component, value) in values.iter().enumerate() { for (component, value) in values.iter().enumerate() {
self.draft.set_quad(field, component, value.to_string()); self.draft.set_quad(field, component, value.to_string());
} }
+2 -1
View File
@@ -2,6 +2,7 @@ use wayscriber::i18n::{tr, trf};
use wayscriber::config::ConfigDocument; use wayscriber::config::ConfigDocument;
use crate::app::document_workflow::LeaveAction;
use crate::models::ConfigDraft; use crate::models::ConfigDraft;
use super::super::super::effects::Effect; use super::super::super::effects::Effect;
@@ -60,7 +61,7 @@ impl ConfiguratorApp {
} }
pub(in crate::app::update) fn handle_reload_requested(&mut self) -> Vec<Effect> { pub(in crate::app::update) fn handle_reload_requested(&mut self) -> Vec<Effect> {
self.request_leave(crate::app::document_workflow::LeaveAction::Reload) self.request_leave(LeaveAction::Reload)
} }
pub(in crate::app::update) fn begin_config_reload(&mut self) -> Vec<Effect> { pub(in crate::app::update) fn begin_config_reload(&mut self) -> Vec<Effect> {
+21 -17
View File
@@ -1,7 +1,11 @@
use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use wayscriber::config;
use wayscriber::config::{Action, CURRENT_CONFIG_REVISION, Config, ConfigDocument}; use wayscriber::config::{Action, CURRENT_CONFIG_REVISION, Config, ConfigDocument};
use crate::test_temp;
use super::save::save_validation_errors; use super::save::save_validation_errors;
use super::*; use super::*;
use crate::app::effects::Effect; use crate::app::effects::Effect;
@@ -16,9 +20,9 @@ fn status_contains(status: &StatusMessage, needle: &str) -> bool {
} }
fn temp_config_document(name: &str, contents: &str) -> (TempDir, Box<ConfigDocument>) { fn temp_config_document(name: &str, contents: &str) -> (TempDir, Box<ConfigDocument>) {
let temp = crate::test_temp::tempdir().expect("temporary config directory"); let temp = test_temp::tempdir().expect("temporary config directory");
let path = temp.path().join(format!("{name}.toml")); let path = temp.path().join(format!("{name}.toml"));
std::fs::write(&path, contents).expect("write test config"); fs::write(&path, contents).expect("write test config");
let document = ConfigDocument::load_from_path(&path).expect("load test config document"); let document = ConfigDocument::load_from_path(&path).expect("load test config document");
(temp, Box::new(document)) (temp, Box::new(document))
} }
@@ -128,7 +132,7 @@ fn handle_config_loaded_surfaces_resolved_shortcut_conflicts() {
"shortcut-conflict", "shortcut-conflict",
&format!( &format!(
"config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\"]\ncycle_toolbar_display = [\"F2\"]\n", "config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\"]\ncycle_toolbar_display = [\"F2\"]\n",
wayscriber::config::CURRENT_CONFIG_REVISION config::CURRENT_CONFIG_REVISION
), ),
); );
@@ -155,7 +159,7 @@ fn handle_config_loaded_surfaces_skipped_default_shortcuts() {
"skipped-default", "skipped-default",
&format!( &format!(
"config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\", \"F9\"]\n", "config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\", \"F9\"]\n",
wayscriber::config::CURRENT_CONFIG_REVISION config::CURRENT_CONFIG_REVISION
), ),
); );
@@ -185,7 +189,7 @@ fn handle_config_loaded_surfaces_shortcuts_that_could_not_be_parsed() {
"invalid-shortcut", "invalid-shortcut",
&format!( &format!(
"config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\n", "config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\n",
wayscriber::config::CURRENT_CONFIG_REVISION config::CURRENT_CONFIG_REVISION
), ),
); );
@@ -211,7 +215,7 @@ fn handle_config_loaded_separates_every_keybinding_diagnostic_kind() {
"invalid-and-conflicting", "invalid-and-conflicting",
&format!( &format!(
"config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\ntoggle_toolbar = [\"F2\", \"F9\"]\nundo = [\"Ctrl+Alt+U\"]\nredo = [\"Ctrl+Alt+U\"]\n", "config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\ntoggle_toolbar = [\"F2\", \"F9\"]\nundo = [\"Ctrl+Alt+U\"]\nredo = [\"Ctrl+Alt+U\"]\n",
wayscriber::config::CURRENT_CONFIG_REVISION config::CURRENT_CONFIG_REVISION
), ),
); );
@@ -900,9 +904,9 @@ const LEGACY_REVISION_ZERO_CONFIG: &str = "config_revision = 0\n\n[drawing]\ndef
/// A config file of its own, in a directory the test owns: an applied /// A config file of its own, in a directory the test owns: an applied
/// migration saves, and a save drops its `.bak` next to the file. /// migration saves, and a save drops its `.bak` next to the file.
fn app_with_config_file(contents: &str) -> (ConfiguratorApp, TempDir, PathBuf) { fn app_with_config_file(contents: &str) -> (ConfiguratorApp, TempDir, PathBuf) {
let dir = crate::test_temp::tempdir().expect("temporary test directory"); let dir = test_temp::tempdir().expect("temporary test directory");
let path = dir.path().join("config.toml"); let path = dir.path().join("config.toml");
std::fs::write(&path, contents).expect("write test config"); fs::write(&path, contents).expect("write test config");
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
load_config_file(&mut app, &path); load_config_file(&mut app, &path);
(app, dir, path) (app, dir, path)
@@ -954,7 +958,7 @@ fn config_setting(contents: &str, key: &str) -> Option<String> {
} }
fn read_config(path: &Path) -> String { fn read_config(path: &Path) -> String {
std::fs::read_to_string(path).expect("read the saved config") fs::read_to_string(path).expect("read the saved config")
} }
/// A file that is valid TOML but holds one value serde cannot map used to /// A file that is valid TOML but holds one value serde cannot map used to
@@ -963,9 +967,9 @@ fn read_config(path: &Path) -> String {
/// defaults instead of calling the file unparseable, and Save keeps the rest. /// defaults instead of calling the file unparseable, and Save keeps the rest.
#[test] #[test]
fn a_file_with_one_unreadable_value_loads_its_other_settings_and_saves_them_back() { fn a_file_with_one_unreadable_value_loads_its_other_settings_and_saves_them_back() {
let dir = crate::test_temp::tempdir().expect("temporary test directory"); let dir = test_temp::tempdir().expect("temporary test directory");
let path = dir.path().join("config.toml"); let path = dir.path().join("config.toml");
std::fs::write( fs::write(
&path, &path,
"# mine\n[drawing]\ndefault_thickness = 7.0\n\n[keybindings]\nundo = \"Ctrl+Z\"\n", "# mine\n[drawing]\ndefault_thickness = 7.0\n\n[keybindings]\nundo = \"Ctrl+Z\"\n",
) )
@@ -1061,7 +1065,7 @@ fn applying_and_saving_writes_the_reviewed_fields_the_revision_and_a_backup() {
let backup = save_draft(&mut app).expect("the save creates a backup"); let backup = save_draft(&mut app).expect("the save creates a backup");
assert_eq!( assert_eq!(
std::fs::read_to_string(&backup).expect("read the backup"), fs::read_to_string(&backup).expect("read the backup"),
LEGACY_REVISION_ZERO_CONFIG, LEGACY_REVISION_ZERO_CONFIG,
"the backup holds the file as it was before the migration" "the backup holds the file as it was before the migration"
); );
@@ -1332,11 +1336,11 @@ fn a_fully_customized_apply_records_the_revision_without_reoffering() {
fn dismissal_follows_the_file_not_the_path_across_a_retarget() { fn dismissal_follows_the_file_not_the_path_across_a_retarget() {
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
let dir = crate::test_temp::tempdir().expect("temporary test directory"); let dir = test_temp::tempdir().expect("temporary test directory");
let first = dir.path().join("profile-a.toml"); let first = dir.path().join("profile-a.toml");
let second = dir.path().join("profile-b.toml"); let second = dir.path().join("profile-b.toml");
std::fs::write(&first, LEGACY_REVISION_ZERO_CONFIG).expect("write the first profile"); fs::write(&first, LEGACY_REVISION_ZERO_CONFIG).expect("write the first profile");
std::fs::write(&second, LEGACY_REVISION_ZERO_CONFIG).expect("write the second profile"); fs::write(&second, LEGACY_REVISION_ZERO_CONFIG).expect("write the second profile");
let link = dir.path().join("config.toml"); let link = dir.path().join("config.toml");
symlink(&first, &link).expect("link the config path at the first profile"); symlink(&first, &link).expect("link the config path at the first profile");
@@ -1353,7 +1357,7 @@ fn dismissal_follows_the_file_not_the_path_across_a_retarget() {
"the same file reloaded is not the user asking again" "the same file reloaded is not the user asking again"
); );
std::fs::remove_file(&link).expect("unlink the config path"); fs::remove_file(&link).expect("unlink the config path");
symlink(&second, &link).expect("retarget the config path at the second profile"); symlink(&second, &link).expect("retarget the config path at the second profile");
load_config_file(&mut app, &link); load_config_file(&mut app, &link);
@@ -1380,7 +1384,7 @@ fn an_empty_or_missing_file_never_offers_a_migration() {
let (app, _dir, _path) = app_with_config_file(""); let (app, _dir, _path) = app_with_config_file("");
assert!(app.pending_migration().is_none()); assert!(app.pending_migration().is_none());
let dir = crate::test_temp::tempdir().expect("temporary test directory"); let dir = test_temp::tempdir().expect("temporary test directory");
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
load_config_file(&mut app, &dir.path().join("missing.toml")); load_config_file(&mut app, &dir.path().join("missing.toml"));
@@ -5,9 +5,13 @@
//! it selects reaches no other test. The override and locale variables are //! it selects reaches no other test. The override and locale variables are
//! cleared there, so `auto` resolves to English. //! cleared there, so `auto` resolves to English.
use std::fs;
use wayscriber::domain::Language; use wayscriber::domain::Language;
use wayscriber::i18n; use wayscriber::i18n;
use crate::test_env;
use super::{app_with_config_file, load_config_file, save_draft, status_contains}; use super::{app_with_config_file, load_config_file, save_draft, status_contains};
use crate::app::effects::Effect; use crate::app::effects::Effect;
use crate::models::LanguageOption; use crate::models::LanguageOption;
@@ -17,7 +21,7 @@ const CHINESE_FILE: &str = "[ui]\nlanguage = \"zh-CN\"\n";
#[test] #[test]
fn the_first_load_applies_the_saved_language() { fn the_first_load_applies_the_saved_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::the_first_load_applies_the_saved_language" "::the_first_load_applies_the_saved_language"
@@ -35,7 +39,7 @@ fn the_first_load_applies_the_saved_language() {
#[test] #[test]
fn selecting_a_language_previews_it_without_writing() { fn selecting_a_language_previews_it_without_writing() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::selecting_a_language_previews_it_without_writing" "::selecting_a_language_previews_it_without_writing"
@@ -55,7 +59,7 @@ fn selecting_a_language_previews_it_without_writing() {
#[test] #[test]
fn a_successful_reload_applies_the_loaded_language() { fn a_successful_reload_applies_the_loaded_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::a_successful_reload_applies_the_loaded_language" "::a_successful_reload_applies_the_loaded_language"
@@ -78,7 +82,7 @@ fn a_successful_reload_applies_the_loaded_language() {
#[test] #[test]
fn a_failed_reload_keeps_the_preview() { fn a_failed_reload_keeps_the_preview() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!(module_path!(), "::a_failed_reload_keeps_the_preview"), concat!(module_path!(), "::a_failed_reload_keeps_the_preview"),
|| { || {
let (mut app, _dir, _path) = app_with_config_file(ENGLISH_FILE); let (mut app, _dir, _path) = app_with_config_file(ENGLISH_FILE);
@@ -97,7 +101,7 @@ fn a_failed_reload_keeps_the_preview() {
#[test] #[test]
fn confirmed_defaults_apply_the_default_language() { fn confirmed_defaults_apply_the_default_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::confirmed_defaults_apply_the_default_language" "::confirmed_defaults_apply_the_default_language"
@@ -122,7 +126,7 @@ fn confirmed_defaults_apply_the_default_language() {
#[test] #[test]
fn a_canceled_defaults_request_keeps_the_language() { fn a_canceled_defaults_request_keeps_the_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::a_canceled_defaults_request_keeps_the_language" "::a_canceled_defaults_request_keeps_the_language"
@@ -140,7 +144,7 @@ fn a_canceled_defaults_request_keeps_the_language() {
#[test] #[test]
fn a_finished_save_applies_the_saved_language() { fn a_finished_save_applies_the_saved_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::a_finished_save_applies_the_saved_language" "::a_finished_save_applies_the_saved_language"
@@ -162,7 +166,7 @@ fn a_finished_save_applies_the_saved_language() {
#[test] #[test]
fn canceling_the_leave_prompt_keeps_the_preview() { fn canceling_the_leave_prompt_keeps_the_preview() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::canceling_the_leave_prompt_keeps_the_preview" "::canceling_the_leave_prompt_keeps_the_preview"
@@ -183,7 +187,7 @@ fn canceling_the_leave_prompt_keeps_the_preview() {
#[test] #[test]
fn discarding_to_reload_applies_the_document_that_loads() { fn discarding_to_reload_applies_the_document_that_loads() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::discarding_to_reload_applies_the_document_that_loads" "::discarding_to_reload_applies_the_document_that_loads"
@@ -196,7 +200,7 @@ fn discarding_to_reload_applies_the_document_that_loads() {
let effects = app.discard_before_leave(); let effects = app.discard_before_leave();
assert!(matches!(effects.as_slice(), [Effect::LoadConfig])); assert!(matches!(effects.as_slice(), [Effect::LoadConfig]));
// Another editor saved in the meantime: what loads is what counts. // Another editor saved in the meantime: what loads is what counts.
std::fs::write(&path, CHINESE_FILE).expect("rewrite config"); fs::write(&path, CHINESE_FILE).expect("rewrite config");
load_config_file(&mut app, &path); load_config_file(&mut app, &path);
assert_eq!(app.draft.ui_language, LanguageOption::SimplifiedChinese); assert_eq!(app.draft.ui_language, LanguageOption::SimplifiedChinese);
+1 -1
View File
@@ -194,7 +194,7 @@ mod tests {
}] }]
)); ));
let _ = app.handle_daemon_action_completed(Ok(DaemonActionResult { let _ = app.handle_daemon_action_completed(Ok(DaemonActionResult {
status: new_status.clone(), status: new_status,
message: "refresh complete".to_string(), message: "refresh complete".to_string(),
partial_failure: false, partial_failure: false,
})); }));
@@ -1,3 +1,5 @@
use std::cmp::Ordering;
use crate::models::{ use crate::models::{
ArrowStyleOption, ColorMode, ColorPickerId, DragColorOption, DragMouseButton, DragToolField, ArrowStyleOption, ColorMode, ColorPickerId, DragColorOption, DragMouseButton, DragToolField,
DragToolOption, EraserModeOption, NamedColorOption, DragToolOption, EraserModeOption, NamedColorOption,
@@ -108,9 +110,9 @@ impl ConfiguratorApp {
self.status = StatusMessage::idle(); self.status = StatusMessage::idle();
if self.draft.drawing_quick_colors.remove_entry(index) { if self.draft.drawing_quick_colors.remove_entry(index) {
self.remap_quick_color_pickers(|picker_index| match picker_index.cmp(&index) { self.remap_quick_color_pickers(|picker_index| match picker_index.cmp(&index) {
std::cmp::Ordering::Less => Some(picker_index), Ordering::Less => Some(picker_index),
std::cmp::Ordering::Equal => None, Ordering::Equal => None,
std::cmp::Ordering::Greater => picker_index.checked_sub(1), Ordering::Greater => picker_index.checked_sub(1),
}); });
self.refresh_dirty_flag(); self.refresh_dirty_flag();
} }
@@ -178,7 +180,7 @@ impl ConfiguratorApp {
pub(in crate::app::update) fn handle_font_cycle_changed( pub(in crate::app::update) fn handle_font_cycle_changed(
&mut self, &mut self,
index: usize, index: usize,
family: String, family: &str,
) -> Vec<Effect> { ) -> Vec<Effect> {
match self.draft.drawing_font_cycle.set(index, family) { match self.draft.drawing_font_cycle.set(index, family) {
Ok(changed) => { Ok(changed) => {
+4 -2
View File
@@ -1,3 +1,5 @@
use wayscriber::draw;
use super::*; use super::*;
use wayscriber::config::{ColorSpec, Config}; use wayscriber::config::{ColorSpec, Config};
@@ -164,7 +166,7 @@ fn choosing_a_duplicate_font_keeps_the_list_and_explains_the_rejection() {
config.drawing.font_cycle = vec!["Sans".to_string(), "Serif".to_string()]; config.drawing.font_cycle = vec!["Sans".to_string(), "Serif".to_string()];
app.draft = ConfigDraft::from_config(&config); app.draft = ConfigDraft::from_config(&config);
let _ = app.handle_font_cycle_changed(1, "sans".to_string()); let _ = app.handle_font_cycle_changed(1, "sans");
assert_eq!(app.draft.drawing_font_cycle.entries(), ["Sans", "Serif"]); assert_eq!(app.draft.drawing_font_cycle.entries(), ["Sans", "Serif"]);
let feedback = app let feedback = app
@@ -179,7 +181,7 @@ fn choosing_a_duplicate_font_keeps_the_list_and_explains_the_rejection() {
fn add_font_never_creates_a_blank_or_duplicate_when_the_catalog_is_exhausted() { fn add_font_never_creates_a_blank_or_duplicate_when_the_catalog_is_exhausted() {
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
let mut config = Config::default(); let mut config = Config::default();
config.drawing.font_cycle = wayscriber::draw::system_font_families().to_vec(); config.drawing.font_cycle = draw::system_font_families().to_vec();
app.draft = ConfigDraft::from_config(&config); app.draft = ConfigDraft::from_config(&config);
let before = app.draft.drawing_font_cycle.entries().to_vec(); let before = app.draft.drawing_font_cycle.entries().to_vec();
+3 -3
View File
@@ -134,7 +134,7 @@ impl ConfiguratorApp {
self.handle_session_catalog_clear_confirmed(id) self.handle_session_catalog_clear_confirmed(id)
} }
Message::SessionCatalogClearCanceled(id) => { Message::SessionCatalogClearCanceled(id) => {
self.handle_session_catalog_clear_canceled(id) self.handle_session_catalog_clear_canceled(&id)
} }
Message::SearchChanged(value) => self.handle_search_changed(value), Message::SearchChanged(value) => self.handle_search_changed(value),
Message::SearchCleared => self.handle_search_cleared(), Message::SearchCleared => self.handle_search_cleared(),
@@ -180,7 +180,7 @@ impl ConfiguratorApp {
Message::FontCycleRemoved(index) => self.handle_font_cycle_removed(index), Message::FontCycleRemoved(index) => self.handle_font_cycle_removed(index),
Message::FontCycleMoved(index, delta) => self.handle_font_cycle_moved(index, delta), Message::FontCycleMoved(index, delta) => self.handle_font_cycle_moved(index, delta),
Message::FontCycleChanged(index, family) => { Message::FontCycleChanged(index, family) => {
self.handle_font_cycle_changed(index, family) self.handle_font_cycle_changed(index, &family)
} }
Message::QuickColorModeChanged(index, mode) => { Message::QuickColorModeChanged(index, mode) => {
self.handle_quick_color_mode_changed(index, mode) self.handle_quick_color_mode_changed(index, mode)
@@ -344,7 +344,7 @@ impl ConfiguratorApp {
self.handle_shortcut_sequence_remove_last_step() self.handle_shortcut_sequence_remove_last_step()
} }
Message::ShortcutRemoved(field, binding) => { Message::ShortcutRemoved(field, binding) => {
self.handle_shortcut_removed(field, binding) self.handle_shortcut_removed(field, &binding)
} }
Message::ShortcutResetRequested(field) => self.handle_shortcut_reset_requested(field), Message::ShortcutResetRequested(field) => self.handle_shortcut_reset_requested(field),
Message::ShortcutTextEditStarted(field) => { Message::ShortcutTextEditStarted(field) => {
+4 -4
View File
@@ -1,8 +1,8 @@
use wayscriber::config::PRESET_SLOTS_MAX; use wayscriber::config::PRESET_SLOTS_MAX;
use crate::models::{ use crate::models::{
ColorMode, NamedColorOption, PresetEraserKindOption, PresetEraserModeOption, PresetTextField, ColorMode, NamedColorOption, OverrideOption, PresetEraserKindOption, PresetEraserModeOption,
PresetToggleField, PresetTextField, PresetToggleField, ToolOption,
}; };
use super::super::effects::Effect; use super::super::effects::Effect;
@@ -73,7 +73,7 @@ impl ConfiguratorApp {
pub(super) fn handle_preset_tool_changed( pub(super) fn handle_preset_tool_changed(
&mut self, &mut self,
slot_index: usize, slot_index: usize,
tool: crate::models::ToolOption, tool: ToolOption,
) -> Vec<Effect> { ) -> Vec<Effect> {
self.status = StatusMessage::idle(); self.status = StatusMessage::idle();
if let Some(slot) = self.draft.presets.slot_mut(slot_index) { if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
@@ -167,7 +167,7 @@ impl ConfiguratorApp {
&mut self, &mut self,
slot_index: usize, slot_index: usize,
field: PresetToggleField, field: PresetToggleField,
value: crate::models::OverrideOption, value: OverrideOption,
) -> Vec<Effect> { ) -> Vec<Effect> {
self.status = StatusMessage::idle(); self.status = StatusMessage::idle();
if let Some(slot) = self.draft.presets.slot_mut(slot_index) { if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
@@ -1,3 +1,5 @@
use wayscriber::render_profiles;
use crate::models::{ use crate::models::{
ColorPickerId, RenderProfileExportOption, RenderProfileMappingDraft, RenderProfileMappingSide, ColorPickerId, RenderProfileExportOption, RenderProfileMappingDraft, RenderProfileMappingSide,
RenderProfileTextField, RenderProfileTextField,
@@ -57,7 +59,7 @@ impl ConfiguratorApp {
if let Some(profile) = self.draft.render_profiles.profiles.get_mut(index) { if let Some(profile) = self.draft.render_profiles.profiles.get_mut(index) {
match field { match field {
RenderProfileTextField::Id => { RenderProfileTextField::Id => {
let next_id = wayscriber::render_profiles::normalize_profile_id(&value); let next_id = render_profiles::normalize_profile_id(&value);
profile.id = value; profile.id = value;
if self.draft.render_profiles.active == old_id { if self.draft.render_profiles.active == old_id {
self.draft.render_profiles.active = next_id.clone(); self.draft.render_profiles.active = next_id.clone();
@@ -228,8 +228,8 @@ impl ConfiguratorApp {
vec![Effect::ClearSessionEntry { id }] vec![Effect::ClearSessionEntry { id }]
} }
pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: String) -> Vec<Effect> { pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: &str) -> Vec<Effect> {
if self.pending_session_clear_id() != Some(id.as_str()) { if self.pending_session_clear_id() != Some(id) {
return Vec::new(); return Vec::new();
} }
@@ -1,5 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
use wayscriber::i18n;
use crate::models::DaemonRuntimeStatus;
use crate::models::session::SessionArtifactSummary;
use crate::test_env::EnvGuard;
use crate::{test_env, test_temp};
use super::*; use super::*;
use crate::models::{ use crate::models::{
DesktopEnvironment, LightShortcutApplyCapability, SessionCatalogState, ShortcutApplyCapability, DesktopEnvironment, LightShortcutApplyCapability, SessionCatalogState, ShortcutApplyCapability,
@@ -18,7 +25,7 @@ fn catalog_item(id: &str, display_name: &str) -> SessionCatalogItem {
created_at_millis: 1_767_261_600_000, created_at_millis: 1_767_261_600_000,
last_opened_at_millis: None, last_opened_at_millis: None,
last_saved_at_millis: None, last_saved_at_millis: None,
artifacts: crate::models::session::SessionArtifactSummary { artifacts: SessionArtifactSummary {
primary_exists: false, primary_exists: false,
backup_exists: false, backup_exists: false,
recovery_exists: false, recovery_exists: false,
@@ -30,8 +37,8 @@ fn catalog_item(id: &str, display_name: &str) -> SessionCatalogItem {
} }
} }
fn inactive_daemon_status() -> crate::models::DaemonRuntimeStatus { fn inactive_daemon_status() -> DaemonRuntimeStatus {
crate::models::DaemonRuntimeStatus { DaemonRuntimeStatus {
desktop: DesktopEnvironment::Unknown, desktop: DesktopEnvironment::Unknown,
shortcut_backend: ShortcutBackend::Manual, shortcut_backend: ShortcutBackend::Manual,
shortcut_apply_capability: ShortcutApplyCapability::Manual, shortcut_apply_capability: ShortcutApplyCapability::Manual,
@@ -124,14 +131,14 @@ fn catalog_load_preserves_a_defaults_confirmation() {
#[test] #[test]
fn a_finished_load_clears_its_notice_in_every_language() { fn a_finished_load_clears_its_notice_in_every_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::a_finished_load_clears_its_notice_in_every_language" "::a_finished_load_clears_its_notice_in_every_language"
), ),
|| { || {
for language in [Language::English, Language::SimplifiedChinese] { for language in [Language::English, Language::SimplifiedChinese] {
wayscriber::i18n::set_language(language); i18n::set_language(language);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
let effects = app.handle_session_catalog_refresh_requested(); let effects = app.handle_session_catalog_refresh_requested();
@@ -148,7 +155,7 @@ fn a_finished_load_clears_its_notice_in_every_language() {
#[test] #[test]
fn the_loading_notice_is_drawn_in_the_current_language() { fn the_loading_notice_is_drawn_in_the_current_language() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::the_loading_notice_is_drawn_in_the_current_language" "::the_loading_notice_is_drawn_in_the_current_language"
@@ -158,7 +165,7 @@ fn the_loading_notice_is_drawn_in_the_current_language() {
let _ = app.handle_session_catalog_refresh_requested(); let _ = app.handle_session_catalog_refresh_requested();
assert_eq!(app.status.text(), Some("Loading sessions...")); assert_eq!(app.status.text(), Some("Loading sessions..."));
wayscriber::i18n::set_language(Language::SimplifiedChinese); i18n::set_language(Language::SimplifiedChinese);
assert_eq!(app.status.text(), Some("正在加载会话...")); assert_eq!(app.status.text(), Some("正在加载会话..."));
}, },
@@ -191,8 +198,8 @@ fn a_failed_load_stops_loading_and_shows_the_error() {
#[test] #[test]
fn duplicate_request_blocks_without_daemon_status() { fn duplicate_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -208,8 +215,8 @@ fn duplicate_request_blocks_without_daemon_status() {
#[test] #[test]
fn duplicate_request_sets_busy_when_safe() { fn duplicate_request_sets_busy_when_safe() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -228,8 +235,8 @@ fn duplicate_request_sets_busy_when_safe() {
#[test] #[test]
fn move_request_blocks_without_daemon_status() { fn move_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -245,8 +252,8 @@ fn move_request_blocks_without_daemon_status() {
#[test] #[test]
fn move_request_sets_busy_when_safe() { fn move_request_sets_busy_when_safe() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -265,8 +272,8 @@ fn move_request_sets_busy_when_safe() {
#[test] #[test]
fn clear_request_blocks_without_daemon_status() { fn clear_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -281,8 +288,8 @@ fn clear_request_blocks_without_daemon_status() {
#[test] #[test]
fn clear_tool_state_request_blocks_without_daemon_status() { fn clear_tool_state_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -298,8 +305,8 @@ fn clear_tool_state_request_blocks_without_daemon_status() {
#[test] #[test]
fn clear_tool_state_request_sets_busy_when_safe() { fn clear_tool_state_request_sets_busy_when_safe() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -319,8 +326,8 @@ fn clear_tool_state_request_sets_busy_when_safe() {
#[test] #[test]
fn clear_request_sets_pending_confirmation_when_safe() { fn clear_request_sets_pending_confirmation_when_safe() {
let temp = crate::test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -391,7 +398,7 @@ fn clear_canceled_disarms_and_clears_its_confirmation_status() {
app.daemon.status = Some(inactive_daemon_status()); app.daemon.status = Some(inactive_daemon_status());
let _ = app.handle_session_catalog_clear_requested("s-1".to_string()); let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
let effects = app.handle_session_catalog_clear_canceled("s-1".to_string()); let effects = app.handle_session_catalog_clear_canceled("s-1");
assert!(effects.is_empty()); assert!(effects.is_empty());
assert!(app.pending_session_clear_id().is_none()); assert!(app.pending_session_clear_id().is_none());
@@ -424,7 +431,7 @@ fn clear_canceled_preserves_status_that_replaced_its_confirmation() {
let _ = app.handle_session_catalog_clear_requested("s-1".to_string()); let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
app.status = StatusMessage::error("A newer session operation failed"); app.status = StatusMessage::error("A newer session operation failed");
let _ = app.handle_session_catalog_clear_canceled("s-1".to_string()); let _ = app.handle_session_catalog_clear_canceled("s-1");
assert!(app.pending_session_clear_id().is_none()); assert!(app.pending_session_clear_id().is_none());
assert!(matches!(app.status, StatusMessage::Error(_))); assert!(matches!(app.status, StatusMessage::Error(_)));
@@ -440,7 +447,7 @@ fn stray_clear_cancel_preserves_unrelated_status() {
app.pending_confirmation = None; app.pending_confirmation = None;
app.status = StatusMessage::success("A completed operation"); app.status = StatusMessage::success("A completed operation");
let _ = app.handle_session_catalog_clear_canceled("s-1".to_string()); let _ = app.handle_session_catalog_clear_canceled("s-1");
assert!(matches!(app.status, StatusMessage::Success(_))); assert!(matches!(app.status, StatusMessage::Success(_)));
assert!(status_contains(&app.status, "completed operation")); assert!(status_contains(&app.status, "completed operation"));
@@ -458,7 +465,7 @@ fn stale_clear_cancel_does_not_disarm_a_newer_confirmation() {
let _ = app.handle_session_catalog_clear_requested("s-1".to_string()); let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
let _ = app.handle_session_catalog_clear_requested("s-2".to_string()); let _ = app.handle_session_catalog_clear_requested("s-2".to_string());
let effects = app.handle_session_catalog_clear_canceled("s-1".to_string()); let effects = app.handle_session_catalog_clear_canceled("s-1");
assert!(effects.is_empty()); assert!(effects.is_empty());
assert_eq!(app.pending_session_clear_id(), Some("s-2")); assert_eq!(app.pending_session_clear_id(), Some("s-2"));
@@ -470,8 +477,8 @@ fn stale_clear_cancel_does_not_disarm_a_newer_confirmation() {
/// the busy flag, the status, the effect — is untouched. /// the busy flag, the status, the effect — is untouched.
#[test] #[test]
fn clear_confirmed_consumes_the_pending_confirmation() { fn clear_confirmed_consumes_the_pending_confirmation() {
let temp = crate::test_temp::tempdir().expect("temporary test directory"); let temp = test_temp::tempdir().expect("temporary test directory");
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -494,8 +501,8 @@ fn clear_confirmed_consumes_the_pending_confirmation() {
/// stale one from a dialog — must not start it again. /// stale one from a dialog — must not start it again.
#[test] #[test]
fn clear_confirmed_twice_starts_only_one_clear() { fn clear_confirmed_twice_starts_only_one_clear() {
let temp = crate::test_temp::tempdir().expect("temporary test directory"); let temp = test_temp::tempdir().expect("temporary test directory");
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog app.session_catalog
@@ -516,8 +523,8 @@ fn clear_confirmed_twice_starts_only_one_clear() {
/// starts a clear nor consumes the confirmation that is armed. /// starts a clear nor consumes the confirmation that is armed.
#[test] #[test]
fn clear_confirmed_for_another_row_leaves_the_pending_one_armed() { fn clear_confirmed_for_another_row_leaves_the_pending_one_armed() {
let temp = crate::test_temp::tempdir().expect("temporary test directory"); let temp = test_temp::tempdir().expect("temporary test directory");
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading(); app.session_catalog = SessionCatalogState::loading();
app.session_catalog.replace_items(vec![ app.session_catalog.replace_items(vec![
+13 -14
View File
@@ -6,7 +6,7 @@ mod tests;
use wayscriber::config::Shortcut; use wayscriber::config::Shortcut;
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use crate::app::search::keybinding_row_visible; use crate::app::search::{AppSearchSummary, keybinding_row_visible};
use crate::app::state::{ConfirmationPrompt, PendingConfirmation}; use crate::app::state::{ConfirmationPrompt, PendingConfirmation};
use crate::models::keybindings::{ use crate::models::keybindings::{
AppendOutcome, KeyboardModifiers, RecordedDevice, RecordedKeyboard, RecorderDeviceKind, AppendOutcome, KeyboardModifiers, RecordedDevice, RecordedKeyboard, RecorderDeviceKind,
@@ -16,7 +16,7 @@ use crate::models::keybindings::{
other_claimants, parse_keybindings, remove_binding, reset_field, reset_fields, other_claimants, parse_keybindings, remove_binding, reset_field, reset_fields,
text_conflicts_for, text_conflicts_for,
}; };
use crate::models::{KeybindingField, keybinding_tab}; use crate::models::{KeybindingField, PendingShortcutConflict, keybinding_tab};
use super::super::effects::Effect; use super::super::effects::Effect;
use super::super::state::{ConfiguratorApp, StatusMessage}; use super::super::state::{ConfiguratorApp, StatusMessage};
@@ -127,12 +127,12 @@ impl ConfiguratorApp {
pub(super) fn handle_shortcut_removed( pub(super) fn handle_shortcut_removed(
&mut self, &mut self,
field: KeybindingField, field: KeybindingField,
binding: Shortcut, binding: &Shortcut,
) -> Vec<Effect> { ) -> Vec<Effect> {
if self.shortcuts.conflict().is_some() { if self.shortcuts.conflict().is_some() {
return Vec::new(); return Vec::new();
} }
match remove_binding(&mut self.draft.keybindings, field, &binding) { match remove_binding(&mut self.draft.keybindings, field, binding) {
Ok(()) => { Ok(()) => {
self.status = StatusMessage::idle(); self.status = StatusMessage::idle();
self.refresh_dirty_flag(); self.refresh_dirty_flag();
@@ -197,7 +197,7 @@ impl ConfiguratorApp {
} }
self.shortcuts.set_editor(None); self.shortcuts.set_editor(None);
self.shortcuts self.shortcuts
.set_conflict(Some(crate::models::PendingShortcutConflict::Text { .set_conflict(Some(PendingShortcutConflict::Text {
target: editor.field, target: editor.field,
new_value: editor.text, new_value: editor.text,
conflicts, conflicts,
@@ -223,12 +223,12 @@ impl ConfiguratorApp {
return Vec::new(); return Vec::new();
}; };
let result = match pending { let result = match pending {
crate::models::PendingShortcutConflict::Recorded { PendingShortcutConflict::Recorded {
target, target,
binding, binding,
claimants, claimants,
} => apply_recorded_replace(&mut self.draft.keybindings, target, &binding, &claimants), } => apply_recorded_replace(&mut self.draft.keybindings, target, &binding, &claimants),
crate::models::PendingShortcutConflict::Text { PendingShortcutConflict::Text {
target, target,
new_value, new_value,
conflicts, conflicts,
@@ -274,7 +274,7 @@ impl ConfiguratorApp {
pub(crate) fn prepare_shortcut_manager_refresh( pub(crate) fn prepare_shortcut_manager_refresh(
&self, &self,
search: &crate::app::search::AppSearchSummary, search: &AppSearchSummary,
) -> (ShortcutManagerSummary, Vec<KeybindingField>) { ) -> (ShortcutManagerSummary, Vec<KeybindingField>) {
let summary = self.shortcut_manager_summary(); let summary = self.shortcut_manager_summary();
let fields = self.visible_keybinding_fields_with(&summary, search); let fields = self.visible_keybinding_fields_with(&summary, search);
@@ -284,7 +284,7 @@ impl ConfiguratorApp {
fn visible_keybinding_fields_with( fn visible_keybinding_fields_with(
&self, &self,
summary: &ShortcutManagerSummary, summary: &ShortcutManagerSummary,
search: &crate::app::search::AppSearchSummary, search: &AppSearchSummary,
) -> Vec<KeybindingField> { ) -> Vec<KeybindingField> {
let scope = (!self.keybindings_show_all).then_some(self.active_keybindings_tab); let scope = (!self.keybindings_show_all).then_some(self.active_keybindings_tab);
summary.visible_fields(self.shortcut_filter, self.shortcut_sort, scope, |field| { summary.visible_fields(self.shortcut_filter, self.shortcut_sort, scope, |field| {
@@ -416,13 +416,12 @@ impl ConfiguratorApp {
match next_review_conflict(&self.draft.keybindings) { match next_review_conflict(&self.draft.keybindings) {
Some((field, binding, claimants)) => { Some((field, binding, claimants)) => {
self.select_keybinding_field(field); self.select_keybinding_field(field);
self.shortcuts.set_conflict(Some( self.shortcuts
crate::models::PendingShortcutConflict::Recorded { .set_conflict(Some(PendingShortcutConflict::Recorded {
target: field, target: field,
binding, binding,
claimants, claimants,
}, }));
));
self.status = StatusMessage::info(tr("Review the next conflicting shortcut.")); self.status = StatusMessage::info(tr("Review the next conflicting shortcut."));
} }
None => { None => {
@@ -440,7 +439,7 @@ impl ConfiguratorApp {
let claimants = other_claimants(&self.draft.keybindings, field, &binding); let claimants = other_claimants(&self.draft.keybindings, field, &binding);
if !claimants.is_empty() { if !claimants.is_empty() {
self.shortcuts self.shortcuts
.set_conflict(Some(crate::models::PendingShortcutConflict::Recorded { .set_conflict(Some(PendingShortcutConflict::Recorded {
target: field, target: field,
binding, binding,
claimants, claimants,
+21 -12
View File
@@ -1,3 +1,9 @@
use std::fs;
use std::path::PathBuf;
#[cfg(feature = "tablet-input")]
use wayscriber::config::Action;
use crate::models::{ShortcutManagerFilter, ShortcutManagerSort}; use crate::models::{ShortcutManagerFilter, ShortcutManagerSort};
use wayscriber::config::{CURRENT_CONFIG_REVISION, ConfigDocument, Shortcut}; use wayscriber::config::{CURRENT_CONFIG_REVISION, ConfigDocument, Shortcut};
@@ -5,18 +11,20 @@ use crate::app::effects::Effect;
use crate::app::state::{ConfiguratorApp, PendingConfirmation, StatusMessage}; use crate::app::state::{ConfiguratorApp, PendingConfirmation, StatusMessage};
use crate::models::keybindings::keyval; use crate::models::keybindings::keyval;
use crate::models::{ use crate::models::{
KeybindingField, KeyboardModifiers, RecorderDeviceKind, SearchQuery, keybinding_tab, KeybindingField, KeyboardModifiers, PendingShortcutConflict, RecorderDeviceKind, SearchQuery,
keybinding_tab,
}; };
use crate::test_temp;
use crate::test_temp::TempDir; use crate::test_temp::TempDir;
fn status_contains(status: &StatusMessage, needle: &str) -> bool { fn status_contains(status: &StatusMessage, needle: &str) -> bool {
status.text().is_some_and(|text| text.contains(needle)) status.text().is_some_and(|text| text.contains(needle))
} }
fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, std::path::PathBuf) { fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, PathBuf) {
let dir = crate::test_temp::tempdir().expect("temporary test directory"); let dir = test_temp::tempdir().expect("temporary test directory");
let path = dir.path().join("config.toml"); let path = dir.path().join("config.toml");
std::fs::write(&path, contents).expect("write test config"); fs::write(&path, contents).expect("write test config");
let document = ConfigDocument::load_from_path(&path).expect("load test config document"); let document = ConfigDocument::load_from_path(&path).expect("load test config document");
let _ = app.handle_config_loaded(Ok((Box::new(document), None))); let _ = app.handle_config_loaded(Ok((Box::new(document), None)));
(dir, path) (dir, path)
@@ -125,7 +133,7 @@ fn recording_reset_and_removal_dirty_without_saving() {
); );
let effects = app.handle_shortcut_removed( let effects = app.handle_shortcut_removed(
KeybindingField::ToggleFloatingBadge, KeybindingField::ToggleFloatingBadge,
Shortcut::parse("F5").expect("parses"), &Shortcut::parse("F5").expect("parses"),
); );
assert!(effects.is_empty()); assert!(effects.is_empty());
assert_eq!( assert_eq!(
@@ -201,7 +209,7 @@ fn save_after_confirmed_replacement_writes_only_the_intended_fields() {
let _ = app.handle_shortcut_conflict_replace_confirmed(); let _ = app.handle_shortcut_conflict_replace_confirmed();
save_draft(&mut app); save_draft(&mut app);
let contents = std::fs::read_to_string(&path).expect("read saved config"); let contents = fs::read_to_string(&path).expect("read saved config");
assert!(contents.contains("# keep me"), "{contents}"); assert!(contents.contains("# keep me"), "{contents}");
assert!(contents.contains("unknown_future = true"), "{contents}"); assert!(contents.contains("unknown_future = true"), "{contents}");
let revision = format!("config_revision = {CURRENT_CONFIG_REVISION}"); let revision = format!("config_revision = {CURRENT_CONFIG_REVISION}");
@@ -307,7 +315,7 @@ fn resetting_an_explicit_binding_stays_authored_through_save_reload() {
assert!(app.draft.keybindings.is_authored(KeybindingField::Undo)); assert!(app.draft.keybindings.is_authored(KeybindingField::Undo));
save_draft(&mut app); save_draft(&mut app);
let contents = std::fs::read_to_string(path).expect("read saved config"); let contents = fs::read_to_string(path).expect("read saved config");
assert_eq!( assert_eq!(
config_setting(&contents, "undo").as_deref(), config_setting(&contents, "undo").as_deref(),
Some("undo = [\"Ctrl+Z\"]") Some("undo = [\"Ctrl+Z\"]")
@@ -374,7 +382,7 @@ fn recording_stylus_primary_prompts_to_move_the_default_legacy_barrel() {
let (mut app, _effects) = ConfiguratorApp::new_app(); let (mut app, _effects) = ConfiguratorApp::new_app();
assert_eq!( assert_eq!(
app.draft.keybindings.legacy_tablet.stylus_primary, app.draft.keybindings.legacy_tablet.stylus_primary,
Some(wayscriber::config::Action::ToggleRadialMenu) Some(Action::ToggleRadialMenu)
); );
let _ = app.handle_shortcut_recording_started(KeybindingField::Undo); let _ = app.handle_shortcut_recording_started(KeybindingField::Undo);
let _ = app.handle_shortcut_recorder_button( let _ = app.handle_shortcut_recorder_button(
@@ -688,7 +696,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() {
assert!(app.shortcuts.review); assert!(app.shortcuts.review);
assert!(app.shortcuts.conflict().is_some()); assert!(app.shortcuts.conflict().is_some());
let first_target = match app.shortcuts.conflict() { let first_target = match app.shortcuts.conflict() {
Some(crate::models::PendingShortcutConflict::Recorded { target, .. }) => *target, Some(PendingShortcutConflict::Recorded { target, .. }) => *target,
other => panic!("expected a recorded conflict, got {other:?}"), other => panic!("expected a recorded conflict, got {other:?}"),
}; };
@@ -702,7 +710,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() {
.conflict() .conflict()
.expect("the next conflict should be armed"); .expect("the next conflict should be armed");
match second { match second {
crate::models::PendingShortcutConflict::Recorded { target, .. } => { PendingShortcutConflict::Recorded { target, .. } => {
assert_ne!(*target, first_target); assert_ne!(*target, first_target);
} }
other => panic!("expected a recorded conflict, got {other:?}"), other => panic!("expected a recorded conflict, got {other:?}"),
@@ -737,7 +745,7 @@ fn jump_to_conflict_selects_the_other_claimant() {
let jump = app let jump = app
.shortcuts .shortcuts
.conflict() .conflict()
.and_then(crate::models::PendingShortcutConflict::jump_field) .and_then(PendingShortcutConflict::jump_field)
.expect("a claimant to jump to"); .expect("a claimant to jump to");
let _ = app.handle_shortcut_manager_jump_to(jump); let _ = app.handle_shortcut_manager_jump_to(jump);
assert_eq!(app.selected_keybinding, Some(jump)); assert_eq!(app.selected_keybinding, Some(jump));
@@ -765,7 +773,8 @@ fn prepared_manager_refresh_reuses_analysis_for_filters() {
#[ignore = "release measurement: cargo test --release shortcut_refresh_cost -- --ignored --nocapture"] #[ignore = "release measurement: cargo test --release shortcut_refresh_cost -- --ignored --nocapture"]
fn shortcut_refresh_cost() { fn shortcut_refresh_cost() {
use crate::models::keybindings::take_parse_calls; use crate::models::keybindings::take_parse_calls;
use std::{hint::black_box, time::Instant}; use std::hint::black_box;
use std::time::Instant;
let (mut app, _) = ConfiguratorApp::new_app(); let (mut app, _) = ConfiguratorApp::new_app();
for query in ["", "tool", "Ctrl"] { for query in ["", "tool", "Ctrl"] {
app.handle_search_changed(query.into()); app.handle_search_changed(query.into());
+3 -1
View File
@@ -6,10 +6,12 @@ mod test_env;
#[cfg(test)] #[cfg(test)]
mod test_temp; mod test_temp;
use std::env;
use models::{StartupRequest, startup_usage}; use models::{StartupRequest, startup_usage};
fn main() { fn main() {
let startup = StartupRequest::from_args(std::env::args_os()); let startup = StartupRequest::from_args(env::args_os());
if startup.wants_help() { if startup.wants_help() {
print!("{}", startup_usage()); print!("{}", startup_usage());
return; return;
+2 -1
View File
@@ -1,6 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use wayscriber::config::{ConfigDocument, Shortcut, ToolbarItemId, ToolbarItemOrderGroup}; use wayscriber::config::{ConfigDocument, Shortcut, ToolbarItemId, ToolbarItemOrderGroup};
use wayscriber::domain::BoardGridKind;
use crate::models::{ use crate::models::{
ArrowStyleOption, BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode, ArrowStyleOption, BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode,
@@ -150,7 +151,7 @@ pub enum Message {
BoardsCollapseToggled(usize), BoardsCollapseToggled(usize),
BoardsDefaultChanged(String), BoardsDefaultChanged(String),
BoardsItemTextChanged(usize, BoardItemTextField, String), BoardsItemTextChanged(usize, BoardItemTextField, String),
BoardsGridKindChanged(usize, wayscriber::domain::BoardGridKind), BoardsGridKindChanged(usize, BoardGridKind),
BoardsBackgroundKindChanged(usize, BoardBackgroundOption), BoardsBackgroundKindChanged(usize, BoardBackgroundOption),
BoardsBackgroundColorChanged(usize, usize, String), BoardsBackgroundColorChanged(usize, usize, String),
BoardsDefaultPenEnabledChanged(usize, bool), BoardsDefaultPenEnabledChanged(usize, bool),
+4 -3
View File
@@ -2,6 +2,7 @@ use wayscriber::i18n::{tr, trf};
use wayscriber::config::enums::ColorSpec; use wayscriber::config::enums::ColorSpec;
use wayscriber::draw::Color; use wayscriber::draw::Color;
use wayscriber::input::state;
use wayscriber::util::{ConfigHexColorError, name_to_color, parse_config_hex_color}; use wayscriber::util::{ConfigHexColorError, name_to_color, parse_config_hex_color};
use super::super::error::FormError; use super::super::error::FormError;
@@ -22,7 +23,7 @@ impl ColorInput {
mode: ColorMode::Named, mode: ColorMode::Named,
name: name.clone(), name: name.clone(),
rgb: color_to_rgb_strings(spec.to_color()), rgb: color_to_rgb_strings(spec.to_color()),
selected_named: NamedColorOption::from_str(name), selected_named: NamedColorOption::from_name(name),
}, },
ColorSpec::Rgb([r, g, b]) => Self { ColorSpec::Rgb([r, g, b]) => Self {
mode: ColorMode::Rgb, mode: ColorMode::Rgb,
@@ -35,7 +36,7 @@ impl ColorInput {
// silently flattened to opaque by the three RGB boxes. // silently flattened to opaque by the three RGB boxes.
ColorSpec::Rgba(_) => Self { ColorSpec::Rgba(_) => Self {
mode: ColorMode::Named, mode: ColorMode::Named,
name: wayscriber::input::state::color_to_hex(spec.to_color()), name: state::color_to_hex(spec.to_color()),
rgb: color_to_rgb_strings(spec.to_color()), rgb: color_to_rgb_strings(spec.to_color()),
selected_named: NamedColorOption::Custom, selected_named: NamedColorOption::Custom,
}, },
@@ -116,7 +117,7 @@ impl ColorInput {
} }
pub fn update_named_from_current(&mut self) { pub fn update_named_from_current(&mut self) {
self.selected_named = NamedColorOption::from_str(&self.name); self.selected_named = NamedColorOption::from_name(&self.name);
} }
pub fn sync_rgb_from_preview(&mut self) -> bool { pub fn sync_rgb_from_preview(&mut self) -> bool {
+6 -3
View File
@@ -1,3 +1,6 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -62,7 +65,7 @@ impl NamedColorOption {
} }
} }
pub fn from_str(value: &str) -> Self { pub fn from_name(value: &str) -> Self {
match value.trim().to_lowercase().as_str() { match value.trim().to_lowercase().as_str() {
"red" => Self::Red, "red" => Self::Red,
"green" => Self::Green, "green" => Self::Green,
@@ -77,8 +80,8 @@ impl NamedColorOption {
} }
} }
impl std::fmt::Display for NamedColorOption { impl Display for NamedColorOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
+7 -3
View File
@@ -1,6 +1,10 @@
mod mapping; mod mapping;
mod validation; mod validation;
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::domain::BoardGridKind;
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use super::super::color::ColorTripletInput; use super::super::color::ColorTripletInput;
@@ -24,8 +28,8 @@ impl BoardBackgroundOption {
} }
} }
impl std::fmt::Display for BoardBackgroundOption { impl Display for BoardBackgroundOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -56,7 +60,7 @@ pub struct BoardItemDraft {
pub name: String, pub name: String,
pub background_kind: BoardBackgroundOption, pub background_kind: BoardBackgroundOption,
pub background_color: ColorTripletInput, pub background_color: ColorTripletInput,
pub grid_kind: wayscriber::domain::BoardGridKind, pub grid_kind: BoardGridKind,
pub grid_spacing: String, pub grid_spacing: String,
pub default_pen_color: OptionalTripletInput, pub default_pen_color: OptionalTripletInput,
pub auto_adjust_pen: bool, pub auto_adjust_pen: bool,
@@ -3,8 +3,10 @@ use wayscriber::i18n::tr;
use std::collections::HashSet; use std::collections::HashSet;
use wayscriber::config::{ use wayscriber::config::{
BoardBackgroundConfig, BoardColorConfig, BoardItemConfig, BoardsConfig, Config, BoardBackgroundConfig, BoardColorConfig, BoardGridConfig, BoardItemConfig, BoardsConfig, Config,
}; };
use wayscriber::domain;
use wayscriber::domain::BoardGridKind;
use crate::models::color::ColorTripletInput; use crate::models::color::ColorTripletInput;
use crate::models::error::FormError; use crate::models::error::FormError;
@@ -103,8 +105,8 @@ impl BoardItemDraft {
let spacing = match self.grid_spacing.trim().parse::<i64>() { let spacing = match self.grid_spacing.trim().parse::<i64>() {
Ok(value) Ok(value)
if (i64::from(wayscriber::domain::BOARD_GRID_MIN_SPACING) if (i64::from(domain::BOARD_GRID_MIN_SPACING)
..=i64::from(wayscriber::domain::BOARD_GRID_MAX_SPACING)) ..=i64::from(domain::BOARD_GRID_MAX_SPACING))
.contains(&value) => .contains(&value) =>
{ {
value value
@@ -117,9 +119,9 @@ impl BoardItemDraft {
return None; return None;
} }
}; };
let grid = wayscriber::config::BoardGridConfig { let grid = BoardGridConfig {
kind: if background.is_transparent() { kind: if background.is_transparent() {
wayscriber::domain::BoardGridKind::None BoardGridKind::None
} else { } else {
self.grid_kind self.grid_kind
} }
@@ -230,8 +232,8 @@ impl BoardsDraft {
name, name,
background_kind: BoardBackgroundOption::Color, background_kind: BoardBackgroundOption::Color,
background_color: ColorTripletInput::from([0.992, 0.992, 0.992]), background_color: ColorTripletInput::from([0.992, 0.992, 0.992]),
grid_kind: wayscriber::domain::BoardGridKind::None, grid_kind: BoardGridKind::None,
grid_spacing: wayscriber::domain::BOARD_GRID_DEFAULT_SPACING.to_string(), grid_spacing: domain::BOARD_GRID_DEFAULT_SPACING.to_string(),
default_pen_color: OptionalTripletInput::from_option( default_pen_color: OptionalTripletInput::from_option(
Some(&BoardColorConfig::Rgb([0.0, 0.0, 0.0])), Some(&BoardColorConfig::Rgb([0.0, 0.0, 0.0])),
[0.0, 0.0, 0.0], [0.0, 0.0, 0.0],
@@ -1,5 +1,7 @@
use wayscriber::config;
use wayscriber::config::{KeybindingAuthorship, ToolbarSectionVisibility};
use super::super::super::color::{ColorInput, ColorQuadInput}; use super::super::super::color::{ColorInput, ColorQuadInput};
use super::super::super::fields::ZoomChipDisplayOption;
use super::super::super::fields::{ use super::super::super::fields::{
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption, ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption, InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
@@ -7,7 +9,7 @@ use super::super::super::fields::{
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption, PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption, ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
UiThemeOption, UiThemeOption, ZoomChipDisplayOption,
}; };
#[cfg(feature = "tablet-input")] #[cfg(feature = "tablet-input")]
use super::super::super::fields::{ use super::super::super::fields::{
@@ -31,7 +33,7 @@ impl ConfigDraft {
FontWeightOption::from_value(&config.drawing.font_weight); FontWeightOption::from_value(&config.drawing.font_weight);
let drawing_drag_tools = config.drawing.effective_drag_tools(); let drawing_drag_tools = config.drawing.effective_drag_tools();
let mut toolbar_items = config.ui.toolbar.items.clone(); let mut toolbar_items = config.ui.toolbar.items.clone();
let mut legacy_toolbar_visibility = wayscriber::config::ToolbarSectionVisibility { let mut legacy_toolbar_visibility = ToolbarSectionVisibility {
show_actions_section: config.ui.toolbar.show_actions_section, show_actions_section: config.ui.toolbar.show_actions_section,
show_actions_advanced: config.ui.toolbar.show_actions_advanced, show_actions_advanced: config.ui.toolbar.show_actions_advanced,
show_zoom_actions: config.ui.toolbar.show_zoom_actions, show_zoom_actions: config.ui.toolbar.show_zoom_actions,
@@ -48,13 +50,13 @@ impl ConfigDraft {
.mode_overrides .mode_overrides
.for_mode(config.ui.toolbar.layout_mode), .for_mode(config.ui.toolbar.layout_mode),
); );
wayscriber::config::fold_legacy_section_flags( config::fold_legacy_section_flags(
&legacy_toolbar_visibility, &legacy_toolbar_visibility,
config.ui.toolbar.layout_mode, config.ui.toolbar.layout_mode,
&config.ui.toolbar.mode_overrides, &config.ui.toolbar.mode_overrides,
&mut toolbar_items, &mut toolbar_items,
); );
let toolbar_visibility = wayscriber::config::resolve_section_visibility( let toolbar_visibility = config::resolve_section_visibility(
config.ui.toolbar.layout_mode, config.ui.toolbar.layout_mode,
&config.ui.toolbar.mode_overrides, &config.ui.toolbar.mode_overrides,
&toolbar_items.resolved(), &toolbar_items.resolved(),
@@ -311,14 +313,12 @@ impl ConfigDraft {
session_persist_history: config.session.persist_history, session_persist_history: config.session.persist_history,
session_restore_tool_state: config.session.restore_tool_state, session_restore_tool_state: config.session.restore_tool_state,
session_per_output: config.session.per_output, session_per_output: config.session.per_output,
session_storage_mode: SessionStorageModeOption::from_mode( session_storage_mode: SessionStorageModeOption::from_mode(config.session.storage),
config.session.storage.clone(),
),
session_custom_directory: config.session.custom_directory.clone().unwrap_or_default(), session_custom_directory: config.session.custom_directory.clone().unwrap_or_default(),
session_max_shapes_per_frame: config.session.max_shapes_per_frame.to_string(), session_max_shapes_per_frame: config.session.max_shapes_per_frame.to_string(),
session_max_file_size_mb: config.session.max_file_size_mb.to_string(), session_max_file_size_mb: config.session.max_file_size_mb.to_string(),
session_compression: SessionCompressionOption::from_compression( session_compression: SessionCompressionOption::from_compression(
config.session.compress.clone(), config.session.compress,
), ),
session_auto_compress_threshold_kb: config session_auto_compress_threshold_kb: config
.session .session
@@ -373,8 +373,8 @@ impl ConfigDraft {
// Built-in and in-memory configs are AllExplicit for // Built-in and in-memory configs are AllExplicit for
// validation. The UI treats them as omitted defaults so // validation. The UI treats them as omitted defaults so
// Reset All / Defaults does not badge every row Authored. // Reset All / Defaults does not badge every row Authored.
wayscriber::config::KeybindingAuthorship::AllExplicit => { KeybindingAuthorship::AllExplicit => {
wayscriber::config::KeybindingAuthorship::FromFile(Default::default()) KeybindingAuthorship::FromFile(Default::default())
} }
from_file => from_file.clone(), from_file => from_file.clone(),
}); });
+1 -2
View File
@@ -1,7 +1,6 @@
mod from_config; mod from_config;
use super::super::color::{ColorInput, ColorQuadInput}; use super::super::color::{ColorInput, ColorQuadInput};
use super::super::fields::ZoomChipDisplayOption;
use super::super::fields::{ use super::super::fields::{
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption, ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption, InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
@@ -9,7 +8,7 @@ use super::super::fields::{
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption, PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption, ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
UiThemeOption, UiThemeOption, ZoomChipDisplayOption,
}; };
#[cfg(feature = "tablet-input")] #[cfg(feature = "tablet-input")]
use super::super::fields::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; use super::super::fields::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption};
+5 -11
View File
@@ -131,7 +131,7 @@ impl FontCycleDraft {
} }
/// Set one row's family without allowing a blank or repeated identity. /// Set one row's family without allowing a blank or repeated identity.
pub(crate) fn set(&mut self, index: usize, family: String) -> Result<bool, FontCycleEditError> { pub(crate) fn set(&mut self, index: usize, family: &str) -> Result<bool, FontCycleEditError> {
let Some(current) = self.entries.get(index) else { let Some(current) = self.entries.get(index) else {
return Err(FontCycleEditError::MissingEntry); return Err(FontCycleEditError::MissingEntry);
}; };
@@ -202,12 +202,12 @@ mod tests {
let mut draft = draft(&["Sans", "Serif"]); let mut draft = draft(&["Sans", "Serif"]);
assert_eq!( assert_eq!(
draft.set(1, "sans".to_string()), draft.set(1, "sans"),
Err(FontCycleEditError::DuplicateFamily("sans".to_string())) Err(FontCycleEditError::DuplicateFamily("sans".to_string()))
); );
assert_eq!(draft.entries(), ["Sans", "Serif"]); assert_eq!(draft.entries(), ["Sans", "Serif"]);
assert_eq!(draft.set(1, "Monospace".to_string()), Ok(true)); assert_eq!(draft.set(1, "Monospace"), Ok(true));
assert_eq!(draft.entries(), ["Sans", "Monospace"]); assert_eq!(draft.entries(), ["Sans", "Monospace"]);
} }
@@ -215,14 +215,8 @@ mod tests {
fn blank_and_missing_row_edits_are_rejected() { fn blank_and_missing_row_edits_are_rejected() {
let mut draft = draft(&["Sans"]); let mut draft = draft(&["Sans"]);
assert_eq!( assert_eq!(draft.set(0, " "), Err(FontCycleEditError::BlankFamily));
draft.set(0, " ".to_string()), assert_eq!(draft.set(4, "Serif"), Err(FontCycleEditError::MissingEntry));
Err(FontCycleEditError::BlankFamily)
);
assert_eq!(
draft.set(4, "Serif".to_string()),
Err(FontCycleEditError::MissingEntry)
);
assert_eq!(draft.entries(), ["Sans"]); assert_eq!(draft.entries(), ["Sans"]);
} }
@@ -82,13 +82,17 @@ impl ConfigDraft {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use wayscriber::config::{PERFORMANCE_FIELD_METADATA, PerformanceFieldId, ScalarConstraint}; use wayscriber::config::{
Config, PERFORMANCE_FIELD_METADATA, PerformanceFieldId, ScalarConstraint,
};
use crate::models::{TextField, ToggleField};
use super::*; use super::*;
#[test] #[test]
fn every_performance_descriptor_routes_through_a_typed_binding() { fn every_performance_descriptor_routes_through_a_typed_binding() {
let mut draft = ConfigDraft::from_config(&wayscriber::config::Config::default()); let mut draft = ConfigDraft::from_config(&Config::default());
for metadata in PERFORMANCE_FIELD_METADATA { for metadata in PERFORMANCE_FIELD_METADATA {
match metadata.constraint { match metadata.constraint {
ScalarConstraint::Boolean => { ScalarConstraint::Boolean => {
@@ -106,17 +110,11 @@ mod tests {
#[test] #[test]
fn typed_bindings_update_each_performance_draft_field() { fn typed_bindings_update_each_performance_draft_field() {
let mut draft = ConfigDraft::from_config(&wayscriber::config::Config::default()); let mut draft = ConfigDraft::from_config(&Config::default());
draft.set_performance_choice(PerformanceFieldId::BufferCount, 4); draft.set_performance_choice(PerformanceFieldId::BufferCount, 4);
draft.set_toggle(crate::models::ToggleField::PerformanceVsync, true); draft.set_toggle(ToggleField::PerformanceVsync, true);
draft.set_text( draft.set_text(TextField::PerformanceMaxFpsNoVsync, "144".to_string());
crate::models::TextField::PerformanceMaxFpsNoVsync, draft.set_text(TextField::PerformanceUiAnimationFps, "60".to_string());
"144".to_string(),
);
draft.set_text(
crate::models::TextField::PerformanceUiAnimationFps,
"60".to_string(),
);
assert_eq!(draft.performance_buffer_count, 4); assert_eq!(draft.performance_buffer_count, 4);
assert!(draft.performance_enable_vsync); assert!(draft.performance_enable_vsync);
@@ -126,7 +124,7 @@ mod tests {
#[test] #[test]
fn conversion_rejects_values_outside_shared_performance_constraints() { fn conversion_rejects_values_outside_shared_performance_constraints() {
let base = wayscriber::config::Config::default(); let base = Config::default();
let mut draft = ConfigDraft::from_config(&base); let mut draft = ConfigDraft::from_config(&base);
draft.performance_buffer_count = 5; draft.performance_buffer_count = 5;
draft.performance_ui_animation_fps = "241".to_string(); draft.performance_ui_animation_fps = "241".to_string();
+1 -1
View File
@@ -203,7 +203,7 @@ pub struct PresetsDraft {
impl PresetsDraft { impl PresetsDraft {
pub fn from_config(config: &Config) -> Self { pub fn from_config(config: &Config) -> Self {
let slots = (1..=PRESET_SLOTS_MAX) let slots = (1..=PRESET_SLOTS_MAX)
.map(|slot| PresetSlotDraft::from_config(config.presets.get_slot(slot), config)) .map(|slot| PresetSlotDraft::from_config(config.presets.slot(slot), config))
.collect(); .collect();
Self { Self {
slot_count: config.presets.slot_count, slot_count: config.presets.slot_count,
@@ -1,3 +1,6 @@
use std::fmt::{Display, Formatter};
use std::{fmt, iter};
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use wayscriber::config::{ use wayscriber::config::{
@@ -56,8 +59,8 @@ impl RenderProfileExportOption {
} }
} }
impl std::fmt::Display for RenderProfileExportOption { impl Display for RenderProfileExportOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -70,7 +73,7 @@ pub enum RenderProfileSelectionOption {
impl RenderProfileSelectionOption { impl RenderProfileSelectionOption {
pub fn list(profile_ids: &[String]) -> Vec<Self> { pub fn list(profile_ids: &[String]) -> Vec<Self> {
std::iter::once(Self::Off) iter::once(Self::Off)
.chain(profile_ids.iter().cloned().map(Self::Profile)) .chain(profile_ids.iter().cloned().map(Self::Profile))
.collect() .collect()
} }
@@ -84,8 +87,8 @@ impl RenderProfileSelectionOption {
} }
} }
impl std::fmt::Display for RenderProfileSelectionOption { impl Display for RenderProfileSelectionOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Off => f.write_str("Off"), Self::Off => f.write_str("Off"),
Self::Profile(id) => f.write_str(id), Self::Profile(id) => f.write_str(id),
+3 -1
View File
@@ -1,3 +1,5 @@
use wayscriber::config;
use super::super::fields::{ use super::super::fields::{
DragColorOption, DragMouseButton, DragToolField, DragToolOption, FontStyleOption, DragColorOption, DragMouseButton, DragToolField, DragToolOption, FontStyleOption,
FontWeightOption, OverrideOption, QuadField, TextField, ToggleField, ToolbarLayoutModeOption, FontWeightOption, OverrideOption, QuadField, TextField, ToggleField, ToolbarLayoutModeOption,
@@ -19,7 +21,7 @@ impl ConfigDraft {
} }
fn refresh_toolbar_section_visibility(&mut self) { fn refresh_toolbar_section_visibility(&mut self) {
let visibility = wayscriber::config::resolve_section_visibility( let visibility = config::resolve_section_visibility(
self.ui_toolbar_layout_mode.to_mode(), self.ui_toolbar_layout_mode.to_mode(),
&self.ui_toolbar_mode_overrides.to_config(), &self.ui_toolbar_mode_overrides.to_config(),
&self.ui_toolbar_items.resolved(), &self.ui_toolbar_items.resolved(),
+55 -52
View File
@@ -1,3 +1,17 @@
use crate::models::KeybindingField;
use crate::{test_env, test_temp};
use wayscriber::config::{
BoardGridConfig, BoardGridKindConfig, BoardsConfig, RegionPicker, ToolbarRebindModifier,
TrayIconStyle,
};
use wayscriber::domain::BoardGridKind;
use wayscriber::draw::ArrowStyle;
use wayscriber::input::DragBindableTool;
use wayscriber::{config, domain, i18n};
use std::fs;
use super::super::color::ColorInput; use super::super::color::ColorInput;
#[test] #[test]
@@ -95,16 +109,16 @@ fn shape_pen_recognition_feedback_round_trips() {
#[test] #[test]
fn board_grid_draft_preserves_patterns_and_rejects_invalid_spacing() { fn board_grid_draft_preserves_patterns_and_rejects_invalid_spacing() {
let mut config = Config::default(); let mut config = Config::default();
let mut boards = wayscriber::config::BoardsConfig::default(); let mut boards = BoardsConfig::default();
boards.items[1].grid = wayscriber::config::BoardGridConfig { boards.items[1].grid = BoardGridConfig {
kind: wayscriber::config::BoardGridKindConfig::IsometricDots, kind: BoardGridKindConfig::IsometricDots,
spacing: 20, spacing: 20,
}; };
config.boards = Some(boards); config.boards = Some(boards);
let mut draft = ConfigDraft::from_config(&config); let mut draft = ConfigDraft::from_config(&config);
assert_eq!( assert_eq!(
draft.boards.items[1].grid_kind, draft.boards.items[1].grid_kind,
wayscriber::domain::BoardGridKind::IsometricDots BoardGridKind::IsometricDots
); );
assert_eq!( assert_eq!(
draft.to_config(&config).unwrap().boards.unwrap().items[1].grid, draft.to_config(&config).unwrap().boards.unwrap().items[1].grid,
@@ -222,7 +236,7 @@ use super::super::fields::{
#[test] #[test]
fn config_draft_round_trips_toolbar_rebind_modifier() { fn config_draft_round_trips_toolbar_rebind_modifier() {
let mut config = Config::default(); let mut config = Config::default();
config.ui.toolbar.rebind_modifier = wayscriber::config::ToolbarRebindModifier::CtrlAlt; config.ui.toolbar.rebind_modifier = ToolbarRebindModifier::CtrlAlt;
let mut draft = ConfigDraft::from_config(&config); let mut draft = ConfigDraft::from_config(&config);
assert_eq!( assert_eq!(
draft.ui_toolbar_rebind_modifier, draft.ui_toolbar_rebind_modifier,
@@ -235,7 +249,7 @@ fn config_draft_round_trips_toolbar_rebind_modifier() {
.expect("toolbar modifier round trip"); .expect("toolbar modifier round trip");
assert_eq!( assert_eq!(
round_trip.ui.toolbar.rebind_modifier, round_trip.ui.toolbar.rebind_modifier,
wayscriber::config::ToolbarRebindModifier::ShiftAlt ToolbarRebindModifier::ShiftAlt
); );
} }
@@ -245,13 +259,13 @@ fn config_draft_round_trips_arrow_style() {
// that sets a style loads as Standard in the UI, and saving anything from // that sets a style loads as Standard in the UI, and saving anything from
// any tab writes that Standard back over the user's choice. // any tab writes that Standard back over the user's choice.
let mut config = Config::default(); let mut config = Config::default();
config.arrow.style = wayscriber::draw::ArrowStyle::Curved; config.arrow.style = ArrowStyle::Curved;
let mut draft = ConfigDraft::from_config(&config); let mut draft = ConfigDraft::from_config(&config);
assert_eq!(draft.arrow_style, ArrowStyleOption::Curved); assert_eq!(draft.arrow_style, ArrowStyleOption::Curved);
draft.arrow_style = ArrowStyleOption::Double; draft.arrow_style = ArrowStyleOption::Double;
let round_trip = draft.to_config(&config).expect("arrow style round trip"); let round_trip = draft.to_config(&config).expect("arrow style round trip");
assert_eq!(round_trip.arrow.style, wayscriber::draw::ArrowStyle::Double); assert_eq!(round_trip.arrow.style, ArrowStyle::Double);
} }
#[test] #[test]
@@ -347,10 +361,7 @@ fn config_draft_round_trips_region_capture_settings() {
let round_trip = draft let round_trip = draft
.to_config(&config) .to_config(&config)
.expect("region capture settings should round trip"); .expect("region capture settings should round trip");
assert_eq!( assert_eq!(round_trip.capture.region.picker, RegionPicker::Slurp);
round_trip.capture.region.picker,
wayscriber::config::RegionPicker::Slurp
);
assert!(!round_trip.capture.region.show_size_readout); assert!(!round_trip.capture.region.show_size_readout);
assert!(round_trip.capture.region.show_loupe); assert!(round_trip.capture.region.show_loupe);
assert!(!round_trip.capture.region.show_legend); assert!(!round_trip.capture.region.show_legend);
@@ -508,13 +519,13 @@ fn config_draft_rejects_path_escaping_save_names() {
#[test] #[test]
fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() { fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() {
let temp = crate::test_temp::tempdir().expect("create temp directory"); let temp = test_temp::tempdir().expect("create temp directory");
let path = temp.path().join("config.toml"); let path = temp.path().join("config.toml");
let original = format!( let original = format!(
"config_revision = {}\n# intentionally sparse\n", "config_revision = {}\n# intentionally sparse\n",
wayscriber::config::CURRENT_CONFIG_REVISION config::CURRENT_CONFIG_REVISION
); );
std::fs::write(&path, &original).expect("write sparse config"); fs::write(&path, &original).expect("write sparse config");
let document = ConfigDocument::load_from_path(&path).expect("load sparse config"); let document = ConfigDocument::load_from_path(&path).expect("load sparse config");
let draft = ConfigDraft::from_config(document.config()); let draft = ConfigDraft::from_config(document.config());
let updated = draft let updated = draft
@@ -526,7 +537,7 @@ fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() {
.expect("save untouched sparse draft"); .expect("save untouched sparse draft");
assert_eq!( assert_eq!(
std::fs::read_to_string(path).expect("read sparse config"), fs::read_to_string(path).expect("read sparse config"),
original original
); );
} }
@@ -559,9 +570,9 @@ fn config_draft_round_trips_precise_floats_without_truncation() {
#[test] #[test]
fn sparse_configurator_edit_adds_only_the_edited_field_path() { fn sparse_configurator_edit_adds_only_the_edited_field_path() {
let temp = crate::test_temp::tempdir().expect("create temp directory"); let temp = test_temp::tempdir().expect("create temp directory");
let path = temp.path().join("config.toml"); let path = temp.path().join("config.toml");
std::fs::write(&path, "# intentionally sparse\n").expect("write sparse config"); fs::write(&path, "# intentionally sparse\n").expect("write sparse config");
let document = ConfigDocument::load_from_path(&path).expect("load sparse config"); let document = ConfigDocument::load_from_path(&path).expect("load sparse config");
let mut draft = ConfigDraft::from_config(document.config()); let mut draft = ConfigDraft::from_config(document.config());
draft.performance_max_fps_no_vsync = "144".to_string(); draft.performance_max_fps_no_vsync = "144".to_string();
@@ -573,7 +584,7 @@ fn sparse_configurator_edit_adds_only_the_edited_field_path() {
.save_with_backup(updated) .save_with_backup(updated)
.expect("save sparse draft edit"); .expect("save sparse draft edit");
let saved = std::fs::read_to_string(path).expect("read sparse config"); let saved = fs::read_to_string(path).expect("read sparse config");
assert!(saved.contains("# intentionally sparse")); assert!(saved.contains("# intentionally sparse"));
assert!(saved.contains("[performance]")); assert!(saved.contains("[performance]"));
assert!(saved.contains("max_fps_no_vsync = 144")); assert!(saved.contains("max_fps_no_vsync = 144"));
@@ -634,13 +645,13 @@ fn config_draft_round_trips_quick_colors() {
/// would come back on the next save. /// would come back on the next save.
#[test] #[test]
fn a_config_saved_in_chinese_keeps_its_font_and_color_names() { fn a_config_saved_in_chinese_keeps_its_font_and_color_names() {
crate::test_env::in_language_child( test_env::in_language_child(
concat!( concat!(
module_path!(), module_path!(),
"::a_config_saved_in_chinese_keeps_its_font_and_color_names" "::a_config_saved_in_chinese_keeps_its_font_and_color_names"
), ),
|| { || {
wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); i18n::set_language(domain::Language::SimplifiedChinese);
let mut config = Config::default(); let mut config = Config::default();
config.drawing.font_style = "italic".to_string(); config.drawing.font_style = "italic".to_string();
config.drawing.font_weight = "light".to_string(); config.drawing.font_weight = "light".to_string();
@@ -687,7 +698,7 @@ fn config_draft_preserves_implicit_quick_color_defaults() {
#[test] #[test]
fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file() { fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file() {
let temp = crate::test_temp::tempdir().expect("create temp directory"); let temp = test_temp::tempdir().expect("create temp directory");
let path = temp.path().join("config.toml"); let path = temp.path().join("config.toml");
let original = format!( let original = format!(
r#"config_revision = {} r#"config_revision = {}
@@ -695,9 +706,9 @@ fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file(
label = "Only configured color" label = "Only configured color"
color = "blue" color = "blue"
"#, "#,
wayscriber::config::CURRENT_CONFIG_REVISION config::CURRENT_CONFIG_REVISION
); );
std::fs::write(&path, &original).expect("write sparse quick colors"); fs::write(&path, &original).expect("write sparse quick colors");
let document = ConfigDocument::load_from_path(&path).expect("load sparse quick colors"); let document = ConfigDocument::load_from_path(&path).expect("load sparse quick colors");
assert_eq!( assert_eq!(
document document
@@ -723,7 +734,7 @@ color = "blue"
.save_with_backup(updated) .save_with_backup(updated)
.expect("save sparse quick colors"); .expect("save sparse quick colors");
assert_eq!( assert_eq!(
std::fs::read_to_string(path).expect("read sparse quick colors"), fs::read_to_string(path).expect("read sparse quick colors"),
original original
); );
} }
@@ -1112,7 +1123,7 @@ fn config_draft_round_trips_render_profiles() {
#[test] #[test]
fn config_draft_round_trips_board_pan_settings() { fn config_draft_round_trips_board_pan_settings() {
let mut config = Config::default(); let mut config = Config::default();
config.boards = Some(wayscriber::config::BoardsConfig { config.boards = Some(BoardsConfig {
pan_enabled: false, pan_enabled: false,
show_pan_badge: false, show_pan_badge: false,
..Default::default() ..Default::default()
@@ -1380,7 +1391,7 @@ fn section_toggle_replaces_overlay_item_override_on_save() {
.contains(&ToolbarSectionFlag::Presets.item_id()) .contains(&ToolbarSectionFlag::Presets.item_id())
); );
assert!( assert!(
wayscriber::config::resolve_section_visibility( config::resolve_section_visibility(
saved.ui.toolbar.layout_mode, saved.ui.toolbar.layout_mode,
&saved.ui.toolbar.mode_overrides, &saved.ui.toolbar.mode_overrides,
&resolved, &resolved,
@@ -1580,7 +1591,7 @@ fn config_draft_round_trips_presets_and_history() {
config.history.custom_redo_steps config.history.custom_redo_steps
); );
assert_eq!(round_trip.presets.slot_count, config.presets.slot_count); assert_eq!(round_trip.presets.slot_count, config.presets.slot_count);
assert_eq!(round_trip.presets.get_slot(1), config.presets.get_slot(1)); assert_eq!(round_trip.presets.slot(1), config.presets.slot(1));
} }
#[test] #[test]
@@ -1626,7 +1637,7 @@ fn preset_tool_change_loads_selected_tool_profile_values() {
let round_trip = draft let round_trip = draft
.to_config(&config) .to_config(&config)
.expect("expected config to round trip"); .expect("expected config to round trip");
let preset = round_trip.presets.get_slot(1).expect("preset"); let preset = round_trip.presets.slot(1).expect("preset");
let settings = preset.tool_settings.as_ref().expect("tool settings"); let settings = preset.tool_settings.as_ref().expect("tool settings");
assert_eq!(preset.tool, Tool::Marker); assert_eq!(preset.tool, Tool::Marker);
@@ -1683,7 +1694,7 @@ fn preset_visible_edits_update_selected_tool_profile_only() {
let round_trip = draft let round_trip = draft
.to_config(&config) .to_config(&config)
.expect("expected config to round trip"); .expect("expected config to round trip");
let preset = round_trip.presets.get_slot(1).expect("preset"); let preset = round_trip.presets.slot(1).expect("preset");
let settings = preset.tool_settings.as_ref().expect("tool settings"); let settings = preset.tool_settings.as_ref().expect("tool settings");
assert_eq!(preset.tool, Tool::Marker); assert_eq!(preset.tool, Tool::Marker);
@@ -1701,11 +1712,11 @@ fn preset_visible_edits_update_selected_tool_profile_only() {
#[test] #[test]
fn config_draft_round_trips_drag_tool_mapping() { fn config_draft_round_trips_drag_tool_mapping() {
let mut config = Config::default(); let mut config = Config::default();
config.drawing.drag_tool = wayscriber::input::DragBindableTool::Arrow; config.drawing.drag_tool = DragBindableTool::Arrow;
config.drawing.shift_drag_tool = wayscriber::input::DragBindableTool::Eraser; config.drawing.shift_drag_tool = DragBindableTool::Eraser;
config.drawing.ctrl_drag_tool = wayscriber::input::DragBindableTool::Pen; config.drawing.ctrl_drag_tool = DragBindableTool::Pen;
config.drawing.ctrl_shift_drag_tool = wayscriber::input::DragBindableTool::Rect; config.drawing.ctrl_shift_drag_tool = DragBindableTool::Rect;
config.drawing.tab_drag_tool = wayscriber::input::DragBindableTool::Ellipse; config.drawing.tab_drag_tool = DragBindableTool::Ellipse;
let mut drag_tools = config.drawing.effective_drag_tools(); let mut drag_tools = config.drawing.effective_drag_tools();
drag_tools.right.drag_tool = DragTool::Pen; drag_tools.right.drag_tool = DragTool::Pen;
drag_tools.right.drag_color = Some(ColorSpec::Name("blue".to_string())); drag_tools.right.drag_color = Some(ColorSpec::Name("blue".to_string()));
@@ -1826,10 +1837,10 @@ fn config_draft_round_trips_ui_language() {
#[test] #[test]
fn config_draft_ui_language_defaults_to_auto_when_key_missing() { fn config_draft_ui_language_defaults_to_auto_when_key_missing() {
let temp = crate::test_temp::tempdir().expect("create temp directory"); let temp = test_temp::tempdir().expect("create temp directory");
let missing = temp.path().join("missing-language.toml"); let missing = temp.path().join("missing-language.toml");
std::fs::write(&missing, "[ui]\ntheme = \"dark\"\n").expect("write config without language"); fs::write(&missing, "[ui]\ntheme = \"dark\"\n").expect("write config without language");
let document = ConfigDocument::load_from_path(&missing).expect("load config without language"); let document = ConfigDocument::load_from_path(&missing).expect("load config without language");
assert_eq!(document.config().ui.language, Language::Auto); assert_eq!(document.config().ui.language, Language::Auto);
assert_eq!( assert_eq!(
@@ -1843,10 +1854,10 @@ fn config_draft_ui_language_defaults_to_auto_when_key_missing() {
/// of reaching the draft as a silently rewritten value. /// of reaching the draft as a silently rewritten value.
#[test] #[test]
fn config_document_salvages_unknown_ui_language_to_auto() { fn config_document_salvages_unknown_ui_language_to_auto() {
let temp = crate::test_temp::tempdir().expect("create temp directory"); let temp = test_temp::tempdir().expect("create temp directory");
let unknown = temp.path().join("unknown-language.toml"); let unknown = temp.path().join("unknown-language.toml");
std::fs::write(&unknown, "[ui]\nlanguage = \"fr\"\n").expect("write unknown language"); fs::write(&unknown, "[ui]\nlanguage = \"fr\"\n").expect("write unknown language");
let (document, warning) = ConfigDocument::load_for_editing_from_path(&unknown) let (document, warning) = ConfigDocument::load_for_editing_from_path(&unknown)
.expect("expected the editing load to salvage the unknown language"); .expect("expected the editing load to salvage the unknown language");
@@ -1895,39 +1906,31 @@ fn config_draft_keeps_the_documents_revision_until_a_migration_is_applied() {
.expect("expected config to round trip"); .expect("expected config to round trip");
assert_eq!(untouched.config_revision, 0); assert_eq!(untouched.config_revision, 0);
draft.config_revision = Some(wayscriber::config::CURRENT_CONFIG_REVISION); draft.config_revision = Some(config::CURRENT_CONFIG_REVISION);
let migrated = draft let migrated = draft
.to_config(&base) .to_config(&base)
.expect("expected config to round trip"); .expect("expected config to round trip");
assert_eq!( assert_eq!(migrated.config_revision, config::CURRENT_CONFIG_REVISION);
migrated.config_revision,
wayscriber::config::CURRENT_CONFIG_REVISION
);
} }
#[test] #[test]
fn config_draft_preserves_tray_icon_style() { fn config_draft_preserves_tray_icon_style() {
let mut config = Config::default(); let mut config = Config::default();
config.tray.icon_style = wayscriber::config::TrayIconStyle::Colored; config.tray.icon_style = TrayIconStyle::Colored;
let draft = ConfigDraft::from_config(&config); let draft = ConfigDraft::from_config(&config);
let round_trip = draft let round_trip = draft
.to_config(&config) .to_config(&config)
.expect("expected config to round trip"); .expect("expected config to round trip");
assert_eq!( assert_eq!(round_trip.tray.icon_style, TrayIconStyle::Colored);
round_trip.tray.icon_style,
wayscriber::config::TrayIconStyle::Colored
);
} }
#[test] #[test]
fn compiled_defaults_are_not_shown_as_authored_keybindings() { fn compiled_defaults_are_not_shown_as_authored_keybindings() {
let draft = ConfigDraft::from_config(&Config::default()); let draft = ConfigDraft::from_config(&Config::default());
assert!( assert!(
!draft !draft.keybindings.is_authored(KeybindingField::Undo),
.keybindings
.is_authored(crate::models::KeybindingField::Undo),
"Config::default() is AllExplicit for validation, not for UI source badges" "Config::default() is AllExplicit for validation, not for UI source badges"
); );
} }
@@ -3,12 +3,11 @@ use super::super::parse::{
parse_field_in_range, parse_u8_in_range, parse_usize_at_least, parse_usize_in_range, parse_field_in_range, parse_u8_in_range, parse_usize_at_least, parse_usize_in_range,
}; };
use crate::models::error::FormError; use crate::models::error::FormError;
use wayscriber::config::MAX_SHAPE_RECOGNITION_SENSITIVITY;
use wayscriber::config::{ use wayscriber::config::{
ARROW_ANGLE_MAX, ARROW_ANGLE_MIN, ARROW_LENGTH_MAX, ARROW_LENGTH_MIN, Config, ARROW_ANGLE_MAX, ARROW_ANGLE_MIN, ARROW_LENGTH_MAX, ARROW_LENGTH_MIN, Config,
MAX_SHAPE_RECOGNITION_SENSITIVITY,
}; };
use wayscriber::domain::{DragBindableTool, DragTool}; use wayscriber::domain::{DragBindableTool, DragTool, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS};
use wayscriber::domain::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS};
use wayscriber::draw::{MAX_PEN_SMOOTHING, REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES}; use wayscriber::draw::{MAX_PEN_SMOOTHING, REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES};
impl ConfigDraft { impl ConfigDraft {
@@ -1,3 +1,5 @@
use wayscriber::config;
use super::super::draft::ConfigDraft; use super::super::draft::ConfigDraft;
use super::super::parse::{ use super::super::parse::{
parse_field, parse_field_in_range, parse_u64_field, parse_u64_in_range, parse_usize_field, parse_field, parse_field_in_range, parse_u64_field, parse_u64_in_range, parse_usize_field,
@@ -65,7 +67,7 @@ impl ConfigDraft {
config.ui.toolbar.stroke_controls = self.ui_toolbar_stroke_controls.to_config(); config.ui.toolbar.stroke_controls = self.ui_toolbar_stroke_controls.to_config();
config.ui.toolbar.mode_overrides = self.ui_toolbar_mode_overrides.to_config(); config.ui.toolbar.mode_overrides = self.ui_toolbar_mode_overrides.to_config();
config.ui.toolbar.items = self.ui_toolbar_items.clone(); config.ui.toolbar.items = self.ui_toolbar_items.clone();
let visibility = wayscriber::config::resolve_section_visibility( let visibility = config::resolve_section_visibility(
config.ui.toolbar.layout_mode, config.ui.toolbar.layout_mode,
&config.ui.toolbar.mode_overrides, &config.ui.toolbar.mode_overrides,
&config.ui.toolbar.items.resolved(), &config.ui.toolbar.items.resolved(),
+5 -3
View File
@@ -1,3 +1,5 @@
use std::env;
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use wayscriber::env_vars::{ use wayscriber::env_vars::{
@@ -18,11 +20,11 @@ pub enum DesktopEnvironment {
impl DesktopEnvironment { impl DesktopEnvironment {
pub fn detect_current() -> Self { pub fn detect_current() -> Self {
if std::env::var_os(HYPRLAND_INSTANCE_SIGNATURE_ENV).is_some() { if env::var_os(HYPRLAND_INSTANCE_SIGNATURE_ENV).is_some() {
return Self::Hyprland; return Self::Hyprland;
} }
let current = std::env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default(); let current = env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default();
let session = std::env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default(); let session = env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default();
Self::from_desktop_strings(&current, &session) Self::from_desktop_strings(&current, &session)
} }
+5 -2
View File
@@ -1,3 +1,6 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use wayscriber::draw::ArrowStyle; use wayscriber::draw::ArrowStyle;
@@ -49,8 +52,8 @@ impl ArrowStyleOption {
} }
} }
impl std::fmt::Display for ArrowStyleOption { impl Display for ArrowStyleOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
+5 -2
View File
@@ -1,3 +1,6 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use wayscriber::config::RegionPicker; use wayscriber::config::RegionPicker;
@@ -35,8 +38,8 @@ impl RegionPickerOption {
} }
} }
impl std::fmt::Display for RegionPickerOption { impl Display for RegionPickerOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
+9 -6
View File
@@ -1,3 +1,6 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use wayscriber::domain::EraserMode; use wayscriber::domain::EraserMode;
@@ -36,8 +39,8 @@ impl EraserModeOption {
} }
} }
impl std::fmt::Display for EraserModeOption { impl Display for EraserModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -79,8 +82,8 @@ impl PresetEraserKindOption {
} }
} }
impl std::fmt::Display for PresetEraserKindOption { impl Display for PresetEraserKindOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -122,8 +125,8 @@ impl PresetEraserModeOption {
} }
} }
impl std::fmt::Display for PresetEraserModeOption { impl Display for PresetEraserModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
+15 -12
View File
@@ -1,3 +1,6 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::i18n::tr; use wayscriber::i18n::tr;
use wayscriber::config::{ use wayscriber::config::{
@@ -46,8 +49,8 @@ impl PdfPageSizeOption {
} }
} }
impl std::fmt::Display for PdfPageSizeOption { impl Display for PdfPageSizeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -89,8 +92,8 @@ impl PdfOrientationOption {
} }
} }
impl std::fmt::Display for PdfOrientationOption { impl Display for PdfOrientationOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -136,8 +139,8 @@ impl PdfFitModeOption {
} }
} }
impl std::fmt::Display for PdfFitModeOption { impl Display for PdfFitModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -175,8 +178,8 @@ impl PdfTransparentBackgroundOption {
} }
} }
impl std::fmt::Display for PdfTransparentBackgroundOption { impl Display for PdfTransparentBackgroundOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -232,8 +235,8 @@ impl PdfLabelPositionOption {
} }
} }
impl std::fmt::Display for PdfLabelPositionOption { impl Display for PdfLabelPositionOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
@@ -289,8 +292,8 @@ impl PdfLabelContentModeOption {
} }
} }
impl std::fmt::Display for PdfLabelContentModeOption { impl Display for PdfLabelContentModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
Loaded 100 of 1262 files, more files were not shown because too many files have changed in this diff. Show more