Merge upstream quattro for the Qt 6.12 palette fix (#14553)
Qt 6.12 shadows the bare Color singleton, so the bar and every Color role read undefined on fresh installs. Keep our I18n text in the four conflicting files, qualify #14433's notification button color, give the new dictation menu rows and notifications Chinese, and drop table entries for the removed Voxtype and FIDO2 texts.
This commit is contained in:
commit
fcd681c099
239 files changed
+3996
-616
No files matched your search
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "background" as BackgroundPlugin
|
||||
|
||||
ShellRoot {
|
||||
property bool reloaded: false
|
||||
BackgroundPlugin.Background { id: background }
|
||||
FileView { id: command; path: Quickshell.env("RELOAD_TEST_COMMAND"); watchChanges: true; onFileChanged: reload() }
|
||||
FileView { id: result; path: Quickshell.env("RELOAD_TEST_RESULT"); atomicWrites: true }
|
||||
|
||||
Timer { interval: 600; running: true; onTriggered: result.setText("initial") }
|
||||
Timer { id: settled; interval: 600; onTriggered: result.setText("reloaded") }
|
||||
Timer {
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (command.text().trim() === "done") {
|
||||
Qt.quit()
|
||||
} else if (command.text().trim() === "reload" && !reloaded) {
|
||||
reloaded = true
|
||||
background.setBackground(background.currentBackground, true)
|
||||
settled.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "services"
|
||||
import "clone" as Clone
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var services: ({ "intro-test.background": background })
|
||||
function firstPartyServiceFor(id) { return id === "omarchy.background" ? background : null }
|
||||
|
||||
PluginShellApi { id: facade; pluginId: "intro-test.background" }
|
||||
Clone.Background { id: background; shell: facade }
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
property bool stillReported: false
|
||||
FileView { id: release; path: Quickshell.env("INTRO_TEST_RELEASE"); watchChanges: true; onFileChanged: reload() }
|
||||
FileView { id: result; path: Quickshell.env("INTRO_TEST_RESULT"); atomicWrites: true }
|
||||
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: result.setText(JSON.stringify({
|
||||
phase: "cover",
|
||||
scoped: background.shell.pluginId === "intro-test.background",
|
||||
privateCoordinator: background.shell.bootIntro === undefined,
|
||||
selected: background.displayedBackground.endsWith("still.png"),
|
||||
covered: intro.cover
|
||||
}))
|
||||
}
|
||||
Timer {
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (release.text().trim() === "done") {
|
||||
Qt.quit()
|
||||
} else if (release.text().trim() === "captured" && !intro.cover && !intro.startupPending && !test.stillReported) {
|
||||
test.stillReported = true
|
||||
result.setText(JSON.stringify({ phase: "still", covered: intro.cover }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "services"
|
||||
import "Commons" as Commons
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property bool failed: false
|
||||
property var services: ({})
|
||||
function firstPartyServiceFor(id) { return services[id] || null }
|
||||
function check(ok, message) {
|
||||
if (!ok) {
|
||||
failed = true
|
||||
console.log("RESULT fail " + message)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject { id: retainedBackground; property bool suspended: false }
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
|
||||
Process {
|
||||
id: revealVideo
|
||||
command: ["touch", Quickshell.env("INTRO_TEST_FRAME_READY")]
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 300
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.checked, "startup runs even when OWE left the background disabled for a video")
|
||||
test.services = ({ "omarchy.background": retainedBackground })
|
||||
intro.prepareTheme(Quickshell.env("INTRO_TEST_COVER"), "theme-one", Qt.btoa('background = "#123456"'), "")
|
||||
test.check(intro.themeCoverStatus("theme-one") === "loading", "the cover waits for decoding and presentation")
|
||||
test.check(intro.themeStatus("theme-one") === "pending", "app retints wait for the intro reveal")
|
||||
test.check(!Qt.colorEqual(Commons.Color.background, "#123456"), "the palette waits for OWE's first frame")
|
||||
intro.finishTheme("superseded-theme")
|
||||
test.check(intro.themeToken === "theme-one", "an older intro cannot release the pending handoff")
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 500
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.cover, "the still stays covered while the launcher waits")
|
||||
test.check(intro.themeCoverStatus("theme-one") === "ready", "the outgoing cover is presented before the shell is hidden")
|
||||
retainedBackground.suspended = true
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.themeToken === "theme-one", "a leftover renderer still stays covered until video is revealed")
|
||||
revealVideo.running = true
|
||||
handoffDeadline = Date.now() + 3000
|
||||
handoff.start()
|
||||
}
|
||||
}
|
||||
property double handoffDeadline: 0
|
||||
property bool openingFadeObserved: false
|
||||
Connections {
|
||||
target: intro
|
||||
function onThemeTokenChanged() {
|
||||
if (handoff.running && intro.themeToken === "") {
|
||||
Qt.callLater(function() {
|
||||
test.check(intro.themeStatus("theme-one") === "pending", "app retints also wait through the opening crossfade")
|
||||
test.openingFadeObserved = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: handoff
|
||||
interval: 16
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (intro.themeToken || !test.openingFadeObserved) {
|
||||
if (Date.now() >= test.handoffDeadline) {
|
||||
test.check(false, "the ready video releases its cover before the deadline")
|
||||
Qt.quit()
|
||||
}
|
||||
return
|
||||
}
|
||||
stop()
|
||||
test.check(!intro.cover && intro.checked, "OWE taking the background releases the cover")
|
||||
test.check(intro.themeToken === "" && Qt.colorEqual(Commons.Color.background, "#123456"), "first-frame handoff starts the palette and wallpaper fade together")
|
||||
retainedBackground.suspended = false
|
||||
Qt.callLater(function() { test.check(!intro.cover, "resuming the retained background cannot cover or retry playback") })
|
||||
completionDeadline = Date.now() + 3000
|
||||
completion.start()
|
||||
}
|
||||
}
|
||||
property double completionDeadline: 0
|
||||
Timer {
|
||||
id: completion
|
||||
interval: 16
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (intro.themeStatus("theme-one") !== "ready" || !intro.startupSettled) {
|
||||
if (Date.now() >= test.completionDeadline) {
|
||||
test.check(false, "the fade and launcher complete before the deadline")
|
||||
Qt.quit()
|
||||
}
|
||||
return
|
||||
}
|
||||
stop()
|
||||
test.check(intro.themeStatus("theme-one") === "ready", "the completed crossfade releases app retints")
|
||||
intro.prepareTheme("", "failed-theme", Qt.btoa('background = "#654321"'), "")
|
||||
intro.finishTheme("failed-theme")
|
||||
test.check(Qt.colorEqual(Commons.Color.background, "#654321"), "failed playback still releases its pending palette")
|
||||
intro.prepareTheme("", "cancelled-theme", Qt.btoa('background = "#abcdef"'), "")
|
||||
intro.cancelTheme()
|
||||
intro.finishTheme("cancelled-theme")
|
||||
test.check(Qt.colorEqual(Commons.Color.background, "#654321") && !intro.themeToken, "a superseding theme cannot be overwritten by an older completion")
|
||||
test.check(!intro.cover, "launcher completion leaves the still uncovered")
|
||||
if (!test.failed) console.log("RESULT pass")
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons as Commons
|
||||
import "background" as BackgroundPlugin
|
||||
|
||||
ShellRoot {
|
||||
id: shell
|
||||
function firstPartyServiceFor(id) { return id === "omarchy.background" ? background : null }
|
||||
BackgroundPlugin.Background { id: background }
|
||||
|
||||
// APPLY_THEME_FUNCTION
|
||||
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
var oldColors = Qt.btoa('accent = "#ff0000"\nbackground = "#000000"\nforeground = "#ffffff"\n')
|
||||
var newColors = Qt.btoa('accent = "#123456"\nbackground = "#000000"\nforeground = "#ffffff"\n')
|
||||
background.transitionBackgroundWithTheme("", "/missing-still.png", "/missing-still.png", oldColors, "")
|
||||
shell.applyTheme(newColors, "")
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 1300
|
||||
running: true
|
||||
onTriggered: {
|
||||
if (Qt.colorEqual(Commons.Color.accent, "#123456")) {
|
||||
console.log("RESULT pass")
|
||||
} else {
|
||||
console.log("RESULT fail previous colors returned: " + Commons.Color.accent)
|
||||
}
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "services"
|
||||
import "background" as BackgroundPlugin
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var services: ({ "omarchy.background": background })
|
||||
property var bar: ({})
|
||||
property bool failed: false
|
||||
function firstPartyServiceFor(id) { return services[id] || null }
|
||||
function check(ok, message) {
|
||||
if (!ok) {
|
||||
failed = true
|
||||
console.log("RESULT fail " + message)
|
||||
}
|
||||
}
|
||||
|
||||
BackgroundPlugin.Background { id: background }
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(background.displayedBackground.endsWith("deleted.png"), "the missing wallpaper is selected")
|
||||
test.check(!background.ready && intro.startupPending && intro.cover, "the failed image cannot release startup through readiness")
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 11000
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!background.ready, "the missing wallpaper never becomes ready")
|
||||
test.check(!intro.startupPending && !intro.cover && intro.startupOpacity === 0, "the deadline fades the cover away despite the failed wallpaper")
|
||||
if (!test.failed) console.log("RESULT pass")
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "services"
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var services: ({})
|
||||
property var pluginRegistry: null
|
||||
property var bar: null
|
||||
property bool failed: false
|
||||
function firstPartyServiceFor(id) { return services[id] || null }
|
||||
function check(ok, message) {
|
||||
if (!ok) {
|
||||
failed = true
|
||||
console.log("RESULT fail " + message)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: registry
|
||||
property var installedPlugins: ({ "omarchy.background": {} })
|
||||
signal pluginsChanged()
|
||||
function resolveEnabledId(id) { return id }
|
||||
function isEnabled(id) { return false }
|
||||
}
|
||||
QtObject {
|
||||
id: background
|
||||
property bool suspended: false
|
||||
property bool ready: false
|
||||
}
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
FileView { id: cursorLog; path: Quickshell.env("STARTUP_CURSOR_LOG"); printErrors: false }
|
||||
|
||||
Timer {
|
||||
interval: 400
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!intro.backgroundActive, "the plugin has not loaded yet")
|
||||
test.check(intro.cover && !intro.themeBackground, "fresh startup does not prepare an outgoing still image")
|
||||
test.check(intro.startupSettled, "the launcher has finished without an intro")
|
||||
test.check(intro.startupPending && intro.startupOpacity === 1, "a fresh login hides the desktop until its media and bar are ready")
|
||||
cursorLog.reload()
|
||||
cursorLog.waitForJob()
|
||||
test.check(!cursorLog.text().trim(), "startup does not restore the cursor while media is loading")
|
||||
test.services = ({ "omarchy.background": background })
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.cover, "a loading plugin does not release the startup wallpaper")
|
||||
background.ready = true
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 800
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!intro.cover && !intro.themeBackground, "a ready plugin releases the startup cover and its image")
|
||||
test.check(intro.startupPending && intro.startupOpacity === 1, "a ready background alone cannot reveal an unready bar")
|
||||
cursorLog.reload()
|
||||
cursorLog.waitForJob()
|
||||
test.check(!cursorLog.text().trim(), "startup does not restore the cursor before the bar is ready")
|
||||
test.bar = ({})
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 1400
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!intro.startupPending && intro.startupOpacity === 0, "the desktop fades in after both the media and bar are ready")
|
||||
cursorLog.reload()
|
||||
cursorLog.waitForJob()
|
||||
test.check(cursorLog.text().includes("omarchy_startup_cursor_restore()"), "the opening fade restores the cursor")
|
||||
test.services = ({})
|
||||
intro.cover = true
|
||||
test.pluginRegistry = registry
|
||||
registry.pluginsChanged()
|
||||
test.check(!intro.cover, "a disabled background plugin does not leave a startup cover on screen")
|
||||
if (!test.failed) console.log("RESULT pass")
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,16 +198,6 @@ ShellRoot {
|
||||
root.assertTrue(root.commandCount("omarchy-capture-screenrecording --stop-recording") === 1, "Screen Recording left click stops active recording")
|
||||
}
|
||||
|
||||
var dictation = root.createIndicator("Dictation")
|
||||
if (dictation) {
|
||||
dictation.moduleName = "Dictation"
|
||||
root.injectBar(dictation)
|
||||
dictation.triggerPress(Qt.LeftButton)
|
||||
dictation.triggerPress(Qt.RightButton)
|
||||
root.assertTrue(root.commandCount("omarchy-voxtype-config") === 2, "Dictation clicks run config command")
|
||||
root.assertTrue(root.commandCount("omarchy-voxtype-model") === 0, "Dictation clicks do not run model command")
|
||||
}
|
||||
|
||||
var stayAwake = root.createIndicator("StayAwake")
|
||||
if (stayAwake) {
|
||||
stayAwake.moduleName = "StayAwake"
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Networking
|
||||
import qs.Commons
|
||||
import qs.Commons as Commons
|
||||
import "mocks"
|
||||
import "network" as Network
|
||||
|
||||
@@ -21,9 +22,9 @@ ShellRoot {
|
||||
Network.Panel {
|
||||
id: panel
|
||||
bar: QtObject {
|
||||
property color foreground: Color.foreground
|
||||
property color barForeground: Color.foreground
|
||||
property color urgent: Color.urgent
|
||||
property color foreground: Commons.Color.foreground
|
||||
property color barForeground: Commons.Color.foreground
|
||||
property color urgent: Commons.Color.urgent
|
||||
property string fontFamily: Style.font.family
|
||||
property string position: "top"
|
||||
property int barSize: 24
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons as Commons
|
||||
import "mocks"
|
||||
import "network" as Network
|
||||
import "network/Model.js" as Model
|
||||
@@ -25,9 +26,9 @@ ShellRoot {
|
||||
Network.Panel {
|
||||
id: panel
|
||||
bar: QtObject {
|
||||
property color foreground: Color.foreground
|
||||
property color barForeground: Color.foreground
|
||||
property color urgent: Color.urgent
|
||||
property color foreground: Commons.Color.foreground
|
||||
property color barForeground: Commons.Color.foreground
|
||||
property color urgent: Commons.Color.urgent
|
||||
property string fontFamily: Style.font.family
|
||||
property string position: "top"
|
||||
property int barSize: 24
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
|
||||
o = {
|
||||
shell_quote = function(value) return "'" .. value:gsub("'", "'\\''") .. "'" end,
|
||||
launch = function(value) return value end,
|
||||
}
|
||||
local exec = hl.exec_cmd
|
||||
hl.exec_cmd = function(command)
|
||||
if command == "omarchy-launch-shell" then
|
||||
local file = assert(io.open(os.getenv("CURSOR_TEST_STAGE") .. "/session", "w"))
|
||||
file:write(os.getenv("WAYLAND_DISPLAY") .. "\n" .. os.getenv("HYPRLAND_INSTANCE_SIGNATURE"))
|
||||
file:close()
|
||||
exec("quickshell -p " .. o.shell_quote(os.getenv("CURSOR_TEST_STAGE")))
|
||||
elseif command:match("^hyprctl ") then
|
||||
exec(command)
|
||||
end
|
||||
end
|
||||
require("default.hypr.autostart")
|
||||
hl.config({
|
||||
misc = { disable_hyprland_logo = true, disable_splash_rendering = true, background_color = "rgb(000000)", disable_watchdog_warning = true },
|
||||
xwayland = { enabled = false },
|
||||
cursor = { enable_hyprcursor = false },
|
||||
})
|
||||
hl.monitor({ output = "", mode = "640x480@60", position = "auto", scale = 1 })
|
||||
hl.layer_rule({ match = { namespace = "omarchy-background" }, no_anim = true })
|
||||
hl.bind("SUPER + Q", hl.dsp.exec_cmd("true"))
|
||||
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import "services"
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var bar: ({})
|
||||
property var services: ({ "omarchy.background": background })
|
||||
function firstPartyServiceFor(id) { return background }
|
||||
QtObject { id: background; property bool suspended: false; property bool ready: false }
|
||||
BackgroundIntro {
|
||||
id: intro
|
||||
host: test
|
||||
onStartupPendingChanged: if (!startupPending) phase.setText("revealed")
|
||||
}
|
||||
FileView {
|
||||
id: release
|
||||
path: Quickshell.env("CURSOR_TEST_STAGE") + "/release"
|
||||
watchChanges: true
|
||||
onLoaded: background.ready = text().trim() === "reveal"
|
||||
onFileChanged: reload()
|
||||
}
|
||||
FileView {
|
||||
id: phase
|
||||
path: Quickshell.env("CURSOR_TEST_STAGE") + "/phase"
|
||||
atomicWrites: true
|
||||
}
|
||||
PanelWindow {
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Bottom
|
||||
mask: Region {}
|
||||
color: "magenta"
|
||||
}
|
||||
Component.onCompleted: phase.setText("holding")
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import pathlib
|
||||
import pwd
|
||||
import signal
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
root = pathlib.Path(os.environ['ROOT'])
|
||||
with tempfile.TemporaryDirectory(prefix='theme-ssh-') as directory:
|
||||
stage = pathlib.Path(directory)
|
||||
for name in ('host', 'identity'):
|
||||
subprocess.run(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', str(stage / name)], check=True)
|
||||
try:
|
||||
with socket.socket() as port_probe:
|
||||
port_probe.bind(('127.0.0.1', 0))
|
||||
port = port_probe.getsockname()[1]
|
||||
except PermissionError:
|
||||
print('ok - sandbox blocks loopback sockets; skipping real SSH connection reuse # SKIP')
|
||||
raise SystemExit(0)
|
||||
remote = stage / 'remote'
|
||||
remote.write_text('#!/bin/bash\ncat >>"' + str(stage / 'received') + '"\necho theme-received\n')
|
||||
remote.chmod(0o700)
|
||||
server_config = stage / 'sshd.conf'
|
||||
server_config.write_text(f'''Port {port}
|
||||
ListenAddress 127.0.0.1
|
||||
HostKey {stage / 'host'}
|
||||
PidFile {stage / 'server.pid'}
|
||||
AuthorizedKeysFile {stage / 'identity.pub'}
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
UsePAM no
|
||||
StrictModes no
|
||||
AllowUsers {pwd.getpwuid(os.getuid()).pw_name}
|
||||
LogLevel VERBOSE
|
||||
ForceCommand /bin/bash {remote}
|
||||
''')
|
||||
host_key = (stage / 'host.pub').read_text().split()
|
||||
(stage / 'known_hosts').write_text(f'[127.0.0.1]:{port} {host_key[0]} {host_key[1]}\n')
|
||||
client_config = stage / 'ssh.conf'
|
||||
client_config.write_text(f'''Host fixture
|
||||
HostName 127.0.0.1
|
||||
Port {port}
|
||||
User {pwd.getpwuid(os.getuid()).pw_name}
|
||||
IdentityFile {stage / 'identity'}
|
||||
IdentityAgent none
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking yes
|
||||
UserKnownHostsFile {stage / 'known_hosts'}
|
||||
''')
|
||||
home = stage / 'home'
|
||||
current = home / '.local/state/omarchy/current'
|
||||
current.mkdir(parents=True)
|
||||
toggles = home / '.local/state/omarchy/toggles'
|
||||
toggles.mkdir()
|
||||
(toggles / 'herdr-theme-sync').touch()
|
||||
runtime = stage / 'run'
|
||||
runtime.mkdir(mode=0o700)
|
||||
stub_bin = stage / 'bin'
|
||||
stub_bin.mkdir()
|
||||
scripts = {
|
||||
'herdr': "#!/bin/bash\nprintf 'id\\tfixture\\tfixture\\tdefault\\tenabled\\n'\n",
|
||||
'ssh': '#!/bin/bash\nexec /usr/bin/ssh -F "' + str(client_config) + '" "$@"\n',
|
||||
}
|
||||
for name, content in scripts.items():
|
||||
path = stub_bin / name
|
||||
path.write_text(content)
|
||||
path.chmod(0o700)
|
||||
environment = {**os.environ, 'HOME': str(home), 'XDG_RUNTIME_DIR': str(runtime), 'PATH': str(stub_bin) + ':' + str(root / 'bin') + ':' + os.environ['PATH']}
|
||||
log_path = stage / 'sshd.log'
|
||||
with log_path.open('w') as log:
|
||||
server = subprocess.Popen([shutil.which('sshd'), '-D', '-e', '-f', str(server_config)], stdout=log, stderr=log, start_new_session=True)
|
||||
try:
|
||||
deadline = time.monotonic() + 5
|
||||
while True:
|
||||
try:
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=0.1):
|
||||
break
|
||||
except OSError:
|
||||
if server.poll() is not None and 'Missing privilege separation directory' in log_path.read_text():
|
||||
print('ok - SSH server runtime directory is unavailable; skipping real SSH connection reuse # SKIP')
|
||||
raise SystemExit(0)
|
||||
assert server.poll() is None and time.monotonic() < deadline, log_path.read_text()
|
||||
time.sleep(0.05)
|
||||
for theme in ('tokyo-night', 'nord'):
|
||||
(current / 'theme.name').write_text(theme + '\n')
|
||||
result = subprocess.run([str(root / 'bin/omarchy-theme-set-herdr-machines')], env=environment, capture_output=True, text=True, timeout=10)
|
||||
assert result.returncode == 0 and 'theme-received' in result.stdout, (result.stdout, result.stderr, log_path.read_text())
|
||||
if theme == 'tokyo-night':
|
||||
(stage / 'identity').rename(stage / 'identity-unavailable')
|
||||
assert log_path.read_text().count('Accepted publickey') == 1, log_path.read_text()
|
||||
received = (stage / 'received').read_text()
|
||||
assert 'theme=tokyo-night' in received and 'theme=nord' in received
|
||||
assert (runtime / 'omarchy-theme-sync').stat().st_mode & 0o777 == 0o700
|
||||
lock = runtime / 'omarchy-theme-set-herdr-machines.lock'
|
||||
assert subprocess.run(['flock', '-n', str(lock), 'true']).returncode == 0, 'SSH master retained the theme sync lock'
|
||||
print('ok - two theme syncs authenticate once, even with the key removed before the second sync')
|
||||
print('ok - persistent SSH master leaves the sync lock available')
|
||||
finally:
|
||||
subprocess.run(['/usr/bin/ssh', '-F', str(client_config), '-o', 'ControlPath=' + str(runtime / 'omarchy-theme-sync/%C'), '-O', 'exit', 'fixture'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)
|
||||
if server.poll() is None:
|
||||
os.killpg(server.pid, signal.SIGTERM)
|
||||
server.wait(timeout=5)
|
||||
Reference in new issue
Block a user