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
6 changes: 3 additions & 3 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
37 changes: 31 additions & 6 deletions src/cli/args.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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.*;
}
};
}
}

Expand Down Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions src/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
127 changes: 119 additions & 8 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -833,14 +833,20 @@ palette: Palette = .{},
/// * `never`
@"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.
/// Multiplier for scrolling distance with the mouse wheel.
///
/// A value of "3" (default) scrolls 3 lines per tick.
/// 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.0
@"mouse-scroll-multiplier": f64 = 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
Expand Down Expand Up @@ -4077,7 +4083,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.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
self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity"));
Expand Down Expand Up @@ -6508,7 +6515,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();
Expand Down Expand Up @@ -7010,6 +7017,7 @@ pub const RepeatableCommand = struct {
inputpkg.Command,
alloc,
input,
null,
);
try self.value.append(alloc, cmd);
}
Expand Down Expand Up @@ -7319,6 +7327,108 @@ pub const MouseShiftCapture = enum {
never,
};

/// See mouse-scroll-multiplier
pub const MouseScrollMultiplier = struct {
const Self = @This();

precision: f64 = 1,
discrete: f64 = 3,

pub const default: MouseScrollMultiplier = .{};

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.
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 = std.fmt.bufPrint(
&buf,
"precision:{d},discrete:{d}",
.{ self.precision, self.discrete },
) catch return error.OutOfMemory;
try formatter.formatEntry([]const u8, formatted);
}

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(alloc, "3");
try testing.expectApproxEqAbs(3, args.precision, epsilon);
try testing.expectApproxEqAbs(3, args.discrete, epsilon);

args = .{ .precision = 0.1, .discrete = 3 };
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 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 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 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" {
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
pub const ClipboardAccess = enum {
allow,
Expand Down Expand Up @@ -7933,6 +8043,7 @@ pub const Theme = struct {
Theme,
alloc,
input,
null,
);
return;
}
Expand Down