Exploring the iTerm2 Python API

Exploring the iTerm2 Python API

As a big surprise I found a new version of the iTerm2 macOS terminal replacement waiting for me when I returned from my summer holiday. The release packed a number of big features, like a minimal UI theme, a magic global status bar and finally a native Python SDK.

The UI looks a lot more modern now. Bezels and borders tuned down and an experience that is even more content centric. I mean, tell me this doesn't look quite good!?

The rainbow colored line is the status bar that displays the current working directory, git status, process info and finally CPU + mem usage. You can tweak it with a number of ready-to-go components, this is just the stuff I decided to throw in there to start with. You can also build your own components by utilising the new API.

iTerm2 top area

Anyway, enough free advertisment of iTerm2, lets get to briefly exploring that API.

Automate your workflows

If you do much of your work in a terminal, like me, you likely have some steps to take to start working on a project. I like to close everything up after work as I'm a one-laptop type of person and thus use it for different purposes after work. So every morning I go through a number of steps to get started: Open a terminal with some splits and run different processes in each. Open a few tabs for different services that I work on in conjunction. Its not that the steps are long, but from time to time I forget to start one service for example.

Lets see how we can automate these steps quite succinctly. I'll use what I believe is a common scenario as an example:

  • Split a tab vertically and run an API on one side and a frontend on the other side
  • Open one tab in the API directory and one in the frontend directory
  • Run git status in both to give me a glimpse of any unfinished work
  • When the frontend is ready, open my preferred working browser on the correct page

I also run some databases on a docker host with random ports and my real life script polls docker for the management ports to open that interface in a browser as well, but for brevity I'll skip those steps. There is a docker-py package that is fairly straight forward to use for this scenario.

iTerm2 has a tutorial you should check out as well, and you might want to use the built-in functionality to generate the script file, which will place it inside the Application Data for iTerm2 and also make it accessible through the iTerm2 menu. I prefer to keep my scripts close to home and version controlled there, so I opted to install the required dependencies from pip and just create it myself. If you do so as well, first install the dependencies using pip install iterm2 asyncio

I'll leave you with the code with comments as its fairly clear and understandable:

start-work.py

Download as file
#!/usr/local/bin/python3

import iterm2
import asyncio
import webbrowser

apiDir = '~/work/api'
frontendDir = '~/work/frontend'

# A helper to wait for some output in a terminal to match a string
async def output_matches(session, match):
  async with session.get_screen_streamer() as streamer:
    ready = False
    while ready != True:
      content = await streamer.async_get()
      for i in range(content.number_of_lines):
        line = content.line(i)
        if match in line.string:
          return

async def main(connection):
  # Grab the App instance which is a container to get iTerm2 components from
  app = await iterm2.async_get_app(connection)

  # Get the current terminal window: There can be multiple windows ofc
  window = app.current_terminal_window


  # Create two splits: Left for API, right for frontend
  # You can go grazy with splits here if you need more
  left = window.current_tab.current_session
  right = await left.async_split_pane(vertical=True)

  # Also set a manual title to make it clear what this tab is doing
  await window.current_tab.async_set_title("Running: API + frontend")

  # Start API by sending text sequences as if we typed them in the terminal
  await left.async_send_text(f"cd {apiDir}\n")
  await left.async_send_text("yarn dev\n")

  # Start frontend by sending text sequences as if we typed them in the terminal
  await right.async_send_text(f"cd {frontendDir}\n")
  await right.async_send_text("yarn dev\n")

  # Create tab for frontend; I have 1 tab for each service to run vim in
  frontend = await window.async_create_tab()
  await frontend.async_select()
  await frontend.current_session.async_send_text(f"cd {frontendDir}\n")
  await frontend.current_session.async_send_text("git st\n")

  # Create tab for api
  api = await window.async_create_tab()
  await api.async_select()
  await api.current_session.async_send_text(f"cd {apiDir}\n")
  await api.current_session.async_send_text("git st\n")

  # Wait until frontend process prints a line matching the second argument here
  await output_matches(right, 'Compiled successfully')

  # Start firefox with the page open
  webbrowser.get('firefox').open("http://localhost:8080")
  # Here i open db management tabs as well, based on that docker lookup logic

# And finally we have to tell iterm2 to run this script until it finishes
iterm2.run_until_complete(main)

In order to run this you can stuff the file anywhere and just run it (python start-work.py) or you can store it where iTerm2 wants you to and run it through the menu.

Next steps

I always have an open terminal so I just run the script by hand at the start of the day. However, you can also register script functions with iTerm2 itself and actually add a button to that new status bar that will call the function. For a number of scripts that might be the right approach to take.

Explore the full API docs to get a grasp of what you can do with this toolbelt. One neat thing I can imagine is being able to configure an output matcher on a session that has a running script. You might have started this long running job you can't really stop midway and its producing a lot of garbage that you now want to filter but its too late to pipe it to a logfile via grep. You could write a function that does this with arguments for what to grep that will actually connect to the session and filter the text output as iTerm2 renders it.

I'm just happy that I could whip this up fairly quickly and save myself a few key strokes every day. Happy hacking!