"""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.")