A Planet Position Widget
A Planet Position Widget
August 12, 2026 at 12:16 PM by Drang
After discovering how to utilize TerminalWidget for creating Mac widgets and leveraging Astropy to calculate the coordinates of celestial bodies, I decided to merge these two tools. The result is a custom widget designed to track the positions of the planets.
🌌 Scope and Definitions
For this project, I have adopted an outdated ancient definition of "planets." This means the list includes the Sun and the Moon, but excludes any celestial bodies located beyond Saturn.
| Body | Included? |
|---|---|
| Sun | ✅ |
| Moon | ✅ |
| Mercury | ✅ |
| Venus | ✅ |
| Mars | ✅ |
| Jupiter | ✅ |
| Saturn | ✅ |
| Uranus/Neptune | ❌ |
The widget displays two primary values: Azimuth and Altitude, rounded to the nearest degree.
Note: In one specific screenshot taken shortly after 10:00 PM, every object was listed as being below the horizon, rendering a trip outside unnecessary.
While most people don't need a widget to find the Sun, and other bodies are generally invisible during the day, I included the Sun for two reasons:
- To ensure the data set was complete.
- Because the visibility of other planets often depends on their angular separation from the Sun.
🛠️ The Technical Implementation
The core of the widget is a Python script titled planets.
The Logic Flow
The Source Code
import astropy.units as u
from astropy.time import Time
from astropy.coordinates import get_body, AltAz, EarthLocation
from subprocess import run
def direction(az):
'Return a string indication of the azimuth (given in degrees).'
dirs = 'N NNE NE ENE E ESE SE SSE S SSW SW WSW W WNW NW NNW'.split()
i = int(((az + 11.25) % 360) / 22.5)
return dirs[i]
# Current time in UTC.
# (Time definition omitted for brevity in original snippet)
home = EarthLocation(lat=41.81433*u.deg, lon=-88.07093*u.deg, height=208*u.m)
# Bodies of interest.
planets = 'Moon Sun Mercury Venus Mars Jupiter Saturn'.split()
# Current positions of all the bodies.
pos = {}
const = {}
for p in planets:
pos[p] = get_body(p, ut).transform_to(AltAz(obstime=ut, location=home))
const[p] = pos[p].get_constellation()
# Assemble the results.
output = []
for p in planets:
output.append(f'{p: 8s}: {pos[p].az.value:3.0f} {direction(pos[p].az.value):3s} {pos[p].alt.value:3.0f} {const[p]}')
# Pipe the results through TerminalWidget.
tw = '/Applications/TerminalWidget.app/Contents/MacOS/TerminalWidget --target planets --font Menlo --bg eeeeee --fg 000000 --text -'.split()
run(tw, input='\n'.join(output).encode())
# print('\n'.join(output))
Breakdown of the Script:
- Compass Conversion: The
directionfunction converts a numerical azimuth (e.g., ) into a human-readable compass point (e.g., "Southwest"). I wrote this manually because it's more intuitive than raw degrees. - The Math: The function utilizes a 16-point compass. Since a circle is , each point covers:
The formula
((az + 11.25) % 360) / 22.5accounts for the fact that North is centered at (ranging from to ). - Location: The coordinates in the code above point to the Morton Arboretum, though my personal version uses my home's specific latitude and longitude.
- Data Retrieval:
get_bodyfetches the position, which is then transformed intoAltAzcoordinates and used byget_constellationto identify the current constellation. - Output: The final strings are passed to the
TerminalWidgetexecutable viasubprocess.run. - Debugging:
Line 40is a print statement I used during development to verify output in the Terminal before piping it to the widget.
⏱️ Automation via launchd
To keep the data fresh, the script runs every 30 minutes. This is handled by a launchd agent named com.leancrew.planets.plist.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.leancrew.planets</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/python</string>
<string>/path/to/planets</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Minute</key>
<integer>30</integer>
</dict>
</array>
</dict>
</plist>
Key Configuration Details:
- Executable: The first argument is the full path to Python (removing the need for a shebang line in the script).
- Script: The second argument is the path to the
planetsscript. - Schedule: The
StartCalendarIntervalensures execution at:00and:30of every hour.
🌑 Final Thoughts: The Eclipse
Recently, the Chicago area experienced a partial solar eclipse where only 1% of the Sun was obscured. Unfortunately, 100% of the Sun was obscured by clouds, making it impossible to see the event visually. However, my widget confirmed that the eclipse was indeed occurring—just safely tucked away above the cloud layer.
Visuals
The widget in action.
The 16-point compass logic.
Tracking the solar eclipse.