From bbfa984aec99c8d3e2e7dde1a10c7520f4f873cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 May 2026 09:10:30 -0700 Subject: [PATCH] libghostty: GhosttySelectionGestureEvent --- include/ghostty/vt/selection.h | 136 ++++++++++++ include/ghostty/vt/types.h | 32 +++ src/lib_vt.zig | 3 + src/terminal/c/main.zig | 3 + src/terminal/c/selection_gesture.zig | 312 +++++++++++++++++++++++++++ src/terminal/c/types.zig | 21 ++ 6 files changed, 507 insertions(+) diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h index dcdf9108c..053c3bc44 100644 --- a/include/ghostty/vt/selection.h +++ b/include/ghostty/vt/selection.h @@ -50,6 +50,16 @@ extern "C" { */ typedef struct GhosttySelectionGestureImpl* GhosttySelectionGesture; +/** + * Opaque handle to reusable input data for selection gesture operations. + * + * Event options are set with ghostty_selection_gesture_event_set(). Individual + * gesture operations document which options are required or optional. + * + * @ingroup selection + */ +typedef struct GhosttySelectionGestureEventImpl* GhosttySelectionGestureEvent; + /** * A snapshot selection range defined by two grid references. * @@ -315,6 +325,22 @@ typedef enum GHOSTTY_ENUM_TYPED { GHOSTTY_SELECTION_GESTURE_BEHAVIOR_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttySelectionGestureBehavior; +/** + * Selection behaviors for single-, double-, and triple-click gestures. + * + * @ingroup selection + */ +typedef struct { + /** Behavior for single-click selection gestures. */ + GhosttySelectionGestureBehavior single_click; + + /** Behavior for double-click selection gestures. */ + GhosttySelectionGestureBehavior double_click; + + /** Behavior for triple-click selection gestures. */ + GhosttySelectionGestureBehavior triple_click; +} GhosttySelectionGestureBehaviors; + /** * Current autoscroll direction for an active selection drag gesture. * @@ -364,6 +390,116 @@ typedef enum GHOSTTY_ENUM_TYPED { GHOSTTY_SELECTION_GESTURE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttySelectionGestureData; +/** + * Selection gesture event type. + * + * The event type is fixed when the event is created. Each event type documents + * which options are valid and which options are required by gesture operations. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Press event for ghostty_selection_gesture_press(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS = 0, + + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionGestureEventType; + +/** + * Options stored on a reusable selection gesture event. + * + * Passing NULL as the value to ghostty_selection_gesture_event_set() clears the + * corresponding option. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Grid reference under the pointer: GhosttyGridRef*. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF = 0, + + /** Surface-space pointer position: GhosttySurfacePosition*. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION = 1, + + /** Maximum repeat-click distance in pixels: double*. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_DISTANCE = 2, + + /** + * Optional monotonic event time in nanoseconds: uint64_t*. + * + * If unset, press treats the event as untimed and only single-click behavior + * is available. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_TIME_NS = 3, + + /** Maximum interval between repeat clicks in nanoseconds: uint64_t*. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_INTERVAL_NS = 4, + + /** + * Word-boundary codepoints: GhosttyCodepoints*. + * + * The codepoints are copied into event-owned storage when set. If unset, + * operations that need word boundaries use Ghostty's defaults. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS = 5, + + /** + * Selection behavior table: GhosttySelectionGestureBehaviors*. + * + * If unset, press uses the default behavior table: cell, word, line. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_BEHAVIORS = 6, + + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionGestureEventOption; + +/** + * Create a reusable selection gesture event object. + * + * @param allocator Allocator, or NULL for the default allocator + * @param out_event Receives the created event handle + * @param type Event type. This is fixed for the lifetime of the event. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if out_event is + * NULL or type is invalid, or GHOSTTY_OUT_OF_MEMORY if allocation fails + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_new( + const GhosttyAllocator* allocator, + GhosttySelectionGestureEvent* out_event, + GhosttySelectionGestureEventType type); + +/** + * Free a selection gesture event object. + * + * Passing NULL is allowed and is a no-op. + * + * @param event Selection gesture event handle to free + * + * @ingroup selection + */ +GHOSTTY_API void ghostty_selection_gesture_event_free( + GhosttySelectionGestureEvent event); + +/** + * Set or clear an option on a selection gesture event. + * + * The value type depends on option and is documented by + * GhosttySelectionGestureEventOption. Passing NULL for value clears the option. + * + * @param event Selection gesture event handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param option Event option to set or clear + * @param value Pointer to the input value for option, or NULL to clear + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if copying + * event-owned data fails, or GHOSTTY_INVALID_VALUE if event, option, or + * value is invalid + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_set( + GhosttySelectionGestureEvent event, + GhosttySelectionGestureEventOption option, + const void* value); + /** * Create a selection gesture object. * diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h index f3874153f..1bec223d6 100644 --- a/include/ghostty/vt/types.h +++ b/include/ghostty/vt/types.h @@ -227,6 +227,38 @@ typedef struct { size_t len; } GhosttyString; +/** + * A surface-space position in pixels. + * + * This is not a terminal grid coordinate. It represents an x/y position in the + * rendered surface coordinate space, with (0, 0) at the top-left of the + * surface. + */ +typedef struct { + /** X position in surface pixels. */ + double x; + + /** Y position in surface pixels. */ + double y; +} GhosttySurfacePosition; + +/** + * A borrowed list of Unicode scalar values. + * + * Values are encoded as uint32_t scalar values. The memory is not owned by this + * struct. The pointer is only valid for the lifetime documented by the API that + * consumes or produces it. + * + * APIs may document special handling for NULL + len 0, such as “use defaults”. + */ +typedef struct { + /** Pointer to Unicode scalar values. */ + const uint32_t* ptr; + + /** Number of entries in ptr. */ + size_t len; +} GhosttyCodepoints; + /** * Initialize a sized struct to zero and set its size field. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 1806d74bc..a7139476f 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -256,6 +256,9 @@ comptime { @export(&c.selection_gesture_reset, .{ .name = "ghostty_selection_gesture_reset" }); @export(&c.selection_gesture_get, .{ .name = "ghostty_selection_gesture_get" }); @export(&c.selection_gesture_get_multi, .{ .name = "ghostty_selection_gesture_get_multi" }); + @export(&c.selection_gesture_event_new, .{ .name = "ghostty_selection_gesture_event_new" }); + @export(&c.selection_gesture_event_free, .{ .name = "ghostty_selection_gesture_event_free" }); + @export(&c.selection_gesture_event_set, .{ .name = "ghostty_selection_gesture_event_set" }); @export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" }); @export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" }); @export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 3b853b467..959edaabe 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -188,6 +188,9 @@ pub const selection_gesture_free = selection_gesture.free; pub const selection_gesture_reset = selection_gesture.reset; pub const selection_gesture_get = selection_gesture.get; pub const selection_gesture_get_multi = selection_gesture.get_multi; +pub const selection_gesture_event_new = selection_gesture.event_new; +pub const selection_gesture_event_free = selection_gesture.event_free; +pub const selection_gesture_event_set = selection_gesture.event_set; pub const terminal_grid_ref = terminal.grid_ref; pub const terminal_grid_ref_track = terminal.grid_ref_track; pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref; diff --git a/src/terminal/c/selection_gesture.zig b/src/terminal/c/selection_gesture.zig index 70c3d76b5..8087a88a9 100644 --- a/src/terminal/c/selection_gesture.zig +++ b/src/terminal/c/selection_gesture.zig @@ -1,10 +1,13 @@ const std = @import("std"); const testing = std.testing; +const builtin = @import("builtin"); const lib = @import("../lib.zig"); const CAllocator = lib.alloc.Allocator; const SelectionGesture = @import("../SelectionGesture.zig"); +const selection_codepoints = @import("../selection_codepoints.zig"); const grid_ref = @import("grid_ref.zig"); const terminal_c = @import("terminal.zig"); +const types = @import("types.zig"); const Result = @import("result.zig").Result; const log = std.log.scoped(.selection_gesture_c); @@ -12,17 +15,71 @@ const log = std.log.scoped(.selection_gesture_c); /// C: GhosttySelectionGesture pub const Gesture = ?*GestureWrapper; +/// C: GhosttySelectionGestureEvent +pub const Event = ?*EventWrapper; + const GestureWrapper = struct { alloc: std.mem.Allocator, gesture: SelectionGesture = .init, }; +const EventWrapper = struct { + alloc: std.mem.Allocator, + event: union(EventType) { + press: SelectionGesture.Press, + }, + + // Backing storage for Press.word_boundary_codepoints. The C API receives + // codepoints as borrowed uint32_t values, but SelectionGesture.Press stores + // a []const u21 slice. We copy/convert into event-owned storage so the real + // Press payload can safely point at it until the event is changed or freed. + word_boundary_codepoints: ?[]u21 = null, + + // Backing storage for Press.behaviors. The C API sets behaviors as a value + // struct, but SelectionGesture.Press stores a pointer to a [3]Behavior. + // Keep the array on the event wrapper so the Press payload can point at a + // stable location for the lifetime of the event. + behaviors: [3]Behavior = SelectionGesture.default_behaviors, + + fn init(self: *EventWrapper, event_type: EventType) void { + self.event = switch (event_type) { + .press => .{ .press = self.defaultPress() }, + }; + } + + fn defaultPress(self: *EventWrapper) SelectionGesture.Press { + return .{ + .time = null, + .pin = undefined, + .xpos = 0, + .ypos = 0, + .max_distance = 0, + .repeat_interval = 0, + .word_boundary_codepoints = &selection_codepoints.default_word_boundaries, + .behaviors = &self.behaviors, + }; + } + + fn deinit(self: *EventWrapper) void { + if (self.word_boundary_codepoints) |cps| { + if (cps.len > 0) self.alloc.free(cps); + } + } +}; + /// C: GhosttySelectionGestureBehavior pub const Behavior = SelectionGesture.Behavior; /// C: GhosttySelectionGestureAutoscroll pub const Autoscroll = SelectionGesture.Autoscroll; +/// C: GhosttySelectionGestureBehaviors +pub const Behaviors = extern struct { + single_click: Behavior, + double_click: Behavior, + triple_click: Behavior, +}; + /// C: GhosttySelectionGestureData pub const Data = enum(c_int) { click_count = 0, @@ -42,6 +99,34 @@ pub const Data = enum(c_int) { } }; +/// C: GhosttySelectionGestureEventType +pub const EventType = enum(c_int) { + press = 0, +}; + +/// C: GhosttySelectionGestureEventOption +pub const EventOption = enum(c_int) { + ref = 0, + position = 1, + repeat_distance = 2, + time_ns = 3, + repeat_interval_ns = 4, + word_boundary_codepoints = 5, + behaviors = 6, + + pub fn Type(comptime self: EventOption) type { + return switch (self) { + .ref => grid_ref.CGridRef, + .position => types.SurfacePosition, + .repeat_distance => f64, + .time_ns => u64, + .repeat_interval_ns => u64, + .word_boundary_codepoints => types.Codepoints, + .behaviors => Behaviors, + }; + } +}; + pub fn new( alloc_: ?*const CAllocator, out_gesture: ?*Gesture, @@ -60,6 +145,29 @@ pub fn new( return .success; } +pub fn event_new( + alloc_: ?*const CAllocator, + out_event: ?*Event, + event_type: EventType, +) callconv(lib.calling_conv) Result { + const out = out_event orelse return .invalid_value; + _ = std.meta.intToEnum(EventType, @intFromEnum(event_type)) catch + return .invalid_value; + + const alloc = lib.alloc.default(alloc_); + const event = alloc.create(EventWrapper) catch { + out.* = null; + return .out_of_memory; + }; + event.* = .{ + .alloc = alloc, + .event = undefined, + }; + event.init(event_type); + out.* = event; + return .success; +} + pub fn free( gesture_: Gesture, terminal: terminal_c.Terminal, @@ -72,6 +180,13 @@ pub fn free( alloc.destroy(wrapper); } +pub fn event_free(event_: Event) callconv(lib.calling_conv) void { + const event = event_ orelse return; + event.deinit(); + const alloc = event.alloc; + alloc.destroy(event); +} + pub fn reset( gesture_: Gesture, terminal: terminal_c.Terminal, @@ -81,6 +196,27 @@ pub fn reset( wrapper.gesture.reset(t); } +pub fn event_set( + event_: Event, + option: EventOption, + value: ?*const anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(EventOption, @intFromEnum(option)) catch { + log.warn("selection_gesture_event_set invalid option value={d}", .{@intFromEnum(option)}); + return .invalid_value; + }; + } + + return switch (option) { + inline else => |comptime_option| eventSetTyped( + event_, + comptime_option, + if (value) |ptr| @ptrCast(@alignCast(ptr)) else null, + ), + }; +} + pub fn get( gesture_: Gesture, terminal: terminal_c.Terminal, @@ -151,6 +287,102 @@ fn getTyped( return .success; } +fn eventSetTyped( + event_: Event, + comptime option: EventOption, + value: ?*const option.Type(), +) Result { + const event = event_ orelse return .invalid_value; + return switch (event.event) { + .press => |*press| pressSetTyped(event, press, option, value), + }; +} + +fn pressSetTyped( + event: *EventWrapper, + press: *SelectionGesture.Press, + comptime option: EventOption, + value: ?*const option.Type(), +) Result { + const v = value orelse { + switch (option) { + .ref => {}, + .position => { + press.xpos = 0; + press.ypos = 0; + }, + .repeat_distance => press.max_distance = 0, + .time_ns => press.time = null, + .repeat_interval_ns => press.repeat_interval = 0, + .word_boundary_codepoints => clearPressCodepoints(event, press), + .behaviors => { + event.behaviors = SelectionGesture.default_behaviors; + press.behaviors = &event.behaviors; + }, + } + return .success; + }; + + switch (option) { + .ref => press.pin = v.toPin() orelse return .invalid_value, + .position => { + press.xpos = v.x; + press.ypos = v.y; + }, + .repeat_distance => press.max_distance = v.*, + .time_ns => press.time = instantFromNs(v.*), + .repeat_interval_ns => press.repeat_interval = v.*, + .word_boundary_codepoints => { + if (v.len > 0 and v.ptr == null) return .invalid_value; + clearPressCodepoints(event, press); + const ptr = v.ptr orelse { + event.word_boundary_codepoints = &.{}; + press.word_boundary_codepoints = event.word_boundary_codepoints.?; + return .success; + }; + const copy = event.alloc.alloc(u21, v.len) catch return .out_of_memory; + errdefer event.alloc.free(copy); + for (copy, ptr[0..v.len]) |*dst, cp| { + dst.* = std.math.cast(u21, cp) orelse return .invalid_value; + } + event.word_boundary_codepoints = copy; + press.word_boundary_codepoints = copy; + }, + .behaviors => { + if (!validBehavior(v.single_click) or + !validBehavior(v.double_click) or + !validBehavior(v.triple_click)) return .invalid_value; + event.behaviors = .{ v.single_click, v.double_click, v.triple_click }; + press.behaviors = &event.behaviors; + }, + } + + return .success; +} + +fn clearPressCodepoints(event: *EventWrapper, press: *SelectionGesture.Press) void { + if (event.word_boundary_codepoints) |cps| { + if (cps.len > 0) event.alloc.free(cps); + } + event.word_boundary_codepoints = null; + press.word_boundary_codepoints = &selection_codepoints.default_word_boundaries; +} + +fn instantFromNs(ns: u64) std.time.Instant { + return switch (builtin.os.tag) { + .windows, .uefi, .wasi => .{ .timestamp = ns }, + else => .{ .timestamp = .{ + .sec = @intCast(ns / std.time.ns_per_s), + .nsec = @intCast(ns % std.time.ns_per_s), + } }, + }; +} + +fn validBehavior(behavior: Behavior) bool { + _ = std.meta.intToEnum(Behavior, @intFromEnum(behavior)) catch return false; + return true; +} + test "selection gesture lifecycle and get" { var terminal: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( @@ -267,6 +499,86 @@ test "selection gesture get_multi returns first failing index" { try testing.expect(dragged); } +test "selection gesture event set clear and free" { + var event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &event, + .press, + )); + defer event_free(event); + + const in_pos: types.SurfacePosition = .{ .x = 12.5, .y = -3.25 }; + try testing.expectEqual(Result.success, event_set(event, .position, &in_pos)); + try testing.expectEqual(@as(f64, 12.5), event.?.event.press.xpos); + try testing.expectEqual(@as(f64, -3.25), event.?.event.press.ypos); + + try testing.expectEqual(Result.success, event_set(event, .position, null)); + try testing.expectEqual(@as(f64, 0), event.?.event.press.xpos); + try testing.expectEqual(@as(f64, 0), event.?.event.press.ypos); + + const repeat_distance: f64 = 4.0; + try testing.expectEqual(Result.success, event_set(event, .repeat_distance, &repeat_distance)); + try testing.expectEqual(repeat_distance, event.?.event.press.max_distance); +} + +test "selection gesture event copies clears and frees codepoints" { + var event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &event, + .press, + )); + defer event_free(event); + + var values = [_]u32{ ' ', '\t' }; + const in: types.Codepoints = .{ .ptr = &values, .len = values.len }; + try testing.expectEqual(Result.success, event_set(event, .word_boundary_codepoints, &in)); + + values[0] = 'x'; + + try testing.expectEqual(@as(usize, 2), event.?.event.press.word_boundary_codepoints.len); + try testing.expectEqual(@as(u21, ' '), event.?.event.press.word_boundary_codepoints[0]); + try testing.expectEqual(@as(u21, '\t'), event.?.event.press.word_boundary_codepoints[1]); + + const invalid: types.Codepoints = .{ .ptr = null, .len = 1 }; + try testing.expectEqual(Result.invalid_value, event_set(event, .word_boundary_codepoints, &invalid)); + + try testing.expectEqual(Result.success, event_set(event, .word_boundary_codepoints, null)); + try testing.expectEqual( + selection_codepoints.default_word_boundaries.len, + event.?.event.press.word_boundary_codepoints.len, + ); + + const empty: types.Codepoints = .{ .ptr = null, .len = 0 }; + try testing.expectEqual(Result.success, event_set(event, .word_boundary_codepoints, &empty)); + try testing.expectEqual(@as(usize, 0), event.?.event.press.word_boundary_codepoints.len); +} + +test "selection gesture event behaviors" { + var event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &event, + .press, + )); + defer event_free(event); + + const in: Behaviors = .{ + .single_click = .cell, + .double_click = .word, + .triple_click = .line, + }; + try testing.expectEqual(Result.success, event_set(event, .behaviors, &in)); + try testing.expectEqual(Behavior.cell, event.?.event.press.behaviors[0]); + try testing.expectEqual(Behavior.word, event.?.event.press.behaviors[1]); + try testing.expectEqual(Behavior.line, event.?.event.press.behaviors[2]); +} + test "selection gesture free null" { free(null, null); } + +test "selection gesture event free null" { + event_free(null); +} diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index d9ece57ee..9a4ef3d56 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -14,15 +14,29 @@ const size_report = @import("size_report.zig"); const terminal = @import("terminal.zig"); const formatter = @import("formatter.zig"); const selection = @import("selection.zig"); +const selection_gesture = @import("selection_gesture.zig"); const render = @import("render.zig"); const style_c = @import("style.zig"); const mouse_encode = @import("mouse_encode.zig"); const grid_ref = @import("grid_ref.zig"); +/// C: GhosttySurfacePosition +pub const SurfacePosition = extern struct { + x: f64, + y: f64, +}; + +/// C: GhosttyCodepoints +pub const Codepoints = extern struct { + ptr: ?[*]const u32 = null, + len: usize = 0, +}; + /// All C API structs and their Ghostty C names. pub const structs: std.StaticStringMap(StructInfo) = structs: { @setEvalBranchQuota(10_000); break :structs .initComptime(.{ + .{ "GhosttyCodepoints", StructInfo.init(Codepoints) }, .{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) }, .{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) }, .{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) }, @@ -41,8 +55,10 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: { .{ "GhosttyPoint", StructInfo.init(point.Point.C) }, .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) }, .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) }, + .{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) }, .{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) }, .{ "GhosttyString", StructInfo.init(lib.String) }, + .{ "GhosttySurfacePosition", StructInfo.init(SurfacePosition) }, .{ "GhosttyStyle", StructInfo.init(style_c.Style) }, .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) }, .{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) }, @@ -150,6 +166,11 @@ fn jsonWriteAll(writer: *std.Io.Writer) std.Io.Writer.Error!void { fn typeName(comptime T: type) []const u8 { return switch (@typeInfo(T)) { .bool => "bool", + .float => |info| switch (info.bits) { + 32 => "f32", + 64 => "f64", + else => @compileError("unsupported float size"), + }, .int => |info| switch (info.signedness) { .signed => switch (info.bits) { 8 => "i8",