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
+18853
-14839
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.
|
- Review changed functions after `cargo fmt`. Formatter success verifies mechanical layout; also verify visually that logical chunks are clearly separated and easy to scan.
|
||||||
- Apply these rules to new and materially changed code. Do not reformat unrelated existing code solely to adopt them.
|
- Apply these rules to new and materially changed code. Do not reformat unrelated existing code solely to adopt them.
|
||||||
|
|
||||||
|
## Rust Style
|
||||||
|
- Rust code follows the [Google Rust style guide](https://google.github.io/styleguide/rustguide.html) as application code. Its rules that Clippy can check are workspace lints in `Cargo.toml`, configured in `clippy.toml`. Fix a warning rather than suppressing it. When a site truly cannot comply, use `#[expect(lint, reason = "...")]` with the concrete constraint, never `#[allow]`.
|
||||||
|
- Each `use` statement imports from one module: `use foo::bar::{Bar, Baz};`, not `use foo::{Foo, bar::Bar};`. `tests/repository_guards/use_statements.rs` checks this because rustfmt's `imports_granularity` is unstable.
|
||||||
|
- Do not use glob imports, except `use super::*` in test modules and third-party preludes.
|
||||||
|
- Import type and trait names and use them unqualified; call functions through their module (`fs::read`). Keep ambiguous names such as `io::Error` and `fmt::Result` module-qualified. A gated import carries the same `#[cfg(...)]` as the code that uses it.
|
||||||
|
- Borrow instead of cloning or taking ownership (`&str`, `&[T]`, `Option<&T>`). Replace more than three bool parameters with enums or an options struct.
|
||||||
|
- Use `expect` with a reason instead of `unwrap` in production code. Give assertions messages that explain the invariant, and document panics in `# Panics`.
|
||||||
|
- Never reach inside a poisoned lock. Use `expect` when the critical section cannot panic, or a fallback that leaves the data alone. Tests that serialize on process-wide state use `test_env::lock()` or a `test_env::Lease`.
|
||||||
|
- `catch_unwind` is reserved for the reviewed worker-isolation sites that carry an `#[expect]` reason. Tests observe panics through a joined scoped thread.
|
||||||
|
- Getters are `foo()`/`foo_mut()`, a fallible constructor is `try_new`, and type lifetimes have descriptive names. Every `unsafe` block has a `// SAFETY:` comment that says why it is sound.
|
||||||
|
|
||||||
## Coupled Changes
|
## Coupled Changes
|
||||||
- Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests.
|
- Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests.
|
||||||
- Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests.
|
- Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests.
|
||||||
- Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`.
|
- Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles.
|
- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, use statements, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles.
|
||||||
- That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests.
|
- That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests.
|
||||||
- Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well.
|
- Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well.
|
||||||
- For docs-only `AGENTS.md` edits, make new files visible to Git before whitespace checks, for example `rg --files --hidden -g AGENTS.md -0 | xargs -0 git add -N --` followed by `git diff --check`.
|
- For docs-only `AGENTS.md` edits, make new files visible to Git before whitespace checks, for example `rg --files --hidden -g AGENTS.md -0 | xargs -0 git add -N --` followed by `git diff --check`.
|
||||||
|
|||||||
+18
@@ -14,6 +14,24 @@ default-members = ["."]
|
|||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
cognitive_complexity = "warn"
|
cognitive_complexity = "warn"
|
||||||
|
# Rules from the Google Rust style guide that Clippy can check. CI promotes
|
||||||
|
# these warnings to errors; clippy.toml holds their settings.
|
||||||
|
absolute_paths = "warn"
|
||||||
|
enum_glob_use = "warn"
|
||||||
|
fn_params_excessive_bools = "warn"
|
||||||
|
missing_assert_message = "warn"
|
||||||
|
missing_panics_doc = "warn"
|
||||||
|
must_use_candidate = "warn"
|
||||||
|
needless_for_each = "warn"
|
||||||
|
needless_pass_by_value = "warn"
|
||||||
|
redundant_clone = "warn"
|
||||||
|
ref_option = "warn"
|
||||||
|
return_self_not_must_use = "warn"
|
||||||
|
string_add = "warn"
|
||||||
|
undocumented_unsafe_blocks = "warn"
|
||||||
|
unnecessary_safety_comment = "warn"
|
||||||
|
unwrap_used = "warn"
|
||||||
|
wildcard_imports = "warn"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::env;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-env-changed=WAYSCRIBER_RELEASE_VERSION");
|
println!("cargo:rerun-if-env-changed=WAYSCRIBER_RELEASE_VERSION");
|
||||||
|
|||||||
+13
@@ -1,3 +1,16 @@
|
|||||||
# Keep functions within the agreed cognitive-complexity budget. Clippy emits a
|
# Keep functions within the agreed cognitive-complexity budget. Clippy emits a
|
||||||
# warning above this value, and CI promotes warnings to errors.
|
# warning above this value, and CI promotes warnings to errors.
|
||||||
cognitive-complexity-threshold = 20
|
cognitive-complexity-threshold = 20
|
||||||
|
|
||||||
|
# Tests may panic for assertions, so `unwrap_used` applies to production code.
|
||||||
|
allow-unwrap-in-tests = true
|
||||||
|
|
||||||
|
# Never reach inside a poisoned lock, and do not rely on catching panics.
|
||||||
|
disallowed-methods = [
|
||||||
|
{ path = "std::panic::catch_unwind", reason = "panics are bugs; recover only as defense in depth with a reviewed `#[expect]`" },
|
||||||
|
{ path = "std::sync::PoisonError::get_mut", reason = "a poisoned lock may hold broken invariants" },
|
||||||
|
{ path = "std::sync::PoisonError::get_ref", reason = "a poisoned lock may hold broken invariants" },
|
||||||
|
{ path = "std::sync::PoisonError::into_inner", reason = "a poisoned lock may hold broken invariants" },
|
||||||
|
{ path = "std::sync::Mutex::clear_poison", reason = "a poisoned lock may hold broken invariants" },
|
||||||
|
{ path = "std::sync::RwLock::clear_poison", reason = "a poisoned lock may hold broken invariants" },
|
||||||
|
]
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
use tokio::task;
|
||||||
|
|
||||||
const PRODUCTION_BLOCKING_JOB_LIMIT: usize = 2;
|
const PRODUCTION_BLOCKING_JOB_LIMIT: usize = 2;
|
||||||
const SLOW_JOB_THRESHOLD: Duration = Duration::from_millis(250);
|
const SLOW_JOB_THRESHOLD: Duration = Duration::from_millis(250);
|
||||||
@@ -65,21 +65,21 @@ impl BlockingJobRunner {
|
|||||||
.map_err(|_| format!("{kind} blocking job queue is unavailable"))?;
|
.map_err(|_| format!("{kind} blocking job queue is unavailable"))?;
|
||||||
report_slow_phase(kind, "queue wait", queued_at.elapsed());
|
report_slow_phase(kind, "queue wait", queued_at.elapsed());
|
||||||
|
|
||||||
let joined = tokio::task::spawn_blocking(move || {
|
let joined = task::spawn_blocking(move || {
|
||||||
let _permit = permit;
|
let _permit = permit;
|
||||||
let started_at = Instant::now();
|
let started_at = Instant::now();
|
||||||
let outcome = catch_unwind(AssertUnwindSafe(job));
|
let result = job();
|
||||||
report_slow_phase(kind, "execution", started_at.elapsed());
|
report_slow_phase(kind, "execution", started_at.elapsed());
|
||||||
outcome
|
result
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let outcome =
|
// A panicking job unwinds its blocking thread, which releases the
|
||||||
joined.map_err(|err| format!("{kind} blocking job did not complete: {err}"))?;
|
// permit; tokio reports the panic through the join error.
|
||||||
|
match joined {
|
||||||
match outcome {
|
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(_) => Err(format!("{kind} blocking job panicked")),
|
Err(err) if err.is_panic() => Err(format!("{kind} blocking job panicked")),
|
||||||
|
Err(err) => Err(format!("{kind} blocking job did not complete: {err}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,7 @@ mod tests {
|
|||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
use tokio::sync::oneshot;
|
||||||
use tokio::time::{Duration, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -134,7 +135,7 @@ mod tests {
|
|||||||
async fn blocking_work_is_offloaded_while_executor_work_progresses() {
|
async fn blocking_work_is_offloaded_while_executor_work_progresses() {
|
||||||
let runner = BlockingJobRunner::new(1);
|
let runner = BlockingJobRunner::new(1);
|
||||||
let caller_thread = thread::current().id();
|
let caller_thread = thread::current().id();
|
||||||
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
|
let (started_tx, started_rx) = oneshot::channel();
|
||||||
let (release_tx, release_rx) = mpsc::channel();
|
let (release_tx, release_rx) = mpsc::channel();
|
||||||
|
|
||||||
let job = tokio::spawn(async move {
|
let job = tokio::spawn(async move {
|
||||||
@@ -169,7 +170,7 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn one_permit_prevents_a_second_closure_from_starting() {
|
async fn one_permit_prevents_a_second_closure_from_starting() {
|
||||||
let runner = BlockingJobRunner::new(1);
|
let runner = BlockingJobRunner::new(1);
|
||||||
let (first_started_tx, first_started_rx) = tokio::sync::oneshot::channel();
|
let (first_started_tx, first_started_rx) = oneshot::channel();
|
||||||
let (first_release_tx, first_release_rx) = mpsc::channel();
|
let (first_release_tx, first_release_rx) = mpsc::channel();
|
||||||
let first_runner = runner.clone();
|
let first_runner = runner.clone();
|
||||||
let first = tokio::spawn(async move {
|
let first = tokio::spawn(async move {
|
||||||
@@ -186,7 +187,7 @@ mod tests {
|
|||||||
.expect("first closure should start")
|
.expect("first closure should start")
|
||||||
.expect("first closure should report startup");
|
.expect("first closure should report startup");
|
||||||
|
|
||||||
let (second_started_tx, mut second_started_rx) = tokio::sync::oneshot::channel();
|
let (second_started_tx, mut second_started_rx) = oneshot::channel();
|
||||||
let second = tokio::spawn(async move {
|
let second = tokio::spawn(async move {
|
||||||
runner
|
runner
|
||||||
.run(BlockingJobKind::SessionCatalogLoad, move || {
|
.run(BlockingJobKind::SessionCatalogLoad, move || {
|
||||||
@@ -238,7 +239,7 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn dropping_the_waiter_does_not_release_a_running_jobs_permit() {
|
async fn dropping_the_waiter_does_not_release_a_running_jobs_permit() {
|
||||||
let runner = BlockingJobRunner::new(1);
|
let runner = BlockingJobRunner::new(1);
|
||||||
let (first_started_tx, first_started_rx) = tokio::sync::oneshot::channel();
|
let (first_started_tx, first_started_rx) = oneshot::channel();
|
||||||
let (first_release_tx, first_release_rx) = mpsc::channel();
|
let (first_release_tx, first_release_rx) = mpsc::channel();
|
||||||
let first_runner = runner.clone();
|
let first_runner = runner.clone();
|
||||||
let first = tokio::spawn(async move {
|
let first = tokio::spawn(async move {
|
||||||
@@ -268,7 +269,7 @@ mod tests {
|
|||||||
"the detached blocking closure must retain its permit"
|
"the detached blocking closure must retain its permit"
|
||||||
);
|
);
|
||||||
|
|
||||||
let (second_started_tx, mut second_started_rx) = tokio::sync::oneshot::channel();
|
let (second_started_tx, mut second_started_rx) = oneshot::channel();
|
||||||
let second = tokio::spawn(async move {
|
let second = tokio::spawn(async move {
|
||||||
runner
|
runner
|
||||||
.run(BlockingJobKind::SessionCatalogLoad, move || {
|
.run(BlockingJobKind::SessionCatalogLoad, move || {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fn load_app_stylesheet() {
|
|||||||
// overlay crate's `v4_12` and then deprecates this method.
|
// overlay crate's `v4_12` and then deprecates this method.
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
provider.load_from_data(include_str!("style.css"));
|
provider.load_from_data(include_str!("style.css"));
|
||||||
if let Some(display) = gtk::gdk::Display::default() {
|
if let Some(display) = Display::default() {
|
||||||
gtk::style_context_add_provider_for_display(
|
gtk::style_context_add_provider_for_display(
|
||||||
&display,
|
&display,
|
||||||
&provider,
|
&provider,
|
||||||
@@ -46,6 +46,9 @@ fn load_app_stylesheet() {
|
|||||||
const APP_ID: &str = "org.wayscriber.Configurator";
|
const APP_ID: &str = "org.wayscriber.Configurator";
|
||||||
|
|
||||||
use effects::spawn_effect;
|
use effects::spawn_effect;
|
||||||
|
use gtk::gdk::Display;
|
||||||
|
use gtk::gio::ApplicationFlags;
|
||||||
|
use wayscriber::domain::Language;
|
||||||
|
|
||||||
pub(crate) fn run(startup: StartupRequest) {
|
pub(crate) fn run(startup: StartupRequest) {
|
||||||
// Every launch is its own window, as it was under Iced: the overlay and
|
// Every launch is its own window, as it was under Iced: the overlay and
|
||||||
@@ -56,7 +59,7 @@ pub(crate) fn run(startup: StartupRequest) {
|
|||||||
// safe: the guarded `ConfigDocument` save revision arbitrates writes.
|
// safe: the guarded `ConfigDocument` save revision arbitrates writes.
|
||||||
let application = adw::Application::builder()
|
let application = adw::Application::builder()
|
||||||
.application_id(APP_ID)
|
.application_id(APP_ID)
|
||||||
.flags(gtk::gio::ApplicationFlags::NON_UNIQUE)
|
.flags(ApplicationFlags::NON_UNIQUE)
|
||||||
.build();
|
.build();
|
||||||
let app = RelmApp::from_app(application);
|
let app = RelmApp::from_app(application);
|
||||||
// GTK must not parse our argv: `--open <destination>` is ours and was
|
// GTK must not parse our argv: `--open <destination>` is ours and was
|
||||||
@@ -92,7 +95,7 @@ pub(crate) struct AppWidgets {
|
|||||||
bindings: Vec<Binding>,
|
bindings: Vec<Binding>,
|
||||||
/// The language the pages were built in. When the active language moves
|
/// The language the pages were built in. When the active language moves
|
||||||
/// away from it, the next refresh rebuilds them.
|
/// away from it, the next refresh rebuilds them.
|
||||||
language: wayscriber::domain::Language,
|
language: Language,
|
||||||
chrome: shell::Chrome,
|
chrome: shell::Chrome,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use relm4::ComponentSender;
|
use relm4::ComponentSender;
|
||||||
|
use relm4::adw::ApplicationWindow;
|
||||||
|
|
||||||
use crate::messages::CommandMessage;
|
use crate::messages::CommandMessage;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ use super::super::{daemon_setup, io, session_catalog};
|
|||||||
pub(super) fn spawn_effect(
|
pub(super) fn spawn_effect(
|
||||||
effect: Effect,
|
effect: Effect,
|
||||||
sender: &ComponentSender<ConfiguratorApp>,
|
sender: &ComponentSender<ConfiguratorApp>,
|
||||||
root: &relm4::adw::ApplicationWindow,
|
root: &ApplicationWindow,
|
||||||
) {
|
) {
|
||||||
use relm4::gtk::prelude::*;
|
use relm4::gtk::prelude::*;
|
||||||
match effect {
|
match effect {
|
||||||
|
|||||||
@@ -10,10 +10,14 @@
|
|||||||
//! it, and how far the visible page is scrolled. This module carries those
|
//! it, and how far the visible page is scrolled. This module carries those
|
||||||
//! across.
|
//! across.
|
||||||
|
|
||||||
|
use std::iter;
|
||||||
|
|
||||||
use relm4::{ComponentSender, gtk};
|
use relm4::{ComponentSender, gtk};
|
||||||
|
|
||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
use gtk::prelude::*;
|
use gtk::glib::GString;
|
||||||
|
use gtk::prelude::{GtkWindowExt, *};
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
use super::super::pages;
|
use super::super::pages;
|
||||||
use super::super::state::ConfiguratorApp;
|
use super::super::state::ConfiguratorApp;
|
||||||
@@ -56,7 +60,7 @@ impl WidgetPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn children(widget: >k::Widget) -> impl Iterator<Item = gtk::Widget> {
|
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.
|
/// The caret and selection of an editable control, in characters.
|
||||||
@@ -69,7 +73,7 @@ struct Caret {
|
|||||||
/// View state from the pages being replaced, applied to the new ones.
|
/// View state from the pages being replaced, applied to the new ones.
|
||||||
pub(super) struct ViewState {
|
pub(super) struct ViewState {
|
||||||
/// The stack name of the page the state was read from.
|
/// The stack name of the page the state was read from.
|
||||||
page: Option<gtk::glib::GString>,
|
page: Option<GString>,
|
||||||
/// Set when focus was inside the visible page; focus elsewhere sits in
|
/// Set when focus was inside the visible page; focus elsewhere sits in
|
||||||
/// persistent chrome, which the rebuild leaves alone.
|
/// persistent chrome, which the rebuild leaves alone.
|
||||||
focus: Option<(Option<WidgetPath>, Option<Caret>)>,
|
focus: Option<(Option<WidgetPath>, Option<Caret>)>,
|
||||||
@@ -84,7 +88,7 @@ pub(super) fn rebuild_if_language_changed(
|
|||||||
widgets: &mut AppWidgets,
|
widgets: &mut AppWidgets,
|
||||||
sender: &ComponentSender<ConfiguratorApp>,
|
sender: &ComponentSender<ConfiguratorApp>,
|
||||||
) -> Option<ViewState> {
|
) -> Option<ViewState> {
|
||||||
let language = wayscriber::i18n::language();
|
let language = i18n::language();
|
||||||
if widgets.language == language {
|
if widgets.language == language {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -123,7 +127,7 @@ fn capture(widgets: &AppWidgets) -> ViewState {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let focus = gtk::prelude::GtkWindowExt::focus(&widgets.root)
|
let focus = GtkWindowExt::focus(&widgets.root)
|
||||||
.filter(|focused| focused.is_ancestor(&page))
|
.filter(|focused| focused.is_ancestor(&page))
|
||||||
.map(|focused| {
|
.map(|focused| {
|
||||||
let caret = focused
|
let caret = focused
|
||||||
@@ -157,7 +161,7 @@ fn capture(widgets: &AppWidgets) -> ViewState {
|
|||||||
|
|
||||||
fn descendants(widget: >k::Widget) -> impl Iterator<Item = gtk::Widget> {
|
fn descendants(widget: >k::Widget) -> impl Iterator<Item = gtk::Widget> {
|
||||||
let mut pending = vec![widget.clone()];
|
let mut pending = vec![widget.clone()];
|
||||||
std::iter::from_fn(move || {
|
iter::from_fn(move || {
|
||||||
let next = pending.pop()?;
|
let next = pending.pop()?;
|
||||||
pending.extend(children(&next));
|
pending.extend(children(&next));
|
||||||
Some(next)
|
Some(next)
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use relm4::{ComponentSender, adw, gtk};
|
|||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
use gtk::gdk::{Key, ModifierType};
|
||||||
|
use gtk::glib::Propagation;
|
||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
use wayscriber::{build_info, i18n};
|
||||||
|
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
use crate::models::TabId;
|
use crate::models::TabId;
|
||||||
@@ -39,7 +42,7 @@ pub(super) fn build(
|
|||||||
let sender = sender.clone();
|
let sender = sender.clone();
|
||||||
root.connect_close_request(move |_| {
|
root.connect_close_request(move |_| {
|
||||||
sender.input(Message::CloseRequested);
|
sender.input(Message::CloseRequested);
|
||||||
gtk::glib::Propagation::Stop
|
Propagation::Stop
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// ---- Sidebar ----------------------------------------------------
|
// ---- Sidebar ----------------------------------------------------
|
||||||
@@ -267,13 +270,11 @@ pub(super) fn build(
|
|||||||
// lets widgets such as SearchEntry perform their own Escape behavior.
|
// lets widgets such as SearchEntry perform their own Escape behavior.
|
||||||
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||||
controller.connect_key_pressed(move |_, key, _, modifiers| {
|
controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||||
if modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK)
|
if modifiers.contains(ModifierType::CONTROL_MASK) && matches!(key, Key::f | Key::F) {
|
||||||
&& matches!(key, gtk::gdk::Key::f | gtk::gdk::Key::F)
|
|
||||||
{
|
|
||||||
sender.input(Message::SearchFocusRequested);
|
sender.input(Message::SearchFocusRequested);
|
||||||
return gtk::glib::Propagation::Stop;
|
return Propagation::Stop;
|
||||||
}
|
}
|
||||||
if key == gtk::gdk::Key::Escape {
|
if key == Key::Escape {
|
||||||
// The recorder owns Escape while it is open so a recorded
|
// The recorder owns Escape while it is open so a recorded
|
||||||
// Escape becomes a binding. Confirmation cancel is the
|
// Escape becomes a binding. Confirmation cancel is the
|
||||||
// fallback when nothing is recording.
|
// fallback when nothing is recording.
|
||||||
@@ -281,10 +282,10 @@ pub(super) fn build(
|
|||||||
}
|
}
|
||||||
// Tab is the user moving focus deliberately; a still-pending
|
// Tab is the user moving focus deliberately; a still-pending
|
||||||
// startup search focus must not steal it back later.
|
// startup search focus must not steal it back later.
|
||||||
if matches!(key, gtk::gdk::Key::Tab | gtk::gdk::Key::ISO_Left_Tab) {
|
if matches!(key, Key::Tab | Key::ISO_Left_Tab) {
|
||||||
sender.input(Message::StartupInteractionObserved);
|
sender.input(Message::StartupInteractionObserved);
|
||||||
}
|
}
|
||||||
gtk::glib::Propagation::Proceed
|
Propagation::Proceed
|
||||||
});
|
});
|
||||||
root.add_controller(controller);
|
root.add_controller(controller);
|
||||||
}
|
}
|
||||||
@@ -325,7 +326,7 @@ pub(super) fn build(
|
|||||||
search_entry,
|
search_entry,
|
||||||
seen_focus_serial: 0,
|
seen_focus_serial: 0,
|
||||||
bindings,
|
bindings,
|
||||||
language: wayscriber::i18n::language(),
|
language: i18n::language(),
|
||||||
chrome: Chrome {
|
chrome: Chrome {
|
||||||
build_identity,
|
build_identity,
|
||||||
content_page,
|
content_page,
|
||||||
@@ -385,10 +386,7 @@ pub(super) fn retitle(widgets: &AppWidgets) {
|
|||||||
}
|
}
|
||||||
chrome.build_identity.set_label(&trf(
|
chrome.build_identity.set_label(&trf(
|
||||||
"Version {} · commit {}",
|
"Version {} · commit {}",
|
||||||
&[
|
&[&build_info::version(), &build_info::commit_hash()],
|
||||||
&wayscriber::build_info::version(),
|
|
||||||
&wayscriber::build_info::commit_hash(),
|
|
||||||
],
|
|
||||||
));
|
));
|
||||||
widgets
|
widgets
|
||||||
.window_title
|
.window_title
|
||||||
|
|||||||
@@ -17,17 +17,23 @@
|
|||||||
//! every supported language, whichever language is active.
|
//! every supported language, whichever language is active.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use std::{env, fs, iter, thread};
|
||||||
|
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
use relm4::{adw, gtk};
|
use relm4::{adw, gtk};
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
use gtk::glib::MainContext;
|
||||||
|
use gtk::prelude::GtkWindowExt;
|
||||||
use wayscriber::domain::Language;
|
use wayscriber::domain::Language;
|
||||||
|
use wayscriber::{env_vars, i18n};
|
||||||
|
|
||||||
use crate::messages::{CommandMessage, Message};
|
use crate::messages::{CommandMessage, Message};
|
||||||
use crate::models::{KeybindingField, LanguageOption, StartupRequest, TabId, UiTabId};
|
use crate::models::{KeybindingField, LanguageOption, StartupRequest, TabId, UiTabId};
|
||||||
|
use crate::test_temp;
|
||||||
|
|
||||||
use super::super::state::ConfiguratorApp;
|
use super::super::state::ConfiguratorApp;
|
||||||
|
|
||||||
@@ -37,14 +43,14 @@ const CHILD_ENV: &str = "WAYSCRIBER_CONFIGURATOR_GTK_CHILD";
|
|||||||
#[test]
|
#[test]
|
||||||
fn language_changes_rebuild_the_window_in_place() {
|
fn language_changes_rebuild_the_window_in_place() {
|
||||||
// A window is allowed only on the dedicated test compositor.
|
// A window is allowed only on the dedicated test compositor.
|
||||||
if std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none()
|
if env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none()
|
||||||
|| std::env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests")
|
|| env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests")
|
||||||
{
|
{
|
||||||
eprintln!("skipping configurator GTK test: not on the private test compositor");
|
eprintln!("skipping configurator GTK test: not on the private test compositor");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if std::env::var_os(CHILD_ENV).is_none() {
|
if env::var_os(CHILD_ENV).is_none() {
|
||||||
run_in_child();
|
run_in_child();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,8 +63,8 @@ fn language_changes_rebuild_the_window_in_place() {
|
|||||||
/// homes, so nothing reads or writes the user's files, no setup tool runs,
|
/// homes, so nothing reads or writes the user's files, no setup tool runs,
|
||||||
/// and the catalogs load from the embedded copies.
|
/// and the catalogs load from the embedded copies.
|
||||||
fn run_in_child() {
|
fn run_in_child() {
|
||||||
let homes = crate::test_temp::tempdir().expect("child homes");
|
let homes = test_temp::tempdir().expect("child homes");
|
||||||
let mut command = std::process::Command::new(std::env::current_exe().expect("test binary"));
|
let mut command = Command::new(env::current_exe().expect("test binary"));
|
||||||
command
|
command
|
||||||
.args([
|
.args([
|
||||||
concat!(
|
concat!(
|
||||||
@@ -72,21 +78,21 @@ fn run_in_child() {
|
|||||||
"--nocapture",
|
"--nocapture",
|
||||||
])
|
])
|
||||||
.env(CHILD_ENV, "1")
|
.env(CHILD_ENV, "1")
|
||||||
.env_remove(wayscriber::env_vars::LANGUAGE_OVERRIDE_ENV);
|
.env_remove(env_vars::LANGUAGE_OVERRIDE_ENV);
|
||||||
for (key, directory) in [
|
for (key, directory) in [
|
||||||
("XDG_CONFIG_HOME", "config"),
|
("XDG_CONFIG_HOME", "config"),
|
||||||
("XDG_DATA_HOME", "data"),
|
("XDG_DATA_HOME", "data"),
|
||||||
("XDG_STATE_HOME", "state"),
|
("XDG_STATE_HOME", "state"),
|
||||||
("XDG_CACHE_HOME", "cache"),
|
("XDG_CACHE_HOME", "cache"),
|
||||||
(wayscriber::env_vars::LOCALE_DIR_ENV, "locales"),
|
(env_vars::LOCALE_DIR_ENV, "locales"),
|
||||||
// No setup tool (systemctl, gsettings) is reachable from here.
|
// No setup tool (systemctl, gsettings) is reachable from here.
|
||||||
("PATH", "bin"),
|
("PATH", "bin"),
|
||||||
] {
|
] {
|
||||||
let path = homes.path().join(directory);
|
let path = homes.path().join(directory);
|
||||||
std::fs::create_dir_all(&path).expect("child home");
|
fs::create_dir_all(&path).expect("child home");
|
||||||
command.env(key, path);
|
command.env(key, path);
|
||||||
}
|
}
|
||||||
for key in wayscriber::env_vars::LOCALE_ENV_KEYS {
|
for key in env_vars::LOCALE_ENV_KEYS {
|
||||||
command.env_remove(key);
|
command.env_remove(key);
|
||||||
}
|
}
|
||||||
write_config(&homes.path().join("config"), "[ui]\nlanguage = \"en\"\n");
|
write_config(&homes.path().join("config"), "[ui]\nlanguage = \"en\"\n");
|
||||||
@@ -97,8 +103,8 @@ fn run_in_child() {
|
|||||||
|
|
||||||
fn write_config(config_home: &Path, contents: &str) {
|
fn write_config(config_home: &Path, contents: &str) {
|
||||||
let directory = config_home.join("wayscriber");
|
let directory = config_home.join("wayscriber");
|
||||||
std::fs::create_dir_all(&directory).expect("config directory");
|
fs::create_dir_all(&directory).expect("config directory");
|
||||||
std::fs::write(directory.join("config.toml"), contents).expect("write config");
|
fs::write(directory.join("config.toml"), contents).expect("write config");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_scenario() {
|
fn run_scenario() {
|
||||||
@@ -111,7 +117,7 @@ fn run_scenario() {
|
|||||||
!app.model().document.is_loading()
|
!app.model().document.is_loading()
|
||||||
});
|
});
|
||||||
settle();
|
settle();
|
||||||
assert_eq!(wayscriber::i18n::language(), Language::English);
|
assert_eq!(i18n::language(), Language::English);
|
||||||
assert_eq!(sidebar_label(&app, TabId::Drawing), "Drawing");
|
assert_eq!(sidebar_label(&app, TabId::Drawing), "Drawing");
|
||||||
let pages = app.widgets().stack.observe_children().n_items();
|
let pages = app.widgets().stack.observe_children().n_items();
|
||||||
let bindings = app.widgets().bindings.len();
|
let bindings = app.widgets().bindings.len();
|
||||||
@@ -199,7 +205,7 @@ fn service_buttons_describe_service_actions_in_each_language(app: &Controller<Co
|
|||||||
let buttons: Vec<_> = visible_page_descendants(app)
|
let buttons: Vec<_> = visible_page_descendants(app)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|widget| {
|
.filter(|widget| {
|
||||||
std::iter::successors(Some(widget.clone()), gtk::Widget::parent)
|
iter::successors(Some(widget.clone()), gtk::Widget::parent)
|
||||||
.take_while(|ancestor| *ancestor != page)
|
.take_while(|ancestor| *ancestor != page)
|
||||||
.all(|ancestor| ancestor.is_visible())
|
.all(|ancestor| ancestor.is_visible())
|
||||||
})
|
})
|
||||||
@@ -430,7 +436,7 @@ fn a_load_that_changes_the_language_keeps_its_resets(app: &Controller<Configurat
|
|||||||
app.emit(Message::ShortcutRecordingStarted(KeybindingField::Undo));
|
app.emit(Message::ShortcutRecordingStarted(KeybindingField::Undo));
|
||||||
settle();
|
settle();
|
||||||
write_config(
|
write_config(
|
||||||
Path::new(&std::env::var_os("XDG_CONFIG_HOME").expect("child config home")),
|
Path::new(&env::var_os("XDG_CONFIG_HOME").expect("child config home")),
|
||||||
"[ui]\nlanguage = \"zh-CN\"\n",
|
"[ui]\nlanguage = \"zh-CN\"\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -440,8 +446,7 @@ fn a_load_that_changes_the_language_keeps_its_resets(app: &Controller<Configurat
|
|||||||
settle();
|
settle();
|
||||||
app.emit(Message::LeaveDiscardRequested);
|
app.emit(Message::LeaveDiscardRequested);
|
||||||
pump_until("the reload", || {
|
pump_until("the reload", || {
|
||||||
!app.model().document.is_loading()
|
!app.model().document.is_loading() && i18n::language() == Language::SimplifiedChinese
|
||||||
&& wayscriber::i18n::language() == Language::SimplifiedChinese
|
|
||||||
});
|
});
|
||||||
settle();
|
settle();
|
||||||
|
|
||||||
@@ -492,7 +497,7 @@ fn assert_caret(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus(app: &Controller<ConfiguratorApp>) -> Option<gtk::Widget> {
|
fn focus(app: &Controller<ConfiguratorApp>) -> Option<gtk::Widget> {
|
||||||
gtk::prelude::GtkWindowExt::focus(app.widget())
|
GtkWindowExt::focus(app.widget())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible_page_descendants(app: &Controller<ConfiguratorApp>) -> Vec<gtk::Widget> {
|
fn visible_page_descendants(app: &Controller<ConfiguratorApp>) -> Vec<gtk::Widget> {
|
||||||
@@ -504,7 +509,7 @@ fn descendants(root: gtk::Widget) -> Vec<gtk::Widget> {
|
|||||||
let mut found = Vec::new();
|
let mut found = Vec::new();
|
||||||
let mut pending = vec![root];
|
let mut pending = vec![root];
|
||||||
while let Some(widget) = pending.pop() {
|
while let Some(widget) = pending.pop() {
|
||||||
pending.extend(std::iter::successors(
|
pending.extend(iter::successors(
|
||||||
widget.first_child(),
|
widget.first_child(),
|
||||||
gtk::Widget::next_sibling,
|
gtk::Widget::next_sibling,
|
||||||
));
|
));
|
||||||
@@ -542,25 +547,25 @@ fn scrollable(app: &Controller<ConfiguratorApp>) -> Option<gtk::ScrolledWindow>
|
|||||||
|
|
||||||
/// Runs the main loop until `done`, failing after a generous timeout.
|
/// Runs the main loop until `done`, failing after a generous timeout.
|
||||||
fn pump_until(what: &str, mut done: impl FnMut() -> bool) {
|
fn pump_until(what: &str, mut done: impl FnMut() -> bool) {
|
||||||
let context = gtk::glib::MainContext::default();
|
let context = MainContext::default();
|
||||||
let deadline = Instant::now() + Duration::from_secs(30);
|
let deadline = Instant::now() + Duration::from_secs(30);
|
||||||
while !done() {
|
while !done() {
|
||||||
assert!(Instant::now() < deadline, "timed out waiting for {what}");
|
assert!(Instant::now() < deadline, "timed out waiting for {what}");
|
||||||
while context.pending() {
|
while context.pending() {
|
||||||
context.iteration(false);
|
context.iteration(false);
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(5));
|
thread::sleep(Duration::from_millis(5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lets queued messages, refreshes and a few frames run.
|
/// Lets queued messages, refreshes and a few frames run.
|
||||||
fn settle() {
|
fn settle() {
|
||||||
let context = gtk::glib::MainContext::default();
|
let context = MainContext::default();
|
||||||
let until = Instant::now() + Duration::from_millis(300);
|
let until = Instant::now() + Duration::from_millis(300);
|
||||||
while Instant::now() < until {
|
while Instant::now() < until {
|
||||||
while context.pending() {
|
while context.pending() {
|
||||||
context.iteration(false);
|
context.iteration(false);
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(5));
|
thread::sleep(Duration::from_millis(5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ use relm4::{ComponentSender, adw, gtk};
|
|||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
use gtk::prelude::GtkWindowExt;
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use super::super::pages;
|
use super::super::pages;
|
||||||
@@ -78,7 +79,7 @@ fn write_widgets(app: &ConfiguratorApp, widgets: &mut AppWidgets) {
|
|||||||
let leave_pending = app.pending_leave().is_some();
|
let leave_pending = app.pending_leave().is_some();
|
||||||
let leave_was_pending = widgets.leave_actions.get_visible();
|
let leave_was_pending = widgets.leave_actions.get_visible();
|
||||||
if leave_pending && !leave_was_pending {
|
if leave_pending && !leave_was_pending {
|
||||||
widgets.leave_previous_focus = gtk::prelude::GtkWindowExt::focus(&widgets.root);
|
widgets.leave_previous_focus = GtkWindowExt::focus(&widgets.root);
|
||||||
}
|
}
|
||||||
set_visible(&widgets.leave_actions, leave_pending);
|
set_visible(&widgets.leave_actions, leave_pending);
|
||||||
if leave_pending && !leave_was_pending {
|
if leave_pending && !leave_was_pending {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
use wayscriber::{durable_io, paths};
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
@@ -85,7 +86,7 @@ impl HyprlandLightControlsInstallResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn read_light_controls_status() -> HyprlandLightControlsStatus {
|
pub(super) fn read_light_controls_status() -> HyprlandLightControlsStatus {
|
||||||
let Some(config_root) = wayscriber::paths::config_dir() else {
|
let Some(config_root) = paths::config_dir() else {
|
||||||
return HyprlandLightControlsStatus {
|
return HyprlandLightControlsStatus {
|
||||||
include_path: None,
|
include_path: None,
|
||||||
include_exists: false,
|
include_exists: false,
|
||||||
@@ -111,7 +112,7 @@ fn read_light_controls_status_from_config_root(config_root: &Path) -> HyprlandLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn install_light_controls() -> Result<HyprlandLightControlsInstallResult, String> {
|
pub(super) fn install_light_controls() -> Result<HyprlandLightControlsInstallResult, String> {
|
||||||
let config_root = wayscriber::paths::config_dir().ok_or_else(|| {
|
let config_root = paths::config_dir().ok_or_else(|| {
|
||||||
tr("Cannot resolve config directory; failed to determine Hyprland config path.").to_string()
|
tr("Cannot resolve config directory; failed to determine Hyprland config path.").to_string()
|
||||||
})?;
|
})?;
|
||||||
let binary_path = resolve_wayscriber_binary_path()?;
|
let binary_path = resolve_wayscriber_binary_path()?;
|
||||||
@@ -151,7 +152,7 @@ fn write_light_controls(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let include_path = light_controls_include_path(config_root);
|
let include_path = light_controls_include_path(config_root);
|
||||||
wayscriber::durable_io::write_text_atomic(
|
durable_io::write_text_atomic(
|
||||||
&include_path,
|
&include_path,
|
||||||
&render_light_controls(binary_path),
|
&render_light_controls(binary_path),
|
||||||
AtomicWriteOptions::user_config_file(),
|
AtomicWriteOptions::user_config_file(),
|
||||||
@@ -174,7 +175,7 @@ fn write_light_controls(
|
|||||||
source_configured = true;
|
source_configured = true;
|
||||||
source_updated = changed;
|
source_updated = changed;
|
||||||
if changed {
|
if changed {
|
||||||
wayscriber::durable_io::write_text_atomic(
|
durable_io::write_text_atomic(
|
||||||
&main_config_path,
|
&main_config_path,
|
||||||
&updated_content,
|
&updated_content,
|
||||||
AtomicWriteOptions::user_config_file(),
|
AtomicWriteOptions::user_config_file(),
|
||||||
@@ -326,7 +327,7 @@ fn strip_matching_quotes(value: &str) -> &str {
|
|||||||
|
|
||||||
fn normalize_source_target(target: &str) -> PathBuf {
|
fn normalize_source_target(target: &str) -> PathBuf {
|
||||||
let expanded = if let Some(stripped) = target.strip_prefix("~/") {
|
let expanded = if let Some(stripped) = target.strip_prefix("~/") {
|
||||||
wayscriber::paths::home_dir()
|
paths::home_dir()
|
||||||
.map(|home| home.join(stripped))
|
.map(|home| home.join(stripped))
|
||||||
.unwrap_or_else(|| PathBuf::from(target))
|
.unwrap_or_else(|| PathBuf::from(target))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use crate::test_env::EnvGuard;
|
||||||
|
use crate::test_temp;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use wayscriber::env_vars::HOME_ENV;
|
use wayscriber::env_vars::HOME_ENV;
|
||||||
|
|
||||||
@@ -76,9 +79,9 @@ fn has_source_line_matches_quoted_and_inline_commented_targets() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn has_source_line_matches_tilde_target() {
|
fn has_source_line_matches_tilde_target() {
|
||||||
let tmp = crate::test_temp::tempdir().unwrap();
|
let tmp = test_temp::tempdir().unwrap();
|
||||||
let home = tmp.path();
|
let home = tmp.path();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(HOME_ENV, home.as_os_str())]);
|
let _env = EnvGuard::set(&[(HOME_ENV, home.as_os_str())]);
|
||||||
|
|
||||||
let absolute = home
|
let absolute = home
|
||||||
.join(".config")
|
.join(".config")
|
||||||
@@ -93,7 +96,7 @@ fn has_source_line_matches_tilde_target() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_light_controls_writes_include_and_sources_existing_main() {
|
fn write_light_controls_writes_include_and_sources_existing_main() {
|
||||||
let tmp = crate::test_temp::tempdir().unwrap();
|
let tmp = test_temp::tempdir().unwrap();
|
||||||
let hypr_dir = tmp.path().join(HYPRLAND_DIR);
|
let hypr_dir = tmp.path().join(HYPRLAND_DIR);
|
||||||
fs::create_dir_all(&hypr_dir).unwrap();
|
fs::create_dir_all(&hypr_dir).unwrap();
|
||||||
let main = hypr_dir.join(MAIN_CONFIG);
|
let main = hypr_dir.join(MAIN_CONFIG);
|
||||||
@@ -113,7 +116,7 @@ fn write_light_controls_writes_include_and_sources_existing_main() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_light_controls_is_idempotent_for_existing_source() {
|
fn write_light_controls_is_idempotent_for_existing_source() {
|
||||||
let tmp = crate::test_temp::tempdir().unwrap();
|
let tmp = test_temp::tempdir().unwrap();
|
||||||
let hypr_dir = tmp.path().join(HYPRLAND_DIR);
|
let hypr_dir = tmp.path().join(HYPRLAND_DIR);
|
||||||
fs::create_dir_all(&hypr_dir).unwrap();
|
fs::create_dir_all(&hypr_dir).unwrap();
|
||||||
let main = hypr_dir.join(MAIN_CONFIG);
|
let main = hypr_dir.join(MAIN_CONFIG);
|
||||||
@@ -131,7 +134,7 @@ fn write_light_controls_is_idempotent_for_existing_source() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_light_controls_handles_missing_main_config() {
|
fn write_light_controls_handles_missing_main_config() {
|
||||||
let tmp = crate::test_temp::tempdir().unwrap();
|
let tmp = test_temp::tempdir().unwrap();
|
||||||
|
|
||||||
let result = write_light_controls(tmp.path(), Path::new("/tmp/wayscriber")).unwrap();
|
let result = write_light_controls(tmp.path(), Path::new("/tmp/wayscriber")).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use wayscriber::i18n::{tr, trf};
|
|||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
DaemonAction, DaemonActionResult, DaemonRuntimeStatus, DesktopEnvironment,
|
DaemonAction, DaemonActionResult, DaemonRuntimeStatus, DesktopEnvironment,
|
||||||
LightShortcutApplyCapability, ShortcutBackend,
|
LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend,
|
||||||
};
|
};
|
||||||
|
|
||||||
use command::command_available;
|
use command::command_available;
|
||||||
@@ -139,7 +139,7 @@ pub(super) fn load_daemon_runtime_status_sync() -> Result<DaemonRuntimeStatus, S
|
|||||||
read_portal_shortcut_dropin_state(),
|
read_portal_shortcut_dropin_state(),
|
||||||
portal_runtime_supported,
|
portal_runtime_supported,
|
||||||
);
|
);
|
||||||
let shortcut_apply_capability = crate::models::ShortcutApplyCapability::from_environment(
|
let shortcut_apply_capability = ShortcutApplyCapability::from_environment(
|
||||||
desktop,
|
desktop,
|
||||||
gsettings_available,
|
gsettings_available,
|
||||||
systemctl_available,
|
systemctl_available,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
use wayscriber::durable_io;
|
||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
use crate::models::DesktopEnvironment;
|
use crate::models::DesktopEnvironment;
|
||||||
use wayscriber::durable_io::AtomicWriteOptions;
|
use wayscriber::durable_io::AtomicWriteOptions;
|
||||||
@@ -115,7 +115,7 @@ pub(super) fn install_or_update_user_service() -> Result<PathBuf, String> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let contents = render_user_service_file(&binary_path);
|
let contents = render_user_service_file(&binary_path);
|
||||||
wayscriber::durable_io::write_text_atomic(
|
durable_io::write_text_atomic(
|
||||||
&service_path,
|
&service_path,
|
||||||
&contents,
|
&contents,
|
||||||
AtomicWriteOptions::user_config_file(),
|
AtomicWriteOptions::user_config_file(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
use wayscriber::durable_io;
|
||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend};
|
use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend};
|
||||||
use wayscriber::durable_io::AtomicWriteOptions;
|
use wayscriber::durable_io::AtomicWriteOptions;
|
||||||
@@ -176,7 +178,7 @@ fn run_gsettings_command(args: &[&str]) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_portal_shortcut_dropin(shortcut: &str) -> Result<std::path::PathBuf, String> {
|
fn write_portal_shortcut_dropin(shortcut: &str) -> Result<PathBuf, String> {
|
||||||
let dropin_path = portal_shortcut_dropin_path().ok_or_else(|| {
|
let dropin_path = portal_shortcut_dropin_path().ok_or_else(|| {
|
||||||
tr("Cannot resolve home directory; failed to determine systemd drop-in path.").to_string()
|
tr("Cannot resolve home directory; failed to determine systemd drop-in path.").to_string()
|
||||||
})?;
|
})?;
|
||||||
@@ -193,7 +195,7 @@ fn write_portal_shortcut_dropin(shortcut: &str) -> Result<std::path::PathBuf, St
|
|||||||
let escaped_shortcut = escape_systemd_env_value(shortcut);
|
let escaped_shortcut = escape_systemd_env_value(shortcut);
|
||||||
let escaped_app_id = escape_systemd_env_value(PORTAL_APP_ID);
|
let escaped_app_id = escape_systemd_env_value(PORTAL_APP_ID);
|
||||||
let contents = render_portal_shortcut_dropin(&escaped_shortcut, &escaped_app_id);
|
let contents = render_portal_shortcut_dropin(&escaped_shortcut, &escaped_app_id);
|
||||||
wayscriber::durable_io::write_text_atomic(
|
durable_io::write_text_atomic(
|
||||||
&dropin_path,
|
&dropin_path,
|
||||||
&contents,
|
&contents,
|
||||||
AtomicWriteOptions::user_config_file(),
|
AtomicWriteOptions::user_config_file(),
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ fn action_pending_message(action: DaemonAction) -> &'static str {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
|
use crate::test_env;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use wayscriber::domain::Language;
|
use wayscriber::domain::Language;
|
||||||
|
|
||||||
@@ -298,7 +302,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn feedback_renders_in_the_language_active_when_it_is_drawn() {
|
fn feedback_renders_in_the_language_active_when_it_is_drawn() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::feedback_renders_in_the_language_active_when_it_is_drawn"
|
"::feedback_renders_in_the_language_active_when_it_is_drawn"
|
||||||
@@ -313,7 +317,7 @@ mod tests {
|
|||||||
let detecting = DaemonFeedback::Status(StatusFeedback::Detecting);
|
let detecting = DaemonFeedback::Status(StatusFeedback::Detecting);
|
||||||
let english = (failed.text(), detecting.text());
|
let english = (failed.text(), detecting.text());
|
||||||
|
|
||||||
wayscriber::i18n::set_language(Language::SimplifiedChinese);
|
i18n::set_language(Language::SimplifiedChinese);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
english.0,
|
english.0,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Owns document transfer to effects and mutually exclusive load/save phases.
|
//! Owns document transfer to effects and mutually exclusive load/save phases.
|
||||||
|
use std::mem;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use wayscriber::config::{ConfigDocument, ConfigValidationReport};
|
use wayscriber::config::{ConfigDocument, ConfigValidationReport};
|
||||||
|
|
||||||
@@ -78,8 +79,7 @@ impl DocumentWorkflow {
|
|||||||
if !self.is_saving() {
|
if !self.is_saving() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let DocumentPhase::Saving(context) =
|
let DocumentPhase::Saving(context) = mem::replace(&mut self.phase, DocumentPhase::Idle)
|
||||||
std::mem::replace(&mut self.phase, DocumentPhase::Idle)
|
|
||||||
else {
|
else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use wayscriber::config::{Config, ConfigDocument};
|
use wayscriber::config::{Config, ConfigDocument};
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
use crate::messages::ConfigSaveResult;
|
use crate::messages::ConfigSaveResult;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ pub(super) async fn load_config_from_disk() -> Result<(Box<ConfigDocument>, Opti
|
|||||||
// Every catalog, whatever language the file names: the Language row
|
// Every catalog, whatever language the file names: the Language row
|
||||||
// can preview any of them later, and reading a user override is file
|
// can preview any of them later, and reading a user override is file
|
||||||
// work that must not happen on the GTK thread.
|
// work that must not happen on the GTK thread.
|
||||||
wayscriber::i18n::prepare_catalogs();
|
i18n::prepare_catalogs();
|
||||||
ConfigDocument::load_for_editing()
|
ConfigDocument::load_for_editing()
|
||||||
.map(|(document, warning)| (Box::new(document), warning))
|
.map(|(document, warning)| (Box::new(document), warning))
|
||||||
.map_err(|err| format!("{err:#}"))
|
.map_err(|err| format!("{err:#}"))
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
"Shape of the next arrow drawn. Every arrow keeps its own style, so \
|
"Shape of the next arrow drawn. Every arrow keeps its own style, so \
|
||||||
changing this never restyles existing drawings.",
|
changing this never restyles existing drawings.",
|
||||||
ArrowStyleOption::list(),
|
ArrowStyleOption::list(),
|
||||||
ArrowStyleOption::list()
|
&ArrowStyleOption::list()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|option| option.label().to_string())
|
.map(|option| option.label().to_string())
|
||||||
.collect(),
|
.collect::<Vec<_>>(),
|
||||||
|app| app.draft.arrow_style,
|
|app| app.draft.arrow_style,
|
||||||
Message::ArrowStyleChanged,
|
Message::ArrowStyleChanged,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ use wayscriber::i18n::tr;
|
|||||||
|
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
use relm4::{adw, gtk};
|
use relm4::{adw, gtk};
|
||||||
|
use wayscriber::domain::BoardGridKind;
|
||||||
|
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
@@ -238,7 +239,7 @@ struct SectionLayout {
|
|||||||
visible: bool,
|
visible: bool,
|
||||||
expanded: bool,
|
expanded: bool,
|
||||||
background_kind: BoardBackgroundOption,
|
background_kind: BoardBackgroundOption,
|
||||||
grid_kind: wayscriber::domain::BoardGridKind,
|
grid_kind: BoardGridKind,
|
||||||
pen_enabled: bool,
|
pen_enabled: bool,
|
||||||
auto_adjust: bool,
|
auto_adjust: bool,
|
||||||
persist: bool,
|
persist: bool,
|
||||||
@@ -271,18 +272,18 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec<Sec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One board row's values: everything the layout deliberately left out.
|
/// One board row's values: everything the layout deliberately left out.
|
||||||
struct BoardValues<'a> {
|
struct BoardValues<'draft> {
|
||||||
id: &'a str,
|
id: &'draft str,
|
||||||
name: &'a str,
|
name: &'draft str,
|
||||||
grid_spacing: &'a str,
|
grid_spacing: &'draft str,
|
||||||
background: ColorValues<'a>,
|
background: ColorValues<'draft>,
|
||||||
pen: ColorValues<'a>,
|
pen: ColorValues<'draft>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One color field's values: the picker's editing hex, and the draft's own
|
/// One color field's values: the picker's editing hex, and the draft's own
|
||||||
/// triplet, so the swatch parses the components by the same rules the draft
|
/// triplet, so the swatch parses the components by the same rules the draft
|
||||||
/// does.
|
/// does.
|
||||||
struct ColorValues<'a> {
|
struct ColorValues<'draft> {
|
||||||
hex: &'a str,
|
hex: &'draft str,
|
||||||
color: &'a ColorTripletInput,
|
color: &'draft ColorTripletInput,
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
use std::array;
|
||||||
|
|
||||||
use relm4::{ComponentSender, gtk};
|
use relm4::{ComponentSender, gtk};
|
||||||
|
|
||||||
|
use gtk::gdk::RGBA;
|
||||||
use gtk::glib::SignalHandlerId;
|
use gtk::glib::SignalHandlerId;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ impl ColorRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let rgb = parse_triplet_values(&values.color.components);
|
let rgb = parse_triplet_values(&values.color.components);
|
||||||
let rgba = gtk::gdk::RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0);
|
let rgba = RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0);
|
||||||
set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba);
|
set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +91,7 @@ pub(super) fn build_color_row(
|
|||||||
};
|
};
|
||||||
controls.append(&swatch);
|
controls.append(&swatch);
|
||||||
|
|
||||||
let components = std::array::from_fn(|component| {
|
let components = array::from_fn(|component| {
|
||||||
let entry = gtk::Entry::builder()
|
let entry = gtk::Entry::builder()
|
||||||
.placeholder_text(COMPONENT_PLACEHOLDERS.get(component).copied().unwrap_or(""))
|
.placeholder_text(COMPONENT_PLACEHOLDERS.get(component).copied().unwrap_or(""))
|
||||||
.width_chars(6)
|
.width_chars(6)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use super::{
|
use super::SectionLayout;
|
||||||
SectionLayout,
|
use super::rows::{TextRow, build_text_row};
|
||||||
rows::{TextRow, build_text_row},
|
|
||||||
};
|
|
||||||
use crate::app::state::ConfiguratorApp;
|
use crate::app::state::ConfiguratorApp;
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
use crate::models::{BoardBackgroundOption, BoardItemTextField};
|
use crate::models::{BoardBackgroundOption, BoardItemTextField};
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Selection frontend"),
|
tr("Selection frontend"),
|
||||||
tr("Native draws the picker over a frozen desktop image. Slurp uses the external selector."),
|
tr("Native draws the picker over a frozen desktop image. Slurp uses the external selector."),
|
||||||
RegionPickerOption::list(),
|
RegionPickerOption::list(),
|
||||||
labels(RegionPickerOption::list(), RegionPickerOption::label),
|
&labels(RegionPickerOption::list(), RegionPickerOption::label),
|
||||||
|app| app.draft.capture.region_picker,
|
|app| app.draft.capture.region_picker,
|
||||||
Message::CaptureRegionPickerChanged,
|
Message::CaptureRegionPickerChanged,
|
||||||
)
|
)
|
||||||
@@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Page size"),
|
tr("Page size"),
|
||||||
"",
|
"",
|
||||||
PdfPageSizeOption::list(),
|
PdfPageSizeOption::list(),
|
||||||
labels(PdfPageSizeOption::list(), PdfPageSizeOption::label),
|
&labels(PdfPageSizeOption::list(), PdfPageSizeOption::label),
|
||||||
|app| app.draft.export_pdf_page_size,
|
|app| app.draft.export_pdf_page_size,
|
||||||
Message::ExportPdfPageSizeChanged,
|
Message::ExportPdfPageSizeChanged,
|
||||||
)
|
)
|
||||||
@@ -134,7 +134,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Orientation"),
|
tr("Orientation"),
|
||||||
"",
|
"",
|
||||||
PdfOrientationOption::list(),
|
PdfOrientationOption::list(),
|
||||||
labels(PdfOrientationOption::list(), PdfOrientationOption::label),
|
&labels(PdfOrientationOption::list(), PdfOrientationOption::label),
|
||||||
|app| app.draft.export_pdf_orientation,
|
|app| app.draft.export_pdf_orientation,
|
||||||
Message::ExportPdfOrientationChanged,
|
Message::ExportPdfOrientationChanged,
|
||||||
)
|
)
|
||||||
@@ -142,7 +142,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Fit"),
|
tr("Fit"),
|
||||||
"",
|
"",
|
||||||
PdfFitModeOption::list(),
|
PdfFitModeOption::list(),
|
||||||
labels(PdfFitModeOption::list(), PdfFitModeOption::label),
|
&labels(PdfFitModeOption::list(), PdfFitModeOption::label),
|
||||||
|app| app.draft.export_pdf_fit,
|
|app| app.draft.export_pdf_fit,
|
||||||
Message::ExportPdfFitChanged,
|
Message::ExportPdfFitChanged,
|
||||||
)
|
)
|
||||||
@@ -150,7 +150,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Transparent page background"),
|
tr("Transparent page background"),
|
||||||
"",
|
"",
|
||||||
PdfTransparentBackgroundOption::list(),
|
PdfTransparentBackgroundOption::list(),
|
||||||
labels(
|
&labels(
|
||||||
PdfTransparentBackgroundOption::list(),
|
PdfTransparentBackgroundOption::list(),
|
||||||
PdfTransparentBackgroundOption::label,
|
PdfTransparentBackgroundOption::label,
|
||||||
),
|
),
|
||||||
@@ -185,7 +185,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Label position"),
|
tr("Label position"),
|
||||||
"",
|
"",
|
||||||
PdfLabelPositionOption::list(),
|
PdfLabelPositionOption::list(),
|
||||||
labels(
|
&labels(
|
||||||
PdfLabelPositionOption::list(),
|
PdfLabelPositionOption::list(),
|
||||||
PdfLabelPositionOption::label,
|
PdfLabelPositionOption::label,
|
||||||
),
|
),
|
||||||
@@ -196,7 +196,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Label content"),
|
tr("Label content"),
|
||||||
"",
|
"",
|
||||||
PdfLabelContentModeOption::list(),
|
PdfLabelContentModeOption::list(),
|
||||||
labels(
|
&labels(
|
||||||
PdfLabelContentModeOption::list(),
|
PdfLabelContentModeOption::list(),
|
||||||
PdfLabelContentModeOption::label,
|
PdfLabelContentModeOption::label,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
use relm4::{adw, gtk};
|
use relm4::{adw, gtk};
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use gtk::gdk::RGBA;
|
||||||
use gtk::glib::SignalHandlerId;
|
use gtk::glib::SignalHandlerId;
|
||||||
|
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
@@ -30,7 +31,7 @@ pub(crate) type ResolvedColor = Option<(f64, f64, f64, f64)>;
|
|||||||
|
|
||||||
/// Formats a dialog selection the way the hex handler expects: `#rrggbb`,
|
/// Formats a dialog selection the way the hex handler expects: `#rrggbb`,
|
||||||
/// with an alpha byte only when the color is actually translucent.
|
/// with an alpha byte only when the color is actually translucent.
|
||||||
pub(crate) fn dialog_hex(rgba: >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 channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||||
let (r, g, b, a) = (
|
let (r, g, b, a) = (
|
||||||
channel(rgba.red()),
|
channel(rgba.red()),
|
||||||
@@ -47,7 +48,7 @@ pub(crate) fn dialog_hex(rgba: >k::gdk::RGBA) -> String {
|
|||||||
|
|
||||||
/// Formats a selection for a four-component field, retaining `FF` so choosing
|
/// Formats a selection for a four-component field, retaining `FF` so choosing
|
||||||
/// fully opaque replaces any translucent alpha already in the draft.
|
/// fully opaque replaces any translucent alpha already in the draft.
|
||||||
fn dialog_hex_with_alpha(rgba: >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 channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||||
let (r, g, b, a) = (
|
let (r, g, b, a) = (
|
||||||
channel(rgba.red()),
|
channel(rgba.red()),
|
||||||
@@ -86,7 +87,7 @@ pub(crate) fn mark_hex_error(widget: &impl IsA<gtk::Widget>, text: &str) {
|
|||||||
pub(crate) fn set_swatch_blocked(
|
pub(crate) fn set_swatch_blocked(
|
||||||
button: >k::ColorDialogButton,
|
button: >k::ColorDialogButton,
|
||||||
handler: &SignalHandlerId,
|
handler: &SignalHandlerId,
|
||||||
rgba: >k::gdk::RGBA,
|
rgba: &RGBA,
|
||||||
) {
|
) {
|
||||||
if dialog_hex(&button.rgba()) == dialog_hex(rgba) {
|
if dialog_hex(&button.rgba()) == dialog_hex(rgba) {
|
||||||
return;
|
return;
|
||||||
@@ -140,7 +141,7 @@ pub(crate) fn color_row(
|
|||||||
set_text_blocked(&row, &row_handler, &text);
|
set_text_blocked(&row, &row_handler, &text);
|
||||||
mark_hex_error(&row, &text);
|
mark_hex_error(&row, &text);
|
||||||
if let Some((r, g, b, a)) = resolved(app) {
|
if let Some((r, g, b, a)) = resolved(app) {
|
||||||
let rgba = gtk::gdk::RGBA::new(r as f32, g as f32, b as f32, a as f32);
|
let rgba = RGBA::new(r as f32, g as f32, b as f32, a as f32);
|
||||||
set_swatch_blocked(&dialog_button, &button_handler, &rgba);
|
set_swatch_blocked(&dialog_button, &button_handler, &rgba);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -153,7 +154,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alpha_dialog_hex_keeps_an_explicit_opaque_byte() {
|
fn alpha_dialog_hex_keeps_an_explicit_opaque_byte() {
|
||||||
let rgba = gtk::gdk::RGBA::new(18.0 / 255.0, 52.0 / 255.0, 86.0 / 255.0, 1.0);
|
let rgba = RGBA::new(18.0 / 255.0, 52.0 / 255.0, 86.0 / 255.0, 1.0);
|
||||||
|
|
||||||
assert_eq!(dialog_hex_with_alpha(&rgba), "#123456FF");
|
assert_eq!(dialog_hex_with_alpha(&rgba), "#123456FF");
|
||||||
assert_eq!(dialog_hex(&rgba), "#123456");
|
assert_eq!(dialog_hex(&rgba), "#123456");
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
use wayscriber::domain::Language;
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
|
use crate::test_env;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability,
|
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability,
|
||||||
@@ -122,14 +127,14 @@ fn overall_status_follows_service_state() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn feedback_tone_follows_the_outcome_in_every_language() {
|
fn feedback_tone_follows_the_outcome_in_every_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::feedback_tone_follows_the_outcome_in_every_language"
|
"::feedback_tone_follows_the_outcome_in_every_language"
|
||||||
),
|
),
|
||||||
|| {
|
|| {
|
||||||
use crate::app::daemon_workflow::{ActionFeedback, DaemonFeedback, StatusFeedback};
|
use crate::app::daemon_workflow::{ActionFeedback, DaemonFeedback, StatusFeedback};
|
||||||
wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese);
|
i18n::set_language(Language::SimplifiedChinese);
|
||||||
let failed = DaemonFeedback::Action(ActionFeedback::Failed("权限不足".into()));
|
let failed = DaemonFeedback::Action(ActionFeedback::Failed("权限不足".into()));
|
||||||
let loaded = DaemonFeedback::Status(StatusFeedback::Loaded);
|
let loaded = DaemonFeedback::Status(StatusFeedback::Loaded);
|
||||||
|
|
||||||
|
|||||||
@@ -92,13 +92,13 @@ fn section_combo_row<O>(
|
|||||||
list: >k::ListBox,
|
list: >k::ListBox,
|
||||||
title: &str,
|
title: &str,
|
||||||
values: Vec<O>,
|
values: Vec<O>,
|
||||||
labels: Vec<String>,
|
labels: &[String],
|
||||||
get: impl Fn(&ConfiguratorApp) -> O + 'static,
|
get: impl Fn(&ConfiguratorApp) -> O + 'static,
|
||||||
to_message: impl Fn(O) -> Message + 'static,
|
to_message: impl Fn(O) -> Message + 'static,
|
||||||
) where
|
) where
|
||||||
O: Copy + PartialEq + 'static,
|
O: Copy + PartialEq + 'static,
|
||||||
{
|
{
|
||||||
let row = combo_row_widget(title, &labels);
|
let row = combo_row_widget(title, labels);
|
||||||
let handler = connect_combo(&row, page.sender(), values.clone(), to_message);
|
let handler = connect_combo(&row, page.sender(), values.clone(), to_message);
|
||||||
list.append(&row);
|
list.append(&row);
|
||||||
page.bind(move |app, _summary| {
|
page.bind(move |app, _summary| {
|
||||||
@@ -191,8 +191,8 @@ fn set_visible_if_changed(widget: &impl IsA<gtk::Widget>, visible: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_if_changed<O: PartialEq>(row: &adw::ComboRow, values: &[O], current: O) {
|
fn select_if_changed<O: PartialEq>(row: &adw::ComboRow, values: &[O], current: &O) {
|
||||||
if let Some(index) = values.iter().position(|value| *value == current) {
|
if let Some(index) = values.iter().position(|value| value == current) {
|
||||||
let index = index as u32;
|
let index = index as u32;
|
||||||
if row.selected() != index {
|
if row.selected() != index {
|
||||||
row.set_selected(index);
|
row.set_selected(index);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub(super) fn build(page: &mut PageBuilder) {
|
|||||||
tr("Color mode"),
|
tr("Color mode"),
|
||||||
tr("A palette name or hex string, or explicit RGB components."),
|
tr("A palette name or hex string, or explicit RGB components."),
|
||||||
COLOR_MODES.to_vec(),
|
COLOR_MODES.to_vec(),
|
||||||
vec![tr("Named color").to_string(), tr("RGB color").to_string()],
|
&[tr("Named color").to_string(), tr("RGB color").to_string()],
|
||||||
|app| app.draft.drawing_color.mode,
|
|app| app.draft.drawing_color.mode,
|
||||||
Message::ColorModeChanged,
|
Message::ColorModeChanged,
|
||||||
);
|
);
|
||||||
@@ -29,7 +29,7 @@ pub(super) fn build(page: &mut PageBuilder) {
|
|||||||
&named,
|
&named,
|
||||||
tr("Named color"),
|
tr("Named color"),
|
||||||
NamedColorOption::list(),
|
NamedColorOption::list(),
|
||||||
named_color_labels(),
|
&named_color_labels(),
|
||||||
|app| app.draft.drawing_color.selected_named,
|
|app| app.draft.drawing_color.selected_named,
|
||||||
Message::NamedColorSelected,
|
Message::NamedColorSelected,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use wayscriber::config;
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
@@ -37,10 +38,10 @@ pub(super) fn build(page: &mut PageBuilder) {
|
|||||||
tr("Eraser mode"),
|
tr("Eraser mode"),
|
||||||
"",
|
"",
|
||||||
EraserModeOption::list(),
|
EraserModeOption::list(),
|
||||||
EraserModeOption::list()
|
&EraserModeOption::list()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|option| option.label().to_string())
|
.map(|option| option.label().to_string())
|
||||||
.collect(),
|
.collect::<Vec<_>>(),
|
||||||
|app| app.draft.drawing_default_eraser_mode,
|
|app| app.draft.drawing_default_eraser_mode,
|
||||||
Message::EraserModeChanged,
|
Message::EraserModeChanged,
|
||||||
)
|
)
|
||||||
@@ -64,7 +65,7 @@ pub(super) fn build(page: &mut PageBuilder) {
|
|||||||
validate_usize_range(
|
validate_usize_range(
|
||||||
&app.draft.drawing_shape_recognition_sensitivity,
|
&app.draft.drawing_shape_recognition_sensitivity,
|
||||||
0,
|
0,
|
||||||
usize::from(wayscriber::config::MAX_SHAPE_RECOGNITION_SENSITIVITY),
|
usize::from(config::MAX_SHAPE_RECOGNITION_SENSITIVITY),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ fn build_drag_button_section(page: &mut PageBuilder, button: DragMouseButton) {
|
|||||||
&list,
|
&list,
|
||||||
field.label(),
|
field.label(),
|
||||||
tools.clone(),
|
tools.clone(),
|
||||||
tool_labels.clone(),
|
&tool_labels,
|
||||||
move |app| drag_tool_for_field(drag_button_config(app, button), field),
|
move |app| drag_tool_for_field(drag_button_config(app, button), field),
|
||||||
move |option| Message::DrawingMouseDragToolChanged(button, field, option),
|
move |option| Message::DrawingMouseDragToolChanged(button, field, option),
|
||||||
);
|
);
|
||||||
@@ -95,7 +95,7 @@ fn build_drag_button_section(page: &mut PageBuilder, button: DragMouseButton) {
|
|||||||
&list,
|
&list,
|
||||||
&trf("{} color", &[&field.label()]),
|
&trf("{} color", &[&field.label()]),
|
||||||
colors.clone(),
|
colors.clone(),
|
||||||
color_labels.clone(),
|
&color_labels,
|
||||||
move |app| drag_color_for_field(drag_button_config(app, button), field),
|
move |app| drag_color_for_field(drag_button_config(app, button), field),
|
||||||
move |option| Message::DrawingMouseDragColorChanged(button, field, option),
|
move |option| Message::DrawingMouseDragColorChanged(button, field, option),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::rc::Rc;
|
|||||||
use relm4::{ComponentSender, adw, gtk};
|
use relm4::{ComponentSender, adw, gtk};
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use gtk::pango::{AttrFontDesc, AttrList, FontDescription};
|
||||||
|
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
use crate::models::{FontStyleOption, FontWeightOption, TextField};
|
use crate::models::{FontStyleOption, FontWeightOption, TextField};
|
||||||
@@ -27,10 +28,10 @@ pub(super) fn build(page: &mut PageBuilder) {
|
|||||||
tr("Font weight"),
|
tr("Font weight"),
|
||||||
"",
|
"",
|
||||||
FontWeightOption::list(),
|
FontWeightOption::list(),
|
||||||
FontWeightOption::list()
|
&FontWeightOption::list()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|option| option.label().to_string())
|
.map(|option| option.label().to_string())
|
||||||
.collect(),
|
.collect::<Vec<_>>(),
|
||||||
|app| app.draft.drawing_font_weight_option,
|
|app| app.draft.drawing_font_weight_option,
|
||||||
Message::FontWeightOptionSelected,
|
Message::FontWeightOptionSelected,
|
||||||
)
|
)
|
||||||
@@ -43,10 +44,10 @@ pub(super) fn build(page: &mut PageBuilder) {
|
|||||||
tr("Font style"),
|
tr("Font style"),
|
||||||
"",
|
"",
|
||||||
FontStyleOption::list(),
|
FontStyleOption::list(),
|
||||||
FontStyleOption::list()
|
&FontStyleOption::list()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|option| option.label().to_string())
|
.map(|option| option.label().to_string())
|
||||||
.collect(),
|
.collect::<Vec<_>>(),
|
||||||
|app| app.draft.drawing_font_style_option,
|
|app| app.draft.drawing_font_style_option,
|
||||||
Message::FontStyleOptionSelected,
|
Message::FontStyleOptionSelected,
|
||||||
);
|
);
|
||||||
@@ -130,7 +131,7 @@ fn build_font_cycle(page: &mut PageBuilder) {
|
|||||||
.map(|row| row.layout)
|
.map(|row| row.layout)
|
||||||
.eq(layouts.iter().copied())
|
.eq(layouts.iter().copied())
|
||||||
{
|
{
|
||||||
rows = rebuild_font_cycle(&list, &families, Rc::clone(&installed), &layouts, &sender);
|
rows = rebuild_font_cycle(&list, &families, &installed, &layouts, &sender);
|
||||||
}
|
}
|
||||||
for (family, row) in app
|
for (family, row) in app
|
||||||
.draft
|
.draft
|
||||||
@@ -177,7 +178,7 @@ struct BoundFontRow {
|
|||||||
fn rebuild_font_cycle(
|
fn rebuild_font_cycle(
|
||||||
list: >k::ListBox,
|
list: >k::ListBox,
|
||||||
families: >k::StringList,
|
families: >k::StringList,
|
||||||
family_names: Rc<[String]>,
|
family_names: &Rc<[String]>,
|
||||||
layouts: &[FontCycleLayout],
|
layouts: &[FontCycleLayout],
|
||||||
sender: &ComponentSender<ConfiguratorApp>,
|
sender: &ComponentSender<ConfiguratorApp>,
|
||||||
) -> Vec<BoundFontRow> {
|
) -> Vec<BoundFontRow> {
|
||||||
@@ -189,7 +190,7 @@ fn rebuild_font_cycle(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|layout| {
|
.map(|layout| {
|
||||||
let (row, refresh) =
|
let (row, refresh) =
|
||||||
build_font_cycle_row(*layout, families, Rc::clone(&family_names), sender);
|
build_font_cycle_row(*layout, families, Rc::clone(family_names), sender);
|
||||||
list.append(&row);
|
list.append(&row);
|
||||||
BoundFontRow {
|
BoundFontRow {
|
||||||
layout: *layout,
|
layout: *layout,
|
||||||
@@ -292,8 +293,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
label.set_label(&family);
|
label.set_label(&family);
|
||||||
let attributes = gtk::pango::AttrList::new();
|
let attributes = AttrList::new();
|
||||||
attributes.insert(gtk::pango::AttrFontDesc::new(&family_preview_font(&family)));
|
attributes.insert(AttrFontDesc::new(&family_preview_font(&family)));
|
||||||
label.set_attributes(Some(&attributes));
|
label.set_attributes(Some(&attributes));
|
||||||
});
|
});
|
||||||
factory
|
factory
|
||||||
@@ -304,8 +305,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory {
|
|||||||
/// Built field by field: `FontDescription::from_string` reads trailing style,
|
/// Built field by field: `FontDescription::from_string` reads trailing style,
|
||||||
/// weight, and stretch words off the family, so "Roboto Condensed" would preview
|
/// weight, and stretch words off the family, so "Roboto Condensed" would preview
|
||||||
/// in Roboto and "Archivo Black" in Archivo.
|
/// in Roboto and "Archivo Black" in Archivo.
|
||||||
fn family_preview_font(family: &str) -> gtk::pango::FontDescription {
|
fn family_preview_font(family: &str) -> FontDescription {
|
||||||
let mut description = gtk::pango::FontDescription::new();
|
let mut description = FontDescription::new();
|
||||||
description.set_family(family);
|
description.set_family(family);
|
||||||
description
|
description
|
||||||
}
|
}
|
||||||
@@ -361,10 +362,13 @@ fn missing_family_note(family: &str) -> Option<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use gtk::pango::{Stretch, Weight};
|
||||||
|
use wayscriber::draw;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn installed() -> String {
|
fn installed() -> String {
|
||||||
wayscriber::draw::system_font_families()
|
draw::system_font_families()
|
||||||
.first()
|
.first()
|
||||||
.expect("at least one family")
|
.expect("at least one family")
|
||||||
.clone()
|
.clone()
|
||||||
@@ -403,8 +407,8 @@ mod tests {
|
|||||||
let description = family_preview_font(family);
|
let description = family_preview_font(family);
|
||||||
|
|
||||||
assert_eq!(description.family().as_deref(), Some(family));
|
assert_eq!(description.family().as_deref(), Some(family));
|
||||||
assert_eq!(description.weight(), gtk::pango::Weight::Normal);
|
assert_eq!(description.weight(), Weight::Normal);
|
||||||
assert_eq!(description.stretch(), gtk::pango::Stretch::Normal);
|
assert_eq!(description.stretch(), Stretch::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use wayscriber::i18n::{tr, trf};
|
|||||||
use relm4::{ComponentSender, adw, gtk};
|
use relm4::{ComponentSender, adw, gtk};
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use gtk::gdk::RGBA;
|
||||||
|
|
||||||
use wayscriber::config::{QUICK_COLOR_RENDER_LIMIT, QuickColorSlot};
|
use wayscriber::config::{QUICK_COLOR_RENDER_LIMIT, QuickColorSlot};
|
||||||
|
|
||||||
@@ -120,10 +121,10 @@ fn quick_color_layouts(app: &ConfiguratorApp) -> Vec<QuickColorLayout> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One quick color row's values: everything the layout deliberately left out.
|
/// One quick color row's values: everything the layout deliberately left out.
|
||||||
struct QuickColorValues<'a> {
|
struct QuickColorValues<'draft> {
|
||||||
label: &'a str,
|
label: &'draft str,
|
||||||
name: &'a str,
|
name: &'draft str,
|
||||||
hex: &'a str,
|
hex: &'draft str,
|
||||||
named: NamedColorOption,
|
named: NamedColorOption,
|
||||||
/// The color the entry resolves to, `None` when it resolves to nothing —
|
/// The color the entry resolves to, `None` when it resolves to nothing —
|
||||||
/// which is also what makes the name field an error.
|
/// which is also what makes the name field an error.
|
||||||
@@ -224,7 +225,7 @@ fn build_quick_color_row(
|
|||||||
tr("Color mode"),
|
tr("Color mode"),
|
||||||
&[tr("Named or hex").to_string(), "RGB".to_string()],
|
&[tr("Named or hex").to_string(), "RGB".to_string()],
|
||||||
);
|
);
|
||||||
select_if_changed(&mode, &COLOR_MODES, layout.mode);
|
select_if_changed(&mode, &COLOR_MODES, &layout.mode);
|
||||||
connect_combo(&mode, sender.clone(), COLOR_MODES.to_vec(), move |value| {
|
connect_combo(&mode, sender.clone(), COLOR_MODES.to_vec(), move |value| {
|
||||||
Message::QuickColorModeChanged(index, value)
|
Message::QuickColorModeChanged(index, value)
|
||||||
});
|
});
|
||||||
@@ -307,7 +308,7 @@ fn build_quick_color_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some((r, g, b, a)) = values.preview {
|
if let Some((r, g, b, a)) = values.preview {
|
||||||
let rgba = gtk::gdk::RGBA::new(r as f32, g as f32, b as f32, a as f32);
|
let rgba = RGBA::new(r as f32, g as f32, b as f32, a as f32);
|
||||||
set_swatch_blocked(&swatch, &swatch_handler, &rgba);
|
set_swatch_blocked(&swatch, &swatch_handler, &rgba);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ mod widgets;
|
|||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
@@ -47,11 +47,11 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
"Record a shortcut or sequence, reset one action, or edit the comma-separated list as text. Filter and sort to review many actions at once.",
|
"Record a shortcut or sequence, reset one action, or edit the comma-separated list as text. Filter and sort to review many actions at once.",
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
for field in fields
|
let tab_fields = fields
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|field| keybinding_tab(*field) == tab)
|
.filter(|field| keybinding_tab(*field) == tab);
|
||||||
{
|
for field in tab_fields {
|
||||||
let row = binding_row(&group, field, sender, &mut bindings, refresh.clone());
|
let row = binding_row(&group, field, sender, &mut bindings, refresh.clone());
|
||||||
slots.push((field, row, group.clone()));
|
slots.push((field, row, group.clone()));
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ fn conflict_banner(
|
|||||||
.child(&row)
|
.child(&row)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let jump_field = std::rc::Rc::new(std::cell::Cell::new(None::<KeybindingField>));
|
let jump_field = Rc::new(Cell::new(None::<KeybindingField>));
|
||||||
{
|
{
|
||||||
let sender = sender.clone();
|
let sender = sender.clone();
|
||||||
let jump_field = jump_field.clone();
|
let jump_field = jump_field.clone();
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use relm4::{ComponentSender, gtk};
|
use relm4::{ComponentSender, gtk};
|
||||||
|
|
||||||
|
use gtk::gdk::{ButtonEvent, EventType, InputSource, Key, ModifierType};
|
||||||
|
use gtk::glib::Propagation;
|
||||||
use gtk::glib::object::IsA;
|
use gtk::glib::object::IsA;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
@@ -151,25 +153,24 @@ fn attach_key_controller(popover: >k::Popover, sender: &ComponentSender<Config
|
|||||||
let controller = gtk::EventControllerKey::new();
|
let controller = gtk::EventControllerKey::new();
|
||||||
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||||
{
|
{
|
||||||
let sender = sender.clone();
|
|
||||||
let pressed = pressed.clone();
|
let pressed = pressed.clone();
|
||||||
controller.connect_key_pressed(move |_, key, keycode, modifiers| {
|
controller.connect_key_pressed(move |_, key, keycode, modifiers| {
|
||||||
// GTK repeats key-pressed for auto-repeat. Sequence recording
|
// GTK repeats key-pressed for auto-repeat. Sequence recording
|
||||||
// would otherwise append Ctrl+K > Ctrl+K > Ctrl+K from a hold.
|
// would otherwise append Ctrl+K > Ctrl+K > Ctrl+K from a hold.
|
||||||
if !pressed.borrow_mut().insert(keycode) {
|
if !pressed.borrow_mut().insert(keycode) {
|
||||||
return gtk::glib::Propagation::Stop;
|
return Propagation::Stop;
|
||||||
}
|
}
|
||||||
let keyval = gdk_keyval(key);
|
let keyval = gdk_keyval(key);
|
||||||
let recorded = KeyboardModifiers {
|
let recorded = KeyboardModifiers {
|
||||||
ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK),
|
ctrl: modifiers.contains(ModifierType::CONTROL_MASK),
|
||||||
shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK),
|
shift: modifiers.contains(ModifierType::SHIFT_MASK),
|
||||||
alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK),
|
alt: modifiers.contains(ModifierType::ALT_MASK),
|
||||||
super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK)
|
super_held: modifiers.contains(ModifierType::SUPER_MASK)
|
||||||
|| modifiers.contains(gtk::gdk::ModifierType::META_MASK)
|
|| modifiers.contains(ModifierType::META_MASK)
|
||||||
|| modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK),
|
|| modifiers.contains(ModifierType::HYPER_MASK),
|
||||||
};
|
};
|
||||||
sender.input(Message::ShortcutRecorderKey(keyval, recorded));
|
sender.input(Message::ShortcutRecorderKey(keyval, recorded));
|
||||||
gtk::glib::Propagation::Stop
|
Propagation::Stop
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -215,30 +216,30 @@ fn attach_button_controller(popover: >k::Popover, sender: &ComponentSender<Con
|
|||||||
let controller = gtk::EventControllerLegacy::new();
|
let controller = gtk::EventControllerLegacy::new();
|
||||||
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||||
controller.connect_event(move |_, event| {
|
controller.connect_event(move |_, event| {
|
||||||
if event.event_type() != gtk::gdk::EventType::ButtonPress {
|
if event.event_type() != EventType::ButtonPress {
|
||||||
return gtk::glib::Propagation::Proceed;
|
return Propagation::Proceed;
|
||||||
}
|
}
|
||||||
let Some(button_event) = event.downcast_ref::<gtk::gdk::ButtonEvent>() else {
|
let Some(button_event) = event.downcast_ref::<ButtonEvent>() else {
|
||||||
return gtk::glib::Propagation::Proceed;
|
return Propagation::Proceed;
|
||||||
};
|
};
|
||||||
let kind = event
|
let kind = event
|
||||||
.device()
|
.device()
|
||||||
.map(|device| match device.source() {
|
.map(|device| match device.source() {
|
||||||
gtk::gdk::InputSource::Mouse
|
InputSource::Mouse | InputSource::Touchpad | InputSource::Trackpoint => {
|
||||||
| gtk::gdk::InputSource::Touchpad
|
RecorderDeviceKind::Mouse
|
||||||
| gtk::gdk::InputSource::Trackpoint => RecorderDeviceKind::Mouse,
|
}
|
||||||
gtk::gdk::InputSource::Pen => RecorderDeviceKind::Pen,
|
InputSource::Pen => RecorderDeviceKind::Pen,
|
||||||
_ => RecorderDeviceKind::Other,
|
_ => RecorderDeviceKind::Other,
|
||||||
})
|
})
|
||||||
.unwrap_or(RecorderDeviceKind::Other);
|
.unwrap_or(RecorderDeviceKind::Other);
|
||||||
let modifiers = event.modifier_state();
|
let modifiers = event.modifier_state();
|
||||||
let recorded = KeyboardModifiers {
|
let recorded = KeyboardModifiers {
|
||||||
ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK),
|
ctrl: modifiers.contains(ModifierType::CONTROL_MASK),
|
||||||
shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK),
|
shift: modifiers.contains(ModifierType::SHIFT_MASK),
|
||||||
alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK),
|
alt: modifiers.contains(ModifierType::ALT_MASK),
|
||||||
super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK)
|
super_held: modifiers.contains(ModifierType::SUPER_MASK)
|
||||||
|| modifiers.contains(gtk::gdk::ModifierType::META_MASK)
|
|| modifiers.contains(ModifierType::META_MASK)
|
||||||
|| modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK),
|
|| modifiers.contains(ModifierType::HYPER_MASK),
|
||||||
};
|
};
|
||||||
// Primary clicks must reach Finish / Remove Last Step / Cancel. Stop
|
// Primary clicks must reach Finish / Remove Last Step / Cancel. Stop
|
||||||
// only bindable auxiliary mouse or stylus barrel events.
|
// only bindable auxiliary mouse or stylus barrel events.
|
||||||
@@ -249,15 +250,15 @@ fn attach_button_controller(popover: >k::Popover, sender: &ComponentSender<Con
|
|||||||
kind,
|
kind,
|
||||||
recorded,
|
recorded,
|
||||||
));
|
));
|
||||||
gtk::glib::Propagation::Stop
|
Propagation::Stop
|
||||||
}
|
}
|
||||||
RecordedDevice::Unsupported { .. } => gtk::glib::Propagation::Proceed,
|
RecordedDevice::Unsupported { .. } => Propagation::Proceed,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
popover.add_controller(controller);
|
popover.add_controller(controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gdk_keyval(key: gtk::gdk::Key) -> u32 {
|
fn gdk_keyval(key: Key) -> u32 {
|
||||||
use gtk::glib::translate::IntoGlib;
|
use gtk::glib::translate::IntoGlib;
|
||||||
key.into_glib()
|
key.into_glib()
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
use relm4::{adw, gtk};
|
use relm4::{adw, gtk};
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use gtk::pango::EllipsizeMode;
|
||||||
use wayscriber::config::Shortcut;
|
use wayscriber::config::Shortcut;
|
||||||
|
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
@@ -82,7 +83,7 @@ pub(super) fn binding_row(
|
|||||||
|
|
||||||
let compact_summary = gtk::Label::builder()
|
let compact_summary = gtk::Label::builder()
|
||||||
.css_classes(["dim-label"])
|
.css_classes(["dim-label"])
|
||||||
.ellipsize(gtk::pango::EllipsizeMode::End)
|
.ellipsize(EllipsizeMode::End)
|
||||||
.max_width_chars(24)
|
.max_width_chars(24)
|
||||||
.xalign(1.0)
|
.xalign(1.0)
|
||||||
.build();
|
.build();
|
||||||
@@ -162,16 +163,11 @@ pub(super) fn binding_row(
|
|||||||
|
|
||||||
let recorder = RecorderPopover::build(&row, field, sender);
|
let recorder = RecorderPopover::build(&row, field, sender);
|
||||||
let text_editor = TextEditorPopover::build(&row, field, sender);
|
let text_editor = TextEditorPopover::build(&row, field, sender);
|
||||||
let seen_chips = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
|
let seen_chips = Rc::new(RefCell::new(Vec::<String>::new()));
|
||||||
let seen_badges = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
|
let seen_badges = Rc::new(RefCell::new(Vec::<String>::new()));
|
||||||
let compact_mode = std::rc::Rc::new(std::cell::Cell::new(false));
|
let compact_mode = Rc::new(Cell::new(false));
|
||||||
let last_focus_serial = std::rc::Rc::new(std::cell::Cell::new(0));
|
let last_focus_serial = Rc::new(Cell::new(0));
|
||||||
{
|
{
|
||||||
let compact = compact.clone();
|
|
||||||
let inline_host = inline_host.clone();
|
|
||||||
let compact_mode = compact_mode.clone();
|
|
||||||
let editor_popover = editor_popover.clone();
|
|
||||||
let editor = editor.clone();
|
|
||||||
watch_compact(&row, move |is_compact| {
|
watch_compact(&row, move |is_compact| {
|
||||||
compact_mode.set(is_compact);
|
compact_mode.set(is_compact);
|
||||||
set_visible(&compact, is_compact);
|
set_visible(&compact, is_compact);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use wayscriber::i18n::tr;
|
|||||||
|
|
||||||
use relm4::{ComponentSender, gtk};
|
use relm4::{ComponentSender, gtk};
|
||||||
|
|
||||||
|
use gtk::glib::SignalHandlerId;
|
||||||
use gtk::glib::object::IsA;
|
use gtk::glib::object::IsA;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ use super::widgets::{field_canceled, set_accessible_label, set_label, unparent_o
|
|||||||
pub(super) struct TextEditorPopover {
|
pub(super) struct TextEditorPopover {
|
||||||
pub popover: gtk::Popover,
|
pub popover: gtk::Popover,
|
||||||
entry: gtk::Entry,
|
entry: gtk::Entry,
|
||||||
entry_handler: gtk::glib::SignalHandlerId,
|
entry_handler: SignalHandlerId,
|
||||||
error: gtk::Label,
|
error: gtk::Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
|
||||||
use relm4::{gtk, prelude::*};
|
use relm4::gtk;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ fn actions_bar(
|
|||||||
row.append(&reset_all);
|
row.append(&reset_all);
|
||||||
row.append(&review);
|
row.append(&review);
|
||||||
|
|
||||||
let sort_for_bind = sort.clone();
|
let sort_for_bind = sort;
|
||||||
bindings.push(Box::new(move |app, _summary| {
|
bindings.push(Box::new(move |app, _summary| {
|
||||||
let selected = ShortcutManagerSort::ALL
|
let selected = ShortcutManagerSort::ALL
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use relm4::{ComponentSender, gtk};
|
use relm4::{ComponentSender, gtk};
|
||||||
|
|
||||||
|
use gtk::accessible::Property;
|
||||||
use gtk::glib::object::IsA;
|
use gtk::glib::object::IsA;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::{AccessibleExt, *};
|
||||||
|
|
||||||
use crate::messages::Message;
|
use crate::messages::Message;
|
||||||
use crate::models::KeybindingField;
|
use crate::models::KeybindingField;
|
||||||
@@ -20,8 +21,8 @@ pub(super) fn icon_button(icon: &str, tooltip: &str) -> gtk::Button {
|
|||||||
button
|
button
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_accessible_label(widget: &impl gtk::prelude::AccessibleExt, label: &str) {
|
pub(super) fn set_accessible_label(widget: &impl AccessibleExt, label: &str) {
|
||||||
widget.update_property(&[gtk::accessible::Property::Label(label)]);
|
widget.update_property(&[Property::Label(label)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the widget's own visibility flag, never `is_visible`: a widget
|
/// Writes the widget's own visibility flag, never `is_visible`: a widget
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ mod session;
|
|||||||
mod tablet;
|
mod tablet;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
use relm4::{adw, gtk};
|
use relm4::{adw, gtk};
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@ use super::state::ConfiguratorApp;
|
|||||||
pub(crate) type Binding = Box<dyn FnMut(&ConfiguratorApp, &AppSearchSummary)>;
|
pub(crate) type Binding = Box<dyn FnMut(&ConfiguratorApp, &AppSearchSummary)>;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static REPLACING_PAGES: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
static REPLACING_PAGES: Cell<bool> = const { Cell::new(false) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks the pages as being replaced for as long as it lives.
|
/// Marks the pages as being replaced for as long as it lives.
|
||||||
@@ -76,7 +78,7 @@ impl Drop for ReplacingPages {
|
|||||||
|
|
||||||
/// Whether the pages are being replaced right now; see [`ReplacingPages`].
|
/// Whether the pages are being replaced right now; see [`ReplacingPages`].
|
||||||
pub(crate) fn replacing_pages() -> bool {
|
pub(crate) fn replacing_pages() -> bool {
|
||||||
REPLACING_PAGES.with(std::cell::Cell::get)
|
REPLACING_PAGES.with(Cell::get)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes editable text the model owns without reporting it as a user edit.
|
/// Writes editable text the model owns without reporting it as a user edit.
|
||||||
@@ -349,7 +351,7 @@ impl PageBuilder {
|
|||||||
title: &str,
|
title: &str,
|
||||||
subtitle: &str,
|
subtitle: &str,
|
||||||
values: Vec<O>,
|
values: Vec<O>,
|
||||||
labels: Vec<String>,
|
labels: &[String],
|
||||||
get: impl Fn(&ConfiguratorApp) -> O + 'static,
|
get: impl Fn(&ConfiguratorApp) -> O + 'static,
|
||||||
to_message: impl Fn(O) -> Message + 'static,
|
to_message: impl Fn(O) -> Message + 'static,
|
||||||
) -> &mut Self
|
) -> &mut Self
|
||||||
@@ -376,7 +378,7 @@ impl PageBuilder {
|
|||||||
where
|
where
|
||||||
O: Copy + PartialEq + 'static,
|
O: Copy + PartialEq + 'static,
|
||||||
{
|
{
|
||||||
let row = self.add_combo_row(title, subtitle, values, labels, get, to_message);
|
let row = self.add_combo_row(title, subtitle, values, &labels, get, to_message);
|
||||||
row.add_prefix(>k::Image::from_icon_name(icon_name));
|
row.add_prefix(>k::Image::from_icon_name(icon_name));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -386,7 +388,7 @@ impl PageBuilder {
|
|||||||
title: &str,
|
title: &str,
|
||||||
subtitle: &str,
|
subtitle: &str,
|
||||||
values: Vec<O>,
|
values: Vec<O>,
|
||||||
labels: Vec<String>,
|
labels: &[String],
|
||||||
get: impl Fn(&ConfiguratorApp) -> O + 'static,
|
get: impl Fn(&ConfiguratorApp) -> O + 'static,
|
||||||
to_message: impl Fn(O) -> Message + 'static,
|
to_message: impl Fn(O) -> Message + 'static,
|
||||||
) -> adw::ComboRow
|
) -> adw::ComboRow
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
buffer_metadata.label,
|
buffer_metadata.label,
|
||||||
buffer_metadata.help,
|
buffer_metadata.help,
|
||||||
buffer_counts,
|
buffer_counts,
|
||||||
buffer_labels,
|
&buffer_labels,
|
||||||
|app| app.draft.performance_buffer_count,
|
|app| app.draft.performance_buffer_count,
|
||||||
Message::BufferCountChanged,
|
Message::BufferCountChanged,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Visible slots"),
|
tr("Visible slots"),
|
||||||
tr("How many preset slots the toolbar offers."),
|
tr("How many preset slots the toolbar offers."),
|
||||||
counts,
|
counts,
|
||||||
count_labels,
|
&count_labels,
|
||||||
|app| app.draft.presets.slot_count,
|
|app| app.draft.presets.slot_count,
|
||||||
Message::PresetSlotCountChanged,
|
Message::PresetSlotCountChanged,
|
||||||
);
|
);
|
||||||
@@ -187,7 +187,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
|
|||||||
rows.combo_row(
|
rows.combo_row(
|
||||||
tr("Tool"),
|
tr("Tool"),
|
||||||
tools,
|
tools,
|
||||||
tool_labels,
|
&tool_labels,
|
||||||
move |app| app.draft.presets.slot(slot).map(|draft| draft.tool),
|
move |app| app.draft.presets.slot(slot).map(|draft| draft.tool),
|
||||||
Message::PresetToolChanged,
|
Message::PresetToolChanged,
|
||||||
);
|
);
|
||||||
@@ -215,7 +215,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
|
|||||||
rows.combo_row(
|
rows.combo_row(
|
||||||
tr("Eraser kind"),
|
tr("Eraser kind"),
|
||||||
kinds,
|
kinds,
|
||||||
kind_labels,
|
&kind_labels,
|
||||||
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_kind),
|
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_kind),
|
||||||
Message::PresetEraserKindChanged,
|
Message::PresetEraserKindChanged,
|
||||||
);
|
);
|
||||||
@@ -224,7 +224,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) {
|
|||||||
rows.combo_row(
|
rows.combo_row(
|
||||||
tr("Eraser mode"),
|
tr("Eraser mode"),
|
||||||
modes,
|
modes,
|
||||||
mode_labels,
|
&mode_labels,
|
||||||
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_mode),
|
move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_mode),
|
||||||
Message::PresetEraserModeChanged,
|
Message::PresetEraserModeChanged,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub(super) fn build_color_rows(rows: &mut SlotBuilder<'_>, slot: usize) {
|
|||||||
let mode_row = rows.combo_row(
|
let mode_row = rows.combo_row(
|
||||||
tr("Color mode"),
|
tr("Color mode"),
|
||||||
vec![ColorMode::Named, ColorMode::Rgb],
|
vec![ColorMode::Named, ColorMode::Rgb],
|
||||||
vec![tr("Named").to_string(), "RGB".to_string()],
|
&[tr("Named").to_string(), "RGB".to_string()],
|
||||||
move |app| app.draft.presets.slot(slot).map(|draft| draft.color.mode),
|
move |app| app.draft.presets.slot(slot).map(|draft| draft.color.mode),
|
||||||
Message::PresetColorModeChanged,
|
Message::PresetColorModeChanged,
|
||||||
);
|
);
|
||||||
@@ -52,7 +52,7 @@ pub(super) fn build_color_rows(rows: &mut SlotBuilder<'_>, slot: usize) {
|
|||||||
let named_row = rows.combo_row(
|
let named_row = rows.combo_row(
|
||||||
tr("Named color"),
|
tr("Named color"),
|
||||||
named,
|
named,
|
||||||
named_labels,
|
&named_labels,
|
||||||
move |app| {
|
move |app| {
|
||||||
app.draft
|
app.draft
|
||||||
.presets
|
.presets
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use super::super::{PageBuilder, set_selected_blocked, set_text_blocked};
|
|||||||
/// Mirrors the [`PageBuilder`] row helpers, but adds rows to the slot's
|
/// Mirrors the [`PageBuilder`] row helpers, but adds rows to the slot's
|
||||||
/// expander and reads the slot out of the model, so a slot the draft does
|
/// expander and reads the slot out of the model, so a slot the draft does
|
||||||
/// not hold simply leaves its rows alone.
|
/// not hold simply leaves its rows alone.
|
||||||
pub(super) struct SlotBuilder<'a> {
|
pub(super) struct SlotBuilder<'page> {
|
||||||
pub(super) page: &'a mut PageBuilder,
|
pub(super) page: &'page mut PageBuilder,
|
||||||
pub(super) expander: adw::ExpanderRow,
|
pub(super) expander: adw::ExpanderRow,
|
||||||
pub(super) slot: usize,
|
pub(super) slot: usize,
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ impl SlotBuilder<'_> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
title: &str,
|
title: &str,
|
||||||
values: Vec<O>,
|
values: Vec<O>,
|
||||||
labels: Vec<String>,
|
labels: &[String],
|
||||||
get: impl Fn(&ConfiguratorApp) -> Option<O> + 'static,
|
get: impl Fn(&ConfiguratorApp) -> Option<O> + 'static,
|
||||||
to_message: impl Fn(usize, O) -> Message + 'static,
|
to_message: impl Fn(usize, O) -> Message + 'static,
|
||||||
) -> adw::ComboRow
|
) -> adw::ComboRow
|
||||||
@@ -117,7 +117,7 @@ impl SlotBuilder<'_> {
|
|||||||
) -> adw::ComboRow {
|
) -> adw::ComboRow {
|
||||||
let values = OverrideOption::list();
|
let values = OverrideOption::list();
|
||||||
let labels = labels_of(&values, OverrideOption::label);
|
let labels = labels_of(&values, OverrideOption::label);
|
||||||
self.combo_row(title, values, labels, get, move |slot, value| {
|
self.combo_row(title, values, &labels, get, move |slot, value| {
|
||||||
Message::PresetToggleOptionChanged(slot, field, value)
|
Message::PresetToggleOptionChanged(slot, field, value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Canvas export profile"),
|
tr("Canvas export profile"),
|
||||||
"",
|
"",
|
||||||
export_options,
|
export_options,
|
||||||
export_labels,
|
&export_labels,
|
||||||
|app| app.draft.render_profiles.export,
|
|app| app.draft.render_profiles.export,
|
||||||
Message::RenderProfileExportChanged,
|
Message::RenderProfileExportChanged,
|
||||||
);
|
);
|
||||||
@@ -279,15 +279,15 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec<Sec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One profile row's values: everything the layout deliberately left out.
|
/// One profile row's values: everything the layout deliberately left out.
|
||||||
struct ProfileValues<'a> {
|
struct ProfileValues<'draft> {
|
||||||
id: &'a str,
|
id: &'draft str,
|
||||||
name: &'a str,
|
name: &'draft str,
|
||||||
/// One entry per mapping row, in mapping order.
|
/// One entry per mapping row, in mapping order.
|
||||||
mappings: Vec<MappingValues<'a>>,
|
mappings: Vec<MappingValues<'draft>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One mapping row's two editing hex fields.
|
/// One mapping row's two editing hex fields.
|
||||||
struct MappingValues<'a> {
|
struct MappingValues<'draft> {
|
||||||
from: &'a str,
|
from: &'draft str,
|
||||||
to: &'a str,
|
to: &'draft str,
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ use wayscriber::i18n::tr;
|
|||||||
use super::rows::{connect_button, plain_row, row_content_box, side_label};
|
use super::rows::{connect_button, plain_row, row_content_box, side_label};
|
||||||
use relm4::{ComponentSender, gtk};
|
use relm4::{ComponentSender, gtk};
|
||||||
|
|
||||||
|
use gtk::gdk::RGBA;
|
||||||
use gtk::glib::SignalHandlerId;
|
use gtk::glib::SignalHandlerId;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ impl ColorField {
|
|||||||
// rather than flashing it to black on every keystroke.
|
// rather than flashing it to black on every keystroke.
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let rgba = gtk::gdk::RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0);
|
let rgba = RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0);
|
||||||
set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba);
|
set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,12 +208,12 @@ fn item_card(
|
|||||||
|
|
||||||
let clear_was_armed = confirm.get_visible();
|
let clear_was_armed = confirm.get_visible();
|
||||||
let answer_has_focus = confirm_button.has_focus() || cancel_button.has_focus();
|
let answer_has_focus = confirm_button.has_focus() || cancel_button.has_focus();
|
||||||
let focus_after_refresh = clear_focus_after_refresh(
|
let focus_after_refresh = clear_focus_after_refresh(ClearConfirmTransition {
|
||||||
clear_was_armed,
|
was_armed: clear_was_armed,
|
||||||
values.clear_armed,
|
is_armed: values.clear_armed,
|
||||||
values.clear_enabled,
|
clear_enabled: values.clear_enabled,
|
||||||
answer_has_focus,
|
answer_has_focus,
|
||||||
);
|
});
|
||||||
if focus_after_refresh == ClearFocusTarget::Catalog {
|
if focus_after_refresh == ClearFocusTarget::Catalog {
|
||||||
// Confirm enters the busy state and disables the row's actions.
|
// Confirm enters the busy state and disables the row's actions.
|
||||||
// Temporarily make the stable page target focusable and move there
|
// Temporarily make the stable page target focusable and move there
|
||||||
@@ -242,10 +242,10 @@ fn item_card(
|
|||||||
CatalogRow { card, refresh }
|
CatalogRow { card, refresh }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InputRow<'a> {
|
struct InputRow<'text> {
|
||||||
id: &'a str,
|
id: &'text str,
|
||||||
placeholder: &'a str,
|
placeholder: &'text str,
|
||||||
button_label: &'a str,
|
button_label: &'text str,
|
||||||
on_input: fn(String, String) -> Message,
|
on_input: fn(String, String) -> Message,
|
||||||
request: Message,
|
request: Message,
|
||||||
}
|
}
|
||||||
@@ -293,12 +293,27 @@ enum ClearFocusTarget {
|
|||||||
Catalog,
|
Catalog,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_focus_after_refresh(
|
/// The clear confirmation's state across one refresh.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
struct ClearConfirmTransition {
|
||||||
|
/// The confirmation was showing before this refresh.
|
||||||
was_armed: bool,
|
was_armed: bool,
|
||||||
|
/// The confirmation shows after this refresh.
|
||||||
is_armed: bool,
|
is_armed: bool,
|
||||||
|
/// The clear action stays enabled after this refresh.
|
||||||
clear_enabled: bool,
|
clear_enabled: bool,
|
||||||
|
/// Keyboard focus is on the confirm or cancel answer.
|
||||||
answer_has_focus: bool,
|
answer_has_focus: bool,
|
||||||
) -> ClearFocusTarget {
|
}
|
||||||
|
|
||||||
|
fn clear_focus_after_refresh(transition: ClearConfirmTransition) -> ClearFocusTarget {
|
||||||
|
let ClearConfirmTransition {
|
||||||
|
was_armed,
|
||||||
|
is_armed,
|
||||||
|
clear_enabled,
|
||||||
|
answer_has_focus,
|
||||||
|
} = transition;
|
||||||
|
|
||||||
if is_armed && !was_armed {
|
if is_armed && !was_armed {
|
||||||
return ClearFocusTarget::Confirm;
|
return ClearFocusTarget::Confirm;
|
||||||
}
|
}
|
||||||
@@ -319,22 +334,38 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn clear_confirmation_focus_has_enabled_targets_for_every_transition() {
|
fn clear_confirmation_focus_has_enabled_targets_for_every_transition() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clear_focus_after_refresh(false, true, true, false),
|
clear_focus_after_refresh(ClearConfirmTransition {
|
||||||
|
is_armed: true,
|
||||||
|
clear_enabled: true,
|
||||||
|
..ClearConfirmTransition::default()
|
||||||
|
}),
|
||||||
ClearFocusTarget::Confirm,
|
ClearFocusTarget::Confirm,
|
||||||
"arming focuses the newly revealed confirmation"
|
"arming focuses the newly revealed confirmation"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clear_focus_after_refresh(true, false, true, true),
|
clear_focus_after_refresh(ClearConfirmTransition {
|
||||||
|
was_armed: true,
|
||||||
|
clear_enabled: true,
|
||||||
|
answer_has_focus: true,
|
||||||
|
..ClearConfirmTransition::default()
|
||||||
|
}),
|
||||||
ClearFocusTarget::ClearAction,
|
ClearFocusTarget::ClearAction,
|
||||||
"cancel returns to the restored clear action"
|
"cancel returns to the restored clear action"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clear_focus_after_refresh(true, false, false, true),
|
clear_focus_after_refresh(ClearConfirmTransition {
|
||||||
|
was_armed: true,
|
||||||
|
answer_has_focus: true,
|
||||||
|
..ClearConfirmTransition::default()
|
||||||
|
}),
|
||||||
ClearFocusTarget::Catalog,
|
ClearFocusTarget::Catalog,
|
||||||
"confirm moves away from the row actions disabled by busy state"
|
"confirm moves away from the row actions disabled by busy state"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clear_focus_after_refresh(true, false, false, false),
|
clear_focus_after_refresh(ClearConfirmTransition {
|
||||||
|
was_armed: true,
|
||||||
|
..ClearConfirmTransition::default()
|
||||||
|
}),
|
||||||
ClearFocusTarget::Unchanged,
|
ClearFocusTarget::Unchanged,
|
||||||
"a pointer-triggered transition does not steal keyboard focus"
|
"a pointer-triggered transition does not steal keyboard focus"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
|
use crate::test_env;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::app::state::PendingConfirmation;
|
use crate::app::state::PendingConfirmation;
|
||||||
use crate::models::session::SessionArtifactSummary;
|
use crate::models::session::SessionArtifactSummary;
|
||||||
@@ -182,7 +186,7 @@ fn catalog_focus_fallback_leaves_the_tab_chain_when_busy_work_finishes() {
|
|||||||
/// chosen when the layout is drawn, and a new language rebuilds the card.
|
/// chosen when the layout is drawn, and a new language rebuilds the card.
|
||||||
#[test]
|
#[test]
|
||||||
fn card_labels_follow_the_language_the_list_is_drawn_in() {
|
fn card_labels_follow_the_language_the_list_is_drawn_in() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::card_labels_follow_the_language_the_list_is_drawn_in"
|
"::card_labels_follow_the_language_the_list_is_drawn_in"
|
||||||
@@ -193,7 +197,7 @@ fn card_labels_follow_the_language_the_list_is_drawn_in() {
|
|||||||
let english = catalog_layout(&app);
|
let english = catalog_layout(&app);
|
||||||
assert_eq!(english.items[0].last_opened_label, "Never");
|
assert_eq!(english.items[0].last_opened_label, "Never");
|
||||||
|
|
||||||
wayscriber::i18n::set_language(Language::SimplifiedChinese);
|
i18n::set_language(Language::SimplifiedChinese);
|
||||||
let chinese = catalog_layout(&app);
|
let chinese = catalog_layout(&app);
|
||||||
|
|
||||||
assert_eq!(chinese.items[0].last_opened_label, "从不");
|
assert_eq!(chinese.items[0].last_opened_label, "从不");
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ pub(super) fn add(page: &mut PageBuilder) {
|
|||||||
tr("Storage mode"),
|
tr("Storage mode"),
|
||||||
"",
|
"",
|
||||||
SessionStorageModeOption::list(),
|
SessionStorageModeOption::list(),
|
||||||
option_labels(SessionStorageModeOption::list(), |option| option.label()),
|
&option_labels(&SessionStorageModeOption::list(), |option| option.label()),
|
||||||
|app| app.draft.session_storage_mode,
|
|app| app.draft.session_storage_mode,
|
||||||
Message::SessionStorageModeChanged,
|
Message::SessionStorageModeChanged,
|
||||||
);
|
);
|
||||||
@@ -65,7 +65,7 @@ pub(super) fn add(page: &mut PageBuilder) {
|
|||||||
tr("Compression"),
|
tr("Compression"),
|
||||||
"",
|
"",
|
||||||
SessionCompressionOption::list(),
|
SessionCompressionOption::list(),
|
||||||
option_labels(SessionCompressionOption::list(), |option| option.label()),
|
&option_labels(&SessionCompressionOption::list(), |option| option.label()),
|
||||||
|app| app.draft.session_compression,
|
|app| app.draft.session_compression,
|
||||||
Message::SessionCompressionChanged,
|
Message::SessionCompressionChanged,
|
||||||
)
|
)
|
||||||
@@ -131,7 +131,7 @@ pub(super) fn add(page: &mut PageBuilder) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn option_labels<O: Copy>(options: Vec<O>, label: impl Fn(&O) -> &'static str) -> Vec<String> {
|
fn option_labels<O: Copy>(options: &[O], label: impl Fn(&O) -> &'static str) -> Vec<String> {
|
||||||
options
|
options
|
||||||
.iter()
|
.iter()
|
||||||
.map(|option| label(option).to_string())
|
.map(|option| label(option).to_string())
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ use super::{BuiltPage, PageBuilder};
|
|||||||
|
|
||||||
pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
||||||
let edit_modes = PressureThicknessEditModeOption::list();
|
let edit_modes = PressureThicknessEditModeOption::list();
|
||||||
let edit_mode_labels = edit_modes
|
let edit_mode_labels: Vec<String> = edit_modes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|mode| mode.label().to_string())
|
.map(|mode| mode.label().to_string())
|
||||||
.collect();
|
.collect();
|
||||||
let entry_modes = PressureThicknessEntryModeOption::list();
|
let entry_modes = PressureThicknessEntryModeOption::list();
|
||||||
let entry_mode_labels = entry_modes
|
let entry_mode_labels: Vec<String> = entry_modes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|mode| mode.label().to_string())
|
.map(|mode| mode.label().to_string())
|
||||||
.collect();
|
.collect();
|
||||||
@@ -82,7 +82,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Pressure thickness edit mode"),
|
tr("Pressure thickness edit mode"),
|
||||||
"",
|
"",
|
||||||
edit_modes,
|
edit_modes,
|
||||||
edit_mode_labels,
|
&edit_mode_labels,
|
||||||
|app| app.draft.tablet_pressure_thickness_edit_mode,
|
|app| app.draft.tablet_pressure_thickness_edit_mode,
|
||||||
Message::TabletPressureEditModeChanged,
|
Message::TabletPressureEditModeChanged,
|
||||||
)
|
)
|
||||||
@@ -90,7 +90,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Pressure thickness entry mode"),
|
tr("Pressure thickness entry mode"),
|
||||||
"",
|
"",
|
||||||
entry_modes,
|
entry_modes,
|
||||||
entry_mode_labels,
|
&entry_mode_labels,
|
||||||
|app| app.draft.tablet_pressure_thickness_entry_mode,
|
|app| app.draft.tablet_pressure_thickness_entry_mode,
|
||||||
Message::TabletPressureEntryModeChanged,
|
Message::TabletPressureEntryModeChanged,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use wayscriber::domain::Language;
|
use wayscriber::domain::Language;
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
|
use crate::test_env;
|
||||||
|
|
||||||
use super::validate_u32_range;
|
use super::validate_u32_range;
|
||||||
|
|
||||||
@@ -22,10 +25,10 @@ fn an_invalid_whole_number_names_the_actual_bounds() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_bounds_survive_translation() {
|
fn the_bounds_survive_translation() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(module_path!(), "::the_bounds_survive_translation"),
|
concat!(module_path!(), "::the_bounds_survive_translation"),
|
||||||
|| {
|
|| {
|
||||||
wayscriber::i18n::set_language(Language::SimplifiedChinese);
|
i18n::set_language(Language::SimplifiedChinese);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
validate_u32_range("256", 0, 255).as_deref(),
|
validate_u32_range("256", 0, 255).as_deref(),
|
||||||
Some("请输入 0 到 255 之间的整数。")
|
Some("请输入 0 到 255 之间的整数。")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Theme"),
|
tr("Theme"),
|
||||||
tr("\"Auto\" uses dark. \"Light\" supports the command palette, including shortcut editing and tooltips; other popups still use dark."),
|
tr("\"Auto\" uses dark. \"Light\" supports the command palette, including shortcut editing and tooltips; other popups still use dark."),
|
||||||
themes,
|
themes,
|
||||||
theme_labels,
|
&theme_labels,
|
||||||
|app| app.draft.ui_theme,
|
|app| app.draft.ui_theme,
|
||||||
Message::UiThemeChanged,
|
Message::UiThemeChanged,
|
||||||
)
|
)
|
||||||
@@ -40,7 +40,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Reduced motion"),
|
tr("Reduced motion"),
|
||||||
tr("\"On\" disables UI animations. \"Auto\" follows the system preference in a future release and keeps full motion for now."),
|
tr("\"On\" disables UI animations. \"Auto\" follows the system preference in a future release and keeps full motion for now."),
|
||||||
motions,
|
motions,
|
||||||
motion_labels,
|
&motion_labels,
|
||||||
|app| app.draft.ui_reduced_motion,
|
|app| app.draft.ui_reduced_motion,
|
||||||
Message::UiReducedMotionChanged,
|
Message::UiReducedMotionChanged,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Input source"),
|
tr("Input source"),
|
||||||
tr("\"Overlay only\" shows what Wayscriber itself receives. \"System-wide\" also shows input that goes to other apps; it needs a build with the input-monitor feature and read access to /dev/input (usually `input` group membership), and it sees every keystroke on the seat - including passwords typed elsewhere."),
|
tr("\"Overlay only\" shows what Wayscriber itself receives. \"System-wide\" also shows input that goes to other apps; it needs a build with the input-monitor feature and read access to /dev/input (usually `input` group membership), and it sees every keystroke on the seat - including passwords typed elsewhere."),
|
||||||
modes,
|
modes,
|
||||||
mode_labels,
|
&mode_labels,
|
||||||
|app| app.draft.input_hud_mode,
|
|app| app.draft.input_hud_mode,
|
||||||
Message::InputHudModeChanged,
|
Message::InputHudModeChanged,
|
||||||
)
|
)
|
||||||
@@ -38,7 +38,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Screen position"),
|
tr("Screen position"),
|
||||||
"",
|
"",
|
||||||
positions,
|
positions,
|
||||||
position_labels,
|
&position_labels,
|
||||||
|app| app.draft.input_hud_position,
|
|app| app.draft.input_hud_position,
|
||||||
Message::InputHudPositionChanged,
|
Message::InputHudPositionChanged,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Top toolbar while presenting"),
|
tr("Top toolbar while presenting"),
|
||||||
"",
|
"",
|
||||||
toolbar_modes,
|
toolbar_modes,
|
||||||
toolbar_mode_labels,
|
&toolbar_mode_labels,
|
||||||
|app| app.draft.presenter_toolbar_mode,
|
|app| app.draft.presenter_toolbar_mode,
|
||||||
Message::PresenterToolbarModeChanged,
|
Message::PresenterToolbarModeChanged,
|
||||||
)
|
)
|
||||||
@@ -69,7 +69,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Tool behavior"),
|
tr("Tool behavior"),
|
||||||
"",
|
"",
|
||||||
behaviors,
|
behaviors,
|
||||||
behavior_labels,
|
&behavior_labels,
|
||||||
|app| app.draft.presenter_tool_behavior,
|
|app| app.draft.presenter_tool_behavior,
|
||||||
Message::PresenterToolBehaviorChanged,
|
Message::PresenterToolBehaviorChanged,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Status bar position"),
|
tr("Status bar position"),
|
||||||
"",
|
"",
|
||||||
positions,
|
positions,
|
||||||
position_labels,
|
&position_labels,
|
||||||
|app| app.draft.ui_status_position,
|
|app| app.draft.ui_status_position,
|
||||||
Message::StatusPositionChanged,
|
Message::StatusPositionChanged,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Layout mode"),
|
tr("Layout mode"),
|
||||||
tr("Simple: core pens. Regular: adds Shape Pen, Line, Arrow, presets. Advanced: shapes inline, multi-step undo."),
|
tr("Simple: core pens. Regular: adds Shape Pen, Line, Arrow, presets. Advanced: shapes inline, multi-step undo."),
|
||||||
layout_modes,
|
layout_modes,
|
||||||
layout_labels,
|
&layout_labels,
|
||||||
|app| app.draft.ui_toolbar_layout_mode,
|
|app| app.draft.ui_toolbar_layout_mode,
|
||||||
Message::ToolbarLayoutModeChanged,
|
Message::ToolbarLayoutModeChanged,
|
||||||
)
|
)
|
||||||
@@ -44,7 +44,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Zoom chip"),
|
tr("Zoom chip"),
|
||||||
"",
|
"",
|
||||||
zoom_chips,
|
zoom_chips,
|
||||||
zoom_chip_labels,
|
&zoom_chip_labels,
|
||||||
|app| app.draft.ui_toolbar_zoom_chip_display,
|
|app| app.draft.ui_toolbar_zoom_chip_display,
|
||||||
Message::ToolbarZoomChipDisplayChanged,
|
Message::ToolbarZoomChipDisplayChanged,
|
||||||
)
|
)
|
||||||
@@ -58,7 +58,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Shortcut edit click"),
|
tr("Shortcut edit click"),
|
||||||
"",
|
"",
|
||||||
rebinds,
|
rebinds,
|
||||||
rebind_labels,
|
&rebind_labels,
|
||||||
|app| app.draft.ui_toolbar_rebind_modifier,
|
|app| app.draft.ui_toolbar_rebind_modifier,
|
||||||
Message::ToolbarRebindModifierChanged,
|
Message::ToolbarRebindModifierChanged,
|
||||||
)
|
)
|
||||||
@@ -66,7 +66,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Stroke controls"),
|
tr("Stroke controls"),
|
||||||
tr("How the style pill shows pen smoothing and Shape Pen sensitivity. Panel: one Pen feel button opening meters with a live smoothing preview. Meters or Steppers: inline controls."),
|
tr("How the style pill shows pen smoothing and Shape Pen sensitivity. Panel: one Pen feel button opening meters with a live smoothing preview. Meters or Steppers: inline controls."),
|
||||||
stroke_styles,
|
stroke_styles,
|
||||||
stroke_style_labels,
|
&stroke_style_labels,
|
||||||
|app| app.draft.ui_toolbar_stroke_controls,
|
|app| app.draft.ui_toolbar_stroke_controls,
|
||||||
Message::ToolbarStrokeControlsChanged,
|
Message::ToolbarStrokeControlsChanged,
|
||||||
)
|
)
|
||||||
@@ -179,7 +179,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
tr("Edit mode"),
|
tr("Edit mode"),
|
||||||
tr("Overrides below apply to the mode selected here; \"Default\" keeps the mode preset."),
|
tr("Overrides below apply to the mode selected here; \"Default\" keeps the mode preset."),
|
||||||
override_modes,
|
override_modes,
|
||||||
override_mode_labels,
|
&override_mode_labels,
|
||||||
|app| app.override_mode,
|
|app| app.override_mode,
|
||||||
Message::ToolbarOverrideModeChanged,
|
Message::ToolbarOverrideModeChanged,
|
||||||
);
|
);
|
||||||
@@ -198,7 +198,7 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|
|||||||
field.label(),
|
field.label(),
|
||||||
"",
|
"",
|
||||||
values,
|
values,
|
||||||
labels,
|
&labels,
|
||||||
move |app| toolbar_override(app, field),
|
move |app| toolbar_override(app, field),
|
||||||
move |value| Message::ToolbarOverrideChanged(field, value),
|
move |value| Message::ToolbarOverrideChanged(field, value),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
|
use std::iter;
|
||||||
|
|
||||||
|
use wayscriber::domain::Language;
|
||||||
|
|
||||||
use crate::app::state::ConfiguratorApp;
|
use crate::app::state::ConfiguratorApp;
|
||||||
use crate::models::{KeybindingsTabId, SearchQuery, TabId, UiTabId, keybinding_tab};
|
use crate::models::{KeybindingsTabId, SearchQuery, TabId, UiTabId, keybinding_tab};
|
||||||
use wayscriber::config::{
|
use wayscriber::config::{
|
||||||
PERFORMANCE_FIELD_METADATA, PerformanceFieldGroup, action_meta, toolbar_item_definitions,
|
PERFORMANCE_FIELD_METADATA, PerformanceFieldGroup, action_meta, toolbar_item_definitions,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::terms::*;
|
#[cfg(feature = "tablet-input")]
|
||||||
|
use super::terms::TABLET_TERMS;
|
||||||
|
use super::terms::{
|
||||||
|
ARROW_TERMS, BOARD_GENERAL_TERMS, CAPTURE_FILE_TERMS, CAPTURE_PDF_IDENTITY_TERMS,
|
||||||
|
CAPTURE_PDF_TERMS, CAPTURE_REGION_TERMS, DAEMON_LIGHT_TERMS, DAEMON_SERVICE_TERMS,
|
||||||
|
DAEMON_SHORTCUT_TERMS, DAEMON_STATUS_TERMS, DRAWING_COLOR_TERMS, DRAWING_DEFAULT_TERMS,
|
||||||
|
DRAWING_DRAG_TERMS, DRAWING_FONT_TERMS, DRAWING_LASER_TERMS, HISTORY_CUSTOM_TERMS,
|
||||||
|
HISTORY_MAIN_TERMS, PRESET_CONTROL_TERMS, RENDER_PROFILE_GENERAL_TERMS, SESSION_CATALOG_TERMS,
|
||||||
|
SESSION_PERSISTENCE_TERMS, UI_GENERAL_TERMS, tab_aliases, tab_scope_aliases, ui_tab_aliases,
|
||||||
|
ui_tab_terms,
|
||||||
|
};
|
||||||
use super::types::{AppSearchSummary, SearchArea, TabSearchSummary};
|
use super::types::{AppSearchSummary, SearchArea, TabSearchSummary};
|
||||||
|
|
||||||
pub(super) fn build_search_summary(app: &ConfiguratorApp) -> AppSearchSummary {
|
pub(super) fn build_search_summary(app: &ConfiguratorApp) -> AppSearchSummary {
|
||||||
@@ -167,10 +181,10 @@ fn performance_matches(query: &SearchQuery, summary: &mut TabSearchSummary) {
|
|||||||
] {
|
] {
|
||||||
let mut parts = vec![identity];
|
let mut parts = vec![identity];
|
||||||
parts.extend(area.section_titles());
|
parts.extend(area.section_titles());
|
||||||
for metadata in PERFORMANCE_FIELD_METADATA
|
let group_fields = PERFORMANCE_FIELD_METADATA
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|metadata| metadata.group == group)
|
.filter(|metadata| metadata.group == group);
|
||||||
{
|
for metadata in group_fields {
|
||||||
parts.extend([metadata.path, metadata.label, metadata.help]);
|
parts.extend([metadata.path, metadata.label, metadata.help]);
|
||||||
parts.extend_from_slice(metadata.search_terms);
|
parts.extend_from_slice(metadata.search_terms);
|
||||||
}
|
}
|
||||||
@@ -187,7 +201,7 @@ fn ui_matches(query: &SearchQuery, summary: &mut TabSearchSummary) {
|
|||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.chain(
|
.chain(
|
||||||
wayscriber::domain::Language::ALL
|
Language::ALL
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|language| [language.native_name(), language.word_for_language()])
|
.flat_map(|language| [language.native_name(), language.word_for_language()])
|
||||||
.flatten(),
|
.flatten(),
|
||||||
@@ -468,7 +482,7 @@ impl ScopedSearchQuery for SearchQuery {
|
|||||||
.chain(parts.iter().copied()),
|
.chain(parts.iter().copied()),
|
||||||
) || tab_scope_aliases(tab)
|
) || tab_scope_aliases(tab)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|alias| self.matches_parts(std::iter::once(*alias).chain(parts.iter().copied())))
|
.any(|alias| self.matches_parts(iter::once(*alias).chain(parts.iter().copied())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matches_any_raw_text(&self, values: &[&str]) -> bool {
|
fn matches_any_raw_text(&self, values: &[&str]) -> bool {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
//! that every control label a page shows is indexed by the search terms —
|
//! that every control label a page shows is indexed by the search terms —
|
||||||
//! return with the GTK page registry, once every page is ported.
|
//! return with the GTK page registry, once every page is ported.
|
||||||
|
|
||||||
|
use wayscriber::domain::Language;
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -12,6 +15,7 @@ use crate::models::session::SessionArtifactSummary;
|
|||||||
use crate::models::{
|
use crate::models::{
|
||||||
KeybindingField, KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId,
|
KeybindingField, KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId,
|
||||||
};
|
};
|
||||||
|
use crate::test_env;
|
||||||
use wayscriber::config::toolbar_item_ids as ids;
|
use wayscriber::config::toolbar_item_ids as ids;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -856,13 +860,13 @@ fn catalog_item(id: &str) -> SessionCatalogItem {
|
|||||||
/// found, and titles as displayed find their tab, sub-tab and section.
|
/// found, and titles as displayed find their tab, sub-tab and section.
|
||||||
#[test]
|
#[test]
|
||||||
fn chinese_ui_search_matches_english_terms_and_displayed_titles() {
|
fn chinese_ui_search_matches_english_terms_and_displayed_titles() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::chinese_ui_search_matches_english_terms_and_displayed_titles"
|
"::chinese_ui_search_matches_english_terms_and_displayed_titles"
|
||||||
),
|
),
|
||||||
|| {
|
|| {
|
||||||
wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese);
|
i18n::set_language(Language::SimplifiedChinese);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
let tabs_for = |app: &mut ConfiguratorApp, query: &str| {
|
let tabs_for = |app: &mut ConfiguratorApp, query: &str| {
|
||||||
app.search_query = SearchQuery::new(query);
|
app.search_query = SearchQuery::new(query);
|
||||||
@@ -889,17 +893,14 @@ fn chinese_ui_search_matches_english_terms_and_displayed_titles() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn language_search_reveals_general_ui_in_either_language() {
|
fn language_search_reveals_general_ui_in_either_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::language_search_reveals_general_ui_in_either_language"
|
"::language_search_reveals_general_ui_in_either_language"
|
||||||
),
|
),
|
||||||
|| {
|
|| {
|
||||||
for language in [
|
for language in [Language::English, Language::SimplifiedChinese] {
|
||||||
wayscriber::domain::Language::English,
|
i18n::set_language(language);
|
||||||
wayscriber::domain::Language::SimplifiedChinese,
|
|
||||||
] {
|
|
||||||
wayscriber::i18n::set_language(language);
|
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
for query in ["language", "locale", "English", "简体中文", "中文", "语言"] {
|
for query in ["language", "locale", "English", "简体中文", "中文", "语言"] {
|
||||||
app.search_query = SearchQuery::new(query);
|
app.search_query = SearchQuery::new(query);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::fs;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -17,6 +18,9 @@ mod move_file;
|
|||||||
|
|
||||||
pub(super) use duplicate::duplicate_session_catalog_entry;
|
pub(super) use duplicate::duplicate_session_catalog_entry;
|
||||||
pub(super) use move_file::move_session_catalog_entry;
|
pub(super) use move_file::move_session_catalog_entry;
|
||||||
|
use wayscriber::config::{Config, LoadedConfig};
|
||||||
|
use wayscriber::session::{ClearToolStateOutcome, SessionOptions, catalog};
|
||||||
|
use wayscriber::{paths, session};
|
||||||
|
|
||||||
pub(super) async fn load_session_catalog() -> Result<Vec<SessionCatalogItem>, String> {
|
pub(super) async fn load_session_catalog() -> Result<Vec<SessionCatalogItem>, String> {
|
||||||
run_blocking(
|
run_blocking(
|
||||||
@@ -30,8 +34,7 @@ pub(super) async fn forget_session_catalog_entry(
|
|||||||
id: String,
|
id: String,
|
||||||
) -> Result<SessionCatalogActionResult, String> {
|
) -> Result<SessionCatalogActionResult, String> {
|
||||||
run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
|
run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
|
||||||
let removed = wayscriber::session::catalog::forget_session_by_id(&id)
|
let removed = catalog::forget_session_by_id(&id).map_err(|err| err.to_string())?;
|
||||||
.map_err(|err| err.to_string())?;
|
|
||||||
let items = load_session_catalog_sync()?;
|
let items = load_session_catalog_sync()?;
|
||||||
Ok(SessionCatalogActionResult::success(
|
Ok(SessionCatalogActionResult::success(
|
||||||
if removed {
|
if removed {
|
||||||
@@ -50,11 +53,8 @@ pub(super) async fn rename_session_catalog_entry(
|
|||||||
display_name: String,
|
display_name: String,
|
||||||
) -> Result<SessionCatalogActionResult, String> {
|
) -> Result<SessionCatalogActionResult, String> {
|
||||||
run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
|
run_blocking(BlockingJobKind::SessionCatalogMutation, move || {
|
||||||
let renamed = wayscriber::session::catalog::rename_session_display_name_by_id(
|
let renamed = catalog::rename_session_display_name_by_id(&id, display_name.trim())
|
||||||
&id,
|
.map_err(|err| err.to_string())?;
|
||||||
display_name.trim(),
|
|
||||||
)
|
|
||||||
.map_err(|err| err.to_string())?;
|
|
||||||
let items = load_session_catalog_sync()?;
|
let items = load_session_catalog_sync()?;
|
||||||
Ok(SessionCatalogActionResult::success(
|
Ok(SessionCatalogActionResult::success(
|
||||||
if let Some(entry) = renamed {
|
if let Some(entry) = renamed {
|
||||||
@@ -95,7 +95,7 @@ pub(super) async fn clear_session_catalog_entry(
|
|||||||
return Err(blocker);
|
return Err(blocker);
|
||||||
}
|
}
|
||||||
|
|
||||||
let outcome = wayscriber::session::clear_named_session_non_lock_artifacts(&item.path)
|
let outcome = session::clear_named_session_non_lock_artifacts(&item.path)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
let items = load_session_catalog_sync()?;
|
let items = load_session_catalog_sync()?;
|
||||||
Ok(SessionCatalogActionResult::success(
|
Ok(SessionCatalogActionResult::success(
|
||||||
@@ -125,8 +125,7 @@ pub(super) async fn clear_session_catalog_tool_state_entry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let options = named_session_options_for_catalog_item(&item)?;
|
let options = named_session_options_for_catalog_item(&item)?;
|
||||||
let outcome =
|
let outcome = session::clear_tool_state(&options).map_err(|err| err.to_string())?;
|
||||||
wayscriber::session::clear_tool_state(&options).map_err(|err| err.to_string())?;
|
|
||||||
let items = load_session_catalog_sync()?;
|
let items = load_session_catalog_sync()?;
|
||||||
Ok(SessionCatalogActionResult::success(
|
Ok(SessionCatalogActionResult::success(
|
||||||
clear_tool_state_catalog_message(&item.display_name, outcome),
|
clear_tool_state_catalog_message(&item.display_name, outcome),
|
||||||
@@ -144,7 +143,7 @@ fn service_status_blocker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_session_catalog_sync() -> Result<Vec<SessionCatalogItem>, String> {
|
fn load_session_catalog_sync() -> Result<Vec<SessionCatalogItem>, String> {
|
||||||
wayscriber::session::catalog::recent_sessions()
|
catalog::recent_sessions()
|
||||||
.map_err(|err| err.to_string())?
|
.map_err(|err| err.to_string())?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(SessionCatalogItem::from_entry)
|
.map(SessionCatalogItem::from_entry)
|
||||||
@@ -160,10 +159,10 @@ fn find_session_catalog_item(id: &str) -> Result<SessionCatalogItem, String> {
|
|||||||
|
|
||||||
fn named_session_options_for_catalog_item(
|
fn named_session_options_for_catalog_item(
|
||||||
item: &SessionCatalogItem,
|
item: &SessionCatalogItem,
|
||||||
) -> Result<wayscriber::session::SessionOptions, String> {
|
) -> Result<SessionOptions, String> {
|
||||||
let loaded = wayscriber::config::Config::load().map_err(|err| err.to_string())?;
|
let loaded = Config::load().map_err(|err| err.to_string())?;
|
||||||
ensure_session_config_available_for_destructive_action(&loaded)?;
|
ensure_session_config_available_for_destructive_action(&loaded)?;
|
||||||
let mut options = wayscriber::session::options_from_config_for_named_file(
|
let mut options = session::options_from_config_for_named_file(
|
||||||
&loaded.config.session,
|
&loaded.config.session,
|
||||||
item.path.clone(),
|
item.path.clone(),
|
||||||
None,
|
None,
|
||||||
@@ -173,7 +172,7 @@ fn named_session_options_for_catalog_item(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_session_config_available_for_destructive_action(
|
fn ensure_session_config_available_for_destructive_action(
|
||||||
loaded: &wayscriber::config::LoadedConfig,
|
loaded: &LoadedConfig,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if loaded.section_failed("session") {
|
if loaded.section_failed("session") {
|
||||||
return Err(
|
return Err(
|
||||||
@@ -184,24 +183,21 @@ fn ensure_session_config_available_for_destructive_action(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_tool_state_catalog_message(
|
fn clear_tool_state_catalog_message(display_name: &str, outcome: ClearToolStateOutcome) -> String {
|
||||||
display_name: &str,
|
|
||||||
outcome: wayscriber::session::ClearToolStateOutcome,
|
|
||||||
) -> String {
|
|
||||||
match outcome {
|
match outcome {
|
||||||
wayscriber::session::ClearToolStateOutcome::NoSession => {
|
ClearToolStateOutcome::NoSession => {
|
||||||
trf("No saved session file found for {}.", &[&display_name])
|
trf("No saved session file found for {}.", &[&display_name])
|
||||||
}
|
}
|
||||||
wayscriber::session::ClearToolStateOutcome::NoToolState => {
|
ClearToolStateOutcome::NoToolState => {
|
||||||
trf("No saved tool state found for {}.", &[&display_name])
|
trf("No saved tool state found for {}.", &[&display_name])
|
||||||
}
|
}
|
||||||
wayscriber::session::ClearToolStateOutcome::Cleared {
|
ClearToolStateOutcome::Cleared {
|
||||||
preserved_board_data: true,
|
preserved_board_data: true,
|
||||||
} => trf(
|
} => trf(
|
||||||
tr("Cleared saved tool state for {}. Boards and history were preserved."),
|
tr("Cleared saved tool state for {}. Boards and history were preserved."),
|
||||||
&[&display_name],
|
&[&display_name],
|
||||||
),
|
),
|
||||||
wayscriber::session::ClearToolStateOutcome::Cleared {
|
ClearToolStateOutcome::Cleared {
|
||||||
preserved_board_data: false,
|
preserved_board_data: false,
|
||||||
} => trf(
|
} => trf(
|
||||||
"Cleared saved tool state for {}. No board data was present.",
|
"Cleared saved tool state for {}. No board data was present.",
|
||||||
@@ -238,8 +234,8 @@ enum RuntimeLockKind {
|
|||||||
impl RuntimeLockKind {
|
impl RuntimeLockKind {
|
||||||
fn path(self) -> PathBuf {
|
fn path(self) -> PathBuf {
|
||||||
match self {
|
match self {
|
||||||
Self::Daemon => wayscriber::paths::daemon_lock_file(),
|
Self::Daemon => paths::daemon_lock_file(),
|
||||||
Self::Overlay => wayscriber::paths::overlay_lock_file(),
|
Self::Overlay => paths::overlay_lock_file(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +316,7 @@ fn acquire_runtime_lock_for_inactive_operation(
|
|||||||
) -> Result<File, String> {
|
) -> Result<File, String> {
|
||||||
let path = kind.path();
|
let path = kind.path();
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|err| {
|
fs::create_dir_all(parent).map_err(|err| {
|
||||||
format!(
|
format!(
|
||||||
"{} is disabled because the {} lock directory could not be created: {} ({err})",
|
"{} is disabled because the {} lock directory could not be created: {} ({err})",
|
||||||
operation.label(),
|
operation.label(),
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
use wayscriber::session;
|
||||||
|
use wayscriber::session::catalog;
|
||||||
|
use wayscriber::session::catalog::CatalogEvent;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -39,11 +42,11 @@ fn duplicate_session_catalog_entry_sync(
|
|||||||
return Err(blocker);
|
return Err(blocker);
|
||||||
}
|
}
|
||||||
|
|
||||||
let outcome = wayscriber::session::duplicate_named_session_primary(&item.path, target)
|
let outcome = session::duplicate_named_session_primary(&item.path, target)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
let entry = match wayscriber::session::catalog::upsert_session_event_with_display_name(
|
let entry = match catalog::upsert_session_event_with_display_name(
|
||||||
&outcome.target,
|
&outcome.target,
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
CatalogEvent::Saved,
|
||||||
&item.display_name,
|
&item.display_name,
|
||||||
) {
|
) {
|
||||||
Ok(entry) => entry,
|
Ok(entry) => entry,
|
||||||
@@ -81,11 +84,14 @@ fn duplicate_session_catalog_entry_sync(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
|
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
|
||||||
ShortcutApplyCapability, ShortcutBackend,
|
ShortcutApplyCapability, ShortcutBackend,
|
||||||
};
|
};
|
||||||
|
use crate::test_env::EnvGuard;
|
||||||
|
use crate::test_temp;
|
||||||
use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV};
|
use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -110,22 +116,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_session_catalog_entry_copies_primary_and_catalogs_new_entry() {
|
fn duplicate_session_catalog_entry_copies_primary_and_catalogs_new_entry() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[
|
let _env = EnvGuard::set(&[
|
||||||
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
||||||
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
||||||
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
||||||
]);
|
]);
|
||||||
let source = temp.path().join("lecture.wayscriber-session");
|
let source = temp.path().join("lecture.wayscriber-session");
|
||||||
let target = temp.path().join("lecture-copy.wayscriber-session");
|
let target = temp.path().join("lecture-copy.wayscriber-session");
|
||||||
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
|
let source_artifacts = session::named_session_artifact_paths(&source);
|
||||||
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target);
|
let target_artifacts = session::named_session_artifact_paths(&target);
|
||||||
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
|
fs::write(&source_artifacts.primary, b"primary").unwrap();
|
||||||
std::fs::write(&source_artifacts.backup, b"backup").unwrap();
|
fs::write(&source_artifacts.backup, b"backup").unwrap();
|
||||||
std::fs::write(&source_artifacts.lock, b"lock").unwrap();
|
fs::write(&source_artifacts.lock, b"lock").unwrap();
|
||||||
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
|
let source_entry = catalog::upsert_session_event_with_display_name(
|
||||||
&source,
|
&source,
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
CatalogEvent::Saved,
|
||||||
"Lecture",
|
"Lecture",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -134,14 +140,11 @@ mod tests {
|
|||||||
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary");
|
||||||
std::fs::read(&target_artifacts.primary).unwrap(),
|
|
||||||
b"primary"
|
|
||||||
);
|
|
||||||
assert!(!target_artifacts.backup.exists());
|
assert!(!target_artifacts.backup.exists());
|
||||||
assert!(!target_artifacts.lock.exists());
|
assert!(!target_artifacts.lock.exists());
|
||||||
assert!(result.message.contains("Duplicated Lecture"));
|
assert!(result.message.contains("Duplicated Lecture"));
|
||||||
let recents = wayscriber::session::catalog::recent_sessions().unwrap();
|
let recents = catalog::recent_sessions().unwrap();
|
||||||
assert_eq!(recents.len(), 2);
|
assert_eq!(recents.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recents
|
recents
|
||||||
@@ -155,52 +158,47 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_session_catalog_entry_warns_when_catalog_update_fails_after_copy() {
|
fn duplicate_session_catalog_entry_warns_when_catalog_update_fails_after_copy() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[
|
let _env = EnvGuard::set(&[
|
||||||
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
||||||
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
||||||
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
||||||
]);
|
]);
|
||||||
let source = temp.path().join("lecture.wayscriber-session");
|
let source = temp.path().join("lecture.wayscriber-session");
|
||||||
let target = temp.path().join("lecture-copy.wayscriber-session");
|
let target = temp.path().join("lecture-copy.wayscriber-session");
|
||||||
std::fs::write(&source, b"primary").unwrap();
|
fs::write(&source, b"primary").unwrap();
|
||||||
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
|
let source_entry = catalog::upsert_session_event_with_display_name(
|
||||||
&source,
|
&source,
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
CatalogEvent::Saved,
|
||||||
"Lecture",
|
"Lecture",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let lock_path = catalog_lock_path();
|
let lock_path = catalog_lock_path();
|
||||||
std::fs::remove_file(&lock_path).unwrap();
|
fs::remove_file(&lock_path).unwrap();
|
||||||
std::fs::create_dir(&lock_path).unwrap();
|
fs::create_dir(&lock_path).unwrap();
|
||||||
|
|
||||||
let result =
|
let result =
|
||||||
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
std::fs::remove_dir(&lock_path).unwrap();
|
fs::remove_dir(&lock_path).unwrap();
|
||||||
assert!(result.warning);
|
assert!(result.warning);
|
||||||
assert!(
|
assert!(
|
||||||
result
|
result
|
||||||
.message
|
.message
|
||||||
.contains("failed to update the session catalog")
|
.contains("failed to update the session catalog")
|
||||||
);
|
);
|
||||||
assert_eq!(std::fs::read(&target).unwrap(), b"primary");
|
assert_eq!(fs::read(&target).unwrap(), b"primary");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.items.len(),
|
result.items.len(),
|
||||||
1,
|
1,
|
||||||
"warning should keep the pre-copy catalog rows visible"
|
"warning should keep the pre-copy catalog rows visible"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(catalog::recent_sessions().unwrap().len(), 1);
|
||||||
wayscriber::session::catalog::recent_sessions()
|
|
||||||
.unwrap()
|
|
||||||
.len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn catalog_lock_path() -> std::path::PathBuf {
|
fn catalog_lock_path() -> PathBuf {
|
||||||
let catalog_path = wayscriber::session::catalog::catalog_path();
|
let catalog_path = catalog::catalog_path();
|
||||||
let mut raw = OsString::from(catalog_path.as_os_str());
|
let mut raw = OsString::from(catalog_path.as_os_str());
|
||||||
raw.push(".lock");
|
raw.push(".lock");
|
||||||
raw.into()
|
raw.into()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use wayscriber::i18n::{tr, trf};
|
use wayscriber::i18n::{tr, trf};
|
||||||
|
use wayscriber::session;
|
||||||
|
use wayscriber::session::{NamedSessionMoveOutcome, catalog};
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -41,29 +43,27 @@ fn move_session_catalog_entry_sync(
|
|||||||
return Err(blocker);
|
return Err(blocker);
|
||||||
}
|
}
|
||||||
|
|
||||||
let outcome = wayscriber::session::move_named_session_non_lock_artifacts(&item.path, target)
|
let outcome = session::move_named_session_non_lock_artifacts(&item.path, target)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
let entry =
|
let entry = match catalog::move_session_path_by_id(&item.id, &outcome.target) {
|
||||||
match wayscriber::session::catalog::move_session_path_by_id(&item.id, &outcome.target) {
|
Ok(Some(entry)) => entry,
|
||||||
Ok(Some(entry)) => entry,
|
Ok(None) => {
|
||||||
Ok(None) => {
|
return rollback_moved_artifacts_after_catalog_failure(
|
||||||
return rollback_moved_artifacts_after_catalog_failure(
|
&item,
|
||||||
&item,
|
&outcome,
|
||||||
&outcome,
|
initial_items,
|
||||||
initial_items,
|
tr("the original catalog entry disappeared before metadata could be updated"),
|
||||||
tr("the original catalog entry disappeared before metadata could be updated")
|
);
|
||||||
.to_string(),
|
}
|
||||||
);
|
Err(err) => {
|
||||||
}
|
return rollback_moved_artifacts_after_catalog_failure(
|
||||||
Err(err) => {
|
&item,
|
||||||
return rollback_moved_artifacts_after_catalog_failure(
|
&outcome,
|
||||||
&item,
|
initial_items,
|
||||||
&outcome,
|
&format!("failed to update the session catalog: {err}"),
|
||||||
initial_items,
|
);
|
||||||
format!("failed to update the session catalog: {err}"),
|
}
|
||||||
);
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
let items = match load_session_catalog_sync() {
|
let items = match load_session_catalog_sync() {
|
||||||
Ok(items) => items,
|
Ok(items) => items,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -88,11 +88,11 @@ fn move_session_catalog_entry_sync(
|
|||||||
|
|
||||||
fn rollback_moved_artifacts_after_catalog_failure(
|
fn rollback_moved_artifacts_after_catalog_failure(
|
||||||
item: &SessionCatalogItem,
|
item: &SessionCatalogItem,
|
||||||
outcome: &wayscriber::session::NamedSessionMoveOutcome,
|
outcome: &NamedSessionMoveOutcome,
|
||||||
initial_items: Vec<SessionCatalogItem>,
|
initial_items: Vec<SessionCatalogItem>,
|
||||||
reason: String,
|
reason: &str,
|
||||||
) -> Result<SessionCatalogActionResult, String> {
|
) -> Result<SessionCatalogActionResult, String> {
|
||||||
match wayscriber::session::rollback_named_session_non_lock_artifacts_move(outcome) {
|
match session::rollback_named_session_non_lock_artifacts_move(outcome) {
|
||||||
Ok(_) => Err(trf(
|
Ok(_) => Err(trf(
|
||||||
"Move Session failed because {}; rolled back moved files for {}.",
|
"Move Session failed because {}; rolled back moved files for {}.",
|
||||||
&[&reason, &item.display_name],
|
&[&reason, &item.display_name],
|
||||||
@@ -118,10 +118,10 @@ fn reject_catalog_target_collision(
|
|||||||
source_id: &str,
|
source_id: &str,
|
||||||
target: &Path,
|
target: &Path,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(existing) = items.iter().find(|item| {
|
if let Some(existing) = items
|
||||||
item.id != source_id
|
.iter()
|
||||||
&& wayscriber::session::catalog::session_paths_match(&item.path, target)
|
.find(|item| item.id != source_id && catalog::session_paths_match(&item.path, target))
|
||||||
}) {
|
{
|
||||||
return Err(trf(
|
return Err(trf(
|
||||||
"Move Session target is already in the catalog as {}.",
|
"Move Session target is already in the catalog as {}.",
|
||||||
&[&existing.display_name],
|
&[&existing.display_name],
|
||||||
@@ -133,11 +133,16 @@ fn reject_catalog_target_collision(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use wayscriber::session::catalog::CatalogEvent;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
|
DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability,
|
||||||
ShortcutApplyCapability, ShortcutBackend,
|
ShortcutApplyCapability, ShortcutBackend,
|
||||||
};
|
};
|
||||||
|
use crate::test_env::EnvGuard;
|
||||||
|
use crate::test_temp;
|
||||||
use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV};
|
use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -162,22 +167,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_session_catalog_entry_moves_artifacts_and_preserves_catalog_id() {
|
fn move_session_catalog_entry_moves_artifacts_and_preserves_catalog_id() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[
|
let _env = EnvGuard::set(&[
|
||||||
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
||||||
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
||||||
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
||||||
]);
|
]);
|
||||||
let source = temp.path().join("lecture.wayscriber-session");
|
let source = temp.path().join("lecture.wayscriber-session");
|
||||||
let target = temp.path().join("archive.wayscriber-session");
|
let target = temp.path().join("archive.wayscriber-session");
|
||||||
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
|
let source_artifacts = session::named_session_artifact_paths(&source);
|
||||||
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target);
|
let target_artifacts = session::named_session_artifact_paths(&target);
|
||||||
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
|
fs::write(&source_artifacts.primary, b"primary").unwrap();
|
||||||
std::fs::write(&source_artifacts.backup, b"backup").unwrap();
|
fs::write(&source_artifacts.backup, b"backup").unwrap();
|
||||||
std::fs::write(&source_artifacts.lock, b"source lock").unwrap();
|
fs::write(&source_artifacts.lock, b"source lock").unwrap();
|
||||||
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
|
let source_entry = catalog::upsert_session_event_with_display_name(
|
||||||
&source,
|
&source,
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
CatalogEvent::Saved,
|
||||||
"Lecture",
|
"Lecture",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -186,18 +191,12 @@ mod tests {
|
|||||||
move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()).unwrap();
|
move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()).unwrap();
|
||||||
|
|
||||||
assert!(!result.warning);
|
assert!(!result.warning);
|
||||||
assert_eq!(
|
assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary");
|
||||||
std::fs::read(&target_artifacts.primary).unwrap(),
|
assert_eq!(fs::read(&target_artifacts.backup).unwrap(), b"backup");
|
||||||
b"primary"
|
assert_eq!(fs::read(&source_artifacts.lock).unwrap(), b"source lock");
|
||||||
);
|
|
||||||
assert_eq!(std::fs::read(&target_artifacts.backup).unwrap(), b"backup");
|
|
||||||
assert_eq!(
|
|
||||||
std::fs::read(&source_artifacts.lock).unwrap(),
|
|
||||||
b"source lock"
|
|
||||||
);
|
|
||||||
assert!(!source_artifacts.primary.exists());
|
assert!(!source_artifacts.primary.exists());
|
||||||
assert!(!source_artifacts.backup.exists());
|
assert!(!source_artifacts.backup.exists());
|
||||||
let recents = wayscriber::session::catalog::recent_sessions().unwrap();
|
let recents = catalog::recent_sessions().unwrap();
|
||||||
assert_eq!(recents.len(), 1);
|
assert_eq!(recents.len(), 1);
|
||||||
assert_eq!(recents[0].id, source_entry.id);
|
assert_eq!(recents[0].id, source_entry.id);
|
||||||
assert_eq!(Path::new(&recents[0].path), target.as_path());
|
assert_eq!(Path::new(&recents[0].path), target.as_path());
|
||||||
@@ -205,22 +204,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_session_catalog_entry_failure_keeps_catalog_and_source_artifacts() {
|
fn move_session_catalog_entry_failure_keeps_catalog_and_source_artifacts() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[
|
let _env = EnvGuard::set(&[
|
||||||
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
||||||
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
||||||
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
||||||
]);
|
]);
|
||||||
let source = temp.path().join("lecture.wayscriber-session");
|
let source = temp.path().join("lecture.wayscriber-session");
|
||||||
let target = temp.path().join("archive.wayscriber-session");
|
let target = temp.path().join("archive.wayscriber-session");
|
||||||
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
|
let source_artifacts = session::named_session_artifact_paths(&source);
|
||||||
let target_artifacts = wayscriber::session::named_session_artifact_paths(&target);
|
let target_artifacts = session::named_session_artifact_paths(&target);
|
||||||
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
|
fs::write(&source_artifacts.primary, b"primary").unwrap();
|
||||||
std::fs::write(&source_artifacts.backup, b"backup").unwrap();
|
fs::write(&source_artifacts.backup, b"backup").unwrap();
|
||||||
std::fs::write(&target_artifacts.clear_marker, b"cleared").unwrap();
|
fs::write(&target_artifacts.clear_marker, b"cleared").unwrap();
|
||||||
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
|
let source_entry = catalog::upsert_session_event_with_display_name(
|
||||||
&source,
|
&source,
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
CatalogEvent::Saved,
|
||||||
"Lecture",
|
"Lecture",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -229,16 +228,13 @@ mod tests {
|
|||||||
.expect_err("target artifact should block move");
|
.expect_err("target artifact should block move");
|
||||||
|
|
||||||
assert!(err.contains("already has session artifacts"));
|
assert!(err.contains("already has session artifacts"));
|
||||||
|
assert_eq!(fs::read(&source_artifacts.primary).unwrap(), b"primary");
|
||||||
|
assert_eq!(fs::read(&source_artifacts.backup).unwrap(), b"backup");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read(&source_artifacts.primary).unwrap(),
|
fs::read(&target_artifacts.clear_marker).unwrap(),
|
||||||
b"primary"
|
|
||||||
);
|
|
||||||
assert_eq!(std::fs::read(&source_artifacts.backup).unwrap(), b"backup");
|
|
||||||
assert_eq!(
|
|
||||||
std::fs::read(&target_artifacts.clear_marker).unwrap(),
|
|
||||||
b"cleared"
|
b"cleared"
|
||||||
);
|
);
|
||||||
let recents = wayscriber::session::catalog::recent_sessions().unwrap();
|
let recents = catalog::recent_sessions().unwrap();
|
||||||
assert_eq!(recents.len(), 1);
|
assert_eq!(recents.len(), 1);
|
||||||
assert_eq!(recents[0].id, source_entry.id);
|
assert_eq!(recents[0].id, source_entry.id);
|
||||||
assert_eq!(Path::new(&recents[0].path), source.as_path());
|
assert_eq!(Path::new(&recents[0].path), source.as_path());
|
||||||
@@ -246,75 +242,71 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_session_catalog_entry_rejects_catalog_target_collision_before_disk_move() {
|
fn move_session_catalog_entry_rejects_catalog_target_collision_before_disk_move() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[
|
let _env = EnvGuard::set(&[
|
||||||
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
||||||
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
||||||
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
||||||
]);
|
]);
|
||||||
let source = temp.path().join("lecture.wayscriber-session");
|
let source = temp.path().join("lecture.wayscriber-session");
|
||||||
let target = temp.path().join("archive.wayscriber-session");
|
let target = temp.path().join("archive.wayscriber-session");
|
||||||
std::fs::write(&source, b"primary").unwrap();
|
fs::write(&source, b"primary").unwrap();
|
||||||
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
|
let source_entry = catalog::upsert_session_event_with_display_name(
|
||||||
&source,
|
&source,
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
CatalogEvent::Saved,
|
||||||
"Lecture",
|
"Lecture",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
wayscriber::session::catalog::upsert_session_event_with_display_name(
|
catalog::upsert_session_event_with_display_name(&target, CatalogEvent::Saved, "Archive")
|
||||||
&target,
|
.unwrap();
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
|
||||||
"Archive",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
||||||
.expect_err("target catalog entry should block move");
|
.expect_err("target catalog entry should block move");
|
||||||
|
|
||||||
assert!(err.contains("already in the catalog"));
|
assert!(err.contains("already in the catalog"));
|
||||||
assert_eq!(std::fs::read(&source).unwrap(), b"primary");
|
assert_eq!(fs::read(&source).unwrap(), b"primary");
|
||||||
assert!(!target.exists());
|
assert!(!target.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_session_catalog_entry_rolls_back_when_catalog_update_fails_after_move() {
|
fn move_session_catalog_entry_rolls_back_when_catalog_update_fails_after_move() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[
|
let _env = EnvGuard::set(&[
|
||||||
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
(CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()),
|
||||||
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
(XDG_DATA_HOME_ENV, temp.path().as_os_str()),
|
||||||
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()),
|
||||||
]);
|
]);
|
||||||
let source = temp.path().join("lecture.wayscriber-session");
|
let source = temp.path().join("lecture.wayscriber-session");
|
||||||
let target = temp.path().join("archive.wayscriber-session");
|
let target = temp.path().join("archive.wayscriber-session");
|
||||||
let source_artifacts = wayscriber::session::named_session_artifact_paths(&source);
|
let source_artifacts = session::named_session_artifact_paths(&source);
|
||||||
std::fs::write(&source_artifacts.primary, b"primary").unwrap();
|
fs::write(&source_artifacts.primary, b"primary").unwrap();
|
||||||
std::fs::create_dir(&source_artifacts.backup).unwrap();
|
fs::create_dir(&source_artifacts.backup).unwrap();
|
||||||
let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name(
|
let source_entry = catalog::upsert_session_event_with_display_name(
|
||||||
&source,
|
&source,
|
||||||
wayscriber::session::catalog::CatalogEvent::Saved,
|
CatalogEvent::Saved,
|
||||||
"Lecture",
|
"Lecture",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let lock_path = catalog_lock_path();
|
let lock_path = catalog_lock_path();
|
||||||
std::fs::remove_file(&lock_path).unwrap();
|
fs::remove_file(&lock_path).unwrap();
|
||||||
std::fs::create_dir(&lock_path).unwrap();
|
fs::create_dir(&lock_path).unwrap();
|
||||||
|
|
||||||
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status())
|
||||||
.expect_err("catalog update failure should roll back disk move");
|
.expect_err("catalog update failure should roll back disk move");
|
||||||
|
|
||||||
std::fs::remove_dir(&lock_path).unwrap();
|
fs::remove_dir(&lock_path).unwrap();
|
||||||
assert!(err.contains("failed to update the session catalog"));
|
assert!(err.contains("failed to update the session catalog"));
|
||||||
assert!(err.contains("rolled back moved files"));
|
assert!(err.contains("rolled back moved files"));
|
||||||
assert_eq!(std::fs::read(&source).unwrap(), b"primary");
|
assert_eq!(fs::read(&source).unwrap(), b"primary");
|
||||||
assert!(source_artifacts.backup.is_dir());
|
assert!(source_artifacts.backup.is_dir());
|
||||||
assert!(!target.exists());
|
assert!(!target.exists());
|
||||||
let recents = wayscriber::session::catalog::recent_sessions().unwrap();
|
let recents = catalog::recent_sessions().unwrap();
|
||||||
assert_eq!(recents.len(), 1);
|
assert_eq!(recents.len(), 1);
|
||||||
assert_eq!(Path::new(&recents[0].path), source.as_path());
|
assert_eq!(Path::new(&recents[0].path), source.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn catalog_lock_path() -> PathBuf {
|
fn catalog_lock_path() -> PathBuf {
|
||||||
let catalog_path = wayscriber::session::catalog::catalog_path();
|
let catalog_path = catalog::catalog_path();
|
||||||
let mut raw = OsString::from(catalog_path.as_os_str());
|
let mut raw = OsString::from(catalog_path.as_os_str());
|
||||||
raw.push(".lock");
|
raw.push(".lock");
|
||||||
raw.into()
|
raw.into()
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
use wayscriber::config::{ConfigSectionError, ConfigSource, ConfigValidationReport};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::models::session::SessionArtifactSummary;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend,
|
DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend,
|
||||||
};
|
};
|
||||||
|
use crate::test_env::EnvGuard;
|
||||||
|
use crate::test_temp;
|
||||||
use wayscriber::env_vars::XDG_RUNTIME_DIR_ENV;
|
use wayscriber::env_vars::XDG_RUNTIME_DIR_ENV;
|
||||||
|
|
||||||
fn daemon_status(active: bool) -> DaemonRuntimeStatus {
|
fn daemon_status(active: bool) -> DaemonRuntimeStatus {
|
||||||
@@ -26,8 +31,8 @@ fn daemon_status(active: bool) -> DaemonRuntimeStatus {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_policy_blocks_running_daemon() {
|
fn clear_policy_blocks_running_daemon() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let status = daemon_status(true);
|
let status = daemon_status(true);
|
||||||
|
|
||||||
let blocker = SessionCatalogOperation::Clear
|
let blocker = SessionCatalogOperation::Clear
|
||||||
@@ -39,8 +44,8 @@ fn clear_policy_blocks_running_daemon() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_policy_allows_inactive_daemon() {
|
fn clear_policy_allows_inactive_daemon() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let status = daemon_status(false);
|
let status = daemon_status(false);
|
||||||
|
|
||||||
let blocker = SessionCatalogOperation::Clear.cached_status_blocker(Some(&status));
|
let blocker = SessionCatalogOperation::Clear.cached_status_blocker(Some(&status));
|
||||||
@@ -50,8 +55,8 @@ fn clear_policy_allows_inactive_daemon() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_policy_uses_duplicate_status_message() {
|
fn duplicate_policy_uses_duplicate_status_message() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let blocker = SessionCatalogOperation::Duplicate
|
let blocker = SessionCatalogOperation::Duplicate
|
||||||
.cached_status_blocker(None)
|
.cached_status_blocker(None)
|
||||||
.expect("unknown status should block duplicate");
|
.expect("unknown status should block duplicate");
|
||||||
@@ -61,8 +66,8 @@ fn duplicate_policy_uses_duplicate_status_message() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_policy_uses_move_status_message() {
|
fn move_policy_uses_move_status_message() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let blocker = SessionCatalogOperation::Move
|
let blocker = SessionCatalogOperation::Move
|
||||||
.cached_status_blocker(None)
|
.cached_status_blocker(None)
|
||||||
.expect("unknown status should block move");
|
.expect("unknown status should block move");
|
||||||
@@ -72,8 +77,8 @@ fn move_policy_uses_move_status_message() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_policy_blocks_unknown_daemon_status() {
|
fn clear_policy_blocks_unknown_daemon_status() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let blocker = SessionCatalogOperation::Clear
|
let blocker = SessionCatalogOperation::Clear
|
||||||
.cached_status_blocker(None)
|
.cached_status_blocker(None)
|
||||||
.expect("unknown status should block clear");
|
.expect("unknown status should block clear");
|
||||||
@@ -83,8 +88,8 @@ fn clear_policy_blocks_unknown_daemon_status() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_tool_state_policy_uses_tool_state_status_message() {
|
fn clear_tool_state_policy_uses_tool_state_status_message() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let blocker = SessionCatalogOperation::ClearToolState
|
let blocker = SessionCatalogOperation::ClearToolState
|
||||||
.cached_status_blocker(None)
|
.cached_status_blocker(None)
|
||||||
.expect("unknown status should block tool reset");
|
.expect("unknown status should block tool reset");
|
||||||
@@ -95,8 +100,8 @@ fn clear_tool_state_policy_uses_tool_state_status_message() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn session_clear_transaction_guard_blocks_manual_daemon_lock() {
|
fn session_clear_transaction_guard_blocks_manual_daemon_lock() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
||||||
let error = acquire_runtime_lock_for_inactive_operation(
|
let error = acquire_runtime_lock_for_inactive_operation(
|
||||||
RuntimeLockKind::Daemon,
|
RuntimeLockKind::Daemon,
|
||||||
@@ -109,8 +114,8 @@ fn session_clear_transaction_guard_blocks_manual_daemon_lock() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock() {
|
fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
|
|
||||||
let reservation = reserve_inactive_session_operation(SessionCatalogOperation::Clear).unwrap();
|
let reservation = reserve_inactive_session_operation(SessionCatalogOperation::Clear).unwrap();
|
||||||
|
|
||||||
@@ -130,8 +135,8 @@ fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock()
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inactive_session_reservation_refuses_a_running_daemon() {
|
fn inactive_session_reservation_refuses_a_running_daemon() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
||||||
|
|
||||||
let error = match reserve_inactive_session_operation(SessionCatalogOperation::Move) {
|
let error = match reserve_inactive_session_operation(SessionCatalogOperation::Move) {
|
||||||
@@ -148,8 +153,8 @@ fn inactive_session_reservation_refuses_a_running_daemon() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cached_status_blocker_does_not_probe_runtime_locks() {
|
fn cached_status_blocker_does_not_probe_runtime_locks() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
||||||
let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap();
|
let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap();
|
||||||
let status = daemon_status(false);
|
let status = daemon_status(false);
|
||||||
@@ -164,8 +169,8 @@ fn cached_status_blocker_does_not_probe_runtime_locks() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_runtime_guards_hold_daemon_and_overlay_locks() {
|
fn clear_runtime_guards_hold_daemon_and_overlay_locks() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap();
|
||||||
let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap();
|
let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap();
|
||||||
|
|
||||||
@@ -190,7 +195,7 @@ fn session_artifact_status_reports_size_when_present() {
|
|||||||
created_at_millis: 1_767_261_600_000,
|
created_at_millis: 1_767_261_600_000,
|
||||||
last_opened_at_millis: None,
|
last_opened_at_millis: None,
|
||||||
last_saved_at_millis: None,
|
last_saved_at_millis: None,
|
||||||
artifacts: crate::models::session::SessionArtifactSummary {
|
artifacts: SessionArtifactSummary {
|
||||||
primary_exists: true,
|
primary_exists: true,
|
||||||
backup_exists: false,
|
backup_exists: false,
|
||||||
recovery_exists: false,
|
recovery_exists: false,
|
||||||
@@ -206,11 +211,11 @@ fn session_artifact_status_reports_size_when_present() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_tool_state_rejects_defaulted_session_config() {
|
fn clear_tool_state_rejects_defaulted_session_config() {
|
||||||
let loaded = wayscriber::config::LoadedConfig {
|
let loaded = LoadedConfig {
|
||||||
config: wayscriber::config::Config::default(),
|
config: Config::default(),
|
||||||
source: wayscriber::config::ConfigSource::Primary,
|
source: ConfigSource::Primary,
|
||||||
validation: wayscriber::config::ConfigValidationReport::default(),
|
validation: ConfigValidationReport::default(),
|
||||||
section_errors: vec![wayscriber::config::ConfigSectionError {
|
section_errors: vec![ConfigSectionError {
|
||||||
section: "session".to_string(),
|
section: "session".to_string(),
|
||||||
error: "invalid value".to_string(),
|
error: "invalid value".to_string(),
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
//! Shortcut edit modes are mutually exclusive and clear as one workflow.
|
//! Shortcut edit modes are mutually exclusive and clear as one workflow.
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
KeybindingField, PendingShortcutConflict, ShortcutRecorderState, ShortcutTextEditor,
|
KeybindingField, PendingShortcutConflict, ShortcutRecorderState, ShortcutTextEditor,
|
||||||
};
|
};
|
||||||
@@ -59,7 +61,7 @@ impl ShortcutWorkflow {
|
|||||||
}
|
}
|
||||||
pub(crate) fn take_recorder(&mut self) -> Option<ShortcutRecorderState> {
|
pub(crate) fn take_recorder(&mut self) -> Option<ShortcutRecorderState> {
|
||||||
self.recorder()?;
|
self.recorder()?;
|
||||||
match std::mem::take(&mut self.phase) {
|
match mem::take(&mut self.phase) {
|
||||||
ShortcutPhase::Recording(value) => Some(value),
|
ShortcutPhase::Recording(value) => Some(value),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -98,7 +100,7 @@ impl ShortcutWorkflow {
|
|||||||
}
|
}
|
||||||
pub(crate) fn take_conflict(&mut self) -> Option<PendingShortcutConflict> {
|
pub(crate) fn take_conflict(&mut self) -> Option<PendingShortcutConflict> {
|
||||||
self.conflict()?;
|
self.conflict()?;
|
||||||
match std::mem::take(&mut self.phase) {
|
match mem::take(&mut self.phase) {
|
||||||
ShortcutPhase::Conflict(value) => Some(value),
|
ShortcutPhase::Conflict(value) => Some(value),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
use std::mem;
|
||||||
|
|
||||||
use wayscriber::configurator_destination::ConfiguratorScreen;
|
use wayscriber::configurator_destination::ConfiguratorScreen;
|
||||||
|
|
||||||
use crate::models::{SearchQuery, TabId, UiTabId};
|
use crate::models::{SearchQuery, TabId, UiTabId, keybindings};
|
||||||
|
|
||||||
use super::effects::Effect;
|
use super::effects::Effect;
|
||||||
use super::state::ConfiguratorApp;
|
use super::state::ConfiguratorApp;
|
||||||
@@ -12,7 +14,7 @@ impl ConfiguratorApp {
|
|||||||
/// Reload is the user asking for the file again, not a fresh launch, and
|
/// Reload is the user asking for the file again, not a fresh launch, and
|
||||||
/// snapping the tabs back would undo wherever they navigated in between.
|
/// snapping the tabs back would undo wherever they navigated in between.
|
||||||
pub(crate) fn apply_startup_request(&mut self) -> Vec<Effect> {
|
pub(crate) fn apply_startup_request(&mut self) -> Vec<Effect> {
|
||||||
let request = std::mem::take(&mut self.startup_request);
|
let request = mem::take(&mut self.startup_request);
|
||||||
|
|
||||||
if let Some(problem) = request.problem() {
|
if let Some(problem) = request.problem() {
|
||||||
self.status = self.status.clone().with_note(&problem.message());
|
self.status = self.status.clone().with_note(&problem.message());
|
||||||
@@ -40,8 +42,7 @@ impl ConfiguratorApp {
|
|||||||
// nothing to show lands where the matches actually are.
|
// nothing to show lands where the matches actually are.
|
||||||
self.align_active_tabs_for_search();
|
self.align_active_tabs_for_search();
|
||||||
if self.active_tab == TabId::Keybindings
|
if self.active_tab == TabId::Keybindings
|
||||||
&& let Some(field) =
|
&& let Some(field) = keybindings::field_matching_search_term(&term)
|
||||||
crate::models::keybindings::field_matching_search_term(&term)
|
|
||||||
{
|
{
|
||||||
self.select_keybinding_field(field);
|
self.select_keybinding_field(field);
|
||||||
}
|
}
|
||||||
@@ -102,11 +103,18 @@ impl ConfiguratorApp {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::Path;
|
use std::ffi::OsString;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::{fs, iter};
|
||||||
|
|
||||||
use wayscriber::config::ConfigDocument;
|
use wayscriber::config::ConfigDocument;
|
||||||
|
use wayscriber::configurator_destination;
|
||||||
use wayscriber::configurator_destination::{ConfiguratorDestination, KeybindingsSection};
|
use wayscriber::configurator_destination::{ConfiguratorDestination, KeybindingsSection};
|
||||||
|
|
||||||
|
use crate::app::search::SearchArea;
|
||||||
|
use crate::models::KeybindingField;
|
||||||
|
use crate::test_env;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::app::state::StatusMessage;
|
use crate::app::state::StatusMessage;
|
||||||
use crate::messages::CommandMessage;
|
use crate::messages::CommandMessage;
|
||||||
@@ -117,18 +125,18 @@ mod tests {
|
|||||||
status.text().unwrap_or_default().to_string()
|
status.text().unwrap_or_default().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn args(values: &[&str]) -> Vec<std::ffi::OsString> {
|
fn args(values: &[&str]) -> Vec<OsString> {
|
||||||
std::iter::once("wayscriber-configurator")
|
iter::once("wayscriber-configurator")
|
||||||
.chain(values.iter().copied())
|
.chain(values.iter().copied())
|
||||||
.map(std::ffi::OsString::from)
|
.map(OsString::from)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A config file of its own, so the load path under test is the real one.
|
/// A config file of its own, so the load path under test is the real one.
|
||||||
fn config_file() -> (TempDir, std::path::PathBuf) {
|
fn config_file() -> (TempDir, PathBuf) {
|
||||||
let dir = tempdir().expect("temporary test directory");
|
let dir = tempdir().expect("temporary test directory");
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.toml");
|
||||||
std::fs::write(&path, "").expect("write test config");
|
fs::write(&path, "").expect("write test config");
|
||||||
(dir, path)
|
(dir, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,8 +148,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The app as the launcher would start it, after its first config load.
|
/// The app as the launcher would start it, after its first config load.
|
||||||
fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, std::path::PathBuf) {
|
fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, PathBuf) {
|
||||||
crate::test_env::sanitize_language_environment();
|
test_env::sanitize_language_environment();
|
||||||
let (dir, path) = config_file();
|
let (dir, path) = config_file();
|
||||||
let (mut app, _effects) =
|
let (mut app, _effects) =
|
||||||
ConfiguratorApp::new_app_with_startup(StartupRequest::from_args(args(values)));
|
ConfiguratorApp::new_app_with_startup(StartupRequest::from_args(args(values)));
|
||||||
@@ -237,8 +245,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_onboarding_hint_destination_lands_on_the_general_ui_setting() {
|
fn the_onboarding_hint_destination_lands_on_the_general_ui_setting() {
|
||||||
let launched =
|
let launched = configurator_destination::onboarding_hints_destination().as_arg();
|
||||||
wayscriber::configurator_destination::onboarding_hints_destination().as_arg();
|
|
||||||
let (app, _dir, _path) = app_launched_with(&["--open", &launched]);
|
let (app, _dir, _path) = app_launched_with(&["--open", &launched]);
|
||||||
|
|
||||||
assert_eq!(app.active_tab, TabId::Ui);
|
assert_eq!(app.active_tab, TabId::Ui);
|
||||||
@@ -247,7 +254,7 @@ mod tests {
|
|||||||
let ui = summary
|
let ui = summary
|
||||||
.tab(TabId::Ui)
|
.tab(TabId::Ui)
|
||||||
.expect("General UI destination match");
|
.expect("General UI destination match");
|
||||||
assert!(ui.area_matches(crate::app::search::SearchArea::UiGeneral));
|
assert!(ui.area_matches(SearchArea::UiGeneral));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The shortcut-action row: subtab plus action search.
|
/// The shortcut-action row: subtab plus action search.
|
||||||
@@ -259,10 +266,7 @@ mod tests {
|
|||||||
assert_eq!(app.active_tab, TabId::Keybindings);
|
assert_eq!(app.active_tab, TabId::Keybindings);
|
||||||
assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing);
|
assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing);
|
||||||
assert_eq!(app.search_query.raw(), "Clear Canvas");
|
assert_eq!(app.search_query.raw(), "Clear Canvas");
|
||||||
assert_eq!(
|
assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas));
|
||||||
app.selected_keybinding,
|
|
||||||
Some(crate::models::KeybindingField::ClearCanvas)
|
|
||||||
);
|
|
||||||
assert!(app.search_summary().tab(TabId::Keybindings).is_some());
|
assert!(app.search_summary().tab(TabId::Keybindings).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,10 +279,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(app.active_tab, TabId::Keybindings);
|
assert_eq!(app.active_tab, TabId::Keybindings);
|
||||||
assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing);
|
assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing);
|
||||||
assert_eq!(
|
assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas));
|
||||||
app.selected_keybinding,
|
|
||||||
Some(crate::models::KeybindingField::ClearCanvas)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A term with no match on the requested screen is the one case where
|
/// A term with no match on the requested screen is the one case where
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ use wayscriber::i18n::tr;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use wayscriber::config::{Config, MigrationPreview, PRESET_SLOTS_MAX};
|
use wayscriber::config::{Config, MigrationPreview, PRESET_SLOTS_MAX};
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
ColorPickerId, ConfigDraft, DesktopEnvironment, DragMouseButton, KeybindingField,
|
ColorPickerId, ConfigDraft, DesktopEnvironment, DragMouseButton, KeybindingField,
|
||||||
KeybindingsTabId, SearchQuery, SessionCatalogState, ShortcutManagerFilter, ShortcutManagerSort,
|
KeybindingsTabId, SearchQuery, SessionCatalogState, ShortcutManagerFilter, ShortcutManagerSort,
|
||||||
StartupRequest, TabId, ToolbarLayoutModeOption, UiTabId,
|
StartupRequest, TabId, ToolbarLayoutModeOption, UiTabId,
|
||||||
};
|
};
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::test_env;
|
||||||
|
|
||||||
use super::effects::Effect;
|
use super::effects::Effect;
|
||||||
|
|
||||||
@@ -222,7 +225,7 @@ impl ConfiguratorApp {
|
|||||||
/// about to draw. A transition that fails keeps the language it found.
|
/// about to draw. A transition that fails keeps the language it found.
|
||||||
/// The shell notices the change and rebuilds its pages in that language.
|
/// The shell notices the change and rebuilds its pages in that language.
|
||||||
pub(crate) fn apply_draft_language(&self) {
|
pub(crate) fn apply_draft_language(&self) {
|
||||||
wayscriber::i18n::set_language(self.draft.ui_language.to_language());
|
i18n::set_language(self.draft.ui_language.to_language());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The app as a launch with no destination.
|
/// The app as a launch with no destination.
|
||||||
@@ -231,7 +234,7 @@ impl ConfiguratorApp {
|
|||||||
/// when it is the empty one.
|
/// when it is the empty one.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn new_app() -> (Self, Vec<Effect>) {
|
pub(crate) fn new_app() -> (Self, Vec<Effect>) {
|
||||||
crate::test_env::sanitize_language_environment();
|
test_env::sanitize_language_environment();
|
||||||
Self::new_app_with_startup(StartupRequest::default())
|
Self::new_app_with_startup(StartupRequest::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use wayscriber::domain::BoardGridKind;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorPickerId,
|
BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorPickerId,
|
||||||
};
|
};
|
||||||
@@ -9,7 +11,7 @@ impl ConfiguratorApp {
|
|||||||
pub(super) fn handle_boards_grid_kind_changed(
|
pub(super) fn handle_boards_grid_kind_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
index: usize,
|
index: usize,
|
||||||
value: wayscriber::domain::BoardGridKind,
|
value: BoardGridKind,
|
||||||
) -> Vec<Effect> {
|
) -> Vec<Effect> {
|
||||||
if let Some(item) = self.draft.boards.items.get_mut(index) {
|
if let Some(item) = self.draft.boards.items.get_mut(index) {
|
||||||
item.grid_kind = value;
|
item.grid_kind = value;
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ impl ConfiguratorApp {
|
|||||||
value: String,
|
value: String,
|
||||||
) -> Vec<Effect> {
|
) -> Vec<Effect> {
|
||||||
self.status = StatusMessage::idle();
|
self.status = StatusMessage::idle();
|
||||||
self.color_picker_hex.insert(id, value.clone());
|
let parsed = parse_hex(&value);
|
||||||
|
self.color_picker_hex.insert(id, value);
|
||||||
|
|
||||||
if let Some((rgb, alpha)) = parse_hex(&value) {
|
if let Some((rgb, alpha)) = parsed {
|
||||||
let alpha = if id.uses_alpha() {
|
let alpha = if id.uses_alpha() {
|
||||||
alpha.or_else(|| self.current_alpha_for_id(id))
|
alpha.or_else(|| self.current_alpha_for_id(id))
|
||||||
} else {
|
} else {
|
||||||
@@ -169,41 +170,41 @@ impl ConfiguratorApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ColorPickerId::StatusBarBg => {
|
ColorPickerId::StatusBarBg => {
|
||||||
self.apply_quad_rgb(QuadField::StatusBarBg, values, alpha);
|
self.apply_quad_rgb(QuadField::StatusBarBg, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::StatusBarText => {
|
ColorPickerId::StatusBarText => {
|
||||||
self.apply_quad_rgb(QuadField::StatusBarText, values, alpha);
|
self.apply_quad_rgb(QuadField::StatusBarText, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::HighlightFill => {
|
ColorPickerId::HighlightFill => {
|
||||||
self.apply_quad_rgb(QuadField::HighlightFill, values, alpha);
|
self.apply_quad_rgb(QuadField::HighlightFill, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::HighlightOutline => {
|
ColorPickerId::HighlightOutline => {
|
||||||
self.apply_quad_rgb(QuadField::HighlightOutline, values, alpha);
|
self.apply_quad_rgb(QuadField::HighlightOutline, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::LaserColor => {
|
ColorPickerId::LaserColor => {
|
||||||
self.apply_quad_rgb(QuadField::LaserColor, values, alpha);
|
self.apply_quad_rgb(QuadField::LaserColor, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::HelpBg => {
|
ColorPickerId::HelpBg => {
|
||||||
self.apply_quad_rgb(QuadField::HelpBg, values, alpha);
|
self.apply_quad_rgb(QuadField::HelpBg, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::HelpBorder => {
|
ColorPickerId::HelpBorder => {
|
||||||
self.apply_quad_rgb(QuadField::HelpBorder, values, alpha);
|
self.apply_quad_rgb(QuadField::HelpBorder, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::HelpText => {
|
ColorPickerId::HelpText => {
|
||||||
self.apply_quad_rgb(QuadField::HelpText, values, alpha);
|
self.apply_quad_rgb(QuadField::HelpText, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::ExportPdfLabelText => {
|
ColorPickerId::ExportPdfLabelText => {
|
||||||
self.apply_quad_rgb(QuadField::ExportPdfLabelText, values, alpha);
|
self.apply_quad_rgb(QuadField::ExportPdfLabelText, &values, alpha);
|
||||||
}
|
}
|
||||||
ColorPickerId::ExportPdfLabelBackground => {
|
ColorPickerId::ExportPdfLabelBackground => {
|
||||||
self.apply_quad_rgb(QuadField::ExportPdfLabelBackground, values, alpha);
|
self.apply_quad_rgb(QuadField::ExportPdfLabelBackground, &values, alpha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sync_color_picker_hex_for_id(id);
|
self.sync_color_picker_hex_for_id(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_quad_rgb(&mut self, field: QuadField, values: [String; 3], alpha: Option<f64>) {
|
fn apply_quad_rgb(&mut self, field: QuadField, values: &[String; 3], alpha: Option<f64>) {
|
||||||
for (component, value) in values.iter().enumerate() {
|
for (component, value) in values.iter().enumerate() {
|
||||||
self.draft.set_quad(field, component, value.to_string());
|
self.draft.set_quad(field, component, value.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use wayscriber::i18n::{tr, trf};
|
|||||||
|
|
||||||
use wayscriber::config::ConfigDocument;
|
use wayscriber::config::ConfigDocument;
|
||||||
|
|
||||||
|
use crate::app::document_workflow::LeaveAction;
|
||||||
use crate::models::ConfigDraft;
|
use crate::models::ConfigDraft;
|
||||||
|
|
||||||
use super::super::super::effects::Effect;
|
use super::super::super::effects::Effect;
|
||||||
@@ -60,7 +61,7 @@ impl ConfiguratorApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::app::update) fn handle_reload_requested(&mut self) -> Vec<Effect> {
|
pub(in crate::app::update) fn handle_reload_requested(&mut self) -> Vec<Effect> {
|
||||||
self.request_leave(crate::app::document_workflow::LeaveAction::Reload)
|
self.request_leave(LeaveAction::Reload)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::app::update) fn begin_config_reload(&mut self) -> Vec<Effect> {
|
pub(in crate::app::update) fn begin_config_reload(&mut self) -> Vec<Effect> {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use wayscriber::config;
|
||||||
use wayscriber::config::{Action, CURRENT_CONFIG_REVISION, Config, ConfigDocument};
|
use wayscriber::config::{Action, CURRENT_CONFIG_REVISION, Config, ConfigDocument};
|
||||||
|
|
||||||
|
use crate::test_temp;
|
||||||
|
|
||||||
use super::save::save_validation_errors;
|
use super::save::save_validation_errors;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::app::effects::Effect;
|
use crate::app::effects::Effect;
|
||||||
@@ -16,9 +20,9 @@ fn status_contains(status: &StatusMessage, needle: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn temp_config_document(name: &str, contents: &str) -> (TempDir, Box<ConfigDocument>) {
|
fn temp_config_document(name: &str, contents: &str) -> (TempDir, Box<ConfigDocument>) {
|
||||||
let temp = crate::test_temp::tempdir().expect("temporary config directory");
|
let temp = test_temp::tempdir().expect("temporary config directory");
|
||||||
let path = temp.path().join(format!("{name}.toml"));
|
let path = temp.path().join(format!("{name}.toml"));
|
||||||
std::fs::write(&path, contents).expect("write test config");
|
fs::write(&path, contents).expect("write test config");
|
||||||
let document = ConfigDocument::load_from_path(&path).expect("load test config document");
|
let document = ConfigDocument::load_from_path(&path).expect("load test config document");
|
||||||
(temp, Box::new(document))
|
(temp, Box::new(document))
|
||||||
}
|
}
|
||||||
@@ -128,7 +132,7 @@ fn handle_config_loaded_surfaces_resolved_shortcut_conflicts() {
|
|||||||
"shortcut-conflict",
|
"shortcut-conflict",
|
||||||
&format!(
|
&format!(
|
||||||
"config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\"]\ncycle_toolbar_display = [\"F2\"]\n",
|
"config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\"]\ncycle_toolbar_display = [\"F2\"]\n",
|
||||||
wayscriber::config::CURRENT_CONFIG_REVISION
|
config::CURRENT_CONFIG_REVISION
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -155,7 +159,7 @@ fn handle_config_loaded_surfaces_skipped_default_shortcuts() {
|
|||||||
"skipped-default",
|
"skipped-default",
|
||||||
&format!(
|
&format!(
|
||||||
"config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\", \"F9\"]\n",
|
"config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\", \"F9\"]\n",
|
||||||
wayscriber::config::CURRENT_CONFIG_REVISION
|
config::CURRENT_CONFIG_REVISION
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -185,7 +189,7 @@ fn handle_config_loaded_surfaces_shortcuts_that_could_not_be_parsed() {
|
|||||||
"invalid-shortcut",
|
"invalid-shortcut",
|
||||||
&format!(
|
&format!(
|
||||||
"config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\n",
|
"config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\n",
|
||||||
wayscriber::config::CURRENT_CONFIG_REVISION
|
config::CURRENT_CONFIG_REVISION
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -211,7 +215,7 @@ fn handle_config_loaded_separates_every_keybinding_diagnostic_kind() {
|
|||||||
"invalid-and-conflicting",
|
"invalid-and-conflicting",
|
||||||
&format!(
|
&format!(
|
||||||
"config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\ntoggle_toolbar = [\"F2\", \"F9\"]\nundo = [\"Ctrl+Alt+U\"]\nredo = [\"Ctrl+Alt+U\"]\n",
|
"config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\ntoggle_toolbar = [\"F2\", \"F9\"]\nundo = [\"Ctrl+Alt+U\"]\nredo = [\"Ctrl+Alt+U\"]\n",
|
||||||
wayscriber::config::CURRENT_CONFIG_REVISION
|
config::CURRENT_CONFIG_REVISION
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -900,9 +904,9 @@ const LEGACY_REVISION_ZERO_CONFIG: &str = "config_revision = 0\n\n[drawing]\ndef
|
|||||||
/// A config file of its own, in a directory the test owns: an applied
|
/// A config file of its own, in a directory the test owns: an applied
|
||||||
/// migration saves, and a save drops its `.bak` next to the file.
|
/// migration saves, and a save drops its `.bak` next to the file.
|
||||||
fn app_with_config_file(contents: &str) -> (ConfiguratorApp, TempDir, PathBuf) {
|
fn app_with_config_file(contents: &str) -> (ConfiguratorApp, TempDir, PathBuf) {
|
||||||
let dir = crate::test_temp::tempdir().expect("temporary test directory");
|
let dir = test_temp::tempdir().expect("temporary test directory");
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.toml");
|
||||||
std::fs::write(&path, contents).expect("write test config");
|
fs::write(&path, contents).expect("write test config");
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
load_config_file(&mut app, &path);
|
load_config_file(&mut app, &path);
|
||||||
(app, dir, path)
|
(app, dir, path)
|
||||||
@@ -954,7 +958,7 @@ fn config_setting(contents: &str, key: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_config(path: &Path) -> String {
|
fn read_config(path: &Path) -> String {
|
||||||
std::fs::read_to_string(path).expect("read the saved config")
|
fs::read_to_string(path).expect("read the saved config")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A file that is valid TOML but holds one value serde cannot map used to
|
/// A file that is valid TOML but holds one value serde cannot map used to
|
||||||
@@ -963,9 +967,9 @@ fn read_config(path: &Path) -> String {
|
|||||||
/// defaults instead of calling the file unparseable, and Save keeps the rest.
|
/// defaults instead of calling the file unparseable, and Save keeps the rest.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_file_with_one_unreadable_value_loads_its_other_settings_and_saves_them_back() {
|
fn a_file_with_one_unreadable_value_loads_its_other_settings_and_saves_them_back() {
|
||||||
let dir = crate::test_temp::tempdir().expect("temporary test directory");
|
let dir = test_temp::tempdir().expect("temporary test directory");
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.toml");
|
||||||
std::fs::write(
|
fs::write(
|
||||||
&path,
|
&path,
|
||||||
"# mine\n[drawing]\ndefault_thickness = 7.0\n\n[keybindings]\nundo = \"Ctrl+Z\"\n",
|
"# mine\n[drawing]\ndefault_thickness = 7.0\n\n[keybindings]\nundo = \"Ctrl+Z\"\n",
|
||||||
)
|
)
|
||||||
@@ -1061,7 +1065,7 @@ fn applying_and_saving_writes_the_reviewed_fields_the_revision_and_a_backup() {
|
|||||||
|
|
||||||
let backup = save_draft(&mut app).expect("the save creates a backup");
|
let backup = save_draft(&mut app).expect("the save creates a backup");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(&backup).expect("read the backup"),
|
fs::read_to_string(&backup).expect("read the backup"),
|
||||||
LEGACY_REVISION_ZERO_CONFIG,
|
LEGACY_REVISION_ZERO_CONFIG,
|
||||||
"the backup holds the file as it was before the migration"
|
"the backup holds the file as it was before the migration"
|
||||||
);
|
);
|
||||||
@@ -1332,11 +1336,11 @@ fn a_fully_customized_apply_records_the_revision_without_reoffering() {
|
|||||||
fn dismissal_follows_the_file_not_the_path_across_a_retarget() {
|
fn dismissal_follows_the_file_not_the_path_across_a_retarget() {
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
let dir = crate::test_temp::tempdir().expect("temporary test directory");
|
let dir = test_temp::tempdir().expect("temporary test directory");
|
||||||
let first = dir.path().join("profile-a.toml");
|
let first = dir.path().join("profile-a.toml");
|
||||||
let second = dir.path().join("profile-b.toml");
|
let second = dir.path().join("profile-b.toml");
|
||||||
std::fs::write(&first, LEGACY_REVISION_ZERO_CONFIG).expect("write the first profile");
|
fs::write(&first, LEGACY_REVISION_ZERO_CONFIG).expect("write the first profile");
|
||||||
std::fs::write(&second, LEGACY_REVISION_ZERO_CONFIG).expect("write the second profile");
|
fs::write(&second, LEGACY_REVISION_ZERO_CONFIG).expect("write the second profile");
|
||||||
let link = dir.path().join("config.toml");
|
let link = dir.path().join("config.toml");
|
||||||
symlink(&first, &link).expect("link the config path at the first profile");
|
symlink(&first, &link).expect("link the config path at the first profile");
|
||||||
|
|
||||||
@@ -1353,7 +1357,7 @@ fn dismissal_follows_the_file_not_the_path_across_a_retarget() {
|
|||||||
"the same file reloaded is not the user asking again"
|
"the same file reloaded is not the user asking again"
|
||||||
);
|
);
|
||||||
|
|
||||||
std::fs::remove_file(&link).expect("unlink the config path");
|
fs::remove_file(&link).expect("unlink the config path");
|
||||||
symlink(&second, &link).expect("retarget the config path at the second profile");
|
symlink(&second, &link).expect("retarget the config path at the second profile");
|
||||||
load_config_file(&mut app, &link);
|
load_config_file(&mut app, &link);
|
||||||
|
|
||||||
@@ -1380,7 +1384,7 @@ fn an_empty_or_missing_file_never_offers_a_migration() {
|
|||||||
let (app, _dir, _path) = app_with_config_file("");
|
let (app, _dir, _path) = app_with_config_file("");
|
||||||
assert!(app.pending_migration().is_none());
|
assert!(app.pending_migration().is_none());
|
||||||
|
|
||||||
let dir = crate::test_temp::tempdir().expect("temporary test directory");
|
let dir = test_temp::tempdir().expect("temporary test directory");
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
load_config_file(&mut app, &dir.path().join("missing.toml"));
|
load_config_file(&mut app, &dir.path().join("missing.toml"));
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
//! it selects reaches no other test. The override and locale variables are
|
//! it selects reaches no other test. The override and locale variables are
|
||||||
//! cleared there, so `auto` resolves to English.
|
//! cleared there, so `auto` resolves to English.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use wayscriber::domain::Language;
|
use wayscriber::domain::Language;
|
||||||
use wayscriber::i18n;
|
use wayscriber::i18n;
|
||||||
|
|
||||||
|
use crate::test_env;
|
||||||
|
|
||||||
use super::{app_with_config_file, load_config_file, save_draft, status_contains};
|
use super::{app_with_config_file, load_config_file, save_draft, status_contains};
|
||||||
use crate::app::effects::Effect;
|
use crate::app::effects::Effect;
|
||||||
use crate::models::LanguageOption;
|
use crate::models::LanguageOption;
|
||||||
@@ -17,7 +21,7 @@ const CHINESE_FILE: &str = "[ui]\nlanguage = \"zh-CN\"\n";
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_first_load_applies_the_saved_language() {
|
fn the_first_load_applies_the_saved_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::the_first_load_applies_the_saved_language"
|
"::the_first_load_applies_the_saved_language"
|
||||||
@@ -35,7 +39,7 @@ fn the_first_load_applies_the_saved_language() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn selecting_a_language_previews_it_without_writing() {
|
fn selecting_a_language_previews_it_without_writing() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::selecting_a_language_previews_it_without_writing"
|
"::selecting_a_language_previews_it_without_writing"
|
||||||
@@ -55,7 +59,7 @@ fn selecting_a_language_previews_it_without_writing() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_successful_reload_applies_the_loaded_language() {
|
fn a_successful_reload_applies_the_loaded_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::a_successful_reload_applies_the_loaded_language"
|
"::a_successful_reload_applies_the_loaded_language"
|
||||||
@@ -78,7 +82,7 @@ fn a_successful_reload_applies_the_loaded_language() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_failed_reload_keeps_the_preview() {
|
fn a_failed_reload_keeps_the_preview() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(module_path!(), "::a_failed_reload_keeps_the_preview"),
|
concat!(module_path!(), "::a_failed_reload_keeps_the_preview"),
|
||||||
|| {
|
|| {
|
||||||
let (mut app, _dir, _path) = app_with_config_file(ENGLISH_FILE);
|
let (mut app, _dir, _path) = app_with_config_file(ENGLISH_FILE);
|
||||||
@@ -97,7 +101,7 @@ fn a_failed_reload_keeps_the_preview() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn confirmed_defaults_apply_the_default_language() {
|
fn confirmed_defaults_apply_the_default_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::confirmed_defaults_apply_the_default_language"
|
"::confirmed_defaults_apply_the_default_language"
|
||||||
@@ -122,7 +126,7 @@ fn confirmed_defaults_apply_the_default_language() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_canceled_defaults_request_keeps_the_language() {
|
fn a_canceled_defaults_request_keeps_the_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::a_canceled_defaults_request_keeps_the_language"
|
"::a_canceled_defaults_request_keeps_the_language"
|
||||||
@@ -140,7 +144,7 @@ fn a_canceled_defaults_request_keeps_the_language() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_finished_save_applies_the_saved_language() {
|
fn a_finished_save_applies_the_saved_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::a_finished_save_applies_the_saved_language"
|
"::a_finished_save_applies_the_saved_language"
|
||||||
@@ -162,7 +166,7 @@ fn a_finished_save_applies_the_saved_language() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canceling_the_leave_prompt_keeps_the_preview() {
|
fn canceling_the_leave_prompt_keeps_the_preview() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::canceling_the_leave_prompt_keeps_the_preview"
|
"::canceling_the_leave_prompt_keeps_the_preview"
|
||||||
@@ -183,7 +187,7 @@ fn canceling_the_leave_prompt_keeps_the_preview() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn discarding_to_reload_applies_the_document_that_loads() {
|
fn discarding_to_reload_applies_the_document_that_loads() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::discarding_to_reload_applies_the_document_that_loads"
|
"::discarding_to_reload_applies_the_document_that_loads"
|
||||||
@@ -196,7 +200,7 @@ fn discarding_to_reload_applies_the_document_that_loads() {
|
|||||||
let effects = app.discard_before_leave();
|
let effects = app.discard_before_leave();
|
||||||
assert!(matches!(effects.as_slice(), [Effect::LoadConfig]));
|
assert!(matches!(effects.as_slice(), [Effect::LoadConfig]));
|
||||||
// Another editor saved in the meantime: what loads is what counts.
|
// Another editor saved in the meantime: what loads is what counts.
|
||||||
std::fs::write(&path, CHINESE_FILE).expect("rewrite config");
|
fs::write(&path, CHINESE_FILE).expect("rewrite config");
|
||||||
load_config_file(&mut app, &path);
|
load_config_file(&mut app, &path);
|
||||||
|
|
||||||
assert_eq!(app.draft.ui_language, LanguageOption::SimplifiedChinese);
|
assert_eq!(app.draft.ui_language, LanguageOption::SimplifiedChinese);
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ mod tests {
|
|||||||
}]
|
}]
|
||||||
));
|
));
|
||||||
let _ = app.handle_daemon_action_completed(Ok(DaemonActionResult {
|
let _ = app.handle_daemon_action_completed(Ok(DaemonActionResult {
|
||||||
status: new_status.clone(),
|
status: new_status,
|
||||||
message: "refresh complete".to_string(),
|
message: "refresh complete".to_string(),
|
||||||
partial_failure: false,
|
partial_failure: false,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
ArrowStyleOption, ColorMode, ColorPickerId, DragColorOption, DragMouseButton, DragToolField,
|
ArrowStyleOption, ColorMode, ColorPickerId, DragColorOption, DragMouseButton, DragToolField,
|
||||||
DragToolOption, EraserModeOption, NamedColorOption,
|
DragToolOption, EraserModeOption, NamedColorOption,
|
||||||
@@ -108,9 +110,9 @@ impl ConfiguratorApp {
|
|||||||
self.status = StatusMessage::idle();
|
self.status = StatusMessage::idle();
|
||||||
if self.draft.drawing_quick_colors.remove_entry(index) {
|
if self.draft.drawing_quick_colors.remove_entry(index) {
|
||||||
self.remap_quick_color_pickers(|picker_index| match picker_index.cmp(&index) {
|
self.remap_quick_color_pickers(|picker_index| match picker_index.cmp(&index) {
|
||||||
std::cmp::Ordering::Less => Some(picker_index),
|
Ordering::Less => Some(picker_index),
|
||||||
std::cmp::Ordering::Equal => None,
|
Ordering::Equal => None,
|
||||||
std::cmp::Ordering::Greater => picker_index.checked_sub(1),
|
Ordering::Greater => picker_index.checked_sub(1),
|
||||||
});
|
});
|
||||||
self.refresh_dirty_flag();
|
self.refresh_dirty_flag();
|
||||||
}
|
}
|
||||||
@@ -178,7 +180,7 @@ impl ConfiguratorApp {
|
|||||||
pub(in crate::app::update) fn handle_font_cycle_changed(
|
pub(in crate::app::update) fn handle_font_cycle_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
index: usize,
|
index: usize,
|
||||||
family: String,
|
family: &str,
|
||||||
) -> Vec<Effect> {
|
) -> Vec<Effect> {
|
||||||
match self.draft.drawing_font_cycle.set(index, family) {
|
match self.draft.drawing_font_cycle.set(index, family) {
|
||||||
Ok(changed) => {
|
Ok(changed) => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use wayscriber::draw;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use wayscriber::config::{ColorSpec, Config};
|
use wayscriber::config::{ColorSpec, Config};
|
||||||
|
|
||||||
@@ -164,7 +166,7 @@ fn choosing_a_duplicate_font_keeps_the_list_and_explains_the_rejection() {
|
|||||||
config.drawing.font_cycle = vec!["Sans".to_string(), "Serif".to_string()];
|
config.drawing.font_cycle = vec!["Sans".to_string(), "Serif".to_string()];
|
||||||
app.draft = ConfigDraft::from_config(&config);
|
app.draft = ConfigDraft::from_config(&config);
|
||||||
|
|
||||||
let _ = app.handle_font_cycle_changed(1, "sans".to_string());
|
let _ = app.handle_font_cycle_changed(1, "sans");
|
||||||
|
|
||||||
assert_eq!(app.draft.drawing_font_cycle.entries(), ["Sans", "Serif"]);
|
assert_eq!(app.draft.drawing_font_cycle.entries(), ["Sans", "Serif"]);
|
||||||
let feedback = app
|
let feedback = app
|
||||||
@@ -179,7 +181,7 @@ fn choosing_a_duplicate_font_keeps_the_list_and_explains_the_rejection() {
|
|||||||
fn add_font_never_creates_a_blank_or_duplicate_when_the_catalog_is_exhausted() {
|
fn add_font_never_creates_a_blank_or_duplicate_when_the_catalog_is_exhausted() {
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.drawing.font_cycle = wayscriber::draw::system_font_families().to_vec();
|
config.drawing.font_cycle = draw::system_font_families().to_vec();
|
||||||
app.draft = ConfigDraft::from_config(&config);
|
app.draft = ConfigDraft::from_config(&config);
|
||||||
let before = app.draft.drawing_font_cycle.entries().to_vec();
|
let before = app.draft.drawing_font_cycle.entries().to_vec();
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ impl ConfiguratorApp {
|
|||||||
self.handle_session_catalog_clear_confirmed(id)
|
self.handle_session_catalog_clear_confirmed(id)
|
||||||
}
|
}
|
||||||
Message::SessionCatalogClearCanceled(id) => {
|
Message::SessionCatalogClearCanceled(id) => {
|
||||||
self.handle_session_catalog_clear_canceled(id)
|
self.handle_session_catalog_clear_canceled(&id)
|
||||||
}
|
}
|
||||||
Message::SearchChanged(value) => self.handle_search_changed(value),
|
Message::SearchChanged(value) => self.handle_search_changed(value),
|
||||||
Message::SearchCleared => self.handle_search_cleared(),
|
Message::SearchCleared => self.handle_search_cleared(),
|
||||||
@@ -180,7 +180,7 @@ impl ConfiguratorApp {
|
|||||||
Message::FontCycleRemoved(index) => self.handle_font_cycle_removed(index),
|
Message::FontCycleRemoved(index) => self.handle_font_cycle_removed(index),
|
||||||
Message::FontCycleMoved(index, delta) => self.handle_font_cycle_moved(index, delta),
|
Message::FontCycleMoved(index, delta) => self.handle_font_cycle_moved(index, delta),
|
||||||
Message::FontCycleChanged(index, family) => {
|
Message::FontCycleChanged(index, family) => {
|
||||||
self.handle_font_cycle_changed(index, family)
|
self.handle_font_cycle_changed(index, &family)
|
||||||
}
|
}
|
||||||
Message::QuickColorModeChanged(index, mode) => {
|
Message::QuickColorModeChanged(index, mode) => {
|
||||||
self.handle_quick_color_mode_changed(index, mode)
|
self.handle_quick_color_mode_changed(index, mode)
|
||||||
@@ -344,7 +344,7 @@ impl ConfiguratorApp {
|
|||||||
self.handle_shortcut_sequence_remove_last_step()
|
self.handle_shortcut_sequence_remove_last_step()
|
||||||
}
|
}
|
||||||
Message::ShortcutRemoved(field, binding) => {
|
Message::ShortcutRemoved(field, binding) => {
|
||||||
self.handle_shortcut_removed(field, binding)
|
self.handle_shortcut_removed(field, &binding)
|
||||||
}
|
}
|
||||||
Message::ShortcutResetRequested(field) => self.handle_shortcut_reset_requested(field),
|
Message::ShortcutResetRequested(field) => self.handle_shortcut_reset_requested(field),
|
||||||
Message::ShortcutTextEditStarted(field) => {
|
Message::ShortcutTextEditStarted(field) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use wayscriber::config::PRESET_SLOTS_MAX;
|
use wayscriber::config::PRESET_SLOTS_MAX;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
ColorMode, NamedColorOption, PresetEraserKindOption, PresetEraserModeOption, PresetTextField,
|
ColorMode, NamedColorOption, OverrideOption, PresetEraserKindOption, PresetEraserModeOption,
|
||||||
PresetToggleField,
|
PresetTextField, PresetToggleField, ToolOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::effects::Effect;
|
use super::super::effects::Effect;
|
||||||
@@ -73,7 +73,7 @@ impl ConfiguratorApp {
|
|||||||
pub(super) fn handle_preset_tool_changed(
|
pub(super) fn handle_preset_tool_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
slot_index: usize,
|
slot_index: usize,
|
||||||
tool: crate::models::ToolOption,
|
tool: ToolOption,
|
||||||
) -> Vec<Effect> {
|
) -> Vec<Effect> {
|
||||||
self.status = StatusMessage::idle();
|
self.status = StatusMessage::idle();
|
||||||
if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
|
if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
|
||||||
@@ -167,7 +167,7 @@ impl ConfiguratorApp {
|
|||||||
&mut self,
|
&mut self,
|
||||||
slot_index: usize,
|
slot_index: usize,
|
||||||
field: PresetToggleField,
|
field: PresetToggleField,
|
||||||
value: crate::models::OverrideOption,
|
value: OverrideOption,
|
||||||
) -> Vec<Effect> {
|
) -> Vec<Effect> {
|
||||||
self.status = StatusMessage::idle();
|
self.status = StatusMessage::idle();
|
||||||
if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
|
if let Some(slot) = self.draft.presets.slot_mut(slot_index) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use wayscriber::render_profiles;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
ColorPickerId, RenderProfileExportOption, RenderProfileMappingDraft, RenderProfileMappingSide,
|
ColorPickerId, RenderProfileExportOption, RenderProfileMappingDraft, RenderProfileMappingSide,
|
||||||
RenderProfileTextField,
|
RenderProfileTextField,
|
||||||
@@ -57,7 +59,7 @@ impl ConfiguratorApp {
|
|||||||
if let Some(profile) = self.draft.render_profiles.profiles.get_mut(index) {
|
if let Some(profile) = self.draft.render_profiles.profiles.get_mut(index) {
|
||||||
match field {
|
match field {
|
||||||
RenderProfileTextField::Id => {
|
RenderProfileTextField::Id => {
|
||||||
let next_id = wayscriber::render_profiles::normalize_profile_id(&value);
|
let next_id = render_profiles::normalize_profile_id(&value);
|
||||||
profile.id = value;
|
profile.id = value;
|
||||||
if self.draft.render_profiles.active == old_id {
|
if self.draft.render_profiles.active == old_id {
|
||||||
self.draft.render_profiles.active = next_id.clone();
|
self.draft.render_profiles.active = next_id.clone();
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ impl ConfiguratorApp {
|
|||||||
vec![Effect::ClearSessionEntry { id }]
|
vec![Effect::ClearSessionEntry { id }]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: String) -> Vec<Effect> {
|
pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: &str) -> Vec<Effect> {
|
||||||
if self.pending_session_clear_id() != Some(id.as_str()) {
|
if self.pending_session_clear_id() != Some(id) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use wayscriber::i18n;
|
||||||
|
|
||||||
|
use crate::models::DaemonRuntimeStatus;
|
||||||
|
use crate::models::session::SessionArtifactSummary;
|
||||||
|
use crate::test_env::EnvGuard;
|
||||||
|
use crate::{test_env, test_temp};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
DesktopEnvironment, LightShortcutApplyCapability, SessionCatalogState, ShortcutApplyCapability,
|
DesktopEnvironment, LightShortcutApplyCapability, SessionCatalogState, ShortcutApplyCapability,
|
||||||
@@ -18,7 +25,7 @@ fn catalog_item(id: &str, display_name: &str) -> SessionCatalogItem {
|
|||||||
created_at_millis: 1_767_261_600_000,
|
created_at_millis: 1_767_261_600_000,
|
||||||
last_opened_at_millis: None,
|
last_opened_at_millis: None,
|
||||||
last_saved_at_millis: None,
|
last_saved_at_millis: None,
|
||||||
artifacts: crate::models::session::SessionArtifactSummary {
|
artifacts: SessionArtifactSummary {
|
||||||
primary_exists: false,
|
primary_exists: false,
|
||||||
backup_exists: false,
|
backup_exists: false,
|
||||||
recovery_exists: false,
|
recovery_exists: false,
|
||||||
@@ -30,8 +37,8 @@ fn catalog_item(id: &str, display_name: &str) -> SessionCatalogItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inactive_daemon_status() -> crate::models::DaemonRuntimeStatus {
|
fn inactive_daemon_status() -> DaemonRuntimeStatus {
|
||||||
crate::models::DaemonRuntimeStatus {
|
DaemonRuntimeStatus {
|
||||||
desktop: DesktopEnvironment::Unknown,
|
desktop: DesktopEnvironment::Unknown,
|
||||||
shortcut_backend: ShortcutBackend::Manual,
|
shortcut_backend: ShortcutBackend::Manual,
|
||||||
shortcut_apply_capability: ShortcutApplyCapability::Manual,
|
shortcut_apply_capability: ShortcutApplyCapability::Manual,
|
||||||
@@ -124,14 +131,14 @@ fn catalog_load_preserves_a_defaults_confirmation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_finished_load_clears_its_notice_in_every_language() {
|
fn a_finished_load_clears_its_notice_in_every_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::a_finished_load_clears_its_notice_in_every_language"
|
"::a_finished_load_clears_its_notice_in_every_language"
|
||||||
),
|
),
|
||||||
|| {
|
|| {
|
||||||
for language in [Language::English, Language::SimplifiedChinese] {
|
for language in [Language::English, Language::SimplifiedChinese] {
|
||||||
wayscriber::i18n::set_language(language);
|
i18n::set_language(language);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
|
|
||||||
let effects = app.handle_session_catalog_refresh_requested();
|
let effects = app.handle_session_catalog_refresh_requested();
|
||||||
@@ -148,7 +155,7 @@ fn a_finished_load_clears_its_notice_in_every_language() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_loading_notice_is_drawn_in_the_current_language() {
|
fn the_loading_notice_is_drawn_in_the_current_language() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::the_loading_notice_is_drawn_in_the_current_language"
|
"::the_loading_notice_is_drawn_in_the_current_language"
|
||||||
@@ -158,7 +165,7 @@ fn the_loading_notice_is_drawn_in_the_current_language() {
|
|||||||
let _ = app.handle_session_catalog_refresh_requested();
|
let _ = app.handle_session_catalog_refresh_requested();
|
||||||
assert_eq!(app.status.text(), Some("Loading sessions..."));
|
assert_eq!(app.status.text(), Some("Loading sessions..."));
|
||||||
|
|
||||||
wayscriber::i18n::set_language(Language::SimplifiedChinese);
|
i18n::set_language(Language::SimplifiedChinese);
|
||||||
|
|
||||||
assert_eq!(app.status.text(), Some("正在加载会话..."));
|
assert_eq!(app.status.text(), Some("正在加载会话..."));
|
||||||
},
|
},
|
||||||
@@ -191,8 +198,8 @@ fn a_failed_load_stops_loading_and_shows_the_error() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_request_blocks_without_daemon_status() {
|
fn duplicate_request_blocks_without_daemon_status() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -208,8 +215,8 @@ fn duplicate_request_blocks_without_daemon_status() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_request_sets_busy_when_safe() {
|
fn duplicate_request_sets_busy_when_safe() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -228,8 +235,8 @@ fn duplicate_request_sets_busy_when_safe() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_request_blocks_without_daemon_status() {
|
fn move_request_blocks_without_daemon_status() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -245,8 +252,8 @@ fn move_request_blocks_without_daemon_status() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_request_sets_busy_when_safe() {
|
fn move_request_sets_busy_when_safe() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -265,8 +272,8 @@ fn move_request_sets_busy_when_safe() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_request_blocks_without_daemon_status() {
|
fn clear_request_blocks_without_daemon_status() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -281,8 +288,8 @@ fn clear_request_blocks_without_daemon_status() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_tool_state_request_blocks_without_daemon_status() {
|
fn clear_tool_state_request_blocks_without_daemon_status() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -298,8 +305,8 @@ fn clear_tool_state_request_blocks_without_daemon_status() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_tool_state_request_sets_busy_when_safe() {
|
fn clear_tool_state_request_sets_busy_when_safe() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -319,8 +326,8 @@ fn clear_tool_state_request_sets_busy_when_safe() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_request_sets_pending_confirmation_when_safe() {
|
fn clear_request_sets_pending_confirmation_when_safe() {
|
||||||
let temp = crate::test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -391,7 +398,7 @@ fn clear_canceled_disarms_and_clears_its_confirmation_status() {
|
|||||||
app.daemon.status = Some(inactive_daemon_status());
|
app.daemon.status = Some(inactive_daemon_status());
|
||||||
let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
|
let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
|
||||||
|
|
||||||
let effects = app.handle_session_catalog_clear_canceled("s-1".to_string());
|
let effects = app.handle_session_catalog_clear_canceled("s-1");
|
||||||
|
|
||||||
assert!(effects.is_empty());
|
assert!(effects.is_empty());
|
||||||
assert!(app.pending_session_clear_id().is_none());
|
assert!(app.pending_session_clear_id().is_none());
|
||||||
@@ -424,7 +431,7 @@ fn clear_canceled_preserves_status_that_replaced_its_confirmation() {
|
|||||||
let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
|
let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
|
||||||
app.status = StatusMessage::error("A newer session operation failed");
|
app.status = StatusMessage::error("A newer session operation failed");
|
||||||
|
|
||||||
let _ = app.handle_session_catalog_clear_canceled("s-1".to_string());
|
let _ = app.handle_session_catalog_clear_canceled("s-1");
|
||||||
|
|
||||||
assert!(app.pending_session_clear_id().is_none());
|
assert!(app.pending_session_clear_id().is_none());
|
||||||
assert!(matches!(app.status, StatusMessage::Error(_)));
|
assert!(matches!(app.status, StatusMessage::Error(_)));
|
||||||
@@ -440,7 +447,7 @@ fn stray_clear_cancel_preserves_unrelated_status() {
|
|||||||
app.pending_confirmation = None;
|
app.pending_confirmation = None;
|
||||||
app.status = StatusMessage::success("A completed operation");
|
app.status = StatusMessage::success("A completed operation");
|
||||||
|
|
||||||
let _ = app.handle_session_catalog_clear_canceled("s-1".to_string());
|
let _ = app.handle_session_catalog_clear_canceled("s-1");
|
||||||
|
|
||||||
assert!(matches!(app.status, StatusMessage::Success(_)));
|
assert!(matches!(app.status, StatusMessage::Success(_)));
|
||||||
assert!(status_contains(&app.status, "completed operation"));
|
assert!(status_contains(&app.status, "completed operation"));
|
||||||
@@ -458,7 +465,7 @@ fn stale_clear_cancel_does_not_disarm_a_newer_confirmation() {
|
|||||||
let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
|
let _ = app.handle_session_catalog_clear_requested("s-1".to_string());
|
||||||
let _ = app.handle_session_catalog_clear_requested("s-2".to_string());
|
let _ = app.handle_session_catalog_clear_requested("s-2".to_string());
|
||||||
|
|
||||||
let effects = app.handle_session_catalog_clear_canceled("s-1".to_string());
|
let effects = app.handle_session_catalog_clear_canceled("s-1");
|
||||||
|
|
||||||
assert!(effects.is_empty());
|
assert!(effects.is_empty());
|
||||||
assert_eq!(app.pending_session_clear_id(), Some("s-2"));
|
assert_eq!(app.pending_session_clear_id(), Some("s-2"));
|
||||||
@@ -470,8 +477,8 @@ fn stale_clear_cancel_does_not_disarm_a_newer_confirmation() {
|
|||||||
/// the busy flag, the status, the effect — is untouched.
|
/// the busy flag, the status, the effect — is untouched.
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_confirmed_consumes_the_pending_confirmation() {
|
fn clear_confirmed_consumes_the_pending_confirmation() {
|
||||||
let temp = crate::test_temp::tempdir().expect("temporary test directory");
|
let temp = test_temp::tempdir().expect("temporary test directory");
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -494,8 +501,8 @@ fn clear_confirmed_consumes_the_pending_confirmation() {
|
|||||||
/// stale one from a dialog — must not start it again.
|
/// stale one from a dialog — must not start it again.
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_confirmed_twice_starts_only_one_clear() {
|
fn clear_confirmed_twice_starts_only_one_clear() {
|
||||||
let temp = crate::test_temp::tempdir().expect("temporary test directory");
|
let temp = test_temp::tempdir().expect("temporary test directory");
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog
|
app.session_catalog
|
||||||
@@ -516,8 +523,8 @@ fn clear_confirmed_twice_starts_only_one_clear() {
|
|||||||
/// starts a clear nor consumes the confirmation that is armed.
|
/// starts a clear nor consumes the confirmation that is armed.
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_confirmed_for_another_row_leaves_the_pending_one_armed() {
|
fn clear_confirmed_for_another_row_leaves_the_pending_one_armed() {
|
||||||
let temp = crate::test_temp::tempdir().expect("temporary test directory");
|
let temp = test_temp::tempdir().expect("temporary test directory");
|
||||||
let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]);
|
||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
app.session_catalog = SessionCatalogState::loading();
|
app.session_catalog = SessionCatalogState::loading();
|
||||||
app.session_catalog.replace_items(vec![
|
app.session_catalog.replace_items(vec![
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ mod tests;
|
|||||||
use wayscriber::config::Shortcut;
|
use wayscriber::config::Shortcut;
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use crate::app::search::keybinding_row_visible;
|
use crate::app::search::{AppSearchSummary, keybinding_row_visible};
|
||||||
use crate::app::state::{ConfirmationPrompt, PendingConfirmation};
|
use crate::app::state::{ConfirmationPrompt, PendingConfirmation};
|
||||||
use crate::models::keybindings::{
|
use crate::models::keybindings::{
|
||||||
AppendOutcome, KeyboardModifiers, RecordedDevice, RecordedKeyboard, RecorderDeviceKind,
|
AppendOutcome, KeyboardModifiers, RecordedDevice, RecordedKeyboard, RecorderDeviceKind,
|
||||||
@@ -16,7 +16,7 @@ use crate::models::keybindings::{
|
|||||||
other_claimants, parse_keybindings, remove_binding, reset_field, reset_fields,
|
other_claimants, parse_keybindings, remove_binding, reset_field, reset_fields,
|
||||||
text_conflicts_for,
|
text_conflicts_for,
|
||||||
};
|
};
|
||||||
use crate::models::{KeybindingField, keybinding_tab};
|
use crate::models::{KeybindingField, PendingShortcutConflict, keybinding_tab};
|
||||||
|
|
||||||
use super::super::effects::Effect;
|
use super::super::effects::Effect;
|
||||||
use super::super::state::{ConfiguratorApp, StatusMessage};
|
use super::super::state::{ConfiguratorApp, StatusMessage};
|
||||||
@@ -127,12 +127,12 @@ impl ConfiguratorApp {
|
|||||||
pub(super) fn handle_shortcut_removed(
|
pub(super) fn handle_shortcut_removed(
|
||||||
&mut self,
|
&mut self,
|
||||||
field: KeybindingField,
|
field: KeybindingField,
|
||||||
binding: Shortcut,
|
binding: &Shortcut,
|
||||||
) -> Vec<Effect> {
|
) -> Vec<Effect> {
|
||||||
if self.shortcuts.conflict().is_some() {
|
if self.shortcuts.conflict().is_some() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
match remove_binding(&mut self.draft.keybindings, field, &binding) {
|
match remove_binding(&mut self.draft.keybindings, field, binding) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.status = StatusMessage::idle();
|
self.status = StatusMessage::idle();
|
||||||
self.refresh_dirty_flag();
|
self.refresh_dirty_flag();
|
||||||
@@ -197,7 +197,7 @@ impl ConfiguratorApp {
|
|||||||
}
|
}
|
||||||
self.shortcuts.set_editor(None);
|
self.shortcuts.set_editor(None);
|
||||||
self.shortcuts
|
self.shortcuts
|
||||||
.set_conflict(Some(crate::models::PendingShortcutConflict::Text {
|
.set_conflict(Some(PendingShortcutConflict::Text {
|
||||||
target: editor.field,
|
target: editor.field,
|
||||||
new_value: editor.text,
|
new_value: editor.text,
|
||||||
conflicts,
|
conflicts,
|
||||||
@@ -223,12 +223,12 @@ impl ConfiguratorApp {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let result = match pending {
|
let result = match pending {
|
||||||
crate::models::PendingShortcutConflict::Recorded {
|
PendingShortcutConflict::Recorded {
|
||||||
target,
|
target,
|
||||||
binding,
|
binding,
|
||||||
claimants,
|
claimants,
|
||||||
} => apply_recorded_replace(&mut self.draft.keybindings, target, &binding, &claimants),
|
} => apply_recorded_replace(&mut self.draft.keybindings, target, &binding, &claimants),
|
||||||
crate::models::PendingShortcutConflict::Text {
|
PendingShortcutConflict::Text {
|
||||||
target,
|
target,
|
||||||
new_value,
|
new_value,
|
||||||
conflicts,
|
conflicts,
|
||||||
@@ -274,7 +274,7 @@ impl ConfiguratorApp {
|
|||||||
|
|
||||||
pub(crate) fn prepare_shortcut_manager_refresh(
|
pub(crate) fn prepare_shortcut_manager_refresh(
|
||||||
&self,
|
&self,
|
||||||
search: &crate::app::search::AppSearchSummary,
|
search: &AppSearchSummary,
|
||||||
) -> (ShortcutManagerSummary, Vec<KeybindingField>) {
|
) -> (ShortcutManagerSummary, Vec<KeybindingField>) {
|
||||||
let summary = self.shortcut_manager_summary();
|
let summary = self.shortcut_manager_summary();
|
||||||
let fields = self.visible_keybinding_fields_with(&summary, search);
|
let fields = self.visible_keybinding_fields_with(&summary, search);
|
||||||
@@ -284,7 +284,7 @@ impl ConfiguratorApp {
|
|||||||
fn visible_keybinding_fields_with(
|
fn visible_keybinding_fields_with(
|
||||||
&self,
|
&self,
|
||||||
summary: &ShortcutManagerSummary,
|
summary: &ShortcutManagerSummary,
|
||||||
search: &crate::app::search::AppSearchSummary,
|
search: &AppSearchSummary,
|
||||||
) -> Vec<KeybindingField> {
|
) -> Vec<KeybindingField> {
|
||||||
let scope = (!self.keybindings_show_all).then_some(self.active_keybindings_tab);
|
let scope = (!self.keybindings_show_all).then_some(self.active_keybindings_tab);
|
||||||
summary.visible_fields(self.shortcut_filter, self.shortcut_sort, scope, |field| {
|
summary.visible_fields(self.shortcut_filter, self.shortcut_sort, scope, |field| {
|
||||||
@@ -416,13 +416,12 @@ impl ConfiguratorApp {
|
|||||||
match next_review_conflict(&self.draft.keybindings) {
|
match next_review_conflict(&self.draft.keybindings) {
|
||||||
Some((field, binding, claimants)) => {
|
Some((field, binding, claimants)) => {
|
||||||
self.select_keybinding_field(field);
|
self.select_keybinding_field(field);
|
||||||
self.shortcuts.set_conflict(Some(
|
self.shortcuts
|
||||||
crate::models::PendingShortcutConflict::Recorded {
|
.set_conflict(Some(PendingShortcutConflict::Recorded {
|
||||||
target: field,
|
target: field,
|
||||||
binding,
|
binding,
|
||||||
claimants,
|
claimants,
|
||||||
},
|
}));
|
||||||
));
|
|
||||||
self.status = StatusMessage::info(tr("Review the next conflicting shortcut."));
|
self.status = StatusMessage::info(tr("Review the next conflicting shortcut."));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -440,7 +439,7 @@ impl ConfiguratorApp {
|
|||||||
let claimants = other_claimants(&self.draft.keybindings, field, &binding);
|
let claimants = other_claimants(&self.draft.keybindings, field, &binding);
|
||||||
if !claimants.is_empty() {
|
if !claimants.is_empty() {
|
||||||
self.shortcuts
|
self.shortcuts
|
||||||
.set_conflict(Some(crate::models::PendingShortcutConflict::Recorded {
|
.set_conflict(Some(PendingShortcutConflict::Recorded {
|
||||||
target: field,
|
target: field,
|
||||||
binding,
|
binding,
|
||||||
claimants,
|
claimants,
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[cfg(feature = "tablet-input")]
|
||||||
|
use wayscriber::config::Action;
|
||||||
|
|
||||||
use crate::models::{ShortcutManagerFilter, ShortcutManagerSort};
|
use crate::models::{ShortcutManagerFilter, ShortcutManagerSort};
|
||||||
use wayscriber::config::{CURRENT_CONFIG_REVISION, ConfigDocument, Shortcut};
|
use wayscriber::config::{CURRENT_CONFIG_REVISION, ConfigDocument, Shortcut};
|
||||||
|
|
||||||
@@ -5,18 +11,20 @@ use crate::app::effects::Effect;
|
|||||||
use crate::app::state::{ConfiguratorApp, PendingConfirmation, StatusMessage};
|
use crate::app::state::{ConfiguratorApp, PendingConfirmation, StatusMessage};
|
||||||
use crate::models::keybindings::keyval;
|
use crate::models::keybindings::keyval;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
KeybindingField, KeyboardModifiers, RecorderDeviceKind, SearchQuery, keybinding_tab,
|
KeybindingField, KeyboardModifiers, PendingShortcutConflict, RecorderDeviceKind, SearchQuery,
|
||||||
|
keybinding_tab,
|
||||||
};
|
};
|
||||||
|
use crate::test_temp;
|
||||||
use crate::test_temp::TempDir;
|
use crate::test_temp::TempDir;
|
||||||
|
|
||||||
fn status_contains(status: &StatusMessage, needle: &str) -> bool {
|
fn status_contains(status: &StatusMessage, needle: &str) -> bool {
|
||||||
status.text().is_some_and(|text| text.contains(needle))
|
status.text().is_some_and(|text| text.contains(needle))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, std::path::PathBuf) {
|
fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, PathBuf) {
|
||||||
let dir = crate::test_temp::tempdir().expect("temporary test directory");
|
let dir = test_temp::tempdir().expect("temporary test directory");
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.toml");
|
||||||
std::fs::write(&path, contents).expect("write test config");
|
fs::write(&path, contents).expect("write test config");
|
||||||
let document = ConfigDocument::load_from_path(&path).expect("load test config document");
|
let document = ConfigDocument::load_from_path(&path).expect("load test config document");
|
||||||
let _ = app.handle_config_loaded(Ok((Box::new(document), None)));
|
let _ = app.handle_config_loaded(Ok((Box::new(document), None)));
|
||||||
(dir, path)
|
(dir, path)
|
||||||
@@ -125,7 +133,7 @@ fn recording_reset_and_removal_dirty_without_saving() {
|
|||||||
);
|
);
|
||||||
let effects = app.handle_shortcut_removed(
|
let effects = app.handle_shortcut_removed(
|
||||||
KeybindingField::ToggleFloatingBadge,
|
KeybindingField::ToggleFloatingBadge,
|
||||||
Shortcut::parse("F5").expect("parses"),
|
&Shortcut::parse("F5").expect("parses"),
|
||||||
);
|
);
|
||||||
assert!(effects.is_empty());
|
assert!(effects.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -201,7 +209,7 @@ fn save_after_confirmed_replacement_writes_only_the_intended_fields() {
|
|||||||
let _ = app.handle_shortcut_conflict_replace_confirmed();
|
let _ = app.handle_shortcut_conflict_replace_confirmed();
|
||||||
save_draft(&mut app);
|
save_draft(&mut app);
|
||||||
|
|
||||||
let contents = std::fs::read_to_string(&path).expect("read saved config");
|
let contents = fs::read_to_string(&path).expect("read saved config");
|
||||||
assert!(contents.contains("# keep me"), "{contents}");
|
assert!(contents.contains("# keep me"), "{contents}");
|
||||||
assert!(contents.contains("unknown_future = true"), "{contents}");
|
assert!(contents.contains("unknown_future = true"), "{contents}");
|
||||||
let revision = format!("config_revision = {CURRENT_CONFIG_REVISION}");
|
let revision = format!("config_revision = {CURRENT_CONFIG_REVISION}");
|
||||||
@@ -307,7 +315,7 @@ fn resetting_an_explicit_binding_stays_authored_through_save_reload() {
|
|||||||
assert!(app.draft.keybindings.is_authored(KeybindingField::Undo));
|
assert!(app.draft.keybindings.is_authored(KeybindingField::Undo));
|
||||||
save_draft(&mut app);
|
save_draft(&mut app);
|
||||||
|
|
||||||
let contents = std::fs::read_to_string(path).expect("read saved config");
|
let contents = fs::read_to_string(path).expect("read saved config");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config_setting(&contents, "undo").as_deref(),
|
config_setting(&contents, "undo").as_deref(),
|
||||||
Some("undo = [\"Ctrl+Z\"]")
|
Some("undo = [\"Ctrl+Z\"]")
|
||||||
@@ -374,7 +382,7 @@ fn recording_stylus_primary_prompts_to_move_the_default_legacy_barrel() {
|
|||||||
let (mut app, _effects) = ConfiguratorApp::new_app();
|
let (mut app, _effects) = ConfiguratorApp::new_app();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.draft.keybindings.legacy_tablet.stylus_primary,
|
app.draft.keybindings.legacy_tablet.stylus_primary,
|
||||||
Some(wayscriber::config::Action::ToggleRadialMenu)
|
Some(Action::ToggleRadialMenu)
|
||||||
);
|
);
|
||||||
let _ = app.handle_shortcut_recording_started(KeybindingField::Undo);
|
let _ = app.handle_shortcut_recording_started(KeybindingField::Undo);
|
||||||
let _ = app.handle_shortcut_recorder_button(
|
let _ = app.handle_shortcut_recorder_button(
|
||||||
@@ -688,7 +696,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() {
|
|||||||
assert!(app.shortcuts.review);
|
assert!(app.shortcuts.review);
|
||||||
assert!(app.shortcuts.conflict().is_some());
|
assert!(app.shortcuts.conflict().is_some());
|
||||||
let first_target = match app.shortcuts.conflict() {
|
let first_target = match app.shortcuts.conflict() {
|
||||||
Some(crate::models::PendingShortcutConflict::Recorded { target, .. }) => *target,
|
Some(PendingShortcutConflict::Recorded { target, .. }) => *target,
|
||||||
other => panic!("expected a recorded conflict, got {other:?}"),
|
other => panic!("expected a recorded conflict, got {other:?}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -702,7 +710,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() {
|
|||||||
.conflict()
|
.conflict()
|
||||||
.expect("the next conflict should be armed");
|
.expect("the next conflict should be armed");
|
||||||
match second {
|
match second {
|
||||||
crate::models::PendingShortcutConflict::Recorded { target, .. } => {
|
PendingShortcutConflict::Recorded { target, .. } => {
|
||||||
assert_ne!(*target, first_target);
|
assert_ne!(*target, first_target);
|
||||||
}
|
}
|
||||||
other => panic!("expected a recorded conflict, got {other:?}"),
|
other => panic!("expected a recorded conflict, got {other:?}"),
|
||||||
@@ -737,7 +745,7 @@ fn jump_to_conflict_selects_the_other_claimant() {
|
|||||||
let jump = app
|
let jump = app
|
||||||
.shortcuts
|
.shortcuts
|
||||||
.conflict()
|
.conflict()
|
||||||
.and_then(crate::models::PendingShortcutConflict::jump_field)
|
.and_then(PendingShortcutConflict::jump_field)
|
||||||
.expect("a claimant to jump to");
|
.expect("a claimant to jump to");
|
||||||
let _ = app.handle_shortcut_manager_jump_to(jump);
|
let _ = app.handle_shortcut_manager_jump_to(jump);
|
||||||
assert_eq!(app.selected_keybinding, Some(jump));
|
assert_eq!(app.selected_keybinding, Some(jump));
|
||||||
@@ -765,7 +773,8 @@ fn prepared_manager_refresh_reuses_analysis_for_filters() {
|
|||||||
#[ignore = "release measurement: cargo test --release shortcut_refresh_cost -- --ignored --nocapture"]
|
#[ignore = "release measurement: cargo test --release shortcut_refresh_cost -- --ignored --nocapture"]
|
||||||
fn shortcut_refresh_cost() {
|
fn shortcut_refresh_cost() {
|
||||||
use crate::models::keybindings::take_parse_calls;
|
use crate::models::keybindings::take_parse_calls;
|
||||||
use std::{hint::black_box, time::Instant};
|
use std::hint::black_box;
|
||||||
|
use std::time::Instant;
|
||||||
let (mut app, _) = ConfiguratorApp::new_app();
|
let (mut app, _) = ConfiguratorApp::new_app();
|
||||||
for query in ["", "tool", "Ctrl"] {
|
for query in ["", "tool", "Ctrl"] {
|
||||||
app.handle_search_changed(query.into());
|
app.handle_search_changed(query.into());
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ mod test_env;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_temp;
|
mod test_temp;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
|
||||||
use models::{StartupRequest, startup_usage};
|
use models::{StartupRequest, startup_usage};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let startup = StartupRequest::from_args(std::env::args_os());
|
let startup = StartupRequest::from_args(env::args_os());
|
||||||
if startup.wants_help() {
|
if startup.wants_help() {
|
||||||
print!("{}", startup_usage());
|
print!("{}", startup_usage());
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use wayscriber::config::{ConfigDocument, Shortcut, ToolbarItemId, ToolbarItemOrderGroup};
|
use wayscriber::config::{ConfigDocument, Shortcut, ToolbarItemId, ToolbarItemOrderGroup};
|
||||||
|
use wayscriber::domain::BoardGridKind;
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
ArrowStyleOption, BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode,
|
ArrowStyleOption, BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode,
|
||||||
@@ -150,7 +151,7 @@ pub enum Message {
|
|||||||
BoardsCollapseToggled(usize),
|
BoardsCollapseToggled(usize),
|
||||||
BoardsDefaultChanged(String),
|
BoardsDefaultChanged(String),
|
||||||
BoardsItemTextChanged(usize, BoardItemTextField, String),
|
BoardsItemTextChanged(usize, BoardItemTextField, String),
|
||||||
BoardsGridKindChanged(usize, wayscriber::domain::BoardGridKind),
|
BoardsGridKindChanged(usize, BoardGridKind),
|
||||||
BoardsBackgroundKindChanged(usize, BoardBackgroundOption),
|
BoardsBackgroundKindChanged(usize, BoardBackgroundOption),
|
||||||
BoardsBackgroundColorChanged(usize, usize, String),
|
BoardsBackgroundColorChanged(usize, usize, String),
|
||||||
BoardsDefaultPenEnabledChanged(usize, bool),
|
BoardsDefaultPenEnabledChanged(usize, bool),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use wayscriber::i18n::{tr, trf};
|
|||||||
|
|
||||||
use wayscriber::config::enums::ColorSpec;
|
use wayscriber::config::enums::ColorSpec;
|
||||||
use wayscriber::draw::Color;
|
use wayscriber::draw::Color;
|
||||||
|
use wayscriber::input::state;
|
||||||
use wayscriber::util::{ConfigHexColorError, name_to_color, parse_config_hex_color};
|
use wayscriber::util::{ConfigHexColorError, name_to_color, parse_config_hex_color};
|
||||||
|
|
||||||
use super::super::error::FormError;
|
use super::super::error::FormError;
|
||||||
@@ -22,7 +23,7 @@ impl ColorInput {
|
|||||||
mode: ColorMode::Named,
|
mode: ColorMode::Named,
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
rgb: color_to_rgb_strings(spec.to_color()),
|
rgb: color_to_rgb_strings(spec.to_color()),
|
||||||
selected_named: NamedColorOption::from_str(name),
|
selected_named: NamedColorOption::from_name(name),
|
||||||
},
|
},
|
||||||
ColorSpec::Rgb([r, g, b]) => Self {
|
ColorSpec::Rgb([r, g, b]) => Self {
|
||||||
mode: ColorMode::Rgb,
|
mode: ColorMode::Rgb,
|
||||||
@@ -35,7 +36,7 @@ impl ColorInput {
|
|||||||
// silently flattened to opaque by the three RGB boxes.
|
// silently flattened to opaque by the three RGB boxes.
|
||||||
ColorSpec::Rgba(_) => Self {
|
ColorSpec::Rgba(_) => Self {
|
||||||
mode: ColorMode::Named,
|
mode: ColorMode::Named,
|
||||||
name: wayscriber::input::state::color_to_hex(spec.to_color()),
|
name: state::color_to_hex(spec.to_color()),
|
||||||
rgb: color_to_rgb_strings(spec.to_color()),
|
rgb: color_to_rgb_strings(spec.to_color()),
|
||||||
selected_named: NamedColorOption::Custom,
|
selected_named: NamedColorOption::Custom,
|
||||||
},
|
},
|
||||||
@@ -116,7 +117,7 @@ impl ColorInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_named_from_current(&mut self) {
|
pub fn update_named_from_current(&mut self) {
|
||||||
self.selected_named = NamedColorOption::from_str(&self.name);
|
self.selected_named = NamedColorOption::from_name(&self.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_rgb_from_preview(&mut self) -> bool {
|
pub fn sync_rgb_from_preview(&mut self) -> bool {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -62,7 +65,7 @@ impl NamedColorOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(value: &str) -> Self {
|
pub fn from_name(value: &str) -> Self {
|
||||||
match value.trim().to_lowercase().as_str() {
|
match value.trim().to_lowercase().as_str() {
|
||||||
"red" => Self::Red,
|
"red" => Self::Red,
|
||||||
"green" => Self::Green,
|
"green" => Self::Green,
|
||||||
@@ -77,8 +80,8 @@ impl NamedColorOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for NamedColorOption {
|
impl Display for NamedColorOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
mod mapping;
|
mod mapping;
|
||||||
mod validation;
|
mod validation;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
use wayscriber::domain::BoardGridKind;
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use super::super::color::ColorTripletInput;
|
use super::super::color::ColorTripletInput;
|
||||||
@@ -24,8 +28,8 @@ impl BoardBackgroundOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for BoardBackgroundOption {
|
impl Display for BoardBackgroundOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +60,7 @@ pub struct BoardItemDraft {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub background_kind: BoardBackgroundOption,
|
pub background_kind: BoardBackgroundOption,
|
||||||
pub background_color: ColorTripletInput,
|
pub background_color: ColorTripletInput,
|
||||||
pub grid_kind: wayscriber::domain::BoardGridKind,
|
pub grid_kind: BoardGridKind,
|
||||||
pub grid_spacing: String,
|
pub grid_spacing: String,
|
||||||
pub default_pen_color: OptionalTripletInput,
|
pub default_pen_color: OptionalTripletInput,
|
||||||
pub auto_adjust_pen: bool,
|
pub auto_adjust_pen: bool,
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ use wayscriber::i18n::tr;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use wayscriber::config::{
|
use wayscriber::config::{
|
||||||
BoardBackgroundConfig, BoardColorConfig, BoardItemConfig, BoardsConfig, Config,
|
BoardBackgroundConfig, BoardColorConfig, BoardGridConfig, BoardItemConfig, BoardsConfig, Config,
|
||||||
};
|
};
|
||||||
|
use wayscriber::domain;
|
||||||
|
use wayscriber::domain::BoardGridKind;
|
||||||
|
|
||||||
use crate::models::color::ColorTripletInput;
|
use crate::models::color::ColorTripletInput;
|
||||||
use crate::models::error::FormError;
|
use crate::models::error::FormError;
|
||||||
@@ -103,8 +105,8 @@ impl BoardItemDraft {
|
|||||||
|
|
||||||
let spacing = match self.grid_spacing.trim().parse::<i64>() {
|
let spacing = match self.grid_spacing.trim().parse::<i64>() {
|
||||||
Ok(value)
|
Ok(value)
|
||||||
if (i64::from(wayscriber::domain::BOARD_GRID_MIN_SPACING)
|
if (i64::from(domain::BOARD_GRID_MIN_SPACING)
|
||||||
..=i64::from(wayscriber::domain::BOARD_GRID_MAX_SPACING))
|
..=i64::from(domain::BOARD_GRID_MAX_SPACING))
|
||||||
.contains(&value) =>
|
.contains(&value) =>
|
||||||
{
|
{
|
||||||
value
|
value
|
||||||
@@ -117,9 +119,9 @@ impl BoardItemDraft {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let grid = wayscriber::config::BoardGridConfig {
|
let grid = BoardGridConfig {
|
||||||
kind: if background.is_transparent() {
|
kind: if background.is_transparent() {
|
||||||
wayscriber::domain::BoardGridKind::None
|
BoardGridKind::None
|
||||||
} else {
|
} else {
|
||||||
self.grid_kind
|
self.grid_kind
|
||||||
}
|
}
|
||||||
@@ -230,8 +232,8 @@ impl BoardsDraft {
|
|||||||
name,
|
name,
|
||||||
background_kind: BoardBackgroundOption::Color,
|
background_kind: BoardBackgroundOption::Color,
|
||||||
background_color: ColorTripletInput::from([0.992, 0.992, 0.992]),
|
background_color: ColorTripletInput::from([0.992, 0.992, 0.992]),
|
||||||
grid_kind: wayscriber::domain::BoardGridKind::None,
|
grid_kind: BoardGridKind::None,
|
||||||
grid_spacing: wayscriber::domain::BOARD_GRID_DEFAULT_SPACING.to_string(),
|
grid_spacing: domain::BOARD_GRID_DEFAULT_SPACING.to_string(),
|
||||||
default_pen_color: OptionalTripletInput::from_option(
|
default_pen_color: OptionalTripletInput::from_option(
|
||||||
Some(&BoardColorConfig::Rgb([0.0, 0.0, 0.0])),
|
Some(&BoardColorConfig::Rgb([0.0, 0.0, 0.0])),
|
||||||
[0.0, 0.0, 0.0],
|
[0.0, 0.0, 0.0],
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use wayscriber::config;
|
||||||
|
use wayscriber::config::{KeybindingAuthorship, ToolbarSectionVisibility};
|
||||||
|
|
||||||
use super::super::super::color::{ColorInput, ColorQuadInput};
|
use super::super::super::color::{ColorInput, ColorQuadInput};
|
||||||
use super::super::super::fields::ZoomChipDisplayOption;
|
|
||||||
use super::super::super::fields::{
|
use super::super::super::fields::{
|
||||||
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
|
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
|
||||||
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
|
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
|
||||||
@@ -7,7 +9,7 @@ use super::super::super::fields::{
|
|||||||
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
|
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
|
||||||
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
|
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
|
||||||
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
|
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
|
||||||
UiThemeOption,
|
UiThemeOption, ZoomChipDisplayOption,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "tablet-input")]
|
#[cfg(feature = "tablet-input")]
|
||||||
use super::super::super::fields::{
|
use super::super::super::fields::{
|
||||||
@@ -31,7 +33,7 @@ impl ConfigDraft {
|
|||||||
FontWeightOption::from_value(&config.drawing.font_weight);
|
FontWeightOption::from_value(&config.drawing.font_weight);
|
||||||
let drawing_drag_tools = config.drawing.effective_drag_tools();
|
let drawing_drag_tools = config.drawing.effective_drag_tools();
|
||||||
let mut toolbar_items = config.ui.toolbar.items.clone();
|
let mut toolbar_items = config.ui.toolbar.items.clone();
|
||||||
let mut legacy_toolbar_visibility = wayscriber::config::ToolbarSectionVisibility {
|
let mut legacy_toolbar_visibility = ToolbarSectionVisibility {
|
||||||
show_actions_section: config.ui.toolbar.show_actions_section,
|
show_actions_section: config.ui.toolbar.show_actions_section,
|
||||||
show_actions_advanced: config.ui.toolbar.show_actions_advanced,
|
show_actions_advanced: config.ui.toolbar.show_actions_advanced,
|
||||||
show_zoom_actions: config.ui.toolbar.show_zoom_actions,
|
show_zoom_actions: config.ui.toolbar.show_zoom_actions,
|
||||||
@@ -48,13 +50,13 @@ impl ConfigDraft {
|
|||||||
.mode_overrides
|
.mode_overrides
|
||||||
.for_mode(config.ui.toolbar.layout_mode),
|
.for_mode(config.ui.toolbar.layout_mode),
|
||||||
);
|
);
|
||||||
wayscriber::config::fold_legacy_section_flags(
|
config::fold_legacy_section_flags(
|
||||||
&legacy_toolbar_visibility,
|
&legacy_toolbar_visibility,
|
||||||
config.ui.toolbar.layout_mode,
|
config.ui.toolbar.layout_mode,
|
||||||
&config.ui.toolbar.mode_overrides,
|
&config.ui.toolbar.mode_overrides,
|
||||||
&mut toolbar_items,
|
&mut toolbar_items,
|
||||||
);
|
);
|
||||||
let toolbar_visibility = wayscriber::config::resolve_section_visibility(
|
let toolbar_visibility = config::resolve_section_visibility(
|
||||||
config.ui.toolbar.layout_mode,
|
config.ui.toolbar.layout_mode,
|
||||||
&config.ui.toolbar.mode_overrides,
|
&config.ui.toolbar.mode_overrides,
|
||||||
&toolbar_items.resolved(),
|
&toolbar_items.resolved(),
|
||||||
@@ -311,14 +313,12 @@ impl ConfigDraft {
|
|||||||
session_persist_history: config.session.persist_history,
|
session_persist_history: config.session.persist_history,
|
||||||
session_restore_tool_state: config.session.restore_tool_state,
|
session_restore_tool_state: config.session.restore_tool_state,
|
||||||
session_per_output: config.session.per_output,
|
session_per_output: config.session.per_output,
|
||||||
session_storage_mode: SessionStorageModeOption::from_mode(
|
session_storage_mode: SessionStorageModeOption::from_mode(config.session.storage),
|
||||||
config.session.storage.clone(),
|
|
||||||
),
|
|
||||||
session_custom_directory: config.session.custom_directory.clone().unwrap_or_default(),
|
session_custom_directory: config.session.custom_directory.clone().unwrap_or_default(),
|
||||||
session_max_shapes_per_frame: config.session.max_shapes_per_frame.to_string(),
|
session_max_shapes_per_frame: config.session.max_shapes_per_frame.to_string(),
|
||||||
session_max_file_size_mb: config.session.max_file_size_mb.to_string(),
|
session_max_file_size_mb: config.session.max_file_size_mb.to_string(),
|
||||||
session_compression: SessionCompressionOption::from_compression(
|
session_compression: SessionCompressionOption::from_compression(
|
||||||
config.session.compress.clone(),
|
config.session.compress,
|
||||||
),
|
),
|
||||||
session_auto_compress_threshold_kb: config
|
session_auto_compress_threshold_kb: config
|
||||||
.session
|
.session
|
||||||
@@ -373,8 +373,8 @@ impl ConfigDraft {
|
|||||||
// Built-in and in-memory configs are AllExplicit for
|
// Built-in and in-memory configs are AllExplicit for
|
||||||
// validation. The UI treats them as omitted defaults so
|
// validation. The UI treats them as omitted defaults so
|
||||||
// Reset All / Defaults does not badge every row Authored.
|
// Reset All / Defaults does not badge every row Authored.
|
||||||
wayscriber::config::KeybindingAuthorship::AllExplicit => {
|
KeybindingAuthorship::AllExplicit => {
|
||||||
wayscriber::config::KeybindingAuthorship::FromFile(Default::default())
|
KeybindingAuthorship::FromFile(Default::default())
|
||||||
}
|
}
|
||||||
from_file => from_file.clone(),
|
from_file => from_file.clone(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
mod from_config;
|
mod from_config;
|
||||||
|
|
||||||
use super::super::color::{ColorInput, ColorQuadInput};
|
use super::super::color::{ColorInput, ColorQuadInput};
|
||||||
use super::super::fields::ZoomChipDisplayOption;
|
|
||||||
use super::super::fields::{
|
use super::super::fields::{
|
||||||
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
|
ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption,
|
||||||
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
|
InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption,
|
||||||
@@ -9,7 +8,7 @@ use super::super::fields::{
|
|||||||
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
|
PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption,
|
||||||
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
|
ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption,
|
||||||
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
|
ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption,
|
||||||
UiThemeOption,
|
UiThemeOption, ZoomChipDisplayOption,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "tablet-input")]
|
#[cfg(feature = "tablet-input")]
|
||||||
use super::super::fields::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption};
|
use super::super::fields::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption};
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ impl FontCycleDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set one row's family without allowing a blank or repeated identity.
|
/// Set one row's family without allowing a blank or repeated identity.
|
||||||
pub(crate) fn set(&mut self, index: usize, family: String) -> Result<bool, FontCycleEditError> {
|
pub(crate) fn set(&mut self, index: usize, family: &str) -> Result<bool, FontCycleEditError> {
|
||||||
let Some(current) = self.entries.get(index) else {
|
let Some(current) = self.entries.get(index) else {
|
||||||
return Err(FontCycleEditError::MissingEntry);
|
return Err(FontCycleEditError::MissingEntry);
|
||||||
};
|
};
|
||||||
@@ -202,12 +202,12 @@ mod tests {
|
|||||||
let mut draft = draft(&["Sans", "Serif"]);
|
let mut draft = draft(&["Sans", "Serif"]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
draft.set(1, "sans".to_string()),
|
draft.set(1, "sans"),
|
||||||
Err(FontCycleEditError::DuplicateFamily("sans".to_string()))
|
Err(FontCycleEditError::DuplicateFamily("sans".to_string()))
|
||||||
);
|
);
|
||||||
assert_eq!(draft.entries(), ["Sans", "Serif"]);
|
assert_eq!(draft.entries(), ["Sans", "Serif"]);
|
||||||
|
|
||||||
assert_eq!(draft.set(1, "Monospace".to_string()), Ok(true));
|
assert_eq!(draft.set(1, "Monospace"), Ok(true));
|
||||||
assert_eq!(draft.entries(), ["Sans", "Monospace"]);
|
assert_eq!(draft.entries(), ["Sans", "Monospace"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,14 +215,8 @@ mod tests {
|
|||||||
fn blank_and_missing_row_edits_are_rejected() {
|
fn blank_and_missing_row_edits_are_rejected() {
|
||||||
let mut draft = draft(&["Sans"]);
|
let mut draft = draft(&["Sans"]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(draft.set(0, " "), Err(FontCycleEditError::BlankFamily));
|
||||||
draft.set(0, " ".to_string()),
|
assert_eq!(draft.set(4, "Serif"), Err(FontCycleEditError::MissingEntry));
|
||||||
Err(FontCycleEditError::BlankFamily)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
draft.set(4, "Serif".to_string()),
|
|
||||||
Err(FontCycleEditError::MissingEntry)
|
|
||||||
);
|
|
||||||
assert_eq!(draft.entries(), ["Sans"]);
|
assert_eq!(draft.entries(), ["Sans"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,13 +82,17 @@ impl ConfigDraft {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use wayscriber::config::{PERFORMANCE_FIELD_METADATA, PerformanceFieldId, ScalarConstraint};
|
use wayscriber::config::{
|
||||||
|
Config, PERFORMANCE_FIELD_METADATA, PerformanceFieldId, ScalarConstraint,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::models::{TextField, ToggleField};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_performance_descriptor_routes_through_a_typed_binding() {
|
fn every_performance_descriptor_routes_through_a_typed_binding() {
|
||||||
let mut draft = ConfigDraft::from_config(&wayscriber::config::Config::default());
|
let mut draft = ConfigDraft::from_config(&Config::default());
|
||||||
for metadata in PERFORMANCE_FIELD_METADATA {
|
for metadata in PERFORMANCE_FIELD_METADATA {
|
||||||
match metadata.constraint {
|
match metadata.constraint {
|
||||||
ScalarConstraint::Boolean => {
|
ScalarConstraint::Boolean => {
|
||||||
@@ -106,17 +110,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn typed_bindings_update_each_performance_draft_field() {
|
fn typed_bindings_update_each_performance_draft_field() {
|
||||||
let mut draft = ConfigDraft::from_config(&wayscriber::config::Config::default());
|
let mut draft = ConfigDraft::from_config(&Config::default());
|
||||||
draft.set_performance_choice(PerformanceFieldId::BufferCount, 4);
|
draft.set_performance_choice(PerformanceFieldId::BufferCount, 4);
|
||||||
draft.set_toggle(crate::models::ToggleField::PerformanceVsync, true);
|
draft.set_toggle(ToggleField::PerformanceVsync, true);
|
||||||
draft.set_text(
|
draft.set_text(TextField::PerformanceMaxFpsNoVsync, "144".to_string());
|
||||||
crate::models::TextField::PerformanceMaxFpsNoVsync,
|
draft.set_text(TextField::PerformanceUiAnimationFps, "60".to_string());
|
||||||
"144".to_string(),
|
|
||||||
);
|
|
||||||
draft.set_text(
|
|
||||||
crate::models::TextField::PerformanceUiAnimationFps,
|
|
||||||
"60".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(draft.performance_buffer_count, 4);
|
assert_eq!(draft.performance_buffer_count, 4);
|
||||||
assert!(draft.performance_enable_vsync);
|
assert!(draft.performance_enable_vsync);
|
||||||
@@ -126,7 +124,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn conversion_rejects_values_outside_shared_performance_constraints() {
|
fn conversion_rejects_values_outside_shared_performance_constraints() {
|
||||||
let base = wayscriber::config::Config::default();
|
let base = Config::default();
|
||||||
let mut draft = ConfigDraft::from_config(&base);
|
let mut draft = ConfigDraft::from_config(&base);
|
||||||
draft.performance_buffer_count = 5;
|
draft.performance_buffer_count = 5;
|
||||||
draft.performance_ui_animation_fps = "241".to_string();
|
draft.performance_ui_animation_fps = "241".to_string();
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ pub struct PresetsDraft {
|
|||||||
impl PresetsDraft {
|
impl PresetsDraft {
|
||||||
pub fn from_config(config: &Config) -> Self {
|
pub fn from_config(config: &Config) -> Self {
|
||||||
let slots = (1..=PRESET_SLOTS_MAX)
|
let slots = (1..=PRESET_SLOTS_MAX)
|
||||||
.map(|slot| PresetSlotDraft::from_config(config.presets.get_slot(slot), config))
|
.map(|slot| PresetSlotDraft::from_config(config.presets.slot(slot), config))
|
||||||
.collect();
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
slot_count: config.presets.slot_count,
|
slot_count: config.presets.slot_count,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::{fmt, iter};
|
||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use wayscriber::config::{
|
use wayscriber::config::{
|
||||||
@@ -56,8 +59,8 @@ impl RenderProfileExportOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for RenderProfileExportOption {
|
impl Display for RenderProfileExportOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,7 @@ pub enum RenderProfileSelectionOption {
|
|||||||
|
|
||||||
impl RenderProfileSelectionOption {
|
impl RenderProfileSelectionOption {
|
||||||
pub fn list(profile_ids: &[String]) -> Vec<Self> {
|
pub fn list(profile_ids: &[String]) -> Vec<Self> {
|
||||||
std::iter::once(Self::Off)
|
iter::once(Self::Off)
|
||||||
.chain(profile_ids.iter().cloned().map(Self::Profile))
|
.chain(profile_ids.iter().cloned().map(Self::Profile))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -84,8 +87,8 @@ impl RenderProfileSelectionOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for RenderProfileSelectionOption {
|
impl Display for RenderProfileSelectionOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Off => f.write_str("Off"),
|
Self::Off => f.write_str("Off"),
|
||||||
Self::Profile(id) => f.write_str(id),
|
Self::Profile(id) => f.write_str(id),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use wayscriber::config;
|
||||||
|
|
||||||
use super::super::fields::{
|
use super::super::fields::{
|
||||||
DragColorOption, DragMouseButton, DragToolField, DragToolOption, FontStyleOption,
|
DragColorOption, DragMouseButton, DragToolField, DragToolOption, FontStyleOption,
|
||||||
FontWeightOption, OverrideOption, QuadField, TextField, ToggleField, ToolbarLayoutModeOption,
|
FontWeightOption, OverrideOption, QuadField, TextField, ToggleField, ToolbarLayoutModeOption,
|
||||||
@@ -19,7 +21,7 @@ impl ConfigDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_toolbar_section_visibility(&mut self) {
|
fn refresh_toolbar_section_visibility(&mut self) {
|
||||||
let visibility = wayscriber::config::resolve_section_visibility(
|
let visibility = config::resolve_section_visibility(
|
||||||
self.ui_toolbar_layout_mode.to_mode(),
|
self.ui_toolbar_layout_mode.to_mode(),
|
||||||
&self.ui_toolbar_mode_overrides.to_config(),
|
&self.ui_toolbar_mode_overrides.to_config(),
|
||||||
&self.ui_toolbar_items.resolved(),
|
&self.ui_toolbar_items.resolved(),
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
use crate::models::KeybindingField;
|
||||||
|
use crate::{test_env, test_temp};
|
||||||
|
|
||||||
|
use wayscriber::config::{
|
||||||
|
BoardGridConfig, BoardGridKindConfig, BoardsConfig, RegionPicker, ToolbarRebindModifier,
|
||||||
|
TrayIconStyle,
|
||||||
|
};
|
||||||
|
use wayscriber::domain::BoardGridKind;
|
||||||
|
use wayscriber::draw::ArrowStyle;
|
||||||
|
use wayscriber::input::DragBindableTool;
|
||||||
|
use wayscriber::{config, domain, i18n};
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use super::super::color::ColorInput;
|
use super::super::color::ColorInput;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -95,16 +109,16 @@ fn shape_pen_recognition_feedback_round_trips() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn board_grid_draft_preserves_patterns_and_rejects_invalid_spacing() {
|
fn board_grid_draft_preserves_patterns_and_rejects_invalid_spacing() {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
let mut boards = wayscriber::config::BoardsConfig::default();
|
let mut boards = BoardsConfig::default();
|
||||||
boards.items[1].grid = wayscriber::config::BoardGridConfig {
|
boards.items[1].grid = BoardGridConfig {
|
||||||
kind: wayscriber::config::BoardGridKindConfig::IsometricDots,
|
kind: BoardGridKindConfig::IsometricDots,
|
||||||
spacing: 20,
|
spacing: 20,
|
||||||
};
|
};
|
||||||
config.boards = Some(boards);
|
config.boards = Some(boards);
|
||||||
let mut draft = ConfigDraft::from_config(&config);
|
let mut draft = ConfigDraft::from_config(&config);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
draft.boards.items[1].grid_kind,
|
draft.boards.items[1].grid_kind,
|
||||||
wayscriber::domain::BoardGridKind::IsometricDots
|
BoardGridKind::IsometricDots
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
draft.to_config(&config).unwrap().boards.unwrap().items[1].grid,
|
draft.to_config(&config).unwrap().boards.unwrap().items[1].grid,
|
||||||
@@ -222,7 +236,7 @@ use super::super::fields::{
|
|||||||
#[test]
|
#[test]
|
||||||
fn config_draft_round_trips_toolbar_rebind_modifier() {
|
fn config_draft_round_trips_toolbar_rebind_modifier() {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.ui.toolbar.rebind_modifier = wayscriber::config::ToolbarRebindModifier::CtrlAlt;
|
config.ui.toolbar.rebind_modifier = ToolbarRebindModifier::CtrlAlt;
|
||||||
let mut draft = ConfigDraft::from_config(&config);
|
let mut draft = ConfigDraft::from_config(&config);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
draft.ui_toolbar_rebind_modifier,
|
draft.ui_toolbar_rebind_modifier,
|
||||||
@@ -235,7 +249,7 @@ fn config_draft_round_trips_toolbar_rebind_modifier() {
|
|||||||
.expect("toolbar modifier round trip");
|
.expect("toolbar modifier round trip");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
round_trip.ui.toolbar.rebind_modifier,
|
round_trip.ui.toolbar.rebind_modifier,
|
||||||
wayscriber::config::ToolbarRebindModifier::ShiftAlt
|
ToolbarRebindModifier::ShiftAlt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,13 +259,13 @@ fn config_draft_round_trips_arrow_style() {
|
|||||||
// that sets a style loads as Standard in the UI, and saving anything from
|
// that sets a style loads as Standard in the UI, and saving anything from
|
||||||
// any tab writes that Standard back over the user's choice.
|
// any tab writes that Standard back over the user's choice.
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.arrow.style = wayscriber::draw::ArrowStyle::Curved;
|
config.arrow.style = ArrowStyle::Curved;
|
||||||
let mut draft = ConfigDraft::from_config(&config);
|
let mut draft = ConfigDraft::from_config(&config);
|
||||||
assert_eq!(draft.arrow_style, ArrowStyleOption::Curved);
|
assert_eq!(draft.arrow_style, ArrowStyleOption::Curved);
|
||||||
|
|
||||||
draft.arrow_style = ArrowStyleOption::Double;
|
draft.arrow_style = ArrowStyleOption::Double;
|
||||||
let round_trip = draft.to_config(&config).expect("arrow style round trip");
|
let round_trip = draft.to_config(&config).expect("arrow style round trip");
|
||||||
assert_eq!(round_trip.arrow.style, wayscriber::draw::ArrowStyle::Double);
|
assert_eq!(round_trip.arrow.style, ArrowStyle::Double);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -347,10 +361,7 @@ fn config_draft_round_trips_region_capture_settings() {
|
|||||||
let round_trip = draft
|
let round_trip = draft
|
||||||
.to_config(&config)
|
.to_config(&config)
|
||||||
.expect("region capture settings should round trip");
|
.expect("region capture settings should round trip");
|
||||||
assert_eq!(
|
assert_eq!(round_trip.capture.region.picker, RegionPicker::Slurp);
|
||||||
round_trip.capture.region.picker,
|
|
||||||
wayscriber::config::RegionPicker::Slurp
|
|
||||||
);
|
|
||||||
assert!(!round_trip.capture.region.show_size_readout);
|
assert!(!round_trip.capture.region.show_size_readout);
|
||||||
assert!(round_trip.capture.region.show_loupe);
|
assert!(round_trip.capture.region.show_loupe);
|
||||||
assert!(!round_trip.capture.region.show_legend);
|
assert!(!round_trip.capture.region.show_legend);
|
||||||
@@ -508,13 +519,13 @@ fn config_draft_rejects_path_escaping_save_names() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() {
|
fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() {
|
||||||
let temp = crate::test_temp::tempdir().expect("create temp directory");
|
let temp = test_temp::tempdir().expect("create temp directory");
|
||||||
let path = temp.path().join("config.toml");
|
let path = temp.path().join("config.toml");
|
||||||
let original = format!(
|
let original = format!(
|
||||||
"config_revision = {}\n# intentionally sparse\n",
|
"config_revision = {}\n# intentionally sparse\n",
|
||||||
wayscriber::config::CURRENT_CONFIG_REVISION
|
config::CURRENT_CONFIG_REVISION
|
||||||
);
|
);
|
||||||
std::fs::write(&path, &original).expect("write sparse config");
|
fs::write(&path, &original).expect("write sparse config");
|
||||||
let document = ConfigDocument::load_from_path(&path).expect("load sparse config");
|
let document = ConfigDocument::load_from_path(&path).expect("load sparse config");
|
||||||
let draft = ConfigDraft::from_config(document.config());
|
let draft = ConfigDraft::from_config(document.config());
|
||||||
let updated = draft
|
let updated = draft
|
||||||
@@ -526,7 +537,7 @@ fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() {
|
|||||||
.expect("save untouched sparse draft");
|
.expect("save untouched sparse draft");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(path).expect("read sparse config"),
|
fs::read_to_string(path).expect("read sparse config"),
|
||||||
original
|
original
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -559,9 +570,9 @@ fn config_draft_round_trips_precise_floats_without_truncation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sparse_configurator_edit_adds_only_the_edited_field_path() {
|
fn sparse_configurator_edit_adds_only_the_edited_field_path() {
|
||||||
let temp = crate::test_temp::tempdir().expect("create temp directory");
|
let temp = test_temp::tempdir().expect("create temp directory");
|
||||||
let path = temp.path().join("config.toml");
|
let path = temp.path().join("config.toml");
|
||||||
std::fs::write(&path, "# intentionally sparse\n").expect("write sparse config");
|
fs::write(&path, "# intentionally sparse\n").expect("write sparse config");
|
||||||
let document = ConfigDocument::load_from_path(&path).expect("load sparse config");
|
let document = ConfigDocument::load_from_path(&path).expect("load sparse config");
|
||||||
let mut draft = ConfigDraft::from_config(document.config());
|
let mut draft = ConfigDraft::from_config(document.config());
|
||||||
draft.performance_max_fps_no_vsync = "144".to_string();
|
draft.performance_max_fps_no_vsync = "144".to_string();
|
||||||
@@ -573,7 +584,7 @@ fn sparse_configurator_edit_adds_only_the_edited_field_path() {
|
|||||||
.save_with_backup(updated)
|
.save_with_backup(updated)
|
||||||
.expect("save sparse draft edit");
|
.expect("save sparse draft edit");
|
||||||
|
|
||||||
let saved = std::fs::read_to_string(path).expect("read sparse config");
|
let saved = fs::read_to_string(path).expect("read sparse config");
|
||||||
assert!(saved.contains("# intentionally sparse"));
|
assert!(saved.contains("# intentionally sparse"));
|
||||||
assert!(saved.contains("[performance]"));
|
assert!(saved.contains("[performance]"));
|
||||||
assert!(saved.contains("max_fps_no_vsync = 144"));
|
assert!(saved.contains("max_fps_no_vsync = 144"));
|
||||||
@@ -634,13 +645,13 @@ fn config_draft_round_trips_quick_colors() {
|
|||||||
/// would come back on the next save.
|
/// would come back on the next save.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_config_saved_in_chinese_keeps_its_font_and_color_names() {
|
fn a_config_saved_in_chinese_keeps_its_font_and_color_names() {
|
||||||
crate::test_env::in_language_child(
|
test_env::in_language_child(
|
||||||
concat!(
|
concat!(
|
||||||
module_path!(),
|
module_path!(),
|
||||||
"::a_config_saved_in_chinese_keeps_its_font_and_color_names"
|
"::a_config_saved_in_chinese_keeps_its_font_and_color_names"
|
||||||
),
|
),
|
||||||
|| {
|
|| {
|
||||||
wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese);
|
i18n::set_language(domain::Language::SimplifiedChinese);
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.drawing.font_style = "italic".to_string();
|
config.drawing.font_style = "italic".to_string();
|
||||||
config.drawing.font_weight = "light".to_string();
|
config.drawing.font_weight = "light".to_string();
|
||||||
@@ -687,7 +698,7 @@ fn config_draft_preserves_implicit_quick_color_defaults() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file() {
|
fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file() {
|
||||||
let temp = crate::test_temp::tempdir().expect("create temp directory");
|
let temp = test_temp::tempdir().expect("create temp directory");
|
||||||
let path = temp.path().join("config.toml");
|
let path = temp.path().join("config.toml");
|
||||||
let original = format!(
|
let original = format!(
|
||||||
r#"config_revision = {}
|
r#"config_revision = {}
|
||||||
@@ -695,9 +706,9 @@ fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file(
|
|||||||
label = "Only configured color"
|
label = "Only configured color"
|
||||||
color = "blue"
|
color = "blue"
|
||||||
"#,
|
"#,
|
||||||
wayscriber::config::CURRENT_CONFIG_REVISION
|
config::CURRENT_CONFIG_REVISION
|
||||||
);
|
);
|
||||||
std::fs::write(&path, &original).expect("write sparse quick colors");
|
fs::write(&path, &original).expect("write sparse quick colors");
|
||||||
let document = ConfigDocument::load_from_path(&path).expect("load sparse quick colors");
|
let document = ConfigDocument::load_from_path(&path).expect("load sparse quick colors");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
document
|
document
|
||||||
@@ -723,7 +734,7 @@ color = "blue"
|
|||||||
.save_with_backup(updated)
|
.save_with_backup(updated)
|
||||||
.expect("save sparse quick colors");
|
.expect("save sparse quick colors");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(path).expect("read sparse quick colors"),
|
fs::read_to_string(path).expect("read sparse quick colors"),
|
||||||
original
|
original
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1112,7 +1123,7 @@ fn config_draft_round_trips_render_profiles() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn config_draft_round_trips_board_pan_settings() {
|
fn config_draft_round_trips_board_pan_settings() {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.boards = Some(wayscriber::config::BoardsConfig {
|
config.boards = Some(BoardsConfig {
|
||||||
pan_enabled: false,
|
pan_enabled: false,
|
||||||
show_pan_badge: false,
|
show_pan_badge: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -1380,7 +1391,7 @@ fn section_toggle_replaces_overlay_item_override_on_save() {
|
|||||||
.contains(&ToolbarSectionFlag::Presets.item_id())
|
.contains(&ToolbarSectionFlag::Presets.item_id())
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
wayscriber::config::resolve_section_visibility(
|
config::resolve_section_visibility(
|
||||||
saved.ui.toolbar.layout_mode,
|
saved.ui.toolbar.layout_mode,
|
||||||
&saved.ui.toolbar.mode_overrides,
|
&saved.ui.toolbar.mode_overrides,
|
||||||
&resolved,
|
&resolved,
|
||||||
@@ -1580,7 +1591,7 @@ fn config_draft_round_trips_presets_and_history() {
|
|||||||
config.history.custom_redo_steps
|
config.history.custom_redo_steps
|
||||||
);
|
);
|
||||||
assert_eq!(round_trip.presets.slot_count, config.presets.slot_count);
|
assert_eq!(round_trip.presets.slot_count, config.presets.slot_count);
|
||||||
assert_eq!(round_trip.presets.get_slot(1), config.presets.get_slot(1));
|
assert_eq!(round_trip.presets.slot(1), config.presets.slot(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1626,7 +1637,7 @@ fn preset_tool_change_loads_selected_tool_profile_values() {
|
|||||||
let round_trip = draft
|
let round_trip = draft
|
||||||
.to_config(&config)
|
.to_config(&config)
|
||||||
.expect("expected config to round trip");
|
.expect("expected config to round trip");
|
||||||
let preset = round_trip.presets.get_slot(1).expect("preset");
|
let preset = round_trip.presets.slot(1).expect("preset");
|
||||||
let settings = preset.tool_settings.as_ref().expect("tool settings");
|
let settings = preset.tool_settings.as_ref().expect("tool settings");
|
||||||
|
|
||||||
assert_eq!(preset.tool, Tool::Marker);
|
assert_eq!(preset.tool, Tool::Marker);
|
||||||
@@ -1683,7 +1694,7 @@ fn preset_visible_edits_update_selected_tool_profile_only() {
|
|||||||
let round_trip = draft
|
let round_trip = draft
|
||||||
.to_config(&config)
|
.to_config(&config)
|
||||||
.expect("expected config to round trip");
|
.expect("expected config to round trip");
|
||||||
let preset = round_trip.presets.get_slot(1).expect("preset");
|
let preset = round_trip.presets.slot(1).expect("preset");
|
||||||
let settings = preset.tool_settings.as_ref().expect("tool settings");
|
let settings = preset.tool_settings.as_ref().expect("tool settings");
|
||||||
|
|
||||||
assert_eq!(preset.tool, Tool::Marker);
|
assert_eq!(preset.tool, Tool::Marker);
|
||||||
@@ -1701,11 +1712,11 @@ fn preset_visible_edits_update_selected_tool_profile_only() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn config_draft_round_trips_drag_tool_mapping() {
|
fn config_draft_round_trips_drag_tool_mapping() {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.drawing.drag_tool = wayscriber::input::DragBindableTool::Arrow;
|
config.drawing.drag_tool = DragBindableTool::Arrow;
|
||||||
config.drawing.shift_drag_tool = wayscriber::input::DragBindableTool::Eraser;
|
config.drawing.shift_drag_tool = DragBindableTool::Eraser;
|
||||||
config.drawing.ctrl_drag_tool = wayscriber::input::DragBindableTool::Pen;
|
config.drawing.ctrl_drag_tool = DragBindableTool::Pen;
|
||||||
config.drawing.ctrl_shift_drag_tool = wayscriber::input::DragBindableTool::Rect;
|
config.drawing.ctrl_shift_drag_tool = DragBindableTool::Rect;
|
||||||
config.drawing.tab_drag_tool = wayscriber::input::DragBindableTool::Ellipse;
|
config.drawing.tab_drag_tool = DragBindableTool::Ellipse;
|
||||||
let mut drag_tools = config.drawing.effective_drag_tools();
|
let mut drag_tools = config.drawing.effective_drag_tools();
|
||||||
drag_tools.right.drag_tool = DragTool::Pen;
|
drag_tools.right.drag_tool = DragTool::Pen;
|
||||||
drag_tools.right.drag_color = Some(ColorSpec::Name("blue".to_string()));
|
drag_tools.right.drag_color = Some(ColorSpec::Name("blue".to_string()));
|
||||||
@@ -1826,10 +1837,10 @@ fn config_draft_round_trips_ui_language() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_draft_ui_language_defaults_to_auto_when_key_missing() {
|
fn config_draft_ui_language_defaults_to_auto_when_key_missing() {
|
||||||
let temp = crate::test_temp::tempdir().expect("create temp directory");
|
let temp = test_temp::tempdir().expect("create temp directory");
|
||||||
|
|
||||||
let missing = temp.path().join("missing-language.toml");
|
let missing = temp.path().join("missing-language.toml");
|
||||||
std::fs::write(&missing, "[ui]\ntheme = \"dark\"\n").expect("write config without language");
|
fs::write(&missing, "[ui]\ntheme = \"dark\"\n").expect("write config without language");
|
||||||
let document = ConfigDocument::load_from_path(&missing).expect("load config without language");
|
let document = ConfigDocument::load_from_path(&missing).expect("load config without language");
|
||||||
assert_eq!(document.config().ui.language, Language::Auto);
|
assert_eq!(document.config().ui.language, Language::Auto);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1843,10 +1854,10 @@ fn config_draft_ui_language_defaults_to_auto_when_key_missing() {
|
|||||||
/// of reaching the draft as a silently rewritten value.
|
/// of reaching the draft as a silently rewritten value.
|
||||||
#[test]
|
#[test]
|
||||||
fn config_document_salvages_unknown_ui_language_to_auto() {
|
fn config_document_salvages_unknown_ui_language_to_auto() {
|
||||||
let temp = crate::test_temp::tempdir().expect("create temp directory");
|
let temp = test_temp::tempdir().expect("create temp directory");
|
||||||
|
|
||||||
let unknown = temp.path().join("unknown-language.toml");
|
let unknown = temp.path().join("unknown-language.toml");
|
||||||
std::fs::write(&unknown, "[ui]\nlanguage = \"fr\"\n").expect("write unknown language");
|
fs::write(&unknown, "[ui]\nlanguage = \"fr\"\n").expect("write unknown language");
|
||||||
|
|
||||||
let (document, warning) = ConfigDocument::load_for_editing_from_path(&unknown)
|
let (document, warning) = ConfigDocument::load_for_editing_from_path(&unknown)
|
||||||
.expect("expected the editing load to salvage the unknown language");
|
.expect("expected the editing load to salvage the unknown language");
|
||||||
@@ -1895,39 +1906,31 @@ fn config_draft_keeps_the_documents_revision_until_a_migration_is_applied() {
|
|||||||
.expect("expected config to round trip");
|
.expect("expected config to round trip");
|
||||||
assert_eq!(untouched.config_revision, 0);
|
assert_eq!(untouched.config_revision, 0);
|
||||||
|
|
||||||
draft.config_revision = Some(wayscriber::config::CURRENT_CONFIG_REVISION);
|
draft.config_revision = Some(config::CURRENT_CONFIG_REVISION);
|
||||||
let migrated = draft
|
let migrated = draft
|
||||||
.to_config(&base)
|
.to_config(&base)
|
||||||
.expect("expected config to round trip");
|
.expect("expected config to round trip");
|
||||||
assert_eq!(
|
assert_eq!(migrated.config_revision, config::CURRENT_CONFIG_REVISION);
|
||||||
migrated.config_revision,
|
|
||||||
wayscriber::config::CURRENT_CONFIG_REVISION
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_draft_preserves_tray_icon_style() {
|
fn config_draft_preserves_tray_icon_style() {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.tray.icon_style = wayscriber::config::TrayIconStyle::Colored;
|
config.tray.icon_style = TrayIconStyle::Colored;
|
||||||
|
|
||||||
let draft = ConfigDraft::from_config(&config);
|
let draft = ConfigDraft::from_config(&config);
|
||||||
let round_trip = draft
|
let round_trip = draft
|
||||||
.to_config(&config)
|
.to_config(&config)
|
||||||
.expect("expected config to round trip");
|
.expect("expected config to round trip");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(round_trip.tray.icon_style, TrayIconStyle::Colored);
|
||||||
round_trip.tray.icon_style,
|
|
||||||
wayscriber::config::TrayIconStyle::Colored
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn compiled_defaults_are_not_shown_as_authored_keybindings() {
|
fn compiled_defaults_are_not_shown_as_authored_keybindings() {
|
||||||
let draft = ConfigDraft::from_config(&Config::default());
|
let draft = ConfigDraft::from_config(&Config::default());
|
||||||
assert!(
|
assert!(
|
||||||
!draft
|
!draft.keybindings.is_authored(KeybindingField::Undo),
|
||||||
.keybindings
|
|
||||||
.is_authored(crate::models::KeybindingField::Undo),
|
|
||||||
"Config::default() is AllExplicit for validation, not for UI source badges"
|
"Config::default() is AllExplicit for validation, not for UI source badges"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ use super::super::parse::{
|
|||||||
parse_field_in_range, parse_u8_in_range, parse_usize_at_least, parse_usize_in_range,
|
parse_field_in_range, parse_u8_in_range, parse_usize_at_least, parse_usize_in_range,
|
||||||
};
|
};
|
||||||
use crate::models::error::FormError;
|
use crate::models::error::FormError;
|
||||||
use wayscriber::config::MAX_SHAPE_RECOGNITION_SENSITIVITY;
|
|
||||||
use wayscriber::config::{
|
use wayscriber::config::{
|
||||||
ARROW_ANGLE_MAX, ARROW_ANGLE_MIN, ARROW_LENGTH_MAX, ARROW_LENGTH_MIN, Config,
|
ARROW_ANGLE_MAX, ARROW_ANGLE_MIN, ARROW_LENGTH_MAX, ARROW_LENGTH_MIN, Config,
|
||||||
|
MAX_SHAPE_RECOGNITION_SENSITIVITY,
|
||||||
};
|
};
|
||||||
use wayscriber::domain::{DragBindableTool, DragTool};
|
use wayscriber::domain::{DragBindableTool, DragTool, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS};
|
||||||
use wayscriber::domain::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS};
|
|
||||||
use wayscriber::draw::{MAX_PEN_SMOOTHING, REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES};
|
use wayscriber::draw::{MAX_PEN_SMOOTHING, REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES};
|
||||||
|
|
||||||
impl ConfigDraft {
|
impl ConfigDraft {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use wayscriber::config;
|
||||||
|
|
||||||
use super::super::draft::ConfigDraft;
|
use super::super::draft::ConfigDraft;
|
||||||
use super::super::parse::{
|
use super::super::parse::{
|
||||||
parse_field, parse_field_in_range, parse_u64_field, parse_u64_in_range, parse_usize_field,
|
parse_field, parse_field_in_range, parse_u64_field, parse_u64_in_range, parse_usize_field,
|
||||||
@@ -65,7 +67,7 @@ impl ConfigDraft {
|
|||||||
config.ui.toolbar.stroke_controls = self.ui_toolbar_stroke_controls.to_config();
|
config.ui.toolbar.stroke_controls = self.ui_toolbar_stroke_controls.to_config();
|
||||||
config.ui.toolbar.mode_overrides = self.ui_toolbar_mode_overrides.to_config();
|
config.ui.toolbar.mode_overrides = self.ui_toolbar_mode_overrides.to_config();
|
||||||
config.ui.toolbar.items = self.ui_toolbar_items.clone();
|
config.ui.toolbar.items = self.ui_toolbar_items.clone();
|
||||||
let visibility = wayscriber::config::resolve_section_visibility(
|
let visibility = config::resolve_section_visibility(
|
||||||
config.ui.toolbar.layout_mode,
|
config.ui.toolbar.layout_mode,
|
||||||
&config.ui.toolbar.mode_overrides,
|
&config.ui.toolbar.mode_overrides,
|
||||||
&config.ui.toolbar.items.resolved(),
|
&config.ui.toolbar.items.resolved(),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use wayscriber::env_vars::{
|
use wayscriber::env_vars::{
|
||||||
@@ -18,11 +20,11 @@ pub enum DesktopEnvironment {
|
|||||||
|
|
||||||
impl DesktopEnvironment {
|
impl DesktopEnvironment {
|
||||||
pub fn detect_current() -> Self {
|
pub fn detect_current() -> Self {
|
||||||
if std::env::var_os(HYPRLAND_INSTANCE_SIGNATURE_ENV).is_some() {
|
if env::var_os(HYPRLAND_INSTANCE_SIGNATURE_ENV).is_some() {
|
||||||
return Self::Hyprland;
|
return Self::Hyprland;
|
||||||
}
|
}
|
||||||
let current = std::env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default();
|
let current = env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default();
|
||||||
let session = std::env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default();
|
let session = env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default();
|
||||||
Self::from_desktop_strings(¤t, &session)
|
Self::from_desktop_strings(¤t, &session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use wayscriber::draw::ArrowStyle;
|
use wayscriber::draw::ArrowStyle;
|
||||||
@@ -49,8 +52,8 @@ impl ArrowStyleOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ArrowStyleOption {
|
impl Display for ArrowStyleOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use wayscriber::config::RegionPicker;
|
use wayscriber::config::RegionPicker;
|
||||||
@@ -35,8 +38,8 @@ impl RegionPickerOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for RegionPickerOption {
|
impl Display for RegionPickerOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use wayscriber::domain::EraserMode;
|
use wayscriber::domain::EraserMode;
|
||||||
@@ -36,8 +39,8 @@ impl EraserModeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for EraserModeOption {
|
impl Display for EraserModeOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,8 +82,8 @@ impl PresetEraserKindOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PresetEraserKindOption {
|
impl Display for PresetEraserKindOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,8 +125,8 @@ impl PresetEraserModeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PresetEraserModeOption {
|
impl Display for PresetEraserModeOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use wayscriber::i18n::tr;
|
use wayscriber::i18n::tr;
|
||||||
|
|
||||||
use wayscriber::config::{
|
use wayscriber::config::{
|
||||||
@@ -46,8 +49,8 @@ impl PdfPageSizeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PdfPageSizeOption {
|
impl Display for PdfPageSizeOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,8 +92,8 @@ impl PdfOrientationOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PdfOrientationOption {
|
impl Display for PdfOrientationOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,8 +139,8 @@ impl PdfFitModeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PdfFitModeOption {
|
impl Display for PdfFitModeOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,8 +178,8 @@ impl PdfTransparentBackgroundOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PdfTransparentBackgroundOption {
|
impl Display for PdfTransparentBackgroundOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,8 +235,8 @@ impl PdfLabelPositionOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PdfLabelPositionOption {
|
impl Display for PdfLabelPositionOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,8 +292,8 @@ impl PdfLabelContentModeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for PdfLabelContentModeOption {
|
impl Display for PdfLabelContentModeOption {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loaded 100 of 1262 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user