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>
This commit is contained in:
1 parent
8a377af116
commit
db78b1f2e6
9 files changed
+195
-13
No files matched your search
@@ -344,6 +344,7 @@ class Player:
|
||||
PAGE = r"""<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Gyre - Sand Art Table</title>
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<style>
|
||||
:root{--bg:#f4f1ea;--card:#fffdf8;--ink:#2a2622;--mute:#6f675d;--line:#e2dbcf;--acc:#2f6f5e;--acc-ink:#fff;--stop:#c0392b;--bar:#e9e2d4}
|
||||
@media (prefers-color-scheme:dark){:root{--bg:#18191b;--card:#232528;--ink:#ece8e1;--mute:#a59e94;--line:#36393d;--acc:#5fb49c;--acc-ink:#0d1f1a;--bar:#2e3135}}
|
||||
@@ -431,6 +432,7 @@ a{color:var(--acc)}
|
||||
<footer>Click a design to draw it. Each one starts with an erase spiral (turn off "erase first" to draw over the sand as it is).
|
||||
Make your own at <a href="https://sandify.org" target="_blank" rel="noopener">sandify.org</a> (export as Theta Rho .thr), or upload a line drawing as .svg: every line in it is joined into one path for the ball.
|
||||
· <a href="/jog">Jog & calibrate</a>
|
||||
<span id="quit" style="display:none"> · <a href="#" onclick="return quitApp()">Quit</a></span>
|
||||
<span id="power" style="display:none"> · <a href="#" onclick="return powerAct('reboot')">Restart</a> · <a href="#" onclick="return powerAct('poweroff')">Power off</a></span></footer>
|
||||
</main>
|
||||
<script>
|
||||
@@ -506,6 +508,7 @@ async function tick(){
|
||||
else if(m.connected && !p.message && $('err').textContent===lastConnErr) showErr('');
|
||||
lastConnErr=m.connected?'':m.error;
|
||||
$('power').style.display = ST.power_buttons ? 'inline' : 'none';
|
||||
$('quit').style.display = ST.can_quit ? 'inline' : 'none';
|
||||
if(!settingsLoaded){$('speed').value=p.settings.speed;$('speedVal').textContent=p.settings.speed+' mm/s';$('erase').checked=p.settings.erase;settingsLoaded=true;}
|
||||
if(p.message) showErr(p.message);
|
||||
const now=p.now;
|
||||
@@ -521,12 +524,17 @@ async function tick(){
|
||||
markPlaying();
|
||||
}catch(e){$('mach').textContent='offline: is `python3 -m sandtable web` still running?';}
|
||||
}
|
||||
function quitApp(){
|
||||
if(!confirm('Close Gyre? Any design stops and the motors switch off.')) return false;
|
||||
post('quit').then(j=>{ if(j.ok){ clearInterval(TICK); showErr(j.msg); $('mach').textContent='closed'; } });
|
||||
return false;
|
||||
}
|
||||
function powerAct(what){
|
||||
const q = what==='poweroff' ? 'Power off the table\'s computer? Unplug it only after the green light on the Pi stops blinking.' : 'Restart the table\'s computer? The page comes back in about a minute.';
|
||||
if(confirm(q)) post(what).then(j=>{ if(j.ok) showErr(j.msg); });
|
||||
return false;
|
||||
}
|
||||
loadLib(); tick(); setInterval(tick,1000);
|
||||
loadLib(); tick(); const TICK=setInterval(tick,1000);
|
||||
</script></body></html>
|
||||
"""
|
||||
|
||||
@@ -582,6 +590,9 @@ def serve(ctrl, cfg, host='127.0.0.1', port=8765, open_browser=True, open_path='
|
||||
q = parse_qs(u.query)
|
||||
if u.path in ('/', '/index.html'):
|
||||
self._send(200, PAGE, 'text/html; charset=utf-8')
|
||||
elif u.path == '/favicon.svg':
|
||||
with open(os.path.join(os.path.dirname(__file__), 'static', 'gyre.svg'), 'rb') as f:
|
||||
self._send(200, f.read(), 'image/svg+xml')
|
||||
elif u.path == '/jog':
|
||||
self._send(200, jog_page, 'text/html; charset=utf-8')
|
||||
elif u.path == '/api/status':
|
||||
@@ -590,7 +601,8 @@ def serve(ctrl, cfg, host='127.0.0.1', port=8765, open_browser=True, open_path='
|
||||
self._send(200, json.dumps({'machine': st, 'player': player.status(),
|
||||
'state': st['state'], 'theta': st['theta'], 'rho': st['rho'],
|
||||
'switch': st['switch'], 'pins': st['pins'], 'homed': st['homed'], 'steps': st['steps'],
|
||||
'error': st['error'], 'power_buttons': POWER_BUTTONS}))
|
||||
'error': st['error'], 'power_buttons': POWER_BUTTONS,
|
||||
'can_quit': not POWER_BUTTONS}))
|
||||
elif u.path == '/api/library':
|
||||
self._send(200, json.dumps(lib.items()))
|
||||
elif u.path == '/thumb':
|
||||
@@ -642,6 +654,20 @@ def serve(ctrl, cfg, host='127.0.0.1', port=8765, open_browser=True, open_path='
|
||||
lib.remove(body.get('id', '')); ok, msg = True, 'removed'
|
||||
elif act in ('poweroff', 'reboot'):
|
||||
ok, msg = power(act)
|
||||
elif act == 'quit':
|
||||
if POWER_BUTTONS or self.client_address[0] not in ('127.0.0.1', '::1'):
|
||||
ok, msg = False, 'Quit only works on the computer Gyre runs on'
|
||||
else:
|
||||
log('quit from the page or the Gyre icon')
|
||||
player.stop()
|
||||
try:
|
||||
if panel.connected:
|
||||
with panel.lock:
|
||||
ctrl.motors_off()
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=srv.shutdown, daemon=True).start()
|
||||
ok, msg = True, 'Gyre is closed. Start it again from the Gyre icon.'
|
||||
elif act in ('jog', 'home', 'unlock', 'calibrate'):
|
||||
ok, msg = busy() or {
|
||||
'jog': lambda: panel.jog(body.get('axis'), body.get('d', 0)),
|
||||
|
||||
Reference in new issue
Block a user