mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
refactor: standardize Rust style across the workspace
This commit is contained in:
1 parent
c6e89d0072
commit
87e1d057f7
1262 files changed
+18512
-14498
No files matched your search
@@ -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
@@ -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,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
@@ -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" },
|
||||
]
|
||||
@@ -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 || {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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: >k::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: >k::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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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:#}"))
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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: >k::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: >k::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: >k::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: >k::ColorDialogButton,
|
||||
handler: &SignalHandlerId,
|
||||
rgba: >k::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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -92,13 +92,13 @@ fn section_combo_row<O>(
|
||||
list: >k::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),
|
||||
);
|
||||
|
||||
@@ -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: >k::ListBox,
|
||||
families: >k::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: >k::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: >k::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: >k::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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(>k::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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 之间的整数。")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,10 +53,7 @@ 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(),
|
||||
)
|
||||
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(
|
||||
@@ -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,18 +43,16 @@ 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) {
|
||||
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")
|
||||
.to_string(),
|
||||
tr("the original catalog entry disappeared before metadata could be updated"),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -60,7 +60,7 @@ fn move_session_catalog_entry_sync(
|
||||
&item,
|
||||
&outcome,
|
||||
initial_items,
|
||||
format!("failed to update the session catalog: {err}"),
|
||||
&format!("failed to update the session catalog: {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",
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -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(),
|
||||
}],
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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,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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(¤t, &session)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user