mouse middle button click support added
This commit is contained in:
@@ -46,7 +46,7 @@ Constraints it works under:
|
||||
| `vterm.sublime-commands` | Four palette entries: open, open terminal in project folder, clear screen, select theme. |
|
||||
| `vterm.sublime-syntax` | An intentionally empty hidden syntax, so terminal text is never tokenized. |
|
||||
| `Default.sublime-keymap` | 245 generated bindings. **Do not edit by hand** — regenerate with `tools/gen_keymap.py`. |
|
||||
| `Default (OSX).sublime-mousemap` | Two wheel bindings. |
|
||||
| `Default (OSX).sublime-mousemap` | Two wheel bindings and the middle button. |
|
||||
| `dependencies.json` | Package Control libraries. |
|
||||
|
||||
Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-scheme`.
|
||||
@@ -89,6 +89,7 @@ Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-s
|
||||
| `vterm_render` | `VtermRenderCommand` | renderer thread |
|
||||
| `vterm_show_cursor` | `VtermShowCursorCommand` | internal |
|
||||
| `vterm_copy` / `vterm_paste` / `vterm_paste_text` | `commands.py` | `on_text_command`, IME capture |
|
||||
| `vterm_paste_selection` | `commands.py` | mousemap, middle button |
|
||||
| `vterm_select_theme` | `VtermSelectThemeCommand` | palette |
|
||||
| `vterm_clear_screen` | `VtermClearScreenCommand` | palette, `super+shift+k` |
|
||||
| `vterm_cleanup` | `VtermCleanupCommand` | renderer, on process exit |
|
||||
@@ -312,6 +313,14 @@ mouse, and only the plain single-button form, so shift-click and add-selection s
|
||||
native. The first click on an unfocused tab only focuses it — we replaced the command
|
||||
that normally does that — and subsequent clicks send a press/release pair.
|
||||
|
||||
**The middle button** pastes the view's own selection into the app, as X11 does, and
|
||||
never touches the clipboard. The binding is a `press_command`, not a `command`: Sublime's
|
||||
own `button3` on macOS is a column `drag_select`, which would clear the selection on
|
||||
press before a release command could read it. Shadowing it costs middle-drag column
|
||||
select inside terminal tabs; the context makes the binding fall through everywhere else.
|
||||
It is not gated on mouse reporting — an empty selection is already a no-op, and the alt
|
||||
screen rarely holds one.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
- **Tab closed** → `on_pre_close` → `Terminal.kill()`: unsubscribe, terminate the child,
|
||||
|
||||
@@ -10,5 +10,12 @@
|
||||
"command": "vterm_scroll",
|
||||
"args": {"up": false},
|
||||
"context": [{"key": "vterm_view.wheel_captured", "operand": true}]
|
||||
},
|
||||
{
|
||||
// press_command: sublime's own button3 binding is a column drag_select,
|
||||
// which would clear the selection before a release command could read it
|
||||
"button": "button3",
|
||||
"press_command": "vterm_paste_selection",
|
||||
"context": [{"key": "vterm_view", "operand": true}]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -48,6 +48,8 @@ the view over the scrollback and Shift+Up/Down move it one line, instead of
|
||||
reaching the app, as xterm and kitty do — `scroll_modifier` moves that to Ctrl,
|
||||
or turns it off. Copy/paste use the normal Sublime keys, and pastes are
|
||||
bracketed when the app asks for it. Copying re-joins soft-wrapped lines. The
|
||||
middle button pastes the current selection straight into the app, X11-style,
|
||||
without going near the clipboard; it does nothing when nothing is selected. The
|
||||
wheel scrolls the view normally; while the app tracks the mouse the wheel and
|
||||
left click go to it instead, and on the alternate screen without tracking each
|
||||
wheel tick sends arrow keys (alternate scroll, as kitty/iTerm2 do — this is
|
||||
@@ -122,7 +124,8 @@ terminal protocols — see `Architecture.md`.
|
||||
|
||||
Supported: alt screen (1049), synchronized output (2026), bracketed paste (2004),
|
||||
mouse tracking 1000/1002/1003 + SGR 1006 (wheel and left click are reported; no
|
||||
drag, no right or middle button), focus events (1004),
|
||||
drag, no right button; the middle button pastes the selection instead of being
|
||||
reported), focus events (1004),
|
||||
xterm modifyOtherKeys (`CSI > 4 ; n m`, for the modified keys with no
|
||||
unambiguous legacy encoding: enter, tab, backspace, space), cursor styles (DECSCUSR —
|
||||
block/bar and blink/steady via the view's caret settings; underline falls back
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/Users/vaclavt/.pi/agent/skills/pi-skills/brave-search
|
||||
1
main.py
1
main.py
@@ -18,6 +18,7 @@ from .vterm.commands import (
|
||||
VtermOpenCommand,
|
||||
VtermOpenTerminalProjectFolderCommand,
|
||||
VtermPasteCommand,
|
||||
VtermPasteSelectionCommand,
|
||||
VtermPasteTextCommand,
|
||||
VtermSelectThemeCommand,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
""" Theme picker flows: preview, cancel, confirm.
|
||||
""" Theme picker flows and the selection-to-text helper.
|
||||
Run with: python3 tests/test_commands.py """
|
||||
import _bootstrap # sets sys.path for the imports below
|
||||
from _bootstrap import FakeProcess
|
||||
|
||||
import sublime
|
||||
from vterm.commands import VtermSelectThemeCommand
|
||||
from vterm.commands import VtermPasteSelectionCommand, VtermSelectThemeCommand, selection_text
|
||||
from vterm.render import CONTINUATION
|
||||
from vterm.utils import SETTINGS_FILE
|
||||
from vterm.themes import THEMES
|
||||
|
||||
@@ -124,3 +125,59 @@ assert len(scheme_manager.rules) == first, \
|
||||
"rules duplicated: {} -> {}".format(first, len(scheme_manager.rules))
|
||||
|
||||
print("scheme file tests passed ({} seeded rules, idempotent)".format(first))
|
||||
|
||||
|
||||
class FakeView:
|
||||
"""Just enough view for selection_text and the paste-selection command."""
|
||||
|
||||
def __init__(self, text, regions):
|
||||
self.text = text
|
||||
self.regions = list(regions)
|
||||
self.commands = []
|
||||
|
||||
def substr(self, region):
|
||||
return self.text[region.begin():region.end()]
|
||||
|
||||
def sel(self):
|
||||
return self
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.regions)
|
||||
|
||||
def clear(self):
|
||||
self.regions = []
|
||||
|
||||
def run_command(self, name, args=None):
|
||||
self.commands.append((name, args))
|
||||
|
||||
|
||||
def paste(view):
|
||||
command = VtermPasteSelectionCommand.__new__(VtermPasteSelectionCommand)
|
||||
command.view = view
|
||||
command.run(None)
|
||||
return view.commands
|
||||
|
||||
|
||||
# soft-wrapped rows re-join: the sentinel and the newline behind it both go
|
||||
wrapped = "abc" + CONTINUATION + "\ndef"
|
||||
view = FakeView(wrapped, [sublime.Region(0, len(wrapped))])
|
||||
assert selection_text(view) == "abcdef", repr(selection_text(view))
|
||||
|
||||
# a trailing sentinel with no newline is still stripped
|
||||
view = FakeView("abc" + CONTINUATION, [sublime.Region(0, 3 + len(CONTINUATION))])
|
||||
assert selection_text(view) == "abc", repr(selection_text(view))
|
||||
|
||||
# multiple regions join with a newline
|
||||
view = FakeView("abcdef", [sublime.Region(0, 3), sublime.Region(3, 6)])
|
||||
assert selection_text(view) == "abc\ndef", repr(selection_text(view))
|
||||
|
||||
# the middle button pastes the selection and drops it, so the caret follows again
|
||||
view = FakeView("abcdef", [sublime.Region(0, 3)])
|
||||
assert paste(view) == [("vterm_paste_text", {"text": "abc"})], view.commands
|
||||
assert list(view.sel()) == [], "selection must be cleared"
|
||||
|
||||
# nothing selected, nothing sent
|
||||
view = FakeView("abcdef", [sublime.Region(2, 2)])
|
||||
assert paste(view) == [], view.commands
|
||||
|
||||
print("selection tests passed (sentinel stripping, paste, empty no-op)")
|
||||
|
||||
@@ -178,21 +178,20 @@ class VtermKeypressCommand(sublime_plugin.TextCommand):
|
||||
return True
|
||||
|
||||
|
||||
def selection_text(view):
|
||||
"""The view's selection as one string, with soft-wrapped rows re-joined."""
|
||||
text = "\n".join(view.substr(s) for s in view.sel())
|
||||
return text.replace(CONTINUATION + "\n", "").replace(CONTINUATION, "")
|
||||
|
||||
|
||||
class VtermCopyCommand(sublime_plugin.TextCommand):
|
||||
"""Copies the selection, re-joining rows the emulator soft-wrapped."""
|
||||
|
||||
def run(self, edit):
|
||||
"""Strip the soft-wrap sentinels, then copy as usual."""
|
||||
view = self.view
|
||||
if not view.settings().get("vterm_view"):
|
||||
if not self.view.settings().get("vterm_view"):
|
||||
return
|
||||
text = "\n".join(view.substr(s) for s in view.sel())
|
||||
|
||||
# re-join soft-wrapped lines
|
||||
text = text.replace(CONTINUATION + "\n", "")
|
||||
text = text.replace(CONTINUATION, "")
|
||||
|
||||
sublime.set_clipboard(text)
|
||||
sublime.set_clipboard(selection_text(self.view))
|
||||
|
||||
|
||||
class VtermPasteCommand(sublime_plugin.TextCommand):
|
||||
@@ -226,6 +225,19 @@ class VtermPasteTextCommand(sublime_plugin.TextCommand):
|
||||
terminal.send_key(key="bracketed_paste_end")
|
||||
|
||||
|
||||
class VtermPasteSelectionCommand(sublime_plugin.TextCommand):
|
||||
"""Sends the view's own selection to the app; the middle button, X11-style."""
|
||||
|
||||
def run(self, edit):
|
||||
"""Bracketed like any paste, and the clipboard is never touched."""
|
||||
text = selection_text(self.view)
|
||||
if not text:
|
||||
return
|
||||
# as typing does, so the caret follows the terminal cursor again
|
||||
self.view.sel().clear()
|
||||
self.view.run_command("vterm_paste_text", {"text": text})
|
||||
|
||||
|
||||
class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
||||
"""Pick a colour palette, previewing each as it is highlighted"""
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
""" Event listeners: printable/IME input capture, command rerouting,
|
||||
keymap context, focus reporting, lifecycle """
|
||||
""" Event listeners: printable/IME input capture, command rerouting, keymap context, focus reporting, lifecycle """
|
||||
import sublime
|
||||
import sublime_plugin
|
||||
|
||||
@@ -87,10 +86,10 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
|
||||
self._cursor[view.id()] = view.sel()[0].end()
|
||||
|
||||
def on_query_context(self, view, key, operator, operand, match_all):
|
||||
"""Answer the one context the mousemap needs; keymap contexts are native."""
|
||||
"""Answer the contexts the mousemap needs; keymap contexts are native."""
|
||||
# keymap contexts are native "setting.x"; mousemaps do not evaluate
|
||||
# those the same way, so the wheel flag still needs the hook
|
||||
if key != "vterm_view.wheel_captured":
|
||||
# those the same way, so their flags still need the hook
|
||||
if key not in ("vterm_view", "vterm_view.wheel_captured"):
|
||||
return None
|
||||
value = view.settings().get(key, False)
|
||||
if operator == sublime.OP_EQUAL:
|
||||
|
||||
Reference in New Issue
Block a user