feat: added widgets capture, music visualiser, bluetooth, notes
This commit is contained in:
+277
-23
@@ -4,6 +4,8 @@ import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import "./services"
|
||||
import "./overlay"
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
@@ -22,7 +24,7 @@ PluginComponent {
|
||||
"icon": "volume_up",
|
||||
"width": 430,
|
||||
"height": 470,
|
||||
"x": 0.04,
|
||||
"x": 0.03,
|
||||
"y": 0.14,
|
||||
"defaultVisible": true
|
||||
},
|
||||
@@ -32,18 +34,28 @@ PluginComponent {
|
||||
"icon": "tune",
|
||||
"width": 400,
|
||||
"height": 360,
|
||||
"x": 0.30,
|
||||
"x": 0.3,
|
||||
"y": 0.14,
|
||||
"defaultVisible": true
|
||||
},
|
||||
{
|
||||
"id": "capture",
|
||||
"title": "Capture",
|
||||
"icon": "videocam",
|
||||
"width": 380,
|
||||
"height": 230,
|
||||
"x": 0.03,
|
||||
"y": 0.62,
|
||||
"defaultVisible": false
|
||||
},
|
||||
{
|
||||
"id": "media",
|
||||
"title": "Media",
|
||||
"icon": "music_note",
|
||||
"width": 430,
|
||||
"height": 300,
|
||||
"x": 0.04,
|
||||
"y": 0.56,
|
||||
"x": 0.3,
|
||||
"y": 0.52,
|
||||
"defaultVisible": true
|
||||
},
|
||||
{
|
||||
@@ -52,8 +64,38 @@ PluginComponent {
|
||||
"icon": "home",
|
||||
"width": 420,
|
||||
"height": 420,
|
||||
"x": 0.56,
|
||||
"y": 0.50,
|
||||
"x": 0.57,
|
||||
"y": 0.14,
|
||||
"defaultVisible": false
|
||||
},
|
||||
{
|
||||
"id": "visualizer",
|
||||
"title": "Visualiser",
|
||||
"icon": "graphic_eq",
|
||||
"width": 360,
|
||||
"height": 180,
|
||||
"x": 0.3,
|
||||
"y": 0.79,
|
||||
"defaultVisible": false
|
||||
},
|
||||
{
|
||||
"id": "bluetooth",
|
||||
"title": "Bluetooth",
|
||||
"icon": "bluetooth",
|
||||
"width": 380,
|
||||
"height": 300,
|
||||
"x": 0.8,
|
||||
"y": 0.14,
|
||||
"defaultVisible": false
|
||||
},
|
||||
{
|
||||
"id": "notepad",
|
||||
"title": "Notes",
|
||||
"icon": "edit_note",
|
||||
"width": 400,
|
||||
"height": 320,
|
||||
"x": 0.57,
|
||||
"y": 0.58,
|
||||
"defaultVisible": false
|
||||
},
|
||||
{
|
||||
@@ -62,8 +104,8 @@ PluginComponent {
|
||||
"icon": "speed",
|
||||
"width": 380,
|
||||
"height": 330,
|
||||
"x": 0.56,
|
||||
"y": 0.14,
|
||||
"x": 0.8,
|
||||
"y": 0.45,
|
||||
"defaultVisible": false
|
||||
}
|
||||
]
|
||||
@@ -100,39 +142,185 @@ PluginComponent {
|
||||
// ── Positions ────────────────────────────────────────────────────────────
|
||||
// Stored per screen as fractions of the screen size, so the same layout
|
||||
// survives monitors of different resolutions (and is re-clamped on load).
|
||||
// ── Brightness device mapping ────────────────────────────────────────────
|
||||
// DisplayService only knows DDC devices as i2c bus ids, and the shell maps
|
||||
// them to screens through manual pins. "ddcutil detect" reports the DRM
|
||||
// connector for each bus, which is the Wayland output name, so the mapping
|
||||
// can be worked out once and cached.
|
||||
property var brightnessMap: ({})
|
||||
property bool brightnessMapScanning: false
|
||||
|
||||
// Picks the cached map up as soon as plugin data arrives; surfaces are
|
||||
// created before the daemon registers, so they cannot drive this.
|
||||
readonly property var storedBrightnessMap: (pluginData && pluginData.brightnessMap && typeof pluginData.brightnessMap === "object") ? pluginData.brightnessMap : ({})
|
||||
|
||||
onStoredBrightnessMapChanged: {
|
||||
if (Object.keys(root.storedBrightnessMap).length > 0)
|
||||
root.brightnessMap = root.storedBrightnessMap;
|
||||
|
||||
root.ensureBrightnessMap();
|
||||
}
|
||||
|
||||
function brightnessDeviceForScreen(screenName) {
|
||||
if (!screenName)
|
||||
return "";
|
||||
|
||||
// A pin set in the shell's own UI always wins.
|
||||
const pins = (typeof CacheData !== "undefined" && CacheData.brightnessDevicePins) ? CacheData.brightnessDevicePins : {};
|
||||
const screen = Quickshell.screens.find(s => s.name === screenName);
|
||||
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
|
||||
if (pins[pinKey] && DisplayService.devices.some(d => d.id === pins[pinKey]))
|
||||
return pins[pinKey];
|
||||
|
||||
const mapped = root.brightnessMap[screenName];
|
||||
if (mapped && DisplayService.devices.some(d => d.id === mapped))
|
||||
return mapped;
|
||||
|
||||
const backlight = DisplayService.devices.find(d => d.class === "backlight");
|
||||
if (backlight)
|
||||
return backlight.id;
|
||||
|
||||
const ddc = DisplayService.devices.find(d => d.class === "ddc");
|
||||
return ddc ? ddc.id : "";
|
||||
}
|
||||
|
||||
// The shell's own Control Center brightness slider resolves its device from
|
||||
// per-screen pins, so publishing the detected map there makes the built-in
|
||||
// slider (and its icon menu) follow whichever monitor the panel is on.
|
||||
function syncBrightnessPins() {
|
||||
if (pluginData.autoPinBrightness === false)
|
||||
return;
|
||||
|
||||
if (typeof CacheData === "undefined" || !CacheData)
|
||||
return;
|
||||
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
|
||||
let changed = false;
|
||||
|
||||
for (const screen of Quickshell.screens) {
|
||||
const device = root.brightnessMap[screen.name];
|
||||
if (!device)
|
||||
continue;
|
||||
|
||||
const key = SettingsData.getScreenDisplayName(screen) || screen.name;
|
||||
// Never overwrite a pin the user set by hand.
|
||||
if (pins[key])
|
||||
continue;
|
||||
|
||||
pins[key] = device;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
CacheData.set("brightnessDevicePins", pins);
|
||||
}
|
||||
|
||||
function ensureBrightnessMap() {
|
||||
if (root.brightnessMapScanning)
|
||||
return;
|
||||
|
||||
if (Object.keys(root.brightnessMap).length === 0 && Object.keys(root.storedBrightnessMap).length > 0)
|
||||
root.brightnessMap = root.storedBrightnessMap;
|
||||
|
||||
const screens = Quickshell.screens.map(s => s.name);
|
||||
const missing = screens.filter(name => !root.brightnessMap[name]);
|
||||
if (missing.length === 0) {
|
||||
root.syncBrightnessPins();
|
||||
return;
|
||||
}
|
||||
|
||||
root.brightnessMapScanning = true;
|
||||
Proc.runCommand("gameBar.ddcDetect", ["ddcutil", "detect", "--brief"], (stdout, exitCode) => {
|
||||
root.brightnessMapScanning = false;
|
||||
if (exitCode !== 0)
|
||||
return;
|
||||
|
||||
const map = {};
|
||||
let bus = "";
|
||||
for (const line of String(stdout || "").split("\n")) {
|
||||
const busMatch = line.match(/I2C bus:\s*\/dev\/i2c-([0-9]+)/);
|
||||
if (busMatch) {
|
||||
bus = "ddc:i2c-" + busMatch[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
const connectorMatch = line.match(/DRM connector:\s*card[0-9]+-(.+)$/);
|
||||
if (connectorMatch && bus !== "") {
|
||||
map[connectorMatch[1].trim()] = bus;
|
||||
bus = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(map).length === 0)
|
||||
return;
|
||||
|
||||
root.brightnessMap = map;
|
||||
root.savePluginValue("brightnessMap", map);
|
||||
root.syncBrightnessPins();
|
||||
}, 0, 20000);
|
||||
}
|
||||
|
||||
function focusedMonitorName() {
|
||||
const screen = CompositorService.getFocusedScreen();
|
||||
return (screen && screen.name) ? screen.name : "focused";
|
||||
}
|
||||
|
||||
// Positions are stored per monitor *and* per resolution: a layout arranged at
|
||||
// 4K means nothing at 1440p, and switching back must restore the original
|
||||
// arrangement rather than a rescaled approximation of the other one.
|
||||
function screenKey(screen) {
|
||||
if (!screen)
|
||||
return "default";
|
||||
|
||||
return SettingsData.getScreenDisplayName(screen) || screen.name || "default";
|
||||
const base = SettingsData.getScreenDisplayName(screen) || screen.name || "default";
|
||||
const w = Math.round(screen.width);
|
||||
const h = Math.round(screen.height);
|
||||
return (w > 0 && h > 0) ? base + "@" + w + "x" + h : base;
|
||||
}
|
||||
|
||||
function widgetPosition(screen, widgetId) {
|
||||
function legacyScreenKey(screen) {
|
||||
if (!screen)
|
||||
return "";
|
||||
|
||||
return SettingsData.getScreenDisplayName(screen) || screen.name || "";
|
||||
}
|
||||
|
||||
// Returns absolute pixel coordinates for the given panel size.
|
||||
function widgetPosition(screen, widgetId, panelWidth, panelHeight) {
|
||||
const layout = pluginData.layout || {};
|
||||
const perScreen = layout[root.screenKey(screen)];
|
||||
const saved = perScreen ? perScreen[widgetId] : null;
|
||||
if (saved && saved.x !== undefined && saved.y !== undefined)
|
||||
return saved;
|
||||
return {
|
||||
"x": saved.x,
|
||||
"y": saved.y
|
||||
};
|
||||
|
||||
// Entries written before 2.5.0 were fractions keyed by screen name only.
|
||||
const legacyPerScreen = layout[root.legacyScreenKey(screen)];
|
||||
const legacy = legacyPerScreen ? legacyPerScreen[widgetId] : null;
|
||||
if (legacy && legacy.x !== undefined && legacy.y !== undefined && legacy.x <= 1 && legacy.y <= 1)
|
||||
return {
|
||||
"x": legacy.x * panelWidth,
|
||||
"y": legacy.y * panelHeight
|
||||
};
|
||||
|
||||
const def = widgetDef(widgetId);
|
||||
return def ? ({
|
||||
"x": def.x,
|
||||
"y": def.y
|
||||
}) : ({
|
||||
"x": 0.1,
|
||||
"y": 0.15
|
||||
});
|
||||
return {
|
||||
"x": (def ? def.x : 0.1) * panelWidth,
|
||||
"y": (def ? def.y : 0.15) * panelHeight
|
||||
};
|
||||
}
|
||||
|
||||
function saveWidgetPosition(screen, widgetId, fractionX, fractionY) {
|
||||
function saveWidgetPosition(screen, widgetId, x, y) {
|
||||
const layout = JSON.parse(JSON.stringify(pluginData.layout || {}));
|
||||
const key = root.screenKey(screen);
|
||||
if (!layout[key])
|
||||
layout[key] = {};
|
||||
|
||||
layout[key][widgetId] = {
|
||||
"x": fractionX,
|
||||
"y": fractionY
|
||||
"x": Math.round(x),
|
||||
"y": Math.round(y)
|
||||
};
|
||||
root.savePluginValue("layout", layout);
|
||||
}
|
||||
@@ -207,6 +395,52 @@ PluginComponent {
|
||||
return "SUCCESS";
|
||||
}
|
||||
|
||||
function record() : string {
|
||||
// The recorder stops asynchronously, so report the action taken.
|
||||
const wasRecording = capture.recording;
|
||||
capture.toggleRecording(root.focusedMonitorName());
|
||||
return wasRecording ? "STOPPED" : "RECORDING";
|
||||
}
|
||||
|
||||
function replay() : string {
|
||||
const wasArmed = capture.replayArmed;
|
||||
capture.toggleReplay(root.focusedMonitorName());
|
||||
return wasArmed ? "STOPPED" : "ARMED";
|
||||
}
|
||||
|
||||
function saveReplay() : string {
|
||||
if (!capture.replayArmed)
|
||||
return "REPLAY_NOT_ARMED";
|
||||
|
||||
capture.saveReplay();
|
||||
return "SUCCESS";
|
||||
}
|
||||
|
||||
function layoutStatus() : string {
|
||||
const lines = Quickshell.screens.map(screen => {
|
||||
const key = root.screenKey(screen);
|
||||
const parts = root.widgetDefs.map(def => {
|
||||
const pos = root.widgetPosition(screen, def.id, screen.width, screen.height);
|
||||
return def.id + "(" + Math.round(pos.x) + "," + Math.round(pos.y) + ")";
|
||||
});
|
||||
return key + ": " + parts.join(" ");
|
||||
});
|
||||
return lines.join(" | ");
|
||||
}
|
||||
|
||||
function brightnessStatus() : string {
|
||||
const pins = (typeof CacheData !== "undefined" && CacheData.brightnessDevicePins) ? CacheData.brightnessDevicePins : {};
|
||||
const perScreen = Quickshell.screens.map(screen => {
|
||||
const key = SettingsData.getScreenDisplayName(screen) || screen.name;
|
||||
return screen.name + "[key=" + key + " device=" + root.brightnessDeviceForScreen(screen.name) + " value=" + DisplayService.getDeviceBrightness(root.brightnessDeviceForScreen(screen.name)) + "]";
|
||||
});
|
||||
return "map=" + JSON.stringify(root.brightnessMap) + " pins=" + JSON.stringify(pins) + " screens=" + perScreen.join(" ") + " currentDevice=" + DisplayService.currentDevice;
|
||||
}
|
||||
|
||||
function captureStatus() : string {
|
||||
return "mode=" + (capture.mode || "idle") + " monitor=" + (capture.targetMonitor || "-") + " elapsed=" + capture.elapsedSeconds + "s dir=" + capture.saveDirectory + " lastFile=" + (capture.lastFile || "(none)") + " error=" + (capture.lastError || "(none)");
|
||||
}
|
||||
|
||||
function homeStatus() : string {
|
||||
const picked = Array.isArray(root.pluginData.haEntities) ? root.pluginData.haEntities.length : 0;
|
||||
return "configured=" + haService.configured + " url=" + (haService.baseUrl || "(none)") + " tokenChars=" + haService.token.length + " http=" + haService.lastStatus + " entities=" + haService.entityList.length + " picked=" + picked + " error=" + (haService.lastError || "(none)");
|
||||
@@ -224,20 +458,40 @@ PluginComponent {
|
||||
}
|
||||
|
||||
readonly property alias homeService: haService
|
||||
readonly property alias captureService: capture
|
||||
|
||||
GameBarHomeService {
|
||||
CaptureService {
|
||||
id: capture
|
||||
|
||||
daemon: root
|
||||
onNotice: message => {
|
||||
if (typeof ToastService !== "undefined" && ToastService)
|
||||
ToastService.showInfo(message);
|
||||
}
|
||||
}
|
||||
|
||||
HomeService {
|
||||
id: haService
|
||||
|
||||
daemon: root
|
||||
}
|
||||
|
||||
GameBarOverlay {
|
||||
Connections {
|
||||
target: Quickshell
|
||||
|
||||
function onScreensChanged() {
|
||||
root.ensureBrightnessMap();
|
||||
}
|
||||
}
|
||||
|
||||
OverlayModal {
|
||||
id: overlay
|
||||
|
||||
daemon: root
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.ensureBrightnessMap();
|
||||
if (pluginService && pluginId) {
|
||||
const instances = Object.assign({}, pluginService.pluginInstances);
|
||||
instances[pluginId] = root;
|
||||
|
||||
Reference in New Issue
Block a user