1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
import Quickshell
import Quickshell.Wayland
import QtQuick
// One tab per consumer with manual steps pending. Centred on the vertical
// screen and kept open on purpose: the commands are pasted into a root shell
// elsewhere, so the window takes keyboard focus only when clicked and stays
// until closed. Done deletes a tab's file; the producer also clears it once
// the consumer is up to date.
Scope {
id: root
property string monitor: "DP-3"
property bool open: false
property string tab: ""
readonly property var screenObj:
Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0]
readonly property var current: Steps.tabs.find(t => t.name === root.tab) ?? Steps.tabs[0] ?? null
// A poke from udt-steps: reload, and open when something is pending.
// Also what a keybind calls to bring the window back after closing it.
function poke() {
root.opening = true;
Steps.refresh();
}
property bool opening: false
Connections {
target: Steps
function onLoaded() {
if (Steps.tabs.length === 0) root.open = false;
else if (root.opening) root.open = true;
root.opening = false;
}
}
LazyLoader {
active: root.open
PanelWindow {
id: win
screen: root.screenObj
// No anchors: a layer surface anchored nowhere is centred.
implicitWidth: 960
implicitHeight: body.implicitHeight + 40
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "quickshell-steps"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Rectangle {
anchors.fill: parent
radius: 14
color: Qt.alpha(Theme.base, 0.92)
border.width: 1
border.color: Qt.alpha(Theme.text, 0.12)
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: root.open = false
}
Column {
id: body
anchors { top: parent.top; left: parent.left; right: parent.right; margins: 20 }
spacing: 16
Item {
width: parent.width
height: tabBar.implicitHeight
Row {
id: tabBar
spacing: 8
Repeater {
model: Steps.tabs
Tab {
required property var modelData
text: modelData.name
selected: root.current !== null && root.current.name === modelData.name
onClicked: root.tab = modelData.name
}
}
}
Tab {
anchors.right: parent.right
text: "Close"
onClicked: root.open = false
}
}
Flickable {
width: parent.width
// Sized from the screen, never from the window: the
// window's height comes from this, and a binding back
// to it collapsed the whole thing to its margins.
height: Math.min(content.implicitHeight,
win.screen.height - 240 - tabBar.implicitHeight - actions.implicitHeight - 32)
contentHeight: content.implicitHeight
clip: true
Column {
id: content
width: parent.width
spacing: 12
Repeater {
model: root.current ? root.current.blocks : []
Loader {
required property var modelData
width: content.width
sourceComponent: modelData.cmd ? cmdBlock : prose
property var block: modelData
}
}
}
}
Row {
id: actions
spacing: 8
anchors.right: parent.right
Tab {
text: "Copy all commands"
enabled: root.current !== null && Steps.commands(root.current) !== ""
onClicked: Steps.copy(Steps.commands(root.current))
}
Tab {
text: "Done"
enabled: root.current !== null
onClicked: Steps.dismiss(root.current.name)
}
}
}
}
Component {
id: prose
Text {
text: block.text
wrapMode: Text.Wrap
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 }
color: Theme.text
}
}
Component {
id: cmdBlock
Rectangle {
implicitHeight: Math.max(code.implicitHeight, copyBtn.implicitHeight) + 24
radius: 8
color: Qt.alpha(Theme.crust, 0.8)
border.width: 1
border.color: Qt.alpha(Theme.accent, 0.3)
// Selectable as well as copyable: sometimes one line of a
// block is what is wanted.
TextEdit {
id: code
anchors { left: parent.left; right: copyBtn.left; verticalCenter: parent.verticalCenter; margins: 12 }
text: block.text
readOnly: true
selectByMouse: true
wrapMode: TextEdit.WrapAnywhere
font { family: "Inconsolata Nerd Font Mono"; pixelSize: Theme.fontSize - 1 }
color: Theme.text
selectionColor: Qt.alpha(Theme.accent, 0.4)
selectedTextColor: Theme.text
}
Tab {
id: copyBtn
anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter }
text: "Copy"
onClicked: Steps.copy(block.text)
}
}
}
}
}
}
|