Merge pull request #822 from gpanders/split-resizing

macos: implement split resizing
This commit is contained in:
Mitchell Hashimoto
2023-11-06 09:35:43 -08:00
committed by GitHub
16 changed files with 466 additions and 159 deletions

View File

@@ -2436,6 +2436,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
} else log.warn("runtime doesn't implement gotoSplit", .{});
},
.resize_split => |param| {
if (@hasDecl(apprt.Surface, "resizeSplit")) {
const direction = param[0];
const amount = param[1];
self.rt_surface.resizeSplit(direction, amount);
} else log.warn("runtime doesn't implement resizeSplit", .{});
},
.toggle_split_zoom => {
if (@hasDecl(apprt.Surface, "toggleSplitZoom")) {
self.rt_surface.toggleSplitZoom();

View File

@@ -92,6 +92,9 @@ pub const App = struct {
/// Focus the previous/next split (if any).
focus_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null,
/// Resize the current split.
resize_split: ?*const fn (SurfaceUD, input.SplitResizeDirection, u16) callconv(.C) void = null,
/// Zoom the current split.
toggle_split_zoom: ?*const fn (SurfaceUD) callconv(.C) void = null,
@@ -384,6 +387,15 @@ pub const Surface = struct {
func(self.opts.userdata, direction);
}
pub fn resizeSplit(self: *const Surface, direction: input.SplitResizeDirection, amount: u16) void {
const func = self.app.opts.resize_split orelse {
log.info("runtime embedder does not support resize split", .{});
return;
};
func(self.opts.userdata, direction, amount);
}
pub fn toggleSplitZoom(self: *const Surface) void {
const func = self.app.opts.toggle_split_zoom orelse {
log.info("runtime embedder does not support split zoom", .{});
@@ -1374,6 +1386,14 @@ pub const CAPI = struct {
ptr.gotoSplit(direction);
}
/// Resize the current split by moving the split divider in the given
/// direction. `direction` specifies which direction the split divider will
/// move relative to the focused split. `amount` is a fractional value
/// between 0 and 1 that specifies by how much the divider will move.
export fn ghostty_surface_split_resize(ptr: *Surface, direction: input.SplitResizeDirection, amount: u16) void {
ptr.resizeSplit(direction, amount);
}
/// Invoke an action on the surface.
export fn ghostty_surface_binding_action(
ptr: *Surface,

View File

@@ -980,6 +980,26 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .right, .mods = .{ .super = true, .alt = true } },
.{ .goto_split = .right },
);
try result.keybind.set.put(
alloc,
.{ .key = .up, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .up, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .down, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .down, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .left, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .left, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .right, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .right, 10 } },
);
// Inspector, matching Chromium
try result.keybind.set.put(

View File

@@ -11,6 +11,7 @@ pub const KeyEncoder = @import("input/KeyEncoder.zig");
pub const InspectorMode = Binding.Action.InspectorMode;
pub const SplitDirection = Binding.Action.SplitDirection;
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;
pub const SplitResizeDirection = Binding.Action.SplitResizeDirection;
// Keymap is only available on macOS right now. We could implement it
// in theory for XKB too on Linux but we don't need it right now.

View File

@@ -179,6 +179,10 @@ pub const Action = union(enum) {
/// zoom/unzoom the current split.
toggle_split_zoom: void,
/// Resize the current split by moving the split divider in the given
/// direction
resize_split: SplitResizeParameter,
/// Show, hide, or toggle the terminal inspector for the currently
/// focused terminal.
inspector: InspectorMode,
@@ -227,6 +231,19 @@ pub const Action = union(enum) {
right,
};
// Extern because it is used in the embedded runtime ABI.
pub const SplitResizeDirection = enum(c_int) {
up,
down,
left,
right,
};
pub const SplitResizeParameter = struct {
SplitResizeDirection,
u16,
};
// Extern because it is used in the embedded runtime ABI.
pub const InspectorMode = enum(c_int) {
toggle,
@@ -234,6 +251,53 @@ pub const Action = union(enum) {
hide,
};
fn parseEnum(comptime T: type, value: []const u8) !T {
return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat;
}
fn parseInt(comptime T: type, value: []const u8) !T {
return std.fmt.parseInt(T, value, 10) catch return Error.InvalidFormat;
}
fn parseFloat(comptime T: type, value: []const u8) !T {
return std.fmt.parseFloat(T, value) catch return Error.InvalidFormat;
}
fn parseParameter(
comptime field: std.builtin.Type.UnionField,
param: []const u8,
) !field.type {
return switch (@typeInfo(field.type)) {
.Enum => try parseEnum(field.type, param),
.Int => try parseInt(field.type, param),
.Float => try parseFloat(field.type, param),
.Struct => |info| blk: {
// Only tuples are supported to avoid ambiguity with field
// ordering
comptime assert(info.is_tuple);
var it = std.mem.split(u8, param, ",");
var value: field.type = undefined;
inline for (info.fields) |field_| {
const next = it.next() orelse return Error.InvalidFormat;
@field(value, field_.name) = switch (@typeInfo(field_.type)) {
.Enum => try parseEnum(field_.type, next),
.Int => try parseInt(field_.type, next),
.Float => try parseFloat(field_.type, next),
else => unreachable,
};
}
// If we have extra parameters it is an error
if (it.next() != null) return Error.InvalidFormat;
break :blk value;
},
else => unreachable,
};
}
/// Parse an action in the format of "key=value" where key is the
/// action name and value is the action parameter. The parameter
/// is optional depending on the action.
@@ -266,35 +330,14 @@ pub const Action = union(enum) {
// Cursor keys can't be set currently
Action.CursorKey => return Error.InvalidAction,
else => switch (@typeInfo(field.type)) {
.Enum => {
const idx = colonIdx orelse return Error.InvalidFormat;
const param = input[idx + 1 ..];
const value = std.meta.stringToEnum(
field.type,
param,
) orelse return Error.InvalidFormat;
return @unionInit(Action, field.name, value);
},
.Int => {
const idx = colonIdx orelse return Error.InvalidFormat;
const param = input[idx + 1 ..];
const value = std.fmt.parseInt(field.type, param, 10) catch
return Error.InvalidFormat;
return @unionInit(Action, field.name, value);
},
.Float => {
const idx = colonIdx orelse return Error.InvalidFormat;
const param = input[idx + 1 ..];
const value = std.fmt.parseFloat(field.type, param) catch
return Error.InvalidFormat;
return @unionInit(Action, field.name, value);
},
else => unreachable,
else => {
const idx = colonIdx orelse return Error.InvalidFormat;
const param = input[idx + 1 ..];
return @unionInit(
Action,
field.name,
try parseParameter(field, param),
);
},
}
}
@@ -316,24 +359,38 @@ pub const Action = union(enum) {
switch (self) {
inline else => |value| {
const Value = @TypeOf(value);
const value_info = @typeInfo(Value);
// All actions start with the tag.
try writer.print("{s}", .{@tagName(self)});
// Write the value depending on the type
switch (Value) {
void => {},
[]const u8 => try writer.print(":{s}", .{value}),
else => switch (value_info) {
.Enum => try writer.print(":{s}", .{@tagName(value)}),
.Float => try writer.print(":{d}", .{value}),
.Int => try writer.print(":{d}", .{value}),
.Struct => try writer.print("{} (not configurable)", .{value}),
else => @compileError("unhandled type: " ++ @typeName(Value)),
},
}
try writer.writeAll(":");
try formatValue(writer, value);
},
}
}
fn formatValue(
writer: anytype,
value: anytype,
) !void {
const Value = @TypeOf(value);
const value_info = @typeInfo(Value);
switch (Value) {
void => {},
[]const u8 => try writer.print("{s}", .{value}),
else => switch (value_info) {
.Enum => try writer.print("{s}", .{@tagName(value)}),
.Float => try writer.print("{d}", .{value}),
.Int => try writer.print("{d}", .{value}),
.Struct => |info| if (!info.is_tuple) {
try writer.print("{} (not configurable)", .{value});
} else {
inline for (info.fields, 0..) |field, i| {
try formatValue(writer, @field(value, field.name));
if (i + 1 < info.fields.len) try writer.writeAll(",");
}
},
else => @compileError("unhandled type: " ++ @typeName(Value)),
},
}
}
@@ -775,6 +832,27 @@ test "parse: action with float" {
}
}
test "parse: action with a tuple" {
const testing = std.testing;
// parameter
{
const binding = try parse("a=resize_split:up,10");
try testing.expect(binding.action == .resize_split);
try testing.expectEqual(Action.SplitResizeDirection.up, binding.action.resize_split[0]);
try testing.expectEqual(@as(u16, 10), binding.action.resize_split[1]);
}
// missing parameter
try testing.expectError(Error.InvalidFormat, parse("a=resize_split:up"));
// too many
try testing.expectError(Error.InvalidFormat, parse("a=resize_split:up,10,12"));
// invalid type
try testing.expectError(Error.InvalidFormat, parse("a=resize_split:up,four"));
}
test "set: maintains reverse mapping" {
const testing = std.testing;
const alloc = testing.allocator;