- 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>
113 lines
4.3 KiB
Python
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
|