22 Commits

Author SHA1 Message Date
zach ad23da4eda initial commit, primitive loading of QML code outside of shell directory 2026-05-25 13:32:06 +02:00
zach 06ebc4ffbf remove irrelevant settings options plus bugfixes 2026-05-25 11:51:16 +02:00
zach f2f9fa1302 fix opening links on non-uwsm and disable file watcher when shell is installed 2026-05-25 11:01:01 +02:00
zach b4020438f9 Merge pull request 'Tune transparency scale impact on luminance scaling' (#98) from fix/transparency-luminance-scaling into main
Reviewed-on: #98
Reviewed-by: AramJonghu <2+aramjonghu@noreply.git.zach-dev.cc>
2026-05-24 22:37:03 +02:00
zach 184ab20d11 Merge branch 'main' into fix/transparency-luminance-scaling
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 26s
Python / test (pull_request) Successful in 42s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m45s
2026-05-24 22:36:52 +02:00
zach 5097e30a77 fix anchors used in Layouts 2026-05-24 19:33:06 +02:00
zach ef71ae8afd button to install colorscheme and wallpaper to greeter in settings 2026-05-24 19:28:02 +02:00
AramJonghu 6533533936 Merge branch 'main' into fix/transparency-luminance-scaling
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 23s
Python / test (pull_request) Successful in 51s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m47s
2026-05-24 18:53:39 +02:00
zach 9688072e93 record.py 2026-05-24 18:48:03 +02:00
AramJonghu 9c36f0de5b pycache in cache removal 2026-05-24 18:28:32 +02:00
zach 9ca46967d9 Merge pull request 'hotfix(cli): replace raw subprocess tracebacks with styled error messages and fix restart race condition' (#96) from hotfix-restart-race-condition into main
Reviewed-on: #96
Reviewed-by: zach <zach@brohn.se>
2026-05-24 18:23:43 +02:00
zach 16e84ca998 fixed region selection for recording, plus cache file cleanup
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 18s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m34s
2026-05-24 18:21:37 +02:00
AramJonghu c30128cf95 check every 50ms -> 250ms for restart
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 30s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m44s
2026-05-24 18:03:32 +02:00
zach ba9926af18 Increased floor and decreased ceiling of offset for brightening darker colors in dark mode/darkening brighter colors in light mode
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Python / lint-format (pull_request) Successful in 28s
Python / test (pull_request) Successful in 43s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m45s
2026-05-24 11:40:44 +02:00
AramJonghu 78fcf33b3a refactor(cli): clean shell start/restart, drop redundant ipc check
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 24s
Python / test (pull_request) Successful in 48s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m44s
2026-05-24 03:09:19 +02:00
AramJonghu 5e9b373405 tests did not match changed code logic
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 25s
Python / test (pull_request) Successful in 50s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m46s
2026-05-23 20:54:35 +02:00
AramJonghu b49165e7ea minor typer adjustments to use typer in error/exception throws
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 14s
Python / lint-format (pull_request) Successful in 34s
Python / test (pull_request) Failing after 53s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m48s
2026-05-23 20:48:51 +02:00
AramJonghu d0cda51639 wait for instance to fully terminate before restart 2026-05-23 20:31:48 +02:00
zach fd620e7487 prep for replay 2026-05-22 20:42:51 +02:00
zach 0ec426e0f0 Record module added to sidebar, file list and buttons. Region recording is broken 2026-05-22 12:51:06 +02:00
zach ec5e6d3995 add typer command 2026-05-22 11:06:17 +02:00
zach 41a129bb90 init commit 2026-05-22 11:04:54 +02:00
42 changed files with 1915 additions and 808 deletions
+8 -1
View File
@@ -31,6 +31,13 @@ if("shell" IN_LIST ENABLE_MODULES)
foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths) foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths)
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
endforeach() endforeach()
install(FILES shell.qml DESTINATION "${INSTALL_QSCONFDIR}")
# Disable watching for changes
file(READ shell.qml SHELL_QML)
string(REPLACE "settings.watchFiles: true" "settings.watchFiles: false" SHELL_QML "${SHELL_QML}")
file(WRITE "${CMAKE_BINARY_DIR}/qml/shell.qml" "${SHELL_QML}")
install(FILES "${CMAKE_BINARY_DIR}/qml/shell.qml" DESTINATION "${INSTALL_QSCONFDIR}")
# Greeter
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}") install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
endif() endif()
+1
View File
@@ -15,6 +15,7 @@ Text {
color: DynamicColors.palette.m3onSurface color: DynamicColors.palette.m3onSurface
font.family: Appearance.font.family.sans font.family: Appearance.font.family.sans
font.pointSize: Appearance.font.size.normal font.pointSize: Appearance.font.size.normal
linkColor: DynamicColors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering renderType: Text.NativeRendering
textFormat: Text.PlainText textFormat: Text.PlainText
+12 -1
View File
@@ -22,6 +22,7 @@ Singleton {
property alias notifs: adapter.notifs property alias notifs: adapter.notifs
property alias osd: adapter.osd property alias osd: adapter.osd
property alias overview: adapter.overview property alias overview: adapter.overview
property alias plugins: adapter.plugins
property bool recentlySaved: false property bool recentlySaved: false
property alias screenshot: adapter.screenshot property alias screenshot: adapter.screenshot
property alias services: adapter.services property alias services: adapter.services
@@ -140,7 +141,8 @@ Singleton {
launcher: serializeLauncher(), launcher: serializeLauncher(),
colors: serializeColors(), colors: serializeColors(),
dock: serializeDock(), dock: serializeDock(),
screenshot: serializeScreenshot() screenshot: serializeScreenshot(),
plugins: serializePlugins()
}; };
} }
@@ -289,6 +291,13 @@ Singleton {
}; };
} }
function serializePlugins(): var {
return {
enabled: plugins.enabled,
entries: plugins.entries
};
}
function serializeScreenshot(): var { function serializeScreenshot(): var {
return { return {
enable_pp: screenshot.enable_pp, enable_pp: screenshot.enable_pp,
@@ -458,6 +467,8 @@ Singleton {
} }
property Overview overview: Overview { property Overview overview: Overview {
} }
property PluginConfig plugins: PluginConfig {
}
property Screenshot screenshot: Screenshot { property Screenshot screenshot: Screenshot {
} }
property Services services: Services { property Services services: Services {
+3 -2
View File
@@ -30,9 +30,10 @@ Singleton {
readonly property alias wallLuminance: analyser.luminance readonly property alias wallLuminance: analyser.luminance
function alterColor(c: color, a: real, layer: int): color { function alterColor(c: color, a: real, layer: int): color {
const luminance = getLuminance(c); const initLuminance = getLuminance(c);
const luminance = Math.max(initLuminance, 0.001);
const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5)); const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (0.2 + 0.3 * (1 - transparency.base)) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));
const scale = (luminance + offset) / luminance; const scale = (luminance + offset) / luminance;
const r = Math.max(0, Math.min(1, c.r * scale)); const r = Math.max(0, Math.min(1, c.r * scale));
const g = Math.max(0, Math.min(1, c.g * scale)); const g = Math.max(0, Math.min(1, c.g * scale));
+11
View File
@@ -0,0 +1,11 @@
import Quickshell.Io
JsonObject {
property bool enabled: false
property list<var> entries: [
{
id: "Plugin",
enabled: false
},
]
}
+18
View File
@@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import ZShell.Models
Singleton {
id: root
property alias plugins: plugins.entries
FileSystemModel {
id: plugins
nameFilters: ["*.qml"]
path: Quickshell.env("HOME") + "/.config/zshell"
recursive: false
}
}
+17
View File
@@ -0,0 +1,17 @@
import Quickshell
import QtQuick
import ZShell.Models
import qs.Config
Repeater {
model: FetchPlugins.plugins
LazyLoader {
required property FileSystemEntry modelData
activeAsync: Config.plugins.entries.some(p => {
return p.id === modelData.baseName && p.enabled;
})
source: modelData.path
}
}
+82 -41
View File
@@ -1,41 +1,82 @@
// pragma Singleton pragma Singleton
//
// import Quickshell import Quickshell
// import QtQuick import Quickshell.Io
// import QtQuick
// Singleton {
// id: root Singleton {
// id: root
// function start(extraArgs = []): void {
// needsStart = true; readonly property alias elapsed: props.elapsed
// startArgs = extraArgs; property bool needsPause
// checkProc.running = true; property bool needsStart
// } property bool needsStop
// readonly property alias paused: props.paused
// PersistentProperties { readonly property alias running: props.running
// id: props property list<string> startArgs
//
// property real elapsed: 0 function start(extraArgs = []): void {
// property bool paused: false needsStart = true;
// property bool running: false startArgs = extraArgs;
// checkProc.running = true;
// reloadableId: "recorder" }
// }
// function stop(): void {
// Process { needsStop = true;
// id: checkProc checkProc.running = true;
// }
// command: ["pidof", "gpu-screen-recorder"]
// running: true function togglePause(): void {
// needsPause = true;
// onExited: code => { checkProc.running = true;
// props.running = code === 0; }
//
// if (code === 0) { PersistentProperties {
// if (root.needsStop) { id: props
// Quickshell.execDetached(["zshell-cli"]);
// } property real elapsed: 0
// } property bool paused: false
// } property bool running: false
// }
// } reloadableId: "recorder"
}
Process {
id: checkProc
command: ["pidof", "gpu-screen-recorder"]
running: true
onExited: code => {
props.running = code === 0;
if (code === 0) {
if (root.needsStop) {
Quickshell.execDetached(["zshell-cli", "record", "record"]);
props.running = false;
props.paused = false;
} else if (root.needsPause) {
Quickshell.execDetached(["zshell-cli", "record", "record", "-p"]);
props.paused = !props.paused;
}
} else if (root.needsStart) {
Quickshell.execDetached(["zshell-cli", "record", "record", ...root.startArgs]);
props.running = true;
props.paused = false;
props.elapsed = 0;
}
root.needsStart = false;
root.needsStop = false;
root.needsPause = false;
}
}
Connections {
function onSecondsChanged(): void {
props.elapsed++;
}
target: Time // qmllint disable incompatible-type
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import Quickshell
import "../scripts/fzf.js" as Fzf import "../scripts/fzf.js" as Fzf
import "../scripts/fuzzysort.js" as Fuzzy import "../scripts/fuzzysort.js" as Fuzzy
import QtQuick import QtQuick
import Quickshell
Singleton { Singleton {
property var extraOpts: ({}) property var extraOpts: ({})
+3
View File
@@ -136,7 +136,10 @@ CustomRect {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
onLinkActivated: link => { onLinkActivated: link => {
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "-O", "--", link]); Quickshell.execDetached(["app2unit", "-O", "--", link]);
else
Quickshell.execDetached(["xdg-open", link]);
root.visibilities.sidebar = false; root.visibilities.sidebar = false;
} }
} }
@@ -0,0 +1,290 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
import qs.Helpers
CustomRect {
id: root
required property var props
required property PersistentProperties visibilities
Layout.fillWidth: true
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Appearance.rounding.smallest
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
RowLayout {
spacing: Appearance.spacing.normal
z: 1
CustomRect {
color: Recorder.running ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3secondaryContainer
implicitHeight: {
const h = icon.implicitHeight + Appearance.padding.smaller * 2;
return h - (h % 2);
}
implicitWidth: implicitHeight
radius: Appearance.rounding.full
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -0.5
anchors.verticalCenterOffset: 1.5
color: Recorder.running ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSecondaryContainer
font.pointSize: Appearance.font.size.large
text: "screen_record"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.normal
text: qsTr("Screen Recorder")
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off")
}
}
CustomSplitButton {
active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0]
disabled: Recorder.running
menuItems: [
MenuItem {
activeText: qsTr("Fullscreen")
icon: "fullscreen"
text: qsTr("Record fullscreen")
onClicked: Recorder.start()
},
MenuItem {
activeText: qsTr("Region")
icon: "screenshot_region"
text: qsTr("Record region")
onClicked: Recorder.start(["-r"])
},
MenuItem {
activeText: qsTr("Fullscreen")
icon: "select_to_speak"
text: qsTr("Record fullscreen with sound")
onClicked: Recorder.start(["-s"])
},
MenuItem {
activeText: qsTr("Region")
icon: "volume_up"
text: qsTr("Record region with sound")
onClicked: Recorder.start(["-s", "-r"])
}
]
menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text
}
}
Loader {
id: listOrControls
property bool running: Recorder.running
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
asynchronous: true
sourceComponent: running ? recordingControls : recordingList
Behavior on Layout.preferredHeight {
id: locHeightAnim
enabled: false
Anim {
}
}
Behavior on running {
SequentialAnimation {
ParallelAnimation {
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardAccel
property: "scale"
target: listOrControls
to: 0.7
}
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardAccel
property: "opacity"
target: listOrControls
to: 0
}
}
PropertyAction {
property: "enabled"
target: locHeightAnim
value: true
}
PropertyAction {
}
PropertyAction {
property: "enabled"
target: locHeightAnim
value: false
}
ParallelAnimation {
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardDecel
property: "scale"
target: listOrControls
to: 1
}
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardDecel
property: "opacity"
target: listOrControls
to: 1
}
}
}
}
}
}
Component {
id: recordingList
RecordingList {
props: root.props
visibilities: root.visibilities
}
}
Component {
id: recordingControls
RowLayout {
spacing: Appearance.spacing.normal
CustomRect {
color: Recorder.paused ? DynamicColors.palette.m3tertiary : DynamicColors.palette.m3error
implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2
implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2
radius: Appearance.rounding.full
Behavior on implicitWidth {
Anim {
}
}
SequentialAnimation on opacity {
alwaysRunToEnd: true
loops: Animation.Infinite
running: !Recorder.paused
Anim {
duration: Appearance.anim.durations.large
easing: Appearance.anim.curves.emphasizedAccel
from: 1
to: 0
}
Anim {
duration: Appearance.anim.durations.extraLarge
easing: Appearance.anim.curves.emphasizedDecel
from: 0
to: 1
}
}
CustomText {
id: recText
anchors.centerIn: parent
animate: true
color: Recorder.paused ? DynamicColors.palette.m3onTertiary : DynamicColors.palette.m3onError
font.family: Appearance.font.family.mono
text: Recorder.paused ? "PAUSED" : "REC"
}
}
CustomText {
font.pointSize: Appearance.font.size.normal
text: {
const elapsed = Recorder.elapsed;
const hours = Math.floor(elapsed / 3600);
const mins = Math.floor((elapsed % 3600) / 60);
const secs = Math.floor(elapsed % 60).toString().padStart(2, "0");
let time;
if (hours > 0)
time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
else
time = `${mins}:${secs}`;
return qsTr("Recording for %1").arg(time);
}
}
Item {
Layout.fillWidth: true
}
IconButton {
checked: Recorder.paused
font.pointSize: Appearance.font.size.large
icon: Recorder.paused ? "play_arrow" : "pause"
label.animate: true
toggle: true
type: IconButton.Tonal
onClicked: {
Recorder.togglePause();
internalChecked = Recorder.paused;
}
}
IconButton {
font.pointSize: Appearance.font.size.large
icon: "stop"
inactiveColour: DynamicColors.palette.m3error
inactiveOnColour: DynamicColors.palette.m3onError
onClicked: Recorder.stop()
}
}
}
}
@@ -0,0 +1,226 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ZShell.Models
import qs.Components
import qs.Helpers
import qs.Paths
import qs.Config
ColumnLayout {
id: root
required property var props
required property PersistentProperties visibilities
spacing: 0
WrapperMouseArea {
Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
RowLayout {
spacing: Appearance.spacing.smaller
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
font.pointSize: Appearance.font.size.large
text: "list"
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
font.pointSize: Appearance.font.size.normal
text: qsTr("Recordings")
}
IconButton {
icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more"
label.animate: true
type: IconButton.Text
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
}
}
}
CustomListView {
id: list
Layout.fillWidth: true
Layout.rightMargin: -Appearance.spacing.small
clip: true
implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
Anim {
from: 0.5
property: "scale"
to: 1
}
}
delegate: RowLayout {
id: recording
property string baseName
required property FileSystemEntry modelData
anchors.left: list.contentItem.left
anchors.right: list.contentItem.right
anchors.rightMargin: Appearance.spacing.small
spacing: Appearance.spacing.small / 2
Component.onCompleted: baseName = modelData.baseName
CustomText {
Layout.fillWidth: true
Layout.rightMargin: Appearance.spacing.small / 2
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
text: {
const time = recording.baseName;
const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/);
if (!matches)
return time;
const date = new Date(...matches.slice(1));
date.setMonth(date.getMonth() - 1);
return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
}
}
IconButton {
icon: "play_arrow"
type: IconButton.Text
onClicked: {
root.visibilities.sidebar = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
}
}
IconButton {
icon: "folder"
type: IconButton.Text
onClicked: {
root.visibilities.sidebar = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
}
}
}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim {
property: "y"
}
}
Behavior on implicitHeight {
Anim {
}
}
model: FileSystemModel {
nameFilters: ["recording_*.mp4"]
path: Paths.recsdir
sortReverse: true
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim {
property: "scale"
to: 0.5
}
}
Loader {
active: opacity > 0
anchors.centerIn: parent
asynchronous: true
opacity: list.count === 0 ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.extraLarge
opacity: root.props.recordingListExpanded ? 1 : 0
scale: root.props.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredHeight {
Anim {
}
}
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
}
RowLayout {
spacing: Appearance.spacing.smaller
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0
color: DynamicColors.palette.m3outline
opacity: !root.props.recordingListExpanded ? 1 : 0
scale: !root.props.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredWidth {
Anim {
}
}
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
}
CustomText {
color: DynamicColors.palette.m3outline
text: qsTr("No recordings found")
}
}
}
}
}
}
@@ -1,13 +1,14 @@
import qs.Modules.Notifications.Sidebar.Utils.Cards import Quickshell
import qs.Config
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Modules.Notifications.Sidebar.Utils.Cards
import qs.Config
Item { Item {
id: root id: root
required property Item popouts required property Item popouts
required property var props required property PersistentProperties props
required property var visibilities required property var visibilities
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
@@ -22,6 +23,12 @@ Item {
IdleInhibit { IdleInhibit {
} }
Record {
props: root.props
visibilities: root.visibilities
z: 1
}
Toggles { Toggles {
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
+5 -3
View File
@@ -100,13 +100,15 @@ Item {
icon: `brightness_${(Math.round(value * 6) + 1)}` icon: `brightness_${(Math.round(value * 6) + 1)}`
value: root.brightness value: root.brightness
onMoved: { onPressedChanged: {
if (!pressed) {
if (Config.osd.allMonBrightness) { if (Config.osd.allMonBrightness) {
root.monitor?.setBrightness(value);
} else {
for (const mon of Brightness.monitors) { for (const mon of Brightness.monitors) {
mon.setBrightness(value); mon.setBrightness(value);
} }
} else {
root.monitor?.setBrightness(value);
}
} }
} }
} }
+6
View File
@@ -116,6 +116,12 @@ Item {
key: "updates" key: "updates"
name: "Updates" name: "Updates"
} }
ListElement {
icon: "extension"
key: "plugins"
name: "Extensions"
}
} }
CustomClippingRect { CustomClippingRect {
+111 -111
View File
@@ -19,8 +19,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Media update interval"
min: 0 min: 0
name: "Media update interval"
object: Config.dashboard object: Config.dashboard
setting: "mediaUpdateInterval" setting: "mediaUpdateInterval"
step: 50 step: 50
@@ -30,8 +30,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Resource update interval"
min: 0 min: 0
name: "Resource update interval"
object: Config.dashboard object: Config.dashboard
setting: "resourceUpdateInterval" setting: "resourceUpdateInterval"
step: 50 step: 50
@@ -41,8 +41,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Drag threshold"
min: 0 min: 0
name: "Drag threshold"
object: Config.dashboard object: Config.dashboard
setting: "dragThreshold" setting: "dragThreshold"
} }
@@ -107,112 +107,112 @@ SettingsPage {
} }
} }
SettingsSection { // SettingsSection {
sectionId: "Layout Sizes" // sectionId: "Layout Sizes"
//
SettingsHeader { // SettingsHeader {
name: "Layout Sizes" // name: "Layout Sizes"
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Tab indicator height" // name: "Tab indicator height"
value: String(Config.dashboard.sizes.tabIndicatorHeight) // value: String(Config.dashboard.sizes.tabIndicatorHeight)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Tab indicator spacing" // name: "Tab indicator spacing"
value: String(Config.dashboard.sizes.tabIndicatorSpacing) // value: String(Config.dashboard.sizes.tabIndicatorSpacing)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Info width" // name: "Info width"
value: String(Config.dashboard.sizes.infoWidth) // value: String(Config.dashboard.sizes.infoWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Info icon size" // name: "Info icon size"
value: String(Config.dashboard.sizes.infoIconSize) // value: String(Config.dashboard.sizes.infoIconSize)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Date time width" // name: "Date time width"
value: String(Config.dashboard.sizes.dateTimeWidth) // value: String(Config.dashboard.sizes.dateTimeWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media width" // name: "Media width"
value: String(Config.dashboard.sizes.mediaWidth) // value: String(Config.dashboard.sizes.mediaWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media progress sweep" // name: "Media progress sweep"
value: String(Config.dashboard.sizes.mediaProgressSweep) // value: String(Config.dashboard.sizes.mediaProgressSweep)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media progress thickness" // name: "Media progress thickness"
value: String(Config.dashboard.sizes.mediaProgressThickness) // value: String(Config.dashboard.sizes.mediaProgressThickness)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Resource progress thickness" // name: "Resource progress thickness"
value: String(Config.dashboard.sizes.resourceProgessThickness) // value: String(Config.dashboard.sizes.resourceProgessThickness)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Weather width" // name: "Weather width"
value: String(Config.dashboard.sizes.weatherWidth) // value: String(Config.dashboard.sizes.weatherWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media cover art size" // name: "Media cover art size"
value: String(Config.dashboard.sizes.mediaCoverArtSize) // value: String(Config.dashboard.sizes.mediaCoverArtSize)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media visualiser size" // name: "Media visualiser size"
value: String(Config.dashboard.sizes.mediaVisualiserSize) // value: String(Config.dashboard.sizes.mediaVisualiserSize)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Resource size" // name: "Resource size"
value: String(Config.dashboard.sizes.resourceSize) // value: String(Config.dashboard.sizes.resourceSize)
} // }
} // }
} }
@@ -103,6 +103,18 @@ SettingsPage {
} }
} }
SettingsSection {
sectionId: "Greeter"
SettingsHeader {
name: "Greeter"
}
SettingsIconButton {
name: "Install wallpaper and color scheme to greeter"
}
}
SettingsSection { SettingsSection {
sectionId: "Idle" sectionId: "Idle"
@@ -9,6 +9,8 @@ import qs.Modules.Settings.Controls
ColumnLayout { ColumnLayout {
id: root id: root
property bool shouldBeActive: true
function addTimeoutEntry() { function addTimeoutEntry() {
let list = [...Config.general.idle.timeouts]; let list = [...Config.general.idle.timeouts];
@@ -40,8 +42,26 @@ ColumnLayout {
Config.save(); Config.save();
} }
Layout.fillWidth: true anchors.left: parent.left
anchors.right: parent.right
height: shouldBeActive ? implicitHeight : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
spacing: Appearance.spacing.smaller spacing: Appearance.spacing.smaller
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Settings { Settings {
name: "Idle Monitors" name: "Idle Monitors"
@@ -52,6 +72,8 @@ ColumnLayout {
SettingList { SettingList {
Layout.fillWidth: true Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
onAddActiveActionRequested: { onAddActiveActionRequested: {
root.updateTimeoutEntry(index, "activeAction", ""); root.updateTimeoutEntry(index, "activeAction", "");
+18
View File
@@ -0,0 +1,18 @@
import qs.Modules.Settings.Controls
import qs.Config
SettingsPage {
SettingsSection {
sectionId: "Plugins"
SettingsHeader {
name: "Plugins"
}
SettingBarEntryList {
name: "Enable or disable plugins"
object: Config.plugins
setting: "entries"
}
}
}
+97 -97
View File
@@ -19,8 +19,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Max toasts"
min: 1 min: 1
name: "Max toasts"
object: Config.utilities object: Config.utilities
setting: "maxToasts" setting: "maxToasts"
} }
@@ -29,8 +29,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Panel width"
min: 1 min: 1
name: "Panel width"
object: Config.utilities.sizes object: Config.utilities.sizes
setting: "width" setting: "width"
} }
@@ -39,8 +39,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Toast width"
min: 1 min: 1
name: "Toast width"
object: Config.utilities.sizes object: Config.utilities.sizes
setting: "toastWidth" setting: "toastWidth"
} }
@@ -77,100 +77,100 @@ SettingsPage {
setting: "gameModeChanged" setting: "gameModeChanged"
} }
Separator { // Separator {
// }
//
// SettingSwitch {
// name: "Do not disturb changed"
// object: Config.utilities.toasts
// setting: "dndChanged"
// }
//
// Separator {
// }
//
// SettingSwitch {
// name: "Audio output changed"
// object: Config.utilities.toasts
// setting: "audioOutputChanged"
// }
//
// Separator {
// }
//
// SettingSwitch {
// name: "Audio input changed"
// object: Config.utilities.toasts
// setting: "audioInputChanged"
// }
//
// Separator {
// }
//
// SettingSwitch {
// name: "Caps lock changed"
// object: Config.utilities.toasts
// setting: "capsLockChanged"
// }
//
// Separator {
// }
//
// SettingSwitch {
// name: "Num lock changed"
// object: Config.utilities.toasts
// setting: "numLockChanged"
// }
//
// Separator {
// }
//
// SettingSwitch {
// name: "Keyboard layout changed"
// object: Config.utilities.toasts
// setting: "kbLayoutChanged"
// }
//
// Separator {
// }
//
// SettingSwitch {
// name: "VPN changed"
// object: Config.utilities.toasts
// setting: "vpnChanged"
// }
//
// Separator {
// }
//
// SettingSwitch {
// name: "Now playing"
// object: Config.utilities.toasts
// setting: "nowPlaying"
// }
} }
SettingSwitch { // SettingsSection {
name: "Do not disturb changed" // sectionId: "VPN"
object: Config.utilities.toasts //
setting: "dndChanged" // SettingsHeader {
} // name: "VPN"
// }
Separator { //
} // SettingSwitch {
// name: "Enable VPN integration"
SettingSwitch { // object: Config.utilities.vpn
name: "Audio output changed" // setting: "enabled"
object: Config.utilities.toasts // }
setting: "audioOutputChanged" //
} // Separator {
// }
Separator { //
} // SettingStringList {
// name: "Provider"
SettingSwitch { // addLabel: qsTr("Add VPN provider")
name: "Audio input changed" // object: Config.utilities.vpn
object: Config.utilities.toasts // setting: "provider"
setting: "audioInputChanged" // }
} // }
Separator {
}
SettingSwitch {
name: "Caps lock changed"
object: Config.utilities.toasts
setting: "capsLockChanged"
}
Separator {
}
SettingSwitch {
name: "Num lock changed"
object: Config.utilities.toasts
setting: "numLockChanged"
}
Separator {
}
SettingSwitch {
name: "Keyboard layout changed"
object: Config.utilities.toasts
setting: "kbLayoutChanged"
}
Separator {
}
SettingSwitch {
name: "VPN changed"
object: Config.utilities.toasts
setting: "vpnChanged"
}
Separator {
}
SettingSwitch {
name: "Now playing"
object: Config.utilities.toasts
setting: "nowPlaying"
}
}
SettingsSection {
sectionId: "VPN"
SettingsHeader {
name: "VPN"
}
SettingSwitch {
name: "Enable VPN integration"
object: Config.utilities.vpn
setting: "enabled"
}
Separator {
}
SettingStringList {
name: "Provider"
addLabel: qsTr("Add VPN provider")
object: Config.utilities.vpn
setting: "provider"
}
}
} }
+10 -1
View File
@@ -79,6 +79,8 @@ Item {
stack.push(screenshot); stack.push(screenshot);
else if (currentCategory === "updates") else if (currentCategory === "updates")
stack.push(updates); stack.push(updates);
else if (currentCategory === "plugins")
stack.push(plugins);
} }
target: root target: root
@@ -134,7 +136,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: searchBar.bottom anchors.top: searchBar.bottom
anchors.topMargin: Appearance.spacing.smaller anchors.topMargin: Appearance.spacing.smaller
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainerLowest
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
StackView { StackView {
@@ -245,4 +247,11 @@ Item {
Cat.SystemUpdates { Cat.SystemUpdates {
} }
} }
Component {
id: plugins
Cat.Plugins {
}
}
} }
@@ -127,6 +127,9 @@ ColumnLayout {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
@@ -207,6 +210,8 @@ ColumnLayout {
StringListEditor { StringListEditor {
Layout.fillWidth: true Layout.fillWidth: true
addLabel: qsTr("Add command argument") addLabel: qsTr("Add command argument")
anchors.left: undefined
anchors.right: undefined
values: [...(modelData.command ?? [])] values: [...(modelData.command ?? [])]
onListEdited: function (values) { onListEdited: function (values) {
@@ -215,6 +220,9 @@ ColumnLayout {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
@@ -233,6 +241,9 @@ ColumnLayout {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
+25 -13
View File
@@ -6,7 +6,7 @@ import qs.Components
import qs.Config import qs.Config
import qs.Helpers import qs.Helpers
ColumnLayout { CustomRect {
id: root id: root
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
@@ -43,10 +43,9 @@ ColumnLayout {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: shouldBeActive ? implicitHeight : 0 height: shouldBeActive ? layout.implicitHeight : 0
opacity: shouldBeActive ? 1 : 0 opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8 scale: shouldBeActive ? 1 : 0.8
spacing: Appearance.spacing.smaller
visible: opacity > 0 visible: opacity > 0
Behavior on opacity { Behavior on opacity {
@@ -77,6 +76,14 @@ ColumnLayout {
} }
} }
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.smaller
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
font.pointSize: Appearance.font.size.larger font.pointSize: Appearance.font.size.larger
@@ -121,15 +128,17 @@ ColumnLayout {
} }
CustomRect { CustomRect {
Layout.fillWidth: true
Layout.preferredHeight: 33 Layout.preferredHeight: 33
Layout.preferredWidth: Math.max(Math.min(fromTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
color: DynamicColors.tPalette.m3surfaceContainerHigh color: DynamicColors.tPalette.m3surfaceContainerHigh
radius: Appearance.rounding.full radius: Appearance.rounding.full
CustomTextField { CustomTextField {
anchors.fill: parent id: fromTextField
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
text: modelData.from ?? "" text: modelData.from ?? ""
onEditingFinished: root.updateAlias(index, "from", text) onEditingFinished: root.updateAlias(index, "from", text)
@@ -154,15 +163,17 @@ ColumnLayout {
} }
CustomRect { CustomRect {
Layout.fillWidth: true
Layout.preferredHeight: 33 Layout.preferredHeight: 33
color: DynamicColors.tPalette.m3surface Layout.preferredWidth: Math.max(Math.min(toTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
radius: Appearance.rounding.small color: DynamicColors.tPalette.m3surfaceContainerHigh
radius: Appearance.rounding.full
CustomTextField { CustomTextField {
anchors.fill: parent id: toTextField
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
text: modelData.to ?? "" text: modelData.to ?? ""
onEditingFinished: root.updateAlias(index, "to", text) onEditingFinished: root.updateAlias(index, "to", text)
@@ -189,3 +200,4 @@ ColumnLayout {
} }
} }
} }
}
@@ -194,6 +194,9 @@ Item {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
@@ -225,6 +228,9 @@ Item {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
Item { Item {
@@ -0,0 +1,83 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Paths
import qs.Components
import qs.Config
import qs.Helpers
Item {
id: root
property alias button: iButton
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
required property string name
property bool shouldBeActive: true
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle {
anchors.fill: parent
anchors.margins: -Appearance.padding.smaller
color: DynamicColors.palette.m3primaryContainer
opacity: root.highlighted ? 0.5 : 0
radius: Appearance.rounding.small
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.normal
}
}
}
RowLayout {
id: row
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
CustomText {
id: text
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
font.pointSize: Appearance.font.size.larger
text: root.name
}
IconButton {
id: iButton
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
icon: "download"
onClicked: {
const lockBg = `${Paths.state}/lockscreen_bg.png`;
const scheme = `${Paths.state}/scheme.json`;
const face = `${Paths.home}/.face`;
const destination = "/etc/zshell-greeter/images";
Quickshell.execDetached(["pkexec", "sh", "-c", `mkdir -p ${destination}; cp ${lockBg} ${destination}; cp ${scheme} /etc/zshell-greeter; cp ${face} ${destination}`]);
}
}
}
}
+16 -25
View File
@@ -12,7 +12,8 @@ FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePat
, m_path(path) , m_path(path)
, m_relativePath(relativePath) , m_relativePath(relativePath)
, m_isImageInitialised(false) , m_isImageInitialised(false)
, m_mimeTypeInitialised(false) {} , m_mimeTypeInitialised(false) {
}
QString FileSystemEntry::path() const { QString FileSystemEntry::path() const {
return m_path; return m_path;
@@ -57,8 +58,8 @@ bool FileSystemEntry::isImage() const {
QString FileSystemEntry::mimeType() const { QString FileSystemEntry::mimeType() const {
if (!m_mimeTypeInitialised) { if (!m_mimeTypeInitialised) {
const QMimeDatabase db; static const QMimeDatabase s_db;
m_mimeType = db.mimeTypeForFile(m_path).name(); m_mimeType = s_db.mimeTypeForFile(m_path).name();
m_mimeTypeInitialised = true; m_mimeTypeInitialised = true;
} }
return m_mimeType; return m_mimeType;
@@ -219,7 +220,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
if (m_recursive && m_watchChanges) { if (m_recursive && m_watchChanges) {
const auto currentDir = m_dir; const auto currentDir = m_dir;
const bool showHidden = m_showHidden; const bool showHidden = m_showHidden;
const auto future = QtConcurrent::run([showHidden, path]() { auto future = QtConcurrent::run([showHidden, path]() {
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
@@ -232,16 +233,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
} }
return dirs; return dirs;
}); });
const auto watcher = new QFutureWatcher<QStringList>(this); future.then(this, [currentDir, showHidden, this](const QStringList& paths) {
connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() {
const auto paths = watcher->result();
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
// Ignore if dir or showHidden has changed // Ignore if dir or showHidden has changed
m_watcher.addPaths(paths); m_watcher.addPaths(paths);
} }
watcher->deleteLater();
}); });
watcher->setFuture(future);
} }
} }
@@ -295,7 +292,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
oldPaths << entry->path(); oldPaths << entry->path();
} }
const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) { auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString> > >& promise) {
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
std::optional<QDirIterator> iter; std::optional<QDirIterator> iter;
@@ -353,7 +350,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
newPaths.insert(path); newPaths.insert(path);
} }
if (promise.isCanceled() || newPaths == oldPaths) { if (promise.isCanceled()) {
return; return;
} }
@@ -365,23 +362,17 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
} }
m_futures.insert(dir, future); m_futures.insert(dir, future);
const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this); future
.then(this,
connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() { [dir, this](QPair<QSet<QString>, QSet<QString> > result) {
m_futures.remove(dir); m_futures.remove(dir);
if (!result.first.isEmpty() || !result.second.isEmpty()) {
if (!watcher->future().isResultReadyAt(0)) {
watcher->deleteLater();
return;
}
const auto result = watcher->result();
applyChanges(result.first, result.second); applyChanges(result.first, result.second);
}
watcher->deleteLater(); })
.onCanceled(this, [dir, this]() {
m_futures.remove(dir);
}); });
watcher->setFuture(future);
} }
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) { void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
+1 -1
View File
@@ -132,7 +132,7 @@ private:
bool m_recursive; bool m_recursive;
bool m_watchChanges; bool m_watchChanges;
bool m_showHidden; bool m_showHidden;
bool m_sortReverse; bool m_sortReverse = false;
Filter m_filter; Filter m_filter;
QStringList m_nameFilters; QStringList m_nameFilters;
+1
View File
@@ -9,6 +9,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"typer", "typer",
"pillow", "pillow",
"jinja2",
"materialyoucolor" "materialyoucolor"
] ]
+2 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import typer import typer
from zshell.subcommands import shell, scheme, screenshot, wallpaper from zshell.subcommands import shell, scheme, screenshot, wallpaper, record
app = typer.Typer() app = typer.Typer()
@@ -8,6 +8,7 @@ app.add_typer(shell.app, name="shell")
app.add_typer(scheme.app, name="scheme") app.add_typer(scheme.app, name="scheme")
app.add_typer(screenshot.app, name="screenshot") app.add_typer(screenshot.app, name="screenshot")
app.add_typer(wallpaper.app, name="wallpaper") app.add_typer(wallpaper.app, name="wallpaper")
app.add_typer(record.app, name="record")
# app.add_typer(preset.app, name="preset") # app.add_typer(preset.app, name="preset")
+214
View File
@@ -0,0 +1,214 @@
import os
import json
import subprocess
import time
from pathlib import Path
from typing import Optional
import typer
app = typer.Typer()
RECORDER = "gpu-screen-recorder"
HOME = str(os.getenv("HOME", str(Path.home())))
CONFIG = Path(HOME) / ".config/zshell/config.json"
STATE_DIR = Path(HOME) / ".local/state/zshell/record"
TEMP_RECORDING = STATE_DIR / "recording.mp4"
REPLAY_RECORDING = STATE_DIR / "replay.mp4"
NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR",
str(Path(HOME) / "Videos/Recordings"))
def _read_extra_args() -> list[str]:
try:
if CONFIG.is_file():
data = json.loads(CONFIG.read_text())
return data.get("record", {}).get("extraArgs", [])
except Exception:
pass
return []
def _is_recording() -> bool:
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
def _notify(summary: str, body: str = "", actions: list = None, timeout: int = 5000) -> Optional[int]:
args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
if actions:
for action in actions:
args.extend(["-A", action])
try:
proc = subprocess.run(args, capture_output=True, text=True)
return int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None
except Exception:
return None
def _close_notification(notif_id: int):
subprocess.run(["notify-send", "--close", str(notif_id)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _get_monitors() -> list[dict]:
try:
res = subprocess.run(["hyprctl", "monitors", "-j"],
capture_output=True, text=True)
return json.loads(res.stdout)
except Exception:
return []
def _focused_monitor_name() -> Optional[str]:
for m in _get_monitors():
if m.get("focused"):
return m["name"]
return None
def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
region = (x, y, x + w, y + h)
intersecting = []
for m in _get_monitors():
mx, my, mw, mh = m["x"], m["y"], m["width"], m["height"]
if not (region[2] <= mx or region[0] >= mx + mw or region[3] <= my or region[1] >= my + mh):
intersecting.append(m)
return intersecting
def _highest_refresh(monitors: list[dict]) -> float:
return max((m["refreshRate"] for m in monitors), default=60.0)
def _slurp_region() -> Optional[str]:
try:
return subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True).strip()
except subprocess.CalledProcessError:
return None
def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
import re
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
if match:
return int(match.group(3)), int(match.group(4)), int(match.group(1)), int(match.group(2))
return None
def start_recording(region: Optional[str], sound: bool):
STATE_DIR.mkdir(parents=True, exist_ok=True)
cmd = [RECORDER]
extra_args = _read_extra_args()
if region:
if region.lower() == "slurp" or not region:
geometry = _slurp_region()
if not geometry:
typer.echo("Region selection cancelled.")
raise typer.Abort()
else:
geometry = region
parsed = _parse_geometry(geometry)
if not parsed:
typer.echo("Invalid geometry format.")
raise typer.Abort()
x, y, w, h = parsed
monitors = _monitors_intersecting_region(x, y, w, h)
framerate = _highest_refresh(monitors)
cmd.extend(["-w", "region", "-region", geometry, "-f", str(int(framerate))])
else:
monitor_name = _focused_monitor_name()
if not monitor_name:
typer.echo("No focused monitor found.")
raise typer.Abort()
monitors = _get_monitors()
mon = next((m for m in monitors if m["name"] == monitor_name), None)
rate = int(mon["refreshRate"]) if mon else 60
cmd.extend(["-w", monitor_name, "-f", str(rate)])
if sound:
cmd.extend(["-a", "default_output"])
cmd.extend(extra_args)
cmd.extend(["-o", str(TEMP_RECORDING)])
subprocess.Popen(cmd, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
if notif_id is not None:
NOTIF_ID_FILE.write_text(str(notif_id))
time.sleep(1)
if not _is_recording():
_notify("Recording failed",
"Check gpu-screen-recorder output.", timeout=5000)
raise typer.Exit(code=1)
def stop_recording(clipboard: bool):
subprocess.run(["pkill", "-f", RECORDER],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(50):
if not _is_recording():
break
time.sleep(0.1)
dest_dir = Path(RECORDINGS_DIR)
dest_dir.mkdir(parents=True, exist_ok=True)
timestamp = time.strftime("%Y-%m-%d_%H-%M-%S")
final_path = dest_dir / f"recording_{timestamp}.mp4"
if TEMP_RECORDING.exists():
TEMP_RECORDING.rename(final_path)
if NOTIF_ID_FILE.is_file():
try:
_close_notification(int(NOTIF_ID_FILE.read_text().strip()))
except Exception:
pass
NOTIF_ID_FILE.unlink()
if clipboard:
subprocess.run(["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
def toggle_pause():
subprocess.run(["pkill", "-USR2", "-f", RECORDER],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
typer.echo("Toggled pause.")
@app.command()
def record(
region: Optional[str] = typer.Option(
None, "--region", "-r",
help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.",
),
sound: bool = typer.Option(
False, "--sound", "-s", help="Record audio from default output."),
pause: bool = typer.Option(
False, "--pause", "-p", help="Toggle pause/resume."),
clipboard: bool = typer.Option(
False, "--clipboard", "-c", help="Copy the final recording path to clipboard."),
):
"""Start or stop a screen recording with gpu-screen-recorder."""
if pause:
toggle_pause()
raise typer.Exit()
if _is_recording():
stop_recording(clipboard)
else:
start_recording(region, sound)
+45 -8
View File
@@ -1,4 +1,8 @@
import subprocess import subprocess
import sys
import time
import click
import typer import typer
args = ["qs", "-c", "zshell"] args = ["qs", "-c", "zshell"]
@@ -8,35 +12,68 @@ app = typer.Typer()
@app.command() @app.command()
def kill(): def kill():
subprocess.run(args + ["kill"], check=True) result = subprocess.run(args + ["kill"], capture_output=True)
if result.returncode != 0:
raise click.ClickException("No running instance to kill.")
sys.stderr.write(result.stderr.decode())
def start_instance(no_daemon: bool = False) -> None:
result = subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True)
stdout = result.stdout.decode().strip()
if stdout:
if "already running" in stdout.lower():
raise click.ClickException(stdout)
if result.returncode != 0:
stderr = result.stderr.decode().strip()
raise click.ClickException(stderr)
@app.command() @app.command()
def start(no_daemon: bool = False): def start(no_daemon: bool = False):
subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), check=True) start_instance(no_daemon)
@app.command() @app.command()
def restart(no_daemon: bool = False): def restart(no_daemon: bool = False):
subprocess.run(args + ["kill"], check=False) subprocess.run(args + ["kill"], capture_output=True)
subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), check=True) deadline = time.monotonic() + 2.5
while time.monotonic() < deadline:
result = subprocess.run(args + ["kill"], capture_output=True)
if result.returncode == 255:
break
time.sleep(0.25)
start_instance(no_daemon=no_daemon)
@app.command() @app.command()
def show(): def show():
subprocess.run(args + ["ipc"] + ["show"], check=True) result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True)
if result.returncode != 0:
raise click.ClickException(result.stderr.decode().strip())
sys.stderr.write(result.stderr.decode())
@app.command() @app.command()
def log(): def log():
subprocess.run(args + ["log"], check=True) result = subprocess.run(args + ["log"], capture_output=True)
if result.returncode != 0:
raise click.ClickException(result.stderr.decode().strip())
sys.stdout.write(result.stdout.decode())
sys.stderr.write(result.stderr.decode())
@app.command() @app.command()
def lock(): def lock():
subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], check=True) result = subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], capture_output=True)
if result.returncode != 0:
raise click.ClickException(result.stderr.decode().strip())
sys.stderr.write(result.stderr.decode())
@app.command() @app.command()
def call(target: str, method: str, method_args: list[str] = typer.Argument(None)): def call(target: str, method: str, method_args: list[str] = typer.Argument(None)):
subprocess.run(args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []), check=True) result = subprocess.run(args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []), capture_output=True)
if result.returncode != 0:
raise click.ClickException(result.stderr.decode().strip())
sys.stderr.write(result.stderr.decode())
+62 -19
View File
@@ -1,13 +1,15 @@
from __future__ import annotations from __future__ import annotations
from subprocess import CompletedProcess
from unittest.mock import patch, call from unittest.mock import patch, call
from typer.testing import CliRunner
from zshell.subcommands.shell import app from zshell.subcommands.shell import app
def invoke(*args: str) -> None:
from typer.testing import CliRunner
runner = CliRunner() runner = CliRunner()
def invoke(*args: str):
result = runner.invoke(app, args) result = runner.invoke(app, args)
if result.exit_code != 0: if result.exit_code != 0:
raise RuntimeError(result.output) raise RuntimeError(result.output)
@@ -16,72 +18,113 @@ def invoke(*args: str) -> None:
class TestKill: class TestKill:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_kill_runs_qs_kill(self, mock_run): def test_kill_runs_qs_kill_success(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"Killed abc\n")
invoke("kill") invoke("kill")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "kill"], check=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "kill"], capture_output=True)
@patch("zshell.subcommands.shell.subprocess.run")
def test_kill_no_instance_errors(self, mock_run):
mock_run.return_value = CompletedProcess([], 255, b"", b"No running instances\n")
result = runner.invoke(app, ["kill"])
assert result.exit_code != 0
assert "No running instance to kill" in result.output
class TestStart: class TestStart:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_start_default_daemon(self, mock_run): def test_start_default_daemon(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n")
invoke("start") invoke("start")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n", "-d"], check=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n", "-d"], capture_output=True)
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_start_no_daemon(self, mock_run): def test_start_no_daemon(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n")
invoke("start", "--no-daemon") invoke("start", "--no-daemon")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n"], check=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n"], capture_output=True)
@patch("zshell.subcommands.shell.subprocess.run")
def test_start_already_running_errors(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"An instance of this configuration is already running.\n", b"")
result = runner.invoke(app, ["start"])
assert result.exit_code != 0
assert "already running" in result.output
@patch("zshell.subcommands.shell.subprocess.run")
def test_start_other_failure_errors(self, mock_run):
mock_run.return_value = CompletedProcess([], 1, b"", b"Config error\n")
result = runner.invoke(app, ["start"])
assert result.exit_code != 0
assert "Config error" in result.output
class TestShow: class TestShow:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_show_runs_ipc_show(self, mock_run): def test_show_runs_ipc_show(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"target visibilities\n")
invoke("show") invoke("show")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], check=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], capture_output=True)
class TestLog: class TestLog:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_log_runs_qs_log(self, mock_run): def test_log_runs_qs_log(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"log output\n", b"")
invoke("log") invoke("log")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "log"], check=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "log"], capture_output=True)
class TestLock: class TestLock:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_lock_runs_ipc_call_lock(self, mock_run): def test_lock_runs_ipc_call_lock(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"")
invoke("lock") invoke("lock")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "lock", "lock"], check=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "lock", "lock"], capture_output=True)
class TestCall: class TestCall:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_call_no_args(self, mock_run): def test_call_no_args(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"")
invoke("call", "target", "method") invoke("call", "target", "method")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "target", "method"], check=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "target", "method"], capture_output=True)
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_call_with_args(self, mock_run): def test_call_with_args(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"")
invoke("call", "target", "method", "arg1", "arg2") invoke("call", "target", "method", "arg1", "arg2")
mock_run.assert_called_once_with( mock_run.assert_called_once_with(
["qs", "-c", "zshell", "ipc", "call", "target", "method", "arg1", "arg2"], ["qs", "-c", "zshell", "ipc", "call", "target", "method", "arg1", "arg2"],
check=True, capture_output=True,
) )
class TestRestart: class TestRestart:
@patch("zshell.subcommands.shell.start_instance")
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_restart_kills_then_starts_daemon(self, mock_run): def test_restart_kills_then_starts(self, mock_run, mock_start):
mock_run.side_effect = [
CompletedProcess([], 0, b"", b"Killed abc\n"), # first kill (captured)
CompletedProcess([], 255, b"", b""), # poll → no instance
]
invoke("restart") invoke("restart")
assert mock_run.call_args_list == [ assert mock_run.call_args_list == [
call(["qs", "-c", "zshell", "kill"], check=False), call(["qs", "-c", "zshell", "kill"], capture_output=True),
call(["qs", "-c", "zshell", "-n", "-d"], check=True), call(["qs", "-c", "zshell", "kill"], capture_output=True),
] ]
mock_start.assert_called_once_with(no_daemon=False)
@patch("zshell.subcommands.shell.start_instance")
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_restart_no_daemon(self, mock_run): def test_restart_no_daemon(self, mock_run, mock_start):
mock_run.side_effect = [
CompletedProcess([], 0, b"", b"Killed abc\n"),
CompletedProcess([], 255, b"", b""),
]
invoke("restart", "--no-daemon") invoke("restart", "--no-daemon")
assert mock_run.call_args_list == [ assert mock_run.call_args_list == [
call(["qs", "-c", "zshell", "kill"], check=False), call(["qs", "-c", "zshell", "kill"], capture_output=True),
call(["qs", "-c", "zshell", "-n"], check=True), call(["qs", "-c", "zshell", "kill"], capture_output=True),
] ]
mock_start.assert_called_once_with(no_daemon=True)
+6
View File
@@ -6,6 +6,7 @@
//@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round //@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round
//@ pragma DropExpensiveFonts //@ pragma DropExpensiveFonts
import Quickshell import Quickshell
import qs.Extensions
import qs.Modules import qs.Modules
import qs.Modules.Wallpaper import qs.Modules.Wallpaper
import qs.Modules.Lock import qs.Modules.Lock
@@ -14,6 +15,8 @@ import qs.Helpers
import qs.Modules.Polkit import qs.Modules.Polkit
ShellRoot { ShellRoot {
settings.watchFiles: true
Windows { Windows {
} }
@@ -36,4 +39,7 @@ ShellRoot {
Polkit { Polkit {
} }
LoadExtensions {
}
} }