Skip to content

Commit 1faaf39

Browse files
authored
Click Rate redesign: split delay vs. frequency + Taneth theme + CI/release fixes
Merged — see PR #1 for full details.
1 parent bddf737 commit 1faaf39

14 files changed

Lines changed: 754 additions & 90 deletions

.github/workflows/build-windows.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,8 @@ jobs:
2626
run: dotnet build windows/QuadClicker.csproj -c Release --no-restore
2727

2828
- name: Test
29-
run: dotnet test windows/Tests/QuadClicker.Tests.csproj --no-build -c Release -v normal
30-
# Note: test project builds separately since it references source files directly
31-
continue-on-error: false
32-
33-
- name: Test (build + run)
29+
# Test project Compile-Includes source files from the main project, so its
30+
# output dir is independent — let `dotnet test` handle build + run in one shot.
3431
run: dotnet test windows/Tests/QuadClicker.Tests.csproj -c Release -v normal
3532

3633
- name: Publish

.github/workflows/release.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ jobs:
1515
with:
1616
dotnet-version: '10.0.x'
1717
dotnet-quality: 'preview'
18-
- name: Build & Publish
19-
run: dotnet publish windows/QuadClicker.csproj -c Release -r win-x64 --self-contained false -o artifacts/windows
18+
- name: Build & Publish (self-contained single-file)
19+
run: dotnet publish windows/QuadClicker.csproj -c Release -r win-x64
20+
--self-contained true
21+
-p:PublishSingleFile=true
22+
-p:IncludeNativeLibrariesForSelfExtract=true
23+
-p:EnableCompressionInSingleFile=true
24+
-o artifacts/windows
2025
- uses: actions/upload-artifact@v4
2126
with:
2227
name: QuadClicker-win-x64

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ QuadClicker is the definitive open-source auto-clicker: fully configurable, scri
2323

2424
## Features
2525

26-
- **Click Rate**Enter any format: `100ms`, `10/s`, `10cps`, `600/min`, `600cpm`, or a bare integer (ms)
26+
- **Click Rate**Pick **Delay** (`ms` / `sec` / `min`) or **Frequency** (per sec / per min / per hour) via radio. CLI also accepts free-form: `100ms`, `5s`, `2min`, `10/s`, `10cps`, `600/min`, `600cpm`, `60/h`, `60cph`, or a bare integer (ms). Bounds: 1 ms ≤ delay ≤ 360 min.
2727
- **Mouse Button** — Left, Right, or Middle
2828
- **Click Type** — Single or Double (uses OS double-click interval)
2929
- **Location Modes**
@@ -128,7 +128,7 @@ quadclicker [OPTIONS]
128128

