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-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. |
|
| `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.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. |
|
| `dependencies.json` | Package Control libraries. |
|
||||||
|
|
||||||
Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-scheme`.
|
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_render` | `VtermRenderCommand` | renderer thread |
|
||||||
| `vterm_show_cursor` | `VtermShowCursorCommand` | internal |
|
| `vterm_show_cursor` | `VtermShowCursorCommand` | internal |
|
||||||
| `vterm_copy` / `vterm_paste` / `vterm_paste_text` | `commands.py` | `on_text_command`, IME capture |
|
| `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_select_theme` | `VtermSelectThemeCommand` | palette |
|
||||||
| `vterm_clear_screen` | `VtermClearScreenCommand` | palette, `super+shift+k` |
|
| `vterm_clear_screen` | `VtermClearScreenCommand` | palette, `super+shift+k` |
|
||||||
| `vterm_cleanup` | `VtermCleanupCommand` | renderer, on process exit |
|
| `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
|
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.
|
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
|
### Lifecycle
|
||||||
|
|
||||||
- **Tab closed** → `on_pre_close` → `Terminal.kill()`: unsubscribe, terminate the child,
|
- **Tab closed** → `on_pre_close` → `Terminal.kill()`: unsubscribe, terminate the child,
|
||||||
|
|||||||
@@ -10,5 +10,12 @@
|
|||||||
"command": "vterm_scroll",
|
"command": "vterm_scroll",
|
||||||
"args": {"up": false},
|
"args": {"up": false},
|
||||||
"context": [{"key": "vterm_view.wheel_captured", "operand": true}]
|
"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,
|
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
|
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
|
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
|
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
|
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
|
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),
|
Supported: alt screen (1049), synchronized output (2026), bracketed paste (2004),
|
||||||
mouse tracking 1000/1002/1003 + SGR 1006 (wheel and left click are reported; no
|
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
|
xterm modifyOtherKeys (`CSI > 4 ; n m`, for the modified keys with no
|
||||||
unambiguous legacy encoding: enter, tab, backspace, space), cursor styles (DECSCUSR —
|
unambiguous legacy encoding: enter, tab, backspace, space), cursor styles (DECSCUSR —
|
||||||
block/bar and blink/steady via the view's caret settings; underline falls back
|
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,
|
VtermOpenCommand,
|
||||||
VtermOpenTerminalProjectFolderCommand,
|
VtermOpenTerminalProjectFolderCommand,
|
||||||
VtermPasteCommand,
|
VtermPasteCommand,
|
||||||
|
VtermPasteSelectionCommand,
|
||||||
VtermPasteTextCommand,
|
VtermPasteTextCommand,
|
||||||
VtermSelectThemeCommand,
|
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 """
|
Run with: python3 tests/test_commands.py """
|
||||||
import _bootstrap # sets sys.path for the imports below
|
import _bootstrap # sets sys.path for the imports below
|
||||||
from _bootstrap import FakeProcess
|
from _bootstrap import FakeProcess
|
||||||
|
|
||||||
import sublime
|
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.utils import SETTINGS_FILE
|
||||||
from vterm.themes import THEMES
|
from vterm.themes import THEMES
|
||||||
|
|
||||||
@@ -124,3 +125,59 @@ assert len(scheme_manager.rules) == first, \
|
|||||||
"rules duplicated: {} -> {}".format(first, len(scheme_manager.rules))
|
"rules duplicated: {} -> {}".format(first, len(scheme_manager.rules))
|
||||||
|
|
||||||
print("scheme file tests passed ({} seeded rules, idempotent)".format(first))
|
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
|
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):
|
class VtermCopyCommand(sublime_plugin.TextCommand):
|
||||||
"""Copies the selection, re-joining rows the emulator soft-wrapped."""
|
"""Copies the selection, re-joining rows the emulator soft-wrapped."""
|
||||||
|
|
||||||
def run(self, edit):
|
def run(self, edit):
|
||||||
"""Strip the soft-wrap sentinels, then copy as usual."""
|
"""Strip the soft-wrap sentinels, then copy as usual."""
|
||||||
view = self.view
|
if not self.view.settings().get("vterm_view"):
|
||||||
if not view.settings().get("vterm_view"):
|
|
||||||
return
|
return
|
||||||
text = "\n".join(view.substr(s) for s in view.sel())
|
sublime.set_clipboard(selection_text(self.view))
|
||||||
|
|
||||||
# re-join soft-wrapped lines
|
|
||||||
text = text.replace(CONTINUATION + "\n", "")
|
|
||||||
text = text.replace(CONTINUATION, "")
|
|
||||||
|
|
||||||
sublime.set_clipboard(text)
|
|
||||||
|
|
||||||
|
|
||||||
class VtermPasteCommand(sublime_plugin.TextCommand):
|
class VtermPasteCommand(sublime_plugin.TextCommand):
|
||||||
@@ -226,6 +225,19 @@ class VtermPasteTextCommand(sublime_plugin.TextCommand):
|
|||||||
terminal.send_key(key="bracketed_paste_end")
|
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):
|
class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
||||||
"""Pick a colour palette, previewing each as it is highlighted"""
|
"""Pick a colour palette, previewing each as it is highlighted"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
""" Event listeners: printable/IME input capture, command rerouting,
|
""" Event listeners: printable/IME input capture, command rerouting, keymap context, focus reporting, lifecycle """
|
||||||
keymap context, focus reporting, lifecycle """
|
|
||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
@@ -87,10 +86,10 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
|
|||||||
self._cursor[view.id()] = view.sel()[0].end()
|
self._cursor[view.id()] = view.sel()[0].end()
|
||||||
|
|
||||||
def on_query_context(self, view, key, operator, operand, match_all):
|
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
|
# keymap contexts are native "setting.x"; mousemaps do not evaluate
|
||||||
# those the same way, so the wheel flag still needs the hook
|
# those the same way, so their flags still need the hook
|
||||||
if key != "vterm_view.wheel_captured":
|
if key not in ("vterm_view", "vterm_view.wheel_captured"):
|
||||||
return None
|
return None
|
||||||
value = view.settings().get(key, False)
|
value = view.settings().get(key, False)
|
||||||
if operator == sublime.OP_EQUAL:
|
if operator == sublime.OP_EQUAL:
|
||||||
|
|||||||
Reference in New Issue
Block a user