1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

add shell

This commit is contained in:
bbedward
2025-07-10 11:28:27 -04:00
parent 2ef160d210
commit 53687266a1
21 changed files with 10854 additions and 0 deletions

943
AppLauncher.qml Normal file
View File

@@ -0,0 +1,943 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
PanelWindow {
id: launcher
property var theme
property bool isVisible: false
// Default theme fallback
property var defaultTheme: QtObject {
property color primary: "#D0BCFF"
property color background: "#10121E"
property color surfaceContainer: "#1D1B20"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color outline: "#938F99"
property real cornerRadius: 12
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeLarge: 16
property real fontSizeMedium: 14
property real fontSizeSmall: 12
property real iconSize: 24
property real iconSizeLarge: 32
property real barHeight: 48
property string iconFont: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int shortDuration: 150
property int mediumDuration: 300
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
}
property var activeTheme: theme || defaultTheme
// Full screen overlay setup for proper focus
anchors {
top: true
left: true
right: true
bottom: true
}
// Proper layer shell configuration
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-launcher"
visible: isVisible
color: "transparent"
// Enhanced app management
property var currentApp: ({})
property var allApps: []
property var categories: ["All"]
property string selectedCategory: "All"
property var recentApps: []
property var pinnedApps: ["firefox", "code", "terminal", "file-manager"]
property bool showCategories: false
property string viewMode: "list" // "list" or "grid"
property var appCategories: ({
"AudioVideo": "Media",
"Audio": "Media",
"Video": "Media",
"Development": "Development",
"TextEditor": "Development",
"IDE": "Development",
"Programming": "Development",
"Education": "Education",
"Game": "Games",
"Graphics": "Graphics",
"Photography": "Graphics",
"Network": "Internet",
"WebBrowser": "Internet",
"Office": "Office",
"WordProcessor": "Office",
"Spreadsheet": "Office",
"Presentation": "Office",
"Science": "Science",
"Settings": "Settings",
"System": "System",
"Utility": "Utilities",
"Accessories": "Utilities",
"FileManager": "Utilities",
"TerminalEmulator": "Utilities"
})
ListModel { id: filteredModel }
ListModel { id: categoryModel }
// Background dim with click to close
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.3)
opacity: launcher.isVisible ? 1.0 : 0.0
visible: launcher.isVisible
Behavior on opacity {
NumberAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
MouseArea {
anchors.fill: parent
enabled: launcher.isVisible
onClicked: launcher.hide()
}
}
// Desktop applications scanning
Process {
id: desktopScanner
command: ["sh", "-c", `
for dir in "/usr/share/applications/" "/usr/local/share/applications/" "$HOME/.local/share/applications/" "/run/current-system/sw/share/applications/"; do
if [ -d "$dir" ]; then
find "$dir" -name "*.desktop" 2>/dev/null | while read file; do
echo "===FILE:$file"
sed -n '/^\\[Desktop Entry\\]/,/^\\[.*\\]/{/^\\[Desktop Entry\\]/d; /^\\[.*\\]/q; /^Name=/p; /^Exec=/p; /^Icon=/p; /^Hidden=/p; /^NoDisplay=/p; /^Categories=/p; /^Comment=/p}' "$file" 2>/dev/null || true
done
fi
done
`]
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.startsWith("===FILE:")) {
// Save previous app if valid
if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) {
allApps.push({
name: currentApp.name,
exec: currentApp.exec,
icon: currentApp.icon || "application-x-executable",
comment: currentApp.comment || "",
categories: currentApp.categories || []
})
}
// Start new app
currentApp = { name: "", exec: "", icon: "", comment: "", categories: [], hidden: false, noDisplay: false }
} else if (line.startsWith("Name=")) {
currentApp.name = line.substring(5)
} else if (line.startsWith("Exec=")) {
currentApp.exec = line.substring(5)
} else if (line.startsWith("Icon=")) {
currentApp.icon = line.substring(5)
} else if (line.startsWith("Comment=")) {
currentApp.comment = line.substring(8)
} else if (line.startsWith("Categories=")) {
currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0)
} else if (line === "Hidden=true") {
currentApp.hidden = true
} else if (line === "NoDisplay=true") {
currentApp.noDisplay = true
}
}
}
onExited: {
// Save last app
if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) {
allApps.push({
name: currentApp.name,
exec: currentApp.exec,
icon: currentApp.icon || "application-x-executable",
comment: currentApp.comment || "",
categories: currentApp.categories || []
})
}
// Extract unique categories
let uniqueCategories = new Set(["All"])
allApps.forEach(app => {
app.categories.forEach(cat => {
if (appCategories[cat]) {
uniqueCategories.add(appCategories[cat])
}
})
})
categories = Array.from(uniqueCategories)
console.log("Loaded", allApps.length, "applications with", categories.length, "categories")
updateFilteredModel()
}
}
function updateFilteredModel() {
filteredModel.clear()
let apps = allApps
// Filter by category
if (selectedCategory !== "All") {
apps = apps.filter(app => {
return app.categories.some(cat => appCategories[cat] === selectedCategory)
})
}
// Filter by search
if (searchField.text.length > 0) {
const query = searchField.text.toLowerCase()
apps = apps.filter(app => {
return app.name.toLowerCase().includes(query) ||
(app.comment && app.comment.toLowerCase().includes(query))
}).sort((a, b) => {
// Sort by relevance
const aName = a.name.toLowerCase()
const bName = b.name.toLowerCase()
const aStartsWith = aName.startsWith(query)
const bStartsWith = bName.startsWith(query)
if (aStartsWith && !bStartsWith) return -1
if (!aStartsWith && bStartsWith) return 1
return aName.localeCompare(bName)
})
}
// Sort alphabetically if no search
if (searchField.text.length === 0) {
apps.sort((a, b) => a.name.localeCompare(b.name))
}
// Add to model
apps.forEach(app => {
filteredModel.append(app)
})
}
// Main launcher panel with enhanced design
Rectangle {
id: launcherPanel
width: 520
height: 600
anchors {
top: parent.top
left: parent.left
topMargin: 50
leftMargin: activeTheme.spacingL
}
color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98)
radius: activeTheme.cornerRadiusXLarge
// Material 3 elevation with multiple layers
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
// Animated entrance with spring effect
transform: [
Scale {
id: scaleTransform
origin.x: 0
origin.y: 0
xScale: launcher.isVisible ? 1.0 : 0.92
yScale: launcher.isVisible ? 1.0 : 0.92
Behavior on xScale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
Behavior on yScale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
},
Translate {
id: translateTransform
x: launcher.isVisible ? 0 : -30
y: launcher.isVisible ? 0 : -15
Behavior on x {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
Behavior on y {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
}
]
opacity: launcher.isVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
// Content with focus management
Item {
anchors.fill: parent
focus: true
// Handle keyboard shortcuts
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
launcher.hide()
event.accepted = true
}
}
Column {
anchors.fill: parent
anchors.margins: activeTheme.spacingXL
spacing: activeTheme.spacingL
// Header section
Row {
width: parent.width
height: 40
// App launcher title
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
font.pixelSize: activeTheme.fontSizeLarge + 4
font.weight: Font.Bold
color: activeTheme.surfaceText
}
Item { width: parent.width - 200; height: 1 }
// Quick stats
Text {
anchors.verticalCenter: parent.verticalCenter
text: filteredModel.count + " apps"
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceVariantText
}
}
// Enhanced search field
Rectangle {
id: searchContainer
width: parent.width
height: 52
radius: activeTheme.cornerRadiusLarge
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.6)
border.width: searchField.activeFocus ? 2 : 1
border.color: searchField.activeFocus ? activeTheme.primary :
Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.3)
Behavior on border.color {
ColorAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
Row {
anchors.fill: parent
anchors.leftMargin: activeTheme.spacingL
anchors.rightMargin: activeTheme.spacingL
spacing: activeTheme.spacingM
Text {
anchors.verticalCenter: parent.verticalCenter
text: "search"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: searchField.activeFocus ? activeTheme.primary : activeTheme.surfaceVariantText
font.weight: activeTheme.iconFontWeight
}
TextInput {
id: searchField
anchors.verticalCenter: parent.verticalCenter
width: parent.width - parent.spacing - activeTheme.iconSize - 32
height: parent.height - activeTheme.spacingS
color: activeTheme.surfaceText
font.pixelSize: activeTheme.fontSizeLarge
verticalAlignment: TextInput.AlignVCenter
focus: launcher.isVisible
selectByMouse: true
activeFocusOnTab: true
// Placeholder text
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Search applications..."
color: activeTheme.surfaceVariantText
font.pixelSize: activeTheme.fontSizeLarge
visible: searchField.text.length === 0 && !searchField.activeFocus
}
// Clear button
Rectangle {
width: 24
height: 24
radius: 12
color: clearSearchArea.containsMouse ? Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12) : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: searchField.text.length > 0
Text {
anchors.centerIn: parent
text: "close"
font.family: activeTheme.iconFont
font.pixelSize: 16
color: clearSearchArea.containsMouse ? activeTheme.outline : activeTheme.surfaceVariantText
}
MouseArea {
id: clearSearchArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: searchField.text = ""
}
}
onTextChanged: updateFilteredModel()
Keys.onPressed: function (event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count) {
launcher.launchApp(filteredModel.get(0).exec)
launcher.hide()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
launcher.hide()
event.accepted = true
}
}
}
}
}
// Category filter and view mode controls
Row {
width: parent.width
height: 40
spacing: activeTheme.spacingM
visible: searchField.text.length === 0
// Category filter
Rectangle {
width: 200
height: 36
radius: activeTheme.cornerRadius
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: activeTheme.spacingS
Text {
text: "category"
font.family: activeTheme.iconFont
font.pixelSize: 18
color: activeTheme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: selectedCategory
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceText
anchors.verticalCenter: parent.verticalCenter
font.weight: Font.Medium
}
}
Text {
anchors.right: parent.right
anchors.rightMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: showCategories ? "expand_less" : "expand_more"
font.family: activeTheme.iconFont
font.pixelSize: 18
color: activeTheme.surfaceVariantText
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showCategories = !showCategories
}
}
Item { width: parent.width - 300; height: 1 }
// View mode toggle
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
// List view button
Rectangle {
width: 36
height: 36
radius: activeTheme.cornerRadius
color: viewMode === "list" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) :
listViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
text: "view_list"
font.family: activeTheme.iconFont
font.pixelSize: 20
color: viewMode === "list" ? activeTheme.primary : activeTheme.surfaceText
}
MouseArea {
id: listViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: viewMode = "list"
}
}
// Grid view button
Rectangle {
width: 36
height: 36
radius: activeTheme.cornerRadius
color: viewMode === "grid" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) :
gridViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
text: "grid_view"
font.family: activeTheme.iconFont
font.pixelSize: 20
color: viewMode === "grid" ? activeTheme.primary : activeTheme.surfaceText
}
MouseArea {
id: gridViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: viewMode = "grid"
}
}
}
}
// Category dropdown
Rectangle {
width: 200
height: Math.min(250, categories.length * 40 + activeTheme.spacingM * 2)
radius: activeTheme.cornerRadiusLarge
color: activeTheme.surfaceContainer
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
visible: showCategories
z: 100
// Drop shadow
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.1)
border.width: 1
z: -1
}
ScrollView {
anchors.fill: parent
anchors.margins: activeTheme.spacingS
clip: true
ListView {
model: categories
spacing: 4
delegate: Rectangle {
width: ListView.view.width
height: 36
radius: activeTheme.cornerRadiusSmall
color: catArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: activeTheme.fontSizeMedium
color: selectedCategory === modelData ? activeTheme.primary : activeTheme.surfaceText
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
}
MouseArea {
id: catArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
showCategories = false
updateFilteredModel()
}
}
}
}
}
}
// App grid/list container
Rectangle {
width: parent.width
height: parent.height - searchContainer.height - (searchField.text.length === 0 ? 128 : 60) - parent.spacing * 3
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
Item {
anchors.fill: parent
// List view
ListView {
id: appList
anchors.fill: parent
anchors.margins: activeTheme.spacingS
spacing: activeTheme.spacingS
visible: viewMode === "list"
model: filteredModel
delegate: Rectangle {
width: appList.width
height: 72
radius: activeTheme.cornerRadiusLarge
color: appMouseArea.hovered ?
Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
border.width: 1
Behavior on color {
ColorAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
Row {
anchors.fill: parent
anchors.margins: activeTheme.spacingM
spacing: activeTheme.spacingL
Item {
width: 56
height: 56
anchors.verticalCenter: parent.verticalCenter
IconImage {
anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable")
smooth: true
visible: status === Image.Ready
onStatusChanged: {
if (status === Image.Error && model.name.includes("Avahi")) {
console.log("Avahi icon failed to load:", model.icon, "->", source)
}
}
}
// Fallback for missing icons
Rectangle {
anchors.fill: parent
visible: !parent.children[0].visible
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
radius: activeTheme.cornerRadiusLarge
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: activeTheme.iconSizeLarge
color: activeTheme.surfaceVariantText
font.weight: Font.Medium
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 56 - activeTheme.spacingL
spacing: activeTheme.spacingXS
Text {
width: parent.width
text: model.name
font.pixelSize: activeTheme.fontSizeLarge
color: activeTheme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
Text {
width: parent.width
text: model.comment || "Application"
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceVariantText
elide: Text.ElideRight
visible: model.comment && model.comment.length > 0
}
}
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
launcher.launchApp(model.exec)
launcher.hide()
}
}
}
}
// Grid view
GridView {
id: appGrid
anchors.fill: parent
anchors.margins: activeTheme.spacingS
// Responsive cell sizes based on screen width
property int baseCellWidth: Math.max(100, Math.min(140, width / 8))
property int baseCellHeight: baseCellWidth + 20
cellWidth: baseCellWidth
cellHeight: baseCellHeight
visible: viewMode === "grid"
// Center the grid content
property int columnsCount: Math.floor(width / cellWidth)
property int remainingSpace: width - (columnsCount * cellWidth)
anchors.leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2)
anchors.rightMargin: anchors.leftMargin
model: filteredModel
delegate: Rectangle {
width: appGrid.cellWidth - 8
height: appGrid.cellHeight - 8
radius: activeTheme.cornerRadiusLarge
color: gridAppArea.hovered ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
border.width: 1
Behavior on color {
ColorAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
Column {
anchors.centerIn: parent
spacing: activeTheme.spacingS
Item {
property int iconSize: Math.min(56, Math.max(32, appGrid.cellWidth * 0.6))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable")
smooth: true
visible: status === Image.Ready
onStatusChanged: {
if (status === Image.Error && model.name.includes("Avahi")) {
console.log("Avahi grid icon failed to load:", model.icon, "->", source)
}
}
}
// Fallback for missing icons
Rectangle {
anchors.fill: parent
visible: !parent.children[0].visible
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
radius: activeTheme.cornerRadiusLarge
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: activeTheme.iconSizeLarge
color: activeTheme.surfaceVariantText
font.weight: Font.Medium
}
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
width: 88
text: model.name
font.pixelSize: activeTheme.fontSizeSmall
color: activeTheme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: gridAppArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
launcher.launchApp(model.exec)
launcher.hide()
}
}
}
}
}
}
}
}
}
}
Process {
id: appLauncher
function start(exec) {
// Clean up exec command (remove field codes)
var cleanExec = exec.replace(/%[fFuU]/g, "").trim()
command = ["sh", "-c", cleanExec]
running = true
}
onExited: {
if (exitCode !== 0) {
console.log("Failed to launch application, exit code:", exitCode)
}
}
}
function launchApp(exec) {
appLauncher.start(exec)
}
function show() {
launcher.isVisible = true
Qt.callLater(function() {
searchField.forceActiveFocus()
})
}
function hide() {
launcher.isVisible = false
searchField.text = ""
showCategories = false
}
function toggle() {
if (launcher.isVisible) {
hide()
} else {
show()
}
}
Component.onCompleted: {
desktopScanner.running = true
}
}

