Mouse Reporting #8

Implements all known formats and event types for mouse reporting. This makes vim, htop, etc. handle mouse events!

Mouse formats:

  * X10
  * UTF-8
  * SGR
  * urxvt
  * SGR Pixels

Event types:

  * X10
  * "Normal" - mouse button press/release, including scroll wheel
  * "Button" - "Normal" + mouse motion events while a button is pressed
  * "Any" - "Normal" + mouse motion events at anytime (even if a button is not pressed)

See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
This commit is contained in:
Mitchell Hashimoto
2022-08-26 15:10:14 -07:00
committed by GitHub
5 changed files with 408 additions and 73 deletions

View File

@ -137,20 +137,25 @@ const Cursor = struct {
/// Mouse state for the window. /// Mouse state for the window.
const Mouse = struct { const Mouse = struct {
/// The current state of mouse click. /// The last tracked mouse button state by button.
click_state: ClickState = .none, click_state: [input.MouseButton.max]input.MouseButtonState = .{.release} ** input.MouseButton.max,
/// The point at which the mouse click happened. This is in screen /// The last mods state when the last mouse button (whatever it was) was
/// pressed or release.
mods: input.Mods = .{},
/// The point at which the left mouse click happened. This is in screen
/// coordinates so that scrolling preserves the location. /// coordinates so that scrolling preserves the location.
click_point: terminal.point.ScreenPoint = .{}, left_click_point: terminal.point.ScreenPoint = .{},
/// The starting xpos/ypos of the click. This is only useful initially. /// The starting xpos/ypos of the left click. Note that if scrolling occurs,
/// As soon as scrolling occurs, these are no longer accurate to calculate /// these will point to different "cells", but the xpos/ypos will stay
/// the screen point. /// stable during scrolling relative to the window.
click_xpos: f64 = 0, left_click_xpos: f64 = 0,
click_ypos: f64 = 0, left_click_ypos: f64 = 0,
const ClickState = enum { none, left }; /// The last x/y sent for mouse reports.
event_point: terminal.point.Viewport = .{},
}; };
/// Create a new window. This allocates and returns a pointer because we /// Create a new window. This allocates and returns a pointer because we
@ -754,6 +759,19 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
const win = window.getUserPointer(Window) orelse return; const win = window.getUserPointer(Window) orelse return;
// If we're scrolling up or down, then send a mouse event
if (yoff != 0) {
const pos = window.getCursorPos() catch |err| {
log.err("error reading cursor position: {}", .{err});
return;
};
win.mouseReport(if (yoff < 0) .four else .five, .press, win.mouse.mods, pos) catch |err| {
log.err("error reporting mouse event: {}", .{err});
return;
};
}
//log.info("SCROLL: {} {}", .{ xoff, yoff }); //log.info("SCROLL: {} {}", .{ xoff, yoff });
_ = xoff; _ = xoff;
@ -769,10 +787,178 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
win.render_timer.schedule() catch unreachable; win.render_timer.schedule() catch unreachable;
} }
/// The type of action to report for a mouse event.
const MouseReportAction = enum { press, release, motion };
fn mouseReport(
self: *Window,
button: ?input.MouseButton,
action: MouseReportAction,
mods: input.Mods,
unscaled_pos: glfw.Window.CursorPos,
) !void {
// TODO: posToViewport currently clamps to the window boundary,
// do we want to not report mouse events at all outside the window?
// Depending on the event, we may do nothing at all.
switch (self.terminal.modes.mouse_event) {
.none => return,
// X10 only reports clicks with mouse button 1, 2, 3. We verify
// the button later.
.x10 => if (action != .press or
button == null or
!(button.? == .left or
button.? == .right or
button.? == .middle)) return,
// Doesn't report motion
.normal => if (action == .motion) return,
// Button must be pressed
.button => if (button == null) return,
// Everything
.any => {},
}
// This format reports X/Y
const pos = self.cursorPosToPixels(unscaled_pos);
const viewport_point = self.posToViewport(pos.xpos, pos.ypos);
// For button events, we only report if we moved cells
if (self.terminal.modes.mouse_event == .button or
self.terminal.modes.mouse_event == .any)
{
if (self.mouse.event_point.x == viewport_point.x and
self.mouse.event_point.y == viewport_point.y) return;
// Record our new point
self.mouse.event_point = viewport_point;
}
// Get the code we'll actually write
const button_code: u8 = code: {
var acc: u8 = 0;
// Determine our initial button value
if (button == null) {
// Null button means motion without a button pressed
acc = 3;
} else if (action == .release and self.terminal.modes.mouse_format != .sgr) {
// Release is 3. It is NOT 3 in SGR mode because SGR can tell
// the application what button was released.
acc = 3;
} else {
acc = switch (button.?) {
.left => 0,
.right => 1,
.middle => 2,
.four => 64,
.five => 65,
else => return, // unsupported
};
}
// X10 doesn't have modifiers
if (self.terminal.modes.mouse_event != .x10) {
if (mods.shift) acc += 4;
if (mods.super) acc += 8;
if (mods.ctrl) acc += 16;
}
// Motion adds another bit
if (action == .motion) acc += 32;
break :code acc;
};
switch (self.terminal.modes.mouse_format) {
.x10 => {
if (viewport_point.x > 222 or viewport_point.y > 222) {
log.info("X10 mouse format can only encode X/Y up to 223", .{});
return;
}
// + 1 below is because our x/y is 0-indexed and proto wants 1
var buf = [_]u8{ '\x1b', '[', 'M', 0, 0, 0 };
buf[3] = 32 + button_code;
buf[4] = 32 + @intCast(u8, viewport_point.x) + 1;
buf[5] = 32 + @intCast(u8, viewport_point.y) + 1;
try self.queueWrite(&buf);
},
.utf8 => {
// Maximum of 12 because at most we have 2 fully UTF-8 encoded chars
var buf: [12]u8 = undefined;
buf[0] = '\x1b';
buf[1] = '[';
buf[2] = 'M';
// The button code will always fit in a single u8
buf[3] = 32 + button_code;
// UTF-8 encode the x/y
var i: usize = 4;
i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.x + 1), buf[i..]);
i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.y + 1), buf[i..]);
try self.queueWrite(buf[0..i]);
},
.sgr => {
// Final character to send in the CSI
const final: u8 = if (action == .release) 'm' else 'M';
// Response always is at least 4 chars, so this leaves the
// remainder for numbers which are very large...
var buf: [32]u8 = undefined;
const resp = try std.fmt.bufPrint(&buf, "\x1B[<{d};{d};{d}{c}", .{
button_code,
viewport_point.x + 1,
viewport_point.y + 1,
final,
});
try self.queueWrite(resp);
},
.urxvt => {
// Response always is at least 4 chars, so this leaves the
// remainder for numbers which are very large...
var buf: [32]u8 = undefined;
const resp = try std.fmt.bufPrint(&buf, "\x1B[{d};{d};{d}M", .{
32 + button_code,
viewport_point.x + 1,
viewport_point.y + 1,
});
try self.queueWrite(resp);
},
.sgr_pixels => {
// Final character to send in the CSI
const final: u8 = if (action == .release) 'm' else 'M';
// Response always is at least 4 chars, so this leaves the
// remainder for numbers which are very large...
var buf: [32]u8 = undefined;
const resp = try std.fmt.bufPrint(&buf, "\x1B[<{d};{d};{d}{c}", .{
button_code,
pos.xpos,
pos.ypos,
final,
});
try self.queueWrite(resp);
},
}
}
fn mouseButtonCallback( fn mouseButtonCallback(
window: glfw.Window, window: glfw.Window,
button: glfw.MouseButton, glfw_button: glfw.MouseButton,
action: glfw.Action, glfw_action: glfw.Action,
mods: glfw.Mods, mods: glfw.Mods,
) void { ) void {
_ = mods; _ = mods;
@ -780,42 +966,71 @@ fn mouseButtonCallback(
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
if (button == .left) { const win = window.getUserPointer(Window) orelse return;
switch (action) {
.press => {
const win = window.getUserPointer(Window) orelse return;
const pos = win.cursorPosToPixels(window.getCursorPos() catch |err| {
log.err("error reading cursor position: {}", .{err});
return;
});
// Store it // Convert glfw button to input button
const point = win.posToViewport(pos.xpos, pos.ypos); const button: input.MouseButton = switch (glfw_button) {
win.mouse.click_state = .left; .left => .left,
win.mouse.click_point = point.toScreen(&win.terminal.screen); .right => .right,
win.mouse.click_xpos = pos.xpos; .middle => .middle,
win.mouse.click_ypos = pos.ypos; .four => .four,
log.debug("click start state={} viewport={} screen={}", .{ .five => .five,
win.mouse.click_state, .six => .six,
point, .seven => .seven,
win.mouse.click_point, .eight => .eight,
}); };
const action: input.MouseButtonState = switch (glfw_action) {
.press => .press,
.release => .release,
else => unreachable,
};
// Selection is always cleared // Always record our latest mouse state
if (win.terminal.selection != null) { win.mouse.click_state[@enumToInt(button)] = action;
win.terminal.selection = null; win.mouse.mods = @bitCast(input.Mods, mods);
win.render_timer.schedule() catch |err|
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
}
},
.release => { // Report mouse events if enabled
const win = window.getUserPointer(Window) orelse return; if (win.terminal.modes.mouse_event != .none) {
win.mouse.click_state = .none; const pos = window.getCursorPos() catch |err| {
log.debug("click end", .{}); log.err("error reading cursor position: {}", .{err});
}, return;
};
.repeat => {}, const report_action: MouseReportAction = switch (action) {
.press => .press,
.release => .release,
};
win.mouseReport(
button,
report_action,
win.mouse.mods,
pos,
) catch |err| {
log.err("error reporting mouse event: {}", .{err});
return;
};
}
// For left button clicks we always record some information for
// selection/highlighting purposes.
if (button == .left and action == .press) {
const pos = win.cursorPosToPixels(window.getCursorPos() catch |err| {
log.err("error reading cursor position: {}", .{err});
return;
});
// Store it
const point = win.posToViewport(pos.xpos, pos.ypos);
win.mouse.left_click_point = point.toScreen(&win.terminal.screen);
win.mouse.left_click_xpos = pos.xpos;
win.mouse.left_click_ypos = pos.ypos;
// Selection is always cleared
if (win.terminal.selection != null) {
win.terminal.selection = null;
win.render_timer.schedule() catch |err|
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
} }
} }
} }
@ -830,8 +1045,30 @@ fn cursorPosCallback(
const win = window.getUserPointer(Window) orelse return; const win = window.getUserPointer(Window) orelse return;
// Do a mouse report
if (win.terminal.modes.mouse_event != .none) {
// We use the first mouse button we find pressed in order to report
// since the spec (afaict) does not say...
const button: ?input.MouseButton = button: for (win.mouse.click_state) |state, i| {
if (state == .press)
break :button @intToEnum(input.MouseButton, i);
} else null;
win.mouseReport(button, .motion, win.mouse.mods, .{
.xpos = unscaled_xpos,
.ypos = unscaled_ypos,
}) catch |err| {
log.err("error reporting mouse event: {}", .{err});
return;
};
// If we're doing mouse motion tracking, we do not support text
// selection.
return;
}
// If the cursor isn't clicked currently, it doesn't matter // If the cursor isn't clicked currently, it doesn't matter
if (win.mouse.click_state != .left) return; if (win.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return;
// All roads lead to requiring a re-render at this pont. // All roads lead to requiring a re-render at this pont.
win.render_timer.schedule() catch |err| win.render_timer.schedule() catch |err|
@ -880,13 +1117,13 @@ fn cursorPosCallback(
const cell_xboundary = win.grid.cell_size.width * 0.6; const cell_xboundary = win.grid.cell_size.width * 0.6;
// first xpos of the clicked cell // first xpos of the clicked cell
const cell_xstart = @intToFloat(f32, win.mouse.click_point.x) * win.grid.cell_size.width; const cell_xstart = @intToFloat(f32, win.mouse.left_click_point.x) * win.grid.cell_size.width;
const cell_start_xpos = win.mouse.click_xpos - cell_xstart; const cell_start_xpos = win.mouse.left_click_xpos - cell_xstart;
// If this is the same cell, then we only start the selection if weve // If this is the same cell, then we only start the selection if weve
// moved past the boundary point the opposite direction from where we // moved past the boundary point the opposite direction from where we
// started. // started.
if (std.meta.eql(screen_point, win.mouse.click_point)) { if (std.meta.eql(screen_point, win.mouse.left_click_point)) {
const cell_xpos = xpos - cell_xstart; const cell_xpos = xpos - cell_xstart;
const selected: bool = if (cell_start_xpos < cell_xboundary) const selected: bool = if (cell_start_xpos < cell_xboundary)
cell_xpos >= cell_xboundary cell_xpos >= cell_xboundary
@ -908,9 +1145,9 @@ fn cursorPosCallback(
// the starting cell if we started after the boundary, else // the starting cell if we started after the boundary, else
// we start selection of the prior cell. // we start selection of the prior cell.
// - Inverse logic for a point after the start. // - Inverse logic for a point after the start.
const click_point = win.mouse.click_point; const click_point = win.mouse.left_click_point;
const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: { const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: {
if (win.mouse.click_xpos > cell_xboundary) { if (win.mouse.left_click_xpos > cell_xboundary) {
break :start click_point; break :start click_point;
} else { } else {
break :start if (click_point.x > 0) terminal.point.ScreenPoint{ break :start if (click_point.x > 0) terminal.point.ScreenPoint{
@ -922,7 +1159,7 @@ fn cursorPosCallback(
}; };
} }
} else start: { } else start: {
if (win.mouse.click_xpos < cell_xboundary) { if (win.mouse.left_click_xpos < cell_xboundary) {
break :start click_point; break :start click_point;
} else { } else {
break :start if (click_point.x < win.terminal.screen.cols - 1) terminal.point.ScreenPoint{ break :start if (click_point.x < win.terminal.screen.cols - 1) terminal.point.ScreenPoint{
@ -1069,7 +1306,7 @@ fn renderTimerCallback(t: *libuv.Timer) void {
win.grid.background = bg; win.grid.background = bg;
win.grid.foreground = fg; win.grid.foreground = fg;
} }
if (win.terminal.modes.reverse_colors == 1) { if (win.terminal.modes.reverse_colors) {
win.grid.background = fg; win.grid.background = fg;
win.grid.foreground = bg; win.grid.foreground = bg;
} }
@ -1080,7 +1317,7 @@ fn renderTimerCallback(t: *libuv.Timer) void {
g: f32, g: f32,
b: f32, b: f32,
a: f32, a: f32,
} = if (win.terminal.modes.reverse_colors == 1) .{ } = if (win.terminal.modes.reverse_colors) .{
.r = @intToFloat(f32, fg.r) / 255, .r = @intToFloat(f32, fg.r) / 255,
.g = @intToFloat(f32, fg.g) / 255, .g = @intToFloat(f32, fg.g) / 255,
.b = @intToFloat(f32, fg.b) / 255, .b = @intToFloat(f32, fg.b) / 255,
@ -1167,7 +1404,7 @@ pub fn setCursorCol(self: *Window, col: u16) !void {
} }
pub fn setCursorRow(self: *Window, row: u16) !void { pub fn setCursorRow(self: *Window, row: u16) !void {
if (self.terminal.modes.origin == 1) { if (self.terminal.modes.origin) {
// TODO // TODO
log.err("setCursorRow: implement origin mode", .{}); log.err("setCursorRow: implement origin mode", .{});
unreachable; unreachable;
@ -1234,19 +1471,19 @@ pub fn setTopAndBottomMargin(self: *Window, top: u16, bot: u16) !void {
pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void { pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void {
switch (mode) { switch (mode) {
.reverse_colors => { .reverse_colors => {
self.terminal.modes.reverse_colors = @boolToInt(enabled); self.terminal.modes.reverse_colors = enabled;
// Schedule a render since we changed colors // Schedule a render since we changed colors
try self.render_timer.schedule(); try self.render_timer.schedule();
}, },
.origin => { .origin => {
self.terminal.modes.origin = @boolToInt(enabled); self.terminal.modes.origin = enabled;
self.terminal.setCursorPos(1, 1); self.terminal.setCursorPos(1, 1);
}, },
.autowrap => { .autowrap => {
self.terminal.modes.autowrap = @boolToInt(enabled); self.terminal.modes.autowrap = enabled;
}, },
.cursor_visible => { .cursor_visible => {
@ -1284,6 +1521,16 @@ pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void {
if (enabled) .@"132_cols" else .@"80_cols", if (enabled) .@"132_cols" else .@"80_cols",
), ),
.mouse_event_x10 => self.terminal.modes.mouse_event = if (enabled) .x10 else .none,
.mouse_event_normal => self.terminal.modes.mouse_event = if (enabled) .normal else .none,
.mouse_event_button => self.terminal.modes.mouse_event = if (enabled) .button else .none,
.mouse_event_any => self.terminal.modes.mouse_event = if (enabled) .any else .none,
.mouse_format_utf8 => self.terminal.modes.mouse_format = if (enabled) .utf8 else .x10,
.mouse_format_sgr => self.terminal.modes.mouse_format = if (enabled) .sgr else .x10,
.mouse_format_urxvt => self.terminal.modes.mouse_format = if (enabled) .urxvt else .x10,
.mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10,
else => if (enabled) log.warn("unimplemented mode: {}", .{mode}), else => if (enabled) log.warn("unimplemented mode: {}", .{mode}),
} }
} }
@ -1323,7 +1570,7 @@ pub fn deviceStatusReport(
const pos: struct { const pos: struct {
x: usize, x: usize,
y: usize, y: usize,
} = if (self.terminal.modes.origin == 1) .{ } = if (self.terminal.modes.origin) .{
// TODO: what do we do if cursor is outside scrolling region? // TODO: what do we do if cursor is outside scrolling region?
.x = self.terminal.screen.cursor.x, .x = self.terminal.screen.cursor.x,
.y = self.terminal.screen.cursor.y -| self.terminal.scrolling_region.top, .y = self.terminal.screen.cursor.y -| self.terminal.scrolling_region.top,

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
pub usingnamespace @import("input/mouse.zig");
pub usingnamespace @import("input/key.zig"); pub usingnamespace @import("input/key.zig");
pub const Binding = @import("input/Binding.zig"); pub const Binding = @import("input/Binding.zig");

38
src/input/mouse.zig Normal file
View File

@ -0,0 +1,38 @@
/// The state of a mouse button.
pub const MouseButtonState = enum(u1) {
release = 0,
press = 1,
};
/// Possible mouse buttons. We only track up to 11 because thats the maximum
/// button input that terminal mouse tracking handles without becoming
/// ambiguous.
///
/// Its a bit silly to name numbers like this but given its a restricted
/// set, it feels better than passing around raw numeric literals.
pub const MouseButton = enum(u4) {
const Self = @This();
/// The maximum value in this enum. This can be used to create a densely
/// packed array, for example.
pub const max = max: {
var cur = 0;
for (@typeInfo(Self).Enum.fields) |field| {
if (field.value > cur) cur = field.value;
}
break :max cur;
};
left = 1,
right = 2,
middle = 3,
four = 4,
five = 5,
six = 6,
seven = 7,
eight = 8,
nine = 9,
ten = 10,
eleven = 11,
};

View File

@ -61,21 +61,44 @@ scrolling_region: ScrollingRegion,
modes: packed struct { modes: packed struct {
const Self = @This(); const Self = @This();
reverse_colors: u1 = 0, // 5, reverse_colors: bool = false, // 5,
origin: u1 = 0, // 6 origin: bool = false, // 6
autowrap: u1 = 1, // 7 autowrap: bool = true, // 7
deccolm: u1 = 0, // 3, deccolm: bool = false, // 3,
deccolm_supported: u1 = 0, // 40 deccolm_supported: bool = false, // 40
mouse_event: MouseEvents = .none,
mouse_format: MouseFormat = .x10,
test { test {
// We have this here so that we explicitly fail when we change the // We have this here so that we explicitly fail when we change the
// size of modes. The size of modes is NOT particularly important, // size of modes. The size of modes is NOT particularly important,
// we just want to be mentally aware when it happens. // we just want to be mentally aware when it happens.
try std.testing.expectEqual(1, @sizeOf(Self)); try std.testing.expectEqual(2, @sizeOf(Self));
} }
} = .{}, } = .{},
/// The event types that can be reported for mouse-related activities.
/// These are all mutually exclusive (hence in a single enum).
pub const MouseEvents = enum(u3) {
none = 0,
x10 = 1, // 9
normal = 2, // 1000
button = 3, // 1002
any = 4, // 1003
};
/// The format of mouse events when enabled.
/// These are all mutually exclusive (hence in a single enum).
pub const MouseFormat = enum(u3) {
x10 = 0,
utf8 = 1, // 1005
sgr = 2, // 1006
urxvt = 3, // 1015
sgr_pixels = 4, // 1016
};
/// Scrolling region is the area of the screen designated where scrolling /// Scrolling region is the area of the screen designated where scrolling
/// occurs. Wen scrolling the screen, only this viewport is scrolled. /// occurs. Wen scrolling the screen, only this viewport is scrolled.
const ScrollingRegion = struct { const ScrollingRegion = struct {
@ -197,10 +220,10 @@ pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void {
// bit. If the mode "?40" is set, then "?3" (DECCOLM) is supported. This // bit. If the mode "?40" is set, then "?3" (DECCOLM) is supported. This
// doesn't exactly match VT100 semantics but modern terminals no longer // doesn't exactly match VT100 semantics but modern terminals no longer
// blindly accept mode 3 since its so weird in modern practice. // blindly accept mode 3 since its so weird in modern practice.
if (self.modes.deccolm_supported == 0) return; if (!self.modes.deccolm_supported) return;
// Enable it // Enable it
self.modes.deccolm = @enumToInt(mode); self.modes.deccolm = mode == .@"132_cols";
// Resize -- we can set cols to 0 because deccolm will force it // Resize -- we can set cols to 0 because deccolm will force it
try self.resize(alloc, 0, self.rows); try self.resize(alloc, 0, self.rows);
@ -217,7 +240,7 @@ pub fn setDeccolmSupported(self: *Terminal, v: bool) void {
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
self.modes.deccolm_supported = @boolToInt(v); self.modes.deccolm_supported = v;
} }
/// Resize the underlying terminal. /// Resize the underlying terminal.
@ -228,8 +251,8 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) !
// If we have deccolm supported then we are fixed at either 80 or 132 // If we have deccolm supported then we are fixed at either 80 or 132
// columns depending on if mode 3 is set or not. // columns depending on if mode 3 is set or not.
// TODO: test // TODO: test
const cols: usize = if (self.modes.deccolm_supported == 1) const cols: usize = if (self.modes.deccolm_supported)
@as(usize, if (self.modes.deccolm == 1) 132 else 80) @as(usize, if (self.modes.deccolm) 132 else 80)
else else
cols_req; cols_req;
@ -370,7 +393,7 @@ pub fn print(self: *Terminal, c: u21) !void {
if (width == 0) return; if (width == 0) return;
// If we're soft-wrapping, then handle that first. // If we're soft-wrapping, then handle that first.
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) if (self.screen.cursor.pending_wrap and self.modes.autowrap)
_ = self.printWrap(); _ = self.printWrap();
switch (width) { switch (width) {
@ -598,7 +621,7 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void {
y_offset: usize = 0, y_offset: usize = 0,
x_max: usize, x_max: usize,
y_max: usize, y_max: usize,
} = if (self.modes.origin == 1) .{ } = if (self.modes.origin) .{
.x_offset = 0, // TODO: left/right margins .x_offset = 0, // TODO: left/right margins
.y_offset = self.scrolling_region.top, .y_offset = self.scrolling_region.top,
.x_max = self.cols, // TODO: left/right margins .x_max = self.cols, // TODO: left/right margins
@ -630,7 +653,7 @@ pub fn setCursorColAbsolute(self: *Terminal, col_req: usize) void {
// TODO: test // TODO: test
assert(self.modes.origin == 0); // TODO assert(!self.modes.origin); // TODO
if (self.status_display != .main) return; // TODO if (self.status_display != .main) return; // TODO
@ -1325,7 +1348,7 @@ test "Terminal: setCursorPosition" {
try testing.expect(!t.screen.cursor.pending_wrap); try testing.expect(!t.screen.cursor.pending_wrap);
// Origin mode // Origin mode
t.modes.origin = 1; t.modes.origin = true;
// No change without a scroll region // No change without a scroll region
t.setCursorPos(81, 81); t.setCursorPos(81, 81);

View File

@ -58,6 +58,9 @@ pub const Mode = enum(u16) {
/// Enable or disable automatic line wrapping. /// Enable or disable automatic line wrapping.
autowrap = 7, autowrap = 7,
/// Click-only (press) mouse reporting.
mouse_event_x10 = 9,
/// Set whether the cursor is visible or not. /// Set whether the cursor is visible or not.
cursor_visible = 25, cursor_visible = 25,
@ -66,6 +69,29 @@ pub const Mode = enum(u16) {
/// mode ?3 is set or unset. /// mode ?3 is set or unset.
enable_mode_3 = 40, enable_mode_3 = 40,
/// "Normal" mouse events: click/release, scroll
mouse_event_normal = 1000,
/// Same as normal mode but also send events for mouse motion
/// while the button is pressed when the cell in the grid changes.
mouse_event_button = 1002,
/// Same as button mode but doesn't require a button to be pressed
/// to track mouse movement.
mouse_event_any = 1003,
/// Report mouse position in the utf8 format to support larger screens.
mouse_format_utf8 = 1005,
/// Report mouse position in the SGR format.
mouse_format_sgr = 1006,
/// Report mouse position in the urxvt format.
mouse_format_urxvt = 1015,
/// Report mouse position in the SGR format as pixels, instead of cells.
mouse_format_sgr_pixels = 1016,
/// Alternate screen mode with save cursor and clear on enter. /// Alternate screen mode with save cursor and clear on enter.
alt_screen_save_cursor_clear_enter = 1049, alt_screen_save_cursor_clear_enter = 1049,