From 4a3e1e15e5c7ef4e8a5e9d5b45e6630b03b40dcd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 20 Dec 2023 21:33:13 -0800 Subject: [PATCH] core: click to move cursor --- src/Surface.zig | 71 +++++++++++++++++++++++++++++++++++---- src/terminal/Screen.zig | 35 +++++++++++++++---- src/terminal/Terminal.zig | 2 +- 3 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 339b2c5b7..9da95a89c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1981,6 +1981,16 @@ pub fn mouseButtonCallback( } } + // For left button click release we check if we are moving our cursor. + if (button == .left and action == .release and mods.alt) { + // Moving always resets the click count so that we don't highlight. + self.mouse.left_click_count = 0; + + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + try self.clickMoveCursor(self.mouse.left_click_point); + } + // For left button clicks we always record some information for // selection/highlighting purposes. if (button == .left and action == .press) { @@ -1988,6 +1998,8 @@ pub fn mouseButtonCallback( defer self.renderer_state.mutex.unlock(); const pos = try self.rt_surface.getCursorPos(); + const pt_viewport = self.posToViewport(pos.x, pos.y); + const pt_screen = pt_viewport.toScreen(&self.io.terminal.screen); // If we move our cursor too much between clicks then we reset // the multi-click state. @@ -2002,8 +2014,7 @@ pub fn mouseButtonCallback( } // Store it - const point = self.posToViewport(pos.x, pos.y); - self.mouse.left_click_point = point.toScreen(&self.io.terminal.screen); + self.mouse.left_click_point = pt_screen; self.mouse.left_click_xpos = pos.x; self.mouse.left_click_ypos = pos.y; @@ -2029,10 +2040,13 @@ pub fn mouseButtonCallback( } switch (self.mouse.left_click_count) { - // First mouse click, clear selection - 1 => if (self.io.terminal.screen.selection != null) { - self.setSelection(null); - try self.queueRender(); + // Single click + 1 => { + // If we have a selection, clear it. This always happens. + if (self.io.terminal.screen.selection != null) { + self.setSelection(null); + try self.queueRender(); + } }, // Double click, select the word under our mouse @@ -2075,6 +2089,51 @@ pub fn mouseButtonCallback( } } +fn clickMoveCursor(self: *Surface, to: terminal.point.ScreenPoint) !void { + // Get our path + const from = (terminal.point.Viewport{ + .x = self.io.terminal.screen.cursor.x, + .y = self.io.terminal.screen.cursor.y, + }).toScreen(&self.io.terminal.screen); + const path = self.io.terminal.screen.promptPath(from, to); + log.debug("click-to-move-cursor from={} to={} path={}", .{ from, to, path }); + + // If we aren't moving at all, fast path out of here. + if (path.x == 0 and path.y == 0) return; + + // Convert our path to arrow key inputs. Yes, that is how this works. + // Yes, that is pretty sad. Yes, this could backfire in various ways. + // But its the best we can do. + + // We do Y first because it prevents any weird wrap behavior. + if (path.y != 0) { + const arrow = if (path.y < 0) arrow: { + break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOA" else "\x1b[A"; + } else arrow: { + break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOB" else "\x1b[B"; + }; + for (0..@abs(path.y)) |_| { + _ = self.io_thread.mailbox.push(.{ + .write_stable = arrow, + }, .{ .instant = {} }); + } + } + if (path.x != 0) { + const arrow = if (path.x < 0) arrow: { + break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOD" else "\x1b[D"; + } else arrow: { + break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOC" else "\x1b[C"; + }; + for (0..@abs(path.x)) |_| { + _ = self.io_thread.mailbox.push(.{ + .write_stable = arrow, + }, .{ .instant = {} }); + } + } + + try self.io_thread.wakeup.notify(); +} + /// Returns the link at the given cursor position, if any. fn linkAtPos( self: *Surface, diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index d7fda6094..3f9dadd43 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1886,24 +1886,43 @@ pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection { pub fn selectPrompt(self: *Screen, pt: point.ScreenPoint) ?Selection { // Ensure that the line the point is on is a prompt. const pt_row = self.getRow(.{ .screen = pt.y }); - switch (pt_row.getSemanticPrompt()) { - .prompt, .prompt_continuation, .input => {}, - .command, .unknown => return null, - } + const is_known = switch (pt_row.getSemanticPrompt()) { + .prompt, .prompt_continuation, .input => true, + .command => return null, + + // We allow unknown to continue because not all shells output any + // semantic prompt information for continuation lines. This has the + // possibility of making this function VERY slow (we look at all + // scrollback) so we should try to avoid this in the future by + // setting a flag or something if we have EVER seen a semantic + // prompt sequence. + .unknown => false, + }; // Find the start of the prompt. + var saw_semantic_prompt = is_known; const start: usize = start: for (0..pt.y) |offset| { const y = pt.y - offset; const row = self.getRow(.{ .screen = y - 1 }); switch (row.getSemanticPrompt()) { // A prompt, we continue searching. - .prompt, .prompt_continuation, .input => {}, + .prompt, .prompt_continuation, .input => saw_semantic_prompt = true, + + // See comment about "unknown" a few lines above. If we have + // previously seen a semantic prompt then if we see an unknown + // we treat it as a boundary. + .unknown => if (saw_semantic_prompt) break :start y, // Command output or unknown, definitely not a prompt. - .command, .unknown => break :start y, + .command => break :start y, } } else 0; + // If we never saw a semantic prompt flag, then we can't trust our + // start value and we return null. This scenario usually means that + // semantic prompts aren't enabled via the shell. + if (!saw_semantic_prompt) return null; + // Find the end of the prompt. const end: usize = end: for (pt.y..self.rowsWritten()) |y| { const row = self.getRow(.{ .screen = y }); @@ -4891,10 +4910,12 @@ test "Screen: selectPrompt prompt at start" { row.setSemanticPrompt(.prompt); row = s.getRow(.{ .screen = 1 }); row.setSemanticPrompt(.input); + row = s.getRow(.{ .screen = 2 }); + row.setSemanticPrompt(.command); // Not at a prompt { - const sel = s.selectPrompt(.{ .x = 0, .y = 2 }); + const sel = s.selectPrompt(.{ .x = 0, .y = 3 }); try testing.expect(sel == null); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 54086c903..9b6b1f42f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2115,7 +2115,7 @@ pub fn setLeftAndRightMargin(self: *Terminal, left_req: usize, right_req: usize) /// (OSC 133) only allow setting this for wherever the current active cursor /// is located. pub fn markSemanticPrompt(self: *Terminal, p: SemanticPrompt) void { - //log.warn("semantic_prompt y={} p={}", .{ self.screen.cursor.y, p }); + //log.debug("semantic_prompt y={} p={}", .{ self.screen.cursor.y, p }); const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); row.setSemanticPrompt(switch (p) { .prompt => .prompt,