627
ClipboardHistory.qml Normal file
View File

@@ -0,0 +1,627 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
PanelWindow {
id: clipboardHistory
property var theme
property bool isVisible: false
// Default theme fallback
property var defaultTheme: QtObject {
property color primary: "#D0BCFF"
property color background: "#10121E"
property color surfaceContainer: "#1D1B20"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color outline: "#938F99"
property color error: "#F2B8B5"
property real cornerRadius: 12
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real cornerRadiusSmall: 8
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeLarge: 16
property real fontSizeMedium: 14
property real fontSizeSmall: 12
property real iconSize: 24
property real iconSizeLarge: 32
property string iconFont: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int shortDuration: 150
property int mediumDuration: 300
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
}
property var activeTheme: theme || defaultTheme
// Window properties
color: "transparent"
visible: isVisible
anchors {
top: true
left: true
right: true
bottom: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
// Clipboard entries model
property var clipboardEntries: []
ListModel {
id: clipboardModel
}
ListModel {
id: filteredClipboardModel
}
function updateFilteredModel() {
filteredClipboardModel.clear()
for (let i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry
if (searchField.text.trim().length === 0) {
filteredClipboardModel.append({"entry": entry})
} else {
const content = getEntryPreview(entry).toLowerCase()
if (content.includes(searchField.text.toLowerCase())) {
filteredClipboardModel.append({"entry": entry})
}
}
}
}
function toggle() {
if (isVisible) {
hide()
} else {
show()
}
}
function show() {
clipboardHistory.isVisible = true
searchField.focus = true
refreshClipboard()
console.log("ClipboardHistory: Opening and refreshing")
}
function hide() {
clipboardHistory.isVisible = false
searchField.focus = false
searchField.text = ""
}
function refreshClipboard() {
clipboardProcess.running = true
}
function copyEntry(entry) {
const entryId = entry.split('\t')[0]
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`]
copyProcess.running = true
hide()
}
function deleteEntry(entry) {
const entryId = entry.split('\t')[0]
deleteProcess.command = ["cliphist", "delete-query", entryId]
deleteProcess.running = true
}
function clearAll() {
clearProcess.running = true
}
function getEntryPreview(entry) {
// Remove cliphist ID prefix and clean up content
let content = entry.replace(/^\s*\d+\s+/, "")
// Handle different content types
if (content.includes("image/")) {
const match = content.match(/(\d+)x(\d+)/)
return match ? `Image ${match[1]}×${match[2]}` : "Image"
}
// Truncate long text
if (content.length > 100) {
return content.substring(0, 100) + "..."
}
return content
}
function getEntryType(entry) {
if (entry.includes("image/")) return "image"
if (entry.length > 200) return "long_text"
return "text"
}
// Background overlay
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
visible: clipboardHistory.isVisible
Behavior on opacity {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
MouseArea {
anchors.fill: parent
enabled: clipboardHistory.isVisible
onClicked: clipboardHistory.hide()
}
}
// Main clipboard container
Rectangle {
id: clipboardContainer
width: Math.min(600, parent.width - 200)
height: Math.min(500, parent.height - 100)
anchors.centerIn: parent
color: activeTheme.surfaceContainer
radius: activeTheme.cornerRadiusXLarge
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
scale: clipboardHistory.isVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
// Header section
Column {
id: headerSection
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: activeTheme.spacingXL
spacing: activeTheme.spacingL
// Title and actions
Row {
width: parent.width
height: 40
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Clipboard History"
font.pixelSize: activeTheme.fontSizeLarge + 4
font.weight: Font.Bold
color: activeTheme.surfaceText
}
Item {
width: parent.width - 180 - (clearAllButton.visible ? 48 : 0)
height: 1
}
// Clear all button
Rectangle {
id: clearAllButton
width: 40
height: 32
radius: activeTheme.cornerRadius
color: clearArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: clipboardModel.count > 0
Text {
anchors.centerIn: parent
text: "delete_sweep"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: clearArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: clearAll()
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
// Close button
Rectangle {
width: 40
height: 32
radius: activeTheme.cornerRadius
color: closeArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 4
Text {
anchors.centerIn: parent
text: "close"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: closeArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: clipboardHistory.hide()
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
// Search field
Rectangle {
width: parent.width
height: 48
radius: activeTheme.cornerRadiusLarge
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
border.color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: searchField.focus ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: activeTheme.spacingM
Text {
text: "search"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
TextInput {
id: searchField
width: parent.parent.width - 80
height: parent.parent.height
font.pixelSize: activeTheme.fontSizeLarge
color: activeTheme.surfaceText
verticalAlignment: TextInput.AlignVCenter
onTextChanged: updateFilteredModel()
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
clipboardHistory.hide()
}
}
// Placeholder text
Text {
text: "Search clipboard entries..."
font: searchField.font
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
visible: searchField.text.length === 0 && !searchField.focus
}
}
}
Behavior on border.color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
// Clipboard entries
Rectangle {
anchors.top: headerSection.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: activeTheme.spacingXL
anchors.topMargin: activeTheme.spacingL
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
ListView {
id: clipboardList
model: filteredClipboardModel
spacing: activeTheme.spacingS
delegate: Rectangle {
width: clipboardList.width
height: Math.max(60, contentColumn.implicitHeight + activeTheme.spacingM * 2)
radius: activeTheme.cornerRadius
color: entryArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.05)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.1)
border.width: 1
property string entryType: getEntryType(model.entry)
property string entryPreview: getEntryPreview(model.entry)
Row {
anchors.fill: parent
anchors.margins: activeTheme.spacingM
spacing: activeTheme.spacingL
// Entry type icon
Rectangle {
width: 36
height: 36
radius: activeTheme.cornerRadius
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: {
switch (entryType) {
case "image": return "image"
case "long_text": return "subject"
default: return "content_paste"
}
}
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize - 4
color: activeTheme.primary
}
}
// Entry content
Column {
id: contentColumn
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 100
spacing: activeTheme.spacingXS
Text {
text: {
switch (entryType) {
case "image": return "Image • " + entryPreview
case "long_text": return "Long Text"
default: return "Text"
}
}
font.pixelSize: activeTheme.fontSizeSmall
color: activeTheme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
Text {
text: entryPreview
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 2
elide: Text.ElideRight
visible: entryType !== "image"
}
}
// Actions
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: activeTheme.spacingXS
// Copy button
Rectangle {
width: 28
height: 28
radius: activeTheme.cornerRadiusSmall
color: copyArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "content_copy"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize - 8
color: copyArea.containsMouse ? activeTheme.primary : activeTheme.surfaceText
}
MouseArea {
id: copyArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
// Delete button
Rectangle {
width: 28
height: 28
radius: activeTheme.cornerRadiusSmall
color: deleteArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "delete"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize - 8
color: deleteArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
}
MouseArea {
id: deleteArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: deleteEntry(model.entry)
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
}
MouseArea {
id: entryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
// Empty state
Column {
anchors.centerIn: parent
spacing: activeTheme.spacingL
visible: filteredClipboardModel.count === 0
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "content_paste_off"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSizeLarge + 16
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.3)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "No clipboard history"
font.pixelSize: activeTheme.fontSizeLarge
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Copy something to see it here"
font.pixelSize: activeTheme.fontSizeMedium
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.4)
}
}
}
}
}
// Clipboard processes
Process {
id: clipboardProcess
command: ["cliphist", "list"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.trim()) {
clipboardHistory.clipboardEntries.push(line)
clipboardModel.append({"entry": line})
console.log("ClipboardHistory: Adding entry:", line.substring(0, 50) + "...")
}
}
}
onStarted: {
clipboardHistory.clipboardEntries = []
clipboardModel.clear()
console.log("ClipboardHistory: Starting cliphist process...")
}
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("ClipboardHistory: Loaded", clipboardModel.count, "entries")
updateFilteredModel()
} else {
console.warn("ClipboardHistory: Failed to load clipboard history")
}
}
}
Process {
id: copyProcess
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("ClipboardHistory: Failed to copy entry")
}
}
}
Process {
id: deleteProcess
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
refreshClipboard()
}
}
}
Process {
id: clearProcess
command: ["cliphist", "wipe"]
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
clipboardHistory.clipboardEntries = []
clipboardModel.clear()
}
}
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
hide()
}
}
}

420
MediaPlayer.qml Normal file
View File

@@ -0,0 +1,420 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.Mpris
import "Services"
PanelWindow {
id: mediaPlayer
property var theme
property bool isVisible: false
property MprisPlayer activePlayer: MprisController.activePlayer
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
property var defaultTheme: QtObject {
property color primary: "#D0BCFF"
property color background: "#10121E"
property color surfaceContainer: "#1D1B20"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color outline: "#938F99"
property color error: "#F2B8B5"
property real cornerRadius: 12
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real cornerRadiusSmall: 8
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeLarge: 16
property real fontSizeMedium: 14
property real fontSizeSmall: 12
property real iconSize: 24
property real iconSizeLarge: 32
property string iconFont: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int shortDuration: 150
property int mediumDuration: 300
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
}
property var activeTheme: theme || defaultTheme
onHasActiveMediaChanged: {
if (!hasActiveMedia && isVisible) {
hide()
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-media-player"
visible: isVisible
color: "transparent"
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.3)
opacity: mediaPlayer.isVisible ? 1.0 : 0.0
visible: mediaPlayer.isVisible
Behavior on opacity {
NumberAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
MouseArea {
anchors.fill: parent
enabled: mediaPlayer.isVisible
onClicked: mediaPlayer.hide()
}
}
Rectangle {
id: mediaPanel
width: 480
height: 320
anchors.centerIn: parent
color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98)
radius: activeTheme.cornerRadiusXLarge
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
transform: [
Scale {
origin.x: mediaPanel.width / 2
origin.y: mediaPanel.height / 2
xScale: mediaPlayer.isVisible ? 1.0 : 0.9
yScale: mediaPlayer.isVisible ? 1.0 : 0.9
Behavior on xScale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
Behavior on yScale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
}
]
opacity: mediaPlayer.isVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: activeTheme.spacingXL
spacing: activeTheme.spacingL
Row {
width: parent.width
height: 32
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Now Playing"
font.pixelSize: activeTheme.fontSizeLarge + 4
font.weight: Font.Bold
color: activeTheme.surfaceText
}
Item { width: parent.width - 200; height: 1 }
Rectangle {
width: 32
height: 32
radius: activeTheme.cornerRadius
color: closeArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "close"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: closeArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: mediaPlayer.hide()
}
}
}
Row {
width: parent.width
height: parent.height - 80
spacing: activeTheme.spacingXL
Rectangle {
width: 180
height: parent.height
radius: activeTheme.cornerRadiusLarge
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: activePlayer?.trackArtUrl || ""
fillMode: Image.PreserveAspectCrop
smooth: true
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
Text {
anchors.centerIn: parent
text: "album"
font.family: activeTheme.iconFont
font.pixelSize: 48
color: activeTheme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 180 - activeTheme.spacingXL
height: parent.height
spacing: activeTheme.spacingM
Column {
width: parent.width
spacing: activeTheme.spacingS
Text {
text: activePlayer?.trackTitle || "No title"
font.pixelSize: activeTheme.fontSizeLarge + 2
font.weight: Font.Bold
color: activeTheme.surfaceText
elide: Text.ElideRight
width: parent.width
}
Text {
text: activePlayer?.trackArtist || "Unknown artist"
font.pixelSize: activeTheme.fontSizeLarge
color: activeTheme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
Text {
text: activePlayer?.trackAlbum || ""
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
visible: text.length > 0
}
}
Item { height: activeTheme.spacingM }
Column {
width: parent.width
spacing: activeTheme.spacingS
Rectangle {
width: parent.width
height: 6
radius: 3
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
Rectangle {
width: parent.width * (activePlayer?.position / Math.max(activePlayer?.length || 1, 1))
height: parent.height
radius: parent.radius
color: activeTheme.primary
}
}
Row {
width: parent.width
Text {
text: formatTime(activePlayer?.position || 0)
font.pixelSize: activeTheme.fontSizeSmall
color: activeTheme.surfaceVariantText
}
Item { width: parent.width - 100; height: 1 }
Text {
text: formatTime(activePlayer?.length || 0)
font.pixelSize: activeTheme.fontSizeSmall
color: activeTheme.surfaceVariantText
}
}
}
Item { height: activeTheme.spacingL }
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: activeTheme.spacingL
Rectangle {
width: 48
height: 48
radius: 24
color: prevArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: activeTheme.surfaceText
}
MouseArea {
id: prevArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.previous()
}
}
Rectangle {
width: 56
height: 56
radius: 28
color: activeTheme.primary
Text {
anchors.centerIn: parent
text: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSizeLarge
color: activeTheme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.togglePlaying()
}
}
Rectangle {
width: 48
height: 48
radius: 24
color: nextArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: activeTheme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.next()
}
}
}
}
}
}
}
Timer {
running: activePlayer?.playbackState === MprisPlaybackState.Playing
interval: 1000
repeat: true
onTriggered: activePlayer?.positionChanged()
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return mins + ":" + (secs < 10 ? "0" : "") + secs
}
function show() {
mediaPlayer.isVisible = true
}
function hide() {
mediaPlayer.isVisible = false
}
function toggle() {
if (mediaPlayer.isVisible) {
hide()
} else {
show()
}
}
}

View File

@@ -0,0 +1,162 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQml.Models
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
/**
* A service that provides easy access to the active Mpris player.
*/
Singleton {
id: root
property MprisPlayer trackedPlayer: null
property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null
signal trackChanged(reverse: bool)
property bool __reverse: false
property var activeTrack
Instantiator {
model: Mpris.players
Connections {
required property MprisPlayer modelData
target: modelData
Component.onCompleted: {
console.log("MPRIS Player connected:", modelData.identity)
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData
}
}
Component.onDestruction: {
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
for (const player of Mpris.players.values) {
if (player.playbackState.isPlaying) {
root.trackedPlayer = player
break
}
}
if (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0]
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData
}
}
}
Connections {
target: activePlayer
function onPostTrackChanged() {
root.updateTrack()
}
function onTrackArtUrlChanged() {
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
const r = root.__reverse
root.updateTrack()
root.__reverse = r
}
}
}
onActivePlayerChanged: this.updateTrack()
function updateTrack() {
console.log(`MPRIS Track Update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtist}`)
this.activeTrack = {
uniqueId: this.activePlayer?.uniqueId ?? 0,
artUrl: this.activePlayer?.trackArtUrl ?? "",
title: this.activePlayer?.trackTitle || "Unknown Title",
artist: this.activePlayer?.trackArtist || "Unknown Artist",
album: this.activePlayer?.trackAlbum || "Unknown Album",
}
this.trackChanged(__reverse)
this.__reverse = false
}
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false
function togglePlaying() {
if (this.canTogglePlaying) this.activePlayer.togglePlaying()
}
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false
function previous() {
if (this.canGoPrevious) {
this.__reverse = true
this.activePlayer.previous()
}
}
property bool canGoNext: this.activePlayer?.canGoNext ?? false
function next() {
if (this.canGoNext) {
this.__reverse = false
this.activePlayer.next()
}
}
property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl
property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl
property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None
function setLoopState(loopState) {
if (this.loopSupported) {
this.activePlayer.loopState = loopState
}
}
property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl
property bool hasShuffle: this.activePlayer?.shuffle ?? false
function setShuffle(shuffle) {
if (this.shuffleSupported) {
this.activePlayer.shuffle = shuffle
}
}
function setActivePlayer(player) {
const targetPlayer = player ?? Mpris.players[0]
console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`)
if (targetPlayer && this.activePlayer) {
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer)
} else {
this.__reverse = false
}
this.trackedPlayer = targetPlayer
}
// Debug timer
Timer {
interval: 3000
running: true
repeat: true
onTriggered: {
console.log(`[MprisController] Players: ${Mpris.players.length}, Active: ${activePlayer?.identity || 'none'}, Playing: ${isPlaying}`)
if (activePlayer) {
console.log(` Track: ${activePlayer.trackTitle || 'Unknown'} by ${activePlayer.trackArtist || 'Unknown'}`)
console.log(` State: ${activePlayer.playbackState}`)
} else if (Mpris.players.length === 0) {
console.log(" No MPRIS players detected. Try:")
console.log(" - mpv --script-opts=mpris-title='{{media-title}}' file.mp3")
console.log(" - firefox/chromium (YouTube, Spotify Web)")
console.log(" - vlc file.mp3")
console.log(" Check available players: busctl --user list | grep mpris")
}
}
}
}

