#!/usr/bin/env python3

import argparse
import asyncio
try:
  import iterm2
except ModuleNotFoundError:
  print("The current version of Python doesn't have the 'iterm2' module installed. Please run:")
  print("\n    $ python3 -m pip install iterm2\n")
  exit()
import logging
import re
import sys
import traceback

async def list_sessions(connection, args):
  a = await iterm2.async_get_app(connection)
  for w in a.terminal_windows:
    for t in w.tabs:
      sessions = t.sessions
      for s in sessions:
        print(s.pretty_str(), end='')
  print("")
  print("Buried sessions:")
  for s in a.buried_sessions:
    print(s.pretty_str(), end='')

async def show_hierarchy(connection, args):
  a = await iterm2.async_get_app(connection)
  print(a.pretty_str())

async def send_text(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  await s.async_send_text(args.text)

async def create_tab(connection, args):
  a = await iterm2.async_get_app(connection)
  if args.window is not None:
    window_id = args.window
    try:
      window = next(window for window in a.terminal_windows if window.window_id == window_id)
      tab = await window.async_create_tab(profile=args.profile, command=args.command, index=args.index)
    except:
      print("bad window id {}".format(window_id))
      sys.exit(1)
  else:
    window = await iterm2.Window.async_create(connection, profile=args.profile, command=args.command)
    if not window:
        return
    tab = window.tabs[0]
  session = tab.sessions[0]
  print(session.pretty_str())

async def split_pane(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  session = await s.async_split_pane(vertical=args.vertical, before=args.before, profile=args.profile)
  print(session.pretty_str())

async def get_buffer(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  contents = await s.async_get_screen_contents()
  for i in range(contents.number_of_lines):
    line = contents.line(i)
    print(line.string)

async def get_prompt(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  result = await iterm2.async_get_last_prompt(connection, s.session_id)
  print("working_directory: \"{}\"".format(result.working_directory))
  print("command: \"{}\"".format(result.command))

def profile_property_type_map():
  map = {
      "allow_title_reporting":                       "bool",
      "allow_title_setting":                         "bool",
      "ambiguous_double_width":                      "bool",
      "ansi_0_color":                                "color",
      "ansi_10_color":                               "color",
      "ansi_11_color":                               "color",
      "ansi_12_color":                               "color",
      "ansi_13_color":                               "color",
      "ansi_14_color":                               "color",
      "ansi_15_color":                               "color",
      "ansi_1_color":                                "color",
      "ansi_2_color":                                "color",
      "ansi_3_color":                                "color",
      "ansi_4_color":                                "color",
      "ansi_5_color":                                "color",
      "ansi_6_color":                                "color",
      "ansi_7_color":                                "color",
      "ansi_8_color":                                "color",
      "ansi_9_color":                                "color",
      "answerback_string":                           "str",
      "application_keypad_allowed":                  "bool",
      "ascii_anti_aliased":                          "bool",
      "ascii_ligatures":                             "bool",
      "background_color":                            "color",
      "background_image_is_tiled":                   "bool",
      "badge_color":                                 "color",
      "badge_text":                                  "str",
      "blend":                                       "float",
      "blink_allowed":                               "bool",
      "blinking_cursor":                             "bool",
      "blur":                                        "float",
      "blur_radius":                                 "float",
      "bm_growl":                                    "bool",
      "bold_color":                                  "color",
      "character_encoding":                          "int",
      "close_sessions_on_end":                       "bool",
      "cursor_boost":                                "float",
      "cursor_color":                                "color",
      "cursor_guide_color":                          "color",
      "cursor_text_color":                           "color",
      "cursor_type":                                 "int",
      "disable_printing":                            "bool",
      "disable_smcup_rmcup":                         "bool",
      "disable_window_resizing":                     "bool",
      "flashing_bell":                               "bool",
      "foreground_color":                            "color",
      "horizontal_spacing":                          "float",
      "idle_code":                                   "int",
      "idle_period":                                 "float",
      "link_color":                                  "color",
      "minimum_contrast":                            "float",
      "mouse_reporting":                             "bool",
      "mouse_reporting_allow_mouse_wheel":           "bool",
      "name":                                        "str",
      "non_ascii_anti_aliased":                      "bool",
      "non_ascii_ligatures":                         "bool",
      "only_the_default_bg_color_uses_transparency": "bool",
      "left_option_key_sends":                       "int",
      "place_prompt_at_first_column":                "bool",
      "prompt_before_closing":                       "bool",
      "reduce_flicker":                              "bool",
      "right_option_key_sends":                      "int",
      "scrollback_in_alternate_screen":              "bool",
      "scrollback_lines":                            "int",
      "scrollback_with_status_bar":                  "bool",
      "selected_text_color":                         "color",
      "selection_color":                             "color",
      "send_bell_alert":                             "bool",
      "send_code_when_idle":                         "bool",
      "send_idle_alert":                             "bool",
      "send_new_output_alert":                       "bool",
      "send_session_ended_alert":                    "bool",
      "send_terminal_generated_alerts":              "bool",
      "session_close_undo_timeout":                  "float",
      "show_mark_indicators":                        "bool",
      "silence_bell":                                "bool",
      "smart_cursor_color":                          "color",
      "smart_cursor_color":                          "color",
      "sync_title":                                  "str",
      "tab_color":                                   "color",
      "thin_strokes":                                "int",
      "transparency":                                "float",
      "underline_color":                             "color",
      "unicode_normalization":                       "int",
      "unicode_version":                             "int",
      "unlimited_scrollback":                        "bool",
      "use_bold_font":                               "bool",
      "use_bright_bold":                             "bool",
      "use_cursor_guide":                            "bool",
      "use_italic_font":                             "bool",
      "use_non_ascii_font":                          "bool",
      "use_tab_color":                               "bool",
      "use_underline_color":                         "bool",
      "vertical_spacing":                            "float",
      "visual_bell":                                 "bool",
      "triggers":                                    "dict",
      "smart_selection_rules":                       "list",
      "semantic_history":                            "dict",
      "automatic_profile_switching_rules":           "list",
      "advanced_working_directory_window_setting":   "string",
      "advanced_working_directory_window_directory": "string",
      "advanced_working_directory_tab_setting":      "string",
      "advanced_working_directory_tab_directory":    "string",
      "advanced_working_directory_pane_setting":     "string",
      "advanced_working_directory_pane_directory":   "string",
      "normal_font":                                 "string",
      "non_ascii_font":                              "string",
      "background_image_location":                   "string",
      "key_mappings":                                "dict",
      "touchbar_mappings":                           "dict" }
  return map

def profile_properties():
  return list(profile_property_type_map().keys())

def profile_property_type(key):
  return profile_property_type_map()[key]

async def get_profile_property(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  profile = await s.async_get_profile()
  if args.keys is not None:
    keys = args.keys.split(",")
  else:
    keys = profile_properties()
  for prop in keys:
    fname = prop
    value = getattr(profile, fname)
    print("{}: {}".format(prop, value))

def encode_property_value(key, value):
  type = profile_property_type(key)
  if type == "bool":
    assert value == "true" or value == "false"
    return value == "true"
  elif type == "str":
    return value
  elif type == "float":
    return float(value)
  elif type == "int":
    return int(value)
  elif type == "dict" or type == "list":
    class TypeNotSupportedException(Exception): Pass
    raise TypeNotSupportedException("this property's type is not supported")
  elif type == "color":
    # Accepted values look like: "(0,0,0,255 sRGB)"
    regex = r"\(([0-9]+), *([0-9]+), *([0-9]+), *([0-9]+)  *([A-Za-z]+)\)"
    match = re.search(regex, value)
    assert match is not None
    return iterm2.Color(
        float(match.group(1)),
        float(match.group(2)),
        float(match.group(3)),
        float(match.group(4)),
        iterm2.ColorSpace(match.group(5)))

async def set_profile_property(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)

  encoded_value = encode_property_value(args.key, args.value)
  profile = await s.async_get_profile()
  fname = "async_set_" + args.key
  f = getattr(profile, fname)
  await f(encoded_value)

async def read(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  if args.mode == "char":
    async with iterm2.KeystrokeMonitor(connection) as mon:
      keystroke = await mon.async_get()
      print(keystroke)
  elif args.mode == "line":
    async with s.get_keystroke_reader() as reader:
      eol = False
      line = ""
      while not eol:
        k = await reader.get()
        for e in k:
          c = e.characters
          if c == "\r" or c == "\n":
            eol = True
            break
          line += c

      print(line)

async def get_window_property(connection, args):
  a = await iterm2.async_get_app(connection)
  w = a.get_window_by_id(args.id)
  if w is None:
    print("bad window ID")
  else:
    if args.name == "frame":
      frame = await w.async_get_frame()
      print("{},{},{},{}".format(frame.origin.x,frame.origin.y,frame.size.width,frame.size.height))
    elif args.name == "fullscreen":
      print(await w.async_get_fullscreen(connection))

async def set_window_property(connection, args):
  a = await iterm2.async_get_app(connection)
  w = a.get_window_by_id(args.id)
  if w is None:
    print("bad window ID")
  else:
    if args.name == "frame":
      parts = args.value.split(",")
      frame = iterm2.Frame(iterm2.Point(int(parts[0]), int(parts[1])), iterm2.Size(int(parts[2]), int(parts[3])))
      await w.async_set_frame(frame)
    elif args.name == "fullscreen":
      await w.async_set_fullscreen(args.value == "true")

async def inject(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  if s is None:
    print("bad session ID")
  else:
    await s.async_inject(args.data.encode())

async def activate(connection, args):
  a = await iterm2.async_get_app(connection)
  if args.mode == "session":
    s = a.get_session_by_id(args.id)
    if s is None:
      print("bad session ID")
    else:
      await s.async_activate()
  elif args.mode == "tab":
    t = a.get_tab_by_id(args.id)
    if t is None:
      print("bad tab ID")
    else:
      await t.async_select()
  elif args.mode == "window":
    w = a.get_window_by_id(args.id)
    if w is None:
      print("bad window ID")
    else:
      await w.async_activate()

async def activate_app(connection, args):
  a = await iterm2.async_get_app(connection)
  await a.async_activate(raise_all_windows=args.raise_all_windows, ignoring_other_apps=args.ignoring_other_apps)

async def set_variable(connection, args):
  a = await iterm2.async_get_app(connection)
  if args.session:
    s = a.get_session_by_id(args.session)
    if s is None:
      print("bad session ID")
      return
    await s.async_set_variable(args.name, args.value)
  elif args.tab:
    t = a.get_tab_by_id(args.tab)
    if t is None:
      print("bad tab ID")
      return
    await t.async_set_variable(args.name, args.value)
  else:
    await a.async_set_variable(args.name, args.value)

async def get_variable(connection, args):
  a = await iterm2.async_get_app(connection)
  if args.session:
    s = a.get_session_by_id(args.session)
    if s is None:
      print("bad session ID")
      return
    value = await s.async_get_variable(args.name)
    print(value)
  elif args.tab:
    t = a.get_tab_by_id(args.tab)
    if t is None:
      print("bad tab ID")
      return
    value = await t.async_get_variable(args.name)
    print(value)
  else:
    value = await a.async_get_variable(args.name)
    print(value)

async def list_variables(connection, args):
  a = await iterm2.async_get_app(connection)
  if args.session:
    s = a.get_session_by_id(args.session)
    if s is None:
      print("bad session ID")
      return
    value = await s.async_get_variable("*")
    for name in value:
      print(name)
  elif args.tab:
    t = a.get_tab_by_id(args.tab)
    if t is None:
      print("bad tab ID")
      return
    value = await t.async_get_variable("*")
    for name in value:
      print(name)
  else:
    value = await a.async_get_variable("*")
    for name in value:
      print(name)

async def saved_arrangement(connection, args):
  if args.window is not None:
    a = await iterm2.async_get_app(connection)
    w = a.get_window_by_id(args.window)
    if w is None:
      print("bad window ID")
      return
    if args.action == "save":
      await w.async_save_window_as_arrangement(args.name)
    elif args.action == "restore":
      await w.async_restore_window_arrangement(args.name)
  else:
    if args.action == "save":
      await iterm2.Arrangement.async_save(connection, args.name)
    elif args.action == "restore":
      await iterm2.Arrangement.async_restore(connection, args.name)

async def show_focus(connection, args):
  a = await iterm2.async_get_app(connection)
  if a.app_active:
    print("App is active")
  w = a.current_terminal_window
  print("Key window: {}".format(w.window_id))
  print("")
  for w in a.terminal_windows:
    t = a.get_tab_by_id(w.selected_tab_id)
    print("Selected tab in {}: {}".format(w.window_id, t.tab_id))
    s = a.get_session_by_id(t.active_session_id)
    print("  Active session is: {}".format(s.pretty_str()))

async def list_profiles(connection, args):
  guids = args.guids.split(",") if args.guids is not None else None
  properties = args.properties.split(",") if args.properties is not None else None
  profiles = await iterm2.PartialProfile.async_query(connection, guids=guids, properties=properties)
  for profile in profiles:
    keys = list(profile.all_properties.keys())
    keys.sort()
    for k in keys:
      v = profile.all_properties[k]
      print("{}: {}".format(k, v))
    print("")

async def set_grid_size(connection, args):
  a = await iterm2.async_get_app(connection)
  s = a.get_session_by_id(args.session)
  await s.async_set_grid_size(iterm2.Size(args.width, args.height))

async def list_tmux_connections(connection, args):
  connections = await iterm2.async_get_tmux_connections(connection)
  for connection in connections:
    print("Connection ID: {}\nOwning session: {}".format(connection.connection_id, connection.owning_session))

async def send_tmux_command(connection, args):
  connections = await iterm2.async_get_tmux_connections(connection)
  ids = []
  for connection in connections:
    if connection.connection_id == args.connection_id:
      print(await connection.async_send_command(args.command))
      return;
    ids.append(connection.connection_id)
  print("No connection with id {} found. Have: {}".format(args.connection_id, ", ".join(ids)))

async def set_tmux_window_visible(connection, args):
  connections = await iterm2.async_get_tmux_connections(connection)
  ids = []
  for connection in connections:
    if connection.connection_id == args.connection_id:
      await connection.async_set_tmux_window_visible(args.window_id, args.visible)
      return;
    ids.append(connection.connection_id)
  print("No connection with id {} found. Have: {}".format(args.connection_id, ", ".join(ids)))

async def sort_tabs(connection, args):
  app = await iterm2.async_get_app(connection)
  for w in app.terminal_windows:
    tabs = w.tabs
    for t in tabs:
      t.tab_name = await t.async_get_variable("currentSession.session.name")
    def tab_name(t):
      return t.tab_name
    sorted_tabs = sorted(tabs, key=tab_name)
    await w.async_set_tabs(sorted_tabs)

async def list_color_presets(connection, args):
  presets = await iterm2.ColorPreset.async_get_list(connection)
  for preset in presets:
    print(preset)

async def set_color_preset(connection, args):
  preset = await iterm2.ColorPreset.async_get(connection, args.preset)
  profiles = await iterm2.PartialProfile.async_query(connection, properties=['Guid', 'Name'])
  for partial in profiles:
    if partial.name == args.profile:
      profile = await partial.async_get_full_profile()
      await profile.async_set_color_preset(preset)

async def monitor_variable(connection, args):
  if args.session:
    scope = iterm2.VariableScopes.SESSION
    identifier = args.session
  elif args.tab:
    scope = iterm2.VariableScopes.TAB
    identifier = args.tab
  elif args.window:
    scope = iterm2.VariableScopes.WINDOW
    identifier = args.window
  elif args.app:
    scope = iterm2.VariableScopes.APP
    identifier = ''
  else:
    assert False
  async with iterm2.VariableMonitor(connection, scope, args.name, identifier) as monitor:
    value = await monitor.async_get()
    print(f"New value: {value}")

async def monitor_focus(connection, args):
    async with iterm2.FocusMonitor(connection) as monitor:
        update = await monitor.async_get_next_update()
        print("Update: {}".format(update))

async def set_cursor_color(connection, args):
    a = await iterm2.async_get_app(connection)
    s = a.get_session_by_id(args.session)
    partial = iterm2.LocalWriteOnlyProfile()
    r, g, b = list(map(int, args.color.split(",")))
    c = iterm2.Color(r, g, b)
    partial.set_cursor_color(c)
    await s.async_set_profile_properties(partial)

async def monitor_screen(connection, args):
    a = await iterm2.async_get_app(connection)
    s = a.get_session_by_id(args.session)
    async with s.get_screen_streamer() as streamer:
        done = False
        while not done:
            contents = await streamer.async_get()
            for i in range(contents.number_of_lines):
                line = contents.line(i)
                if args.query in line.string:
                    return
    
async def show_selection(connection, args):
    a = await iterm2.async_get_app(connection)
    s = a.get_session_by_id(args.session)
    selection = await s.async_get_selection()
    for sub in selection.subSelections:
        print("Sub selection: {}".format(await sub.async_get_string(connection, s.session_id)))
    print("Text: {}".format(await selection.async_get_string(connection, s.session_id, s.grid_size.width)))

def make_parser():
  parser = argparse.ArgumentParser(description='iTerm2 CLI')
  subparsers = parser.add_subparsers(help='Commands')

  list_sessions_parser = subparsers.add_parser("list-sessions", help="List sessions")
  list_sessions_parser.set_defaults(func=list_sessions)

  show_hierarchy_parser = subparsers.add_parser("show-hierarchy", help="Show all windows, tabs, and sessions")
  show_hierarchy_parser.set_defaults(func=show_hierarchy)

  send_text_parser = subparsers.add_parser("send-text", help="Send text as though the user had typed it")
  send_text_parser.add_argument('session', type=str, help='Session ID')
  send_text_parser.add_argument("text", type=str, help='Text to send')
  send_text_parser.set_defaults(func=send_text)

  create_tab_parser = subparsers.add_parser("create-tab", help="Create a new tab or window")
  create_tab_parser.add_argument('--profile', type=str, nargs='?', help='Profile name')
  create_tab_parser.add_argument('--window', type=str, nargs='?', help='Window ID')
  create_tab_parser.add_argument('--index', type=int, nargs='?', help='Desired tab index')
  create_tab_parser.add_argument('--command', type=str, nargs='?', help='Command')
  create_tab_parser.set_defaults(func=create_tab)

  split_pane_parser = subparsers.add_parser("split-pane", help="Split a pane into two")
  split_pane_parser.add_argument('session', type=str, help='Session ID')
  split_pane_parser.add_argument('--vertical', action='store_true', help='Split vertically?', default=False)
  split_pane_parser.add_argument('--before', action='store_true', help='Spilt left or above target', default=False)
  split_pane_parser.add_argument('--profile', type=str, nargs='?', help='Profile name')
  split_pane_parser.set_defaults(func=split_pane)

  get_buffer_parser = subparsers.add_parser("get-buffer", help="Get screen contents")
  get_buffer_parser.add_argument("session", type=str, help="Session ID")
  get_buffer_parser.set_defaults(func=get_buffer)

  get_prompt_parser = subparsers.add_parser("get-prompt", help="Get info about prompt, if available. Gives either the current prompt or the last prompt if a command is being run. Requires shell integration for prompt detection.")
  get_prompt_parser.add_argument("session", type=str, help="Session ID")
  get_prompt_parser.set_defaults(func=get_prompt)

  get_profile_property_parser = subparsers.add_parser("get-profile-property", help="Get a session's profile settings")
  get_profile_property_parser.add_argument("session", type=str, help="Session ID")
  get_profile_property_parser.add_argument("keys", type=str, nargs='?', help="Comma separated keys. Omit to get all. Valid keys are: " + ", ".join(profile_properties()))
  get_profile_property_parser.set_defaults(func=get_profile_property)

  set_profile_parser = subparsers.add_parser("set-profile-property", help="Set a session's profile setting")
  set_profile_parser.add_argument("session", type=str, help="Session ID")
  set_profile_parser.add_argument("key", type=str, help="Key to set. Valid keys are: " + ", ".join(profile_properties()))
  set_profile_parser.add_argument("value", type=str, help="New value.")
  set_profile_parser.set_defaults(func=set_profile_property)

  read_parser = subparsers.add_parser("read", help="Wait for a input.")
  read_parser.add_argument("session", type=str, help="Session ID")
  read_parser.add_argument("mode", type=str, help="What to read", choices=[ "char", "line" ])
  read_parser.set_defaults(func=read)

  get_window_property_parser = subparsers.add_parser("get-window-property", help="Get a property of a window")
  get_window_property_parser.add_argument("id", type=str, help="Window ID")
  get_window_property_parser.add_argument("name", type=str, help="Property name", choices=["frame", "fullscreen"])
  get_window_property_parser.set_defaults(func=get_window_property)

  set_window_property_parser = subparsers.add_parser("set-window-property", help="Set a property of a window")
  set_window_property_parser.add_argument("id", type=str, help="Window ID")
  set_window_property_parser.add_argument("name", type=str, help="Property name", choices=["frame", "fullscreen"])
  set_window_property_parser.add_argument("value", type=str, help="New value. For frame: x,y,width,height; for fullscreen: true or false")
  set_window_property_parser.set_defaults(func=set_window_property)

  inject_parser = subparsers.add_parser("inject", help="Inject a string as though it were program output")
  inject_parser.add_argument("session", type=str, help="Session ID")
  inject_parser.add_argument("data", type=str, help="Data to inject")
  inject_parser.set_defaults(func=inject)

  activate_parser = subparsers.add_parser("activate", help="Activate a session, tab, or window.")
  activate_parser.add_argument("mode", type=str, help="What kind of object to activate", choices=["session", "tab", "window"])
  activate_parser.add_argument("id", type=str, help="ID of object to activate")
  activate_parser.set_defaults(func=activate)

  activate_app_parser = subparsers.add_parser("activate-app", help="Activate the app")
  activate_app_parser.add_argument('--raise_all_windows', action='store_true', help='Raise all windows?', default=False)
  activate_app_parser.add_argument('--ignoring_other_apps', action='store_true', help='Activate ignoring other apps (may steal focus)', default=False)
  activate_app_parser.set_defaults(func=activate_app)

  set_variable_parser = subparsers.add_parser("set-variable", help="Set a user-defined variable in a session. See Badges documentation for details.")
  set_variable_parser.add_argument("--session", type=str, nargs='?', help="Session ID")
  set_variable_parser.add_argument("--tab", type=str, nargs='?', help="Tab ID")
  set_variable_parser.add_argument("name", type=str, help="Variable name. Starts with \"user.\"")
  set_variable_parser.add_argument("value", type=str, help="New value")
  set_variable_parser.set_defaults(func=set_variable)

  get_variable_parser = subparsers.add_parser("get-variable", help="Get a variable in a session. See Badges documentation for details.")
  get_variable_parser.add_argument("--session", type=str, nargs='?', help="Session ID")
  get_variable_parser.add_argument("--tab", type=str, nargs='?', help="Tab ID")
  get_variable_parser.add_argument("name", type=str, help="Variable name. Starts with \"user.\"")
  get_variable_parser.set_defaults(func=get_variable)

  list_variables_parser = subparsers.add_parser("list-variables", help="Lists variable names available in a session.")
  list_variables_parser.add_argument("--session", type=str, nargs='?', help="Session ID")
  list_variables_parser.add_argument("--tab", type=str, nargs='?', help="Tab ID")
  list_variables_parser.set_defaults(func=list_variables)

  saved_arrangement_parser = subparsers.add_parser("saved-arrangement", help="Saves and restores window arrangements")
  saved_arrangement_parser.add_argument("action", type=str, help="Action to perform", choices=["save", "restore"])
  saved_arrangement_parser.add_argument("name", type=str, help="Arrangement name")
  saved_arrangement_parser.add_argument('--window', type=str, nargs='?', help='Window ID to save/restore to')
  saved_arrangement_parser.set_defaults(func=saved_arrangement)

  show_focus_parser = subparsers.add_parser("show-focus", help="Show active windows, tabs, and panes")
  show_focus_parser.set_defaults(func=show_focus)

  list_profiles_parser = subparsers.add_parser("list-profiles", help="List profiles")
  list_profiles_parser.add_argument("--guids", type=str, nargs='?', help="Comma-delimited list of profiles to list. Omit to get all of them.")
  list_profiles_parser.add_argument("--properties", type=str, nargs='?', help="Comma-delimited list of properties to request. Omit to get all of them.")
  list_profiles_parser.set_defaults(func=list_profiles)

  set_grid_size_parser = subparsers.add_parser("set-grid-size", help="Set size of session")
  set_grid_size_parser.add_argument("session", type=str, help="Session ID")
  set_grid_size_parser.add_argument("width", type=int, help="Width in columns")
  set_grid_size_parser.add_argument("height", type=int, help="Height in rows")
  set_grid_size_parser.set_defaults(func=set_grid_size)

  list_tmux_connections_parser = subparsers.add_parser("list-tmux-connections", help="List tmux integration connections")
  list_tmux_connections_parser.set_defaults(func=list_tmux_connections)

  send_tmux_command_parser = subparsers.add_parser("send-tmux-command", help="Send a tmux command to a tmux integration connection")
  send_tmux_command_parser.add_argument("connection_id", type=str, help="tmux connection ID")
  send_tmux_command_parser.add_argument("command", type=str, help="Command to send")
  send_tmux_command_parser.set_defaults(func=send_tmux_command)

  set_tmux_window_visible_parser = subparsers.add_parser("set-tmux-window-visible", help="Show or hide a tmux integration window (represented as a tab in iTerm2)")
  set_tmux_window_visible_parser.add_argument("connection_id", type=str, help="tmux connection ID")
  set_tmux_window_visible_parser.add_argument("window_id", type=str, help="tmux window ID (number)")
  set_tmux_window_visible_parser.add_argument('--visible', dest='visible', action='store_true')
  set_tmux_window_visible_parser.add_argument('--no-visible', dest='visible', action='store_false')
  set_tmux_window_visible_parser.set_defaults(visible=True)
  set_tmux_window_visible_parser.set_defaults(func=set_tmux_window_visible)

  sort_tabs_parser = subparsers.add_parser("sort-tabs", help="Sort tabs alphabetically by name")
  sort_tabs_parser.set_defaults(func=sort_tabs)

  list_color_presets_parser = subparsers.add_parser("list-color-presets", help="Lists names of color presets")
  list_color_presets_parser.set_defaults(func=list_color_presets)

  set_color_preset_parser = subparsers.add_parser("set-color-preset", help="Lists names of color presets")
  set_color_preset_parser.add_argument("profile", type=str, help="Profile name")
  set_color_preset_parser.add_argument("preset", type=str, help="Color preset name")
  set_color_preset_parser.set_defaults(func=set_color_preset)

  monitor_variable_parser = subparsers.add_parser("monitor-variable", help="Monitor changes to a variable")
  monitor_variable_parser.add_argument("name", type=str, help="variable name")
  monitor_variable_parser.add_argument('--session', type=str, nargs='?', help='Session ID for the variable scope')
  monitor_variable_parser.add_argument('--tab', type=str, nargs='?', help='Tab ID for the variable scope')
  monitor_variable_parser.add_argument('--window', type=str, nargs='?', help='Window ID for the variable scope')
  monitor_variable_parser.add_argument('--app', action='store_true', help='App scope', default=False)
  monitor_variable_parser.set_defaults(func=monitor_variable)

  monitor_focus_parser = subparsers.add_parser("monitor-focus", help="Monitor changes to focus")
  monitor_focus_parser.set_defaults(func=monitor_focus)

  set_cursor_color_parser = subparsers.add_parser("set-cursor-color", help="Set cursor color")
  set_cursor_color_parser.add_argument("session", type=str, help="Session ID")
  set_cursor_color_parser.add_argument("color", type=str, help="Color as red,green,blue where each value is in 0-255")
  set_cursor_color_parser.set_defaults(func=set_cursor_color)

  monitor_screen_parser = subparsers.add_parser("monitor-screen", help="Monitor screen contents")
  monitor_screen_parser.add_argument("session", type=str, help="Session ID")
  monitor_screen_parser.add_argument("query", type=str, help="Stop when this text is seen")
  monitor_screen_parser.set_defaults(func=monitor_screen)

  show_selection_parser = subparsers.add_parser("show-selection", help="Shows the selected text in a session")
  show_selection_parser.add_argument("session", type=str, help="Session ID")
  show_selection_parser.set_defaults(func=show_selection)

  return parser

def main(argv):
  logging.basicConfig()

  parser = make_parser()
  args = parser.parse_args(argv[1:])
  if "func" not in args:
    print(parser.format_help())
    raise argparse.ArgumentTypeError('Missing command')

  async def wrapper(connection):
    try:
      await args.func(connection, args)
    except Exception as e:
      print(traceback.format_exc())

  iterm2.run_until_complete(wrapper)

if __name__ == "__main__":
  try:
    main(sys.argv)
  except Exception as e:
    print(traceback.format_exc())
    sys.exit(1)
