Merge branch 'main' into miniplayer-eyecandy
This commit is contained in:
commit
e51c5cf4bd
56 changed files with 3481 additions and 3542 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
183
Services/IdleInhibitorService.qml
Normal file
183
Services/IdleInhibitorService.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue