Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f7f5aef8a |
No files matched your search
+1
-1
@@ -17,7 +17,7 @@ return {
|
|||||||
-- results appear as you type; a dmenu could only filter a list it was
|
-- results appear as you type; a dmenu could only filter a list it was
|
||||||
-- handed once, which made searching a two-step. It spawns its own terminal
|
-- handed once, which made searching a two-step. It spawns its own terminal
|
||||||
-- (class pdfs-prompt, floated in lua/rules/windows.lua).
|
-- (class pdfs-prompt, floated in lua/rules/windows.lua).
|
||||||
pdfs = "pdfs-prompt --fzf",
|
pdfs = "pdfs-prompt --gtk",
|
||||||
|
|
||||||
-- bluetooth menu
|
-- bluetooth menu
|
||||||
bluetooth = "fluxo bt menu",
|
bluetooth = "fluxo bt menu",
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ hl.on("hyprland.start", function()
|
|||||||
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
||||||
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
||||||
|
|
||||||
|
-- pdfs-prompt has no hidden/resident mode, so the first search of the
|
||||||
|
-- session is cold. Run one throwaway query once the Proton Drive daemon
|
||||||
|
-- is up to pull the binary into the page cache and warm the daemon's index.
|
||||||
|
hl.exec_cmd("sh -c 'sleep 15; pdfs-prompt --feed a >/dev/null 2>&1'")
|
||||||
|
|
||||||
-- hl.exec_cmd("uwsm app -- nextcloud --background")
|
-- hl.exec_cmd("uwsm app -- nextcloud --background")
|
||||||
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
|
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
|
||||||
-- hl.exec_cmd("uwsm app -- protonvpn-app")
|
-- hl.exec_cmd("uwsm app -- protonvpn-app")
|
||||||
|
|||||||
@@ -12,3 +12,7 @@ shell("ask", { key = mod .. " + A", description = "ask claude" })
|
|||||||
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
|
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
|
||||||
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
|
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
|
||||||
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
|
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
|
||||||
|
|
||||||
|
-- Not a shell call, but it lives with the other session toggles.
|
||||||
|
local mirror = require("lua/mirror")
|
||||||
|
hl.bind(mod .. " + SHIFT + P", mirror.toggle, { description = "mirror laptop to all screens" })
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
-- Presentation mode: every external output shows the laptop panel.
|
||||||
|
--
|
||||||
|
-- Toggled from lua/binds/shell.lua. Mirroring is just a `mirror` field on the
|
||||||
|
-- monitor rule, so switching it off clears that field and re-applies the specs
|
||||||
|
-- from lua/monitors.lua to put the Samsung back where it belongs.
|
||||||
|
--
|
||||||
|
-- Anything plugged in while mirroring is on gets mirrored too, which is the
|
||||||
|
-- usual order at a talk: press the key, then find the HDMI cable.
|
||||||
|
|
||||||
|
local monitors = require("lua/monitors")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local function notify(text)
|
||||||
|
hl.notification.create({ text = text, timeout = 3000 })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The panel is off while docked with the lid shut, see lua/lid.lua. Mirroring
|
||||||
|
-- a disabled output would leave every screen black.
|
||||||
|
local function builtin()
|
||||||
|
for _, mon in ipairs(hl.get_monitors()) do
|
||||||
|
if mon.name == monitors.laptop then
|
||||||
|
return mon
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A mirroring output drops out of hl.get_monitors(), so the only place left
|
||||||
|
-- to find it is the panel's own `mirrors` list. That list is also the state:
|
||||||
|
-- a Lua flag would reset on every config reload while the outputs stay
|
||||||
|
-- mirrored.
|
||||||
|
local function mirroring(panel)
|
||||||
|
local names = {}
|
||||||
|
for _, mon in ipairs(panel and panel.mirrors or {}) do
|
||||||
|
table.insert(names, mon.name)
|
||||||
|
end
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Outputs keep their own mode while mirroring, and the panel image gets
|
||||||
|
-- rescaled to fit it every frame. The Dells at 1920x1200 flicker with that,
|
||||||
|
-- so match the panel's resolution; Hyprland falls back to the preferred mode
|
||||||
|
-- if the output cannot do it.
|
||||||
|
local function mirror_all(panel)
|
||||||
|
local mode = panel.width .. "x" .. panel.height
|
||||||
|
for _, mon in ipairs(monitors.externals_present()) do
|
||||||
|
hl.monitor({ output = mon.name, mirror = monitors.laptop, mode = mode, position = "auto" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The bar and popouts stay on the outputs they were created for, so
|
||||||
|
-- respawn the shell the same way lua/autostart.lua starts it.
|
||||||
|
local function restart_clients()
|
||||||
|
hl.exec_cmd("systemctl restart --user hyprpaper")
|
||||||
|
hl.exec_cmd("sh -c 'qs kill; exec uwsm app -- qs'")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- hl.monitor() only takes effect after the calling handler returns, so
|
||||||
|
-- anything that has to see the new layout goes on a timer.
|
||||||
|
local function later(fn)
|
||||||
|
hl.timer(fn, { timeout = 500, type = "oneshot" })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clearing `mirror` alone brings the image back, but Hyprland never
|
||||||
|
-- re-announces the output to clients: hyprpaper and Quickshell keep seeing
|
||||||
|
-- only the panel, however often they restart. Cycling the output through
|
||||||
|
-- disabled makes it a fresh wl_output. Undeclared outputs get their auto
|
||||||
|
-- defaults back, apply_external() then overrides what lua/monitors.lua knows
|
||||||
|
-- better.
|
||||||
|
local function unmirror_all(names)
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
hl.monitor({ output = name, mirror = "", disabled = true })
|
||||||
|
end
|
||||||
|
later(function()
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
hl.monitor({ output = name, disabled = false, mode = "preferred", position = "auto" })
|
||||||
|
end
|
||||||
|
monitors.apply_external()
|
||||||
|
later(restart_clients)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.toggle()
|
||||||
|
local panel = builtin()
|
||||||
|
local mirrored = mirroring(panel)
|
||||||
|
|
||||||
|
if #mirrored > 0 then
|
||||||
|
unmirror_all(mirrored)
|
||||||
|
notify("Mirroring off")
|
||||||
|
elseif panel then
|
||||||
|
mirror_all(panel)
|
||||||
|
later(restart_clients)
|
||||||
|
notify("Mirroring " .. monitors.laptop .. " to all screens")
|
||||||
|
else
|
||||||
|
notify("Laptop panel is off, open the lid first")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
hl.on("monitor.added", function()
|
||||||
|
local panel = builtin()
|
||||||
|
if #mirroring(panel) > 0 then
|
||||||
|
mirror_all(panel)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return M
|
||||||
+23
-6
@@ -16,15 +16,32 @@ Column {
|
|||||||
spacing: Theme.padS + 2
|
spacing: Theme.padS + 2
|
||||||
visible: root.shownItems.length > 0
|
visible: root.shownItems.length > 0
|
||||||
|
|
||||||
// Applets whose whole job the rail already does. nm-applet and blueman keep
|
// Applets whose whole job the rail already does. blueman keeps running as
|
||||||
// running — they are the Wi-Fi secret agent and the pairing agent — but
|
// the pairing agent, but its icon was a second bluetooth glyph a few slots
|
||||||
// their icons were a second network and a second bluetooth glyph a few
|
// below the rail's own.
|
||||||
// slots below the rail's own.
|
readonly property var hiddenIds: ["blueman"]
|
||||||
readonly property var hiddenIds: ["nm-applet", "blueman"]
|
|
||||||
|
|
||||||
readonly property var shownItems: SystemTray.items.values
|
readonly property var shownItems: SystemTray.items.values
|
||||||
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
|
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
|
||||||
|
|
||||||
|
// Electron and Chromium apps send only a full-colour pixmap, and flattening
|
||||||
|
// that loses everything inside the outline: Brave's lion became a plain
|
||||||
|
// shield. The icon theme often ships a panel glyph drawn for exactly this
|
||||||
|
// job, so look one up by the app's name and use the pixmap only when none
|
||||||
|
// exists. The id is no help here ("chrome_status_icon_1" for Brave and
|
||||||
|
// Proton Pass alike), so the tooltip title is the name we go by.
|
||||||
|
function themedIcon(item: SystemTrayItem): string {
|
||||||
|
if (item.icon.startsWith("image://icon/")) return item.icon;
|
||||||
|
|
||||||
|
const name = (item.tooltipTitle || item.title || item.id)
|
||||||
|
.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
||||||
|
for (const suffix of ["-tray", "-panel", "-symbolic"]) {
|
||||||
|
const path = Quickshell.iconPath(name + suffix, true);
|
||||||
|
if (path !== "") return path;
|
||||||
|
}
|
||||||
|
return item.icon;
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.shownItems
|
model: root.shownItems
|
||||||
|
|
||||||
@@ -45,7 +62,7 @@ Column {
|
|||||||
IconImage {
|
IconImage {
|
||||||
id: img
|
id: img
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: entry.modelData.icon
|
source: root.themedIcon(entry.modelData)
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
|
|||||||
@@ -304,18 +304,25 @@ Singleton {
|
|||||||
// So when the target declares no placeholder we build the command line
|
// So when the target declares no placeholder we build the command line
|
||||||
// ourselves and hand uwsm an argv instead of an ID. It is still scoped and
|
// ourselves and hand uwsm an argv instead of an ID. It is still scoped and
|
||||||
// still supervised; we just did the expansion.
|
// still supervised; we just did the expansion.
|
||||||
|
//
|
||||||
|
// The same fallback covers entries whose file name uwsm will not accept as
|
||||||
|
// an ID at all. Its check is the regex below, so `Proton Authenticator
|
||||||
|
// .desktop` and most Steam/Lutris shortcuts ("Hollow Knight.desktop") fail
|
||||||
|
// with "Invalid Desktop Entry ID" before anything runs.
|
||||||
readonly property var fieldRe: /%[fFuU]/
|
readonly property var fieldRe: /%[fFuU]/
|
||||||
readonly property var anyFieldRe: /^%[a-zA-Z]$/
|
readonly property var anyFieldRe: /^%[a-zA-Z]$/
|
||||||
|
readonly property var uwsmIdRe: /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*\.desktop(:.*)?$/
|
||||||
|
|
||||||
function launch(item: var, arg: string): void {
|
function launch(item: var, arg: string): void {
|
||||||
root._remember(item.key);
|
root._remember(item.key);
|
||||||
|
const byId = root.uwsmIdRe.test(item.key);
|
||||||
|
|
||||||
if (arg === "") {
|
if (byId && arg === "") {
|
||||||
Quickshell.execDetached(["uwsm", "app", "--", item.key]);
|
Quickshell.execDetached(["uwsm", "app", "--", item.key]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.fieldRe.test(item.exec || "")) {
|
if (byId && root.fieldRe.test(item.exec || "")) {
|
||||||
Quickshell.execDetached(["uwsm", "app", "--", item.key, arg]);
|
Quickshell.execDetached(["uwsm", "app", "--", item.key, arg]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -323,10 +330,10 @@ Singleton {
|
|||||||
const argv = root._tokenize(item.exec || "")
|
const argv = root._tokenize(item.exec || "")
|
||||||
.filter(t => !root.anyFieldRe.test(t));
|
.filter(t => !root.anyFieldRe.test(t));
|
||||||
if (argv.length === 0) {
|
if (argv.length === 0) {
|
||||||
Quickshell.execDetached(["uwsm", "app", "--", item.key]);
|
if (byId) Quickshell.execDetached(["uwsm", "app", "--", item.key]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
argv.push(arg);
|
if (arg !== "") argv.push(arg);
|
||||||
|
|
||||||
// Without this the unit is named after the bare executable and every
|
// Without this the unit is named after the bare executable and every
|
||||||
// app launched this way collides in the unit namespace.
|
// app launched this way collides in the unit namespace.
|
||||||
|
|||||||
Reference in new issue
Block a user