Merge branch 'dev' into npanel-refactor

This commit is contained in:
quadbyte 2025-08-20 10:37:49 -04:00
commit 524135800e
21 changed files with 1234 additions and 322 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

@ -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,31 @@ 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 +132,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 +152,75 @@ 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

@ -0,0 +1,184 @@
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()
}
}