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.
- 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
- 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.
- 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
- 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.
- 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`.
+18
View File
@@ -14,6 +14,24 @@ default-members = ["."]
[workspace.lints.clippy]
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]
workspace = true
+1 -2
View File
@@ -1,7 +1,6 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
fn main() {
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
# warning above this value, and CI promotes warnings to errors.
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::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;
use tokio::task;
const PRODUCTION_BLOCKING_JOB_LIMIT: usize = 2;
const SLOW_JOB_THRESHOLD: Duration = Duration::from_millis(250);
@@ -65,21 +65,21 @@ impl BlockingJobRunner {
.map_err(|_| format!("{kind} blocking job queue is unavailable"))?;
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 started_at = Instant::now();
let outcome = catch_unwind(AssertUnwindSafe(job));
let result = job();
report_slow_phase(kind, "execution", started_at.elapsed());
outcome
result
})
.await;
let outcome =
joined.map_err(|err| format!("{kind} blocking job did not complete: {err}"))?;
match outcome {
// A panicking job unwinds its blocking thread, which releases the
// permit; tokio reports the panic through the join error.
match joined {
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::thread;
use tokio::sync::oneshot;
use tokio::time::{Duration, timeout};
use super::*;
@@ -134,7 +135,7 @@ mod tests {
async fn blocking_work_is_offloaded_while_executor_work_progresses() {
let runner = BlockingJobRunner::new(1);
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 job = tokio::spawn(async move {
@@ -169,7 +170,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn one_permit_prevents_a_second_closure_from_starting() {
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_runner = runner.clone();
let first = tokio::spawn(async move {
@@ -186,7 +187,7 @@ mod tests {
.expect("first closure should start")
.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 {
runner
.run(BlockingJobKind::SessionCatalogLoad, move || {
@@ -238,7 +239,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dropping_the_waiter_does_not_release_a_running_jobs_permit() {
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_runner = runner.clone();
let first = tokio::spawn(async move {
@@ -268,7 +269,7 @@ mod tests {
"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 {
runner
.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.
#[allow(deprecated)]
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(
&display,
&provider,
@@ -46,6 +46,9 @@ fn load_app_stylesheet() {
const APP_ID: &str = "org.wayscriber.Configurator";
use effects::spawn_effect;
use gtk::gdk::Display;
use gtk::gio::ApplicationFlags;
use wayscriber::domain::Language;
pub(crate) fn run(startup: StartupRequest) {
// 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.
let application = adw::Application::builder()
.application_id(APP_ID)
.flags(gtk::gio::ApplicationFlags::NON_UNIQUE)
.flags(ApplicationFlags::NON_UNIQUE)
.build();
let app = RelmApp::from_app(application);
// GTK must not parse our argv: `--open <destination>` is ours and was
@@ -92,7 +95,7 @@ pub(crate) struct AppWidgets {
bindings: Vec<Binding>,
/// The language the pages were built in. When the active language moves
/// away from it, the next refresh rebuilds them.
language: wayscriber::domain::Language,
language: Language,
chrome: shell::Chrome,
}
+2 -1
View File
@@ -1,4 +1,5 @@
use relm4::ComponentSender;
use relm4::adw::ApplicationWindow;
use crate::messages::CommandMessage;
@@ -11,7 +12,7 @@ use super::super::{daemon_setup, io, session_catalog};
pub(super) fn spawn_effect(
effect: Effect,
sender: &ComponentSender<ConfiguratorApp>,
root: &relm4::adw::ApplicationWindow,
root: &ApplicationWindow,
) {
use relm4::gtk::prelude::*;
match effect {
+10 -6
View File
@@ -10,10 +10,14 @@
//! it, and how far the visible page is scrolled. This module carries those
//! across.
use std::iter;
use relm4::{ComponentSender, gtk};
use gtk::glib;
use gtk::prelude::*;
use gtk::glib::GString;
use gtk::prelude::{GtkWindowExt, *};
use wayscriber::i18n;
use super::super::pages;
use super::super::state::ConfiguratorApp;
@@ -56,7 +60,7 @@ impl WidgetPath {
}
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.
@@ -69,7 +73,7 @@ struct Caret {
/// View state from the pages being replaced, applied to the new ones.
pub(super) struct ViewState {
/// 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
/// persistent chrome, which the rebuild leaves alone.
focus: Option<(Option<WidgetPath>, Option<Caret>)>,
@@ -84,7 +88,7 @@ pub(super) fn rebuild_if_language_changed(
widgets: &mut AppWidgets,
sender: &ComponentSender<ConfiguratorApp>,
) -> Option<ViewState> {
let language = wayscriber::i18n::language();
let language = i18n::language();
if widgets.language == language {
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))
.map(|focused| {
let caret = focused
@@ -157,7 +161,7 @@ fn capture(widgets: &AppWidgets) -> ViewState {
fn descendants(widget: &gtk::Widget) -> impl Iterator<Item = gtk::Widget> {
let mut pending = vec![widget.clone()];
std::iter::from_fn(move || {
iter::from_fn(move || {
let next = pending.pop()?;
pending.extend(children(&next));
Some(next)
+11 -13
View File
@@ -2,7 +2,10 @@ use relm4::{ComponentSender, adw, gtk};
use adw::prelude::*;
use gtk::gdk::{Key, ModifierType};
use gtk::glib::Propagation;
use wayscriber::i18n::{tr, trf};
use wayscriber::{build_info, i18n};
use crate::messages::Message;
use crate::models::TabId;
@@ -39,7 +42,7 @@ pub(super) fn build(
let sender = sender.clone();
root.connect_close_request(move |_| {
sender.input(Message::CloseRequested);
gtk::glib::Propagation::Stop
Propagation::Stop
});
}
// ---- Sidebar ----------------------------------------------------
@@ -267,13 +270,11 @@ pub(super) fn build(
// lets widgets such as SearchEntry perform their own Escape behavior.
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
controller.connect_key_pressed(move |_, key, _, modifiers| {
if modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK)
&& matches!(key, gtk::gdk::Key::f | gtk::gdk::Key::F)
{
if modifiers.contains(ModifierType::CONTROL_MASK) && matches!(key, Key::f | Key::F) {
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
// Escape becomes a binding. Confirmation cancel is the
// fallback when nothing is recording.
@@ -281,10 +282,10 @@ pub(super) fn build(
}
// Tab is the user moving focus deliberately; a still-pending
// 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);
}
gtk::glib::Propagation::Proceed
Propagation::Proceed
});
root.add_controller(controller);
}
@@ -325,7 +326,7 @@ pub(super) fn build(
search_entry,
seen_focus_serial: 0,
bindings,
language: wayscriber::i18n::language(),
language: i18n::language(),
chrome: Chrome {
build_identity,
content_page,
@@ -385,10 +386,7 @@ pub(super) fn retitle(widgets: &AppWidgets) {
}
chrome.build_identity.set_label(&trf(
"Version {} · commit {}",
&[
&wayscriber::build_info::version(),
&wayscriber::build_info::commit_hash(),
],
&[&build_info::version(), &build_info::commit_hash()],
));
widgets
.window_title
+27 -22
View File
@@ -17,17 +17,23 @@
//! every supported language, whichever language is active.
use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
use std::{env, fs, iter, thread};
use relm4::prelude::*;
use relm4::{adw, gtk};
use adw::prelude::*;
use gtk::glib::MainContext;
use gtk::prelude::GtkWindowExt;
use wayscriber::domain::Language;
use wayscriber::{env_vars, i18n};
use crate::messages::{CommandMessage, Message};
use crate::models::{KeybindingField, LanguageOption, StartupRequest, TabId, UiTabId};
use crate::test_temp;
use super::super::state::ConfiguratorApp;
@@ -37,14 +43,14 @@ const CHILD_ENV: &str = "WAYSCRIBER_CONFIGURATOR_GTK_CHILD";
#[test]
fn language_changes_rebuild_the_window_in_place() {
// A window is allowed only on the dedicated test compositor.
if std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none()
|| std::env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests")
if env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none()
|| env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests")
{
eprintln!("skipping configurator GTK test: not on the private test compositor");
return;
}
if std::env::var_os(CHILD_ENV).is_none() {
if env::var_os(CHILD_ENV).is_none() {
run_in_child();
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,
/// and the catalogs load from the embedded copies.
fn run_in_child() {
let homes = crate::test_temp::tempdir().expect("child homes");
let mut command = std::process::Command::new(std::env::current_exe().expect("test binary"));
let homes = test_temp::tempdir().expect("child homes");
let mut command = Command::new(env::current_exe().expect("test binary"));
command
.args([
concat!(
@@ -72,21 +78,21 @@ fn run_in_child() {
"--nocapture",
])
.env(CHILD_ENV, "1")
.env_remove(wayscriber::env_vars::LANGUAGE_OVERRIDE_ENV);
.env_remove(env_vars::LANGUAGE_OVERRIDE_ENV);
for (key, directory) in [
("XDG_CONFIG_HOME", "config"),
("XDG_DATA_HOME", "data"),
("XDG_STATE_HOME", "state"),
("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.
("PATH", "bin"),
] {
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);
}
for key in wayscriber::env_vars::LOCALE_ENV_KEYS {
for key in env_vars::LOCALE_ENV_KEYS {
command.env_remove(key);
}
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) {
let directory = config_home.join("wayscriber");
std::fs::create_dir_all(&directory).expect("config directory");
std::fs::write(directory.join("config.toml"), contents).expect("write config");
fs::create_dir_all(&directory).expect("config directory");
fs::write(directory.join("config.toml"), contents).expect("write config");
}
fn run_scenario() {
@@ -111,7 +117,7 @@ fn run_scenario() {
!app.model().document.is_loading()
});
settle();
assert_eq!(wayscriber::i18n::language(), Language::English);
assert_eq!(i18n::language(), Language::English);
assert_eq!(sidebar_label(&app, TabId::Drawing), "Drawing");
let pages = app.widgets().stack.observe_children().n_items();
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)
.into_iter()
.filter(|widget| {
std::iter::successors(Some(widget.clone()), gtk::Widget::parent)
iter::successors(Some(widget.clone()), gtk::Widget::parent)
.take_while(|ancestor| *ancestor != page)
.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));
settle();
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",
);
@@ -440,8 +446,7 @@ fn a_load_that_changes_the_language_keeps_its_resets(app: &Controller<Configurat
settle();
app.emit(Message::LeaveDiscardRequested);
pump_until("the reload", || {
!app.model().document.is_loading()
&& wayscriber::i18n::language() == Language::SimplifiedChinese
!app.model().document.is_loading() && i18n::language() == Language::SimplifiedChinese
});
settle();
@@ -492,7 +497,7 @@ fn assert_caret(
}
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> {
@@ -504,7 +509,7 @@ fn descendants(root: gtk::Widget) -> Vec<gtk::Widget> {
let mut found = Vec::new();
let mut pending = vec![root];
while let Some(widget) = pending.pop() {
pending.extend(std::iter::successors(
pending.extend(iter::successors(
widget.first_child(),
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.
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);
while !done() {
assert!(Instant::now() < deadline, "timed out waiting for {what}");
while context.pending() {
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.
fn settle() {
let context = gtk::glib::MainContext::default();
let context = MainContext::default();
let until = Instant::now() + Duration::from_millis(300);
while Instant::now() < until {
while context.pending() {
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 gtk::prelude::GtkWindowExt;
use wayscriber::i18n::tr;
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_was_pending = widgets.leave_actions.get_visible();
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);
if leave_pending && !leave_was_pending {
@@ -1,4 +1,5 @@
use wayscriber::i18n::{tr, trf};
use wayscriber::{durable_io, paths};
use std::fs;
use std::io::ErrorKind;
@@ -85,7 +86,7 @@ impl HyprlandLightControlsInstallResult {
}
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 {
include_path: None,
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> {
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()
})?;
let binary_path = resolve_wayscriber_binary_path()?;
@@ -151,7 +152,7 @@ fn write_light_controls(
})?;
let include_path = light_controls_include_path(config_root);
wayscriber::durable_io::write_text_atomic(
durable_io::write_text_atomic(
&include_path,
&render_light_controls(binary_path),
AtomicWriteOptions::user_config_file(),
@@ -174,7 +175,7 @@ fn write_light_controls(
source_configured = true;
source_updated = changed;
if changed {
wayscriber::durable_io::write_text_atomic(
durable_io::write_text_atomic(
&main_config_path,
&updated_content,
AtomicWriteOptions::user_config_file(),
@@ -326,7 +327,7 @@ fn strip_matching_quotes(value: &str) -> &str {
fn normalize_source_target(target: &str) -> PathBuf {
let expanded = if let Some(stripped) = target.strip_prefix("~/") {
wayscriber::paths::home_dir()
paths::home_dir()
.map(|home| home.join(stripped))
.unwrap_or_else(|| PathBuf::from(target))
} else {
@@ -1,3 +1,6 @@
use crate::test_env::EnvGuard;
use crate::test_temp;
use super::*;
use wayscriber::env_vars::HOME_ENV;
@@ -76,9 +79,9 @@ fn has_source_line_matches_quoted_and_inline_commented_targets() {
#[test]
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 _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
.join(".config")
@@ -93,7 +96,7 @@ fn has_source_line_matches_tilde_target() {
#[test]
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);
fs::create_dir_all(&hypr_dir).unwrap();
let main = hypr_dir.join(MAIN_CONFIG);
@@ -113,7 +116,7 @@ fn write_light_controls_writes_include_and_sources_existing_main() {
#[test]
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);
fs::create_dir_all(&hypr_dir).unwrap();
let main = hypr_dir.join(MAIN_CONFIG);
@@ -131,7 +134,7 @@ fn write_light_controls_is_idempotent_for_existing_source() {
#[test]
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();
+2 -2
View File
@@ -7,7 +7,7 @@ use wayscriber::i18n::{tr, trf};
use crate::models::{
DaemonAction, DaemonActionResult, DaemonRuntimeStatus, DesktopEnvironment,
LightShortcutApplyCapability, ShortcutBackend,
LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend,
};
use command::command_available;
@@ -139,7 +139,7 @@ pub(super) fn load_daemon_runtime_status_sync() -> Result<DaemonRuntimeStatus, S
read_portal_shortcut_dropin_state(),
portal_runtime_supported,
);
let shortcut_apply_capability = crate::models::ShortcutApplyCapability::from_environment(
let shortcut_apply_capability = ShortcutApplyCapability::from_environment(
desktop,
gsettings_available,
systemctl_available,
+3 -3
View File
@@ -1,9 +1,9 @@
use wayscriber::durable_io;
use wayscriber::i18n::{tr, trf};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use crate::models::DesktopEnvironment;
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);
wayscriber::durable_io::write_text_atomic(
durable_io::write_text_atomic(
&service_path,
&contents,
AtomicWriteOptions::user_config_file(),
@@ -1,6 +1,8 @@
use wayscriber::durable_io;
use wayscriber::i18n::{tr, trf};
use std::fs;
use std::path::PathBuf;
use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend};
use wayscriber::durable_io::AtomicWriteOptions;
@@ -176,7 +178,7 @@ fn run_gsettings_command(args: &[&str]) -> Result<(), String> {
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(|| {
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_app_id = escape_systemd_env_value(PORTAL_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,
&contents,
AtomicWriteOptions::user_config_file(),
+6 -2
View File
@@ -240,6 +240,10 @@ fn action_pending_message(action: DaemonAction) -> &'static str {
#[cfg(test)]
mod tests {
use wayscriber::i18n;
use crate::test_env;
use super::*;
use wayscriber::domain::Language;
@@ -298,7 +302,7 @@ mod tests {
#[test]
fn feedback_renders_in_the_language_active_when_it_is_drawn() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::feedback_renders_in_the_language_active_when_it_is_drawn"
@@ -313,7 +317,7 @@ mod tests {
let detecting = DaemonFeedback::Status(StatusFeedback::Detecting);
let english = (failed.text(), detecting.text());
wayscriber::i18n::set_language(Language::SimplifiedChinese);
i18n::set_language(Language::SimplifiedChinese);
assert_eq!(
english.0,
+2 -2
View File
@@ -1,4 +1,5 @@
//! Owns document transfer to effects and mutually exclusive load/save phases.
use std::mem;
use std::path::PathBuf;
use wayscriber::config::{ConfigDocument, ConfigValidationReport};
@@ -78,8 +79,7 @@ impl DocumentWorkflow {
if !self.is_saving() {
return None;
}
let DocumentPhase::Saving(context) =
std::mem::replace(&mut self.phase, DocumentPhase::Idle)
let DocumentPhase::Saving(context) = mem::replace(&mut self.phase, DocumentPhase::Idle)
else {
return None;
};
+2 -1
View File
@@ -1,4 +1,5 @@
use wayscriber::config::{Config, ConfigDocument};
use wayscriber::i18n;
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
// can preview any of them later, and reading a user override is file
// work that must not happen on the GTK thread.
wayscriber::i18n::prepare_catalogs();
i18n::prepare_catalogs();
ConfigDocument::load_for_editing()
.map(|(document, warning)| (Box::new(document), warning))
.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 \
changing this never restyles existing drawings.",
ArrowStyleOption::list(),
ArrowStyleOption::list()
&ArrowStyleOption::list()
.iter()
.map(|option| option.label().to_string())
.collect(),
.collect::<Vec<_>>(),
|app| app.draft.arrow_style,
Message::ArrowStyleChanged,
);
+11 -10
View File
@@ -34,6 +34,7 @@ use wayscriber::i18n::tr;
use relm4::prelude::*;
use relm4::{adw, gtk};
use wayscriber::domain::BoardGridKind;
use crate::messages::Message;
use crate::models::{
@@ -238,7 +239,7 @@ struct SectionLayout {
visible: bool,
expanded: bool,
background_kind: BoardBackgroundOption,
grid_kind: wayscriber::domain::BoardGridKind,
grid_kind: BoardGridKind,
pen_enabled: bool,
auto_adjust: 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.
struct BoardValues<'a> {
id: &'a str,
name: &'a str,
grid_spacing: &'a str,
background: ColorValues<'a>,
pen: ColorValues<'a>,
struct BoardValues<'draft> {
id: &'draft str,
name: &'draft str,
grid_spacing: &'draft str,
background: ColorValues<'draft>,
pen: ColorValues<'draft>,
}
/// 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
/// does.
struct ColorValues<'a> {
hex: &'a str,
color: &'a ColorTripletInput,
struct ColorValues<'draft> {
hex: &'draft str,
color: &'draft ColorTripletInput,
}
+5 -2
View File
@@ -1,5 +1,8 @@
use std::array;
use relm4::{ComponentSender, gtk};
use gtk::gdk::RGBA;
use gtk::glib::SignalHandlerId;
use gtk::prelude::*;
@@ -44,7 +47,7 @@ impl ColorRow {
}
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);
}
}
@@ -88,7 +91,7 @@ pub(super) fn build_color_row(
};
controls.append(&swatch);
let components = std::array::from_fn(|component| {
let components = array::from_fn(|component| {
let entry = gtk::Entry::builder()
.placeholder_text(COMPONENT_PLACEHOLDERS.get(component).copied().unwrap_or(""))
.width_chars(6)
+2 -4
View File
@@ -1,9 +1,7 @@
use wayscriber::i18n::tr;
use super::{
SectionLayout,
rows::{TextRow, build_text_row},
};
use super::SectionLayout;
use super::rows::{TextRow, build_text_row};
use crate::app::state::ConfiguratorApp;
use crate::messages::Message;
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("Native draws the picker over a frozen desktop image. Slurp uses the external selector."),
RegionPickerOption::list(),
labels(RegionPickerOption::list(), RegionPickerOption::label),
&labels(RegionPickerOption::list(), RegionPickerOption::label),
|app| app.draft.capture.region_picker,
Message::CaptureRegionPickerChanged,
)
@@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Page size"),
"",
PdfPageSizeOption::list(),
labels(PdfPageSizeOption::list(), PdfPageSizeOption::label),
&labels(PdfPageSizeOption::list(), PdfPageSizeOption::label),
|app| app.draft.export_pdf_page_size,
Message::ExportPdfPageSizeChanged,
)
@@ -134,7 +134,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Orientation"),
"",
PdfOrientationOption::list(),
labels(PdfOrientationOption::list(), PdfOrientationOption::label),
&labels(PdfOrientationOption::list(), PdfOrientationOption::label),
|app| app.draft.export_pdf_orientation,
Message::ExportPdfOrientationChanged,
)
@@ -142,7 +142,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Fit"),
"",
PdfFitModeOption::list(),
labels(PdfFitModeOption::list(), PdfFitModeOption::label),
&labels(PdfFitModeOption::list(), PdfFitModeOption::label),
|app| app.draft.export_pdf_fit,
Message::ExportPdfFitChanged,
)
@@ -150,7 +150,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Transparent page background"),
"",
PdfTransparentBackgroundOption::list(),
labels(
&labels(
PdfTransparentBackgroundOption::list(),
PdfTransparentBackgroundOption::label,
),
@@ -185,7 +185,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Label position"),
"",
PdfLabelPositionOption::list(),
labels(
&labels(
PdfLabelPositionOption::list(),
PdfLabelPositionOption::label,
),
@@ -196,7 +196,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Label content"),
"",
PdfLabelContentModeOption::list(),
labels(
&labels(
PdfLabelContentModeOption::list(),
PdfLabelContentModeOption::label,
),
+6 -5
View File
@@ -16,6 +16,7 @@
use relm4::{adw, gtk};
use adw::prelude::*;
use gtk::gdk::RGBA;
use gtk::glib::SignalHandlerId;
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`,
/// 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 (r, g, b, a) = (
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
/// 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 (r, g, b, a) = (
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(
button: &gtk::ColorDialogButton,
handler: &SignalHandlerId,
rgba: &gtk::gdk::RGBA,
rgba: &RGBA,
) {
if dialog_hex(&button.rgba()) == dialog_hex(rgba) {
return;
@@ -140,7 +141,7 @@ pub(crate) fn color_row(
set_text_blocked(&row, &row_handler, &text);
mark_hex_error(&row, &text);
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);
}
});
@@ -153,7 +154,7 @@ mod tests {
#[test]
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(&rgba), "#123456");
+7 -2
View File
@@ -1,3 +1,8 @@
use wayscriber::domain::Language;
use wayscriber::i18n;
use crate::test_env;
use super::*;
use crate::models::{
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability,
@@ -122,14 +127,14 @@ fn overall_status_follows_service_state() {
#[test]
fn feedback_tone_follows_the_outcome_in_every_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::feedback_tone_follows_the_outcome_in_every_language"
),
|| {
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 loaded = DaemonFeedback::Status(StatusFeedback::Loaded);
+4 -4
View File
@@ -92,13 +92,13 @@ fn section_combo_row<O>(
list: &gtk::ListBox,
title: &str,
values: Vec<O>,
labels: Vec<String>,
labels: &[String],
get: impl Fn(&ConfiguratorApp) -> O + 'static,
to_message: impl Fn(O) -> Message + 'static,
) where
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);
list.append(&row);
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) {
if let Some(index) = values.iter().position(|value| *value == current) {
fn select_if_changed<O: PartialEq>(row: &adw::ComboRow, values: &[O], current: &O) {
if let Some(index) = values.iter().position(|value| value == current) {
let index = index as u32;
if row.selected() != index {
row.set_selected(index);
@@ -18,7 +18,7 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Color mode"),
tr("A palette name or hex string, or explicit RGB components."),
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,
Message::ColorModeChanged,
);
@@ -29,7 +29,7 @@ pub(super) fn build(page: &mut PageBuilder) {
&named,
tr("Named color"),
NamedColorOption::list(),
named_color_labels(),
&named_color_labels(),
|app| app.draft.drawing_color.selected_named,
Message::NamedColorSelected,
);
@@ -1,3 +1,4 @@
use wayscriber::config;
use wayscriber::i18n::tr;
use crate::messages::Message;
@@ -37,10 +38,10 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Eraser mode"),
"",
EraserModeOption::list(),
EraserModeOption::list()
&EraserModeOption::list()
.iter()
.map(|option| option.label().to_string())
.collect(),
.collect::<Vec<_>>(),
|app| app.draft.drawing_default_eraser_mode,
Message::EraserModeChanged,
)
@@ -64,7 +65,7 @@ pub(super) fn build(page: &mut PageBuilder) {
validate_usize_range(
&app.draft.drawing_shape_recognition_sensitivity,
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,
field.label(),
tools.clone(),
tool_labels.clone(),
&tool_labels,
move |app| drag_tool_for_field(drag_button_config(app, button), field),
move |option| Message::DrawingMouseDragToolChanged(button, field, option),
);
@@ -95,7 +95,7 @@ fn build_drag_button_section(page: &mut PageBuilder, button: DragMouseButton) {
&list,
&trf("{} color", &[&field.label()]),
colors.clone(),
color_labels.clone(),
&color_labels,
move |app| drag_color_for_field(drag_button_config(app, button), field),
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 adw::prelude::*;
use gtk::pango::{AttrFontDesc, AttrList, FontDescription};
use crate::messages::Message;
use crate::models::{FontStyleOption, FontWeightOption, TextField};
@@ -27,10 +28,10 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Font weight"),
"",
FontWeightOption::list(),
FontWeightOption::list()
&FontWeightOption::list()
.iter()
.map(|option| option.label().to_string())
.collect(),
.collect::<Vec<_>>(),
|app| app.draft.drawing_font_weight_option,
Message::FontWeightOptionSelected,
)
@@ -43,10 +44,10 @@ pub(super) fn build(page: &mut PageBuilder) {
tr("Font style"),
"",
FontStyleOption::list(),
FontStyleOption::list()
&FontStyleOption::list()
.iter()
.map(|option| option.label().to_string())
.collect(),
.collect::<Vec<_>>(),
|app| app.draft.drawing_font_style_option,
Message::FontStyleOptionSelected,
);
@@ -130,7 +131,7 @@ fn build_font_cycle(page: &mut PageBuilder) {
.map(|row| row.layout)
.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
.draft
@@ -177,7 +178,7 @@ struct BoundFontRow {
fn rebuild_font_cycle(
list: &gtk::ListBox,
families: &gtk::StringList,
family_names: Rc<[String]>,
family_names: &Rc<[String]>,
layouts: &[FontCycleLayout],
sender: &ComponentSender<ConfiguratorApp>,
) -> Vec<BoundFontRow> {
@@ -189,7 +190,7 @@ fn rebuild_font_cycle(
.iter()
.map(|layout| {
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);
BoundFontRow {
layout: *layout,
@@ -292,8 +293,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory {
return;
};
label.set_label(&family);
let attributes = gtk::pango::AttrList::new();
attributes.insert(gtk::pango::AttrFontDesc::new(&family_preview_font(&family)));
let attributes = AttrList::new();
attributes.insert(AttrFontDesc::new(&family_preview_font(&family)));
label.set_attributes(Some(&attributes));
});
factory
@@ -304,8 +305,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory {
/// Built field by field: `FontDescription::from_string` reads trailing style,
/// weight, and stretch words off the family, so "Roboto Condensed" would preview
/// in Roboto and "Archivo Black" in Archivo.
fn family_preview_font(family: &str) -> gtk::pango::FontDescription {
let mut description = gtk::pango::FontDescription::new();
fn family_preview_font(family: &str) -> FontDescription {
let mut description = FontDescription::new();
description.set_family(family);
description
}
@@ -361,10 +362,13 @@ fn missing_family_note(family: &str) -> Option<String> {
#[cfg(test)]
mod tests {
use gtk::pango::{Stretch, Weight};
use wayscriber::draw;
use super::*;
fn installed() -> String {
wayscriber::draw::system_font_families()
draw::system_font_families()
.first()
.expect("at least one family")
.clone()
@@ -403,8 +407,8 @@ mod tests {
let description = family_preview_font(family);
assert_eq!(description.family().as_deref(), Some(family));
assert_eq!(description.weight(), gtk::pango::Weight::Normal);
assert_eq!(description.stretch(), gtk::pango::Stretch::Normal);
assert_eq!(description.weight(), Weight::Normal);
assert_eq!(description.stretch(), Stretch::Normal);
}
}
@@ -3,6 +3,7 @@ use wayscriber::i18n::{tr, trf};
use relm4::{ComponentSender, adw, gtk};
use adw::prelude::*;
use gtk::gdk::RGBA;
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.
struct QuickColorValues<'a> {
label: &'a str,
name: &'a str,
hex: &'a str,
struct QuickColorValues<'draft> {
label: &'draft str,
name: &'draft str,
hex: &'draft str,
named: NamedColorOption,
/// The color the entry resolves to, `None` when it resolves to nothing —
/// which is also what makes the name field an error.
@@ -224,7 +225,7 @@ fn build_quick_color_row(
tr("Color mode"),
&[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| {
Message::QuickColorModeChanged(index, value)
});
@@ -307,7 +308,7 @@ fn build_quick_color_row(
}
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);
}
});
@@ -8,7 +8,7 @@ mod widgets;
use wayscriber::i18n::tr;
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
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.",
))
.build();
for field in fields
let tab_fields = fields
.iter()
.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());
slots.push((field, row, group.clone()));
}
@@ -173,7 +173,7 @@ fn conflict_banner(
.child(&row)
.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 jump_field = jump_field.clone();
@@ -6,6 +6,8 @@ use std::rc::Rc;
use relm4::{ComponentSender, gtk};
use gtk::gdk::{ButtonEvent, EventType, InputSource, Key, ModifierType};
use gtk::glib::Propagation;
use gtk::glib::object::IsA;
use gtk::prelude::*;
@@ -151,25 +153,24 @@ fn attach_key_controller(popover: &gtk::Popover, sender: &ComponentSender<Config
let controller = gtk::EventControllerKey::new();
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
{
let sender = sender.clone();
let pressed = pressed.clone();
controller.connect_key_pressed(move |_, key, keycode, modifiers| {
// GTK repeats key-pressed for auto-repeat. Sequence recording
// would otherwise append Ctrl+K > Ctrl+K > Ctrl+K from a hold.
if !pressed.borrow_mut().insert(keycode) {
return gtk::glib::Propagation::Stop;
return Propagation::Stop;
}
let keyval = gdk_keyval(key);
let recorded = KeyboardModifiers {
ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK),
shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK),
alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK),
super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::META_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK),
ctrl: modifiers.contains(ModifierType::CONTROL_MASK),
shift: modifiers.contains(ModifierType::SHIFT_MASK),
alt: modifiers.contains(ModifierType::ALT_MASK),
super_held: modifiers.contains(ModifierType::SUPER_MASK)
|| modifiers.contains(ModifierType::META_MASK)
|| modifiers.contains(ModifierType::HYPER_MASK),
};
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();
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
controller.connect_event(move |_, event| {
if event.event_type() != gtk::gdk::EventType::ButtonPress {
return gtk::glib::Propagation::Proceed;
if event.event_type() != EventType::ButtonPress {
return Propagation::Proceed;
}
let Some(button_event) = event.downcast_ref::<gtk::gdk::ButtonEvent>() else {
return gtk::glib::Propagation::Proceed;
let Some(button_event) = event.downcast_ref::<ButtonEvent>() else {
return Propagation::Proceed;
};
let kind = event
.device()
.map(|device| match device.source() {
gtk::gdk::InputSource::Mouse
| gtk::gdk::InputSource::Touchpad
| gtk::gdk::InputSource::Trackpoint => RecorderDeviceKind::Mouse,
gtk::gdk::InputSource::Pen => RecorderDeviceKind::Pen,
InputSource::Mouse | InputSource::Touchpad | InputSource::Trackpoint => {
RecorderDeviceKind::Mouse
}
InputSource::Pen => RecorderDeviceKind::Pen,
_ => RecorderDeviceKind::Other,
})
.unwrap_or(RecorderDeviceKind::Other);
let modifiers = event.modifier_state();
let recorded = KeyboardModifiers {
ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK),
shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK),
alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK),
super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::META_MASK)
|| modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK),
ctrl: modifiers.contains(ModifierType::CONTROL_MASK),
shift: modifiers.contains(ModifierType::SHIFT_MASK),
alt: modifiers.contains(ModifierType::ALT_MASK),
super_held: modifiers.contains(ModifierType::SUPER_MASK)
|| modifiers.contains(ModifierType::META_MASK)
|| modifiers.contains(ModifierType::HYPER_MASK),
};
// Primary clicks must reach Finish / Remove Last Step / Cancel. Stop
// only bindable auxiliary mouse or stylus barrel events.
@@ -249,15 +250,15 @@ fn attach_button_controller(popover: &gtk::Popover, sender: &ComponentSender<Con
kind,
recorded,
));
gtk::glib::Propagation::Stop
Propagation::Stop
}
RecordedDevice::Unsupported { .. } => gtk::glib::Propagation::Proceed,
RecordedDevice::Unsupported { .. } => Propagation::Proceed,
}
});
popover.add_controller(controller);
}
fn gdk_keyval(key: gtk::gdk::Key) -> u32 {
fn gdk_keyval(key: Key) -> u32 {
use gtk::glib::translate::IntoGlib;
key.into_glib()
}
+7 -11
View File
@@ -1,12 +1,13 @@
use wayscriber::i18n::{tr, trf};
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use relm4::prelude::*;
use relm4::{adw, gtk};
use adw::prelude::*;
use gtk::pango::EllipsizeMode;
use wayscriber::config::Shortcut;
use crate::messages::Message;
@@ -82,7 +83,7 @@ pub(super) fn binding_row(
let compact_summary = gtk::Label::builder()
.css_classes(["dim-label"])
.ellipsize(gtk::pango::EllipsizeMode::End)
.ellipsize(EllipsizeMode::End)
.max_width_chars(24)
.xalign(1.0)
.build();
@@ -162,16 +163,11 @@ pub(super) fn binding_row(
let recorder = RecorderPopover::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_badges = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
let compact_mode = std::rc::Rc::new(std::cell::Cell::new(false));
let last_focus_serial = std::rc::Rc::new(std::cell::Cell::new(0));
let seen_chips = Rc::new(RefCell::new(Vec::<String>::new()));
let seen_badges = Rc::new(RefCell::new(Vec::<String>::new()));
let compact_mode = Rc::new(Cell::new(false));
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| {
compact_mode.set(is_compact);
set_visible(&compact, is_compact);
@@ -2,6 +2,7 @@ use wayscriber::i18n::tr;
use relm4::{ComponentSender, gtk};
use gtk::glib::SignalHandlerId;
use gtk::glib::object::IsA;
use gtk::prelude::*;
@@ -15,7 +16,7 @@ use super::widgets::{field_canceled, set_accessible_label, set_label, unparent_o
pub(super) struct TextEditorPopover {
pub popover: gtk::Popover,
entry: gtk::Entry,
entry_handler: gtk::glib::SignalHandlerId,
entry_handler: SignalHandlerId,
error: gtk::Label,
}
@@ -2,7 +2,8 @@
use wayscriber::i18n::{tr, trf};
use relm4::{gtk, prelude::*};
use relm4::gtk;
use relm4::prelude::*;
use gtk::prelude::*;
@@ -138,7 +139,7 @@ fn actions_bar(
row.append(&reset_all);
row.append(&review);
let sort_for_bind = sort.clone();
let sort_for_bind = sort;
bindings.push(Box::new(move |app, _summary| {
let selected = ShortcutManagerSort::ALL
.iter()
@@ -1,7 +1,8 @@
use relm4::{ComponentSender, gtk};
use gtk::accessible::Property;
use gtk::glib::object::IsA;
use gtk::prelude::*;
use gtk::prelude::{AccessibleExt, *};
use crate::messages::Message;
use crate::models::KeybindingField;
@@ -20,8 +21,8 @@ pub(super) fn icon_button(icon: &str, tooltip: &str) -> gtk::Button {
button
}
pub(super) fn set_accessible_label(widget: &impl gtk::prelude::AccessibleExt, label: &str) {
widget.update_property(&[gtk::accessible::Property::Label(label)]);
pub(super) fn set_accessible_label(widget: &impl AccessibleExt, label: &str) {
widget.update_property(&[Property::Label(label)]);
}
/// 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 ui;
use std::cell::Cell;
use relm4::prelude::*;
use relm4::{adw, gtk};
@@ -49,7 +51,7 @@ use super::state::ConfiguratorApp;
pub(crate) type Binding = Box<dyn FnMut(&ConfiguratorApp, &AppSearchSummary)>;
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.
@@ -76,7 +78,7 @@ impl Drop for ReplacingPages {
/// Whether the pages are being replaced right now; see [`ReplacingPages`].
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.
@@ -349,7 +351,7 @@ impl PageBuilder {
title: &str,
subtitle: &str,
values: Vec<O>,
labels: Vec<String>,
labels: &[String],
get: impl Fn(&ConfiguratorApp) -> O + 'static,
to_message: impl Fn(O) -> Message + 'static,
) -> &mut Self
@@ -376,7 +378,7 @@ impl PageBuilder {
where
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));
self
}
@@ -386,7 +388,7 @@ impl PageBuilder {
title: &str,
subtitle: &str,
values: Vec<O>,
labels: Vec<String>,
labels: &[String],
get: impl Fn(&ConfiguratorApp) -> O + 'static,
to_message: impl Fn(O) -> Message + 'static,
) -> adw::ComboRow
+1 -1
View File
@@ -41,7 +41,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
buffer_metadata.label,
buffer_metadata.help,
buffer_counts,
buffer_labels,
&buffer_labels,
|app| app.draft.performance_buffer_count,
Message::BufferCountChanged,
)
+4 -4
View File
@@ -42,7 +42,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Visible slots"),
tr("How many preset slots the toolbar offers."),
counts,
count_labels,
&count_labels,
|app| app.draft.presets.slot_count,
Message::PresetSlotCountChanged,
);
@@ -187,7 +187,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
rows.combo_row(
tr("Tool"),
tools,
tool_labels,
&tool_labels,
move |app| app.draft.presets.slot(slot).map(|draft| draft.tool),
Message::PresetToolChanged,
);
@@ -215,7 +215,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
rows.combo_row(
tr("Eraser kind"),
kinds,
kind_labels,
&kind_labels,
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_kind),
Message::PresetEraserKindChanged,
);
@@ -224,7 +224,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
rows.combo_row(
tr("Eraser mode"),
modes,
mode_labels,
&mode_labels,
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_mode),
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(
tr("Color mode"),
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),
Message::PresetColorModeChanged,
);
@@ -52,7 +52,7 @@ pub(super) fn build_color_rows(rows: &mut SlotBuilder<'_>, slot: usize) {
let named_row = rows.combo_row(
tr("Named color"),
named,
named_labels,
&named_labels,
move |app| {
app.draft
.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
/// expander and reads the slot out of the model, so a slot the draft does
/// not hold simply leaves its rows alone.
pub(super) struct SlotBuilder<'a> {
pub(super) page: &'a mut PageBuilder,
pub(super) struct SlotBuilder<'page> {
pub(super) page: &'page mut PageBuilder,
pub(super) expander: adw::ExpanderRow,
pub(super) slot: usize,
}
@@ -69,7 +69,7 @@ impl SlotBuilder<'_> {
&mut self,
title: &str,
values: Vec<O>,
labels: Vec<String>,
labels: &[String],
get: impl Fn(&ConfiguratorApp) -> Option<O> + 'static,
to_message: impl Fn(usize, O) -> Message + 'static,
) -> adw::ComboRow
@@ -117,7 +117,7 @@ impl SlotBuilder<'_> {
) -> adw::ComboRow {
let values = OverrideOption::list();
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)
})
}
@@ -79,7 +79,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Canvas export profile"),
"",
export_options,
export_labels,
&export_labels,
|app| app.draft.render_profiles.export,
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.
struct ProfileValues<'a> {
id: &'a str,
name: &'a str,
struct ProfileValues<'draft> {
id: &'draft str,
name: &'draft str,
/// One entry per mapping row, in mapping order.
mappings: Vec<MappingValues<'a>>,
mappings: Vec<MappingValues<'draft>>,
}
/// One mapping row's two editing hex fields.
struct MappingValues<'a> {
from: &'a str,
to: &'a str,
struct MappingValues<'draft> {
from: &'draft 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 relm4::{ComponentSender, gtk};
use gtk::gdk::RGBA;
use gtk::glib::SignalHandlerId;
use gtk::prelude::*;
@@ -35,7 +36,7 @@ impl ColorField {
// rather than flashing it to black on every keystroke.
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);
}
}
@@ -208,12 +208,12 @@ fn item_card(
let clear_was_armed = confirm.get_visible();
let answer_has_focus = confirm_button.has_focus() || cancel_button.has_focus();
let focus_after_refresh = clear_focus_after_refresh(
clear_was_armed,
values.clear_armed,
values.clear_enabled,
let focus_after_refresh = clear_focus_after_refresh(ClearConfirmTransition {
was_armed: clear_was_armed,
is_armed: values.clear_armed,
clear_enabled: values.clear_enabled,
answer_has_focus,
);
});
if focus_after_refresh == ClearFocusTarget::Catalog {
// Confirm enters the busy state and disables the row's actions.
// Temporarily make the stable page target focusable and move there
@@ -242,10 +242,10 @@ fn item_card(
CatalogRow { card, refresh }
}
struct InputRow<'a> {
id: &'a str,
placeholder: &'a str,
button_label: &'a str,
struct InputRow<'text> {
id: &'text str,
placeholder: &'text str,
button_label: &'text str,
on_input: fn(String, String) -> Message,
request: Message,
}
@@ -293,12 +293,27 @@ enum ClearFocusTarget {
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,
/// The confirmation shows after this refresh.
is_armed: bool,
/// The clear action stays enabled after this refresh.
clear_enabled: bool,
/// Keyboard focus is on the confirm or cancel answer.
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 {
return ClearFocusTarget::Confirm;
}
@@ -319,22 +334,38 @@ mod tests {
#[test]
fn clear_confirmation_focus_has_enabled_targets_for_every_transition() {
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,
"arming focuses the newly revealed confirmation"
);
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,
"cancel returns to the restored clear action"
);
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,
"confirm moves away from the row actions disabled by busy state"
);
assert_eq!(
clear_focus_after_refresh(true, false, false, false),
clear_focus_after_refresh(ClearConfirmTransition {
was_armed: true,
..ClearConfirmTransition::default()
}),
ClearFocusTarget::Unchanged,
"a pointer-triggered transition does not steal keyboard focus"
);
@@ -1,5 +1,9 @@
use std::path::PathBuf;
use wayscriber::i18n;
use crate::test_env;
use super::*;
use crate::app::state::PendingConfirmation;
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.
#[test]
fn card_labels_follow_the_language_the_list_is_drawn_in() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::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);
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);
assert_eq!(chinese.items[0].last_opened_label, "从不");
@@ -56,7 +56,7 @@ pub(super) fn add(page: &mut PageBuilder) {
tr("Storage mode"),
"",
SessionStorageModeOption::list(),
option_labels(SessionStorageModeOption::list(), |option| option.label()),
&option_labels(&SessionStorageModeOption::list(), |option| option.label()),
|app| app.draft.session_storage_mode,
Message::SessionStorageModeChanged,
);
@@ -65,7 +65,7 @@ pub(super) fn add(page: &mut PageBuilder) {
tr("Compression"),
"",
SessionCompressionOption::list(),
option_labels(SessionCompressionOption::list(), |option| option.label()),
&option_labels(&SessionCompressionOption::list(), |option| option.label()),
|app| app.draft.session_compression,
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
.iter()
.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 {
let edit_modes = PressureThicknessEditModeOption::list();
let edit_mode_labels = edit_modes
let edit_mode_labels: Vec<String> = edit_modes
.iter()
.map(|mode| mode.label().to_string())
.collect();
let entry_modes = PressureThicknessEntryModeOption::list();
let entry_mode_labels = entry_modes
let entry_mode_labels: Vec<String> = entry_modes
.iter()
.map(|mode| mode.label().to_string())
.collect();
@@ -82,7 +82,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Pressure thickness edit mode"),
"",
edit_modes,
edit_mode_labels,
&edit_mode_labels,
|app| app.draft.tablet_pressure_thickness_edit_mode,
Message::TabletPressureEditModeChanged,
)
@@ -90,7 +90,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Pressure thickness entry mode"),
"",
entry_modes,
entry_mode_labels,
&entry_mode_labels,
|app| app.draft.tablet_pressure_thickness_entry_mode,
Message::TabletPressureEntryModeChanged,
);
+5 -2
View File
@@ -1,4 +1,7 @@
use wayscriber::domain::Language;
use wayscriber::i18n;
use crate::test_env;
use super::validate_u32_range;
@@ -22,10 +25,10 @@ fn an_invalid_whole_number_names_the_actual_bounds() {
#[test]
fn the_bounds_survive_translation() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(module_path!(), "::the_bounds_survive_translation"),
|| {
wayscriber::i18n::set_language(Language::SimplifiedChinese);
i18n::set_language(Language::SimplifiedChinese);
assert_eq!(
validate_u32_range("256", 0, 255).as_deref(),
Some("请输入 0 到 255 之间的整数。")
+2 -2
View File
@@ -32,7 +32,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Theme"),
tr("\"Auto\" uses dark. \"Light\" supports the command palette, including shortcut editing and tooltips; other popups still use dark."),
themes,
theme_labels,
&theme_labels,
|app| app.draft.ui_theme,
Message::UiThemeChanged,
)
@@ -40,7 +40,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Reduced motion"),
tr("\"On\" disables UI animations. \"Auto\" follows the system preference in a future release and keeps full motion for now."),
motions,
motion_labels,
&motion_labels,
|app| app.draft.ui_reduced_motion,
Message::UiReducedMotionChanged,
)
+2 -2
View File
@@ -30,7 +30,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
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."),
modes,
mode_labels,
&mode_labels,
|app| app.draft.input_hud_mode,
Message::InputHudModeChanged,
)
@@ -38,7 +38,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Screen position"),
"",
positions,
position_labels,
&position_labels,
|app| app.draft.input_hud_position,
Message::InputHudPositionChanged,
)
@@ -37,7 +37,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Top toolbar while presenting"),
"",
toolbar_modes,
toolbar_mode_labels,
&toolbar_mode_labels,
|app| app.draft.presenter_toolbar_mode,
Message::PresenterToolbarModeChanged,
)
@@ -69,7 +69,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Tool behavior"),
"",
behaviors,
behavior_labels,
&behavior_labels,
|app| app.draft.presenter_tool_behavior,
Message::PresenterToolBehaviorChanged,
)
+1 -1
View File
@@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Status bar position"),
"",
positions,
position_labels,
&position_labels,
|app| app.draft.ui_status_position,
Message::StatusPositionChanged,
);
+6 -6
View File
@@ -36,7 +36,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Layout mode"),
tr("Simple: core pens. Regular: adds Shape Pen, Line, Arrow, presets. Advanced: shapes inline, multi-step undo."),
layout_modes,
layout_labels,
&layout_labels,
|app| app.draft.ui_toolbar_layout_mode,
Message::ToolbarLayoutModeChanged,
)
@@ -44,7 +44,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Zoom chip"),
"",
zoom_chips,
zoom_chip_labels,
&zoom_chip_labels,
|app| app.draft.ui_toolbar_zoom_chip_display,
Message::ToolbarZoomChipDisplayChanged,
)
@@ -58,7 +58,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Shortcut edit click"),
"",
rebinds,
rebind_labels,
&rebind_labels,
|app| app.draft.ui_toolbar_rebind_modifier,
Message::ToolbarRebindModifierChanged,
)
@@ -66,7 +66,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
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."),
stroke_styles,
stroke_style_labels,
&stroke_style_labels,
|app| app.draft.ui_toolbar_stroke_controls,
Message::ToolbarStrokeControlsChanged,
)
@@ -179,7 +179,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
tr("Edit mode"),
tr("Overrides below apply to the mode selected here; \"Default\" keeps the mode preset."),
override_modes,
override_mode_labels,
&override_mode_labels,
|app| app.override_mode,
Message::ToolbarOverrideModeChanged,
);
@@ -198,7 +198,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
field.label(),
"",
values,
labels,
&labels,
move |app| toolbar_override(app, field),
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::models::{KeybindingsTabId, SearchQuery, TabId, UiTabId, keybinding_tab};
use wayscriber::config::{
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};
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];
parts.extend(area.section_titles());
for metadata in PERFORMANCE_FIELD_METADATA
let group_fields = PERFORMANCE_FIELD_METADATA
.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_from_slice(metadata.search_terms);
}
@@ -187,7 +201,7 @@ fn ui_matches(query: &SearchQuery, summary: &mut TabSearchSummary) {
.iter()
.copied()
.chain(
wayscriber::domain::Language::ALL
Language::ALL
.into_iter()
.flat_map(|language| [language.native_name(), language.word_for_language()])
.flatten(),
@@ -468,7 +482,7 @@ impl ScopedSearchQuery for SearchQuery {
.chain(parts.iter().copied()),
) || tab_scope_aliases(tab)
.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 {
+9 -8
View File
@@ -5,6 +5,9 @@
//! that every control label a page shows is indexed by the search terms —
//! return with the GTK page registry, once every page is ported.
use wayscriber::domain::Language;
use wayscriber::i18n;
use super::*;
use std::path::PathBuf;
@@ -12,6 +15,7 @@ use crate::models::session::SessionArtifactSummary;
use crate::models::{
KeybindingField, KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId,
};
use crate::test_env;
use wayscriber::config::toolbar_item_ids as ids;
#[test]
@@ -856,13 +860,13 @@ fn catalog_item(id: &str) -> SessionCatalogItem {
/// found, and titles as displayed find their tab, sub-tab and section.
#[test]
fn chinese_ui_search_matches_english_terms_and_displayed_titles() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::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 tabs_for = |app: &mut ConfiguratorApp, query: &str| {
app.search_query = SearchQuery::new(query);
@@ -889,17 +893,14 @@ fn chinese_ui_search_matches_english_terms_and_displayed_titles() {
#[test]
fn language_search_reveals_general_ui_in_either_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::language_search_reveals_general_ui_in_either_language"
),
|| {
for language in [
wayscriber::domain::Language::English,
wayscriber::domain::Language::SimplifiedChinese,
] {
wayscriber::i18n::set_language(language);
for language in [Language::English, Language::SimplifiedChinese] {
i18n::set_language(language);
let (mut app, _effects) = ConfiguratorApp::new_app();
for query in ["language", "locale", "English", "简体中文", "中文", "语言"] {
app.search_query = SearchQuery::new(query);
+22 -26
View File
@@ -1,3 +1,4 @@
use std::fs;
use std::fs::{File, OpenOptions};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
@@ -17,6 +18,9 @@ mod move_file;
pub(super) use duplicate::duplicate_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> {
run_blocking(
@@ -30,8 +34,7 @@ pub(super) async fn forget_session_catalog_entry(
id: String,
) -> Result<SessionCatalogActionResult, String> {
run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
let removed = wayscriber::session::catalog::forget_session_by_id(&id)
.map_err(|err| err.to_string())?;
let removed = catalog::forget_session_by_id(&id).map_err(|err| err.to_string())?;
let items = load_session_catalog_sync()?;
Ok(SessionCatalogActionResult::success(
if removed {
@@ -50,11 +53,8 @@ pub(super) async fn rename_session_catalog_entry(
display_name: String,
) -> Result<SessionCatalogActionResult, String> {
run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
let renamed = wayscriber::session::catalog::rename_session_display_name_by_id(
&id,
display_name.trim(),
)
.map_err(|err| err.to_string())?;
let renamed = catalog::rename_session_display_name_by_id(&id, display_name.trim())
.map_err(|err| err.to_string())?;
let items = load_session_catalog_sync()?;
Ok(SessionCatalogActionResult::success(
if let Some(entry) = renamed {
@@ -95,7 +95,7 @@ pub(super) async fn clear_session_catalog_entry(
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())?;
let items = load_session_catalog_sync()?;
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 outcome =
wayscriber::session::clear_tool_state(&options).map_err(|err| err.to_string())?;
let outcome = session::clear_tool_state(&options).map_err(|err| err.to_string())?;
let items = load_session_catalog_sync()?;
Ok(SessionCatalogActionResult::success(
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> {
wayscriber::session::catalog::recent_sessions()
catalog::recent_sessions()
.map_err(|err| err.to_string())?
.into_iter()
.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(
item: &SessionCatalogItem,
) -> Result<wayscriber::session::SessionOptions, String> {
let loaded = wayscriber::config::Config::load().map_err(|err| err.to_string())?;
) -> Result<SessionOptions, String> {
let loaded = Config::load().map_err(|err| err.to_string())?;
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,
item.path.clone(),
None,
@@ -173,7 +172,7 @@ fn named_session_options_for_catalog_item(
}
fn ensure_session_config_available_for_destructive_action(
loaded: &wayscriber::config::LoadedConfig,
loaded: &LoadedConfig,
) -> Result<(), String> {
if loaded.section_failed("session") {
return Err(
@@ -184,24 +183,21 @@ fn ensure_session_config_available_for_destructive_action(
Ok(())
}
fn clear_tool_state_catalog_message(
display_name: &str,
outcome: wayscriber::session::ClearToolStateOutcome,
) -> String {
fn clear_tool_state_catalog_message(display_name: &str, outcome: ClearToolStateOutcome) -> String {
match outcome {
wayscriber::session::ClearToolStateOutcome::NoSession => {
ClearToolStateOutcome::NoSession => {
trf("No saved session file found for {}.", &[&display_name])
}
wayscriber::session::ClearToolStateOutcome::NoToolState => {
ClearToolStateOutcome::NoToolState => {
trf("No saved tool state found for {}.", &[&display_name])
}
wayscriber::session::ClearToolStateOutcome::Cleared {
ClearToolStateOutcome::Cleared {
preserved_board_data: true,
} => trf(
tr("Cleared saved tool state for {}. Boards and history were preserved."),
&[&display_name],
),
wayscriber::session::ClearToolStateOutcome::Cleared {
ClearToolStateOutcome::Cleared {
preserved_board_data: false,
} => trf(
"Cleared saved tool state for {}. No board data was present.",
@@ -238,8 +234,8 @@ enum RuntimeLockKind {
impl RuntimeLockKind {
fn path(self) -> PathBuf {
match self {
Self::Daemon => wayscriber::paths::daemon_lock_file(),
Self::Overlay => wayscriber::paths::overlay_lock_file(),
Self::Daemon => paths::daemon_lock_file(),
Self::Overlay => paths::overlay_lock_file(),
}
}
@@ -320,7 +316,7 @@ fn acquire_runtime_lock_for_inactive_operation(
) -> Result<File, String> {
let path = kind.path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
fs::create_dir_all(parent).map_err(|err| {
format!(
"{} is disabled because the {} lock directory could not be created: {} ({err})",
operation.label(),
@@ -1,4 +1,7 @@
use wayscriber::i18n::{tr, trf};
use wayscriber::session;
use wayscriber::session::catalog;
use wayscriber::session::catalog::CatalogEvent;
use std::path::{Path, PathBuf};
@@ -39,11 +42,11 @@ fn duplicate_session_catalog_entry_sync(
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())?;
let entry = match wayscriber::session::catalog::upsert_session_event_with_display_name(
let entry = match catalog::upsert_session_event_with_display_name(
&outcome.target,
wayscriber::session::catalog::CatalogEvent::Saved,
CatalogEvent::Saved,
&item.display_name,
) {
Ok(entry) => entry,
@@ -81,11 +84,14 @@ fn duplicate_session_catalog_entry_sync(
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use std::fs;
use crate::models::{
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
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 super::*;
@@ -110,22 +116,22 @@ mod tests {
#[test]
fn duplicate_session_catalog_entry_copies_primary_and_catalogs_new_entry() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]);
let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("lecture-copy.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target);
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::write(&source_artifacts.backup, b"backup").unwrap();
std::fs::write(&source_artifacts.lock, b"lock").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
let source_artifacts = session::named_session_artifact_paths(&source);
let target_artifacts = session::named_session_artifact_paths(&target);
fs::write(&source_artifacts.primary, b"primary").unwrap();
fs::write(&source_artifacts.backup, b"backup").unwrap();
fs::write(&source_artifacts.lock, b"lock").unwrap();
let source_entry = catalog::upsert_session_event_with_display_name(
&source,
wayscriber::session::catalog::CatalogEvent::Saved,
CatalogEvent::Saved,
"Lecture",
)
.unwrap();
@@ -134,14 +140,11 @@ mod tests {
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.unwrap();
assert_eq!(
std::fs::read(&target_artifacts.primary).unwrap(),
b"primary"
);
assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary");
assert!(!target_artifacts.backup.exists());
assert!(!target_artifacts.lock.exists());
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
@@ -155,52 +158,47 @@ mod tests {
#[test]
fn duplicate_session_catalog_entry_warns_when_catalog_update_fails_after_copy() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]);
let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("lecture-copy.wayscriber-session");
std::fs::write(&source, b"primary").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
fs::write(&source, b"primary").unwrap();
let source_entry = catalog::upsert_session_event_with_display_name(
&source,
wayscriber::session::catalog::CatalogEvent::Saved,
CatalogEvent::Saved,
"Lecture",
)
.unwrap();
let lock_path = catalog_lock_path();
std::fs::remove_file(&lock_path).unwrap();
std::fs::create_dir(&lock_path).unwrap();
fs::remove_file(&lock_path).unwrap();
fs::create_dir(&lock_path).unwrap();
let result =
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.unwrap();
std::fs::remove_dir(&lock_path).unwrap();
fs::remove_dir(&lock_path).unwrap();
assert!(result.warning);
assert!(
result
.message
.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!(
result.items.len(),
1,
"warning should keep the pre-copy catalog rows visible"
);
assert_eq!(
wayscriber::session::catalog::recent_sessions()
.unwrap()
.len(),
1
);
assert_eq!(catalog::recent_sessions().unwrap().len(), 1);
}
fn catalog_lock_path() -> std::path::PathBuf {
let catalog_path = wayscriber::session::catalog::catalog_path();
fn catalog_lock_path() -> PathBuf {
let catalog_path = catalog::catalog_path();
let mut raw = OsString::from(catalog_path.as_os_str());
raw.push(".lock");
raw.into()
@@ -1,4 +1,6 @@
use wayscriber::i18n::{tr, trf};
use wayscriber::session;
use wayscriber::session::{NamedSessionMoveOutcome, catalog};
use std::path::{Path, PathBuf};
@@ -41,29 +43,27 @@ fn move_session_catalog_entry_sync(
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())?;
let entry =
match wayscriber::session::catalog::move_session_path_by_id(&item.id, &outcome.target) {
Ok(Some(entry)) => entry,
Ok(None) => {
return rollback_moved_artifacts_after_catalog_failure(
&item,
&outcome,
initial_items,
tr("the original catalog entry disappeared before metadata could be updated")
.to_string(),
);
}
Err(err) => {
return rollback_moved_artifacts_after_catalog_failure(
&item,
&outcome,
initial_items,
format!("failed to update the session catalog: {err}"),
);
}
};
let entry = match catalog::move_session_path_by_id(&item.id, &outcome.target) {
Ok(Some(entry)) => entry,
Ok(None) => {
return rollback_moved_artifacts_after_catalog_failure(
&item,
&outcome,
initial_items,
tr("the original catalog entry disappeared before metadata could be updated"),
);
}
Err(err) => {
return rollback_moved_artifacts_after_catalog_failure(
&item,
&outcome,
initial_items,
&format!("failed to update the session catalog: {err}"),
);
}
};
let items = match load_session_catalog_sync() {
Ok(items) => items,
Err(err) => {
@@ -88,11 +88,11 @@ fn move_session_catalog_entry_sync(
fn rollback_moved_artifacts_after_catalog_failure(
item: &SessionCatalogItem,
outcome: &wayscriber::session::NamedSessionMoveOutcome,
outcome: &NamedSessionMoveOutcome,
initial_items: Vec<SessionCatalogItem>,
reason: String,
reason: &str,
) -> 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(
"Move Session failed because {}; rolled back moved files for {}.",
&[&reason, &item.display_name],
@@ -118,10 +118,10 @@ fn reject_catalog_target_collision(
source_id: &str,
target: &Path,
) -> Result<(), String> {
if let Some(existing) = items.iter().find(|item| {
item.id != source_id
&& wayscriber::session::catalog::session_paths_match(&item.path, target)
}) {
if let Some(existing) = items
.iter()
.find(|item| item.id != source_id && catalog::session_paths_match(&item.path, target))
{
return Err(trf(
"Move Session target is already in the catalog as {}.",
&[&existing.display_name],
@@ -133,11 +133,16 @@ fn reject_catalog_target_collision(
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use std::fs;
use wayscriber::session::catalog::CatalogEvent;
use crate::models::{
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
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 super::*;
@@ -162,22 +167,22 @@ mod tests {
#[test]
fn move_session_catalog_entry_moves_artifacts_and_preserves_catalog_id() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]);
let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target);
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::write(&source_artifacts.backup, b"backup").unwrap();
std::fs::write(&source_artifacts.lock, b"source lock").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
let source_artifacts = session::named_session_artifact_paths(&source);
let target_artifacts = session::named_session_artifact_paths(&target);
fs::write(&source_artifacts.primary, b"primary").unwrap();
fs::write(&source_artifacts.backup, b"backup").unwrap();
fs::write(&source_artifacts.lock, b"source lock").unwrap();
let source_entry = catalog::upsert_session_event_with_display_name(
&source,
wayscriber::session::catalog::CatalogEvent::Saved,
CatalogEvent::Saved,
"Lecture",
)
.unwrap();
@@ -186,18 +191,12 @@ mod tests {
move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()).unwrap();
assert!(!result.warning);
assert_eq!(
std::fs::read(&target_artifacts.primary).unwrap(),
b"primary"
);
assert_eq!(std::fs::read(&target_artifacts.backup).unwrap(), b"backup");
assert_eq!(
std::fs::read(&source_artifacts.lock).unwrap(),
b"source lock"
);
assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary");
assert_eq!(fs::read(&target_artifacts.backup).unwrap(), b"backup");
assert_eq!(fs::read(&source_artifacts.lock).unwrap(), b"source lock");
assert!(!source_artifacts.primary.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[0].id, source_entry.id);
assert_eq!(Path::new(&recents[0].path), target.as_path());
@@ -205,22 +204,22 @@ mod tests {
#[test]
fn move_session_catalog_entry_failure_keeps_catalog_and_source_artifacts() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]);
let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target);
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::write(&source_artifacts.backup, b"backup").unwrap();
std::fs::write(&target_artifacts.clear_marker, b"cleared").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
let source_artifacts = session::named_session_artifact_paths(&source);
let target_artifacts = session::named_session_artifact_paths(&target);
fs::write(&source_artifacts.primary, b"primary").unwrap();
fs::write(&source_artifacts.backup, b"backup").unwrap();
fs::write(&target_artifacts.clear_marker, b"cleared").unwrap();
let source_entry = catalog::upsert_session_event_with_display_name(
&source,
wayscriber::session::catalog::CatalogEvent::Saved,
CatalogEvent::Saved,
"Lecture",
)
.unwrap();
@@ -229,16 +228,13 @@ mod tests {
.expect_err("target artifact should block move");
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!(
std::fs::read(&source_artifacts.primary).unwrap(),
b"primary"
);
assert_eq!(std::fs::read(&source_artifacts.backup).unwrap(), b"backup");
assert_eq!(
std::fs::read(&target_artifacts.clear_marker).unwrap(),
fs::read(&target_artifacts.clear_marker).unwrap(),
b"cleared"
);
let recents = wayscriber::session::catalog::recent_sessions().unwrap();
let recents = catalog::recent_sessions().unwrap();
assert_eq!(recents.len(), 1);
assert_eq!(recents[0].id, source_entry.id);
assert_eq!(Path::new(&recents[0].path), source.as_path());
@@ -246,75 +242,71 @@ mod tests {
#[test]
fn move_session_catalog_entry_rejects_catalog_target_collision_before_disk_move() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]);
let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session");
std::fs::write(&source, b"primary").unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
fs::write(&source, b"primary").unwrap();
let source_entry = catalog::upsert_session_event_with_display_name(
&source,
wayscriber::session::catalog::CatalogEvent::Saved,
CatalogEvent::Saved,
"Lecture",
)
.unwrap();
wayscriber::session::catalog::upsert_session_event_with_display_name(
&target,
wayscriber::session::catalog::CatalogEvent::Saved,
"Archive",
)
.unwrap();
catalog::upsert_session_event_with_display_name(&target, CatalogEvent::Saved, "Archive")
.unwrap();
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.expect_err("target catalog entry should block move");
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());
}
#[test]
fn move_session_catalog_entry_rolls_back_when_catalog_update_fails_after_move() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
]);
let source = temp.path().join("lecture.wayscriber-session");
let target = temp.path().join("archive.wayscriber-session");
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
std::fs::create_dir(&source_artifacts.backup).unwrap();
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
let source_artifacts = session::named_session_artifact_paths(&source);
fs::write(&source_artifacts.primary, b"primary").unwrap();
fs::create_dir(&source_artifacts.backup).unwrap();
let source_entry = catalog::upsert_session_event_with_display_name(
&source,
wayscriber::session::catalog::CatalogEvent::Saved,
CatalogEvent::Saved,
"Lecture",
)
.unwrap();
let lock_path = catalog_lock_path();
std::fs::remove_file(&lock_path).unwrap();
std::fs::create_dir(&lock_path).unwrap();
fs::remove_file(&lock_path).unwrap();
fs::create_dir(&lock_path).unwrap();
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
.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("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!(!target.exists());
let recents = wayscriber::session::catalog::recent_sessions().unwrap();
let recents = catalog::recent_sessions().unwrap();
assert_eq!(recents.len(), 1);
assert_eq!(Path::new(&recents[0].path), source.as_path());
}
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());
raw.push(".lock");
raw.into()
+33 -28
View File
@@ -1,9 +1,14 @@
use wayscriber::config::{ConfigSectionError, ConfigSource, ConfigValidationReport};
use super::*;
use std::path::PathBuf;
use crate::models::session::SessionArtifactSummary;
use crate::models::{
DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend,
};
use crate::test_env::EnvGuard;
use crate::test_temp;
use wayscriber::env_vars::XDG_RUNTIME_DIR_ENV;
fn daemon_status(active: bool) -> DaemonRuntimeStatus {
@@ -26,8 +31,8 @@ fn daemon_status(active: bool) -> DaemonRuntimeStatus {
#[test]
fn clear_policy_blocks_running_daemon() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let status = daemon_status(true);
let blocker = SessionCatalogOperation::Clear
@@ -39,8 +44,8 @@ fn clear_policy_blocks_running_daemon() {
#[test]
fn clear_policy_allows_inactive_daemon() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let status = daemon_status(false);
let blocker = SessionCatalogOperation::Clear.cached_status_blocker(Some(&status));
@@ -50,8 +55,8 @@ fn clear_policy_allows_inactive_daemon() {
#[test]
fn duplicate_policy_uses_duplicate_status_message() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::Duplicate
.cached_status_blocker(None)
.expect("unknown status should block duplicate");
@@ -61,8 +66,8 @@ fn duplicate_policy_uses_duplicate_status_message() {
#[test]
fn move_policy_uses_move_status_message() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::Move
.cached_status_blocker(None)
.expect("unknown status should block move");
@@ -72,8 +77,8 @@ fn move_policy_uses_move_status_message() {
#[test]
fn clear_policy_blocks_unknown_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::Clear
.cached_status_blocker(None)
.expect("unknown status should block clear");
@@ -83,8 +88,8 @@ fn clear_policy_blocks_unknown_daemon_status() {
#[test]
fn clear_tool_state_policy_uses_tool_state_status_message() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let blocker = SessionCatalogOperation::ClearToolState
.cached_status_blocker(None)
.expect("unknown status should block tool reset");
@@ -95,8 +100,8 @@ fn clear_tool_state_policy_uses_tool_state_status_message() {
#[test]
fn session_clear_transaction_guard_blocks_manual_daemon_lock() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
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 error = acquire_runtime_lock_for_inactive_operation(
RuntimeLockKind::Daemon,
@@ -109,8 +114,8 @@ fn session_clear_transaction_guard_blocks_manual_daemon_lock() {
#[test]
fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
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]
fn inactive_session_reservation_refuses_a_running_daemon() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
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 error = match reserve_inactive_session_operation(SessionCatalogOperation::Move) {
@@ -148,8 +153,8 @@ fn inactive_session_reservation_refuses_a_running_daemon() {
#[test]
fn cached_status_blocker_does_not_probe_runtime_locks() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
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 _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap();
let status = daemon_status(false);
@@ -164,8 +169,8 @@ fn cached_status_blocker_does_not_probe_runtime_locks() {
#[test]
fn clear_runtime_guards_hold_daemon_and_overlay_locks() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
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 _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,
last_opened_at_millis: None,
last_saved_at_millis: None,
artifacts: crate::models::session::SessionArtifactSummary {
artifacts: SessionArtifactSummary {
primary_exists: true,
backup_exists: false,
recovery_exists: false,
@@ -206,11 +211,11 @@ fn session_artifact_status_reports_size_when_present() {
#[test]
fn clear_tool_state_rejects_defaulted_session_config() {
let loaded = wayscriber::config::LoadedConfig {
config: wayscriber::config::Config::default(),
source: wayscriber::config::ConfigSource::Primary,
validation: wayscriber::config::ConfigValidationReport::default(),
section_errors: vec![wayscriber::config::ConfigSectionError {
let loaded = LoadedConfig {
config: Config::default(),
source: ConfigSource::Primary,
validation: ConfigValidationReport::default(),
section_errors: vec![ConfigSectionError {
section: "session".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.
use std::mem;
use crate::models::{
KeybindingField, PendingShortcutConflict, ShortcutRecorderState, ShortcutTextEditor,
};
@@ -59,7 +61,7 @@ impl ShortcutWorkflow {
}
pub(crate) fn take_recorder(&mut self) -> Option<ShortcutRecorderState> {
self.recorder()?;
match std::mem::take(&mut self.phase) {
match mem::take(&mut self.phase) {
ShortcutPhase::Recording(value) => Some(value),
_ => None,
}
@@ -98,7 +100,7 @@ impl ShortcutWorkflow {
}
pub(crate) fn take_conflict(&mut self) -> Option<PendingShortcutConflict> {
self.conflict()?;
match std::mem::take(&mut self.phase) {
match mem::take(&mut self.phase) {
ShortcutPhase::Conflict(value) => Some(value),
_ => None,
}
+24 -23
View File
@@ -1,6 +1,8 @@
use std::mem;
use wayscriber::configurator_destination::ConfiguratorScreen;
use crate::models::{SearchQuery, TabId, UiTabId};
use crate::models::{SearchQuery, TabId, UiTabId, keybindings};
use super::effects::Effect;
use super::state::ConfiguratorApp;
@@ -12,7 +14,7 @@ impl ConfiguratorApp {
/// 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.
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() {
self.status = self.status.clone().with_note(&problem.message());
@@ -40,8 +42,7 @@ impl ConfiguratorApp {
// nothing to show lands where the matches actually are.
self.align_active_tabs_for_search();
if self.active_tab == TabId::Keybindings
&& let Some(field) =
crate::models::keybindings::field_matching_search_term(&term)
&& let Some(field) = keybindings::field_matching_search_term(&term)
{
self.select_keybinding_field(field);
}
@@ -102,11 +103,18 @@ impl ConfiguratorApp {
#[cfg(test)]
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::configurator_destination;
use wayscriber::configurator_destination::{ConfiguratorDestination, KeybindingsSection};
use crate::app::search::SearchArea;
use crate::models::KeybindingField;
use crate::test_env;
use super::*;
use crate::app::state::StatusMessage;
use crate::messages::CommandMessage;
@@ -117,18 +125,18 @@ mod tests {
status.text().unwrap_or_default().to_string()
}
fn args(values: &[&str]) -> Vec<std::ffi::OsString> {
std::iter::once("wayscriber-configurator")
fn args(values: &[&str]) -> Vec<OsString> {
iter::once("wayscriber-configurator")
.chain(values.iter().copied())
.map(std::ffi::OsString::from)
.map(OsString::from)
.collect()
}
/// 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 path = dir.path().join("config.toml");
std::fs::write(&path, "").expect("write test config");
fs::write(&path, "").expect("write test config");
(dir, path)
}
@@ -140,8 +148,8 @@ mod tests {
}
/// The app as the launcher would start it, after its first config load.
fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, std::path::PathBuf) {
crate::test_env::sanitize_language_environment();
fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, PathBuf) {
test_env::sanitize_language_environment();
let (dir, path) = config_file();
let (mut app, _effects) =
ConfiguratorApp::new_app_with_startup(StartupRequest::from_args(args(values)));
@@ -237,8 +245,7 @@ mod tests {
#[test]
fn the_onboarding_hint_destination_lands_on_the_general_ui_setting() {
let launched =
wayscriber::configurator_destination::onboarding_hints_destination().as_arg();
let launched = configurator_destination::onboarding_hints_destination().as_arg();
let (app, _dir, _path) = app_launched_with(&["--open", &launched]);
assert_eq!(app.active_tab, TabId::Ui);
@@ -247,7 +254,7 @@ mod tests {
let ui = summary
.tab(TabId::Ui)
.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.
@@ -259,10 +266,7 @@ mod tests {
assert_eq!(app.active_tab, TabId::Keybindings);
assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing);
assert_eq!(app.search_query.raw(), "Clear Canvas");
assert_eq!(
app.selected_keybinding,
Some(crate::models::KeybindingField::ClearCanvas)
);
assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas));
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_keybindings_tab, KeybindingsTabId::Drawing);
assert_eq!(
app.selected_keybinding,
Some(crate::models::KeybindingField::ClearCanvas)
);
assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas));
}
/// 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 wayscriber::config::{Config, MigrationPreview, PRESET_SLOTS_MAX};
use wayscriber::i18n;
use crate::models::{
ColorPickerId, ConfigDraft, DesktopEnvironment, DragMouseButton, KeybindingField,
KeybindingsTabId, SearchQuery, SessionCatalogState, ShortcutManagerFilter, ShortcutManagerSort,
StartupRequest, TabId, ToolbarLayoutModeOption, UiTabId,
};
#[cfg(test)]
use crate::test_env;
use super::effects::Effect;
@@ -222,7 +225,7 @@ impl ConfiguratorApp {
/// about to draw. A transition that fails keeps the language it found.
/// The shell notices the change and rebuilds its pages in that language.
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.
@@ -231,7 +234,7 @@ impl ConfiguratorApp {
/// when it is the empty one.
#[cfg(test)]
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())
}
+3 -1
View File
@@ -1,3 +1,5 @@
use wayscriber::domain::BoardGridKind;
use crate::models::{
BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorPickerId,
};
@@ -9,7 +11,7 @@ impl ConfiguratorApp {
pub(super) fn handle_boards_grid_kind_changed(
&mut self,
index: usize,
value: wayscriber::domain::BoardGridKind,
value: BoardGridKind,
) -> Vec<Effect> {
if let Some(item) = self.draft.boards.items.get_mut(index) {
item.grid_kind = value;
+14 -13
View File
@@ -15,9 +15,10 @@ impl ConfiguratorApp {
value: String,
) -> Vec<Effect> {
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() {
alpha.or_else(|| self.current_alpha_for_id(id))
} else {
@@ -169,41 +170,41 @@ impl ConfiguratorApp {
}
}
ColorPickerId::StatusBarBg => {
self.apply_quad_rgb(QuadField::StatusBarBg, values, alpha);
self.apply_quad_rgb(QuadField::StatusBarBg, &values, alpha);
}
ColorPickerId::StatusBarText => {
self.apply_quad_rgb(QuadField::StatusBarText, values, alpha);
self.apply_quad_rgb(QuadField::StatusBarText, &values, alpha);
}
ColorPickerId::HighlightFill => {
self.apply_quad_rgb(QuadField::HighlightFill, values, alpha);
self.apply_quad_rgb(QuadField::HighlightFill, &values, alpha);
}
ColorPickerId::HighlightOutline => {
self.apply_quad_rgb(QuadField::HighlightOutline, values, alpha);
self.apply_quad_rgb(QuadField::HighlightOutline, &values, alpha);
}
ColorPickerId::LaserColor => {
self.apply_quad_rgb(QuadField::LaserColor, values, alpha);
self.apply_quad_rgb(QuadField::LaserColor, &values, alpha);
}
ColorPickerId::HelpBg => {
self.apply_quad_rgb(QuadField::HelpBg, values, alpha);
self.apply_quad_rgb(QuadField::HelpBg, &values, alpha);
}
ColorPickerId::HelpBorder => {
self.apply_quad_rgb(QuadField::HelpBorder, values, alpha);
self.apply_quad_rgb(QuadField::HelpBorder, &values, alpha);
}
ColorPickerId::HelpText => {
self.apply_quad_rgb(QuadField::HelpText, values, alpha);
self.apply_quad_rgb(QuadField::HelpText, &values, alpha);
}
ColorPickerId::ExportPdfLabelText => {
self.apply_quad_rgb(QuadField::ExportPdfLabelText, values, alpha);
self.apply_quad_rgb(QuadField::ExportPdfLabelText, &values, alpha);
}
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);
}
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() {
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 crate::app::document_workflow::LeaveAction;
use crate::models::ConfigDraft;
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> {
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> {
+21 -17
View File
@@ -1,7 +1,11 @@
use std::fs;
use std::path::{Path, PathBuf};
use wayscriber::config;
use wayscriber::config::{Action, CURRENT_CONFIG_REVISION, Config, ConfigDocument};
use crate::test_temp;
use super::save::save_validation_errors;
use super::*;
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>) {
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"));
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");
(temp, Box::new(document))
}
@@ -128,7 +132,7 @@ fn handle_config_loaded_surfaces_resolved_shortcut_conflicts() {
"shortcut-conflict",
&format!(
"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",
&format!(
"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",
&format!(
"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",
&format!(
"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
/// migration saves, and a save drops its `.bak` next to the file.
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");
std::fs::write(&path, contents).expect("write test config");
fs::write(&path, contents).expect("write test config");
let (mut app, _effects) = ConfiguratorApp::new_app();
load_config_file(&mut app, &path);
(app, dir, path)
@@ -954,7 +958,7 @@ fn config_setting(contents: &str, key: &str) -> Option<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
@@ -963,9 +967,9 @@ fn read_config(path: &Path) -> String {
/// defaults instead of calling the file unparseable, and Save keeps the rest.
#[test]
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");
std::fs::write(
fs::write(
&path,
"# 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");
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,
"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() {
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 second = dir.path().join("profile-b.toml");
std::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(&first, LEGACY_REVISION_ZERO_CONFIG).expect("write the first profile");
fs::write(&second, LEGACY_REVISION_ZERO_CONFIG).expect("write the second profile");
let link = dir.path().join("config.toml");
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"
);
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");
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("");
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();
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
//! cleared there, so `auto` resolves to English.
use std::fs;
use wayscriber::domain::Language;
use wayscriber::i18n;
use crate::test_env;
use super::{app_with_config_file, load_config_file, save_draft, status_contains};
use crate::app::effects::Effect;
use crate::models::LanguageOption;
@@ -17,7 +21,7 @@ const CHINESE_FILE: &str = "[ui]\nlanguage = \"zh-CN\"\n";
#[test]
fn the_first_load_applies_the_saved_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::the_first_load_applies_the_saved_language"
@@ -35,7 +39,7 @@ fn the_first_load_applies_the_saved_language() {
#[test]
fn selecting_a_language_previews_it_without_writing() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::selecting_a_language_previews_it_without_writing"
@@ -55,7 +59,7 @@ fn selecting_a_language_previews_it_without_writing() {
#[test]
fn a_successful_reload_applies_the_loaded_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::a_successful_reload_applies_the_loaded_language"
@@ -78,7 +82,7 @@ fn a_successful_reload_applies_the_loaded_language() {
#[test]
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"),
|| {
let (mut app, _dir, _path) = app_with_config_file(ENGLISH_FILE);
@@ -97,7 +101,7 @@ fn a_failed_reload_keeps_the_preview() {
#[test]
fn confirmed_defaults_apply_the_default_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::confirmed_defaults_apply_the_default_language"
@@ -122,7 +126,7 @@ fn confirmed_defaults_apply_the_default_language() {
#[test]
fn a_canceled_defaults_request_keeps_the_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::a_canceled_defaults_request_keeps_the_language"
@@ -140,7 +144,7 @@ fn a_canceled_defaults_request_keeps_the_language() {
#[test]
fn a_finished_save_applies_the_saved_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::a_finished_save_applies_the_saved_language"
@@ -162,7 +166,7 @@ fn a_finished_save_applies_the_saved_language() {
#[test]
fn canceling_the_leave_prompt_keeps_the_preview() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::canceling_the_leave_prompt_keeps_the_preview"
@@ -183,7 +187,7 @@ fn canceling_the_leave_prompt_keeps_the_preview() {
#[test]
fn discarding_to_reload_applies_the_document_that_loads() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::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();
assert!(matches!(effects.as_slice(), [Effect::LoadConfig]));
// 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);
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 {
status: new_status.clone(),
status: new_status,
message: "refresh complete".to_string(),
partial_failure: false,
}));
@@ -1,3 +1,5 @@
use std::cmp::Ordering;
use crate::models::{
ArrowStyleOption, ColorMode, ColorPickerId, DragColorOption, DragMouseButton, DragToolField,
DragToolOption, EraserModeOption, NamedColorOption,
@@ -108,9 +110,9 @@ impl ConfiguratorApp {
self.status = StatusMessage::idle();
if self.draft.drawing_quick_colors.remove_entry(index) {
self.remap_quick_color_pickers(|picker_index| match picker_index.cmp(&index) {
std::cmp::Ordering::Less => Some(picker_index),
std::cmp::Ordering::Equal => None,
std::cmp::Ordering::Greater => picker_index.checked_sub(1),
Ordering::Less => Some(picker_index),
Ordering::Equal => None,
Ordering::Greater => picker_index.checked_sub(1),
});
self.refresh_dirty_flag();
}
@@ -178,7 +180,7 @@ impl ConfiguratorApp {
pub(in crate::app::update) fn handle_font_cycle_changed(
&mut self,
index: usize,
family: String,
family: &str,
) -> Vec<Effect> {
match self.draft.drawing_font_cycle.set(index, family) {
Ok(changed) => {
+4 -2
View File
@@ -1,3 +1,5 @@
use wayscriber::draw;
use super::*;
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()];
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"]);
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() {
let (mut app, _effects) = ConfiguratorApp::new_app();
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);
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)
}
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::SearchCleared => self.handle_search_cleared(),
@@ -180,7 +180,7 @@ impl ConfiguratorApp {
Message::FontCycleRemoved(index) => self.handle_font_cycle_removed(index),
Message::FontCycleMoved(index, delta) => self.handle_font_cycle_moved(index, delta),
Message::FontCycleChanged(index, family) => {
self.handle_font_cycle_changed(index, family)
self.handle_font_cycle_changed(index, &family)
}
Message::QuickColorModeChanged(index, mode) => {
self.handle_quick_color_mode_changed(index, mode)
@@ -344,7 +344,7 @@ impl ConfiguratorApp {
self.handle_shortcut_sequence_remove_last_step()
}
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::ShortcutTextEditStarted(field) => {
+4 -4
View File
@@ -1,8 +1,8 @@
use wayscriber::config::PRESET_SLOTS_MAX;
use crate::models::{
ColorMode, NamedColorOption, PresetEraserKindOption, PresetEraserModeOption, PresetTextField,
PresetToggleField,
ColorMode, NamedColorOption, OverrideOption, PresetEraserKindOption, PresetEraserModeOption,
PresetTextField, PresetToggleField, ToolOption,
};
use super::super::effects::Effect;
@@ -73,7 +73,7 @@ impl ConfiguratorApp {
pub(super) fn handle_preset_tool_changed(
&mut self,
slot_index: usize,
tool: crate::models::ToolOption,
tool: ToolOption,
) -> Vec<Effect> {
self.status = StatusMessage::idle();
if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
@@ -167,7 +167,7 @@ impl ConfiguratorApp {
&mut self,
slot_index: usize,
field: PresetToggleField,
value: crate::models::OverrideOption,
value: OverrideOption,
) -> Vec<Effect> {
self.status = StatusMessage::idle();
if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
@@ -1,3 +1,5 @@
use wayscriber::render_profiles;
use crate::models::{
ColorPickerId, RenderProfileExportOption, RenderProfileMappingDraft, RenderProfileMappingSide,
RenderProfileTextField,
@@ -57,7 +59,7 @@ impl ConfiguratorApp {
if let Some(profile) = self.draft.render_profiles.profiles.get_mut(index) {
match field {
RenderProfileTextField::Id => {
let next_id = wayscriber::render_profiles::normalize_profile_id(&value);
let next_id = render_profiles::normalize_profile_id(&value);
profile.id = value;
if self.draft.render_profiles.active == old_id {
self.draft.render_profiles.active = next_id.clone();
@@ -228,8 +228,8 @@ impl ConfiguratorApp {
vec![Effect::ClearSessionEntry { id }]
}
pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: String) -> Vec<Effect> {
if self.pending_session_clear_id() != Some(id.as_str()) {
pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: &str) -> Vec<Effect> {
if self.pending_session_clear_id() != Some(id) {
return Vec::new();
}
@@ -1,5 +1,12 @@
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 crate::models::{
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,
last_opened_at_millis: None,
last_saved_at_millis: None,
artifacts: crate::models::session::SessionArtifactSummary {
artifacts: SessionArtifactSummary {
primary_exists: false,
backup_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 {
crate::models::DaemonRuntimeStatus {
fn inactive_daemon_status() -> DaemonRuntimeStatus {
DaemonRuntimeStatus {
desktop: DesktopEnvironment::Unknown,
shortcut_backend: ShortcutBackend::Manual,
shortcut_apply_capability: ShortcutApplyCapability::Manual,
@@ -124,14 +131,14 @@ fn catalog_load_preserves_a_defaults_confirmation() {
#[test]
fn a_finished_load_clears_its_notice_in_every_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::a_finished_load_clears_its_notice_in_every_language"
),
|| {
for language in [Language::English, Language::SimplifiedChinese] {
wayscriber::i18n::set_language(language);
i18n::set_language(language);
let (mut app, _effects) = ConfiguratorApp::new_app();
let effects = app.handle_session_catalog_refresh_requested();
@@ -148,7 +155,7 @@ fn a_finished_load_clears_its_notice_in_every_language() {
#[test]
fn the_loading_notice_is_drawn_in_the_current_language() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::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();
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("正在加载会话..."));
},
@@ -191,8 +198,8 @@ fn a_failed_load_stops_loading_and_shows_the_error() {
#[test]
fn duplicate_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -208,8 +215,8 @@ fn duplicate_request_blocks_without_daemon_status() {
#[test]
fn duplicate_request_sets_busy_when_safe() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -228,8 +235,8 @@ fn duplicate_request_sets_busy_when_safe() {
#[test]
fn move_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -245,8 +252,8 @@ fn move_request_blocks_without_daemon_status() {
#[test]
fn move_request_sets_busy_when_safe() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -265,8 +272,8 @@ fn move_request_sets_busy_when_safe() {
#[test]
fn clear_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -281,8 +288,8 @@ fn clear_request_blocks_without_daemon_status() {
#[test]
fn clear_tool_state_request_blocks_without_daemon_status() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -298,8 +305,8 @@ fn clear_tool_state_request_blocks_without_daemon_status() {
#[test]
fn clear_tool_state_request_sets_busy_when_safe() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -319,8 +326,8 @@ fn clear_tool_state_request_sets_busy_when_safe() {
#[test]
fn clear_request_sets_pending_confirmation_when_safe() {
let temp = crate::test_temp::tempdir().unwrap();
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().unwrap();
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -391,7 +398,7 @@ fn clear_canceled_disarms_and_clears_its_confirmation_status() {
app.daemon.status = Some(inactive_daemon_status());
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!(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());
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!(matches!(app.status, StatusMessage::Error(_)));
@@ -440,7 +447,7 @@ fn stray_clear_cancel_preserves_unrelated_status() {
app.pending_confirmation = None;
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!(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-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_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.
#[test]
fn clear_confirmed_consumes_the_pending_confirmation() {
let temp = crate::test_temp::tempdir().expect("temporary test directory");
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().expect("temporary test directory");
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog
@@ -494,8 +501,8 @@ fn clear_confirmed_consumes_the_pending_confirmation() {
/// stale one from a dialog — must not start it again.
#[test]
fn clear_confirmed_twice_starts_only_one_clear() {
let temp = crate::test_temp::tempdir().expect("temporary test directory");
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().expect("temporary test directory");
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
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.
#[test]
fn clear_confirmed_for_another_row_leaves_the_pending_one_armed() {
let temp = crate::test_temp::tempdir().expect("temporary test directory");
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let temp = test_temp::tempdir().expect("temporary test directory");
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
let (mut app, _effects) = ConfiguratorApp::new_app();
app.session_catalog = SessionCatalogState::loading();
app.session_catalog.replace_items(vec![
+13 -14
View File
@@ -6,7 +6,7 @@ mod tests;
use wayscriber::config::Shortcut;
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::models::keybindings::{
AppendOutcome, KeyboardModifiers, RecordedDevice, RecordedKeyboard, RecorderDeviceKind,
@@ -16,7 +16,7 @@ use crate::models::keybindings::{
other_claimants, parse_keybindings, remove_binding, reset_field, reset_fields,
text_conflicts_for,
};
use crate::models::{KeybindingField, keybinding_tab};
use crate::models::{KeybindingField, PendingShortcutConflict, keybinding_tab};
use super::super::effects::Effect;
use super::super::state::{ConfiguratorApp, StatusMessage};
@@ -127,12 +127,12 @@ impl ConfiguratorApp {
pub(super) fn handle_shortcut_removed(
&mut self,
field: KeybindingField,
binding: Shortcut,
binding: &Shortcut,
) -> Vec<Effect> {
if self.shortcuts.conflict().is_some() {
return Vec::new();
}
match remove_binding(&mut self.draft.keybindings, field, &binding) {
match remove_binding(&mut self.draft.keybindings, field, binding) {
Ok(()) => {
self.status = StatusMessage::idle();
self.refresh_dirty_flag();
@@ -197,7 +197,7 @@ impl ConfiguratorApp {
}
self.shortcuts.set_editor(None);
self.shortcuts
.set_conflict(Some(crate::models::PendingShortcutConflict::Text {
.set_conflict(Some(PendingShortcutConflict::Text {
target: editor.field,
new_value: editor.text,
conflicts,
@@ -223,12 +223,12 @@ impl ConfiguratorApp {
return Vec::new();
};
let result = match pending {
crate::models::PendingShortcutConflict::Recorded {
PendingShortcutConflict::Recorded {
target,
binding,
claimants,
} => apply_recorded_replace(&mut self.draft.keybindings, target, &binding, &claimants),
crate::models::PendingShortcutConflict::Text {
PendingShortcutConflict::Text {
target,
new_value,
conflicts,
@@ -274,7 +274,7 @@ impl ConfiguratorApp {
pub(crate) fn prepare_shortcut_manager_refresh(
&self,
search: &crate::app::search::AppSearchSummary,
search: &AppSearchSummary,
) -> (ShortcutManagerSummary, Vec<KeybindingField>) {
let summary = self.shortcut_manager_summary();
let fields = self.visible_keybinding_fields_with(&summary, search);
@@ -284,7 +284,7 @@ impl ConfiguratorApp {
fn visible_keybinding_fields_with(
&self,
summary: &ShortcutManagerSummary,
search: &crate::app::search::AppSearchSummary,
search: &AppSearchSummary,
) -> Vec<KeybindingField> {
let scope = (!self.keybindings_show_all).then_some(self.active_keybindings_tab);
summary.visible_fields(self.shortcut_filter, self.shortcut_sort, scope, |field| {
@@ -416,13 +416,12 @@ impl ConfiguratorApp {
match next_review_conflict(&self.draft.keybindings) {
Some((field, binding, claimants)) => {
self.select_keybinding_field(field);
self.shortcuts.set_conflict(Some(
crate::models::PendingShortcutConflict::Recorded {
self.shortcuts
.set_conflict(Some(PendingShortcutConflict::Recorded {
target: field,
binding,
claimants,
},
));
}));
self.status = StatusMessage::info(tr("Review the next conflicting shortcut."));
}
None => {
@@ -440,7 +439,7 @@ impl ConfiguratorApp {
let claimants = other_claimants(&self.draft.keybindings, field, &binding);
if !claimants.is_empty() {
self.shortcuts
.set_conflict(Some(crate::models::PendingShortcutConflict::Recorded {
.set_conflict(Some(PendingShortcutConflict::Recorded {
target: field,
binding,
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 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::models::keybindings::keyval;
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;
fn status_contains(status: &StatusMessage, needle: &str) -> bool {
status.text().is_some_and(|text| text.contains(needle))
}
fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, std::path::PathBuf) {
let dir = crate::test_temp::tempdir().expect("temporary test directory");
fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, PathBuf) {
let dir = test_temp::tempdir().expect("temporary test directory");
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 _ = app.handle_config_loaded(Ok((Box::new(document), None)));
(dir, path)
@@ -125,7 +133,7 @@ fn recording_reset_and_removal_dirty_without_saving() {
);
let effects = app.handle_shortcut_removed(
KeybindingField::ToggleFloatingBadge,
Shortcut::parse("F5").expect("parses"),
&Shortcut::parse("F5").expect("parses"),
);
assert!(effects.is_empty());
assert_eq!(
@@ -201,7 +209,7 @@ fn save_after_confirmed_replacement_writes_only_the_intended_fields() {
let _ = app.handle_shortcut_conflict_replace_confirmed();
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("unknown_future = true"), "{contents}");
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));
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!(
config_setting(&contents, "undo").as_deref(),
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();
assert_eq!(
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_recorder_button(
@@ -688,7 +696,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() {
assert!(app.shortcuts.review);
assert!(app.shortcuts.conflict().is_some());
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:?}"),
};
@@ -702,7 +710,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() {
.conflict()
.expect("the next conflict should be armed");
match second {
crate::models::PendingShortcutConflict::Recorded { target, .. } => {
PendingShortcutConflict::Recorded { target, .. } => {
assert_ne!(*target, first_target);
}
other => panic!("expected a recorded conflict, got {other:?}"),
@@ -737,7 +745,7 @@ fn jump_to_conflict_selects_the_other_claimant() {
let jump = app
.shortcuts
.conflict()
.and_then(crate::models::PendingShortcutConflict::jump_field)
.and_then(PendingShortcutConflict::jump_field)
.expect("a claimant to jump to");
let _ = app.handle_shortcut_manager_jump_to(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"]
fn shortcut_refresh_cost() {
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();
for query in ["", "tool", "Ctrl"] {
app.handle_search_changed(query.into());
+3 -1
View File
@@ -6,10 +6,12 @@ mod test_env;
#[cfg(test)]
mod test_temp;
use std::env;
use models::{StartupRequest, startup_usage};
fn main() {
let startup = StartupRequest::from_args(std::env::args_os());
let startup = StartupRequest::from_args(env::args_os());
if startup.wants_help() {
print!("{}", startup_usage());
return;
+2 -1
View File
@@ -1,6 +1,7 @@
use std::path::PathBuf;
use wayscriber::config::{ConfigDocument, Shortcut, ToolbarItemId, ToolbarItemOrderGroup};
use wayscriber::domain::BoardGridKind;
use crate::models::{
ArrowStyleOption, BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode,
@@ -150,7 +151,7 @@ pub enum Message {
BoardsCollapseToggled(usize),
BoardsDefaultChanged(String),
BoardsItemTextChanged(usize, BoardItemTextField, String),
BoardsGridKindChanged(usize, wayscriber::domain::BoardGridKind),
BoardsGridKindChanged(usize, BoardGridKind),
BoardsBackgroundKindChanged(usize, BoardBackgroundOption),
BoardsBackgroundColorChanged(usize, usize, String),
BoardsDefaultPenEnabledChanged(usize, bool),
+4 -3
View File
@@ -2,6 +2,7 @@ use wayscriber::i18n::{tr, trf};
use wayscriber::config::enums::ColorSpec;
use wayscriber::draw::Color;
use wayscriber::input::state;
use wayscriber::util::{ConfigHexColorError, name_to_color, parse_config_hex_color};
use super::super::error::FormError;
@@ -22,7 +23,7 @@ impl ColorInput {
mode: ColorMode::Named,
name: name.clone(),
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 {
mode: ColorMode::Rgb,
@@ -35,7 +36,7 @@ impl ColorInput {
// silently flattened to opaque by the three RGB boxes.
ColorSpec::Rgba(_) => Self {
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()),
selected_named: NamedColorOption::Custom,
},
@@ -116,7 +117,7 @@ impl ColorInput {
}
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 {
+6 -3
View File
@@ -1,3 +1,6 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::i18n::tr;
#[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() {
"red" => Self::Red,
"green" => Self::Green,
@@ -77,8 +80,8 @@ impl NamedColorOption {
}
}
impl std::fmt::Display for NamedColorOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for NamedColorOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
+7 -3
View File
@@ -1,6 +1,10 @@
mod mapping;
mod validation;
use std::fmt;
use std::fmt::{Display, Formatter};
use wayscriber::domain::BoardGridKind;
use wayscriber::i18n::tr;
use super::super::color::ColorTripletInput;
@@ -24,8 +28,8 @@ impl BoardBackgroundOption {
}
}
impl std::fmt::Display for BoardBackgroundOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for BoardBackgroundOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -56,7 +60,7 @@ pub struct BoardItemDraft {
pub name: String,
pub background_kind: BoardBackgroundOption,
pub background_color: ColorTripletInput,
pub grid_kind: wayscriber::domain::BoardGridKind,
pub grid_kind: BoardGridKind,
pub grid_spacing: String,
pub default_pen_color: OptionalTripletInput,
pub auto_adjust_pen: bool,
@@ -3,8 +3,10 @@ use wayscriber::i18n::tr;
use std::collections::HashSet;
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::error::FormError;
@@ -103,8 +105,8 @@ impl BoardItemDraft {
let spacing = match self.grid_spacing.trim().parse::<i64>() {
Ok(value)
if (i64::from(wayscriber::domain::BOARD_GRID_MIN_SPACING)
..=i64::from(wayscriber::domain::BOARD_GRID_MAX_SPACING))
if (i64::from(domain::BOARD_GRID_MIN_SPACING)
..=i64::from(domain::BOARD_GRID_MAX_SPACING))
.contains(&value) =>
{
value
@@ -117,9 +119,9 @@ impl BoardItemDraft {
return None;
}
};
let grid = wayscriber::config::BoardGridConfig {
let grid = BoardGridConfig {
kind: if background.is_transparent() {
wayscriber::domain::BoardGridKind::None
BoardGridKind::None
} else {
self.grid_kind
}
@@ -230,8 +232,8 @@ impl BoardsDraft {
name,
background_kind: BoardBackgroundOption::Color,
background_color: ColorTripletInput::from([0.992, 0.992, 0.992]),
grid_kind: wayscriber::domain::BoardGridKind::None,
grid_spacing: wayscriber::domain::BOARD_GRID_DEFAULT_SPACING.to_string(),
grid_kind: BoardGridKind::None,
grid_spacing: domain::BOARD_GRID_DEFAULT_SPACING.to_string(),
default_pen_color: OptionalTripletInput::from_option(
Some(&BoardColorConfig::Rgb([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::fields::ZoomChipDisplayOption;
use super::super::super::fields::{
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
@@ -7,7 +9,7 @@ use super::super::super::fields::{
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
UiThemeOption,
UiThemeOption, ZoomChipDisplayOption,
};
#[cfg(feature = "tablet-input")]
use super::super::super::fields::{
@@ -31,7 +33,7 @@ impl ConfigDraft {
FontWeightOption::from_value(&config.drawing.font_weight);
let drawing_drag_tools = config.drawing.effective_drag_tools();
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_advanced: config.ui.toolbar.show_actions_advanced,
show_zoom_actions: config.ui.toolbar.show_zoom_actions,
@@ -48,13 +50,13 @@ impl ConfigDraft {
.mode_overrides
.for_mode(config.ui.toolbar.layout_mode),
);
wayscriber::config::fold_legacy_section_flags(
config::fold_legacy_section_flags(
&legacy_toolbar_visibility,
config.ui.toolbar.layout_mode,
&config.ui.toolbar.mode_overrides,
&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.mode_overrides,
&toolbar_items.resolved(),
@@ -311,14 +313,12 @@ impl ConfigDraft {
session_persist_history: config.session.persist_history,
session_restore_tool_state: config.session.restore_tool_state,
session_per_output: config.session.per_output,
session_storage_mode: SessionStorageModeOption::from_mode(
config.session.storage.clone(),
),
session_storage_mode: SessionStorageModeOption::from_mode(config.session.storage),
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_file_size_mb: config.session.max_file_size_mb.to_string(),
session_compression: SessionCompressionOption::from_compression(
config.session.compress.clone(),
config.session.compress,
),
session_auto_compress_threshold_kb: config
.session
@@ -373,8 +373,8 @@ impl ConfigDraft {
// Built-in and in-memory configs are AllExplicit for
// validation. The UI treats them as omitted defaults so
// Reset All / Defaults does not badge every row Authored.
wayscriber::config::KeybindingAuthorship::AllExplicit => {
wayscriber::config::KeybindingAuthorship::FromFile(Default::default())
KeybindingAuthorship::AllExplicit => {
KeybindingAuthorship::FromFile(Default::default())
}
from_file => from_file.clone(),
});
+1 -2
View File
@@ -1,7 +1,6 @@
mod from_config;
use super::super::color::{ColorInput, ColorQuadInput};
use super::super::fields::ZoomChipDisplayOption;
use super::super::fields::{
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
@@ -9,7 +8,7 @@ use super::super::fields::{
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
UiThemeOption,
UiThemeOption, ZoomChipDisplayOption,
};
#[cfg(feature = "tablet-input")]
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.
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 {
return Err(FontCycleEditError::MissingEntry);
};
@@ -202,12 +202,12 @@ mod tests {
let mut draft = draft(&["Sans", "Serif"]);
assert_eq!(
draft.set(1, "sans".to_string()),
draft.set(1, "sans"),
Err(FontCycleEditError::DuplicateFamily("sans".to_string()))
);
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"]);
}
@@ -215,14 +215,8 @@ mod tests {
fn blank_and_missing_row_edits_are_rejected() {
let mut draft = draft(&["Sans"]);
assert_eq!(
draft.set(0, " ".to_string()),
Err(FontCycleEditError::BlankFamily)
);
assert_eq!(
draft.set(4, "Serif".to_string()),
Err(FontCycleEditError::MissingEntry)
);
assert_eq!(draft.set(0, " "), Err(FontCycleEditError::BlankFamily));
assert_eq!(draft.set(4, "Serif"), Err(FontCycleEditError::MissingEntry));
assert_eq!(draft.entries(), ["Sans"]);
}
@@ -82,13 +82,17 @@ impl ConfigDraft {
#[cfg(test)]
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::*;
#[test]
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 {
match metadata.constraint {
ScalarConstraint::Boolean => {
@@ -106,17 +110,11 @@ mod tests {
#[test]
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_toggle(crate::models::ToggleField::PerformanceVsync, true);
draft.set_text(
crate::models::TextField::PerformanceMaxFpsNoVsync,
"144".to_string(),
);
draft.set_text(
crate::models::TextField::PerformanceUiAnimationFps,
"60".to_string(),
);
draft.set_toggle(ToggleField::PerformanceVsync, true);
draft.set_text(TextField::PerformanceMaxFpsNoVsync, "144".to_string());
draft.set_text(TextField::PerformanceUiAnimationFps, "60".to_string());
assert_eq!(draft.performance_buffer_count, 4);
assert!(draft.performance_enable_vsync);
@@ -126,7 +124,7 @@ mod tests {
#[test]
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);
draft.performance_buffer_count = 5;
draft.performance_ui_animation_fps = "241".to_string();
+1 -1
View File
@@ -203,7 +203,7 @@ pub struct PresetsDraft {
impl PresetsDraft {
pub fn from_config(config: &Config) -> Self {
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();
Self {
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::config::{
@@ -56,8 +59,8 @@ impl RenderProfileExportOption {
}
}
impl std::fmt::Display for RenderProfileExportOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for RenderProfileExportOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -70,7 +73,7 @@ pub enum RenderProfileSelectionOption {
impl RenderProfileSelectionOption {
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))
.collect()
}
@@ -84,8 +87,8 @@ impl RenderProfileSelectionOption {
}
}
impl std::fmt::Display for RenderProfileSelectionOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for RenderProfileSelectionOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Off => f.write_str("Off"),
Self::Profile(id) => f.write_str(id),
+3 -1
View File
@@ -1,3 +1,5 @@
use wayscriber::config;
use super::super::fields::{
DragColorOption, DragMouseButton, DragToolField, DragToolOption, FontStyleOption,
FontWeightOption, OverrideOption, QuadField, TextField, ToggleField, ToolbarLayoutModeOption,
@@ -19,7 +21,7 @@ impl ConfigDraft {
}
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_mode_overrides.to_config(),
&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;
#[test]
@@ -95,16 +109,16 @@ fn shape_pen_recognition_feedback_round_trips() {
#[test]
fn board_grid_draft_preserves_patterns_and_rejects_invalid_spacing() {
let mut config = Config::default();
let mut boards = wayscriber::config::BoardsConfig::default();
boards.items[1].grid = wayscriber::config::BoardGridConfig {
kind: wayscriber::config::BoardGridKindConfig::IsometricDots,
let mut boards = BoardsConfig::default();
boards.items[1].grid = BoardGridConfig {
kind: BoardGridKindConfig::IsometricDots,
spacing: 20,
};
config.boards = Some(boards);
let mut draft = ConfigDraft::from_config(&config);
assert_eq!(
draft.boards.items[1].grid_kind,
wayscriber::domain::BoardGridKind::IsometricDots
BoardGridKind::IsometricDots
);
assert_eq!(
draft.to_config(&config).unwrap().boards.unwrap().items[1].grid,
@@ -222,7 +236,7 @@ use super::super::fields::{
#[test]
fn config_draft_round_trips_toolbar_rebind_modifier() {
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);
assert_eq!(
draft.ui_toolbar_rebind_modifier,
@@ -235,7 +249,7 @@ fn config_draft_round_trips_toolbar_rebind_modifier() {
.expect("toolbar modifier round trip");
assert_eq!(
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
// any tab writes that Standard back over the user's choice.
let mut config = Config::default();
config.arrow.style = wayscriber::draw::ArrowStyle::Curved;
config.arrow.style = ArrowStyle::Curved;
let mut draft = ConfigDraft::from_config(&config);
assert_eq!(draft.arrow_style, ArrowStyleOption::Curved);
draft.arrow_style = ArrowStyleOption::Double;
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]
@@ -347,10 +361,7 @@ fn config_draft_round_trips_region_capture_settings() {
let round_trip = draft
.to_config(&config)
.expect("region capture settings should round trip");
assert_eq!(
round_trip.capture.region.picker,
wayscriber::config::RegionPicker::Slurp
);
assert_eq!(round_trip.capture.region.picker, RegionPicker::Slurp);
assert!(!round_trip.capture.region.show_size_readout);
assert!(round_trip.capture.region.show_loupe);
assert!(!round_trip.capture.region.show_legend);
@@ -508,13 +519,13 @@ fn config_draft_rejects_path_escaping_save_names() {
#[test]
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 original = format!(
"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 draft = ConfigDraft::from_config(document.config());
let updated = draft
@@ -526,7 +537,7 @@ fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() {
.expect("save untouched sparse draft");
assert_eq!(
std::fs::read_to_string(path).expect("read sparse config"),
fs::read_to_string(path).expect("read sparse config"),
original
);
}
@@ -559,9 +570,9 @@ fn config_draft_round_trips_precise_floats_without_truncation() {
#[test]
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");
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 mut draft = ConfigDraft::from_config(document.config());
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)
.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("[performance]"));
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.
#[test]
fn a_config_saved_in_chinese_keeps_its_font_and_color_names() {
crate::test_env::in_language_child(
test_env::in_language_child(
concat!(
module_path!(),
"::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();
config.drawing.font_style = "italic".to_string();
config.drawing.font_weight = "light".to_string();
@@ -687,7 +698,7 @@ fn config_draft_preserves_implicit_quick_color_defaults() {
#[test]
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 original = format!(
r#"config_revision = {}
@@ -695,9 +706,9 @@ fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file(
label = "Only configured color"
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");
assert_eq!(
document
@@ -723,7 +734,7 @@ color = "blue"
.save_with_backup(updated)
.expect("save sparse quick colors");
assert_eq!(
std::fs::read_to_string(path).expect("read sparse quick colors"),
fs::read_to_string(path).expect("read sparse quick colors"),
original
);
}
@@ -1112,7 +1123,7 @@ fn config_draft_round_trips_render_profiles() {
#[test]
fn config_draft_round_trips_board_pan_settings() {
let mut config = Config::default();
config.boards = Some(wayscriber::config::BoardsConfig {
config.boards = Some(BoardsConfig {
pan_enabled: false,
show_pan_badge: false,
..Default::default()
@@ -1380,7 +1391,7 @@ fn section_toggle_replaces_overlay_item_override_on_save() {
.contains(&ToolbarSectionFlag::Presets.item_id())
);
assert!(
wayscriber::config::resolve_section_visibility(
config::resolve_section_visibility(
saved.ui.toolbar.layout_mode,
&saved.ui.toolbar.mode_overrides,
&resolved,
@@ -1580,7 +1591,7 @@ fn config_draft_round_trips_presets_and_history() {
config.history.custom_redo_steps
);
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]
@@ -1626,7 +1637,7 @@ fn preset_tool_change_loads_selected_tool_profile_values() {
let round_trip = draft
.to_config(&config)
.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");
assert_eq!(preset.tool, Tool::Marker);
@@ -1683,7 +1694,7 @@ fn preset_visible_edits_update_selected_tool_profile_only() {
let round_trip = draft
.to_config(&config)
.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");
assert_eq!(preset.tool, Tool::Marker);
@@ -1701,11 +1712,11 @@ fn preset_visible_edits_update_selected_tool_profile_only() {
#[test]
fn config_draft_round_trips_drag_tool_mapping() {
let mut config = Config::default();
config.drawing.drag_tool = wayscriber::input::DragBindableTool::Arrow;
config.drawing.shift_drag_tool = wayscriber::input::DragBindableTool::Eraser;
config.drawing.ctrl_drag_tool = wayscriber::input::DragBindableTool::Pen;
config.drawing.ctrl_shift_drag_tool = wayscriber::input::DragBindableTool::Rect;
config.drawing.tab_drag_tool = wayscriber::input::DragBindableTool::Ellipse;
config.drawing.drag_tool = DragBindableTool::Arrow;
config.drawing.shift_drag_tool = DragBindableTool::Eraser;
config.drawing.ctrl_drag_tool = DragBindableTool::Pen;
config.drawing.ctrl_shift_drag_tool = DragBindableTool::Rect;
config.drawing.tab_drag_tool = DragBindableTool::Ellipse;
let mut drag_tools = config.drawing.effective_drag_tools();
drag_tools.right.drag_tool = DragTool::Pen;
drag_tools.right.drag_color = Some(ColorSpec::Name("blue".to_string()));
@@ -1826,10 +1837,10 @@ fn config_draft_round_trips_ui_language() {
#[test]
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");
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");
assert_eq!(document.config().ui.language, Language::Auto);
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.
#[test]
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");
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)
.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");
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
.to_config(&base)
.expect("expected config to round trip");
assert_eq!(
migrated.config_revision,
wayscriber::config::CURRENT_CONFIG_REVISION
);
assert_eq!(migrated.config_revision, config::CURRENT_CONFIG_REVISION);
}
#[test]
fn config_draft_preserves_tray_icon_style() {
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 round_trip = draft
.to_config(&config)
.expect("expected config to round trip");
assert_eq!(
round_trip.tray.icon_style,
wayscriber::config::TrayIconStyle::Colored
);
assert_eq!(round_trip.tray.icon_style, TrayIconStyle::Colored);
}
#[test]
fn compiled_defaults_are_not_shown_as_authored_keybindings() {
let draft = ConfigDraft::from_config(&Config::default());
assert!(
!draft
.keybindings
.is_authored(crate::models::KeybindingField::Undo),
!draft.keybindings.is_authored(KeybindingField::Undo),
"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,
};
use crate::models::error::FormError;
use wayscriber::config::MAX_SHAPE_RECOGNITION_SENSITIVITY;
use wayscriber::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::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS};
use wayscriber::domain::{DragBindableTool, DragTool, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS};
use wayscriber::draw::{MAX_PEN_SMOOTHING, REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES};
impl ConfigDraft {
@@ -1,3 +1,5 @@
use wayscriber::config;
use super::super::draft::ConfigDraft;
use super::super::parse::{
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.mode_overrides = self.ui_toolbar_mode_overrides.to_config();
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.mode_overrides,
&config.ui.toolbar.items.resolved(),
+5 -3
View File
@@ -1,3 +1,5 @@
use std::env;
use wayscriber::i18n::tr;
use wayscriber::env_vars::{
@@ -18,11 +20,11 @@ pub enum DesktopEnvironment {
impl DesktopEnvironment {
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;
}
let current = std::env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default();
let session = std::env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default();
let current = env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default();
let session = env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default();
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::draw::ArrowStyle;
@@ -49,8 +52,8 @@ impl ArrowStyleOption {
}
}
impl std::fmt::Display for ArrowStyleOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for ArrowStyleOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
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::config::RegionPicker;
@@ -35,8 +38,8 @@ impl RegionPickerOption {
}
}
impl std::fmt::Display for RegionPickerOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for RegionPickerOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
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::domain::EraserMode;
@@ -36,8 +39,8 @@ impl EraserModeOption {
}
}
impl std::fmt::Display for EraserModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for EraserModeOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -79,8 +82,8 @@ impl PresetEraserKindOption {
}
}
impl std::fmt::Display for PresetEraserKindOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PresetEraserKindOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -122,8 +125,8 @@ impl PresetEraserModeOption {
}
}
impl std::fmt::Display for PresetEraserModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PresetEraserModeOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
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::config::{
@@ -46,8 +49,8 @@ impl PdfPageSizeOption {
}
}
impl std::fmt::Display for PdfPageSizeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PdfPageSizeOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -89,8 +92,8 @@ impl PdfOrientationOption {
}
}
impl std::fmt::Display for PdfOrientationOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PdfOrientationOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -136,8 +139,8 @@ impl PdfFitModeOption {
}
}
impl std::fmt::Display for PdfFitModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PdfFitModeOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -175,8 +178,8 @@ impl PdfTransparentBackgroundOption {
}
}
impl std::fmt::Display for PdfTransparentBackgroundOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PdfTransparentBackgroundOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -232,8 +235,8 @@ impl PdfLabelPositionOption {
}
}
impl std::fmt::Display for PdfLabelPositionOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PdfLabelPositionOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
@@ -289,8 +292,8 @@ impl PdfLabelContentModeOption {
}
}
impl std::fmt::Display for PdfLabelContentModeOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for PdfLabelContentModeOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
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