View File

@@ -0,0 +1,85 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
id: osService
property string osLogo: ""
property string osName: ""
Process {
id: osDetector
command: ["lsb_release", "-i", "-s"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let osId = data.trim().toLowerCase()
console.log("Detected OS:", osId)
setOSInfo(osId)
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
osDetectorFallback.running = true
}
}
}
Process {
id: osDetectorFallback
command: ["sh", "-c", "cat /etc/os-release | grep '^ID=' | cut -d'=' -f2 | tr -d '\"'"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let osId = data.trim().toLowerCase()
console.log("Detected OS (fallback):", osId)
setOSInfo(osId)
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
osService.osLogo = ""
osService.osName = "Linux"
console.log("OS detection failed, using generic icon")
}
}
}
function setOSInfo(osId) {
if (osId.includes("arch")) {
osService.osLogo = "\uf303"
osService.osName = "Arch Linux"
} else if (osId.includes("ubuntu")) {
osService.osLogo = "\uf31b"
osService.osName = "Ubuntu"
} else if (osId.includes("fedora")) {
osService.osLogo = "\uf30a"
osService.osName = "Fedora"
} else if (osId.includes("debian")) {
osService.osLogo = "\uf306"
osService.osName = "Debian"
} else if (osId.includes("opensuse")) {
osService.osLogo = "\uef6d"
osService.osName = "openSUSE"
} else if (osId.includes("manjaro")) {
osService.osLogo = "\uf312"
osService.osName = "Manjaro"
} else {
osService.osLogo = "\uf033"
osService.osName = "Linux"
}
}
}

