Files
sandTable/software/sandtable/launcher.py
T
ckochandClaude Opus 5.5 db78b1f2e6 Start Gyre from an icon: launch / quit / install-desktop
- python3 -m sandtable install-desktop puts a Gyre icon in the app grid and on
  the desktop (freedesktop .desktop entry with a "Quit Gyre" action).
- The icon runs `launch`: starts the web page in the background (no terminal,
  own session, log in .cache/server.log) if it isn't running, then opens it.
  A dialog shows the reason if it can't start.
- Quit link on the page (local computer only) and `quit`: stop any design,
  motors off, close the page.
- Gyre icon (sandtable/static/gyre.svg), also the page's tab icon.
- Simulator takes several lines per tick, so streaming tests aren't slowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:25:10 -05:00

113 lines
4.3 KiB
Python

"""Start Gyre from a desktop icon: run the web page in the background (no terminal window) and
open it in the browser. Used by the Gyre icon (`install-desktop` puts it in the app grid and on
the desktop):
python3 -m sandtable launch start if needed, then open the page
python3 -m sandtable quit stop any design, motors off, close Gyre
"""
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.request
import webbrowser
SOFTWARE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ICON = os.path.join(SOFTWARE, 'sandtable', 'static', 'gyre.svg')
LOG = os.path.join(SOFTWARE, '.cache', 'server.log')
def status(port):
"""The running Gyre page's status, or None if nothing (or something else) answers."""
try:
with urllib.request.urlopen(f'http://127.0.0.1:{port}/api/status', timeout=1.5) as r:
d = json.load(r)
return d if 'machine' in d else None
except Exception:
return None
def say(title, msg, error=False):
"""Tell the person at the desktop: a dialog if possible, else a notification."""
print(f'{title}: {msg}', flush=True)
if shutil.which('zenity'):
subprocess.run(['zenity', '--error' if error else '--info', '--title', title, '--width', '420',
'--text', msg], stderr=subprocess.DEVNULL)
elif shutil.which('notify-send'):
subprocess.run(['notify-send', '-a', 'Gyre', title, msg])
def launch(port=8765):
if status(port) is None:
os.makedirs(os.path.dirname(LOG), exist_ok=True)
log = open(LOG, 'a')
log.write(time.strftime('\n=== %Y-%m-%d %H:%M:%S starting from the Gyre icon\n'))
log.flush()
subprocess.Popen([sys.executable, '-m', 'sandtable', 'web', '--no-browser', '--http-port', str(port)],
cwd=SOFTWARE, stdout=log, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,
start_new_session=True) # keeps running after this launcher exits
t_end = time.time() + 25
while time.time() < t_end and status(port) is None:
time.sleep(0.4)
if status(port) is None:
with open(LOG) as f:
tail = ''.join(f.readlines()[-12:])
say('Gyre could not start', f'The page did not come up. The last lines of its log:\n\n{tail}', error=True)
return 1
webbrowser.open(f'http://127.0.0.1:{port}/')
return 0
def quit_gyre(port=8765):
try:
req = urllib.request.Request(f'http://127.0.0.1:{port}/api/quit', data=b'{}', method='POST',
headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=40) as r:
ok = json.load(r).get('ok')
except Exception:
ok = False
if not ok:
say('Gyre', 'Gyre is not running.')
return 0
def install_desktop():
"""Put a Gyre icon in the app grid and on the desktop (Linux desktops that follow the
freedesktop.org standard: GNOME, KDE, Cinnamon, Xfce...)."""
entry = f"""[Desktop Entry]
Type=Application
Name=Gyre
GenericName=Sand art table
Comment=Draw designs on the Gyre sand table
Exec={sys.executable} -m sandtable launch
Path={SOFTWARE}
Icon={ICON}
Terminal=false
Categories=Utility;
StartupNotify=false
Actions=quit;
[Desktop Action quit]
Name=Quit Gyre
Exec={sys.executable} -m sandtable quit
"""
apps = os.path.expanduser('~/.local/share/applications')
os.makedirs(apps, exist_ok=True)
places = [os.path.join(apps, 'gyre.desktop')]
desktop = subprocess.run(['xdg-user-dir', 'DESKTOP'], capture_output=True, text=True).stdout.strip() \
if shutil.which('xdg-user-dir') else os.path.expanduser('~/Desktop')
if desktop and os.path.isdir(desktop) and desktop != os.path.expanduser('~'):
places.append(os.path.join(desktop, 'Gyre.desktop'))
for p in places:
with open(p, 'w') as f:
f.write(entry)
os.chmod(p, 0o755)
if shutil.which('gio'): # GNOME: allow launching from the desktop
subprocess.run(['gio', 'set', p, 'metadata::trusted', 'true'], stderr=subprocess.DEVNULL)
if shutil.which('update-desktop-database'):
subprocess.run(['update-desktop-database', apps], stderr=subprocess.DEVNULL)
for p in places:
print(' wrote', p)
return places