Watch
1
0
Fork
You've already forked souveraine
0

usb: build the full KVM composite surface

This commit is contained in:
Fimeg 2026-08-07 14:48:07 -04:00
commit 8cd2adb828
15 changed files with 1562 additions and 47 deletions

View file

@ -43,6 +43,7 @@ services/ChargeRate.qml souveraine/services/ChargeRate.qml
services/Haptics.qml souveraine/services/Haptics.qml
services/Selection.qml souveraine/services/Selection.qml
services/DeviceEvidence.qml souveraine/services/DeviceEvidence.qml
services/UsbState.qml souveraine/services/UsbState.qml
services/Gestures.qml souveraine/services/Gestures.qml
modules/souveraine/dial/RadialDial.qml souveraine/modules/souveraine/dial/RadialDial.qml
modules/souveraine/dial/DialHost.qml souveraine/modules/souveraine/dial/DialHost.qml
@ -56,6 +57,7 @@ modules/souveraine/selection/qmldir souveraine/modules/souveraine/selection/q
modules/souveraine/windowSheet/WindowSheet.qml souveraine/modules/souveraine/windowSheet/WindowSheet.qml
modules/souveraine/windowSheet/SheetButton.qml souveraine/modules/souveraine/windowSheet/SheetButton.qml
modules/souveraine/windowSheet/PowerMenu.qml souveraine/modules/souveraine/windowSheet/PowerMenu.qml
modules/souveraine/windowSheet/PowerOptionRow.qml souveraine/modules/souveraine/windowSheet/PowerOptionRow.qml
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
services/Face.qml souveraine/services/Face.qml
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml

View file

