Skip to content

Commit 305c3d9

Browse files
authored
Pointer: Fix "Revert to system defaults" (#994)
Previously, clicking "Revert to system defaults" computed and wrote concrete numeric values back into configuration. It could be subtly different from the true default values — especially on trackpads. Fixes #944, fixes #967.
1 parent c14ff0b commit 305c3d9

File tree

8 files changed

+168
-29
lines changed

8 files changed

+168
-29
lines changed

Documentation/Configuration.d.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ type HexString = string;
1111

1212
type Button = Primary | Secondary | Auxiliary | Back | Forward | number;
1313

14+
/**
15+
* @title Unset
16+
* @description A special value that explicitly restores a setting to the system or device default. Currently supported in pointer settings; may be supported more broadly in the future.
17+
*/
18+
export type Unset = "unset";
19+
1420
/**
1521
* @description Primary button, usually the left button.
1622
*/
@@ -299,19 +305,19 @@ declare namespace Scheme {
299305
type Pointer = {
300306
/**
301307
* @title Pointer acceleration
302-
* @description If the value is not specified, system default value will be used.
308+
* @description A number to set acceleration, or "unset" to restore system default. If omitted, the previous/merged value applies.
303309
* @minimum 0
304310
* @maximum 20
305311
*/
306-
acceleration?: number;
312+
acceleration?: number | Unset;
307313

308314
/**
309315
* @title Pointer speed
310-
* @description If the value is not specified, device default value will be used.
316+
* @description A number to set speed, or "unset" to restore device default. If omitted, the previous/merged value applies.
311317
* @minimal 0
312318
* @maximum 1
313319
*/
314-
speed?: number;
320+
speed?: number | Unset;
315321

316322
/**
317323
* @title Disable pointer acceleration

Documentation/Configuration.json

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -842,11 +842,18 @@
842842
"additionalProperties": false,
843843
"properties": {
844844
"acceleration": {
845-
"description": "If the value is not specified, system default value will be used.",
845+
"anyOf": [
846+
{
847+
"type": "number"
848+
},
849+
{
850+
"$ref": "#/definitions/Unset"
851+
}
852+
],
853+
"description": "A number to set acceleration, or \"unset\" to restore system default. If omitted, the previous/merged value applies.",
846854
"maximum": 20,
847855
"minimum": 0,
848-
"title": "Pointer acceleration",
849-
"type": "number"
856+
"title": "Pointer acceleration"
850857
},
851858
"disableAcceleration": {
852859
"default": false,
@@ -861,10 +868,17 @@
861868
"type": "boolean"
862869
},
863870
"speed": {
864-
"description": "If the value is not specified, device default value will be used.",
871+
"anyOf": [
872+
{
873+
"type": "number"
874+
},
875+
{
876+
"$ref": "#/definitions/Unset"
877+
}
878+
],
879+
"description": "A number to set speed, or \"unset\" to restore device default. If omitted, the previous/merged value applies.",
865880
"maximum": 1,
866-
"title": "Pointer speed",
867-
"type": "number"
881+
"title": "Pointer speed"
868882
}
869883
},
870884
"type": "object"
@@ -1193,6 +1207,12 @@
11931207
"type": "array"
11941208
}
11951209
]
1210+
},
1211+
"Unset": {
1212+
"const": "unset",
1213+
"description": "A special value that explicitly restores a setting to the system or device default. Currently supported in pointer settings; may be supported more broadly in the future.",
1214+
"title": "Unset",
1215+
"type": "string"
11961216
}
11971217
}
11981218
}

Documentation/Configuration.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ I would create two schemes and specify the vendor ID and product ID:
118118
Then, the pointer speed of my Logitech mouse and Microsoft mouse will be set to 0.36 and 0.4
119119
respectively.
120120

121+
### Unsetting values
122+
123+
LinearMouse supports a special "unset" value to explicitly restore settings back to their system or
124+
device defaults. This differs from omitting a field, which keeps the previously merged value.
125+
126+
Currently, "unset" is supported for pointer acceleration and speed.
127+
128+
```json
129+
{
130+
"schemes": [
131+
{
132+
"if": {
133+
"device": { "category": "mouse" }
134+
},
135+
"pointer": { "acceleration": "unset", "speed": "unset" }
136+
}
137+
]
138+
}
139+
```
140+
121141
## App matching
122142

123143
App bundle ID can be provided to match a specific app.

LinearMouse/Device/DeviceManager.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,12 @@ class DeviceManager: ObservableObject {
246246
// convention, but here, the pointerAcceleration actually refers to
247247
// the tracking speed.
248248
if let pointerAcceleration = scheme.pointer.acceleration {
249-
device.pointerAcceleration = pointerAcceleration.asTruncatedDouble
249+
switch pointerAcceleration {
250+
case let .value(v):
251+
device.pointerAcceleration = v.asTruncatedDouble
252+
case .unset:
253+
device.restorePointerAcceleration()
254+
}
250255
} else {
251256
device.restorePointerAcceleration()
252257
}
@@ -262,13 +267,23 @@ class DeviceManager: ObservableObject {
262267
}
263268

264269
if let pointerSpeed = scheme.pointer.speed {
265-
device.pointerSpeed = pointerSpeed.asTruncatedDouble
270+
switch pointerSpeed {
271+
case let .value(v):
272+
device.pointerSpeed = v.asTruncatedDouble
273+
case .unset:
274+
device.restorePointerSpeed()
275+
}
266276
} else {
267277
device.restorePointerSpeed()
268278
}
269279

270280
if let pointerAcceleration = scheme.pointer.acceleration {
271-
device.pointerAcceleration = pointerAcceleration.asTruncatedDouble
281+
switch pointerAcceleration {
282+
case let .value(v):
283+
device.pointerAcceleration = v.asTruncatedDouble
284+
case .unset:
285+
device.restorePointerAcceleration()
286+
}
272287
} else {
273288
device.restorePointerAcceleration()
274289
}

LinearMouse/Model/Configuration/Scheme/Pointer.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,47 @@ import Foundation
55

66
extension Scheme {
77
struct Acceleration: Equatable, ClampRange {
8-
typealias Value = Decimal
8+
typealias Value = Unsettable<Decimal>
9+
typealias RangeValue = Decimal
910

10-
static var range: ClosedRange<Value> = 0 ... 20
11+
static var range: ClosedRange<RangeValue> = 0 ... 20
12+
13+
static func clamp(_ value: Value?) -> Value? {
14+
guard let value else {
15+
return nil
16+
}
17+
switch value {
18+
case let .value(v):
19+
return .value(v.clamped(to: range))
20+
case .unset:
21+
return .unset
22+
}
23+
}
1124
}
1225

1326
struct Speed: Equatable, ClampRange {
14-
typealias Value = Decimal
27+
typealias Value = Unsettable<Decimal>
28+
typealias RangeValue = Decimal
1529

16-
static var range: ClosedRange<Value> = 0 ... 1
30+
static var range: ClosedRange<RangeValue> = 0 ... 1
31+
32+
static func clamp(_ value: Value?) -> Value? {
33+
guard let value else {
34+
return nil
35+
}
36+
switch value {
37+
case let .value(v):
38+
return .value(v.clamped(to: range))
39+
case .unset:
40+
return .unset
41+
}
42+
}
1743
}
1844

1945
struct Pointer: Codable, Equatable, ImplicitInitable {
20-
@Clamp<Acceleration> var acceleration: Decimal?
46+
@Clamp<Acceleration> var acceleration: Unsettable<Decimal>?
2147

22-
@Clamp<Speed> var speed: Decimal?
48+
@Clamp<Speed> var speed: Unsettable<Decimal>?
2349

2450
var disableAcceleration: Bool?
2551
var redirectsToScroll: Bool?

LinearMouse/UI/PointerSettings/PointerSettingsState.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extension PointerSettingsState {
4040

4141
var pointerAcceleration: Double {
4242
get {
43-
mergedScheme.pointer.acceleration?.asTruncatedDouble
43+
mergedScheme.pointer.acceleration?.unwrapped?.asTruncatedDouble
4444
?? mergedScheme.firstMatchedDevice?.pointerAcceleration
4545
?? Device.fallbackPointerAcceleration
4646
}
@@ -49,13 +49,13 @@ extension PointerSettingsState {
4949
return
5050
}
5151

52-
scheme.pointer.acceleration = Decimal(newValue).rounded(4)
52+
scheme.pointer.acceleration = .value(Decimal(newValue).rounded(4))
5353
}
5454
}
5555

5656
var pointerSpeed: Double {
5757
get {
58-
mergedScheme.pointer.speed?.asTruncatedDouble
58+
mergedScheme.pointer.speed?.unwrapped?.asTruncatedDouble
5959
?? mergedScheme.firstMatchedDevice?.pointerSpeed
6060
?? Device.fallbackPointerSpeed
6161
}
@@ -64,7 +64,7 @@ extension PointerSettingsState {
6464
return
6565
}
6666

67-
scheme.pointer.speed = Decimal(newValue).rounded(4)
67+
scheme.pointer.speed = .value(Decimal(newValue).rounded(4))
6868
}
6969
}
7070

@@ -93,8 +93,8 @@ extension PointerSettingsState {
9393

9494
Scheme(
9595
pointer: Scheme.Pointer(
96-
acceleration: Decimal(device?.pointerAcceleration ?? Device.fallbackPointerAcceleration),
97-
speed: Decimal(device?.pointerSpeed ?? Device.fallbackPointerSpeed),
96+
acceleration: .unset,
97+
speed: .unset,
9898
disableAcceleration: false,
9999
redirectsToScroll: false
100100
)

LinearMouse/Utilities/Codable/Clamp.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22
// Copyright (c) 2021-2025 LinearMouse
33

44
protocol ClampRange {
5-
associatedtype Value: Codable, Comparable
5+
associatedtype Value: Codable
6+
associatedtype RangeValue: Comparable
67

7-
static var range: ClosedRange<Value> { get }
8+
static var range: ClosedRange<RangeValue> { get }
9+
10+
// Clamp a possibly-optional value to range; default provided below
11+
static func clamp(_ value: Value?) -> Value?
12+
}
13+
14+
extension ClampRange where Value: Comparable, Value == RangeValue {
15+
static func clamp(_ value: Value?) -> Value? {
16+
value.map { $0.clamped(to: range) }
17+
}
818
}
919

1020
@propertyWrapper
@@ -14,7 +24,7 @@ struct Clamp<T: ClampRange> {
1424
var wrappedValue: T.Value? {
1525
get { value }
1626
set {
17-
value = newValue.map { $0.clamped(to: T.range) }
27+
value = T.clamp(newValue)
1828
}
1929
}
2030

@@ -35,7 +45,7 @@ extension Clamp: Codable {
3545
}
3646
}
3747

38-
extension Clamp: Equatable where T: Equatable {}
48+
extension Clamp: Equatable where T.Value: Equatable {}
3949

4050
extension KeyedDecodingContainer {
4151
func decode<T: ClampRange>(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// MIT License
2+
// Copyright (c) 2021-2025 LinearMouse
3+
4+
import Foundation
5+
6+
enum Unsettable<T: Codable & Equatable>: Equatable, Codable {
7+
case value(T)
8+
case unset
9+
10+
init(from decoder: Decoder) throws {
11+
let container = try decoder.singleValueContainer()
12+
13+
// Try decoding the sentinel string "unset"
14+
if let string = try? container.decode(String.self), string == "unset" {
15+
self = .unset
16+
return
17+
}
18+
19+
// Otherwise, decode the underlying value
20+
let value = try container.decode(T.self)
21+
self = .value(value)
22+
}
23+
24+
func encode(to encoder: Encoder) throws {
25+
var container = encoder.singleValueContainer()
26+
switch self {
27+
case let .value(value):
28+
try container.encode(value)
29+
case .unset:
30+
try container.encode("unset")
31+
}
32+
}
33+
}
34+
35+
extension Unsettable {
36+
var unwrapped: T? {
37+
if case let .value(value) = self {
38+
return value
39+
}
40+
return nil
41+
}
42+
}

0 commit comments

Comments
 (0)