mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 01:09:46 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8adc625d16 | ||
|
|
e4bc1f65da |
No files matched your search
+9
-17
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
@@ -2788,23 +2789,14 @@ func (m model) View() tea.View {
|
||||
// only when the value changes, so gating this costs nothing (§10).
|
||||
view.KeyboardEnhancements.ReportEventTypes = m.dictation.voiceModeEnabled
|
||||
if m.wantsMouseCapture() {
|
||||
if isRunningUnderPRoot() {
|
||||
// Under PRoot the AllMotion (1003) sequence doesn't work
|
||||
// reliably, breaking touch-gesture scrolling. Fall back to
|
||||
// CellMotion which still delivers wheel events, clicks, and
|
||||
// drag — the only thing lost is hover-highlighting.
|
||||
view.MouseMode = tea.MouseModeCellMotion
|
||||
} else {
|
||||
// AllMotion (not CellMotion) is required for hover highlighting:
|
||||
// it reports cursor movement even with no button pressed.
|
||||
// CellMotion only reports motion while a button is held (drag) —
|
||||
// see bubbletea's MouseMode docs. AllMotion has marginally worse
|
||||
// terminal compatibility but is well supported by the terminals
|
||||
// this app targets; the existing 15ms mouse-event throttle
|
||||
// (mouseEventThrottleInterval) already bounds the redraw rate
|
||||
// from the extra motion events.
|
||||
view.MouseMode = tea.MouseModeAllMotion
|
||||
}
|
||||
// AllMotion (1003) is what hover highlighting needs: it reports cursor
|
||||
// movement with no button pressed, where CellMotion (1002) reports motion
|
||||
// only while a button is held. mouseModeFor decides which is safe here,
|
||||
// and documents why one is not simply better than the other: a terminal
|
||||
// that does not implement 1003 sends nothing at all rather than degrading.
|
||||
// The 15ms throttle (mouseEventThrottleInterval) already bounds the redraw
|
||||
// rate from AllMotion's extra motion events.
|
||||
view.MouseMode = mouseModeFor(runtime.GOOS, os.Getenv, isRunningUnderPRoot())
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
@@ -8,6 +8,70 @@ import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
// mouseModeEnv forces a mouse mode: "all", "cell", or "off"/"none". It exists
|
||||
// because the failure it guards against is invisible from here and total for the
|
||||
// user: a terminal we guessed wrong about delivers no mouse events at all, and
|
||||
// there is no in-app way to recover. Anything unrecognised is ignored rather
|
||||
// than treated as "off", so a typo cannot silently kill the mouse.
|
||||
const mouseModeEnv = "ZERO_MOUSE_MODE"
|
||||
|
||||
// mouseModeFor picks the mouse reporting mode, preferring the widest support
|
||||
// over the nicest behaviour.
|
||||
//
|
||||
// The trade is asymmetric. AllMotion (1003) buys exactly one thing, hover
|
||||
// highlighting, because wheel, click and drag all arrive under CellMotion
|
||||
// (1002). But when a terminal does not implement 1003, it does not degrade to
|
||||
// 1002: no mouse event arrives at all. The app holds the alternate screen, so
|
||||
// the terminal's own scrollback and wheel are gone too, and the user is left
|
||||
// with no way to scroll by mouse. A long answer then looks like a hang rather
|
||||
// than like a missing highlight, which is how #870 was reported.
|
||||
//
|
||||
// So AllMotion is asked for only where it is known to work.
|
||||
func mouseModeFor(goos string, env func(string) string, underPRoot bool) tea.MouseMode {
|
||||
switch strings.ToLower(strings.TrimSpace(env(mouseModeEnv))) {
|
||||
case "all":
|
||||
return tea.MouseModeAllMotion
|
||||
case "cell":
|
||||
return tea.MouseModeCellMotion
|
||||
case "off", "none":
|
||||
return tea.MouseModeNone
|
||||
}
|
||||
if underPRoot {
|
||||
return tea.MouseModeCellMotion
|
||||
}
|
||||
if goos == "windows" && !windowsTerminalReportsAllMotion(env) {
|
||||
return tea.MouseModeCellMotion
|
||||
}
|
||||
return tea.MouseModeAllMotion
|
||||
}
|
||||
|
||||
// windowsTerminalReportsAllMotion reports whether the Windows terminal in use is
|
||||
// one that handles 1003.
|
||||
//
|
||||
// Identified terminals are trusted: Windows Terminal sets WT_SESSION, and hosts
|
||||
// like VS Code and mintty set TERM_PROGRAM. The legacy console host sets
|
||||
// neither, and it is what a user gets by double-clicking zero.exe or running it
|
||||
// from an old cmd window, so an unidentified Windows terminal is assumed to be
|
||||
// that one. Guessing wrong in this direction costs a hover highlight; guessing
|
||||
// wrong in the other direction costs every mouse event.
|
||||
// KNOWN LIMIT: these variables are INHERITED, so they answer for the process
|
||||
// that set them rather than for the console host attached right now. A shell
|
||||
// launched from Windows Terminal, or from Git Bash, that later runs zero.exe
|
||||
// against the legacy console still carries them, and this returns true for a
|
||||
// host that may drop every mouse event.
|
||||
//
|
||||
// Left as is on purpose. Deciding it properly means asking the console host
|
||||
// rather than the environment, and erring the other way costs every Windows
|
||||
// Terminal user their hover highlighting for a case that needs an unusual
|
||||
// launch path to reach. ZERO_MOUSE_MODE=cell is the recourse in the meantime,
|
||||
// which is the main reason that override exists.
|
||||
// TestInheritedWindowsTerminalEnvStillAsksForAllMotion pins this, so it is a
|
||||
// documented limit rather than a surprise.
|
||||
func windowsTerminalReportsAllMotion(env func(string) string) bool {
|
||||
return strings.TrimSpace(env("WT_SESSION")) != "" ||
|
||||
strings.TrimSpace(env("TERM_PROGRAM")) != ""
|
||||
}
|
||||
|
||||
// parseTracerPid returns true when /proc/self/status contains a non-zero
|
||||
// TracerPid. This detects any ptrace tracer (PRoot, gdb, strace, dlv, etc.),
|
||||
// not PRoot specifically. That's intentional: under any ptrace tracer the
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
func envFrom(pairs map[string]string) func(string) string {
|
||||
return func(key string) string { return pairs[key] }
|
||||
}
|
||||
|
||||
// AllMotion (1003) buys hover highlighting and nothing else: wheel, click and
|
||||
// drag all arrive under CellMotion (1002). When a terminal does not support
|
||||
// 1003 the cost is not a missing highlight, it is that NO mouse event arrives
|
||||
// at all, and because the app holds the alternate screen the terminal's own
|
||||
// scrollback is gone too. The user cannot scroll by any means, and a screen of
|
||||
// cut-off output reads as a hang (#870).
|
||||
//
|
||||
// So the rule is: ask for AllMotion only where it is known to work.
|
||||
func TestMouseModeFallsBackWhereAllMotionIsUnreliable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
env map[string]string
|
||||
underPRoot bool
|
||||
want tea.MouseMode
|
||||
}{
|
||||
{
|
||||
name: "linux terminal",
|
||||
goos: "linux",
|
||||
want: tea.MouseModeAllMotion,
|
||||
},
|
||||
{
|
||||
name: "macOS terminal",
|
||||
goos: "darwin",
|
||||
want: tea.MouseModeAllMotion,
|
||||
},
|
||||
{
|
||||
// Pre-existing carve-out: under PRoot the 1003 sequence is unreliable
|
||||
// and breaks touch-gesture scrolling.
|
||||
name: "under PRoot",
|
||||
goos: "linux",
|
||||
underPRoot: true,
|
||||
want: tea.MouseModeCellMotion,
|
||||
},
|
||||
{
|
||||
// #870. The legacy Windows console does not handle 1003 reliably, and
|
||||
// it is what a user gets by running zero.exe from anywhere that is not
|
||||
// Windows Terminal.
|
||||
name: "legacy Windows console",
|
||||
goos: "windows",
|
||||
want: tea.MouseModeCellMotion,
|
||||
},
|
||||
{
|
||||
name: "Windows Terminal",
|
||||
goos: "windows",
|
||||
env: map[string]string{"WT_SESSION": "d1c2-session"},
|
||||
want: tea.MouseModeAllMotion,
|
||||
},
|
||||
{
|
||||
name: "VS Code terminal on Windows",
|
||||
goos: "windows",
|
||||
env: map[string]string{"TERM_PROGRAM": "vscode"},
|
||||
want: tea.MouseModeAllMotion,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := mouseModeFor(test.goos, envFrom(test.env), test.underPRoot)
|
||||
if got != test.want {
|
||||
t.Fatalf("mouseModeFor(%q) = %v, want %v", test.goos, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A KNOWN LIMIT, pinned deliberately rather than left to be rediscovered.
|
||||
//
|
||||
// Windows children inherit the parent environment, so WT_SESSION or
|
||||
// TERM_PROGRAM can outlive the terminal that set them: a shell started from
|
||||
// Windows Terminal, or from Git Bash, that later runs zero.exe against a
|
||||
// different console host still carries them, and mouseModeFor then asks for
|
||||
// AllMotion on a host that may drop every mouse event. That is the #870 failure
|
||||
// class arriving by a narrower door.
|
||||
//
|
||||
// It is not fixed here, and this test says so out loud. Telling an inherited
|
||||
// variable from a live one needs the actual console host rather than the
|
||||
// environment, and guessing wrong in the other direction costs every Windows
|
||||
// Terminal user their hover highlighting. ZERO_MOUSE_MODE=cell is the recourse
|
||||
// until that host check exists, which is why the override is not a nicety.
|
||||
//
|
||||
// If someone later adds real host detection, this test SHOULD fail. That is the
|
||||
// point of it.
|
||||
func TestInheritedWindowsTerminalEnvStillAsksForAllMotion(t *testing.T) {
|
||||
for _, env := range []map[string]string{
|
||||
{"WT_SESSION": "inherited-from-an-ancestor"},
|
||||
{"TERM_PROGRAM": "vscode"},
|
||||
} {
|
||||
if got := mouseModeFor("windows", envFrom(env), false); got != tea.MouseModeAllMotion {
|
||||
t.Fatalf("mouseModeFor(windows, %v) = %v; if this now returns CellMotion the "+
|
||||
"inherited-environment limit has been fixed, so delete this test and its note", env, got)
|
||||
}
|
||||
// The escape hatch has to keep working for exactly this case, since it is
|
||||
// the only recourse a user has when the guess is wrong.
|
||||
withOverride := map[string]string{mouseModeEnv: "cell"}
|
||||
for key, value := range env {
|
||||
withOverride[key] = value
|
||||
}
|
||||
if got := mouseModeFor("windows", envFrom(withOverride), false); got != tea.MouseModeCellMotion {
|
||||
t.Fatalf("ZERO_MOUSE_MODE=cell did not override inherited %v, so an affected user has no recourse", env)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The override exists so a user who hits an unsupported terminal can fix it
|
||||
// themselves without waiting for a release, and so a maintainer can ask a bug
|
||||
// reporter to test one specific mode.
|
||||
func TestMouseModeOverrideWins(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
want tea.MouseMode
|
||||
}{
|
||||
{"cell", tea.MouseModeCellMotion},
|
||||
{"CELL", tea.MouseModeCellMotion},
|
||||
{" cell ", tea.MouseModeCellMotion},
|
||||
{"all", tea.MouseModeAllMotion},
|
||||
{"off", tea.MouseModeNone},
|
||||
{"none", tea.MouseModeNone},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.value, func(t *testing.T) {
|
||||
// windows + PRoot would otherwise force CellMotion, so "all" proves the
|
||||
// override beats the fallbacks rather than agreeing with them.
|
||||
got := mouseModeFor("windows", envFrom(map[string]string{mouseModeEnv: test.value}), true)
|
||||
if got != test.want {
|
||||
t.Fatalf("%s=%q gave %v, want %v", mouseModeEnv, test.value, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An unreadable override must not silently disable the mouse: fall through to
|
||||
// the normal decision rather than guessing.
|
||||
func TestMouseModeIgnoresAnUnknownOverride(t *testing.T) {
|
||||
got := mouseModeFor("linux", envFrom(map[string]string{mouseModeEnv: "sideways"}), false)
|
||||
if got != tea.MouseModeAllMotion {
|
||||
t.Fatalf("an unknown override changed the mode to %v, want the normal AllMotion", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user