332 lines
8.4 KiB
Python
332 lines
8.4 KiB
Python
"""WizardKit: tmux Functions"""
|
|
# vim: sts=2 sw=2 ts=2
|
|
|
|
import logging
|
|
import pathlib
|
|
|
|
from wk.exe import run_program
|
|
from wk.std import PLATFORM
|
|
|
|
|
|
# STATIC_VARIABLES
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
# Functions
|
|
def capture_pane(pane_id=None):
|
|
"""Capture text from current or target pane, returns str."""
|
|
cmd = ['tmux', 'capture-pane', '-p']
|
|
if pane_id:
|
|
cmd.extend(['-t', pane_id])
|
|
|
|
# Capture and return
|
|
proc = run_program(cmd, check=False)
|
|
return proc.stdout.strip()
|
|
|
|
|
|
def clear_pane(pane_id=None):
|
|
"""Clear pane buffer for current or target pane."""
|
|
commands = [
|
|
['tmux', 'send-keys', '-R'],
|
|
['tmux', 'clear-history'],
|
|
]
|
|
if pane_id:
|
|
commands = [[*cmd, '-t', pane_id] for cmd in commands]
|
|
|
|
# Clear pane
|
|
for cmd in commands:
|
|
run_program(cmd, check=False)
|
|
|
|
|
|
def fix_layout(layout, forced=False):
|
|
"""Fix pane sizes based on layout."""
|
|
resize_kwargs = []
|
|
|
|
# Bail early
|
|
if not (forced or layout_needs_fixed(layout)):
|
|
# Layout should be fine
|
|
return
|
|
|
|
# Remove closed panes
|
|
for data in layout.values():
|
|
data['Panes'] = [pane for pane in data['Panes'] if poll_pane(pane)]
|
|
|
|
# Update main panes
|
|
resize_pane(height=999) # Set active pane too large and then adjust down
|
|
for section, data in layout.items():
|
|
if section == 'Workers':
|
|
# Skip for now
|
|
continue
|
|
|
|
if 'height' in data:
|
|
for pane_id in data['Panes']:
|
|
resize_kwargs.append({'pane_id': pane_id, 'height': data['height']})
|
|
if 'width' in data:
|
|
for pane_id in data['Panes']:
|
|
resize_kwargs.append({'pane_id': pane_id, 'width': data['width']})
|
|
for kwargs in resize_kwargs:
|
|
try:
|
|
resize_pane(**kwargs)
|
|
except RuntimeError:
|
|
# Assuming pane was closed just before resizing
|
|
pass
|
|
|
|
# Update "group" panes widths
|
|
for group in ('Title', 'Info'):
|
|
num_panes = len(layout[group]['Panes'])
|
|
if num_panes <= 1:
|
|
continue
|
|
width = int( (get_pane_size()[0] - (1 - num_panes)) / num_panes )
|
|
for pane_id in layout[group]['Panes']:
|
|
resize_pane(pane_id, width=width)
|
|
if group == 'Title':
|
|
# (re)fix Started pane
|
|
#TODO: REstore: resize_pane(layout['Started']['Panes'][0], width=TMUX_SIDE_WIDTH)
|
|
resize_pane(layout['Started']['Panes'][0], width=21)
|
|
|
|
# Bail early
|
|
if not (layout['Workers']['Panes'] and layout['Workers']['height']):
|
|
return
|
|
|
|
# Update worker heights
|
|
worker_height = layout['Workers']['height']
|
|
workers = layout['Workers']['Panes'].copy()
|
|
num_workers = len(workers)
|
|
avail_height = sum(get_pane_size(pane)[1] for pane in workers)
|
|
avail_height += get_pane_size()[1] # Current pane
|
|
# Check if window is too small
|
|
if avail_height < (worker_height*num_workers) + 3:
|
|
# Just leave things as-is
|
|
return
|
|
# Resize current pane
|
|
resize_pane(height=avail_height-(worker_height*num_workers))
|
|
# Resize bottom pane
|
|
resize_pane(workers.pop(0), height=worker_height)
|
|
# Resize the rest of the panes by adjusting the ones above them
|
|
while len(workers) > 1:
|
|
next_height = sum(get_pane_size(pane)[1] for pane in workers[:2])
|
|
next_height -= worker_height
|
|
resize_pane(workers[1], height=next_height)
|
|
workers.pop(0)
|
|
|
|
|
|
def get_pane_size(pane_id=None):
|
|
"""Get current or target pane size, returns tuple."""
|
|
cmd = ['tmux', 'display', '-p']
|
|
if pane_id:
|
|
cmd.extend(['-t', pane_id])
|
|
cmd.append('#{pane_width} #{pane_height}')
|
|
|
|
# Get resolution
|
|
proc = run_program(cmd, check=False)
|
|
width, height = proc.stdout.strip().split()
|
|
width = int(width)
|
|
height = int(height)
|
|
|
|
# Done
|
|
return (width, height)
|
|
|
|
|
|
def kill_all_panes(pane_id=None):
|
|
"""Kill all panes except for the current or target pane."""
|
|
cmd = ['tmux', 'kill-pane', '-a']
|
|
if pane_id:
|
|
cmd.extend(['-t', pane_id])
|
|
|
|
# Kill
|
|
run_program(cmd, check=False)
|
|
|
|
|
|
def kill_pane(*pane_ids):
|
|
"""Kill pane(s) by id."""
|
|
cmd = ['tmux', 'kill-pane', '-t']
|
|
|
|
# Iterate over all passed pane IDs
|
|
for pane_id in pane_ids:
|
|
run_program(cmd+[pane_id], check=False)
|
|
|
|
|
|
def layout_needs_fixed(layout):
|
|
"""Check if layout needs fixed, returns bool."""
|
|
needs_fixed = False
|
|
|
|
# Check panes
|
|
for data in layout.values():
|
|
if 'height' in data:
|
|
needs_fixed = needs_fixed or any(
|
|
get_pane_size(pane)[1] != data['height'] for pane in data['Panes']
|
|
)
|
|
if 'width' in data:
|
|
needs_fixed = needs_fixed or any(
|
|
get_pane_size(pane)[0] != data['width'] for pane in data['Panes']
|
|
)
|
|
|
|
# TODO: Re-enable?
|
|
## Group panes
|
|
#for group in ('Title', 'Info'):
|
|
# num_panes = len(layout[group]['Panes'])
|
|
# if num_panes <= 1:
|
|
# continue
|
|
# width = int( (get_pane_size()[0] - (1 - num_panes)) / num_panes )
|
|
# for pane in layout[group]['Panes']:
|
|
# needs_fixed = needs_fixed or abs(get_pane_size(pane)[0] - width) > 2
|
|
|
|
# Done
|
|
return needs_fixed
|
|
|
|
|
|
def poll_pane(pane_id):
|
|
"""Check if pane exists, returns bool."""
|
|
cmd = ['tmux', 'list-panes', '-F', '#D']
|
|
|
|
# Get list of panes
|
|
proc = run_program(cmd, check=False)
|
|
existant_panes = proc.stdout.splitlines()
|
|
|
|
# Check if pane exists
|
|
return pane_id in existant_panes
|
|
|
|
|
|
def prep_action(
|
|
cmd=None, working_dir=None, text=None, watch_file=None, watch_cmd='cat'):
|
|
"""Prep action to perform during a tmux call, returns list.
|
|
|
|
This will prep for running a basic command, displaying text on screen,
|
|
or monitoring a file. The last option uses cat by default but can be
|
|
overridden by using the watch_cmd.
|
|
"""
|
|
action_cmd = []
|
|
if working_dir:
|
|
action_cmd.extend(['-c', working_dir])
|
|
|
|
if cmd:
|
|
# Basic command
|
|
action_cmd.append(cmd)
|
|
elif text:
|
|
# Display text
|
|
echo_cmd = ['echo']
|
|
if PLATFORM == 'Linux':
|
|
echo_cmd.append('-e')
|
|
action_cmd.extend([
|
|
'watch',
|
|
'--color',
|
|
'--exec',
|
|
'--no-title',
|
|
'--interval', '1',
|
|
])
|
|
action_cmd.extend(echo_cmd)
|
|
action_cmd.append(text)
|
|
elif watch_file:
|
|
# Monitor file
|
|
prep_file(watch_file)
|
|
if watch_cmd == 'cat':
|
|
action_cmd.extend([
|
|
'watch',
|
|
'--color',
|
|
'--no-title',
|
|
'--interval', '1',
|
|
'cat',
|
|
])
|
|
elif watch_cmd == 'tail':
|
|
action_cmd.extend(['tail', '-f'])
|
|
action_cmd.append(watch_file)
|
|
else:
|
|
LOG.error('No action specified')
|
|
raise RuntimeError('No action specified')
|
|
|
|
# Done
|
|
return action_cmd
|
|
|
|
|
|
def prep_file(path):
|
|
"""Check if file exists and create empty file if not."""
|
|
path = pathlib.Path(path).resolve()
|
|
try:
|
|
path.touch(exist_ok=False)
|
|
except FileExistsError:
|
|
# Leave existing files alone
|
|
pass
|
|
|
|
|
|
def resize_pane(pane_id=None, width=None, height=None, **kwargs):
|
|
"""Resize current or target pane.
|
|
|
|
NOTE: kwargs is only here to make calling this function easier
|
|
by dropping any extra kwargs passed.
|
|
"""
|
|
cmd = ['tmux', 'resize-pane']
|
|
|
|
# Safety checks
|
|
if not (width or height):
|
|
LOG.error('Neither width nor height specified')
|
|
raise RuntimeError('Neither width nor height specified')
|
|
|
|
# Finish building cmd
|
|
if pane_id:
|
|
cmd.extend(['-t', pane_id])
|
|
if width:
|
|
cmd.extend(['-x', str(width)])
|
|
if height:
|
|
cmd.extend(['-y', str(height)])
|
|
|
|
# Resize
|
|
run_program(cmd, check=False)
|
|
|
|
|
|
def split_window(
|
|
lines=None, percent=None,
|
|
behind=False, vertical=False,
|
|
target_id=None, **action):
|
|
"""Split tmux window, run action, and return pane_id as str."""
|
|
cmd = ['tmux', 'split-window', '-d', '-PF', '#D']
|
|
|
|
# Safety checks
|
|
if not (lines or percent):
|
|
LOG.error('Neither lines nor percent specified')
|
|
raise RuntimeError('Neither lines nor percent specified')
|
|
|
|
# New pane placement
|
|
if behind:
|
|
cmd.append('-b')
|
|
if vertical:
|
|
cmd.append('-v')
|
|
else:
|
|
cmd.append('-h')
|
|
if target_id:
|
|
cmd.extend(['-t', target_id])
|
|
|
|
# New pane size
|
|
if lines:
|
|
cmd.extend(['-l', str(lines)])
|
|
elif percent:
|
|
cmd.extend(['-p', str(percent)])
|
|
|
|
# New pane action
|
|
cmd.extend(prep_action(**action))
|
|
|
|
# Run and return pane_id
|
|
proc = run_program(cmd, check=False)
|
|
return proc.stdout.strip()
|
|
|
|
|
|
def respawn_pane(pane_id, **action):
|
|
"""Respawn pane with action."""
|
|
cmd = ['tmux', 'respawn-pane', '-k', '-t', pane_id]
|
|
cmd.extend(prep_action(**action))
|
|
|
|
# Respawn
|
|
run_program(cmd, check=False)
|
|
|
|
|
|
def zoom_pane(pane_id=None):
|
|
"""Toggle zoom status for current or target pane."""
|
|
cmd = ['tmux', 'resize-pane', '-Z']
|
|
if pane_id:
|
|
cmd.extend(['-t', pane_id])
|
|
|
|
# Toggle
|
|
run_program(cmd, check=False)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("This file is not meant to be called directly.")
|