View File

@@ -0,0 +1,78 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
id: weatherService
property var weather: ({
available: false,
temp: 0,
tempF: 0,
city: "",
wCode: "113",
humidity: 0,
wind: "",
sunrise: "06:00",
sunset: "18:00",
uv: 0,
pressure: 0
})
Process {
id: weatherFetcher
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1' | jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim() && text.trim().startsWith("{")) {
try {
let parsedData = JSON.parse(text.trim())
if (parsedData.current && parsedData.location) {
weatherService.weather = {
available: true,
temp: parseInt(parsedData.current.temp_C || 0),
tempF: parseInt(parsedData.current.temp_F || 0),
city: parsedData.location.areaName[0]?.value || "Unknown",
wCode: parsedData.current.weatherCode || "113",
humidity: parseInt(parsedData.current.humidity || 0),
wind: (parsedData.current.windspeedKmph || 0) + " km/h",
sunrise: parsedData.astronomy?.sunrise || "06:00",
sunset: parsedData.astronomy?.sunset || "18:00",
uv: parseInt(parsedData.current.uvIndex || 0),
pressure: parseInt(parsedData.current.pressure || 0)
}
console.log("Weather updated:", weatherService.weather.city, weatherService.weather.temp + "°C")
}
} catch (e) {
console.warn("Failed to parse weather data:", e.message)
weatherService.weather.available = false
}
} else {
console.warn("No valid weather data received")
weatherService.weather.available = false
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Weather fetch failed with exit code:", exitCode)
weatherService.weather.available = false
}
}
}
Timer {
interval: 600000 // 10 minutes
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
weatherFetcher.running = true
}
}
}

