Merge branch 'main' into miniplayer-eyecandy

This commit is contained in:
Lemmy 2025-08-20 20:40:17 -04:00 committed by GitHub
commit e51c5cf4bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 3481 additions and 3542 deletions

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
Singleton {
id: root
@ -34,7 +35,7 @@ Singleton {
readonly property alias muted: root._muted
property bool _muted: !!sink?.audio?.muted
readonly property real stepVolume: 0.05
readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
PwObjectTracker {
objects: [...root.sinks, ...root.sources]

View file

@ -27,6 +27,15 @@ Singleton {
return methods
}
// Global helpers for IPC and shortcuts
function increaseBrightness(): void {
monitors.forEach(m => m.increaseBrightness())
}
function decreaseBrightness(): void {
monitors.forEach(m => m.decreaseBrightness())
}
function getDetectedDisplays(): list<var> {
return detectedDisplays
}

View file

@ -37,7 +37,7 @@ Singleton {
Process {
id: process
stdinEnabled: true
running: (Settings.data.audio.visualizerType !== "none") && (PanelService.sidePanel.isLoaded || Settings.data.audio.showMiniplayerCava)
running: (Settings.data.audio.visualizerType !== "none") && (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava)
command: ["cava", "-p", "/dev/stdin"]
onExited: {
stdinEnabled = true

View file

@ -11,12 +11,40 @@ Singleton {
property var history: []
property bool initialized: false
property int maxHistory: 50 // Limit clipboard history entries
// Internal state
property bool _enabled: true
// Cached history file path
property string historyFile: Quickshell.env("NOCTALIA_CLIPBOARD_HISTORY_FILE")
|| (Settings.cacheDir + "clipboard.json")
// Persisted storage for clipboard history
property FileView historyFileView: FileView {
id: historyFileView
objectName: "clipboardHistoryFileView"
path: historyFile
watchChanges: false // We don't need to watch changes for clipboard
onAdapterUpdated: writeAdapter()
Component.onCompleted: reload()
onLoaded: loadFromHistory()
onLoadFailed: function (error) {
// Create file on first use
if (error.toString().includes("No such file") || error === 2) {
writeAdapter()
}
}
JsonAdapter {
id: historyAdapter
property var history: []
property double timestamp: 0
}
}
Timer {
interval: 1000
interval: 2000
repeat: true
running: root._enabled
onTriggered: root.refresh()
@ -32,14 +60,17 @@ Singleton {
if (exitCode === 0) {
currentTypes = String(stdout.text).trim().split('\n').filter(t => t)
// Always check for text first
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
textProcess.isLoading = true
textProcess.running = true
// Also check for images if available
const imageType = currentTypes.find(t => t.startsWith('image/'))
if (imageType) {
imageProcess.mimeType = imageType
imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]
imageProcess.running = true
} else {
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
textProcess.running = true
}
} else {
typeProcess.isLoading = false
@ -65,17 +96,32 @@ Singleton {
"timestamp": new Date().getTime()
}
// Check if this exact image already exists
const exists = root.history.find(item => item.type === 'image' && item.data === entry.data)
if (!exists) {
root.history = [entry, ...root.history].slice(0, 20)
// Normalize existing history and add the new image
const normalizedHistory = root.history.map(item => {
if (typeof item === 'string') {
return {
"type": 'text',
"content": item,
"timestamp": new Date().getTime(
) - 1000 // Make it slightly older
}
}
return item
})
root.history = [entry, ...normalizedHistory].slice(0, maxHistory)
saveHistory()
}
}
}
// Always mark as initialized when done
if (!textProcess.isLoading) {
root.initialized = true
typeProcess.isLoading = false
}
typeProcess.isLoading = false
}
stdout: StdioCollector {}
@ -87,15 +133,18 @@ Singleton {
property bool isLoading: false
onExited: (exitCode, exitStatus) => {
textProcess.isLoading = false
if (exitCode === 0) {
const content = String(stdout.text).trim()
if (content) {
if (content && content.length > 0) {
const entry = {
"type": 'text',
"content": content,
"timestamp": new Date().getTime()
}
// Check if this exact text content already exists
const exists = root.history.find(item => {
if (item.type === 'text') {
return item.content === content
@ -104,36 +153,76 @@ Singleton {
})
if (!exists) {
const newHistory = root.history.map(item => {
if (typeof item === 'string') {
return {
"type": 'text',
"content": item,
"timestamp": new Date().getTime()
}
}
return item
})
// Normalize existing history entries
const normalizedHistory = root.history.map(item => {
if (typeof item === 'string') {
return {
"type": 'text',
"content": item,
"timestamp": new Date().getTime(
) - 1000 // Make it slightly older
}
}
return item
})
root.history = [entry, ...newHistory].slice(0, 20)
root.history = [entry, ...normalizedHistory].slice(0, maxHistory)
saveHistory()
}
}
} else {
textProcess.isLoading = false
}
// Mark as initialized and clean up loading states
root.initialized = true
typeProcess.isLoading = false
if (!imageProcess.running) {
typeProcess.isLoading = false
}
}
stdout: StdioCollector {}
}
function refresh() {
if (!typeProcess.isLoading && !textProcess.isLoading) {
if (!typeProcess.isLoading && !textProcess.isLoading && !imageProcess.running) {
typeProcess.isLoading = true
typeProcess.command = ["wl-paste", "-l"]
typeProcess.running = true
}
}
function loadFromHistory() {
// Populate in-memory history from cached file
try {
const items = historyAdapter.history || []
root.history = items.slice(0, maxHistory) // Apply limit when loading
Logger.log("Clipboard", "Loaded", root.history.length, "entries from cache")
} catch (e) {
Logger.error("Clipboard", "Failed to load history:", e)
root.history = []
}
}
function saveHistory() {
try {
// Ensure we don't exceed the maximum history limit
const limitedHistory = root.history.slice(0, maxHistory)
historyAdapter.history = limitedHistory
historyAdapter.timestamp = Time.timestamp
// Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir])
Qt.callLater(function () {
historyFileView.writeAdapter()
})
} catch (e) {
Logger.error("Clipboard", "Failed to save history:", e)
}
}
function clearHistory() {
root.history = []
saveHistory()
}
}

View file

@ -396,6 +396,25 @@ Singleton {
}
}
// Get current workspace
function getCurrentWorkspace() {
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
if (ws.isFocused) {
return ws
}
}
return null
}
// Get focused window
function getFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
return windows[focusedWindowIndex]
}
return null
}
// Generic logout/shutdown commands
function logout() {
if (isHyprland) {
@ -415,22 +434,15 @@ Singleton {
}
}
// Get current workspace
function getCurrentWorkspace() {
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
if (ws.isFocused) {
return ws
}
}
return null
function shutdown() {
Quickshell.execDetached(["shutdown", "-h", "now"])
}
// Get focused window
function getFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
return windows[focusedWindowIndex]
}
return null
function reboot() {
Quickshell.execDetached(["reboot"])
}
function suspend() {
Quickshell.execDetached(["systemctl", "suspend"])
}
}

View file

@ -0,0 +1,183 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
Singleton {
id: root
property bool isInhibited: false
property string reason: "User requested"
property var activeInhibitors: []
// Different inhibitor strategies
property string strategy: "systemd" // "systemd", "wayland", or "auto"
Component.onCompleted: {
Logger.log("IdleInhibitor", "Service started")
detectStrategy()
// Restore previous state from settings
if (Settings.data.ui.idleInhibitorEnabled) {
addInhibitor("manual", "Restored from previous session")
Logger.log("IdleInhibitor", "Restored previous manual inhibition state")
}
}
// Auto-detect the best strategy
function detectStrategy() {
if (strategy === "auto") {
// Check if systemd-inhibit is available
try {
var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"])
strategy = "systemd"
Logger.log("IdleInhibitor", "Using systemd-inhibit strategy")
return
} catch (e) {
// systemd-inhibit not found, try Wayland tools
}
try {
var waylandResult = Quickshell.execDetached(["which", "wayhibitor"])
strategy = "wayland"
Logger.log("IdleInhibitor", "Using wayhibitor strategy")
return
} catch (e) {
// wayhibitor not found
}
Logger.warn("IdleInhibitor", "No suitable inhibitor found - will try systemd as fallback")
strategy = "systemd" // Fallback to systemd even if not detected
}
}
// Add an inhibitor
function addInhibitor(id, reason = "Application request") {
if (activeInhibitors.includes(id)) {
Logger.warn("IdleInhibitor", "Inhibitor already active:", id)
return false
}
activeInhibitors.push(id)
updateInhibition(reason)
Logger.log("IdleInhibitor", "Added inhibitor:", id)
return true
}
// Remove an inhibitor
function removeInhibitor(id) {
const index = activeInhibitors.indexOf(id)
if (index === -1) {
Logger.warn("IdleInhibitor", "Inhibitor not found:", id)
return false
}
activeInhibitors.splice(index, 1)
updateInhibition()
Logger.log("IdleInhibitor", "Removed inhibitor:", id)
return true
}
// Update the actual system inhibition
function updateInhibition(newReason = reason) {
const shouldInhibit = activeInhibitors.length > 0
if (shouldInhibit === isInhibited) {
return
// No change needed
}
if (shouldInhibit) {
startInhibition(newReason)
} else {
stopInhibition()
}
}
// Start system inhibition
function startInhibition(newReason) {
reason = newReason
if (strategy === "systemd") {
startSystemdInhibition()
} else if (strategy === "wayland") {
startWaylandInhibition()
} else {
Logger.warn("IdleInhibitor", "No inhibition strategy available")
return
}
isInhibited = true
Logger.log("IdleInhibitor", "Started inhibition:", reason)
}
// Stop system inhibition
function stopInhibition() {
if (!isInhibited)
return
if (inhibitorProcess.running) {
inhibitorProcess.signal(15) // SIGTERM
}
isInhibited = false
Logger.log("IdleInhibitor", "Stopped inhibition")
}
// Systemd inhibition using systemd-inhibit
function startSystemdInhibition() {
inhibitorProcess.command = ["systemd-inhibit", "--what=idle:sleep:handle-lid-switch", "--why="
+ reason, "--mode=block", "sleep", "infinity"]
inhibitorProcess.running = true
}
// Wayland inhibition using wayhibitor or similar
function startWaylandInhibition() {
inhibitorProcess.command = ["wayhibitor"]
inhibitorProcess.running = true
}
// Process for maintaining the inhibition
Process {
id: inhibitorProcess
running: false
onExited: function (exitCode, exitStatus) {
if (isInhibited) {
Logger.warn("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode)
isInhibited = false
}
}
onStarted: function () {
Logger.log("IdleInhibitor", "Inhibitor process started successfully")
}
}
// Manual toggle for user control
function manualToggle() {
if (activeInhibitors.includes("manual")) {
removeInhibitor("manual")
Settings.data.ui.idleInhibitorEnabled = false
ToastService.showNotice("Keep Awake", "Disabled", false, 3000)
Logger.log("IdleInhibitor", "Manual inhibition disabled and saved to settings")
return false
} else {
addInhibitor("manual", "Manually activated by user")
Settings.data.ui.idleInhibitorEnabled = true
ToastService.showNotice("Keep Awake", "Enabled", false, 3000)
Logger.log("IdleInhibitor", "Manual inhibition enabled and saved to settings")
return true
}
}
// Clean up on shutdown
Component.onDestruction: {
stopInhibition()
}
}

View file

@ -5,8 +5,16 @@ import Quickshell
Singleton {
id: root
// A ref. to the sidePanel, so it's accessible from other services
property var sidePanel: null
// Currently opened panel
property var openedPanel: null
property var sidePanel: null
function registerOpen(panel) {
if (openedPanel && openedPanel != panel) {
openedPanel.close()
}
openedPanel = panel
}
}

View file

@ -9,7 +9,15 @@ Singleton {
// -------------------------------------------
// Manual scaling via Settings
function scale(aScreen) {
return scaleByName(aScreen.name)
try {
if (aScreen !== undefined && aScreen.name !== undefined) {
return scaleByName(aScreen.name)
}
} catch (e) {
//Logger.warn(e)
}
return 1.0
}
function scaleByName(aScreenName) {