From 33e0701965303d880365529ff71e816b29bfd33d Mon Sep 17 00:00:00 2001 From: Toufiq Shishir Date: Fri, 26 Sep 2025 23:35:49 +0600 Subject: [PATCH 1/3] feat: enable separate scaling for precision and discrete mouse scrolling --- src/Surface.zig | 6 +-- src/config.zig | 1 + src/config/Config.zig | 121 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 8edeadf83f..03974dfc67 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -260,7 +260,7 @@ const DerivedConfig = struct { font: font.SharedGridSet.DerivedConfig, mouse_interval: u64, mouse_hide_while_typing: bool, - mouse_scroll_multiplier: f64, + mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, macos_option_as_alt: ?configpkg.OptionAsAlt, @@ -2829,7 +2829,7 @@ pub fn scrollCallback( // scroll events to pixels by multiplying the wheel tick value and the cell size. This means // that a wheel tick of 1 results in single scroll event. const yoff_adjusted: f64 = if (scroll_mods.precision) - yoff + yoff * self.config.mouse_scroll_multiplier.precision else yoff_adjusted: { // Round out the yoff to an absolute minimum of 1. macos tries to // simulate precision scrolling with non precision events by @@ -2843,7 +2843,7 @@ pub fn scrollCallback( else @min(yoff, -1); - break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier; + break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier.discrete; }; // Add our previously saved pending amount to the offset to get the diff --git a/src/config.zig b/src/config.zig index e83dff530b..569d4bec24 100644 --- a/src/config.zig +++ b/src/config.zig @@ -27,6 +27,7 @@ pub const FontStyle = Config.FontStyle; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; +pub const MouseScrollMultiplier = Config.MouseScrollMultiplier; pub const NonNativeFullscreen = Config.NonNativeFullscreen; pub const OptionAsAlt = Config.OptionAsAlt; pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; diff --git a/src/config/Config.zig b/src/config/Config.zig index 66e63fd3f7..27966fee0d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -834,13 +834,14 @@ palette: Palette = .{}, @"mouse-shift-capture": MouseShiftCapture = .false, /// Multiplier for scrolling distance with the mouse wheel. Any value less -/// than 0.01 or greater than 10,000 will be clamped to the nearest valid -/// value. +/// than 0.01 (0.01 for precision scroll) or greater than 10,000 will be clamped +/// to the nearest valid value. /// -/// A value of "3" (default) scrolls 3 lines per tick. +/// A discrete value of "3" (default) scrolls about 3 lines per wheel tick. +/// And a precision value of "0.1" (default) scales pixel-level scrolling. /// -/// Available since: 1.2.0 -@"mouse-scroll-multiplier": f64 = 3.0, +/// Available since: 1.2.1 +@"mouse-scroll-multiplier": MouseScrollMultiplier = .{ .precision = 0.1, .discrete = 3.0 }, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -4077,7 +4078,8 @@ pub fn finalize(self: *Config) !void { } // Clamp our mouse scroll multiplier - self.@"mouse-scroll-multiplier" = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier")); + self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.1, self.@"mouse-scroll-multiplier".precision)); + self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete)); // Clamp our split opacity self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity")); @@ -6508,7 +6510,7 @@ pub const RepeatableCodepointMap = struct { return .{ .map = try self.map.clone(alloc) }; } - /// Compare if two of our value are requal. Required by Config. + /// Compare if two of our value are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.map.list.slice(); const itemsB = other.map.list.slice(); @@ -7319,6 +7321,111 @@ pub const MouseShiftCapture = enum { never, }; +/// See mouse-scroll-multiplier +pub const MouseScrollMultiplier = struct { + const Self = @This(); + + precision: f64, + discrete: f64, + + pub fn parseCLI(self: *Self, input_: ?[]const u8) !void { + const input_raw = input_ orelse return error.ValueRequired; + const whitespace = " \t"; + const input = std.mem.trim(u8, input_raw, whitespace); + if (input.len == 0) return error.ValueRequired; + + const value = std.fmt.parseFloat(f64, input) catch null; + if (value) |val| { + self.precision = val; + self.discrete = val; + return; + } + + const comma_idx = std.mem.indexOf(u8, input, ","); + if (comma_idx) |idx| { + if (std.mem.indexOf(u8, input[idx + 1 ..], ",")) |_| return error.InvalidValue; + + const lhs = std.mem.trim(u8, input[0..idx], whitespace); + const rhs = std.mem.trim(u8, input[idx + 1 ..], whitespace); + if (lhs.len == 0 or rhs.len == 0) return error.InvalidValue; + + const lcolon_idx = std.mem.indexOf(u8, lhs, ":") orelse return error.InvalidValue; + const rcolon_idx = std.mem.indexOf(u8, rhs, ":") orelse return error.InvalidValue; + const lkey = lhs[0..lcolon_idx]; + const lvalstr = std.mem.trim(u8, lhs[lcolon_idx + 1 ..], whitespace); + const rkey = rhs[0..rcolon_idx]; + const rvalstr = std.mem.trim(u8, rhs[rcolon_idx + 1 ..], whitespace); + + // Only "precision" and "discrete" are valid keys. They + // must be different. + if (std.mem.eql(u8, lkey, rkey)) return error.InvalidValue; + + var found_precision = false; + var found_discrete = false; + var precision_val = self.precision; + var discrete_val = self.discrete; + + if (std.mem.eql(u8, lkey, "precision")) { + precision_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; + found_precision = true; + } else if (std.mem.eql(u8, lkey, "discrete")) { + discrete_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; + found_discrete = true; + } else return error.InvalidValue; + + if (std.mem.eql(u8, rkey, "precision")) { + precision_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; + found_precision = true; + } else if (std.mem.eql(u8, rkey, "discrete")) { + discrete_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; + found_discrete = true; + } else return error.InvalidValue; + + if (!found_precision or !found_discrete) return error.InvalidValue; + if (precision_val == 0 or discrete_val == 0) return error.InvalidValue; + + self.precision = precision_val; + self.discrete = discrete_val; + + return; + } else { + const colon_idx = std.mem.indexOf(u8, input, ":") orelse return error.InvalidValue; + const key = input[0..colon_idx]; + const valstr = std.mem.trim(u8, input[colon_idx + 1 ..], whitespace); + if (valstr.len == 0) return error.InvalidValue; + + const val = std.fmt.parseFloat(f64, valstr) catch return error.InvalidValue; + if (val == 0) return error.InvalidValue; + + if (std.mem.eql(u8, key, "precision")) { + self.precision = val; + return; + } else if (std.mem.eql(u8, key, "discrete")) { + self.discrete = val; + return; + } else return error.InvalidValue; + } + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + _ = alloc; + return self.*; + } + + /// Compare if two of our value are equal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return self.precision == other.precision and self.discrete == other.discrete; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + var buf: [32]u8 = undefined; + const formatted = try std.fmt.bufPrint(&buf, "precision:{d},discrete:{d}", .{ self.precision, self.discrete }); + try formatter.formatEntry([]const u8, formatted); + } +}; + /// How to treat requests to write to or read from the clipboard pub const ClipboardAccess = enum { allow, From 9597cead92eaf053ff38113a54882a016bc0c40b Mon Sep 17 00:00:00 2001 From: Toufiq Shishir Date: Sat, 27 Sep 2025 00:52:56 +0600 Subject: [PATCH 2/3] add: unit tests for MouseScrollMultiplier parsing and formatting --- src/config/Config.zig | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 27966fee0d..63db072359 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7424,6 +7424,53 @@ pub const MouseScrollMultiplier = struct { const formatted = try std.fmt.bufPrint(&buf, "precision:{d},discrete:{d}", .{ self.precision, self.discrete }); try formatter.formatEntry([]const u8, formatted); } + + test "parse MouseScrollMultiplier" { + const testing = std.testing; + + var args: Self = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("3"); + try testing.expect(args.precision == 3 and args.discrete == 3); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("precision:1"); + try testing.expect(args.precision == 1 and args.discrete == 3); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("discrete:5"); + try testing.expect(args.precision == 0.1 and args.discrete == 5); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("precision:3,discrete:7"); + try testing.expect(args.precision == 3 and args.discrete == 7); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("discrete:8,precision:6"); + try testing.expect(args.precision == 6 and args.discrete == 8); + + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("foo:1")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:bar")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,precision:3")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.ValueRequired, args.parseCLI("")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,foo:5")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,,discrete:3")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI(",precision:1,discrete:3")); + } + + test "format entry MouseScrollMultiplier" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var args: Self = .{ .precision = 1.5, .discrete = 2.5 }; + try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", buf.writer())); + try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.items); + } }; /// How to treat requests to write to or read from the clipboard From 10316297412e9a57dbfbdc19b7fdff66e17b2a60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 10:00:17 -0700 Subject: [PATCH 3/3] config: modify MouseScrollMultiplier to lean on args parsing --- src/cli/args.zig | 37 +++++++-- src/config/Config.zig | 185 ++++++++++++++++-------------------------- 2 files changed, 102 insertions(+), 120 deletions(-) diff --git a/src/cli/args.zig b/src/cli/args.zig index 2d2d199be7..b8f393864d 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -507,13 +507,18 @@ pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T { fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { return switch (@typeInfo(T).@"struct".layout) { - .auto => parseAutoStruct(T, alloc, v), + .auto => parseAutoStruct(T, alloc, v, null), .@"packed" => parsePackedStruct(T, v), else => @compileError("unsupported struct layout"), }; } -pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { +pub fn parseAutoStruct( + comptime T: type, + alloc: Allocator, + v: []const u8, + default_: ?T, +) !T { const info = @typeInfo(T).@"struct"; comptime assert(info.layout == .auto); @@ -573,9 +578,18 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { // Ensure all required fields are set inline for (info.fields, 0..) |field, i| { if (!fields_set.isSet(i)) { - const default_ptr = field.default_value_ptr orelse return error.InvalidValue; - const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); - @field(result, field.name) = typed_ptr.*; + @field(result, field.name) = default: { + // If we're given a default value then we inherit those. + // Otherwise we use the default values as specified by the + // struct. + if (default_) |default| { + break :default @field(default, field.name); + } else { + const default_ptr = field.default_value_ptr orelse return error.InvalidValue; + const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); + break :default typed_ptr.*; + } + }; } } @@ -1194,7 +1208,18 @@ test "parseIntoField: struct with basic fields" { try testing.expectEqual(84, data.value.b); try testing.expectEqual(24, data.value.c); - // Missing require dfield + // Set with explicit default + data.value = try parseAutoStruct( + @TypeOf(data.value), + alloc, + "a:hello", + .{ .a = "oh no", .b = 42 }, + ); + try testing.expectEqualStrings("hello", data.value.a); + try testing.expectEqual(42, data.value.b); + try testing.expectEqual(12, data.value.c); + + // Missing required field try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"), diff --git a/src/config/Config.zig b/src/config/Config.zig index 63db072359..fdea944ad2 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -833,15 +833,20 @@ palette: Palette = .{}, /// * `never` @"mouse-shift-capture": MouseShiftCapture = .false, -/// Multiplier for scrolling distance with the mouse wheel. Any value less -/// than 0.01 (0.01 for precision scroll) or greater than 10,000 will be clamped -/// to the nearest valid value. +/// Multiplier for scrolling distance with the mouse wheel. /// -/// A discrete value of "3" (default) scrolls about 3 lines per wheel tick. -/// And a precision value of "0.1" (default) scales pixel-level scrolling. +/// A prefix of `precision:` or `discrete:` can be used to set the multiplier +/// only for scrolling with the specific type of devices. These can be +/// comma-separated to set both types of multipliers at the same time, e.g. +/// `precision:0.1,discrete:3`. If no prefix is used, the multiplier applies +/// to all scrolling devices. Specifying a prefix was introduced in Ghostty +/// 1.2.1. /// -/// Available since: 1.2.1 -@"mouse-scroll-multiplier": MouseScrollMultiplier = .{ .precision = 0.1, .discrete = 3.0 }, +/// The value will be clamped to [0.01, 10,000]. Both of these are extreme +/// and you're likely to have a bad experience if you set either extreme. +/// +/// The default value is "3" for discrete devices and "1" for precision devices. +@"mouse-scroll-multiplier": MouseScrollMultiplier = .default, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -4078,7 +4083,7 @@ pub fn finalize(self: *Config) !void { } // Clamp our mouse scroll multiplier - self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.1, self.@"mouse-scroll-multiplier".precision)); + self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".precision)); self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete)); // Clamp our split opacity @@ -7012,6 +7017,7 @@ pub const RepeatableCommand = struct { inputpkg.Command, alloc, input, + null, ); try self.value.append(alloc, cmd); } @@ -7325,86 +7331,31 @@ pub const MouseShiftCapture = enum { pub const MouseScrollMultiplier = struct { const Self = @This(); - precision: f64, - discrete: f64, - - pub fn parseCLI(self: *Self, input_: ?[]const u8) !void { - const input_raw = input_ orelse return error.ValueRequired; - const whitespace = " \t"; - const input = std.mem.trim(u8, input_raw, whitespace); - if (input.len == 0) return error.ValueRequired; - - const value = std.fmt.parseFloat(f64, input) catch null; - if (value) |val| { - self.precision = val; - self.discrete = val; - return; - } + precision: f64 = 1, + discrete: f64 = 3, - const comma_idx = std.mem.indexOf(u8, input, ","); - if (comma_idx) |idx| { - if (std.mem.indexOf(u8, input[idx + 1 ..], ",")) |_| return error.InvalidValue; - - const lhs = std.mem.trim(u8, input[0..idx], whitespace); - const rhs = std.mem.trim(u8, input[idx + 1 ..], whitespace); - if (lhs.len == 0 or rhs.len == 0) return error.InvalidValue; - - const lcolon_idx = std.mem.indexOf(u8, lhs, ":") orelse return error.InvalidValue; - const rcolon_idx = std.mem.indexOf(u8, rhs, ":") orelse return error.InvalidValue; - const lkey = lhs[0..lcolon_idx]; - const lvalstr = std.mem.trim(u8, lhs[lcolon_idx + 1 ..], whitespace); - const rkey = rhs[0..rcolon_idx]; - const rvalstr = std.mem.trim(u8, rhs[rcolon_idx + 1 ..], whitespace); - - // Only "precision" and "discrete" are valid keys. They - // must be different. - if (std.mem.eql(u8, lkey, rkey)) return error.InvalidValue; - - var found_precision = false; - var found_discrete = false; - var precision_val = self.precision; - var discrete_val = self.discrete; - - if (std.mem.eql(u8, lkey, "precision")) { - precision_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; - found_precision = true; - } else if (std.mem.eql(u8, lkey, "discrete")) { - discrete_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; - found_discrete = true; - } else return error.InvalidValue; - - if (std.mem.eql(u8, rkey, "precision")) { - precision_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; - found_precision = true; - } else if (std.mem.eql(u8, rkey, "discrete")) { - discrete_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; - found_discrete = true; - } else return error.InvalidValue; - - if (!found_precision or !found_discrete) return error.InvalidValue; - if (precision_val == 0 or discrete_val == 0) return error.InvalidValue; - - self.precision = precision_val; - self.discrete = discrete_val; + pub const default: MouseScrollMultiplier = .{}; - return; - } else { - const colon_idx = std.mem.indexOf(u8, input, ":") orelse return error.InvalidValue; - const key = input[0..colon_idx]; - const valstr = std.mem.trim(u8, input[colon_idx + 1 ..], whitespace); - if (valstr.len == 0) return error.InvalidValue; - - const val = std.fmt.parseFloat(f64, valstr) catch return error.InvalidValue; - if (val == 0) return error.InvalidValue; - - if (std.mem.eql(u8, key, "precision")) { - self.precision = val; - return; - } else if (std.mem.eql(u8, key, "discrete")) { - self.discrete = val; - return; - } else return error.InvalidValue; - } + pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { + const input = input_ orelse return error.ValueRequired; + self.* = cli.args.parseAutoStruct( + MouseScrollMultiplier, + alloc, + input, + self.*, + ) catch |err| switch (err) { + error.InvalidValue => bare: { + const v = std.fmt.parseFloat( + f64, + input, + ) catch return error.InvalidValue; + break :bare .{ + .precision = v, + .discrete = v, + }; + }, + else => return err, + }; } /// Deep copy of the struct. Required by Config. @@ -7421,45 +7372,50 @@ pub const MouseScrollMultiplier = struct { /// Used by Formatter pub fn formatEntry(self: Self, formatter: anytype) !void { var buf: [32]u8 = undefined; - const formatted = try std.fmt.bufPrint(&buf, "precision:{d},discrete:{d}", .{ self.precision, self.discrete }); + const formatted = std.fmt.bufPrint( + &buf, + "precision:{d},discrete:{d}", + .{ self.precision, self.discrete }, + ) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, formatted); } - test "parse MouseScrollMultiplier" { + test "parse" { const testing = std.testing; + const alloc = testing.allocator; + const epsilon = 0.00001; var args: Self = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("3"); - try testing.expect(args.precision == 3 and args.discrete == 3); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("precision:1"); - try testing.expect(args.precision == 1 and args.discrete == 3); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("discrete:5"); - try testing.expect(args.precision == 0.1 and args.discrete == 5); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("precision:3,discrete:7"); - try testing.expect(args.precision == 3 and args.discrete == 7); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("discrete:8,precision:6"); - try testing.expect(args.precision == 6 and args.discrete == 8); + try args.parseCLI(alloc, "3"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("foo:1")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:bar")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,precision:3")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.ValueRequired, args.parseCLI("")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,foo:5")); + try args.parseCLI(alloc, "precision:1"); + try testing.expectApproxEqAbs(1, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); + args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,,discrete:3")); + try args.parseCLI(alloc, "discrete:5"); + try testing.expectApproxEqAbs(0.1, args.precision, epsilon); + try testing.expectApproxEqAbs(5, args.discrete, epsilon); + args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,")); + try args.parseCLI(alloc, "precision:3,discrete:7"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(7, args.discrete, epsilon); + args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI(",precision:1,discrete:3")); + try args.parseCLI(alloc, "discrete:8,precision:6"); + try testing.expectApproxEqAbs(6, args.precision, epsilon); + try testing.expectApproxEqAbs(8, args.discrete, epsilon); + + args = .default; + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "foo:1")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:bar")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,discrete:3,foo:5")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,,discrete:3")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, ",precision:1,discrete:3")); } test "format entry MouseScrollMultiplier" { @@ -8087,6 +8043,7 @@ pub const Theme = struct { Theme, alloc, input, + null, ); return; }