1
Services/qmldir Normal file
View File

@@ -0,0 +1 @@
singleton MprisController 1.0 MprisController.qml

View File

@@ -0,0 +1,58 @@
import QtQuick
import QtQuick.Controls
Rectangle {
id: archLauncher
property var theme
property var root
width: Math.max(120, launcherRow.implicitWidth + theme.spacingM * 2)
height: 32
radius: theme.cornerRadius
color: launcherArea.containsMouse ? Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.12) : Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Row {
id: launcherRow
anchors.centerIn: parent
spacing: theme.spacingS
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.osLogo || "apps"
font.family: root.osLogo ? "NerdFont" : theme.iconFont
font.pixelSize: root.osLogo ? theme.iconSize - 2 : theme.iconSize - 2
font.weight: theme.iconFontWeight
color: theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
font.pixelSize: theme.fontSizeMedium
font.weight: Font.Medium
color: theme.surfaceText
}
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.appLauncher.toggle()
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}

View File

@@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Controls
Rectangle {
property var theme
property var root
width: 40
height: 32
radius: theme.cornerRadius
color: clipboardArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "content_paste"
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 6
font.weight: theme.iconFontWeight
color: theme.surfaceText
}
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clipboardHistoryPopup.toggle()
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}

291
Widgets/ClockWidget.qml Normal file
View File

