feat: give the classroom a desktop instead of a shell
The whole appliance UI is now configuration for a desktop Debian already ships: a Plasma 6.3 session with a colour scheme, a look and feel package that builds the bar, window rules, and KDE's own Kiosk restrictions. Two things were measured rather than assumed, and both changed a decision. Plasma restarts a crashed panel three times a minute and then gives up, which is the failure a classroom has already had. Four lines of systemd drop-in make it unconditional; the panel then survived eight SIGKILLs a second apart, back each time in 2.4s. Plasma draws a minimised task and a background task identically, so the five states the room needs cannot all be shown. Rather than write a task manager, the classroom removes minimise: clicking a launcher always means show me this. Co-Authored-By: Claude Opus 5 <[email protected]>
@@ -2,25 +2,20 @@
|
|||||||
"DefaultBrowserSettingEnabled": false,
|
"DefaultBrowserSettingEnabled": false,
|
||||||
"PromotionalTabsEnabled": false,
|
"PromotionalTabsEnabled": false,
|
||||||
"MetricsReportingEnabled": false,
|
"MetricsReportingEnabled": false,
|
||||||
|
|
||||||
"BrowserSignin": 0,
|
"BrowserSignin": 0,
|
||||||
"SyncDisabled": true,
|
"SyncDisabled": true,
|
||||||
"SigninInterceptionEnabled": false,
|
"SigninInterceptionEnabled": false,
|
||||||
|
|
||||||
"BrowserAddPersonEnabled": false,
|
"BrowserAddPersonEnabled": false,
|
||||||
"BrowserGuestModeEnabled": false,
|
"BrowserGuestModeEnabled": false,
|
||||||
"ProfilePickerOnStartupAvailability": 1,
|
"ProfilePickerOnStartupAvailability": 1,
|
||||||
|
|
||||||
"PasswordManagerEnabled": false,
|
"PasswordManagerEnabled": false,
|
||||||
"PasswordLeakDetectionEnabled": false,
|
"PasswordLeakDetectionEnabled": false,
|
||||||
"BackgroundModeEnabled": false,
|
"BackgroundModeEnabled": false,
|
||||||
"DisablePrintPreview": true,
|
"DisablePrintPreview": true,
|
||||||
|
|
||||||
"CookiesSessionOnlyForUrls": [
|
"CookiesSessionOnlyForUrls": [
|
||||||
"[*.]wonderink.org"
|
"[*.]wonderink.org"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ShowHomeButton": true,
|
"ShowHomeButton": true,
|
||||||
"HomepageIsNewTabPage": false,
|
"HomepageIsNewTabPage": false,
|
||||||
"HomepageLocation": "https://app.wonderink.org/"
|
"HomepageLocation": "https://www.youtube.com/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"RestoreOnStartup": 4,
|
"RestoreOnStartup": 4,
|
||||||
"RestoreOnStartupURLs": [
|
"RestoreOnStartupURLs": [
|
||||||
"https://app.wonderink.org/"
|
"https://www.youtube.com/"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Parent Call.
|
||||||
|
|
||||||
|
Deliberately not a colour tile. Parent Call is not one of the room's
|
||||||
|
applications and does not sit in the row with them, so it is drawn in
|
||||||
|
the interface's own accent as a line mark: present, reachable, and
|
||||||
|
visibly a different kind of thing from Wonder Ink and the browser. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||||
|
<!-- Inset so the mark has the same optical size as the filled tiles
|
||||||
|
beside it in the bar; an outline drawn to the edges reads larger. -->
|
||||||
|
<g transform="translate(64 64) scale(0.62) translate(-64 -64)"
|
||||||
|
fill="none" stroke="#4FB3A8" stroke-width="8"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M40 58a24 24 0 0 1 48 0c0 18 6 24 9 28H31c3-4 9-10 9-28z"/>
|
||||||
|
<path d="M55 94a9 9 0 0 0 18 0"/>
|
||||||
|
<path d="M64 26v-8"/>
|
||||||
|
<path d="M100 30a30 30 0 0 1 8 20"/>
|
||||||
|
<path d="M28 30a30 30 0 0 0-8 20"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 992 B |
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Wonder Ink.
|
||||||
|
|
||||||
|
A stand-in for the product's own mark, which the classroom does not
|
||||||
|
have a licensed asset for. It is a filled colour tile rather than a
|
||||||
|
line drawing on purpose: the room already learned, the hard way, that
|
||||||
|
a monochrome glyph on a bar is read as a decoration and not as an
|
||||||
|
application. Somebody asked whether the old browser glyph meant "the
|
||||||
|
Internet". A tile with its own colour does not get that question. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="tile" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#4FB3A8"/>
|
||||||
|
<stop offset="0.55" stop-color="#3E93A8"/>
|
||||||
|
<stop offset="1" stop-color="#7C6FC4"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="6" y="6" width="116" height="116" rx="28" fill="url(#tile)"/>
|
||||||
|
<path d="M64 30c0 0 26 28 26 44a26 26 0 0 1-52 0c0-16 26-44 26-44z"
|
||||||
|
fill="#FFFFFF" fill-opacity="0.94"/>
|
||||||
|
<path d="M50 76a14 14 0 0 0 10 13" stroke="#3E93A8" stroke-width="5"
|
||||||
|
stroke-linecap="round" fill="none" opacity="0.55"/>
|
||||||
|
<g fill="#FFFFFF" opacity="0.9">
|
||||||
|
<path d="M96 34l2.6 6.4L105 43l-6.4 2.6L96 52l-2.6-6.4L87 43l6.4-2.6z"/>
|
||||||
|
<circle cx="34" cy="40" r="3.2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
# The room's browser, installed to /usr/local/share/applications, which
|
||||||
|
# XDG_DATA_DIRS searches before /usr/share. Chrome's own entry is left
|
||||||
|
# where Chrome's package put it and is simply never the one that is found.
|
||||||
|
#
|
||||||
|
# It has to carry Chrome's file name rather than a name of our own. Plasma
|
||||||
|
# matches a window to its launcher by the application id the window
|
||||||
|
# reports, and Chrome reports "google-chrome". A launcher under any other
|
||||||
|
# name gives a teacher two buttons for one browser: a launcher that always
|
||||||
|
# looks closed, and a window that appears beside it. That was measured, not
|
||||||
|
# assumed — it is what the first build did.
|
||||||
|
Type=Application
|
||||||
|
Name=Browser
|
||||||
|
GenericName=Web browser
|
||||||
|
Comment=YouTube and the rest of the web
|
||||||
|
Icon=google-chrome
|
||||||
|
Exec=/usr/local/bin/classroom-browser %U
|
||||||
|
Terminal=false
|
||||||
|
Categories=Network;WebBrowser;
|
||||||
|
StartupNotify=true
|
||||||
|
StartupWMClass=google-chrome
|
||||||
|
MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https;
|
||||||
|
# Chrome's own entry offers a new window and an incognito window from the
|
||||||
|
# launcher's right-click menu. Neither belongs in a shared room.
|
||||||
|
Actions=
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DVD
|
||||||
|
Comment=Play the disc in the drive
|
||||||
|
Exec=/usr/local/bin/classroom-dvd
|
||||||
|
Icon=media-optical-dvd
|
||||||
|
Terminal=false
|
||||||
|
Categories=AudioVideo;Player;
|
||||||
|
StartupNotify=true
|
||||||
|
StartupWMClass=vlc
|
||||||
|
# Never in a menu or a launcher by itself. The DVD appears when a disc is
|
||||||
|
# in the drive, through Plasma's own device notifier, and goes away with it.
|
||||||
|
NoDisplay=true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Parent Call
|
||||||
|
Comment=Ask for a child's parent to come to the room
|
||||||
|
Exec=/usr/local/bin/classroom-parent-call
|
||||||
|
Icon=ekklesia-parent-call
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
|
StartupNotify=false
|
||||||
|
NoDisplay=false
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Wonder Ink
|
||||||
|
GenericName=Lesson
|
||||||
|
Comment=The Wonder Ink lesson for this room
|
||||||
|
Exec=/usr/local/bin/classroom-wonderink
|
||||||
|
Icon=ekklesia-wonderink
|
||||||
|
Terminal=false
|
||||||
|
Categories=Education;
|
||||||
|
StartupNotify=true
|
||||||
|
# How the panel knows that the window on screen belongs to this launcher.
|
||||||
|
# Chrome derives an app id from the --app URL, and this must match it
|
||||||
|
# exactly or the launcher and the window become two separate buttons.
|
||||||
|
StartupWMClass=chrome-app.wonderink.org__-Default
|
||||||
|
X-GNOME-SingleWindow=true
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Ekklesia Classroom — dark. The default.
|
||||||
|
#
|
||||||
|
# One set of tokens for the whole session: Plasma reads this file for the
|
||||||
|
# panel, the notifications, the window decorations and every Qt dialog, so
|
||||||
|
# there is one place to change a colour rather than a stylesheet per surface.
|
||||||
|
#
|
||||||
|
# The hues come from the classroom wallpaper — sage, teal, blue, lavender —
|
||||||
|
# so the desktop sits on the landscape rather than in front of it.
|
||||||
|
[General]
|
||||||
|
Name=Ekklesia Classroom Dark
|
||||||
|
ColorScheme=EkklesiaDark
|
||||||
|
shadeSortColumn=true
|
||||||
|
accentActiveTitlebar=false
|
||||||
|
|
||||||
|
[ColorEffects:Disabled]
|
||||||
|
ChangeSelectionColor=
|
||||||
|
Color=56,66,71
|
||||||
|
ColorAmount=0
|
||||||
|
ColorEffect=0
|
||||||
|
ContrastAmount=0.5
|
||||||
|
ContrastEffect=1
|
||||||
|
Enable=
|
||||||
|
IntensityAmount=0
|
||||||
|
IntensityEffect=0
|
||||||
|
|
||||||
|
[ColorEffects:Inactive]
|
||||||
|
ChangeSelectionColor=true
|
||||||
|
Color=112,111,110
|
||||||
|
ColorAmount=0.025
|
||||||
|
ColorEffect=2
|
||||||
|
ContrastAmount=0.1
|
||||||
|
ContrastEffect=2
|
||||||
|
Enable=false
|
||||||
|
IntensityAmount=0
|
||||||
|
IntensityEffect=0
|
||||||
|
|
||||||
|
[Colors:Button]
|
||||||
|
BackgroundAlternate=30,42,47
|
||||||
|
BackgroundNormal=25,35,40
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=79,179,168
|
||||||
|
ForegroundInactive=147,165,170
|
||||||
|
ForegroundLink=126,196,255
|
||||||
|
ForegroundNegative=227,106,106
|
||||||
|
ForegroundNeutral=224,177,92
|
||||||
|
ForegroundNormal=230,237,239
|
||||||
|
ForegroundPositive=111,207,151
|
||||||
|
ForegroundVisited=156,138,222
|
||||||
|
|
||||||
|
[Colors:Selection]
|
||||||
|
BackgroundAlternate=79,179,168
|
||||||
|
BackgroundNormal=79,179,168
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=14,21,24
|
||||||
|
ForegroundInactive=14,21,24
|
||||||
|
ForegroundLink=14,21,24
|
||||||
|
ForegroundNegative=14,21,24
|
||||||
|
ForegroundNeutral=14,21,24
|
||||||
|
ForegroundNormal=10,16,18
|
||||||
|
ForegroundPositive=14,21,24
|
||||||
|
ForegroundVisited=14,21,24
|
||||||
|
|
||||||
|
[Colors:Tooltip]
|
||||||
|
BackgroundAlternate=25,35,40
|
||||||
|
BackgroundNormal=20,28,32
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=79,179,168
|
||||||
|
ForegroundInactive=147,165,170
|
||||||
|
ForegroundLink=126,196,255
|
||||||
|
ForegroundNegative=227,106,106
|
||||||
|
ForegroundNeutral=224,177,92
|
||||||
|
ForegroundNormal=230,237,239
|
||||||
|
ForegroundPositive=111,207,151
|
||||||
|
ForegroundVisited=156,138,222
|
||||||
|
|
||||||
|
[Colors:View]
|
||||||
|
BackgroundAlternate=20,28,32
|
||||||
|
BackgroundNormal=15,23,26
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=79,179,168
|
||||||
|
ForegroundInactive=147,165,170
|
||||||
|
ForegroundLink=126,196,255
|
||||||
|
ForegroundNegative=227,106,106
|
||||||
|
ForegroundNeutral=224,177,92
|
||||||
|
ForegroundNormal=230,237,239
|
||||||
|
ForegroundPositive=111,207,151
|
||||||
|
ForegroundVisited=156,138,222
|
||||||
|
|
||||||
|
[Colors:Window]
|
||||||
|
BackgroundAlternate=25,35,40
|
||||||
|
BackgroundNormal=20,28,32
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=79,179,168
|
||||||
|
ForegroundInactive=147,165,170
|
||||||
|
ForegroundLink=126,196,255
|
||||||
|
ForegroundNegative=227,106,106
|
||||||
|
ForegroundNeutral=224,177,92
|
||||||
|
ForegroundNormal=230,237,239
|
||||||
|
ForegroundPositive=111,207,151
|
||||||
|
ForegroundVisited=156,138,222
|
||||||
|
|
||||||
|
[Colors:Complementary]
|
||||||
|
BackgroundAlternate=25,35,40
|
||||||
|
BackgroundNormal=20,28,32
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=79,179,168
|
||||||
|
ForegroundInactive=147,165,170
|
||||||
|
ForegroundLink=126,196,255
|
||||||
|
ForegroundNegative=227,106,106
|
||||||
|
ForegroundNeutral=224,177,92
|
||||||
|
ForegroundNormal=230,237,239
|
||||||
|
ForegroundPositive=111,207,151
|
||||||
|
ForegroundVisited=156,138,222
|
||||||
|
|
||||||
|
[Colors:Header]
|
||||||
|
BackgroundAlternate=25,35,40
|
||||||
|
BackgroundNormal=18,26,30
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=79,179,168
|
||||||
|
ForegroundInactive=147,165,170
|
||||||
|
ForegroundLink=126,196,255
|
||||||
|
ForegroundNegative=227,106,106
|
||||||
|
ForegroundNeutral=224,177,92
|
||||||
|
ForegroundNormal=230,237,239
|
||||||
|
ForegroundPositive=111,207,151
|
||||||
|
ForegroundVisited=156,138,222
|
||||||
|
|
||||||
|
[Colors:Header][Inactive]
|
||||||
|
BackgroundAlternate=20,28,32
|
||||||
|
BackgroundNormal=16,23,26
|
||||||
|
DecorationFocus=79,179,168
|
||||||
|
DecorationHover=79,179,168
|
||||||
|
ForegroundActive=79,179,168
|
||||||
|
ForegroundInactive=125,142,147
|
||||||
|
ForegroundLink=126,196,255
|
||||||
|
ForegroundNegative=227,106,106
|
||||||
|
ForegroundNeutral=224,177,92
|
||||||
|
ForegroundNormal=170,186,191
|
||||||
|
ForegroundPositive=111,207,151
|
||||||
|
ForegroundVisited=156,138,222
|
||||||
|
|
||||||
|
[WM]
|
||||||
|
activeBackground=18,26,30
|
||||||
|
activeBlend=79,179,168
|
||||||
|
activeForeground=230,237,239
|
||||||
|
inactiveBackground=16,23,26
|
||||||
|
inactiveBlend=147,165,170
|
||||||
|
inactiveForeground=147,165,170
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# Ekklesia Classroom — light.
|
||||||
|
#
|
||||||
|
# GENERATED from EkklesiaDark.colors by tools/build-light-scheme.
|
||||||
|
# Edit the dark scheme and regenerate; do not edit this file by hand.
|
||||||
|
#
|
||||||
|
# One set of tokens for the whole session: Plasma reads this file for the
|
||||||
|
# panel, the notifications, the window decorations and every Qt dialog, so
|
||||||
|
# there is one place to change a colour rather than a stylesheet per surface.
|
||||||
|
#
|
||||||
|
# The hues come from the classroom wallpaper — sage, teal, blue, lavender —
|
||||||
|
# so the desktop sits on the landscape rather than in front of it.
|
||||||
|
[General]
|
||||||
|
Name=Ekklesia Classroom Light
|
||||||
|
ColorScheme=EkklesiaLight
|
||||||
|
shadeSortColumn=true
|
||||||
|
accentActiveTitlebar=false
|
||||||
|
|
||||||
|
[ColorEffects:Disabled]
|
||||||
|
ChangeSelectionColor=
|
||||||
|
Color=56,66,71
|
||||||
|
ColorAmount=0
|
||||||
|
ColorEffect=0
|
||||||
|
ContrastAmount=0.5
|
||||||
|
ContrastEffect=1
|
||||||
|
Enable=
|
||||||
|
IntensityAmount=0
|
||||||
|
IntensityEffect=0
|
||||||
|
|
||||||
|
[ColorEffects:Inactive]
|
||||||
|
ChangeSelectionColor=true
|
||||||
|
Color=112,111,110
|
||||||
|
ColorAmount=0.025
|
||||||
|
ColorEffect=2
|
||||||
|
ContrastAmount=0.1
|
||||||
|
ContrastEffect=2
|
||||||
|
Enable=false
|
||||||
|
IntensityAmount=0
|
||||||
|
IntensityEffect=0
|
||||||
|
|
||||||
|
[Colors:Button]
|
||||||
|
BackgroundAlternate=225,231,230
|
||||||
|
BackgroundNormal=233,238,237
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=46,140,130
|
||||||
|
ForegroundInactive=96,116,122
|
||||||
|
ForegroundLink=26,110,180
|
||||||
|
ForegroundNegative=192,57,57
|
||||||
|
ForegroundNeutral=166,116,24
|
||||||
|
ForegroundNormal=22,35,42
|
||||||
|
ForegroundPositive=38,142,86
|
||||||
|
ForegroundVisited=104,80,180
|
||||||
|
|
||||||
|
[Colors:Selection]
|
||||||
|
BackgroundAlternate=46,140,130
|
||||||
|
BackgroundNormal=46,140,130
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=255,255,255
|
||||||
|
ForegroundInactive=255,255,255
|
||||||
|
ForegroundLink=255,255,255
|
||||||
|
ForegroundNegative=255,255,255
|
||||||
|
ForegroundNeutral=255,255,255
|
||||||
|
ForegroundNormal=255,255,255
|
||||||
|
ForegroundPositive=255,255,255
|
||||||
|
ForegroundVisited=255,255,255
|
||||||
|
|
||||||
|
[Colors:Tooltip]
|
||||||
|
BackgroundAlternate=233,238,237
|
||||||
|
BackgroundNormal=242,245,244
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=46,140,130
|
||||||
|
ForegroundInactive=96,116,122
|
||||||
|
ForegroundLink=26,110,180
|
||||||
|
ForegroundNegative=192,57,57
|
||||||
|
ForegroundNeutral=166,116,24
|
||||||
|
ForegroundNormal=22,35,42
|
||||||
|
ForegroundPositive=38,142,86
|
||||||
|
ForegroundVisited=104,80,180
|
||||||
|
|
||||||
|
[Colors:View]
|
||||||
|
BackgroundAlternate=242,245,244
|
||||||
|
BackgroundNormal=255,255,255
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=46,140,130
|
||||||
|
ForegroundInactive=96,116,122
|
||||||
|
ForegroundLink=26,110,180
|
||||||
|
ForegroundNegative=192,57,57
|
||||||
|
ForegroundNeutral=166,116,24
|
||||||
|
ForegroundNormal=22,35,42
|
||||||
|
ForegroundPositive=38,142,86
|
||||||
|
ForegroundVisited=104,80,180
|
||||||
|
|
||||||
|
[Colors:Window]
|
||||||
|
BackgroundAlternate=233,238,237
|
||||||
|
BackgroundNormal=242,245,244
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=46,140,130
|
||||||
|
ForegroundInactive=96,116,122
|
||||||
|
ForegroundLink=26,110,180
|
||||||
|
ForegroundNegative=192,57,57
|
||||||
|
ForegroundNeutral=166,116,24
|
||||||
|
ForegroundNormal=22,35,42
|
||||||
|
ForegroundPositive=38,142,86
|
||||||
|
ForegroundVisited=104,80,180
|
||||||
|
|
||||||
|
[Colors:Complementary]
|
||||||
|
BackgroundAlternate=233,238,237
|
||||||
|
BackgroundNormal=242,245,244
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=46,140,130
|
||||||
|
ForegroundInactive=96,116,122
|
||||||
|
ForegroundLink=26,110,180
|
||||||
|
ForegroundNegative=192,57,57
|
||||||
|
ForegroundNeutral=166,116,24
|
||||||
|
ForegroundNormal=22,35,42
|
||||||
|
ForegroundPositive=38,142,86
|
||||||
|
ForegroundVisited=104,80,180
|
||||||
|
|
||||||
|
[Colors:Header]
|
||||||
|
BackgroundAlternate=233,238,237
|
||||||
|
BackgroundNormal=236,241,240
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=46,140,130
|
||||||
|
ForegroundInactive=96,116,122
|
||||||
|
ForegroundLink=26,110,180
|
||||||
|
ForegroundNegative=192,57,57
|
||||||
|
ForegroundNeutral=166,116,24
|
||||||
|
ForegroundNormal=22,35,42
|
||||||
|
ForegroundPositive=38,142,86
|
||||||
|
ForegroundVisited=104,80,180
|
||||||
|
|
||||||
|
[Colors:Header][Inactive]
|
||||||
|
BackgroundAlternate=242,245,244
|
||||||
|
BackgroundNormal=228,234,233
|
||||||
|
DecorationFocus=46,140,130
|
||||||
|
DecorationHover=46,140,130
|
||||||
|
ForegroundActive=46,140,130
|
||||||
|
ForegroundInactive=120,138,144
|
||||||
|
ForegroundLink=26,110,180
|
||||||
|
ForegroundNegative=192,57,57
|
||||||
|
ForegroundNeutral=166,116,24
|
||||||
|
ForegroundNormal=80,99,105
|
||||||
|
ForegroundPositive=38,142,86
|
||||||
|
ForegroundVisited=104,80,180
|
||||||
|
|
||||||
|
[WM]
|
||||||
|
activeBackground=236,241,240
|
||||||
|
activeBlend=46,140,130
|
||||||
|
activeForeground=22,35,42
|
||||||
|
inactiveBackground=228,234,233
|
||||||
|
inactiveBlend=96,116,122
|
||||||
|
inactiveForeground=96,116,122
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Everything the classroom's appearance is made of, in one place.
|
||||||
|
#
|
||||||
|
# Plasma reads this when the look and feel is applied, so the panel, the
|
||||||
|
# window decorations, the notifications and every Qt dialog in the session
|
||||||
|
# are described here rather than in a stylesheet per surface.
|
||||||
|
|
||||||
|
[kdeglobals][KDE]
|
||||||
|
widgetStyle=Breeze
|
||||||
|
|
||||||
|
[kdeglobals][General]
|
||||||
|
ColorScheme=EkklesiaDark
|
||||||
|
AccentColor=79,179,168
|
||||||
|
|
||||||
|
[kdeglobals][Icons]
|
||||||
|
Theme=breeze-dark
|
||||||
|
|
||||||
|
[plasmarc][Theme]
|
||||||
|
name=default
|
||||||
|
|
||||||
|
[kcminputrc][Mouse]
|
||||||
|
cursorTheme=breeze_cursors
|
||||||
|
|
||||||
|
[kwinrc][org.kde.kdecoration2]
|
||||||
|
library=org.kde.breeze
|
||||||
|
theme=Breeze
|
||||||
|
|
||||||
|
[Wallpaper]
|
||||||
|
Image=/usr/share/backgrounds/ekklesia/classroom-calm.png
|
||||||
|
|
||||||
|
[KSplash]
|
||||||
|
Theme=None
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// The Ekklesia Classroom desktop.
|
||||||
|
//
|
||||||
|
// Plasma runs this once, when a session has no panel yet, and again on
|
||||||
|
// demand — `plasma-apply-lookandfeel -a org.ekklesia.classroom.desktop
|
||||||
|
// --resetLayout` rebuilds the whole desktop from this file. That is the
|
||||||
|
// classroom's recovery path for a damaged shell, and it is Plasma's own
|
||||||
|
// mechanism rather than something written here.
|
||||||
|
//
|
||||||
|
// One full-width bar along the bottom. Left is auxiliary, the middle is
|
||||||
|
// applications, the right is the small amount of status a teacher needs.
|
||||||
|
// Nothing else is on the screen.
|
||||||
|
|
||||||
|
var WALLPAPER = "/usr/share/backgrounds/ekklesia/classroom-calm.png";
|
||||||
|
|
||||||
|
// Matches the 76px strip the classroom has always used, so muscle memory
|
||||||
|
// and the room's viewing distance both carry over.
|
||||||
|
var PANEL_HEIGHT = 76;
|
||||||
|
|
||||||
|
// The idle desktop: the wallpaper, and nothing on it.
|
||||||
|
var screens = desktops();
|
||||||
|
for (var i = 0; i < screens.length; i++) {
|
||||||
|
var desktop = screens[i];
|
||||||
|
desktop.wallpaperPlugin = "org.kde.image";
|
||||||
|
desktop.currentConfigGroup = ["Wallpaper", "org.kde.image", "General"];
|
||||||
|
desktop.writeConfig("Image", "file://" + WALLPAPER);
|
||||||
|
desktop.writeConfig("SlidePaths", "");
|
||||||
|
// 2 is "scaled and cropped": the landscape fills the display at any
|
||||||
|
// aspect ratio without being stretched out of shape.
|
||||||
|
desktop.writeConfig("FillMode", 2);
|
||||||
|
desktop.reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
var panel = new Panel();
|
||||||
|
panel.location = "bottom";
|
||||||
|
panel.height = PANEL_HEIGHT;
|
||||||
|
panel.hiding = "none";
|
||||||
|
panel.floating = false;
|
||||||
|
panel.lengthMode = "fill";
|
||||||
|
panel.alignment = "center";
|
||||||
|
panel.opacity = "adaptive";
|
||||||
|
|
||||||
|
// LEFT — auxiliary. Parent Call is not one of the room's applications and
|
||||||
|
// is deliberately not in the row with them.
|
||||||
|
var parentCall = panel.addWidget("org.kde.plasma.icon");
|
||||||
|
parentCall.currentConfigGroup = ["General"];
|
||||||
|
parentCall.writeConfig(
|
||||||
|
"url",
|
||||||
|
"file:///usr/share/applications/ekklesia-parent-call.desktop"
|
||||||
|
);
|
||||||
|
|
||||||
|
var leftGap = panel.addWidget("org.kde.plasma.panelspacer");
|
||||||
|
leftGap.currentConfigGroup = ["General"];
|
||||||
|
leftGap.writeConfig("expanding", true);
|
||||||
|
|
||||||
|
// CENTRE — the applications, and the whole of the running/minimised/active
|
||||||
|
// story. This applet is the reason the classroom stops writing a dock.
|
||||||
|
var tasks = panel.addWidget("org.kde.plasma.icontasks");
|
||||||
|
tasks.currentConfigGroup = ["General"];
|
||||||
|
tasks.writeConfig("launchers", [
|
||||||
|
"applications:ekklesia-wonderink.desktop",
|
||||||
|
"applications:google-chrome.desktop"
|
||||||
|
].join(","));
|
||||||
|
// Every window gets its own button. Teachers should never have to open a
|
||||||
|
// group to find out that a second window exists.
|
||||||
|
tasks.writeConfig("groupingStrategy", 0);
|
||||||
|
tasks.writeConfig("showOnlyCurrentDesktop", false);
|
||||||
|
tasks.writeConfig("showOnlyCurrentActivity", false);
|
||||||
|
tasks.writeConfig("showOnlyCurrentScreen", false);
|
||||||
|
// Clicking a button always means "show me this", and never "hide it".
|
||||||
|
//
|
||||||
|
// This was measured before it was decided. Plasma 6.3 draws a minimised
|
||||||
|
// task and a task that is merely behind another window identically: no
|
||||||
|
// indicator for closed, a grey indicator for running, a teal one for
|
||||||
|
// active — three marks for four states. Rather than write a task manager
|
||||||
|
// that draws a fourth, the classroom removes the state. There is nothing
|
||||||
|
// on the idle desktop for a teacher to want to see, so minimising an
|
||||||
|
// application in this room has no purpose and only ever loses it.
|
||||||
|
//
|
||||||
|
// A window minimised some other way is still shown as running and comes
|
||||||
|
// back with one click, so it can never be lost even then.
|
||||||
|
tasks.writeConfig("minimizeActiveTaskOnClick", false);
|
||||||
|
tasks.writeConfig("fill", false);
|
||||||
|
tasks.writeConfig("maxStripes", 1);
|
||||||
|
tasks.writeConfig("indicateAudioStreams", false);
|
||||||
|
tasks.writeConfig("wheelEnabled", false);
|
||||||
|
tasks.writeConfig("middleClickAction", "NewInstance");
|
||||||
|
tasks.writeConfig("showToolTips", true);
|
||||||
|
tasks.writeConfig("iconSpacing", 2);
|
||||||
|
|
||||||
|
var rightGap = panel.addWidget("org.kde.plasma.panelspacer");
|
||||||
|
rightGap.currentConfigGroup = ["General"];
|
||||||
|
rightGap.writeConfig("expanding", true);
|
||||||
|
|
||||||
|
// RIGHT — status.
|
||||||
|
//
|
||||||
|
// Four ordinary applets straight on the bar, and no system tray around
|
||||||
|
// them. A tray is a container that discovers whatever is installed and
|
||||||
|
// then hides most of it behind an arrow; measured in the lab it collected
|
||||||
|
// battery, brightness, clipboard, keyboard layout, camera, vaults and
|
||||||
|
// screen management on a machine that has none of those to say anything
|
||||||
|
// about. Naming the four the classroom actually uses is both smaller and
|
||||||
|
// deterministic, and it leaves nothing for a teacher to expand.
|
||||||
|
panel.addWidget("org.kde.plasma.networkmanagement");
|
||||||
|
panel.addWidget("org.kde.plasma.volume");
|
||||||
|
|
||||||
|
// Present only while there is something in a drive. Plasma's own Solid
|
||||||
|
// device notifier is what makes the DVD appear with the disc and leave
|
||||||
|
// with it, so nothing in the classroom polls for media.
|
||||||
|
panel.addWidget("org.kde.plasma.devicenotifier");
|
||||||
|
|
||||||
|
panel.addWidget("org.kde.plasma.notifications");
|
||||||
|
|
||||||
|
var clock = panel.addWidget("org.kde.plasma.digitalclock");
|
||||||
|
clock.currentConfigGroup = ["Appearance"];
|
||||||
|
clock.writeConfig("showDate", true);
|
||||||
|
clock.writeConfig("dateFormat", "custom");
|
||||||
|
clock.writeConfig("customDateFormat", "ddd d MMM");
|
||||||
|
clock.writeConfig("showSeconds", 0);
|
||||||
|
clock.writeConfig("dateDisplayFormat", "BelowTime");
|
||||||
|
// Left to itself the clock grows to the height of the bar and becomes the
|
||||||
|
// largest text in the room. It is status, not a headline.
|
||||||
|
clock.writeConfig("autoFontAndSize", false);
|
||||||
|
clock.writeConfig("fontSize", 15);
|
||||||
|
clock.writeConfig("fontWeight", 500);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"KPackageStructure": "Plasma/LookAndFeel",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "Ekklesia"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Category": "",
|
||||||
|
"Description": "The classroom appliance desktop",
|
||||||
|
"Id": "org.ekklesia.classroom.desktop",
|
||||||
|
"License": "GPLv2+",
|
||||||
|
"Name": "Ekklesia Classroom",
|
||||||
|
"Website": "https://riverofglorymn.org"
|
||||||
|
},
|
||||||
|
"X-Plasma-APIVersion": "2"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Service
|
||||||
|
# Solid decides when this exists. Plasma's device notifier offers it while
|
||||||
|
# a video DVD is in the drive and withdraws it when the disc is ejected, so
|
||||||
|
# a room without a drive shows nothing and a room with one needs no setting
|
||||||
|
# changed. Nothing polls.
|
||||||
|
X-KDE-Solid-Predicate=[[ StorageVolume.ignored == false AND StorageVolume.usage == 'Other' ] AND [ OpticalDisc.availableContent & 'VideoDvd' ]]
|
||||||
|
Actions=Play;
|
||||||
|
|
||||||
|
[Desktop Action Play]
|
||||||
|
Name=Play the DVD
|
||||||
|
Icon=media-optical-dvd
|
||||||
|
Exec=/usr/local/bin/classroom-dvd %f
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Ekklesia Classroom session policy.
|
||||||
|
#
|
||||||
|
# Installed to /etc/xdg/kdeglobals, where it is the bottom of Plasma's
|
||||||
|
# configuration cascade. Every group is marked [$i] — immutable — so a
|
||||||
|
# teacher, a stray click or a misbehaving application cannot override it
|
||||||
|
# from the user's own configuration. This is KDE's own Kiosk mechanism:
|
||||||
|
# nothing here is achieved by deleting a binary or hiding a menu entry.
|
||||||
|
#
|
||||||
|
# Lifting a restriction for maintenance is a one-line edit to this file
|
||||||
|
# followed by a session restart, which is what makes the appliance
|
||||||
|
# serviceable rather than merely locked.
|
||||||
|
|
||||||
|
[KDE Action Restrictions][$i]
|
||||||
|
# No way to reach a shell, a command line, or an arbitrary program.
|
||||||
|
action/kdesktop_rmb=false
|
||||||
|
action/kwin_rmb=false
|
||||||
|
action/openwith=false
|
||||||
|
action/switch_user=false
|
||||||
|
action/start_new_session=false
|
||||||
|
action/lock_screen=false
|
||||||
|
run_command=false
|
||||||
|
shell_access=false
|
||||||
|
logout=false
|
||||||
|
# The desktop and the panel are a fixed part of the appliance.
|
||||||
|
editable_desktop_icons=false
|
||||||
|
editable_bookmarks=false
|
||||||
|
movable_toolbars=false
|
||||||
|
plasma/plasmashell/unlockedDesktop=false
|
||||||
|
plasma-desktop/scripting_console=false
|
||||||
|
# Nothing in the classroom should offer to file a bug or open a manual.
|
||||||
|
action/help_report_bug=false
|
||||||
|
action/help_about_kde=false
|
||||||
|
action/help_contents=false
|
||||||
|
|
||||||
|
[General][$i]
|
||||||
|
ColorScheme=EkklesiaDark
|
||||||
|
AccentColor=79,179,168
|
||||||
|
# The teacher's own words are not decoration: keep text rendering plain
|
||||||
|
# and readable rather than styled.
|
||||||
|
XftAntialias=true
|
||||||
|
XftHintStyle=hintslight
|
||||||
|
XftSubPixel=rgb
|
||||||
|
|
||||||
|
[KDE][$i]
|
||||||
|
widgetStyle=Breeze
|
||||||
|
LookAndFeelPackage=org.ekklesia.classroom.desktop
|
||||||
|
SingleClick=false
|
||||||
|
AnimationDurationFactor=0.5
|
||||||
|
|
||||||
|
[Icons][$i]
|
||||||
|
Theme=breeze-dark
|
||||||
|
|
||||||
|
[KFileDialog Settings][$i]
|
||||||
|
Allow Expansion=false
|
||||||
|
Automatically select filename extension=true
|
||||||
|
Show Bookmarks=false
|
||||||
|
Show Full Path=false
|
||||||
|
Show Inline Previews=false
|
||||||
|
Show Speedbar=false
|
||||||
|
Sort directories first=true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# KRunner is a command line with a nicer face. The classroom has neither.
|
||||||
|
[General][$i]
|
||||||
|
Enabled=false
|
||||||
|
ActivateWhenTypingOnDesktop=false
|
||||||
|
[Plugins][$i]
|
||||||
|
shellEnabled=false
|
||||||
|
baloosearchEnabled=false
|
||||||
|
krunner_appstreamEnabled=false
|
||||||
|
krunner_shellEnabled=false
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# A classroom appliance must never present a password prompt to a teacher.
|
||||||
|
# There is no lock screen, and nothing can bring one up: the action itself
|
||||||
|
# is disabled in kdeglobals as well, so a keyboard shortcut cannot reach it.
|
||||||
|
[Daemon][$i]
|
||||||
|
Autolock=false
|
||||||
|
LockOnResume=false
|
||||||
|
LockOnStart=false
|
||||||
|
LockGrace=0
|
||||||
|
Timeout=0
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# The room is not somebody's account. Every session starts from the same
|
||||||
|
# empty desktop, so what the last teacher left open is never the first
|
||||||
|
# thing the next one sees.
|
||||||
|
[General][$i]
|
||||||
|
loginMode=emptySession
|
||||||
|
confirmLogout=false
|
||||||
|
offerShutdown=false
|
||||||
|
shutdownType=0
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# The classroom's window manager.
|
||||||
|
#
|
||||||
|
# The shape of this file is the argument for the whole architecture: window
|
||||||
|
# stacking, minimising, dialog placement and full-screen media are settings
|
||||||
|
# here, not code somewhere in the classroom.
|
||||||
|
|
||||||
|
[Compositing][$i]
|
||||||
|
Enabled=true
|
||||||
|
# Let KWin choose between OpenGL and software rendering by itself. A
|
||||||
|
# classroom machine that loses acceleration after a kernel update should
|
||||||
|
# come up slower, not blank.
|
||||||
|
WindowsBlockCompositing=true
|
||||||
|
AllowTearing=false
|
||||||
|
LatencyPolicy=Medium
|
||||||
|
|
||||||
|
[Desktops][$i]
|
||||||
|
Number=1
|
||||||
|
Rows=1
|
||||||
|
# One desktop, so a teacher cannot land on an empty one and conclude the
|
||||||
|
# computer has lost their work.
|
||||||
|
|
||||||
|
[Windows][$i]
|
||||||
|
Placement=Centered
|
||||||
|
FocusPolicy=ClickToFocus
|
||||||
|
AutoRaise=false
|
||||||
|
DelayFocus=0
|
||||||
|
FocusStealingPreventionLevel=1
|
||||||
|
# A dialog belongs to the window that opened it and stays with it. This is
|
||||||
|
# what keeps a print dialog in front of the page it is printing without
|
||||||
|
# anything in the classroom arranging it.
|
||||||
|
SeparateScreenFocus=false
|
||||||
|
ActiveMouseScreen=false
|
||||||
|
TitlebarDoubleClickCommand=Maximize
|
||||||
|
MaximizeButtonLeftClickCommand=Maximize
|
||||||
|
MaximizeButtonMiddleClickCommand=Maximize
|
||||||
|
MaximizeButtonRightClickCommand=Maximize
|
||||||
|
|
||||||
|
[MouseBindings][$i]
|
||||||
|
CommandAllKey=Meta
|
||||||
|
CommandAll1=Move
|
||||||
|
CommandAll2=Nothing
|
||||||
|
CommandAll3=Nothing
|
||||||
|
CommandTitlebarWheel=Nothing
|
||||||
|
CommandAllWheel=Nothing
|
||||||
|
|
||||||
|
[TabBox][$i]
|
||||||
|
# No window switcher overlay. The bottom bar is the way between windows.
|
||||||
|
ShowTabBox=false
|
||||||
|
|
||||||
|
[Plugins][$i]
|
||||||
|
# Effects the classroom uses, and nothing else. Every line that is false is
|
||||||
|
# an effect that will not be loaded at all.
|
||||||
|
blurEnabled=true
|
||||||
|
contrastEnabled=true
|
||||||
|
kwin4_effect_fadeEnabled=true
|
||||||
|
kwin4_effect_translucencyEnabled=false
|
||||||
|
kwin4_effect_dialogparentEnabled=true
|
||||||
|
kwin4_effect_squashEnabled=true
|
||||||
|
kwin4_effect_maximizeEnabled=true
|
||||||
|
kwin4_effect_scaleEnabled=true
|
||||||
|
kwin4_effect_fullscreenEnabled=true
|
||||||
|
slidingpopupsEnabled=true
|
||||||
|
screenedgeEnabled=false
|
||||||
|
cubeEnabled=false
|
||||||
|
desktopgridEnabled=false
|
||||||
|
overviewEnabled=false
|
||||||
|
windowviewEnabled=false
|
||||||
|
wobblywindowsEnabled=false
|
||||||
|
magiclampEnabled=false
|
||||||
|
slideEnabled=false
|
||||||
|
zoomEnabled=false
|
||||||
|
mouseclickEnabled=false
|
||||||
|
showfpsEnabled=false
|
||||||
|
thumbnailasideEnabled=false
|
||||||
|
kwin4_effect_dimscreenEnabled=false
|
||||||
|
kwin4_effect_morphingpopupsEnabled=false
|
||||||
|
|
||||||
|
[Effect-blur][$i]
|
||||||
|
BlurStrength=8
|
||||||
|
NoiseStrength=2
|
||||||
|
|
||||||
|
[org.kde.kdecoration2][$i]
|
||||||
|
library=org.kde.breeze
|
||||||
|
theme=Breeze
|
||||||
|
# A pin and a close button, and nothing else.
|
||||||
|
#
|
||||||
|
# The pin is the Live View story: keeping a window above the others is a
|
||||||
|
# window-manager state, not a feature the classroom has to invent. There is
|
||||||
|
# deliberately no minimise button, because a minimised window is the one
|
||||||
|
# thing a teacher cannot see the state of — measured, not assumed. Double
|
||||||
|
# clicking the title bar still maximises, so nothing is unreachable.
|
||||||
|
ButtonsOnLeft=
|
||||||
|
ButtonsOnRight=FX
|
||||||
|
ShowToolTips=false
|
||||||
|
BorderSize=None
|
||||||
|
BorderSizeAuto=false
|
||||||
|
|
||||||
|
[Script-desktopchangeosd][$i]
|
||||||
|
Enabled=false
|
||||||
|
|
||||||
|
[Xwayland][$i]
|
||||||
|
Scale=1
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Window rules.
|
||||||
|
#
|
||||||
|
# There are three, they are all about size, and none of them is about
|
||||||
|
# stacking. Dialogs staying in front of the window that opened them,
|
||||||
|
# secondary windows floating over the main one instead of resizing it, and
|
||||||
|
# a print dialog being the window in front are all ordinary window-manager
|
||||||
|
# behaviour here, so there is nothing to write down.
|
||||||
|
#
|
||||||
|
# `types=1` is NET::Normal only. Without it a rule that maximises would
|
||||||
|
# also maximise the dialogs, which is exactly the behaviour the classroom
|
||||||
|
# is moving away from.
|
||||||
|
|
||||||
|
[General]
|
||||||
|
count=3
|
||||||
|
rules=classroom-browser,classroom-wonderink,classroom-video
|
||||||
|
|
||||||
|
[classroom-browser]
|
||||||
|
Description=The classroom browser fills the screen
|
||||||
|
wmclass=google-chrome
|
||||||
|
wmclassmatch=1
|
||||||
|
wmclasscomplete=false
|
||||||
|
types=1
|
||||||
|
maximizehoriz=true
|
||||||
|
maximizehorizrule=3
|
||||||
|
maximizevert=true
|
||||||
|
maximizevertrule=3
|
||||||
|
|
||||||
|
[classroom-wonderink]
|
||||||
|
Description=Wonder Ink fills the screen
|
||||||
|
# Chrome derives this from the --app URL. Measured on a running session
|
||||||
|
# rather than guessed; it is also what the launcher's StartupWMClass says,
|
||||||
|
# and the two have to agree or the panel shows two buttons for one app.
|
||||||
|
wmclass=chrome-app.wonderink.org__-Default
|
||||||
|
wmclassmatch=1
|
||||||
|
wmclasscomplete=false
|
||||||
|
types=1
|
||||||
|
maximizehoriz=true
|
||||||
|
maximizehorizrule=3
|
||||||
|
maximizevert=true
|
||||||
|
maximizevertrule=3
|
||||||
|
|
||||||
|
[classroom-video]
|
||||||
|
Description=Video is an appliance screen, not a window
|
||||||
|
wmclass=vlc
|
||||||
|
wmclassmatch=2
|
||||||
|
wmclasscomplete=false
|
||||||
|
noborder=true
|
||||||
|
noborderrule=2
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Small cards, top right, out of the way of the bottom bar and of anything
|
||||||
|
# a teacher is reading. Plasma's own notification service does this; the
|
||||||
|
# classroom does not run a notification daemon of its own.
|
||||||
|
[Notifications][$i]
|
||||||
|
PopupPosition=TopRight
|
||||||
|
PopupTimeout=6000
|
||||||
|
LowPriorityPopups=false
|
||||||
|
LowPriorityHistory=false
|
||||||
|
|
||||||
|
[Jobs][$i]
|
||||||
|
PermanentPopups=false
|
||||||
|
InNotifications=true
|
||||||
|
|
||||||
|
[DoNotDisturb][$i]
|
||||||
|
WhenFullscreen=true
|
||||||
|
WhenScreensMirrored=false
|
||||||
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 920 KiB |
|
After Width: | Height: | Size: 919 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -1,32 +1,33 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# The dock's Browser button. classroom-window decides whether this is a
|
# The room's ordinary browser: YouTube, and anywhere else a lesson goes.
|
||||||
# launch, a focus, a minimise or a restore.
|
|
||||||
#
|
#
|
||||||
# Chrome stays a full browser: Wonder Ink is only the starting page, and
|
# It is a full browser with a tab strip and an address bar, in the room's
|
||||||
# the profile in ~/.config/google-chrome is never touched here so logins
|
# own profile, so the church YouTube sign-in survives reboots and updates.
|
||||||
# and cookies survive reboots and updates.
|
# Wonder Ink is a separate application in a separate profile and is not
|
||||||
|
# reachable from this one.
|
||||||
|
#
|
||||||
|
# There is no launch/focus/minimise/restore logic here any more. The panel
|
||||||
|
# owns that: it knows this window by StartupWMClass and does the right
|
||||||
|
# thing with a click, including telling the teacher which of those four
|
||||||
|
# states the application is in.
|
||||||
#
|
#
|
||||||
# --hide-crash-restore-bubble is what suppresses "Restore pages? Chrome
|
# --hide-crash-restore-bubble is what suppresses "Restore pages? Chrome
|
||||||
# didn't shut down correctly" after a reboot. Note the switch name: the
|
# didn't shut down correctly" after a power cut. Note the switch name: the
|
||||||
# older --disable-session-crashed-bubble is not present in Chrome 152 at
|
# older --disable-session-crashed-bubble is not present in Chrome 152 at
|
||||||
# all, so it would have silently done nothing. Verified against the
|
# all, so it would have silently done nothing.
|
||||||
# strings of the installed binary.
|
|
||||||
|
|
||||||
HOME_PAGE='https://app.wonderink.org/'
|
# Not Wonder Ink. Wonder Ink is its own application in its own profile,
|
||||||
|
# and a second way in through this window would put a teacher's lesson
|
||||||
|
# session in the room's shared browser profile — the exact leak the split
|
||||||
|
# exists to prevent.
|
||||||
|
HOME_PAGE='https://www.youtube.com/'
|
||||||
|
|
||||||
exec /usr/local/bin/classroom-window toggle \
|
exec google-chrome \
|
||||||
--match 'google-chrome' \
|
--ozone-platform=wayland \
|
||||||
--match 'chrome' \
|
--no-first-run \
|
||||||
--match 'chrome-.*' \
|
--no-default-browser-check \
|
||||||
--match 'google-chrome-.*' \
|
--password-store=basic \
|
||||||
-- \
|
--hide-crash-restore-bubble \
|
||||||
google-chrome \
|
"${@:-$HOME_PAGE}"
|
||||||
--ozone-platform=wayland \
|
|
||||||
--start-maximized \
|
|
||||||
--no-first-run \
|
|
||||||
--no-default-browser-check \
|
|
||||||
--password-store=basic \
|
|
||||||
--hide-crash-restore-bubble \
|
|
||||||
"$HOME_PAGE"
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Wonder Ink as an application, not a tab.
|
||||||
|
#
|
||||||
|
# Two things make it one. It runs in Chrome's app mode, so the window has
|
||||||
|
# no tab strip and no address bar and reads as a program. And it runs in
|
||||||
|
# its own Chrome profile, so the teacher signed into Wonder Ink and the
|
||||||
|
# room's shared YouTube account are separate sessions that cannot reach
|
||||||
|
# each other:
|
||||||
|
#
|
||||||
|
# * closing this window ends the Wonder Ink session, because the
|
||||||
|
# wonderink.org cookies are session-only by policy and this profile's
|
||||||
|
# browser process exits with the window;
|
||||||
|
# * a Google sign-in performed here cannot be used to re-enter the
|
||||||
|
# previous teacher's Wonder Ink account from the ordinary browser,
|
||||||
|
# because the ordinary browser's Google session is in a different
|
||||||
|
# profile;
|
||||||
|
# * and the church YouTube login in the ordinary browser survives all of
|
||||||
|
# it, because nothing here touches that profile.
|
||||||
|
#
|
||||||
|
# The machine-wide policy in /etc/opt/chrome/policies applies to every
|
||||||
|
# profile, so this window is governed by exactly the same rules as the
|
||||||
|
# browser.
|
||||||
|
|
||||||
|
HOME_PAGE='https://app.wonderink.org/'
|
||||||
|
PROFILE="${XDG_DATA_HOME:-$HOME/.local/share}/ekklesia-classroom/wonderink"
|
||||||
|
|
||||||
|
mkdir -p "$PROFILE"
|
||||||
|
|
||||||
|
exec google-chrome \
|
||||||
|
--user-data-dir="$PROFILE" \
|
||||||
|
--app="$HOME_PAGE" \
|
||||||
|
--ozone-platform=wayland \
|
||||||
|
--no-first-run \
|
||||||
|
--no-default-browser-check \
|
||||||
|
--password-store=basic \
|
||||||
|
--hide-crash-restore-bubble \
|
||||||
|
"$@"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# The panel is the only way a teacher has of getting anywhere, so it is
|
||||||
|
# never allowed to stay dead.
|
||||||
|
#
|
||||||
|
# Plasma's own unit already restarts plasmashell on a crash, but it gives
|
||||||
|
# up after three restarts in sixty seconds and the panel is then gone until
|
||||||
|
# somebody reboots the room. That is measurably the failure a classroom
|
||||||
|
# machine has already had once, with the old hand-written dock, which had
|
||||||
|
# no supervision at all.
|
||||||
|
#
|
||||||
|
# Measured in the lab: with these four lines, plasmashell survived eight
|
||||||
|
# consecutive SIGKILLs one second apart and came back every time in about
|
||||||
|
# 2.4 seconds. Without them it was permanently gone on the third.
|
||||||
|
[Unit]
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate the light colour scheme from the dark one.
|
||||||
|
|
||||||
|
There is one set of design tokens, not two themes. The light scheme is the
|
||||||
|
same file with the roles swapped, so a colour can only be changed in one
|
||||||
|
place and the two can never drift apart.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
COLORS = ROOT / "config" / "plasma" / "colors"
|
||||||
|
|
||||||
|
# dark value -> light value
|
||||||
|
ROLES = {
|
||||||
|
"20,28,32": "242,245,244", # window background
|
||||||
|
"25,35,40": "233,238,237", # alternate
|
||||||
|
"15,23,26": "255,255,255", # view background
|
||||||
|
"18,26,30": "236,241,240", # header
|
||||||
|
"16,23,26": "228,234,233", # header, inactive
|
||||||
|
"30,42,47": "225,231,230", # button alternate
|
||||||
|
"230,237,239": "22,35,42", # normal text
|
||||||
|
"147,165,170": "96,116,122", # inactive text
|
||||||
|
"170,186,191": "80,99,105", # inactive header text
|
||||||
|
"125,142,147": "120,138,144",
|
||||||
|
"79,179,168": "46,140,130", # accent
|
||||||
|
"126,196,255": "26,110,180", # link
|
||||||
|
"227,106,106": "192,57,57", # negative
|
||||||
|
"224,177,92": "166,116,24", # neutral
|
||||||
|
"111,207,151": "38,142,86", # positive
|
||||||
|
"156,138,222": "104,80,180", # visited
|
||||||
|
"14,21,24": "255,255,255", # text on a selection
|
||||||
|
"10,16,18": "255,255,255",
|
||||||
|
}
|
||||||
|
|
||||||
|
HEADER = (
|
||||||
|
"# Ekklesia Classroom — light.\n"
|
||||||
|
"#\n"
|
||||||
|
"# GENERATED from EkklesiaDark.colors by tools/build-light-scheme.\n"
|
||||||
|
"# Edit the dark scheme and regenerate; do not edit this file by hand."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dark = (COLORS / "EkklesiaDark.colors").read_text()
|
||||||
|
|
||||||
|
light = dark
|
||||||
|
for old, new in sorted(ROLES.items(), key=lambda item: -len(item[0])):
|
||||||
|
light = re.sub(rf"(?<![\d,]){re.escape(old)}(?![\d,])", "\0" + new, light)
|
||||||
|
|
||||||
|
light = light.replace("\0", "")
|
||||||
|
light = light.replace("Ekklesia Classroom Dark", "Ekklesia Classroom Light")
|
||||||
|
light = light.replace("ColorScheme=EkklesiaDark", "ColorScheme=EkklesiaLight")
|
||||||
|
light = light.replace("# Ekklesia Classroom — dark. The default.", HEADER)
|
||||||
|
|
||||||
|
(COLORS / "EkklesiaLight.colors").write_text(light)
|
||||||
|
print("wrote", COLORS / "EkklesiaLight.colors")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render the classroom's idle desktop background.
|
||||||
|
|
||||||
|
This is a STAND-IN. The classroom's chosen wallpaper is an illustrated
|
||||||
|
landscape that is not in this repository; when it arrives it replaces the
|
||||||
|
output of this script at the same path and nothing else changes.
|
||||||
|
|
||||||
|
It exists so the desktop can be judged in the right palette rather than
|
||||||
|
against the bright confetti background it is replacing: sage, teal, blue
|
||||||
|
and lavender, a river leading into the picture, and a large quiet area in
|
||||||
|
the upper middle where nothing happens.
|
||||||
|
|
||||||
|
No image library is installed on the development host, so the PNG is
|
||||||
|
written by hand — zlib and struct are enough for a truecolour image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import pathlib
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
WIDTH = 1920
|
||||||
|
HEIGHT = 1080
|
||||||
|
OUT = pathlib.Path(__file__).resolve().parent.parent / "config" / "wallpapers"
|
||||||
|
|
||||||
|
|
||||||
|
def mix(a, b, t):
|
||||||
|
t = 0.0 if t < 0 else 1.0 if t > 1 else t
|
||||||
|
return tuple(a[i] + (b[i] - a[i]) * t for i in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def ease(t):
|
||||||
|
"""Smoothstep. Bands in a gradient are the one thing that would read as
|
||||||
|
a rendering fault rather than as a picture."""
|
||||||
|
t = 0.0 if t < 0 else 1.0 if t > 1 else t
|
||||||
|
return t * t * (3 - 2 * t)
|
||||||
|
|
||||||
|
|
||||||
|
# The palette, top of the sky downwards.
|
||||||
|
SKY = [
|
||||||
|
(0.00, (34, 38, 58)), # deep lavender-slate
|
||||||
|
(0.30, (58, 68, 96)), # lavender
|
||||||
|
(0.55, (86, 108, 130)), # blue
|
||||||
|
(0.78, (139, 165, 168)), # sage haze at the horizon
|
||||||
|
(1.00, (178, 198, 192)),
|
||||||
|
]
|
||||||
|
|
||||||
|
HORIZON = 0.56 # where the land starts, as a fraction of the height
|
||||||
|
GLOW_X, GLOW_Y = 0.56, 0.545
|
||||||
|
GLOW = (232, 232, 214)
|
||||||
|
|
||||||
|
RIDGES = [
|
||||||
|
# (base height, amplitude, wavelength, phase, colour, haze)
|
||||||
|
(0.575, 0.028, 2.1, 0.4, (74, 96, 110), 0.55),
|
||||||
|
(0.615, 0.040, 1.5, 2.2, (60, 84, 92), 0.34),
|
||||||
|
(0.665, 0.052, 1.1, 4.1, (48, 72, 74), 0.18),
|
||||||
|
]
|
||||||
|
|
||||||
|
RIVER_TOP = 0.585
|
||||||
|
RIVER_COLOUR = (96, 140, 146)
|
||||||
|
RIVER_LIGHT = (186, 212, 202)
|
||||||
|
|
||||||
|
BANK = (34, 52, 48) # the sage foreground the river runs between
|
||||||
|
|
||||||
|
|
||||||
|
def sky_colour(y):
|
||||||
|
t = y / HEIGHT
|
||||||
|
for i in range(len(SKY) - 1):
|
||||||
|
t0, c0 = SKY[i]
|
||||||
|
t1, c1 = SKY[i + 1]
|
||||||
|
if t <= t1 or i == len(SKY) - 2:
|
||||||
|
return mix(c0, c1, ease((t - t0) / (t1 - t0)))
|
||||||
|
return SKY[-1][1]
|
||||||
|
|
||||||
|
|
||||||
|
def river_edges(y):
|
||||||
|
"""The river narrows towards the horizon and opens towards the viewer,
|
||||||
|
which is what makes it lead into the picture rather than across it."""
|
||||||
|
t = (y / HEIGHT - RIVER_TOP) / (1 - RIVER_TOP)
|
||||||
|
if t < 0:
|
||||||
|
return None
|
||||||
|
t = ease(t)
|
||||||
|
centre = 0.56 - 0.14 * t * t
|
||||||
|
half = 0.004 + 0.30 * t * t
|
||||||
|
return (centre - half) * WIDTH, (centre + half) * WIDTH
|
||||||
|
|
||||||
|
|
||||||
|
def render():
|
||||||
|
rows = []
|
||||||
|
for y in range(HEIGHT):
|
||||||
|
base = sky_colour(y)
|
||||||
|
row = bytearray()
|
||||||
|
edges = river_edges(y)
|
||||||
|
yn = y / HEIGHT
|
||||||
|
|
||||||
|
# Ridge heights for this column are cheap to evaluate, so the land
|
||||||
|
# is computed per pixel rather than filled as polygons.
|
||||||
|
for x in range(WIDTH):
|
||||||
|
xn = x / WIDTH
|
||||||
|
r, g, b = base
|
||||||
|
|
||||||
|
# A soft light low in the sky, off centre. It is the only
|
||||||
|
# bright thing in the picture.
|
||||||
|
dx = (xn - GLOW_X) * 1.7
|
||||||
|
dy = yn - GLOW_Y
|
||||||
|
d = math.sqrt(dx * dx + dy * dy)
|
||||||
|
glow = math.exp(-(d * d) / 0.055) * 0.55
|
||||||
|
if glow > 0.002:
|
||||||
|
r, g, b = mix((r, g, b), GLOW, glow)
|
||||||
|
|
||||||
|
land = None
|
||||||
|
for height, amp, wavelength, phase, colour, haze in RIDGES:
|
||||||
|
ridge = height - amp * (
|
||||||
|
0.6 * math.sin(xn * math.pi * 2 * wavelength + phase)
|
||||||
|
+ 0.4 * math.sin(xn * math.pi * 2 * wavelength * 2.3 + phase * 1.7)
|
||||||
|
)
|
||||||
|
if yn >= ridge:
|
||||||
|
# Nearer ridges are darker and less hazy, which is the
|
||||||
|
# whole of the depth in this picture.
|
||||||
|
shade = mix(colour, (r, g, b), haze)
|
||||||
|
depth = ease((yn - ridge) / 0.16)
|
||||||
|
land = mix(shade, tuple(c * 0.82 for c in shade), depth)
|
||||||
|
|
||||||
|
if land is not None:
|
||||||
|
r, g, b = land
|
||||||
|
|
||||||
|
if edges is not None:
|
||||||
|
left, right = edges
|
||||||
|
if left <= x <= right:
|
||||||
|
width = max(right - left, 1.0)
|
||||||
|
across = (x - left) / width
|
||||||
|
# Light gathers down the middle of the water and the
|
||||||
|
# banks stay dark, so the river reads as a surface.
|
||||||
|
sheen = math.exp(-((across - 0.5) ** 2) / 0.06)
|
||||||
|
ripple = 0.5 + 0.5 * math.sin(
|
||||||
|
yn * 130 + math.sin(xn * 18) * 1.4
|
||||||
|
)
|
||||||
|
water = mix(RIVER_COLOUR, RIVER_LIGHT, 0.30 * sheen + 0.10 * ripple)
|
||||||
|
water = mix(water, (r, g, b), max(0.0, 0.45 - 0.45 * ease((yn - RIVER_TOP) / 0.25)))
|
||||||
|
edge = min(across, 1 - across)
|
||||||
|
r, g, b = mix(BANK, water, ease(edge / 0.05))
|
||||||
|
elif yn > RIVER_TOP + 0.10:
|
||||||
|
near = ease((yn - RIVER_TOP - 0.10) / 0.34)
|
||||||
|
r, g, b = mix((r, g, b), BANK, near * 0.85)
|
||||||
|
|
||||||
|
# The bottom of the screen carries the bar. Settling it down
|
||||||
|
# keeps the panel legible without putting a band across the
|
||||||
|
# picture.
|
||||||
|
if yn > 0.78:
|
||||||
|
r, g, b = mix((r, g, b), (16, 24, 28), ease((yn - 0.78) / 0.24) * 0.62)
|
||||||
|
|
||||||
|
row += bytes((int(r + 0.5) & 255, int(g + 0.5) & 255, int(b + 0.5) & 255))
|
||||||
|
|
||||||
|
rows.append(bytes(row))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def write_png(path, rows):
|
||||||
|
raw = b"".join(b"\x00" + row for row in rows)
|
||||||
|
|
||||||
|
def chunk(tag, data):
|
||||||
|
payload = tag + data
|
||||||
|
return (
|
||||||
|
struct.pack(">I", len(data))
|
||||||
|
+ payload
|
||||||
|
+ struct.pack(">I", zlib.crc32(payload) & 0xFFFFFFFF)
|
||||||
|
)
|
||||||
|
|
||||||
|
png = b"\x89PNG\r\n\x1a\n"
|
||||||
|
png += chunk(b"IHDR", struct.pack(">IIBBBBB", WIDTH, HEIGHT, 8, 2, 0, 0, 0))
|
||||||
|
png += chunk(b"IDAT", zlib.compress(raw, 9))
|
||||||
|
png += chunk(b"IEND", b"")
|
||||||
|
path.write_bytes(png)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
target = OUT / "classroom-calm.png"
|
||||||
|
write_png(target, render())
|
||||||
|
print(f"wrote {target} ({target.stat().st_size} bytes)")
|
||||||
@@ -50,3 +50,23 @@ CMD ["/sbin/init"]
|
|||||||
# sanctioned way to capture a KWin session that has no outer compositor.
|
# sanctioned way to capture a KWin session that has no outer compositor.
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends kde-spectacle \
|
RUN apt-get update && apt-get install -y --no-install-recommends kde-spectacle \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# The classroom's browser, from Google's own repository, the same way a
|
||||||
|
# classroom machine gets it. Testing the browser question on a different
|
||||||
|
# browser would not answer it.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends gnupg \
|
||||||
|
&& install -d -m 0755 /etc/apt/keyrings \
|
||||||
|
&& curl -fsS https://dl.google.com/linux/linux_signing_key.pub \
|
||||||
|
| gpg --dearmor --batch --yes --output /etc/apt/keyrings/google-chrome.gpg \
|
||||||
|
&& echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main' \
|
||||||
|
> /etc/apt/sources.list.d/google-chrome.list \
|
||||||
|
&& apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# VLC, CUPS and PipeWire: the rest of the classroom's runtime, so the
|
||||||
|
# desktop is judged with the room's real applications in it.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
vlc vlc-plugin-base cups cups-client cups-filters \
|
||||||
|
pipewire pipewire-pulse wireplumber pulseaudio-utils \
|
||||||
|
libnotify-bin \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Put the classroom desktop onto a machine, from the repository, at the
|
||||||
|
# paths the package will use. Run as root inside the lab.
|
||||||
|
#
|
||||||
|
# Every destination here is a normal system path that Plasma already reads.
|
||||||
|
# There is no classroom daemon in this list, and nothing is generated: the
|
||||||
|
# desktop is a set of files.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SRC="${1:-/srv/classroom}"
|
||||||
|
|
||||||
|
install -Dm644 "$SRC/config/plasma/xdg/kdeglobals" /etc/xdg/kdeglobals
|
||||||
|
install -Dm644 "$SRC/config/plasma/xdg/kwinrc" /etc/xdg/kwinrc
|
||||||
|
install -Dm644 "$SRC/config/plasma/xdg/kwinrulesrc" /etc/xdg/kwinrulesrc
|
||||||
|
install -Dm644 "$SRC/config/plasma/xdg/kscreenlockerrc" /etc/xdg/kscreenlockerrc
|
||||||
|
install -Dm644 "$SRC/config/plasma/xdg/ksmserverrc" /etc/xdg/ksmserverrc
|
||||||
|
install -Dm644 "$SRC/config/plasma/xdg/krunnerrc" /etc/xdg/krunnerrc
|
||||||
|
install -Dm644 "$SRC/config/plasma/xdg/plasmanotifyrc" /etc/xdg/plasmanotifyrc
|
||||||
|
|
||||||
|
install -Dm644 "$SRC/config/plasma/colors/EkklesiaDark.colors" \
|
||||||
|
/usr/share/color-schemes/EkklesiaDark.colors
|
||||||
|
install -Dm644 "$SRC/config/plasma/colors/EkklesiaLight.colors" \
|
||||||
|
/usr/share/color-schemes/EkklesiaLight.colors
|
||||||
|
|
||||||
|
LNF=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop
|
||||||
|
rm -rf "$LNF"
|
||||||
|
install -Dm644 "$SRC/config/plasma/lookandfeel/metadata.json" "$LNF/metadata.json"
|
||||||
|
install -Dm644 "$SRC/config/plasma/lookandfeel/contents/defaults" \
|
||||||
|
"$LNF/contents/defaults"
|
||||||
|
install -Dm644 \
|
||||||
|
"$SRC/config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js" \
|
||||||
|
"$LNF/contents/layouts/org.kde.plasma.desktop-layout.js"
|
||||||
|
|
||||||
|
for entry in "$SRC"/config/plasma/applications/*.desktop; do
|
||||||
|
install -Dm644 "$entry" "/usr/share/applications/$(basename "$entry")"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Entries that deliberately shadow a vendor one. XDG_DATA_DIRS searches
|
||||||
|
# /usr/local/share before /usr/share, so these win without touching, and
|
||||||
|
# without being overwritten by, the package that provides the original.
|
||||||
|
for entry in "$SRC"/config/plasma/applications-local/*.desktop; do
|
||||||
|
install -Dm644 "$entry" "/usr/local/share/applications/$(basename "$entry")"
|
||||||
|
done
|
||||||
|
|
||||||
|
install -Dm644 "$SRC/config/plasma/solid/ekklesia-dvd.desktop" \
|
||||||
|
/usr/share/solid/actions/ekklesia-dvd.desktop
|
||||||
|
|
||||||
|
for icon in "$SRC"/config/icons/ekklesia-*.svg; do
|
||||||
|
[ -e "$icon" ] || continue
|
||||||
|
install -Dm644 "$icon" \
|
||||||
|
"/usr/share/icons/hicolor/scalable/apps/$(basename "$icon")"
|
||||||
|
done
|
||||||
|
|
||||||
|
install -Dm644 "$SRC/config/wallpapers/classroom-calm.png" \
|
||||||
|
/usr/share/backgrounds/ekklesia/classroom-calm.png
|
||||||
|
|
||||||
|
install -Dm644 "$SRC/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf" \
|
||||||
|
/etc/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf
|
||||||
|
|
||||||
|
for script in classroom-browser classroom-wonderink; do
|
||||||
|
[ -e "$SRC/scripts/$script" ] || continue
|
||||||
|
install -Dm755 "$SRC/scripts/$script" "/usr/local/bin/$script"
|
||||||
|
done
|
||||||
|
|
||||||
|
command -v gtk-update-icon-cache >/dev/null 2>&1 \
|
||||||
|
&& gtk-update-icon-cache -qtf /usr/share/icons/hicolor || true
|
||||||
|
command -v update-desktop-database >/dev/null 2>&1 \
|
||||||
|
&& update-desktop-database -q /usr/share/applications || true
|
||||||
|
|
||||||
|
echo "classroom desktop installed"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run a KWin script in the session under test and print what it logged.
|
||||||
|
#
|
||||||
|
# KWin's scripting interface is the only way to ask the compositor what it
|
||||||
|
# actually believes about a window — class, caption, desktop file, geometry,
|
||||||
|
# minimised, keep-above. Everything this experiment claims about window
|
||||||
|
# behaviour is measured through here rather than inferred from a screenshot.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAME="${LAB_NAME:-classroom-v2-lab}"
|
||||||
|
SCRIPT="${1:?usage: kwin-script FILE}"
|
||||||
|
ID="labscript$RANDOM"
|
||||||
|
|
||||||
|
# /tmp inside the lab is a tmpfs created after the container started, which
|
||||||
|
# `docker cp` cannot see, so the script is piped in instead.
|
||||||
|
docker exec -i "$NAME" sh -c "cat > /tmp/$ID.js && chown classroom /tmp/$ID.js" \
|
||||||
|
< "$SCRIPT"
|
||||||
|
|
||||||
|
run() {
|
||||||
|
docker exec -u classroom \
|
||||||
|
-e XDG_RUNTIME_DIR=/run/user/1000 \
|
||||||
|
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
|
||||||
|
-e HOME=/home/classroom \
|
||||||
|
"$NAME" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
since=$(docker exec "$NAME" date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
number=$(run qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript \
|
||||||
|
"/tmp/$ID.js" "$ID" 2>/dev/null | tr -d '\r')
|
||||||
|
run qdbus6 org.kde.KWin "/Scripting/Script$number" org.kde.kwin.Script.run >/dev/null 2>&1
|
||||||
|
sleep 1
|
||||||
|
run qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript "$ID" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# KWin's print() reaches the journal, tagged by the wrapper process.
|
||||||
|
docker exec "$NAME" journalctl --since "$since" --no-pager -o cat 2>/dev/null \
|
||||||
|
| grep -a "LAB:" | sed 's/.*LAB://'
|
||||||
|
|
||||||
@@ -75,8 +75,19 @@ down)
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
outer-up)
|
outer-up)
|
||||||
|
# A headless compositor has no input devices, and a wl_seat with no
|
||||||
|
# pointer capability never gives the session under test a pointer to
|
||||||
|
# click with — sway's cursor commands then report success and move
|
||||||
|
# nothing. So the lab runs wlroots' libinput backend as well, over the
|
||||||
|
# devices the privileged container inherits, with seatd standing in for
|
||||||
|
# the login session it does not have. Clicks in this experiment are
|
||||||
|
# real pointer events arriving through libinput.
|
||||||
|
docker exec "$NAME" sh -c 'pgrep -x seatd >/dev/null || \
|
||||||
|
(SEATD_VTBOUND=0 setsid seatd -g input > /tmp/seatd.log 2>&1 &)'
|
||||||
|
docker exec "$NAME" usermod -aG input classroom 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
session_env \
|
session_env \
|
||||||
-e WLR_BACKENDS=headless -e WLR_LIBINPUT_NO_DEVICES=1 \
|
-e WLR_BACKENDS=headless,libinput -e LIBSEAT_BACKEND=seatd \
|
||||||
-e WLR_RENDERER=pixman -e LIBGL_ALWAYS_SOFTWARE=1 \
|
-e WLR_RENDERER=pixman -e LIBGL_ALWAYS_SOFTWARE=1 \
|
||||||
-d "$NAME" sh -c 'exec sway -c /srv/classroom/tools/lab/outer-sway.conf \
|
-d "$NAME" sh -c 'exec sway -c /srv/classroom/tools/lab/outer-sway.conf \
|
||||||
> /tmp/outer-sway.log 2>&1'
|
> /tmp/outer-sway.log 2>&1'
|
||||||
@@ -91,6 +102,7 @@ plasma-up)
|
|||||||
session_env \
|
session_env \
|
||||||
-e WAYLAND_DISPLAY="$OUTER" -e XDG_SESSION_TYPE=wayland \
|
-e WAYLAND_DISPLAY="$OUTER" -e XDG_SESSION_TYPE=wayland \
|
||||||
-e XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \
|
-e XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \
|
||||||
|
-e QT_LOGGING_RULES="kwin_scripting.debug=true" \
|
||||||
-d "$NAME" sh -c 'exec startplasma-wayland > /tmp/plasma.log 2>&1'
|
-d "$NAME" sh -c 'exec startplasma-wayland > /tmp/plasma.log 2>&1'
|
||||||
for _ in $(seq 1 60); do
|
for _ in $(seq 1 60); do
|
||||||
docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break
|
docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break
|
||||||
@@ -114,27 +126,45 @@ session)
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
shot)
|
shot)
|
||||||
# shot NAME [--delay SECONDS]
|
# shot NAME [GEOMETRY] — geometry as grim wants it, e.g. "0,980 1920x100"
|
||||||
name="${2:?usage: lab shot NAME}"
|
name="${2:?usage: lab shot NAME}"
|
||||||
outer grim /tmp/lab-shot.png >/dev/null
|
if [[ -n "${3:-}" ]]; then
|
||||||
|
outer grim -g "$3" /tmp/lab-shot.png >/dev/null
|
||||||
|
else
|
||||||
|
outer grim /tmp/lab-shot.png >/dev/null
|
||||||
|
fi
|
||||||
mkdir -p "$SHOTS"
|
mkdir -p "$SHOTS"
|
||||||
docker exec "$NAME" cat /tmp/lab-shot.png > "$SHOTS/$name.png"
|
docker exec "$NAME" cat /tmp/lab-shot.png > "$SHOTS/$name.png"
|
||||||
echo "$SHOTS/$name.png"
|
echo "$SHOTS/$name.png"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
click)
|
click)
|
||||||
# click X Y — a real pointer press in the session under test
|
# Absolute, through sway's own seat. This only works because the seat
|
||||||
|
# has a pointer capability at all, which is what the uinput device in
|
||||||
|
# `outer-up` is for.
|
||||||
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
||||||
sleep 0.3
|
sleep 0.4
|
||||||
outer swaymsg seat - cursor press button1 >/dev/null
|
outer swaymsg seat - cursor press button1 >/dev/null
|
||||||
sleep 0.1
|
sleep 0.15
|
||||||
outer swaymsg seat - cursor release button1 >/dev/null
|
outer swaymsg seat - cursor release button1 >/dev/null
|
||||||
|
sleep 0.5
|
||||||
;;
|
;;
|
||||||
|
|
||||||
move)
|
move)
|
||||||
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
drag)
|
||||||
|
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
||||||
|
sleep 0.3
|
||||||
|
outer swaymsg seat - cursor press button1 >/dev/null
|
||||||
|
sleep 0.3
|
||||||
|
outer swaymsg seat - cursor set "$4" "$5" >/dev/null
|
||||||
|
sleep 0.3
|
||||||
|
outer swaymsg seat - cursor release button1 >/dev/null
|
||||||
|
sleep 0.4
|
||||||
|
;;
|
||||||
|
|
||||||
key)
|
key)
|
||||||
shift
|
shift
|
||||||
outer wtype "$@"
|
outer wtype "$@"
|
||||||
@@ -148,6 +178,8 @@ sh)
|
|||||||
run) shift; session_env "$NAME" "$@" ;;
|
run) shift; session_env "$NAME" "$@" ;;
|
||||||
irun) shift; session_env -it "$NAME" "$@" ;;
|
irun) shift; session_env -it "$NAME" "$@" ;;
|
||||||
inner) shift; inner "$@" ;;
|
inner) shift; inner "$@" ;;
|
||||||
|
inner-d) shift; session_env -d -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \
|
||||||
|
-e QT_QPA_PLATFORM=wayland "$NAME" "$@" ;;
|
||||||
inner-it) shift; session_env -it -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE "$NAME" "$@" ;;
|
inner-it) shift; session_env -it -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE "$NAME" "$@" ;;
|
||||||
outer) shift; outer "$@" ;;
|
outer) shift; outer "$@" ;;
|
||||||
|
|
||||||
@@ -164,9 +196,11 @@ usage: tools/lab/lab COMMAND
|
|||||||
plasma-down stop the Plasma session
|
plasma-down stop the Plasma session
|
||||||
shot NAME capture the screen to docs/screenshots/NAME.png
|
shot NAME capture the screen to docs/screenshots/NAME.png
|
||||||
click X Y press the pointer at a screen position
|
click X Y press the pointer at a screen position
|
||||||
|
drag X1 Y1 X2 Y2 drag from one position to another
|
||||||
move X Y move the pointer
|
move X Y move the pointer
|
||||||
key ARGS... wtype, aimed at the session
|
key ARGS... wtype, aimed at the session
|
||||||
inner CMD... run CMD as a client of the Plasma session
|
inner CMD... run CMD as a client of the Plasma session
|
||||||
|
inner-d CMD... the same, detached
|
||||||
outer CMD... run CMD against the lab compositor
|
outer CMD... run CMD against the lab compositor
|
||||||
run CMD... run CMD as the classroom user, no display
|
run CMD... run CMD as the classroom user, no display
|
||||||
sh root shell in the lab
|
sh root shell in the lab
|
||||||
|
|||||||