Merge branch 'main' into zshell-img-tools
This commit is contained in:
+8
-1
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,10 @@ Singleton {
|
|||||||
},
|
},
|
||||||
idle: {
|
idle: {
|
||||||
timeouts: general.idle.timeouts
|
timeouts: general.idle.timeouts
|
||||||
|
},
|
||||||
|
battery: {
|
||||||
|
popupThresholds: general.battery.popupThresholds,
|
||||||
|
critPerc: general.battery.critPerc
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import Quickshell
|
|||||||
JsonObject {
|
JsonObject {
|
||||||
property Apps apps: Apps {
|
property Apps apps: Apps {
|
||||||
}
|
}
|
||||||
|
property Battery battery: Battery {
|
||||||
|
}
|
||||||
property Color color: Color {
|
property Color color: Color {
|
||||||
}
|
}
|
||||||
property string dateFormat: "ddd d MMM - hh:mm:ss"
|
property string dateFormat: "ddd d MMM - hh:mm:ss"
|
||||||
@@ -19,6 +21,17 @@ JsonObject {
|
|||||||
property list<string> playback: ["mpv"]
|
property list<string> playback: ["mpv"]
|
||||||
property list<string> terminal: ["kitty"]
|
property list<string> terminal: ["kitty"]
|
||||||
}
|
}
|
||||||
|
component Battery: JsonObject {
|
||||||
|
property int critPerc: 5
|
||||||
|
property list<var> popupThresholds: [
|
||||||
|
{
|
||||||
|
perc: 20,
|
||||||
|
name: qsTr("Low battery"),
|
||||||
|
message: qsTr("Battery at %1%").arg(Battery.currentPerc * 100),
|
||||||
|
icon: "battery_android_frame_2"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
component Color: JsonObject {
|
component Color: JsonObject {
|
||||||
property int hyprsunsetTemp: 5000
|
property int hyprsunsetTemp: 5000
|
||||||
property string mode: "dark"
|
property string mode: "dark"
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import QtQuick
|
||||||
|
import ZShell
|
||||||
|
import qs.Config
|
||||||
|
import qs.Components.Toast
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property real currentPerc: UPower.displayDevice.percentage
|
||||||
|
readonly property list<var> popupThresholds: [...Config.general.battery.popupThresholds].sort((a, b) => b.perc - a.perc)
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onOnBatteryChanged(): void {
|
||||||
|
if (UPower.onBattery) {
|
||||||
|
if (Config.utilities.toasts.chargingChanged)
|
||||||
|
Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off");
|
||||||
|
} else {
|
||||||
|
if (Config.utilities.toasts.chargingChanged)
|
||||||
|
Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power");
|
||||||
|
for (const level of root.popupThresholds)
|
||||||
|
level.warned = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: UPower
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onPercentageChanged(): void {
|
||||||
|
if (!UPower.onBattery)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const p = UPower.displayDevice.percentage * 100;
|
||||||
|
for (const perc of root.popupThresholds) {
|
||||||
|
if (p <= perc.perc && !perc.warned) {
|
||||||
|
perc.warned = true;
|
||||||
|
Toaster.toast(perc.title ?? qsTr("Battery warning"), perc.message ?? qsTr("Battery perc is low"), perc.icon ?? "battery_android_alert", perc.critical ? Toast.Error : Toast.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: UPower.displayDevice
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -35,7 +35,7 @@ Variants {
|
|||||||
property var root: Quickshell.shellDir
|
property var root: Quickshell.shellDir
|
||||||
|
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
// WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
contentItem.focus: true
|
contentItem.focus: true
|
||||||
mask: visibilities.isDrawing ? null : region
|
mask: visibilities.isDrawing ? null : region
|
||||||
|
|||||||
+82
-41
@@ -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,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: ({})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Quickshell
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
import qs.Modules.Launcher.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -19,26 +20,17 @@ Item {
|
|||||||
max -= panels.popouts.nonAnimHeight;
|
max -= panels.popouts.nonAnimHeight;
|
||||||
return max;
|
return max;
|
||||||
}
|
}
|
||||||
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
required property var panels
|
required property var panels
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
required property PersistentProperties visibilities
|
|
||||||
readonly property bool shouldBeActive: visibilities.launcher
|
readonly property bool shouldBeActive: visibilities.launcher
|
||||||
property real offsetScale: shouldBeActive ? 0 : 1
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
onShouldBeActiveChanged: {
|
|
||||||
if (shouldBeActive) {
|
|
||||||
implicitHeight = Qt.binding(() => content.implicitHeight);
|
|
||||||
timer.stop();
|
|
||||||
} else {
|
|
||||||
implicitHeight = implicitHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: offsetScale < 1
|
|
||||||
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
implicitWidth: content.implicitWidth || 400
|
implicitWidth: content.implicitWidth || 400
|
||||||
opacity: 1 - offsetScale
|
opacity: 1 - offsetScale
|
||||||
|
visible: offsetScale < 1
|
||||||
|
|
||||||
Behavior on offsetScale {
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {
|
||||||
@@ -47,61 +39,26 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMaxHeightChanged: timer.start()
|
Component.onCompleted: Qt.callLater(() => Apps)
|
||||||
|
onShouldBeActiveChanged: {
|
||||||
Connections {
|
if (shouldBeActive)
|
||||||
function onEnabledChanged(): void {
|
implicitHeight = Qt.binding(() => content.implicitHeight);
|
||||||
timer.start();
|
else
|
||||||
}
|
implicitHeight = implicitHeight;
|
||||||
|
|
||||||
function onMaxShownChanged(): void {
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Config.launcher
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onValuesChanged(): void {
|
|
||||||
if (DesktopEntries.applications.values.length < Config.launcher.maxAppsShown)
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: DesktopEntries.applications
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: timer
|
|
||||||
|
|
||||||
interval: Appearance.anim.durations.small
|
|
||||||
|
|
||||||
onRunningChanged: {
|
|
||||||
if (running && !root.shouldBeActive) {
|
|
||||||
content.visible = false;
|
|
||||||
content.active = true;
|
|
||||||
} else {
|
|
||||||
root.contentHeight = Math.min(root.maxHeight, content.implicitHeight);
|
|
||||||
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
|
|
||||||
content.visible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
active: false
|
active: root.shouldBeActive || root.visible
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
maxHeight: root.maxHeight
|
maxHeight: root.maxHeight
|
||||||
panels: root.panels
|
panels: root.panels
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
|
||||||
Component.onCompleted: root.contentHeight = implicitHeight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: timer.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,10 @@ CustomRect {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
onLinkActivated: link => {
|
onLinkActivated: link => {
|
||||||
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
if (Config.launcher.uwsm)
|
||||||
|
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
|
||||||
|
|||||||
@@ -100,12 +100,14 @@ Item {
|
|||||||
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
||||||
value: root.brightness
|
value: root.brightness
|
||||||
|
|
||||||
onMoved: {
|
onPressedChanged: {
|
||||||
if (Config.osd.allMonBrightness) {
|
if (!pressed) {
|
||||||
root.monitor?.setBrightness(value);
|
if (Config.osd.allMonBrightness) {
|
||||||
} else {
|
for (const mon of Brightness.monitors) {
|
||||||
for (const mon of Brightness.monitors) {
|
mon.setBrightness(value);
|
||||||
mon.setBrightness(value);
|
}
|
||||||
|
} else {
|
||||||
|
root.monitor?.setBrightness(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", "");
|
||||||
|
|||||||
@@ -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 {
|
// SettingSwitch {
|
||||||
name: "Do not disturb changed"
|
// name: "Do not disturb changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "dndChanged"
|
// setting: "dndChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Audio output changed"
|
// name: "Audio output changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "audioOutputChanged"
|
// setting: "audioOutputChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Audio input changed"
|
// name: "Audio input changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "audioInputChanged"
|
// setting: "audioInputChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Caps lock changed"
|
// name: "Caps lock changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "capsLockChanged"
|
// setting: "capsLockChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Num lock changed"
|
// name: "Num lock changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "numLockChanged"
|
// setting: "numLockChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Keyboard layout changed"
|
// name: "Keyboard layout changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "kbLayoutChanged"
|
// setting: "kbLayoutChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "VPN changed"
|
// name: "VPN changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "vpnChanged"
|
// setting: "vpnChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Now playing"
|
// name: "Now playing"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "nowPlaying"
|
// setting: "nowPlaying"
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection {
|
// SettingsSection {
|
||||||
sectionId: "VPN"
|
// sectionId: "VPN"
|
||||||
|
//
|
||||||
SettingsHeader {
|
// SettingsHeader {
|
||||||
name: "VPN"
|
// name: "VPN"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Enable VPN integration"
|
// name: "Enable VPN integration"
|
||||||
object: Config.utilities.vpn
|
// object: Config.utilities.vpn
|
||||||
setting: "enabled"
|
// setting: "enabled"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingStringList {
|
// SettingStringList {
|
||||||
name: "Provider"
|
// name: "Provider"
|
||||||
addLabel: qsTr("Add VPN provider")
|
// addLabel: qsTr("Add VPN provider")
|
||||||
object: Config.utilities.vpn
|
// object: Config.utilities.vpn
|
||||||
setting: "provider"
|
// setting: "provider"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,115 +76,128 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
id: layout
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
text: root.name
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
anchors.left: parent.left
|
||||||
model: [...root.object[root.setting]]
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
Item {
|
spacing: Appearance.spacing.smaller
|
||||||
required property int index
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: -(Appearance.spacing.smaller / 2)
|
|
||||||
color: DynamicColors.tPalette.m3outlineVariant
|
|
||||||
implicitHeight: 1
|
|
||||||
visible: index !== 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: layout
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Appearance.padding.small
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: qsTr("From")
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 33
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
|
||||||
radius: Appearance.rounding.full
|
|
||||||
|
|
||||||
CustomTextField {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Appearance.padding.normal
|
|
||||||
anchors.rightMargin: Appearance.padding.normal
|
|
||||||
text: modelData.from ?? ""
|
|
||||||
|
|
||||||
onEditingFinished: root.updateAlias(index, "from", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
icon: "delete"
|
|
||||||
type: IconButton.Tonal
|
|
||||||
|
|
||||||
onClicked: root.removeAlias(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: qsTr("To")
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 33
|
|
||||||
color: DynamicColors.tPalette.m3surface
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
CustomTextField {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Appearance.padding.normal
|
|
||||||
anchors.rightMargin: Appearance.padding.normal
|
|
||||||
text: modelData.to ?? ""
|
|
||||||
|
|
||||||
onEditingFinished: root.updateAlias(index, "to", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
icon: "add"
|
|
||||||
|
|
||||||
onClicked: root.addAlias()
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: qsTr("Add alias")
|
font.pointSize: Appearance.font.size.larger
|
||||||
|
text: root.name
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [...root.object[root.setting]]
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: -(Appearance.spacing.smaller / 2)
|
||||||
|
color: DynamicColors.tPalette.m3outlineVariant
|
||||||
|
implicitHeight: 1
|
||||||
|
visible: index !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.small
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("From")
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.preferredHeight: 33
|
||||||
|
Layout.preferredWidth: Math.max(Math.min(fromTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
CustomTextField {
|
||||||
|
id: fromTextField
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
|
||||||
|
text: modelData.from ?? ""
|
||||||
|
|
||||||
|
onEditingFinished: root.updateAlias(index, "from", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
icon: "delete"
|
||||||
|
type: IconButton.Tonal
|
||||||
|
|
||||||
|
onClicked: root.removeAlias(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("To")
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.preferredHeight: 33
|
||||||
|
Layout.preferredWidth: Math.max(Math.min(toTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
CustomTextField {
|
||||||
|
id: toTextField
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
|
||||||
|
text: modelData.to ?? ""
|
||||||
|
|
||||||
|
onEditingFinished: root.updateAlias(index, "to", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
icon: "add"
|
||||||
|
|
||||||
|
onClicked: root.addAlias()
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Add alias")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import qs.Modules.DesktopIcons
|
|||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: Config.background.enabled
|
active: Config.background.enabled
|
||||||
asynchronous: true
|
asynchronous: false
|
||||||
|
|
||||||
sourceComponent: Variants {
|
sourceComponent: Variants {
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"typer",
|
"typer",
|
||||||
"pillow",
|
"pillow",
|
||||||
|
"jinja2",
|
||||||
"materialyoucolor"
|
"materialyoucolor"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,53 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import typer
|
import sys
|
||||||
from zshell.subcommands import shell, scheme, screenshot, wallpaper
|
from pathlib import Path
|
||||||
|
|
||||||
app = typer.Typer()
|
import click
|
||||||
|
import typer
|
||||||
|
from typer._completion_shared import install, _get_shell_name
|
||||||
|
from zshell.subcommands import shell, scheme, screenshot, wallpaper, record
|
||||||
|
|
||||||
|
app = typer.Typer(name="zshell-cli", add_completion=False)
|
||||||
|
|
||||||
app.add_typer(shell.app, name="shell")
|
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(preset.app, name="preset")
|
app.add_typer(record.app, name="record")
|
||||||
|
|
||||||
|
|
||||||
|
def _completion_installed() -> bool:
|
||||||
|
shell = _get_shell_name()
|
||||||
|
match shell:
|
||||||
|
case "zsh":
|
||||||
|
return (Path.home() / ".zfunc" / "_zshell-cli").exists()
|
||||||
|
case "bash":
|
||||||
|
return (Path.home() / ".bash_completions" / "zshell-cli.sh").exists()
|
||||||
|
case "fish":
|
||||||
|
return (Path.home() / ".config" / "fish" / "completions" / "zshell-cli.fish").exists()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _install_completion() -> None:
|
||||||
|
if _completion_installed():
|
||||||
|
click.echo("zshell-cli: Shell completion already installed.")
|
||||||
|
raise typer.Exit()
|
||||||
|
shell = _get_shell_name()
|
||||||
|
if shell is None:
|
||||||
|
click.echo("zshell-cli: Unable to detect shell type.", err=True)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
try:
|
||||||
|
_, path = install(prog_name="zshell-cli")
|
||||||
|
click.secho(f"zshell-cli: Shell completion installed ({shell}: {path})", fg="green")
|
||||||
|
click.echo("zshell-cli: Restart your shell or source the file to enable tab-completion.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
if "--install-autocomplete" in sys.argv:
|
||||||
|
_install_completion()
|
||||||
|
return
|
||||||
|
if sys.stdout.isatty() and not _completion_installed():
|
||||||
|
click.echo("zshell-cli: Tip: run with --install-autocomplete for tab completion.", err=True)
|
||||||
app()
|
app()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,210 @@
|
|||||||
|
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 = 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)
|
||||||
@@ -2,6 +2,7 @@ import typer
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -15,11 +16,61 @@ from materialyoucolor.score.score import Score
|
|||||||
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
|
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
|
||||||
from materialyoucolor.hct.hct import Hct
|
from materialyoucolor.hct.hct import Hct
|
||||||
from materialyoucolor.utils.color_utils import argb_from_rgb
|
from materialyoucolor.utils.color_utils import argb_from_rgb
|
||||||
from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
|
from materialyoucolor.utils.math_utils import (
|
||||||
|
difference_degrees,
|
||||||
|
rotation_direction,
|
||||||
|
sanitize_degrees_double,
|
||||||
|
)
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
def _complete_scheme_name(incomplete):
|
||||||
|
schemes = [
|
||||||
|
"fruit-salad",
|
||||||
|
"expressive",
|
||||||
|
"monochrome",
|
||||||
|
"rainbow",
|
||||||
|
"tonal-spot",
|
||||||
|
"neutral",
|
||||||
|
"fidelity",
|
||||||
|
"content",
|
||||||
|
"vibrant",
|
||||||
|
]
|
||||||
|
return [s for s in schemes if incomplete in s]
|
||||||
|
|
||||||
|
|
||||||
|
def _complete_preset(incomplete):
|
||||||
|
results = []
|
||||||
|
for sid, meta in list_schemes().items():
|
||||||
|
for v in meta.variants:
|
||||||
|
preset = f"{sid}:{v.id}"
|
||||||
|
if incomplete in preset:
|
||||||
|
results.append((preset, f"{meta.name} - {v.name}"))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _complete_mode(incomplete):
|
||||||
|
return [m for m in ("dark", "light") if incomplete in m]
|
||||||
|
|
||||||
|
|
||||||
|
def _complete_accent(ctx, incomplete):
|
||||||
|
preset_val = ctx.params.get("preset")
|
||||||
|
if preset_val:
|
||||||
|
try:
|
||||||
|
p_scheme, p_variant = resolve_preset(preset_val)
|
||||||
|
for v in list_schemes()[p_scheme].variants:
|
||||||
|
if v.id == p_variant:
|
||||||
|
return [a for a in v.accents if incomplete in a]
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
pass
|
||||||
|
all_accents = set()
|
||||||
|
for meta in list_schemes().values():
|
||||||
|
for v in meta.variants:
|
||||||
|
all_accents.update(v.accents)
|
||||||
|
return [a for a in sorted(all_accents) if incomplete in a]
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def list_presets(
|
def list_presets(
|
||||||
json_format: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
json_format: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
||||||
@@ -30,7 +81,7 @@ def list_presets(
|
|||||||
for sid, meta in sorted(schemes.items()):
|
for sid, meta in sorted(schemes.items()):
|
||||||
variants = {}
|
variants = {}
|
||||||
for v in meta.variants:
|
for v in meta.variants:
|
||||||
entry = {"modes": sorted(v.modes)}
|
entry: dict[str, Any] = {"modes": sorted(v.modes)}
|
||||||
if v.accents:
|
if v.accents:
|
||||||
entry["accents"] = sorted(v.accents)
|
entry["accents"] = sorted(v.accents)
|
||||||
entry["default_accent"] = sorted(v.accents)[0]
|
entry["default_accent"] = sorted(v.accents)[0]
|
||||||
@@ -55,14 +106,35 @@ def list_presets(
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def generate(
|
def generate(
|
||||||
image_path: Optional[Path] = typer.Option(None, help="Path to source image. Required for image mode."),
|
image_path: Optional[Path] = typer.Option(
|
||||||
scheme: Optional[str] = typer.Option(
|
None, help="Path to source image. Required for image mode."
|
||||||
None, help="Color scheme algorithm to use for image mode. Ignored in preset mode."
|
),
|
||||||
|
scheme: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Color scheme algorithm to use for image mode. Ignored in preset mode.",
|
||||||
|
autocompletion=_complete_scheme_name,
|
||||||
|
),
|
||||||
|
preset: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Name of a premade scheme in this format: <scheme>:<variant>",
|
||||||
|
autocompletion=_complete_preset,
|
||||||
|
),
|
||||||
|
mode: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Mode of the preset scheme (dark or light).",
|
||||||
|
autocompletion=_complete_mode,
|
||||||
|
),
|
||||||
|
accent: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Accent for schemes that support it (e.g. mauve).",
|
||||||
|
autocompletion=_complete_accent,
|
||||||
),
|
),
|
||||||
preset: Optional[str] = typer.Option(None, help="Name of a premade scheme in this format: <scheme>:<variant>"),
|
|
||||||
mode: Optional[str] = typer.Option(None, help="Mode of the preset scheme (dark or light)."),
|
|
||||||
accent: Optional[str] = typer.Option(None, help="Accent for schemes that support it (e.g. mauve)."),
|
|
||||||
):
|
):
|
||||||
|
if not any([image_path, scheme, preset, mode, accent]):
|
||||||
|
print(
|
||||||
|
"Hint: use --preset <scheme>:<variant> or --image-path <path>",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
HOME = str(os.getenv("HOME"))
|
HOME = str(os.getenv("HOME"))
|
||||||
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
||||||
@@ -200,11 +272,15 @@ def generate(
|
|||||||
def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct:
|
def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct:
|
||||||
diff = difference_degrees(from_hct.hue, to_hct.hue)
|
diff = difference_degrees(from_hct.hue, to_hct.hue)
|
||||||
rotation = min(diff * 0.8, 100)
|
rotation = min(diff * 0.8, 100)
|
||||||
output_hue = sanitize_degrees_double(from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue))
|
output_hue = sanitize_degrees_double(
|
||||||
|
from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue)
|
||||||
|
)
|
||||||
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
|
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
|
||||||
return Hct.from_hct(output_hue, from_hct.chroma, tone)
|
return Hct.from_hct(output_hue, from_hct.chroma, tone)
|
||||||
|
|
||||||
def terminal_palette(colors: dict[str, str], mode: str, variant: str) -> dict[str, str]:
|
def terminal_palette(
|
||||||
|
colors: dict[str, str], mode: str, variant: str
|
||||||
|
) -> dict[str, str]:
|
||||||
light = mode.lower() == "light"
|
light = mode.lower() == "light"
|
||||||
|
|
||||||
key_hex = (
|
key_hex = (
|
||||||
@@ -236,7 +312,7 @@ def generate(
|
|||||||
|
|
||||||
image = Image.open(image_path)
|
image = Image.open(image_path)
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
image.thumbnail(size, Image.NEAREST)
|
image.thumbnail(size, Image.Resampling.NEAREST)
|
||||||
|
|
||||||
thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
|
thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
image.save(thumbnail_path, "JPEG")
|
image.save(thumbnail_path, "JPEG")
|
||||||
@@ -268,8 +344,15 @@ def generate(
|
|||||||
is_dark = ""
|
is_dark = ""
|
||||||
|
|
||||||
with Image.open(image_path) as img:
|
with Image.open(image_path) as img:
|
||||||
img.thumbnail((1, 1), Image.LANCZOS)
|
img.thumbnail((1, 1), Image.Resampling.LANCZOS)
|
||||||
hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0))))
|
px = img.getpixel((0, 0))
|
||||||
|
if isinstance(px, (int, float)):
|
||||||
|
r = g = b = int(px)
|
||||||
|
elif px is not None:
|
||||||
|
r, g, b = int(px[0]), int(px[1]), int(px[2])
|
||||||
|
else:
|
||||||
|
r = g = b = 0
|
||||||
|
hct = Hct.from_int(argb_from_rgb(r, g, b))
|
||||||
is_dark = "light" if hct.tone > 50 else "dark"
|
is_dark = "light" if hct.tone > 50 else "dark"
|
||||||
|
|
||||||
return is_dark
|
return is_dark
|
||||||
@@ -431,6 +514,8 @@ def generate(
|
|||||||
|
|
||||||
raw = tpl_path.read_text(encoding="utf-8")
|
raw = tpl_path.read_text(encoding="utf-8")
|
||||||
out_path, body = split_directive_and_body(raw)
|
out_path, body = split_directive_and_body(raw)
|
||||||
|
if out_path is None:
|
||||||
|
continue
|
||||||
|
|
||||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -484,23 +569,30 @@ def generate(
|
|||||||
with CONFIG.open() as f:
|
with CONFIG.open() as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
scheme = scheme or config["colors"]["schemeType"]
|
scheme_type = config["colors"].get("schemeType", "fruit-salad")
|
||||||
|
scheme = scheme or scheme_type
|
||||||
|
assert isinstance(scheme, str)
|
||||||
config_mode = config["general"]["color"]["mode"]
|
config_mode = config["general"]["color"]["mode"]
|
||||||
smart = bool(config["general"]["color"].get("smart", False))
|
smart = bool(config["general"]["color"].get("smart", False))
|
||||||
scheme_class = get_scheme_class(scheme)
|
scheme_class = get_scheme_class(scheme)
|
||||||
|
|
||||||
|
p_variant = "default"
|
||||||
if preset:
|
if preset:
|
||||||
p_scheme, p_variant = resolve_preset(preset)
|
p_scheme, p_variant = resolve_preset(preset)
|
||||||
schemes = list_schemes()
|
schemes = list_schemes()
|
||||||
if accent and p_scheme in schemes:
|
if accent and p_scheme in schemes:
|
||||||
meta = schemes[p_scheme]
|
meta = schemes[p_scheme]
|
||||||
var_accents = next((v.accents for v in meta.variants if v.id == p_variant), ())
|
var_accents = next(
|
||||||
|
(v.accents for v in meta.variants if v.id == p_variant), ()
|
||||||
|
)
|
||||||
if accent not in var_accents:
|
if accent not in var_accents:
|
||||||
available = ", ".join(var_accents) if var_accents else "none"
|
available = ", ".join(var_accents) if var_accents else "none"
|
||||||
raise typer.BadParameter(
|
raise typer.BadParameter(
|
||||||
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
|
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
|
||||||
)
|
)
|
||||||
palette_obj = get_palette(p_scheme, p_variant, mode or config_mode, accent=accent)
|
palette_obj = get_palette(
|
||||||
|
p_scheme, p_variant, mode or config_mode, accent=accent
|
||||||
|
)
|
||||||
colors = palette_obj.colors
|
colors = palette_obj.colors
|
||||||
effective_mode = palette_obj.mode
|
effective_mode = palette_obj.mode
|
||||||
name = palette_obj.scheme
|
name = palette_obj.scheme
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ def lockscreen(
|
|||||||
return
|
return
|
||||||
|
|
||||||
if size[0] < 3840 or size[1] < 2160:
|
if size[0] < 3840 or size[1] < 2160:
|
||||||
img = img.resize((size[0] // 2, size[1] // 2), Image.NEAREST)
|
img = img.resize((size[0] // 2, size[1] // 2), Image.Resampling.NEAREST)
|
||||||
else:
|
else:
|
||||||
img = img.resize((size[0] // 4, size[1] // 4), Image.NEAREST)
|
img = img.resize((size[0] // 4, size[1] // 4), Image.Resampling.NEAREST)
|
||||||
|
|
||||||
img = img.filter(ImageFilter.GaussianBlur(blur_amount))
|
img = img.filter(ImageFilter.GaussianBlur(blur_amount))
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+61
-18
@@ -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
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
def invoke(*args: str) -> None:
|
|
||||||
from typer.testing import 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)
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
//@ pragma UseQApplication
|
//@ pragma UseQApplication
|
||||||
//@ pragma Env QSG_RENDER_LOOP=threaded
|
//@ pragma Env QSG_RENDER_LOOP=threaded
|
||||||
// @ pragma Env QSG_RHI_BACKEND=vulkan
|
//@ pragma Env QSG_RHI_BACKEND=vulkan
|
||||||
//@ pragma Env QSG_NO_VSYNC=1
|
//@ pragma Env QSG_NO_VSYNC=1
|
||||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||||
//@ 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 Quickshell.Services.UPower
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Modules.Wallpaper
|
import qs.Modules.Wallpaper
|
||||||
import qs.Modules.Lock
|
import qs.Modules.Lock
|
||||||
import qs.Drawers
|
import qs.Drawers
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Modules.Polkit
|
import qs.Modules.Polkit
|
||||||
|
import qs.Daemons
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
|
||||||
|
|
||||||
|
settings.watchFiles: true
|
||||||
|
|
||||||
Windows {
|
Windows {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,4 +44,11 @@ ShellRoot {
|
|||||||
|
|
||||||
Polkit {
|
Polkit {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
activeAsync: root.laptop
|
||||||
|
|
||||||
|
component: Battery {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user