@@ -0,0 +1,291 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.Mpris
Rectangle {
id: clockContainer
property var theme
property var root
width: Math.min(root.hasActiveMedia ? 500 : (root.weather.available ? 280 : 200), parent.width - theme.spacingL * 2)
height: root.hasActiveMedia ? 80 : 32
radius: theme.cornerRadius
color: clockMouseArea.containsMouse && root.hasActiveMedia ?
Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) :
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08)
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
property date currentDate: new Date()
// Media player content (when active)
Column {
visible: root.hasActiveMedia
anchors.centerIn: parent
width: parent.width - theme.spacingM * 2
spacing: theme.spacingXS
Row {
width: parent.width
spacing: theme.spacingS
Rectangle {
width: 48
height: 48
radius: theme.cornerRadiusSmall
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
anchors.fill: parent
source: root.activePlayer?.trackArtUrl || ""
fillMode: Image.PreserveAspectCrop
smooth: true
}
Rectangle {
anchors.fill: parent
visible: parent.children[0].status !== Image.Ready
color: "transparent"
Text {
anchors.centerIn: parent
text: "music_note"
font.family: theme.iconFont
font.pixelSize: theme.iconSize
color: theme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 48 - theme.spacingS - 120
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.activePlayer?.trackTitle || "Unknown Track"
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
Text {
text: root.activePlayer?.trackArtist || "Unknown Artist"
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: theme.spacingS
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: theme.iconFont
font.pixelSize: 16
color: theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.previous()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: theme.primary
Text {
anchors.centerIn: parent
text: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
font.family: theme.iconFont
font.pixelSize: 16
color: theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: theme.iconFont
font.pixelSize: 16
color: theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.next()
}
}
}
}
Rectangle {
width: parent.width
height: 4
radius: 2
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
Rectangle {
id: progressFill
width: parent.width * (root.activePlayer?.position / Math.max(root.activePlayer?.length || 1, 1))
height: parent.height
radius: parent.radius
color: theme.primary
Behavior on width {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (root.activePlayer && root.activePlayer.length > 0) {
const newPosition = (mouse.x / width) * root.activePlayer.length
root.activePlayer.setPosition(newPosition)
}
}
}
}
}
// Normal clock/weather content (when no media)
Row {
anchors.centerIn: parent
spacing: theme.spacingM
visible: !root.hasActiveMedia
// Weather info (when available)
Row {
spacing: theme.spacingXS
visible: root.weather.available
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.weatherIcons[root.weather.wCode] || "clear_day"
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 2
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
// Separator when weather is available
Text {
text: "•"
font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.weather.available
}
// Time and date
Row {
spacing: theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: Qt.formatTime(clockContainer.currentDate, "h:mm AP")
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "•"
font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Qt.formatDate(clockContainer.currentDate, "ddd d")
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
clockContainer.currentDate = new Date()
}
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: !root.hasActiveMedia ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: !root.hasActiveMedia
onClicked: {
root.calendarVisible = !root.calendarVisible
}
}
}

View File

@@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Controls
Rectangle {
property var theme
property var root
width: 40
height: 32
radius: theme.cornerRadius
color: colorPickerArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "colorize"
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 6
font.weight: theme.iconFontWeight
color: theme.surfaceText
}
MouseArea {
id: colorPickerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.colorPickerProcess.running = true
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
import QtQuick.Controls
Rectangle {
property var theme
property var root
width: 40
height: 32
radius: theme.cornerRadius
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.16) :
Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
property bool hasUnread: root.notificationHistory.count > 0
Text {
anchors.centerIn: parent
text: "notifications"
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 6
font.weight: theme.iconFontWeight
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
theme.primary : theme.surfaceText
}
Rectangle {
width: 8
height: 8
radius: 4
color: theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
visible: parent.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.notificationHistoryVisible = !root.notificationHistoryVisible
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}

View File

@@ -0,0 +1,111 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
Rectangle {
property var theme
property var root
width: Math.max(40, systemTrayRow.implicitWidth + theme.spacingS * 2)
height: 32
radius: theme.cornerRadius
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
visible: systemTrayRow.children.length > 0
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
width: 24
height: 24
radius: theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon || "";
if (!icon) return "";
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
customTrayMenu.showMenu(mouse.x, mouse.y)
} else {
console.log("No menu available for:", trayItem.title || "Unknown")
}
}
}
}
QtObject {
id: customTrayMenu
property bool menuVisible: false
function showMenu(x, y) {
root.currentTrayMenu = customTrayMenu
root.currentTrayItem = trayItem
root.trayMenuX = parent.parent.parent.parent.x + parent.parent.parent.parent.width - 180 - theme.spacingL
root.trayMenuY = theme.barHeight + theme.spacingS
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
menuVisible = true
root.showTrayMenu = true
}
function hideMenu() {
menuVisible = false
root.showTrayMenu = false
root.currentTrayMenu = null
root.currentTrayItem = null
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
}
}
}

117
Widgets/TopBar.qml Normal file
View File

@@ -0,0 +1,117 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Services.SystemTray
import "../Services"
PanelWindow {
id: topBar
property var theme
property var root
anchors {
top: true
left: true
right: true
}
implicitHeight: theme.barHeight
color: "transparent"
Rectangle {
anchors.fill: parent
color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.95)
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
border.width: 1
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08)
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
to: 0.12
duration: theme.extraLongDuration
easing.type: theme.standardEasing
}
NumberAnimation {
to: 0.06
duration: theme.extraLongDuration
easing.type: theme.standardEasing
}
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: theme.spacingL
anchors.rightMargin: theme.spacingL
// Left section - Apps and Workspace Switcher
Row {
id: leftSection
height: parent.height
spacing: theme.spacingL
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
AppLauncherButton {
theme: topBar.theme
root: topBar.root
}
WorkspaceSwitcher {
theme: topBar.theme
root: topBar.root
}
}
// Center section - Clock/Media Player
ClockWidget {
id: clockWidget
theme: topBar.theme
root: topBar.root
anchors.centerIn: parent
}
// Right section - System controls
Row {
id: rightSection
height: parent.height
spacing: theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
SystemTrayWidget {
theme: topBar.theme
root: topBar.root
}
ClipboardButton {
theme: topBar.theme
root: topBar.root
}
ColorPickerButton {
theme: topBar.theme
root: topBar.root
}
NotificationButton {
theme: topBar.theme
root: topBar.root
}
}
}
}

