a31fa75d559ecbd7dcab7bf3e38bb5917b7016fd
24
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a31fa75d55 |
fix: the bar keeps its order when an application opens
On the laptop the browser jumped behind the disc player the moment it was
opened. 0.2.4 caused that, and the cause is two lines of the stock applet:
main.qml:153 hideActivatedLaunchers: tasks.iconsOnly || ...
main.qml:155 launchInPlace: tasks.iconsOnly && sortingStrategy === 1
For an icons-only bar the first is always true — a launcher is hidden the
instant its application starts — and the second is what puts the running
window back where the launcher was. It only does that at sortingStrategy
1. 0.2.4 set it to 0 to stop teachers dragging the icons about, so the
launcher vanished and the window was appended after the others.
Reproduced in the lab with a real Chrome window before changing anything,
and the bar read off the screen: Wonder Ink, the player, then the
browser. With 1 it is Wonder Ink, the browser, the player, through
opening, a second window, another application opening beside it, and
every close.
The launcher identity was suspected and cleared: only one browser button
appears, because google-chrome.desktop already carries Chrome's own file
name and StartupWMClass for exactly this reason.
THE COST, AND IT IS REAL. Manual sorting is also the only mode in which
MouseHandler.qml accepts a reordering drop, so a teacher who knows to drag
can rearrange the bar again. There is no value that gives both and no
Kiosk key that separates them. classroom-desktop-reset already rebuilds
the bar from the package at every login, so a drag never outlives the
session. docs/testing.md states the trade rather than burying it, and the
configuration escape is written down there too as still open.
tools/lab/order-check is the test this needed and did not have. 0.2.4
passed a full set of configuration assertions while the bar did something
else, so this one opens real application windows and reads the panel
strip as pixels — there is no image library in the lab, so it captures
PPM and names each icon by colour. Six checks, all of them behaviour.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
52301084a5 |
feat: an external-only display mode, chosen rather than inferred
The women's Bible study laptop is a fixed appliance: AC power, battery out, lid shut, one television on HDMI. Its own panel should be off. DISPLAY_MODE=external-only says so, and it is a setting rather than something inferred from having a lid, because another laptop may want its own screen or both. Nothing reads DEVICE_TYPE to decide it. The built-in panel is identified by kscreen's output TYPE — 7 is Panel — rather than by connector name. Matching "eDP-1" would be matching one machine's spelling: eDP, LVDS and DSI are all internal, numbered differently per driver and renamed between kernel versions. The names are kept only as a fallback for a driver that reports no type, which some virtual devices do. One rule overrides the setting: never leave the machine with no screen. A laptop asked for external-only whose television is off keeps its own panel and says so in the journal. A machine nobody can see is one nobody can fix standing in front of it, and it corrects itself the next time this runs with the set on. Only the outputs that are not already right are touched, so a television power cycle is not a stream of mode sets. Applied at login from an autostart entry named to sort before the scale one, so the scale lands on the displays this leaves enabled. The lid itself needed no new code — logind ignores every lid event, PowerDevil's lidAction is 0 in all three profiles with triggerLidActionWhenExternalMonitorPresent=false, the profiles are immutable and the classroom account cannot reach suspend through polkit. That was all already true for towers. What was missing was a test saying so, and there is one now, because it is the property the laptop depends on and nothing would have noticed it regressing. KNOWN LIMITATION, and it is written into docs/testing.md rather than left to be discovered: the mode is applied at login, not continuously. If KWin re-enables the internal panel when the television is power cycled, it stays enabled until the next login. The lab has no lid and no HDMI hotplug, so this cannot be settled here; the physical procedure says to check it specifically, because the answer decides whether the follow-up needs an event-driven applier like the audio policy's. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
7f4a122900 |
feat: one setting per application, and never the shape of the machine
A laptop is going in for the women's Bible study, and the obvious way to
build that — a laptop profile that turns off the children's church
applications — is the wrong way. A laptop may well take children's church,
and a tower may well be a Bible study. Hardware cannot be allowed to
decide purpose, so every optional application gets a setting of its own:
WONDERINK_ENABLED already existed; behaviour unchanged
DVD_ENABLED new
PARENT_CALL_ENABLED new
and DEVICE_TYPE says only what the hardware IS. classroom-desktop-layout
never reads it, and there is a test that says so, because this is the rule
that will be under pressure the first time somebody adds a device.
All three default to true, so an existing classroom that has never heard
of any of them comes up exactly as it did before. The settings are
appended to device.conf by the same mechanism every other setting has
used, so an upgrade gains them without losing a byte an administrator
wrote.
The disc player is the one place hardware still votes, and it is a veto:
DVD_ENABLED can take the player away from a machine that has a drive,
nothing can put it on a machine that has none. A button that cannot work
is worse than no button.
The layout's launcher markers are replaced by a feature block of three
plain booleans. The old mechanism commented a line out of an array, which
worked for launchers and could not express Parent Call, because that is a
whole widget rather than one entry. Three booleans express both, and the
launcher list is now built from them in one fixed order, so a disabled
application closes the row up rather than leaving a gap, and the same
enabled set produces the same bar on every machine in the church.
Also here, because it is the bar and not a feature: sortingStrategy=0.
Plasma's default is 1, "manually", and the task manager accepts a
reordering drop only in that mode — MouseHandler.qml tests
tasksModel.sortMode === SortManual before it will move anything. The
classroom never wrote the key, so it had the default, and any teacher
could drag the launchers into a different order. This is the applet's own
lever refusing the drop rather than something of ours putting the bar back
afterwards.
The test now runs the layout the package actually ships, in node, with
Plasma's API stubbed, and asserts the launcher string that comes out.
Asserting against a copy of the file's text in the test was how the old
one worked, and a copy is a thing that drifts.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
27cd64a668 |
fix: the room asks for its output again, not only once at boot
River Kids came up silent on stable 0.2.2. The decision this policy makes
was right and was never asked for.
classroom-audio applied its policy once at startup and then only when a
PipeWire event arrived. pactl talks to PipeWire's PulseAudio server, and
the unit was ordered after pipewire and wireplumber but not after that.
At boot it therefore raced the server and lost: the single startup apply
failed with "Connection refused", the subscription attached cleanly a few
seconds later, and from that moment the daemon waited for an event that
had already happened. systemd said active, the subscription was live, and
the policy had never run at all.
That is why the card was found on output:analog-stereo+input:analog-stereo
with its HDMI port available. That profile is WirePlumber's own pick, not
one this policy would ever produce, which is the evidence it never ran.
Reproduced in the lab against real PipeWire 1.4.2 and WirePlumber 0.5.8 by
starting the daemon with pipewire-pulse stopped and then starting it: the
old daemon sits idle forever, the new one converges.
The shape is now: wait for the server, apply, subscribe, apply again the
moment the subscription attaches, and coalesce each burst of events into
one decision. Applying on attach is the fix for the race. While the room
has not got the output it would prefer it also looks again on a backing
off timer capped at a minute, so a television switched on hours later is
picked up whether or not ALSA's jack change ever reaches us as an event.
Once the room has what it wants it stops looking entirely, and a machine
with no digital output never looks at all.
Also here, all of it found by measuring the real server rather than
reading the code:
* sink lookup matched on fields a sink does not have. Two of its three
conditions could never be true; the one that worked returned whichever
sink of the card came first rather than the one for the chosen port.
* volume and mute are no longer touched. The old code unmuted and forced
70% on every output change, so every television power cycle reset a
volume the teacher had set and could unmute an output muted on
purpose. WirePlumber already remembers volume per device. A machine
this policy has never configured still gets 70% once.
* the profile chosen keeps the card's input, which is the difference
between output:hdmi-stereo and the output:hdmi-stereo+input:analog-stereo
an administrator reaches for by hand.
* a card profile is only set when it is not already in place, and only
so many times, so a sink that never appears cannot become a switching
loop.
* a machine with no sound server at all says so once and backs off,
rather than filling the journal. pactl subscribe's own stderr is
dropped because saying it is this daemon's job, and a subscription is
only treated as healthy once it has lasted, since pactl starts
successfully and then fails to connect.
* the fixtures described a machine nobody has. Real pactl puts no
"direction" on a port at all and says "availability unknown" rather
than nothing, so a test could not have caught this.
Existing streams are moved explicitly. Measured both ways on the real
stack: changing the default sink does move a stream that never asked for
a particular sink, but a stream pinned to a device stays where it is with
no policy running, and this moves it back without the application being
restarted.
tests/test-classroom-audio-daemon.sh drives the real daemon through the
boot race, a television switched on with and without an event announcing
it, a power cycle, a set that never comes on, and a room already correct.
It uses a pactl mock because no loopback or dummy sound driver has an HDMI
port, so the one thing a lab cannot prove is that the real jack change
arrives; docs/testing.md carries the physical procedure for that and says
not to take a passing build as evidence of it.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
de5025ad22 |
feat: give Wonder Ink tabs, and let a device say it has no Wonder Ink
Teachers prepare a lesson across several tabs — the lesson, its video,
the activity — and app mode is the one thing that cannot give them that.
Chrome cannot be asked for "tabs but no omnibox". App mode has no tabs; a
normal window has an address bar and no policy, flag or extension removes
it; tabbed web apps need a manifest on a site that is not ours and push
off-origin links out into a normal browser window, which is the whole of
what this design exists to prevent. So the tab strip is ours, and the
engine is Chromium either way.
QtWebEngine, from Debian, measured before it was chosen rather than
after. The usual and correct objection is codecs: on Debian 13 it reports
H.264, AAC, MP3, VP8 and VP9 all playable, MediaSource supports avc1,
mp4a and vp9, and a 720p H.264 file decoded and played to the end in the
lab. app.wonderink.org loads, and its sign-in is an ordinary email and
password form rather than a Google flow that would mind an embedded
browser. The user agent has the QtWebEngine token stripped so a site
gating on it sees the Chromium this actually is, and Widevine is handed
over from Chrome's own bundle when the machine has Chrome, which a
classroom always does.
[ Lesson ] [ Video ] [ Activity ]
The strip appears only above one tab, so an ordinary lesson looks exactly
like the application it replaces. A tab can only come from the lesson —
target=_blank, window.open, a middle click, a Ctrl+click — and there is
no address bar, no search and no button that makes a blank tab. No
allowlist of domains either, on purpose: a list would have to be right
about every video service a lesson might ever embed.
Three bugs the lab caught and this fixes. A lesson calling window.print()
did nothing at all, which is worse than no print button because a teacher
concludes the printer is broken; printRequested is now bound to the page
that asked. Leaving fullscreen left the window 200 pixels wide in the
corner — a window that has only ever been maximised has no ordinary
geometry to return to — so the fullscreen bit is cleared rather than
maximised being re-requested. And the Present view video failure written
up in docs/wonder-ink.md is acted on at last: that investigation named
the autoplay gesture chain as the likeliest cause and declined to fix it,
because in Chrome the only lever governed the ordinary browser as well.
Here it does not — this profile is Wonder Ink and nothing else — so a
lesson may start its own video and no other page in the room may.
Reproduced both ways in the lab with an unmuted video started by a script
with no gesture: NotAllowedError at Chrome's default, playing as shipped.
That is not proof Present view is fixed; the lab has no Wonder Ink
account, and candidates 2 and 3 in that document are still open.
Measured on a rebooted machine, from the package: tabs open from a link
and from window.open, switch, and close; closing one leaves the others;
closing the last closes Wonder Ink; H.264 video plays and fills the
display in fullscreen and comes back maximised with its tabs; the print
dialog offers the room's own Elmer queue; a download lands in ~/Downloads;
back and forward work from a right-click menu that is built rather than
filtered; pressing the panel button twice raises one window rather than
starting a second on the same profile; KWin reports the window as
ekklesia-wonderink, which is one panel button and not two.
Separate from the browser as before, in its own profile, with cookies
that live only as long as the window — what the Chrome policy did for
wonderink.org, and the reason closing Wonder Ink ends a teacher's
session. Chrome's old profile directory is left untouched beside it, and
the browser's own policy is not edited by any of this.
Wonder Ink is now optional per device, through WONDERINK_ENABLED in
device.conf, because one future device is a laptop that will not have it.
It rides the mechanism the disc player already uses: one marked line in
the packaged layout, edited by classroom-desktop-layout. False takes the
launcher off the panel and makes the command refuse to start; the
browser, Parent Call and the disc player are untouched and the panel
stays balanced. Absent means yes, so every existing room is unchanged.
One scale, still. Chrome has to be handed UI_SCALE because it was
measured not to follow the compositor; Qt does follow it — devicePixelRatio
2.0 at UI_SCALE=2.0, measured — so Wonder Ink is deliberately not told,
or the room would get it twice.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
ba8768cee5 |
fix: a room that was set up, a card where its button is, and larger cards
Parent Call told rooms they had never been set up. Nothing about the rooms was wrong. Read from the church broker: …/coordinator and …/pending are retained and …/mode is absent, and the coordinator's own source says why — it publishes the retained mode only in answer to an operator writing to …/mode/set, and nobody ever has. Every classroom waited a second and a half for a message that will never exist. A missing mode is now read as the coordinator's own default rather than as an unconfigured room: the room offers the call and lets the sanctuary end answer, because that is the end that knows. A mode that really is off still refuses, in the coordinator's words, and a broker that is missing or unreachable still says which of those it is. Proved against a rig broker made to match the church's exactly, with a call landing on the sanctuary mock. The card is anchored to the bottom-left corner instead of centred, by layer shell, which is what the disc player's cover has used under KWin for months. Anchoring rather than positioning is what makes it right at 1080p and at 4K without a coordinate anywhere. It falls back to the centred card if the library is missing. Notifications are sized for a room. Every Plasma measurement is a grid unit and a grid unit is the height of the general font, so the font is the lever — 15 point instead of Breeze's 10, which takes the card from about 350x110 to about 410x145 at 100% and doubles again at 200%. Text, icon, padding and buttons move together. The chime stays off. No shell package: that path cost the panel and is not coming back. And the product name is gone from everything a teacher sees. The notification card is from Classroom, the disc player's from DVD. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
34fd02e08f |
feat: give every room the broker, one scale, and its own panel back
Little Waders took the Plasma build and reported what a container cannot. A disc player on a machine with no optical drive, a clock nobody at the back of the room could read, a browser at half the size of the panel around it, and Parent Call with no broker to call. The broker is the same for every classroom, so it is a default now and not something to fill in room by room. classroom-migrate brings an existing room to the same place without touching its name, its username or its password, and Parent Call distinguishes a broker it cannot reach from one that was never set up: those send somebody to look at completely different things. The disc player's detection was right and ran too late — alongside the session rather than before it, and the panel is built from the layout the moment a session starts. It runs before the display manager now, and again from udev when a drive is plugged in. UI_SCALE had a ceiling of 1.5, which cannot express a 4K television; a clock pinned at 15 points, which is the same size at every scale; and a browser that was never told the number at all. All three are fixed and the room doubles together at 200%. This also brings 0.1.47's work into the repository, where it had only ever existed in a build directory under /tmp: the hidden display dialog, its authenticated writer, the polkit action, and the church's own wallpaper. One piece of it is deliberately left out. The custom volume display needed a shell package of our own, and with one selected plasmashell builds no panel at all — measured twice, with that build's own files, on a classroom that deletes its panel configuration at every login by design. The lab connected to the real church broker the moment the package defaulted to it. Nothing was left behind, which was checked, but the guard that should have stopped it had never worked: it used iptables, which the lab image does not have, and probed with /dev/tcp under dash, where that always fails. Both reported success. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
13aac85b87 |
docs: record Parent Call, the DVD migration and what is left to test
Three things in this window that a container cannot answer, and they are now the top of the next-steps list: a real key pressed into Parent Call, a television switched on after the computer, and a disc that plays. Also brings the comments in the shared libraries up to date. The only mention of sway left in the tree is the upgrade step that removes the sway-era login shell from a room being upgraded. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
d2d30d6b0c |
feat: take the last of sway out of the classroom
The DVD player was the only thing still speaking a compositor the room no longer runs. It asked sway's tree whether VLC's window had really gone fullscreen, because VLC reports playing while its window is still the black startup one the loading cover exists to hide. VLC's own status was measured as a replacement and is not one: it reports fullscreen true in the same 250ms poll as playing, because the flag is the option it was started with and not a report about its window. So the cover is held for a floor and capped by the deadline that was already there, and the compositor is not asked anything. Focus-or-launch went the same way. The session already takes an exclusive lock, so a second one cannot start, and bringing an existing one to the front is the panel's job. classroom-window, classroom_sway and classroom_chrome are gone with it — the last of them was reachable from nothing at all. DVD also moves out of the status row, where it sat beside the clock as though a disc were a machine reading. It is an application: it is in the row with Wonder Ink and the browser, and classroom-provision removes it from the layout on a machine with no optical drive, which retires DVD_ENABLED. A computer that moves between rooms takes its drive with it; nobody remembers to edit a file when it does. The panel is rebuilt from the package at every login now. Plasma keeps whatever panel it first built, so an update could change the bar in the package and never change it in a room — measured, with an icon that outlived the file it came from. Editing the live panel configuration was tried instead and destroyed the panel, because the shell owns that file. Removing it before the shell starts is safe and makes the desktop stateless. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
7790003c03 |
feat: let a teacher ask the sanctuary to call a child's parent
The coordinator already existed on the control computer and its contract is frozen, so this is the classroom half of a contract that was read from its README and source rather than guessed: four fields out, the status topic subscribed before anything is published, and every word the teacher reads taken from the coordinator's own message field. It is a separate process, not a Plasma applet. A broker library, a network timeout and a text entry have no business inside the only way a teacher has of getting anywhere; if this dies the bar is untouched. The panel mark is Breeze's call-outgoing. The old one was a bell, sitting two inches from Plasma's notification bell in the same bar and meaning something entirely different. Two things it will not do: claim a call was displayed, and let a teacher type the room. The machine already knows which room it is. Sixty tests cover the contract without a broker. The live path was proven against the real coordinator running in the lab, on a loopback broker, with ProPresenter mocked and the church's networks rejected from inside the container first. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
d5cc6e463f |
fix: keep the DVD player whole
Removing gtk-layer-shell from the dependencies broke classroom-dvd on the first run, and deleting the dock's stylesheet took the DVD control bar's styling with it. Both go back: the template under a name that says what still uses it, and the dependency with a line in the package description explaining that it is there for the disc player and not for a dock. A probe overlay confirms gtk-layer-shell works under KWin, so the loading cover survives the move. The focus helper still speaks sway, which is written down rather than papered over. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
2ff9d47be9 |
feat: decide the classroom's audio output from what is plugged in
The room that prompted this had a television on HDMI with working PCM audio and nothing in the analog jack, and WirePlumber chose analog. A teacher who corrected it by hand then had to restart the browser before any sound came out. The policy is a pure function so it can be tested against that room rather than against a machine: an unavailable port is not a candidate, an analog port merely existing is not evidence of a cable, and an explicit AUDIO_OUTPUT overrides the lot. Twelve cases, the real room among them. The daemon follows PipeWire's own event stream rather than polling, and moves streams that are already playing, which is the part that stops a teacher having to close and reopen the browser. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
aadb1b8819 |
fix: keep the system print dialog in front of the window that rendered it
The render helper sat on top of the Print dialog, so the dialog could not be reached and nothing could be printed. Two causes. The daemon could not see window titles at all — the tree walk never collected them — and both windows arrive close enough together to be configured in the same pass, in whatever order the tree lists them. Configuring a window focuses it, so whichever was done last ended up in front, and that was not reliably the dialog. The dialog is now recognised by its exact title and configured last, and if a Chrome popup takes focus while it exists, focus is handed straight back. Focus is only ever reclaimed from another popup: clicking the main browser or the dock is the teacher's business and is left alone. A title match, narrowly, and only for this one known dialog. Creation order still decides which Chrome window is the browser; nothing else is classified by what it happens to be called. Nothing auto-closes the helper. After Print is cancelled it stays, and the sticky dock X still closes it. Proven by looking rather than by state. The render window is given a saturated background in the fixture so a screenshot answers "which window is in front" unambiguously: Print opens in front, the helper is then deliberately raised over it and Print comes back in front, cancelling Print leaves the helper visible with the dock X still offering it, and that X closes exactly the helper and leaves the main browser untouched. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f65c814eff |
fix: put the popup close button in the dock, and clear the printer location
Three things, none of them redesigns. The close affordance for a secondary Chrome window was a separate layer-shell surface anchored to that window's corner, repositioned by a five-per-second poll. Any position change tore the surface down and built another, so on real hardware it chased the window and flickered. Removed entirely: the GTK button process, the geometry maths that placed it, the --close-button mode that spawned it, and the poll loop that drove it. The window helper no longer imports GTK at all. The behaviour underneath it was right and is kept. The first Chrome top-level is the classroom browser, tiled and borderless; later ones float centred at about three quarters of the output. Creation order is the only thing that can tell them apart — confirmed on a real session, where every Chrome top-level shares app_id, shell, PID and a null transient_for, and sway exposes no window properties for Wayland toplevels at all. Titles are still not used. It now follows sway's own window events instead of polling, so dragging a popup costs nothing. Close moved into the dock, which was already on screen and does not move, so it cannot chase anything. It appears only while a secondary window is focused and runs kill on that one container. The helper publishes which window that is; the dock watches the file rather than polling. A stock sway title bar has no close button, so restoring the border would not have solved this either. Two bugs found by looking rather than by testing state. Chrome renegotiates the size of a popup opened with explicit dimensions after the resize returns, which left windows centred for a width they no longer had; placement now settles and repeats once, at creation only, so dragging is still free. And the dock's Close button came up empty because show_all does not recurse into a no-show-all widget. The printer's Location is now blank. CUPS prints it under the queue name in every dialog, so "Ekklesia Classroom" was a second line the teacher had to read past; the identity is one word. The already-configured check includes Location, so a machine set up before this is corrected rather than left as close enough. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
7f66228b9e | Release 0.1.37 deployment prep | ||
|
|
dfd42f6fdf |
feat: give the rooms a line to the church broker
The first piece of turning these machines into connected endpoints. classroom-mqtt is one small service per room that subscribes, validates what arrives, and hands the work to commands that already exist. Nothing in the room depends on it. A broker that is down, a network that is missing, a machine with no broker configured at all — each is a service sitting quietly, not a classroom that fails. Nothing waits for it at boot and the shipped default is off, so every existing classroom upgrades into this release with messaging disabled until a broker exists. The room is the hostname, which is already how these machines are named. Topics carry no product name because the broker is the church's and these are just rooms: classroom/notify/all, classroom/notify/<room>, and a retained classroom/status/<room> so anyone asking later can be told which rooms are up without waiting for them to say so again. Notifications are events and are never retained; status is state and is. Stale messages cannot come back, which is the property that matters most and has two halves. The session is clean, so a broker never hands over a backlog from while the room was off. And retained notifications are refused on arrival, because otherwise one would replay into every room at every reboot forever. The honest limit is written down: MQTT only flags a retained message when it is replayed to a new subscription, so a retained publish delivered live is indistinguishable from an ordinary one and is shown once — which is right, since it is live at that moment, and the replay is the actual hazard. Delivery reuses the session-aware sender rather than a second copy of it. classroom-notify-test grew into the real thing, so it is now classroom-notify with the sample sender a wrapper over it, and its 36 tests came along. The broker password lives in device.conf, so that file stops being world readable: 0640 root:classroom, which everything that reads it already satisfies. Transport logic is kept free of any broker library so it can be tested without one — 40 checks on room identity, the topic contract and which payloads become notifications. Separately verified against a real mosquitto: connect, subscribe, retained status, per-room and all-room delivery, urgency, duplicate and expired and malformed messages ignored, the broker killed with the daemon surviving, reconnect, and a stale retained notification left on the broker not reaching the screen when a room boots. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
a1a218b29f |
fix: draw classroom icons at the display's real pixel density
Icons went soft under UI_SCALE while the clock text beside them stayed sharp, which is visible on a 24" monitor even if it hides on a TV. new_from_file_at_scale rasterises an SVG at its *logical* size. The artwork is vector, but by the time sway scaled the session it was already a fixed raster, and the compositor enlarged that. Text never had the problem because GTK draws it at the real density. GTK 3 has no fractional scaling, so sway advertises the ceiling integer scale and scales the finished buffer down. Measured under sway: scale factor 1 at output scale 1.0, and 2 at 1.25, 1.5 and 2.0. So each icon is now rasterised at size x scale_factor physical pixels and handed to GTK as a cairo surface carrying that device scale. The icon lands at the real density; its logical size, and the whole dock layout, is unchanged. Icons redraw on notify::scale-factor as well as at realize, since the scale factor is not known until a widget is on a display and changes if UI_SCALE is changed under a running session. Browser, DVD, LIVE and all six DVD control icons go through it, in a shared module so the dock and the DVD session cannot diverge. The throbber is drawn rather than loaded and had the same flaw, so it draws on a device-scaled surface too rather than being the one soft thing left in a sharp dock. The artwork is untouched. Verified by rendering the dock under sway at 1.0, 1.25, 1.5 and 2.0 against the previous build: apparent icon size is identical at every scale (28, 35, 42, 56 px), 1.0 is unchanged pixel-for-pixel, and peak edge contrast on the Chrome icon goes from 60 to 99 at 1.25, 49 to 83 at 1.5, and 37 to 82 at 2.0 out of 255. The DVD control bar was checked the same way. Also records that Wonder Ink session-only cookies are confirmed working on the endpoint, which settles the question 0.1.34 shipped as unverified and tells us the session is in cookies rather than localStorage. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
d76e911612 |
feat: acknowledge a dock click that takes a moment
Clicking Browser and watching nothing happen for several seconds is the one place the classroom felt broken rather than quiet. The button now becomes a throbber the instant it is pressed, and stays one until the window actually appears in the sway tree. Only a real launch spins. The dock asks sway whether Chrome is already open first, so focusing or restoring an existing window stays immediate, which is how it should feel. DVD is left alone: it has its own full-screen loading surface and does not need two answers to one click. A launch that never arrives gives up after thirty seconds, puts the icon back and says so, so a broken start cannot spin all morning. The throbber is drawn rather than a GtkSpinner: the stock one is capped well below the size of the icon it replaces, so it read as a hesitation rather than as work in progress. Pre-rendered frames swapped into the same Gtk.Image keep the button an ordinary image with ordinary clicks, and it falls back to GtkSpinner if cairo is ever missing. The pointer also turns busy while it is over the dock. Wayland gives the cursor to whichever client the pointer is over and sway offers no way to set one globally, so this only holds on our own surface — which is exactly where the pointer is at the moment a dock button is pressed. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
8bed3d186a |
feat: scale the whole session, not just the classroom's own windows
UI_SCALE only ever reached the surfaces we draw ourselves. Chrome stayed tiny — its tabs, omnibox and menus are Chrome's own pixels, and Ctrl+ only scales page content, which is no use to a volunteer reading a toolbar from across the room. UI_SCALE is a property of the television, not of one program, so it now sets sway's output scale, which every client is measured against. classroom-scale runs first in the session and applies it. The display mode is untouched: the panel stays at 1920x1080 and only the logical coordinate space shrinks, which is what makes everything in it larger. Our own surfaces stop scaling themselves. They are measured in the same logical pixels sway is already scaling, so applying UI_SCALE again would double it. SHELL_SCALE is that decision in one named place, and setting it back to ui_scale(config) would restore per-surface scaling without touching a template. Measured at 1920x1080 with Chrome 152. Chrome's tab strip 40 -> 49 -> 59px against 1.0, 1.25, 1.5; sway's title bar 25 -> 31 -> 38px; the classroom dock 74 -> 92 -> 110px. Everything scales once, together, and 1.0 is identical to no scaling at all. Two tradeoffs, both measured rather than assumed. GTK 3 has no fractional scaling, so at 1.5 it renders at 2x and sway scales down — very slightly softer, invisible at classroom distance. XWayland is softer too: terminal text at 1.5 showed about 50% more intermediate-tone pixels than at 1.0. VLC is the only XWayland application here, and in DVD mode none of its interface is on screen, only video from a 720x480 disc that is being upscaled regardless. Chrome is a native Wayland client and stays sharp, which is what matters for reading. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
719b856040 |
refactor: share the sway helpers
classroom-dvd-session needs to ask sway whether VLC has taken the screen, which is the same tree walking classroom-window already does. One copy, in a module both import; classroom-window loses a third of its length. The state machine is worth protecting through a move like that, so its regression test comes with it: the exact swaymsg commands for launch, focus, minimise, restore, Super+M, Super+Shift+M, the scratchpad rescue and sway not running at all. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f8ddfbe345 |
feat: make DVD playback a classroom surface, not a media player
Pressing DVD now enters DVD mode rather than opening VLC. VLC is still the backend, because its dvdnav module is what decodes disc menus and nothing else on Debian does that reliably, but nothing a person can see or press belongs to it any more. classroom-dvd-session owns the whole thing. It puts a full-screen black layer-shell surface up the moment the button is pressed, starts VLC behind it, and drops it once playback is actually on screen — so the harsh cut from wallpaper to VLC's black window is gone, and no partial VLC window or reserved dock strip is ever visible. A deadline drops the surface regardless, because DVD mode must never leave a black screen with nothing behind it. The controls are a classroom bar on the overlay layer, the one layer sway draws above a fullscreen window. It slides up when the pointer reaches the bottom of the screen and hides again shortly after it leaves. It is drawn from the dock's own stylesheet at the room's UI scale, so it is the same surface, the same radius and the same white icons. While hidden it only accepts the pointer in a thin strip along the bottom edge, so a click on a disc menu goes to the disc. The bar reaches VLC through an HTTP interface on loopback behind a password generated per session, sending hotkey *actions* by name. That is what makes the next part possible. All 108 hotkeys VLC 3.0.23 defines are unset, then Space, Esc and the arrow keys plus Enter are bound. Crop, aspect ratio, subtitles, audio tracks and deinterlacing are no longer reachable by pressing a key at random, and the arrow keys no longer change VLC's volume — the television owns volume, and CEC will own it properly later. Esc quits instead of leaving fullscreen. There is nothing to leave, and leaving it was what stranded people on a black VLC window. Close, Eject and Esc all ask VLC to quit and escalate to SIGTERM then SIGKILL if it does not; when VLC exits by any route, including a crash, the session ends and the classroom desktop comes back. Verified against VLC 3.0.23 on Trixie: every argument accepted, every control action accepted over the control interface, a wrong password refused, and quit exiting cleanly. The surfaces were rendered under a headless sway and screenshotted to check the bar against the dock. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
3e69f3f05c |
refactor: share the stylesheet renderer
The DVD control bar has to look like the dock, which is best guaranteed by drawing it from the same template with the same measurements rather than by keeping two stylesheets in step by hand. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
428eb27a74 |
feat: scale the shell to the room's television
A 32" panel a few feet from the children and a 65" set across a hall want the same design at different sizes. UI_SCALE in device.conf now sets that, and nobody has to edit CSS or source to tune a room. The stylesheet and the notification config become templates carrying measurement tokens. classroom-dock resolves the stylesheet in memory at startup; classroom-notifications resolves the notification config into the session's runtime directory and execs mako on it, so the process is still just mako and everything that looks for it by name still finds it. Neither template is rewritten on disk, so an update cannot lose a room's setting and a room cannot drift from the packaged design. Dock height and the reserved strip, corner radii, padding, button size, spacing, every icon, the clock and its inset, and the notification geometry all follow the scale. Colours, translucency and the hairline borders do not: those are the design, not the size. At UI_SCALE=1.0 the rendered stylesheet is byte-identical to the one the shell used before any of this existed, which is the property worth keeping. Values outside 0.8 to 1.5 are clamped and anything that is not a number falls back to 1.0, so a typo in device.conf cannot produce an unusable classroom. Device configuration moves into a shared module, since the dock and the notification daemon now have to agree about what a setting means. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
0c1acfe5f5 |
feat: detect and play DVDs where a drive is fitted
DVD support stays optional per room: DVD_ENABLED decides whether the button is built at all, and rooms without it poll nothing. Upgrades append a commented DVD_ENABLED=false so the option is discoverable without changing what those rooms already do. The dock polls the drive with CDROM_DRIVE_STATUS, which is a status query rather than a read, so it does not spin the disc up. A disc that becomes ready announces itself once and brings the DVD logo up to full strength; an empty or unplugged drive holds it back, the same gray versus colour language the LIVE icon already uses. Nothing autoplays — playback always needs a deliberate press. Playback is VLC rather than mpv because mpv dropped DVD menu support, and menus are the part that matters for a commercial disc. It goes through classroom-window like everything else, so the player minimizes and restores from the dock exactly as the browser does. Pressing the button with no drive, no disc, or a disc still spinning up gives a plain-language notification instead of failing silently. Reading the drive needs group cdrom, which the postinst grants and the OTA reboot activates. Commercial CSS discs additionally need libdvdcss, which Debian cannot ship; classroom-dvd-css-setup bootstraps it from contrib. That is deliberately not run from the postinst, which executes while dpkg already holds the apt lock. The optical helpers live in a shared module both the dock and classroom-dvd import, and the dock imports it defensively: if it ever goes missing the shell loses disc detection rather than failing to start. Co-Authored-By: Claude Opus 5 <[email protected]> |