mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
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:
363
src/Window.zig
363
src/Window.zig
@ -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,
|
||||||
|
@ -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
38
src/input/mouse.zig
Normal 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,
|
||||||
|
};
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user