129129
| Argument | Type | Default | Description |
130130
|---|---|---|---|
131-
| `--rate <value>` | string | required | Click rate. Formats: `100ms` · `10/s` · `600/min` |
131+
| `--rate <value>` | string | required | Click rate. Formats: `100ms` · `5s` · `2min` · `10/s` · `600/min` · `60/h` (1 ms – 360 min) |
132132
| `--button <left\|right\|middle>` | enum | `left` | Mouse button |
133133
| `--type <single\|double>` | enum | `single` | Click type |
134134
| `--location <x,y>` | int pair | cursor | Fixed screen coordinate |
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Click Rate Redesign — Design Spec
2+
3+
**Date:** 2026-04-25
4+
**Scope:** Windows app (`windows/`). macOS and Linux tracks unchanged.
5+
**Status:** Approved for implementation.
6+
7+
## Problem
8+
9+
The current Click Rate input mixes two paradigms (delay-per-click vs. clicks-per-time-unit) into a single value+unit row:
10+
11+
```
12+
Click Rate: [100] [ms ▾] ← unit dropdown contains: ms, /s, /min
13+
```
14+
15+
Users have to mentally translate: "I want 10 clicks/sec" → "that's 100 ms" — or rely on the parser to accept either. The current ms default also implies a frame-of-reference (delay) that not all users share. The dropdown silently mixes `ms` (a duration) with `/s` and `/min` (rates), which is the same kind of category error as putting "miles" and "miles per hour" in the same dropdown.
16+
17+
## Goal
18+
19+
Split the control into a **Mode** (Delay vs. Frequency) chosen by radio button, and surface only the units valid for that mode. Show a live conversion hint so users build intuition without having to think in two paradigms at once. Add a "very fast" warning before users land on rates that misbehave on most apps.
20+
21+
## Non-goals
22+
23+
- Not redesigning any other field (mouse button, click type, location, stop conditions, hotkeys).
24+
- Not changing macOS or Linux apps.
25+
- Not changing the click engine or input injection.
26+
- Not breaking existing CLI scripts.
27+
28+
## Design
29+
30+
### 1. UX / Layout (`MainWindow.xaml`)
31+
32+
Replace the single Click Rate row with a two-row block:
33+
34+
```
35+
Click Rate: ◉ Delay ○ Frequency
36+
[ 100 ] [ ms ▾ ]
37+
≈ 10 clicks/sec
38+
```
39+
40+
Toggling to **Frequency**:
41+
```
42+
Click Rate: ○ Delay ◉ Frequency
43+
[ 10 ] [ per sec ▾ ]
44+
≈ 100 ms between clicks
45+
```
46+
47+
**Behavior**
48+
49+
- Mode radios live on the same row as the `Click Rate:` label, on the right, in a horizontal `StackPanel`.
50+
- The value `TextBox` is fixed-width (~80px), the unit `ComboBox` (~110px) follows.
51+
- Toggling Mode swaps the dropdown's items and re-validates the current value against the new mode's bounds. If out of range for the new mode, the value is clamped (silent — not an error).
52+
- The hint line below uses `TextSecondary` color, 11px, single line.
53+
- When the effective rate is **> 100 clicks/second** (delay < 10ms), the hint switches to `Danger` color and shows `⚠ Very fast — input may not register reliably` followed by the conversion (`≈ 50 ms between clicks`) on the same wrapped block.
54+
- Numeric value box: positive numbers only. Decimals allowed for `sec`, `min`, `per min`, `per hour`. Integer-only enforced for `ms` and `per sec`.
55+
- Empty / zero / out-of-range hard-rejects only on Start (existing inline `ErrorLabel`) — the live hint just gives feedback.
56+
- Window height grows ~30px (one extra row). Stays within `MinHeight=430`.
57+
58+
**Units exposed**
59+
60+
| Mode | Dropdown items (Tag values) | Bounds |
61+
|---|---|---|
62+
| Delay | `ms`, `sec`, `min` | 1 ms – 360 min |
63+
| Frequency | `per sec`, `per min`, `per hour` | 1 / hour – 1000 / sec |
64+
65+
Hard floor is **1 ms** (= 1000/sec). Hard ceiling for delay is **360 min** (= 6 hours). These bounds are enforced in the parser, not just the UI.
66+
67+
### 2. Parser (`Core/ClickRateParser.cs`)
68+
69+
**Additive only** — every existing format keeps working. New unit tokens:
70+
71+
| New token forms | Parses to |
72+
|---|---|
73+
| `s`, `sec`, `seconds`, `secs` (suffix) | seconds → ms |
74+
| `m`, `min`, `minutes`, `mins` (suffix) | minutes → ms |
75+
| `/h`, `cph`, `times per hour` | per hour → ms delay |
76+
77+
**Disambiguation**: `m` alone is ambiguous (minutes vs. milliseconds-prefix-typo) — accept it as **minutes**, since `ms` is parsed first. Order of suffix checks: `ms``min`/`m``sec`/`s``/h`/`cph``/min`/`cpm``/s`/`cps` → bare integer.
78+
79+
**Bounds enforcement** (all paths, not just new ones):
80+
81+
```
82+
const long MaxDelayMs = 360L * 60_000; // 360 minutes
83+
const double MinDelayMs = 1.0;
84+
```
85+
86+
If parsed delay falls outside `[MinDelayMs, MaxDelayMs]`, return `false` with a clear error message.
87+
88+
**Public surface unchanged**:
89+
```csharp
90+
public static bool TryParse(string text, out TimeSpan delay, out string error)
91+
```
92+
93+
CLI consumers and existing tests continue to work.
94+
95+
### 3. Settings (`Models/AppSettings.cs`)
96+
97+
**New schema**:
98+
```csharp
99+
public ClickRateMode ClickRateMode { get; set; } = ClickRateMode.Delay;
100+
public string ClickRateValue { get; set; } = "100";
101+
public string ClickRateUnit { get; set; } = "ms"; // canonical Tag value
102+
```
103+
104+
**New enum** in `Models/ClickRateMode.cs`:
105+
```csharp
106+
public enum ClickRateMode { Delay, Frequency }
107+
```
108+
109+
**Canonical unit tags** (stored as strings for forward-compat, enum'd at runtime):
110+
- Delay: `"ms"`, `"sec"`, `"min"`
111+
- Frequency: `"per_sec"`, `"per_min"`, `"per_hour"`
112+
113+
**Migration** (handled in `AppSettings.Load()`):
114+
- After deserialization, if `ClickRateMode` is missing/default AND the old `ClickRateUnit` is one of the legacy values:
115+
- `"ms"` → Mode=Delay, Unit=`"ms"`
116+
- `"/s"` → Mode=Frequency, Unit=`"per_sec"`
117+
- `"/min"` → Mode=Frequency, Unit=`"per_min"`
118+
- Migration runs once; the next `Save()` writes the new schema and the legacy unit string is overwritten.
119+
- Detection: a `[JsonIgnore]` flag isn't needed; we detect by checking whether the deserialized `ClickRateUnit` is one of the legacy values that no longer match the new canonical set.
120+
121+
### 4. Code-behind (`MainWindow.xaml.cs`)
122+
123+
New private state: none (all state lives on the controls).
124+
125+
**New handlers**:
126+
- `ClickRateMode_Changed` — fires when either Mode radio toggles. Repopulates `ClickRateUnitBox` items, restores selection from the matching settings unit (or the first item), and recomputes the hint.
127+
- `ClickRateValueBox_TextChanged` and `ClickRateUnitBox_SelectionChanged` — recompute the hint by calling the existing `ClickRateParser.TryParse` against the composed string and rendering a counterpart-side view.
128+
129+
**Hint rendering helper** (private static):
130+
```csharp
131+
private void UpdateRateHint()
132+
{
133+
if (!ClickRateParser.TryParse(ComposedRateText, out var delay, out _))
134+
{
135+
RateHintLabel.Text = string.Empty;
136+
return;
137+
}
138+
double cps = 1000.0 / delay.TotalMilliseconds;
139+
bool isDelayMode = ModeDelay.IsChecked == true;
140+
string conversion = isDelayMode ? FormatRate(cps) : FormatDelay(delay);
141+
bool veryFast = cps > 100;
142+
RateHintLabel.Foreground = veryFast ? DangerBrush : TextSecondaryBrush;
143+
RateHintLabel.Text = veryFast
144+
? $"⚠ Very fast — input may not register reliably (≈ {conversion})"
145+
: $"≈ {conversion}";
146+
}
147+
```
148+
149+
Existing `TryBuildSession` updates: composes `value + unit` differently per mode (e.g., `"10per_sec"` → translate `per_sec``/s` for parser input, since the parser still speaks the old DSL externally). Or simpler: have a small `ComposeRateString(mode, value, unitTag)` helper that maps internal tag → parser-compatible suffix.
150+
151+
**Persistence**: `SaveSettings()` records `ClickRateMode`, the numeric `ClickRateValue`, and the canonical unit tag.
152+
153+
### 5. Tests
154+
155+
Add to `Tests/ClickRateParserTests.cs`:
156+
- `Seconds_ValidInput``"5s"`, `"5 sec"`, `"5seconds"` → 5000ms.
157+
- `Minutes_ValidInput``"2min"`, `"2 minutes"`, `"2m"` → 120000ms.
158+
- `PerHour_ValidInput``"60/h"`, `"60cph"`, `"60 times per hour"` → 60000ms.
159+
- `MaxDelay_AcceptedAtBoundary``"360min"` parses; `"361min"` rejects.
160+
- `MaxRate_AcceptedAtBoundary``"1000/s"` parses (= 1ms); `"1001/s"` rejects.
161+
- `MinDelay_RejectsBelowOne``"0.5ms"` rejects.
162+
163+
New `Tests/AppSettingsMigrationTests.cs`:
164+
- Old JSON with `"ClickRateUnit": "/s"` and no `ClickRateMode` field → loads as Frequency / per_sec.
165+
- Old JSON with `"/min"` → Frequency / per_min.
166+
- Old JSON with `"ms"` → Delay / ms.
167+
- New JSON round-trips unchanged.
168+
169+
## Out-of-scope risks accepted
170+
171+
- **Multi-monitor / DPI**: untouched.
172+
- **Localization**: hint text is English-only (matches the rest of the app).
173+
- **Accessibility**: radio buttons + textbox + combobox are all natively keyboard-accessible. No new accessibility work needed.
174+
175+
## Rollback
176+
177+
Single-revert friendly. Settings migration is forward-only; users who downgrade get default Delay/100/ms — minor inconvenience, no corruption.

windows/App.xaml

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@
66
<Application.Resources>
77
<ResourceDictionary>
88

9-
<!-- ── Color Palette ─────────────────────────────────────────────── -->
10-
<Color x:Key="AccentColor">#50C878</Color>
11-
<Color x:Key="AccentHoverColor">#3DAF62</Color>
12-
<Color x:Key="AccentPressedColor">#2E9150</Color>
13-
<Color x:Key="DangerColor">#E05252</Color>
14-
<Color x:Key="DangerHoverColor">#C43C3C</Color>
15-
<Color x:Key="BackgroundColor">#1A1A1A</Color>
16-
<Color x:Key="SurfaceColor">#242424</Color>
17-
<Color x:Key="SurfaceElevatedColor">#2E2E2E</Color>
18-
<Color x:Key="BorderColor">#3A3A3A</Color>
19-
<Color x:Key="TextPrimaryColor">#F0F0F0</Color>
20-
<Color x:Key="TextSecondaryColor">#9A9A9A</Color>
21-
<Color x:Key="TextDisabledColor">#555555</Color>
22-
<Color x:Key="StatusWaitingColor">#E0A030</Color>
9+
<!-- ── Color Palette (Taneth: deep green hull, gold HUD accent) ──── -->
10+
<Color x:Key="AccentColor">#E8B547</Color>
11+
<Color x:Key="AccentHoverColor">#F5C75A</Color>
12+
<Color x:Key="AccentPressedColor">#B88A2A</Color>
13+
<Color x:Key="AccentForegroundColor">#0A1410</Color>
14+
<Color x:Key="DangerColor">#E04030</Color>
15+
<Color x:Key="DangerHoverColor">#C8331E</Color>
16+
<Color x:Key="BackgroundColor">#0A1410</Color>
17+
<Color x:Key="SurfaceColor">#13211C</Color>
18+
<Color x:Key="SurfaceElevatedColor">#1B2E27</Color>
19+
<Color x:Key="BorderColor">#2D5448</Color>
20+
<Color x:Key="TextPrimaryColor">#E8DCB0</Color>
21+
<Color x:Key="TextSecondaryColor">#7A9088</Color>
22+
<Color x:Key="TextDisabledColor">#3D5048</Color>
23+
<Color x:Key="StatusWaitingColor">#5BA89A</Color>
2324

2425
<!-- ── Brushes ────────────────────────────────────────────────────── -->
2526
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}" />
@@ -35,6 +36,7 @@
3536
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondaryColor}" />
3637
<SolidColorBrush x:Key="TextDisabledBrush" Color="{StaticResource TextDisabledColor}" />
3738
<SolidColorBrush x:Key="StatusWaitingBrush" Color="{StaticResource StatusWaitingColor}" />
39+
<SolidColorBrush x:Key="AccentForegroundBrush" Color="{StaticResource AccentForegroundColor}" />
3840

