"""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