@ -15,12 +15,11 @@
//
// ## What is honest here today
//
// The data role is real and readable `/sys/class/usb_role//role` reports
// `device`, and `pmi8998-charger` reports `online=1`, so "attached, drawing
// power" is a measured fact rather than a guess. **Switching** the role is not
// offered yet: the sysfs node is root-owned, so the flip has to be a sessiond
// verb, and a button that cannot do the thing it names is the exact failure
// this project keeps writing down. It is shown as state, and it says so.
// The data role and charger attachment are measured rather than guessed.
// UsbState reads sessiond's combined snapshot: usb-signaller owns the configfs
// mechanism, souveraine-upower is adjacent charging evidence, and sessiond is
// the authority that can turn a requested posture into an audited Action.
// This surface reads and asks; it never writes sysfs or calls the gadget daemon.
//
// There is no `/sys/class/typec/` on this device the tcpm driver is not
// bound so the *power* role genuinely is not knowable here yet. The row
@ -48,20 +47,27 @@ Scope {
id: scope
property bool menuOpen: false
property bool powerOptionsOpen: false
property bool attachmentOptionsOpen: false
property bool batteryOptionsOpen: false
property bool hidOptionsOpen: false
// Measured, not assumed. Re-read each time the menu opens rather than
// polled: the port's state only matters while someone is looking at it,
// and a timer here would be a background reader of a file nobody needs.
property string dataRole: "unknown"
property bool chargerOnline: false
readonly property string dataRole: UsbState.dataRole
readonly property bool chargerOnline: UsbState.chargerOnline
function refresh() {
roleProc.running = true;
chargerProc.running = true;
UsbState.refresh();
}
function open() {
scope.refresh();
scope.powerOptionsOpen = false;
scope.attachmentOptionsOpen = false;
scope.batteryOptionsOpen = false;
scope.hidOptionsOpen = false;
scope.menuOpen = true;
}
@ -85,22 +91,6 @@ Scope {
}
}
Process {
id: roleProc
command: ["cat", "/sys/class/usb_role/a600000.usb-role-switch/role"]
stdout: StdioCollector {
onStreamFinished: scope.dataRole = text.trim() || "unknown"
}
}
Process {
id: chargerProc
command: ["cat", "/sys/class/power_supply/pmi8998-charger/online"]
stdout: StdioCollector {
onStreamFinished: scope.chargerOnline = text.trim() === "1"
}
}
Variants {
model: Quickshell.screens
@ -129,7 +119,8 @@ Scope {
ColumnLayout {
anchors.centerIn: parent
spacing: 30
width: Math.min(win.width - 32, 500)
spacing: 22
RowLayout {
Layout.alignment: Qt.AlignHCenter
@ -170,16 +161,239 @@ Scope {
}
}
// The port, as measured. Not a control yet see the header.
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: win.width * 0.8
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: "#c8ffffff"
font.pixelSize: Appearance.font.pixelSize.small
text: "USB-C: " + scope.dataRole
+ (scope.chargerOnline ? " · charger attached" : " · no charger")
// One gentle door into the more peculiar things the phone can
// become. Live leaves ask sessiond; unfinished leaves keep their
// future shape without pretending that a tap did anything.
ColumnLayout {
Layout.fillWidth: true
spacing: 9
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "power"
title: "Power Options"
detail: "USB roles, KVM and hardware modes"
expandable: true
expanded: scope.powerOptionsOpen
onTapped: {
scope.powerOptionsOpen = !scope.powerOptionsOpen;
if (!scope.powerOptionsOpen) {
scope.attachmentOptionsOpen = false;
scope.batteryOptionsOpen = false;
scope.hidOptionsOpen = false;
}
}
}
ColumnLayout {
Layout.fillWidth: true
visible: scope.powerOptionsOpen
spacing: 8
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "usb"
title: "Port & attachment"
detail: scope.dataRole
+ (!UsbState.chargerKnown ? " · power unknown"
: scope.chargerOnline ? " · drawing power" : " · no charger")
badge: UsbState.available ? "live" : "waiting"
inset: 10
expandable: true
expanded: scope.attachmentOptionsOpen
onTapped: {
scope.attachmentOptionsOpen = !scope.attachmentOptionsOpen;
if (scope.attachmentOptionsOpen) {
scope.batteryOptionsOpen = false;
scope.hidOptionsOpen = false;
}
}
}
ColumnLayout {
Layout.fillWidth: true
visible: scope.attachmentOptionsOpen
spacing: 7
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "fingerprint"
title: "Attached identity"
detail: UsbState.attachedIdentity
? UsbState.attachedIdentity : "USB enumeration + federation handshake"
badge: UsbState.attachedIdentity ? "known" : "next"
inset: 28
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "hub"
title: "Probe ownership"
detail: UsbState.probeOwner
? UsbState.probeOwner : "Which trusted node already has instruments here"
badge: UsbState.probeOwner ? "claimed" : "next"
inset: 28
}
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "battery_android_frame_shield"
title: "Battery care"
detail: "Charge ceiling, resume floor, agent discretion"
badge: "policy"
inset: 10
expandable: true
expanded: scope.batteryOptionsOpen
onTapped: {
scope.batteryOptionsOpen = !scope.batteryOptionsOpen;
if (scope.batteryOptionsOpen) {
scope.attachmentOptionsOpen = false;
scope.hidOptionsOpen = false;
}
}
}
ColumnLayout {
Layout.fillWidth: true
visible: scope.batteryOptionsOpen
spacing: 7
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "battery_full_alt"
title: "Charge ceiling"
detail: "Stay below 100% inside a human safety envelope"
badge: "draft"
inset: 28
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "battery_horiz_050"
title: "Resume floor"
detail: "Wide hysteresis instead of twitching at 98/99"
badge: "draft"
inset: 28
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "neurology"
title: "Agent-managed"
detail: "May move the pair; every change is bounded and written"
badge: "planned"
inset: 28
}
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "keyboard"
title: "USB HID Injection"
detail: "Let this screen become the other machine's hands"
badge: UsbState.mode === "hid" ? "active"
: UsbState.hasMode("hid_mode") ? "wired" : "building"
inset: 10
expandable: true
expanded: scope.hidOptionsOpen
onTapped: {
scope.hidOptionsOpen = !scope.hidOptionsOpen;
if (scope.hidOptionsOpen) {
scope.attachmentOptionsOpen = false;
scope.batteryOptionsOpen = false;
}
}
}
ColumnLayout {
Layout.fillWidth: true
visible: scope.hidOptionsOpen
spacing: 7
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "keyboard_mouse"
title: "Keyboard + pointer"
detail: UsbState.mode === "hid"
? "Active · tap to restore developer USB"
: "Tap to arm boot keyboard, pointer and NCM"
badge: UsbState.busy ? "switching"
: UsbState.mode === "hid" ? "active"
: UsbState.hasMode("hid_mode") ? "ready" : "not installed"
inset: 28
available: UsbState.hasMode("hid_mode") && !UsbState.busy
onTapped: UsbState.setMode(
UsbState.mode === "hid" ? "developer" : "hid")
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "text_fields"
title: "Type text…"
detail: "A guarded text payload for the attached host"
badge: "planned"
inset: 28
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "security"
title: "Secure attention"
detail: "Hold to send Ctrl + Alt + Delete"
badge: "planned"
inset: 28
}
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "desktop_windows"
title: "Full USB KVM"
detail: UsbState.mode === "kvm"
? "Active · tap to restore developer USB"
: "GUD display · HID input · NCM control · smoo"
badge: UsbState.busy ? "switching"
: UsbState.mode === "kvm" ? "active"
: UsbState.hasMode("kvm_mode") ? "ready" : "not installed"
inset: 10
available: UsbState.hasMode("kvm_mode") && !UsbState.busy
onTapped: UsbState.setMode(
UsbState.mode === "kvm" ? "developer" : "kvm")
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "hard_drive"
title: "Host-backed volume"
detail: "Mount storage offered by the attached host"
badge: UsbState.mode === "kvm" ? "active" : "smoo"
inset: 10
}
SheetLocal.PowerOptionRow {
Layout.fillWidth: true
icon: "electrical_services"
title: "USB host + power share"
detail: "Become the host and source 5 V"
badge: "kernel"
inset: 10
}
}
// The port, as measured. Not a control yet see the
// architecture note at the top of this file.
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: win.width * 0.8
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: "#aaffffff"
font.pixelSize: Appearance.font.pixelSize.small
text: "USB-C: " + scope.dataRole
+ (!UsbState.chargerKnown ? " · power unknown"
: scope.chargerOnline ? " · charger attached" : " · no charger")
}
}
}
}

View file

@ -0,0 +1,99 @@
// One nested choice in the held-power-button sheet.
//
// `available` is deliberately separate from Item.enabled. An unavailable
// option still opens when it owns children, so the hand can see the shape of
// the mode being built; a leaf that cannot act simply has no click target and
// wears its status instead of pretending a tap did something.
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Rectangle {
id: root
property string icon: ""
property string title: ""
property string detail: ""
property string badge: ""
property bool expandable: false
property bool expanded: false
property bool available: false
property int inset: 0
signal tapped
implicitHeight: detail === "" ? 58 : 70
radius: 18
color: hit.pressed
? Appearance.colors.colLayer2
: Appearance.colors.colLayer1
border.width: 1
border.color: Appearance.colors.colLayer1Active
opacity: available || expandable ? 1.0 : 0.72
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16 + root.inset
anchors.rightMargin: 16
spacing: 13
MaterialSymbol {
text: root.icon
iconSize: 27
color: Appearance.colors.colOnLayer1
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
StyledText {
Layout.fillWidth: true
text: root.title
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.normal
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
visible: root.detail !== ""
text: root.detail
color: "#aaffffff"
font.pixelSize: Appearance.font.pixelSize.small
elide: Text.ElideRight
}
}
Rectangle {
visible: root.badge !== ""
implicitWidth: badgeText.implicitWidth + 18
implicitHeight: 26
radius: 13
color: Appearance.colors.colLayer2
StyledText {
id: badgeText
anchors.centerIn: parent
text: root.badge
color: "#c8ffffff"
font.pixelSize: Appearance.font.pixelSize.smaller
}
}
MaterialSymbol {
visible: root.expandable
text: root.expanded ? "expand_less" : "expand_more"
iconSize: 25
color: Appearance.colors.colOnLayer1
}
}
MouseArea {
id: hit
anchors.fill: parent
enabled: root.available || root.expandable
onClicked: root.tapped()
}
}