111
Widgets/TopBarSimple.qml Normal file
View File

@@ -0,0 +1,111 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Services.SystemTray
PanelWindow {
id: topBar
property var theme
property var root
anchors {
top: true
left: true
right: true
}
implicitHeight: theme.barHeight
color: "transparent"
Rectangle {
anchors.fill: parent
color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.95)
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
border.width: 1
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08)
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
to: 0.12
duration: theme.extraLongDuration
easing.type: theme.standardEasing
}
NumberAnimation {
to: 0.06
duration: theme.extraLongDuration
easing.type: theme.standardEasing
}
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: theme.spacingL
anchors.rightMargin: theme.spacingL
// Left section - Apps and Workspace Switcher
Row {
id: leftSection
height: parent.height
spacing: theme.spacingL
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
AppLauncherButton {
theme: topBar.theme
root: topBar.root
}
WorkspaceSwitcher {
theme: topBar.theme
root: topBar.root
}
}
// Center section - Clock/Media Player
ClockWidget {
id: clockWidget
theme: topBar.theme
root: topBar.root
anchors.centerIn: parent
}
// Right section - System controls
Row {
id: rightSection
height: parent.height
spacing: theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
ClipboardButton {
theme: topBar.theme
root: topBar.root
}
ColorPickerButton {
theme: topBar.theme
root: topBar.root
}
NotificationButton {
theme: topBar.theme
root: topBar.root
}
}
}
}

View File

@@ -0,0 +1,145 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
Rectangle {
id: workspaceSwitcher
property var theme
property var root
width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2)
height: 32
radius: theme.cornerRadiusLarge
color: Qt.rgba(theme.surfaceContainerHigh.r, theme.surfaceContainerHigh.g, theme.surfaceContainerHigh.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
property int currentWorkspace: 1
property var workspaceList: []
Process {
id: workspaceQuery
command: ["niri", "msg", "workspaces"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
workspaceSwitcher.parseWorkspaceOutput(data.trim())
}
}
}
}
function parseWorkspaceOutput(data) {
const lines = data.split('\n')
let currentOutputName = ""
let focusedOutput = ""
let focusedWorkspace = 1
let outputWorkspaces = {}
for (const line of lines) {
if (line.startsWith('Output "')) {
const outputMatch = line.match(/Output "(.+)"/)
if (outputMatch) {
currentOutputName = outputMatch[1]
outputWorkspaces[currentOutputName] = []
}
continue
}
if (line.trim() && line.match(/^\s*\*?\s*(\d+)$/)) {
const wsMatch = line.match(/^\s*(\*?)\s*(\d+)$/)
if (wsMatch) {
const isActive = wsMatch[1] === '*'
const wsNum = parseInt(wsMatch[2])
if (currentOutputName && outputWorkspaces[currentOutputName]) {
outputWorkspaces[currentOutputName].push(wsNum)
}
if (isActive) {
focusedOutput = currentOutputName
focusedWorkspace = wsNum
}
}
}
}
currentWorkspace = focusedWorkspace
if (focusedOutput && outputWorkspaces[focusedOutput]) {
workspaceList = outputWorkspaces[focusedOutput]
} else {
workspaceList = [1, 2]
}
}
Timer {
interval: 500
running: true
repeat: true
onTriggered: {
workspaceQuery.running = true
}
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: theme.spacingS
Repeater {
model: workspaceSwitcher.workspaceList
Rectangle {
property bool isActive: modelData === workspaceSwitcher.currentWorkspace
property bool isHovered: mouseArea.containsMouse
width: isActive ? theme.spacingXL + theme.spacingS : theme.spacingL
height: theme.spacingS
radius: height / 2
color: isActive ? theme.primary :
isHovered ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.5) :
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.3)
Behavior on width {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()]
switchProcess.running = true
workspaceSwitcher.currentWorkspace = modelData
Qt.callLater(() => {
workspaceQuery.running = true
})
}
}
}
}
}
Process {
id: switchProcess
running: false
}
}

9
Widgets/qmldir Normal file
View File

@@ -0,0 +1,9 @@
TopBar 1.0 TopBar.qml
TopBarSimple 1.0 TopBarSimple.qml
AppLauncherButton 1.0 AppLauncherButton.qml
WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml
ClockWidget 1.0 ClockWidget.qml
SystemTrayWidget 1.0 SystemTrayWidget.qml
ClipboardButton 1.0 ClipboardButton.qml
ColorPickerButton 1.0 ColorPickerButton.qml
NotificationButton 1.0 NotificationButton.qml

274
shell-refactored.qml Normal file
View File

