Automatically Running Project Startup Commands in iTerm2
Getting your development environment set up with a single command in iTerm2 so you can start working on your project quickly.
This post is part of a series of useful commands I've built for myself as part of my day-to-day workflow. I hope you find them useful!
Ok, the workday has started. Time to jump into a project. Let's work on Blip. Fire up Alfred, punch in term blip
and we've jumped to the project directory in the terminal.
Let's get our environment set up. We need to:
Open the project in VS Code
Start compiling the assets
Start Laravel's local scheduler
Start Laravel's local queue listener
Start listening for Stripe webhooks using the Stripe CLI
Open up Ray and HELO (side note: if you are not using Ray and HELO I cannot recommend enough, game changers for local development )
Phew. I decided years ago that a) I don't want to type all of these commands every time and b) I still wanted them to run as if I had actually typed them. Thus cs
(custom start) was born.
iTerm2 Python Integration
Thankfully, iTerm2 comes with a pretty solid Python API. This allows us to control iTerm2 via a Python script, which is exactly what we're looking to do.
Writing the Command
Let's get into it. As a refresher, in case you haven't read the previous post, we're using Laravel Zero and the app name is util
. So let's scaffold a command:
1php util make:command CustomStartRun
The idea here is that we are going to run a command (cs
) and the script will:
Check for the existence of a
custom-start.py
script in the directoryIf it finds one, it runs it
If it doesn't find one, it offers to create it
The command itself is going to be very simple (honestly, we probably don't even need Laravel Zero for it but it's my comfort zone so here we are). Just like in the last post, we're going to add custom-start.py
to our global git ignore so we don't have to ignore it repo-by-repo.
Here's the command in its entirety:
1<?php 2 3namespace App\Commands; 4 5use LaravelZero\Framework\Commands\Command; 6 7class CustomStartRun extends Command 8{ 9 /**10 * The signature of the command.11 *12 * @var string13 */14 protected $signature = 'custom-start:run';15 16 /**17 * The description of the command.18 *19 * @var string20 */21 protected $description = 'Run a custom start script for the project';22 23 /**24 * Execute the console command.25 *26 * @return mixed27 */28 public function handle()29 {30 $dir = exec('pwd');31 $path = $dir . '/custom-start.py';32 33 if (!$this->createScriptFile($path)) {34 return;35 }36 37 exec('./custom-start.py');38 }39 40 protected function createScriptFile($path): bool41 {42 if (file_exists($path)) {43 return true;44 }45 46 $this->error("Script missing: {$path}");47 48 if (!$this->confirm('Create script file?', true)) {49 return false;50 }51 52 file_put_contents(53 $path,54 file_get_contents(base_path('templates/custom-start.py'))55 );56 57 exec("chmod +x {$path}");58 59 $this->info("File created: {$path}");60 61 // Still return false, we just created the file and we don't want to execute it right away62 return false;63 }64}
Looks good! Let's see what the Python script looks like:
Note: I write in a bunch of different languages, but Python is generally not one of them. So this might be terrible Python code, but it's worked for me for years so 🤷♂️. Open to suggestions.
1#!/usr/bin/env python3 2 3import asyncio 4import iterm2 5 6async def run_command_in_new_tab(window, title, command, closeWhenDone=False, newLine=True): 7 tab = await window.async_create_tab() 8 await tab.async_activate() 9 10 if (title):11 await tab.async_set_title(title)12 13 append = '\n' if newLine is True else ''14 final_command = ' && '.join(command) if isinstance(15 command, list) else command16 17 await tab.current_session.async_send_text(final_command + append)18 19 if closeWhenDone:20 # Give the command a moment to execute before closing the tab21 await asyncio.sleep(2)22 await tab.async_close()23 24async def main(connection):25 app = await iterm2.async_get_app(connection)26 27 # Foreground the app28 await app.async_activate()29 30 window = app.current_terminal_window31 32 await window.current_tab.current_session.async_send_text('code .\n')33 # await run_command_in_new_tab(window, 'Vite', 'yarn dev')34 # await run_command_in_new_tab(window, 'Cron', 'php artisan schedule:work')35 # await run_command_in_new_tab(window, 'Queue', 'php artisan queue:listen')36 # await run_command_in_new_tab(window, 'Open Apps', 'open -a Ray.app', True)37 # await run_command_in_new_tab(window, 'Stripe Listener', 'stripe listen --forward-to DOMAIN/stripe/webhook')38 39# Passing True for the second parameter means keep trying to40# connect until the app launches.41iterm2.run_until_complete(main, True)
I've preloaded it with some commented-out commands that I generally run for projects.
Some nice features here:
Each tab that is opened has a title so it's very clear what is running.
If it runs a command (such as opening Ray and HELO) that doesn't need to keep a tab open afterward, we can specify that the tab should close after the command is run.
The script simulates actually typing into the terminal, so if you need to restart a process, just switch over to that tab and restart it. The command is right there in iTerm2 already.
Finally, let's alias the command as cs
in my .zshrc
:
1helperPHPPath="/opt/homebrew/Cellar/php/8.1.10_1/bin/php"2alias cs="$helperPHPPath ~/Dev/util-zero/util custom-start:run"
The Result
That's it! Super simple. For Blip, these are the commands I run to set up the dev environment:
1await window.current_tab.current_session.async_send_text('code .\n')2await window.current_tab.current_session.async_send_text('open https://blip.test\n')3await run_command_in_new_tab(window, 'Vite', 'yarn dev')4await run_command_in_new_tab(window, 'Cron', 'php artisan schedule:work')5await run_command_in_new_tab(window, 'Queue', 'php artisan queue:listen')6await run_command_in_new_tab(window, 'Stripe Listener', 'stripe listen --forward-to https://blip.test/stripe/webhook')7await run_command_in_new_tab(window, 'Open Apps', 'open -a Ray.app && open -a HELO.app', True)
Let's see it in action:
Have any ideas for how I could improve this? Does this spark any ideas of your own? I'm all ears! Let me know.
Thanks for following along! Stay tuned for more helpful commands as I continue the series.