Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
- Preserve existing Rust module style. This repo intentionally mixes `foo.rs`, `foo/`, and `foo/mod.rs`.
- For Rust split modules, an `AGENTS.md` in `foo/` does not apply to sibling `foo.rs`; put shared rules in the parent guide.

## Code Readability
- Let `rustfmt` own mechanical Rust layout. Use standard Rust spacing: `call(value)`, `name: Type`, `T: Trait`, and `left + right`; omit spaces inside parentheses and around `::`, `.`, `?`, and ranges, and omit unnecessary control-flow parentheses.
- Separate logical steps within functions with one blank line: validation and early exits, setup, execution or mutation, state and effect publication, and returning or mapping the result. Keep closely related statements together; add spacing when the purpose changes rather than after every statement.
- Keep a `let ... else`, `match`, or parsed value with its immediate validation and error path. Use `?` for ordinary error propagation instead of expanding it solely to imitate another language's guard style.
- Let `rustfmt` decide wrapping, same-line braces, `} else {`, indentation, and multiline collection or argument layout. Do not use formatter-skip attributes to force a competing layout.
- Apply this grouping consistently to production Rust and tests. Preserve intentional blank lines while editing and formatting.
- 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.

## Coupled Changes
- Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests.
- Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests.
Expand All @@ -29,6 +38,7 @@
## Validation
- Full local CI is `./tools/lint-and-test.sh`.
- That script runs version/package checks, `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests.
- Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well.
- For docs-only `AGENTS.md` edits, make new files visible to Git before whitespace checks, for example `rg --files --hidden -g AGENTS.md -0 | xargs -0 git add -N --` followed by `git diff --check`.
- On PowerShell, use `rg --files --hidden -g AGENTS.md | ForEach-Object { git add -N -- $_ }` followed by `git diff --check`.
- If you do not want to alter the index, run an explicit trailing-whitespace check across the untracked `AGENTS.md` files instead of relying on plain `git diff --check`.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ The v0.9.23+ prebuilt `wayscriber` packages require glibc 2.39 and GTK 4.12. See

### Boards
- Named boards with transparent overlay or custom backgrounds
- Cartesian graph paper, isometric lines, and isometric dots with adjustable spacing
- Isolated pages per board with auto-contrast pens
- Pan solid boards with <kbd>Space</kbd> + left-drag; reset from the context menu
- Jump slots: <kbd>Ctrl+Shift+1..9</kbd>
Expand Down
5 changes: 5 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ board_new = ["Ctrl+Shift+N"]
board_duplicate = ["Ctrl+Shift+D"]
board_delete = ["Ctrl+Shift+Delete"]
board_picker = ["Ctrl+Shift+B"]
# Edit the active board's paper (unbound by default; also in the command palette)
board_paper_edit = []

# Page navigation
# Ubuntu/GNOME defaults avoid Ctrl+Alt workspace shortcuts (Ctrl+ArrowLeft/Right, Ctrl+PageUp/PageDown).
Expand Down Expand Up @@ -923,6 +925,9 @@ persist = true
[[boards.items]]
id = "whiteboard"
name = "Whiteboard"
# Paper: none, cartesian, isometric, or isometric-dots. Spacing: 8–200 logical px.
# Spacing is the square/triangle side length; 20 and 40 are convenient presets.
grid = { kind = "none", spacing = 40 }
background = { rgb = [0.992, 0.992, 0.992] }
# Tuned black #241F31; the built-in default bit-matches the "black" quick color
default_pen_color = { rgb = [0.141, 0.122, 0.192] }
Expand Down
1 change: 1 addition & 0 deletions configurator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ if its UI task is no longer observed.
- **Drawing, Arrow, Performance, UI, Board, Capture** – numeric fields with inline validation, toggles, and color editors (RGBA/RGB components).
- **Default color** – toggle between named colors and custom RGB triples.
- **Keybindings** – a bulk shortcut manager over the same per-action chips, recorder, and conflict flow. Filter by All / Changed / Conflicts / Unbound / Device / Sequences, sort by category, name, or changed status, and reset visible or all keybindings with confirmation (draft-only until Save). Review Conflicts walks each collision without picking a winner. `--open keybindings/<section>?search=...` still opens that category and now selects the matching action. Press-to-bind recording covers keys, auxiliary mouse buttons, and stylus barrel buttons, plus **Record Sequence** for two- or three-chord keyboard sequences (`Ctrl+K then Ctrl+C`). Per-row reset and a raw comma-separated text editor remain available (`F5, Ctrl+K > Ctrl+C`). Super/Meta chords record when the desktop delivers them. Legacy `[tablet.stylus_button]` assignments can be moved into the keybinding list with an explicit confirmation. Source badges mark Default, Authored, Legacy Tablet, and Unavailable shortcuts.
- **Boards** – edit new-session templates with Cartesian, isometric line, or isometric dot paper and 8–200 logical-pixel spacing. Transparent boards disable paper controls.
- **Session** – persistence settings plus named-session catalog management. Rename display labels, reveal files, and forget metadata without touching files. Clear Tool State preserves boards/history while removing persisted tool defaults. Duplicate, Move, Clear Tool State, and Clear are disabled while an overlay, manually started daemon, or background service is active.
- Live dirty-state indicator plus status banner for success/error details. Editing is temporarily disabled during loading and saving; failed operations restore editing and retain the draft.
- Non-fatal warnings list unrecognized config paths. Those values are preserved for forward compatibility instead of being deleted.
Expand Down
5 changes: 5 additions & 0 deletions configurator/src/app/pages/boards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
//! components to whatever the 8-bit hex said.

mod color;
mod grid;
mod header;
mod rows;
mod section;
Expand Down Expand Up @@ -207,6 +208,7 @@ fn add_board_list(page: &mut PageBuilder) {
let values = BoardValues {
id: &item.id,
name: &item.name,
grid_spacing: &item.grid_spacing,
background: ColorValues {
hex: picker_hex(app, ColorPickerId::BoardBackground(index)),
color: &item.background_color,
Expand Down Expand Up @@ -234,6 +236,7 @@ struct SectionLayout {
visible: bool,
expanded: bool,
background_kind: BoardBackgroundOption,
grid_kind: wayscriber::domain::BoardGridKind,
pen_enabled: bool,
auto_adjust: bool,
persist: bool,
Expand All @@ -256,6 +259,7 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec<Sec
// that stays folded shut is a match the user cannot read.
expanded: !show_all || !is_collapsed(app, index),
background_kind: item.background_kind,
grid_kind: item.grid_kind,
pen_enabled: item.default_pen_color.enabled,
auto_adjust: item.auto_adjust_pen,
persist: item.persist,
Expand All @@ -268,6 +272,7 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec<Sec
struct BoardValues<'a> {
id: &'a str,
name: &'a str,
grid_spacing: &'a str,
background: ColorValues<'a>,
pen: ColorValues<'a>,
}
Expand Down
56 changes: 56 additions & 0 deletions configurator/src/app/pages/boards/grid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use super::{
SectionLayout,
rows::{TextRow, build_text_row},
};
use crate::app::state::ConfiguratorApp;
use crate::messages::Message;
use crate::models::{BoardBackgroundOption, BoardItemTextField};
use adw::prelude::*;
use relm4::{ComponentSender, adw, gtk};
use wayscriber::domain::BoardGridKind;

pub(super) fn build(
index: usize,
layout: SectionLayout,
sender: &ComponentSender<ConfiguratorApp>,
) -> (adw::ComboRow, TextRow) {
let labels: Vec<_> = BoardGridKind::ALL.iter().map(|kind| kind.label()).collect();
let grid = adw::ComboRow::builder()
.title("Paper pattern")
.model(&gtk::StringList::new(&labels))
.visible(layout.expanded)
.build();
grid.set_selected(
BoardGridKind::ALL
.iter()
.position(|kind| *kind == layout.grid_kind)
.unwrap_or(0) as u32,
);
let solid = layout.background_kind != BoardBackgroundOption::Transparent;
grid.set_sensitive(solid);
if !solid {
grid.set_subtitle("Paper patterns require a solid board background");
}
let grid_sender = sender.clone();
grid.connect_selected_notify(move |row| {
if let Some(kind) = BoardGridKind::ALL.get(row.selected() as usize) {
grid_sender.input(Message::BoardsGridKindChanged(index, *kind));
}
});
let spacing_title = match layout.grid_kind {
BoardGridKind::None => "Grid spacing (8–200 logical pixels)",
BoardGridKind::Cartesian => "Square side (8–200 logical pixels)",
BoardGridKind::Isometric | BoardGridKind::IsometricDots => {
"Equilateral triangle side (8–200 logical pixels)"
}
};
let spacing = build_text_row(
spacing_title,
index,
BoardItemTextField::GridSpacing,
sender,
);
spacing.row.set_visible(layout.expanded);
spacing.row.set_sensitive(solid);
(grid, spacing)
}
18 changes: 18 additions & 0 deletions configurator/src/app/pages/boards/section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ fn build_section(
.set_visible(layout.expanded && layout.background_kind == BoardBackgroundOption::Color);
section.append(&background.row);

let (grid_kind, grid_spacing) = super::grid::build(index, layout, sender);
section.append(&grid_kind);
section.append(&grid_spacing.row);

let pen_enabled = adw::SwitchRow::builder()
.title("Override default pen color")
.active(layout.pen_enabled)
Expand Down Expand Up @@ -148,6 +152,20 @@ fn build_section(
set_text_blocked(&id.row, &id.handler, values.id);
set_text_blocked(&name.row, &name.handler, values.name);
background.refresh(&values.background);
set_text_blocked(
&grid_spacing.row,
&grid_spacing.handler,
values.grid_spacing,
);
let valid = values
.grid_spacing
.parse::<i64>()
.is_ok_and(|s| (8..=200).contains(&s));
if valid {
grid_spacing.row.remove_css_class("error");
} else {
grid_spacing.row.add_css_class("error");
}
pen.refresh(&values.pen);
});

Expand Down
4 changes: 3 additions & 1 deletion configurator/src/app/search/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,13 @@ fn board_matches(app: &ConfiguratorApp, query: &SearchQuery, summary: &mut TabSe
);
for (index, item) in app.draft.boards.items.iter().enumerate() {
let text = format!(
"board {} board id display name background background color override default pen color pen color auto-adjust pen auto adjust pen persist pinned duplicate remove up down collapse expand {} {} {} background pen persist pinned auto adjust",
"board {} board id display name background background color override default pen color pen color auto-adjust pen auto adjust pen persist pinned duplicate remove up down collapse expand {} {} {} background pen persist pinned auto adjust graph paper pattern grid spacing cartesian isometric dots spacing logical pixels {} {}",
index + 1,
item.id,
item.name,
item.background_kind.label(),
item.grid_kind.label(),
item.grid_spacing,
);
if query.matches_text(&text) {
summary.add_board_index(index);
Expand Down
9 changes: 8 additions & 1 deletion configurator/src/app/search/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,14 @@ fn startup_interaction_cancels_the_deferred_search_focus() {

#[test]
fn board_item_static_labels_match_board_rows() {
for query in ["display name", "board id", "override default pen color"] {
for query in [
"display name",
"board id",
"override default pen color",
"graph paper",
"isometric",
"grid spacing",
] {
let (mut app, _effects) = ConfiguratorApp::new_app();
app.search_query = SearchQuery::new(query);

Expand Down
12 changes: 12 additions & 0 deletions configurator/src/app/update/boards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ use super::super::effects::Effect;
use super::super::state::{ConfiguratorApp, StatusMessage};

impl ConfiguratorApp {
pub(super) fn handle_boards_grid_kind_changed(
&mut self,
index: usize,
value: wayscriber::domain::BoardGridKind,
) -> Vec<Effect> {
if let Some(item) = self.draft.boards.items.get_mut(index) {
item.grid_kind = value;
}
self.refresh_dirty_flag();
Vec::new()
}
pub(super) fn handle_boards_add_item(&mut self) -> Vec<Effect> {
self.status = StatusMessage::idle();
let new_item = self.draft.boards.new_item();
Expand Down Expand Up @@ -99,6 +110,7 @@ impl ConfiguratorApp {
let old_effective_id = self.draft.boards.effective_id_for_index(index);
if let Some(item) = self.draft.boards.items.get_mut(index) {
match field {
BoardItemTextField::GridSpacing => item.grid_spacing = value,
BoardItemTextField::Id => {
let trimmed = value.trim();
let new_effective_id = if trimmed.is_empty() {
Expand Down
3 changes: 3 additions & 0 deletions configurator/src/app/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ impl ConfiguratorApp {
Message::BoardsItemTextChanged(index, field, value) => {
self.handle_boards_item_text_changed(index, field, value)
}
Message::BoardsGridKindChanged(index, value) => {
self.handle_boards_grid_kind_changed(index, value)
}
Message::BoardsBackgroundKindChanged(index, value) => {
self.handle_boards_background_kind_changed(index, value)
}
Expand Down
1 change: 1 addition & 0 deletions configurator/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ pub enum Message {
BoardsCollapseToggled(usize),
BoardsDefaultChanged(String),
BoardsItemTextChanged(usize, BoardItemTextField, String),
BoardsGridKindChanged(usize, wayscriber::domain::BoardGridKind),
BoardsBackgroundKindChanged(usize, BoardBackgroundOption),
BoardsBackgroundColorChanged(usize, usize, String),
BoardsDefaultPenEnabledChanged(usize, bool),
Expand Down
3 changes: 3 additions & 0 deletions configurator/src/models/config/boards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ impl std::fmt::Display for BoardBackgroundOption {

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoardItemTextField {
GridSpacing,
Id,
Name,
}
Expand All @@ -53,6 +54,8 @@ pub struct BoardItemDraft {
pub name: String,
pub background_kind: BoardBackgroundOption,
pub background_color: ColorTripletInput,
pub grid_kind: wayscriber::domain::BoardGridKind,
pub grid_spacing: String,
pub default_pen_color: OptionalTripletInput,
pub auto_adjust_pen: bool,
pub persist: bool,
Expand Down
31 changes: 31 additions & 0 deletions configurator/src/models/config/boards/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ impl BoardItemDraft {
name: item.name.clone(),
background_kind,
background_color,
grid_kind: item.grid.kind.into(),
grid_spacing: item.grid.spacing.to_string(),
default_pen_color: OptionalTripletInput::from_option(
item.default_pen_color.as_ref(),
fallback_pen,
Expand Down Expand Up @@ -97,10 +99,37 @@ impl BoardItemDraft {
.default_pen_color
.to_option(&format!("boards.items[{index}].default_pen_color"), errors);

let spacing = match self.grid_spacing.trim().parse::<i64>() {
Ok(value)
if (i64::from(wayscriber::domain::BOARD_GRID_MIN_SPACING)
..=i64::from(wayscriber::domain::BOARD_GRID_MAX_SPACING))
.contains(&value) =>
{
value
}
_ => {
errors.push(FormError::new(
format!("boards.items[{index}].grid.spacing"),
"Enter a whole number from 8 to 200.",
));
return None;
}
};
let grid = wayscriber::config::BoardGridConfig {
kind: if background.is_transparent() {
wayscriber::domain::BoardGridKind::None
} else {
self.grid_kind
}
.into(),
spacing,
};

Some(BoardItemConfig {
id,
name,
background,
grid,
default_pen_color,
auto_adjust_pen: self.auto_adjust_pen,
persist: self.persist,
Expand Down Expand Up @@ -196,6 +225,8 @@ impl BoardsDraft {
name,
background_kind: BoardBackgroundOption::Color,
background_color: ColorTripletInput::from([0.992, 0.992, 0.992]),
grid_kind: wayscriber::domain::BoardGridKind::None,
grid_spacing: wayscriber::domain::BOARD_GRID_DEFAULT_SPACING.to_string(),
default_pen_color: OptionalTripletInput::from_option(
Some(&BoardColorConfig::Rgb([0.0, 0.0, 0.0])),
[0.0, 0.0, 0.0],
Expand Down
25 changes: 25 additions & 0 deletions configurator/src/models/config/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
use super::super::color::ColorInput;

#[test]
fn board_grid_draft_preserves_patterns_and_rejects_invalid_spacing() {
let mut config = Config::default();
let mut boards = wayscriber::config::BoardsConfig::default();
boards.items[1].grid = wayscriber::config::BoardGridConfig {
kind: wayscriber::config::BoardGridKindConfig::IsometricDots,
spacing: 20,
};
config.boards = Some(boards);
let mut draft = ConfigDraft::from_config(&config);
assert_eq!(
draft.boards.items[1].grid_kind,
wayscriber::domain::BoardGridKind::IsometricDots
);
assert_eq!(
draft.to_config(&config).unwrap().boards.unwrap().items[1].grid,
config.boards.as_ref().unwrap().items[1].grid
);
for invalid in ["", "-1", "7", "201", "20.5", "paper"] {
draft.boards.items[1].grid_spacing = invalid.to_string();
assert!(draft.to_config(&config).is_err(), "{invalid}");
assert_eq!(draft.boards.items[1].grid_spacing, invalid);
}
}
use super::super::fields::{
ArrowStyleOption, DragMouseButton, DragToolField, DragToolOption, FontWeightOption,
InputHudModeOption, InputHudPositionOption, OverrideOption, PdfFitModeOption,
Expand Down
1 change: 1 addition & 0 deletions configurator/src/models/keybindings/field/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const PREFERRED_ORDER: &[KeybindingField] = &[
KeybindingField::BoardDuplicate,
KeybindingField::BoardDelete,
KeybindingField::BoardPicker,
KeybindingField::BoardPaperEdit,
KeybindingField::ToggleHelp,
KeybindingField::ToggleQuickHelp,
KeybindingField::ToggleStatusBar,
Expand Down
Loading
Loading