def __init__(self, identity: str, name: str, is_first: bool = False) -> "Window": """ Sets up details about the window Parameters ---------- identity : string A unique ID representing this window name : string The name of the window is_first : boolean Is this the first window that was created? """ self.identity = identity self.name = name self.first = is_first self.panes = [] self.base_width = 0 self.base_height = 0 self.panes_added = 0 self.last_pane = None self.objects = ObjectTracker() self.objects.add_window(self)
def __init__(self, identity: str, window: "Window", direction: str, is_first: bool = False) -> "Pane": """ Sets up details about the pane Parameters ---------- identity : string A unique ID representing this pane window : Window The Window object this pane belongs to direction : string|None Which direction to split this pane - 'h' for horizontal, 'v' for vertical is_first : bool Is this the first pane that was created? """ self.identity = identity self.window = window self.direction = direction self.is_first = is_first self.session_name = None self.command = "" self.split_from = None self.width = 0 self.height = 0 self.number = None self.parent_number = None self.start_dir = None self.objects = ObjectTracker() self.objects.add_pane(self)
def __init__(self, session_name: str, starting_directory: str = "", detach: bool = True) -> "TmuxBuilder": """ Sets up details about the tmux builder Parameters ---------- session_name : string The name of the session to create starting_directory : string The directory to base all windows/panes out of detach : bool Whether to detach this session after creatio """ self.session_name = session_name self.objects = ObjectTracker() self.windows = [] self.start_dir = starting_directory self.detach = bool(detach) self.__get_terminal_size()
def test_adding_duplicate_ids(): """ Try adding two items with the same ID """ tracker = ObjectTracker() tracker.add_pane(Pane("foo")) try: tracker.add_pane(Pane("foo")) assert False, "Expected ValueError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, 'Object ID "foo" is already in use!', ValueError) try: tracker.add_window(Window("foo")) assert False, "Expected ValueError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, 'Object ID "foo" is already in use!', ValueError)
def test_adding_mismatched_types(): """ Try adding a pane as a window, or vice versa """ tracker = ObjectTracker() try: tracker.add_window(Pane("foo")) assert False, "Expected TypeError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "Object provided is not a window!", TypeError) try: tracker.add_pane(Window("foo")) assert False, "Expected TypeError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "Object provided is not a pane!", TypeError)
def test_adding_non_classes(): """ Try adding an integer as a window, etc. """ tracker = ObjectTracker() try: tracker.add_pane(Integer(5)) assert False, "Expected TypeError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "Object provided is not a pane!", TypeError) try: tracker.add_window(Integer(3)) assert False, "Expected TypeError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "Object provided is not a window!", TypeError)
class Window: """ Represents a window in Tmux Attributes ---------- identity : string A unique ID representing this window name : string The name of the window number : number The number of this window is_first : boolean Is this the first window that was created? panes : list The panes to create in this window session_name : string The name of the current session start_dir : string The starting directory for the window """ COMPARABLE_PROPERTIES = ["name", "start_dir", "session_name", "first"] # Contains <window_number>: <window_name><window_marker> (N panes) # [<window_width>x<window_height>] [layout <layout_id,... WINDOW_TRAIL_REGEX = r"([\*~#\!MZ\-]+)" LAYOUT_DETAILS = r"([a-fA-F0-9]+),([0-9x]+),([0-9]+),([0-9]+)" DIRECTION_MAP = {"[": SPLIT_VERTICAL, "{": SPLIT_HORIZONTAL} LAYOUT_PAIRS = {"[": "]", "{": "}"} PANE_LAYOUT = r"([0-9]+x[0-9]+,[0-9]+,[0-9]+)" number = None start_dir = None session_name = None layout_id = None layout_size = None offset_left = None offset_top = None def __init__(self, identity: str, name: str, is_first: bool = False) -> "Window": """ Sets up details about the window Parameters ---------- identity : string A unique ID representing this window name : string The name of the window is_first : boolean Is this the first window that was created? """ self.identity = identity self.name = name self.first = is_first self.panes = [] self.base_width = 0 self.base_height = 0 self.panes_added = 0 self.last_pane = None self.objects = ObjectTracker() self.objects.add_window(self) def set_number(self, num: int) -> "Window": """ Set the window number Parameters ---------- num : number The window number Returns ------- This instance """ self.number = num return self def set_session_name(self, name: str) -> "Window": """ Sets the session name for the window Parameters ---------- name : string The name of the current session Returns ------- This instance """ self.session_name = name return self def set_start_dir(self, directory: str) -> "Window": """ Sets the starting directory for the window Parameters ---------- directory : string The starting directory of the current session Returns ------- This instance """ self.start_dir = directory return self def get_pane_count(self) -> int: """ Returns the number of panes in this window Returns ------- number The number of panes """ return len(self.panes) def add_pane(self, pane: Pane) -> "Window": """ Adds a pane to this window Parameters ---------- pane : Pane The pane to add to the window Returns ------- This instance """ self.panes.append(pane) if pane.split_from is not None: parent_pane_number = self.objects.get_pane_by_id( pane.split_from).number # Tmux numbers panes left-to-right & top-down, order dependent on initial split # Each time a pane is split, the number increases by one from where it was split # Thus, anything equal to or larger than the initial pane will increase by one # This emulates that logic in the cached panes here if pane.split_from is not None: for window_pane in self.panes: if window_pane.number >= parent_pane_number + 1: window_pane.number = window_pane.number + 1 pane.number = parent_pane_number + 1 return self def get_pane(self, idx: int) -> Pane: """ Returns a pane at a given index Parameters ---------- idx : number The index of the pane to get Returns ------- Pane The pane at the given index """ return self.panes[idx] def is_first(self) -> bool: """ Returns true if this is the first window Returns ------- boolean True if this is the first window """ return self.first def get_create_command(self) -> str: """ Gets the creation command for the window Returns ------- string The creation command """ # First window will be created automatically, so just need to rename it command = "" if not self.is_first(): command = "new-window -n {0} {1}".format(self.name, self.start_dir) else: command = "rename-window " + self.name return command def get_difference(self, window: "Window") -> dict: """ Compare this instance to another window Checks properties, not direct reference Parameters ---------- window : Window The window to compare against Returns ------- dict A key-value comparison of this and the other window """ return get_object_differences(self, window, self.COMPARABLE_PROPERTIES) def get_pane_differences(self, panes: list) -> dict: """ Compare this instance's panes to other panes Checks properties, not direct reference Parameters ---------- panes : list[Pane] The panes to compare against Returns ------- dict A key-value comparison of this window's panes and the provided list """ differences = [] for i in range(len(self.panes)): differences.append( get_object_differences(self.panes[i], panes[i], panes[i].COMPARABLE_PROPERTIES)) return differences def load_from_layout(self, layout: str) -> None: """ Load details about this window from a layout Parameters ---------- layout : string The layout string to load details from Will be in a format of <num>: <name><status> (<N> panes) [<width>x<height>] [layout XXXX,...] May be zoomed, so this argument could be incomplete """ match = re.match(r"^([0-9]+): ", layout) win_layout_num = match.groups()[0] debug("window", "loadLayout", "Parsing layout {0}".format(win_layout_num)) layout = layout[len(win_layout_num) + 2:] self.name = re.split(self.WINDOW_TRAIL_REGEX, layout)[0] layout = layout[len(self.name):] chars = re.match(self.WINDOW_TRAIL_REGEX, layout) if chars: layout = layout[len(chars.groups()[0]):] layout = layout.strip() match = re.match(r".*\[([0-9]+x[0-9]+)\]", layout) win_size = parse_size(match.groups()[0]) self.base_width = int(win_size["width"]) self.base_height = int(win_size["height"]) # Make sure layout is not zoomed select_pane(self.session_name, win_layout_num, 0) # Pull only the layout details win_layout = get_tmux_details(self.session_name, win_layout_num, None, WINDOW_LAYOUT_VARIABLE) self.parse_layout(win_layout) def parse_layout(self, layout: str) -> None: """ Parse a layout string into panes The layout string should be of the format: <pane_indicator>,<pane_number or further layout data> Parameters ---------- layout : string The layout string to parse Will be in the form: <layout_id>,<win_size>,<offset_left>,<offset_top>,<pane_data> """ match = re.match(self.LAYOUT_DETAILS, layout) ( self.layout_id, self.layout_size, self.offset_left, self.offset_top, ) = match.groups() # Three commas separating fields pane_data = layout[len(self.layout_id) + len(self.layout_size) + len(self.offset_left) + len(self.offset_top) + 3:] # This will recursively populate all panes self.add_pane_row(pane_data) for pane in self.panes: pane.set_command( get_pane_command(self.session_name, self.number, pane.number)) pane.set_start_dir( get_pane_working_directory(self.session_name, self.number, pane.number)) def add_simple_pane(self, win_id: str, parent_id: str = None) -> None: """ Adds a pane to this window Parameters ---------- win_id : str The window ID to prefix this pane with parent_id = None : str Optionally, the ID of this pane's parent """ if parent_id: number = self.objects.get_pane_by_id(parent_id).number + 1 else: number = 0 debug("Window", "add_simple_pane", "Adding pane: " + str(number)) # The layout is a single pane pane = Pane(win_id + str(number), self, None, True) self.panes_added += 1 # All that's left after the comma is the pane number pane.set_number(number) self.panes.append(pane) # pylint: disable=bad-continuation # black auto-format disagrees def add_pane_row(self, layout: str, parent_id: str = None, prevailing_split: str = None) -> None: """ Add a row of panes, either horizontally or vertically Parameters ---------- layout : string The layout for the row, should start with either a [ or {, depending on whether the split is vertical or horizontal parent_id : string The identity of the pane to base these off of prevailing_split: string The current split direction, for the first pane to be based off of The following data will be a comma-separated array of pane data See add_pane_by_layout """ win_id = "win{0}pane".format(self.number) debug("Window", "add_pane_row", "Adding pane row") debug("Window", "add_pane_row", layout) if layout[0] == ",": self.add_simple_pane(win_id, parent_id) else: direction = self.DIRECTION_MAP[layout[0]] direction = SPLIT_HORIZONTAL if layout[0] == "{" else SPLIT_VERTICAL debug("Window", "add_pane_row", "Layout split direction: " + direction) # Strip outermost direction indicators layout = layout[1:-1] debug("Window", "add_pane_row", "Resolved layout: " + layout) deferred_layouts = self.identify_deferred_layouts( layout, direction, parent_id, prevailing_split) for deferred_layout in deferred_layouts: debug("Window", "add_pane_row", "Processing deferred layout:") debug("Window", "add_pane_row", "Layout: " + deferred_layout["layout"]) debug( "Window", "add_pane_row", "Parent: " + str(deferred_layout["parent"]) + ", number: " + str( self.objects.get_pane_by_id( deferred_layout["parent"]).number), ) debug( "Window", "add_pane_row", "Split direction: " + deferred_layout["split"], ) self.add_pane_row( deferred_layout["layout"], deferred_layout["parent"], deferred_layout["split"], ) # pylint: disable=bad-continuation # black auto-format disagrees def identify_deferred_layouts( self, layout: str, direction: str, parent_id: str = None, prevailing_split: str = None, ) -> list: """ Identifies any layouts needing to be resolved later This algorithm requires breadth-first parsing, to have correct numbering/re-creation behaviors Parameters ---------- layout : str The layout to analyze direction : str The direction of this layout parent_id = None : str The original parent this layout should be created for prevailing_split = None : str The direction of the layout prior to this one Returns ------- list A list of layouts which need to be processed later """ first_found = True deferred_layouts = [] while True: match = re.match(self.PANE_LAYOUT, layout) if not match: break pane_layout = match.groups()[0] debug("Window", "identify_deferred", "Pane details:") debug("Window", "identify_deferred", pane_layout) layout = layout[len(pane_layout):] debug("Window", "identify_deferred", "Remaining layout:") debug("Window", "identify_deferred", layout) if layout[0] == ",": debug("Window", "identify_deferred", "Got pane number:") match = re.match(r",([0-9]+)", layout) pane_number = match.groups()[0] debug("Window", "identify_deferred", pane_number) match = re.match(r"(,[0-9]+,?)", layout) to_remove = match.groups()[0] layout = layout[len(to_remove):] debug("Window", "identify_deferred", "Stripping number, leaves:") debug("Window", "identify_deferred", layout) debug( "Window", "identify_deferred", "Adding pane with split direction: " + (direction if not first_found or not prevailing_split else prevailing_split), ) self.add_pane_by_layout( pane_layout, pane_number, direction if not first_found or not prevailing_split else prevailing_split, parent_id if parent_id else self.last_pane, ) first_found = False else: debug("Window", "identify_deferred", "Resolving layout recursively") # Find the end of the nested layout # Tracks the levels of layouts # e.g. for [ { [ ] } ] will have [, {, [ nested_layouts = [layout[0]] length = 0 for char in layout[1:]: if char in self.LAYOUT_PAIRS.keys(): nested_layouts.append(char) elif char == self.LAYOUT_PAIRS[nested_layouts[-1]]: nested_layouts.pop() # If this was the last one, then stop searching the string length += 1 if len(nested_layouts) == 0: break debug("Window", "identify_deferred", "Found deferred layout:") debug("Window", "identify_deferred", "Layout: " + layout[:length + 1]) debug( "Window", "identify_deferred", "Parent: " + str(self.panes_added - 1), ) debug("Window", "identify_deferred", "Split direction: " + direction) pane_details = self.get_pane_details_from_layout( layout[1:length]) self.add_pane_by_layout( pane_details["selfLayout"], pane_details["number"], direction, parent_id, ) deferred_layouts.append({ # Keep the first character of the layout - [ or { "layout": layout[0] # Deferred layout is everything after the first pane through # the end of the matched (nested) layout # This will be added later, since # we need to parse layouts breadth-first + layout[len(pane_details["selfLayout"]) + 1:length + 1], "parent": self.last_pane, # Deferred layouts will contain the next direction, # not the current direction "split": self.DIRECTION_MAP[layout[0]], }) # Strip the part that was nested out of the layout # and remove a left-leading comma, if there is one layout = layout[length + 1:].lstrip(",") debug("Window", "identify_deferred", "Removing deferred layout leaves:") debug("Window", "identify_deferred", layout) # For deferred layouts where multiple panes will be added, # chain parent to this pane # Otherwise, you end up with 0 - 2 - 1 for pane numbering, # when you really should have 0 - 1 - 2 # This is because the "add_pane" bit will # shift numbers 1 and greater up one, but that doesn't match # the order of creation/insertion that is expected if parent_id: parent_id = self.last_pane return deferred_layouts def add_pane_by_layout(self, layout: str, pane_number: int, direction: str, parent_identity: str = None) -> None: """ Adds a new pane, using the layout to fill in details Parameters ---------- layout : string The layout string for the pane Will be in the following format: <size>,<offset_left>,<offset_top> pane_number : number The number of the pane, from the layout direction : string The direction this pane should be split from the previous one parent_identity : string The identity of the parent pane Used to re-order panes, if needed """ size = parse_size(layout.split(",")[0]) pane_id = "win{0}pane{1}".format(self.number, self.panes_added) pane = (Pane(pane_id, self, direction, self.panes_added == 0).set_session_name( self.session_name).set_size( 100 * size["width"] / self.base_width, 100 * size["height"] / self.base_height, )) parent = None try: parent = self.objects.get_pane_by_id(parent_identity) parent_number = parent.number except NameError: parent_number = None pane_number = (parent_number + 1 if parent_number is not None else self.panes_added) if parent_number is not None and self.panes_added > pane_number: debug( "Window", "add_pane", "Incrementing pane numbers above {0}".format(pane_number), ) for pane_to_check in self.panes: if pane_to_check.number >= pane_number: pane_to_check.set_number(pane_to_check.number + 1) pane.set_number(pane_number) if self.last_pane is not None and parent_number is None: pane.set_target(self.last_pane) elif parent: pane.set_target(parent.identity) self.last_pane = pane.identity self.panes_added += 1 self.panes.append(pane) debug( "Window", "add_pane", "Adding pane number {0} for parent {1} with size {2}".format( pane_number, parent_number, size), ) def get_pane_details_from_layout(self, layout: str) -> dict: """ Get details about a pane layout Parameters ---------- layout : str The layout string Returns ------- dict containing details about the pane """ match = re.match(self.PANE_LAYOUT, layout) if not match: debug( "Window", "pane_details", "Layout did not match pane format! ({0})".format(layout), ) return {} details = match.groups()[0] size, offset_left, offset_top = details.split(",") pane_number = layout.split(",")[3] layout = details + ",{0},".format(pane_number) return { "size": size, "left": offset_left, "top": offset_top, "number": pane_number, "selfLayout": layout, }
def setup(): """ Reset the object tracker windows and panes """ ObjectTracker().reset()
def setup(): """ Reset the object tracker """ ObjectTracker().reset()
def test_adding_and_retrieving(): """ Try adding two items with the same ID """ tracker = ObjectTracker() pane1 = Pane("pane1") tracker.add_pane(pane1) pane2 = Pane("pane2") tracker.add_pane(pane2) window1 = Window("window1") tracker.add_window(window1) window2 = Window("window2") tracker.add_window(window2) ids = ["pane1", "pane2", "window1", "window2"] for expected in ids: if "pane" in expected: actual = tracker.get_pane_by_id(expected).identity else: actual = tracker.get_window_by_id(expected).identity assert actual == expected, "Retrieved {0} does not match!".format( expected)
def test_getting_nonexistent_items(): """ Try getting items which do not exist """ tracker = ObjectTracker() try: tracker.get_window_by_id("foo") assert False, "Expected NameError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "No such object: foo!", NameError) try: tracker.get_pane_by_id("foo") assert False, "Expected NameError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "No such object: foo!", NameError) tracker.add_pane(Pane("foo")) try: tracker.get_window_by_id("foo") assert False, "Expected TypeError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "Identity provided is not a window!", TypeError) tracker.add_window(Window("foo2")) try: tracker.get_pane_by_id("foo2") assert False, "Expected TypeError exception, but one was not thrown" # pylint: disable=broad-except except Exception as error: check_exception(error, "Identity provided is not a pane!", TypeError)
def set_session_parameters( session: str, session_start_directory: str, windows: list ) -> str: """ Sets session data based off of parameters Parameters ---------- session: string The name of the session session_start_directory : string The path to the session's starting directory windows : list A list of window information Each needs: - identity - number - title - postfix (appended to title in window list) - dir - layout - panes (list of objects) - identity - number - dir - command [optional] - parent [optional] Returns ------- string The window list to analyze """ inject_session_data(SESSION_INITIAL_DIR_KEY, session_start_directory) layout = "" for window in windows: win_object = ( FakeWindow(window["identity"]) .set_session_name(session) .set_name(window["title"]) .set_number(window["number"]) .set_layout(window["layout"]) .set_directory(window.get("dir", session_start_directory)) ) win_object.inject() ObjectTracker().add_window(win_object) for pane in window["panes"]: pane_object = ( FakePane(pane["identity"]) .set_session_name(session) .set_number(pane["number"]) .set_window(win_object) .set_directory( pane.get("dir", window.get("dir", session_start_directory)) ) .set_command(pane.get("command", "")) ) if "parent" in pane: pane_object.set_split_from(pane["parent"]) pane_object.inject() ObjectTracker().add_pane(pane_object) layout += "{0}: {1}{2} [200x60] ({3} panes) [{4}] @{0}\n".format( window["number"], window["title"], window["postfix"], len(window["panes"]), window["layout"], ) inject_session_data(WINDOW_LIST_KEY, layout) return layout
class Pane: """ Represents a pane in a Tmux window Attributes ---------- identity : string A unique ID representing this pane window : string The unique ID for the window this pane belongs to direction : string|None Which direction to split this pane - 'h' for horizontal, 'v' for vertical Can be None ONLY if this is the first pane is_first : bool Is this the first pane created in the window? number : int The number of this window session_name : string The name of the current session start_dir : string The starting directory for the pane split_from : string The ID of the pane to split this one from width : int The width of the pane, in lines height : int The height of the pane, in lines command : string The command to run in this pane """ COMPARABLE_PROPERTIES = [ "width", "height", "split_from", "direction", "start_dir", "command", ] # pylint: disable=bad-continuation # black auto-format disagrees def __init__(self, identity: str, window: "Window", direction: str, is_first: bool = False) -> "Pane": """ Sets up details about the pane Parameters ---------- identity : string A unique ID representing this pane window : Window The Window object this pane belongs to direction : string|None Which direction to split this pane - 'h' for horizontal, 'v' for vertical is_first : bool Is this the first pane that was created? """ self.identity = identity self.window = window self.direction = direction self.is_first = is_first self.session_name = None self.command = "" self.split_from = None self.width = 0 self.height = 0 self.number = None self.parent_number = None self.start_dir = None self.objects = ObjectTracker() self.objects.add_pane(self) def set_number(self, num: int) -> "Pane": """ Set the pane number Parameters ---------- num : int The pane number Returns ------- This instance """ self.number = num return self def set_session_name(self, name: str) -> "Pane": """ Sets the session name for the pane Parameters ---------- name : string The name of the current session Returns ------- This instance """ self.session_name = name return self def set_start_dir(self, directory: str) -> "Pane": """ Sets the starting directory for the pane Parameters ---------- directory : string The starting directory of the current session Returns ------- This instance """ self.start_dir = directory return self def set_target(self, target: str) -> "Pane": """ Set the target to split this pane from Parameters ---------- target : string The identity of the pane to split this pane from Returns ------- This instance """ self.split_from = target self.parent_number = self.objects.get_pane_by_id(target).number return self def set_size(self, width: int = 0, height: int = 0) -> "Pane": """ Set the size of this pane Parameters ---------- width : int|0 The line width of the pane height : int|0 The line height of the pane Returns ------- This instance """ if height: self.height = height if width: self.width = width return self def set_command(self, command: str, needs_enter: bool = True) -> "Pane": """ Set the command to run in this pane Parameters ---------- command : string The command to run in this pane needs_enter : bool Whether the command needs an "enter" keystroke at the end of it Returns ------- This instance """ self.command = escape_command(command, needs_enter) return self def set_parent_directly(self, parent_number: int) -> "Pane": """ Set the number of the parent directly, rather than by ID Needed for scripting a Tmux session, since panes need to be created twice Parameters ---------- parent_number : number The number of the parent of this pane Returns ------- self This instance """ self.parent_number = parent_number return self def get_target(self, target_self: bool = False) -> str: """ Return the target for this pane, if any Parameters ---------- target_self : bool Whether the target should be myself Returns ------- string|None The tmux-formatted target for this pane None if no target set to split from """ target = "" if target_self: target = get_pane_syntax(self.session_name, self.window.number, self.number) elif self.parent_number: target = get_pane_syntax(self.session_name, self.window.number, self.parent_number) elif self.split_from is not None: parent_pane = self.objects.get_pane_by_id(self.split_from) target = get_pane_syntax(self.session_name, self.window.number, parent_pane.number) else: target = None return target def get_split_command(self) -> str: """ Creates the split command for this pane Returns ------- string The command used to split this pane """ split_command = "" # First pane does not need a command if self.is_first: pass else: split_command = "split-window " target = self.get_target() target = "-t " + target if target else "" if self.start_dir and target: split_command += "-{0} {2} {1}".format(self.direction, self.start_dir, target) elif self.start_dir: split_command += "-{0} {1}".format(self.direction, self.start_dir) elif target: split_command += "-{0} {1}".format(self.direction, target) else: split_command += "-{0}".format(self.direction) return split_command def get_run_command(self) -> str: """ Returns the tmux-formatted run command for this pane Returns ------- string The tmux-formatted run command """ if self.command and len(self.command) > 0: return "send-keys {0}".format(self.command) return "" def get_size_command(self) -> str: """ Returns the command to set the pane size Returns ------- string The command to set this pane's size """ target = self.get_target(True) width = "" if self.width: width = "-x {0}".format(self.width) height = "" if self.height: height = "-y {0}".format(self.height) if width or height: return "resize-pane -t {0} {1} {2}".format(target, width, height) return "" def get_difference(self, pane: "Pane") -> dict: """ Compare this instance to another pane Checks properties, not direct reference Parameters ---------- pane : Pane The pane to compare against Returns ------- dict A key-value comparison of the two panes, containing this versus the argument """ return get_object_differences(self, pane, self.COMPARABLE_PROPERTIES) def to_string(self) -> str: """ Makes a string representation of this class """ return ("identity : {0}" " number : {1}" " parent : {2}" " size : {3}" " command : {4}").format( self.identity, self.number, self.split_from, "{0}x{1}".format(self.width, self.height), self.command, )
class TmuxBuilder: """ The class to build a Tmux session this was designed to chain methods, such as: TmuxBuilder('A session') \\ \n .add_window('main') \\ \n .add_pane('test') \\ \n .run_command('echo "foo"') \\ \\n ... Attributes ---------- windows : list[Window] A list of the windows to create session_name : string The name of the session start_dir : string A directory to base all windows/panes from current_window : int The index of the currently-active window current_pane : int The index of the currently-active pane detach : bool Whether to detach the session after creation objects_by_id : dictionary A list of windows/panes indexed by ID, for target reference """ size_warning_printed = False session_name = "" start_dir = "" current_window = -1 current_pane = 0 terminal_width = None terminal_height = None detach = True # pylint: disable=bad-continuation # black auto-format disagrees def __init__(self, session_name: str, starting_directory: str = "", detach: bool = True) -> "TmuxBuilder": """ Sets up details about the tmux builder Parameters ---------- session_name : string The name of the session to create starting_directory : string The directory to base all windows/panes out of detach : bool Whether to detach this session after creatio """ self.session_name = session_name self.objects = ObjectTracker() self.windows = [] self.start_dir = starting_directory self.detach = bool(detach) self.__get_terminal_size() def __get_terminal_size(self) -> None: """ Gets the terminal size using the tput command NOTE: ASSUMES CURRENT SIZE IS THE SAME SIZE AS THE INTENDED SESSION """ if self.terminal_width and self.terminal_height: return self.terminal_height = int( check_output(["tput", "lines"]).decode("utf-8").strip()) self.terminal_width = int( check_output(["tput", "cols"]).decode("utf-8").strip()) # pylint: disable=bad-continuation # black auto-format disagrees def set_terminal_size(self, terminal_width: int, terminal_height: int) -> "TmuxBuilder": """ Sets the terminal height, rather than using tput This is used for testing Parameters ---------- terminal_width : int The width of the terminal terminal_height : int The height of the terminal Returns ------- This instance """ self.terminal_width = terminal_width self.terminal_height = terminal_height return self def __coalesce_window(self, window: str = None) -> Window: """ Determines which window to target If none provided, defaults to the current window Parameters ---------- window : string|None The window ID to target Returns ------- Window The window object """ return (self.objects.get_window_by_id(window) if window else self.windows[self.current_window]) def __coalesce_pane(self, pane: str = None) -> Window: """ Determines which pane to target If none provided, defaults to the current pane Parameters ---------- pane : string|None The pane ID to target Returns ------- Window The pane object to target """ if pane == 0: return pane return (self.objects.get_pane_by_id(pane) if pane else self.windows[self.current_window].get_pane(self.current_pane)) def add_window( self, window_id: str, pane_id: str, window_name: str, working_directory: str = None, ) -> "TmuxBuilder": """ Adds a new window Parameters ---------- window_id : string A unique ID for the new window pane_id : string A unique ID for the new pane in the window window_name : string The name of the new window working_directory : string|None The working directory for the first pane Returns ------- This instance """ window_num = len(self.windows) window = (Window( window_id, window_name, window_num == 0).set_number(window_num).set_session_name( self.session_name).set_start_dir( format_working_directory(working_directory or self.start_dir))) self.windows.append(window) self.current_pane = 0 self.current_window = len(self.windows) - 1 return self.add_pane(pane_id, None, window_id) # pylint: disable=too-many-arguments def add_pane( self, identity: str, split_dir: str, window: str = None, split_from: str = None, working_directory: str = None, ) -> "TmuxBuilder": """ Adds a new pane to target or current window Parameters ---------- identity : string A unique ID for this pane split_dir : string The direction to split the window, either 'h' (horizontal) or 'v' (vertical) window : string|None The window ID to create the pane in, defaults to the current window split_from : string|None The pane ID to split this one from working_directory : string|None The working directory to set for this pane Returns ------- This instance """ if split_dir not in [None, "h", "v"]: raise ValueError( 'Invalid split direction specified: must be "v" or "h"!') if split_from: target = self.objects.get_pane_by_id(split_from) window = target.window else: window = self.__coalesce_window(window) target = None panes = window.get_pane_count() start_dir = None if working_directory: start_dir = format_working_directory(working_directory) elif window.start_dir: start_dir = window.start_dir else: start_dir = format_working_directory(self.start_dir) pane = (Pane(identity, window, split_dir, panes == 0).set_number(panes).set_session_name( self.session_name).set_start_dir(start_dir)) if split_from: pane.set_target(split_from) window.add_pane(pane) # The index of the current pane, not the number self.current_pane = window.get_pane_count() - 1 return self def __print_size_warning(self) -> None: """ Prints a warning about the size of the terminal """ if not self.size_warning_printed: print("NOTE: USING CURRENT TERMINAL SIZE AS TEMPLATE FOR SESSION", file=stderr) self.size_warning_printed = True def set_pane_height(self, height: int, pane: str = None) -> "TmuxBuilder": """ Sets the height of the given pane, percent of available Can target a specific pane, or default to current Parameters ---------- height : int The percentage of the screen this pane should be high pane : string|None The target pane ID Returns ------- This instance """ self.__print_size_warning() return self.__set_pane_size(0, height, pane) def set_pane_width(self, width: int, pane: str = None) -> "TmuxBuilder": """ Sets the width of the given pane, percent of available Can target a specific pane, or default to current Parameters ---------- width : int The percentage of the screen this pane should be wide pane : string|None The target pane ID Returns ------- This instance """ self.__print_size_warning() return self.__set_pane_size(width, 0, pane) def __set_pane_size(self, width: int = 0, height: int = 0, pane: str = None) -> "TmuxBuilder": """ Set the actual pane size, for a given target pane Defaults to current window/pane Parameters ---------- width : int The width to set the pane to height : int The height to set the pane to pane : string|None The pane ID to target Returns ------- This instance """ pane = self.__coalesce_pane(pane) line_width = floor(self.terminal_width * (width / 100)) line_height = floor(self.terminal_height * (height / 100)) pane.set_size(line_width, line_height) return self def set_pane_parent(self, parent_number: int, pane_id: str = None): """ Sets the parent number of the pane Parameters ---------- parent_number : number The number of the pane's parent pane_id = None : string Optionally, the ID to set the parent for Defaults to the most recently-added pane """ # If pane is passed in, use that, otherwise get the current pane pane = (self.objects.get_pane_by_id(pane_id) if pane_id else self.__coalesce_pane()) pane.set_parent_directly(parent_number) def run_command(self, command: str, window: str = None, pane: str = None): """ Sets the command to run for a given window/pane Defaults to current window/pane Parameters ---------- command : string The command to run in the pane window : string|None The window ID to run the command in pane : string|None The pane ID to run the command in Returns ------- This instance """ window = self.__coalesce_window(window) if not pane: pane = window.get_pane(self.current_pane) else: pane = self.__coalesce_pane(pane) pane.set_command(command) return self def debug(self) -> None: """ Prints the built tmux command """ print(self.build()) def build(self) -> str: """ Builds the actual bash command that will create the tmux session Parameters ---------- Returns ------- string The bash command to execute """ if len(self.windows) == 0: raise RuntimeError("No windows provided!") commands = [ "tmux new-session {1} -s {0}".format(self.session_name, "-d" if self.detach else "") ] # Create windows, panes, and commands for window in self.windows: commands.append(window.get_create_command()) for pane in window.panes: # Only split window for a new pane if not the first pane in the window split_command = pane.get_split_command() if split_command: commands.append(split_command) run_cmd = pane.get_run_command() if run_cmd: commands.append(run_cmd) # Set the panel dimensions later, since can't set dimensions if related panes aren't created # e.g., can't set a vertical height if the second pane doesn't exist yet for window in self.windows: for pane in window.panes: size_command = pane.get_size_command() if size_command: commands.append(size_command) commands = " \\; \\\n".join(commands) return 'pushd "{0}"\n'.format( self.start_dir) + commands + "\n" + "popd" def run(self) -> None: """ Execute the commands to build a session """ cmd = self.build() system(cmd)