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
|
|
@ -53,6 +53,8 @@ jobs:
|
||||||
# old primary/public split: a signal that reads like coverage and isn't.
|
# old primary/public split: a signal that reads like coverage and isn't.
|
||||||
- name: cargo test (sessiond)
|
- name: cargo test (sessiond)
|
||||||
run: cargo test --features sessiond --bin souveraine-sessiond
|
run: cargo test --features sessiond --bin souveraine-sessiond
|
||||||
|
- name: cargo test (USB KVM receiver)
|
||||||
|
run: cargo test --features usb-kvm --bin souveraine-usb-kvm
|
||||||
- name: cargo clippy
|
- name: cargo clippy
|
||||||
run: cargo clippy -- -D warnings
|
run: cargo clippy -- -D warnings
|
||||||
# Advisory, deliberately. Turning this on for the first time surfaced 9
|
# Advisory, deliberately. Turning this on for the first time surfaced 9
|
||||||
|
|
@ -64,6 +66,8 @@ jobs:
|
||||||
- name: cargo clippy (sessiond)
|
- name: cargo clippy (sessiond)
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: cargo clippy --features sessiond --bin souveraine-sessiond -- -D warnings
|
run: cargo clippy --features sessiond --bin souveraine-sessiond -- -D warnings
|
||||||
|
- name: cargo clippy (USB KVM receiver)
|
||||||
|
run: cargo clippy --features usb-kvm --bin souveraine-usb-kvm -- -D warnings
|
||||||
|
|
||||||
no-ai-attribution:
|
no-ai-attribution:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -157,6 +161,12 @@ jobs:
|
||||||
file "$NBIN" | grep -q aarch64 || { echo "sensord is not aarch64" >&2; exit 1; }
|
file "$NBIN" | grep -q aarch64 || { echo "sensord is not aarch64" >&2; exit 1; }
|
||||||
cp "$NBIN" souveraine-sensord-aarch64
|
cp "$NBIN" souveraine-sensord-aarch64
|
||||||
|
|
||||||
|
# Full USB KVM receiver: GUD display-in and the HID return surface.
|
||||||
|
./scripts/build-cross.sh --features usb-kvm --bin souveraine-usb-kvm
|
||||||
|
UBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-usb-kvm"
|
||||||
|
file "$UBIN" | grep -q aarch64 || { echo "usb-kvm is not aarch64" >&2; exit 1; }
|
||||||
|
cp "$UBIN" souveraine-usb-kvm-aarch64
|
||||||
|
|
||||||
# The wry host: self-hosted sites as apps, and the rig she wears.
|
# The wry host: self-hosted sites as apps, and the rig she wears.
|
||||||
# Needs webkit2gtk on the build sysroot; it is already installed on
|
# Needs webkit2gtk on the build sysroot; it is already installed on
|
||||||
# the phone (TASK-59 phase 1 measured against it).
|
# the phone (TASK-59 phase 1 measured against it).
|
||||||
|
|
@ -218,6 +228,7 @@ jobs:
|
||||||
|
|
||||||
sha256sum souveraine-aarch64 souveraine-x86_64 \
|
sha256sum souveraine-aarch64 souveraine-x86_64 \
|
||||||
souveraine-sessiond-aarch64 souveraine-sensord-aarch64 \
|
souveraine-sessiond-aarch64 souveraine-sensord-aarch64 \
|
||||||
|
souveraine-usb-kvm-aarch64 \
|
||||||
souveraine-secrets-aarch64 souveraine-secrets-x86_64 \
|
souveraine-secrets-aarch64 souveraine-secrets-x86_64 \
|
||||||
souveraine-machined-aarch64 souveraine-machined-x86_64 \
|
souveraine-machined-aarch64 souveraine-machined-x86_64 \
|
||||||
souveraine-sessiond-x86_64 \
|
souveraine-sessiond-x86_64 \
|
||||||
|
|
@ -375,6 +386,7 @@ jobs:
|
||||||
cp packaging/souveraine-sessiond.service "$PKG_WORK/"
|
cp packaging/souveraine-sessiond.service "$PKG_WORK/"
|
||||||
cp "souveraine-sensord-$ARCH" "$PKG_WORK/souveraine-sensord-binary"
|
cp "souveraine-sensord-$ARCH" "$PKG_WORK/souveraine-sensord-binary"
|
||||||
cp packaging/souveraine-sensord.service "$PKG_WORK/"
|
cp packaging/souveraine-sensord.service "$PKG_WORK/"
|
||||||
|
cp "souveraine-usb-kvm-$ARCH" "$PKG_WORK/souveraine-usb-kvm-binary"
|
||||||
# The wry host. Guarded, and the PKGBUILD source line is removed
|
# The wry host. Guarded, and the PKGBUILD source line is removed
|
||||||
# when it is absent: it needs webkit2gtk in the aarch64 sysroot,
|
# when it is absent: it needs webkit2gtk in the aarch64 sysroot,
|
||||||
# and a missing avatar must not stop sessiond reaching the phone.
|
# and a missing avatar must not stop sessiond reaching the phone.
|
||||||
|
|
@ -398,6 +410,7 @@ jobs:
|
||||||
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine'
|
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine'
|
||||||
if [ "$ARCH" = aarch64 ]; then
|
if [ "$ARCH" = aarch64 ]; then
|
||||||
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sessiond'
|
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sessiond'
|
||||||
|
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-usb-kvm'
|
||||||
fi
|
fi
|
||||||
cp "$PKG" "$ARCH_REPO/"
|
cp "$PKG" "$ARCH_REPO/"
|
||||||
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
|
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
|
||||||
|
|
|
||||||
187
Cargo.lock
generated
187
Cargo.lock
generated
|
|
@ -2112,7 +2112,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
|
checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctor-proc-macro",
|
"ctor-proc-macro",
|
||||||
"dtor",
|
"dtor 0.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctor"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf"
|
||||||
|
dependencies = [
|
||||||
|
"dtor 0.8.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2669,6 +2678,46 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "drm"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"bytemuck",
|
||||||
|
"drm-ffi",
|
||||||
|
"drm-fourcc",
|
||||||
|
"libc",
|
||||||
|
"rustix 0.38.44",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "drm-ffi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690"
|
||||||
|
dependencies = [
|
||||||
|
"drm-sys",
|
||||||
|
"rustix 1.1.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "drm-fourcc"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "drm-sys"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys 0.9.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
|
@ -2693,6 +2742,12 @@ dependencies = [
|
||||||
"dtor-proc-macro",
|
"dtor-proc-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dtor"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtor-proc-macro"
|
name = "dtor-proc-macro"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
|
|
@ -2766,6 +2821,12 @@ version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
|
@ -3404,6 +3465,27 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gadgetry-most-foul"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "706c81a929c7009eaa6427d32b3af28ce5dd2133abafe63ff79f9c8fd79e849d"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"byteorder",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"macaddr",
|
||||||
|
"proc-mounts",
|
||||||
|
"rustix 1.1.4",
|
||||||
|
"slab",
|
||||||
|
"strum 0.28.0",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gdk"
|
name = "gdk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
|
|
@ -3897,6 +3979,21 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gud-gadget"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/samcday/gud-gadget?rev=33024b6152633eb8359375419281fa3ebac9c9cb#33024b6152633eb8359375419281fa3ebac9c9cb"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bytes",
|
||||||
|
"gadgetry-most-foul",
|
||||||
|
"lz4",
|
||||||
|
"serde",
|
||||||
|
"ssmarshal",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
|
@ -5140,6 +5237,12 @@ version = "0.4.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -5210,6 +5313,25 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lz4"
|
||||||
|
version = "1.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4"
|
||||||
|
dependencies = [
|
||||||
|
"lz4-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lz4-sys"
|
||||||
|
version = "1.11.1+lz4-1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lz4_flex"
|
name = "lz4_flex"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
|
|
@ -5226,6 +5348,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macaddr"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mach2"
|
name = "mach2"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
@ -6696,6 +6824,15 @@ dependencies = [
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "partition-identity"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b25ada56da40bd4266a924109037ec9a9c59306e002dd2c30b00c0c3d648b93"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "password-hash"
|
name = "password-hash"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -7201,6 +7338,15 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-mounts"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d652f8435d0ab70bf4f3590a6a851d59604831a458086541b95238cc51ffcf2"
|
||||||
|
dependencies = [
|
||||||
|
"partition-identity",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "profiling"
|
name = "profiling"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
|
@ -9105,8 +9251,12 @@ version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
|
checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"as-raw-xcb-connection",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
"drm",
|
||||||
|
"fastrand",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"memmap2",
|
||||||
"ndk 0.9.0",
|
"ndk 0.9.0",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
|
@ -9115,10 +9265,16 @@ dependencies = [
|
||||||
"objc2-quartz-core 0.3.2",
|
"objc2-quartz-core 0.3.2",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"redox_syscall 0.5.18",
|
"redox_syscall 0.5.18",
|
||||||
|
"rustix 1.1.4",
|
||||||
|
"tiny-xlib",
|
||||||
"tracing",
|
"tracing",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-sys",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9177,9 +9333,11 @@ dependencies = [
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"figlet-rs",
|
"figlet-rs",
|
||||||
"futures",
|
"futures",
|
||||||
|
"gadgetry-most-foul",
|
||||||
"git2",
|
"git2",
|
||||||
"glob",
|
"glob",
|
||||||
"gtk",
|
"gtk",
|
||||||
|
"gud-gadget",
|
||||||
"hex",
|
"hex",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"hound",
|
"hound",
|
||||||
|
|
@ -9206,6 +9364,7 @@ dependencies = [
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"sha2",
|
"sha2",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
|
"softbuffer",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tao 0.36.0",
|
"tao 0.36.0",
|
||||||
|
|
@ -9232,6 +9391,7 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols",
|
"wayland-protocols",
|
||||||
|
"winit",
|
||||||
"wry 0.56.0",
|
"wry 0.56.0",
|
||||||
"zbus",
|
"zbus",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
|
|
@ -9476,6 +9636,16 @@ dependencies = [
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssmarshal"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
@ -10233,7 +10403,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"brotli",
|
"brotli",
|
||||||
"cargo_metadata",
|
"cargo_metadata",
|
||||||
"ctor",
|
"ctor 0.8.0",
|
||||||
"dom_query 0.27.0",
|
"dom_query 0.27.0",
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
|
|
@ -10510,6 +10680,19 @@ dependencies = [
|
||||||
"strict-num",
|
"strict-num",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-xlib"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4"
|
||||||
|
dependencies = [
|
||||||
|
"as-raw-xcb-connection",
|
||||||
|
"ctor 0.10.1",
|
||||||
|
"libloading 0.8.9",
|
||||||
|
"pkg-config",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
|
||||||
16
Cargo.toml
16
Cargo.toml
|
|
@ -73,6 +73,13 @@ ratatui-ratty = { version = "0.2", optional = true } # Inline 3D graphics via R
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
# Full USB KVM receiver. Optional because only the aarch64 phone package ships
|
||||||
|
# it; the main substrate and desktop builds do not need a windowing stack.
|
||||||
|
gadgetry-most-foul = { version = "0.1.1", optional = true }
|
||||||
|
gud-gadget = { git = "https://github.com/samcday/gud-gadget", rev = "33024b6152633eb8359375419281fa3ebac9c9cb", optional = true }
|
||||||
|
softbuffer = { version = "0.4", optional = true }
|
||||||
|
winit = { version = "0.30", optional = true }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
@ -215,6 +222,11 @@ required-features = ["sessiond"]
|
||||||
name = "souveraine-sensord"
|
name = "souveraine-sensord"
|
||||||
path = "src/bin/souveraine-sensord.rs"
|
path = "src/bin/souveraine-sensord.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "souveraine-usb-kvm"
|
||||||
|
path = "src/bin/souveraine-usb-kvm.rs"
|
||||||
|
required-features = ["usb-kvm"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
|
|
@ -238,10 +250,14 @@ gui = ["tuie/gui"]
|
||||||
secrets = ["dep:zbus", "dep:zvariant", "dep:aes", "dep:cbc", "dep:cipher", "dep:hkdf", "dep:num-bigint", "dep:num-traits", "dep:aes-gcm", "dep:argon2"]
|
secrets = ["dep:zbus", "dep:zvariant", "dep:aes", "dep:cbc", "dep:cipher", "dep:hkdf", "dep:num-bigint", "dep:num-traits", "dep:aes-gcm", "dep:argon2"]
|
||||||
# Session authority daemon — holds ext-session-lock before/under the shell.
|
# Session authority daemon — holds ext-session-lock before/under the shell.
|
||||||
sessiond = ["dep:wayland-client", "dep:wayland-protocols"]
|
sessiond = ["dep:wayland-client", "dep:wayland-protocols"]
|
||||||
|
usb-kvm = ["dep:gadgetry-most-foul", "dep:gud-gadget", "dep:softbuffer", "dep:winit"]
|
||||||
wry = ["dep:wry"]
|
wry = ["dep:wry"]
|
||||||
tao = ["dep:tao"]
|
tao = ["dep:tao"]
|
||||||
gtk = ["dep:gtk"]
|
gtk = ["dep:gtk"]
|
||||||
|
|
||||||
|
[patch."https://github.com/samcday/gadgetry-most-foul.git"]
|
||||||
|
gadgetry-most-foul = { version = "0.1.1" }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "souveraine-web"
|
name = "souveraine-web"
|
||||||
path = "src/bin/souveraine-web.rs"
|
path = "src/bin/souveraine-web.rs"
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,10 @@ sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP')
|
||||||
# rather than declaring them as sources for every arch.
|
# rather than declaring them as sources for every arch.
|
||||||
if [ "$CARCH" = aarch64 ]; then
|
if [ "$CARCH" = aarch64 ]; then
|
||||||
source+=('souveraine-sessiond-binary' 'souveraine-sessiond.service'
|
source+=('souveraine-sessiond-binary' 'souveraine-sessiond.service'
|
||||||
'souveraine-sensord-binary' 'souveraine-sensord.service')
|
'souveraine-sensord-binary' 'souveraine-sensord.service'
|
||||||
sha256sums+=('SKIP' 'SKIP' 'SKIP' 'SKIP')
|
'souveraine-usb-kvm-binary')
|
||||||
|
sha256sums+=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP')
|
||||||
|
depends+=('wayland' 'libx11' 'libxcursor' 'libxi' 'libxkbcommon' 'libxrandr')
|
||||||
# The wry host links the system webview, so it only cross-builds once
|
# The wry host links the system webview, so it only cross-builds once
|
||||||
# webkit2gtk is in the aarch64 sysroot. Declared only when CI actually
|
# webkit2gtk is in the aarch64 sysroot. Declared only when CI actually
|
||||||
# produced one: makepkg validates every entry in `source`, so naming a file
|
# produced one: makepkg validates every entry in `source`, so naming a file
|
||||||
|
|
@ -64,6 +66,10 @@ package() {
|
||||||
install -Dm644 "$srcdir/souveraine-sensord.service" \
|
install -Dm644 "$srcdir/souveraine-sensord.service" \
|
||||||
"$pkgdir/usr/lib/systemd/user/souveraine-sensord.service"
|
"$pkgdir/usr/lib/systemd/user/souveraine-sensord.service"
|
||||||
fi
|
fi
|
||||||
|
if [ -f "$srcdir/souveraine-usb-kvm-binary" ]; then
|
||||||
|
install -Dm755 "$srcdir/souveraine-usb-kvm-binary" \
|
||||||
|
"$pkgdir/usr/bin/souveraine-usb-kvm"
|
||||||
|
fi
|
||||||
# Hardware button reporter. Every arch: it decides nothing and depends on
|
# Hardware button reporter. Every arch: it decides nothing and depends on
|
||||||
# nothing, and a laptop with a power button is the same shape. Replaces
|
# nothing, and a laptop with a power button is the same shape. Replaces
|
||||||
# blueline-power-button, which was owned by no package and carried its own
|
# blueline-power-button, which was owned by no package and carried its own
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,26 @@ export SOUVERAINE_AARCH64_SYSROOT="$SYSROOT"
|
||||||
export SOUVERAINE_AARCH64_LINKER_SCRIPTS="$LINKER_SCRIPTS"
|
export SOUVERAINE_AARCH64_LINKER_SCRIPTS="$LINKER_SCRIPTS"
|
||||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$PWD/scripts/aarch64-linker"
|
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$PWD/scripts/aarch64-linker"
|
||||||
|
|
||||||
|
BIN_NAME=souveraine
|
||||||
|
EXPECT_BIN_NAME=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
if $EXPECT_BIN_NAME; then
|
||||||
|
BIN_NAME="$arg"
|
||||||
|
EXPECT_BIN_NAME=false
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
--bin) EXPECT_BIN_NAME=true ;;
|
||||||
|
--bin=*) BIN_NAME="${arg#--bin=}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
cargo build --release --target aarch64-unknown-linux-gnu "$@"
|
cargo build --release --target aarch64-unknown-linux-gnu "$@"
|
||||||
|
|
||||||
BIN="$TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine"
|
BIN="$TARGET_DIR/aarch64-unknown-linux-gnu/release/$BIN_NAME"
|
||||||
|
[[ -x "$BIN" ]] || {
|
||||||
|
echo "requested build produced no executable: $BIN" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
echo "== built: $BIN =="
|
echo "== built: $BIN =="
|
||||||
file "$BIN"
|
file "$BIN"
|
||||||
|
|
|
||||||
631
src/bin/souveraine-usb-kvm.rs
Normal file
631
src/bin/souveraine-usb-kvm.rs
Normal file
|
|
@ -0,0 +1,631 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{mpsc, Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use gadgetry_most_foul::function::custom::{Custom, Interface};
|
||||||
|
use gadgetry_most_foul::Class;
|
||||||
|
use gud_gadget::{DisplayMode, Event, GUD_PIXEL_FORMAT_RGB565};
|
||||||
|
use softbuffer::{Context as SoftContext, Surface};
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::dpi::{LogicalSize, PhysicalPosition};
|
||||||
|
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
|
||||||
|
use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
|
||||||
|
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||||
|
use winit::window::{Window, WindowAttributes, WindowId};
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(about = "Receive a GUD display into a Wayland KVM surface")]
|
||||||
|
struct Args {
|
||||||
|
#[arg(long, default_value = "/run/souveraine-usb/ffs/gud")]
|
||||||
|
ffs_dir: PathBuf,
|
||||||
|
#[arg(long, default_value = "/run/souveraine-usb/keyboard")]
|
||||||
|
keyboard: PathBuf,
|
||||||
|
#[arg(long, default_value = "/run/souveraine-usb/pointer")]
|
||||||
|
pointer: PathBuf,
|
||||||
|
#[arg(long, default_value = "/run/souveraine-usb/gud.ready")]
|
||||||
|
ready_file: PathBuf,
|
||||||
|
#[arg(long, default_value_t = 1080)]
|
||||||
|
width: u32,
|
||||||
|
#[arg(long, default_value_t = 1920)]
|
||||||
|
height: u32,
|
||||||
|
#[arg(long, default_value = "USB KVM")]
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum UserEvent {
|
||||||
|
Frame,
|
||||||
|
GudStopped(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Frame {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
rgb565: Vec<u8>,
|
||||||
|
xrgb8888: Vec<u32>,
|
||||||
|
generation: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame {
|
||||||
|
fn new(width: u32, height: u32) -> Self {
|
||||||
|
let pixels = width as usize * height as usize;
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgb565: vec![0; pixels * 2],
|
||||||
|
xrgb8888: vec![0xff10_1010; pixels],
|
||||||
|
generation: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_rect(&mut self, x: u32, y: u32, width: u32, height: u32) {
|
||||||
|
let end_x = x.saturating_add(width).min(self.width);
|
||||||
|
let end_y = y.saturating_add(height).min(self.height);
|
||||||
|
for row in y..end_y {
|
||||||
|
for column in x..end_x {
|
||||||
|
let pixel = row as usize * self.width as usize + column as usize;
|
||||||
|
let byte = pixel * 2;
|
||||||
|
let raw = u16::from_le_bytes([self.rgb565[byte], self.rgb565[byte + 1]]);
|
||||||
|
let r = u32::from((raw >> 11) & 0x1f) * 255 / 31;
|
||||||
|
let g = u32::from((raw >> 5) & 0x3f) * 255 / 63;
|
||||||
|
let b = u32::from(raw & 0x1f) * 255 / 31;
|
||||||
|
self.xrgb8888[pixel] = 0xff00_0000 | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HidReport {
|
||||||
|
Keyboard([u8; 8]),
|
||||||
|
Pointer([u8; 4]),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_hid_writer(keyboard: PathBuf, pointer: PathBuf) -> mpsc::Sender<HidReport> {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
thread::spawn(move || {
|
||||||
|
for report in rx {
|
||||||
|
let (path, bytes): (&Path, &[u8]) = match &report {
|
||||||
|
HidReport::Keyboard(bytes) => (&keyboard, bytes),
|
||||||
|
HidReport::Pointer(bytes) => (&pointer, bytes),
|
||||||
|
};
|
||||||
|
let result = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
|
.and_then(|mut endpoint| endpoint.write_all(bytes));
|
||||||
|
if let Err(error) = result {
|
||||||
|
warn!(path = %path.display(), %error, "HID report was not delivered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_mode(width: u32, height: u32) -> DisplayMode {
|
||||||
|
let hsync_start = width.saturating_add(8);
|
||||||
|
let hsync_end = hsync_start.saturating_add(8);
|
||||||
|
let htotal = width.saturating_add(40);
|
||||||
|
let vsync_start = height.saturating_add(3);
|
||||||
|
let vsync_end = vsync_start.saturating_add(5);
|
||||||
|
let vtotal = height.saturating_add(40);
|
||||||
|
DisplayMode {
|
||||||
|
clock: htotal.saturating_mul(vtotal).saturating_mul(60) / 1000,
|
||||||
|
hdisplay: width as u16,
|
||||||
|
hsync_start: hsync_start as u16,
|
||||||
|
hsync_end: hsync_end as u16,
|
||||||
|
htotal: htotal as u16,
|
||||||
|
vdisplay: height as u16,
|
||||||
|
vsync_start: vsync_start as u16,
|
||||||
|
vsync_end: vsync_end as u16,
|
||||||
|
vtotal: vtotal as u16,
|
||||||
|
flags: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_gud(
|
||||||
|
ffs_dir: PathBuf,
|
||||||
|
ready_file: PathBuf,
|
||||||
|
frame: Arc<Mutex<Frame>>,
|
||||||
|
proxy: EventLoopProxy<UserEvent>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> thread::JoinHandle<()> {
|
||||||
|
thread::spawn(move || {
|
||||||
|
let result = (|| -> Result<()> {
|
||||||
|
let (mut pixel_data, pixel_endpoint) = gud_gadget::PixelDataEndpoint::new();
|
||||||
|
let mut gud = Custom::builder()
|
||||||
|
.with_interface(
|
||||||
|
Interface::new(Class::vendor_specific(Class::VENDOR_SPECIFIC, 0), "GUD")
|
||||||
|
.with_endpoint(pixel_endpoint),
|
||||||
|
)
|
||||||
|
.existing(&ffs_dir)
|
||||||
|
.with_context(|| format!("initializing {}", ffs_dir.display()))?;
|
||||||
|
|
||||||
|
if let Some(parent) = ready_file.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("creating {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&ready_file, b"ready\n")
|
||||||
|
.with_context(|| format!("writing {}", ready_file.display()))?;
|
||||||
|
info!(ffs = %ffs_dir.display(), "GUD FunctionFS responder ready");
|
||||||
|
|
||||||
|
let mode = display_mode(width, height);
|
||||||
|
while running.load(Ordering::Relaxed) {
|
||||||
|
let Some(event) = gud.event_timeout(Duration::from_millis(100))? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(event) = gud_gadget::event(event)? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match event {
|
||||||
|
Event::GetDescriptor(request) => {
|
||||||
|
request.send_descriptor(width, height, width, height)?;
|
||||||
|
}
|
||||||
|
Event::GetPixelFormats(request) => {
|
||||||
|
request.send_pixel_formats(&[GUD_PIXEL_FORMAT_RGB565])?;
|
||||||
|
}
|
||||||
|
Event::GetDisplayModes(request) => request
|
||||||
|
.send_modes(&[display_mode(mode.hdisplay.into(), mode.vdisplay.into())])?,
|
||||||
|
Event::Buffer(update) => {
|
||||||
|
let dirty = (update.x, update.y, update.width, update.height);
|
||||||
|
let mut frame = frame.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let pitch = frame.width as usize * 2;
|
||||||
|
pixel_data.recv_buffer(update, &mut frame.rgb565, pitch, 2)?;
|
||||||
|
frame.convert_rect(dirty.0, dirty.1, dirty.2, dirty.3);
|
||||||
|
let _ = proxy.send_event(UserEvent::Frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
let _ = std::fs::remove_file(&ready_file);
|
||||||
|
if let Err(error) = result {
|
||||||
|
error!(%error, "GUD responder stopped");
|
||||||
|
let _ = proxy.send_event(UserEvent::GudStopped(error.to_string()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
title: String,
|
||||||
|
frame: Arc<Mutex<Frame>>,
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
context: Option<SoftContext<Arc<Window>>>,
|
||||||
|
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
|
||||||
|
rendered_generation: u64,
|
||||||
|
rendered_size: Option<(u32, u32)>,
|
||||||
|
hid: mpsc::Sender<HidReport>,
|
||||||
|
keys: BTreeSet<u8>,
|
||||||
|
modifiers: u8,
|
||||||
|
pointer_buttons: u8,
|
||||||
|
cursor: Option<PhysicalPosition<f64>>,
|
||||||
|
touch: Option<TouchTrack>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TouchTrack {
|
||||||
|
id: u64,
|
||||||
|
last: PhysicalPosition<f64>,
|
||||||
|
travel: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn keyboard_report(&self) {
|
||||||
|
let mut report = [0u8; 8];
|
||||||
|
report[0] = self.modifiers;
|
||||||
|
for (slot, key) in self.keys.iter().take(6).enumerate() {
|
||||||
|
report[slot + 2] = *key;
|
||||||
|
}
|
||||||
|
let _ = self.hid.send(HidReport::Keyboard(report));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pointer_report(&self, x: i32, y: i32, wheel: i32) {
|
||||||
|
let mut x = x;
|
||||||
|
let mut y = y;
|
||||||
|
let mut wheel = wheel;
|
||||||
|
loop {
|
||||||
|
let dx = x.clamp(-127, 127) as i8;
|
||||||
|
let dy = y.clamp(-127, 127) as i8;
|
||||||
|
let dw = wheel.clamp(-127, 127) as i8;
|
||||||
|
let _ = self.hid.send(HidReport::Pointer([
|
||||||
|
self.pointer_buttons,
|
||||||
|
dx as u8,
|
||||||
|
dy as u8,
|
||||||
|
dw as u8,
|
||||||
|
]));
|
||||||
|
x -= i32::from(dx);
|
||||||
|
y -= i32::from(dy);
|
||||||
|
wheel -= i32::from(dw);
|
||||||
|
if x == 0 && y == 0 && wheel == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_mouse_button(&mut self, button: MouseButton, pressed: bool) {
|
||||||
|
let mask = match button {
|
||||||
|
MouseButton::Left => 1,
|
||||||
|
MouseButton::Right => 2,
|
||||||
|
MouseButton::Middle => 4,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
if pressed {
|
||||||
|
self.pointer_buttons |= mask;
|
||||||
|
} else {
|
||||||
|
self.pointer_buttons &= !mask;
|
||||||
|
}
|
||||||
|
self.pointer_report(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self) -> Result<()> {
|
||||||
|
let Some(window) = &self.window else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some(surface) = &mut self.surface else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let size = window.inner_size();
|
||||||
|
let width = NonZeroU32::new(size.width.max(1)).unwrap();
|
||||||
|
let height = NonZeroU32::new(size.height.max(1)).unwrap();
|
||||||
|
surface
|
||||||
|
.resize(width, height)
|
||||||
|
.map_err(|error| anyhow!(error.to_string()))?;
|
||||||
|
|
||||||
|
let frame = self.frame.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let size_key = (size.width, size.height);
|
||||||
|
if frame.generation == self.rendered_generation && self.rendered_size == Some(size_key) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut buffer = surface
|
||||||
|
.buffer_mut()
|
||||||
|
.map_err(|error| anyhow!(error.to_string()))?;
|
||||||
|
for y in 0..size.height {
|
||||||
|
let source_y = y as usize * frame.height as usize / size.height.max(1) as usize;
|
||||||
|
for x in 0..size.width {
|
||||||
|
let source_x = x as usize * frame.width as usize / size.width.max(1) as usize;
|
||||||
|
buffer[y as usize * size.width as usize + x as usize] =
|
||||||
|
frame.xrgb8888[source_y * frame.width as usize + source_x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.rendered_generation = frame.generation;
|
||||||
|
self.rendered_size = Some(size_key);
|
||||||
|
buffer
|
||||||
|
.present()
|
||||||
|
.map_err(|error| anyhow!(error.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler<UserEvent> for App {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.window.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let frame = self.frame.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let attributes = WindowAttributes::default()
|
||||||
|
.with_title(&self.title)
|
||||||
|
.with_inner_size(LogicalSize::new(frame.width / 2, frame.height / 2));
|
||||||
|
drop(frame);
|
||||||
|
match event_loop.create_window(attributes) {
|
||||||
|
Ok(window) => {
|
||||||
|
let window = Arc::new(window);
|
||||||
|
match SoftContext::new(window.clone()).and_then(|context| {
|
||||||
|
Surface::new(&context, window.clone()).map(|surface| (context, surface))
|
||||||
|
}) {
|
||||||
|
Ok((context, surface)) => {
|
||||||
|
self.context = Some(context);
|
||||||
|
self.surface = Some(surface);
|
||||||
|
window.request_redraw();
|
||||||
|
self.window = Some(window);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!(%error, "could not create KVM drawing surface");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!(%error, "could not create KVM window");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||||
|
match event {
|
||||||
|
UserEvent::Frame => {
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserEvent::GudStopped(reason) => {
|
||||||
|
error!(%reason, "closing KVM surface");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
if let Err(error) = self.draw() {
|
||||||
|
error!(%error, "KVM redraw failed");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(_) => {
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
|
let PhysicalKey::Code(code) = event.physical_key else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let pressed = event.state == ElementState::Pressed;
|
||||||
|
if let Some(mask) = modifier_for(code) {
|
||||||
|
if pressed {
|
||||||
|
self.modifiers |= mask
|
||||||
|
} else {
|
||||||
|
self.modifiers &= !mask
|
||||||
|
}
|
||||||
|
} else if let Some(key) = hid_key(code) {
|
||||||
|
if pressed {
|
||||||
|
self.keys.insert(key);
|
||||||
|
} else {
|
||||||
|
self.keys.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.keyboard_report();
|
||||||
|
}
|
||||||
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
if let Some(last) = self.cursor.replace(position) {
|
||||||
|
self.pointer_report(
|
||||||
|
(position.x - last.x).round() as i32,
|
||||||
|
(position.y - last.y).round() as i32,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::CursorLeft { .. } => self.cursor = None,
|
||||||
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
|
self.set_mouse_button(button, state == ElementState::Pressed);
|
||||||
|
}
|
||||||
|
WindowEvent::MouseWheel { delta, .. } => {
|
||||||
|
let wheel = match delta {
|
||||||
|
MouseScrollDelta::LineDelta(_, y) => (y * 3.0).round() as i32,
|
||||||
|
MouseScrollDelta::PixelDelta(position) => (position.y / 24.0).round() as i32,
|
||||||
|
};
|
||||||
|
self.pointer_report(0, 0, wheel);
|
||||||
|
}
|
||||||
|
WindowEvent::Touch(touch) => match touch.phase {
|
||||||
|
TouchPhase::Started => {
|
||||||
|
self.touch = Some(TouchTrack {
|
||||||
|
id: touch.id,
|
||||||
|
last: touch.location,
|
||||||
|
travel: 0.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TouchPhase::Moved => {
|
||||||
|
let Some(track) = &mut self.touch else { return };
|
||||||
|
if track.id != touch.id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dx = touch.location.x - track.last.x;
|
||||||
|
let dy = touch.location.y - track.last.y;
|
||||||
|
track.last = touch.location;
|
||||||
|
track.travel += dx.abs() + dy.abs();
|
||||||
|
self.pointer_report(dx.round() as i32, dy.round() as i32, 0);
|
||||||
|
}
|
||||||
|
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||||
|
if self
|
||||||
|
.touch
|
||||||
|
.take()
|
||||||
|
.is_some_and(|track| track.id == touch.id && track.travel < 12.0)
|
||||||
|
{
|
||||||
|
self.pointer_buttons |= 1;
|
||||||
|
self.pointer_report(0, 0, 0);
|
||||||
|
self.pointer_buttons &= !1;
|
||||||
|
self.pointer_report(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WindowEvent::Focused(false) => {
|
||||||
|
self.keys.clear();
|
||||||
|
self.modifiers = 0;
|
||||||
|
self.pointer_buttons = 0;
|
||||||
|
self.keyboard_report();
|
||||||
|
self.pointer_report(0, 0, 0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
|
||||||
|
self.running.store(false, Ordering::Relaxed);
|
||||||
|
self.keys.clear();
|
||||||
|
self.modifiers = 0;
|
||||||
|
self.pointer_buttons = 0;
|
||||||
|
self.keyboard_report();
|
||||||
|
self.pointer_report(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modifier_for(code: KeyCode) -> Option<u8> {
|
||||||
|
Some(match code {
|
||||||
|
KeyCode::ControlLeft => 0x01,
|
||||||
|
KeyCode::ShiftLeft => 0x02,
|
||||||
|
KeyCode::AltLeft => 0x04,
|
||||||
|
KeyCode::SuperLeft => 0x08,
|
||||||
|
KeyCode::ControlRight => 0x10,
|
||||||
|
KeyCode::ShiftRight => 0x20,
|
||||||
|
KeyCode::AltRight => 0x40,
|
||||||
|
KeyCode::SuperRight => 0x80,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hid_key(code: KeyCode) -> Option<u8> {
|
||||||
|
Some(match code {
|
||||||
|
KeyCode::KeyA => 0x04,
|
||||||
|
KeyCode::KeyB => 0x05,
|
||||||
|
KeyCode::KeyC => 0x06,
|
||||||
|
KeyCode::KeyD => 0x07,
|
||||||
|
KeyCode::KeyE => 0x08,
|
||||||
|
KeyCode::KeyF => 0x09,
|
||||||
|
KeyCode::KeyG => 0x0a,
|
||||||
|
KeyCode::KeyH => 0x0b,
|
||||||
|
KeyCode::KeyI => 0x0c,
|
||||||
|
KeyCode::KeyJ => 0x0d,
|
||||||
|
KeyCode::KeyK => 0x0e,
|
||||||
|
KeyCode::KeyL => 0x0f,
|
||||||
|
KeyCode::KeyM => 0x10,
|
||||||
|
KeyCode::KeyN => 0x11,
|
||||||
|
KeyCode::KeyO => 0x12,
|
||||||
|
KeyCode::KeyP => 0x13,
|
||||||
|
KeyCode::KeyQ => 0x14,
|
||||||
|
KeyCode::KeyR => 0x15,
|
||||||
|
KeyCode::KeyS => 0x16,
|
||||||
|
KeyCode::KeyT => 0x17,
|
||||||
|
KeyCode::KeyU => 0x18,
|
||||||
|
KeyCode::KeyV => 0x19,
|
||||||
|
KeyCode::KeyW => 0x1a,
|
||||||
|
KeyCode::KeyX => 0x1b,
|
||||||
|
KeyCode::KeyY => 0x1c,
|
||||||
|
KeyCode::KeyZ => 0x1d,
|
||||||
|
KeyCode::Digit1 => 0x1e,
|
||||||
|
KeyCode::Digit2 => 0x1f,
|
||||||
|
KeyCode::Digit3 => 0x20,
|
||||||
|
KeyCode::Digit4 => 0x21,
|
||||||
|
KeyCode::Digit5 => 0x22,
|
||||||
|
KeyCode::Digit6 => 0x23,
|
||||||
|
KeyCode::Digit7 => 0x24,
|
||||||
|
KeyCode::Digit8 => 0x25,
|
||||||
|
KeyCode::Digit9 => 0x26,
|
||||||
|
KeyCode::Digit0 => 0x27,
|
||||||
|
KeyCode::Enter => 0x28,
|
||||||
|
KeyCode::Escape => 0x29,
|
||||||
|
KeyCode::Backspace => 0x2a,
|
||||||
|
KeyCode::Tab => 0x2b,
|
||||||
|
KeyCode::Space => 0x2c,
|
||||||
|
KeyCode::Minus => 0x2d,
|
||||||
|
KeyCode::Equal => 0x2e,
|
||||||
|
KeyCode::BracketLeft => 0x2f,
|
||||||
|
KeyCode::BracketRight => 0x30,
|
||||||
|
KeyCode::Backslash => 0x31,
|
||||||
|
KeyCode::Semicolon => 0x33,
|
||||||
|
KeyCode::Quote => 0x34,
|
||||||
|
KeyCode::Backquote => 0x35,
|
||||||
|
KeyCode::Comma => 0x36,
|
||||||
|
KeyCode::Period => 0x37,
|
||||||
|
KeyCode::Slash => 0x38,
|
||||||
|
KeyCode::CapsLock => 0x39,
|
||||||
|
KeyCode::F1 => 0x3a,
|
||||||
|
KeyCode::F2 => 0x3b,
|
||||||
|
KeyCode::F3 => 0x3c,
|
||||||
|
KeyCode::F4 => 0x3d,
|
||||||
|
KeyCode::F5 => 0x3e,
|
||||||
|
KeyCode::F6 => 0x3f,
|
||||||
|
KeyCode::F7 => 0x40,
|
||||||
|
KeyCode::F8 => 0x41,
|
||||||
|
KeyCode::F9 => 0x42,
|
||||||
|
KeyCode::F10 => 0x43,
|
||||||
|
KeyCode::F11 => 0x44,
|
||||||
|
KeyCode::F12 => 0x45,
|
||||||
|
KeyCode::PrintScreen => 0x46,
|
||||||
|
KeyCode::ScrollLock => 0x47,
|
||||||
|
KeyCode::Pause => 0x48,
|
||||||
|
KeyCode::Insert => 0x49,
|
||||||
|
KeyCode::Home => 0x4a,
|
||||||
|
KeyCode::PageUp => 0x4b,
|
||||||
|
KeyCode::Delete => 0x4c,
|
||||||
|
KeyCode::End => 0x4d,
|
||||||
|
KeyCode::PageDown => 0x4e,
|
||||||
|
KeyCode::ArrowRight => 0x4f,
|
||||||
|
KeyCode::ArrowLeft => 0x50,
|
||||||
|
KeyCode::ArrowDown => 0x51,
|
||||||
|
KeyCode::ArrowUp => 0x52,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt::layer())
|
||||||
|
.with(EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
let args = Args::parse();
|
||||||
|
anyhow::ensure!(
|
||||||
|
args.width <= u16::MAX.into() && args.height <= u16::MAX.into(),
|
||||||
|
"display is too large for GUD mode fields"
|
||||||
|
);
|
||||||
|
|
||||||
|
let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
|
||||||
|
let frame = Arc::new(Mutex::new(Frame::new(args.width, args.height)));
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
let gud = spawn_gud(
|
||||||
|
args.ffs_dir,
|
||||||
|
args.ready_file,
|
||||||
|
frame.clone(),
|
||||||
|
event_loop.create_proxy(),
|
||||||
|
running.clone(),
|
||||||
|
args.width,
|
||||||
|
args.height,
|
||||||
|
);
|
||||||
|
let mut app = App {
|
||||||
|
title: args.title,
|
||||||
|
frame,
|
||||||
|
window: None,
|
||||||
|
context: None,
|
||||||
|
surface: None,
|
||||||
|
rendered_generation: u64::MAX,
|
||||||
|
rendered_size: None,
|
||||||
|
hid: spawn_hid_writer(args.keyboard, args.pointer),
|
||||||
|
keys: BTreeSet::new(),
|
||||||
|
modifiers: 0,
|
||||||
|
pointer_buttons: 0,
|
||||||
|
cursor: None,
|
||||||
|
touch: None,
|
||||||
|
running,
|
||||||
|
};
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
app.running.store(false, Ordering::Relaxed);
|
||||||
|
let _ = gud.join();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rgb565_dirty_rect_becomes_xrgb8888() {
|
||||||
|
let mut frame = Frame::new(2, 1);
|
||||||
|
frame.rgb565 = [0x00, 0xf8, 0xe0, 0x07].to_vec();
|
||||||
|
frame.convert_rect(0, 0, 2, 1);
|
||||||
|
assert_eq!(frame.xrgb8888, [0xffff_0000, 0xff00_ff00]);
|
||||||
|
assert_eq!(frame.generation, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn physical_keys_use_usb_hid_usage_ids() {
|
||||||
|
assert_eq!(hid_key(KeyCode::KeyA), Some(0x04));
|
||||||
|
assert_eq!(hid_key(KeyCode::Digit0), Some(0x27));
|
||||||
|
assert_eq!(hid_key(KeyCode::F12), Some(0x45));
|
||||||
|
assert_eq!(hid_key(KeyCode::Delete), Some(0x4c));
|
||||||
|
assert_eq!(modifier_for(KeyCode::ControlLeft), Some(0x01));
|
||||||
|
assert_eq!(modifier_for(KeyCode::AltRight), Some(0x40));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ use tracing::{info, warn};
|
||||||
use crate::sessiond::bearer::{Bearer, BearerEvidence};
|
use crate::sessiond::bearer::{Bearer, BearerEvidence};
|
||||||
use crate::sessiond::protocol::{
|
use crate::sessiond::protocol::{
|
||||||
Button, ButtonEdge, ButtonGesture, InputTrigger, SensorSource, SensorValue, TouchGesture,
|
Button, ButtonEdge, ButtonGesture, InputTrigger, SensorSource, SensorValue, TouchGesture,
|
||||||
|
UsbMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How long a locked, lit panel waits for input before it blanks.
|
/// How long a locked, lit panel waits for input before it blanks.
|
||||||
|
|
@ -439,6 +440,13 @@ pub enum Action {
|
||||||
/// changes what every other peripheral means. Casey, 2026-08-05: "that
|
/// changes what every other peripheral means. Casey, 2026-08-05: "that
|
||||||
/// whole power usb state probably belongs in the power switch options".
|
/// whole power usb state probably belongs in the power switch options".
|
||||||
PowerMenu,
|
PowerMenu,
|
||||||
|
/// Change the port's gadget posture through usb-signaller.
|
||||||
|
///
|
||||||
|
/// The mode daemon is the mechanism and souveraine-upower is its adjacent
|
||||||
|
/// power sensor. Neither decides. sessiond emits this action so the mode
|
||||||
|
/// change is named, audited, and later refusable when a known peer or an
|
||||||
|
/// active probe lease says the port is occupied.
|
||||||
|
UsbMode(UsbMode),
|
||||||
/// Request the session lock, because something wants the panel dark and
|
/// Request the session lock, because something wants the panel dark and
|
||||||
/// the session is not locked yet.
|
/// the session is not locked yet.
|
||||||
///
|
///
|
||||||
|
|
@ -2479,6 +2487,22 @@ impl DeviceStateMachine {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask for a USB gadget posture.
|
||||||
|
///
|
||||||
|
/// The decision is intentionally tiny today: all advertised device-role
|
||||||
|
/// modes are admissible. The seam matters because attachment identity and
|
||||||
|
/// probe ownership will make that conditional; when they land, the refusal
|
||||||
|
/// belongs here rather than in QML or usb-signaller. Every request is
|
||||||
|
/// written now, so that future rule has a trail to inherit.
|
||||||
|
pub fn request_usb_mode(&mut self, mode: UsbMode, why: &str) -> Vec<Action> {
|
||||||
|
self.record_decision(
|
||||||
|
"usb-mode-request",
|
||||||
|
serde_json::json!({ "mode": mode.as_str() }),
|
||||||
|
why,
|
||||||
|
);
|
||||||
|
vec![Action::UsbMode(mode)]
|
||||||
|
}
|
||||||
|
|
||||||
/// The executor reports the panel's real state. Turning on counts as
|
/// The executor reports the panel's real state. Turning on counts as
|
||||||
/// input, so a dt2w wake starts the blank budget from the wake itself.
|
/// input, so a dt2w wake starts the blank budget from the wake itself.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,20 @@ pub const VERBS: &[VerbDoc] = &[
|
||||||
refuses: &[],
|
refuses: &[],
|
||||||
example: r#"{"op":"bearer"}"#,
|
example: r#"{"op":"bearer"}"#,
|
||||||
},
|
},
|
||||||
|
VerbDoc {
|
||||||
|
op: "usb",
|
||||||
|
mutates: false,
|
||||||
|
summary: "USB-C role, gadget mode, charger evidence, attachment identity and probe owner",
|
||||||
|
refuses: &[RefusalCode::Unavailable],
|
||||||
|
example: r#"{"op":"usb"}"#,
|
||||||
|
},
|
||||||
|
VerbDoc {
|
||||||
|
op: "set_usb_mode",
|
||||||
|
mutates: true,
|
||||||
|
summary: "ask the device-state authority to change the USB gadget posture",
|
||||||
|
refuses: &[RefusalCode::RefusedByState, RefusalCode::Unavailable],
|
||||||
|
example: r#"{"op":"set_usb_mode","mode":"hid"}"#,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// A request plus the caller's declared intent.
|
/// A request plus the caller's declared intent.
|
||||||
|
|
@ -381,6 +395,15 @@ pub enum Request {
|
||||||
/// *policy* (`set_policy`), which is the difference between steering the
|
/// *policy* (`set_policy`), which is the difference between steering the
|
||||||
/// machine and reaching around it.
|
/// machine and reaching around it.
|
||||||
Bearer,
|
Bearer,
|
||||||
|
/// Port proprioception. The mechanism is adjacent to souveraine-upower:
|
||||||
|
/// usb-signaller reports/acts on the data posture, while UPower reports
|
||||||
|
/// charging posture. sessiond is the one place the two become a snapshot.
|
||||||
|
Usb,
|
||||||
|
/// Ask for a gadget posture. This does not let the shell write configfs;
|
||||||
|
/// it becomes an Action and is executed through usb-signaller's system
|
||||||
|
/// D-Bus API. Full KVM is admitted because usb-signaller prepares both
|
||||||
|
/// FunctionFS responders and rolls the composite back as one transaction.
|
||||||
|
SetUsbMode { mode: UsbMode },
|
||||||
/// Change the timed policy. Every field is optional; omitted fields keep
|
/// Change the timed policy. Every field is optional; omitted fields keep
|
||||||
/// their current value. This is the seam the Settings control center reads
|
/// their current value. This is the seam the Settings control center reads
|
||||||
/// and writes, so a control there is a view over the owning daemon rather
|
/// and writes, so a control there is a view over the owning daemon rather
|
||||||
|
|
@ -412,6 +435,41 @@ pub enum Request {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// USB postures sessiond is prepared to own today.
|
||||||
|
///
|
||||||
|
/// Keep this narrower than usb-signaller's raw vocabulary. MTP and tethering
|
||||||
|
/// are not power-menu promises; host mode is unsafe until the SMB2/TCPM lane
|
||||||
|
/// can source VBUS. Full KVM is here because GUD and smoo now share one
|
||||||
|
/// readiness-gated, rollback-capable lifecycle in usb-signaller.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum UsbMode {
|
||||||
|
Developer,
|
||||||
|
Hid,
|
||||||
|
Kvm,
|
||||||
|
ChargingOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsbMode {
|
||||||
|
pub fn as_usb_moded(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Developer => "developer_mode",
|
||||||
|
Self::Hid => "hid_mode",
|
||||||
|
Self::Kvm => "kvm_mode",
|
||||||
|
Self::ChargingOnly => "charging_only",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Developer => "developer",
|
||||||
|
Self::Hid => "hid",
|
||||||
|
Self::Kvm => "kvm",
|
||||||
|
Self::ChargingOnly => "charging_only",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What produced a user input. Recorded so the forensic trail can tell a
|
/// What produced a user input. Recorded so the forensic trail can tell a
|
||||||
/// deliberate power-button press from an accidental pocket touch.
|
/// deliberate power-button press from an accidental pocket touch.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -611,12 +669,10 @@ mod tests {
|
||||||
|
|
||||||
// And the other direction: every variant must be advertised. Bump this
|
// And the other direction: every variant must be advertised. Bump this
|
||||||
// deliberately when a verb is added, having added its VerbDoc.
|
// deliberately when a verb is added, having added its VerbDoc.
|
||||||
// 17 since `gesture` (2026-08-03): the compositor names what the
|
// 19 since USB posture became a read plus a request (2026-08-07).
|
||||||
// fingers did and the machine decides what it means, so the naming had
|
|
||||||
// to become a verb rather than the compositor calling a tool itself.
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
VERBS.len(),
|
VERBS.len(),
|
||||||
17,
|
19,
|
||||||
"a Request variant was added without a VerbDoc"
|
"a Request variant was added without a VerbDoc"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -652,4 +708,11 @@ mod tests {
|
||||||
r#""awaiting_shell_lock""#
|
r#""awaiting_shell_lock""#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_kvm_is_a_named_usb_posture() {
|
||||||
|
let req: Request = serde_json::from_str(r#"{"op":"set_usb_mode","mode":"kvm"}"#).unwrap();
|
||||||
|
assert!(matches!(req, Request::SetUsbMode { mode: UsbMode::Kvm }));
|
||||||
|
assert_eq!(UsbMode::Kvm.as_usb_moded(), "kvm_mode");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use crate::sessiond::idle;
|
||||||
use crate::sessiond::lock::{self, LockController, Msg, SessionOutcome};
|
use crate::sessiond::lock::{self, LockController, Msg, SessionOutcome};
|
||||||
use crate::sessiond::lockhint;
|
use crate::sessiond::lockhint;
|
||||||
use crate::sessiond::protocol::{
|
use crate::sessiond::protocol::{
|
||||||
Envelope, InputTrigger, Phase, RefusalCode, Request, SensorSource, SensorValue,
|
Envelope, InputTrigger, Phase, RefusalCode, Request, SensorSource, SensorValue, UsbMode,
|
||||||
LOCKED_ACK_TIMEOUT_SECS, MAX_REQUEST_BYTES, VERBS,
|
LOCKED_ACK_TIMEOUT_SECS, MAX_REQUEST_BYTES, VERBS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -283,6 +283,17 @@ fn execute(shared: &Arc<Shared>, action: Action) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Action::UsbMode(mode) = action {
|
||||||
|
if let Err(error) = set_usb_mode(mode) {
|
||||||
|
warn!("USB mode {} failed: {error:#}", mode.as_str());
|
||||||
|
shared
|
||||||
|
.lock()
|
||||||
|
.device_state
|
||||||
|
.record_error("device-state", "usb-mode", &error.to_string());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Dim and restore carry a number, so they are not table entries. The
|
// Dim and restore carry a number, so they are not table entries. The
|
||||||
// machine remembers the exact brightness the panel was at; the executor
|
// machine remembers the exact brightness the panel was at; the executor
|
||||||
// reads the hardware and sets it back. Neither `brightnessctl -s/-r` nor
|
// reads the hardware and sets it back. Neither `brightnessctl -s/-r` nor
|
||||||
|
|
@ -404,6 +415,7 @@ fn execute(shared: &Arc<Shared>, action: Action) {
|
||||||
vec!["-c", "souveraine", "ipc", "call", "powerMenu", "open"],
|
vec!["-c", "souveraine", "ipc", "call", "powerMenu", "open"],
|
||||||
"power-menu-open",
|
"power-menu-open",
|
||||||
),
|
),
|
||||||
|
Action::UsbMode(_) => unreachable!("handled above; expressed through usb-signaller"),
|
||||||
Action::Blank => {
|
Action::Blank => {
|
||||||
// The invariant, enforced where it cannot be reasoned around: the
|
// The invariant, enforced where it cannot be reasoned around: the
|
||||||
// panel does not go dark unless logind says this session is
|
// panel does not go dark unless logind says this session is
|
||||||
|
|
@ -435,6 +447,94 @@ fn execute(shared: &Arc<Shared>, action: Action) {
|
||||||
run_executor(shared, program, &args, label, action);
|
run_executor(shared, program, &args, label, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const USB_MODED_DESTINATION: &str = "com.meego.usb_moded";
|
||||||
|
const USB_MODED_PATH: &str = "/com/meego/usb_moded";
|
||||||
|
const USB_MODED_INTERFACE: &str = "com.meego.usb_moded";
|
||||||
|
|
||||||
|
fn usb_moded_call(member: &str, argument: Option<&str>) -> Result<String> {
|
||||||
|
let mut command = std::process::Command::new("busctl");
|
||||||
|
command.args([
|
||||||
|
"--system",
|
||||||
|
"call",
|
||||||
|
USB_MODED_DESTINATION,
|
||||||
|
USB_MODED_PATH,
|
||||||
|
USB_MODED_INTERFACE,
|
||||||
|
member,
|
||||||
|
]);
|
||||||
|
if let Some(argument) = argument {
|
||||||
|
command.args(["s", argument]);
|
||||||
|
}
|
||||||
|
let output = command
|
||||||
|
.output()
|
||||||
|
.with_context(|| format!("calling usb-signaller {member}"))?;
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"usb-signaller {member}: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let stdout = String::from_utf8(output.stdout).context("usb-signaller returned non-UTF8")?;
|
||||||
|
let start = stdout
|
||||||
|
.find('"')
|
||||||
|
.context("usb-signaller reply had no string")?
|
||||||
|
+ 1;
|
||||||
|
let end = stdout[start..]
|
||||||
|
.find('"')
|
||||||
|
.map(|offset| start + offset)
|
||||||
|
.context("usb-signaller reply had an unterminated string")?;
|
||||||
|
Ok(stdout[start..end].to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_usb_role() -> Option<String> {
|
||||||
|
let entries = std::fs::read_dir("/sys/class/usb_role").ok()?;
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if let Ok(role) = std::fs::read_to_string(entry.path().join("role")) {
|
||||||
|
return Some(role.trim().to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usb_snapshot() -> Result<serde_json::Value> {
|
||||||
|
let raw_mode = usb_moded_call("mode_request", None)?;
|
||||||
|
let available = usb_moded_call("get_modes", None)?
|
||||||
|
.split(',')
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let charger_online = std::fs::read_to_string("/sys/class/power_supply/pmi8998-charger/online")
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.trim() == "1");
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"ok": true,
|
||||||
|
"mode": raw_mode.strip_suffix("_mode").unwrap_or(&raw_mode),
|
||||||
|
"raw_mode": raw_mode,
|
||||||
|
"available_modes": available,
|
||||||
|
"data_role": read_usb_role(),
|
||||||
|
// Power evidence comes from the charger/upower lane, adjacent to the
|
||||||
|
// data-mode mechanism. A true Type-C source/sink role stays unknown
|
||||||
|
// until TCPM binds; online is not dressed up as more than it says.
|
||||||
|
"charger_online": charger_online,
|
||||||
|
"power_role": serde_json::Value::Null,
|
||||||
|
// Stable fields now, evidence later. Federation supplies identity;
|
||||||
|
// the probe registry supplies ownership. Neither is inferred from a
|
||||||
|
// VID/PID and neither absence is reported as "unclaimed".
|
||||||
|
"attached_identity": serde_json::Value::Null,
|
||||||
|
"probe_owner": serde_json::Value::Null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_usb_mode(mode: UsbMode) -> Result<()> {
|
||||||
|
let _ = usb_moded_call("set_mode", Some(mode.as_usb_moded()))?;
|
||||||
|
let after = usb_moded_call("mode_request", None)?;
|
||||||
|
if after != mode.as_usb_moded() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"requested {}, usb-signaller reports {after}",
|
||||||
|
mode.as_usb_moded()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// The cellular v4 default's metric, which is a fixed reference point rather
|
/// The cellular v4 default's metric, which is a fixed reference point rather
|
||||||
/// than something to set.
|
/// than something to set.
|
||||||
///
|
///
|
||||||
|
|
@ -1350,6 +1450,38 @@ fn handle_request(
|
||||||
let panel_on = shared.lock().device_state.panel_on();
|
let panel_on = shared.lock().device_state.panel_on();
|
||||||
serde_json::json!({ "ok": true, "requested": on, "panel_on": panel_on, "acted": acted })
|
serde_json::json!({ "ok": true, "requested": on, "panel_on": panel_on, "acted": acted })
|
||||||
}
|
}
|
||||||
|
Request::Usb => match usb_snapshot() {
|
||||||
|
Ok(snapshot) => snapshot,
|
||||||
|
Err(error) => refuse(
|
||||||
|
RefusalCode::Unavailable,
|
||||||
|
&format!("USB posture is unavailable: {error:#}"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Request::SetUsbMode { mode } => {
|
||||||
|
let actions = {
|
||||||
|
let mut d = shared.lock();
|
||||||
|
d.device_state
|
||||||
|
.request_usb_mode(mode, "USB mode requested through the session authority")
|
||||||
|
};
|
||||||
|
for action in actions {
|
||||||
|
execute(shared, action);
|
||||||
|
}
|
||||||
|
match usb_snapshot() {
|
||||||
|
Ok(snapshot) if snapshot["raw_mode"] == mode.as_usb_moded() => snapshot,
|
||||||
|
Ok(snapshot) => refuse(
|
||||||
|
RefusalCode::Unavailable,
|
||||||
|
&format!(
|
||||||
|
"USB mode did not settle at {} (reported {})",
|
||||||
|
mode.as_usb_moded(),
|
||||||
|
snapshot["raw_mode"]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Err(error) => refuse(
|
||||||
|
RefusalCode::Unavailable,
|
||||||
|
&format!("USB mode changed but could not be verified: {error:#}"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
Request::Bearer => {
|
Request::Bearer => {
|
||||||
let d = shared.lock();
|
let d = shared.lock();
|
||||||
let b = &d.device_state.bearer;
|
let b = &d.device_state.bearer;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ services/ChargeRate.qml souveraine/services/ChargeRate.qml
|
||||||
services/Haptics.qml souveraine/services/Haptics.qml
|
services/Haptics.qml souveraine/services/Haptics.qml
|
||||||
services/Selection.qml souveraine/services/Selection.qml
|
services/Selection.qml souveraine/services/Selection.qml
|
||||||
services/DeviceEvidence.qml souveraine/services/DeviceEvidence.qml
|
services/DeviceEvidence.qml souveraine/services/DeviceEvidence.qml
|
||||||
|
services/UsbState.qml souveraine/services/UsbState.qml
|
||||||
services/Gestures.qml souveraine/services/Gestures.qml
|
services/Gestures.qml souveraine/services/Gestures.qml
|
||||||
modules/souveraine/dial/RadialDial.qml souveraine/modules/souveraine/dial/RadialDial.qml
|
modules/souveraine/dial/RadialDial.qml souveraine/modules/souveraine/dial/RadialDial.qml
|
||||||
modules/souveraine/dial/DialHost.qml souveraine/modules/souveraine/dial/DialHost.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/WindowSheet.qml souveraine/modules/souveraine/windowSheet/WindowSheet.qml
|
||||||
modules/souveraine/windowSheet/SheetButton.qml souveraine/modules/souveraine/windowSheet/SheetButton.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/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
|
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
|
||||||
services/Face.qml souveraine/services/Face.qml
|
services/Face.qml souveraine/services/Face.qml
|
||||||
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
|
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,11 @@
|
||||||
//
|
//
|
||||||
// ## What is honest here today
|
// ## What is honest here today
|
||||||
//
|
//
|
||||||
// The data role is real and readable — `/sys/class/usb_role/…/role` reports
|
// The data role and charger attachment are measured rather than guessed.
|
||||||
// `device`, and `pmi8998-charger` reports `online=1`, so "attached, drawing
|
// UsbState reads sessiond's combined snapshot: usb-signaller owns the configfs
|
||||||
// power" is a measured fact rather than a guess. **Switching** the role is not
|
// mechanism, souveraine-upower is adjacent charging evidence, and sessiond is
|
||||||
// offered yet: the sysfs node is root-owned, so the flip has to be a sessiond
|
// the authority that can turn a requested posture into an audited Action.
|
||||||
// verb, and a button that cannot do the thing it names is the exact failure
|
// This surface reads and asks; it never writes sysfs or calls the gadget daemon.
|
||||||
// this project keeps writing down. It is shown as state, and it says so.
|
|
||||||
//
|
//
|
||||||
// There is no `/sys/class/typec/` on this device — the tcpm driver is not
|
// 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
|
// bound — so the *power* role genuinely is not knowable here yet. The row
|
||||||
|
|
@ -48,20 +47,27 @@ Scope {
|
||||||
id: scope
|
id: scope
|
||||||
|
|
||||||
property bool menuOpen: false
|
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
|
// 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,
|
// 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.
|
// and a timer here would be a background reader of a file nobody needs.
|
||||||
property string dataRole: "unknown"
|
readonly property string dataRole: UsbState.dataRole
|
||||||
property bool chargerOnline: false
|
readonly property bool chargerOnline: UsbState.chargerOnline
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
roleProc.running = true;
|
UsbState.refresh();
|
||||||
chargerProc.running = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
scope.refresh();
|
scope.refresh();
|
||||||
|
scope.powerOptionsOpen = false;
|
||||||
|
scope.attachmentOptionsOpen = false;
|
||||||
|
scope.batteryOptionsOpen = false;
|
||||||
|
scope.hidOptionsOpen = false;
|
||||||
scope.menuOpen = true;
|
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 {
|
Variants {
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
|
@ -129,7 +119,8 @@ Scope {
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 30
|
width: Math.min(win.width - 32, 500)
|
||||||
|
spacing: 22
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
@ -170,16 +161,239 @@ Scope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The port, as measured. Not a control yet — see the header.
|
// 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 {
|
StyledText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.maximumWidth: win.width * 0.8
|
Layout.maximumWidth: win.width * 0.8
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
color: "#c8ffffff"
|
color: "#aaffffff"
|
||||||
font.pixelSize: Appearance.font.pixelSize.small
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
text: "USB-C: " + scope.dataRole
|
text: "USB-C: " + scope.dataRole
|
||||||
+ (scope.chargerOnline ? " · charger attached" : " · no charger")
|
+ (!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
|
PowerMenu 1.0 PowerMenu.qml
|
||||||
|
PowerOptionRow 1.0 PowerOptionRow.qml
|
||||||
SheetButton 1.0 SheetButton.qml
|
SheetButton 1.0 SheetButton.qml
|
||||||
WindowSheet 1.0 WindowSheet.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 Translation 1.0 Translation.qml
|
||||||
singleton TrayService 1.0 TrayService.qml
|
singleton TrayService 1.0 TrayService.qml
|
||||||
singleton Updates 1.0 Updates.qml
|
singleton Updates 1.0 Updates.qml
|
||||||
|
singleton UsbState 1.0 UsbState.qml
|
||||||
singleton ViewtopControl 1.0 ViewtopControl.qml
|
singleton ViewtopControl 1.0 ViewtopControl.qml
|
||||||
singleton WallpaperAssets 1.0 WallpaperAssets.qml
|
singleton WallpaperAssets 1.0 WallpaperAssets.qml
|
||||||
singleton WallpaperDownload 1.0 WallpaperDownload.qml
|
singleton WallpaperDownload 1.0 WallpaperDownload.qml
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue