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:
ckochandClaude Opus 5.5 committed 2026-11-01 21:25:10 -05:00
1 parent 8a377af116
commit db78b1f2e6
9 files changed
+195 -13

No files matched your search

+2 -1
View File
@@ -195,7 +195,8 @@ class FakeGrbl:
real_dt = now - self.t_last
dt = real_dt * self.speedup
self.t_last = now
self._take_line()
for _ in range(16): # take several queued lines per tick, as fast as GRBL
self._take_line()
if self.sleeping:
self.state = 'Sleep'
continue
+20
View File
@@ -225,6 +225,26 @@ class TestReconnect(unittest.TestCase):
fake.close()
class TestQuit(unittest.TestCase):
def test_quit_closes_the_page_and_rests_the_motors(self):
with tempfile.TemporaryDirectory() as tmp:
lib = make_library(tmp)
fake, c = make_controller(100)
s = socket.socket(); s.bind(('127.0.0.1', 0)); port = s.getsockname()[1]; s.close()
th = threading.Thread(target=serve, args=(c, CFG), kwargs={'port': port, 'open_browser': False, 'library': lib},
daemon=True)
th.start()
from sandtable import launcher
self.assertTrue(wait_for(lambda: launcher.status(port) is not None, 10))
self.assertTrue(launcher.status(port)['can_quit'])
launcher.quit_gyre(port)
th.join(10)
self.assertFalse(th.is_alive()) # the page has closed
self.assertIsNone(launcher.status(port))
self.assertTrue(fake.sleeping) # motors off
fake.close()
class TestHttp(unittest.TestCase):
@classmethod
def setUpClass(cls):