diff --git a/config/bin/media b/config/bin/media deleted file mode 100755 index 521a7ac..0000000 --- a/config/bin/media +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/zsh -##################################################################### - -USAGE="$0 (next|prev|playpause)" - -function ERROR() { - local ERROR_CODE=$1 - local MESSAGE="$2" - - echo -e "$MESSAGE\n\nUSAGE : $USAGE\n" >&2 - notify-send "YouTube media controller" "$MESSAGE" - - exit $ERROR_CODE -} - -##################################################################### - -COMMAND="$1" -case $COMMAND in - next ) COMMAND_KEY='j';; - prev ) COMMAND_KEY='k';; - play ) COMMAND_KEY='space';; - * ) ERROR 1 "unsupported command '$COMMAND'" ;; -esac - -xdotool search --name 'YouTube Music' windowactivate \ - || ERROR 2 "YouTube Music is not running" - - -# Chrome / YouTube only accepts keypresses when window is active -xdotool search --name 'YouTube Music' windowactivate -sleep 0.3 -xdotool key --clearmodifiers "$COMMAND_KEY" diff --git a/config/desktop/hyprland/config.lua.d/02-monitor-layouts.lua b/config/desktop/hyprland/config.lua.d/02-monitor-layouts.lua new file mode 100644 index 0000000..b1ddf9e --- /dev/null +++ b/config/desktop/hyprland/config.lua.d/02-monitor-layouts.lua @@ -0,0 +1,224 @@ +local dotwryn = ... -- ../config.lua +--------------------------------------------------------------------- +-- +-- named monitor layouts (hyprland port of the per-host xrandr layout scripts) +-- +-- --------------------------------------------------------------------- +-- hosts define their monitors and layouts using the following spec: +-- +-- (inside config/local//user/hypr/host.lua) +-- +-- dotwryn.monitors. = { +-- output = "", -- e.g. "DP-1"; a "desc:" rule can't re-enable a disabled monitor +-- modes = { "x@", ... }, -- the modes the monitor advertises +-- excluded_modes = { "x@", ... }, -- (optional) advertised, but tested and not working +-- max_refresh = , -- (optional) never pick a faster mode +-- mirrored_max_refresh = , -- (optional) cap while another monitor mirrors this one +-- } +-- +-- dotwryn.monitor_layouts[""] = { +-- primary = "", -- the monitor an emergency monitor mirrors +-- monitors = { -- omit monitor = monitor disabled +-- = { resolution = "4k", position = "0x0", scale = 1.5 }, +-- = { resolution = "4k", mirror = "" }, +-- }, +-- wallpaper = "theme", -- (optional) set-background selection +-- sfx = "gamedock", -- (optional) play-sfx selection +-- } +-- +-- dotwryn.emergency_monitor = { monitor = "", key = "" } -- (optional) the key toggles as a mirror of the active layout's primary +-- +-- a monitor runs the fastest of its modes at the requested resolution which isn't excluded +-- or over its refresh caps; 'max' takes the highest resolution, and a resolution the monitor +-- doesn't have falls back to 'max'. a resolution chosen in the picker replaces the resolution +-- of every monitor in the layout; the scale is kept unless it doesn't divide the mode evenly (then 1) +-- +-- 'default' is applied on startup and reload (see ./99-autostart.lua) +-- + +dotwryn.monitors = {} +dotwryn.monitor_layouts = {} + +local RESOLUTIONS = { + ["1080"] = "1920x1080", + ["1440"] = "2560x1440", + ["4k"] = "3840x2160", + ["max"] = "max", +} + +local MAX_RESOLUTION_ORDER = { "4k", "1440", "1080" } + +------------------------------------------ + +local get_sorted_keys = function(table_) + local keys = {} + for key, _ in pairs(table_) do table.insert(keys, key) end + table.sort(keys) + return keys +end + +local get_layout = function(layout_name, resolution) + local layout = dotwryn.monitor_layouts[layout_name] + + assert(layout ~= nil, "unknown monitor layout '" .. tostring(layout_name) .. "' (available : " .. table.concat(get_sorted_keys(dotwryn.monitor_layouts), ", ") .. ")") + assert(resolution == nil or RESOLUTIONS[resolution] ~= nil, "unknown resolution '" .. tostring(resolution) .. "' (available : " .. table.concat(get_sorted_keys(RESOLUTIONS), ", ") .. ")") + assert(layout.primary ~= nil and (layout.monitors or {})[layout.primary] ~= nil, "monitor layout '" .. layout_name .. "' needs a primary from its monitors") + + for monitor_name, monitor in pairs(layout.monitors) do + assert(dotwryn.monitors[monitor_name] ~= nil, "monitor layout '" .. layout_name .. "' uses unknown monitor '" .. monitor_name .. "'") + assert(RESOLUTIONS[monitor.resolution or "max"] ~= nil, "monitor layout '" .. layout_name .. "' uses unknown resolution '" .. tostring(monitor.resolution) .. "' for '" .. monitor_name .. "'") + assert(monitor.mirror == nil or layout.monitors[monitor.mirror] ~= nil, "monitor layout '" .. layout_name .. "' mirrors '" .. tostring(monitor.mirror) .. "', which isn't in the layout") + end + + return layout +end + +local get_monitor_mode = function(monitor_name, resolution, is_mirrored) + local monitor = dotwryn.monitors[monitor_name] + + local max_refresh = monitor.max_refresh or math.huge + if is_mirrored and monitor.mirrored_max_refresh ~= nil then max_refresh = math.min(max_refresh, monitor.mirrored_max_refresh) end + + local excluded_modes = {} + for _, mode in ipairs(monitor.excluded_modes or {}) do excluded_modes[mode] = true end + + local get_fastest_mode = function(size) + local fastest_mode, fastest_refresh + for _, mode in ipairs(monitor.modes or {}) do + local mode_size, refresh = mode:match("^(%d+x%d+)@([%d.]+)$") + refresh = tonumber(refresh) + + if mode_size == size and refresh <= max_refresh and not excluded_modes[mode] and (fastest_refresh == nil or refresh > fastest_refresh) then + fastest_mode, fastest_refresh = mode, refresh + end + end + return fastest_mode + end + + local mode = resolution ~= "max" and get_fastest_mode(RESOLUTIONS[resolution]) or nil + for _, max_resolution in ipairs(MAX_RESOLUTION_ORDER) do + if mode ~= nil then break end + mode = get_fastest_mode(RESOLUTIONS[max_resolution]) + end + + assert(mode ~= nil, "monitor '" .. monitor_name .. "' has no usable 1080/1440/4k mode") + return mode +end + +local get_resolution_scale = function(mode, scale) + local width, height = mode:match("^(%d+)x(%d+)") + if width == nil or type(scale) ~= "number" then return scale end + + if (width / scale) % 1 == 0 and (height / scale) % 1 == 0 then return scale end + return 1 +end + +local get_monitor_has_workspaces = function(output) + for _, workspace in ipairs(hl.get_workspaces()) do + if workspace.monitor ~= nil and workspace.monitor.name == output then return true end + end + return false +end + +local emergency_monitor_enabled = false + +dotwryn.set_monitor_layout = function(layout_name, resolution, options) + local layout = get_layout(layout_name, resolution) + + local layout_monitors = {} + for monitor_name, monitor in pairs(layout.monitors) do layout_monitors[monitor_name] = monitor end + + local emergency_monitor = dotwryn.emergency_monitor + if emergency_monitor ~= nil and emergency_monitor_enabled and layout_monitors[emergency_monitor.monitor] == nil then + layout_monitors[emergency_monitor.monitor] = { resolution = "max", mirror = layout.primary } + end + + local mirrored_monitor_names = {} + for _, monitor in pairs(layout_monitors) do + if monitor.mirror ~= nil then mirrored_monitor_names[monitor.mirror] = true end + end + + local get_monitor_spec = function(monitor_name) + local monitor = layout_monitors[monitor_name] + local output = dotwryn.monitors[monitor_name].output + local mode = get_monitor_mode(monitor_name, resolution or monitor.resolution or "max", mirrored_monitor_names[monitor_name]) + + local monitor_spec = { -- every field set; hl.monitor merges into the output's previous rule + output = output, + disabled = false, + mode = mode, + position = monitor.position or "auto", + scale = get_resolution_scale(mode, monitor.scale or 1), + mirror = "", + } + + if monitor.mirror ~= nil then + monitor_spec.mirror = dotwryn.monitors[monitor.mirror].output + + -- after a dpms cycle hyprland can lose a mirror it still reports (the "mirror" + -- owns workspaces); re-setting the same mirror is a no-op, so clear it first + if get_monitor_has_workspaces(output) then hl.monitor({ output = output, mirror = "" }) end + end + + return monitor_spec + end + + -- monitors the others mirror come up first and disabled monitors go down last, + -- so there's never a mirror of a disabled monitor or a moment with no monitors + for monitor_name, monitor in pairs(layout_monitors) do + if monitor.mirror == nil then hl.monitor(get_monitor_spec(monitor_name)) end + end + for monitor_name, monitor in pairs(layout_monitors) do + if monitor.mirror ~= nil then hl.monitor(get_monitor_spec(monitor_name)) end + end + for monitor_name, monitor in pairs(dotwryn.monitors) do + if layout_monitors[monitor_name] == nil then hl.monitor({ output = monitor.output, disabled = true }) end + end + + dotwryn.active_monitor_layout = { name = layout_name, resolution = resolution } + + if not (options or {}).switch then return end + + if layout.wallpaper ~= nil then hl.dispatch(dotwryn.scwrypts.hyprland.set_background(layout.wallpaper)) end + if layout.sfx ~= nil then hl.dispatch(dotwryn.scwrypts.media.play_sfx(layout.sfx)) end +end + +------------------------------------------ + +hl.bind("SUPER + d", hl.dsp.exec_cmd("qs ipc call -- displays show")) + +dotwryn_set_monitor_layout = function(layout_name, resolution) + dotwryn.set_monitor_layout(layout_name, resolution, { switch = true }) +end + +dotwryn_toggle_emergency_monitor = function() + assert(dotwryn.emergency_monitor ~= nil, "no emergency monitor configured for this host") + + emergency_monitor_enabled = not emergency_monitor_enabled + + local layout = dotwryn.active_monitor_layout + dotwryn.set_monitor_layout(layout.name, layout.resolution) +end + +-- eval can't return values; the picker (config/user/quickshell/DisplayPicker.qml) reads this IPC event instead +dotwryn_publish_monitor_layouts = function() + local get_json_string = function(value) + return '"' .. value:gsub('[%c"\\]', function(character) return string.format("\\u%04x", character:byte()) end) .. '"' + end + + local get_json_strings = function(values) + local json_strings = {} + for _, value in ipairs(values) do table.insert(json_strings, get_json_string(value)) end + return "[" .. table.concat(json_strings, ",") .. "]" + end + + local layout_names = get_sorted_keys(dotwryn.monitor_layouts) + for index, layout_name in ipairs(layout_names) do + if layout_name == "default" then table.insert(layout_names, 1, table.remove(layout_names, index)) break end + end + + hl.dispatch(hl.dsp.event("dotwryn-monitor-layouts " .. table.concat({ + '{"layouts":', get_json_strings(layout_names), + ',"resolutions":', get_json_strings(get_sorted_keys(RESOLUTIONS)), "}", + }))) +end diff --git a/config/desktop/hyprland/config.lua.d/15-dwindle-i3-scratchpad.lua b/config/desktop/hyprland/config.lua.d/15-dwindle-i3-scratchpad.lua index f897ff6..40deeb6 100644 --- a/config/desktop/hyprland/config.lua.d/15-dwindle-i3-scratchpad.lua +++ b/config/desktop/hyprland/config.lua.d/15-dwindle-i3-scratchpad.lua @@ -16,7 +16,7 @@ local SCRATCHPAD_FILTERS = { games = { "^steam$", "^dolphin%-emu$", "^Visualboyadvance%-m$", "^steam_app_" }, } -local matches_filter = function(class, filter) +local function matches_filter(class, filter) if filter == nil then for name, _ in pairs(SCRATCHPAD_FILTERS) do if matches_filter(class, name) then return false end diff --git a/config/desktop/hyprland/config.lua.d/82-workspaces-gaps.lua b/config/desktop/hyprland/config.lua.d/82-workspaces-gaps.lua index f76c740..9a77988 100644 --- a/config/desktop/hyprland/config.lua.d/82-workspaces-gaps.lua +++ b/config/desktop/hyprland/config.lua.d/82-workspaces-gaps.lua @@ -22,13 +22,16 @@ local to_pixels = function(value) return math.floor(math.min(monitor.width, monitor.height) / scale * percent / 100) end +local labels = {} -- reattach labels from ./80-workspaces.lua +for _, workspace in ipairs(dotwryn.workspaces) do labels[workspace.name] = workspace.label end + local gaps_rules = {} local set_gaps = function(workspace, inner, outer) if gaps_rules[workspace] ~= nil then gaps_rules[workspace].rule:set_enabled(false) end gaps_rules[workspace] = { inner = inner, outer = outer, - rule = hl.workspace_rule({ workspace = workspace, gaps_in = inner, gaps_out = outer }), + rule = hl.workspace_rule({ workspace = workspace, gaps_in = inner, gaps_out = outer, default_name = labels[workspace] }), } end diff --git a/config/desktop/hyprland/config.lua.d/90-window-rules.lua b/config/desktop/hyprland/config.lua.d/90-window-rules.lua index 9fb409e..c29642e 100644 --- a/config/desktop/hyprland/config.lua.d/90-window-rules.lua +++ b/config/desktop/hyprland/config.lua.d/90-window-rules.lua @@ -23,3 +23,9 @@ hl.window_rule({ match = { class = "^steam_app_.*$" }, workspace = "100", -- 🎮 }) + +hl.window_rule({ + name = "idle-inhibit-fullscreen", + match = { class = ".*" }, + idle_inhibit = "fullscreen", +}) diff --git a/config/desktop/hyprland/config.lua.d/98-host-local-overrides.lua b/config/desktop/hyprland/config.lua.d/98-host-local-overrides.lua index d7283b9..f8b4dcc 100644 --- a/config/desktop/hyprland/config.lua.d/98-host-local-overrides.lua +++ b/config/desktop/hyprland/config.lua.d/98-host-local-overrides.lua @@ -9,7 +9,7 @@ local dofile_if_exists = function(path) local file = io.open(path, "r") if file == nil then return end file:close() - dofile(path) + assert(loadfile(path))(dotwryn) end ------------------------------------------ diff --git a/config/desktop/hyprland/config.lua.d/99-autostart.lua b/config/desktop/hyprland/config.lua.d/99-autostart.lua index 2bc3be7..688c355 100644 --- a/config/desktop/hyprland/config.lua.d/99-autostart.lua +++ b/config/desktop/hyprland/config.lua.d/99-autostart.lua @@ -10,3 +10,11 @@ hl.on("hyprland.start", function() hl.exec_cmd("/usr/lib/xfce-polkit/xfce-polkit") hl.exec_cmd("command -v apps && apps") -- host-local background apps ($DOTWRYN/config/local//bin/apps) end) + +if dotwryn.monitor_layouts["default"] ~= nil then + dotwryn.set_monitor_layout("default") +end + +if dotwryn.emergency_monitor ~= nil then + hl.bind(dotwryn.emergency_monitor.key, dotwryn_toggle_emergency_monitor) +end diff --git a/config/desktop/hyprland/scwrypts.lua b/config/desktop/hyprland/scwrypts.lua index 8a2e500..854e784 100644 --- a/config/desktop/hyprland/scwrypts.lua +++ b/config/desktop/hyprland/scwrypts.lua @@ -54,6 +54,12 @@ local hyprland = { end, } +local media = { + play_sfx = function(...) + return hl.dsp.exec_cmd(scwrypts_desktop({ 'play-sfx' }, { ... })) + end, +} + local wayland = { sync_gtk_settings = function(...) return hl.dsp.exec_cmd(scwrypts({ 'sync-gtk-settings' }, { ... })) @@ -73,6 +79,7 @@ return { scwrypts_desktop = scwrypts_desktop, hyprland = hyprland, + media = media, wayland = wayland, tmux = tmux, } diff --git a/config/local/altaria/user/hypr/host.lua b/config/local/altaria/user/hypr/host.lua new file mode 100644 index 0000000..4df48e6 --- /dev/null +++ b/config/local/altaria/user/hypr/host.lua @@ -0,0 +1,95 @@ +local dotwryn = ... -- $DOTWRYN/config/desktop/hyprland/config.lua +--------------------------------------------------------------------- + +dotwryn.monitors.desk = { + output = "DP-2", + modes = { + "3840x2160@239.99", "3840x2160@180.00", "3840x2160@120.00", "3840x2160@119.88", "3840x2160@60.00", "3840x2160@59.94", + "2560x1440@239.85", "2560x1440@120.00", "2560x1440@59.95", + "1920x1080@239.88", "1920x1080@119.88", "1920x1080@60.00", "1920x1080@59.94", + }, + excluded_modes = { "1920x1080@239.88" }, + mirrored_max_refresh = 120, +} + +dotwryn.monitors.zapdos = { + output = "DP-1", + modes = { + "3840x2160@60.00", "3840x2160@59.94", + "2560x1440@143.97", "2560x1440@120.00", + "1920x1080@143.98", "1920x1080@119.88", "1920x1080@100.00", "1920x1080@60.00", "1920x1080@59.94", + }, + excluded_modes = { "2560x1440@143.97", "1920x1080@119.88" }, +} + +dotwryn.monitors.splitter = { + output = "HDMI-A-1", + modes = { + "3840x2160@119.88", "3840x2160@100.00", "3840x2160@60.00", "3840x2160@59.94", + "2560x1440@144.01", + "1920x1080@119.98", "1920x1080@60.00", "1920x1080@59.94", + }, + max_refresh = 60, +} + +dotwryn.monitors.server_rack = { + output = "DP-3", + modes = { + "1920x1080@144.00", "1920x1080@120.00", "1920x1080@119.88", "1920x1080@100.00", "1920x1080@60.00", "1920x1080@59.94", + }, + max_refresh = 60, +} + +dotwryn.emergency_monitor = { monitor = "server_rack", key = "XF86Calculator" } + +dotwryn.monitor_layouts["default"] = { + primary = "desk", + monitors = { + desk = { resolution = "4k", position = "0x0", scale = 1.5 }, + zapdos = { resolution = "4k", mirror = "desk" }, + splitter = { resolution = "4k", mirror = "desk" }, + }, + wallpaper = "theme", + sfx = "login", +} + +dotwryn.monitor_layouts["broadcast"] = { + primary = "desk", + monitors = { + desk = { resolution = "4k", position = "2560x0", scale = 1.5 }, + zapdos = { resolution = "4k", position = "0x0", scale = 1.5 }, + splitter = { resolution = "4k", mirror = "zapdos" }, + }, + wallpaper = "theme", + sfx = "login", +} + +dotwryn.monitor_layouts["beeg-game"] = { + primary = "zapdos", + monitors = { + zapdos = { resolution = "max", position = "0x0", scale = 1.5 }, + }, + wallpaper = "theme", + sfx = "gamedock", +} + +dotwryn.monitor_layouts["smol-game"] = { + primary = "desk", + monitors = { + desk = { resolution = "max", position = "0x0", scale = 1.5 }, + }, + wallpaper = "theme", + sfx = "gamedock", +} + +-------------------------------------------------------------------------------- + +hl.on("hyprland.start", function() + hl.exec_cmd("hypridle") -- ./hypridle.conf : power the desk monitor off after an hour idle + + -- office mouse + hl.exec_cmd("razer-cli --device 632531H25800834 --brightness 80 --effect breath_dual --color 006942 420069 --dpi 6400 --poll 1000") + + -- zapdos keyboard + hl.exec_cmd("razer-cli --device IO2524F99900428 --brightness 5 --effect multicolor") +end) diff --git a/config/user/quickshell/DisplayPicker.qml b/config/user/quickshell/DisplayPicker.qml new file mode 100644 index 0000000..39a3592 --- /dev/null +++ b/config/user/quickshell/DisplayPicker.qml @@ -0,0 +1,212 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Io + +// monitor layout picker for $DOTWRYN/config/desktop/hyprland/config.lua.d/02-monitor-layouts.lua; +// shown with 'qs ipc call -- displays show' (without '--', 'show' parses as 'qs ipc show') +Scope { + id: picker + + property bool shown: false + property var screen: null + + property var layouts: [] + property var resolutions: [] + + property int selectedLayout: 0 + property int selectedResolution: -1 // -1 : the modes configured in the layout + + function show(): void { + selectedLayout = 0 + selectedResolution = -1 + screen = Quickshell.screens.find(screen => screen.name === Hyprland.focusedMonitor?.name) ?? null + Quickshell.execDetached(["hyprctl", "eval", "dotwryn_publish_monitor_layouts()"]) + shown = true + } + + function moveLayout(offset: int): void { + if (layouts.length > 0) selectedLayout = (selectedLayout + offset + layouts.length) % layouts.length + } + + function moveResolution(offset: int): void { + const count = resolutions.length + 1 + selectedResolution = ((selectedResolution + 1 + offset + count) % count) - 1 + } + + // hyprland 0.56.2 segfaults if a focused layer surface is destroyed by a + // monitor change, so the picker must be gone before the layout is applied + function applyLayout(): void { + if (selectedLayout >= layouts.length) return + + const resolution = selectedResolution < 0 ? "nil" : JSON.stringify(resolutions[selectedResolution]) + evaluation.command = ["hyprctl", "eval", `dotwryn_set_monitor_layout(${JSON.stringify(layouts[selectedLayout])}, ${resolution})`] + + shown = false + applyAfterHidden.restart() + } + + Timer { + id: applyAfterHidden + interval: 250 + onTriggered: evaluation.running = true + } + + Process { + id: evaluation + + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim() + if (result !== "ok") Quickshell.execDetached(["notify-send", "displays", result.replace(/^.*:\d+: /, "")]) + } + } + } + + IpcHandler { + target: "displays" + + function show(): void { picker.show() } + function hide(): void { picker.shown = false } + } + + Connections { + target: Hyprland + + function onRawEvent(event) { + const prefix = "dotwryn-monitor-layouts " + if (event.name !== "custom" || !event.data.startsWith(prefix)) return + + const published = JSON.parse(event.data.slice(prefix.length)) + picker.layouts = published.layouts + picker.resolutions = published.resolutions + } + } + + component PickerText: Text { + color: Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize + } + + PanelWindow { + visible: picker.shown + screen: picker.screen + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + exclusionMode: ExclusionMode.Ignore + + implicitWidth: content.implicitWidth + 24 + implicitHeight: content.implicitHeight + 24 + color: "transparent" + + Rectangle { + anchors.fill: parent + color: Colors.background + border.width: 1 + border.color: Colors.primary + + focus: true + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_J: case Qt.Key_Down: picker.moveLayout(1); break + case Qt.Key_K: case Qt.Key_Up: picker.moveLayout(-1); break + case Qt.Key_L: case Qt.Key_Right: picker.moveResolution(1); break + case Qt.Key_H: case Qt.Key_Left: picker.moveResolution(-1); break + + case Qt.Key_Return: case Qt.Key_Enter: picker.applyLayout(); break + + case Qt.Key_Escape: case Qt.Key_Q: picker.shown = false; break + default: return + } + event.accepted = true + } + + Column { + id: content + anchors.centerIn: parent + spacing: 4 + + PickerText { + text: "monitor layout" + font.bold: true + color: Colors.secondary + } + + PickerText { + visible: picker.layouts.length === 0 + text: "no monitor layouts for this host" + color: Colors.disabled + } + + Repeater { + model: picker.layouts + + Rectangle { + id: layoutRow + + required property int index + required property string modelData + + readonly property bool selected: picker.selectedLayout === index + + implicitWidth: Math.max(layoutLabel.implicitWidth + 24, 280) + implicitHeight: layoutLabel.implicitHeight + 8 + color: selected ? Colors.primary : "transparent" + + PickerText { + id: layoutLabel + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: layoutRow.modelData + font.bold: layoutRow.selected + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: picker.selectedLayout = layoutRow.index + onClicked: picker.applyLayout() + } + } + } + + Row { + visible: picker.layouts.length > 0 + spacing: 8 + topPadding: 8 + + PickerText { + text: "resolution" + color: Colors.secondary + } + + Repeater { + model: ["configured"].concat(picker.resolutions) + + PickerText { + required property int index + required property string modelData + + readonly property bool selected: picker.selectedResolution === index - 1 + + text: selected ? `[${modelData}]` : ` ${modelData} ` + color: selected ? Colors.foreground : Colors.disabled + font.bold: selected + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: picker.selectedResolution = parent.index - 1 + } + } + } + } + } + } + } +} diff --git a/config/user/quickshell/shell.qml b/config/user/quickshell/shell.qml index 3a0be12..48945c0 100644 --- a/config/user/quickshell/shell.qml +++ b/config/user/quickshell/shell.qml @@ -19,4 +19,6 @@ ShellRoot { VolumeOsd {} LogoutPrompt {} + + DisplayPicker {} } diff --git a/config/user/tmux/tmux.conf b/config/user/tmux/tmux.conf index 2a06be4..6208751 100644 --- a/config/user/tmux/tmux.conf +++ b/config/user/tmux/tmux.conf @@ -8,6 +8,7 @@ set -sg repeat-time 0 set-option -g default-shell /bin/zsh set-option -ga update-environment HYPRLAND_INSTANCE_SIGNATURE +set-option -ga update-environment WAYLAND_DISPLAY # wl-clipboard through waypipe on reattach # quick reload bind-key r source-file ~/.config/tmux/tmux.conf\; display "TMUX CONFIG RELOADED" @@ -100,6 +101,6 @@ bind-key -n C-M-r send-keys M-r ##################################################################### -bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'xclip' +bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'if [ -n "$WAYLAND_DISPLAY" ]; then wl-copy; else xclip -selection clipboard; fi' ##################################################################### diff --git a/scwrypts/remote/connect b/scwrypts/remote/connect index e735f2d..8d310a1 100755 --- a/scwrypts/remote/connect +++ b/scwrypts/remote/connect @@ -112,15 +112,16 @@ MAIN() { ".default.command" \ ) + local TMUX_SESSION_NAME=$( + remote.config.query-connection-with-fallback \ + ".sessions.$REMOTE_NAME.tmux.session" \ + ".default.tmux.session" \ + "wryn" \ + ) + [ $REMOTE_COMMAND ] || { case $CONNECTION_TYPE in ( tmux ) - local TMUX_SESSION_NAME=$( - remote.config.query-connection-with-fallback \ - ".sessions.$REMOTE_NAME.tmux.session" \ - ".default.tmux.session" \ - "wryn" \ - ) local TMUX_ARGS=() locale charmap 2>/dev/null | grep -qi 'UTF-8' \ @@ -189,6 +190,57 @@ MAIN() { ;; esac - echo.debug "ssh ${SSH_ARGS[@]} $CONNECTION_STRING \"$REMOTE_COMMAND\"" - ssh ${SSH_ARGS[@]} $CONNECTION_STRING "$REMOTE_COMMAND" + ########################################## + + local host_display_server=none + + [[ "${DISPLAY}" ]] && host_display_server=x11 + [[ "${WAYLAND_DISPLAY}" ]] && host_display_server=wayland + + local ssh_command=(ssh) + + case "${host_display_server}" in + ( none | x11 ) ;; + ( wayland ) + local wayland_errors=0 + local waypipe_display="wayland-waypipe-${TMUX_SESSION_NAME}" + + command -v waypipe &>/dev/null || { + echo.warning "waypipe unavailable on host '$(hostnamectl --static)'" + ((wayland_errors+=1)) + } + + # waypipe only unlinks its display socket on a clean exit; a dropped + # connection leaves it behind and every later bind fails EADDRINUSE + local remote_waypipe_check=" + command -v waypipe || exit 1 + socket=\"\${XDG_RUNTIME_DIR}/${waypipe_display}\" + [ -S \"\${socket}\" ] || exit 0 + ss -xlH | grep -qF \" \${socket} \" || rm -- \"\${socket}\" + exit 0 + " + + ssh "${(@)SSH_ARGS:#-t}" "${CONNECTION_STRING}" "${remote_waypipe_check}" &>/dev/null || { + echo.warning "waypipe unavailable on host '${REMOTE_NAME}'" + ((wayland_errors+=1)) + } + + case "${wayland_errors}" in + ( 0 ) ssh_command=(waypipe --display "${waypipe_display}" ssh) ;; + ( * ) + echo.reminder ' + clipboard WILL NOT REACH the driver on this connection + + fix by installing the following packages on each host: + - wl-clipboard + - waypipe + ' + [[ -t 0 ]] && read -t 5 -k 1 -s + ;; + esac + ;; + esac + + echo.debug "${ssh_command[@]} ${SSH_ARGS[@]} $CONNECTION_STRING \"$REMOTE_COMMAND\"" + "${ssh_command[@]}" ${SSH_ARGS[@]} $CONNECTION_STRING "$REMOTE_COMMAND" } diff --git a/setup/packages/dependency.manifest.zsh b/setup/packages/dependency.manifest.zsh index c0f5261..c45342a 100644 --- a/setup/packages/dependency.manifest.zsh +++ b/setup/packages/dependency.manifest.zsh @@ -48,6 +48,8 @@ dependency rg --yay ripgrep --dnf ripgrep --apt-get ripgre dependency sed --brew gsed dependency tmux dependency vim --dnf vim-enhanced +dependency waypipe +dependency wl-copy --yay wl-clipboard --dnf wl-clipboard --apt-get wl-clipboard --brew wl-clipboard dependency xauth --yay xorg-xauth --dnf xorg-x11-xauth --apt-get xauth dependency yamllint dependency yq --yay go-yq --not-on apt-get @@ -122,11 +124,11 @@ dependency_args=("${INSTALLED_BY__extended[@]}" --only-on yay) # hyprland stack (not available through default dnf/apt-get repositories at the moment) dependency grim +dependency hypridle dependency hyprlock dependency hyprpaper dependency qs --yay quickshell dependency start-hyprland --yay hyprland -dependency wl-copy --yay wl-clipboard dependency xdg-desktop-portal-hyprland --no-executable ########################################## @@ -135,6 +137,7 @@ dependency xdg-desktop-portal-hyprland --no-executable dependency_args=("${INSTALLED_BY__all[@]}" --only-on yay) dependency arch-install-scripts --no-executable +dependency dotool dependency python-rtmidi --no-executable dependency scwrypts diff --git a/vim/rc.vim b/vim/rc.vim index 95a9f6a..dfbadf3 100644 --- a/vim/rc.vim +++ b/vim/rc.vim @@ -43,11 +43,13 @@ nnoremap d :let @d = system("date '+%A, %B %-d, %Y'")od m :set modifiable -" (c)opy / (p)aste from xclip -" TODO: learn how to compile vim with x11 compatibility and delete -vnoremap c :w !xclip -nnoremap v o!!xclip -o -nnoremap sc :'<,'>w !xclip +" (c)opy / (p)aste from the system clipboard +" wayland (local or through waypipe) uses wl-clipboard; otherwise falls back to xclip +let g:clipboard_copy = empty($WAYLAND_DISPLAY) ? 'xclip -selection clipboard' : 'wl-copy' +let g:clipboard_paste = empty($WAYLAND_DISPLAY) ? 'xclip -selection clipboard -o' : 'wl-paste -n' +execute 'vnoremap c :w !' . g:clipboard_copy . '' +execute 'nnoremap v o!!' . g:clipboard_paste . '' +execute "nnoremap sc :'<,'>w !" . g:clipboard_copy . '' " enable/disable true color nnoremap :set invtermguicolors diff --git a/zsh/alias b/zsh/alias index fe87b4c..fb969dd 100644 --- a/zsh/alias +++ b/zsh/alias @@ -7,8 +7,8 @@ alias pd='pushd' alias qd='popd' alias lsd='dirs' -alias pdo='pwd | xclip' # [p]aste [d]irectory [o]n clipboard -alias cdo='cd $(xclip -o)' # [c]hange to [d]irectory [o]n clipboard +alias pdo='pwd | { [[ "${WAYLAND_DISPLAY}" ]] && wl-copy || xclip -selection clipboard; }' # [p]aste [d]irectory [o]n clipboard +alias cdo='cd "$([[ "${WAYLAND_DISPLAY}" ]] && wl-paste -n || xclip -selection clipboard -o)"' # [c]hange to [d]irectory [o]n clipboard ##################################################################### ### default command override ########################################