usb: build the full KVM composite surface
This commit is contained in:
parent
16c6ee1033
commit
8cd2adb828
15 changed files with 1562 additions and 47 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
PowerMenu 1.0 PowerMenu.qml
|
||||
PowerOptionRow 1.0 PowerOptionRow.qml
|
||||
SheetButton 1.0 SheetButton.qml
|
||||
WindowSheet 1.0 WindowSheet.qml
|
||||
|
|
|
|||
112
surfaces/quickshell/services/UsbState.qml
Normal file
112
surfaces/quickshell/services/UsbState.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue