Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a973ae49ef |
@@ -13,6 +13,3 @@
|
|||||||
# Python helper bytecode is local runtime state.
|
# Python helper bytecode is local runtime state.
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
||||||
# Generated from the colour scheme; machine state, not configuration.
|
|
||||||
/config/dot/kitty/current-theme.conf
|
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ hl.config({
|
|||||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
||||||
-- Unfocused windows get no colour at all. The gradient only means
|
-- Unfocused windows get no colour at all. The gradient only means
|
||||||
-- something if exactly one window on screen is wearing it.
|
-- something if exactly one window on screen is wearing it.
|
||||||
-- Follows the colour scheme: a dark neutral is invisible against a
|
inactive_border = "rgba(3b426199)",
|
||||||
-- light desktop. services/ColorScheme.qml applies changes live;
|
|
||||||
-- this is the value a fresh session starts from.
|
|
||||||
inactive_border = prefs.get("colorScheme", "dark") == "light"
|
|
||||||
and "rgba(a8aecb99)" or "rgba(3b426199)",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resize_on_border = true,
|
resize_on_border = true,
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ font_size 14.0
|
|||||||
|
|
||||||
#: Cursor customization {{{
|
#: Cursor customization {{{
|
||||||
|
|
||||||
|
cursor #c8d3f5
|
||||||
|
|
||||||
#: Default cursor color. If set to the special value none the cursor
|
#: Default cursor color. If set to the special value none the cursor
|
||||||
#: will be rendered with a "reverse video" effect. It's color will be
|
#: will be rendered with a "reverse video" effect. It's color will be
|
||||||
@@ -192,6 +193,7 @@ font_size 14.0
|
|||||||
#: precedence. Also, the cursor colors are modified if the cell
|
#: precedence. Also, the cursor colors are modified if the cell
|
||||||
#: background and foreground colors have very low contrast.
|
#: background and foreground colors have very low contrast.
|
||||||
|
|
||||||
|
cursor_text_color #111325
|
||||||
|
|
||||||
#: The color of text under the cursor. If you want it rendered with
|
#: The color of text under the cursor. If you want it rendered with
|
||||||
#: the background color of the cell underneath instead, use the
|
#: the background color of the cell underneath instead, use the
|
||||||
@@ -310,6 +312,7 @@ cursor_shape block
|
|||||||
#: robustly with the ever-changing sea of bugs that is Cocoa is too
|
#: robustly with the ever-changing sea of bugs that is Cocoa is too
|
||||||
#: much effort.
|
#: much effort.
|
||||||
|
|
||||||
|
url_color #4fd6be
|
||||||
# url_style curly
|
# url_style curly
|
||||||
|
|
||||||
#: The color and style for highlighting URLs on mouse-over. url_style
|
#: The color and style for highlighting URLs on mouse-over. url_style
|
||||||
@@ -731,10 +734,12 @@ window_border_width 1pt
|
|||||||
#: placed centrally. A value of top-left means the padding will be
|
#: placed centrally. A value of top-left means the padding will be
|
||||||
#: only at the bottom and right edges.
|
#: only at the bottom and right edges.
|
||||||
|
|
||||||
|
active_border_color #82aaff
|
||||||
|
|
||||||
#: The color for the border of the active window. Set this to none to
|
#: The color for the border of the active window. Set this to none to
|
||||||
#: not draw borders around the active window.
|
#: not draw borders around the active window.
|
||||||
|
|
||||||
|
inactive_border_color #2f334d
|
||||||
|
|
||||||
#: The color for the border of inactive windows.
|
#: The color for the border of inactive windows.
|
||||||
|
|
||||||
@@ -955,11 +960,16 @@ tab_powerline_style round
|
|||||||
#: Template to use for active tabs. If not specified falls back to
|
#: Template to use for active tabs. If not specified falls back to
|
||||||
#: tab_title_template.
|
#: tab_title_template.
|
||||||
|
|
||||||
|
active_tab_foreground #1e2030
|
||||||
|
active_tab_background #82aaff
|
||||||
# active_tab_font_style bold-italic
|
# active_tab_font_style bold-italic
|
||||||
|
inactive_tab_foreground #c8d3f5
|
||||||
|
inactive_tab_background #42465a
|
||||||
# inactive_tab_font_style normal
|
# inactive_tab_font_style normal
|
||||||
|
|
||||||
#: Tab bar colors and styles.
|
#: Tab bar colors and styles.
|
||||||
|
|
||||||
|
tab_bar_background #222436
|
||||||
|
|
||||||
#: Background color for the tab bar. Defaults to using the terminal
|
#: Background color for the tab bar. Defaults to using the terminal
|
||||||
#: background color.
|
#: background color.
|
||||||
@@ -973,8 +983,8 @@ tab_bar_margin_color none
|
|||||||
|
|
||||||
#: Color scheme {{{
|
#: Color scheme {{{
|
||||||
|
|
||||||
|
foreground #c8d3f5
|
||||||
include current-theme.conf
|
background #222436
|
||||||
|
|
||||||
#: The foreground and background colors.
|
#: The foreground and background colors.
|
||||||
|
|
||||||
@@ -1033,6 +1043,8 @@ dim_opacity 1
|
|||||||
#: How much to dim text that has the DIM/FAINT attribute set. One
|
#: How much to dim text that has the DIM/FAINT attribute set. One
|
||||||
#: means no dimming and zero means fully dimmed (i.e. invisible).
|
#: means no dimming and zero means fully dimmed (i.e. invisible).
|
||||||
|
|
||||||
|
selection_foreground #2d3f76
|
||||||
|
selection_background #c8d3f5
|
||||||
|
|
||||||
#: The foreground and background colors for text selected with the
|
#: The foreground and background colors for text selected with the
|
||||||
#: mouse. Setting both of these to none will cause a "reverse video"
|
#: mouse. Setting both of these to none will cause a "reverse video"
|
||||||
@@ -1048,29 +1060,47 @@ dim_opacity 1
|
|||||||
#: dull and bright version, for the first 16 colors. You can set the
|
#: dull and bright version, for the first 16 colors. You can set the
|
||||||
#: remaining 240 colors as color16 to color255.
|
#: remaining 240 colors as color16 to color255.
|
||||||
|
|
||||||
|
color0 #1b1d2b
|
||||||
|
color8 #444a73
|
||||||
|
|
||||||
#: black
|
#: black
|
||||||
|
|
||||||
|
color1 #ff757f
|
||||||
|
color9 #ff757f
|
||||||
|
|
||||||
#: red
|
#: red
|
||||||
|
|
||||||
|
color2 #a5e8b5
|
||||||
|
color10 #a5e8b5
|
||||||
|
|
||||||
#: green
|
#: green
|
||||||
|
color3 #ffc777
|
||||||
|
color11 #ffc777
|
||||||
|
|
||||||
#: yellow
|
#: yellow
|
||||||
|
|
||||||
|
color4 #82aaff
|
||||||
|
color12 #82aaff
|
||||||
|
|
||||||
#: blue
|
#: blue
|
||||||
|
|
||||||
|
color5 #c099ff
|
||||||
|
color13 #c099ff
|
||||||
|
|
||||||
#: magenta
|
#: magenta
|
||||||
|
|
||||||
|
color6 #86e1fc
|
||||||
|
color14 #86e1fc
|
||||||
|
|
||||||
#: cyan
|
#: cyan
|
||||||
|
|
||||||
|
color7 #828bb8
|
||||||
|
color15 #c8d3f5
|
||||||
|
|
||||||
#: white
|
#: white
|
||||||
|
|
||||||
|
color16 #ff966c
|
||||||
|
color17 #c53b53
|
||||||
|
|
||||||
# mark1_foreground black
|
# mark1_foreground black
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# Tokyo Night Day — the palette's own light variant.
|
|
||||||
#
|
|
||||||
# The same hues as Moon at a different lightness, so a terminal in light mode
|
|
||||||
# still belongs to this desktop rather than looking like a different machine.
|
|
||||||
# Kept in the same order as tokyonight-moon.conf so the two can be diffed.
|
|
||||||
|
|
||||||
cursor #3760bf
|
|
||||||
cursor_text_color #e1e2e7
|
|
||||||
url_color #118c74
|
|
||||||
active_border_color #2e7de9
|
|
||||||
inactive_border_color #c4c8da
|
|
||||||
active_tab_foreground #e1e2e7
|
|
||||||
active_tab_background #2e7de9
|
|
||||||
inactive_tab_foreground #3760bf
|
|
||||||
inactive_tab_background #c4c8da
|
|
||||||
tab_bar_background #e1e2e7
|
|
||||||
foreground #3760bf
|
|
||||||
background #e1e2e7
|
|
||||||
selection_foreground #e1e2e7
|
|
||||||
selection_background #99a7df
|
|
||||||
color0 #e9e9ed
|
|
||||||
color8 #a1a6c5
|
|
||||||
color1 #f52a65
|
|
||||||
color9 #f52a65
|
|
||||||
color2 #587539
|
|
||||||
color10 #587539
|
|
||||||
color3 #8c6c3e
|
|
||||||
color11 #8c6c3e
|
|
||||||
color4 #2e7de9
|
|
||||||
color12 #2e7de9
|
|
||||||
color5 #9854f1
|
|
||||||
color13 #9854f1
|
|
||||||
color6 #007197
|
|
||||||
color14 #007197
|
|
||||||
color7 #6172b0
|
|
||||||
color15 #3760bf
|
|
||||||
color16 #b15c00
|
|
||||||
color17 #c64343
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Tokyo Night Moon — the dark theme Panama ships.
|
|
||||||
#
|
|
||||||
# Extracted from kitty.conf so the two schemes can be swapped. kitty.conf
|
|
||||||
# includes current-theme.conf, which Panama generates from the colour scheme
|
|
||||||
# setting; that generated file is gitignored because it is machine state.
|
|
||||||
#
|
|
||||||
# Live changes go through `kitty @ set-colors`, which is why kitty.conf enables
|
|
||||||
# remote control. Without it a scheme change would only reach terminals opened
|
|
||||||
# afterwards.
|
|
||||||
|
|
||||||
cursor #c8d3f5
|
|
||||||
cursor_text_color #111325
|
|
||||||
url_color #4fd6be
|
|
||||||
active_border_color #82aaff
|
|
||||||
inactive_border_color #2f334d
|
|
||||||
active_tab_foreground #1e2030
|
|
||||||
active_tab_background #82aaff
|
|
||||||
inactive_tab_foreground #c8d3f5
|
|
||||||
inactive_tab_background #42465a
|
|
||||||
tab_bar_background #222436
|
|
||||||
foreground #c8d3f5
|
|
||||||
background #222436
|
|
||||||
selection_foreground #2d3f76
|
|
||||||
selection_background #c8d3f5
|
|
||||||
color0 #1b1d2b
|
|
||||||
color8 #444a73
|
|
||||||
color1 #ff757f
|
|
||||||
color9 #ff757f
|
|
||||||
color2 #a5e8b5
|
|
||||||
color10 #a5e8b5
|
|
||||||
color3 #ffc777
|
|
||||||
color11 #ffc777
|
|
||||||
color4 #82aaff
|
|
||||||
color12 #82aaff
|
|
||||||
color5 #c099ff
|
|
||||||
color13 #c099ff
|
|
||||||
color6 #86e1fc
|
|
||||||
color14 #86e1fc
|
|
||||||
color7 #828bb8
|
|
||||||
color15 #c8d3f5
|
|
||||||
color16 #ff966c
|
|
||||||
color17 #c53b53
|
|
||||||
@@ -10,6 +10,3 @@ vim.api.nvim_create_autocmd("BufWritePre", {
|
|||||||
vim.fn.winrestview(view)
|
vim.fn.winrestview(view)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Follow the desktop's light/dark setting while running, not only at startup.
|
|
||||||
require("config.panama").watch()
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
-- Panama desktop integration.
|
|
||||||
--
|
|
||||||
-- Neovim is the one application here that neither reads the desktop portal nor
|
|
||||||
-- has a control socket open by default, so it reads the shared settings file
|
|
||||||
-- directly -- the same ~/.config/panama/settings.json that the shell and the
|
|
||||||
-- Hyprland config read.
|
|
||||||
--
|
|
||||||
-- Nothing here may raise. A missing or malformed settings file must cost the
|
|
||||||
-- user their colour scheme preference and nothing else; editing text is more
|
|
||||||
-- important than matching the desktop.
|
|
||||||
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
local function settings_path()
|
|
||||||
local config_home = os.getenv("XDG_CONFIG_HOME")
|
|
||||||
if config_home == nil or config_home == "" then
|
|
||||||
local home = os.getenv("HOME")
|
|
||||||
if home == nil or home == "" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
config_home = home .. "/.config"
|
|
||||||
end
|
|
||||||
return config_home .. "/panama/settings.json"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- "dark" or "light". Defaults to dark, which is what Panama ships.
|
|
||||||
function M.color_scheme()
|
|
||||||
local path = settings_path()
|
|
||||||
if not path then
|
|
||||||
return "dark"
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, contents = pcall(function()
|
|
||||||
local file = io.open(path, "r")
|
|
||||||
if not file then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local text = file:read("*a")
|
|
||||||
file:close()
|
|
||||||
return text
|
|
||||||
end)
|
|
||||||
if not ok or not contents or contents == "" then
|
|
||||||
return "dark"
|
|
||||||
end
|
|
||||||
|
|
||||||
local decoded_ok, decoded = pcall(vim.json.decode, contents)
|
|
||||||
if not decoded_ok or type(decoded) ~= "table" then
|
|
||||||
return "dark"
|
|
||||||
end
|
|
||||||
|
|
||||||
return decoded.colorScheme == "light" and "light" or "dark"
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.is_light()
|
|
||||||
return M.color_scheme() == "light"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- tokyonight ships a light variant in the same family, so light mode stays the
|
|
||||||
-- same identity rather than becoming a different editor theme.
|
|
||||||
function M.tokyonight_style()
|
|
||||||
return M.is_light() and "day" or "moon"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Re-apply the scheme when the window regains focus.
|
|
||||||
--
|
|
||||||
-- Neovim reads the setting once at startup and has no control socket open by
|
|
||||||
-- default, so an editor already running when the desktop scheme flips would
|
|
||||||
-- otherwise stay on the old palette until it was restarted. FocusGained is
|
|
||||||
-- cheap, happens exactly when you would notice the mismatch, and does nothing
|
|
||||||
-- at all unless the scheme actually changed.
|
|
||||||
function M.watch()
|
|
||||||
local applied = M.color_scheme()
|
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd("FocusGained", {
|
|
||||||
group = vim.api.nvim_create_augroup("PanamaColorScheme", { clear = true }),
|
|
||||||
callback = function()
|
|
||||||
local current = M.color_scheme()
|
|
||||||
if current == applied then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
applied = current
|
|
||||||
vim.o.background = current
|
|
||||||
pcall(vim.cmd.colorscheme, current == "light" and "tokyonight-day" or "tokyonight-moon")
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
@@ -1,68 +1,31 @@
|
|||||||
-- Tokyo Night, following the desktop's colour scheme.
|
|
||||||
--
|
|
||||||
-- Moon when Panama is dark, Day when it is light. Same theme family either way,
|
|
||||||
-- so the editor keeps the identity the rest of the desktop has rather than
|
|
||||||
-- becoming a different-looking application when the scheme flips.
|
|
||||||
--
|
|
||||||
-- The readability fixes below are deliberately dark-only. They were written
|
|
||||||
-- against Moon's palette -- a pale comment colour, a mid-grey gutter -- and
|
|
||||||
-- applying them to Day would put light grey text on a light background, which
|
|
||||||
-- is exactly the legibility problem they exist to solve, inverted.
|
|
||||||
|
|
||||||
local panama = require("config.panama")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{
|
{
|
||||||
"folke/tokyonight.nvim",
|
"folke/tokyonight.nvim",
|
||||||
opts = function()
|
opts = {
|
||||||
local light = panama.is_light()
|
style = "moon",
|
||||||
|
transparent = true,
|
||||||
return {
|
on_colors = function(colors)
|
||||||
style = panama.tokyonight_style(),
|
colors.comment = "#a0a7c5"
|
||||||
light_style = "day",
|
colors.fg_gutter = "#787f93"
|
||||||
transparent = true,
|
colors.terminal_black = "#828bb8"
|
||||||
|
end,
|
||||||
on_colors = function(colors)
|
on_highlights = function(highlights, colors)
|
||||||
if light then
|
-- Fix inline code visibility in markdown
|
||||||
-- Day's defaults are tuned for a light ground and mostly need no
|
highlights["@markup.raw.markdown_inline"] = {
|
||||||
-- help. Comments are the exception: the shipped #848cb5 measures
|
bg = colors.terminal_black,
|
||||||
-- 2.54:1 against the #e1e2e7 background, well under the 3:1 floor
|
fg = colors.fg,
|
||||||
-- for secondary text. This is 3.25:1 -- readable, and still clearly
|
}
|
||||||
-- dimmer than Normal's 4.52:1 so it does not compete with code.
|
highlights["RenderMarkdownCodeInline"] = {
|
||||||
colors.comment = "#7079a8"
|
bg = colors.terminal_black,
|
||||||
return
|
fg = colors.fg,
|
||||||
end
|
}
|
||||||
|
-- Fix LspReference* readability: DiagnosticUnnecessary dims fg for unused
|
||||||
colors.comment = "#a0a7c5"
|
-- imports, making text nearly invisible against LspReferenceText's background
|
||||||
colors.fg_gutter = "#787f93"
|
highlights["LspReferenceText"] = { bg = colors.fg_gutter, fg = colors.fg }
|
||||||
colors.terminal_black = "#828bb8"
|
highlights["LspReferenceRead"] = { bg = colors.fg_gutter, fg = colors.fg }
|
||||||
end,
|
highlights["LspReferenceWrite"] = { bg = colors.fg_gutter, fg = colors.fg }
|
||||||
|
end,
|
||||||
on_highlights = function(highlights, colors)
|
},
|
||||||
-- Inline code in markdown is invisible at both lightnesses without
|
|
||||||
-- an explicit background, because the theme leaves it unset.
|
|
||||||
highlights["@markup.raw.markdown_inline"] = {
|
|
||||||
bg = light and colors.bg_highlight or colors.terminal_black,
|
|
||||||
fg = colors.fg,
|
|
||||||
}
|
|
||||||
highlights["RenderMarkdownCodeInline"] = {
|
|
||||||
bg = light and colors.bg_highlight or colors.terminal_black,
|
|
||||||
fg = colors.fg,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- LspReference* readability: DiagnosticUnnecessary dims fg for unused
|
|
||||||
-- imports, making text nearly invisible against LspReferenceText's
|
|
||||||
-- background.
|
|
||||||
local reference = {
|
|
||||||
bg = light and colors.bg_visual or colors.fg_gutter,
|
|
||||||
fg = colors.fg,
|
|
||||||
}
|
|
||||||
highlights["LspReferenceText"] = reference
|
|
||||||
highlights["LspReferenceRead"] = reference
|
|
||||||
highlights["LspReferenceWrite"] = reference
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"LazyVim/LazyVim",
|
"LazyVim/LazyVim",
|
||||||
|
|||||||
@@ -386,26 +386,6 @@ Singleton {
|
|||||||
detail: "A time earlier than the start simply means the next morning"
|
detail: "A time earlier than the start simply means the next morning"
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Colour scheme ───────────────────────────────────────────────────
|
|
||||||
// Light is Tokyo Night Day, the official light variant, rather than a
|
|
||||||
// palette invented to merely not be dark. Both share the same hues at
|
|
||||||
// different lightness, which is what keeps the Prism identity intact
|
|
||||||
// across the switch.
|
|
||||||
//
|
|
||||||
// services/ColorScheme.qml pushes the choice to GTK and to the
|
|
||||||
// compositor's border colours, because an application toolbar or a
|
|
||||||
// window border still wearing the other scheme is more jarring than
|
|
||||||
// either scheme on its own.
|
|
||||||
{
|
|
||||||
key: "colorScheme", type: "enum", def: "dark", group: "appearance",
|
|
||||||
label: "Appearance",
|
|
||||||
detail: "Light and dark share one identity, not two themes",
|
|
||||||
options: [
|
|
||||||
{ value: "dark", label: "Dark" },
|
|
||||||
{ value: "light", label: "Light" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── Typography ──────────────────────────────────────────────────────
|
// ── Typography ──────────────────────────────────────────────────────
|
||||||
// The single largest thing in this desktop that used to be changeable
|
// The single largest thing in this desktop that used to be changeable
|
||||||
// only by editing Theme.qml.
|
// only by editing Theme.qml.
|
||||||
|
|||||||
@@ -18,47 +18,36 @@ import QtQuick
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// ── Colour scheme ───────────────────────────────────────────────────────
|
|
||||||
// Tokyo Night ships an official light variant (Day), so light mode is that
|
|
||||||
// rather than a palette invented to merely not be dark. The two share the
|
|
||||||
// same hues at different lightness, which is what lets the Prism identity
|
|
||||||
// survive the switch: blue still leads into orchid, it is simply a darker
|
|
||||||
// blue on a lighter ground.
|
|
||||||
//
|
|
||||||
// Every token below is a binding on this, so flipping it repaints the whole
|
|
||||||
// shell without anything needing to know it happened.
|
|
||||||
readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light"
|
|
||||||
|
|
||||||
// ── Palette ─────────────────────────────────────────────────────────────
|
// ── Palette ─────────────────────────────────────────────────────────────
|
||||||
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
|
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
|
||||||
readonly property color bg: root.dark ? "#222436" : "#e1e2e7"
|
readonly property color bg: "#222436"
|
||||||
readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de"
|
readonly property color bgDark: "#1e2030"
|
||||||
readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da"
|
readonly property color bgHighlight: "#2f334d"
|
||||||
readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface
|
readonly property color bgPanel: "#2e2f3d" // dock glass surface
|
||||||
readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background
|
readonly property color bgPopover: "#21212f" // Openbar submenu background
|
||||||
|
|
||||||
readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf"
|
readonly property color fg: "#c8d3f5"
|
||||||
readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0"
|
readonly property color fgDim: "#828bb8"
|
||||||
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
readonly property color fgMuted: "#636da6"
|
||||||
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
readonly property color gutter: "#3b4261"
|
||||||
|
|
||||||
// The pair. `accent` is the primary and carries every state meaning
|
// The pair. `accent` is the primary and carries every state meaning
|
||||||
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
||||||
// theme — it never appears alone, only as the far end of a gradient. That
|
// theme — it never appears alone, only as the far end of a gradient. That
|
||||||
// restraint is the whole point: the two colours meeting is the signature,
|
// restraint is the whole point: the two colours meeting is the signature,
|
||||||
// so the pink stops being special the moment it's used as a flat fill.
|
// so the pink stops being special the moment it's used as a flat fill.
|
||||||
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue
|
readonly property color accent: "#82aaff" // blue
|
||||||
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux
|
readonly property color accentSecondary: "#b172b0" // orchid, from tmux
|
||||||
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
readonly property color accentAlt: "#65bcff" // blue1, a lighter blue
|
||||||
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
readonly property color cyan: "#86e1fc"
|
||||||
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
readonly property color teal: "#4fd6be"
|
||||||
readonly property color green: root.dark ? "#c3e88d" : "#587539"
|
readonly property color green: "#c3e88d"
|
||||||
readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e"
|
readonly property color yellow: "#ffc777"
|
||||||
readonly property color orange: root.dark ? "#ff966c" : "#b15c00"
|
readonly property color orange: "#ff966c"
|
||||||
readonly property color red: root.dark ? "#ff757f" : "#f52a65"
|
readonly property color red: "#ff757f"
|
||||||
readonly property color redDeep: root.dark ? "#c53b53" : "#c64343"
|
readonly property color redDeep: "#c53b53"
|
||||||
readonly property color magenta: root.dark ? "#c099ff" : "#9854f1"
|
readonly property color magenta: "#c099ff"
|
||||||
readonly property color pink: root.dark ? "#fca7ea" : "#d20065"
|
readonly property color pink: "#fca7ea"
|
||||||
|
|
||||||
// Semantic aliases — prefer these in widgets so intent survives a repaint.
|
// Semantic aliases — prefer these in widgets so intent survives a repaint.
|
||||||
readonly property color ok: green
|
readonly property color ok: green
|
||||||
@@ -70,14 +59,11 @@ Singleton {
|
|||||||
// The dock sits on compositor blur, so its fill is deliberately very
|
// The dock sits on compositor blur, so its fill is deliberately very
|
||||||
// translucent. Popovers are near-opaque because they carry text that must
|
// translucent. Popovers are near-opaque because they carry text that must
|
||||||
// stay legible. The bar itself has no surface or alpha token.
|
// stay legible. The bar itself has no surface or alpha token.
|
||||||
// Light surfaces need more opacity for the same sense of a solid panel: the
|
readonly property real dockAlpha: 0.34
|
||||||
// same 0.34 that reads as glass over a dark desktop reads as haze over a
|
readonly property real popoverAlpha: 0.92
|
||||||
// light one, and text on it stops being legible.
|
readonly property real overlayAlpha: 0.55
|
||||||
readonly property real dockAlpha: root.dark ? 0.34 : 0.62
|
readonly property real hoverAlpha: 0.14
|
||||||
readonly property real popoverAlpha: root.dark ? 0.92 : 0.97
|
readonly property real activeAlpha: 0.24
|
||||||
readonly property real overlayAlpha: root.dark ? 0.55 : 0.40
|
|
||||||
readonly property real hoverAlpha: root.dark ? 0.14 : 0.10
|
|
||||||
readonly property real activeAlpha: root.dark ? 0.24 : 0.18
|
|
||||||
|
|
||||||
// ── Geometry ────────────────────────────────────────────────────────────
|
// ── Geometry ────────────────────────────────────────────────────────────
|
||||||
readonly property int barHeight: 36
|
readonly property int barHeight: 36
|
||||||
|
|||||||
@@ -1,44 +1,24 @@
|
|||||||
// Brightness, from whichever source this machine actually has.
|
// Backlight slider, via brightnessctl.
|
||||||
//
|
//
|
||||||
// Two exist and they are not interchangeable:
|
// This machine drives an external DisplayPort monitor and has no backlight
|
||||||
//
|
// class device at all (brightnessctl only reports keyboard/NIC LEDs), so the
|
||||||
// The kernel backlight class, driven by brightnessctl. Laptop panels have it;
|
// row removes itself rather than sitting there as a dead control. Probed once
|
||||||
// this desktop does not -- brightnessctl reports only keyboard and NIC LEDs.
|
// at startup — backlight devices do not appear and disappear.
|
||||||
//
|
|
||||||
// DDC/CI, the channel the buttons on a monitor's bezel drive. That is the
|
|
||||||
// only brightness an external display has, and it is per-monitor.
|
|
||||||
//
|
|
||||||
// A machine may have neither, either, or both, so this renders a row per source
|
|
||||||
// found and removes itself entirely when there are none, rather than sitting
|
|
||||||
// there as a dead control.
|
|
||||||
//
|
|
||||||
// Connector labels appear only when there is more than one row. A single
|
|
||||||
// slider needs no explanation of which screen it dims.
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.widgets
|
import qs.widgets
|
||||||
import qs.config
|
import qs.config
|
||||||
import qs.services
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool backlightAvailable: false
|
property bool available: false
|
||||||
property real backlightValue: 0
|
property real value: 0
|
||||||
|
|
||||||
readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length
|
visible: root.available
|
||||||
readonly property bool labelled: root.rowCount > 1
|
implicitHeight: root.available ? 32 : 0
|
||||||
|
|
||||||
visible: root.rowCount > 0
|
|
||||||
implicitHeight: rows.implicitHeight
|
|
||||||
|
|
||||||
// Probing I2C takes on the order of a second, so it waits until the panel
|
|
||||||
// is actually on screen rather than running at shell startup. Monitors do
|
|
||||||
// not come and go, so once is enough.
|
|
||||||
onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
|
|
||||||
Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
|
|
||||||
|
|
||||||
// `-m` is the machine-readable form: name,class,current,percent,max
|
// `-m` is the machine-readable form: name,class,current,percent,max
|
||||||
Process {
|
Process {
|
||||||
@@ -54,105 +34,36 @@ Item {
|
|||||||
const fields = line.split(",");
|
const fields = line.split(",");
|
||||||
if (fields.length < 5 || fields[1] !== "backlight")
|
if (fields.length < 5 || fields[1] !== "backlight")
|
||||||
continue;
|
continue;
|
||||||
root.backlightAvailable = true;
|
root.available = true;
|
||||||
root.backlightValue = parseInt(fields[3]) / 100;
|
root.value = parseInt(fields[3]) / 100;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBacklight(v: real): void {
|
function apply(v: real): void {
|
||||||
root.backlightValue = v;
|
root.value = v;
|
||||||
// Never go fully dark: a 0% backlight looks like a broken shell.
|
// Never go fully dark: a 0% backlight looks like a broken shell.
|
||||||
Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]);
|
Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
// ValueSlider draws its own leading icon, but symbolic icons need
|
||||||
id: rows
|
// recolouring to be visible — see ThemedIcon.
|
||||||
|
ThemedIcon {
|
||||||
|
id: glyph
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.leftMargin: 6
|
||||||
spacing: 4
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
size: 17
|
||||||
BrightnessRow {
|
icon: "display-brightness-symbolic"
|
||||||
width: rows.width
|
|
||||||
visible: root.backlightAvailable
|
|
||||||
label: "Built-in"
|
|
||||||
value: root.backlightValue
|
|
||||||
onMoved: v => root.applyBacklight(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Brightness.displays
|
|
||||||
|
|
||||||
BrightnessRow {
|
|
||||||
required property var modelData
|
|
||||||
width: rows.width
|
|
||||||
// Hyprland already knows what each output is called, so the
|
|
||||||
// name comes from there rather than from a second source that
|
|
||||||
// could disagree with the Displays page. The connector is the
|
|
||||||
// fallback, so a display is never an unlabelled slider.
|
|
||||||
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
|
||||||
value: modelData.value / 100
|
|
||||||
onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component BrightnessRow: Item {
|
ValueSlider {
|
||||||
id: row
|
anchors.left: glyph.right
|
||||||
|
anchors.leftMargin: 12
|
||||||
property string label: ""
|
anchors.right: parent.right
|
||||||
property real value: 0
|
anchors.rightMargin: 32
|
||||||
signal moved(real value)
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
value: root.value
|
||||||
implicitHeight: caption.height + control.height
|
onMoved: v => root.apply(v)
|
||||||
|
|
||||||
Text {
|
|
||||||
id: caption
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: 6
|
|
||||||
anchors.rightMargin: 6
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: root.labelled
|
|
||||||
height: visible ? implicitHeight + 2 : 0
|
|
||||||
text: row.label
|
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
// The slider and its glyph share one strip so the two stay aligned
|
|
||||||
// whether or not a caption sits above them. Anchoring the glyph to
|
|
||||||
// both a caption and a centre line instead would conflict, and an
|
|
||||||
// anchor set to undefined is not released.
|
|
||||||
Item {
|
|
||||||
id: control
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: caption.bottom
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
// ValueSlider draws its own leading icon, but symbolic icons need
|
|
||||||
// recolouring to be visible — see ThemedIcon.
|
|
||||||
ThemedIcon {
|
|
||||||
id: glyph
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 6
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
size: 17
|
|
||||||
icon: "display-brightness-symbolic"
|
|
||||||
}
|
|
||||||
|
|
||||||
ValueSlider {
|
|
||||||
anchors.left: glyph.right
|
|
||||||
anchors.leftMargin: 12
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 32
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
value: row.value
|
|
||||||
onMoved: v => row.moved(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,15 +73,6 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Colour scheme"
|
|
||||||
subtitle: ColorScheme.lastError !== ""
|
|
||||||
? ColorScheme.lastError
|
|
||||||
: "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow."
|
|
||||||
|
|
||||||
ChoiceRow { setting: "colorScheme"; divider: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Typography"
|
title: "Typography"
|
||||||
subtitle: Fonts.lastError !== ""
|
subtitle: Fonts.lastError !== ""
|
||||||
|
|||||||
@@ -50,13 +50,10 @@ Rectangle {
|
|||||||
|
|
||||||
// Stands in for the wallpaper. Static: an animated gradient here would
|
// Stands in for the wallpaper. Static: an animated gradient here would
|
||||||
// repaint forever behind a settings page.
|
// repaint forever behind a settings page.
|
||||||
// Follows the colour scheme. A preview that stays dark while the shell
|
|
||||||
// around it is light does not read as "your desktop" -- it reads as a
|
|
||||||
// screenshot of someone else's.
|
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
orientation: Gradient.Vertical
|
orientation: Gradient.Vertical
|
||||||
GradientStop { position: 0.0; color: Theme.dark ? "#2b3050" : "#b9c0dd" }
|
GradientStop { position: 0.0; color: "#2b3050" }
|
||||||
GradientStop { position: 1.0; color: Theme.dark ? "#241f33" : "#cbbcd4" }
|
GradientStop { position: 1.0; color: "#241f33" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bar, so the preview reads as this desktop and not a generic one.
|
// The bar, so the preview reads as this desktop and not a generic one.
|
||||||
@@ -206,7 +203,7 @@ Rectangle {
|
|||||||
shadowEnabled: true
|
shadowEnabled: true
|
||||||
shadowColor: win.focused && root.glowOn
|
shadowColor: win.focused && root.glowOn
|
||||||
? Theme.alpha(Theme.accent, 0.5)
|
? Theme.alpha(Theme.accent, 0.5)
|
||||||
: Theme.alpha(Theme.dark ? "#15161e" : "#6172b0", Theme.dark ? 0.85 : 0.45)
|
: Theme.alpha("#15161e", 0.85)
|
||||||
shadowBlur: win.focused && root.glowOn
|
shadowBlur: win.focused && root.glowOn
|
||||||
? Math.min(1.0, root.px(root.glowRange) / 12)
|
? Math.min(1.0, root.px(root.glowRange) / 12)
|
||||||
: Math.min(1.0, root.px(root.shadowRange) / 24)
|
: Math.min(1.0, root.px(root.shadowRange) / 24)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Displays.
|
// Displays.
|
||||||
//
|
//
|
||||||
// Resolution, refresh rate, scale, and rotation, plus panel brightness and the
|
// Resolution, refresh rate, scale, and rotation, plus the gaming display
|
||||||
// gaming display policy that was already here.
|
// policy that was already here.
|
||||||
//
|
//
|
||||||
// Every geometry change goes through an apply-then-confirm countdown. This is
|
// Every geometry change goes through an apply-then-confirm countdown. This is
|
||||||
// the one page where a wrong value can leave the screen unreadable or blank,
|
// the one page where a wrong value can leave the screen unreadable or blank,
|
||||||
@@ -32,14 +32,7 @@ SettingsPage {
|
|||||||
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : "";
|
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Probing I2C for DDC-capable monitors takes on the order of a second, so
|
Component.onCompleted: root.syncSelectedOutput()
|
||||||
// it runs when this page is opened rather than at shell startup. Monitors
|
|
||||||
// do not appear while you are looking at a settings page, so once is enough.
|
|
||||||
Component.onCompleted: {
|
|
||||||
root.syncSelectedOutput();
|
|
||||||
if (!Brightness.scanned)
|
|
||||||
Brightness.refresh();
|
|
||||||
}
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Displays
|
target: Displays
|
||||||
function onMonitorsChanged(): void { root.syncSelectedOutput(); }
|
function onMonitorsChanged(): void { root.syncSelectedOutput(); }
|
||||||
@@ -207,45 +200,6 @@ SettingsPage {
|
|||||||
SliderRow { setting: "nightLightTemperature"; divider: false }
|
SliderRow { setting: "nightLightTemperature"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel brightness, over DDC/CI.
|
|
||||||
//
|
|
||||||
// This is hardware state rather than a stored preference: the monitor
|
|
||||||
// remembers it, the bezel buttons change it behind Panama's back, and
|
|
||||||
// writing it into settings.json would mean restoring a value the panel had
|
|
||||||
// already moved on from. So there is no schema key here and no SliderRow --
|
|
||||||
// the rows read and write the display directly.
|
|
||||||
SettingsCard {
|
|
||||||
visible: Brightness.available || Brightness.lastError !== ""
|
|
||||||
title: "Brightness"
|
|
||||||
subtitle: Brightness.available
|
|
||||||
? "Sent to the monitor over DDC/CI, the same channel its buttons use."
|
|
||||||
: Brightness.lastError
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Brightness.displays
|
|
||||||
|
|
||||||
SettingRow {
|
|
||||||
id: brightnessRow
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
|
||||||
detail: modelData.connector ? modelData.connector + " · " + modelData.value + "%"
|
|
||||||
: modelData.value + "%"
|
|
||||||
divider: brightnessRow.index < Brightness.displays.length - 1
|
|
||||||
controlWidth: 190
|
|
||||||
|
|
||||||
ValueSlider {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: parent.right
|
|
||||||
width: parent.width
|
|
||||||
value: brightnessRow.modelData.value / 100
|
|
||||||
onMoved: v => Brightness.set(brightnessRow.modelData.bus, Math.round(v * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Gaming display policy"
|
title: "Gaming display policy"
|
||||||
subtitle: "Applied immediately and restored when Panama starts."
|
subtitle: "Applied immediately and restored when Panama starts."
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ set -euo pipefail
|
|||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
action="${1:-}"
|
action="${1:-}"
|
||||||
helper_dir="${PANAMA_ACTION_HELPER_DIR:-$script_dir}"
|
|
||||||
confirm_attempts="${PANAMA_ACTION_CONFIRM_ATTEMPTS:-20}"
|
|
||||||
confirm_delay="${PANAMA_ACTION_CONFIRM_DELAY:-0.1}"
|
|
||||||
kill_command="${PANAMA_ACTION_KILL_COMMAND:-/usr/bin/kill}"
|
|
||||||
|
|
||||||
osd_command=("$helper_dir/panama-osd")
|
if command -v panama-osd >/dev/null 2>&1; then
|
||||||
gallery_command=("$helper_dir/panama-prism-gallery")
|
osd_command=(panama-osd)
|
||||||
|
else
|
||||||
|
osd_command=("$script_dir/panama-osd")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v panama-prism-gallery >/dev/null 2>&1; then
|
||||||
|
gallery_command=(panama-prism-gallery)
|
||||||
|
else
|
||||||
|
gallery_command=("$script_dir/panama-prism-gallery")
|
||||||
|
fi
|
||||||
|
|
||||||
report_failure() {
|
report_failure() {
|
||||||
local status=$?
|
local status=$?
|
||||||
@@ -30,8 +35,8 @@ trap report_failure EXIT
|
|||||||
show_state() {
|
show_state() {
|
||||||
local state="$1" on_icon="$2" off_icon="$3" label="$4"
|
local state="$1" on_icon="$2" off_icon="$3" label="$4"
|
||||||
case "$state" in
|
case "$state" in
|
||||||
true) PANAMA_OSD_STRICT=1 "${osd_command[@]}" message "$on_icon" "$label On" ;;
|
true) "${osd_command[@]}" message "$on_icon" "$label On" ;;
|
||||||
false) PANAMA_OSD_STRICT=1 "${osd_command[@]}" message "$off_icon" "$label Off" ;;
|
false) "${osd_command[@]}" message "$off_icon" "$label Off" ;;
|
||||||
*)
|
*)
|
||||||
printf 'Panama action returned an invalid state: %s\n' "$state" >&2
|
printf 'Panama action returned an invalid state: %s\n' "$state" >&2
|
||||||
return 1
|
return 1
|
||||||
@@ -39,69 +44,12 @@ show_state() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
state_is_active() {
|
|
||||||
local target="$1"
|
|
||||||
case "$target" in
|
|
||||||
caffeine)
|
|
||||||
systemd-inhibit --list --no-pager --no-legend 2>/dev/null \
|
|
||||||
| awk '$1 == "Panama" && $(NF - 1) == "Caffeine" && $NF == "block" { found = 1 } END { exit !found }'
|
|
||||||
;;
|
|
||||||
night-light)
|
|
||||||
pgrep -u "$(id -u)" -x hyprsunset >/dev/null 2>&1
|
|
||||||
;;
|
|
||||||
*) return 2 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
release_caffeine_inhibitors() {
|
|
||||||
local inhibitors pid uid
|
|
||||||
uid="$(id -u)"
|
|
||||||
inhibitors="$(systemd-inhibit --list --no-pager --no-legend 2>/dev/null \
|
|
||||||
| awk -v uid="$uid" '$1 == "Panama" && $2 == uid && $(NF - 1) == "Caffeine" && $NF == "block" { print $4 }')"
|
|
||||||
while IFS= read -r pid; do
|
|
||||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
|
||||||
"$kill_command" "$pid" 2>/dev/null || true
|
|
||||||
done <<<"$inhibitors"
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_confirmed_state() {
|
|
||||||
local target="$1" expected="$2" attempt
|
|
||||||
for ((attempt = 0; attempt < confirm_attempts; attempt++)); do
|
|
||||||
if [[ $expected == true ]] && state_is_active "$target"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ $expected == false ]] && ! state_is_active "$target"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep "$confirm_delay"
|
|
||||||
done
|
|
||||||
printf 'Panama action could not confirm %s reached state %s\n' "$target" "$expected" >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle_state() {
|
toggle_state() {
|
||||||
local target="$1" on_icon="$2" off_icon="$3" label="$4" state
|
local target="$1" on_icon="$2" off_icon="$3" label="$4" state
|
||||||
state="$(qs ipc call "$target" toggle)"
|
state="$(qs ipc call "$target" toggle)"
|
||||||
case "$state" in true|false) ;; *) show_state "$state" "$on_icon" "$off_icon" "$label"; return ;; esac
|
|
||||||
if [[ $target == caffeine && $state == false ]]; then
|
|
||||||
release_caffeine_inhibitors
|
|
||||||
fi
|
|
||||||
wait_for_confirmed_state "$target" "$state"
|
|
||||||
show_state "$state" "$on_icon" "$off_icon" "$label"
|
show_state "$state" "$on_icon" "$off_icon" "$label"
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_shell() {
|
|
||||||
local attempt
|
|
||||||
for ((attempt = 0; attempt < confirm_attempts; attempt++)); do
|
|
||||||
if qs ipc show >/dev/null 2>&1; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep "$confirm_delay"
|
|
||||||
done
|
|
||||||
printf 'Panama shell did not become ready after restart\n' >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$action" in
|
case "$action" in
|
||||||
control-center) qs ipc call quicksettings open ;;
|
control-center) qs ipc call quicksettings open ;;
|
||||||
notifications) qs ipc call notifications open ;;
|
notifications) qs ipc call notifications open ;;
|
||||||
@@ -121,30 +69,23 @@ case "$action" in
|
|||||||
toggle_state night-light night-light-symbolic night-light-disabled-symbolic "Night Light"
|
toggle_state night-light night-light-symbolic night-light-disabled-symbolic "Night Light"
|
||||||
;;
|
;;
|
||||||
focus-start)
|
focus-start)
|
||||||
state="$(qs ipc call focus start)"
|
qs ipc call focus start
|
||||||
[[ $state == true ]]
|
"${osd_command[@]}" message preferences-system-time-symbolic "Focus Session Started"
|
||||||
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message preferences-system-time-symbolic "Focus Session Started"
|
|
||||||
;;
|
;;
|
||||||
focus-end)
|
focus-end)
|
||||||
state="$(qs ipc call focus end)"
|
qs ipc call focus end
|
||||||
if [[ $state == true ]]; then
|
"${osd_command[@]}" message media-playback-stop-symbolic "Focus Session Ended"
|
||||||
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message media-playback-stop-symbolic "Focus Session Ended"
|
|
||||||
else
|
|
||||||
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message dialog-information-symbolic "No Focus Session Running"
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
capture) qs ipc call capture open ;;
|
capture) qs ipc call capture open ;;
|
||||||
intelligence) qs ipc call screen-intelligence open ;;
|
intelligence) qs ipc call screen-intelligence open ;;
|
||||||
screenshot) qs ipc call capture screenNow ;;
|
screenshot) qs ipc call capture screenNow ;;
|
||||||
microphone) PANAMA_OSD_STRICT=1 "${osd_command[@]}" microphone toggle ;;
|
microphone) "${osd_command[@]}" microphone toggle ;;
|
||||||
|
|
||||||
gallery) "${gallery_command[@]}" ;;
|
gallery) "${gallery_command[@]}" ;;
|
||||||
restart-shell)
|
restart-shell)
|
||||||
qs kill >/dev/null 2>&1 || true
|
qs kill
|
||||||
quickshell --daemonize
|
quickshell --daemonize
|
||||||
wait_for_shell
|
|
||||||
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message view-refresh-symbolic "Panama Restarted"
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf 'Usage: panama-action {%s}\n' \
|
printf 'Usage: panama-action {%s}\n' \
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# External monitor brightness over DDC/CI.
|
|
||||||
#
|
|
||||||
# A desktop with no backlight class device has no brightness control at all --
|
|
||||||
# brightnessctl only sees keyboard and NIC LEDs. The panel itself still has a
|
|
||||||
# brightness setting, reachable over the monitor's DDC/CI channel (VCP feature
|
|
||||||
# 0x10), which is what the buttons on the bezel drive.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# panama-brightness list -> {"displays":[...],"error":""}
|
|
||||||
# panama-brightness get <bus> -> integer percent
|
|
||||||
# panama-brightness set <bus> <pct> -> applies, prints nothing
|
|
||||||
#
|
|
||||||
# Displays are enumerated from sysfs rather than from `ddcutil detect`. The
|
|
||||||
# kernel publishes the connector-to-I2C-bus mapping directly, as
|
|
||||||
# /sys/class/drm/<card>-<connector>/ddc, along with whether anything is plugged
|
|
||||||
# in. That is better than parsing detect output in three ways: the format is
|
|
||||||
# stable where detect's brief output is undocumented, the connector name comes
|
|
||||||
# out exactly as Hyprland and the Displays page already spell it (DP-2), and
|
|
||||||
# only connectors with a monitor attached get probed -- one bus on this machine
|
|
||||||
# instead of fourteen, which is the difference between a fast scan and a slow
|
|
||||||
# one, since each probe of an empty bus waits for a timeout.
|
|
||||||
#
|
|
||||||
# No model name is reported. Hyprland already knows the human-readable
|
|
||||||
# description of every output, so the UI joins on the connector name rather than
|
|
||||||
# having two sources of truth for what a monitor is called.
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
readonly VCP_BRIGHTNESS=0x10
|
|
||||||
|
|
||||||
# Test seams. The contract needs to exercise enumeration and parsing on a
|
|
||||||
# machine whose real monitors it must not touch, so both roots this script
|
|
||||||
# reads are overridable. Nothing sets them in normal use.
|
|
||||||
readonly DRM_ROOT="${PANAMA_BRIGHTNESS_DRM_ROOT:-/sys/class/drm}"
|
|
||||||
readonly DEV_ROOT="${PANAMA_BRIGHTNESS_DEV_ROOT:-/dev}"
|
|
||||||
|
|
||||||
emit_error() {
|
|
||||||
printf '{"displays":[],"error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
command -v ddcutil >/dev/null 2>&1 || emit_error 'ddcutil is not installed'
|
|
||||||
|
|
||||||
# Reading a VCP value needs read/write access to the monitor's I2C bus. The
|
|
||||||
# udev rule ddcutil ships grants that to the seat user through uaccess, but only
|
|
||||||
# to devices created after the rule was installed -- so a machine that installed
|
|
||||||
# ddcutil without rebooting has the rule in place and no access to show for it.
|
|
||||||
# That is by far the most likely reason for an empty list, and it is fixable in
|
|
||||||
# one command, so say so rather than reporting "no displays".
|
|
||||||
has_accessible_bus() {
|
|
||||||
local dev
|
|
||||||
for dev in "$DEV_ROOT"/i2c-*; do
|
|
||||||
[[ -r "$dev" && -w "$dev" ]] && return 0
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_list() {
|
|
||||||
has_accessible_bus || emit_error 'no I2C bus is accessible. ddcutil ships a udev rule that grants this, but only to devices created after it was installed. Run: sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm'
|
|
||||||
|
|
||||||
local rows=() connector bus value path
|
|
||||||
for path in "$DRM_ROOT"/card*-*; do
|
|
||||||
[[ -e "$path/ddc" ]] || continue
|
|
||||||
[[ "$(cat "$path/status" 2>/dev/null)" == "connected" ]] || continue
|
|
||||||
|
|
||||||
# card1-DP-2 -> DP-2, the name Hyprland uses.
|
|
||||||
connector="$(basename "$path")"
|
|
||||||
connector="${connector#card*-}"
|
|
||||||
|
|
||||||
bus="$(basename "$(readlink -f "$path/ddc")")"
|
|
||||||
bus="${bus#i2c-}"
|
|
||||||
[[ "$bus" =~ ^[0-9]+$ ]] || continue
|
|
||||||
|
|
||||||
# A monitor that does not implement 0x10 is not an error; it simply
|
|
||||||
# cannot be controlled, and is left out rather than shown as a slider
|
|
||||||
# that does nothing.
|
|
||||||
value="$(cmd_get "$bus")" || continue
|
|
||||||
[[ -n "$value" ]] || continue
|
|
||||||
|
|
||||||
rows+=("$(jq -cn \
|
|
||||||
--argjson bus "$bus" \
|
|
||||||
--arg connector "$connector" \
|
|
||||||
--argjson value "$value" \
|
|
||||||
'{bus: $bus, connector: $connector, value: $value}')")
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#rows[@]} -eq 0 ]]; then
|
|
||||||
emit_error 'no connected monitor reports DDC/CI brightness. Some panels implement it only when "DDC/CI" is enabled in their on-screen menu.'
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '{"displays":[%s],"error":""}\n' "$(IFS=,; printf '%s' "${rows[*]}")"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prints the current brightness as a whole percent, or nothing when the display
|
|
||||||
# cannot report it. `getvcp --brief` is documented as machine readable and
|
|
||||||
# answers "VCP 10 C <current> <max>"; the max is almost always 100 but is not
|
|
||||||
# guaranteed to be, so it is read rather than assumed.
|
|
||||||
cmd_get() {
|
|
||||||
local bus="$1" out current max
|
|
||||||
out="$(timeout 10 ddcutil --bus "$bus" getvcp "$VCP_BRIGHTNESS" --brief 2>/dev/null)" || return 1
|
|
||||||
read -r _ _ _ current max <<<"$out"
|
|
||||||
[[ "$current" =~ ^[0-9]+$ && "$max" =~ ^[0-9]+$ && "$max" -gt 0 ]] || return 1
|
|
||||||
printf '%s' "$(( current * 100 / max ))"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_set() {
|
|
||||||
local bus="$1" percent="$2" max out
|
|
||||||
[[ "$percent" =~ ^[0-9]+$ ]] || return 1
|
|
||||||
(( percent > 100 )) && percent=100
|
|
||||||
|
|
||||||
out="$(timeout 10 ddcutil --bus "$bus" getvcp "$VCP_BRIGHTNESS" --brief 2>/dev/null)" || return 1
|
|
||||||
read -r _ _ _ _ max <<<"$out"
|
|
||||||
[[ "$max" =~ ^[0-9]+$ && "$max" -gt 0 ]] || max=100
|
|
||||||
|
|
||||||
timeout 10 ddcutil --bus "$bus" setvcp "$VCP_BRIGHTNESS" "$(( percent * max / 100 ))" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-list}" in
|
|
||||||
list) cmd_list ;;
|
|
||||||
get) cmd_get "${2:?bus required}" ;;
|
|
||||||
set) cmd_set "${2:?bus required}" "${3:?percent required}" ;;
|
|
||||||
*) printf 'usage: panama-brightness [list|get <bus>|set <bus> <percent>]\n' >&2; exit 2 ;;
|
|
||||||
esac
|
|
||||||
@@ -2,24 +2,12 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
readonly PANAMA_OSD_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
strict_delivery() {
|
|
||||||
[[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
show_progress() {
|
show_progress() {
|
||||||
if ! qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1; then
|
qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1 || true
|
||||||
strict_delivery && return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show_message() {
|
show_message() {
|
||||||
if ! qs ipc call osd message "$1" "$2" >/dev/null 2>&1; then
|
qs ipc call osd message "$1" "$2" >/dev/null 2>&1 || true
|
||||||
strict_delivery && return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
volume_state() {
|
volume_state() {
|
||||||
@@ -69,151 +57,18 @@ adjust_microphone() {
|
|||||||
show_volume "$target" microphone
|
show_volume "$target" microphone
|
||||||
}
|
}
|
||||||
|
|
||||||
brightness_percent() {
|
|
||||||
local output="$1" percent
|
|
||||||
percent="$(awk -F, 'NR == 1 { value=$5; gsub(/%/, "", value); print value }' <<<"$output")"
|
|
||||||
[[ $percent =~ ^[0-9]+$ ]] || return 1
|
|
||||||
printf '%s\n' "$percent"
|
|
||||||
}
|
|
||||||
|
|
||||||
brightness_error() {
|
|
||||||
local detail="$1" label="External brightness unavailable"
|
|
||||||
if [[ $detail == *udev* || $detail == *accessible* || $detail == *permission* ]]; then
|
|
||||||
label="Brightness needs permission"
|
|
||||||
fi
|
|
||||||
|
|
||||||
show_message dialog-warning-symbolic "$label" || true
|
|
||||||
if command -v notify-send >/dev/null 2>&1; then
|
|
||||||
notify-send --app-name=Panama --icon=display-brightness-symbolic \
|
|
||||||
"Brightness unavailable" "$detail" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
discover_ddc_bus() {
|
|
||||||
local helper="$1" cache_file="$2" list_json focused selected error bus connector
|
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || {
|
|
||||||
brightness_error "jq is required to discover DDC/CI displays."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
list_json="$("$helper" list 2>/dev/null)" || {
|
|
||||||
brightness_error "The external brightness helper could not inspect connected displays."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if ! jq -e 'type == "object" and (.displays | type == "array")' >/dev/null 2>&1 <<<"$list_json"; then
|
|
||||||
brightness_error "The external brightness helper returned invalid display information."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
error="$(jq -r '.error // empty' <<<"$list_json")"
|
|
||||||
if [[ -n $error ]]; then
|
|
||||||
brightness_error "$error"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
focused="$(hyprctl -j monitors 2>/dev/null \
|
|
||||||
| jq -r '.[] | select(.focused == true) | .name' 2>/dev/null \
|
|
||||||
| head -n1)"
|
|
||||||
selected="$(jq -r --arg connector "$focused" '
|
|
||||||
([.displays[] | select(.connector == $connector)][0] // .displays[0] // empty)
|
|
||||||
| [.bus, .connector]
|
|
||||||
| @tsv
|
|
||||||
' <<<"$list_json")"
|
|
||||||
IFS=$'\t' read -r bus connector <<<"$selected"
|
|
||||||
if [[ ! $bus =~ ^[0-9]+$ ]]; then
|
|
||||||
brightness_error "No connected monitor exposes DDC/CI brightness control."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
umask 077
|
|
||||||
printf '%s\t%s\n' "$bus" "$connector" >"$cache_file"
|
|
||||||
printf '%s\n' "$bus"
|
|
||||||
}
|
|
||||||
|
|
||||||
adjust_ddc_brightness() {
|
|
||||||
local action="$1" step="$2"
|
|
||||||
local helper="${PANAMA_OSD_BRIGHTNESS_HELPER:-$PANAMA_OSD_SCRIPT_DIR/panama-brightness}"
|
|
||||||
local runtime_dir="${PANAMA_OSD_RUNTIME_DIR:-${XDG_RUNTIME_DIR:-/tmp}/panama-osd-${UID}}"
|
|
||||||
local cache_file="$runtime_dir/brightness-bus" lock_file="$runtime_dir/brightness.lock"
|
|
||||||
local bus="" connector="" current target lock_fd
|
|
||||||
|
|
||||||
[[ -x $helper ]] || {
|
|
||||||
brightness_error "The external brightness helper is not installed."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
mkdir -p "$runtime_dir" || return 0
|
|
||||||
chmod 700 "$runtime_dir" 2>/dev/null || true
|
|
||||||
|
|
||||||
exec {lock_fd}>"$lock_file" || return 0
|
|
||||||
# DDC transactions on one I2C bus cannot safely overlap. A short wait also
|
|
||||||
# sheds an excessive key-repeat backlog instead of replaying it seconds later.
|
|
||||||
flock -w 2 "$lock_fd" || return 0
|
|
||||||
|
|
||||||
if [[ -r $cache_file ]]; then
|
|
||||||
IFS=$'\t' read -r bus connector <"$cache_file" || true
|
|
||||||
[[ $bus =~ ^[0-9]+$ ]] || bus=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n $bus ]]; then
|
|
||||||
current="$("$helper" get "$bus" 2>/dev/null)" || current=""
|
|
||||||
if [[ ! $current =~ ^[0-9]+$ ]]; then
|
|
||||||
: >"$cache_file"
|
|
||||||
bus=""
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z $bus ]]; then
|
|
||||||
bus="$(discover_ddc_bus "$helper" "$cache_file")" || return 0
|
|
||||||
current="$("$helper" get "$bus" 2>/dev/null)" || current=""
|
|
||||||
fi
|
|
||||||
if [[ ! $current =~ ^[0-9]+$ ]]; then
|
|
||||||
brightness_error "The selected monitor stopped responding over DDC/CI."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $action == up ]]; then
|
|
||||||
target=$(( current + step ))
|
|
||||||
else
|
|
||||||
target=$(( current - step ))
|
|
||||||
fi
|
|
||||||
(( target > 100 )) && target=100
|
|
||||||
(( target < 0 )) && target=0
|
|
||||||
|
|
||||||
if ! "$helper" set "$bus" "$target" >/dev/null 2>&1; then
|
|
||||||
brightness_error "The selected monitor did not accept the brightness change."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
show_progress brightness "$target" "${target}%"
|
|
||||||
}
|
|
||||||
|
|
||||||
adjust_brightness() {
|
adjust_brightness() {
|
||||||
local action="${1:-}" step="${2:-5}" output percent
|
local action="${1:-}" step="${2:-5}" output percent
|
||||||
[[ $step =~ ^[0-9]+$ ]] || {
|
|
||||||
printf 'Usage: panama-osd brightness up|down [step]\n' >&2
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
case "$action" in
|
case "$action" in
|
||||||
up|down) ;;
|
up) brightnessctl -e4 -n2 set "${step}%+" >/dev/null || return ;;
|
||||||
|
down) brightnessctl -e4 -n2 set "${step}%-" >/dev/null || return ;;
|
||||||
*) printf 'Usage: panama-osd brightness up|down [step]\n' >&2; return 2 ;;
|
*) printf 'Usage: panama-osd brightness up|down [step]\n' >&2; return 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Laptop panels expose a kernel backlight class and remain the fastest,
|
output="$(brightnessctl -m -c backlight 2>/dev/null)" || return 0
|
||||||
# most reliable path. Desktops fall through to DDC/CI monitor control.
|
percent="$(awk -F, 'NR == 1 { value=$5; gsub(/%/, "", value); print value }' <<<"$output")"
|
||||||
output="$(brightnessctl -m -c backlight 2>/dev/null)" || output=""
|
[[ $percent =~ ^[0-9]+$ ]] || return 0
|
||||||
if percent="$(brightness_percent "$output")"; then
|
show_progress brightness "$percent" "${percent}%"
|
||||||
if [[ $action == up ]]; then
|
|
||||||
brightnessctl -e4 -n2 -c backlight set "${step}%+" >/dev/null || return 0
|
|
||||||
else
|
|
||||||
brightnessctl -e4 -n2 -c backlight set "${step}%-" >/dev/null || return 0
|
|
||||||
fi
|
|
||||||
output="$(brightnessctl -m -c backlight 2>/dev/null)" || return 0
|
|
||||||
percent="$(brightness_percent "$output")" || return 0
|
|
||||||
show_progress brightness "$percent" "${percent}%"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
adjust_ddc_brightness "$action" "$step"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
media_action() {
|
media_action() {
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Propagates the colour scheme to applications that do not read the desktop
|
|
||||||
# portal.
|
|
||||||
#
|
|
||||||
# Most modern applications DO follow org.freedesktop.portal.Settings and need
|
|
||||||
# nothing from us: GTK4/libadwaita, Qt6, Chromium and Electron all read
|
|
||||||
# org.freedesktop.appearance color-scheme, which xdg-desktop-portal-gtk serves
|
|
||||||
# from gsettings. Panama sets that, so those follow automatically.
|
|
||||||
#
|
|
||||||
# Terminals are the notable exception -- they predate the standard and carry
|
|
||||||
# their own palettes. kitty is handled here.
|
|
||||||
#
|
|
||||||
# panama-theme-apps dark|light
|
|
||||||
#
|
|
||||||
# kitty gets it twice: the generated include file so terminals opened later
|
|
||||||
# start correct, and a live `set-colors` over its control socket so terminals
|
|
||||||
# already open change now. Without the second, a scheme change appears to do
|
|
||||||
# nothing until you open a new window.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
scheme="${1:-dark}"
|
|
||||||
case "$scheme" in
|
|
||||||
dark|light) ;;
|
|
||||||
*) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
kitty_dir="${XDG_CONFIG_HOME:-$HOME/.config}/kitty"
|
|
||||||
theme_file="$kitty_dir/themes/tokyonight-moon.conf"
|
|
||||||
[[ "$scheme" == "light" ]] && theme_file="$kitty_dir/themes/tokyonight-day.conf"
|
|
||||||
|
|
||||||
status_kitty="skipped"
|
|
||||||
if [[ -r "$theme_file" ]]; then
|
|
||||||
# Written atomically: kitty may read this while a new window is starting.
|
|
||||||
if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \
|
|
||||||
&& mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then
|
|
||||||
status_kitty="written"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Live-apply to running terminals. kitty appends its PID to the socket name
|
|
||||||
# from listen_on, so there is one socket per instance -- "unix:@mykitty"
|
|
||||||
# alone reaches nothing, which is exactly how this looked like remote
|
|
||||||
# control being disabled when it was not.
|
|
||||||
if command -v kitty >/dev/null 2>&1 && command -v ss >/dev/null 2>&1; then
|
|
||||||
applied=0
|
|
||||||
while read -r socket; do
|
|
||||||
[[ -n "$socket" ]] || continue
|
|
||||||
if kitty @ --to "unix:@${socket#@}" set-colors --all --configured "$theme_file" >/dev/null 2>&1; then
|
|
||||||
applied=$((applied + 1))
|
|
||||||
fi
|
|
||||||
done < <(ss -xl 2>/dev/null | grep -oE '@mykitty[^[:space:]]*' | sort -u)
|
|
||||||
(( applied > 0 )) && status_kitty="applied to $applied"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '{"scheme":"%s","kitty":"%s"}\n' "$scheme" "$status_kitty"
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
// Panel brightness for external monitors, over DDC/CI.
|
|
||||||
//
|
|
||||||
// brightnessctl covers laptop panels through the kernel's backlight class. A
|
|
||||||
// desktop driving a DisplayPort monitor has no such device, so it has no
|
|
||||||
// brightness control at all -- the only way to dim the screen is the buttons on
|
|
||||||
// the bezel. DDC/CI is the channel those buttons drive, and monitors expose it
|
|
||||||
// over the same I2C lines that carry EDID.
|
|
||||||
//
|
|
||||||
// Two things shape everything here:
|
|
||||||
//
|
|
||||||
// Detection is slow. Probing every I2C bus takes on the order of a second,
|
|
||||||
// which is far too slow to sit in front of a settings page opening. It runs
|
|
||||||
// once, on demand, and afterwards each display is addressed by its bus number
|
|
||||||
// directly.
|
|
||||||
//
|
|
||||||
// Writes are slow AND rate-limited by the monitor's firmware. A slider drag
|
|
||||||
// emits values continuously; sending each one produces a queue the panel
|
|
||||||
// works through seconds after the user let go, and some monitors drop or
|
|
||||||
// garble writes that arrive too fast. So `value` updates immediately for the
|
|
||||||
// UI and the hardware write is debounced, with only the latest value sent.
|
|
||||||
//
|
|
||||||
// Displays are keyed by DRM connector name (DP-2) so they line up with what
|
|
||||||
// Hyprland, the Displays page, and the monitor list already call them.
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-brightness"
|
|
||||||
|
|
||||||
// [{ bus, connector, model, value }] where value is 0..100.
|
|
||||||
property var displays: []
|
|
||||||
property bool scanning: false
|
|
||||||
|
|
||||||
// Empty when everything is fine. Carries the helper's explanation
|
|
||||||
// otherwise -- most usefully the udev command that grants I2C access,
|
|
||||||
// which is the difference between "brightness is unavailable" and
|
|
||||||
// "brightness is one command away".
|
|
||||||
property string lastError: ""
|
|
||||||
|
|
||||||
readonly property bool available: root.displays.length > 0
|
|
||||||
|
|
||||||
// True once a scan has completed, however it went. Lets the UI tell "not
|
|
||||||
// looked yet" apart from "looked and found nothing", which otherwise render
|
|
||||||
// identically and leave a permanently empty panel with no explanation.
|
|
||||||
property bool scanned: false
|
|
||||||
|
|
||||||
// Pending writes, keyed by bus. A monitor being dragged accumulates exactly
|
|
||||||
// one entry no matter how many values the slider emits.
|
|
||||||
property var pending: ({})
|
|
||||||
|
|
||||||
function refresh(): void {
|
|
||||||
if (root.scanning)
|
|
||||||
return;
|
|
||||||
root.scanning = true;
|
|
||||||
scan.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayFor(connector: string): var {
|
|
||||||
return root.displays.find(display => display.connector === connector) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets brightness for one display. The stored value moves at once so the
|
|
||||||
// slider tracks the pointer; the hardware follows when the drag settles.
|
|
||||||
function set(bus: int, percent: int): void {
|
|
||||||
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
||||||
|
|
||||||
root.displays = root.displays.map(display =>
|
|
||||||
display.bus === bus ? Object.assign({}, display, { value: clamped }) : display);
|
|
||||||
|
|
||||||
const next = Object.assign({}, root.pending);
|
|
||||||
next[String(bus)] = clamped;
|
|
||||||
root.pending = next;
|
|
||||||
writeDebounce.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: scan
|
|
||||||
command: [root.helperPath, "list"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(this.text);
|
|
||||||
root.displays = Array.isArray(parsed.displays) ? parsed.displays : [];
|
|
||||||
root.lastError = String(parsed.error ?? "");
|
|
||||||
} catch (error) {
|
|
||||||
root.displays = [];
|
|
||||||
root.lastError = "Could not read the brightness helper's output.";
|
|
||||||
console.warn("Brightness: could not parse helper output:", error);
|
|
||||||
}
|
|
||||||
root.scanning = false;
|
|
||||||
root.scanned = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Long enough that a drag produces one write rather than dozens, short
|
|
||||||
// enough that a single click still feels immediate.
|
|
||||||
Timer {
|
|
||||||
id: writeDebounce
|
|
||||||
interval: 120
|
|
||||||
onTriggered: root.pump()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writes run one at a time, and each is read back.
|
|
||||||
//
|
|
||||||
// Serial because DDC/CI is a bus protocol with no arbitration: two ddcutil
|
|
||||||
// processes talking to the same monitor interleave their exchanges and both
|
|
||||||
// can come back with garbage. Read back because a write is not a promise --
|
|
||||||
// panels clamp to their own range, ignore values while waking from standby,
|
|
||||||
// and drop writes that arrive too quickly. Without the read the slider shows
|
|
||||||
// what Panama asked for rather than what the monitor did, which is the same
|
|
||||||
// class of lie as trusting `hyprctl keyword` to have applied something.
|
|
||||||
property int writingBus: -1
|
|
||||||
|
|
||||||
function pump(): void {
|
|
||||||
if (writer.running || reader.running)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (const bus in root.pending) {
|
|
||||||
const value = root.pending[bus];
|
|
||||||
const remaining = Object.assign({}, root.pending);
|
|
||||||
delete remaining[bus];
|
|
||||||
root.pending = remaining;
|
|
||||||
|
|
||||||
root.writingBus = parseInt(bus);
|
|
||||||
writer.command = [root.helperPath, "set", bus, String(value)];
|
|
||||||
writer.running = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: writer
|
|
||||||
onExited: {
|
|
||||||
reader.command = [root.helperPath, "get", String(root.writingBus)];
|
|
||||||
reader.running = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: reader
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const actual = parseInt(this.text.trim());
|
|
||||||
if (!isNaN(actual)) {
|
|
||||||
root.displays = root.displays.map(display =>
|
|
||||||
display.bus === root.writingBus
|
|
||||||
? Object.assign({}, display, { value: actual })
|
|
||||||
: display);
|
|
||||||
}
|
|
||||||
root.writingBus = -1;
|
|
||||||
// Anything queued while this write was in flight goes now.
|
|
||||||
root.pump();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
// Keeps everything that draws a window agreeing about light or dark.
|
|
||||||
//
|
|
||||||
// The shell repaints itself from Theme.qml the moment the preference changes,
|
|
||||||
// because every token there is a binding. Two other things draw on this desktop
|
|
||||||
// and do not read Panama's store:
|
|
||||||
//
|
|
||||||
// GTK applications read gsettings colour-scheme and gtk-theme
|
|
||||||
// the compositor draws window borders and shadows
|
|
||||||
//
|
|
||||||
// A toolbar or a window border still wearing the other scheme is more jarring
|
|
||||||
// than either scheme on its own, which is why this exists rather than the
|
|
||||||
// setting simply being a Theme property.
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import QtQuick
|
|
||||||
import qs.config
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property string appThemePath: Quickshell.shellDir + "/scripts/panama-theme-apps"
|
|
||||||
|
|
||||||
readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light"
|
|
||||||
property string lastError: ""
|
|
||||||
|
|
||||||
// Applied one command at a time: Process runs a single command, and several
|
|
||||||
// of these are separate programs.
|
|
||||||
property var pending: []
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: runner
|
|
||||||
onExited: (exitCode, exitStatus) => {
|
|
||||||
if (exitCode !== 0)
|
|
||||||
root.lastError = "The colour scheme could not be applied everywhere.";
|
|
||||||
root.drain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drain(): void {
|
|
||||||
if (runner.running || root.pending.length === 0)
|
|
||||||
return;
|
|
||||||
const next = root.pending[0];
|
|
||||||
root.pending = root.pending.slice(1);
|
|
||||||
runner.exec(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
function enqueue(commands: var): void {
|
|
||||||
root.pending = root.pending.concat(commands);
|
|
||||||
root.drain();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pushed once at startup as well as on change: gsettings and the compositor
|
|
||||||
// do not read Panama's store, so a scheme chosen in a previous session is
|
|
||||||
// only in effect for the shell until this runs.
|
|
||||||
Component.onCompleted: settle.restart()
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: settle
|
|
||||||
interval: 1200
|
|
||||||
onTriggered: root.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DesktopPreferences
|
|
||||||
function onRevisionChanged(): void { coalesce.restart(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: coalesce
|
|
||||||
interval: 250
|
|
||||||
onTriggered: root.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply(): void {
|
|
||||||
root.lastError = "";
|
|
||||||
|
|
||||||
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
|
||||||
// Adwaita's light and dark are the same theme; only the preference and
|
|
||||||
// the -dark suffix differ, so applications that honour either agree.
|
|
||||||
const gtkTheme = root.dark ? "Adwaita-dark" : "Adwaita";
|
|
||||||
|
|
||||||
const commands = [
|
|
||||||
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
|
|
||||||
["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Unfocused window borders. The focused border is the prism gradient and
|
|
||||||
// is already scheme-independent; the inactive one is a flat neutral that
|
|
||||||
// would be invisible against the opposite background.
|
|
||||||
const inactive = root.dark ? "rgba(3b426199)" : "rgba(a8aecb99)";
|
|
||||||
commands.push(["hyprctl", "eval",
|
|
||||||
`hl.config({ general = { col = { inactive_border = "${inactive}" } } })`]);
|
|
||||||
|
|
||||||
// Applications that predate org.freedesktop.appearance and carry their
|
|
||||||
// own palettes -- terminals, chiefly. Everything that reads the portal
|
|
||||||
// (GTK4, Qt6, Chromium, Electron) is already handled by the gsettings
|
|
||||||
// write above and needs nothing here.
|
|
||||||
commands.push([root.appThemePath, root.dark ? "dark" : "light"]);
|
|
||||||
|
|
||||||
root.enqueue(commands);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,6 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool initialized: false
|
property bool initialized: false
|
||||||
property bool suppressStatusEvents: false
|
|
||||||
|
|
||||||
// The manual switch. Ignored while `automatic` is on.
|
// The manual switch. Ignored while `automatic` is on.
|
||||||
property bool enabled: Settings.nightLightEnabledByDefault
|
property bool enabled: Settings.nightLightEnabledByDefault
|
||||||
@@ -54,22 +53,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launcher actions already provide immediate Prism OSD feedback. Keep the
|
|
||||||
// ambient Signal Glass channel quiet for that path so one choice produces
|
|
||||||
// one confirmation instead of two competing transients.
|
|
||||||
function toggleQuietly(): void {
|
|
||||||
root.suppressStatusEvents = true;
|
|
||||||
root.toggle();
|
|
||||||
root.suppressStatusEvents = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function restore(manualEnabled: bool, automaticEnabled: bool): void {
|
|
||||||
root.suppressStatusEvents = true;
|
|
||||||
root.enabled = manualEnabled;
|
|
||||||
root.automatic = automaticEnabled;
|
|
||||||
root.suppressStatusEvents = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The window wraps midnight (17:00 → 10:00), so the comparison flips when
|
// The window wraps midnight (17:00 → 10:00), so the comparison flips when
|
||||||
// `from` is later in the day than `to`.
|
// `from` is later in the day than `to`.
|
||||||
function inWindow(hour: real): bool {
|
function inWindow(hour: real): bool {
|
||||||
@@ -135,7 +118,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!root.initialized || root.suppressStatusEvents)
|
if (!root.initialized)
|
||||||
return;
|
return;
|
||||||
StatusEvents.publish({
|
StatusEvents.publish({
|
||||||
key: "display-night-light",
|
key: "display-night-light",
|
||||||
|
|||||||
@@ -114,10 +114,7 @@ ShellRoot {
|
|||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "focus"
|
target: "focus"
|
||||||
|
|
||||||
function start(): bool {
|
function start(): void { FocusSession.startDefault(); }
|
||||||
FocusSession.startDefault();
|
|
||||||
return FocusSession.active;
|
|
||||||
}
|
|
||||||
function reveal(): void { FocusSession.reveal(); }
|
function reveal(): void { FocusSession.reveal(); }
|
||||||
function dismiss(): void { FocusSession.dismiss(); }
|
function dismiss(): void { FocusSession.dismiss(); }
|
||||||
function pause(): void { FocusSession.pauseOrResume(); }
|
function pause(): void { FocusSession.pauseOrResume(); }
|
||||||
@@ -126,12 +123,7 @@ ShellRoot {
|
|||||||
ShellState.openOverview(FocusSession.workspaceId);
|
ShellState.openOverview(FocusSession.workspaceId);
|
||||||
FocusSession.dismiss();
|
FocusSession.dismiss();
|
||||||
}
|
}
|
||||||
function end(): bool {
|
function end(): void { FocusSession.end(false); }
|
||||||
if (!FocusSession.active)
|
|
||||||
return false;
|
|
||||||
FocusSession.end(false);
|
|
||||||
return !FocusSession.active;
|
|
||||||
}
|
|
||||||
function status(): string {
|
function status(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
active: FocusSession.active,
|
active: FocusSession.active,
|
||||||
@@ -280,21 +272,10 @@ ShellRoot {
|
|||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "night-light"
|
target: "night-light"
|
||||||
function toggle(): bool {
|
function toggle(): bool {
|
||||||
NightLight.toggleQuietly();
|
NightLight.toggle();
|
||||||
return NightLight.active;
|
return NightLight.active;
|
||||||
}
|
}
|
||||||
function status(): bool { return NightLight.active; }
|
function status(): bool { return NightLight.active; }
|
||||||
function settings(): string {
|
|
||||||
return JSON.stringify({
|
|
||||||
enabled: NightLight.enabled,
|
|
||||||
automatic: NightLight.automatic,
|
|
||||||
active: NightLight.active
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function restore(enabled: bool, automatic: bool): bool {
|
|
||||||
NightLight.restore(enabled, automatic);
|
|
||||||
return NightLight.active;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ fi
|
|||||||
gsettings set org.gnome.desktop.screensaver lock-enabled false
|
gsettings set org.gnome.desktop.screensaver lock-enabled false
|
||||||
gsettings set org.gnome.desktop.session idle-delay 0
|
gsettings set org.gnome.desktop.session idle-delay 0
|
||||||
|
|
||||||
# Run each setup stage in its own process. This keeps strict-shell options and
|
# Run scripts
|
||||||
# helper variables local to the script that owns them.
|
for script in ~/.local/share/Panama/setup/scripts/*; do source $script; done
|
||||||
for script in ~/.local/share/Panama/setup/scripts/*; do "$script"; done
|
|
||||||
|
|
||||||
# Revert to normal idle settings
|
# Revert to normal idle settings
|
||||||
gsettings set org.gnome.desktop.screensaver lock-enabled true
|
gsettings set org.gnome.desktop.screensaver lock-enabled true
|
||||||
|
|||||||
@@ -70,27 +70,6 @@ for dir in "${dirs[@]}"; do
|
|||||||
log "Linked $PANAMA_DOT/$dir → $CONFIG/$dir"
|
log "Linked $PANAMA_DOT/$dir → $CONFIG/$dir"
|
||||||
done
|
done
|
||||||
|
|
||||||
# kitty.conf ends with `include current-theme.conf`, and that file is generated
|
|
||||||
# from the desktop colour scheme rather than committed -- it is machine state.
|
|
||||||
# A fresh checkout therefore has no such file, and kitty starts by complaining
|
|
||||||
# about a missing include and falling back to its stock colours. Seed it from
|
|
||||||
# the scheme in settings.json (dark unless the user has chosen otherwise) so a
|
|
||||||
# first launch is themed; ColorScheme.qml overwrites it on every change after.
|
|
||||||
KITTY_THEME="$PANAMA_DOT/kitty/current-theme.conf"
|
|
||||||
if [ -e "$KITTY_THEME" ]; then
|
|
||||||
log "Keeping existing kitty theme at $KITTY_THEME"
|
|
||||||
else
|
|
||||||
scheme="dark"
|
|
||||||
settings="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
|
|
||||||
if [ -r "$settings" ]; then
|
|
||||||
stored="$(jq -r '.colorScheme // "dark"' "$settings" 2>/dev/null || echo dark)"
|
|
||||||
[ "$stored" = "light" ] && scheme="light"
|
|
||||||
fi
|
|
||||||
[ "$scheme" = "light" ] && theme="tokyonight-day" || theme="tokyonight-moon"
|
|
||||||
cp "$PANAMA_DOT/kitty/themes/$theme.conf" "$KITTY_THEME"
|
|
||||||
log "Seeded kitty $scheme theme ($theme) → $KITTY_THEME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Vicinae 0.26 discovers user themes from its XDG data directory rather than
|
# Vicinae 0.26 discovers user themes from its XDG data directory rather than
|
||||||
# from ~/.config/vicinae. Keep the authored theme in Panama with the rest of
|
# from ~/.config/vicinae. Keep the authored theme in Panama with the rest of
|
||||||
# the launcher config and expose only that file at Vicinae's runtime path.
|
# the launcher config and expose only that file at Vicinae's runtime path.
|
||||||
@@ -109,6 +88,11 @@ else
|
|||||||
log "Linked Tokyo Night Moon theme → $VICINAE_THEME"
|
log "Linked Tokyo Night Moon theme → $VICINAE_THEME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install the Panama command collection into Vicinae's XDG data directory.
|
||||||
|
# This is intentionally separate from ~/.config/vicinae: script commands are
|
||||||
|
# user data in Vicinae 0.26, just like custom themes.
|
||||||
|
"$PANAMA_PATH/setup/scripts/link-vicinae-scripts"
|
||||||
|
|
||||||
# Panama-native applications live in the user data directory so launchers can
|
# Panama-native applications live in the user data directory so launchers can
|
||||||
# discover them alongside system desktop entries. Keep each authored file in
|
# discover them alongside system desktop entries. Keep each authored file in
|
||||||
# the repository and expose it with a narrow per-file symlink.
|
# the repository and expose it with a narrow per-file symlink.
|
||||||
|
|||||||
@@ -11,27 +11,14 @@ vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}"
|
|||||||
source_dir="$panama_path/config/local/share/vicinae/scripts"
|
source_dir="$panama_path/config/local/share/vicinae/scripts"
|
||||||
scripts_dir="$vicinae_data_dir/scripts"
|
scripts_dir="$vicinae_data_dir/scripts"
|
||||||
target_dir="$scripts_dir/panama"
|
target_dir="$scripts_dir/panama"
|
||||||
backup_root="$vicinae_data_dir/backups"
|
backup_dir="$scripts_dir/panama.pre-panama"
|
||||||
backup_dir="$backup_root/panama.pre-panama"
|
|
||||||
legacy_backup="$scripts_dir/panama.pre-panama"
|
|
||||||
|
|
||||||
[[ -d "$source_dir" ]] || {
|
[[ -d "$source_dir" ]] || {
|
||||||
printf 'Panama command source is missing: %s\n' "$source_dir" >&2
|
printf 'Panama command source is missing: %s\n' "$source_dir" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir -p "$scripts_dir" "$backup_root"
|
mkdir -p "$scripts_dir"
|
||||||
|
|
||||||
# Older Panama builds preserved this directory alongside searchable commands.
|
|
||||||
# Move it out before reloading so those scripts cannot appear twice.
|
|
||||||
if [[ -e "$legacy_backup" ]]; then
|
|
||||||
if [[ -e "$backup_dir" ]]; then
|
|
||||||
printf 'Refusing to move %s; backup already exists at %s\n' \
|
|
||||||
"$legacy_backup" "$backup_dir" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
mv "$legacy_backup" "$backup_dir"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -L "$target_dir" ]]; then
|
if [[ -L "$target_dir" ]]; then
|
||||||
rm "$target_dir"
|
rm "$target_dir"
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# panama-brightness enumerates monitors from sysfs and speaks DDC/CI to them.
|
|
||||||
#
|
|
||||||
# The parts worth pinning down are the ones that decide whether a slider appears
|
|
||||||
# at all, and whether it appears attached to the right screen:
|
|
||||||
#
|
|
||||||
# * only connectors with something plugged in are probed, because probing an
|
|
||||||
# empty bus costs a timeout each and there are fourteen of them here;
|
|
||||||
# * a panel that cannot report brightness is omitted rather than shown as a
|
|
||||||
# control that does nothing;
|
|
||||||
# * the connector name matches what Hyprland calls the output, since the UI
|
|
||||||
# joins on it to get the monitor's description;
|
|
||||||
# * no I2C access produces the udev command that fixes it, not "no displays".
|
|
||||||
#
|
|
||||||
# Runs entirely against fixtures. Real monitors are never touched: both the
|
|
||||||
# sysfs root and the device root are redirected, and ddcutil is replaced on PATH.
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
helper="$repo_dir/config/dot/quickshell/scripts/panama-brightness"
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
printf 'brightness helper contract: %s\n' "$1" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fixture="$(mktemp -d /tmp/panama-brightness.XXXXXX)"
|
|
||||||
trap 'rm -rf "$fixture"' EXIT
|
|
||||||
|
|
||||||
mkdir -p "$fixture/drm" "$fixture/dev" "$fixture/bin" "$fixture/i2c"
|
|
||||||
|
|
||||||
# Two connectors with a monitor, two without. DP-2 answers DDC; DP-3 is
|
|
||||||
# connected but does not implement brightness. HDMI-A-1 and DP-1 are empty and
|
|
||||||
# must never be probed at all.
|
|
||||||
make_connector() {
|
|
||||||
local name="$1" bus="$2" status="$3"
|
|
||||||
mkdir -p "$fixture/drm/$name"
|
|
||||||
printf '%s\n' "$status" >"$fixture/drm/$name/status"
|
|
||||||
mkdir -p "$fixture/i2c/i2c-$bus"
|
|
||||||
ln -sfn "$fixture/i2c/i2c-$bus" "$fixture/drm/$name/ddc"
|
|
||||||
}
|
|
||||||
make_connector card1-DP-1 4 disconnected
|
|
||||||
make_connector card1-DP-2 5 connected
|
|
||||||
make_connector card1-DP-3 6 connected
|
|
||||||
make_connector card1-HDMI-A-1 7 disconnected
|
|
||||||
|
|
||||||
# has_accessible_bus only needs one readable/writable node to exist.
|
|
||||||
touch "$fixture/dev/i2c-5"
|
|
||||||
|
|
||||||
# Stub ddcutil. Records every bus it is asked about so the test can prove the
|
|
||||||
# disconnected ones were skipped. Bus 6 refuses, standing in for a panel without
|
|
||||||
# VCP 0x10.
|
|
||||||
#
|
|
||||||
# Bus 5 reports its brightness out of 200 rather than 100. Most panels do use
|
|
||||||
# 100, which is exactly the problem: with a maximum of 100 the scaling
|
|
||||||
# arithmetic is the identity, so a helper that ignored the reported maximum
|
|
||||||
# entirely would pass every assertion. 200 makes reads and writes that skip the
|
|
||||||
# conversion visibly wrong.
|
|
||||||
cat >"$fixture/bin/ddcutil" <<'STUB'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
bus=""
|
|
||||||
args=("$@")
|
|
||||||
for ((i = 0; i < ${#args[@]}; i++)); do
|
|
||||||
[[ "${args[$i]}" == "--bus" ]] && bus="${args[$((i + 1))]}"
|
|
||||||
done
|
|
||||||
printf '%s\n' "$bus" >>"$DDCUTIL_PROBE_LOG"
|
|
||||||
|
|
||||||
for arg in "$@"; do
|
|
||||||
if [[ "$arg" == "setvcp" ]]; then
|
|
||||||
printf 'set %s %s\n' "$bus" "${args[-1]}" >>"$DDCUTIL_SET_LOG"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
case "$bus" in
|
|
||||||
5) printf 'VCP 10 C 120 200\n'; exit 0 ;;
|
|
||||||
*) exit 1 ;;
|
|
||||||
esac
|
|
||||||
STUB
|
|
||||||
chmod +x "$fixture/bin/ddcutil"
|
|
||||||
|
|
||||||
export DDCUTIL_PROBE_LOG="$fixture/probes.log"
|
|
||||||
export DDCUTIL_SET_LOG="$fixture/sets.log"
|
|
||||||
: >"$DDCUTIL_PROBE_LOG"
|
|
||||||
: >"$DDCUTIL_SET_LOG"
|
|
||||||
|
|
||||||
run_helper() {
|
|
||||||
PATH="$fixture/bin:$PATH" \
|
|
||||||
PANAMA_BRIGHTNESS_DRM_ROOT="$fixture/drm" \
|
|
||||||
PANAMA_BRIGHTNESS_DEV_ROOT="$fixture/dev" \
|
|
||||||
"$helper" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Enumeration ──────────────────────────────────────────────────────────────
|
|
||||||
listing="$(run_helper list)"
|
|
||||||
jq -e . >/dev/null 2>&1 <<<"$listing" || fail "list did not emit JSON: $listing"
|
|
||||||
|
|
||||||
[[ "$(jq -r '.displays | length' <<<"$listing")" == "1" ]] \
|
|
||||||
|| fail "expected exactly one controllable display, got: $listing"
|
|
||||||
|
|
||||||
[[ "$(jq -r '.displays[0].connector' <<<"$listing")" == "DP-2" ]] \
|
|
||||||
|| fail "the connector name must match Hyprland's output name: $listing"
|
|
||||||
|
|
||||||
[[ "$(jq -r '.displays[0].bus' <<<"$listing")" == "5" ]] \
|
|
||||||
|| fail "the display was mapped to the wrong I2C bus: $listing"
|
|
||||||
|
|
||||||
# 120 of a maximum of 200 is 60%.
|
|
||||||
[[ "$(jq -r '.displays[0].value' <<<"$listing")" == "60" ]] \
|
|
||||||
|| fail "brightness was not read as a percent of the reported maximum: $listing"
|
|
||||||
|
|
||||||
[[ "$(jq -r '.error' <<<"$listing")" == "" ]] \
|
|
||||||
|| fail "a successful listing must not carry an error: $listing"
|
|
||||||
|
|
||||||
# A connected panel that cannot report brightness is dropped, not listed.
|
|
||||||
jq -e '.displays | map(.connector) | index("DP-3") == null' >/dev/null <<<"$listing" \
|
|
||||||
|| fail 'a display without VCP 0x10 was listed as controllable'
|
|
||||||
|
|
||||||
# ── Disconnected connectors are never probed ─────────────────────────────────
|
|
||||||
if grep -qxE '4|7' "$DDCUTIL_PROBE_LOG"; then
|
|
||||||
fail "a disconnected connector was probed -- each empty bus costs a timeout: $(tr '\n' ' ' <"$DDCUTIL_PROBE_LOG")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Writes scale to the reported maximum ─────────────────────────────────────
|
|
||||||
run_helper set 5 40
|
|
||||||
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 5 80" ]] \
|
|
||||||
|| fail "set did not scale to the display's maximum: $(cat "$DDCUTIL_SET_LOG")"
|
|
||||||
|
|
||||||
run_helper set 5 500
|
|
||||||
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 5 200" ]] \
|
|
||||||
|| fail "an out-of-range percent was not clamped: $(cat "$DDCUTIL_SET_LOG")"
|
|
||||||
|
|
||||||
# ── No I2C access explains itself ────────────────────────────────────────────
|
|
||||||
rm -f "$fixture/dev"/i2c-*
|
|
||||||
denied="$(run_helper list)"
|
|
||||||
[[ "$(jq -r '.displays | length' <<<"$denied")" == "0" ]] \
|
|
||||||
|| fail "displays were reported without I2C access: $denied"
|
|
||||||
grep -q 'udevadm' <<<"$(jq -r '.error' <<<"$denied")" \
|
|
||||||
|| fail "the no-access error must name the command that fixes it, got: $(jq -r '.error' <<<"$denied")"
|
|
||||||
|
|
||||||
printf 'brightness helper contract: PASS\n'
|
|
||||||
@@ -113,25 +113,6 @@ config_home="$(mktemp -d /tmp/panama-displays-config.XXXXXX)"
|
|||||||
run() { XDG_CONFIG_HOME="$config_home" qs -p "$harness" "$@"; }
|
run() { XDG_CONFIG_HOME="$config_home" qs -p "$harness" "$@"; }
|
||||||
status() { run ipc call displays-test status; }
|
status() { run ipc call displays-test status; }
|
||||||
|
|
||||||
# The shipped geometry, read from the Hyprland config rather than from the
|
|
||||||
# running compositor.
|
|
||||||
#
|
|
||||||
# Everything below captures "original" from what it observes at start, which is
|
|
||||||
# correct only if the display is already in a good state. A previous run that
|
|
||||||
# failed mid-revert leaves the display changed, and the next run then captures
|
|
||||||
# THAT as the original and faithfully restores the desktop to a broken value.
|
|
||||||
# One flake becomes permanent. So refuse to run from a state that does not match
|
|
||||||
# what the config says, rather than laundering it.
|
|
||||||
shipped_scale="$(sed -n 's/^local shipped_scale *= *\([0-9.]*\).*/\1/p' \
|
|
||||||
"$repo_dir/config/dot/hypr/monitors.lua" | head -1)"
|
|
||||||
[[ -n "$shipped_scale" ]] || fail 'could not read the shipped scale from monitors.lua -- the guard below depends on it, and skipping it silently is how a dirty baseline gets laundered'
|
|
||||||
if [[ -n "$shipped_scale" ]]; then
|
|
||||||
live_scale="$(hyprctl -j monitors | jq -r '.[0].scale')"
|
|
||||||
if ! awk -v a="$live_scale" -v b="$shipped_scale" 'BEGIN { exit !(a == b) }'; then
|
|
||||||
fail "the display is at scale $live_scale but the config ships $shipped_scale -- refusing to capture a dirty state as the baseline. Restore it first: hyprctl eval 'hl.monitor({ output = \"DP-2\", mode = \"4500x3000@60\", scale = $shipped_scale, transform = 0 })'"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
original_mode=""
|
original_mode=""
|
||||||
original_scale=""
|
original_scale=""
|
||||||
original_transform=""
|
original_transform=""
|
||||||
|
|||||||
@@ -13,20 +13,6 @@ cleanup() {
|
|||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
qs ipc call focus end >/dev/null 2>&1 || true
|
qs ipc call focus end >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# focus start records the caffeine state it found so end() can hand it back, and
|
|
||||||
# the assertion below requires caffeine to finish off. Both are only true if
|
|
||||||
# caffeine starts off.
|
|
||||||
#
|
|
||||||
# This test kills and restarts the live shell, so an interrupted run leaves
|
|
||||||
# caffeine on with nothing to turn it back off. The next run then records
|
|
||||||
# "previously on", faithfully restores it, and fails -- and every run after
|
|
||||||
# that fails identically, with the desktop quietly unable to idle or lock the
|
|
||||||
# whole time. Refuse to start from that state rather than latching it in.
|
|
||||||
if [[ "$(qs ipc call caffeine status 2>/dev/null)" != "false" ]]; then
|
|
||||||
fail 'caffeine is on before the session starts -- this test cannot distinguish "restored correctly" from "never released". Turn it off first: qs ipc call caffeine toggle'
|
|
||||||
fi
|
|
||||||
|
|
||||||
qs ipc call focus start >/dev/null
|
qs ipc call focus start >/dev/null
|
||||||
|
|
||||||
state_file=$(find "${XDG_STATE_HOME:-$HOME/.local/state}/quickshell/by-shell" -name focus-session.json -print -quit)
|
state_file=$(find "${XDG_STATE_HOME:-$HOME/.local/state}/quickshell/by-shell" -name focus-session.json -print -quit)
|
||||||
|
|||||||
@@ -26,62 +26,10 @@ printf 'brightnessctl' >>"$OSD_TEST_LOG"
|
|||||||
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
||||||
printf '\n' >>"$OSD_TEST_LOG"
|
printf '\n' >>"$OSD_TEST_LOG"
|
||||||
if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then
|
if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then
|
||||||
[[ ${BACKLIGHT_AVAILABLE:-true} == true ]] || exit 1
|
|
||||||
printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,1000,50%}"
|
printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,1000,50%}"
|
||||||
fi
|
fi
|
||||||
SH
|
SH
|
||||||
|
|
||||||
cat >"$scratch/bin/panama-brightness" <<'SH'
|
|
||||||
#!/bin/bash
|
|
||||||
printf 'panama-brightness' >>"$OSD_TEST_LOG"
|
|
||||||
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
|
||||||
printf '\n' >>"$OSD_TEST_LOG"
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
list)
|
|
||||||
if [[ -n ${DDC_LIST_JSON:-} ]]; then
|
|
||||||
printf '%s\n' "$DDC_LIST_JSON"
|
|
||||||
else
|
|
||||||
printf '%s\n' '{"displays":[],"error":"No displays"}'
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
get)
|
|
||||||
[[ ${DDC_FAIL_GET_BUS:-} != "${2:-}" ]] || exit 1
|
|
||||||
if [[ -s $OSD_DDC_STATE ]]; then
|
|
||||||
cat "$OSD_DDC_STATE"
|
|
||||||
else
|
|
||||||
printf '%s\n' "${DDC_GET_VALUE:-40}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
set)
|
|
||||||
if [[ -n ${DDC_SET_DELAY:-} ]]; then
|
|
||||||
if ! mkdir "$OSD_DDC_PROBE" 2>/dev/null; then
|
|
||||||
printf 'ddc-overlap\n' >>"$OSD_TEST_LOG"
|
|
||||||
fi
|
|
||||||
sleep "$DDC_SET_DELAY"
|
|
||||||
rmdir "$OSD_DDC_PROBE" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
printf '%s\n' "${3:-0}" >"$OSD_DDC_STATE"
|
|
||||||
;;
|
|
||||||
*) exit 2 ;;
|
|
||||||
esac
|
|
||||||
SH
|
|
||||||
|
|
||||||
cat >"$scratch/bin/hyprctl" <<'SH'
|
|
||||||
#!/bin/bash
|
|
||||||
printf 'hyprctl' >>"$OSD_TEST_LOG"
|
|
||||||
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
|
||||||
printf '\n' >>"$OSD_TEST_LOG"
|
|
||||||
printf '[{"name":"%s","focused":true}]\n' "${FOCUSED_MONITOR:-DP-2}"
|
|
||||||
SH
|
|
||||||
|
|
||||||
cat >"$scratch/bin/notify-send" <<'SH'
|
|
||||||
#!/bin/bash
|
|
||||||
printf 'notify-send' >>"$OSD_TEST_LOG"
|
|
||||||
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
|
||||||
printf '\n' >>"$OSD_TEST_LOG"
|
|
||||||
SH
|
|
||||||
|
|
||||||
cat >"$scratch/bin/playerctl" <<'SH'
|
cat >"$scratch/bin/playerctl" <<'SH'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
printf 'playerctl' >>"$OSD_TEST_LOG"
|
printf 'playerctl' >>"$OSD_TEST_LOG"
|
||||||
@@ -99,28 +47,12 @@ cat >"$scratch/bin/qs" <<'SH'
|
|||||||
printf 'qs' >>"$OSD_TEST_LOG"
|
printf 'qs' >>"$OSD_TEST_LOG"
|
||||||
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
||||||
printf '\n' >>"$OSD_TEST_LOG"
|
printf '\n' >>"$OSD_TEST_LOG"
|
||||||
[[ ${OSD_TEST_FAIL_QS:-false} != true ]]
|
|
||||||
SH
|
SH
|
||||||
|
|
||||||
chmod +x "$scratch/bin/"*
|
chmod +x "$scratch/bin/"*
|
||||||
|
|
||||||
run_helper() {
|
run_helper() {
|
||||||
local runtime="${OSD_RUNTIME_DIR:-$scratch/runtime-default}"
|
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" "$helper" "$@"
|
||||||
mkdir -p "$runtime"
|
|
||||||
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" \
|
|
||||||
OSD_TEST_FAIL_QS="${OSD_TEST_FAIL_QS:-false}" \
|
|
||||||
PANAMA_OSD_STRICT="${PANAMA_OSD_STRICT:-false}" \
|
|
||||||
PANAMA_OSD_BRIGHTNESS_HELPER="$scratch/bin/panama-brightness" \
|
|
||||||
PANAMA_OSD_RUNTIME_DIR="$runtime" \
|
|
||||||
OSD_DDC_STATE="$runtime/ddc-state" \
|
|
||||||
OSD_DDC_PROBE="$runtime/ddc-probe" \
|
|
||||||
BACKLIGHT_AVAILABLE="${BACKLIGHT_AVAILABLE:-true}" \
|
|
||||||
DDC_LIST_JSON="${DDC_LIST_JSON:-}" \
|
|
||||||
DDC_GET_VALUE="${DDC_GET_VALUE:-40}" \
|
|
||||||
DDC_FAIL_GET_BUS="${DDC_FAIL_GET_BUS:-}" \
|
|
||||||
DDC_SET_DELAY="${DDC_SET_DELAY:-}" \
|
|
||||||
FOCUSED_MONITOR="${FOCUSED_MONITOR:-DP-2}" \
|
|
||||||
"$helper" "$@"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_line() {
|
assert_line() {
|
||||||
@@ -150,102 +82,9 @@ assert_line 'qs <ipc> <call> <osd> <progress> <microphone-muted> <72> <100> <Mut
|
|||||||
|
|
||||||
: >"$log"
|
: >"$log"
|
||||||
run_helper brightness up 5
|
run_helper brightness up 5
|
||||||
|
assert_line 'brightnessctl <-e4> <-n2> <set> <5%+>'
|
||||||
assert_line 'brightnessctl <-m> <-c> <backlight>'
|
assert_line 'brightnessctl <-m> <-c> <backlight>'
|
||||||
assert_line 'brightnessctl <-e4> <-n2> <-c> <backlight> <set> <5%+>'
|
|
||||||
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <50> <100> <50%>'
|
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <50> <100> <50%>'
|
||||||
if grep -Fq 'panama-brightness' "$log"; then
|
|
||||||
printf 'osd helper contract: DDC fallback ran despite a native backlight\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
: >"$log"
|
|
||||||
OSD_RUNTIME_DIR="$scratch/runtime-ddc" \
|
|
||||||
BACKLIGHT_AVAILABLE=false \
|
|
||||||
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
||||||
run_helper brightness up 5
|
|
||||||
assert_line 'hyprctl <-j> <monitors>'
|
|
||||||
assert_line 'panama-brightness <list>'
|
|
||||||
assert_line 'panama-brightness <get> <5>'
|
|
||||||
assert_line 'panama-brightness <set> <5> <45>'
|
|
||||||
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <45> <100> <45%>'
|
|
||||||
|
|
||||||
# A cached bus avoids the expensive display scan on subsequent key presses.
|
|
||||||
: >"$log"
|
|
||||||
OSD_RUNTIME_DIR="$scratch/runtime-ddc" \
|
|
||||||
BACKLIGHT_AVAILABLE=false \
|
|
||||||
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":45}],"error":""}' \
|
|
||||||
run_helper brightness down 5
|
|
||||||
assert_line 'panama-brightness <get> <5>'
|
|
||||||
assert_line 'panama-brightness <set> <5> <40>'
|
|
||||||
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <40> <100> <40%>'
|
|
||||||
if grep -Fq 'panama-brightness <list>' "$log"; then
|
|
||||||
printf 'osd helper contract: cached DDC bus triggered another display scan\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A disconnected cached monitor is discarded and rediscovered once.
|
|
||||||
mkdir -p "$scratch/runtime-ddc-stale"
|
|
||||||
printf '9\tDP-9\n' >"$scratch/runtime-ddc-stale/brightness-bus"
|
|
||||||
: >"$log"
|
|
||||||
OSD_RUNTIME_DIR="$scratch/runtime-ddc-stale" \
|
|
||||||
BACKLIGHT_AVAILABLE=false \
|
|
||||||
DDC_FAIL_GET_BUS=9 \
|
|
||||||
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
||||||
run_helper brightness up 5
|
|
||||||
assert_line 'panama-brightness <get> <9>'
|
|
||||||
assert_line 'panama-brightness <list>'
|
|
||||||
assert_line 'panama-brightness <get> <5>'
|
|
||||||
assert_line 'panama-brightness <set> <5> <45>'
|
|
||||||
|
|
||||||
# If the focused output is not DDC-capable, use the first discovered display.
|
|
||||||
: >"$log"
|
|
||||||
OSD_RUNTIME_DIR="$scratch/runtime-ddc-first" \
|
|
||||||
BACKLIGHT_AVAILABLE=false \
|
|
||||||
FOCUSED_MONITOR='eDP-1' \
|
|
||||||
DDC_GET_VALUE=35 \
|
|
||||||
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
||||||
run_helper brightness down 10
|
|
||||||
assert_line 'panama-brightness <get> <3>'
|
|
||||||
assert_line 'panama-brightness <set> <3> <25>'
|
|
||||||
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <25> <100> <25%>'
|
|
||||||
|
|
||||||
# Permission and discovery errors must be visible, never masquerade as 0%.
|
|
||||||
: >"$log"
|
|
||||||
OSD_RUNTIME_DIR="$scratch/runtime-ddc-error" \
|
|
||||||
BACKLIGHT_AVAILABLE=false \
|
|
||||||
DDC_LIST_JSON='{"displays":[],"error":"Run sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm"}' \
|
|
||||||
run_helper brightness up 5
|
|
||||||
assert_line 'qs <ipc> <call> <osd> <message> <dialog-warning-symbolic> <Brightness needs permission>'
|
|
||||||
assert_line 'notify-send <--app-name=Panama> <--icon=display-brightness-symbolic> <Brightness unavailable> <Run sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm>'
|
|
||||||
if grep -Fq 'osd> <progress> <brightness>' "$log"; then
|
|
||||||
printf 'osd helper contract: unavailable brightness rendered a false percentage\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Separate key-repeat processes must not overlap their DDC transactions.
|
|
||||||
: >"$log"
|
|
||||||
OSD_RUNTIME_DIR="$scratch/runtime-ddc-lock" \
|
|
||||||
BACKLIGHT_AVAILABLE=false \
|
|
||||||
DDC_SET_DELAY=0.15 \
|
|
||||||
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
||||||
run_helper brightness up 5 &
|
|
||||||
first_pid=$!
|
|
||||||
OSD_RUNTIME_DIR="$scratch/runtime-ddc-lock" \
|
|
||||||
BACKLIGHT_AVAILABLE=false \
|
|
||||||
DDC_SET_DELAY=0.15 \
|
|
||||||
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
||||||
run_helper brightness up 5 &
|
|
||||||
second_pid=$!
|
|
||||||
wait "$first_pid"
|
|
||||||
wait "$second_pid"
|
|
||||||
if grep -Fqx 'ddc-overlap' "$log"; then
|
|
||||||
printf 'osd helper contract: concurrent DDC transactions overlapped\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ $(<"$scratch/runtime-ddc-lock/ddc-state") != 50 ]]; then
|
|
||||||
printf 'osd helper contract: serialized key repeats did not both apply\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
: >"$log"
|
: >"$log"
|
||||||
run_helper media next
|
run_helper media next
|
||||||
@@ -256,13 +95,4 @@ assert_line 'qs <ipc> <call> <osd> <message> <media-next> <Horizon — Tycho>'
|
|||||||
run_helper message notifications-disabled-symbolic 'Do Not Disturb On'
|
run_helper message notifications-disabled-symbolic 'Do Not Disturb On'
|
||||||
assert_line 'qs <ipc> <call> <osd> <message> <notifications-disabled-symbolic> <Do Not Disturb On>'
|
assert_line 'qs <ipc> <call> <osd> <message> <notifications-disabled-symbolic> <Do Not Disturb On>'
|
||||||
|
|
||||||
: >"$log"
|
|
||||||
if OSD_TEST_FAIL_QS=true PANAMA_OSD_STRICT=1 run_helper message dialog-error-symbolic 'Strict failure'; then
|
|
||||||
printf 'osd helper contract: strict delivery swallowed an IPC failure\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
: >"$log"
|
|
||||||
OSD_TEST_FAIL_QS=true run_helper message dialog-information-symbolic 'Best effort'
|
|
||||||
|
|
||||||
printf 'osd helper contract: PASS\n'
|
printf 'osd helper contract: PASS\n'
|
||||||
|
|||||||
@@ -37,13 +37,7 @@ case "$*" in
|
|||||||
'ipc call notifications dnd'|'ipc call caffeine toggle'|'ipc call night-light toggle')
|
'ipc call notifications dnd'|'ipc call caffeine toggle'|'ipc call night-light toggle')
|
||||||
printf '%s\n' "${PANAMA_ACTION_TEST_STATE:-true}"
|
printf '%s\n' "${PANAMA_ACTION_TEST_STATE:-true}"
|
||||||
;;
|
;;
|
||||||
'ipc call focus start'|'ipc call focus end')
|
|
||||||
printf '%s\n' "${PANAMA_ACTION_TEST_FOCUS_STATE:-true}"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
if [[ ${PANAMA_ACTION_TEST_FAIL_KILL:-false} == true && ${1:-} == kill ]]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
[[ ${PANAMA_ACTION_TEST_FAIL_QS:-false} != true ]]
|
[[ ${PANAMA_ACTION_TEST_FAIL_QS:-false} != true ]]
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -59,7 +53,6 @@ make_recorder panama-osd <<'EOF'
|
|||||||
printf 'panama-osd' >>"$PANAMA_ACTION_TEST_LOG"
|
printf 'panama-osd' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
[[ ${PANAMA_ACTION_TEST_FAIL_OSD:-false} != true ]]
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
make_recorder panama-prism-gallery <<'EOF'
|
make_recorder panama-prism-gallery <<'EOF'
|
||||||
@@ -78,46 +71,9 @@ printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
|||||||
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
make_recorder systemd-inhibit <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
if [[ -n ${PANAMA_ACTION_TEST_INHIBITOR_FILE:-} && -s $PANAMA_ACTION_TEST_INHIBITOR_FILE ]]; then
|
|
||||||
while IFS= read -r pid; do
|
|
||||||
printf 'Panama 1000 gib %s systemd-inhibit sleep:idle Caffeine block\n' "$pid"
|
|
||||||
done <"$PANAMA_ACTION_TEST_INHIBITOR_FILE"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if [[ ${PANAMA_ACTION_TEST_CONFIRMED_STATE:-${PANAMA_ACTION_TEST_STATE:-true}} == true ]]; then
|
|
||||||
printf 'Panama 1000 gib 42 systemd-inhibit sleep:idle Caffeine block\n'
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
make_recorder kill <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
printf 'kill' >>"$PANAMA_ACTION_TEST_LOG"
|
|
||||||
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
|
||||||
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
|
||||||
if [[ -n ${PANAMA_ACTION_TEST_INHIBITOR_FILE:-} ]]; then
|
|
||||||
: >"$PANAMA_ACTION_TEST_INHIBITOR_FILE"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
make_recorder pgrep <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
[[ ${PANAMA_ACTION_TEST_CONFIRMED_STATE:-${PANAMA_ACTION_TEST_STATE:-true}} == true ]]
|
|
||||||
EOF
|
|
||||||
|
|
||||||
make_recorder id <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
printf '1000\n'
|
|
||||||
EOF
|
|
||||||
|
|
||||||
run_action() {
|
run_action() {
|
||||||
: >"$command_log"
|
: >"$command_log"
|
||||||
HOME="$work/home" PATH="$fake_bin:$PATH" \
|
HOME="$work/home" PATH="$fake_bin:$PATH" \
|
||||||
PANAMA_ACTION_HELPER_DIR="$fake_bin" \
|
|
||||||
PANAMA_ACTION_KILL_COMMAND="$fake_bin/kill" \
|
|
||||||
PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
|
|
||||||
PANAMA_ACTION_CONFIRM_DELAY=0 \
|
|
||||||
PANAMA_ACTION_TEST_LOG="$command_log" \
|
PANAMA_ACTION_TEST_LOG="$command_log" \
|
||||||
"$dispatcher" "$1"
|
"$dispatcher" "$1"
|
||||||
}
|
}
|
||||||
@@ -154,10 +110,6 @@ assert_commands $'qs <ipc> <call> <notifications> <dnd>\npanama-osd <message> <n
|
|||||||
PANAMA_ACTION_TEST_STATE=false run_action caffeine
|
PANAMA_ACTION_TEST_STATE=false run_action caffeine
|
||||||
assert_commands $'qs <ipc> <call> <caffeine> <toggle>\npanama-osd <message> <weather-clear-night-symbolic> <Caffeine Off>'
|
assert_commands $'qs <ipc> <call> <caffeine> <toggle>\npanama-osd <message> <weather-clear-night-symbolic> <Caffeine Off>'
|
||||||
|
|
||||||
printf '4242\n' >"$work/inhibitors"
|
|
||||||
PANAMA_ACTION_TEST_STATE=false PANAMA_ACTION_TEST_INHIBITOR_FILE="$work/inhibitors" run_action caffeine
|
|
||||||
assert_commands $'qs <ipc> <call> <caffeine> <toggle>\nkill <4242>\npanama-osd <message> <weather-clear-night-symbolic> <Caffeine Off>'
|
|
||||||
|
|
||||||
run_action night-light
|
run_action night-light
|
||||||
assert_commands $'qs <ipc> <call> <night-light> <toggle>\npanama-osd <message> <night-light-symbolic> <Night Light On>'
|
assert_commands $'qs <ipc> <call> <night-light> <toggle>\npanama-osd <message> <night-light-symbolic> <Night Light On>'
|
||||||
|
|
||||||
@@ -167,9 +119,6 @@ assert_commands $'qs <ipc> <call> <focus> <start>\npanama-osd <message> <prefere
|
|||||||
run_action focus-end
|
run_action focus-end
|
||||||
assert_commands $'qs <ipc> <call> <focus> <end>\npanama-osd <message> <media-playback-stop-symbolic> <Focus Session Ended>'
|
assert_commands $'qs <ipc> <call> <focus> <end>\npanama-osd <message> <media-playback-stop-symbolic> <Focus Session Ended>'
|
||||||
|
|
||||||
PANAMA_ACTION_TEST_FOCUS_STATE=false run_action focus-end
|
|
||||||
assert_commands $'qs <ipc> <call> <focus> <end>\npanama-osd <message> <dialog-information-symbolic> <No Focus Session Running>'
|
|
||||||
|
|
||||||
run_action capture
|
run_action capture
|
||||||
assert_commands 'qs <ipc> <call> <capture> <open>'
|
assert_commands 'qs <ipc> <call> <capture> <open>'
|
||||||
|
|
||||||
@@ -186,43 +135,18 @@ run_action gallery
|
|||||||
assert_commands 'panama-prism-gallery'
|
assert_commands 'panama-prism-gallery'
|
||||||
|
|
||||||
run_action restart-shell
|
run_action restart-shell
|
||||||
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>'
|
assert_commands $'qs <kill>\nquickshell <--daemonize>'
|
||||||
|
|
||||||
PANAMA_ACTION_TEST_FAIL_KILL=true run_action restart-shell
|
if HOME="$work/home" PATH="$fake_bin:$PATH" PANAMA_ACTION_TEST_LOG="$command_log" \
|
||||||
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>'
|
|
||||||
|
|
||||||
if HOME="$work/home" PATH="$fake_bin:$PATH" PANAMA_ACTION_HELPER_DIR="$fake_bin" \
|
|
||||||
PANAMA_ACTION_CONFIRM_ATTEMPTS=1 PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
|
|
||||||
"$dispatcher" definitely-not-an-action >/dev/null 2>&1; then
|
"$dispatcher" definitely-not-an-action >/dev/null 2>&1; then
|
||||||
fail 'unknown action was accepted'
|
fail 'unknown action was accepted'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
: >"$command_log"
|
: >"$command_log"
|
||||||
if PANAMA_ACTION_TEST_FAIL_QS=true HOME="$work/home" PATH="$fake_bin:$PATH" \
|
if PANAMA_ACTION_TEST_FAIL_QS=true HOME="$work/home" PATH="$fake_bin:$PATH" \
|
||||||
PANAMA_ACTION_HELPER_DIR="$fake_bin" PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
|
PANAMA_ACTION_TEST_LOG="$command_log" "$dispatcher" control-center >/dev/null 2>&1; then
|
||||||
PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
|
|
||||||
"$dispatcher" control-center >/dev/null 2>&1; then
|
|
||||||
fail 'failed shell action returned success'
|
fail 'failed shell action returned success'
|
||||||
fi
|
fi
|
||||||
assert_commands $'qs <ipc> <call> <quicksettings> <open>\nnotify-send <-a> <Panama> <-i> <dialog-error-symbolic> <Panama action failed> <The “control center” action could not be completed.>'
|
assert_commands $'qs <ipc> <call> <quicksettings> <open>\nnotify-send <-a> <Panama> <-i> <dialog-error-symbolic> <Panama action failed> <The “control center” action could not be completed.>'
|
||||||
|
|
||||||
: >"$command_log"
|
|
||||||
if PANAMA_ACTION_TEST_FAIL_OSD=true HOME="$work/home" PATH="$fake_bin:$PATH" \
|
|
||||||
PANAMA_ACTION_HELPER_DIR="$fake_bin" PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
|
|
||||||
PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
|
|
||||||
"$dispatcher" microphone >/dev/null 2>&1; then
|
|
||||||
fail 'failed Prism OSD delivery returned success'
|
|
||||||
fi
|
|
||||||
assert_commands $'panama-osd <microphone> <toggle>\nnotify-send <-a> <Panama> <-i> <dialog-error-symbolic> <Panama action failed> <The “microphone” action could not be completed.>'
|
|
||||||
|
|
||||||
: >"$command_log"
|
|
||||||
if PANAMA_ACTION_TEST_CONFIRMED_STATE=false HOME="$work/home" PATH="$fake_bin:$PATH" \
|
|
||||||
PANAMA_ACTION_HELPER_DIR="$fake_bin" PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
|
|
||||||
PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
|
|
||||||
"$dispatcher" caffeine >/dev/null 2>&1; then
|
|
||||||
fail 'unconfirmed Caffeine state returned success'
|
|
||||||
fi
|
|
||||||
grep -Fqx 'notify-send <-a> <Panama> <-i> <dialog-error-symbolic> <Panama action failed> <The “caffeine” action could not be completed.>' "$command_log" \
|
|
||||||
|| fail 'unconfirmed state did not send a failure notification'
|
|
||||||
|
|
||||||
printf 'Panama action contract: PASS\n'
|
printf 'Panama action contract: PASS\n'
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ fail() {
|
|||||||
|
|
||||||
active_target=""
|
active_target=""
|
||||||
original_state=""
|
original_state=""
|
||||||
night_light_snapshot=""
|
|
||||||
|
|
||||||
restore_target() {
|
restore_target() {
|
||||||
local target="$1" expected="$2" current
|
local target="$1" expected="$2" current
|
||||||
@@ -23,12 +22,6 @@ cleanup() {
|
|||||||
if [[ -n $active_target ]]; then
|
if [[ -n $active_target ]]; then
|
||||||
restore_target "$active_target" "$original_state"
|
restore_target "$active_target" "$original_state"
|
||||||
fi
|
fi
|
||||||
if [[ -n $night_light_snapshot ]]; then
|
|
||||||
local enabled automatic
|
|
||||||
enabled="$(jq -r '.enabled' <<<"$night_light_snapshot")"
|
|
||||||
automatic="$(jq -r '.automatic' <<<"$night_light_snapshot")"
|
|
||||||
qs ipc call night-light restore "$enabled" "$automatic" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
@@ -58,22 +51,6 @@ rg -q '^target caffeine$' <<<"$ipc" || fail 'caffeine IPC target is missing'
|
|||||||
rg -q '^target night-light$' <<<"$ipc" || fail 'night-light IPC target is missing'
|
rg -q '^target night-light$' <<<"$ipc" || fail 'night-light IPC target is missing'
|
||||||
|
|
||||||
exercise_toggle caffeine
|
exercise_toggle caffeine
|
||||||
|
|
||||||
night_light_snapshot="$(qs ipc call night-light settings)"
|
|
||||||
jq -e '.enabled | type == "boolean"' <<<"$night_light_snapshot" >/dev/null \
|
|
||||||
|| fail 'night-light settings omitted enabled state'
|
|
||||||
jq -e '.automatic | type == "boolean"' <<<"$night_light_snapshot" >/dev/null \
|
|
||||||
|| fail 'night-light settings omitted automatic state'
|
|
||||||
exercise_toggle night-light
|
exercise_toggle night-light
|
||||||
|
|
||||||
original_enabled="$(jq -r '.enabled' <<<"$night_light_snapshot")"
|
|
||||||
original_automatic="$(jq -r '.automatic' <<<"$night_light_snapshot")"
|
|
||||||
qs ipc call night-light restore "$original_enabled" "$original_automatic" >/dev/null
|
|
||||||
restored_snapshot="$(qs ipc call night-light settings)"
|
|
||||||
[[ "$(jq -r '.enabled' <<<"$restored_snapshot")" == "$original_enabled" ]] \
|
|
||||||
|| fail 'night-light enabled state was not restored'
|
|
||||||
[[ "$(jq -r '.automatic' <<<"$restored_snapshot")" == "$original_automatic" ]] \
|
|
||||||
|| fail 'night-light automatic schedule was not restored'
|
|
||||||
night_light_snapshot=""
|
|
||||||
|
|
||||||
printf 'Panama action IPC contract: PASS\n'
|
printf 'Panama action IPC contract: PASS\n'
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ set -euo pipefail
|
|||||||
|
|
||||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
installer="$repo_dir/setup/scripts/link-vicinae-scripts"
|
installer="$repo_dir/setup/scripts/link-vicinae-scripts"
|
||||||
dotfile_installer="$repo_dir/setup/scripts/link-dotfiles"
|
|
||||||
top_level_installer="$repo_dir/install"
|
|
||||||
work="$(mktemp -d /tmp/panama-command-install.XXXXXX)"
|
work="$(mktemp -d /tmp/panama-command-install.XXXXXX)"
|
||||||
data_dir="$work/share/vicinae"
|
data_dir="$work/share/vicinae"
|
||||||
fake_bin="$work/bin"
|
fake_bin="$work/bin"
|
||||||
@@ -22,11 +20,6 @@ cleanup() {
|
|||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
[[ -x "$installer" ]] || fail 'Vicinae script installer is missing or not executable'
|
[[ -x "$installer" ]] || fail 'Vicinae script installer is missing or not executable'
|
||||||
if rg -q 'setup/scripts/link-vicinae-scripts' "$dotfile_installer"; then
|
|
||||||
fail 'link-dotfiles also invokes the command installer'
|
|
||||||
fi
|
|
||||||
rg -Fq 'do "$script"; done' "$top_level_installer" \
|
|
||||||
|| fail 'top-level installer sources setup scripts into one shared shell'
|
|
||||||
|
|
||||||
mkdir -p "$data_dir/scripts/panama" "$fake_bin"
|
mkdir -p "$data_dir/scripts/panama" "$fake_bin"
|
||||||
printf 'user-owned\n' >"$data_dir/scripts/panama/keep.sh"
|
printf 'user-owned\n' >"$data_dir/scripts/panama/keep.sh"
|
||||||
@@ -44,14 +37,12 @@ PATH="$fake_bin:$PATH" PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_dir" \
|
|||||||
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
|
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
|
||||||
|
|
||||||
target="$data_dir/scripts/panama"
|
target="$data_dir/scripts/panama"
|
||||||
backup="$data_dir/backups/panama.pre-panama"
|
backup="$data_dir/scripts/panama.pre-panama"
|
||||||
[[ -L "$target" ]] || fail 'Panama command directory was not linked'
|
[[ -L "$target" ]] || fail 'Panama command directory was not linked'
|
||||||
[[ "$(readlink -f "$target")" == "$(readlink -f "$repo_dir/config/local/share/vicinae/scripts")" ]] \
|
[[ "$(readlink -f "$target")" == "$(readlink -f "$repo_dir/config/local/share/vicinae/scripts")" ]] \
|
||||||
|| fail 'runtime command link points outside the tracked source'
|
|| fail 'runtime command link points outside the tracked source'
|
||||||
[[ -f "$backup/keep.sh" ]] || fail 'pre-existing user command directory was not preserved'
|
[[ -f "$backup/keep.sh" ]] || fail 'pre-existing user command directory was not preserved'
|
||||||
[[ "$(cat "$backup/keep.sh")" == user-owned ]] || fail 'preserved user command changed'
|
[[ "$(cat "$backup/keep.sh")" == user-owned ]] || fail 'preserved user command changed'
|
||||||
[[ ! -e "$data_dir/scripts/panama.pre-panama" ]] \
|
|
||||||
|| fail 'preserved commands remained inside Vicinae search paths'
|
|
||||||
|
|
||||||
expected_calls=$'vicinae <ping>\nvicinae <cmd> <launch> <core:reload-scripts>'
|
expected_calls=$'vicinae <ping>\nvicinae <cmd> <launch> <core:reload-scripts>'
|
||||||
[[ "$(cat "$vicinae_log")" == "$expected_calls" ]] \
|
[[ "$(cat "$vicinae_log")" == "$expected_calls" ]] \
|
||||||
@@ -64,7 +55,7 @@ PATH="$fake_bin:$PATH" PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_dir" \
|
|||||||
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
|
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
|
||||||
[[ -L "$target" ]] || fail 'second install did not leave the command link intact'
|
[[ -L "$target" ]] || fail 'second install did not leave the command link intact'
|
||||||
[[ -f "$backup/keep.sh" ]] || fail 'second install disturbed the original backup'
|
[[ -f "$backup/keep.sh" ]] || fail 'second install disturbed the original backup'
|
||||||
[[ ! -e "$data_dir/backups/panama.pre-panama.pre-panama" ]] \
|
[[ ! -e "$data_dir/scripts/panama.pre-panama.pre-panama" ]] \
|
||||||
|| fail 'second install created a duplicate backup'
|
|| fail 'second install created a duplicate backup'
|
||||||
|
|
||||||
printf 'Panama command install contract: PASS\n'
|
printf 'Panama command install contract: PASS\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user