Make ./install something you could hand a stranger
The audit's third tier: everything between this installer and a fresh machine it has never met. The one path that could cost a person their display: the interview probes Secure Boot with mokutil, which install-packages had not installed yet, so on a minimal base the MOK question silently never fired -- and install-hardware still installed akmod-nvidia and blacklisted nouveau, arming a reboot into an unloadable driver with its fallback disabled. The probe tools (pciutils, mokutil, fwupd) now bootstrap beside gum, and install-hardware re-checks Secure Boot for itself and refuses the driver rather than the display. Secrets leave the checkout: the personal environment moves to ~/.config/panama/env at mode 600 by migration, and .bashrc sources it with a permission check that quietly re-tightens drift. change-settings no longer overwrites /etc/dnf/dnf.conf -- two performance keys are set additively, the defaultyes=True that made every `dnf remove` treat Enter as yes is gone, and a migration strips it from machines that already received it. Package installation survives the world changing: the initial and desktop lists run with --skip-unavailable and a report_missing pass that names what was skipped (resolved through --whatprovides, so capability names like awk do not cry wolf); the openh264, appstream and core-group extras go through soft; RustDesk resolves its RPM for the machine's own architecture; and the Claude Desktop repository script is fetched to a kept file and run, never piped from the network into root. The hardware predicates stop guessing: a wireless mouse's scope=Device battery no longer turns a tower into a laptop, USB-PD-only machines read their power state from the battery's own status instead of being permanently "on AC", the lid falls back to logind's LidClosed where ACPI is silent, and charge limits reach every pack of a two-battery machine in one authorization -- with the reported percentage summed across packs. And the parsers stop assuming this machine: snapper is read through --machine-readable csv with named columns instead of a localized box-drawing table, and reports whether snapshots are even possible so ext4 and unconfigured-btrfs stop looking identical; fprintd is parsed under LC_ALL=C; the hypridle drop-in resolves the binary it points at; the recorder's render node became an "auto" token resolved at record time; update-grub writes the config its firmware actually boots; the nvm prompt hook and the SSH tmux takeover are guarded; hipblas and rocm-opencl move to an opt-in gpu-compute category; and the two interactive python tools' libraries are declared. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
This commit is contained in:
+31
-10
@@ -38,28 +38,37 @@ is_laptop() {
|
||||
[[ "$PORTABLE_CHASSIS" == *" $chassis "* ]]
|
||||
}
|
||||
|
||||
# The first battery, or nothing. Named rather than assumed to be BAT0: the
|
||||
# second battery in a ThinkPad is BAT1, and a machine with only BAT1 exists.
|
||||
# The first SYSTEM battery, or nothing. Named rather than assumed to be BAT0:
|
||||
# the second battery in a ThinkPad is BAT1, and a machine with only BAT1
|
||||
# exists. The scope check is what keeps a desktop a desktop: a wireless mouse
|
||||
# or a game controller publishes type=Battery with scope=Device, and counting
|
||||
# one turned a tower into a "laptop" whose battery readout was the mouse's.
|
||||
battery_path() {
|
||||
local supply type
|
||||
local supply type scope
|
||||
for supply in "$SYS"/class/power_supply/*; do
|
||||
[[ -r "$supply/type" ]] || continue
|
||||
type="$(cat "$supply/type" 2>/dev/null)"
|
||||
if [[ "$type" == "Battery" ]]; then
|
||||
[[ "$type" == "Battery" ]] || continue
|
||||
scope="$(cat "$supply/scope" 2>/dev/null || echo System)"
|
||||
[[ "$scope" == "Device" ]] && continue
|
||||
printf '%s\n' "$supply"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
has_battery() { battery_path >/dev/null; }
|
||||
|
||||
# On wall power. A machine with no mains supply at all is a desktop, and a
|
||||
# desktop is always on wall power -- answering "no" there would make every
|
||||
# battery-aware timing apply to a machine that cannot run out of power.
|
||||
# On wall power. A machine with no mains supply at all and no system battery
|
||||
# is a desktop, and a desktop is always on wall power -- answering "no" there
|
||||
# would make every battery-aware timing apply to a machine that cannot run
|
||||
# out of power. But "no Mains" alone is not "desktop": hardware charged only
|
||||
# over USB-PD exposes type=USB supplies and no Mains at all, and reading that
|
||||
# as permanently-on-AC meant its battery timings never engaged while it ran
|
||||
# down. When no Mains exists but a system battery does, the battery's own
|
||||
# status is the answer: Discharging means battery, everything else means fed.
|
||||
on_ac() {
|
||||
local supply type online found=1
|
||||
local supply type online found=1 battery status
|
||||
for supply in "$SYS"/class/power_supply/*; do
|
||||
[[ -r "$supply/type" ]] || continue
|
||||
type="$(cat "$supply/type" 2>/dev/null)"
|
||||
@@ -70,16 +79,28 @@ on_ac() {
|
||||
done
|
||||
# Mains exists and none of it is online: genuinely on battery.
|
||||
(( found == 0 )) && return 1
|
||||
if battery="$(battery_path)"; then
|
||||
status="$(cat "$battery/status" 2>/dev/null || echo Unknown)"
|
||||
[[ "$status" == "Discharging" ]] && return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ACPI first, logind second. Some platforms expose the lid only as an evdev
|
||||
# switch with no /proc/acpi/button entry; logind watches the switch either
|
||||
# way, so its LidClosed property is the fallback that keeps clamshell
|
||||
# detection honest there. No logind (a container, a test tree) means the
|
||||
# fallback quietly answers open, which is the safe direction.
|
||||
lid_closed() {
|
||||
local state
|
||||
for state in "$ACPI"/button/lid/*/state; do
|
||||
[[ -r "$state" ]] || continue
|
||||
grep -qi closed "$state" && return 0
|
||||
done
|
||||
return 1
|
||||
done
|
||||
[[ -d "$ACPI/button/lid" ]] && return 1
|
||||
busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
|
||||
org.freedesktop.login1.Manager LidClosed 2>/dev/null | grep -q 'b true'
|
||||
}
|
||||
|
||||
# A connected output that is not the built-in panel. eDP, LVDS and DSI are the
|
||||
|
||||
@@ -18,6 +18,19 @@ if [ -d "$PANAMA_BASH" ]; then
|
||||
[ -f "$f" ] && . "$f"
|
||||
done
|
||||
unset f
|
||||
|
||||
# Personal environment -- API keys, tokens -- lives OUTSIDE the checkout,
|
||||
# where agents, backup tools and `panama update` walk, and is kept
|
||||
# owner-only every time it is read: a secrets file that drifts to 644 is
|
||||
# quietly re-tightened rather than trusted. (config/bash/env, its old home
|
||||
# inside the repo, is still sourced by the glob above if a machine has not
|
||||
# been migrated yet.)
|
||||
PANAMA_ENV="${XDG_CONFIG_HOME:-$HOME/.config}/panama/env"
|
||||
if [ -f "$PANAMA_ENV" ]; then
|
||||
[ "$(stat -c %a "$PANAMA_ENV" 2>/dev/null)" = "600" ] || chmod 600 "$PANAMA_ENV"
|
||||
. "$PANAMA_ENV"
|
||||
fi
|
||||
unset PANAMA_ENV
|
||||
else
|
||||
if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]; then
|
||||
PATH="$HOME/.local/bin:$HOME/bin:$PATH"
|
||||
|
||||
+10
-1
@@ -6,7 +6,16 @@ alias :wq="exit"
|
||||
alias sourcerc="source ~/.bashrc"
|
||||
alias c="clear"
|
||||
alias shutdown="systemctl poweroff"
|
||||
alias update-grub="sudo grub-mkconfig -o /etc/grub2-efi.cfg"
|
||||
# The config target differs by firmware: EFI machines regenerate the EFI
|
||||
# config, BIOS machines /boot/grub2/grub.cfg -- writing the EFI path on a BIOS
|
||||
# machine updates a file nothing boots from.
|
||||
update-grub() {
|
||||
if [ -d /sys/firmware/efi ]; then
|
||||
sudo grub2-mkconfig -o /etc/grub2-efi.cfg
|
||||
else
|
||||
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
|
||||
fi
|
||||
}
|
||||
alias nvidia-smi-docker='sudo docker run --rm --gpus all --privileged nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi'
|
||||
alias ncconnect='sudo docker exec -u www-data -it nextcloud-aio-nextcloud bash'
|
||||
alias avante='nvim -c "lua vim.defer_fn(function()require(\"avante.api\").zen_mode()end, 100)"'
|
||||
|
||||
+11
-3
@@ -28,14 +28,22 @@ export PATH="$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:
|
||||
[ -f /etc/profile.d/nvm.sh ] && source /etc/profile.d/nvm.sh
|
||||
# Auto-switch Node version when entering a directory with .nvmrc
|
||||
_nvm_auto_use() {
|
||||
if [[ -f .nvmrc ]]; then
|
||||
# Guarded on nvm actually being loaded: without this, a machine where the
|
||||
# nvm profile script is absent printed "command not found" on every single
|
||||
# prompt in any directory carrying a .nvmrc.
|
||||
if [[ -f .nvmrc ]] && type -t nvm >/dev/null 2>&1; then
|
||||
nvm use --silent
|
||||
fi
|
||||
}
|
||||
export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }_nvm_auto_use"
|
||||
|
||||
# Auto-start or attach tmux for SSH interactive shells
|
||||
if [[ -n "$SSH_CONNECTION" && -z "$TMUX" && $- == *i* ]]; then
|
||||
# Auto-start or attach tmux for SSH interactive shells. A deliberate Panama
|
||||
# behavior (tmux is in initial-packages, and a dropped SSH session keeping its
|
||||
# work is the point), but guarded: it must not replace the shell of someone
|
||||
# whose machine lacks tmux, and PANAMA_SSH_TMUX=off turns it off for people
|
||||
# who want a plain shell -- set it in ~/.config/panama/env.
|
||||
if [[ -n "$SSH_CONNECTION" && -z "$TMUX" && $- == *i* \
|
||||
&& "${PANAMA_SSH_TMUX:-on}" != "off" ]] && command -v tmux >/dev/null 2>&1; then
|
||||
exec tmux new-session -A -s main
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# see `man dnf.conf` for defaults and possible options
|
||||
|
||||
[main]
|
||||
fastestmirror=True
|
||||
max_parallel_downloads=10
|
||||
defaultyes=True
|
||||
keepcache=True
|
||||
deltarpm=True
|
||||
@@ -1395,14 +1395,18 @@ Singleton {
|
||||
label: "Recording folder",
|
||||
detail: "Where screen recordings are saved. Relative to your home folder unless it starts with /"
|
||||
},
|
||||
// "auto" stands in for the render node until record time:
|
||||
// /dev/dri/renderD128 was baked into every option once, which is one
|
||||
// machine's enumeration and frequently the wrong node on hybrid
|
||||
// graphics. Capture.qml resolves it when recording starts.
|
||||
{
|
||||
key: "recorderArgs", type: "enum", def: "-c h264_vaapi -d /dev/dri/renderD128",
|
||||
key: "recorderArgs", type: "enum", def: "-c h264_vaapi -d auto",
|
||||
group: "capture",
|
||||
label: "Recording encoder",
|
||||
detail: "Hardware encoding keeps recording off the processor while gaming",
|
||||
options: [
|
||||
{ value: "-c h264_vaapi -d /dev/dri/renderD128", label: "VAAPI H.264" },
|
||||
{ value: "-c hevc_vaapi -d /dev/dri/renderD128", label: "VAAPI HEVC" },
|
||||
{ value: "-c h264_vaapi -d auto", label: "VAAPI H.264" },
|
||||
{ value: "-c hevc_vaapi -d auto", label: "VAAPI HEVC" },
|
||||
{ value: "-c libx264", label: "CPU x264" }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -31,6 +31,22 @@ battery_dir() {
|
||||
"$HW" battery-path 2>/dev/null
|
||||
}
|
||||
|
||||
# Every system battery, for the machines that have two. The primary pack
|
||||
# answers `paths` and drives the watch files; these answer the questions
|
||||
# where ignoring the second pack gives a wrong answer -- the total charge,
|
||||
# and which packs a threshold write must reach.
|
||||
all_battery_dirs() {
|
||||
local supply type scope
|
||||
for supply in "$SYS"/class/power_supply/*; do
|
||||
[[ -r "$supply/type" ]] || continue
|
||||
type="$(cat "$supply/type" 2>/dev/null)"
|
||||
[[ "$type" == "Battery" ]] || continue
|
||||
scope="$(cat "$supply/scope" 2>/dev/null || echo System)"
|
||||
[[ "$scope" == "Device" ]] && continue
|
||||
printf '%s\n' "$supply"
|
||||
done
|
||||
}
|
||||
|
||||
# The mains supply, if the machine has one. A desktop has none, and that is
|
||||
# not an error: panama-hw's `ac` predicate treats "no mains at all" as being on
|
||||
# wall power, and the shell falls back to the same assumption.
|
||||
@@ -78,6 +94,21 @@ cmd_status() {
|
||||
capacity="$(read_int "$battery/capacity")" || capacity=""
|
||||
[[ -r "$battery/status" ]] && state="$(cat "$battery/status" 2>/dev/null)"
|
||||
threshold="$(read_int "$battery/charge_control_end_threshold")" || threshold=0
|
||||
|
||||
# With two packs, one pack's percentage is not the machine's: sum the
|
||||
# stored and full energy across every system battery and answer with
|
||||
# the real total. Single-battery machines never reach this.
|
||||
local dirs=() dir now full total_now=0 total_full=0
|
||||
mapfile -t dirs < <(all_battery_dirs)
|
||||
if (( ${#dirs[@]} > 1 )); then
|
||||
for dir in "${dirs[@]}"; do
|
||||
now="$(read_int "$dir/energy_now" || read_int "$dir/charge_now")" || continue
|
||||
full="$(read_int "$dir/energy_full" || read_int "$dir/charge_full")" || continue
|
||||
total_now=$(( total_now + now ))
|
||||
total_full=$(( total_full + full ))
|
||||
done
|
||||
(( total_full > 0 )) && capacity=$(( (total_now * 100 + total_full / 2) / total_full ))
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$mains" ]]; then
|
||||
online="$(read_int "$mains/online")" || online=0
|
||||
@@ -95,19 +126,25 @@ cmd_set_threshold() {
|
||||
[[ "$value" =~ ^[0-9]+$ ]] || { echo 'set-threshold needs a percentage' >&2; return 2; }
|
||||
(( value >= 50 && value <= 100 )) || { echo 'threshold must be between 50 and 100' >&2; return 2; }
|
||||
|
||||
battery="$(battery_dir)" || { echo 'no battery on this machine' >&2; return 1; }
|
||||
file="$battery/charge_control_end_threshold"
|
||||
[[ -e "$file" ]] || { echo 'this machine cannot set a charge threshold' >&2; return 1; }
|
||||
# Every pack that has the attribute, not just the first: capping one
|
||||
# battery of a two-battery machine leaves the other charging to full,
|
||||
# which is the opposite of what the person asked for.
|
||||
local files=() dir
|
||||
while IFS= read -r dir; do
|
||||
[[ -e "$dir/charge_control_end_threshold" ]] && files+=("$dir/charge_control_end_threshold")
|
||||
done < <(all_battery_dirs)
|
||||
(( ${#files[@]} > 0 )) || { echo 'this machine cannot set a charge threshold' >&2; return 1; }
|
||||
|
||||
# tee rather than a redirect: the redirect is performed by the calling
|
||||
# shell, which is not the one holding root.
|
||||
# shell, which is not the one holding root. One authorization writes every
|
||||
# pack.
|
||||
"$PANAMA_PATH/bin/panama-sudo" \
|
||||
--reason "Capping battery charging at ${value}% to reduce wear" \
|
||||
-- sh -c "printf '%s\n' '$value' | tee '$file' >/dev/null" || return 1
|
||||
-- sh -c "printf '%s\n' '$value' | tee ${files[*]} >/dev/null" || return 1
|
||||
|
||||
# Read it back rather than reporting success from the write's exit code:
|
||||
# some firmware silently clamps or ignores the value.
|
||||
read_int "$file"
|
||||
read_int "${files[0]}"
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
|
||||
@@ -39,7 +39,10 @@ cmd_status() {
|
||||
# so this also copes with the daemon not running yet) and names the
|
||||
# enrolled fingers in one call.
|
||||
local listing
|
||||
if ! listing="$(timeout 10 fprintd-list "$USER" 2>&1)"; then
|
||||
# LC_ALL=C: the "no devices" match below reads fprintd's message, and a
|
||||
# translated daemon would turn every readerless non-English machine into
|
||||
# a permanent error card.
|
||||
if ! listing="$(LC_ALL=C timeout 10 fprintd-list "$USER" 2>&1)"; then
|
||||
# "No devices available" is the normal no-reader machine; anything
|
||||
# else is a real problem worth surfacing.
|
||||
if grep -qi 'no devices' <<<"$listing"; then
|
||||
|
||||
@@ -155,7 +155,7 @@ install_dropin() {
|
||||
# Panama Settings rather than by editing a file in the Panama repository.
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/hypridle -c $generated
|
||||
ExecStart=$(command -v hypridle || echo /usr/bin/hypridle) -c $generated
|
||||
EOF
|
||||
systemctl --user daemon-reload
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ hit.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -70,52 +72,47 @@ def require_number(value: str) -> int:
|
||||
return number
|
||||
|
||||
|
||||
def config_names() -> list[str]:
|
||||
result = run(["snapper", "list-configs"])
|
||||
# Everything snapper answers is read through --machine-readable csv and a
|
||||
# DictReader. The first version split snapper's box-drawing table by column
|
||||
# INDEX, which is the presentation layer: it is localized, and its columns
|
||||
# have moved between snapper versions -- a German desktop parsed to zero
|
||||
# snapshots while `snapper list` showed twelve. Named columns survive both.
|
||||
def snapper_csv(args: list[str], timeout: int = 20) -> list[dict]:
|
||||
result = run(["snapper", "--machine-readable", "csv", *args], timeout=timeout)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
names = []
|
||||
for line in result.stdout.splitlines()[2:]:
|
||||
parts = [part.strip() for part in line.split("│")]
|
||||
if len(parts) >= 2 and CONFIG_NAME.fullmatch(parts[0]):
|
||||
names.append(parts[0])
|
||||
return names
|
||||
return list(csv.DictReader(io.StringIO(result.stdout)))
|
||||
|
||||
|
||||
def config_names() -> list[str]:
|
||||
return [row["config"] for row in snapper_csv(["list-configs"])
|
||||
if CONFIG_NAME.fullmatch(row.get("config", ""))]
|
||||
|
||||
|
||||
def config_settings(name: str) -> dict:
|
||||
result = run(["snapper", "-c", name, "get-config"])
|
||||
if result.returncode != 0:
|
||||
return {}
|
||||
values = {}
|
||||
for line in result.stdout.splitlines()[2:]:
|
||||
parts = [part.strip() for part in line.split("│")]
|
||||
if len(parts) >= 2:
|
||||
values[parts[0]] = parts[1]
|
||||
return values
|
||||
return {row["key"]: row.get("value", "")
|
||||
for row in snapper_csv(["-c", name, "get-config"]) if row.get("key")}
|
||||
|
||||
|
||||
def snapshots_for(name: str) -> list[dict]:
|
||||
result = run(["snapper", "-c", name, "list"], timeout=45)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
entries = []
|
||||
for line in result.stdout.splitlines()[2:]:
|
||||
parts = [part.strip() for part in line.split("│")]
|
||||
if len(parts) < 7 or not parts[0].isdigit():
|
||||
for row in snapper_csv(["-c", name, "list"], timeout=45):
|
||||
if not str(row.get("number", "")).isdigit():
|
||||
continue
|
||||
number = int(parts[0])
|
||||
number = int(row["number"])
|
||||
if number == 0:
|
||||
continue
|
||||
cleanup = row.get("cleanup", "") or ""
|
||||
entries.append({
|
||||
"number": number,
|
||||
"kind": parts[1],
|
||||
"date": parts[3],
|
||||
"user": parts[4],
|
||||
"cleanup": parts[5],
|
||||
"description": parts[6],
|
||||
"kind": row.get("type", ""),
|
||||
"date": row.get("date", ""),
|
||||
"user": row.get("user", ""),
|
||||
"cleanup": cleanup,
|
||||
"description": row.get("description", ""),
|
||||
# A snapshot with no cleanup algorithm is not on the timeline's
|
||||
# list to remove, which is what "kept" means to someone reading it.
|
||||
"kept": parts[5] == "",
|
||||
"kept": cleanup == "",
|
||||
})
|
||||
entries.sort(key=lambda entry: entry["number"], reverse=True)
|
||||
return entries
|
||||
@@ -193,6 +190,10 @@ def snapshot() -> dict:
|
||||
"unprotected": unprotected,
|
||||
"timelineRunning": timeline_running(),
|
||||
"space": free_space(),
|
||||
# An ext4 machine and an unconfigured btrfs machine used to look
|
||||
# identical: an empty page. "supported" is whether snapshots are even
|
||||
# possible here, so absence can say which kind of absence it is.
|
||||
"supported": bool(btrfs_subvolumes()),
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +194,20 @@ Singleton {
|
||||
Quickshell.execDetached(args);
|
||||
}
|
||||
|
||||
// The render node "auto" in recorderArgs resolves to. Enumerated once at
|
||||
// startup, which is as often as it changes; empty until the scan lands,
|
||||
// and the map below falls back to the conventional first node then.
|
||||
property string renderNode: ""
|
||||
|
||||
Process {
|
||||
id: renderNodeScan
|
||||
command: ["sh", "-c", "ls /dev/dri/renderD* 2>/dev/null | head -1"]
|
||||
running: true
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.renderNode = this.text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
function recordRegion(geom: string): void {
|
||||
if (recProc.running)
|
||||
return;
|
||||
@@ -205,7 +219,13 @@ Singleton {
|
||||
// mkdir + exec so the PID we later SIGINT is wf-recorder itself and not
|
||||
// the shell wrapping it.
|
||||
let cmd = ["sh", "-c", 'mkdir -p "$1" && shift && exec "$@"', "qs-capture", root.recDir, "wf-recorder", "-y"];
|
||||
cmd = cmd.concat(Settings.recorderArgs.split(" ").filter(a => a !== ""));
|
||||
// "auto" becomes a real render node here rather than in the schema:
|
||||
// the stored option must not carry one machine's device path to the
|
||||
// next machine. First node wins; a hybrid machine that needs the
|
||||
// other one can store explicit args.
|
||||
const args = Settings.recorderArgs.split(" ").filter(a => a !== "")
|
||||
.map(a => a === "auto" ? (root.renderNode || "/dev/dri/renderD128") : a);
|
||||
cmd = cmd.concat(args);
|
||||
if (geom !== "")
|
||||
cmd.push("-g", geom);
|
||||
else if (root._outputName !== "")
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ Found on **Screen Intelligence**.
|
||||
|---|---|---|
|
||||
| **Screenshot folder**<br>`screenshotDir` | Pictures/Screenshots | Where screenshots are saved. Relative to your home folder unless it starts with / |
|
||||
| **Recording folder**<br>`recordingDir` | Videos/Screencasts | Where screen recordings are saved. Relative to your home folder unless it starts with / |
|
||||
| **Recording encoder**<br>`recorderArgs` | -c h264_vaapi -d /dev/dri/renderD128 | Hardware encoding keeps recording off the processor while gaming Choices: VAAPI H.264, VAAPI HEVC, CPU x264. |
|
||||
| **Recording encoder**<br>`recorderArgs` | -c h264_vaapi -d auto | Hardware encoding keeps recording off the processor while gaming Choices: VAAPI H.264, VAAPI HEVC, CPU x264. |
|
||||
|
||||
## clock
|
||||
|
||||
|
||||
@@ -14,14 +14,23 @@ source "$PANAMA_PATH/bin/ascii"
|
||||
# installed, and nothing asks again afterwards. That is the whole bargain: the
|
||||
# rest of the run takes twenty minutes and needs nobody watching it.
|
||||
#
|
||||
# gum is bootstrapped first because the interview is built on it and it cannot
|
||||
# install itself -- it is declared in initial-packages, which install-packages
|
||||
# installs, which runs after this. One small dnf call buys a real interface for
|
||||
# the only part of the install a person actually interacts with.
|
||||
if ! command -v gum >/dev/null 2>&1; then
|
||||
echo "Installing gum, which the setup questions are built on"
|
||||
sudo dnf install -y gum >/dev/null || {
|
||||
echo "Could not install gum, so the setup questions cannot be asked." >&2
|
||||
# The interview's tools are bootstrapped first because it cannot install them
|
||||
# itself -- they are declared in the package lists, which install-packages
|
||||
# installs, which runs after this. gum is the interface; pciutils, mokutil and
|
||||
# fwupd are the interview's eyes.
|
||||
# It probes for an NVIDIA card, Secure Boot state and updatable firmware
|
||||
# BEFORE install-packages runs, and a missing probe tool degrades the answer
|
||||
# silently to "no" -- which for Secure Boot once meant installing a driver
|
||||
# that could never load. Workstation ships all four; a minimal base does not.
|
||||
bootstrap=()
|
||||
command -v gum >/dev/null 2>&1 || bootstrap+=(gum)
|
||||
command -v lspci >/dev/null 2>&1 || bootstrap+=(pciutils)
|
||||
command -v mokutil >/dev/null 2>&1 || bootstrap+=(mokutil)
|
||||
command -v fwupdmgr >/dev/null 2>&1 || bootstrap+=(fwupd)
|
||||
if (( ${#bootstrap[@]} > 0 )); then
|
||||
echo "Installing what the setup questions are built on: ${bootstrap[*]}"
|
||||
sudo dnf install -y "${bootstrap[@]}" >/dev/null || {
|
||||
echo "Could not install ${bootstrap[*]}, so the setup questions cannot be asked." >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Remove defaultyes=True from /etc/dnf/dnf.conf on machines Panama configured.
|
||||
#
|
||||
# change-settings used to copy a whole dnf.conf over the machine's own, and
|
||||
# that file set defaultyes=True -- so every `dnf remove`, for every user,
|
||||
# treated a bare Enter as confirmation. The installer no longer ships the
|
||||
# file; this repairs the machines that already received it. Only the one
|
||||
# behavior key is touched: the performance keys it carried are harmless and
|
||||
# now applied additively by change-settings.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
conf=/etc/dnf/dnf.conf
|
||||
|
||||
[[ -r "$conf" ]] || exit 0
|
||||
grep -q '^defaultyes=True$' "$conf" || exit 0
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
sudo_cmd=(sudo)
|
||||
if [[ -t 0 && -x "$PANAMA_PATH/bin/panama-sudo" ]]; then
|
||||
sudo_cmd=(
|
||||
"$PANAMA_PATH/bin/panama-sudo" --reason
|
||||
"Removing defaultyes=True from dnf.conf, which made every dnf remove treat Enter as yes"
|
||||
--
|
||||
)
|
||||
fi
|
||||
|
||||
"${sudo_cmd[@]}" sed -i '/^defaultyes=True$/d' "$conf"
|
||||
echo "Removed defaultyes=True from $conf; dnf prompts default to No again."
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Move the personal environment file out of the checkout.
|
||||
#
|
||||
# config/bash/env holds API keys and tokens, and it lived inside the working
|
||||
# tree that agents, backup tools and `panama update`'s diff walk routinely --
|
||||
# world-readable by default, one careless `git add -f` away from a remote.
|
||||
# Its home is now ~/.config/panama/env, owner-only, which .bashrc sources
|
||||
# with a permission check. The repo path keeps working unmigrated (the glob
|
||||
# in .bashrc still sources it), so this move can safely run at any login.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
old="$PANAMA_PATH/config/bash/env"
|
||||
new="${XDG_CONFIG_HOME:-$HOME/.config}/panama/env"
|
||||
|
||||
[[ -f "$old" ]] || exit 0
|
||||
|
||||
if [[ -f "$new" ]]; then
|
||||
# Both exist: the person already started one at the new home. Refusing to
|
||||
# merge secrets automatically is the safe answer; say what is where.
|
||||
chmod 600 "$old" "$new"
|
||||
echo "Both $old and $new exist; not merging them automatically."
|
||||
echo "Move what you still need into $new and delete the old file."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$new")"
|
||||
mv "$old" "$new"
|
||||
chmod 600 "$new"
|
||||
echo "Moved the personal environment to $new (owner-only)."
|
||||
@@ -45,7 +45,6 @@ gstreamer1-plugins-good-extras
|
||||
gstreamer1-plugins-good-gtk
|
||||
gstreamer1-plugins-good-qt
|
||||
gstreamer1-plugins-good-qt6
|
||||
hipblas
|
||||
jetbrainsmono-nerd-fonts
|
||||
kernel-devel
|
||||
lame
|
||||
@@ -79,7 +78,6 @@ python3-dnf-plugin-versionlock
|
||||
# The launcher's Copy Password command; talks to the same Bitwarden account
|
||||
# the desktop app signs into. See panama-pick.
|
||||
rbw
|
||||
rocm-opencl
|
||||
# The Snapshots page is snapper end to end; see scripts/panama-snapshots.
|
||||
snapper
|
||||
sushi
|
||||
|
||||
@@ -18,6 +18,10 @@ maven
|
||||
# would win every switch. install-packages does the rest.
|
||||
nvm
|
||||
pipx
|
||||
# bin/get-port and bin/cp-files are interactive python tools; these are the
|
||||
# libraries they import, and without them both crash on their first line.
|
||||
python3-questionary
|
||||
python3-pyperclip
|
||||
php
|
||||
php-fpm
|
||||
# Rootless containers, and the backend for the Containers settings page.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# AMD GPU compute -- hundreds of megabytes that only an AMD card can use.
|
||||
# These were in desktop-packages once, installed unconditionally on Intel and
|
||||
# NVIDIA machines that could never load them.
|
||||
hipblas | AMD hipBLAS (ROCm BLAS for GPU compute)
|
||||
rocm-opencl | AMD ROCm OpenCL runtime
|
||||
@@ -15,7 +15,18 @@ exists() { command -v "$1" >/dev/null 2>&1; }
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
|
||||
echo -e "\n--- Copying System & User files ---"
|
||||
log "Changing DNF Settings"
|
||||
|
||||
# dnf options are applied as individual keys rather than shipped as a file.
|
||||
# config/copy once carried a whole /etc/dnf/dnf.conf, which clobbered proxies
|
||||
# and mirrors a machine already had -- and set defaultyes=True system-wide, so
|
||||
# every `dnf remove` for every user treated a bare Enter as yes. These two are
|
||||
# performance knobs with no behavior change; anything else stays the machine's
|
||||
# own business. (fastestmirror and deltarpm were dnf4-era keys dnf5 ignores,
|
||||
# so they simply stop being written.)
|
||||
log "Setting dnf performance options (max_parallel_downloads, keepcache)"
|
||||
sudo dnf config-manager setopt max_parallel_downloads=10 keepcache=True 2>/dev/null \
|
||||
|| log "Could not set dnf options; defaults apply"
|
||||
log "Copying system files (udev rules, systemd drop-ins)"
|
||||
sudo cp -r "$PANAMA_PATH/config/copy/." "/"
|
||||
|
||||
# The GPU symlink rules land via the copy above. Apply them without a reboot so
|
||||
|
||||
@@ -40,6 +40,18 @@ fi
|
||||
# ── NVIDIA ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "${PANAMA_NVIDIA:-no}" == yes ]]; then
|
||||
# The interview asks about MOK enrollment only when mokutil was present to
|
||||
# see Secure Boot at all. Re-check here rather than trusting that the
|
||||
# question was ever asked: installing akmod-nvidia and blacklisting
|
||||
# nouveau under Secure Boot with no key to enroll produces a machine that
|
||||
# reboots into an unloadable driver with its fallback disabled -- the one
|
||||
# failure in this installer that costs a person their display.
|
||||
if mokutil --sb-state 2>/dev/null | grep -qi 'secureboot enabled' \
|
||||
&& [[ -z "${PANAMA_MOK_HASH:-}" ]]; then
|
||||
warn "Secure Boot is on and no MOK enrollment was prepared; refusing to install"
|
||||
warn "the NVIDIA driver, which could not load. Re-run ./install and answer the"
|
||||
warn "Secure Boot question, or disable Secure Boot first."
|
||||
else
|
||||
log "Installing the NVIDIA driver"
|
||||
if sudo dnf install -y akmod-nvidia xorg-x11-drv-nvidia-cuda; then
|
||||
# nouveau has to be out of the way before the kernel would otherwise
|
||||
@@ -62,6 +74,7 @@ if [[ "${PANAMA_NVIDIA:-no}" == yes ]]; then
|
||||
warn "The NVIDIA driver did not install; skipping its kernel arguments and services"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Secure Boot ──────────────────────────────────────────────────────────────
|
||||
#
|
||||
|
||||
@@ -22,6 +22,20 @@ packages_in() {
|
||||
sed 's/#.*//' "$1" | tr "\n" " "
|
||||
}
|
||||
|
||||
# Names a list asked for that still are not installed, so --skip-unavailable
|
||||
# above can never silently shrink a list: a skipped font is a warning somebody
|
||||
# reads, not an absence somebody debugs a month later.
|
||||
report_missing() {
|
||||
local file="$1" name missing=()
|
||||
for name in $(packages_in "$file"); do
|
||||
# --whatprovides, because several list entries are capabilities a
|
||||
# differently-named package satisfies: awk is gawk, wget is wget2-wget.
|
||||
rpm -q --whatprovides "$name" >/dev/null 2>&1 || missing+=("$name")
|
||||
done
|
||||
(( ${#missing[@]} > 0 )) && log "WARNING: not available on this machine: ${missing[*]}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Runs something whose failure must not cost you the desktop.
|
||||
#
|
||||
# `set -e` above is right for the packages Panama cannot work without and wrong
|
||||
@@ -58,10 +72,13 @@ echo -e "\n--- Installing Repositories ---"
|
||||
log "Installing RPM Fusion Free and Nonfree Repositories"
|
||||
sudo dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm > /dev/null
|
||||
log "Enabling Fedora Cisco OpenH264 Repository"
|
||||
sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1
|
||||
# soft: this repo does not exist on every spin, and its absence must not cost
|
||||
# the desktop -- the ordering rule at soft()'s definition applies to the
|
||||
# repository extras just as much as to the codec swaps below.
|
||||
soft "enabling the openh264 repository" sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1
|
||||
log "Installing RPM Fusion AppStream Metadata"
|
||||
sudo dnf update @core -y > /dev/null
|
||||
sudo dnf install -y rpmfusion-\*-appstream-data > /dev/null
|
||||
soft "the core group update" sudo dnf update @core -y
|
||||
soft "the RPM Fusion appstream metadata" sudo dnf install -y rpmfusion-\*-appstream-data
|
||||
# Terra bootstraps itself: --repofrompath defines a throwaway repo just long
|
||||
# enough to install terra-release, which then writes the real /etc/yum.repos.d
|
||||
# entry. Doing that a second time is not harmless -- dnf5 refuses the whole
|
||||
@@ -90,7 +107,12 @@ if [[ -f "$PACKAGES_FILE" ]]; then
|
||||
log "Installing Initial Packages"
|
||||
echo -e "Includes the following packages:"
|
||||
echo -e "$(<"$PACKAGES_FILE")"
|
||||
sudo dnf install -y $INITIAL_PACKAGES > /dev/null
|
||||
# --skip-unavailable: dnf5 refuses a whole transaction over one missing
|
||||
# name, so a single rotted entry in this list used to cost every package
|
||||
# in it -- and the desktop below never installed. The skipped names are
|
||||
# reported afterwards rather than silently dropped.
|
||||
sudo dnf install -y --skip-unavailable $INITIAL_PACKAGES > /dev/null
|
||||
report_missing "$PACKAGES_FILE"
|
||||
log "Initial packages installed!"
|
||||
else
|
||||
log "Package list was not in specified path: $PACKAGES_FILE"
|
||||
@@ -103,7 +125,8 @@ if [[ -f "$DESKTOP_FILE" ]]; then
|
||||
log "Installing Desktop Packages"
|
||||
echo -e "Includes the following packages:"
|
||||
echo -e "$(<"$DESKTOP_FILE")"
|
||||
sudo dnf install -y $DESKTOP_PACKAGES > /dev/null
|
||||
sudo dnf install -y --skip-unavailable $DESKTOP_PACKAGES > /dev/null
|
||||
report_missing "$DESKTOP_FILE"
|
||||
log "Desktop packages installed!"
|
||||
else
|
||||
log "Package list was not in specified path: $DESKTOP_FILE"
|
||||
@@ -263,8 +286,19 @@ if rpm -q claude-desktop-extra >/dev/null 2>&1; then
|
||||
else
|
||||
if [[ ! -f /etc/yum.repos.d/claude-desktop.repo ]]; then
|
||||
log "Adding the Claude Desktop repository..."
|
||||
curl -fsSL https://patrickjaja.github.io/claude-desktop-extra/install-rpm.sh \
|
||||
| sudo bash > /dev/null 2>&1 || log "Could not add the Claude Desktop repository"
|
||||
# Fetched to a file and then run, never piped into root: a pipe executes
|
||||
# whatever the network answered with no chance to look, and this one is an
|
||||
# unpinned script from a personal GitHub Pages site -- the least trusted
|
||||
# thing this installer touches. The file is kept next to the run so what
|
||||
# executed is still on disk to read afterwards.
|
||||
claude_repo_script="$(mktemp -t claude-desktop-repo.XXXXXX.sh)"
|
||||
if curl -fsSL https://patrickjaja.github.io/claude-desktop-extra/install-rpm.sh \
|
||||
-o "$claude_repo_script"; then
|
||||
sudo bash "$claude_repo_script" > /dev/null 2>&1 \
|
||||
|| log "Could not add the Claude Desktop repository (script kept at $claude_repo_script)"
|
||||
else
|
||||
log "Could not download the Claude Desktop repository script"
|
||||
fi
|
||||
fi
|
||||
log "Installing Claude Desktop..."
|
||||
sudo dnf install -y claude-desktop-extra > /dev/null \
|
||||
@@ -283,7 +317,7 @@ else
|
||||
# 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)' \
|
||||
| jq -r --arg arch "$(uname -m)" '.assets[].browser_download_url | select(test($arch + "\\.rpm$")) | select(test("suse") | not)' \
|
||||
| head -1 || true)"
|
||||
if [[ -n "$rustdesk_url" ]]; then
|
||||
log "Installing RustDesk from $rustdesk_url"
|
||||
|
||||
@@ -36,7 +36,7 @@ SHELL_WORDS='^(if|then|else|elif|fi|for|while|until|do|done|case|esac|in|functio
|
||||
# authselect is on the list for the same reason: it manages Fedora's PAM and
|
||||
# nsswitch profiles and arrives with fprintd-pam, realmd and nss-mdns, so the
|
||||
# fingerprint aliases in config/bash can rely on it without declaring it.
|
||||
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|lsof|authselect|setsid|nohup)$'
|
||||
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|lsof|authselect|setsid|nohup|grub2-mkconfig)$'
|
||||
|
||||
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user