More KDE patches for VM color identification
Decorations on task manager, alt-tab and thumbnails. If windows from multiple VMs are grouped, the group is only colored if all windows match.
This commit is contained in:
parent
0d36da9f55
commit
8ccd7868e7
10 changed files with 1297 additions and 918 deletions
|
|
@ -1,104 +0,0 @@
|
|||
# Root Overlay Ephemeral Disk Design
|
||||
|
||||
## Problem
|
||||
|
||||
VM writes to the root filesystem currently go to a tmpfs overlayfs upper layer, consuming host RAM. This becomes problematic with many VMs or write-heavy workloads.
|
||||
|
||||
## Solution
|
||||
|
||||
Use a sparse qcow2 ephemeral disk as the overlayfs upper layer by default. The disk is created at VM start and deleted at VM stop. Writes go to host disk instead of RAM, with TRIM/discard support to reclaim space when files are deleted in the guest.
|
||||
|
||||
The current tmpfs behavior is preserved as an option.
|
||||
|
||||
## NixOS Module Options
|
||||
|
||||
New option in `modules/options.nix`:
|
||||
|
||||
```nix
|
||||
rootOverlay = {
|
||||
type = "qcow2"; # enum: "qcow2" | "tmpfs", default "qcow2"
|
||||
size = "10G"; # string, only applies when type = "qcow2"
|
||||
};
|
||||
```
|
||||
|
||||
- `type = "qcow2"` (default): sparse qcow2 on host, formatted ext4 in guest
|
||||
- `type = "tmpfs"`: current behavior, RAM-backed upper layer
|
||||
- `size`: max size of ephemeral disk, parsed by `qemu-img create`
|
||||
|
||||
## Host-Side Implementation
|
||||
|
||||
Changes to VM systemd service in `modules/services.nix`:
|
||||
|
||||
**ExecStartPre** (when `rootOverlay.type = "qcow2"`):
|
||||
```bash
|
||||
qemu-img create -f qcow2 /run/vmsilo/<name>-ephemeral.qcow2 <size>
|
||||
```
|
||||
|
||||
**crosvm args:**
|
||||
```
|
||||
--block path=/run/vmsilo/<name>-ephemeral.qcow2,ro=false,id=<sanitized_name>_ephemeral
|
||||
```
|
||||
|
||||
VM name is sanitized to alphanumeric + underscores for the device ID.
|
||||
|
||||
**Kernel param:**
|
||||
```
|
||||
vmsilo.rootOverlay=qcow2,<sanitized_name>_ephemeral
|
||||
# or
|
||||
vmsilo.rootOverlay=tmpfs
|
||||
```
|
||||
|
||||
**ExecStopPost:**
|
||||
```bash
|
||||
rm -f /run/vmsilo/<name>-ephemeral.qcow2
|
||||
```
|
||||
|
||||
## Guest-Side Implementation
|
||||
|
||||
Changes to `rootfs-nixos/configuration.nix`, specifically the `prepare-overlay` service:
|
||||
|
||||
**Parse kernel param:**
|
||||
```bash
|
||||
OVERLAY_TYPE=$(get_kernel_param "vmsilo.rootOverlay" "tmpfs")
|
||||
# Returns "tmpfs" or "qcow2,<device_id>"
|
||||
```
|
||||
|
||||
**If tmpfs** (current behavior):
|
||||
```bash
|
||||
mount -t tmpfs -o mode=0755 tmpfs /sysroot/.overlay-rw
|
||||
```
|
||||
|
||||
**If qcow2:**
|
||||
```bash
|
||||
DEVICE_ID="${OVERLAY_TYPE#qcow2,}"
|
||||
DEVICE="/dev/disk/by-id/virtio-${DEVICE_ID}"
|
||||
|
||||
# Wait for device (with timeout)
|
||||
udevadm settle --timeout=10
|
||||
|
||||
# Format and mount with discard for TRIM support
|
||||
mkfs.ext4 -q "$DEVICE"
|
||||
mount -o discard "$DEVICE" /sysroot/.overlay-rw
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Device wait timeout: fail boot
|
||||
- mkfs failure: fail boot
|
||||
- No silent fallback to tmpfs (would mask problems and cause unexpected RAM usage)
|
||||
|
||||
User can debug via serial console (`vm-shell <name>`).
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `modules/options.nix` - add `rootOverlay` option with type and size
|
||||
2. `modules/services.nix` - qcow2 lifecycle (create/delete), crosvm args, kernel param
|
||||
3. `rootfs-nixos/configuration.nix` - prepare-overlay service logic
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Default case**: Start VM, verify `/` is overlay with ext4 upper, write large file, confirm host RAM doesn't grow
|
||||
2. **tmpfs mode**: Set `rootOverlay.type = "tmpfs"`, verify current behavior
|
||||
3. **Discard works**: Write then delete large file in guest, confirm host qcow2 doesn't grow permanently
|
||||
4. **Size limit**: Fill disk past limit, verify ENOSPC in guest
|
||||
5. **Cleanup**: Stop VM, confirm qcow2 deleted from `/run/vmsilo/`
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
# Consumer-side CoDel: Implementation Notes
|
||||
|
||||
## Phase 1 notes
|
||||
|
||||
### Plan deviations
|
||||
|
||||
- **`args.rs` also needed updating** — it constructs `FqCodelConfig` with struct
|
||||
literal syntax, so adding `codel_enabled` required setting `codel_enabled: true`
|
||||
there. Not mentioned in the Phase 1 plan.
|
||||
|
||||
### Notes carried forward
|
||||
|
||||
1. **Timestamp is threaded end-to-end.** The flow is:
|
||||
`FqCodel::enqueue()` → `QueuedPacket` → `dequeue_and_push()` →
|
||||
`Producer::push_with_timestamp()` → SPSC ring → `Consumer::pop_with_timestamp()`.
|
||||
Phase 3 just needs to read the timestamp from `pop_with_timestamp()` and feed it
|
||||
into `enqueue_with_time()` on the consumer side.
|
||||
|
||||
2. **`pop_with_timestamp()` already exists.** Returns `(Vec<u8>, u64)` — no ring.rs
|
||||
changes needed in Phase 2/3.
|
||||
|
||||
3. **`enqueue_with_time()` takes both `Instant` and `u64`.** On the consumer side,
|
||||
you'll need to convert the `u64` CLOCK_MONOTONIC nanos back to an `Instant` for
|
||||
CoDel's sojourn calculation. This requires an Instant-from-monotonic-nanos helper
|
||||
(e.g., `Instant::now() - Duration::from_nanos(now_ns - enqueue_ns)`).
|
||||
|
||||
4. **Broadcast bypasses FQ-CoDel entirely on the producer side.** `forward_broadcast()` uses plain
|
||||
`push()` with push-time timestamps.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 notes
|
||||
|
||||
### Plan deviations
|
||||
|
||||
- **`PeerDropCounters` type alias added.** The plan showed inline
|
||||
`HashMap<String, (u64, u64)>` types. Implementation uses a
|
||||
`type PeerDropCounters = HashMap<String, (u64, u64)>` alias at the top of
|
||||
`fq_codel.rs` to avoid repetition.
|
||||
|
||||
- **`Default` for `FlowIdentity` lives in `metrics.rs`, not `flow.rs`.** The plan
|
||||
said to add it in `flow.rs`, but `FlowIdentity` is defined in `metrics.rs` and
|
||||
re-exported from `flow.rs` via `pub use crate::metrics::FlowIdentity`. The
|
||||
`Default` impl was added in `metrics.rs` next to the struct definition.
|
||||
|
||||
- **`FlowHash` uses `#[derive(Default)]` instead of manual impl.** The plan showed
|
||||
a manual `Default` impl. Since `FlowHash(pub u32)` gets `Default` for free via
|
||||
derive (defaulting to 0), we used derive instead.
|
||||
|
||||
- **Consumer-side metrics resets global counters but binds them with `let _ =`.**
|
||||
The plan's step 2.5 showed reading and resetting global consumer-side counters
|
||||
(`consumer_codel`, `consumer_tail`, etc.) but didn't specify what to do with them
|
||||
beyond per-peer attribution. The implementation resets them (to prevent unbounded
|
||||
growth) and suppresses the unused-variable warning. Phase 3 could expose these as
|
||||
aggregate consumer-side metrics if desired.
|
||||
|
||||
### Notes for Phase 3
|
||||
|
||||
1. **`enqueue_ingress()` and `drain_rx()` are ready but not wired in.** The event
|
||||
loop in `process.rs` still calls `poll_ingress()` + `inject_rx_frame()` directly.
|
||||
Phase 3 switches to `enqueue_ingress()` + `drain_rx()`.
|
||||
|
||||
2. **`poll_ingress()` is still used by existing tests.** Several tests in
|
||||
`child::forwarder::tests` call `poll_ingress()` directly. Phase 3 should either
|
||||
keep `poll_ingress()` as `#[cfg(test)]` or migrate those tests to the new path.
|
||||
|
||||
3. **`drain_rx()` takes `&ChildVhostBackend` directly.** The method calls
|
||||
`backend.inject_rx_frame()` in a loop. This means the event loop needs access to
|
||||
both the forwarder and the backend when handling Ingress/RxReady events — the
|
||||
Phase 3 plan already accounts for this.
|
||||
|
||||
4. **`rx_full` is an `AtomicBool`, not behind a `Mutex`.** This means
|
||||
`is_rx_full()` and `clear_rx_full()` can be called without acquiring any lock,
|
||||
which is important for the fast-path check in the Ingress handler
|
||||
(`if !forwarder.is_rx_full() { forwarder.drain_rx(...) }`).
|
||||
|
||||
5. **`enqueue_with_time()` now takes a `source_peer: Option<String>` parameter.**
|
||||
The Phase 3 event loop should pass `Some(peer_name.clone())` when calling via
|
||||
`enqueue_ingress()` (already implemented) — but any other callers of
|
||||
`enqueue_with_time()` need updating to pass the extra arg. Currently there are no
|
||||
other callers besides `enqueue_ingress()` and the unit test.
|
||||
|
||||
6. **The `rx_fq_codel` config has `codel_enabled: true` regardless of the
|
||||
producer-side config.** `PacketForwarder::new()` clones the config and overrides
|
||||
`codel_enabled` to `true` for the consumer-side instance. This is intentional:
|
||||
CoDel dropping has been moved from producer-side (disabled in Phase 1) to
|
||||
consumer-side (enabled here).
|
||||
|
||||
7. **Seccomp note for Phase 3:** The `rx_ready_eventfd` is created before seccomp
|
||||
is applied. The `eventfd` syscall is allowed in the main filter but not the child
|
||||
filter — however the child creates the eventfd before `apply_child_seccomp()`, so
|
||||
this should be fine. The child seccomp filter already allows `read`/`write` which
|
||||
are needed for eventfd I/O.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 notes
|
||||
|
||||
### Plan deviations
|
||||
|
||||
- **`poll_ingress()` marked `#[allow(dead_code)]` instead of `#[cfg(test)]`.**
|
||||
The plan suggested `#[cfg(test)]` but `poll_ingress()` is also used by the
|
||||
integration test `tests/packet_flow.rs` (a separate crate), which wouldn't see
|
||||
`#[cfg(test)]`-gated methods. `#[allow(dead_code)]` preserves availability for
|
||||
all test types.
|
||||
|
||||
### Notes for Phase 4
|
||||
|
||||
1. **No new seccomp issues.** The `rx_ready_eventfd` is created before
|
||||
`apply_child_seccomp()` and only uses `read`/`write` in the datapath, both
|
||||
of which are already in the child filter whitelist. Confirmed working.
|
||||
|
||||
2. **The event loop priority order is now:** Control > TxKick > SpaceAvailable >
|
||||
RxReady > Ingress/Timeout. This ensures consumer FQ-CoDel backlog drains
|
||||
before ingesting new frames from SPSC rings.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 notes
|
||||
|
||||
### Plan deviations
|
||||
|
||||
- **`poll_ingress()` left as `#[allow(dead_code)]` (no change from Phase 3).**
|
||||
The plan's step 4.3 recommended `#[cfg(test)]`, but Phase 3 already chose
|
||||
`#[allow(dead_code)]` because `poll_ingress()` is used by the integration
|
||||
test `tests/packet_flow.rs` (a separate crate that can't see `#[cfg(test)]`
|
||||
items). No change needed.
|
||||
|
||||
- **Consumer-side global counters now just reset (no `let _ =` binding).**
|
||||
Phase 2 used `let _ = (consumer_codel, ...)` to suppress unused warnings.
|
||||
Phase 4 replaced this with the queue state debug logging, making the
|
||||
intermediate variables unnecessary.
|
||||
|
||||
### Notes for Phase 5
|
||||
|
||||
1. **`poll_ingress()` migration deferred.** Step 4.3 Option B (migrating tests
|
||||
to use `enqueue_ingress()` + `drain_rx()`) was deferred. The existing tests
|
||||
in `mod.rs` and `tests/packet_flow.rs` still use `poll_ingress()` directly.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 notes
|
||||
|
||||
### Plan deviations
|
||||
|
||||
- **`consumer_codel_drops_stale_packets` test required CoDel interval sleep.**
|
||||
The plan's test assumed that setting old `enqueue_time` values would trigger
|
||||
CoDel drops on immediate dequeue. In practice, CoDel's `should_drop()` uses
|
||||
wall-clock `now` to track how long the queue has been above target — it
|
||||
doesn't matter when packets were enqueued. The test was restructured to:
|
||||
(1) dequeue one packet to set `first_above_time`, (2) sleep for the interval
|
||||
(15ms > 10ms interval), (3) dequeue remaining packets so CoDel enters
|
||||
dropping state.
|
||||
|
||||
- **Pre-existing `clippy::too_many_arguments` warning fixed.** The `event_loop`
|
||||
function in `child/process.rs` (added in Phase 3) had 8 arguments, exceeding
|
||||
clippy's default limit of 7. Added `#[allow(clippy::too_many_arguments)]`.
|
||||
|
||||
- **Pre-existing formatting issues fixed.** `cargo fmt` reformatted many files
|
||||
beyond the test changes (pre-existing formatting drift from earlier phases).
|
||||
|
||||
- **Section 5.4 tests adapted to actual API.** The plan's pseudocode used
|
||||
simplified constructors. Tests were written to match the actual `PacketForwarder`
|
||||
API, using `FqCodelConfig`, `FlowHash`, and `FlowIdentity` with proper imports.
|
||||
|
||||
### Test summary
|
||||
|
||||
New tests added (10 total):
|
||||
- `fq_codel::tests::codel_disabled_skips_drops` — DRR works without CoDel
|
||||
- `fq_codel::tests::codel_disabled_preserves_drr_fairness` — fairness preserved
|
||||
- `fq_codel::tests::codel_disabled_overload_drops_still_work` — tail drops work
|
||||
- `fq_codel::tests::source_peer_drop_attribution` — drops attributed to source peer
|
||||
- `ring::tests::push_with_timestamp_vs_push` — timestamp behavior comparison
|
||||
- `forwarder::tests::enqueue_ingress_then_drain_delivers_to_rx_fq_codel` — end-to-end ingress
|
||||
- `forwarder::tests::enqueue_ingress_rejects_bad_mac_and_ip` — validation in ingress path
|
||||
- `forwarder::tests::consumer_codel_drops_stale_packets` — CoDel drops stale packets
|
||||
- `forwarder::tests::rx_full_flag_controls_drain_attempts` — rx_full flag behavior
|
||||
- `forwarder::tests::collect_metrics_includes_consumer_drops` — drop attribution in metrics
|
||||
|
|
@ -11,17 +11,23 @@ let
|
|||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Override kwin to add VM decoration color support via security context
|
||||
nixpkgs.overlays = [
|
||||
(final: prev: {
|
||||
kdePackages = prev.kdePackages.overrideScope (
|
||||
kfinal: kprev: {
|
||||
kwin = kprev.kwin.overrideAttrs (old: {
|
||||
patches = (old.patches or [ ]) ++ [
|
||||
../patches/0001-decorations-support-vmsilo-security-context-color-ov.patch
|
||||
../patches/0002-decorations-force-SSD-and-Tiny-borders-for-vmsilo-se.patch
|
||||
../patches/0003-decorations-draw-border-outline-on-vmsilo-popup-wind.patch
|
||||
../patches/0004-draw-colored-border-on-window-icons-for-vmsilo-secur.patch
|
||||
../patches/kwin-vmsilo-add-security-context-color-borders-and-protoc.patch
|
||||
];
|
||||
});
|
||||
plasma-workspace = kprev.plasma-workspace.overrideAttrs (old: {
|
||||
patches = (old.patches or [ ]) ++ [
|
||||
../patches/plasma-workspace-libtaskmanager-add-VmSilo-roles-for-VM-sandboxed-win.patch
|
||||
];
|
||||
});
|
||||
plasma-desktop = kprev.plasma-desktop.overrideAttrs (old: {
|
||||
patches = (old.patches or [ ]) ++ [
|
||||
../patches/plasma-desktop-taskmanager-add-vmsilo-border-and-VM-name-in-tooltip.patch
|
||||
];
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
From 94e5ef62603f65bb1a61f445b237e1512ba1fb1e Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
|
||||
Date: Sun, 8 Feb 2026 23:31:28 +0000
|
||||
Subject: [PATCH 1/4] decorations: support vmsilo security context color
|
||||
override
|
||||
|
||||
Add support for changing window decoration colors based on Wayland
|
||||
security context. When a window's security context has the format
|
||||
"vmsilo:<vmname>:<color>", the decoration uses the specified color
|
||||
for the frame and titlebar, with automatic contrast adjustment for
|
||||
foreground text.
|
||||
|
||||
Windows without a security context or with non-vmsilo contexts
|
||||
continue to use standard palette behavior.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
||||
---
|
||||
src/decorations/decoratedwindow.cpp | 49 ++++++++++++++++++++++++++++-
|
||||
1 file changed, 48 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/decorations/decoratedwindow.cpp b/src/decorations/decoratedwindow.cpp
|
||||
index ff1a4ad245..300dd996cb 100644
|
||||
--- a/src/decorations/decoratedwindow.cpp
|
||||
+++ b/src/decorations/decoratedwindow.cpp
|
||||
@@ -17,11 +17,32 @@
|
||||
#include <KDecoration3/DecoratedWindow>
|
||||
#include <KDecoration3/Decoration>
|
||||
|
||||
+#include "wayland/clientconnection.h"
|
||||
+#include "wayland/surface.h"
|
||||
+
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include <QStyle>
|
||||
#include <QToolTip>
|
||||
|
||||
+namespace {
|
||||
+QColor parseVmsiloColor(const QString &securityContextAppId)
|
||||
+{
|
||||
+ if (securityContextAppId.isEmpty() ||
|
||||
+ !securityContextAppId.startsWith(QLatin1String("vmsilo:"))) {
|
||||
+ return QColor();
|
||||
+ }
|
||||
+
|
||||
+ const QStringList parts = securityContextAppId.split(QLatin1Char(':'));
|
||||
+ if (parts.size() != 3) {
|
||||
+ return QColor();
|
||||
+ }
|
||||
+
|
||||
+ // parts[0] = "vmsilo", parts[1] = vmname, parts[2] = color
|
||||
+ return QColor(parts[2]);
|
||||
+}
|
||||
+} // anonymous namespace
|
||||
+
|
||||
namespace KWin
|
||||
{
|
||||
namespace Decoration
|
||||
@@ -187,11 +208,37 @@ void DecoratedWindowImpl::requestClose()
|
||||
|
||||
QColor DecoratedWindowImpl::color(KDecoration3::ColorGroup group, KDecoration3::ColorRole role) const
|
||||
{
|
||||
+ // Check for vmsilo security context color override
|
||||
+ if (SurfaceInterface *surf = m_window->surface()) {
|
||||
+ if (ClientConnection *client = surf->client()) {
|
||||
+ const QColor vmsiloColor = parseVmsiloColor(client->securityContextAppId());
|
||||
+ if (vmsiloColor.isValid()) {
|
||||
+ switch (role) {
|
||||
+ case KDecoration3::ColorRole::Frame:
|
||||
+ case KDecoration3::ColorRole::TitleBar:
|
||||
+ if (group == KDecoration3::ColorGroup::Inactive) {
|
||||
+ return vmsiloColor.darker(120);
|
||||
+ }
|
||||
+ return vmsiloColor;
|
||||
+ case KDecoration3::ColorRole::Foreground: {
|
||||
+ // Calculate contrasting foreground color
|
||||
+ const qreal luminance = 0.299 * vmsiloColor.redF()
|
||||
+ + 0.587 * vmsiloColor.greenF()
|
||||
+ + 0.114 * vmsiloColor.blueF();
|
||||
+ return (luminance > 0.5) ? Qt::black : Qt::white;
|
||||
+ }
|
||||
+ default:
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Fall back to standard palette behavior
|
||||
auto dp = m_window->decorationPalette();
|
||||
if (dp) {
|
||||
return dp->color(group, role);
|
||||
}
|
||||
-
|
||||
return QColor();
|
||||
}
|
||||
|
||||
--
|
||||
2.52.0
|
||||
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
From 86f7752afaff7950ada236c28fdc469efd345404 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
|
||||
Date: Mon, 9 Feb 2026 01:02:36 +0000
|
||||
Subject: [PATCH 2/4] decorations: force SSD and Tiny borders for vmsilo
|
||||
security context
|
||||
|
||||
Windows with Wayland security context app_id starting with "vmsilo:"
|
||||
must always use server-side decorations as a security measure. The app
|
||||
must not be able to override this via xdg-decoration, server-decoration
|
||||
protocol negotiation, or window rules.
|
||||
|
||||
Additionally, override the border size to Tiny (instead of the default
|
||||
NoSides) for these windows so the security-colored border from the
|
||||
previous commit is visible on all edges.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||||
---
|
||||
src/decorations/decorationbridge.cpp | 13 +++++++++++--
|
||||
src/decorations/decorationbridge.h | 4 ++++
|
||||
src/decorations/settings.cpp | 4 +++-
|
||||
src/decorations/settings.h | 7 +++++--
|
||||
src/window.cpp | 11 +++++++++++
|
||||
src/window.h | 2 ++
|
||||
src/xdgshellwindow.cpp | 6 ++++++
|
||||
7 files changed, 42 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/src/decorations/decorationbridge.cpp b/src/decorations/decorationbridge.cpp
|
||||
index bde1d82b06..48726d0fc2 100644
|
||||
--- a/src/decorations/decorationbridge.cpp
|
||||
+++ b/src/decorations/decorationbridge.cpp
|
||||
@@ -89,6 +89,10 @@ void DecorationBridge::init()
|
||||
}
|
||||
m_settings = std::make_shared<KDecoration3::DecorationSettings>(this);
|
||||
|
||||
+ m_pendingBorderSizeOverride = KDecoration3::BorderSize::Tiny;
|
||||
+ m_vmsiloSettings = std::make_shared<KDecoration3::DecorationSettings>(this);
|
||||
+ m_pendingBorderSizeOverride.reset();
|
||||
+
|
||||
const QString pluginId = readPlugin();
|
||||
if (!initPlugin(pluginId)) {
|
||||
if (s_defaultPlugin != pluginId) {
|
||||
@@ -144,6 +148,7 @@ void DecorationBridge::reconfigure()
|
||||
m_plugin = QString();
|
||||
m_factory.reset();
|
||||
m_settings.reset();
|
||||
+ m_vmsiloSettings.reset();
|
||||
} else {
|
||||
// decorations enabled now
|
||||
init();
|
||||
@@ -212,7 +217,7 @@ std::unique_ptr<KDecoration3::DecoratedWindowPrivate> DecorationBridge::createCl
|
||||
|
||||
std::unique_ptr<KDecoration3::DecorationSettingsPrivate> DecorationBridge::settings(KDecoration3::DecorationSettings *parent)
|
||||
{
|
||||
- return std::unique_ptr<SettingsImpl>(new SettingsImpl(parent));
|
||||
+ return std::make_unique<SettingsImpl>(parent, m_pendingBorderSizeOverride);
|
||||
}
|
||||
|
||||
KDecoration3::Decoration *DecorationBridge::createDecoration(Window *window)
|
||||
@@ -229,7 +234,11 @@ KDecoration3::Decoration *DecorationBridge::createDecoration(Window *window)
|
||||
args.insert(QStringLiteral("theme"), m_theme);
|
||||
}
|
||||
auto deco = m_factory->create<KDecoration3::Decoration>(window, QVariantList{args});
|
||||
- deco->setSettings(m_settings);
|
||||
+ if (window->hasVmsiloSecurityContext()) {
|
||||
+ deco->setSettings(m_vmsiloSettings);
|
||||
+ } else {
|
||||
+ deco->setSettings(m_settings);
|
||||
+ }
|
||||
deco->create();
|
||||
deco->init();
|
||||
return deco;
|
||||
diff --git a/src/decorations/decorationbridge.h b/src/decorations/decorationbridge.h
|
||||
index e06a131134..51c96dcd27 100644
|
||||
--- a/src/decorations/decorationbridge.h
|
||||
+++ b/src/decorations/decorationbridge.h
|
||||
@@ -10,7 +10,9 @@
|
||||
|
||||
#include "kwin_export.h"
|
||||
#include <KDecoration3/Private/DecorationBridge>
|
||||
+#include <KDecoration3/DecorationSettings>
|
||||
#include <QObject>
|
||||
+#include <optional>
|
||||
|
||||
class KPluginFactory;
|
||||
namespace KDecoration3
|
||||
@@ -76,6 +78,8 @@ private:
|
||||
QString m_defaultTheme;
|
||||
QString m_theme;
|
||||
std::shared_ptr<KDecoration3::DecorationSettings> m_settings;
|
||||
+ std::shared_ptr<KDecoration3::DecorationSettings> m_vmsiloSettings;
|
||||
+ std::optional<KDecoration3::BorderSize> m_pendingBorderSizeOverride;
|
||||
bool m_noPlugin;
|
||||
};
|
||||
} // Decoration
|
||||
diff --git a/src/decorations/settings.cpp b/src/decorations/settings.cpp
|
||||
index 21b5bbf5cf..70f313d636 100644
|
||||
--- a/src/decorations/settings.cpp
|
||||
+++ b/src/decorations/settings.cpp
|
||||
@@ -22,10 +22,12 @@ namespace KWin
|
||||
{
|
||||
namespace Decoration
|
||||
{
|
||||
-SettingsImpl::SettingsImpl(KDecoration3::DecorationSettings *parent)
|
||||
+SettingsImpl::SettingsImpl(KDecoration3::DecorationSettings *parent,
|
||||
+ std::optional<KDecoration3::BorderSize> borderSizeOverride)
|
||||
: QObject()
|
||||
, DecorationSettingsPrivate(parent)
|
||||
, m_borderSize(KDecoration3::BorderSize::Normal)
|
||||
+ , m_borderSizeOverride(borderSizeOverride)
|
||||
{
|
||||
readSettings();
|
||||
|
||||
diff --git a/src/decorations/settings.h b/src/decorations/settings.h
|
||||
index 128bead161..e6e4e83b60 100644
|
||||
--- a/src/decorations/settings.h
|
||||
+++ b/src/decorations/settings.h
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <KDecoration3/Private/DecorationSettingsPrivate>
|
||||
|
||||
#include <QObject>
|
||||
+#include <optional>
|
||||
|
||||
class KConfigGroup;
|
||||
|
||||
@@ -23,14 +24,15 @@ class SettingsImpl : public QObject, public KDecoration3::DecorationSettingsPriv
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
- explicit SettingsImpl(KDecoration3::DecorationSettings *parent);
|
||||
+ explicit SettingsImpl(KDecoration3::DecorationSettings *parent,
|
||||
+ std::optional<KDecoration3::BorderSize> borderSizeOverride = std::nullopt);
|
||||
~SettingsImpl() override;
|
||||
bool isAlphaChannelSupported() const override;
|
||||
bool isOnAllDesktopsAvailable() const override;
|
||||
bool isCloseOnDoubleClickOnMenu() const override;
|
||||
KDecoration3::BorderSize borderSize() const override
|
||||
{
|
||||
- return m_borderSize;
|
||||
+ return m_borderSizeOverride.value_or(m_borderSize);
|
||||
}
|
||||
QList<KDecoration3::DecorationButtonType> decorationButtonsLeft() const override
|
||||
{
|
||||
@@ -53,6 +55,7 @@ private:
|
||||
QList<KDecoration3::DecorationButtonType> m_leftButtons;
|
||||
QList<KDecoration3::DecorationButtonType> m_rightButtons;
|
||||
KDecoration3::BorderSize m_borderSize;
|
||||
+ std::optional<KDecoration3::BorderSize> m_borderSizeOverride;
|
||||
bool m_autoBorderSize = true;
|
||||
bool m_closeDoubleClickMenu = false;
|
||||
QFont m_font;
|
||||
diff --git a/src/window.cpp b/src/window.cpp
|
||||
index c4dd92c06f..e8f1901252 100644
|
||||
--- a/src/window.cpp
|
||||
+++ b/src/window.cpp
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "useractions.h"
|
||||
#include "virtualdesktops.h"
|
||||
#include "wayland/output.h"
|
||||
+#include "wayland/clientconnection.h"
|
||||
#include "wayland/plasmawindowmanagement.h"
|
||||
#include "wayland/surface.h"
|
||||
#include "wayland_server.h"
|
||||
@@ -4336,6 +4337,16 @@ bool Window::userCanSetNoBorder() const
|
||||
return false;
|
||||
}
|
||||
|
||||
+bool Window::hasVmsiloSecurityContext() const
|
||||
+{
|
||||
+ if (SurfaceInterface *surf = surface()) {
|
||||
+ if (ClientConnection *client = surf->client()) {
|
||||
+ return client->securityContextAppId().startsWith(QLatin1String("vmsilo:"));
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
void Window::setNoBorder(bool set)
|
||||
{
|
||||
qCWarning(KWIN_CORE, "%s doesn't support setting decorations", metaObject()->className());
|
||||
diff --git a/src/window.h b/src/window.h
|
||||
index 2b11fa365c..fcf37d32bb 100644
|
||||
--- a/src/window.h
|
||||
+++ b/src/window.h
|
||||
@@ -844,6 +844,8 @@ public:
|
||||
SurfaceInterface *surface() const;
|
||||
void setSurface(SurfaceInterface *surface);
|
||||
|
||||
+ bool hasVmsiloSecurityContext() const;
|
||||
+
|
||||
/**
|
||||
* @returns Transformation to map from global to window coordinates.
|
||||
*/
|
||||
diff --git a/src/xdgshellwindow.cpp b/src/xdgshellwindow.cpp
|
||||
index 092c284b8c..0439025636 100644
|
||||
--- a/src/xdgshellwindow.cpp
|
||||
+++ b/src/xdgshellwindow.cpp
|
||||
@@ -694,6 +694,9 @@ bool XdgToplevelWindow::isTransient() const
|
||||
|
||||
bool XdgToplevelWindow::userCanSetNoBorder() const
|
||||
{
|
||||
+ if (hasVmsiloSecurityContext()) {
|
||||
+ return false;
|
||||
+ }
|
||||
return (m_serverDecoration || m_xdgDecoration) && !isFullScreen();
|
||||
}
|
||||
|
||||
@@ -704,6 +707,9 @@ bool XdgToplevelWindow::noBorder() const
|
||||
|
||||
void XdgToplevelWindow::setNoBorder(bool set)
|
||||
{
|
||||
+ if (hasVmsiloSecurityContext()) {
|
||||
+ set = false;
|
||||
+ }
|
||||
set = rules()->checkNoBorder(set);
|
||||
if (m_userNoBorder == set) {
|
||||
return;
|
||||
--
|
||||
2.52.0
|
||||
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
From cf8de902570cb6555651853435ee60e406cbbceb Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
|
||||
Date: Fri, 13 Feb 2026 16:44:38 +0000
|
||||
Subject: [PATCH 3/4] decorations: draw border outline on vmsilo popup windows
|
||||
|
||||
Popup windows (xdg_popup) never receive server-side decorations, so a
|
||||
client in a vmsilo security context could spawn an oversized popup that
|
||||
visually impersonates an undecorated standalone window, bypassing the
|
||||
enforced colored-border decoration.
|
||||
|
||||
Add an OutlinedBorderItem to WindowItem for popup windows with a vmsilo
|
||||
security context. This draws a colored border (matching the VM's color)
|
||||
around every popup from that context, using the same rendering
|
||||
infrastructure as decoration outlines.
|
||||
|
||||
Also consolidate the vmsilo color parsing into Window::vmsiloColor() so
|
||||
both the decoration code and the popup border code share the same logic.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||||
---
|
||||
src/decorations/decoratedwindow.cpp | 59 +++++++++--------------------
|
||||
src/scene/windowitem.cpp | 26 +++++++++++++
|
||||
src/scene/windowitem.h | 3 ++
|
||||
src/window.cpp | 16 ++++++++
|
||||
src/window.h | 2 +
|
||||
5 files changed, 64 insertions(+), 42 deletions(-)
|
||||
|
||||
diff --git a/src/decorations/decoratedwindow.cpp b/src/decorations/decoratedwindow.cpp
|
||||
index 300dd996cb..79f3fd5e03 100644
|
||||
--- a/src/decorations/decoratedwindow.cpp
|
||||
+++ b/src/decorations/decoratedwindow.cpp
|
||||
@@ -17,32 +17,11 @@
|
||||
#include <KDecoration3/DecoratedWindow>
|
||||
#include <KDecoration3/Decoration>
|
||||
|
||||
-#include "wayland/clientconnection.h"
|
||||
-#include "wayland/surface.h"
|
||||
-
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include <QStyle>
|
||||
#include <QToolTip>
|
||||
|
||||
-namespace {
|
||||
-QColor parseVmsiloColor(const QString &securityContextAppId)
|
||||
-{
|
||||
- if (securityContextAppId.isEmpty() ||
|
||||
- !securityContextAppId.startsWith(QLatin1String("vmsilo:"))) {
|
||||
- return QColor();
|
||||
- }
|
||||
-
|
||||
- const QStringList parts = securityContextAppId.split(QLatin1Char(':'));
|
||||
- if (parts.size() != 3) {
|
||||
- return QColor();
|
||||
- }
|
||||
-
|
||||
- // parts[0] = "vmsilo", parts[1] = vmname, parts[2] = color
|
||||
- return QColor(parts[2]);
|
||||
-}
|
||||
-} // anonymous namespace
|
||||
-
|
||||
namespace KWin
|
||||
{
|
||||
namespace Decoration
|
||||
@@ -209,28 +188,24 @@ void DecoratedWindowImpl::requestClose()
|
||||
QColor DecoratedWindowImpl::color(KDecoration3::ColorGroup group, KDecoration3::ColorRole role) const
|
||||
{
|
||||
// Check for vmsilo security context color override
|
||||
- if (SurfaceInterface *surf = m_window->surface()) {
|
||||
- if (ClientConnection *client = surf->client()) {
|
||||
- const QColor vmsiloColor = parseVmsiloColor(client->securityContextAppId());
|
||||
- if (vmsiloColor.isValid()) {
|
||||
- switch (role) {
|
||||
- case KDecoration3::ColorRole::Frame:
|
||||
- case KDecoration3::ColorRole::TitleBar:
|
||||
- if (group == KDecoration3::ColorGroup::Inactive) {
|
||||
- return vmsiloColor.darker(120);
|
||||
- }
|
||||
- return vmsiloColor;
|
||||
- case KDecoration3::ColorRole::Foreground: {
|
||||
- // Calculate contrasting foreground color
|
||||
- const qreal luminance = 0.299 * vmsiloColor.redF()
|
||||
- + 0.587 * vmsiloColor.greenF()
|
||||
- + 0.114 * vmsiloColor.blueF();
|
||||
- return (luminance > 0.5) ? Qt::black : Qt::white;
|
||||
- }
|
||||
- default:
|
||||
- break;
|
||||
- }
|
||||
+ const QColor vmsiloColor = m_window->vmsiloColor();
|
||||
+ if (vmsiloColor.isValid()) {
|
||||
+ switch (role) {
|
||||
+ case KDecoration3::ColorRole::Frame:
|
||||
+ case KDecoration3::ColorRole::TitleBar:
|
||||
+ if (group == KDecoration3::ColorGroup::Inactive) {
|
||||
+ return vmsiloColor.darker(120);
|
||||
}
|
||||
+ return vmsiloColor;
|
||||
+ case KDecoration3::ColorRole::Foreground: {
|
||||
+ // Calculate contrasting foreground color
|
||||
+ const qreal luminance = 0.299 * vmsiloColor.redF()
|
||||
+ + 0.587 * vmsiloColor.greenF()
|
||||
+ + 0.114 * vmsiloColor.blueF();
|
||||
+ return (luminance > 0.5) ? Qt::black : Qt::white;
|
||||
+ }
|
||||
+ default:
|
||||
+ break;
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/src/scene/windowitem.cpp b/src/scene/windowitem.cpp
|
||||
index 832b65a909..28ce0fb15b 100644
|
||||
--- a/src/scene/windowitem.cpp
|
||||
+++ b/src/scene/windowitem.cpp
|
||||
@@ -31,6 +31,9 @@ WindowItem::WindowItem(Window *window, Item *parent)
|
||||
connect(window, &Window::decorationChanged, this, &WindowItem::updateDecorationItem);
|
||||
updateDecorationItem();
|
||||
|
||||
+ connect(window, &Window::frameGeometryChanged, this, &WindowItem::updatePopupBorderItem);
|
||||
+ updatePopupBorderItem();
|
||||
+
|
||||
connect(window, &Window::shadowChanged, this, &WindowItem::updateShadowItem);
|
||||
updateShadowItem();
|
||||
|
||||
@@ -278,6 +281,29 @@ void WindowItem::updateDecorationItem()
|
||||
}
|
||||
}
|
||||
|
||||
+void WindowItem::updatePopupBorderItem()
|
||||
+{
|
||||
+ if (m_window->isDeleted()) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const QColor color = m_window->vmsiloColor();
|
||||
+ if (!m_window->isPopupWindow() || !color.isValid() || m_window->decoration()) {
|
||||
+ m_popupBorderItem.reset();
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const QRectF innerRect(QPointF(0, 0), m_window->size());
|
||||
+ const BorderOutline outline(3.0, color, BorderRadius());
|
||||
+
|
||||
+ if (!m_popupBorderItem) {
|
||||
+ m_popupBorderItem = std::make_unique<OutlinedBorderItem>(innerRect, outline, this);
|
||||
+ } else {
|
||||
+ m_popupBorderItem->setInnerRect(innerRect);
|
||||
+ m_popupBorderItem->setOutline(outline);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
void WindowItem::updateOpacity()
|
||||
{
|
||||
setOpacity(m_window->opacity());
|
||||
diff --git a/src/scene/windowitem.h b/src/scene/windowitem.h
|
||||
index 23dcd81874..3387c9bd1f 100644
|
||||
--- a/src/scene/windowitem.h
|
||||
+++ b/src/scene/windowitem.h
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "config-kwin.h"
|
||||
|
||||
#include "scene/item.h"
|
||||
+#include "scene/outlinedborderitem.h"
|
||||
|
||||
namespace KDecoration3
|
||||
{
|
||||
@@ -71,6 +72,7 @@ private Q_SLOTS:
|
||||
void updateOpacity();
|
||||
void updateStackingOrder();
|
||||
void addSurfaceItemDamageConnects(Item *item);
|
||||
+ void updatePopupBorderItem();
|
||||
|
||||
private:
|
||||
bool computeVisibility() const;
|
||||
@@ -82,6 +84,7 @@ private:
|
||||
std::unique_ptr<SurfaceItem> m_surfaceItem;
|
||||
std::unique_ptr<DecorationItem> m_decorationItem;
|
||||
std::unique_ptr<ShadowItem> m_shadowItem;
|
||||
+ std::unique_ptr<OutlinedBorderItem> m_popupBorderItem;
|
||||
std::unique_ptr<EffectWindow> m_effectWindow;
|
||||
std::optional<int> m_elevation;
|
||||
int m_forceVisibleByHiddenCount = 0;
|
||||
diff --git a/src/window.cpp b/src/window.cpp
|
||||
index e8f1901252..e126a97459 100644
|
||||
--- a/src/window.cpp
|
||||
+++ b/src/window.cpp
|
||||
@@ -4347,6 +4347,22 @@ bool Window::hasVmsiloSecurityContext() const
|
||||
return false;
|
||||
}
|
||||
|
||||
+QColor Window::vmsiloColor() const
|
||||
+{
|
||||
+ if (SurfaceInterface *surf = surface()) {
|
||||
+ if (ClientConnection *client = surf->client()) {
|
||||
+ const QString appId = client->securityContextAppId();
|
||||
+ if (appId.startsWith(QLatin1String("vmsilo:"))) {
|
||||
+ const QStringList parts = appId.split(QLatin1Char(':'));
|
||||
+ if (parts.size() == 3) {
|
||||
+ return QColor(parts[2]);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ return QColor();
|
||||
+}
|
||||
+
|
||||
void Window::setNoBorder(bool set)
|
||||
{
|
||||
qCWarning(KWIN_CORE, "%s doesn't support setting decorations", metaObject()->className());
|
||||
diff --git a/src/window.h b/src/window.h
|
||||
index fcf37d32bb..bf76611f87 100644
|
||||
--- a/src/window.h
|
||||
+++ b/src/window.h
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
+#include <QColor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QIcon>
|
||||
#include <QKeySequence>
|
||||
@@ -845,6 +846,7 @@ public:
|
||||
void setSurface(SurfaceInterface *surface);
|
||||
|
||||
bool hasVmsiloSecurityContext() const;
|
||||
+ QColor vmsiloColor() const;
|
||||
|
||||
/**
|
||||
* @returns Transformation to map from global to window coordinates.
|
||||
--
|
||||
2.52.0
|
||||
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
From e54ead384a395f6410ee2a472fa5e2ec8e5c9aa0 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
|
||||
Date: Sun, 15 Feb 2026 02:21:05 +0000
|
||||
Subject: [PATCH 4/4] draw colored border on window icons for vmsilo security
|
||||
context
|
||||
|
||||
When a window has a vmsilo security context color, draw a thin colored
|
||||
border on the icon pixmaps so the security indicator is visible in
|
||||
icon-based UIs (task manager, alt-tab, window list). The bordered icon
|
||||
is pixmap-only (no theme name), ensuring the PlasmaWindowManagement
|
||||
protocol sends the actual pixmap data rather than a theme name that
|
||||
the client would resolve without our border.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||||
---
|
||||
src/window.cpp | 40 +++++++++++++++++++++++++++++++++++++++-
|
||||
1 file changed, 39 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/window.cpp b/src/window.cpp
|
||||
index e126a97459..6193917fdf 100644
|
||||
--- a/src/window.cpp
|
||||
+++ b/src/window.cpp
|
||||
@@ -50,6 +50,7 @@
|
||||
#include <QDir>
|
||||
#include <QJSEngine>
|
||||
#include <QMouseEvent>
|
||||
+#include <QPainter>
|
||||
#include <QStyleHints>
|
||||
|
||||
namespace KWin
|
||||
@@ -480,9 +481,46 @@ void Window::doSetSkipSwitcher()
|
||||
{
|
||||
}
|
||||
|
||||
+static QIcon applyVmsiloBorder(const QIcon &sourceIcon, const QColor &color)
|
||||
+{
|
||||
+ static const int standardSizes[] = {16, 22, 32, 48, 64, 128};
|
||||
+ QIcon borderedIcon;
|
||||
+
|
||||
+ for (int size : standardSizes) {
|
||||
+ QPixmap pix = sourceIcon.pixmap(QSize(size, size));
|
||||
+ if (pix.isNull()) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ const qreal borderWidth = qMax(1.0, static_cast<qreal>(size) / 16.0);
|
||||
+ const qreal halfPen = borderWidth / 2.0;
|
||||
+ const qreal cornerRadius = borderWidth * 2.0;
|
||||
+
|
||||
+ QPainter painter(&pix);
|
||||
+ painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
+ QPen pen(color);
|
||||
+ pen.setWidthF(borderWidth);
|
||||
+ painter.setPen(pen);
|
||||
+ painter.setBrush(Qt::NoBrush);
|
||||
+ painter.drawRoundedRect(
|
||||
+ QRectF(halfPen, halfPen, size - borderWidth, size - borderWidth),
|
||||
+ cornerRadius, cornerRadius);
|
||||
+ painter.end();
|
||||
+
|
||||
+ borderedIcon.addPixmap(pix);
|
||||
+ }
|
||||
+
|
||||
+ return borderedIcon.isNull() ? sourceIcon : borderedIcon;
|
||||
+}
|
||||
+
|
||||
void Window::setIcon(const QIcon &icon)
|
||||
{
|
||||
- m_icon = icon;
|
||||
+ const QColor color = vmsiloColor();
|
||||
+ if (color.isValid() && !icon.isNull()) {
|
||||
+ m_icon = applyVmsiloBorder(icon, color);
|
||||
+ } else {
|
||||
+ m_icon = icon;
|
||||
+ }
|
||||
Q_EMIT iconChanged();
|
||||
}
|
||||
|
||||
--
|
||||
2.52.0
|
||||
|
||||
|
|
@ -0,0 +1,819 @@
|
|||
From 1bb20725332207dd9b191615fca09524eb0cd5a1 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
|
||||
Date: Sun, 15 Feb 2026 13:38:14 +0000
|
||||
Subject: [PATCH] vmsilo: add security context color borders and protocol
|
||||
|
||||
- decorations: support vmsilo security context color override
|
||||
- decorations: force SSD and Tiny borders for vmsilo security context
|
||||
- decorations: draw border outline on vmsilo popup windows
|
||||
- window: centralize vmsilo security context parsing with cached members
|
||||
- wayland: add is_dsg_vmsilo_v1 protocol definition
|
||||
- wayland: implement is_dsg_vmsilo_manager_v1 protocol server-side
|
||||
- effects: add vmsilo color border to window overview thumbnails
|
||||
- tabbox: add vmsilo color border to alt-tab thumbnails
|
||||
- Update protocol license
|
||||
---
|
||||
src/decorations/decoratedwindow.cpp | 24 ++++-
|
||||
src/decorations/decorationbridge.cpp | 13 ++-
|
||||
src/decorations/decorationbridge.h | 4 +
|
||||
src/decorations/settings.cpp | 4 +-
|
||||
src/decorations/settings.h | 7 +-
|
||||
.../private/qml/WindowHeapDelegate.qml | 11 +++
|
||||
src/scene/windowitem.cpp | 26 ++++++
|
||||
src/scene/windowitem.h | 3 +
|
||||
src/tabbox/clientmodel.cpp | 4 +
|
||||
src/tabbox/clientmodel.h | 3 +-
|
||||
.../thumbnail_grid/contents/ui/main.qml | 11 +++
|
||||
src/wayland/CMakeLists.txt | 3 +
|
||||
src/wayland/protocols/is-dsg-vmsilo-v1.xml | 63 +++++++++++++
|
||||
src/wayland/vmsilomanager_v1.cpp | 89 +++++++++++++++++++
|
||||
src/wayland/vmsilomanager_v1.h | 32 +++++++
|
||||
src/wayland_server.cpp | 4 +
|
||||
src/wayland_server.h | 6 ++
|
||||
src/window.cpp | 51 +++++++++++
|
||||
src/window.h | 18 ++++
|
||||
src/xdgshellwindow.cpp | 6 ++
|
||||
20 files changed, 375 insertions(+), 7 deletions(-)
|
||||
create mode 100644 src/wayland/protocols/is-dsg-vmsilo-v1.xml
|
||||
create mode 100644 src/wayland/vmsilomanager_v1.cpp
|
||||
create mode 100644 src/wayland/vmsilomanager_v1.h
|
||||
|
||||
diff --git a/src/decorations/decoratedwindow.cpp b/src/decorations/decoratedwindow.cpp
|
||||
index ff1a4ad245..79f3fd5e03 100644
|
||||
--- a/src/decorations/decoratedwindow.cpp
|
||||
+++ b/src/decorations/decoratedwindow.cpp
|
||||
@@ -187,11 +187,33 @@ void DecoratedWindowImpl::requestClose()
|
||||
|
||||
QColor DecoratedWindowImpl::color(KDecoration3::ColorGroup group, KDecoration3::ColorRole role) const
|
||||
{
|
||||
+ // Check for vmsilo security context color override
|
||||
+ const QColor vmsiloColor = m_window->vmsiloColor();
|
||||
+ if (vmsiloColor.isValid()) {
|
||||
+ switch (role) {
|
||||
+ case KDecoration3::ColorRole::Frame:
|
||||
+ case KDecoration3::ColorRole::TitleBar:
|
||||
+ if (group == KDecoration3::ColorGroup::Inactive) {
|
||||
+ return vmsiloColor.darker(120);
|
||||
+ }
|
||||
+ return vmsiloColor;
|
||||
+ case KDecoration3::ColorRole::Foreground: {
|
||||
+ // Calculate contrasting foreground color
|
||||
+ const qreal luminance = 0.299 * vmsiloColor.redF()
|
||||
+ + 0.587 * vmsiloColor.greenF()
|
||||
+ + 0.114 * vmsiloColor.blueF();
|
||||
+ return (luminance > 0.5) ? Qt::black : Qt::white;
|
||||
+ }
|
||||
+ default:
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Fall back to standard palette behavior
|
||||
auto dp = m_window->decorationPalette();
|
||||
if (dp) {
|
||||
return dp->color(group, role);
|
||||
}
|
||||
-
|
||||
return QColor();
|
||||
}
|
||||
|
||||
diff --git a/src/decorations/decorationbridge.cpp b/src/decorations/decorationbridge.cpp
|
||||
index bde1d82b06..48726d0fc2 100644
|
||||
--- a/src/decorations/decorationbridge.cpp
|
||||
+++ b/src/decorations/decorationbridge.cpp
|
||||
@@ -89,6 +89,10 @@ void DecorationBridge::init()
|
||||
}
|
||||
m_settings = std::make_shared<KDecoration3::DecorationSettings>(this);
|
||||
|
||||
+ m_pendingBorderSizeOverride = KDecoration3::BorderSize::Tiny;
|
||||
+ m_vmsiloSettings = std::make_shared<KDecoration3::DecorationSettings>(this);
|
||||
+ m_pendingBorderSizeOverride.reset();
|
||||
+
|
||||
const QString pluginId = readPlugin();
|
||||
if (!initPlugin(pluginId)) {
|
||||
if (s_defaultPlugin != pluginId) {
|
||||
@@ -144,6 +148,7 @@ void DecorationBridge::reconfigure()
|
||||
m_plugin = QString();
|
||||
m_factory.reset();
|
||||
m_settings.reset();
|
||||
+ m_vmsiloSettings.reset();
|
||||
} else {
|
||||
// decorations enabled now
|
||||
init();
|
||||
@@ -212,7 +217,7 @@ std::unique_ptr<KDecoration3::DecoratedWindowPrivate> DecorationBridge::createCl
|
||||
|
||||
std::unique_ptr<KDecoration3::DecorationSettingsPrivate> DecorationBridge::settings(KDecoration3::DecorationSettings *parent)
|
||||
{
|
||||
- return std::unique_ptr<SettingsImpl>(new SettingsImpl(parent));
|
||||
+ return std::make_unique<SettingsImpl>(parent, m_pendingBorderSizeOverride);
|
||||
}
|
||||
|
||||
KDecoration3::Decoration *DecorationBridge::createDecoration(Window *window)
|
||||
@@ -229,7 +234,11 @@ KDecoration3::Decoration *DecorationBridge::createDecoration(Window *window)
|
||||
args.insert(QStringLiteral("theme"), m_theme);
|
||||
}
|
||||
auto deco = m_factory->create<KDecoration3::Decoration>(window, QVariantList{args});
|
||||
- deco->setSettings(m_settings);
|
||||
+ if (window->hasVmsiloSecurityContext()) {
|
||||
+ deco->setSettings(m_vmsiloSettings);
|
||||
+ } else {
|
||||
+ deco->setSettings(m_settings);
|
||||
+ }
|
||||
deco->create();
|
||||
deco->init();
|
||||
return deco;
|
||||
diff --git a/src/decorations/decorationbridge.h b/src/decorations/decorationbridge.h
|
||||
index e06a131134..51c96dcd27 100644
|
||||
--- a/src/decorations/decorationbridge.h
|
||||
+++ b/src/decorations/decorationbridge.h
|
||||
@@ -10,7 +10,9 @@
|
||||
|
||||
#include "kwin_export.h"
|
||||
#include <KDecoration3/Private/DecorationBridge>
|
||||
+#include <KDecoration3/DecorationSettings>
|
||||
#include <QObject>
|
||||
+#include <optional>
|
||||
|
||||
class KPluginFactory;
|
||||
namespace KDecoration3
|
||||
@@ -76,6 +78,8 @@ private:
|
||||
QString m_defaultTheme;
|
||||
QString m_theme;
|
||||
std::shared_ptr<KDecoration3::DecorationSettings> m_settings;
|
||||
+ std::shared_ptr<KDecoration3::DecorationSettings> m_vmsiloSettings;
|
||||
+ std::optional<KDecoration3::BorderSize> m_pendingBorderSizeOverride;
|
||||
bool m_noPlugin;
|
||||
};
|
||||
} // Decoration
|
||||
diff --git a/src/decorations/settings.cpp b/src/decorations/settings.cpp
|
||||
index 21b5bbf5cf..70f313d636 100644
|
||||
--- a/src/decorations/settings.cpp
|
||||
+++ b/src/decorations/settings.cpp
|
||||
@@ -22,10 +22,12 @@ namespace KWin
|
||||
{
|
||||
namespace Decoration
|
||||
{
|
||||
-SettingsImpl::SettingsImpl(KDecoration3::DecorationSettings *parent)
|
||||
+SettingsImpl::SettingsImpl(KDecoration3::DecorationSettings *parent,
|
||||
+ std::optional<KDecoration3::BorderSize> borderSizeOverride)
|
||||
: QObject()
|
||||
, DecorationSettingsPrivate(parent)
|
||||
, m_borderSize(KDecoration3::BorderSize::Normal)
|
||||
+ , m_borderSizeOverride(borderSizeOverride)
|
||||
{
|
||||
readSettings();
|
||||
|
||||
diff --git a/src/decorations/settings.h b/src/decorations/settings.h
|
||||
index 128bead161..e6e4e83b60 100644
|
||||
--- a/src/decorations/settings.h
|
||||
+++ b/src/decorations/settings.h
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <KDecoration3/Private/DecorationSettingsPrivate>
|
||||
|
||||
#include <QObject>
|
||||
+#include <optional>
|
||||
|
||||
class KConfigGroup;
|
||||
|
||||
@@ -23,14 +24,15 @@ class SettingsImpl : public QObject, public KDecoration3::DecorationSettingsPriv
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
- explicit SettingsImpl(KDecoration3::DecorationSettings *parent);
|
||||
+ explicit SettingsImpl(KDecoration3::DecorationSettings *parent,
|
||||
+ std::optional<KDecoration3::BorderSize> borderSizeOverride = std::nullopt);
|
||||
~SettingsImpl() override;
|
||||
bool isAlphaChannelSupported() const override;
|
||||
bool isOnAllDesktopsAvailable() const override;
|
||||
bool isCloseOnDoubleClickOnMenu() const override;
|
||||
KDecoration3::BorderSize borderSize() const override
|
||||
{
|
||||
- return m_borderSize;
|
||||
+ return m_borderSizeOverride.value_or(m_borderSize);
|
||||
}
|
||||
QList<KDecoration3::DecorationButtonType> decorationButtonsLeft() const override
|
||||
{
|
||||
@@ -53,6 +55,7 @@ private:
|
||||
QList<KDecoration3::DecorationButtonType> m_leftButtons;
|
||||
QList<KDecoration3::DecorationButtonType> m_rightButtons;
|
||||
KDecoration3::BorderSize m_borderSize;
|
||||
+ std::optional<KDecoration3::BorderSize> m_borderSizeOverride;
|
||||
bool m_autoBorderSize = true;
|
||||
bool m_closeDoubleClickMenu = false;
|
||||
QFont m_font;
|
||||
diff --git a/src/plugins/private/qml/WindowHeapDelegate.qml b/src/plugins/private/qml/WindowHeapDelegate.qml
|
||||
index c8779877b0..d25318143c 100644
|
||||
--- a/src/plugins/private/qml/WindowHeapDelegate.qml
|
||||
+++ b/src/plugins/private/qml/WindowHeapDelegate.qml
|
||||
@@ -171,6 +171,17 @@ ExpoCell {
|
||||
thumb.windowHeap.deleteDND(thumb.window.internalId);
|
||||
}
|
||||
|
||||
+ // Vmsilo security context color border
|
||||
+ Rectangle {
|
||||
+ border.width: 3
|
||||
+ border.color: thumb.window.vmsiloColor
|
||||
+ anchors.fill: parent
|
||||
+ anchors.margins: -border.width
|
||||
+ radius: Kirigami.Units.cornerRadius
|
||||
+ color: "transparent"
|
||||
+ visible: thumb.window.vmsiloColor.a > 0
|
||||
+ }
|
||||
+
|
||||
// Not using FrameSvg hover element intentionally for stylistic reasons
|
||||
Rectangle {
|
||||
border.width: 6
|
||||
diff --git a/src/scene/windowitem.cpp b/src/scene/windowitem.cpp
|
||||
index 832b65a909..28ce0fb15b 100644
|
||||
--- a/src/scene/windowitem.cpp
|
||||
+++ b/src/scene/windowitem.cpp
|
||||
@@ -31,6 +31,9 @@ WindowItem::WindowItem(Window *window, Item *parent)
|
||||
connect(window, &Window::decorationChanged, this, &WindowItem::updateDecorationItem);
|
||||
updateDecorationItem();
|
||||
|
||||
+ connect(window, &Window::frameGeometryChanged, this, &WindowItem::updatePopupBorderItem);
|
||||
+ updatePopupBorderItem();
|
||||
+
|
||||
connect(window, &Window::shadowChanged, this, &WindowItem::updateShadowItem);
|
||||
updateShadowItem();
|
||||
|
||||
@@ -278,6 +281,29 @@ void WindowItem::updateDecorationItem()
|
||||
}
|
||||
}
|
||||
|
||||
+void WindowItem::updatePopupBorderItem()
|
||||
+{
|
||||
+ if (m_window->isDeleted()) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const QColor color = m_window->vmsiloColor();
|
||||
+ if (!m_window->isPopupWindow() || !color.isValid() || m_window->decoration()) {
|
||||
+ m_popupBorderItem.reset();
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const QRectF innerRect(QPointF(0, 0), m_window->size());
|
||||
+ const BorderOutline outline(3.0, color, BorderRadius());
|
||||
+
|
||||
+ if (!m_popupBorderItem) {
|
||||
+ m_popupBorderItem = std::make_unique<OutlinedBorderItem>(innerRect, outline, this);
|
||||
+ } else {
|
||||
+ m_popupBorderItem->setInnerRect(innerRect);
|
||||
+ m_popupBorderItem->setOutline(outline);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
void WindowItem::updateOpacity()
|
||||
{
|
||||
setOpacity(m_window->opacity());
|
||||
diff --git a/src/scene/windowitem.h b/src/scene/windowitem.h
|
||||
index 23dcd81874..3387c9bd1f 100644
|
||||
--- a/src/scene/windowitem.h
|
||||
+++ b/src/scene/windowitem.h
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "config-kwin.h"
|
||||
|
||||
#include "scene/item.h"
|
||||
+#include "scene/outlinedborderitem.h"
|
||||
|
||||
namespace KDecoration3
|
||||
{
|
||||
@@ -71,6 +72,7 @@ private Q_SLOTS:
|
||||
void updateOpacity();
|
||||
void updateStackingOrder();
|
||||
void addSurfaceItemDamageConnects(Item *item);
|
||||
+ void updatePopupBorderItem();
|
||||
|
||||
private:
|
||||
bool computeVisibility() const;
|
||||
@@ -82,6 +84,7 @@ private:
|
||||
std::unique_ptr<SurfaceItem> m_surfaceItem;
|
||||
std::unique_ptr<DecorationItem> m_decorationItem;
|
||||
std::unique_ptr<ShadowItem> m_shadowItem;
|
||||
+ std::unique_ptr<OutlinedBorderItem> m_popupBorderItem;
|
||||
std::unique_ptr<EffectWindow> m_effectWindow;
|
||||
std::optional<int> m_elevation;
|
||||
int m_forceVisibleByHiddenCount = 0;
|
||||
diff --git a/src/tabbox/clientmodel.cpp b/src/tabbox/clientmodel.cpp
|
||||
index 62907fadab..7542542a21 100644
|
||||
--- a/src/tabbox/clientmodel.cpp
|
||||
+++ b/src/tabbox/clientmodel.cpp
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
+#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <QUuid>
|
||||
|
||||
@@ -70,6 +71,8 @@ QVariant ClientModel::data(const QModelIndex &index, int role) const
|
||||
return client->isMinimized();
|
||||
case CloseableRole:
|
||||
return client->isCloseable();
|
||||
+ case VmsiloColorRole:
|
||||
+ return client->vmsiloColor();
|
||||
case IconRole:
|
||||
if (client->isDesktop()) {
|
||||
return QIcon::fromTheme(QStringLiteral("user-desktop"));
|
||||
@@ -129,6 +132,7 @@ QHash<int, QByteArray> ClientModel::roleNames() const
|
||||
{MinimizedRole, QByteArrayLiteral("minimized")},
|
||||
{WIdRole, QByteArrayLiteral("windowId")},
|
||||
{CloseableRole, QByteArrayLiteral("closeable")},
|
||||
+ {VmsiloColorRole, QByteArrayLiteral("vmsiloColor")},
|
||||
{IconRole, QByteArrayLiteral("icon")},
|
||||
};
|
||||
}
|
||||
diff --git a/src/tabbox/clientmodel.h b/src/tabbox/clientmodel.h
|
||||
index c491feec95..f14e8bdfcc 100644
|
||||
--- a/src/tabbox/clientmodel.h
|
||||
+++ b/src/tabbox/clientmodel.h
|
||||
@@ -42,7 +42,8 @@ public:
|
||||
IconRole = Qt::UserRole + 3, // TODO: to be removed
|
||||
WIdRole = Qt::UserRole + 5, ///< The window ID of Window
|
||||
MinimizedRole = Qt::UserRole + 6, ///< Window is minimized
|
||||
- CloseableRole = Qt::UserRole + 7 ///< Window can be closed
|
||||
+ CloseableRole = Qt::UserRole + 7, ///< Window can be closed
|
||||
+ VmsiloColorRole = Qt::UserRole + 8 ///< Vmsilo security context color
|
||||
};
|
||||
explicit ClientModel(QObject *parent = nullptr);
|
||||
~ClientModel() override;
|
||||
diff --git a/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml b/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml
|
||||
index aaa437ae8d..bb2aefa391 100644
|
||||
--- a/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml
|
||||
+++ b/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml
|
||||
@@ -148,6 +148,17 @@ KWin.TabBoxSwitcher {
|
||||
wId: windowId
|
||||
}
|
||||
|
||||
+ // Vmsilo security context color border
|
||||
+ Rectangle {
|
||||
+ border.width: 3
|
||||
+ border.color: model.vmsiloColor
|
||||
+ anchors.fill: parent
|
||||
+ anchors.margins: -border.width
|
||||
+ radius: Kirigami.Units.cornerRadius
|
||||
+ color: "transparent"
|
||||
+ visible: model.vmsiloColor.a > 0
|
||||
+ }
|
||||
+
|
||||
Kirigami.Icon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.bottom
|
||||
diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt
|
||||
index 9314a5c475..8f74b13625 100644
|
||||
--- a/src/wayland/CMakeLists.txt
|
||||
+++ b/src/wayland/CMakeLists.txt
|
||||
@@ -37,6 +37,7 @@ ecm_add_qtwayland_server_protocol_kde(WaylandProtocols_xml
|
||||
|
||||
${PROJECT_SOURCE_DIR}/src/wayland/protocols/drm.xml
|
||||
${PROJECT_SOURCE_DIR}/src/wayland/protocols/frog-color-management-v1.xml
|
||||
+ ${PROJECT_SOURCE_DIR}/src/wayland/protocols/is-dsg-vmsilo-v1.xml
|
||||
${PROJECT_SOURCE_DIR}/src/wayland/protocols/wlr-layer-shell-unstable-v1.xml
|
||||
${PROJECT_SOURCE_DIR}/src/wayland/protocols/xx-pip-v1.xml
|
||||
${PROJECT_SOURCE_DIR}/src/wayland/protocols/xx-session-management-v1.xml
|
||||
@@ -108,6 +109,7 @@ target_sources(kwin PRIVATE
|
||||
dpms.cpp
|
||||
drmclientbuffer.cpp
|
||||
drmlease_v1.cpp
|
||||
+ vmsilomanager_v1.cpp
|
||||
externalbrightness_v1.cpp
|
||||
fifo_v1.cpp
|
||||
filtered_display.cpp
|
||||
@@ -202,6 +204,7 @@ install(FILES
|
||||
display.h
|
||||
dpms.h
|
||||
drmlease_v1.h
|
||||
+ vmsilomanager_v1.h
|
||||
externalbrightness_v1.h
|
||||
fifo_v1.h
|
||||
fractionalscale_v1.h
|
||||
diff --git a/src/wayland/protocols/is-dsg-vmsilo-v1.xml b/src/wayland/protocols/is-dsg-vmsilo-v1.xml
|
||||
new file mode 100644
|
||||
index 0000000000..6876661753
|
||||
--- /dev/null
|
||||
+++ b/src/wayland/protocols/is-dsg-vmsilo-v1.xml
|
||||
@@ -0,0 +1,63 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<protocol name="is_dsg_vmsilo_v1">
|
||||
+ <copyright>
|
||||
+ SPDX-FileCopyrightText: 2026, Davíð Steinn Geirsson
|
||||
+
|
||||
+ SPDX-License-Identifier: MIT
|
||||
+ </copyright>
|
||||
+
|
||||
+ <interface name="is_dsg_vmsilo_manager_v1" version="1">
|
||||
+ <description summary="broadcast vmsilo window information">
|
||||
+ The is_dsg_vmsilo_manager_v1 interface allows clients to receive
|
||||
+ information about windows that belong to vmsilo security contexts.
|
||||
+
|
||||
+ The compositor sends window events for each existing vmsilo window
|
||||
+ when a client binds this interface, followed by a done event. After
|
||||
+ the initial batch, the compositor continues to send window and
|
||||
+ window_removed events as vmsilo windows are created and destroyed.
|
||||
+
|
||||
+ Clients can match the uuid values with PlasmaWindowManagement UUIDs
|
||||
+ to associate vmsilo metadata (such as VM name and color) with
|
||||
+ specific windows.
|
||||
+ </description>
|
||||
+
|
||||
+ <request name="destroy" type="destructor">
|
||||
+ <description summary="destroy the manager">
|
||||
+ Destroy this is_dsg_vmsilo_manager_v1 object. The compositor will
|
||||
+ no longer send events to this client.
|
||||
+ </description>
|
||||
+ </request>
|
||||
+
|
||||
+ <event name="window">
|
||||
+ <description summary="a vmsilo window exists">
|
||||
+ Sent for each existing vmsilo window when the client first binds
|
||||
+ the interface, and again whenever a new vmsilo window is created.
|
||||
+
|
||||
+ The uuid identifies the window and corresponds to the UUID exposed
|
||||
+ by the PlasmaWindowManagement protocol. The vm_name is the
|
||||
+ human-readable name of the virtual machine. The color is an ARGB
|
||||
+ value representing the VM's assigned color.
|
||||
+ </description>
|
||||
+ <arg name="uuid" type="string" summary="window UUID matching PlasmaWindowManagement"/>
|
||||
+ <arg name="vm_name" type="string" summary="human-readable VM name"/>
|
||||
+ <arg name="color" type="uint" summary="ARGB color value for the VM"/>
|
||||
+ </event>
|
||||
+
|
||||
+ <event name="window_removed">
|
||||
+ <description summary="a vmsilo window was destroyed">
|
||||
+ Sent when a previously advertised vmsilo window is destroyed.
|
||||
+ The uuid identifies which window was removed.
|
||||
+ </description>
|
||||
+ <arg name="uuid" type="string" summary="UUID of the removed window"/>
|
||||
+ </event>
|
||||
+
|
||||
+ <event name="done">
|
||||
+ <description summary="end of initial window list">
|
||||
+ Sent after the initial batch of window events when a client first
|
||||
+ binds the interface. This signals that all existing vmsilo windows
|
||||
+ have been reported. Subsequent window and window_removed events
|
||||
+ are sent individually as changes occur.
|
||||
+ </description>
|
||||
+ </event>
|
||||
+ </interface>
|
||||
+</protocol>
|
||||
diff --git a/src/wayland/vmsilomanager_v1.cpp b/src/wayland/vmsilomanager_v1.cpp
|
||||
new file mode 100644
|
||||
index 0000000000..a01f8dcc2f
|
||||
--- /dev/null
|
||||
+++ b/src/wayland/vmsilomanager_v1.cpp
|
||||
@@ -0,0 +1,89 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+// SPDX-FileCopyrightText: 2025 David Geiger
|
||||
+
|
||||
+#include "vmsilomanager_v1.h"
|
||||
+#include "display.h"
|
||||
+
|
||||
+#include <QColor>
|
||||
+
|
||||
+#include "qwayland-server-is-dsg-vmsilo-v1.h"
|
||||
+
|
||||
+namespace KWin
|
||||
+{
|
||||
+
|
||||
+class VmsiloManagerV1InterfacePrivate : public QtWaylandServer::is_dsg_vmsilo_manager_v1
|
||||
+{
|
||||
+public:
|
||||
+ VmsiloManagerV1InterfacePrivate(Display *display);
|
||||
+
|
||||
+ // Track active vmsilo windows for initial state on bind
|
||||
+ struct VmsiloWindow {
|
||||
+ QString uuid;
|
||||
+ QString vmName;
|
||||
+ uint32_t color;
|
||||
+ };
|
||||
+ QList<VmsiloWindow> windows;
|
||||
+
|
||||
+protected:
|
||||
+ void is_dsg_vmsilo_manager_v1_bind_resource(Resource *resource) override;
|
||||
+ void is_dsg_vmsilo_manager_v1_destroy(Resource *resource) override;
|
||||
+};
|
||||
+
|
||||
+VmsiloManagerV1InterfacePrivate::VmsiloManagerV1InterfacePrivate(Display *display)
|
||||
+ : QtWaylandServer::is_dsg_vmsilo_manager_v1(*display, 1)
|
||||
+{
|
||||
+}
|
||||
+
|
||||
+void VmsiloManagerV1InterfacePrivate::is_dsg_vmsilo_manager_v1_bind_resource(Resource *resource)
|
||||
+{
|
||||
+ for (const auto &w : std::as_const(windows)) {
|
||||
+ send_window(resource->handle, w.uuid, w.vmName, w.color);
|
||||
+ }
|
||||
+ send_done(resource->handle);
|
||||
+}
|
||||
+
|
||||
+void VmsiloManagerV1InterfacePrivate::is_dsg_vmsilo_manager_v1_destroy(Resource *resource)
|
||||
+{
|
||||
+ wl_resource_destroy(resource->handle);
|
||||
+}
|
||||
+
|
||||
+// Public interface
|
||||
+
|
||||
+VmsiloManagerV1Interface::VmsiloManagerV1Interface(Display *display, QObject *parent)
|
||||
+ : QObject(parent)
|
||||
+ , d(std::make_unique<VmsiloManagerV1InterfacePrivate>(display))
|
||||
+{
|
||||
+}
|
||||
+
|
||||
+VmsiloManagerV1Interface::~VmsiloManagerV1Interface() = default;
|
||||
+
|
||||
+void VmsiloManagerV1Interface::sendWindow(const QString &uuid, const QString &vmName, const QColor &color)
|
||||
+{
|
||||
+ // QColor::rgba() returns #AARRGGBB despite its name
|
||||
+ const uint32_t argb = color.rgba();
|
||||
+
|
||||
+ // Track for future binds
|
||||
+ d->windows.append({uuid, vmName, argb});
|
||||
+
|
||||
+ // Broadcast to all currently bound clients
|
||||
+ const auto resources = d->resourceMap();
|
||||
+ for (auto resource : resources) {
|
||||
+ d->send_window(resource->handle, uuid, vmName, argb);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+void VmsiloManagerV1Interface::sendWindowRemoved(const QString &uuid)
|
||||
+{
|
||||
+ // Remove from tracked windows
|
||||
+ d->windows.removeIf([&uuid](const auto &w) { return w.uuid == uuid; });
|
||||
+
|
||||
+ // Broadcast to all currently bound clients
|
||||
+ const auto resources = d->resourceMap();
|
||||
+ for (auto resource : resources) {
|
||||
+ d->send_window_removed(resource->handle, uuid);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+} // namespace KWin
|
||||
+
|
||||
+#include "moc_vmsilomanager_v1.cpp"
|
||||
diff --git a/src/wayland/vmsilomanager_v1.h b/src/wayland/vmsilomanager_v1.h
|
||||
new file mode 100644
|
||||
index 0000000000..5d35e711ea
|
||||
--- /dev/null
|
||||
+++ b/src/wayland/vmsilomanager_v1.h
|
||||
@@ -0,0 +1,32 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+// SPDX-FileCopyrightText: 2025 David Geiger
|
||||
+
|
||||
+#pragma once
|
||||
+
|
||||
+#include <QObject>
|
||||
+#include <memory>
|
||||
+
|
||||
+class QColor;
|
||||
+
|
||||
+namespace KWin
|
||||
+{
|
||||
+
|
||||
+class Display;
|
||||
+class VmsiloManagerV1InterfacePrivate;
|
||||
+
|
||||
+class VmsiloManagerV1Interface : public QObject
|
||||
+{
|
||||
+ Q_OBJECT
|
||||
+
|
||||
+public:
|
||||
+ explicit VmsiloManagerV1Interface(Display *display, QObject *parent = nullptr);
|
||||
+ ~VmsiloManagerV1Interface() override;
|
||||
+
|
||||
+ void sendWindow(const QString &uuid, const QString &vmName, const QColor &color);
|
||||
+ void sendWindowRemoved(const QString &uuid);
|
||||
+
|
||||
+private:
|
||||
+ std::unique_ptr<VmsiloManagerV1InterfacePrivate> d;
|
||||
+};
|
||||
+
|
||||
+} // namespace KWin
|
||||
diff --git a/src/wayland_server.cpp b/src/wayland_server.cpp
|
||||
index c73d9b39f2..80dab10244 100644
|
||||
--- a/src/wayland_server.cpp
|
||||
+++ b/src/wayland_server.cpp
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "wayland/dpms.h"
|
||||
#include "wayland/drmclientbuffer.h"
|
||||
#include "wayland/drmlease_v1.h"
|
||||
+#include "wayland/vmsilomanager_v1.h"
|
||||
#include "wayland/externalbrightness_v1.h"
|
||||
#include "wayland/fifo_v1.h"
|
||||
#include "wayland/filtered_display.h"
|
||||
@@ -141,6 +142,7 @@ public:
|
||||
QByteArrayLiteral("zkde_screencast_unstable_v1"),
|
||||
QByteArrayLiteral("org_kde_plasma_activation_feedback"),
|
||||
QByteArrayLiteral("kde_lockscreen_overlay_v1"),
|
||||
+ QByteArrayLiteral("is_dsg_vmsilo_manager_v1"),
|
||||
};
|
||||
|
||||
const QSet<QByteArray> inputmethodInterfaces = {"zwp_input_panel_v1", "zwp_input_method_v1"};
|
||||
@@ -459,6 +461,8 @@ bool WaylandServer::init()
|
||||
m_virtualDesktopManagement = new PlasmaVirtualDesktopManagementInterface(m_display, m_display);
|
||||
m_windowManagement->setPlasmaVirtualDesktopManagementInterface(m_virtualDesktopManagement);
|
||||
|
||||
+ m_vmsiloManager = new VmsiloManagerV1Interface(m_display, m_display);
|
||||
+
|
||||
m_plasmaActivationFeedback = new PlasmaWindowActivationFeedbackInterface(m_display, m_display);
|
||||
|
||||
new ShadowManagerInterface(m_display, m_display);
|
||||
diff --git a/src/wayland_server.h b/src/wayland_server.h
|
||||
index c4727724b9..eaa88db5d4 100644
|
||||
--- a/src/wayland_server.h
|
||||
+++ b/src/wayland_server.h
|
||||
@@ -41,6 +41,7 @@ class XdgExportedSurface;
|
||||
class XdgForeignV2Interface;
|
||||
class XdgOutputManagerV1Interface;
|
||||
class DrmClientBufferIntegration;
|
||||
+class VmsiloManagerV1Interface;
|
||||
class LinuxDmaBufV1ClientBufferIntegration;
|
||||
class TabletManagerV2Interface;
|
||||
class KeyboardShortcutsInhibitManagerV1Interface;
|
||||
@@ -114,6 +115,10 @@ public:
|
||||
{
|
||||
return m_windowManagement;
|
||||
}
|
||||
+ VmsiloManagerV1Interface *vmsiloManager() const
|
||||
+ {
|
||||
+ return m_vmsiloManager;
|
||||
+ }
|
||||
ServerSideDecorationManagerInterface *decorationManager() const
|
||||
{
|
||||
return m_decorationManager;
|
||||
@@ -263,6 +268,7 @@ private:
|
||||
PlasmaShellInterface *m_plasmaShell = nullptr;
|
||||
PlasmaWindowActivationFeedbackInterface *m_plasmaActivationFeedback = nullptr;
|
||||
PlasmaWindowManagementInterface *m_windowManagement = nullptr;
|
||||
+ VmsiloManagerV1Interface *m_vmsiloManager = nullptr;
|
||||
PlasmaVirtualDesktopManagementInterface *m_virtualDesktopManagement = nullptr;
|
||||
ServerSideDecorationManagerInterface *m_decorationManager = nullptr;
|
||||
OutputManagementV2Interface *m_outputManagement = nullptr;
|
||||
diff --git a/src/window.cpp b/src/window.cpp
|
||||
index c4dd92c06f..7ffd6d1a81 100644
|
||||
--- a/src/window.cpp
|
||||
+++ b/src/window.cpp
|
||||
@@ -36,6 +36,8 @@
|
||||
#include "useractions.h"
|
||||
#include "virtualdesktops.h"
|
||||
#include "wayland/output.h"
|
||||
+#include "wayland/clientconnection.h"
|
||||
+#include "wayland/vmsilomanager_v1.h"
|
||||
#include "wayland/plasmawindowmanagement.h"
|
||||
#include "wayland/surface.h"
|
||||
#include "wayland_server.h"
|
||||
@@ -332,6 +334,7 @@ void Window::setSurface(SurfaceInterface *surface)
|
||||
return;
|
||||
}
|
||||
m_surface = surface;
|
||||
+ setupVmsiloContext();
|
||||
Q_EMIT surfaceChanged();
|
||||
}
|
||||
|
||||
@@ -1911,6 +1914,17 @@ void Window::setupWindowManagementInterface()
|
||||
});
|
||||
|
||||
m_windowManagementInterface = w;
|
||||
+
|
||||
+ // Notify vmsilo protocol about this window
|
||||
+ if (hasVmsiloSecurityContext()) {
|
||||
+ if (auto *vmsiloMgr = waylandServer()->vmsiloManager()) {
|
||||
+ const QString uuid = internalId().toString();
|
||||
+ vmsiloMgr->sendWindow(uuid, vmsiloName(), vmsiloColor());
|
||||
+ connect(this, &Window::closed, vmsiloMgr, [uuid, vmsiloMgr]() {
|
||||
+ vmsiloMgr->sendWindowRemoved(uuid);
|
||||
+ });
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
void Window::destroyWindowManagementInterface()
|
||||
@@ -4336,6 +4350,43 @@ bool Window::userCanSetNoBorder() const
|
||||
return false;
|
||||
}
|
||||
|
||||
+bool Window::hasVmsiloSecurityContext() const
|
||||
+{
|
||||
+ return m_vmsiloColor.isValid();
|
||||
+}
|
||||
+
|
||||
+QString Window::vmsiloName() const
|
||||
+{
|
||||
+ return m_vmsiloName;
|
||||
+}
|
||||
+
|
||||
+QColor Window::vmsiloColor() const
|
||||
+{
|
||||
+ return m_vmsiloColor;
|
||||
+}
|
||||
+
|
||||
+void Window::setupVmsiloContext()
|
||||
+{
|
||||
+ m_vmsiloName.clear();
|
||||
+ m_vmsiloColor = QColor();
|
||||
+
|
||||
+ if (SurfaceInterface *surf = surface()) {
|
||||
+ if (ClientConnection *client = surf->client()) {
|
||||
+ const QString appId = client->securityContextAppId();
|
||||
+ if (appId.startsWith(QLatin1String("vmsilo:"))) {
|
||||
+ const QStringList parts = appId.split(QLatin1Char(':'));
|
||||
+ if (parts.size() == 3) {
|
||||
+ QColor color(parts[2]);
|
||||
+ if (color.isValid()) {
|
||||
+ m_vmsiloName = parts[1];
|
||||
+ m_vmsiloColor = color;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
void Window::setNoBorder(bool set)
|
||||
{
|
||||
qCWarning(KWIN_CORE, "%s doesn't support setting decorations", metaObject()->className());
|
||||
diff --git a/src/window.h b/src/window.h
|
||||
index 2b11fa365c..10841604eb 100644
|
||||
--- a/src/window.h
|
||||
+++ b/src/window.h
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
+#include <QColor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QIcon>
|
||||
#include <QKeySequence>
|
||||
@@ -571,6 +572,16 @@ class KWIN_EXPORT Window : public QObject
|
||||
*/
|
||||
Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
|
||||
|
||||
+ /**
|
||||
+ * The VM name from the vmsilo security context (e.g. "trusted").
|
||||
+ */
|
||||
+ Q_PROPERTY(QString vmsiloName READ vmsiloName CONSTANT)
|
||||
+
|
||||
+ /**
|
||||
+ * The color associated with the vmsilo security context.
|
||||
+ */
|
||||
+ Q_PROPERTY(QColor vmsiloColor READ vmsiloColor CONSTANT)
|
||||
+
|
||||
public:
|
||||
~Window() override;
|
||||
|
||||
@@ -844,6 +855,10 @@ public:
|
||||
SurfaceInterface *surface() const;
|
||||
void setSurface(SurfaceInterface *surface);
|
||||
|
||||
+ bool hasVmsiloSecurityContext() const;
|
||||
+ QString vmsiloName() const;
|
||||
+ QColor vmsiloColor() const;
|
||||
+
|
||||
/**
|
||||
* @returns Transformation to map from global to window coordinates.
|
||||
*/
|
||||
@@ -1762,6 +1777,7 @@ protected:
|
||||
void setTargetScale(qreal scale);
|
||||
|
||||
void setDescription(const QString &description);
|
||||
+ void setupVmsiloContext();
|
||||
|
||||
Output *m_output = nullptr;
|
||||
QRectF m_frameGeometry;
|
||||
@@ -1787,6 +1803,8 @@ protected:
|
||||
ClientMachine *m_clientMachine;
|
||||
bool m_skipCloseAnimation;
|
||||
QPointer<SurfaceInterface> m_surface;
|
||||
+ QString m_vmsiloName;
|
||||
+ QColor m_vmsiloColor;
|
||||
qreal m_opacity = 1.0;
|
||||
int m_stackingOrder = 0;
|
||||
|
||||
diff --git a/src/xdgshellwindow.cpp b/src/xdgshellwindow.cpp
|
||||
index 092c284b8c..0439025636 100644
|
||||
--- a/src/xdgshellwindow.cpp
|
||||
+++ b/src/xdgshellwindow.cpp
|
||||
@@ -694,6 +694,9 @@ bool XdgToplevelWindow::isTransient() const
|
||||
|
||||
bool XdgToplevelWindow::userCanSetNoBorder() const
|
||||
{
|
||||
+ if (hasVmsiloSecurityContext()) {
|
||||
+ return false;
|
||||
+ }
|
||||
return (m_serverDecoration || m_xdgDecoration) && !isFullScreen();
|
||||
}
|
||||
|
||||
@@ -704,6 +707,9 @@ bool XdgToplevelWindow::noBorder() const
|
||||
|
||||
void XdgToplevelWindow::setNoBorder(bool set)
|
||||
{
|
||||
+ if (hasVmsiloSecurityContext()) {
|
||||
+ set = false;
|
||||
+ }
|
||||
set = rules()->checkNoBorder(set);
|
||||
if (m_userNoBorder == set) {
|
||||
return;
|
||||
--
|
||||
2.52.0
|
||||
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
From 85ce54ef6878a7560a74d32618385f78ccfd9780 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
|
||||
Date: Sun, 15 Feb 2026 14:23:21 +0000
|
||||
Subject: [PATCH] taskmanager: add vmsilo border and VM name in tooltip
|
||||
|
||||
Add colored border overlay and VM name display for VM-sandboxed
|
||||
windows in the task manager. Reads VmSiloColor and VmSiloName
|
||||
model roles from libtaskmanager.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||||
---
|
||||
.../taskmanager/package/contents/ui/Task.qml | 17 ++++++++++++++++-
|
||||
.../package/contents/ui/ToolTipDelegate.qml | 3 +++
|
||||
.../package/contents/ui/ToolTipInstance.qml | 5 +++++
|
||||
3 files changed, 24 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/applets/taskmanager/package/contents/ui/Task.qml b/applets/taskmanager/package/contents/ui/Task.qml
|
||||
index 2018c56478..9ad2c3c66f 100644
|
||||
--- a/applets/taskmanager/package/contents/ui/Task.qml
|
||||
+++ b/applets/taskmanager/package/contents/ui/Task.qml
|
||||
@@ -74,6 +74,9 @@ PlasmaCore.ToolTipArea {
|
||||
readonly property bool playingAudio: hasAudioStream && audioStreams.some(item => !item.corked)
|
||||
readonly property bool muted: hasAudioStream && audioStreams.every(item => item.muted)
|
||||
|
||||
+ readonly property color vmsiloColor: model.VmSiloColor ?? "transparent"
|
||||
+ readonly property bool isVmsilo: task.vmsiloColor.a > 0
|
||||
+
|
||||
readonly property bool highlighted: (inPopup && activeFocus) || (!inPopup && containsMouse)
|
||||
|| (task.contextMenu && task.contextMenu.status === PlasmaExtras.Menu.Open)
|
||||
|| (!!tasksRoot.groupDialog && tasksRoot.groupDialog.visualParent === task)
|
||||
@@ -146,7 +149,7 @@ PlasmaCore.ToolTipArea {
|
||||
id: translateTransform
|
||||
}
|
||||
|
||||
- Accessible.name: model.display
|
||||
+ Accessible.name: model.display + (task.isVmsilo ? " " + i18nc("@info:tooltip VM name indicator for accessibility", "(VM: %1)", model.VmSiloName ?? "") : "")
|
||||
Accessible.description: {
|
||||
if (!model.display) {
|
||||
return "";
|
||||
@@ -365,6 +368,7 @@ PlasmaCore.ToolTipArea {
|
||||
mainItem.virtualDesktops = Qt.binding(() => model.VirtualDesktops);
|
||||
mainItem.isOnAllVirtualDesktops = Qt.binding(() => model.IsOnAllVirtualDesktops);
|
||||
mainItem.activities = Qt.binding(() => model.Activities);
|
||||
+ mainItem.vmSiloName = Qt.binding(() => model.VmSiloName ?? "");
|
||||
|
||||
mainItem.smartLauncherCountVisible = Qt.binding(() => smartLauncherItem?.countVisible ?? false);
|
||||
mainItem.smartLauncherCount = Qt.binding(() => mainItem.smartLauncherCountVisible ? (smartLauncherItem?.count ?? 0) : 0);
|
||||
@@ -517,6 +521,17 @@ PlasmaCore.ToolTipArea {
|
||||
}
|
||||
}
|
||||
|
||||
+ Rectangle {
|
||||
+ id: vmsiloBorder
|
||||
+ anchors.fill: frame
|
||||
+ color: "transparent"
|
||||
+ border.color: task.vmsiloColor
|
||||
+ border.width: 2
|
||||
+ radius: 3
|
||||
+ visible: task.isVmsilo
|
||||
+ z: 10
|
||||
+ }
|
||||
+
|
||||
Loader {
|
||||
id: taskProgressOverlayLoader
|
||||
|
||||
diff --git a/applets/taskmanager/package/contents/ui/ToolTipDelegate.qml b/applets/taskmanager/package/contents/ui/ToolTipDelegate.qml
|
||||
index 0a9e27bb5e..dd1a9e403b 100644
|
||||
--- a/applets/taskmanager/package/contents/ui/ToolTipDelegate.qml
|
||||
+++ b/applets/taskmanager/package/contents/ui/ToolTipDelegate.qml
|
||||
@@ -48,6 +48,7 @@ Loader {
|
||||
|
||||
property bool smartLauncherCountVisible
|
||||
property int smartLauncherCount
|
||||
+ property string vmSiloName
|
||||
|
||||
property bool blockingUpdates: false
|
||||
|
||||
@@ -78,6 +79,7 @@ Loader {
|
||||
isOnAllVirtualDesktops: toolTipDelegate.isOnAllVirtualDesktops
|
||||
virtualDesktops: toolTipDelegate.virtualDesktops
|
||||
activities: toolTipDelegate.activities
|
||||
+ vmSiloName: toolTipDelegate.vmSiloName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +137,7 @@ Loader {
|
||||
activities: model.Activities
|
||||
hasTrackInATitle: groupToolTipListView.hasTrackInATitle
|
||||
orientation: groupToolTipListView.orientation
|
||||
+ vmSiloName: model.VmSiloName ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
diff --git a/applets/taskmanager/package/contents/ui/ToolTipInstance.qml b/applets/taskmanager/package/contents/ui/ToolTipInstance.qml
|
||||
index ebc2957138..4c80e9c424 100644
|
||||
--- a/applets/taskmanager/package/contents/ui/ToolTipInstance.qml
|
||||
+++ b/applets/taskmanager/package/contents/ui/ToolTipInstance.qml
|
||||
@@ -32,6 +32,7 @@ ColumnLayout {
|
||||
required property bool isOnAllVirtualDesktops
|
||||
required property /*list<var>*/ var virtualDesktops // Can't use list<var> because of QTBUG-127600
|
||||
required property list<string> activities
|
||||
+ property string vmSiloName
|
||||
|
||||
property bool hasTrackInATitle: false
|
||||
property int orientation: ListView.Vertical // vertical for compact single-window tooltips
|
||||
@@ -483,6 +484,10 @@ ColumnLayout {
|
||||
function generateSubText(): string {
|
||||
const subTextEntries = [];
|
||||
|
||||
+ if (vmSiloName.length > 0) {
|
||||
+ subTextEntries.push(i18nc("VM sandbox name shown in tooltip", "VM: %1", vmSiloName));
|
||||
+ }
|
||||
+
|
||||
if (!Plasmoid.configuration.showOnlyCurrentDesktop && virtualDesktopInfo.numberOfDesktops > 1) {
|
||||
if (!isOnAllVirtualDesktops && virtualDesktops.length > 0) {
|
||||
const virtualDesktopNameList = virtualDesktops.map(virtualDesktop => {
|
||||
--
|
||||
2.52.0
|
||||
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
From 631b2c947af59ece1d9d26b7887dfb9a580e3d8b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
|
||||
Date: Sun, 15 Feb 2026 14:29:53 +0000
|
||||
Subject: [PATCH] libtaskmanager: add VmSilo roles for VM-sandboxed window
|
||||
metadata
|
||||
|
||||
Expose VmSiloColor and VmSiloName model roles in libtaskmanager by
|
||||
consuming the is-dsg-vmsilo-v1 Wayland protocol. This allows the task
|
||||
manager applet to render colored borders and VM labels for windows
|
||||
running inside VM silos.
|
||||
|
||||
- Add VmSiloColor (QColor) and VmSiloName (QString) to AdditionalRoles
|
||||
- Copy is-dsg-vmsilo-v1.xml protocol and add client code generation
|
||||
- Implement VmsiloManager as a QWaylandClientExtension that maintains
|
||||
a UUID-to-{name,color} map from protocol events
|
||||
- Wire VmsiloManager into WaylandTasksModel::data() for the new roles
|
||||
- Add group aggregation in TaskGroupingProxyModel (unanimous-or-empty)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||||
---
|
||||
libtaskmanager/CMakeLists.txt | 1 +
|
||||
libtaskmanager/abstracttasksmodel.cpp | 5 ++
|
||||
libtaskmanager/abstracttasksmodel.h | 2 +
|
||||
libtaskmanager/protocols/is-dsg-vmsilo-v1.xml | 63 +++++++++++++
|
||||
libtaskmanager/taskgroupingproxymodel.cpp | 25 ++++++
|
||||
libtaskmanager/waylandtasksmodel.cpp | 90 +++++++++++++++++++
|
||||
shell/org.kde.plasmashell.desktop.cmake | 2 +-
|
||||
7 files changed, 187 insertions(+), 1 deletion(-)
|
||||
create mode 100644 libtaskmanager/protocols/is-dsg-vmsilo-v1.xml
|
||||
|
||||
diff --git a/libtaskmanager/CMakeLists.txt b/libtaskmanager/CMakeLists.txt
|
||||
index 9e2133283d..bcb2580220 100644
|
||||
--- a/libtaskmanager/CMakeLists.txt
|
||||
+++ b/libtaskmanager/CMakeLists.txt
|
||||
@@ -51,6 +51,7 @@ qt6_generate_wayland_protocol_client_sources(taskmanager
|
||||
FILES ${PLASMA_WAYLAND_PROTOCOLS_DIR}/org-kde-plasma-virtual-desktop.xml
|
||||
FILES ${PLASMA_WAYLAND_PROTOCOLS_DIR}/plasma-window-management.xml
|
||||
FILES ${PLASMA_WAYLAND_PROTOCOLS_DIR}/zkde-screencast-unstable-v1.xml
|
||||
+ FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/is-dsg-vmsilo-v1.xml
|
||||
)
|
||||
|
||||
generate_export_header(taskmanager)
|
||||
diff --git a/libtaskmanager/abstracttasksmodel.cpp b/libtaskmanager/abstracttasksmodel.cpp
|
||||
index f911ac6319..3a0f1681eb 100644
|
||||
--- a/libtaskmanager/abstracttasksmodel.cpp
|
||||
+++ b/libtaskmanager/abstracttasksmodel.cpp
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "abstracttasksmodel.h"
|
||||
|
||||
+#include <QColor>
|
||||
#include <QMetaEnum>
|
||||
|
||||
namespace TaskManager
|
||||
@@ -104,6 +105,10 @@ QVariant AbstractTasksModel::data(const QModelIndex &, int role) const
|
||||
case CanSetNoBorder:
|
||||
case HasNoBorder:
|
||||
return false;
|
||||
+ case VmSiloColor:
|
||||
+ return QColor();
|
||||
+ case VmSiloName:
|
||||
+ return QString();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
diff --git a/libtaskmanager/abstracttasksmodel.h b/libtaskmanager/abstracttasksmodel.h
|
||||
index dbee2a13ed..6b7cdf5318 100644
|
||||
--- a/libtaskmanager/abstracttasksmodel.h
|
||||
+++ b/libtaskmanager/abstracttasksmodel.h
|
||||
@@ -95,6 +95,8 @@ public:
|
||||
CanLaunchNewInstance, /**< A new instance of the task can be launched. @since 5.24 */
|
||||
HasNoBorder, /**< Whether the task's window has the no border state set. @since 6.4 */
|
||||
CanSetNoBorder, /**< Whether the task can set no border state. @since 6.4 */
|
||||
+ VmSiloColor, /**< QColor: ARGB border color for VM-sandboxed windows. Invalid if not a vmsilo window. @since 6.6 */
|
||||
+ VmSiloName, /**< QString: Human-readable VM name for VM-sandboxed windows. Empty if not a vmsilo window. @since 6.6 */
|
||||
};
|
||||
Q_ENUM(AdditionalRoles)
|
||||
|
||||
diff --git a/libtaskmanager/protocols/is-dsg-vmsilo-v1.xml b/libtaskmanager/protocols/is-dsg-vmsilo-v1.xml
|
||||
new file mode 100644
|
||||
index 0000000000..6876661753
|
||||
--- /dev/null
|
||||
+++ b/libtaskmanager/protocols/is-dsg-vmsilo-v1.xml
|
||||
@@ -0,0 +1,63 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<protocol name="is_dsg_vmsilo_v1">
|
||||
+ <copyright>
|
||||
+ SPDX-FileCopyrightText: 2026, Davíð Steinn Geirsson
|
||||
+
|
||||
+ SPDX-License-Identifier: MIT
|
||||
+ </copyright>
|
||||
+
|
||||
+ <interface name="is_dsg_vmsilo_manager_v1" version="1">
|
||||
+ <description summary="broadcast vmsilo window information">
|
||||
+ The is_dsg_vmsilo_manager_v1 interface allows clients to receive
|
||||
+ information about windows that belong to vmsilo security contexts.
|
||||
+
|
||||
+ The compositor sends window events for each existing vmsilo window
|
||||
+ when a client binds this interface, followed by a done event. After
|
||||
+ the initial batch, the compositor continues to send window and
|
||||
+ window_removed events as vmsilo windows are created and destroyed.
|
||||
+
|
||||
+ Clients can match the uuid values with PlasmaWindowManagement UUIDs
|
||||
+ to associate vmsilo metadata (such as VM name and color) with
|
||||
+ specific windows.
|
||||
+ </description>
|
||||
+
|
||||
+ <request name="destroy" type="destructor">
|
||||
+ <description summary="destroy the manager">
|
||||
+ Destroy this is_dsg_vmsilo_manager_v1 object. The compositor will
|
||||
+ no longer send events to this client.
|
||||
+ </description>
|
||||
+ </request>
|
||||
+
|
||||
+ <event name="window">
|
||||
+ <description summary="a vmsilo window exists">
|
||||
+ Sent for each existing vmsilo window when the client first binds
|
||||
+ the interface, and again whenever a new vmsilo window is created.
|
||||
+
|
||||
+ The uuid identifies the window and corresponds to the UUID exposed
|
||||
+ by the PlasmaWindowManagement protocol. The vm_name is the
|
||||
+ human-readable name of the virtual machine. The color is an ARGB
|
||||
+ value representing the VM's assigned color.
|
||||
+ </description>
|
||||
+ <arg name="uuid" type="string" summary="window UUID matching PlasmaWindowManagement"/>
|
||||
+ <arg name="vm_name" type="string" summary="human-readable VM name"/>
|
||||
+ <arg name="color" type="uint" summary="ARGB color value for the VM"/>
|
||||
+ </event>
|
||||
+
|
||||
+ <event name="window_removed">
|
||||
+ <description summary="a vmsilo window was destroyed">
|
||||
+ Sent when a previously advertised vmsilo window is destroyed.
|
||||
+ The uuid identifies which window was removed.
|
||||
+ </description>
|
||||
+ <arg name="uuid" type="string" summary="UUID of the removed window"/>
|
||||
+ </event>
|
||||
+
|
||||
+ <event name="done">
|
||||
+ <description summary="end of initial window list">
|
||||
+ Sent after the initial batch of window events when a client first
|
||||
+ binds the interface. This signals that all existing vmsilo windows
|
||||
+ have been reported. Subsequent window and window_removed events
|
||||
+ are sent individually as changes occur.
|
||||
+ </description>
|
||||
+ </event>
|
||||
+ </interface>
|
||||
+</protocol>
|
||||
diff --git a/libtaskmanager/taskgroupingproxymodel.cpp b/libtaskmanager/taskgroupingproxymodel.cpp
|
||||
index 46264d818e..3568ed4165 100644
|
||||
--- a/libtaskmanager/taskgroupingproxymodel.cpp
|
||||
+++ b/libtaskmanager/taskgroupingproxymodel.cpp
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "abstracttasksmodel.h"
|
||||
#include "tasktools.h"
|
||||
|
||||
+#include <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QSet>
|
||||
|
||||
@@ -699,6 +700,30 @@ QVariant TaskGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) c
|
||||
return d->all(proxyIndex, AbstractTasksModel::CanSetNoBorder);
|
||||
} else if (role == AbstractTasksModel::HasNoBorder) {
|
||||
return d->all(proxyIndex, AbstractTasksModel::HasNoBorder);
|
||||
+ } else if (role == AbstractTasksModel::VmSiloColor) {
|
||||
+ // Return the color only if all children share the same color
|
||||
+ const QColor first = index(0, 0, proxyIndex).data(AbstractTasksModel::VmSiloColor).value<QColor>();
|
||||
+ if (!first.isValid()) {
|
||||
+ return QColor();
|
||||
+ }
|
||||
+ for (int i = 1; i < rowCount(proxyIndex); ++i) {
|
||||
+ if (index(i, 0, proxyIndex).data(AbstractTasksModel::VmSiloColor).value<QColor>() != first) {
|
||||
+ return QColor();
|
||||
+ }
|
||||
+ }
|
||||
+ return first;
|
||||
+ } else if (role == AbstractTasksModel::VmSiloName) {
|
||||
+ // Return the name only if all children share the same name
|
||||
+ const QString first = index(0, 0, proxyIndex).data(AbstractTasksModel::VmSiloName).toString();
|
||||
+ if (first.isEmpty()) {
|
||||
+ return QString();
|
||||
+ }
|
||||
+ for (int i = 1; i < rowCount(proxyIndex); ++i) {
|
||||
+ if (index(i, 0, proxyIndex).data(AbstractTasksModel::VmSiloName).toString() != first) {
|
||||
+ return QString();
|
||||
+ }
|
||||
+ }
|
||||
+ return first;
|
||||
} else if (role == AbstractTasksModel::IsVirtualDesktopsChangeable) {
|
||||
return d->all(proxyIndex, AbstractTasksModel::IsVirtualDesktopsChangeable);
|
||||
} else if (role == AbstractTasksModel::VirtualDesktops) {
|
||||
diff --git a/libtaskmanager/waylandtasksmodel.cpp b/libtaskmanager/waylandtasksmodel.cpp
|
||||
index 9b944021e2..79f0dfbdbd 100644
|
||||
--- a/libtaskmanager/waylandtasksmodel.cpp
|
||||
+++ b/libtaskmanager/waylandtasksmodel.cpp
|
||||
@@ -14,7 +14,9 @@
|
||||
#include <KWindowSystem>
|
||||
|
||||
#include <qwayland-plasma-window-management.h>
|
||||
+#include <qwayland-is-dsg-vmsilo-v1.h>
|
||||
|
||||
+#include <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
@@ -431,6 +433,49 @@ void PlasmaWindowManagement::org_kde_plasma_window_management_stacking_order_cha
|
||||
m_pendingStackingOrder.reset(new PlasmaStackingOrder(this, org_kde_plasma_window_management_get_stacking_order(object())));
|
||||
}
|
||||
|
||||
+class VmsiloManager : public QWaylandClientExtensionTemplate<VmsiloManager>, public QtWayland::is_dsg_vmsilo_manager_v1
|
||||
+{
|
||||
+ Q_OBJECT
|
||||
+public:
|
||||
+ static constexpr int s_version = 1;
|
||||
+ VmsiloManager()
|
||||
+ : QWaylandClientExtensionTemplate(s_version)
|
||||
+ {
|
||||
+ initialize();
|
||||
+ }
|
||||
+ ~VmsiloManager()
|
||||
+ {
|
||||
+ if (isActive()) {
|
||||
+ destroy();
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ struct Info {
|
||||
+ QString vmName;
|
||||
+ QColor color;
|
||||
+ };
|
||||
+ QHash<QString, Info> windows; // keyed by PlasmaWindowManagement UUID
|
||||
+
|
||||
+protected:
|
||||
+ void is_dsg_vmsilo_manager_v1_window(const QString &uuid, const QString &vm_name, uint32_t color) override
|
||||
+ {
|
||||
+ windows[uuid] = {vm_name, QColor::fromRgba(color)};
|
||||
+ Q_EMIT windowAdded(uuid);
|
||||
+ }
|
||||
+ void is_dsg_vmsilo_manager_v1_window_removed(const QString &uuid) override
|
||||
+ {
|
||||
+ windows.remove(uuid);
|
||||
+ Q_EMIT windowRemoved(uuid);
|
||||
+ }
|
||||
+ void is_dsg_vmsilo_manager_v1_done() override
|
||||
+ {
|
||||
+ }
|
||||
+
|
||||
+Q_SIGNALS:
|
||||
+ void windowAdded(const QString &uuid);
|
||||
+ void windowRemoved(const QString &uuid);
|
||||
+};
|
||||
+
|
||||
class Q_DECL_HIDDEN WaylandTasksModel::Private
|
||||
{
|
||||
public:
|
||||
@@ -444,6 +489,7 @@ public:
|
||||
// key=leader, values=transient children
|
||||
QMultiHash<PlasmaWindow *, PlasmaWindow *> transientsDemandingAttention;
|
||||
std::unique_ptr<PlasmaWindowManagement> windowManagement;
|
||||
+ std::unique_ptr<VmsiloManager> vmsiloManager;
|
||||
KSharedConfig::Ptr rulesConfig;
|
||||
KDirWatch *configWatcher = nullptr;
|
||||
VirtualDesktopInfo *virtualDesktopInfo = nullptr;
|
||||
@@ -464,6 +510,7 @@ public:
|
||||
|
||||
void dataChanged(PlasmaWindow *window, int role);
|
||||
void dataChanged(PlasmaWindow *window, const QList<int> &roles);
|
||||
+ void vmsiloDataChanged(const QString &uuid);
|
||||
|
||||
private:
|
||||
WaylandTasksModel *const q;
|
||||
@@ -545,6 +592,22 @@ void WaylandTasksModel::Private::initWayland()
|
||||
this->dataChanged(window.get(), StackingOrder);
|
||||
}
|
||||
});
|
||||
+
|
||||
+ vmsiloManager = std::make_unique<VmsiloManager>();
|
||||
+
|
||||
+ QObject::connect(vmsiloManager.get(), &VmsiloManager::activeChanged, q, [this] {
|
||||
+ if (!vmsiloManager->isActive()) {
|
||||
+ vmsiloManager->windows.clear();
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ QObject::connect(vmsiloManager.get(), &VmsiloManager::windowAdded, q, [this](const QString &uuid) {
|
||||
+ vmsiloDataChanged(uuid);
|
||||
+ });
|
||||
+
|
||||
+ QObject::connect(vmsiloManager.get(), &VmsiloManager::windowRemoved, q, [this](const QString &uuid) {
|
||||
+ vmsiloDataChanged(uuid);
|
||||
+ });
|
||||
}
|
||||
|
||||
auto WaylandTasksModel::Private::findWindow(PlasmaWindow *window) const
|
||||
@@ -870,6 +933,17 @@ void WaylandTasksModel::Private::dataChanged(PlasmaWindow *window, const QList<i
|
||||
Q_EMIT q->dataChanged(idx, idx, roles);
|
||||
}
|
||||
|
||||
+void WaylandTasksModel::Private::vmsiloDataChanged(const QString &uuid)
|
||||
+{
|
||||
+ for (size_t i = 0; i < windows.size(); ++i) {
|
||||
+ if (windows[i]->uuid == uuid) {
|
||||
+ QModelIndex idx = q->index(i);
|
||||
+ Q_EMIT q->dataChanged(idx, idx, QList<int>{AbstractTasksModel::VmSiloColor, AbstractTasksModel::VmSiloName});
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
WaylandTasksModel::WaylandTasksModel(QObject *parent)
|
||||
: AbstractWindowTasksModel(parent)
|
||||
, d(new Private(this))
|
||||
@@ -983,6 +1057,22 @@ QVariant WaylandTasksModel::data(const QModelIndex &index, int role) const
|
||||
return window->applicationMenuService;
|
||||
} else if (role == CanLaunchNewInstance) {
|
||||
return canLauchNewInstance(d->appData(window));
|
||||
+ } else if (role == VmSiloColor) {
|
||||
+ if (d->vmsiloManager) {
|
||||
+ auto it = d->vmsiloManager->windows.constFind(window->uuid);
|
||||
+ if (it != d->vmsiloManager->windows.constEnd()) {
|
||||
+ return it->color;
|
||||
+ }
|
||||
+ }
|
||||
+ return QColor();
|
||||
+ } else if (role == VmSiloName) {
|
||||
+ if (d->vmsiloManager) {
|
||||
+ auto it = d->vmsiloManager->windows.constFind(window->uuid);
|
||||
+ if (it != d->vmsiloManager->windows.constEnd()) {
|
||||
+ return it->vmName;
|
||||
+ }
|
||||
+ }
|
||||
+ return QString();
|
||||
}
|
||||
|
||||
return AbstractTasksModel::data(index, role);
|
||||
diff --git a/shell/org.kde.plasmashell.desktop.cmake b/shell/org.kde.plasmashell.desktop.cmake
|
||||
index 3fee14565f..f0b51077e0 100644
|
||||
--- a/shell/org.kde.plasmashell.desktop.cmake
|
||||
+++ b/shell/org.kde.plasmashell.desktop.cmake
|
||||
@@ -72,5 +72,5 @@ Icon=plasmashell
|
||||
NoDisplay=true
|
||||
X-systemd-skip=true
|
||||
|
||||
-X-KDE-Wayland-Interfaces=org_kde_plasma_window_management,org_kde_kwin_keystate,zkde_screencast_unstable_v1,org_kde_plasma_activation_feedback,kde_lockscreen_overlay_v1
|
||||
+X-KDE-Wayland-Interfaces=org_kde_plasma_window_management,org_kde_kwin_keystate,zkde_screencast_unstable_v1,org_kde_plasma_activation_feedback,kde_lockscreen_overlay_v1,is_dsg_vmsilo_manager_v1
|
||||
X-KDE-DBUS-Restricted-Interfaces=org.kde.KWin.ScreenShot2
|
||||
--
|
||||
2.52.0
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue