Profiles: named layouts that open apps

A profile is a named layout plus the screens it hides and its apps' windows
(docs/profiles.md). ft-layout save captures them (ft-floatd's "windows",
after the KWin script reports every window as it is now); use opens them:
the screens move, open windows of each app go to their places (on a screen,
maximized or not, or floating), and missing apps start, once and then again
for each window still missing 3 s after the first. Nothing closes.

The desktop starts in FT_PROFILE or default_profile (ft-layout start, from
the session script). Each profile gets a launcher entry (Frametop: NAME, in
SteamVR's Launch a program list) that switches to it or starts the desktop
in it. The relay's profile:NAME action and Input Settings' "Open profile"
entries put one on a key, mouse button, or controller button. Display
Settings' Layout page becomes Layout & profiles: Save as profile, Open
profile, the profile's apps, and Start in profile. Plasma's own session
restore is off in the Frametop session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-30 22:32:10 -06:00
1 parent 9d91ecbcaa
commit 858dbe1562
11 files changed
+619 -72

No files matched your search

+48 -3
View File
@@ -584,16 +584,55 @@ class Backend(QObject):
def renameLayout(self, old, new):
try:
self._edit_layout(lambda l: ft_layout.rename_named(l, old, new))
except RuntimeError as e:
ft_layout.write_launchers(ft_layout.load_layout())
except (RuntimeError, OSError) as e:
self.message.emit(str(e), True)
@Slot(str)
def deleteLayout(self, name):
try:
self._edit_layout(lambda l: ft_layout.delete_named(l, name))
except RuntimeError as e:
ft_layout.write_launchers(ft_layout.load_layout())
except (RuntimeError, OSError) as e:
self.message.emit(str(e), True)
# --- profiles (docs/profiles.md): a named layout's apps and hidden screens ---
@Slot(str, result="QVariantList")
def profileWindows(self, name):
"""A profile's windows, as "app" and "where" for the list."""
out = []
for e in ft_layout.load_layout().get("profiles", {}).get(name, {}).get("windows", []):
app = e.get("app") or os.path.basename((e.get("cmd") or ["?"])[0])
app = app.rsplit(".", 1)[-1] if "." in app and not e.get("cmd") else app
where = "floating" if "float" in e else f"screen {e.get('screen', 1)}" + (", maximized" if e.get("maximized") else "")
out.append({"app": app, "where": where})
return out
@Slot(str, result="QVariantList")
def profileHidden(self, name):
return ft_layout.load_layout().get("profiles", {}).get(name, {}).get("hidden", [])
@Slot(str, int)
def removeProfileWindow(self, name, index):
def edit(layout):
windows = layout.get("profiles", {}).get(name, {}).get("windows", [])
if 0 <= index < len(windows):
windows.pop(index)
self._edit_layout(edit)
@Property(str, notify=changed)
def defaultProfile(self):
return ft_layout.load_layout().get("default_profile", "")
@Slot(str)
def setDefaultProfile(self, name):
def edit(layout):
if name:
layout["default_profile"] = name
else:
layout.pop("default_profile", None)
self._edit_layout(edit)
@Slot(str, "QVariant")
def setPreset(self, key, value):
def edit(layout):
@@ -607,7 +646,13 @@ class Backend(QObject):
@Slot()
def arrange(self):
self._run("Arranging the screens", "apply")
"""Arrange the screens; in a profile, also open its apps (ft-layout use)."""
layout = ft_layout.load_layout()
name = layout.get("active")
if layout.get("mode") == "custom" and name in layout.get("layouts", {}):
self._run(f"Opening {name}", "use", name)
else:
self._run("Arranging the screens", "apply")
@Slot()
def capture(self):
+66 -9
View File
@@ -424,7 +424,7 @@ Kirigami.ApplicationWindow {
id: layoutPage
Kirigami.ScrollablePage {
id: lpage
title: "Layout"
title: "Layout & profiles"
property var layout: backend.layout
property var preset: layout.preset || {}
property bool hasCustom: (layout.screens || []).some(s => s.pos !== undefined)
@@ -440,16 +440,17 @@ Kirigami.ApplicationWindow {
actions: [
Kirigami.Action {
text: "Arrange now"
text: lpage.named ? "Open profile" : "Arrange now"
icon.name: "view-restore"
tooltip: "Float the screens out of the dashboard and put them in this layout, around where you're facing"
tooltip: lpage.named ? "Put the screens in this profile's places, around where you're facing, and open its apps (windows already open move; nothing closes)"
: "Float the screens out of the dashboard and put them in this layout, around where you're facing"
enabled: backend.desktopRunning && backend.busy === ""
onTriggered: backend.arrange()
},
Kirigami.Action {
text: "Save current arrangement…"
text: "Save as profile…"
icon.name: "document-save"
tooltip: "Save where the screens are now (placed by hand) as a named layout, and use it"
tooltip: "Save where the screens are now, which ones are hidden, and the open apps and where their windows are, under a name"
enabled: backend.desktopRunning && backend.busy === ""
onTriggered: nameDialog.openFor("save", lpage.named ? lpage.layout.active
: "Layout " + (lpage.names.length + 1))
@@ -507,16 +508,61 @@ Kirigami.ApplicationWindow {
Controls.Label {
visible: lpage.layout.mode === "custom"
Kirigami.FormData.label: ""
text: lpage.named ? "Where the screens were when you saved it. Arrange now puts them there. "
+ "Save current arrangement updates it or saves a new one."
: lpage.hasCustom ? "Where the screens were when you saved. Save current arrangement "
text: lpage.named ? "Where the screens were when you saved it, and the apps that were open. Open "
+ "profile puts the screens there and opens the apps. Save as profile updates "
+ "it or saves a new one."
: lpage.hasCustom ? "Where the screens were when you saved. Save as profile "
+ "names it. Pick a preset to edit."
: "Nothing saved yet: place the screens by hand, then Save current arrangement."
: "Nothing saved yet: place the screens by hand, open your apps, then Save as profile."
opacity: 0.7
wrapMode: Text.Wrap
Layout.maximumWidth: Kirigami.Units.gridUnit * 20
}
// The profile's apps (docs/profiles.md): each window and where it goes.
ColumnLayout {
id: profileApps
visible: lpage.named
Kirigami.FormData.label: "Apps:"
property var windows: lpage.named ? backend.profileWindows(lpage.layout.active) : []
property var hidden: lpage.named ? backend.profileHidden(lpage.layout.active) : []
Connections {
target: backend
function onChanged() {
profileApps.windows = lpage.named ? backend.profileWindows(lpage.layout.active) : []
profileApps.hidden = lpage.named ? backend.profileHidden(lpage.layout.active) : []
}
}
Controls.Label {
visible: profileApps.windows.length === 0
text: "None saved. Open the apps you want, place their windows, then Save as profile."
opacity: 0.7
wrapMode: Text.Wrap
Layout.maximumWidth: Kirigami.Units.gridUnit * 20
}
Repeater {
model: profileApps.windows
delegate: RowLayout {
required property var modelData
required property int index
Controls.Label { text: modelData.app + " (" + modelData.where + ")" }
Controls.ToolButton {
icon.name: "list-remove"
text: "Leave out"
display: Controls.AbstractButton.IconOnly
Controls.ToolTip.text: "Leave this window out of the profile"
Controls.ToolTip.visible: hovered
onClicked: backend.removeProfileWindow(lpage.layout.active, index)
}
}
}
Controls.Label {
visible: profileApps.hidden.length > 0
text: "Hides screen" + (profileApps.hidden.length > 1 ? "s " : " ") + profileApps.hidden.join(", ")
opacity: 0.7
}
}
Controls.SpinBox {
Kirigami.FormData.label: "Rows:"
visible: lpage.layout.mode !== "custom"
@@ -554,8 +600,19 @@ Kirigami.ApplicationWindow {
Kirigami.FormData.label: "When the desktop starts:"
text: "Float the screens and arrange them"
checked: lpage.layout.auto !== false
enabled: backend.defaultProfile === ""
onToggled: backend.setAuto(checked)
}
Controls.ComboBox {
Kirigami.FormData.label: "Start in profile:"
model: [{ text: "None", value: "" }].concat(lpage.names.map(n => ({ text: n, value: n })))
textRole: "text"
valueRole: "value"
currentIndex: Math.max(0, indexOfValue(backend.defaultProfile))
onActivated: backend.setDefaultProfile(currentValue)
Controls.ToolTip.text: "The desktop starts in this profile: its screens, and its apps open. Each profile also has its own entry in SteamVR's Launch a program list"
Controls.ToolTip.visible: hovered
}
}
// Preview: from above (you at the bottom) and from the front.