View file

@ -1,3 +1,4 @@
PowerMenu 1.0 PowerMenu.qml
PowerOptionRow 1.0 PowerOptionRow.qml
SheetButton 1.0 SheetButton.qml
WindowSheet 1.0 WindowSheet.qml

View file

@ -0,0 +1,112 @@
// One typed view over sessiond's USB posture and mode verbs.
//
// usb-signaller owns configfs, souveraine-upower supplies adjacent charging
// evidence, and federation/probe leases will supply who is attached and who is
// already working there. The shell owns none of it; it asks sessiond and shows
// the answer.
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property bool available: false
property bool busy: false
property string lastError: ""
property string mode: "unknown"
property string rawMode: "unknown_mode"
property var availableModes: []
property string dataRole: "unknown"
property bool chargerKnown: false
property bool chargerOnline: false
property var attachedIdentity: null
property var probeOwner: null
signal refreshed()
signal changeFailed(string reason)
function refresh() {
root._send({ op: "usb" });
}
function setMode(mode) {
if (!["developer", "hid", "kvm", "charging_only"].includes(mode)) {
root.changeFailed("unsupported USB mode: " + mode);
return;
}
root.busy = true;
root._send({ op: "set_usb_mode", mode: mode });
}
function hasMode(rawMode) {
return root.availableModes.includes(rawMode);
}
property var _queued: null
function _send(message) {
if (sock.connected) {
sock.write(JSON.stringify(message) + "\n");
return;
}
root._queued = message;
sock.connected = true;
}
Socket {
id: sock
path: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/sessiond.sock"
onConnectionStateChanged: {
if (connected && root._queued) {
const message = root._queued;
root._queued = null;
sock.write(JSON.stringify(message) + "\n");
} else if (!connected && root._queued) {
root._queued = null;
root.available = false;
root.busy = false;
root.lastError = "sessiond socket unavailable";
root.changeFailed(root.lastError);
}
}
parser: SplitParser {
splitMarker: "\n"
onRead: message => {
let reply;
try {
reply = JSON.parse(message);
} catch (error) {
root.lastError = "unparseable USB reply";
root.busy = false;
root.changeFailed(root.lastError);
return;
}
if (reply.ok !== true) {
root.lastError = reply.reason || "USB request refused";
root.busy = false;
root.changeFailed(root.lastError);
return;
}
root.mode = reply.mode ?? "unknown";
root.rawMode = reply.raw_mode ?? "unknown_mode";
root.availableModes = reply.available_modes ?? [];
root.dataRole = reply.data_role ?? "unknown";
root.chargerKnown = reply.charger_online !== null
&& reply.charger_online !== undefined;
root.chargerOnline = reply.charger_online === true;
root.attachedIdentity = reply.attached_identity ?? null;
root.probeOwner = reply.probe_owner ?? null;
root.available = true;
root.busy = false;
root.lastError = "";
root.refreshed();
}
}
}
}

View file

@ -63,6 +63,7 @@ singleton Todo 1.0 Todo.qml
singleton Translation 1.0 Translation.qml
singleton TrayService 1.0 TrayService.qml
singleton Updates 1.0 Updates.qml
singleton UsbState 1.0 UsbState.qml
singleton ViewtopControl 1.0 ViewtopControl.qml
singleton WallpaperAssets 1.0 WallpaperAssets.qml
singleton WallpaperDownload 1.0 WallpaperDownload.qml