@@ -0,0 +1,274 @@
//@ pragma UseQApplication
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import "Services"
import "Widgets"
ShellRoot {
id: root
property bool calendarVisible: false
property bool showTrayMenu: false
property real trayMenuX: 0
property real trayMenuY: 0
property var currentTrayMenu: null
property var currentTrayItem: null
property bool notificationHistoryVisible: false
property var activeNotification: null
property bool showNotificationPopup: false
property bool mediaPlayerVisible: false
property MprisPlayer activePlayer: MprisController.activePlayer
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
property bool useFahrenheit: true
property var weather: WeatherService.weather
property string osLogo: OSDetectionService.osLogo
property string osName: OSDetectionService.osName
property var notificationHistory: notificationHistoryModel
property var appLauncher: appLauncherPopup
property var clipboardHistoryPopup: clipboardHistoryPopupInstance
property var colorPickerProcess: colorPickerProcessInstance
property var weatherIcons: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "rainy",
"359": "weather_hail",
"362": "rainy",
"365": "rainy",
"368": "cloudy_snowing",
"371": "snowing",
"374": "rainy",
"377": "rainy",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "thunderstorm",
"395": "snowing"
})
QtObject {
id: theme
property color primary: "#D0BCFF"
property color primaryText: "#381E72"
property color primaryContainer: "#4F378B"
property color secondary: "#CCC2DC"
property color surface: "#10121E"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color surfaceTint: "#D0BCFF"
property color background: "#10121E"
property color backgroundText: "#E6E0E9"
property color outline: "#938F99"
property color surfaceContainer: "#1D1B20"
property color surfaceContainerHigh: "#2B2930"
property color archBlue: "#1793D1"
property color success: "#4CAF50"
property color warning: "#FF9800"
property color info: "#2196F3"
property color error: "#F2B8B5"
property int shortDuration: 150
property int mediumDuration: 300
property int longDuration: 500
property int extraLongDuration: 1000
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
property real cornerRadius: 12
property real cornerRadiusSmall: 8
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeSmall: 12
property real fontSizeMedium: 14
property real fontSizeLarge: 16
property real fontSizeXLarge: 20
property real barHeight: 48
property real iconSize: 24
property real iconSizeSmall: 16
property real iconSizeLarge: 32
property real opacityDisabled: 0.38
property real opacityMedium: 0.60
property real opacityHigh: 0.87
property real opacityFull: 1.0
property string iconFont: "Material Symbols Rounded"
property string iconFontFilled: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int iconFontFilledWeight: Font.Medium
}
TopBar {
id: topBar
theme: root.theme
root: root
}
AppLauncher {
id: appLauncherPopup
theme: root.theme
}
ClipboardHistory {
id: clipboardHistoryPopupInstance
theme: root.theme
}
MediaPlayer {
id: mediaPlayer
theme: root.theme
isVisible: root.mediaPlayerVisible
}
Process {
id: colorPickerProcessInstance
command: ["hyprpicker", "-a"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker")
}
}
}
NotificationServer {
id: notificationServer
actionsSupported: true
bodyMarkupSupported: true
imageSupported: true
keepOnReload: false
persistenceSupported: true
onNotification: (notification) => {
if (!notification || !notification.id) return
if (!notification.appName && !notification.summary && !notification.body) {
return
}
console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary")
var notifObj = {
"id": notification.id,
"appName": notification.appName || "App",
"summary": notification.summary || "",
"body": notification.body || "",
"timestamp": new Date(),
"appIcon": notification.appIcon || notification.icon || "",
"icon": notification.icon || "",
"image": notification.image || ""
}
notificationHistoryModel.insert(0, notifObj)
while (notificationHistoryModel.count > 50) {
notificationHistoryModel.remove(notificationHistoryModel.count - 1)
}
root.activeNotification = notifObj
root.showNotificationPopup = true
notificationTimer.restart()
}
}
ListModel {
id: notificationHistoryModel
}
Timer {
id: notificationTimer
interval: 5000
repeat: false
onTriggered: hideNotificationPopup()
}
Timer {
id: clearNotificationTimer
interval: theme.mediumDuration + 50
repeat: false
onTriggered: root.activeNotification = null
}
function showNotificationPopup(notification) {
root.activeNotification = notification
root.showNotificationPopup = true
notificationTimer.restart()
}
function hideNotificationPopup() {
root.showNotificationPopup = false
notificationTimer.stop()
clearNotificationTimer.restart()
}
Timer {
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
interval: 1000
repeat: true
onTriggered: {
if (root.activePlayer) {
root.activePlayer.positionChanged()
}
}
}
Component.onCompleted: {
console.log("DankMaterialDark shell loaded successfully!")
}
}

269
shell-test.qml Normal file
View File

@@ -0,0 +1,269 @@
//@ pragma UseQApplication
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import "Services"
import "Widgets"
ShellRoot {
id: root
property bool calendarVisible: false
property bool showTrayMenu: false
property real trayMenuX: 0
property real trayMenuY: 0
property var currentTrayMenu: null
property var currentTrayItem: null
property bool notificationHistoryVisible: false
property var activeNotification: null
property bool showNotificationPopup: false
property bool mediaPlayerVisible: false
property MprisPlayer activePlayer: MprisController.activePlayer
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
property bool useFahrenheit: true
property var weather: WeatherService.weather
property string osLogo: OSDetectionService.osLogo
property string osName: OSDetectionService.osName
property var notificationHistory: notificationHistoryModel
property var appLauncher: appLauncherPopup
property var clipboardHistoryPopup: clipboardHistoryPopupInstance
property var colorPickerProcess: colorPickerProcessInstance
property var weatherIcons: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "rainy",
"359": "weather_hail",
"362": "rainy",
"365": "rainy",
"368": "cloudy_snowing",
"371": "snowing",
"374": "rainy",
"377": "rainy",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "thunderstorm",
"395": "snowing"
})
QtObject {
id: theme
property color primary: "#D0BCFF"
property color primaryText: "#381E72"
property color primaryContainer: "#4F378B"
property color secondary: "#CCC2DC"
property color surface: "#10121E"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color surfaceTint: "#D0BCFF"
property color background: "#10121E"
property color backgroundText: "#E6E0E9"
property color outline: "#938F99"
property color surfaceContainer: "#1D1B20"
property color surfaceContainerHigh: "#2B2930"
property color archBlue: "#1793D1"
property color success: "#4CAF50"
property color warning: "#FF9800"
property color info: "#2196F3"
property color error: "#F2B8B5"
property int shortDuration: 150
property int mediumDuration: 300
property int longDuration: 500
property int extraLongDuration: 1000
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
property real cornerRadius: 12
property real cornerRadiusSmall: 8
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeSmall: 12
property real fontSizeMedium: 14
property real fontSizeLarge: 16
property real fontSizeXLarge: 20
property real barHeight: 48
property real iconSize: 24
property real iconSizeSmall: 16
property real iconSizeLarge: 32
property real opacityDisabled: 0.38
property real opacityMedium: 0.60
property real opacityHigh: 0.87
property real opacityFull: 1.0
property string iconFont: "Material Symbols Rounded"
property string iconFontFilled: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int iconFontFilledWeight: Font.Medium
}
TopBarSimple {
id: topBar
theme: root.theme
root: root
}
AppLauncher {
id: appLauncherPopup
theme: root.theme
}
ClipboardHistory {
id: clipboardHistoryPopupInstance
theme: root.theme
}
Process {
id: colorPickerProcessInstance
command: ["hyprpicker", "-a"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker")
}
}
}
NotificationServer {
id: notificationServer
actionsSupported: true
bodyMarkupSupported: true
imageSupported: true
keepOnReload: false
persistenceSupported: true
onNotification: (notification) => {
if (!notification || !notification.id) return
if (!notification.appName && !notification.summary && !notification.body) {
return
}
console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary")
var notifObj = {
"id": notification.id,
"appName": notification.appName || "App",
"summary": notification.summary || "",
"body": notification.body || "",
"timestamp": new Date(),
"appIcon": notification.appIcon || notification.icon || "",
"icon": notification.icon || "",
"image": notification.image || ""
}
notificationHistoryModel.insert(0, notifObj)
while (notificationHistoryModel.count > 50) {
notificationHistoryModel.remove(notificationHistoryModel.count - 1)
}
root.activeNotification = notifObj
root.showNotificationPopup = true
notificationTimer.restart()
}
}
ListModel {
id: notificationHistoryModel
}
Timer {
id: notificationTimer
interval: 5000
repeat: false
onTriggered: hideNotificationPopup()
}
Timer {
id: clearNotificationTimer
interval: theme.mediumDuration + 50
repeat: false
onTriggered: root.activeNotification = null
}
function showNotificationPopup(notification) {
root.activeNotification = notification
root.showNotificationPopup = true
notificationTimer.restart()
}
function hideNotificationPopup() {
root.showNotificationPopup = false
notificationTimer.stop()
clearNotificationTimer.restart()
}
Timer {
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
interval: 1000
repeat: true
onTriggered: {
if (root.activePlayer) {
root.activePlayer.positionChanged()
}
}
}
Component.onCompleted: {
console.log("DankMaterialDark shell loaded successfully!")
}
}

2246
shell-working.qml Normal file

File diff suppressed because it is too large Load Diff

4770
shell.qml Normal file

File diff suppressed because it is too large Load Diff