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
7 changes: 2 additions & 5 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ jobs:
run: dotnet build windows/QuadClicker.csproj -c Release --no-restore

- name: Test
run: dotnet test windows/Tests/QuadClicker.Tests.csproj --no-build -c Release -v normal
# Note: test project builds separately since it references source files directly
continue-on-error: false

- name: Test (build + run)
# Test project Compile-Includes source files from the main project, so its
# output dir is independent — let `dotnet test` handle build + run in one shot.
run: dotnet test windows/Tests/QuadClicker.Tests.csproj -c Release -v normal

- name: Publish
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ jobs:
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
- name: Build & Publish
run: dotnet publish windows/QuadClicker.csproj -c Release -r win-x64 --self-contained false -o artifacts/windows
- name: Build & Publish (self-contained single-file)
run: dotnet publish windows/QuadClicker.csproj -c Release -r win-x64
--self-contained true
-p:PublishSingleFile=true
-p:IncludeNativeLibrariesForSelfExtract=true
-p:EnableCompressionInSingleFile=true
-o artifacts/windows
- uses: actions/upload-artifact@v4
with:
name: QuadClicker-win-x64
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ QuadClicker is the definitive open-source auto-clicker: fully configurable, scri

## Features

- **Click Rate** — Enter any format: `100ms`, `10/s`, `10cps`, `600/min`, `600cpm`, or a bare integer (ms)
- **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.
- **Mouse Button** — Left, Right, or Middle
- **Click Type** — Single or Double (uses OS double-click interval)
- **Location Modes**
Expand Down Expand Up @@ -128,7 +128,7 @@ quadclicker [OPTIONS]

| Argument | Type | Default | Description |
|---|---|---|---|
| `--rate <value>` | string | required | Click rate. Formats: `100ms` · `10/s` · `600/min` |
| `--rate <value>` | string | required | Click rate. Formats: `100ms` · `5s` · `2min` · `10/s` · `600/min` · `60/h` (1 ms – 360 min) |
| `--button <left\|right\|middle>` | enum | `left` | Mouse button |
| `--type <single\|double>` | enum | `single` | Click type |
| `--location <x,y>` | int pair | cursor | Fixed screen coordinate |
Expand Down
177 changes: 177 additions & 0 deletions docs/superpowers/specs/2026-04-25-click-rate-redesign-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Click Rate Redesign — Design Spec

**Date:** 2026-04-25
**Scope:** Windows app (`windows/`). macOS and Linux tracks unchanged.
**Status:** Approved for implementation.

## Problem

The current Click Rate input mixes two paradigms (delay-per-click vs. clicks-per-time-unit) into a single value+unit row:

```
Click Rate: [100] [ms ▾] ← unit dropdown contains: ms, /s, /min
```

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.

## Goal

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.

## Non-goals

- Not redesigning any other field (mouse button, click type, location, stop conditions, hotkeys).
- Not changing macOS or Linux apps.
- Not changing the click engine or input injection.
- Not breaking existing CLI scripts.

## Design

### 1. UX / Layout (`MainWindow.xaml`)

Replace the single Click Rate row with a two-row block:

```
Click Rate: ◉ Delay ○ Frequency
[ 100 ] [ ms ▾ ]
≈ 10 clicks/sec
```

Toggling to **Frequency**:
```
Click Rate: ○ Delay ◉ Frequency
[ 10 ] [ per sec ▾ ]
≈ 100 ms between clicks
```

**Behavior**

- Mode radios live on the same row as the `Click Rate:` label, on the right, in a horizontal `StackPanel`.
- The value `TextBox` is fixed-width (~80px), the unit `ComboBox` (~110px) follows.
- 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).
- The hint line below uses `TextSecondary` color, 11px, single line.
- 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.
- Numeric value box: positive numbers only. Decimals allowed for `sec`, `min`, `per min`, `per hour`. Integer-only enforced for `ms` and `per sec`.
- Empty / zero / out-of-range hard-rejects only on Start (existing inline `ErrorLabel`) — the live hint just gives feedback.
- Window height grows ~30px (one extra row). Stays within `MinHeight=430`.

**Units exposed**

| Mode | Dropdown items (Tag values) | Bounds |
|---|---|---|
| Delay | `ms`, `sec`, `min` | 1 ms – 360 min |
| Frequency | `per sec`, `per min`, `per hour` | 1 / hour – 1000 / sec |

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.

### 2. Parser (`Core/ClickRateParser.cs`)

**Additive only** — every existing format keeps working. New unit tokens:

| New token forms | Parses to |
|---|---|
| `s`, `sec`, `seconds`, `secs` (suffix) | seconds → ms |
| `m`, `min`, `minutes`, `mins` (suffix) | minutes → ms |
| `/h`, `cph`, `times per hour` | per hour → ms delay |

**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.

**Bounds enforcement** (all paths, not just new ones):

```
const long MaxDelayMs = 360L * 60_000; // 360 minutes
const double MinDelayMs = 1.0;
```

If parsed delay falls outside `[MinDelayMs, MaxDelayMs]`, return `false` with a clear error message.

**Public surface unchanged**:
```csharp
public static bool TryParse(string text, out TimeSpan delay, out string error)
```

CLI consumers and existing tests continue to work.

### 3. Settings (`Models/AppSettings.cs`)

**New schema**:
```csharp
public ClickRateMode ClickRateMode { get; set; } = ClickRateMode.Delay;
public string ClickRateValue { get; set; } = "100";
public string ClickRateUnit { get; set; } = "ms"; // canonical Tag value
```

**New enum** in `Models/ClickRateMode.cs`:
```csharp
public enum ClickRateMode { Delay, Frequency }
```

