Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42b3cfe0e | ||
|
|
51ceb19480 | ||
|
|
25e2328658 | ||
|
|
9f563c8f94 | ||
|
|
7d633eb06e | ||
|
|
6d1f3f3763 | ||
|
|
de923cb4d5 | ||
|
|
bd9a55c8eb | ||
|
|
cab7699711 |
@@ -1,5 +1,7 @@
|
||||
# Ignore bash environment variables.
|
||||
/config/bash/env
|
||||
# Personal espanso triggers (name, email), seeded per-machine by setup-identity.
|
||||
/config/dot/espanso/match/identity.yml
|
||||
# Ignore backups of old config files
|
||||
/config/old
|
||||
# Ignore Wireguard config of course!
|
||||
|
||||
@@ -3,11 +3,23 @@
|
||||
Formerly Sunhat. A personal config for Fedora, with the intention of helping a
|
||||
user set up their Fedora system with one command.
|
||||
|
||||
```sh
|
||||
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
||||
```
|
||||
|
||||
`boot` installs git if the machine lacks it, clones this repository to
|
||||
`~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is
|
||||
deliberately small enough to read first, and the same two steps by hand work
|
||||
identically:
|
||||
|
||||
```sh
|
||||
git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
|
||||
~/.local/share/Panama/install
|
||||
```
|
||||
|
||||
Both are safe to run again: an existing clone is fast-forwarded rather than
|
||||
replaced, and `install` is the upgrade path.
|
||||
|
||||
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
||||
in order, without stopping again:
|
||||
|
||||
@@ -101,7 +113,7 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## Tests
|
||||
|
||||
131 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||
133 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||
|
||||
```sh
|
||||
panama test # everything
|
||||
@@ -159,6 +171,12 @@ package, a `flatpak:` line is a Flathub id, `| Name` gives the menu something
|
||||
readable, and an indented line belongs to the entry above it — which is how OBS
|
||||
carries its sixteen plugin extensions as one thing to tick.
|
||||
|
||||
`panama-sudo` is pkexec with a stated reason: `panama-sudo --reason "why" --
|
||||
command` shows the reason on Panama's password prompt, clearly labeled as an
|
||||
unverified claim beside polkitd's own action text — meant for agents and
|
||||
scripts, so the person typing the password learns why before they do. Without
|
||||
a reason, a running shell, or `qs` it behaves exactly like pkexec.
|
||||
|
||||
`panama app` is deliberately not part of `./install`. Everything else Panama
|
||||
installs comes from dnf or Flathub; these are built from source because no
|
||||
packaged form exists, and a source build is slow, wants the network throughout,
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# pkexec, with a stated reason on the password prompt.
|
||||
#
|
||||
# panama-sudo --reason "Installing gamemode hooks" -- dnf install gamemode
|
||||
#
|
||||
# The reason travels to the shell over Quickshell IPC before pkexec runs, and
|
||||
# Panama's prompt shows it clearly labeled beside polkitd's own action message
|
||||
# -- beside, never instead of: anything can claim any reason, so the real
|
||||
# action text stays the trust anchor. Meant for agents and scripts, so the
|
||||
# person at the keyboard learns WHY before typing their password.
|
||||
#
|
||||
# Degrades to plain pkexec: no --reason, no running shell, or no qs on PATH
|
||||
# all behave identically to calling pkexec yourself.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
reason=""
|
||||
if [[ "${1:-}" == "--reason" ]]; then
|
||||
reason="${2:?panama-sudo: --reason needs a value}"
|
||||
shift 2
|
||||
fi
|
||||
[[ "${1:-}" == "--" ]] && shift
|
||||
|
||||
if (( $# == 0 )); then
|
||||
echo 'usage: panama-sudo [--reason "why"] -- command [args...]' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -n "$reason" ]] && command -v qs >/dev/null 2>&1; then
|
||||
qs ipc call polkit reason "$reason" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
exec pkexec "$@"
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Panama's front door: the one command a fresh Fedora machine needs.
|
||||
#
|
||||
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
||||
#
|
||||
# Deliberately dumb, because a copy of this script leaves the repository the
|
||||
# moment somebody curls it -- nothing here can be fixed by re-running
|
||||
# ./install, so there is as little here as possible: get git, get the clone,
|
||||
# hand off. Everything with judgment in it lives in `install`, which is also
|
||||
# where re-runs and upgrades already work.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://git.gbrown.org/gib/Panama.git"
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
export PANAMA_PATH
|
||||
|
||||
# Root would put the clone and every dotfile in root's home and run the
|
||||
# desktop setup for the wrong user. sudo is used inside where it is needed.
|
||||
if [[ "$(id -u)" -eq 0 ]]; then
|
||||
echo "Run this as your own user, not root: the install configures YOUR desktop." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Panama assumes Fedora's repositories, package names, and GNOME base install.
|
||||
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
|
||||
echo "This looks like something other than Fedora; Panama only supports Fedora Workstation." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# git is the one dependency the clone itself needs. Everything else -- gum
|
||||
# included -- is bootstrapped by `install`.
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "Installing git, which the clone needs"
|
||||
sudo dnf install -y git
|
||||
fi
|
||||
|
||||
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
||||
# An existing clone makes this the recovery command too. Only a fast-forward:
|
||||
# local work is never rewritten, and a diverged clone still installs from
|
||||
# what it has rather than stopping someone mid-repair.
|
||||
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
||||
git -C "$PANAMA_PATH" pull --ff-only \
|
||||
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
||||
else
|
||||
git clone "$REPO_URL" "$PANAMA_PATH"
|
||||
fi
|
||||
|
||||
# `curl | bash` and `bash <(curl ...)` can leave stdin as the pipe, and the
|
||||
# first thing install runs is the interview, which has to be able to ask.
|
||||
# Reattach the terminal when there is one; without one the interview will say
|
||||
# so itself.
|
||||
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
||||
# controlling terminal passes -r and then fails the redirect.
|
||||
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
|
||||
exec "$PANAMA_PATH/install" </dev/tty
|
||||
fi
|
||||
exec "$PANAMA_PATH/install"
|
||||
+2
-2
@@ -3,7 +3,6 @@
|
||||
# Aliases I like
|
||||
alias :q="exit"
|
||||
alias :wq="exit"
|
||||
alias startsunshine="systemctl --user restart sunshine.service"
|
||||
alias sourcerc="source ~/.bashrc"
|
||||
alias c="clear"
|
||||
alias shutdown="systemctl poweroff"
|
||||
@@ -32,7 +31,8 @@ alias ls='eza -lh --group-directories-first --icons'
|
||||
alias lsa='ls -a'
|
||||
alias lt='eza --tree --level=2 --long --icons --git'
|
||||
alias lta='lt -a'
|
||||
alias ff="fzf --preview 'batcat --style=numbers --color=always {}'"
|
||||
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
|
||||
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
||||
|
||||
# Directories
|
||||
alias ..='cd ..'
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
# espanso match file
|
||||
# https://espanso.org/docs/
|
||||
# Name and email triggers (:fn, :em, ...) are NOT here: they are personal, and
|
||||
# this file is shared. setup-identity seeds them into match/identity.yml --
|
||||
# per-machine, gitignored, yours to edit -- from the install interview.
|
||||
matches:
|
||||
# Name
|
||||
- trigger: ":fn"
|
||||
replace: "Gabriel Brown"
|
||||
- trigger: ":fin"
|
||||
replace: "Gabriel A Brown"
|
||||
# Email
|
||||
- trigger: ":em"
|
||||
replace: "[email protected]"
|
||||
- trigger: ":empro"
|
||||
replace: "[email protected]"
|
||||
|
||||
# Date
|
||||
- trigger: ":date"
|
||||
replace: "{{mydate}}"
|
||||
|
||||
@@ -29,7 +29,7 @@ Don't "fix" them.
|
||||
| `hyprland.lua` | Entry point. Each `require()` is its own error scope |
|
||||
| `prefs.lua` | Reads the settings file the Settings app writes. See below |
|
||||
| `env.lua` | Environment. Note the uwsm caveat below |
|
||||
| `monitors.lua` | DP-2 geometry, scaling, and the HDR decision |
|
||||
| `monitors.lua` | Monitor geometry and scaling (the Kuycon by description), and the HDR decision |
|
||||
| `looks.lua` | Colors, blur, glow, shadows, animations, VRR, scanout |
|
||||
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
||||
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
||||
|
||||
@@ -39,6 +39,14 @@ hl.on("hyprland.start", function()
|
||||
-- prompt if Panama's ever fails to come up.
|
||||
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP && systemctl --user start panama-polkit-agent.service hyprpaper.service vicinae.service hypridle.service")
|
||||
|
||||
-- Text expansion. change-settings runs `espanso service register`, which
|
||||
-- writes and enables espanso's own user unit; the explicit start makes the
|
||||
-- first Hyprland login after a fresh install work rather than the second.
|
||||
-- Started on its own line: unlike the units above it carries no
|
||||
-- ConditionEnvironment, and a missing unit (espanso not yet registered)
|
||||
-- must not muddy the start of the four that lock and wallpaper depend on.
|
||||
hl.exec_cmd("systemctl --user start espanso.service")
|
||||
|
||||
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
||||
-- No systemd unit ships with quickshell, so it runs as a compositor child.
|
||||
hl.exec_cmd("quickshell --daemonize")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Monitors
|
||||
--
|
||||
-- Kuycon P20 on DP-2: 4500x3000 @ 60Hz, 1.5x fractional scale.
|
||||
-- Kuycon P20 (matched by description): 4500x3000 @ 60Hz, 1.5x fractional scale.
|
||||
-- 4500/1.5 = 3000 and 3000/1.5 = 2000, both integers, so this is a "clean"
|
||||
-- fractional scale and Hyprland will not complain.
|
||||
--
|
||||
@@ -125,17 +125,38 @@ local function display_position(entry, fallback)
|
||||
return fallback
|
||||
end
|
||||
|
||||
-- Every connected output uses the same validated per-output store. Automatic
|
||||
-- placement and the compositor's normal color policy unless the entry says
|
||||
-- otherwise.
|
||||
for output, _ in pairs(displays) do
|
||||
local entry = display_entry(output)
|
||||
if entry ~= nil then
|
||||
hl.monitor({
|
||||
output = output,
|
||||
mode = entry.mode,
|
||||
position = display_position(entry, "auto"),
|
||||
scale = entry.scale,
|
||||
transform = entry.transform,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- The Kuycon P20, matched by what it is rather than where it is plugged in.
|
||||
-- This used to be a rule for connector DP-2 outright, which handed the panel's
|
||||
-- 4500x3000 mode and 1.5 scale to whatever monitor a stranger's machine had on
|
||||
-- its most common DisplayPort connector. Emitted after the prefs loop so a
|
||||
-- saved entry for its connector still carries the mode/scale/position, while
|
||||
-- this rule holds the shipped defaults and the panel-specific color policy.
|
||||
local shipped_mode = "4500x3000@60"
|
||||
local shipped_scale = 1.5
|
||||
local shipped_transform = 0
|
||||
local dp2 = display_entry("DP-2")
|
||||
|
||||
local kuycon = display_entry("DP-2")
|
||||
hl.monitor({
|
||||
output = "DP-2",
|
||||
mode = dp2 and dp2.mode or shipped_mode,
|
||||
position = display_position(dp2, "0x0"),
|
||||
scale = dp2 and dp2.scale or shipped_scale,
|
||||
transform = dp2 and dp2.transform or shipped_transform,
|
||||
output = "desc:GVT Kuycon P20",
|
||||
mode = kuycon and kuycon.mode or shipped_mode,
|
||||
position = display_position(kuycon, "0x0"),
|
||||
scale = kuycon and kuycon.scale or shipped_scale,
|
||||
transform = kuycon and kuycon.transform or shipped_transform,
|
||||
|
||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of
|
||||
-- DP 1.4 HBR3, so this relies on DSC. If the display fails to light up or
|
||||
@@ -146,24 +167,6 @@ hl.monitor({
|
||||
cm = "auto",
|
||||
})
|
||||
|
||||
-- Other connected outputs use the same validated per-output store. They keep
|
||||
-- automatic placement and the compositor's normal color policy; DP-2 alone
|
||||
-- carries the panel-specific 10-bit policy documented above.
|
||||
for output, _ in pairs(displays) do
|
||||
if output ~= "DP-2" then
|
||||
local entry = display_entry(output)
|
||||
if entry ~= nil then
|
||||
hl.monitor({
|
||||
output = output,
|
||||
mode = entry.mode,
|
||||
position = display_position(entry, "auto"),
|
||||
scale = entry.scale,
|
||||
transform = entry.transform,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Any monitor not named above: sane defaults rather than nothing.
|
||||
hl.monitor({
|
||||
output = "",
|
||||
|
||||
@@ -94,6 +94,34 @@ PanelWindow {
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// The caller's stated reason, when panama-sudo passed one.
|
||||
// Untrusted commentary from an unprivileged process, so it is
|
||||
// labeled as a claim and drawn beside polkitd's message above --
|
||||
// never in place of it. The real action text is the trust anchor.
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: Polkit.statedReason !== ""
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: "Stated reason (unverified)"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: Polkit.statedReason
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.italic: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: Polkit.users.length > 1
|
||||
|
||||
@@ -38,12 +38,31 @@ Singleton {
|
||||
// How many times a wrong password has been offered for this request.
|
||||
property int attempts: 0
|
||||
|
||||
// A caller's stated reason for the NEXT request, and the one attached to
|
||||
// the request on screen. Untrusted by design -- any process can state one
|
||||
// -- so the prompt shows it clearly labeled beside polkitd's real action
|
||||
// message, never in place of it. See stateReason().
|
||||
property var pendingReason: null
|
||||
property string statedReason: ""
|
||||
|
||||
readonly property bool active: root.request !== null
|
||||
|
||||
// Held only between pressing Enter and the helper accepting it on stdin.
|
||||
property string pendingSecret: ""
|
||||
|
||||
|
||||
// panama-sudo's side channel: state WHY the authentication request about
|
||||
// to arrive is being made, so the prompt can say more than the generic
|
||||
// action text. Single-shot and short-lived -- it attaches only to the next
|
||||
// request, and only if that request arrives within ten seconds -- so a
|
||||
// stale reason can never dress up an unrelated prompt.
|
||||
function stateReason(text: string): void {
|
||||
const trimmed = String(text).trim().slice(0, 200);
|
||||
if (trimmed === "")
|
||||
return;
|
||||
root.pendingReason = { text: trimmed, at: Date.now() };
|
||||
}
|
||||
|
||||
function begin(path: string): void {
|
||||
// A second request while one is open would leave the first
|
||||
// unanswerable; polkit serializes these in practice, and refusing is
|
||||
@@ -69,6 +88,13 @@ Singleton {
|
||||
: (root.users.length > 0 ? String(root.users[0]) : "");
|
||||
root.attempts = 0;
|
||||
root.failureText = "";
|
||||
// Consume the stated reason whether or not it is still fresh:
|
||||
// either way it must not survive to a later request.
|
||||
const pending = root.pendingReason;
|
||||
root.pendingReason = null;
|
||||
root.statedReason = (pending !== null && Date.now() - pending.at <= 10000)
|
||||
? pending.text
|
||||
: "";
|
||||
} catch (error) {
|
||||
console.warn("Polkit: could not read the request:", error);
|
||||
root.dismiss("failed");
|
||||
@@ -114,6 +140,7 @@ Singleton {
|
||||
root.failureText = "";
|
||||
root.pendingSecret = "";
|
||||
root.authenticating = false;
|
||||
root.statedReason = "";
|
||||
}
|
||||
|
||||
function responsePathFor(path: string): string {
|
||||
|
||||
@@ -424,8 +424,15 @@ ShellRoot {
|
||||
target: "polkit"
|
||||
function begin(path: string): void { Polkit.begin(path); }
|
||||
function cancel(): void { Polkit.cancel(); }
|
||||
// panama-sudo's side channel: the reason a privileged command is about
|
||||
// to run, shown labeled on the prompt beside polkitd's own message.
|
||||
function reason(text: string): void { Polkit.stateReason(text); }
|
||||
function status(): string {
|
||||
return JSON.stringify({ active: Polkit.active, action: Polkit.actionId });
|
||||
return JSON.stringify({
|
||||
active: Polkit.active,
|
||||
action: Polkit.actionId,
|
||||
statedReason: Polkit.statedReason
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,13 @@ cleanup() {
|
||||
# design, and one of them is an email address.
|
||||
[[ -n "${PANAMA_ANSWERS:-}" ]] && rm -f "$PANAMA_ANSWERS"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
trap cleanup EXIT
|
||||
# A bare `trap cleanup INT` is not an abort: bash runs the handler and then
|
||||
# carries on with the script, so Ctrl-C would kill only the current stage and
|
||||
# the remaining ones -- MOK enrollment, firmware -- would still run. Exit
|
||||
# explicitly instead; the EXIT trap above does the actual cleanup.
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
gsettings set org.gnome.desktop.screensaver lock-enabled false 2>/dev/null || true
|
||||
gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
|
||||
|
||||
@@ -30,3 +30,6 @@ python3-virtualenv
|
||||
ripgrep
|
||||
ruby
|
||||
sqlite3
|
||||
# vimx: clipboard-capable vim, which the shipped `vim` alias and the vimrc
|
||||
# that link-dotfiles installs both assume. Fedora carries it as vim-X11.
|
||||
vim-X11
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Strict: the copy over / below is the one thing this stage exists to do, and
|
||||
# without set -e its failure fell through to guarded no-ops and exited 0 --
|
||||
# a failed stage the installer's summary could never see.
|
||||
set -euo pipefail
|
||||
|
||||
# --- Helper functions ---
|
||||
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
exists() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# --- Defined Paths ---
|
||||
PANAMA_PATH="$HOME/.local/share/Panama"
|
||||
# The default, not an assignment: an exported PANAMA_PATH from a clone at
|
||||
# another location must win, or the copy over / below reads the wrong tree.
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
|
||||
echo -e "\n--- Copying System & User files ---"
|
||||
log "Changing DNF Settings"
|
||||
@@ -61,6 +68,18 @@ echo -e "\n--- Hyprland session services ---"
|
||||
# PartOf=graphical-session.target.
|
||||
log "Hyprland services are started per-session by hypr/autostart.lua, not enabled globally"
|
||||
|
||||
# espanso is the exception to the rule above: text expansion is wanted under
|
||||
# any compositor and conflicts with nothing GNOME runs, and upstream's own
|
||||
# mechanism is a registered user unit. The RPM ships no unit -- without this,
|
||||
# the package installs, the config links, and no trigger ever fires.
|
||||
# `service register` writes and enables the unit and is idempotent on re-runs;
|
||||
# autostart.lua also starts it so the first Hyprland login after a fresh
|
||||
# install expands text without waiting for the next one.
|
||||
if exists espanso && ! systemctl --user cat espanso.service >/dev/null 2>&1; then
|
||||
log "Registering the espanso text-expansion service"
|
||||
espanso service register >/dev/null 2>&1 || log "Could not register espanso"
|
||||
fi
|
||||
|
||||
# Notification daemons must NOT be installed: mako, dunst and swaync all
|
||||
# register Name=org.freedesktop.Notifications for D-Bus activation, which races
|
||||
# Quickshell's own notification server at login. Whoever wins keeps the name.
|
||||
|
||||
@@ -23,7 +23,10 @@ packages_in() {
|
||||
}
|
||||
|
||||
# --- Defined Paths ---
|
||||
PANAMA_PATH="$HOME/.local/share/Panama"
|
||||
# The default, not an assignment: ./install and link-dotfiles honor an exported
|
||||
# PANAMA_PATH, and clobbering it here made a clone anywhere else source the
|
||||
# extras catalog from a path that does not exist.
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
|
||||
# Reading the extras catalog, shared with `panama apps` so the two front doors
|
||||
# cannot disagree about what a category contains.
|
||||
@@ -214,9 +217,12 @@ if rpm -q rustdesk >/dev/null 2>&1; then
|
||||
log "RustDesk already installed"
|
||||
else
|
||||
log "Resolving the latest RustDesk release..."
|
||||
# `|| true` because a failed curl -- unauthenticated GitHub API calls get
|
||||
# rate-limited -- would otherwise trip set -e and kill the stage before the
|
||||
# empty-result fallback below could do its job.
|
||||
rustdesk_url="$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk/releases/latest 2>/dev/null \
|
||||
| jq -r '.assets[].browser_download_url | select(test("x86_64\\.rpm$")) | select(test("suse") | not)' \
|
||||
| head -1)"
|
||||
| head -1 || true)"
|
||||
if [[ -n "$rustdesk_url" ]]; then
|
||||
log "Installing RustDesk from $rustdesk_url"
|
||||
# The RPM ships rustdesk.service already enabled, which is what provides
|
||||
@@ -271,7 +277,9 @@ install_extra_category() {
|
||||
name="$(basename "$file")"
|
||||
|
||||
local dnf_packages flatpak_ids
|
||||
dnf_packages=$(catalog_all_targets "$file" | grep -v '^flatpak:' | tr "\n" " ")
|
||||
# sed rather than grep -v: most categories are flatpak-only, and grep exits 1
|
||||
# when it selects nothing, which set -e above turns into a dead stage.
|
||||
dnf_packages=$(catalog_all_targets "$file" | sed '/^flatpak:/d' | tr "\n" " ")
|
||||
flatpak_ids=$(catalog_all_targets "$file" | sed -n 's/^flatpak://p' | tr "\n" " ")
|
||||
|
||||
if [[ -n "${dnf_packages// /}" ]]; then
|
||||
|
||||
@@ -122,7 +122,12 @@ if [[ -n "$nvidia_card" ]]; then
|
||||
fi
|
||||
second="$(gum input --password --header "MOK password again")"
|
||||
if [[ "$first" == "$second" ]]; then
|
||||
mok_hash="$(mokutil --generate-hash="$first")"
|
||||
# Fed on stdin, never as an argument: an argument sits
|
||||
# in /proc/<pid>/cmdline for any local process to read
|
||||
# while mokutil runs. mokutil prints its two prompts on
|
||||
# stdout too, so the hash is the last line.
|
||||
mok_hash="$(printf '%s\n%s\n' "$first" "$first" \
|
||||
| mokutil --generate-hash | tail -n 1)"
|
||||
break
|
||||
fi
|
||||
printf 'Those did not match.\n'
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Strict: a failed mv or ln here used to fall through and exit 0, so a machine
|
||||
# could end up half-linked while the installer's summary reported the stage as
|
||||
# fine. Anything genuinely optional below carries its own `|| true`.
|
||||
set -euo pipefail
|
||||
|
||||
# --- Helper functions ---
|
||||
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
|
||||
@@ -10,8 +15,10 @@ PANAMA_DOT="$PANAMA_PATH/config/dot"
|
||||
PANAMA_OLD="$PANAMA_PATH/config/old"
|
||||
CONFIG="$HOME/.config"
|
||||
|
||||
# Make backup folder if it doesn't exist
|
||||
mkdir -p "$PANAMA_OLD"
|
||||
# Make backup folder if it doesn't exist -- and ~/.config itself, which a
|
||||
# truly fresh HOME does not have yet. Before set -e above, its absence made
|
||||
# every symlink below fail silently while the stage still reported success.
|
||||
mkdir -p "$PANAMA_OLD" "$CONFIG"
|
||||
|
||||
# --- Bashrc ---
|
||||
echo -e "\n--- Replacing .bashrc ---"
|
||||
|
||||
@@ -41,6 +41,26 @@ git config --global alias.st status
|
||||
git config --global pull.rebase true
|
||||
log "git aliases and pull.rebase applied"
|
||||
|
||||
# Text-expansion identity. The :fn/:em triggers used to hardcode the repository
|
||||
# owner's name and email in the shared match file; each machine now writes its
|
||||
# own from the interview's answers. Per-machine and gitignored -- espanso loads
|
||||
# every file under match/, so it sits beside base.yml without being shared.
|
||||
# Kept when it already exists: the file is the user's to edit, and a re-run
|
||||
# must not erase what they added.
|
||||
espanso_identity="${XDG_CONFIG_HOME:-$HOME/.config}/espanso/match/identity.yml"
|
||||
if [[ -d "$(dirname "$espanso_identity")" && ! -e "$espanso_identity" ]] \
|
||||
&& [[ -n "$git_name" || -n "$git_email" ]]; then
|
||||
{
|
||||
printf '# Personal expansion triggers, seeded from the install interview.\n'
|
||||
printf '# Per-machine and untracked: add your own freely.\n'
|
||||
printf 'matches:\n'
|
||||
[[ -n "$git_name" ]] && printf ' - trigger: ":fn"\n replace: "%s"\n' "$git_name"
|
||||
[[ -n "$git_email" ]] && printf ' - trigger: ":em"\n replace: "%s"\n' "$git_email"
|
||||
:
|
||||
} > "$espanso_identity"
|
||||
log "Seeded espanso identity triggers at $espanso_identity"
|
||||
fi
|
||||
|
||||
if [[ "${PANAMA_GH_LOGIN:-no}" == yes ]]; then
|
||||
if command -v gh >/dev/null 2>&1; then
|
||||
log "Signing in to GitHub"
|
||||
|
||||
@@ -122,8 +122,14 @@ assert(by_output["DP-2"].mode == "4500x3000@60")
|
||||
assert(by_output["DP-2"].scale == 1.5)
|
||||
assert(by_output["DP-2"].transform == 0)
|
||||
assert(by_output["DP-2"].position == "0x0")
|
||||
assert(by_output["DP-2"].bitdepth == 10)
|
||||
assert(by_output["DP-2"].cm == "auto")
|
||||
-- The panel-specific color policy rides the description-matched rule, not the
|
||||
-- connector: a stranger's monitor on DP-2 must not inherit the Kuycon's mode
|
||||
-- or its 10-bit request.
|
||||
local kuycon = by_output["desc:GVT Kuycon P20"]
|
||||
assert(kuycon ~= nil)
|
||||
assert(kuycon.mode == "4500x3000@60")
|
||||
assert(kuycon.bitdepth == 10)
|
||||
assert(kuycon.cm == "auto")
|
||||
assert(by_output["HDMI-A-1"].mode == "2560x1440@60")
|
||||
assert(by_output["HDMI-A-1"].scale == 1)
|
||||
assert(by_output["HDMI-A-1"].transform == 1)
|
||||
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The stated reason on the authentication prompt.
|
||||
#
|
||||
# panama-sudo lets a caller say WHY it is about to trigger a password prompt.
|
||||
# The reason is untrusted text from an unprivileged process, so the properties
|
||||
# worth pinning are the ones that keep it honest:
|
||||
#
|
||||
# 1. The prompt renders the reason BESIDE polkitd's real action message,
|
||||
# labeled as unverified -- never in place of it. Any process can claim
|
||||
# "Updating your system" while requesting something else; the action text
|
||||
# is the trust anchor and must survive.
|
||||
# 2. Single-shot and short-lived: a reason attaches to the next request
|
||||
# only, is consumed whether or not it was fresh, and expires rather than
|
||||
# dressing up an unrelated prompt minutes later.
|
||||
# 3. panama-sudo degrades to plain pkexec: no --reason, no qs, or a dead
|
||||
# shell must all still run the command.
|
||||
#
|
||||
# The wrapper is exercised for real against stub qs and pkexec; the QML side
|
||||
# is pinned statically, the way the polkit-agent contract pins its rules.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
wrapper="$repo_dir/bin/panama-sudo"
|
||||
service="$repo_dir/config/dot/quickshell/services/Polkit.qml"
|
||||
prompt="$repo_dir/config/dot/quickshell/modules/polkit/PolkitPrompt.qml"
|
||||
shell_qml="$repo_dir/config/dot/quickshell/shell.qml"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
[[ -x "$wrapper" ]] || { printf 'polkit reason contract: %s is not executable\n' "$wrapper" >&2; exit 1; }
|
||||
|
||||
# ── The wrapper, for real ────────────────────────────────────────────────────
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
calls="$work/calls"
|
||||
stub_dir="$work/bin"
|
||||
mkdir -p "$stub_dir"
|
||||
|
||||
for command in qs pkexec; do
|
||||
cat >"$stub_dir/$command" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf '%s %s\n' "$command" "\$*" >>"$calls"
|
||||
STUB
|
||||
chmod +x "$stub_dir/$command"
|
||||
done
|
||||
|
||||
run_wrapper() { PATH="$stub_dir:$PATH" "$wrapper" "$@" >/dev/null 2>&1; }
|
||||
|
||||
# A reason reaches the shell first, then the command runs unchanged.
|
||||
: >"$calls"
|
||||
run_wrapper --reason "Test reason" -- some-command --with args \
|
||||
|| note 'the wrapper failed with a reason and a command'
|
||||
grep -q 'qs ipc call polkit reason Test reason' "$calls" \
|
||||
|| note 'the reason never reaches the shell over IPC'
|
||||
grep -q 'pkexec some-command --with args' "$calls" \
|
||||
|| note 'the command does not reach pkexec unchanged'
|
||||
[[ "$(head -1 "$calls")" == qs* ]] \
|
||||
|| note 'the reason is sent after pkexec instead of before the prompt can appear'
|
||||
|
||||
# No reason means no IPC chatter, and still pkexec.
|
||||
: >"$calls"
|
||||
run_wrapper -- some-command || note 'the wrapper failed without a reason'
|
||||
grep -q 'qs' "$calls" && note 'the wrapper calls qs even when no reason was given'
|
||||
grep -q 'pkexec some-command' "$calls" || note 'a reasonless call does not reach pkexec'
|
||||
|
||||
# A dead shell must not cost the command: qs failing is stepped over.
|
||||
cat >"$stub_dir/qs" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
exit 1
|
||||
STUB
|
||||
chmod +x "$stub_dir/qs"
|
||||
: >"$calls"
|
||||
run_wrapper --reason "Doomed" -- some-command \
|
||||
|| note 'a failing qs stops the command instead of degrading to plain pkexec'
|
||||
grep -q 'pkexec some-command' "$calls" \
|
||||
|| note 'the command is lost when the shell is not answering'
|
||||
|
||||
# No command is a usage error, not a bare pkexec prompt for nothing.
|
||||
run_wrapper --reason "Aimless" -- && note 'the wrapper accepts a reason with no command'
|
||||
|
||||
# ── The QML side, statically ─────────────────────────────────────────────────
|
||||
|
||||
# The IPC door exists and feeds the service.
|
||||
rg -Fq 'target: "polkit"' "$shell_qml" \
|
||||
|| note 'shell.qml has no polkit IPC target'
|
||||
rg -Fq 'Polkit.stateReason(text)' "$shell_qml" \
|
||||
|| note 'the polkit IPC target does not feed Polkit.stateReason'
|
||||
|
||||
# Single-shot, bounded, and cleared: consumed on adopt even when stale, aged
|
||||
# against a ten-second window, and wiped with the rest of the request state.
|
||||
rg -Fq 'root.pendingReason = null' "$service" \
|
||||
|| note 'a stated reason is not consumed when a request arrives'
|
||||
rg -Fq 'pending.at <= 10000' "$service" \
|
||||
|| note 'a stated reason never expires, so it can dress up a later prompt'
|
||||
rg -Fq 'root.statedReason = ""' "$service" \
|
||||
|| note 'the stated reason survives dismissal'
|
||||
|
||||
# The prompt shows the real message AND the labeled reason -- both, in that
|
||||
# trust order.
|
||||
rg -Fq 'text: Polkit.message' "$prompt" \
|
||||
|| note "polkitd's own action message is no longer rendered"
|
||||
rg -Fq 'text: Polkit.statedReason' "$prompt" \
|
||||
|| note 'the stated reason is never rendered'
|
||||
rg -Fq 'Stated reason (unverified)' "$prompt" \
|
||||
|| note 'the stated reason is not labeled as an unverified claim'
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'polkit reason contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'polkit reason contract: PASS\n'
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The front door: `boot` is the script the README tells a fresh machine to
|
||||
# curl, so it runs before anything else Panama ships -- including its own
|
||||
# tests. What it must get right is small and worth pinning:
|
||||
#
|
||||
# * a machine without the clone gets one, from the documented URL, at
|
||||
# PANAMA_PATH, and the install runs
|
||||
# * a machine with the clone is not re-cloned -- the same command is the
|
||||
# recovery command -- and a fast-forward failure does not stop the install
|
||||
# * boot hands off to the clone's own install, with PANAMA_PATH exported,
|
||||
# so a clone at a chosen location installs from that location
|
||||
#
|
||||
# Run against stub git and install in a throwaway PANAMA_PATH; nothing here
|
||||
# touches the real clone or the network.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
boot="$repo_dir/boot"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
[[ -x "$boot" ]] || { printf 'boot contract: %s is not executable\n' "$boot" >&2; exit 1; }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
calls="$work/calls"
|
||||
stub_dir="$work/bin"
|
||||
clone_dir="$work/Panama"
|
||||
mkdir -p "$stub_dir"
|
||||
|
||||
# The stub install records that it ran and what PANAMA_PATH it saw. The stub
|
||||
# git records its arguments, and materializes a clone the way the real one
|
||||
# would -- boot execs the clone's install, so the clone has to contain one.
|
||||
cat >"$work/fake-install" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf 'install PANAMA_PATH=%s\n' "\${PANAMA_PATH:-unset}" >>"$calls"
|
||||
STUB
|
||||
chmod +x "$work/fake-install"
|
||||
|
||||
cat >"$stub_dir/git" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf 'git %s\n' "\$*" >>"$calls"
|
||||
if [[ "\$1" == "clone" ]]; then
|
||||
mkdir -p "\$3/.git"
|
||||
cp "$work/fake-install" "\$3/install"
|
||||
fi
|
||||
STUB
|
||||
chmod +x "$stub_dir/git"
|
||||
|
||||
run_boot() {
|
||||
: >"$calls"
|
||||
PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ── A machine without the clone ──────────────────────────────────────────────
|
||||
|
||||
run_boot || note 'boot failed on a machine without the clone'
|
||||
|
||||
grep -q "git clone https://git.gbrown.org/gib/Panama.git $clone_dir" "$calls" \
|
||||
|| note 'boot does not clone the documented repository to PANAMA_PATH'
|
||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||
|| note 'boot does not hand off to the clone'\''s install with PANAMA_PATH exported'
|
||||
|
||||
# ── A machine that already has it ────────────────────────────────────────────
|
||||
|
||||
run_boot || note 'boot failed on a machine that already has the clone'
|
||||
|
||||
grep -q 'git clone' "$calls" \
|
||||
&& note 'boot re-clones over an existing checkout'
|
||||
grep -q 'git -C .* pull --ff-only' "$calls" \
|
||||
|| note 'boot does not fast-forward an existing clone'
|
||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||
|| note 'boot does not run the install from an existing clone'
|
||||
|
||||
# ── A diverged clone still installs ──────────────────────────────────────────
|
||||
#
|
||||
# pull --ff-only refusing is normal life -- local commits, a rebase upstream.
|
||||
# The command doubles as the repair path, so a refusal must be stepped over.
|
||||
|
||||
cat >"$stub_dir/git" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
[[ "$*" == *pull* ]] && exit 1
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$stub_dir/git"
|
||||
|
||||
: >"$calls"
|
||||
if ! PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1; then
|
||||
note 'a clone that cannot fast-forward stops the install instead of proceeding'
|
||||
fi
|
||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||
|| note 'the install does not run when the fast-forward is refused'
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'boot contract: PASS\n'
|
||||
@@ -115,6 +115,31 @@ if grep 'flatpak install' <<<"$recorded" | grep -q 'from-dnf'; then
|
||||
note 'a dnf package is passed to flatpak'
|
||||
fi
|
||||
|
||||
# A flatpak-only category -- which most of the real ones are -- must survive
|
||||
# the installer's own strict options. Filtering the dnf half with `grep -v`
|
||||
# once left an exit 1 for zero matches, and set -e killed the stage before its
|
||||
# flatpak half ran. Run under those options, not the contract's laxer ones;
|
||||
# the status is captured rather than `||`-guarded because a condition context
|
||||
# would switch errexit off inside the subshell and hide the very failure this
|
||||
# pins.
|
||||
: >"$calls"
|
||||
flatpak_only="$work/flatpak-only"
|
||||
printf 'flatpak:org.example.OnlyFlatpak\n' >"$flatpak_only"
|
||||
(
|
||||
set -euo pipefail
|
||||
PATH="$stub_dir:$PATH"
|
||||
log() { :; }
|
||||
source "$catalog"
|
||||
eval "$filter"
|
||||
eval "$loop"
|
||||
install_extra_category "$flatpak_only"
|
||||
)
|
||||
flatpak_only_status=$?
|
||||
(( flatpak_only_status == 0 )) \
|
||||
|| note 'a flatpak-only category aborts the installer under set -euo pipefail'
|
||||
grep -q 'flatpak install -y flathub org.example.OnlyFlatpak' <<<"$(cat "$calls" 2>/dev/null)" \
|
||||
|| note 'a flatpak-only category installs nothing'
|
||||
|
||||
# Choosing nothing installs nothing.
|
||||
: >"$calls"
|
||||
(
|
||||
|
||||
@@ -52,8 +52,15 @@ done <<<"$consumed"
|
||||
|
||||
# ── 3. Nothing is left behind ────────────────────────────────────────────────
|
||||
|
||||
grep -q 'trap cleanup EXIT INT TERM' "$install_script" \
|
||||
|| note 'install does not arm a cleanup trap on EXIT INT TERM'
|
||||
# Cleanup rides the EXIT trap; INT and TERM must exit explicitly, because a
|
||||
# trap handler that merely cleans up lets bash carry on with the remaining
|
||||
# stages after a Ctrl-C -- MOK enrollment and firmware included.
|
||||
grep -q 'trap cleanup EXIT' "$install_script" \
|
||||
|| note 'install does not arm a cleanup trap on EXIT'
|
||||
grep -qE "trap 'exit [0-9]+' INT" "$install_script" \
|
||||
|| note 'install does not exit on SIGINT, so Ctrl-C would keep installing'
|
||||
grep -qE "trap 'exit [0-9]+' TERM" "$install_script" \
|
||||
|| note 'install does not exit on SIGTERM, so a kill would keep installing'
|
||||
grep -q 'rm -f "$PANAMA_ANSWERS"' "$install_script" \
|
||||
|| note 'the cleanup trap does not delete the answers file'
|
||||
grep -qE 'mktemp' "$install_script" \
|
||||
|
||||
Reference in New Issue
Block a user