test/native_tmux_mouse.py
Ref: Size: 8.0 KiB History
#!/usr/bin/env python3
"""Wayland-only mouse selection shared by muxg and a real foot tmux client."""
import os
import shlex
import shutil
import subprocess
import sys
import time
sys.dont_write_bytecode = True
from native_resize import by_id
from native_selection import SelectionRig, cell_background
from native_tiling import eventually, require
from wayland_pointer import Pointer
def tmux(rig, socket, *args, check=True):
return subprocess.run(['tmux', '-S', str(socket), *args], env=rig.env,
capture_output=True, text=True, timeout=3, check=check)
def find_pid(tree, pid):
if tree.get('pid') == pid:
return tree
for node in tree.get('nodes', []) + tree.get('floating_nodes', []):
found = find_pid(node, pid)
if found:
return found
return None
def foot_point(pointer, pid, cols, rows, col, row):
node = find_pid(pointer.query('get_tree'), pid)
require(node is not None, 'foot window is not mapped')
rect = node['rect']
# The fixture removes Sway decorations and foot padding, so the terminal
# grid starts at the client origin. Keep well away from either edge.
return ((col + .5) * rect['width'] / cols,
(row + .5) * rect['height'] / rows)
def pane_mode(rig, socket, target):
return tmux(rig, socket, 'display-message', '-p', '-t', target,
'#{pane_in_mode}').stdout.strip()
def buffer(rig, socket):
return tmux(rig, socket, 'show-buffer', check=False).stdout
def leave_copy_mode(rig, socket, target):
tmux(rig, socket, 'send-keys', '-t', target, '-X', 'cancel', check=False)
eventually(lambda: pane_mode(rig, socket, target) == '0', 'tmux did not leave copy mode')
def write_tmux_config(root):
config = root / 'tmux.conf'
config.write_text(
'set -g mouse on\n'
'set -g status off\n'
'set -g set-clipboard on\n'
'set -g mode-keys vi\n'
'bind -T copy-mode MouseDragEnd1Pane send -X copy-selection-no-clear\n'
'bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-no-clear\n')
return config
def run(mux, muxg):
if os.environ.get('MUXG_VIDEODRIVER') != 'wayland':
print('Native tmux mouse skipped: requires MUXG_VIDEODRIVER=wayland', flush=True)
return
require(shutil.which('foot') and shutil.which('tmux'), 'native tmux mouse requires foot and tmux')
pointer_binary = os.environ.get('MUXG_TEST_POINTER')
require(pointer_binary, 'native tmux mouse requires MUXG_TEST_POINTER')
rig = SelectionRig(mux, muxg)
foot = None
foot_pointer = None
socket = rig.root / 'shared.tmux.sock'
target = 'shared:0.0'
try:
sock, _ = rig.daemon('tmux')
state = rig.launch_gui(['--sock', sock, '--session', 'tmux'], 'gui')
pane = state['panes'][0]['id']
config = write_tmux_config(rig.root)
paint = ('printf "\\033[2J\\033[HROW-000 alpha alpha\\nROW-001 bravo bravo\\n'
'ROW-002 charlie charlie\\n"; exec sh')
command = ('exec tmux -S ' + shlex.quote(str(socket)) + ' -f ' + shlex.quote(str(config)) +
' new-session -A -s shared ' + shlex.quote(paint))
rig.shell(command)
state = rig.wait_state(lambda s: 'ROW-002 charlie charlie' in by_id(s)[pane]['painted_text'])
eventually(lambda: 'ROW-001 bravo bravo' in tmux(rig, socket, 'capture-pane', '-p', '-e', '-t', target).stdout,
'tmux pane never painted the specimen')
# This is a second real terminal client of the exact same tmux server.
foot_config = rig.root / 'foot.ini'
foot_config.write_text('[main]\npad=0x0\nfont=monospace:size=12\n')
foot = rig.spawn(['foot', '-c', str(foot_config), '-a', 'muxg-tmux-mouse',
'-T', 'muxg-tmux-mouse', '-w', '960x600',
'tmux', '-S', str(socket), 'attach', '-t', 'shared'], 'foot')
foot_pointer = Pointer(pointer_binary, rig.env, foot.pid)
def foot_ready():
node = find_pid(foot_pointer.query('get_tree'), foot.pid)
clients = tmux(rig, socket, 'list-clients', '-F', '#{client_tty}').stdout.splitlines()
return node is not None and len(clients) >= 2
eventually(foot_ready, 'foot did not attach as a second tmux client', seconds=10)
subprocess.run(['swaymsg', f'[pid={foot.pid}] floating enable, border none'], env=rig.env,
capture_output=True, check=True, timeout=3)
# A normal muxg drag reaches tmux's mouse binding. Its copy-mode
# selection remains visible in muxg and becomes the actual tmux buffer.
subprocess.run(['swaymsg', f'[pid={rig.gui.pid}] focus'], env=rig.env, capture_output=True, check=True)
state = rig.state()
before = cell_background(rig, state, pane, 10, 0)
rig.select(pane, (8, 0), (12, 0))
eventually(lambda: pane_mode(rig, socket, target) == '1', 'muxg drag did not enter tmux copy mode')
eventually(lambda: 'alpha' in buffer(rig, socket), 'muxg drag did not populate tmux buffer')
eventually(lambda: cell_background(rig, state, pane, 10, 0) != before,
'tmux selection was not visibly painted by muxg')
rig.ok('ordinary muxg drag enters tmux copy mode, paints selection and copies its buffer')
leave_copy_mode(rig, socket, target)
state = rig.state()
before = cell_background(rig, state, pane, 10, 1)
subprocess.run(['swaymsg', f'[pid={foot.pid}] focus'], env=rig.env, capture_output=True, check=True)
clients = tmux(rig, socket, 'list-clients', '-F', '#{client_termname} #{client_width} #{client_height}').stdout.splitlines()
cols, rows = next(tuple(map(int, line.split()[1:])) for line in clients if line.startswith('foot '))
a = foot_point(foot_pointer, foot.pid, cols, rows, 8, 1)
b = foot_point(foot_pointer, foot.pid, cols, rows, 12, 1)
foot_pointer.event('mousedown', *a)
foot_pointer.event('mousemove', *b)
foot_pointer.event('mouseup', *b)
eventually(lambda: pane_mode(rig, socket, target) == '1', 'foot drag did not enter tmux copy mode')
eventually(lambda: 'bravo' in buffer(rig, socket), 'foot drag did not populate shared tmux buffer')
eventually(lambda: cell_background(rig, state, pane, 10, 1) != before,
'foot tmux selection did not repaint muxg')
rig.ok('ordinary foot drag shares tmux selection state and muxg repaint')
leave_copy_mode(rig, socket, target)
prior = buffer(rig, socket)
subprocess.run(['swaymsg', f'[pid={rig.gui.pid}] focus'], env=rig.env, capture_output=True, check=True)
state = rig.state()
before = cell_background(rig, state, pane, 10, 0)
a = rig.cell_point(state, pane, 8, 0)
b = rig.cell_point(state, pane, 12, 0)
rig.send('mouse:down,' + a + ',0,1', 'mouse:move,' + b + ',0,0',
'mouse:up,' + b + ',0,0')
eventually(lambda: cell_background(rig, state, pane, 10, 0) != before,
'Shift muxg drag did not paint its local selection')
rig.copied('alpha')
time.sleep(.15)
require(pane_mode(rig, socket, target) == '0' and buffer(rig, socket) == prior,
'Shift muxg drag changed tmux selection or buffer')
rig.ok('Shift muxg drag remains local and leaves shared tmux buffer unchanged')
rig.quit()
print('Native tmux mouse acceptance passed', flush=True)
except BaseException:
rig.failure_artifacts()
raise
finally:
if foot_pointer is not None:
foot_pointer.close()
if foot is not None and foot.poll() is None:
foot.terminate()
try:
foot.wait(timeout=3)
except subprocess.TimeoutExpired:
foot.kill()
foot.wait(timeout=3)
tmux(rig, socket, 'kill-server', check=False)
rig.close()
def main():
require(len(sys.argv) == 3, 'usage: native_tmux_mouse.py MUX MUXG')
run(*sys.argv[1:])
if __name__ == '__main__':
main()