**Canonical unit tags** (stored as strings for forward-compat, enum'd at runtime):
- Delay: `"ms"`, `"sec"`, `"min"`
- Frequency: `"per_sec"`, `"per_min"`, `"per_hour"`

**Migration** (handled in `AppSettings.Load()`):
- After deserialization, if `ClickRateMode` is missing/default AND the old `ClickRateUnit` is one of the legacy values:
- `"ms"` → Mode=Delay, Unit=`"ms"`
- `"/s"` → Mode=Frequency, Unit=`"per_sec"`
- `"/min"` → Mode=Frequency, Unit=`"per_min"`
- Migration runs once; the next `Save()` writes the new schema and the legacy unit string is overwritten.
- 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.

### 4. Code-behind (`MainWindow.xaml.cs`)

New private state: none (all state lives on the controls).

**New handlers**:
- `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.
- `ClickRateValueBox_TextChanged` and `ClickRateUnitBox_SelectionChanged` — recompute the hint by calling the existing `ClickRateParser.TryParse` against the composed string and rendering a counterpart-side view.

**Hint rendering helper** (private static):
```csharp
private void UpdateRateHint()
{
if (!ClickRateParser.TryParse(ComposedRateText, out var delay, out _))
{
RateHintLabel.Text = string.Empty;
return;
}
double cps = 1000.0 / delay.TotalMilliseconds;
bool isDelayMode = ModeDelay.IsChecked == true;
string conversion = isDelayMode ? FormatRate(cps) : FormatDelay(delay);
bool veryFast = cps > 100;
RateHintLabel.Foreground = veryFast ? DangerBrush : TextSecondaryBrush;
RateHintLabel.Text = veryFast
? $"⚠ Very fast — input may not register reliably (≈ {conversion})"
: $"≈ {conversion}";
}
```

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.

**Persistence**: `SaveSettings()` records `ClickRateMode`, the numeric `ClickRateValue`, and the canonical unit tag.

### 5. Tests

Add to `Tests/ClickRateParserTests.cs`:
- `Seconds_ValidInput` — `"5s"`, `"5 sec"`, `"5seconds"` → 5000ms.
- `Minutes_ValidInput` — `"2min"`, `"2 minutes"`, `"2m"` → 120000ms.
- `PerHour_ValidInput` — `"60/h"`, `"60cph"`, `"60 times per hour"` → 60000ms.
- `MaxDelay_AcceptedAtBoundary` — `"360min"` parses; `"361min"` rejects.
- `MaxRate_AcceptedAtBoundary` — `"1000/s"` parses (= 1ms); `"1001/s"` rejects.
- `MinDelay_RejectsBelowOne` — `"0.5ms"` rejects.

New `Tests/AppSettingsMigrationTests.cs`:
- Old JSON with `"ClickRateUnit": "/s"` and no `ClickRateMode` field → loads as Frequency / per_sec.
- Old JSON with `"/min"` → Frequency / per_min.
- Old JSON with `"ms"` → Delay / ms.
- New JSON round-trips unchanged.

## Out-of-scope risks accepted

- **Multi-monitor / DPI**: untouched.
- **Localization**: hint text is English-only (matches the rest of the app).
- **Accessibility**: radio buttons + textbox + combobox are all natively keyboard-accessible. No new accessibility work needed.

## Rollback

Single-revert friendly. Settings migration is forward-only; users who downgrade get default Delay/100/ms — minor inconvenience, no corruption.
34 changes: 18 additions & 16 deletions windows/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@
<Application.Resources>
<ResourceDictionary>

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

<!-- ── Brushes ────────────────────────────────────────────────────── -->
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}" />
Expand All @@ -35,6 +36,7 @@
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondaryColor}" />
<SolidColorBrush x:Key="TextDisabledBrush" Color="{StaticResource TextDisabledColor}" />
<SolidColorBrush x:Key="StatusWaitingBrush" Color="{StaticResource StatusWaitingColor}" />
<SolidColorBrush x:Key="AccentForegroundBrush" Color="{StaticResource AccentForegroundColor}" />

<!-- ── Label ─────────────────────────────────────────────────────── -->
<Style TargetType="Label">
Expand Down Expand Up @@ -156,7 +158,7 @@
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="ItemBorder" Property="Background" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Foreground" Value="{StaticResource AccentForegroundBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Expand Down Expand Up @@ -195,7 +197,7 @@
<!-- ── Start Button ──────────────────────────────────────────────── -->
<Style x:Key="StartButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Foreground" Value="{StaticResource AccentForegroundBrush}" />
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="Bold" />
Expand Down
4 changes: 3 additions & 1 deletion windows/Cli/CliEntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ private static void PrintHelp() => Console.WriteLine("""
When run without arguments, launches the GUI.

Options:
--rate <value> Click rate. Formats: 100ms | 10/s | 600/min [required in CLI mode]
--rate <value> Click rate. Formats: 100ms | 5s | 2min | 10/s | 600/min | 60/h [required in CLI mode]
--button <left|right|middle> Mouse button to click (default: left)
--type <single|double> Click type (default: single)
--location <x,y> Fixed screen coordinate (default: current cursor)
Expand All @@ -206,5 +206,7 @@ 130 Ctrl+C / interrupted
quadclicker --rate 10/s --location 500,300 --button right --stop-after-clicks 100
quadclicker --rate 500ms --type double --stop-after-seconds 30
quadclicker --rate 1ms --button middle --stop-after-clicks 50
quadclicker --rate 5s --stop-after-clicks 10
quadclicker --rate 60/h --stop-after-seconds 3600
""");
}
Loading
Loading