Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/services/TaskbarApps.qml
Fimeg 8f42fc953d publish: the public projection begins here
This is a projection, not a development branch. The tree above was constructed
from the internal source named below under a manifest that decides which paths
may leave, then scanned as a whole tree rather than as a series of patches, and
only then published.

Public history starts here because the history before it was not admissible,
and neither was the tree. What used to stand in this repository included a
rescue copy of another machine, a directory of phone handoffs, deployment
wired to one house, and a submodule pointing at a forge no stranger can reach.
None of that was ever the product. It stays in the private forge, which is
allowed to hold the whole working organism, and this is what was deliberately
sent out instead.

Three mechanisms produced this tree, in decreasing order of trust. A top-level
path the manifest does not name never arrives at all, which is the one that
catches directories nobody has thought of yet. Named internal files inside
admitted roots are dropped. A short, reviewed table replaces deployment
defaults that a public build must not carry -- an endpoint aimed at one LAN, a
VPN profile belonging to one phone, packaging built from one checkout path.

Everything after this commit is an ordinary publication with the same three
trailers, so a force push stops being routine and starts meaning that
something deliberate happened. The trailers bind the projection to its source
without pretending the public SHA is the private one: same lineage, different
tree, and the record says so.

Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047
Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27
Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
2026-09-04 15:55:48 -04:00

263 lines
10 KiB
QML

pragma Singleton
import qs.modules.common
import QtQuick
import Quickshell
import Quickshell.Wayland
Singleton {
id: root
function isPinned(appId) {
return Config.options.dock.pinnedApps.indexOf(appId) !== -1;
}
function togglePin(appId) {
if (root.isPinned(appId)) {
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.filter(id => id !== appId)
} else {
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.concat([appId])
}
}
// --- Fan-out stacks ------------------------------------------------
// Backing store: Config.options.dock.stacks, a list<string> — string
// list because nested-object arrays don't survive the JsonAdapter.
// Each entry is a JSON object string:
// {"id":"stack-1","name":"Stack 1","members":["appId","appId"]}
// JSON-per-entry is escaping-safe (the first iteration's "name|app,app"
// format corrupted silently on a | or , in a name). id is the stable
// lookup key; name is the display label (rename never breaks lookups).
// Legacy pipe entries still parse (id doubles as name) and get
// rewritten as JSON on the next stacks write.
function parseStack(entry) {
if (entry.startsWith("{")) {
try {
const o = JSON.parse(entry);
return { id: o.id ?? "", name: o.name ?? o.id ?? "", members: o.members ?? [] };
} catch (e) {
return { id: entry, name: entry, members: [] };
}
}
// Legacy "stackId|appId,appId" format.
const bar = entry.indexOf("|");
if (bar === -1) return { id: entry, name: entry, members: [] };
const id = entry.slice(0, bar);
const rest = entry.slice(bar + 1).trim();
const members = rest.length ? rest.split(",").map(s => s.trim()).filter(s => s.length) : [];
return { id: id, name: id, members: members };
}
function stacksList() {
return (Config.options?.dock.stacks ?? []).map(root.parseStack);
}
// appId -> the stackId that contains it, or "" if none.
function stackContaining(appId) {
const low = appId.toLowerCase();
for (const s of root.stacksList()) {
if (s.members.some(m => m.toLowerCase() === low)) return s.id;
}
return "";
}
function encodeStack(s) {
return JSON.stringify({ id: s.id, name: s.name, members: s.members });
}
// Rewrite the whole stacks list from a parsed [{id, name, members}]
// array, dropping any that end up empty.
function writeStacks(parsed) {
Config.options.dock.stacks = parsed
.filter(s => s.members.length > 0)
.map(root.encodeStack);
}
function addToStack(stackId, appId) {
const parsed = root.stacksList();
const existing = parsed.find(s => s.id === stackId);
if (existing) {
if (!existing.members.some(m => m.toLowerCase() === appId.toLowerCase()))
existing.members = existing.members.concat([appId]);
} else {
parsed.push({ id: stackId, members: [appId] });
}
root.writeStacks(parsed);
}
function removeFromStack(stackId, appId) {
const parsed = root.stacksList();
const existing = parsed.find(s => s.id === stackId);
if (!existing) return;
existing.members = existing.members.filter(m => m.toLowerCase() !== appId.toLowerCase());
root.writeStacks(parsed);
}
// --- Drag interactions (Tier 1) ------------------------------------
// Mint a stable id + display name for a fresh stack. Ids scan for the
// max existing numeric suffix, never reuse after a delete (the old
// count-based scheme collided: delete "Stack 1" of two and the next
// combine minted a second "Stack 2" — and id is the lookup key, so two
// stacks silently shared members).
function mintStack() {
let n = 0;
for (const s of root.stacksList()) {
const mi = /^stack-(\d+)$/.exec(s.id);
if (mi) n = Math.max(n, parseInt(mi[1]));
const mn = /^Stack (\d+)$/.exec(s.name);
if (mn) n = Math.max(n, parseInt(mn[1]));
}
return { id: "stack-" + (n + 1), name: "Stack " + (n + 1) };
}
// Rename a stack's display label. The id (lookup key) never changes.
function renameStack(stackId, newName) {
if (!newName) return;
const parsed = root.stacksList();
const s = parsed.find(x => x.id === stackId);
if (!s) return;
s.name = newName;
root.writeStacks(parsed);
}
// Drag `draggedAppId` onto `targetAppId` -> combine. If target is
// already a stack (stackId non-empty), add into it; else make a new
// stack containing target then dragged (target stays on top = first).
function combineIntoStack(targetAppId, draggedAppId, targetStackId) {
if (!draggedAppId || draggedAppId.toLowerCase() === targetAppId.toLowerCase()) return;
// If the dragged app is currently in some stack, pull it out first.
const from = root.stackContaining(draggedAppId);
if (from) root.removeFromStack(from, draggedAppId);
if (targetStackId) {
root.addToStack(targetStackId, draggedAppId);
} else {
const fresh = root.mintStack();
const parsed = root.stacksList();
parsed.push({ id: fresh.id, name: fresh.name, members: [targetAppId, draggedAppId] });
root.writeStacks(parsed);
}
// An app lives in one place: its stack. The standalone pin actually
// leaves config here (it used to be only render-suppressed, leaving
// a phantom pin string behind); unstackMember re-pins on the way out.
const dropPins = [draggedAppId.toLowerCase()];
if (!targetStackId) dropPins.push(targetAppId.toLowerCase());
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.filter(
id => !dropPins.includes(id.toLowerCase()));
}
// Replace a stack's member order wholesale (arc-reorder commit).
function setStackOrder(stackId, members) {
const parsed = root.stacksList();
const s = parsed.find(x => x.id === stackId);
if (!s) return;
s.members = members;
root.writeStacks(parsed);
}
// Reorder a pinned app within the pinnedApps array. targetPinnedIndex
// is the desired final index (0-based) in Config.options.dock.pinnedApps.
// The app is removed from its current position and spliced in at the
// target; other pins shift to fill / make room.
function reorderPinned(appId, targetPinnedIndex) {
const pinned = Config.options.dock.pinnedApps.slice();
const srcIdx = pinned.findIndex(id => id.toLowerCase() === appId.toLowerCase());
if (srcIdx < 0 || srcIdx === targetPinnedIndex) return;
pinned.splice(srcIdx, 1);
pinned.splice(targetPinnedIndex, 0, appId);
Config.options.dock.pinnedApps = pinned;
}
// Pull a member out of its stack back to a standalone pinned app.
function unstackMember(stackId, appId) {
root.removeFromStack(stackId, appId);
if (!root.isPinned(appId)) root.togglePin(appId);
}
property list<var> apps: {
var map = new Map();
// Fan-out stacks come first, in their configured order. Their
// member appIds are suppressed as standalone pinned entries below
// so an app lives in one place: its stack.
const stacks = root.stacksList();
const stackedMembers = new Set();
const memberToStackKey = new Map();
for (const s of stacks) {
for (const m of s.members) {
stackedMembers.add(m.toLowerCase());
memberToStackKey.set(m.toLowerCase(), "STACK:" + s.id);
}
map.set("STACK:" + s.id, {
pinned: true, toplevels: [], isStack: true, members: s.members,
name: s.name
});
}
// Pinned apps (skip any already living in a stack)
const pinnedApps = Config.options?.dock.pinnedApps ?? [];
for (const appId of pinnedApps) {
if (stackedMembers.has(appId.toLowerCase())) continue;
if (!map.has(appId.toLowerCase())) map.set(appId.toLowerCase(), ({
pinned: true,
toplevels: []
}));
}
// Separator
if (map.size > 0) {
map.set("SEPARATOR", { pinned: false, toplevels: [] });
}
// Ignored apps
const ignoredRegexStrings = Config.options?.dock.ignoredAppRegexes ?? [];
const ignoredRegexes = ignoredRegexStrings.map(pattern => new RegExp(pattern, "i"));
// Open windows
for (const toplevel of ToplevelManager.toplevels.values) {
if (ignoredRegexes.some(re => re.test(toplevel.appId))) continue;
// A running app that belongs to a stack contributes its windows
// to the STACK entry (so tapping the member focuses the open
// window) instead of appearing as a separate icon.
const stackKey = memberToStackKey.get(toplevel.appId.toLowerCase());
if (stackKey) {
map.get(stackKey).toplevels.push(toplevel);
continue;
}
if (!map.has(toplevel.appId.toLowerCase())) map.set(toplevel.appId.toLowerCase(), ({
pinned: false,
toplevels: []
}));
map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel);
}
var values = [];
for (const [key, value] of map) {
values.push(appEntryComp.createObject(null, {
appId: value.isStack ? key.slice(6) : key,
toplevels: value.toplevels,
pinned: value.pinned,
isStack: value.isStack ?? false,
members: value.members ?? [],
stackName: value.name ?? ""
}));
}
return values;
}
component TaskbarAppEntry: QtObject {
id: wrapper
required property string appId
required property list<var> toplevels
required property bool pinned
property bool isStack: false
property list<var> members: []
property string stackName: ""
}
Component {
id: appEntryComp
TaskbarAppEntry {}
}
}