57 Commits

Author SHA1 Message Date
Aram Markarov c4a3206ffd removing 'result' and adjusted .gitignore to not include result symlink folder 2026-03-17 21:37:21 +01:00
Aram Markarov 7a61cc4280 warning in README.md for nix users 2026-03-17 21:28:11 +01:00
AramMarkarov f4c5ce08d2 preparation brightnesscontrol ddcutil and brightnessctl : missing osd functionality 2026-03-17 04:20:25 +01:00
Aram Markarov 08b18880a0 added result folder (nix build file) to gitignore(updated to work) 2026-03-12 18:50:29 +01:00
Aram Markarov 042a92a881 added result folder (nix build file) to gitignore 2026-03-12 17:55:42 +01:00
Aram Markarov 42c4b66399 comments removed from a app2unit.nix file. 2026-03-12 17:39:10 +01:00
Zach 9bfa2f6cca Merge pull request #21 from Zacharias-Brohn/settingsWindow
Settings window
2026-03-07 17:54:39 +01:00
Zacharias-Brohn 1d84248295 smart 2026-03-07 17:52:46 +01:00
Zacharias-Brohn c0f4434fd4 smart 2026-03-07 17:49:05 +01:00
Zach 49b489e7d9 Merge pull request #20 from Zacharias-Brohn/settingsWindow
Settings window
2026-03-06 23:27:24 +01:00
Zach 5d27d603e5 Merge branch 'main' into settingsWindow 2026-03-06 23:27:15 +01:00
Zacharias-Brohn 889c5993df lol 2026-03-06 23:23:39 +01:00
Zacharias-Brohn eac056cf1e lol 2026-03-06 23:12:47 +01:00
Zacharias-Brohn 62b250303a lol 2026-03-06 23:05:34 +01:00
Zacharias-Brohn 6d872c4f8d lol 2026-03-06 22:59:09 +01:00
Aram Markarov 74e417a49c added jinja2 python dependency to zshell-cli nix file 2026-03-05 22:08:48 +01:00
Zacharias-Brohn 6371fa56f8 remove wallust 2026-03-05 21:36:26 +01:00
Zacharias-Brohn 79b5220081 test 2026-03-05 16:48:28 +01:00
Zacharias-Brohn 3ecddee5e9 test 2026-03-05 16:41:12 +01:00
Zacharias-Brohn de91e36228 test 2026-03-05 16:34:35 +01:00
Zacharias-Brohn 467fdc5e5e test 2026-03-05 16:32:15 +01:00
Zacharias-Brohn f5a3b6f98f test 2026-03-05 15:50:34 +01:00
Zacharias-Brohn 2baf91552d test 2026-03-05 15:48:24 +01:00
Zacharias-Brohn 76d5508072 test 2026-03-05 15:31:24 +01:00
Zacharias-Brohn 533f268184 test 2026-03-05 15:23:21 +01:00
Zacharias-Brohn 0b6b5d0491 test 2026-03-05 15:20:20 +01:00
Zacharias-Brohn 75df8e1134 test 2026-03-05 11:43:22 +01:00
Zacharias-Brohn bab9554a60 test 2026-03-05 10:39:08 +01:00
Zacharias-Brohn e195f58125 test 2026-03-04 22:52:08 +01:00
Zacharias-Brohn 71b871c976 test 2026-03-04 22:41:45 +01:00
Zacharias-Brohn c87443e72c test 2026-03-04 22:38:15 +01:00
Zacharias-Brohn 531dd35f50 test 2026-03-04 22:32:11 +01:00
Zacharias-Brohn f6a45d6a21 test 2026-03-04 22:29:08 +01:00
Zacharias-Brohn e31fb851fa test 2026-03-04 22:28:25 +01:00
Zacharias-Brohn 62b623c16d test 2026-03-04 22:26:26 +01:00
Zacharias-Brohn 52cf956323 test 2026-03-04 22:25:12 +01:00
Zacharias-Brohn 52a9049abb oops lol 2026-03-04 20:08:24 +01:00
Zacharias-Brohn a2c9ad6e29 qalc 2026-03-04 20:06:58 +01:00
Zacharias-Brohn 5c428b211f launcher logout icon 2026-03-03 23:13:09 +01:00
Zacharias-Brohn 9c8c48c5ee window title width fixed 2026-03-03 19:27:45 +01:00
Zacharias-Brohn d6ce5af55c window title width fixed 2026-03-03 19:18:10 +01:00
Zacharias-Brohn fb1cc51eda resource widget is no longer broken 2026-03-02 23:16:52 +01:00
Zacharias-Brohn 6e967f8fd6 activewindow 2026-03-02 20:00:16 +01:00
Zacharias-Brohn e5595d8b5d traywidget background 2026-03-02 18:43:16 +01:00
Zacharias-Brohn 7451c52684 resource widget is broken lol 2026-03-02 17:09:39 +01:00
Zacharias-Brohn 9065d693ef stuff 2026-03-02 14:42:03 +01:00
Zacharias-Brohn f0afc7c75a remove console.log 2026-03-02 13:39:24 +01:00
Zacharias-Brohn 9040713231 workspace rework 2026-03-02 13:38:58 +01:00
Zacharias-Brohn cda00f91a3 bar height 2026-03-01 22:21:35 +01:00
Zach 749358dd5b Merge pull request #19 from Zacharias-Brohn/settingsWindow2
Settings window2
2026-03-01 18:13:23 +01:00
Zacharias-Brohn f989f74282 menu switch 2026-03-01 18:12:43 +01:00
Zach 9fd3f8fd9e Merge pull request #18 from Zacharias-Brohn/horizontal-media-widget
Horizontal media widget
2026-02-25 22:22:03 +01:00
Zach a8dd730808 Merge pull request #17 from Zacharias-Brohn/horizontal-media-widget
update
2026-02-25 22:14:19 +01:00
Zach dcfaa21e32 Merge pull request #15 from Zacharias-Brohn/libcava/cava_integration
cava reintroduced : uses libcava or cava for nix : nixos might need fixing…
2026-02-25 22:00:44 +01:00
Zach e124819dcb Merge pull request #16 from Zacharias-Brohn/horizontal-media-widget
Horizontal media widget
2026-02-25 21:59:57 +01:00
Aram Markarov 7861ba5c51 cava reintroduced : uses libcava or cava for nix : needs nixos fixing, but could be isolated issue 2026-02-25 19:14:58 +01:00
Zacharias-Brohn 6faebc986d ideas 2026-02-25 17:30:25 +01:00
55 changed files with 2454 additions and 341 deletions
+1
View File
@@ -1,3 +1,4 @@
./result/
.pyre/
.cache/
.venv/
+24
View File
@@ -0,0 +1,24 @@
import QtQuick
QtObject {
id: root
property real idx1: index
property int idx1Duration: 100
property real idx2: index
property int idx2Duration: 300
required property int index
Behavior on idx1 {
NumberAnimation {
duration: root.idx1Duration
easing.type: Easing.OutSine
}
}
Behavior on idx2 {
NumberAnimation {
duration: root.idx2Duration
easing.type: Easing.OutSine
}
}
}
+169
View File
@@ -0,0 +1,169 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import qs.Config
ComboBox {
id: root
property int cornerRadius: Appearance.rounding.normal
property int fieldHeight: 42
property bool filled: true
property real focusRingOpacity: 0.70
property int hPadding: 16
property int menuCornerRadius: 16
property int menuRowHeight: 46
property int menuVisibleRows: 7
property bool preferPopupWindow: false
hoverEnabled: true
implicitHeight: fieldHeight
implicitWidth: 240
spacing: 8
// ---------- Field background (filled/outlined + state layers + focus ring) ----------
background: Item {
anchors.fill: parent
CustomRect {
id: container
anchors.fill: parent
color: DynamicColors.palette.m3surfaceVariant
radius: root.cornerRadius
StateLayer {
}
}
}
// ---------- Content ----------
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: root.hPadding
anchors.rightMargin: root.hPadding
spacing: 12
// Display text
CustomText {
Layout.fillWidth: true
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pixelSize: 16
font.weight: Font.Medium
text: root.currentText
verticalAlignment: Text.AlignVCenter
}
// Indicator chevron (simple, replace with your icon system)
CustomText {
color: root.enabled ? DynamicColors.palette.m3onSurfaceVariant : DynamicColors.palette.m3onSurfaceVariant
rotation: root.popup.visible ? 180 : 0
text: "▾"
transformOrigin: Item.Center
verticalAlignment: Text.AlignVCenter
Behavior on rotation {
NumberAnimation {
duration: 140
easing.type: Easing.OutCubic
}
}
}
}
popup: Popup {
id: p
implicitHeight: list.contentItem.height + Appearance.padding.small * 2
implicitWidth: root.width
modal: true
popupType: root.preferPopupWindow ? Popup.Window : Popup.Item
y: -list.currentIndex * (root.menuRowHeight + Appearance.spacing.small) - Appearance.padding.small
background: CustomRect {
color: DynamicColors.palette.m3surface
radius: root.menuCornerRadius
}
contentItem: ListView {
id: list
anchors.bottomMargin: Appearance.padding.small
anchors.fill: parent
anchors.topMargin: Appearance.padding.small
clip: true
currentIndex: root.currentIndex
model: root.delegateModel
spacing: Appearance.spacing.small
delegate: CustomRect {
required property int index
required property var modelData
anchors.horizontalCenter: parent.horizontalCenter
color: (index === root.currentIndex) ? DynamicColors.palette.m3primary : "transparent"
implicitHeight: root.menuRowHeight
implicitWidth: p.implicitWidth - Appearance.padding.small * 2
radius: Appearance.rounding.normal - Appearance.padding.small
RowLayout {
anchors.fill: parent
spacing: 10
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurface
elide: Text.ElideRight
font.pixelSize: 15
text: modelData
verticalAlignment: Text.AlignVCenter
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: "✓"
verticalAlignment: Text.AlignVCenter
visible: index === root.currentIndex
}
}
StateLayer {
onClicked: {
root.currentIndex = index;
p.close();
}
}
}
}
// Expressive-ish open/close motion: subtle scale+fade (tune to taste). :contentReference[oaicite:5]{index=5}
enter: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
Anim {
from: 0.98
property: "scale"
to: 1.0
}
}
exit: Transition {
Anim {
from: 1
property: "opacity"
to: 0
}
}
Elevation {
anchors.fill: parent
level: 2
radius: root.menuCornerRadius
z: -1
}
}
}
+159
View File
@@ -0,0 +1,159 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
Row {
id: root
enum Type {
Filled,
Tonal
}
property alias active: menu.active
property color color: type == CustomSplitButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondaryContainer
property bool disabled
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledTextColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property alias expanded: menu.expanded
property string fallbackIcon
property string fallbackText
property real horizontalPadding: Appearance.padding.normal
property alias iconLabel: iconLabel
property alias label: label
property alias menu: menu
property alias menuItems: menu.items
property bool menuOnTop
property alias stateLayer: stateLayer
property color textColor: type == CustomSplitButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer
property int type: CustomSplitButton.Filled
property real verticalPadding: Appearance.padding.smaller
spacing: Math.floor(Appearance.spacing.small / 2)
CustomRect {
bottomRightRadius: Appearance.rounding.small / 2
color: root.disabled ? root.disabledColor : root.color
implicitHeight: expandBtn.implicitHeight
implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2
radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
topRightRadius: Appearance.rounding.small / 2
StateLayer {
id: stateLayer
function onClicked(): void {
root.active?.clicked();
}
color: root.textColor
disabled: root.disabled
rect.bottomRightRadius: parent.bottomRightRadius
rect.topRightRadius: parent.topRightRadius
}
RowLayout {
id: textRow
anchors.centerIn: parent
anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4)
spacing: Appearance.spacing.small
MaterialIcon {
id: iconLabel
Layout.alignment: Qt.AlignVCenter
animate: true
color: root.disabled ? root.disabledTextColor : root.textColor
fill: 1
text: root.active?.activeIcon ?? root.fallbackIcon
}
CustomText {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: implicitWidth
animate: true
clip: true
color: root.disabled ? root.disabledTextColor : root.textColor
text: root.active?.activeText ?? root.fallbackText
Behavior on Layout.preferredWidth {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
}
}
CustomRect {
id: expandBtn
property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2
bottomLeftRadius: rad
color: root.disabled ? root.disabledColor : root.color
implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2
implicitWidth: implicitHeight
radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
topLeftRadius: rad
Behavior on rad {
Anim {
}
}
StateLayer {
id: expandStateLayer
function onClicked(): void {
root.expanded = !root.expanded;
}
color: root.textColor
disabled: root.disabled
rect.bottomLeftRadius: parent.bottomLeftRadius
rect.topLeftRadius: parent.topLeftRadius
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)
color: root.disabled ? root.disabledTextColor : root.textColor
rotation: root.expanded ? 180 : 0
text: "expand_more"
Behavior on anchors.horizontalCenterOffset {
Anim {
}
}
Behavior on rotation {
Anim {
}
}
}
Menu {
id: menu
anchors.bottomMargin: Appearance.spacing.small
anchors.right: parent.right
anchors.top: parent.bottom
anchors.topMargin: Appearance.spacing.small
states: State {
when: root.menuOnTop
AnchorChanges {
anchors.bottom: expandBtn.top
anchors.top: undefined
target: menu
}
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Config
CustomRect {
id: root
property alias active: splitButton.active
property bool enabled: true
property alias expanded: splitButton.expanded
property int expandedZ: 100
required property string label
property alias menuItems: splitButton.menuItems
property alias type: splitButton.type
signal selected(item: MenuItem)
Layout.fillWidth: true
clip: false
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
opacity: enabled ? 1.0 : 0.5
radius: Appearance.rounding.normal
z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1
RowLayout {
id: row
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
CustomText {
Layout.fillWidth: true
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
text: root.label
}
CustomSplitButton {
id: splitButton
enabled: root.enabled
menu.z: 1
type: CustomSplitButton.Filled
menu.onItemSelected: item => {
root.selected(item);
}
stateLayer.onClicked: {
splitButton.expanded = !splitButton.expanded;
}
}
}
}
+12 -4
View File
@@ -13,11 +13,11 @@ Elevation {
signal itemSelected(item: MenuItem)
implicitHeight: root.expanded ? column.implicitHeight : 0
implicitHeight: root.expanded ? column.implicitHeight + Appearance.padding.small * 2 : 0
implicitWidth: Math.max(200, column.implicitWidth)
level: 2
opacity: root.expanded ? 1 : 0
radius: Appearance.rounding.small / 2
radius: Appearance.rounding.normal
Behavior on implicitHeight {
Anim {
@@ -41,7 +41,8 @@ Elevation {
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
anchors.verticalCenter: parent.verticalCenter
spacing: 5
Repeater {
model: root.items
@@ -54,10 +55,16 @@ Elevation {
required property MenuItem modelData
Layout.fillWidth: true
color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0)
implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2
implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2
CustomRect {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small
color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0)
radius: Appearance.rounding.normal - Appearance.padding.small
StateLayer {
function onClicked(): void {
root.itemSelected(item.modelData);
@@ -68,6 +75,7 @@ Elevation {
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
disabled: !root.expanded
}
}
RowLayout {
id: menuOptionRow
+17 -1
View File
@@ -15,6 +15,7 @@ Singleton {
property alias barConfig: adapter.barConfig
property alias colors: adapter.colors
property alias dashboard: adapter.dashboard
property alias dock: adapter.dock
property alias general: adapter.general
property alias launcher: adapter.launcher
property alias lock: adapter.lock
@@ -113,7 +114,8 @@ Singleton {
osd: serializeOsd(),
background: serializeBackground(),
launcher: serializeLauncher(),
colors: serializeColors()
colors: serializeColors(),
dock: serializeDock()
};
}
@@ -149,6 +151,17 @@ Singleton {
};
}
function serializeDock(): var {
return {
enable: dock.enable,
height: dock.height,
hoverRegionHeight: dock.hoverRegionHeight,
hoverToReveal: dock.hoverToReveal,
pinnedApps: dock.pinnedApps,
pinnedOnStartup: dock.pinnedOnStartup
};
}
function serializeGeneral(): var {
return {
logo: general.logo,
@@ -156,6 +169,7 @@ Singleton {
color: {
wallust: general.color.wallust,
mode: general.color.mode,
smart: general.color.smart,
schemeGeneration: general.color.schemeGeneration,
scheduleDarkStart: general.color.scheduleDarkStart,
scheduleDarkEnd: general.color.scheduleDarkEnd,
@@ -381,6 +395,8 @@ Singleton {
}
property DashboardConfig dashboard: DashboardConfig {
}
property DockConfig dock: DockConfig {
}
property General general: General {
}
property Launcher launcher: Launcher {
+10
View File
@@ -0,0 +1,10 @@
import Quickshell.Io
JsonObject {
property bool enable: false
property real height: 60
property real hoverRegionHeight: 2
property bool hoverToReveal: true
property list<string> pinnedApps: ["org.kde.dolphin", "kitty",]
property bool pinnedOnStartup: false
}
+6
View File
@@ -78,6 +78,12 @@ Singleton {
return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1);
}
function setMode(mode: string): void {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
Config.general.color.mode = mode;
Config.save();
}
FileView {
path: `${Paths.state}/scheme.json`
watchChanges: true
+1
View File
@@ -23,6 +23,7 @@ JsonObject {
property int scheduleDarkEnd: 0
property int scheduleDarkStart: 0
property bool schemeGeneration: true
property bool smart: false
property bool wallust: false
}
component Idle: JsonObject {
+26 -2
View File
@@ -6,11 +6,27 @@ JsonObject {
{
name: "Calculator",
icon: "calculate",
description: "Do simple math equations (powered by Qalc)",
description: "Do simple math equations",
command: ["autocomplete", "calc"],
enabled: true,
dangerous: false
},
{
name: "Light",
icon: "light_mode",
description: "Change to light mode",
command: ["setMode", "light"],
enabled: true,
dangerous: false
},
{
name: "Dark",
icon: "dark_mode",
description: "Change to dark mode",
command: ["setMode", "dark"],
enabled: true,
dangerous: false
},
{
name: "Wallpaper",
icon: "image",
@@ -19,6 +35,14 @@ JsonObject {
enabled: true,
dangerous: false
},
{
name: "Variant",
icon: "colors",
description: "Change the current scheme variant",
command: ["autocomplete", "variant"],
enabled: true,
dangerous: false
},
{
name: "Shutdown",
icon: "power_settings_new",
@@ -37,7 +61,7 @@ JsonObject {
},
{
name: "Logout",
icon: "exit_to_app",
icon: "logout",
description: "Log out of the current session",
command: ["loginctl", "terminate-user", ""],
enabled: true,
+8 -8
View File
@@ -12,7 +12,7 @@ import qs.Modules.Osd as Osd
import qs.Modules.Launcher as Launcher
import qs.Modules.Resources as Resources
// import qs.Modules.Settings as Settings
import qs.Modules.Settings as Settings
Shape {
id: root
@@ -85,11 +85,11 @@ Shape {
wrapper: root.panels.sidebar
}
// Settings.Background {
// id: settings
//
// startX: (root.width - wrapper.width) / 2 - rounding
// startY: 0
// wrapper: root.panels.settings
// }
Settings.Background {
id: settings
startX: (root.width - wrapper.width) / 2 - rounding
startY: 0
wrapper: root.panels.settings
}
}
+6 -4
View File
@@ -42,7 +42,7 @@ Variants {
regions: popoutRegions.instances
width: bar.width
x: 0
y: Config.barConfig.autoHide && !visibilities.bar ? 4 : 34
y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height
}
contentItem.Keys.onEscapePressed: {
@@ -62,7 +62,7 @@ Variants {
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "ZShell-Bar-Exclusion"
color: "transparent"
implicitHeight: 34
implicitHeight: backgroundRect.height
screen: bar.screen
anchors {
@@ -185,7 +185,7 @@ Variants {
anchors.top: parent.top
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? -30 : 0
color: "transparent"
implicitHeight: 34
implicitHeight: barLoader.implicitHeight
radius: 0
Behavior on anchors.topMargin {
@@ -200,7 +200,9 @@ Variants {
BarLoader {
id: barLoader
anchors.fill: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
bar: bar
popouts: panels.popouts
screen: scope.modelData
+6 -4
View File
@@ -223,12 +223,8 @@ CustomMouseArea {
root.utilitiesShortcutActive = false;
// Also hide dashboard and OSD if they're not being hovered
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inDashboardArea) {
root.visibilities.dashboard = false;
}
if (!inOsdArea) {
root.visibilities.osd = false;
root.panels.osd.hovered = false;
@@ -249,6 +245,12 @@ CustomMouseArea {
}
}
function onResourcesChanged() {
if (root.visibilities.resources && root.popouts.currentName.startsWith("audio")) {
root.popouts.hasCurrent = false;
}
}
function onSidebarChanged() {
if (root.visibilities.sidebar) {
root.visibilities.dashboard = false;
+10 -10
View File
@@ -10,7 +10,7 @@ import qs.Modules.Osd as Osd
import qs.Components.Toast as Toasts
import qs.Modules.Launcher as Launcher
import qs.Modules.Resources as Resources
// import qs.Modules.Settings as Settings
import qs.Modules.Settings as Settings
import qs.Config
Item {
@@ -24,7 +24,7 @@ Item {
readonly property alias popouts: popouts
readonly property alias resources: resources
required property ShellScreen screen
// readonly property alias settings: settings
readonly property alias settings: settings
readonly property alias sidebar: sidebar
readonly property alias toasts: toasts
readonly property alias utilities: utilities
@@ -127,12 +127,12 @@ Item {
visibilities: root.visibilities
}
// Settings.Wrapper {
// id: settings
//
// anchors.horizontalCenter: parent.horizontalCenter
// anchors.top: parent.top
// panels: root
// visibilities: root.visibilities
// }
Settings.Wrapper {
id: settings
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
panels: root
visibilities: root.visibilities
}
}
+70
View File
@@ -0,0 +1,70 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Components
import qs.Config
Singleton {
id: root
property list<var> apps: {
var map = new Map();
const pinnedApps = Config.dock?.pinnedApps ?? [];
for (const appId of pinnedApps) {
if (!map.has(appId.toLowerCase()))
map.set(appId.toLowerCase(), ({
pinned: true,
toplevels: []
}));
}
if (pinnedApps.length > 0) {
map.set("SEPARATOR", {
pinned: false,
toplevels: []
});
}
var values = [];
for (const [key, value] of map) {
values.push(appEntryComp.createObject(null, {
appId: key,
toplevels: value.toplevels,
pinned: value.pinned
}));
}
return values;
}
function isPinned(appId) {
return Config.dock.pinnedApps.indexOf(appId) !== -1;
}
function togglePin(appId) {
if (root.isPinned(appId)) {
Config.dock.pinnedApps = Config.dock.pinnedApps.filter(id => id !== appId);
} else {
Config.dock.pinnedApps = Config.dock.pinnedApps.concat([appId]);
}
}
Component {
id: appEntryComp
TaskbarAppEntry {
}
}
component TaskbarAppEntry: QtObject {
id: wrapper
required property string appId
required property bool pinned
required property list<var> toplevels
}
}
+2 -4
View File
@@ -19,22 +19,20 @@ Searcher {
function preview(path: string): void {
previewPath = path;
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${previewPath} --thumbnail-path ${Paths.cache}/imagecache/thumbnail.jpg --output ${Paths.state}/scheme.json --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]);
Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${previewPath} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]);
showPreview = true;
}
function setWallpaper(path: string): void {
actualCurrent = path;
WallpaperPath.currentWallpaperPath = path;
if (Config.general.color.wallust)
Wallust.generateColors(WallpaperPath.currentWallpaperPath);
Quickshell.execDetached(["sh", "-c", `zshell-cli wallpaper lockscreen --input-image=${root.actualCurrent} --output-path=${Paths.state}/lockscreen_bg.png --blur-amount=${Config.lock.blurAmount}`]);
}
function stopPreview(): void {
showPreview = false;
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${root.actualCurrent} --thumbnail-path ${Paths.cache}/imagecache/thumbnail.jpg --output ${Paths.state}/scheme.json --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]);
Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${root.actualCurrent} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]);
}
extraOpts: useFuzzy ? ({}) : ({
+1 -1
View File
@@ -29,7 +29,7 @@ Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
height: 22
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: height / 2
}
+2 -1
View File
@@ -52,7 +52,7 @@ RowLayout {
}
}
anchors.fill: parent
implicitHeight: 34
CustomShortcut {
name: "toggle-overview"
@@ -73,6 +73,7 @@ RowLayout {
Repeater {
id: repeater
// model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index)
model: Config.barConfig.entries
DelegateChooser {
+2 -3
View File
@@ -45,16 +45,15 @@ ShapePath {
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
relativeY: -(root.wrapper.height)
}
PathArc {
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
}
+28
View File
@@ -0,0 +1,28 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import qs.Config
Item {
id: root
readonly property int padding: Appearance.padding.small
required property var panels
readonly property int rounding: Appearance.rounding.large
required property PersistentProperties visibilities
implicitHeight: Config.dock.height + root.padding * 2
implicitWidth: dockRow.implicitWidth + root.padding * 2
RowLayout {
id: dockRow
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Appearance.spacing.small
}
}
+100
View File
@@ -0,0 +1,100 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Config
Item {
id: root
property int contentHeight
required property var panels
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.dock
required property PersistentProperties visibilities
implicitHeight: 0
implicitWidth: content.implicitWidth
visible: height > 0
onShouldBeActiveChanged: {
if (shouldBeActive) {
timer.stop();
hideAnim.stop();
showAnim.start();
} else {
showAnim.stop();
hideAnim.start();
}
}
SequentialAnimation {
id: showAnim
Anim {
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
property: "implicitHeight"
target: root
to: root.contentHeight
}
ScriptAction {
script: root.implicitHeight = Qt.binding(() => content.implicitHeight)
}
}
SequentialAnimation {
id: hideAnim
ScriptAction {
script: root.implicitHeight = root.implicitHeight
}
Anim {
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
property: "implicitHeight"
target: root
to: 0
}
}
Timer {
id: timer
interval: Appearance.anim.durations.small
onRunningChanged: {
if (running && !root.shouldBeActive) {
content.visible = false;
content.active = true;
} else {
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
content.visible = true;
if (showAnim.running) {
showAnim.stop();
showAnim.start();
}
}
}
}
Loader {
id: content
active: false
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
visible: false
sourceComponent: Content {
panels: root.panels
visibilities: root.visibilities
Component.onCompleted: root.contentHeight = implicitHeight
}
Component.onCompleted: timer.start()
}
}
+16
View File
@@ -131,6 +131,14 @@ CustomListView {
model.values: [0]
root.delegate: calcItem
}
},
State {
name: "variant"
PropertyChanges {
model.values: SchemeVariants.query(search.text)
root.delegate: variantItem
}
}
]
transitions: Transition {
@@ -211,4 +219,12 @@ CustomListView {
list: root
}
}
Component {
id: variantItem
VariantItem {
list: root
}
}
}
-5
View File
@@ -123,11 +123,6 @@ Item {
search.text = "";
}
function onSessionChanged(): void {
if (!root.visibilities.session)
search.forceActiveFocus();
}
target: root.visibilities
}
}
+74
View File
@@ -0,0 +1,74 @@
import QtQuick
import qs.Components
import qs.Modules.Launcher.Services
import qs.Config
Item {
id: root
required property var list
required property SchemeVariants.Variant modelData
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
function onClicked(): void {
root.modelData?.onClicked(root.list);
}
radius: Appearance.rounding.normal
}
Item {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.larger
anchors.margins: Appearance.padding.smaller
anchors.rightMargin: Appearance.padding.larger
MaterialIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Appearance.font.size.extraLarge
text: root.modelData?.icon ?? ""
}
Column {
anchors.left: icon.right
anchors.leftMargin: Appearance.spacing.larger
anchors.verticalCenter: icon.verticalCenter
spacing: 0
width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0)
CustomText {
font.pointSize: Appearance.font.size.normal
text: root.modelData?.name ?? ""
}
CustomText {
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: root.modelData?.description ?? ""
}
}
Loader {
id: current
active: root.modelData?.variant === Config.colors.schemeType
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: "check"
}
}
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ Searcher {
list.search.text = `${Config.launcher.actionPrefix}${command[1]} `;
} else if (command[0] === "setMode" && command.length > 1) {
list.visibilities.launcher = false;
Colours.setMode(command[1]);
DynamicColors.setMode(command[1]);
} else {
list.visibilities.launcher = false;
Quickshell.execDetached(command);
@@ -0,0 +1,88 @@
pragma Singleton
import Quickshell
import QtQuick
import qs.Modules.Launcher
import qs.Config
import qs.Helpers
Searcher {
id: root
function transformSearch(search: string): string {
return search.slice(`${Config.launcher.actionPrefix}variant `.length);
}
useFuzzy: Config.launcher.useFuzzy.variants
list: [
Variant {
description: qsTr("Maximum chroma at each tone.")
icon: "sentiment_very_dissatisfied"
name: qsTr("Vibrant")
variant: "vibrant"
},
Variant {
description: qsTr("Pastel palette with a low chroma.")
icon: "android"
name: qsTr("Tonal Spot")
variant: "tonalspot"
},
Variant {
description: qsTr("Hue-shifted, artistic or playful colors.")
icon: "compare_arrows"
name: qsTr("Expressive")
variant: "expressive"
},
Variant {
description: qsTr("Preserve source color exactly.")
icon: "compare"
name: qsTr("Fidelity")
variant: "fidelity"
},
Variant {
description: qsTr("Almost identical to fidelity.")
icon: "sentiment_calm"
name: qsTr("Content")
variant: "content"
},
Variant {
description: qsTr("The seed colour's hue does not appear in the theme.")
icon: "nutrition"
name: qsTr("Fruit Salad")
variant: "fruit-salad"
},
Variant {
description: qsTr("Like Fruit Salad but different hues.")
icon: "looks"
name: qsTr("Rainbow")
variant: "rainbow"
},
Variant {
description: qsTr("Close to grayscale, a hint of chroma.")
icon: "contrast"
name: qsTr("Neutral")
variant: "neutral"
},
Variant {
description: qsTr("All colours are grayscale, no chroma.")
icon: "filter_b_and_w"
name: qsTr("Monochrome")
variant: "monochrome"
}
]
component Variant: QtObject {
required property string description
required property string icon
required property string name
required property string variant
function onClicked(list: AppList): void {
list.visibilities.launcher = false;
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]);
Config.colors.schemeType = variant;
Config.save();
}
}
}
+4 -2
View File
@@ -10,8 +10,10 @@ Item {
property int contentHeight
readonly property real maxHeight: {
let max = screen.height - Appearance.spacing.large;
if (visibilities.dashboard)
let max = screen.height - Appearance.spacing.large * 2;
if (visibilities.resources)
max -= panels.resources.nonAnimHeight;
if (visibilities.dashboard && panels.dashboard.x < root.x + root.implicitWidth)
max -= panels.dashboard.nonAnimHeight;
return max;
}
+15
View File
@@ -164,6 +164,21 @@ WlSessionLockSurface {
implicitWidth: size
scale: 0
// MultiEffect {
// anchors.fill: lockBg
// autoPaddingEnabled: false
// blur: 1
// blurEnabled: true
// blurMax: 64
// maskEnabled: true
// maskSource: lockBg
//
// source: ShaderEffectSource {
// sourceItem: background
// sourceRect: Qt.rect(lockBg.x, lockBg.y, lockBg.width, lockBg, height)
// }
// }
CustomRect {
id: lockBg
+1 -1
View File
@@ -25,7 +25,7 @@ Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 22
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: Appearance.rounding.full
}
+116
View File
@@ -0,0 +1,116 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import qs.Components
import qs.Config
Item {
id: root
property color borderColor: warning ? DynamicColors.palette.m3onError : mainColor
required property color mainColor
required property double percentage
property bool shown: true
property color usageColor: warning ? DynamicColors.palette.m3error : mainColor
property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 100
clip: true
implicitHeight: 22
implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth
visible: width > 0 && height > 0
Behavior on percentage {
NumberAnimation {
duration: 300
easing.type: Easing.InOutQuad
}
}
RowLayout {
id: resourceRowLayout
spacing: 2
x: shown ? 0 : -resourceRowLayout.width
anchors {
verticalCenter: parent.verticalCenter
}
Item {
Layout.alignment: Qt.AlignVCenter
implicitHeight: root.implicitHeight
implicitWidth: 14
Rectangle {
id: backgroundCircle
anchors.centerIn: parent
border.color: "#404040"
border.width: 1
color: "#40000000"
height: 14
radius: height / 2
width: 14
}
Shape {
anchors.fill: backgroundCircle
preferredRendererType: Shape.CurveRenderer
smooth: true
ShapePath {
fillColor: root.usageColor
startX: backgroundCircle.width / 2
startY: backgroundCircle.height / 2
strokeWidth: 0
Behavior on fillColor {
CAnim {
}
}
PathLine {
x: backgroundCircle.width / 2
y: 0 + (1 / 2)
}
PathAngleArc {
centerX: backgroundCircle.width / 2
centerY: backgroundCircle.height / 2
radiusX: backgroundCircle.width / 2 - (1 / 2)
radiusY: backgroundCircle.height / 2 - (1 / 2)
startAngle: -90
sweepAngle: 360 * root.percentage
}
PathLine {
x: backgroundCircle.width / 2
y: backgroundCircle.height / 2
}
}
ShapePath {
capStyle: ShapePath.FlatCap
fillColor: "transparent"
strokeColor: root.borderColor
strokeWidth: 1
Behavior on strokeColor {
CAnim {
}
}
PathAngleArc {
centerX: backgroundCircle.width / 2
centerY: backgroundCircle.height / 2
radiusX: backgroundCircle.width / 2 - (1 / 2)
radiusY: backgroundCircle.height / 2 - (1 / 2)
startAngle: -90
sweepAngle: 360 * root.percentage
}
}
}
}
}
}
+56 -79
View File
@@ -7,110 +7,87 @@ import qs.Config
Item {
id: root
property color borderColor: warning ? DynamicColors.palette.m3onError : mainColor
property color accentColor: warning ? DynamicColors.palette.m3error : mainColor
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property string icon
required property color mainColor
required property double percentage
property bool shown: true
property color usageColor: warning ? DynamicColors.palette.m3error : mainColor
property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 100
property int warningThreshold: 80
clip: true
implicitHeight: 22
implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth
height: implicitHeight
implicitHeight: 34
implicitWidth: 34
percentage: 0
visible: width > 0 && height > 0
width: implicitWidth
Behavior on percentage {
NumberAnimation {
duration: 300
easing.type: Easing.InOutQuad
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
RowLayout {
id: resourceRowLayout
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
spacing: 2
x: shown ? 0 : -resourceRowLayout.width
anchors {
verticalCenter: parent.verticalCenter
}
Item {
Layout.alignment: Qt.AlignVCenter
implicitHeight: root.implicitHeight
implicitWidth: 14
Rectangle {
id: backgroundCircle
Canvas {
id: gaugeCanvas
anchors.centerIn: parent
border.color: "#404040"
border.width: 1
color: "#40000000"
height: 14
radius: height / 2
width: 14
}
height: width
width: Math.min(parent.width, parent.height)
Shape {
anchors.fill: backgroundCircle
preferredRendererType: Shape.CurveRenderer
smooth: true
ShapePath {
fillColor: root.usageColor
startX: backgroundCircle.width / 2
startY: backgroundCircle.height / 2
strokeWidth: 0
Behavior on fillColor {
CAnim {
Component.onCompleted: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = (height / 2) + 1;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (root.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = root.accentColor;
ctx.stroke();
}
}
PathLine {
x: backgroundCircle.width / 2
y: 0 + (1 / 2)
Connections {
function onAnimatedPercentageChanged() {
gaugeCanvas.requestPaint();
}
PathAngleArc {
centerX: backgroundCircle.width / 2
centerY: backgroundCircle.height / 2
radiusX: backgroundCircle.width / 2 - (1 / 2)
radiusY: backgroundCircle.height / 2 - (1 / 2)
startAngle: -90
sweepAngle: 360 * root.percentage
target: root
}
PathLine {
x: backgroundCircle.width / 2
y: backgroundCircle.height / 2
Connections {
function onPaletteChanged() {
gaugeCanvas.requestPaint();
}
target: DynamicColors
}
}
ShapePath {
capStyle: ShapePath.FlatCap
fillColor: "transparent"
strokeColor: root.borderColor
strokeWidth: 1
Behavior on strokeColor {
CAnim {
}
}
PathAngleArc {
centerX: backgroundCircle.width / 2
centerY: backgroundCircle.height / 2
radiusX: backgroundCircle.width / 2 - (1 / 2)
radiusY: backgroundCircle.height / 2 - (1 / 2)
startAngle: -90
sweepAngle: 360 * root.percentage
}
}
}
}
MaterialIcon {
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurface
font.pointSize: 12
text: root.icon
}
}
+15 -35
View File
@@ -3,10 +3,9 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Layouts
import Quickshell.Wayland
import qs.Helpers
import qs.Modules
import qs.Config
import qs.Effects
import qs.Components
Item {
@@ -14,15 +13,16 @@ Item {
required property PersistentProperties visibilities
anchors.bottom: parent.bottom
anchors.top: parent.top
clip: true
implicitHeight: 34
implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2
CustomRect {
id: backgroundRect
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 22
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: height / 2
anchors {
@@ -40,57 +40,37 @@ Item {
id: rowLayout
anchors.centerIn: parent
spacing: 6
spacing: 0
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: DynamicColors.palette.m3onSurface
font.pointSize: 14
text: "memory_alt"
Ref {
service: SystemUsage
}
Resource {
Layout.alignment: Qt.AlignVCenter
icon: "memory"
mainColor: DynamicColors.palette.m3primary
percentage: ResourceUsage.memoryUsedPercentage
percentage: SystemUsage.cpuPerc
warningThreshold: 95
}
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: DynamicColors.palette.m3onSurface
font.pointSize: 14
text: "memory"
}
Resource {
icon: "memory_alt"
mainColor: DynamicColors.palette.m3secondary
percentage: ResourceUsage.cpuUsage
percentage: SystemUsage.memPerc
warningThreshold: 80
}
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: DynamicColors.palette.m3onSurface
font.pointSize: 14
text: "gamepad"
}
Resource {
icon: "gamepad"
mainColor: DynamicColors.palette.m3tertiary
percentage: ResourceUsage.gpuUsage
}
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: DynamicColors.palette.m3onSurface
font.pointSize: 14
text: "developer_board"
percentage: SystemUsage.gpuPerc
}
Resource {
icon: "developer_board"
mainColor: DynamicColors.palette.m3primary
percentage: ResourceUsage.gpuMemUsage
percentage: SystemUsage.gpuMemUsed
}
}
}
+2 -3
View File
@@ -28,15 +28,14 @@ ShapePath {
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
relativeY: root.wrapper.height
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
relativeY: -root.roundingY
}
PathLine {
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: 8
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.roundingY, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+178
View File
@@ -0,0 +1,178 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config
import qs.Helpers
Item {
id: root
required property Item content
implicitHeight: clayout.contentHeight + Appearance.padding.smaller * 2
implicitWidth: clayout.contentWidth + Appearance.padding.smaller * 2
ListModel {
id: listModel
ListElement {
icon: "settings"
name: "General"
}
ListElement {
icon: "wallpaper"
name: "Wallpaper"
}
ListElement {
icon: "settop_component"
name: "Bar"
}
ListElement {
icon: "lock"
name: "Lockscreen"
}
ListElement {
icon: "build_circle"
name: "Services"
}
ListElement {
icon: "notifications"
name: "Notifications"
}
ListElement {
icon: "view_sidebar"
name: "Sidebar"
}
ListElement {
icon: "handyman"
name: "Utilities"
}
ListElement {
icon: "dashboard"
name: "Dashboard"
}
ListElement {
icon: "colors"
name: "Appearance"
}
ListElement {
icon: "display_settings"
name: "On screen display"
}
ListElement {
icon: "rocket_launch"
name: "Launcher"
}
ListElement {
icon: "colors"
name: "Colors"
}
}
CustomRect {
anchors.fill: parent
color: DynamicColors.tPalette.m3surfaceContainer
radius: 4
CustomListView {
id: clayout
anchors.centerIn: parent
contentHeight: contentItem.childrenRect.height
contentWidth: contentItem.childrenRect.width
highlightFollowsCurrentItem: false
implicitHeight: contentItem.childrenRect.height
implicitWidth: contentItem.childrenRect.width
model: listModel
spacing: 5
delegate: Category {
}
highlight: CustomRect {
color: DynamicColors.palette.m3primary
implicitHeight: clayout.currentItem?.implicitHeight ?? 0
implicitWidth: clayout.width
radius: 4
y: clayout.currentItem?.y ?? 0
Behavior on y {
Anim {
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
}
}
}
}
}
component Category: CustomRect {
id: categoryItem
required property string icon
required property int index
required property string name
implicitHeight: 42
implicitWidth: 200
radius: 4
RowLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
id: icon
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillHeight: true
Layout.preferredWidth: icon.contentWidth
color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
font.pointSize: 22
text: categoryItem.icon
verticalAlignment: Text.AlignVCenter
}
CustomText {
id: text
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.normal
color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
text: categoryItem.name
verticalAlignment: Text.AlignVCenter
}
}
StateLayer {
id: layer
onClicked: {
root.content.currentCategory = categoryItem.name.toLowerCase();
clayout.currentIndex = categoryItem.index;
}
}
}
}
@@ -0,0 +1,97 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Modules.Settings.Controls
import qs.Config
import qs.Helpers
CustomRect {
id: root
ColumnLayout {
id: clayout
anchors.left: parent.left
anchors.right: parent.right
CustomRect {
Layout.fillWidth: true
Layout.preferredHeight: colorLayout.implicitHeight
color: DynamicColors.tPalette.m3surfaceContainer
ColumnLayout {
id: colorLayout
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.right: parent.right
Settings {
name: "smth"
}
SettingSwitch {
name: "wallust"
object: Config.general.color
setting: "wallust"
}
CustomSplitButtonRow {
enabled: true
label: qsTr("Scheme mode")
menuItems: [
MenuItem {
property string val: "light"
icon: "light_mode"
text: qsTr("Light")
},
MenuItem {
property string val: "dark"
icon: "dark_mode"
text: qsTr("Dark")
}
]
Component.onCompleted: {
if (Config.general.color.mode === "light")
active = menuItems[0];
else
active = menuItems[1];
}
onSelected: item => {
Config.general.color.mode = item.val;
Config.save();
}
}
}
}
}
component Settings: CustomRect {
id: settingsItem
required property string name
Layout.preferredHeight: 42
Layout.preferredWidth: 200
radius: 4
CustomText {
id: text
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
font.bold: true
font.pointSize: 32
text: settingsItem.name
verticalAlignment: Text.AlignVCenter
}
}
}
@@ -0,0 +1,13 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config
import qs.Helpers
CustomRect {
id: root
}
+55
View File
@@ -0,0 +1,55 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config
import qs.Helpers
CustomRect {
id: root
ColumnLayout {
id: clayout
anchors.fill: parent
Settings {
name: "apps"
}
Item {
}
}
component Settings: CustomRect {
id: settingsItem
required property string name
implicitHeight: 42
implicitWidth: 200
radius: 4
RowLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
CustomText {
id: text
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.normal
text: settingsItem.name
verticalAlignment: Text.AlignVCenter
}
}
}
}
+102
View File
@@ -0,0 +1,102 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import qs.Components
import qs.Modules as Modules
import qs.Modules.Settings.Categories as Cat
import qs.Config
import qs.Helpers
Item {
id: root
property string currentCategory: "general"
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + 500 + viewWrapper.anchors.margins * 2
required property PersistentProperties visibilities
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
Connections {
function onCurrentCategoryChanged() {
stack.pop();
if (currentCategory === "general") {
stack.push(general);
} else if (currentCategory === "wallpaper") {
stack.push(background);
} else if (currentCategory === "appearance") {
stack.push(appearance);
}
}
target: root
}
ClippingRectangle {
id: viewWrapper
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
color: "transparent"
Item {
id: view
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
Categories {
id: layout
anchors.fill: parent
content: root
}
}
CustomClippingRect {
id: categoryContent
anchors.bottom: parent.bottom
anchors.left: view.right
anchors.leftMargin: Appearance.spacing.smaller
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
radius: 4
StackView {
id: stack
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
initialItem: general
}
}
}
Component {
id: general
Cat.General {
}
}
Component {
id: background
Cat.Background {
}
}
Component {
id: appearance
Cat.Appearance {
}
}
}
@@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
RowLayout {
id: root
required property string name
required property var object
required property string setting
Layout.fillWidth: true
Layout.preferredHeight: 42
CustomText {
id: text
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
font.pointSize: 16
text: root.name
}
CustomSwitch {
id: cswitch
Layout.alignment: Qt.AlignRight
checked: root.object[root.setting]
onToggled: {
root.object[root.setting] = checked;
Config.save();
}
}
}
+61
View File
@@ -0,0 +1,61 @@
import Quickshell
import QtQuick
import qs.Components
import qs.Config
import qs.Helpers
Item {
id: root
required property var panels
required property PersistentProperties visibilities
implicitHeight: 0
implicitWidth: content.implicitWidth
visible: height > 0
states: State {
name: "visible"
when: root.visibilities.settings
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
},
Transition {
from: "visible"
to: ""
Anim {
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
}
]
Loader {
id: content
active: true
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
visible: true
sourceComponent: Content {
visibilities: root.visibilities
}
}
}
+20 -1
View File
@@ -4,8 +4,10 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.SystemTray
import qs.Components
import qs.Config
Row {
Item {
id: root
required property PanelWindow bar
@@ -15,6 +17,21 @@ Row {
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitHeight: 34
implicitWidth: row.width + Appearance.padding.small * 2
CustomClippingRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: height / 2
Row {
id: row
anchors.centerIn: parent
spacing: 0
Repeater {
@@ -38,3 +55,5 @@ Row {
}
}
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 22
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: height / 2
}
+10 -16
View File
@@ -11,17 +11,19 @@ Item {
required property var bar
property color colour: DynamicColors.palette.m3primary
property Title current: text1
readonly property int maxHeight: {
const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer");
const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0);
// Length - 2 cause repeater counts as a child
return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2;
// readonly property int maxWidth: 300
readonly property int maxWidth: {
const otherModules = bar.children.filter(c => c.enabled && c.id && c.item !== this && c.id !== "spacer");
const otherWidth = otherModules.reduce((acc, curr) => {
return acc + (curr.item?.nonAnimWidth ?? curr.width ?? 0);
}, 0);
return bar.width - otherWidth - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2;
}
required property Brightness.Monitor monitor
clip: true
implicitHeight: current.implicitHeight
implicitWidth: current.implicitWidth + current.anchors.leftMargin
implicitWidth: Math.min(current.implicitWidth, root.maxWidth)
Behavior on implicitWidth {
Anim {
@@ -30,16 +32,6 @@ Item {
}
}
// MaterialIcon {
// id: icon
//
// anchors.verticalCenter: parent.verticalCenter
//
// animate: true
// text: Icons.getAppCategoryIcon(Hypr.activeToplevel?.lastIpcObject.class, "desktop_windows")
// color: root.colour
// }
Title {
id: text1
@@ -53,6 +45,8 @@ Item {
TextMetrics {
id: metrics
elide: Qt.ElideRight
elideWidth: root.maxWidth
font.family: "Rubik"
font.pointSize: 12
text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
+156 -87
View File
@@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
@@ -10,119 +9,189 @@ import qs.Config
import qs.Components
Item {
id: itemRoot
id: root
property real activeWorkspaceMargin: Math.ceil(Appearance.padding.small / 2)
required property PanelWindow bar
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.bar.screen)
property int workspaceButtonWidth: bgRect.implicitHeight - root.activeWorkspaceMargin * 2
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
readonly property int workspacesShown: workspaces.length
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: workspacesRow.implicitWidth + 10
implicitWidth: (root.workspaceButtonWidth * root.workspacesShown) + root.activeWorkspaceMargin * 2
Behavior on implicitWidth {
NumberAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
Anim {
}
}
Rectangle {
id: root
property HyprlandMonitor monitor: Hyprland.monitorFor(itemRoot.bar?.screen)
function shouldShow(monitor) {
Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors();
if (monitor === root.monitor) {
return true;
} else {
return false;
}
}
CustomRect {
id: bgRect
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 22
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: height / 2
Behavior on color {
CAnim {
}
}
RowLayout {
id: workspacesRow
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
spacing: 8
Repeater {
model: Hyprland.workspaces
RowLayout {
id: workspaceIndicator
required property var modelData
visible: root.shouldShow(workspaceIndicator.modelData.monitor)
CustomText {
color: workspaceIndicator.modelData.id === Hyprland.focusedWorkspace.id ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 12
text: workspaceIndicator.modelData.name
visible: true
}
Rectangle {
border.color: workspaceIndicator.modelData.id === Hyprland.focusedWorkspace.id ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
border.width: 1
color: "transparent"
implicitHeight: 14
implicitWidth: 14
opacity: 1.0
radius: height / 2
scale: 1.0
Behavior on border.color {
ColorAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
NumberAnimation on opacity {
duration: 200
from: 0.0
to: 1.0
}
NumberAnimation on scale {
duration: 300
easing.type: Easing.OutBack
from: 0.0
to: 1.0
}
CustomRect {
anchors.centerIn: parent
color: workspaceIndicator.modelData.id === Hyprland.focusedWorkspace.id ? DynamicColors.palette.m3primary : "transparent"
implicitHeight: 8
implicitWidth: 8
radius: implicitHeight / 2
id: indicator
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
property real indicatorThickness: root.workspaceButtonWidth
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3primary
implicitHeight: indicatorThickness
implicitWidth: indicatorLength
radius: Appearance.rounding.full
x: indicatorPosition
z: 2
AnimatedTabIndexPair {
id: idxPair
index: root.workspaces.findIndex(w => w.active)
}
}
MouseArea {
Grid {
anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin
columnSpacing: 0
columns: root.workspacesShown
rowSpacing: 0
rows: 1
z: 3
onClicked: {
workspaceIndicator.modelData.activate();
Repeater {
model: root.workspaces
Button {
id: button
required property int index
required property HyprlandWorkspace modelData
implicitHeight: indicator.indicatorThickness
implicitWidth: indicator.indicatorThickness
width: root.workspaceButtonWidth
background: Item {
id: workspaceButtonBackground
implicitHeight: root.workspaceButtonWidth
implicitWidth: root.workspaceButtonWidth
CustomText {
anchors.centerIn: parent
color: DynamicColors.palette.m3onSecondaryContainer
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: button.modelData.name
verticalAlignment: Text.AlignVCenter
z: 3
}
}
onPressed: {
Hyprland.dispatch(`workspace ${button.modelData.name}`);
}
}
}
}
Item {
id: activeTextSource
anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin
layer.enabled: true
visible: false
z: 4
Grid {
anchors.fill: parent
columnSpacing: 0
columns: root.workspacesShown
rowSpacing: 0
rows: 1
Repeater {
model: root.workspaces
Item {
id: activeWorkspace
required property int index
required property HyprlandWorkspace modelData
implicitHeight: indicator.indicatorThickness
implicitWidth: indicator.indicatorThickness
width: root.workspaceButtonWidth
CustomText {
anchors.centerIn: parent
color: DynamicColors.palette.m3onPrimary
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: activeWorkspace.modelData.name
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
ShaderEffectSource {
id: activeTextTex
anchors.fill: bgRect
anchors.margins: root.activeWorkspaceMargin
hideSource: true
live: true
recursive: true
sourceItem: activeTextSource
}
Item {
id: indicatorMask
anchors.fill: bgRect
visible: false
CustomRect {
color: "white"
height: indicator.height
radius: indicator.radius
width: indicator.width
x: indicator.x
y: indicator.y
}
}
ShaderEffectSource {
id: indicatorMaskEffect
anchors.fill: activeTextSource
live: true
sourceItem: indicatorMask
visible: false
}
MultiEffect {
anchors.fill: activeTextSource
maskEnabled: true
maskInverted: false
maskSource: indicatorMaskEffect
source: activeTextTex
z: 5
}
}
}
+2
View File
@@ -43,11 +43,13 @@ qml_module(ZShell
imageanalyser.hpp imageanalyser.cpp
requests.hpp requests.cpp
toaster.hpp toaster.cpp
qalculator.hpp qalculator.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Sql
PkgConfig::Qalculate
)
add_subdirectory(Models)
+52
View File
@@ -0,0 +1,52 @@
#include "qalculator.hpp"
#include <libqalculate/qalculate.h>
namespace ZShell {
Qalculator::Qalculator(QObject* parent)
: QObject(parent) {
if (!CALCULATOR) {
new Calculator();
CALCULATOR->loadExchangeRates();
CALCULATOR->loadGlobalDefinitions();
CALCULATOR->loadLocalDefinitions();
}
}
QString Qalculator::eval(const QString& expr, bool printExpr) const {
if (expr.isEmpty()) {
return QString();
}
EvaluationOptions eo;
PrintOptions po;
std::string parsed;
std::string result = CALCULATOR->calculateAndPrint(
CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed);
std::string error;
while (CALCULATOR->message()) {
if (!CALCULATOR->message()->message().empty()) {
if (CALCULATOR->message()->type() == MESSAGE_ERROR) {
error += "error: ";
} else if (CALCULATOR->message()->type() == MESSAGE_WARNING) {
error += "warning: ";
}
error += CALCULATOR->message()->message();
}
CALCULATOR->nextMessage();
}
if (!error.empty()) {
return QString::fromStdString(error);
}
if (printExpr) {
return QString("%1 = %2").arg(parsed).arg(result);
}
return QString::fromStdString(result);
}
} // namespace ZShell
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell {
class Qalculator : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
explicit Qalculator(QObject* parent = nullptr);
Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const;
};
} // namespace ZShell
+2
View File
@@ -43,6 +43,8 @@ This installs the QML plugin to `/usr/lib/qt6/qml`.
### NixOS
**Note that not all features work well. This is due to limited testing on NixOS.**
In your flake.nix file, add the following in your inputs.
```nix
+381 -40
View File
@@ -1,7 +1,10 @@
from typing import Annotated, Optional
import typer
import json
import shutil
import os
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Undefined
from typing import Any, Optional, Tuple
from zshell.utils.schemepalettes import PRESETS
from pathlib import Path
from PIL import Image
@@ -9,6 +12,8 @@ from materialyoucolor.quantize import QuantizeCelebi
from materialyoucolor.score.score import Score
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
from materialyoucolor.hct.hct import Hct
from materialyoucolor.utils.color_utils import argb_from_rgb
from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
app = typer.Typer()
@@ -18,47 +23,179 @@ def generate(
# image inputs (optional - used for image mode)
image_path: Optional[Path] = typer.Option(
None, help="Path to source image. Required for image mode."),
thumbnail_path: Optional[Path] = typer.Option(
Path("thumb.jpg"), help="Path to temporary thumbnail (image mode)."),
scheme: Optional[str] = typer.Option(
"fruit-salad", help="Color scheme algorithm to use for image mode. Ignored in preset mode."),
None, help="Color scheme algorithm to use for image mode. Ignored in preset mode."),
# preset inputs (optional - used for preset mode)
preset: Optional[str] = typer.Option(
None, help="Name of a premade scheme in this format: <preset_name>:<preset_flavor>"),
mode: str = typer.Option(
"dark", help="Mode of the preset scheme (dark or light)."),
# output (required)
output: Path = typer.Option(..., help="Output JSON path.")
mode: Optional[str] = typer.Option(
None, help="Mode of the preset scheme (dark or light)."),
):
if preset is None and image_path is None:
raise typer.BadParameter(
"Either --image-path or --preset must be provided.")
HOME = str(os.getenv("HOME"))
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
THUMB_PATH = Path(HOME +
"/.cache/zshell/imagecache/thumbnail.jpg")
WALL_DIR_PATH = Path(HOME +
"/.local/state/zshell/wallpaper_path.json")
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
WALL_PATH = Path()
CONFIG = Path(HOME + "/.config/zshell/config.json")
if preset is not None and image_path is not None:
raise typer.BadParameter(
"Use either --image-path or --preset, not both.")
match scheme:
def get_scheme_class(scheme_name: str):
match scheme_name:
case "fruit-salad":
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
case 'expressive':
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive as Scheme
case 'monochrome':
from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome as Scheme
case 'rainbow':
from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow as Scheme
case 'tonal-spot':
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme
case 'neutral':
from materialyoucolor.scheme.scheme_neutral import SchemeNeutral as Scheme
case 'fidelity':
from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity as Scheme
case 'content':
from materialyoucolor.scheme.scheme_content import SchemeContent as Scheme
case 'vibrant':
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant as Scheme
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad
return SchemeFruitSalad
case "expressive":
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive
return SchemeExpressive
case "monochrome":
from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome
return SchemeMonochrome
case "rainbow":
from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow
return SchemeRainbow
case "tonal-spot":
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
return SchemeTonalSpot
case "neutral":
from materialyoucolor.scheme.scheme_neutral import SchemeNeutral
return SchemeNeutral
case "fidelity":
from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity
return SchemeFidelity
case "content":
from materialyoucolor.scheme.scheme_content import SchemeContent
return SchemeContent
case "vibrant":
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant
return SchemeVibrant
case _:
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad
return SchemeFruitSalad
def hex_to_hct(hex_color: str) -> Hct:
s = hex_color.strip()
if s.startswith("#"):
s = s[1:]
if len(s) != 6:
raise ValueError(f"Expected 6-digit hex color, got: {hex_color!r}")
return Hct.from_int(int("0xFF" + s, 16))
LIGHT_GRUVBOX = list(
map(
hex_to_hct,
[
"FDF9F3",
"FF6188",
"A9DC76",
"FC9867",
"FFD866",
"F47FD4",
"78DCE8",
"333034",
"121212",
"FF6188",
"A9DC76",
"FC9867",
"FFD866",
"F47FD4",
"78DCE8",
"333034",
],
)
)
DARK_GRUVBOX = list(
map(
hex_to_hct,
[
"282828",
"CC241D",
"98971A",
"D79921",
"458588",
"B16286",
"689D6A",
"A89984",
"928374",
"FB4934",
"B8BB26",
"FABD2F",
"83A598",
"D3869B",
"8EC07C",
"EBDBB2",
],
)
)
with WALL_DIR_PATH.open() as f:
path = json.load(f)["currentWallpaperPath"]
WALL_PATH = path
def lighten(color: Hct, amount: float) -> Hct:
diff = (100 - color.tone) * amount
tone = max(0.0, min(100.0, color.tone + diff))
chroma = max(0.0, color.chroma + diff / 5)
return Hct.from_hct(color.hue, chroma, tone)
def darken(color: Hct, amount: float) -> Hct:
diff = color.tone * amount
tone = max(0.0, min(100.0, color.tone - diff))
chroma = max(0.0, color.chroma - diff / 5)
return Hct.from_hct(color.hue, chroma, tone)
def grayscale(color: Hct, light: bool) -> Hct:
color = darken(color, 0.35) if light else lighten(color, 0.65)
color.chroma = 0
return color
def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct:
diff = difference_degrees(from_hct.hue, to_hct.hue)
rotation = min(diff * 0.8, 100)
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)))
return Hct.from_hct(output_hue, from_hct.chroma, tone)
def terminal_palette(
colors: dict[str, str], mode: str, variant: str
) -> dict[str, str]:
light = mode.lower() == "light"
key_hex = (
colors.get("primary_paletteKeyColor")
or colors.get("primaryPaletteKeyColor")
or colors.get("primary")
or int_to_hex(seed.to_int())
)
key_hct = hex_to_hct(key_hex)
base = LIGHT_GRUVBOX if light else DARK_GRUVBOX
out: dict[str, str] = {}
is_mono = variant.lower() == "monochrome"
for i, base_hct in enumerate(base):
if is_mono:
h = grayscale(base_hct, light)
else:
tone_boost = (0.35 if i < 8 else 0.2) * (-1 if light else 1)
h = harmonize(base_hct, key_hct, tone_boost)
out[f"term{i}"] = int_to_hex(h.to_int())
return out
def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)):
thumbnail_file = Path(thumbnail_path)
@@ -70,6 +207,170 @@ def generate(
thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
image.save(thumbnail_path, "JPEG")
def apply_terms(sequences: str, sequences_tmux: str, state_path: Path) -> None:
state_path.parent.mkdir(parents=True, exist_ok=True)
state_path.write_text(sequences, encoding="utf-8")
pts_path = Path("/dev/pts")
if not pts_path.exists():
return
O_NOCTTY = getattr(os, "O_NOCTTY", 0)
for pt in pts_path.iterdir():
if not pt.name.isdigit():
continue
try:
fd = os.open(str(pt), os.O_WRONLY | os.O_NONBLOCK | O_NOCTTY)
try:
os.write(fd, sequences_tmux.encode())
os.write(fd, sequences.encode())
finally:
os.close(fd)
except (PermissionError, OSError, BlockingIOError):
pass
def smart_mode(image_path: Path) -> str:
is_dark = ""
with Image.open(image_path) as img:
img.thumbnail((1, 1), Image.LANCZOS)
hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0))))
is_dark = "light" if hct.tone > 50 else "dark"
return is_dark
def build_template_context(
*,
colors: dict[str, str],
seed: Hct,
mode: str,
wallpaper_path: str,
name: str,
flavor: str,
variant: str,
) -> dict[str, Any]:
ctx: dict[str, Any] = {
"mode": mode,
"wallpaper_path": wallpaper_path,
"source_color": int_to_hex(seed.to_int()),
"name": name,
"seed": seed.to_int(),
"flavor": flavor,
"variant": variant,
"colors": colors
}
for k, v in colors.items():
ctx[k] = v
ctx[f"m3{k}"] = v
term = terminal_palette(colors, mode, variant)
ctx.update(term)
ctx["term"] = [term[f"term{i}"] for i in range(16)]
seq = make_sequences(
term=term,
foreground=ctx["m3onSurface"],
background=ctx["m3surface"],
)
ctx["sequences"] = seq
ctx["sequences_tmux"] = tmux_wrap_sequences(seq)
return ctx
def make_sequences(
*,
term: dict[str, str],
foreground: str,
background: str,
) -> str:
ESC = "\x1b"
ST = ESC + "\\"
parts: list[str] = []
for i in range(16):
parts.append(f"{ESC}]4;{i};{term[f'term{i}']}{ST}")
parts.append(f"{ESC}]10;{foreground}{ST}")
parts.append(f"{ESC}]11;{background}{ST}")
return "".join(parts)
def tmux_wrap_sequences(seq: str) -> str:
ESC = "\x1b"
return f"{ESC}Ptmux;{seq.replace(ESC, ESC+ESC)}{ESC}\\"
def parse_output_directive(first_line: str) -> Optional[Path]:
s = first_line.strip()
if not s.startswith("#") or s.startswith("#!"):
return None
target = s[1:].strip()
if not target:
return None
expanded = os.path.expandvars(os.path.expanduser(target))
return Path(expanded)
def split_directive_and_body(text: str) -> Tuple[Optional[Path], str]:
lines = text.splitlines(keepends=True)
if not lines:
return None, ""
out_path = parse_output_directive(lines[0])
if out_path is None:
return None, text
body = "".join(lines[1:])
return out_path, body
def render_all_templates(
templates_dir: Path,
context: dict[str, object],
*,
strict: bool = True,
) -> list[Path]:
undefined_cls = StrictUndefined if strict else Undefined
env = Environment(
loader=FileSystemLoader(str(templates_dir)),
autoescape=False,
keep_trailing_newline=True,
undefined=undefined_cls,
)
rendered_outputs: list[Path] = []
for tpl_path in sorted(p for p in templates_dir.rglob("*") if p.is_file()):
rel = tpl_path.relative_to(templates_dir)
if any(part.startswith(".") for part in rel.parts):
continue
raw = tpl_path.read_text(encoding="utf-8")
out_path, body = split_directive_and_body(raw)
out_path.parent.mkdir(parents=True, exist_ok=True)
try:
template = env.from_string(body)
text = template.render(**context)
except Exception as e:
raise RuntimeError(
f"Template render failed for '{rel}': {e}") from e
out_path.write_text(text, encoding="utf-8")
try:
shutil.copymode(tpl_path, out_path)
except OSError:
pass
rendered_outputs.append(out_path)
return rendered_outputs
def seed_from_image(image_path: Path) -> Hct:
image = Image.open(image_path)
pixel_len = image.width * image.height
@@ -88,11 +389,11 @@ def generate(
raise typer.BadParameter(
f"Preset '{name}' not found. Available presets: {', '.join(PRESETS.keys())}")
def generate_color_scheme(seed: Hct, mode: str) -> dict[str, str]:
def generate_color_scheme(seed: Hct, mode: str, scheme_class) -> dict[str, str]:
is_dark = mode.lower() == "dark"
scheme = Scheme(
scheme = scheme_class(
seed,
is_dark,
0.0
@@ -111,27 +412,67 @@ def generate(
return "#{:06X}".format(argb_int & 0xFFFFFF)
try:
with CONFIG.open() as f:
config = json.load(f)
scheme = scheme or config["colors"]["schemeType"]
config_mode = config["general"]["color"]["mode"]
smart = bool(config["general"]["color"].get("smart", False))
scheme_class = get_scheme_class(scheme)
if preset:
seed = seed_from_preset(preset)
colors = generate_color_scheme(seed, mode)
effective_mode = mode or config_mode
name, flavor = preset.split(":")
else:
generate_thumbnail(image_path, str(thumbnail_path))
seed = seed_from_image(thumbnail_path)
colors = generate_color_scheme(seed, mode)
image_path = image_path or Path(WALL_PATH)
generate_thumbnail(image_path, str(THUMB_PATH))
seed = seed_from_image(THUMB_PATH)
name = "dynamic"
flavor = "default"
if smart:
effective_mode = smart_mode(THUMB_PATH)
elif mode is not None:
effective_mode = mode
else:
effective_mode = config_mode
colors = generate_color_scheme(seed, effective_mode, scheme_class)
output_dict = {
"name": name,
"flavor": flavor,
"mode": mode,
"mode": effective_mode,
"variant": scheme,
"colors": colors
"colors": colors,
"seed": seed.to_int()
}
output.parent.mkdir(parents=True, exist_ok=True)
with open(output, "w") as f:
if TEMPLATE_DIR is not None:
wp = str(WALL_PATH)
ctx = build_template_context(
colors=colors,
seed=seed,
mode=effective_mode,
wallpaper_path=wp,
name=name,
flavor=flavor,
variant=scheme
)
rendered = render_all_templates(
templates_dir=TEMPLATE_DIR,
context=ctx,
)
apply_terms(ctx["sequences"], ctx["sequences_tmux"], SEQ_STATE)
for p in rendered:
print(f"rendered: {p}")
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
with open(OUTPUT, "w") as f:
json.dump(output_dict, f, indent=4)
except Exception as e:
print(f"Error: {e}")
+2 -2
View File
@@ -1,11 +1,11 @@
{
pkgs, # To ensure the nixpkgs version of app2unit
pkgs,
fetchFromGitHub,
...
}:
pkgs.app2unit.overrideAttrs (
final: prev: rec {
version = "1.0.3"; # Fix old issue related to missing env var
version = "1.0.3";
src = fetchFromGitHub {
owner = "Vladimir-csp";
repo = "app2unit";
+4
View File
@@ -25,6 +25,8 @@
pkg-config,
pythonEnv,
zshell-cli,
ddcutil,
brightnessctl,
}:
let
version = "1.0.0";
@@ -39,6 +41,8 @@ let
bash
hyprland
zshell-cli
ddcutil
brightnessctl
];
fontconfig = makeFontsConf {
+1
View File
@@ -16,6 +16,7 @@ python3.pkgs.buildPythonApplication {
dependencies = with python3.pkgs; [
materialyoucolor
pillow
jinja2
typer
];