3941
<!-- ── Label ─────────────────────────────────────────────────────── -->
4042
<Style TargetType="Label">
@@ -156,7 +158,7 @@
156158
</Trigger>
157159
<Trigger Property="IsSelected" Value="True">
158160
<Setter TargetName="ItemBorder" Property="Background" Value="{StaticResource AccentBrush}" />
159-
<Setter Property="Foreground" Value="White" />
161+
<Setter Property="Foreground" Value="{StaticResource AccentForegroundBrush}" />
160162
</Trigger>
161163
</ControlTemplate.Triggers>
162164
</ControlTemplate>
@@ -195,7 +197,7 @@
195197
<!-- ── Start Button ──────────────────────────────────────────────── -->
196198
<Style x:Key="StartButtonStyle" TargetType="Button">
197199
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
198-
<Setter Property="Foreground" Value="White" />
200+
<Setter Property="Foreground" Value="{StaticResource AccentForegroundBrush}" />
199201
<Setter Property="FontFamily" Value="Segoe UI" />
200202
<Setter Property="FontSize" Value="15" />
201203
<Setter Property="FontWeight" Value="Bold" />

windows/Cli/CliEntryPoint.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ private static void PrintHelp() => Console.WriteLine("""
183183
When run without arguments, launches the GUI.
184184
185185
Options:
186-
--rate <value> Click rate. Formats: 100ms | 10/s | 600/min [required in CLI mode]
186+
--rate <value> Click rate. Formats: 100ms | 5s | 2min | 10/s | 600/min | 60/h [required in CLI mode]
187187
--button <left|right|middle> Mouse button to click (default: left)
188188
--type <single|double> Click type (default: single)
189189
--location <x,y> Fixed screen coordinate (default: current cursor)
@@ -206,5 +206,7 @@ 130 Ctrl+C / interrupted
206206
quadclicker --rate 10/s --location 500,300 --button right --stop-after-clicks 100
207207
quadclicker --rate 500ms --type double --stop-after-seconds 30
208208
quadclicker --rate 1ms --button middle --stop-after-clicks 50
209+
quadclicker --rate 5s --stop-after-clicks 10
210+
quadclicker --rate 60/h --stop-after-seconds 3600
209211
""");
210212
}

0 commit comments

Comments
 (0)