A Python interface for scripting Ghostty on macOS via AppleScript.
- macOS
- Python >= 3.13
- Ghostty >= 1.3.0 (when AppleScript support was introduced)
pip install ghosttpy
Or with uv:
uv add ghosttpy
from ghosttpy import Ghostty
g = Ghostty()
w = g.new_window(working_directory="/tmp")
term = w.selected_tab.focused_terminal
term.input("echo hello\n")Accessing w.name or term.working_directory queries Ghostty to read the current state.
Ghostty's scripting hierarchy is Ghostty > Window > Tab > Terminal. Each object has a stable id and can be compared or used in sets.
g = Ghostty()
g.windows # all open windows
g.front_window # the frontmost window
g.name # "Ghostty"
g.version # e.g. "1.3.1"
g.frontmost # True if Ghostty is the active app
g.is_running # True if the Ghostty process is running
w = g.front_window
w.tabs # tabs in this window
w.terminals # all terminals across all tabs
w.selected_tab # the active tab
tab = w.selected_tab
tab.index # 1-based position in the tab bar
tab.selected # True if this tab is active
tab.focused_terminal
tab.terminals # terminals (split panes) in this tab
term = tab.focused_terminal
term.name
term.working_directoryw = g.new_window()
tab = w.new_tab()
right = term.split("right") # returns the new Terminal
below = term.split("down")Split directions are "right", "left", "down", and "up" (or use the SplitDirection enum).
Resize or equalize splits:
term.resize_split("left", 10) # grow 10 cells to the left
term.resize_split("down") # grow 1 cell downward (default)
term.equalize_splits() # equalize all splits in the tab
tab.equalize_splits() # convenience — delegates to focused terminalClose any object with .close(). Bring a window to the front with w.activate(), select a tab with tab.select(), or focus a terminal with term.focus().
Windows, tabs, and splits accept keyword arguments to configure the terminal surface:
w = g.new_window(font_size=14.0, working_directory="/tmp")
tab = w.new_tab(command="/bin/bash", environment={"EDITOR": "vim"})
new_term = term.split("right", initial_input="ls\n", wait_after_command=True)Available options: font_size, working_directory, command, initial_input, wait_after_command, environment.
To reuse the same configuration across multiple calls, pass a Surface object:
from ghosttpy import Surface
cfg = Surface(font_size=14.0, working_directory="/tmp")
w1 = g.new_window(config=cfg)
w2 = g.new_window(config=cfg)input() sends text as if pasted. send_key() sends individual key events.
term.input("ls -la\n")
term.send_key("c", modifiers="control")
term.send_key("enter")
term.send_key("a", modifiers=["control", "shift"], action="release")perform() executes a Ghostty action string and returns whether it succeeded:
term.perform("copy_to_clipboard")
term.perform("reset")Mouse events are also available:
term.send_mouse_position(x=100.0, y=200.0)
term.send_mouse_button("left button")
term.send_mouse_scroll(x=0, y=-3.0, precision=True)Read or set a window's geometry:
from ghosttpy import Bounds, Point, Size
w.bounds # Bounds(left, top, right, bottom)
w.bounds = Bounds(0, 0, 960, 540)
w.position # Point(x, y)
w.size # Size(width, height)
w.move_to(100, 100)
w.resize_to(800, 600)Tile a window into a predefined screen region with ScreenRegion:
from ghosttpy import ScreenRegion
w.tile(ScreenRegion.left_half)
w.tile(ScreenRegion.top_right, gap=10)Available regions:
| Halves | Quarters | Thirds | Fourths | Sixths |
|---|---|---|---|---|
left_half |
top_left |
left_third |
first_fourth |
first_sixth |
right_half |
top_right |
center_third |
second_fourth |
second_sixth |
top_half |
bottom_left |
right_third |
third_fourth |
third_sixth |
bottom_half |
bottom_right |
left_two_thirds |
last_fourth |
fourth_sixth |
right_two_thirds |
left_three_fourths |
fifth_sixth |
||
right_three_fourths |
last_sixth |
There is also full, which is equivalent to maximize().
w.maximize()
w.center()
# Tile all windows in an auto-sized grid
g.tile_windows()
# Tile specific windows in a 3-column grid with gaps
g.tile_windows([w1, w2, w3], columns=3, gap=10)All layout methods accept an optional screen parameter. When omitted, they use the main display.
scr = g.main_screen() # Screen for the primary display
scr.origin # Point -- top-left corner of usable area
scr.size # Size -- usable width and height (excludes menu bar and Dock)
scr.bounds # Bounds -- computed from origin and size
all_screens = g.screens() # list[Screen] for every displayFor anything the library doesn't wrap, two escape hatches are available:
# Runs inside `tell application "Ghostty" ... end tell`
g.tell('get name of front window')
# Runs raw AppleScript with no wrapping
g.run('tell application "Finder" to get name of front window')Failed AppleScript calls raise GhosttyError, which includes the error message and an optional numeric code:
from ghosttpy import GhosttyError
try:
term.perform("nonexistent_action")
except GhosttyError as e:
print(e, e.code)String arguments like "right", "press", and "left button" are accepted everywhere, but typed enum alternatives are available if you prefer:
SplitDirection--right,left,down,upInputAction--press,releaseMouseButton--left,right,middleScrollMomentum--none,began,changed,ended,cancelled,may_begin,stationary
The examples/ directory contains a few demo programs:
| Script | Description |
|---|---|
tile_columns.py |
Tile open windows into equal columns |
dev_layout.py |
Create a development workspace with splits |
broadcast.py |
Broadcast input to multiple terminal panes |
window_info.py |
Print a tree of windows, tabs, and terminals |
Run any example with --help for options, e.g.:
python examples/tile_columns.py --columns 3 --gap 10