@@ -116,7 +116,7 @@ Lots of development tools including herdr, neovim, tmux, go, hugo, python, rust,
|
||||
|
||||
### Printing and Networking
|
||||
|
||||
The printing service (cups) is turned on. This may need more work to "just work" with other "coredesktop" machines.
|
||||
The printing service (cups) is turned on. Network printers show up in GNOME Settings and browsers on their own, but often not in the GTK print dialog ("Print using system dialog...") until they're added as printers on this computer. `configure printers` does that (see Configure).
|
||||
|
||||
mDNS is installed and enabled so you can find and SSH to your machine on your LAN using {machinename}.local.
|
||||
|
||||
@@ -179,7 +179,7 @@ Besides installing software, CoreDesktop changes these settings. They're listed
|
||||
- **Snapper** (btrfs only): a config for / in /etc/snapper/configs/root.
|
||||
- **NVIDIA** (only if it installs the driver): /etc/mkinitcpio.conf.d/nvidia.conf and NVIDIA's suspend services.
|
||||
- **Your home folder**: lines in ~/.bashrc (CoreDesktop's shell setup, ~/.local/bin on PATH, the Bitwarden SSH agent), the commands in ~/.local/bin (update, refresh, sibling, version, configure, audit), GNOME extensions and their settings, Claude Code's ~/.claude/settings.json if you had none, a user timer that checks for updates, and a user service that pauses your Core's email if its password stops working (see Configure).
|
||||
- **configure** adds more, only for what you set up: your Nextcloud files mount in /etc/fstab, GNOME Online Accounts, passwords in your GNOME keyring, and the backup timer.
|
||||
- **configure** adds more, only for what you set up: your Nextcloud files mount and network storage in /etc/fstab (SMB passwords in /etc/coredesktop/smb, SFTP mounts as user services named coredesktop-sftp-*), printers in CUPS, GNOME Online Accounts, passwords in your GNOME keyring, and the backup timer.
|
||||
|
||||
## Keep your System Up-to-date
|
||||
|
||||
@@ -226,11 +226,12 @@ version
|
||||
|
||||
```
|
||||
CoreDesktop 2026.10.22 (c5f0651)
|
||||
Kernel 7.2.11-arch1-1
|
||||
Kernel 7.2.11-arch1-1, 7.2.13 is available (run update)
|
||||
Video driver Mesa 26.2.6
|
||||
Last update 2026-10-22 13:43
|
||||
```
|
||||
|
||||
The kernel comes from Arch, so it's shown separately. If a newer kernel is waiting for a reboot, "version" says so. It also tells you if a newer version of CoreDesktop is available.
|
||||
The kernel and video driver (NVIDIA's if it's installed, otherwise Mesa, which drives Intel and AMD graphics) come from Arch, so they're shown separately. "version" tells you if Arch has a newer kernel or video driver, or if a newer version of CoreDesktop is available; "update" installs them. If a newer kernel is waiting for a reboot, it says that too.
|
||||
|
||||
You don't have to check by hand: a few minutes after you log in, and every few hours after that, CoreDesktop checks for a new version and shows a notification if there is one. Click it to run "update" in a terminal, or dismiss it. Once dismissed, you won't be notified again until there's an even newer version.
|
||||
|
||||
@@ -264,6 +265,9 @@ It covers:
|
||||
- **Tally for Plausible**, if your main Core runs Plausible Analytics: it's pointed at plausible.example.com.
|
||||
- Signing in to Claude Code, opencode and Hermes Agent and choosing their model providers.
|
||||
- **Languages** to install with mise, such as Ruby on Rails (see Mise above).
|
||||
- **Printers** on your network. It lists the printers it finds, whether they're reached directly or shared by another computer, and adds the ones you choose as printers on this computer, without drivers (IPP Everywhere), so they're in every print dialog. A printer another computer shares that can also be reached directly is marked, and not chosen unless you pick it, so printing doesn't depend on that computer being on. Printers already added, e.g. in GNOME Settings, are left alone. To choose without being asked, set a default printer or try it first, run `~/.local/share/coredesktop/system/setup-printers.sh --help`.
|
||||
- **Network storage**: shared folders from other machines on your network, mounted in /mnt and added to the Files sidebar. It finds SMB servers (Windows, Samba and NAS boxes, and Macs with File Sharing on) and machines you can ssh to, or you enter an address (`smb://nas.local/files`, `sftp://me@server.local/srv/data`). An SMB share is in /etc/fstab and mounts the first time you open its folder, so a server that's off doesn't hold anything up; its password is kept in /etc/coredesktop/smb, readable only by root. An SFTP folder is mounted by sshfs as you, at login, with the SSH keys in your Bitwarden or GNOME SSH agent, so the server has to accept one of them (it can't ask for a password). Agents in the sandbox can read these folders, and change one only if you start the agent in it. "Remove one" unmounts it and takes it back out.
|
||||
- **AI servers** on your network, such as Ollama on a Mac. It finds the machines on your network over mDNS and asks each one on the ports LLM servers use (11434 Ollama, 1234 LM Studio, 8080 llama.cpp, 8000 vLLM), since LLM servers don't announce themselves; or you enter a server's address. For the server you choose, it lets the sandboxed agents reach it (that port only), adds it to opencode with its models, offers to make it Hermes' model, and lets the commands Claude Code runs reach it too. Claude Code itself keeps using Anthropic's models. A server has to accept connections from other machines: in Ollama's settings, turn on "Expose Ollama to the network"; in LM Studio, "Serve on Local Network". "Remove a server" takes it back out of all of them.
|
||||
|
||||
To set up one thing, name it, e.g. `configure hermes`; `configure --list` shows the names.
|
||||
|
||||
@@ -317,6 +321,8 @@ If an agent needs a site or folder the sandbox blocks, add it in `~/.config/core
|
||||
}
|
||||
```
|
||||
|
||||
A server on your local network, such as an LLM server, is added the same way, with its port: `"llm.local:11434"` (`configure ai-servers` does this for you). Use its `.local` name or its IP address.
|
||||
|
||||
The sandbox is Anthropic's [sandbox runtime](https://github.com/anthropics/sandbox-runtime), which uses bubblewrap and socat. The default rules are in [sandbox/config/](sandbox/config/).
|
||||
|
||||
## Open ports
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
#!/bin/bash
|
||||
|
||||
# configure reads TITLE and DESCRIPTION
|
||||
# shellcheck disable=SC2034
|
||||
TITLE="AI servers"
|
||||
# shellcheck disable=SC2034
|
||||
DESCRIPTION="Use LLM servers on your network, e.g. Ollama"
|
||||
|
||||
# The servers you've added, one host:port per line. It exists once you've run
|
||||
# this, even if you added none, so this isn't "to do" on machines without one.
|
||||
AI_SERVERS_CONF="$HOME/.config/coredesktop/ai-servers"
|
||||
# The sandbox config every sandboxed agent reads (see sandbox/agent)
|
||||
SANDBOX_CONF="$HOME/.config/coredesktop/sandbox/base.json"
|
||||
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
||||
OPENCODE_CONF="$HOME/.config/opencode/opencode.json"
|
||||
source "$COREDESKTOP/configure.d/lib/mdns.sh"
|
||||
|
||||
# Ports LLM servers listen on by default, and who uses each. All of them answer
|
||||
# OpenAI's API at /v1, which is how every agent here talks to them.
|
||||
LLM_PORTS=(11434 1234 8080 8000)
|
||||
declare -A LLM_PORT_NAMES=([11434]=Ollama [1234]="LM Studio" [8080]=llama.cpp [8000]=vLLM)
|
||||
|
||||
is_installed() {
|
||||
command -v avahi-browse
|
||||
}
|
||||
|
||||
is_configured() {
|
||||
[ -f "$AI_SERVERS_CONF" ]
|
||||
}
|
||||
|
||||
# The models a server at host:port offers, one per line; fails if it doesn't
|
||||
# answer like an LLM server
|
||||
server_models() {
|
||||
curl -sf -m 3 "http://$1/v1/models" | jq -er '.data[].id' 2> /dev/null
|
||||
}
|
||||
|
||||
# What the server is, e.g. Ollama, for its name in the agents' settings
|
||||
server_kind() {
|
||||
local port="${1##*:}"
|
||||
if curl -sf -m 3 "http://$1/api/version" > /dev/null; then
|
||||
echo Ollama
|
||||
else
|
||||
echo "${LLM_PORT_NAMES[$port]:-LLM server}"
|
||||
fi
|
||||
}
|
||||
|
||||
# The LLM servers on the network: "host:port|models" per line. mDNS finds the
|
||||
# machines, but LLM servers don't announce themselves on it, so each machine
|
||||
# is asked on the usual ports.
|
||||
find_servers() {
|
||||
local dir host port
|
||||
dir="$(mktemp -d)" || return 1
|
||||
for host in $(mdns_hosts); do
|
||||
for port in "${LLM_PORTS[@]}"; do
|
||||
(server_models "$host:$port" | wc -l > "$dir/$host:$port") &
|
||||
done
|
||||
done
|
||||
wait
|
||||
for host in "$dir"/*; do
|
||||
[ -f "$host" ] && [ "$(cat "$host")" -gt 0 ] && echo "$(basename "$host")|$(cat "$host")"
|
||||
done
|
||||
rm -rf "$dir"
|
||||
}
|
||||
|
||||
# Edit a JSON file in place with jq, creating it from $2 if it's missing.
|
||||
# Leaves the file alone if it isn't valid JSON or jq fails.
|
||||
edit_json() {
|
||||
local file="$1" empty="$2" tmp
|
||||
shift 2
|
||||
mkdir -p "$(dirname "$file")" || return 1
|
||||
[ -f "$file" ] || echo "$empty" > "$file"
|
||||
tmp="$(mktemp "$file.XXXXXX")" || return 1
|
||||
if jq "$@" "$file" > "$tmp"; then
|
||||
mv -f "$tmp" "$file"
|
||||
else
|
||||
echo "Couldn't change $file; is it valid JSON?"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Let every sandboxed agent (opencode, Hermes, Cline) reach host:port, and
|
||||
# only that port
|
||||
sandbox_allow() {
|
||||
edit_json "$SANDBOX_CONF" '{}' --arg server "$1" \
|
||||
'.network.allowedDomains = ((.network.allowedDomains // []) - [$server] + [$server])'
|
||||
}
|
||||
|
||||
sandbox_disallow() {
|
||||
[ -f "$SANDBOX_CONF" ] || return 0
|
||||
edit_json "$SANDBOX_CONF" '{}' --arg server "$1" \
|
||||
'.network.allowedDomains -= [$server]'
|
||||
}
|
||||
|
||||
# Claude Code's sandbox only covers the commands it runs, e.g. curl, not its
|
||||
# own connection to Anthropic. Its list replaces the defaults sandbox/setup.sh
|
||||
# gives it, so if it has none yet, it starts from those.
|
||||
claude_allow() {
|
||||
local defaults
|
||||
[ -f "$CLAUDE_SETTINGS" ] || return 0
|
||||
defaults="$(jq '.network.allowedDomains' "$COREDESKTOP/sandbox/config/base.json")" || return 1
|
||||
edit_json "$CLAUDE_SETTINGS" '{}' --arg server "$1" --argjson defaults "$defaults" \
|
||||
'.sandbox.network.allowedDomains = ((.sandbox.network.allowedDomains // $defaults) - [$server] + [$server])'
|
||||
}
|
||||
|
||||
claude_disallow() {
|
||||
[ -f "$CLAUDE_SETTINGS" ] || return 0
|
||||
edit_json "$CLAUDE_SETTINGS" '{}' --arg server "$1" \
|
||||
'if .sandbox.network.allowedDomains then .sandbox.network.allowedDomains -= [$server] else . end'
|
||||
}
|
||||
|
||||
# opencode's name for the server: llm.local:11434 -> llm-11434
|
||||
opencode_provider() {
|
||||
local id="${1%%:*}"
|
||||
id="${id%.local}-${1##*:}"
|
||||
echo "${id//[^a-zA-Z0-9-]/-}" | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
# Add the server to opencode as a provider with its models, and make $3 the
|
||||
# model opencode starts with if you say so
|
||||
opencode_add() {
|
||||
local server="$1" kind="$2" model="$3" id models
|
||||
id="$(opencode_provider "$server")"
|
||||
models="$(server_models "$server" | jq -R '{(.): {name: .}}' | jq -s 'add')" || return 1
|
||||
edit_json "$OPENCODE_CONF" '{"$schema": "https://opencode.ai/config.json"}' \
|
||||
--arg id "$id" --arg name "$kind on ${server%%:*}" --arg url "http://$server/v1" \
|
||||
--argjson models "$models" \
|
||||
'.provider[$id] = {npm: "@ai-sdk/openai-compatible", name: $name, options: {baseURL: $url}, models: $models}' \
|
||||
|| return 1
|
||||
echo "opencode: added $kind on ${server%%:*} (choose its models with /models)."
|
||||
if confirm_yes "Start opencode with $model?"; then
|
||||
edit_json "$OPENCODE_CONF" '{}' --arg model "$id/$model" '.model = $model' || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
opencode_remove() {
|
||||
local id
|
||||
[ -f "$OPENCODE_CONF" ] || return 0
|
||||
id="$(opencode_provider "$1")"
|
||||
edit_json "$OPENCODE_CONF" '{}' --arg id "$id" \
|
||||
'del(.provider[$id]) | if (.model // "") | startswith($id + "/") then del(.model) else . end'
|
||||
}
|
||||
|
||||
# Hermes uses one model at a time, so ask before replacing the one it has
|
||||
hermes_use() {
|
||||
local server="$1" model="$2"
|
||||
confirm_yes "Use $model on ${server%%:*} for Hermes?" || return 0
|
||||
unsandboxed hermes config set model.provider custom > /dev/null \
|
||||
&& unsandboxed hermes config set model.base_url "http://$server/v1" > /dev/null \
|
||||
&& unsandboxed hermes config set model.default "$model" > /dev/null || return 1
|
||||
echo "Hermes: now uses $model."
|
||||
}
|
||||
|
||||
# Hermes' gateway reads the sandbox config when it starts, so it can't reach a
|
||||
# new server until it restarts. (A sandboxed opencode starts its own server
|
||||
# each time, so it reads the config again then.)
|
||||
restart_gateway() {
|
||||
if systemctl --user is-active -q hermes-gateway 2> /dev/null \
|
||||
&& confirm_yes "Restart Hermes' gateway so it can reach the server?"; then
|
||||
systemctl --user restart hermes-gateway || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
saved_servers() {
|
||||
grep -v '^$' "$AI_SERVERS_CONF" 2> /dev/null
|
||||
}
|
||||
|
||||
# Choose a model from the server and set the agents up to use it
|
||||
add_server() {
|
||||
local server="$1" kind models model choice i
|
||||
mapfile -t models < <(server_models "$server")
|
||||
if [ ${#models[@]} -eq 0 ]; then
|
||||
echo "$server doesn't answer like an LLM server (http://$server/v1/models)."
|
||||
return 1
|
||||
fi
|
||||
kind="$(server_kind "$server")"
|
||||
|
||||
echo
|
||||
echo "$kind on $server has these models:"
|
||||
for i in "${!models[@]}"; do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${models[$i]}"
|
||||
done
|
||||
read -rp "Which should the agents use? [1] " choice
|
||||
choice="${choice:-1}"
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#models[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
model="${models[$((choice - 1))]}"
|
||||
|
||||
sandbox_allow "$server" || return 1
|
||||
claude_allow "$server" || return 1
|
||||
grep -qsxF "$server" "$AI_SERVERS_CONF" || echo "$server" >> "$AI_SERVERS_CONF"
|
||||
echo "The sandbox lets the agents reach $server."
|
||||
|
||||
if [ -x /usr/bin/opencode ]; then
|
||||
opencode_add "$server" "$kind" "$model" || return 1
|
||||
fi
|
||||
if [ -x "$HOME/.local/bin/hermes" ]; then
|
||||
hermes_use "$server" "$model" || return 1
|
||||
fi
|
||||
restart_gateway
|
||||
}
|
||||
|
||||
remove_server() {
|
||||
local server="$1" tmp
|
||||
sandbox_disallow "$server" || return 1
|
||||
claude_disallow "$server" || return 1
|
||||
opencode_remove "$server" || return 1
|
||||
tmp="$(mktemp)" || return 1
|
||||
grep -vxF "$server" "$AI_SERVERS_CONF" > "$tmp"
|
||||
mv -f "$tmp" "$AI_SERVERS_CONF"
|
||||
echo "The agents can no longer reach $server."
|
||||
if grep -qsF "$server" "$HOME/.hermes/config.yaml"; then
|
||||
echo "Hermes still uses it; choose another model provider with: configure hermes"
|
||||
fi
|
||||
}
|
||||
|
||||
# Find servers on the network and add the one you choose
|
||||
find_and_add() {
|
||||
local servers server models choice i
|
||||
echo "Looking for LLM servers on your network..."
|
||||
mapfile -t servers < <(find_servers)
|
||||
if [ ${#servers[@]} -eq 0 ]; then
|
||||
echo "No LLM servers found. A server has to accept connections from other machines:"
|
||||
echo "in Ollama's settings, turn on \"Expose Ollama to the network\" (or set OLLAMA_HOST=0.0.0.0);"
|
||||
echo "in LM Studio, turn on \"Serve on Local Network\". Or enter its address instead."
|
||||
return 0
|
||||
fi
|
||||
for i in "${!servers[@]}"; do
|
||||
IFS='|' read -r server models <<< "${servers[$i]}"
|
||||
printf ' %2d) %-28s %s, %s models\n' $((i + 1)) "$server" "$(server_kind "$server")" "$models"
|
||||
done
|
||||
read -rp "Which server? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#servers[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
add_server "${servers[$((choice - 1))]%%|*}"
|
||||
}
|
||||
|
||||
# host, host:port or a URL such as http://host:port/v1 -> host:port
|
||||
enter_and_add() {
|
||||
local address
|
||||
read -rp "The server's address, e.g. llm.local or llm.local:11434: " address
|
||||
address="${address#*://}"
|
||||
address="${address%%/*}"
|
||||
[ -n "$address" ] || return 0
|
||||
[[ "$address" == *:* ]] || address="$address:11434"
|
||||
add_server "$address"
|
||||
}
|
||||
|
||||
choose_and_remove() {
|
||||
local servers choice i
|
||||
mapfile -t servers < <(saved_servers)
|
||||
for i in "${!servers[@]}"; do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${servers[$i]}"
|
||||
done
|
||||
read -rp "Which server? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#servers[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
remove_server "${servers[$((choice - 1))]}"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
local choice
|
||||
mkdir -p "$(dirname "$AI_SERVERS_CONF")" && touch "$AI_SERVERS_CONF" || return 1
|
||||
echo "Agents run in a sandbox that only lets them reach the servers you allow. This finds"
|
||||
echo "LLM servers on your network, such as Ollama on a Mac, allows them and sets up the agents."
|
||||
|
||||
while true; do
|
||||
echo
|
||||
if [ -s "$AI_SERVERS_CONF" ]; then
|
||||
echo "The agents can reach: $(saved_servers | paste -sd' ')"
|
||||
echo
|
||||
fi
|
||||
echo " 1) Find servers on your network"
|
||||
echo " 2) Enter a server's address"
|
||||
[ -s "$AI_SERVERS_CONF" ] && echo " 3) Remove a server"
|
||||
echo " q) Done"
|
||||
echo
|
||||
read -rp "Choose: " choice || return 0
|
||||
case "$choice" in
|
||||
1) find_and_add ;;
|
||||
2) enter_and_add ;;
|
||||
3) [ -s "$AI_SERVERS_CONF" ] && choose_and_remove ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# configure reads TITLE and DESCRIPTION
|
||||
# shellcheck disable=SC2034
|
||||
TITLE="Printers"
|
||||
# shellcheck disable=SC2034
|
||||
DESCRIPTION="Add your network's printers to every print dialog"
|
||||
|
||||
# Exists once you've run this, even if you added none, so this isn't "to do"
|
||||
# on machines without a printer
|
||||
PRINTERS_CONF="$HOME/.config/coredesktop/printers"
|
||||
|
||||
is_installed() {
|
||||
command -v lpadmin
|
||||
}
|
||||
|
||||
# Set up here, or a network printer was already added some other way
|
||||
is_configured() {
|
||||
[ -f "$PRINTERS_CONF" ] || LC_ALL=C lpstat -v 2> /dev/null | grep -qE ': (ipps?|dnssd)://'
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
bash "$COREDESKTOP/system/setup-printers.sh" || return 1
|
||||
mkdir -p "$(dirname "$PRINTERS_CONF")" && touch "$PRINTERS_CONF"
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
#!/bin/bash
|
||||
|
||||
# configure reads TITLE and DESCRIPTION
|
||||
# shellcheck disable=SC2034
|
||||
TITLE="Network storage"
|
||||
# shellcheck disable=SC2034
|
||||
DESCRIPTION="Mount shared folders from your network in /mnt"
|
||||
|
||||
# What's mounted, one "kind|source|mount point" per line, e.g.
|
||||
# smb|//pi-star.local/files|/mnt/pi-star-files. It exists once you've run
|
||||
# this, even if you mounted nothing, so this isn't "to do" without a server.
|
||||
STORAGE_CONF="$HOME/.config/coredesktop/storage"
|
||||
# Passwords for SMB shares, readable only by root, which mounts them
|
||||
SMB_CREDENTIALS="/etc/coredesktop/smb"
|
||||
BOOKMARKS="$HOME/.config/gtk-3.0/bookmarks"
|
||||
source "$COREDESKTOP/configure.d/lib/mdns.sh"
|
||||
|
||||
is_installed() {
|
||||
command -v avahi-browse
|
||||
}
|
||||
|
||||
is_configured() {
|
||||
[ -f "$STORAGE_CONF" ]
|
||||
}
|
||||
|
||||
# A name usable in a path: "Pi-Star Files" -> pi-star-files
|
||||
slug() {
|
||||
local s
|
||||
s="$(tr '[:upper:]' '[:lower:]' <<< "$1" | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')"
|
||||
echo "${s:-share}"
|
||||
}
|
||||
|
||||
# /mnt/<host>[-<name>], with -2, -3... if that's taken
|
||||
mount_point() {
|
||||
local base n=2 point
|
||||
base="/mnt/$(slug "${1%.local}")"
|
||||
[ -n "$2" ] && base="$base-$(slug "$2")"
|
||||
point="$base"
|
||||
while [ -e "$point" ] || grep -qsF "|$point" "$STORAGE_CONF"; do
|
||||
point="$base-$n"
|
||||
n=$((n + 1))
|
||||
done
|
||||
echo "$point"
|
||||
}
|
||||
|
||||
# --- SMB: Windows, Samba (e.g. a NAS) and Mac file sharing --------------------
|
||||
|
||||
# The disk shares on an SMB server, one per line, leaving out admin shares
|
||||
# like C$. smbclient reads the password from PASSWD, not the command line.
|
||||
smb_shares() {
|
||||
local host="$1" user="$2" password="$3" auth=(-N)
|
||||
[ -n "$user" ] && auth=(-U "$user")
|
||||
PASSWD="$password" timeout 20 smbclient -L "//$host" "${auth[@]}" -g 2> /dev/null \
|
||||
| awk -F'|' '$1 == "Disk" && $2 !~ /\$$/ { print $2 }'
|
||||
}
|
||||
|
||||
# The /etc/fstab line: root mounts the share the first time something opens
|
||||
# the folder (x-systemd.automount), as you, and unmounts it after 10 idle
|
||||
# minutes. nofail and the timeout keep a missing server from holding up boot
|
||||
# or hanging whatever opened the folder. Spaces are \040 in fstab.
|
||||
smb_fstab_line() {
|
||||
local source="${1// /\\040}" point="$2" credentials="$3" auth
|
||||
if [ -n "$credentials" ]; then auth="credentials=$credentials"; else auth="guest"; fi
|
||||
echo "$source $point cifs $auth,uid=$(id -u),gid=$(id -g),iocharset=utf8,nofail,_netdev,x-systemd.automount,x-systemd.idle-timeout=600,x-systemd.mount-timeout=20 0 0"
|
||||
}
|
||||
|
||||
add_smb() {
|
||||
local host="$1" share="$2" user password shares point source credentials="" choice i
|
||||
echo "Sign in to $host. Leave the user name empty for a share anyone can open."
|
||||
read -rp "User name: " user
|
||||
password=""
|
||||
if [ -n "$user" ]; then
|
||||
read -rsp "Password: " password
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -z "$share" ]; then
|
||||
mapfile -t shares < <(smb_shares "$host" "$user" "$password")
|
||||
if [ ${#shares[@]} -eq 0 ]; then
|
||||
echo "$host didn't list any shares for ${user:-guests} (wrong password, or it doesn't list them)."
|
||||
read -rp "The share's name, if you know it (Enter to stop): " share
|
||||
[ -n "$share" ] || return 1
|
||||
else
|
||||
for i in "${!shares[@]}"; do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${shares[$i]}"
|
||||
done
|
||||
read -rp "Which share? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#shares[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
share="${shares[$((choice - 1))]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
source="//$host/$share"
|
||||
point="$(mount_point "$host" "$share")"
|
||||
if [ -n "$user" ]; then
|
||||
credentials="$SMB_CREDENTIALS/$(basename "$point")"
|
||||
printf 'username=%s\npassword=%s\n' "$user" "$password" \
|
||||
| sudo install -D -m 600 -o root -g root /dev/stdin "$credentials" || return 1
|
||||
fi
|
||||
sudo install -d "$point" || return 1
|
||||
smb_fstab_line "$source" "$point" "$credentials" | sudo tee -a /etc/fstab > /dev/null || return 1
|
||||
sudo systemctl daemon-reload || return 1
|
||||
sudo systemctl restart "$(systemd-escape -p --suffix=automount "$point")" || return 1
|
||||
finish_add smb "$source" "$point" "$share on ${host%.local}"
|
||||
}
|
||||
|
||||
remove_smb() {
|
||||
local point="$1" tmp
|
||||
sudo systemctl stop "$(systemd-escape -p --suffix=automount "$point")" \
|
||||
"$(systemd-escape -p --suffix=mount "$point")" 2> /dev/null
|
||||
tmp="$(mktemp)" || return 1
|
||||
awk -v point="$point" '!($2 == point && $3 == "cifs")' /etc/fstab > "$tmp"
|
||||
sudo cp /etc/fstab /etc/fstab.bak && sudo install -m 644 "$tmp" /etc/fstab || { rm -f "$tmp"; return 1; }
|
||||
rm -f "$tmp"
|
||||
sudo systemctl daemon-reload
|
||||
sudo rm -f "$SMB_CREDENTIALS/$(basename "$point")"
|
||||
sudo rmdir "$point" 2> /dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- SFTP: any machine you can ssh to --------------------------------------------
|
||||
|
||||
sftp_unit_name() {
|
||||
echo "coredesktop-sftp-$(basename "$1").service"
|
||||
}
|
||||
|
||||
# systemd would expand $ and % in the unit's ExecStart
|
||||
sftp_unit() {
|
||||
local source="$1" point="$2"
|
||||
cat << EOF
|
||||
# Written by CoreDesktop (configure storage)
|
||||
[Unit]
|
||||
Description=Mount $source at $point
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=%h/.local/share/coredesktop/system/sftp-mount "$source" "$point"
|
||||
ExecStop=/usr/bin/fusermount3 -u "$point"
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
}
|
||||
|
||||
add_sftp() {
|
||||
local host="$1" user="${2:-$USER}" path="$3" source point name unit
|
||||
if ! command -v sshfs > /dev/null; then
|
||||
echo "Installing sshfs..."
|
||||
sudo pacman -S --needed --noconfirm sshfs || return 1
|
||||
fi
|
||||
read -rp "User name on $host [$user]: " name
|
||||
user="${name:-$user}"
|
||||
if [ -z "$path" ]; then
|
||||
read -rp "Folder on $host (Enter for your home folder): " path
|
||||
fi
|
||||
if [[ "$user$host$path" == *[\$%\"\\]* ]]; then
|
||||
echo "The user name and folder can't contain \$, %, \" or \\."
|
||||
return 1
|
||||
fi
|
||||
source="$user@$host:$path"
|
||||
|
||||
# Accepts the server's key the first time; the mount can't ask
|
||||
echo "Checking that $host accepts one of your SSH keys..."
|
||||
if [ -S "$HOME/.bitwarden-ssh-agent.sock" ]; then
|
||||
export SSH_AUTH_SOCK="$HOME/.bitwarden-ssh-agent.sock"
|
||||
fi
|
||||
ssh -o ConnectTimeout=10 -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no \
|
||||
"$user@$host" true || {
|
||||
echo "$host didn't accept any of your SSH keys, and the mount can't ask for a password."
|
||||
echo "Add your key to $host first (e.g. ssh-copy-id $user@$host), then try again."
|
||||
return 1
|
||||
}
|
||||
|
||||
point="$(mount_point "$host" "$(basename "${path:-}")")"
|
||||
unit="$(sftp_unit_name "$point")"
|
||||
sudo install -d -o "$USER" -g "$(id -gn)" "$point" || return 1
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
sftp_unit "$source" "$point" > "$HOME/.config/systemd/user/$unit" || return 1
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now "$unit" || return 1
|
||||
finish_add sftp "$source" "$point" "${host%.local}${path:+ $(basename "$path")}"
|
||||
}
|
||||
|
||||
remove_sftp() {
|
||||
local point="$1" unit
|
||||
unit="$(sftp_unit_name "$point")"
|
||||
systemctl --user disable --now "$unit" 2> /dev/null
|
||||
rm -f "$HOME/.config/systemd/user/$unit"
|
||||
systemctl --user daemon-reload
|
||||
sudo rmdir "$point" 2> /dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Both ------------------------------------------------------------------------
|
||||
|
||||
# Record it, add it to the Files sidebar and check it opens
|
||||
finish_add() {
|
||||
local kind="$1" source="$2" point="$3" label="$4"
|
||||
echo "$kind|$source|$point" >> "$STORAGE_CONF"
|
||||
mkdir -p "$(dirname "$BOOKMARKS")"
|
||||
grep -qs "^file://$point " "$BOOKMARKS" || echo "file://$point $label" >> "$BOOKMARKS"
|
||||
if timeout 30 ls "$point" > /dev/null 2>&1 && mountpoint -q "$point"; then
|
||||
echo "$source is at $point (\"$label\" in the Files sidebar)."
|
||||
else
|
||||
echo "$source was added at $point, but didn't open yet. If it doesn't, see:"
|
||||
if [ "$kind" = smb ]; then
|
||||
echo " journalctl -b -u '$(systemd-escape -p --suffix=mount "$point")'"
|
||||
else
|
||||
echo " journalctl --user -u $(sftp_unit_name "$point")"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
remove_storage() {
|
||||
local kind="$1" point="$3" tmp
|
||||
"remove_$kind" "$point" || return 1
|
||||
tmp="$(mktemp)" || return 1
|
||||
grep -vF "|$point" "$STORAGE_CONF" > "$tmp"
|
||||
mv -f "$tmp" "$STORAGE_CONF"
|
||||
if [ -f "$BOOKMARKS" ]; then
|
||||
grep -v "^file://$point " "$BOOKMARKS" > "$tmp"
|
||||
mv -f "$tmp" "$BOOKMARKS"
|
||||
fi
|
||||
echo "$point is no longer mounted."
|
||||
}
|
||||
|
||||
# The file servers on the network: "kind|host|name" per line
|
||||
find_storage() {
|
||||
local own name type host
|
||||
own="$(mdns_own_host)"
|
||||
mdns_services | while IFS='|' read -r name type host _; do
|
||||
[[ "${host,,}" == "${own,,}" ]] && continue
|
||||
case "$type" in
|
||||
_smb._tcp) echo "smb|$host|$name" ;;
|
||||
_sftp-ssh._tcp) echo "sftp|$host|$name" ;;
|
||||
esac
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
find_and_add() {
|
||||
local found kind host name choice i
|
||||
echo "Looking for shared folders on your network..."
|
||||
mapfile -t found < <(find_storage)
|
||||
if [ ${#found[@]} -eq 0 ]; then
|
||||
echo "Nothing found. A Mac shares folders over SMB once File Sharing is on (System Settings >"
|
||||
echo "General > Sharing). Or enter the address instead."
|
||||
return 0
|
||||
fi
|
||||
for i in "${!found[@]}"; do
|
||||
IFS='|' read -r kind host name <<< "${found[$i]}"
|
||||
printf ' %2d) %-26s %s\n' $((i + 1)) "$host" "$( [ "$kind" = smb ] && echo "SMB shared folders" || echo "SFTP (over SSH)")"
|
||||
done
|
||||
read -rp "Which? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#found[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
IFS='|' read -r kind host _ <<< "${found[$((choice - 1))]}"
|
||||
"add_$kind" "$host"
|
||||
}
|
||||
|
||||
# smb://host/share or sftp://[user@]host[/path]
|
||||
enter_and_add() {
|
||||
local address rest host user="" path
|
||||
read -rp "Address, e.g. smb://nas.local/files or sftp://me@server.local/srv/data: " address
|
||||
rest="${address#*://}"
|
||||
case "$address" in
|
||||
smb://*/?*) add_smb "${rest%%/*}" "${rest#*/}" ;;
|
||||
sftp://*)
|
||||
[[ "$rest" == *@* ]] && { user="${rest%%@*}"; rest="${rest#*@}"; }
|
||||
host="${rest%%/*}"
|
||||
path=""
|
||||
[[ "$rest" == */* ]] && path="/${rest#*/}"
|
||||
add_sftp "$host" "$user" "$path" ;;
|
||||
"") return 0 ;;
|
||||
*) echo "Start it with smb:// (and include the share) or sftp://."; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
choose_and_remove() {
|
||||
local entries choice i
|
||||
mapfile -t entries < <(grep -v '^$' "$STORAGE_CONF")
|
||||
for i in "${!entries[@]}"; do
|
||||
IFS='|' read -r _ source point <<< "${entries[$i]}"
|
||||
printf ' %2d) %-30s %s\n' $((i + 1)) "$point" "$source"
|
||||
done
|
||||
read -rp "Which? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#entries[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
IFS='|' read -r -a entry <<< "${entries[$((choice - 1))]}"
|
||||
remove_storage "${entry[@]}"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
local choice source point
|
||||
mkdir -p "$(dirname "$STORAGE_CONF")" && touch "$STORAGE_CONF" || return 1
|
||||
echo "This mounts shared folders from other machines on your network in /mnt: SMB shares"
|
||||
echo "(Windows, a NAS, a Mac with File Sharing on) and, over SSH, any machine you can ssh to."
|
||||
|
||||
while true; do
|
||||
echo
|
||||
if [ -s "$STORAGE_CONF" ]; then
|
||||
while IFS='|' read -r _ source point; do
|
||||
echo " $point <- $source"
|
||||
done < "$STORAGE_CONF"
|
||||
echo
|
||||
fi
|
||||
echo " 1) Find shared folders on your network"
|
||||
echo " 2) Enter an address"
|
||||
[ -s "$STORAGE_CONF" ] && echo " 3) Remove one"
|
||||
echo " q) Done"
|
||||
echo
|
||||
read -rp "Choose: " choice || return 0
|
||||
case "$choice" in
|
||||
1) find_and_add ;;
|
||||
2) enter_and_add ;;
|
||||
3) [ -s "$STORAGE_CONF" ] && choose_and_remove ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Find what's on the local network over mDNS (Avahi), for the
|
||||
# configure.d items that set up servers, printers and storage on it.
|
||||
# Source this file.
|
||||
|
||||
# Every service Avahi can see, or only those of type $1 (e.g. _ipp._tcp), one
|
||||
# per line after waiting a few seconds for answers:
|
||||
# "name|type|host|address|port|txt". IPv4 only, so a machine isn't listed once
|
||||
# per address family. The name has Avahi's escapes decoded.
|
||||
mdns_services() {
|
||||
local name type host address port txt
|
||||
timeout 15 avahi-browse "${1:---all}" --resolve --terminate --parsable --no-db-lookup 2> /dev/null \
|
||||
| awk -F';' '$1 == "=" && $3 == "IPv4"' \
|
||||
| while IFS=';' read -r _ _ _ name type _ host address port txt; do
|
||||
printf '%s|%s|%s|%s|%s|%s\n' "$(mdns_unescape "$name")" "$type" "$host" "$address" "$port" "$txt"
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
# Avahi writes a space as \032 and a dot in a name as \. (decimal escapes)
|
||||
mdns_unescape() {
|
||||
local s="$1" out="" ch code escape='^([^\\]*)\\([0-9][0-9][0-9]|.)(.*)$'
|
||||
while [[ "$s" =~ $escape ]]; do
|
||||
out+="${BASH_REMATCH[1]}"
|
||||
code="${BASH_REMATCH[2]}"
|
||||
s="${BASH_REMATCH[3]}"
|
||||
if [[ "$code" == [0-9][0-9][0-9] ]]; then
|
||||
# shellcheck disable=SC2059
|
||||
printf -v ch "\\$(printf %03o "$((10#$code))")"
|
||||
out+="$ch"
|
||||
else
|
||||
out+="$code"
|
||||
fi
|
||||
done
|
||||
echo "$out$s"
|
||||
}
|
||||
|
||||
# This machine's own .local name, which mdns_hosts leaves out
|
||||
mdns_own_host() {
|
||||
echo "$(hostnamectl hostname 2> /dev/null || cat /etc/hostname).local"
|
||||
}
|
||||
|
||||
# The other machines on the network: one .local name per line
|
||||
mdns_hosts() {
|
||||
local own
|
||||
own="$(mdns_own_host)"
|
||||
mdns_services | cut -d'|' -f3 | grep -vixF "$own" | sort -u
|
||||
}
|
||||
+11
-2
@@ -133,9 +133,18 @@ done < <(jq -rs "$MERGE | .filesystem.allowWrite[]? | strings" "${ours[@]}")
|
||||
# temp files then fail, and GTK aborts. /tmp is writable in every profile.
|
||||
export CLAUDE_CODE_TMPDIR=/tmp
|
||||
|
||||
# srt tells the agent to connect directly to private addresses (192.168.x.x
|
||||
# and so on), but the sandbox has no network to do it with, so a server on
|
||||
# your local network that the config allows by IP, e.g. an Ollama server from
|
||||
# `configure ai-servers`, couldn't be reached. Only loopback is direct here;
|
||||
# everything else goes through the proxy, which still allows only the config's
|
||||
# domains and addresses.
|
||||
NO_PROXY_DIRECT=localhost,127.0.0.1,::1
|
||||
direct=(env "NO_PROXY=$NO_PROXY_DIRECT" "no_proxy=$NO_PROXY_DIRECT")
|
||||
|
||||
# Hermes' terminal reaches its gateway, in another sandbox, through a relay
|
||||
case "$profile" in
|
||||
hermes) exec "$srt" --settings "$settings" -- bash "$SANDBOX/hermes-bridge" "$real" "$@" ;;
|
||||
hermes) exec "$srt" --settings "$settings" -- "${direct[@]}" bash "$SANDBOX/hermes-bridge" "$real" "$@" ;;
|
||||
esac
|
||||
exec "$srt" --settings "$settings" -- "$real" "$@"
|
||||
exec "$srt" --settings "$settings" -- "${direct[@]}" "$real" "$@"
|
||||
}
|
||||
|
||||
Executable
+404
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Purpose : Find the printers on the network and add them as permanent CUPS
|
||||
# queues, so they show up in every print dialog. `configure printers`
|
||||
# runs this; run it yourself for the options below (--help).
|
||||
#
|
||||
# Printers announce themselves over mDNS, and CUPS prints to them through
|
||||
# temporary queues that only exist while in use. GNOME Settings and Chromium
|
||||
# browsers list those, but the GTK print dialog ("Print using system
|
||||
# dialog...") often lists only permanent queues, so a printer can show up in
|
||||
# Settings and the browser but not there. `lpstat -v` then says "No
|
||||
# destinations added" while `lpstat -e` lists the printers. A permanent queue
|
||||
# made with `lpadmin -m everywhere` (driverless IPP Everywhere) fixes that.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COREDESKTOP="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/.." && pwd)"
|
||||
source "$COREDESKTOP/configure.d/lib/mdns.sh"
|
||||
|
||||
TEST_PAGE=/usr/share/cups/data/default-testpage.pdf
|
||||
|
||||
usage() {
|
||||
cat << 'EOF'
|
||||
Usage: setup-printers.sh [options]
|
||||
|
||||
Finds the printers on your network and adds the ones you choose as permanent
|
||||
CUPS queues, so they show up in every print dialog.
|
||||
|
||||
--all Add every printer found, without asking (a printer another
|
||||
computer shares is left out when it can be reached directly)
|
||||
--default NAME Make queue NAME the default printer afterwards
|
||||
--dry-run Show the commands that would change the system; run none
|
||||
--no-deps Don't install packages or turn on services
|
||||
--test-page Print a test page on each queue added
|
||||
-h, --help Show this help
|
||||
|
||||
If a printer is still missing from a print dialog:
|
||||
- Quit the app completely (browsers especially) and open it again; print
|
||||
dialogs read the list of printers when they start.
|
||||
- GTK needs its CUPS print backend: pacman -Ql gtk3 gtk4 | grep printbackend
|
||||
If only the "file" backend is there: sudo pacman -S libcups gtk3 gtk4
|
||||
- Flatpak apps print through the portal, which needs xdg-desktop-portal and
|
||||
xdg-desktop-portal-gnome; the app may also need:
|
||||
flatpak override --user --socket=cups <app id>
|
||||
- $CUPS_SERVER or ~/.cups/client.conf can point apps at another CUPS server
|
||||
than this computer's.
|
||||
- To see what the dialog does: GTK_DEBUG=printing <app>, while watching
|
||||
journalctl -fu cups
|
||||
- cups-browsed can add queues for the printers it finds by itself, instead
|
||||
of this script; this one adds only the queues you choose.
|
||||
EOF
|
||||
}
|
||||
|
||||
DRY_RUN=false ALL=false NO_DEPS=false PRINT_TEST=false DEFAULT=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--all) ALL=true ;;
|
||||
--default)
|
||||
[ $# -ge 2 ] || { echo "--default needs a queue name." >&2; exit 2; }
|
||||
DEFAULT="$2"
|
||||
shift ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--no-deps) NO_DEPS=true ;;
|
||||
--test-page) PRINT_TEST=true ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1 (see --help)" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Run a command that changes the system, or with --dry-run only show it
|
||||
run() {
|
||||
if $DRY_RUN; then
|
||||
printf ' would run:'
|
||||
printf ' %q' "$@"
|
||||
echo
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 1: packages and services ------------------------------------------
|
||||
|
||||
setup_dependencies() {
|
||||
local pkg service line new backup missing=() stopped=()
|
||||
|
||||
for pkg in cups avahi nss-mdns; do
|
||||
pacman -Q "$pkg" &> /dev/null || missing+=("$pkg")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo "Installing ${missing[*]}..."
|
||||
run sudo pacman -S --needed --noconfirm "${missing[@]}"
|
||||
fi
|
||||
|
||||
for service in cups.service avahi-daemon.service; do
|
||||
if ! systemctl is-enabled -q "$service" 2> /dev/null || ! systemctl is-active -q "$service"; then
|
||||
stopped+=("$service")
|
||||
fi
|
||||
done
|
||||
if [ ${#stopped[@]} -gt 0 ]; then
|
||||
echo "Turning on ${stopped[*]}..."
|
||||
run sudo systemctl enable --now "${stopped[@]}"
|
||||
fi
|
||||
|
||||
# Without mdns_minimal, the .local names in printers' addresses don't resolve
|
||||
line="$(grep -m1 '^hosts:' /etc/nsswitch.conf || true)"
|
||||
if [[ "$line" != *"mdns_minimal [NOTFOUND=return]"* ]]; then
|
||||
if [[ "$line" == *" resolve"* ]]; then
|
||||
new="${line/ resolve/ mdns_minimal [NOTFOUND=return] resolve}"
|
||||
elif [[ "$line" == *" dns"* ]]; then
|
||||
new="${line/ dns/ mdns_minimal [NOTFOUND=return] dns}"
|
||||
else
|
||||
new="${line:-hosts:} mdns_minimal [NOTFOUND=return]"
|
||||
fi
|
||||
backup="/etc/nsswitch.conf.bak.$(date +%Y%m%d%H%M%S)"
|
||||
echo "Letting .local names resolve, in /etc/nsswitch.conf (a copy is kept in $backup):"
|
||||
echo " before: $line"
|
||||
echo " after: $new"
|
||||
run sudo cp -p /etc/nsswitch.conf "$backup"
|
||||
if [ -n "$line" ]; then
|
||||
run sudo sed -i "s|^hosts:.*|$new|" /etc/nsswitch.conf
|
||||
else
|
||||
echo "$new" | run sudo tee -a /etc/nsswitch.conf > /dev/null
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
$NO_DEPS || setup_dependencies
|
||||
|
||||
if ! LC_ALL=C lpstat -r 2> /dev/null | grep -q "scheduler is running"; then
|
||||
echo "CUPS isn't running, so printers can't be found or added." >&2
|
||||
echo "Start it with: sudo systemctl enable --now cups.service" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Step 2: find printers ----------------------------------------------------
|
||||
|
||||
# %20 -> space, %40 -> @ and any other percent escape
|
||||
url_decode() {
|
||||
local s="${1//\\/\\\\}"
|
||||
printf '%b' "${s//%/\\x}"
|
||||
}
|
||||
|
||||
# Printers can take a few seconds to answer
|
||||
echo "Looking for printers..."
|
||||
found=""
|
||||
for attempt in 1 2 3; do
|
||||
found="$(LC_ALL=C lpinfo --include-schemes dnssd -v 2> /dev/null | grep '^network dnssd://' || true)"
|
||||
[ -n "$found" ] && break
|
||||
[ "$attempt" -lt 3 ] && sleep 3
|
||||
done
|
||||
if [ -z "$found" ]; then
|
||||
echo "No printers found on the network. Is the printer on, and on the same network?"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# What Avahi knows about each printer, by name: its host, address, port and
|
||||
# path, for spotting queues that already reach it and for the ipp:// fallback
|
||||
declare -A AVAHI_HOST=() AVAHI_ADDR=() AVAHI_PORT=() AVAHI_RP=()
|
||||
while IFS='|' read -r name _ host address port txt; do
|
||||
[ -n "$name" ] || continue
|
||||
AVAHI_HOST["$name"]="$host" AVAHI_ADDR["$name"]="$address" AVAHI_PORT["$name"]="$port"
|
||||
if [[ "$txt" =~ \"rp=([^\"]*)\" ]]; then
|
||||
AVAHI_RP["$name"]="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
done < <(mdns_services _ipp._tcp || true)
|
||||
|
||||
# One entry per printer found: P_NAME is its name as shown, P_BASE the name
|
||||
# without the "@ host" of a shared printer, P_SHARER that host
|
||||
P_NAME=() P_BASE=() P_URI=() P_SHARER=()
|
||||
dnssd='^dnssd://([^/]+)\._ipps?\._tcp\.[^/]*(/[^?]*)'
|
||||
while read -r _ uri; do
|
||||
[[ "$uri" =~ $dnssd ]] || continue
|
||||
name="$(url_decode "${BASH_REMATCH[1]}")"
|
||||
path="${BASH_REMATCH[2]}"
|
||||
sharer=""
|
||||
# Another computer's CUPS shares it, as "<printer> @ <computer>"
|
||||
if [[ "$path" == /cups* && "$name" == *" @ "* ]]; then
|
||||
sharer="${name##* @ }"
|
||||
fi
|
||||
P_NAME+=("$name") P_BASE+=("${name% @ *}") P_URI+=("$uri") P_SHARER+=("$sharer")
|
||||
done <<< "$found"
|
||||
[ "${#P_NAME[@]}" -gt 0 ] || { echo "No printers found on the network."; exit 0; }
|
||||
|
||||
# A shared printer that can also be reached directly: the direct one is used,
|
||||
# so printing doesn't depend on the other computer being on
|
||||
P_DUP=()
|
||||
for i in "${!P_NAME[@]}"; do
|
||||
P_DUP[i]=""
|
||||
[ -n "${P_SHARER[i]}" ] || continue
|
||||
for j in "${!P_NAME[@]}"; do
|
||||
if [ -z "${P_SHARER[j]}" ] && [ "${P_NAME[j]}" = "${P_BASE[i]}" ]; then
|
||||
P_DUP[i]="$j"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# --- Step 3: queue names ------------------------------------------------------
|
||||
|
||||
# The queues there are now: name -> device URI
|
||||
declare -A QUEUE_URI=()
|
||||
while read -r _ _ queue uri; do
|
||||
QUEUE_URI["${queue%:}"]="$uri"
|
||||
done < <(LC_ALL=C lpstat -v 2> /dev/null | grep '^device for ' || true)
|
||||
|
||||
lower() {
|
||||
tr '[:upper:]' '[:lower:]' <<< "${1%.}"
|
||||
}
|
||||
|
||||
# The queue that already prints to printer $1, if any: one with the same URI,
|
||||
# or one made some other way (e.g. GNOME Settings) that reaches the same host
|
||||
# and path, like ipp://NPI3BE90A.local:631/ipp/print
|
||||
existing_queue() {
|
||||
local i="$1" name="${P_NAME[$1]}" queue uri host rp
|
||||
host="$(lower "${AVAHI_HOST[$name]:-}")"
|
||||
rp="${AVAHI_RP[$name]:-}"
|
||||
for queue in "${!QUEUE_URI[@]}"; do
|
||||
uri="${QUEUE_URI[$queue]}"
|
||||
if [ "$uri" = "${P_URI[i]}" ]; then
|
||||
echo "$queue"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$host" ] && [[ "$uri" =~ ^ipps?://([^/:]+)(:[0-9]+)?/(.*)$ ]]; then
|
||||
local uri_host uri_path="${BASH_REMATCH[3]}"
|
||||
uri_host="$(lower "${BASH_REMATCH[1]}")"
|
||||
if { [ "$uri_host" = "$host" ] || [ "$uri_host" = "${AVAHI_ADDR[$name]:-}" ]; } \
|
||||
&& [ "$uri_path" = "$rp" ]; then
|
||||
echo "$queue"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# "HP LaserJet MFP M234dw (3BE90A) @ lab" -> HP_LaserJet_MFP_M234dw_3BE90A_lab
|
||||
queue_name() {
|
||||
local q="${1// @ /_}"
|
||||
q="$(sed -E 's/[^A-Za-z0-9_-]/_/g; s/_+/_/g; s/^_+//; s/_+$//' <<< "$q")"
|
||||
echo "${q:0:127}"
|
||||
}
|
||||
|
||||
# A name no other queue has, adding _2, _3... if needed
|
||||
declare -A TAKEN=()
|
||||
for queue in "${!QUEUE_URI[@]}"; do TAKEN["$queue"]=1; done
|
||||
unique_queue_name() {
|
||||
local base n=2 name
|
||||
base="$(queue_name "$1")"
|
||||
name="$base"
|
||||
while [ -n "${TAKEN[$name]:-}" ]; do
|
||||
name="${base:0:120}_$n"
|
||||
n=$((n + 1))
|
||||
done
|
||||
TAKEN["$name"]=1
|
||||
echo "$name"
|
||||
}
|
||||
|
||||
P_EXISTING=()
|
||||
for i in "${!P_NAME[@]}"; do
|
||||
P_EXISTING[i]="$(existing_queue "$i" || true)"
|
||||
done
|
||||
|
||||
# --- Choose ---------------------------------------------------------------------
|
||||
|
||||
# Chosen unless told otherwise: printers without a queue that aren't a shared
|
||||
# copy of a direct one
|
||||
defaults=()
|
||||
for i in "${!P_NAME[@]}"; do
|
||||
if [ -z "${P_EXISTING[i]}" ] && [ -z "${P_DUP[i]}" ]; then
|
||||
defaults+=("$((i + 1))")
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Printers on your network:"
|
||||
for i in "${!P_NAME[@]}"; do
|
||||
if [ -n "${P_SHARER[i]}" ]; then how="shared by ${P_SHARER[i]}"; else how="direct"; fi
|
||||
note=""
|
||||
if [ -n "${P_EXISTING[i]}" ]; then
|
||||
note="already set up as ${P_EXISTING[i]}"
|
||||
elif [ -n "${P_DUP[i]}" ]; then
|
||||
note="same printer as $((P_DUP[i] + 1)), reached directly"
|
||||
fi
|
||||
printf ' %2d) %-46s %-20s %s\n' "$((i + 1))" "${P_NAME[i]}" "$how" "$note"
|
||||
done
|
||||
echo
|
||||
|
||||
chosen=()
|
||||
if $ALL; then
|
||||
chosen=("${defaults[@]}")
|
||||
else
|
||||
answer=""
|
||||
if [ ${#defaults[@]} -gt 0 ]; then
|
||||
read -rp "Add which printers? Numbers separated by spaces [${defaults[*]}], or n for none: " answer || answer=n
|
||||
answer="${answer:-${defaults[*]}}"
|
||||
else
|
||||
read -rp "Every printer is already set up. Add one anyway? Number, or Enter for none: " answer || answer=""
|
||||
fi
|
||||
for n in ${answer//,/ }; do
|
||||
[[ "$n" == [nN]* ]] && { chosen=(); break; }
|
||||
if [[ "$n" =~ ^[0-9]+$ ]] && (( n >= 1 && n <= ${#P_NAME[@]} )); then
|
||||
chosen+=("$n")
|
||||
else
|
||||
echo "Skipping \"$n\": not a number from the list."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Step 4: add queues ---------------------------------------------------------
|
||||
|
||||
ADDED=() SKIPPED=() FAILED=()
|
||||
|
||||
add_queue() {
|
||||
local i="$1" name="${P_NAME[$1]}" queue out fallback=""
|
||||
if [ -n "${P_EXISTING[i]}" ]; then
|
||||
SKIPPED+=("$name (already set up as ${P_EXISTING[i]})")
|
||||
return 0
|
||||
fi
|
||||
queue="$(unique_queue_name "$name")"
|
||||
echo "Adding $name as $queue..."
|
||||
if $DRY_RUN; then
|
||||
run sudo lpadmin -p "$queue" -E -v "${P_URI[i]}" -m everywhere
|
||||
ADDED+=("$queue")
|
||||
return 0
|
||||
fi
|
||||
|
||||
# lpadmin warns that printer drivers are deprecated; only its status counts
|
||||
if out="$(sudo lpadmin -p "$queue" -E -v "${P_URI[i]}" -m everywhere 2>&1)"; then
|
||||
ADDED+=("$queue")
|
||||
return 0
|
||||
fi
|
||||
|
||||
# The printer's address and path from Avahi instead of its mDNS name
|
||||
if [ -n "${AVAHI_ADDR[$name]:-}" ]; then
|
||||
fallback="ipp://${AVAHI_ADDR[$name]}:${AVAHI_PORT[$name]:-631}/${AVAHI_RP[$name]:-ipp/print}"
|
||||
echo " That didn't work; trying $fallback"
|
||||
if out="$(sudo lpadmin -p "$queue" -E -v "$fallback" -m everywhere 2>&1)"; then
|
||||
ADDED+=("$queue")
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
unset 'TAKEN[$queue]'
|
||||
FAILED+=("$name: $(tail -n1 <<< "$out")")
|
||||
}
|
||||
|
||||
for n in "${chosen[@]}"; do
|
||||
add_queue "$((n - 1))"
|
||||
done
|
||||
|
||||
# --- Step 5: default, check and summary ---------------------------------------
|
||||
|
||||
if [ -z "$DEFAULT" ] && ! $ALL && ! $DRY_RUN && [ ${#ADDED[@]} -gt 0 ]; then
|
||||
current="$(LC_ALL=C lpstat -d 2> /dev/null | sed -n 's/^system default destination: //p')"
|
||||
read -rp "Make one of the new printers the default? ${ADDED[*]} (Enter to leave it${current:+ as $current}): " DEFAULT || DEFAULT=""
|
||||
fi
|
||||
if [ -n "$DEFAULT" ]; then
|
||||
if [ -n "${QUEUE_URI[$DEFAULT]:-}" ] || [[ " ${ADDED[*]} " == *" $DEFAULT "* ]]; then
|
||||
run sudo lpadmin -d "$DEFAULT"
|
||||
else
|
||||
FAILED+=("default printer: there's no queue named $DEFAULT")
|
||||
DEFAULT=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! $DRY_RUN; then
|
||||
ready=()
|
||||
for queue in "${ADDED[@]}"; do
|
||||
if LC_ALL=C lpstat -a "$queue" 2> /dev/null | grep -q "accepting requests"; then
|
||||
ready+=("$queue")
|
||||
if $PRINT_TEST; then
|
||||
lp -d "$queue" "$TEST_PAGE" > /dev/null || FAILED+=("$queue: the test page wasn't sent")
|
||||
fi
|
||||
else
|
||||
FAILED+=("$queue: added, but it isn't accepting jobs (see lpstat -t)")
|
||||
fi
|
||||
done
|
||||
ADDED=("${ready[@]}")
|
||||
fi
|
||||
|
||||
echo
|
||||
if $DRY_RUN; then echo "Would add:"; else echo "Added:"; fi
|
||||
[ ${#ADDED[@]} -gt 0 ] && printf ' %s\n' "${ADDED[@]}" || echo " none"
|
||||
if [ ${#SKIPPED[@]} -gt 0 ]; then
|
||||
echo "Skipped:"
|
||||
printf ' %s\n' "${SKIPPED[@]}"
|
||||
fi
|
||||
if [ ${#FAILED[@]} -gt 0 ]; then
|
||||
echo "Failed:"
|
||||
printf ' %s\n' "${FAILED[@]}"
|
||||
echo "A printer that can't be added without a driver needs its maker's: hplip for HP,"
|
||||
echo "brother-* from the AUR for Brother. This script doesn't install drivers."
|
||||
fi
|
||||
[ -n "$DEFAULT" ] && echo "Default printer: $DEFAULT"
|
||||
if [ ${#ADDED[@]} -gt 0 ] && ! $DRY_RUN; then
|
||||
$PRINT_TEST && echo "A test page was sent to each new printer."
|
||||
echo "Quit and reopen apps that are open (browsers especially) to see the new printers."
|
||||
fi
|
||||
|
||||
# Without its CUPS backend, the GTK dialog can't list printers at all
|
||||
if pacman -Q gtk3 &> /dev/null && ! pacman -Ql gtk3 | grep -q 'printbackend-cups'; then
|
||||
echo "GTK's CUPS print backend is missing; reinstall it: sudo pacman -S libcups gtk3 gtk4"
|
||||
fi
|
||||
|
||||
[ ${#FAILED[@]} -eq 0 ]
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Mount a folder from another machine over SFTP (sshfs) and stay in
|
||||
# the foreground until it's unmounted. A systemd user service that
|
||||
# `configure storage` writes runs it at login.
|
||||
#
|
||||
# It runs as you, not root, so it can use your SSH keys: Bitwarden's SSH agent
|
||||
# while Bitwarden is running it, as in your .bashrc, otherwise GNOME's. It
|
||||
# never asks for a password, so the server has to accept one of those keys.
|
||||
#
|
||||
# Usage: sftp-mount <user@host:path> <mount point>
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "Usage: sftp-mount <user@host:path> <mount point>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ -S "$HOME/.bitwarden-ssh-agent.sock" ]; then
|
||||
export SSH_AUTH_SOCK="$HOME/.bitwarden-ssh-agent.sock"
|
||||
elif [ -S "${XDG_RUNTIME_DIR:-/run/user/$UID}/gcr/ssh" ]; then
|
||||
export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR:-/run/user/$UID}/gcr/ssh"
|
||||
fi
|
||||
|
||||
# A mount left behind by a crash would make sshfs fail
|
||||
mountpoint -q "$2" && fusermount3 -u "$2"
|
||||
|
||||
# reconnect and ServerAlive* bring it back after the network drops (e.g.
|
||||
# suspend); idmap=user shows your files on the server as yours here
|
||||
exec sshfs -f "$1" "$2" \
|
||||
-o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 \
|
||||
-o BatchMode=yes,ConnectTimeout=15,idmap=user
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
# The mDNS helper and the configure items built on it (ai-servers, storage),
|
||||
# with fake avahi-browse, curl and smbclient. Nothing here touches the network.
|
||||
|
||||
setup() {
|
||||
common_setup
|
||||
export COREDESKTOP="$REPO"
|
||||
AVAHI="$BATS_TEST_TMPDIR/avahi"
|
||||
# What avahi-browse --parsable prints, including this machine (basement),
|
||||
# an IPv6 copy of a service, and an unresolved (+) line
|
||||
cat > "$AVAHI" << 'EOF'
|
||||
+;enp5s0;IPv4;llm;_ssh._tcp;local
|
||||
=;enp5s0;IPv4;llm;_ssh._tcp;local;llm.local;192.168.0.5;22;
|
||||
=;enp5s0;IPv6;llm;_ssh._tcp;local;llm.local;fe80::1;22;
|
||||
=;enp5s0;IPv4;llm;_sftp-ssh._tcp;local;llm.local;192.168.0.5;22;
|
||||
=;enp5s0;IPv4;basement;_ssh._tcp;local;basement.local;192.168.0.106;22;
|
||||
=;enp5s0;IPv4;PI-STAR;_smb._tcp;local;pi-star.local;192.168.0.102;445;
|
||||
=;enp5s0;IPv4;HP\032LaserJet\032MFP\032M234dw\032\0403BE90A\041\032\064\032lab;_ipp._tcp;local;lab.local;192.168.0.22;631;"rp=printers/HP"
|
||||
EOF
|
||||
export AVAHI
|
||||
stub avahi-browse 'cat "$AVAHI"'
|
||||
stub hostnamectl 'echo basement'
|
||||
|
||||
confirm_yes() { return 0; }
|
||||
confirm() { return 0; }
|
||||
unsandboxed() { echo "unsandboxed $*" >> "$STUB_LOG"; }
|
||||
export -f confirm_yes confirm unsandboxed
|
||||
}
|
||||
|
||||
# --- lib/mdns.sh --------------------------------------------------------------------
|
||||
|
||||
@test "mdns_unescape decodes Avahi's decimal escapes" {
|
||||
source "$REPO/configure.d/lib/mdns.sh"
|
||||
[ "$(mdns_unescape 'HP\032LaserJet\032\0403BE90A\041\032\064\032lab')" = "HP LaserJet (3BE90A) @ lab" ]
|
||||
[ "$(mdns_unescape 'a\.b')" = "a.b" ]
|
||||
[ "$(mdns_unescape plain)" = "plain" ]
|
||||
}
|
||||
|
||||
@test "mdns_services lists each resolved IPv4 service once, with its name decoded" {
|
||||
source "$REPO/configure.d/lib/mdns.sh"
|
||||
run mdns_services
|
||||
[ "$status" -eq 0 ]
|
||||
[ "${#lines[@]}" -eq 5 ]
|
||||
[[ "$output" == *"HP LaserJet MFP M234dw (3BE90A) @ lab|_ipp._tcp|lab.local|192.168.0.22|631|\"rp=printers/HP\""* ]]
|
||||
[[ "$output" != *"fe80"* ]]
|
||||
}
|
||||
|
||||
@test "mdns_hosts lists the other machines, not this one" {
|
||||
source "$REPO/configure.d/lib/mdns.sh"
|
||||
run mdns_hosts
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "$(printf 'lab.local\nllm.local\npi-star.local')" ]
|
||||
}
|
||||
|
||||
# --- configure ai-servers --------------------------------------------------------------
|
||||
|
||||
setup_ai() {
|
||||
source "$REPO/configure.d/50-ai-servers.sh"
|
||||
# llm.local:11434 is Ollama with two models; nothing else answers
|
||||
stub curl 'case "$*" in
|
||||
*http://llm.local:11434/v1/models*) echo "{\"data\": [{\"id\": \"qwen3-coder:30b\"}, {\"id\": \"gemma4:12b\"}]}" ;;
|
||||
*http://llm.local:11434/api/version*) echo "{\"version\": \"0.14.0\"}" ;;
|
||||
*) exit 7 ;;
|
||||
esac'
|
||||
}
|
||||
|
||||
@test "find_servers asks each machine on the LLM ports and lists the ones that answer" {
|
||||
setup_ai
|
||||
run find_servers
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "llm.local:11434|2" ]
|
||||
[ "$(server_kind llm.local:11434)" = Ollama ]
|
||||
}
|
||||
|
||||
@test "sandbox_allow adds a server once, keeping what's there" {
|
||||
setup_ai
|
||||
mkdir -p "$(dirname "$SANDBOX_CONF")"
|
||||
echo '{"network": {"allowedDomains": ["docs.example.com"]}, "filesystem": {"allowWrite": ["~/notes"]}}' > "$SANDBOX_CONF"
|
||||
sandbox_allow llm.local:11434
|
||||
sandbox_allow llm.local:11434
|
||||
[ "$(jq -c '.network.allowedDomains' "$SANDBOX_CONF")" = '["docs.example.com","llm.local:11434"]' ]
|
||||
[ "$(jq -c '.filesystem.allowWrite' "$SANDBOX_CONF")" = '["~/notes"]' ]
|
||||
sandbox_disallow llm.local:11434
|
||||
[ "$(jq -c '.network.allowedDomains' "$SANDBOX_CONF")" = '["docs.example.com"]' ]
|
||||
}
|
||||
|
||||
@test "sandbox_allow leaves a file that isn't valid JSON alone" {
|
||||
setup_ai
|
||||
mkdir -p "$(dirname "$SANDBOX_CONF")"
|
||||
echo 'not json' > "$SANDBOX_CONF"
|
||||
run sandbox_allow llm.local:11434
|
||||
[ "$status" -ne 0 ]
|
||||
[ "$(cat "$SANDBOX_CONF")" = "not json" ]
|
||||
}
|
||||
|
||||
@test "claude_allow starts from the default domains when Claude has no list, and keeps their order" {
|
||||
setup_ai
|
||||
mkdir -p "$HOME/.claude"
|
||||
echo '{"model": "opus"}' > "$CLAUDE_SETTINGS"
|
||||
claude_allow llm.local:11434
|
||||
[ "$(jq -c '.sandbox.network.allowedDomains[0:2]' "$CLAUDE_SETTINGS")" = '["anthropic.com","*.anthropic.com"]' ]
|
||||
[ "$(jq -r '.sandbox.network.allowedDomains[-1]' "$CLAUDE_SETTINGS")" = llm.local:11434 ]
|
||||
[ "$(jq -r '.model' "$CLAUDE_SETTINGS")" = opus ]
|
||||
claude_disallow llm.local:11434
|
||||
run jq -e '.sandbox.network.allowedDomains | index("llm.local:11434")' "$CLAUDE_SETTINGS"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "claude_allow does nothing without Claude's settings" {
|
||||
setup_ai
|
||||
claude_allow llm.local:11434
|
||||
[ ! -e "$CLAUDE_SETTINGS" ]
|
||||
}
|
||||
|
||||
@test "opencode_add adds the server's models as a provider, and opencode_remove takes it out" {
|
||||
setup_ai
|
||||
opencode_add llm.local:11434 Ollama gemma4:12b > /dev/null
|
||||
[ "$(opencode_provider llm.local:11434)" = llm-11434 ]
|
||||
[ "$(jq -r '.provider["llm-11434"].options.baseURL' "$OPENCODE_CONF")" = "http://llm.local:11434/v1" ]
|
||||
[ "$(jq -r '.provider["llm-11434"].name' "$OPENCODE_CONF")" = "Ollama on llm.local" ]
|
||||
[ "$(jq -c '.provider["llm-11434"].models | keys' "$OPENCODE_CONF")" = '["gemma4:12b","qwen3-coder:30b"]' ]
|
||||
[ "$(jq -r '.model' "$OPENCODE_CONF")" = "llm-11434/gemma4:12b" ]
|
||||
opencode_remove llm.local:11434
|
||||
[ "$(jq -c '.provider' "$OPENCODE_CONF")" = '{}' ]
|
||||
[ "$(jq -r '.model // "none"' "$OPENCODE_CONF")" = none ]
|
||||
}
|
||||
|
||||
@test "hermes_use points Hermes at the server's OpenAI API" {
|
||||
setup_ai
|
||||
hermes_use llm.local:11434 gemma4:12b
|
||||
called unsandboxed hermes config set model.provider custom
|
||||
called unsandboxed hermes config set model.base_url http://llm.local:11434/v1
|
||||
called unsandboxed hermes config set model.default gemma4:12b
|
||||
}
|
||||
|
||||
@test "add_server allows the server and remembers it; remove_server undoes it" {
|
||||
setup_ai
|
||||
mkdir -p "$(dirname "$AI_SERVERS_CONF")"
|
||||
touch "$AI_SERVERS_CONF"
|
||||
stub systemctl 'exit 3'
|
||||
run add_server llm.local:11434 <<< "2"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$(cat "$AI_SERVERS_CONF")" = llm.local:11434 ]
|
||||
jq -e '.network.allowedDomains | index("llm.local:11434")' "$SANDBOX_CONF"
|
||||
|
||||
run remove_server llm.local:11434
|
||||
[ "$status" -eq 0 ]
|
||||
[ ! -s "$AI_SERVERS_CONF" ]
|
||||
run jq -e '.network.allowedDomains | index("llm.local:11434")' "$SANDBOX_CONF"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "add_server refuses an address that isn't an LLM server" {
|
||||
setup_ai
|
||||
run add_server lab.local:11434 <<< "1"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"doesn't answer like an LLM server"* ]]
|
||||
[ ! -e "$SANDBOX_CONF" ]
|
||||
}
|
||||
|
||||
# --- configure storage -------------------------------------------------------------------
|
||||
|
||||
@test "find_storage lists SMB and SFTP servers, not this machine" {
|
||||
source "$REPO/configure.d/60-storage.sh"
|
||||
run find_storage
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "$(printf 'sftp|llm.local|llm\nsmb|pi-star.local|PI-STAR')" ]
|
||||
}
|
||||
|
||||
@test "smb_shares lists disk shares, without admin shares, and passes the password outside the arguments" {
|
||||
source "$REPO/configure.d/60-storage.sh"
|
||||
stub smbclient 'echo "PASSWD=$PASSWD" >> "$STUB_LOG"
|
||||
printf "Disk|files|Shared files\nDisk|C\$|Default share\nIPC|IPC\$|IPC Service\nDisk|My Music|\n"'
|
||||
run smb_shares nas.local me secret
|
||||
[ "$output" = "$(printf 'files\nMy Music')" ]
|
||||
called smbclient -L //nas.local -U me -g
|
||||
called PASSWD=secret
|
||||
}
|
||||
|
||||
@test "smb_fstab_line mounts as you on first use, with \\040 for spaces" {
|
||||
source "$REPO/configure.d/60-storage.sh"
|
||||
run smb_fstab_line "//nas.local/My Files" /mnt/nas-my-files /etc/coredesktop/smb/nas-my-files
|
||||
[[ "$output" == "//nas.local/My\\040Files /mnt/nas-my-files cifs credentials=/etc/coredesktop/smb/nas-my-files,uid=$(id -u),gid=$(id -g),"* ]]
|
||||
[[ "$output" == *",x-systemd.automount,"*" 0 0" ]]
|
||||
run smb_fstab_line //nas.local/pub /mnt/nas-pub ""
|
||||
[[ "$output" == *" cifs guest,"* ]]
|
||||
}
|
||||
|
||||
@test "mount_point names the folder after the host and share, avoiding ones in use" {
|
||||
source "$REPO/configure.d/60-storage.sh"
|
||||
[ "$(mount_point Bats-Test-NAS.local 'My Files')" = /mnt/bats-test-nas-my-files ]
|
||||
[ "$(mount_point bats-test-host.local '')" = /mnt/bats-test-host ]
|
||||
mkdir -p "$(dirname "$STORAGE_CONF")"
|
||||
echo "sftp|me@bats-test-host.local:|/mnt/bats-test-host" > "$STORAGE_CONF"
|
||||
[ "$(mount_point bats-test-host.local '')" = /mnt/bats-test-host-2 ]
|
||||
}
|
||||
|
||||
@test "sftp_unit runs sftp-mount as a user service" {
|
||||
source "$REPO/configure.d/60-storage.sh"
|
||||
run sftp_unit "me@brain.local:/srv/my data" /mnt/brain-my-data
|
||||
[[ "$output" == *'ExecStart=%h/.local/share/coredesktop/system/sftp-mount "me@brain.local:/srv/my data" "/mnt/brain-my-data"'* ]]
|
||||
[[ "$output" == *"WantedBy=default.target"* ]]
|
||||
[ "$(sftp_unit_name /mnt/brain-my-data)" = coredesktop-sftp-brain-my-data.service ]
|
||||
}
|
||||
|
||||
@test "enter_and_add reads smb:// and sftp:// addresses" {
|
||||
source "$REPO/configure.d/60-storage.sh"
|
||||
add_smb() { echo "smb [$1] [$2]"; }
|
||||
add_sftp() { echo "sftp [$1] [$2] [$3]"; }
|
||||
[ "$(enter_and_add <<< 'smb://nas.local/My Files' | tail -n1)" = "smb [nas.local] [My Files]" ]
|
||||
[ "$(enter_and_add <<< 'sftp://brain.local' | tail -n1)" = "sftp [brain.local] [] []" ]
|
||||
[ "$(enter_and_add <<< 'sftp://me@brain.local/srv/data' | tail -n1)" = "sftp [brain.local] [me] [/srv/data]" ]
|
||||
run enter_and_add <<< 'smb://nas.local'
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
# system/setup-printers.sh with fake CUPS and Avahi, set up like a network
|
||||
# with a printer reached directly (M234dw), the same printer shared by "lab",
|
||||
# and a USB printer shared by "gateway". $QUEUES is what lpstat -v lists.
|
||||
|
||||
M234_URI='dnssd://HP%20LaserJet%20MFP%20M234dw%20(3BE90A)._ipp._tcp.local/?uuid=abf23253'
|
||||
LAB_URI='dnssd://HP%20LaserJet%20MFP%20M234dw%20(3BE90A)%20%40%20lab._ipp._tcp.local/cups?uuid=c5d876d6'
|
||||
P1102_URI='dnssd://HP_LaserJet_Professional_P1102w%20%40%20gateway._ipp._tcp.local/cups?uuid=4da44ae6'
|
||||
|
||||
setup() {
|
||||
common_setup
|
||||
SCRIPT="$REPO/system/setup-printers.sh"
|
||||
export LPINFO="network $M234_URI
|
||||
network $LAB_URI
|
||||
network $P1102_URI"
|
||||
export QUEUES="" ADDED_QUEUES="$BATS_TEST_TMPDIR/added"
|
||||
: > "$ADDED_QUEUES"
|
||||
|
||||
AVAHI="$BATS_TEST_TMPDIR/avahi"
|
||||
cat > "$AVAHI" << 'EOF'
|
||||
=;enp5s0;IPv4;HP\032LaserJet\032MFP\032M234dw\032\0403BE90A\041;_ipp._tcp;local;NPI3BE90A.local;192.168.0.114;631;"txtvers=1" "rp=ipp/print"
|
||||
=;enp5s0;IPv4;HP\032LaserJet\032MFP\032M234dw\032\0403BE90A\041\032\064\032lab;_ipp._tcp;local;lab.local;192.168.0.22;631;"rp=printers/HP-LaserJet-MFP-M232-M237"
|
||||
=;enp5s0;IPv4;HP_LaserJet_Professional_P1102w\032\064\032gateway;_ipp._tcp;local;gateway.local;192.168.0.39;631;"rp=printers/HP_LaserJet_Professional_P1102w"
|
||||
EOF
|
||||
export AVAHI
|
||||
stub avahi-browse 'cat "$AVAHI"'
|
||||
stub hostnamectl 'echo basement'
|
||||
stub lpinfo 'echo "$LPINFO"'
|
||||
stub lpstat 'case "$1" in
|
||||
-r) echo "scheduler is running" ;;
|
||||
-v) [ -n "$QUEUES" ] || { echo "lpstat: No destinations added." >&2; exit 1; }
|
||||
echo "$QUEUES" ;;
|
||||
-a) grep -qx "$2" "$ADDED_QUEUES" && echo "$2 accepting requests since now" ;;
|
||||
-d) echo "no system default destination" ;;
|
||||
esac'
|
||||
# Fails for URIs matching $LPADMIN_FAIL; records the queues it adds
|
||||
stub lpadmin 'if [ "$1" = -p ]; then
|
||||
[ -n "$LPADMIN_FAIL" ] && [[ "$5" == $LPADMIN_FAIL ]] && { echo "lpadmin: Unable to connect" >&2; exit 1; }
|
||||
echo "$2" >> "$ADDED_QUEUES"
|
||||
fi'
|
||||
stub sudo '"$@"'
|
||||
stub pacman 'exit 1'
|
||||
}
|
||||
|
||||
@test "the dry run plans only the printer without a queue or a direct equivalent" {
|
||||
QUEUES="device for M234dw: ipp://NPI3BE90A.local:631/ipp/print"
|
||||
run bash "$SCRIPT" --no-deps --dry-run --all
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"HP LaserJet MFP M234dw (3BE90A)"*"direct"*"already set up as M234dw"* ]]
|
||||
[[ "$output" == *"HP LaserJet MFP M234dw (3BE90A) @ lab"*"shared by lab"*"same printer as 1, reached directly"* ]]
|
||||
[[ "$output" == *"would run: sudo lpadmin -p HP_LaserJet_Professional_P1102w_gateway -E -v"* ]]
|
||||
[[ "$output" == *"Would add:"$'\n'" HP_LaserJet_Professional_P1102w_gateway"* ]]
|
||||
not_called sudo
|
||||
not_called lpadmin
|
||||
}
|
||||
|
||||
@test "--all adds each printer once with IPP Everywhere, preferring direct ones" {
|
||||
run bash "$SCRIPT" --no-deps --all
|
||||
[ "$status" -eq 0 ]
|
||||
called lpadmin -p HP_LaserJet_MFP_M234dw_3BE90A -E -v "$M234_URI" -m everywhere
|
||||
called lpadmin -p HP_LaserJet_Professional_P1102w_gateway -E -v "$P1102_URI" -m everywhere
|
||||
[ "$(grep -c '^lpadmin -p' "$STUB_LOG")" -eq 2 ]
|
||||
[[ "$output" == *"Added:"$'\n'" HP_LaserJet_MFP_M234dw_3BE90A"$'\n'" HP_LaserJet_Professional_P1102w_gateway"* ]]
|
||||
}
|
||||
|
||||
@test "a second run adds nothing" {
|
||||
QUEUES="device for HP_LaserJet_MFP_M234dw_3BE90A: $M234_URI
|
||||
device for HP_LaserJet_Professional_P1102w_gateway: $P1102_URI"
|
||||
run bash "$SCRIPT" --no-deps --all
|
||||
[ "$status" -eq 0 ]
|
||||
not_called lpadmin
|
||||
[[ "$output" == *"Added:"$'\n'" none"* ]]
|
||||
}
|
||||
|
||||
@test "a printer can still be chosen through the computer sharing it" {
|
||||
run bash "$SCRIPT" --no-deps <<< "2"
|
||||
[ "$status" -eq 0 ]
|
||||
called lpadmin -p HP_LaserJet_MFP_M234dw_3BE90A_lab -E -v "$LAB_URI" -m everywhere
|
||||
[ "$(grep -c '^lpadmin -p' "$STUB_LOG")" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "n adds nothing" {
|
||||
run bash "$SCRIPT" --no-deps <<< "n"
|
||||
[ "$status" -eq 0 ]
|
||||
not_called lpadmin
|
||||
}
|
||||
|
||||
@test "a queue name in use for another printer gets a number" {
|
||||
QUEUES="device for HP_LaserJet_Professional_P1102w_gateway: ipp://elsewhere.local/ipp/print"
|
||||
run bash "$SCRIPT" --no-deps <<< "3"
|
||||
[ "$status" -eq 0 ]
|
||||
called lpadmin -p HP_LaserJet_Professional_P1102w_gateway_2 -E -v "$P1102_URI" -m everywhere
|
||||
}
|
||||
|
||||
@test "when the mDNS address fails, it tries the printer's IP address" {
|
||||
export LPADMIN_FAIL='dnssd://*'
|
||||
run bash "$SCRIPT" --no-deps <<< "3"
|
||||
[ "$status" -eq 0 ]
|
||||
called lpadmin -p HP_LaserJet_Professional_P1102w_gateway -E -v ipp://192.168.0.39:631/printers/HP_LaserJet_Professional_P1102w -m everywhere
|
||||
}
|
||||
|
||||
@test "a printer that can't be added is reported, and the rest are still added" {
|
||||
export LPADMIN_FAIL='*P1102w*'
|
||||
run bash "$SCRIPT" --no-deps --all
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Added:"$'\n'" HP_LaserJet_MFP_M234dw_3BE90A"* ]]
|
||||
[[ "$output" == *"Failed:"$'\n'" HP_LaserJet_Professional_P1102w @ gateway: lpadmin: Unable to connect"* ]]
|
||||
[[ "$output" == *"hplip"* ]]
|
||||
}
|
||||
|
||||
@test "--default makes a new queue the default" {
|
||||
run bash "$SCRIPT" --no-deps --all --default HP_LaserJet_MFP_M234dw_3BE90A
|
||||
[ "$status" -eq 0 ]
|
||||
called lpadmin -d HP_LaserJet_MFP_M234dw_3BE90A
|
||||
[[ "$output" == *"Default printer: HP_LaserJet_MFP_M234dw_3BE90A"* ]]
|
||||
}
|
||||
|
||||
@test "--default with a queue that doesn't exist fails without changing the default" {
|
||||
run bash "$SCRIPT" --no-deps --all --default Nope
|
||||
[ "$status" -eq 1 ]
|
||||
uncalled lpadmin -d Nope
|
||||
[[ "$output" == *"there's no queue named Nope"* ]]
|
||||
}
|
||||
|
||||
@test "it stops when CUPS isn't running" {
|
||||
stub lpstat 'echo "scheduler is not running"'
|
||||
run bash "$SCRIPT" --no-deps --all
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"CUPS isn't running"* ]]
|
||||
not_called lpinfo
|
||||
}
|
||||
|
||||
@test "it says so when no printers are found" {
|
||||
export LPINFO=""
|
||||
stub sleep 'exit 0'
|
||||
run bash "$SCRIPT" --no-deps --all
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"No printers found"* ]]
|
||||
[ "$(grep -c '^lpinfo ' "$STUB_LOG")" -eq 3 ]
|
||||
}
|
||||
|
||||
@test "unknown options are refused" {
|
||||
run bash "$SCRIPT" --bogus
|
||||
[ "$status" -eq 2 ]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
# sandbox/agent runs a fake agent through a fake srt, which sets NO_PROXY the
|
||||
# way the real one does, saves the settings it was given and runs the command.
|
||||
|
||||
setup() {
|
||||
common_setup
|
||||
SANDBOX="$HOME/.local/share/coredesktop/sandbox"
|
||||
mkdir -p "$SANDBOX/bin" "$SANDBOX/config" "$BATS_TEST_TMPDIR/project"
|
||||
cp "$REPO/sandbox/config/base.json" "$SANDBOX/config/"
|
||||
ln -s "$REPO/sandbox/agent" "$SANDBOX/bin/fakeagent"
|
||||
export XDG_RUNTIME_DIR="$BATS_TEST_TMPDIR/run"
|
||||
mkdir -p "$XDG_RUNTIME_DIR"
|
||||
export SETTINGS_COPY="$BATS_TEST_TMPDIR/settings.json"
|
||||
|
||||
stub srt 'cp "$2" "$SETTINGS_COPY"
|
||||
while [ "$1" != "--" ]; do shift; done
|
||||
shift
|
||||
export NO_PROXY=localhost,127.0.0.1,::1,10.0.0.0/8,192.168.0.0/16 no_proxy=localhost,192.168.0.0/16
|
||||
exec "$@"'
|
||||
stub fakeagent 'echo "NO_PROXY=$NO_PROXY no_proxy=$no_proxy args=$*"'
|
||||
cd "$BATS_TEST_TMPDIR/project"
|
||||
}
|
||||
|
||||
@test "an agent connects directly only to loopback, so allowed LAN addresses go through the proxy" {
|
||||
run "$SANDBOX/bin/fakeagent" one "two words"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "NO_PROXY=localhost,127.0.0.1,::1 no_proxy=localhost,127.0.0.1,::1 args=one two words" ]
|
||||
}
|
||||
|
||||
@test "a server allowed in your own base.json is in the agent's settings" {
|
||||
mkdir -p "$HOME/.config/coredesktop/sandbox"
|
||||
echo '{"network": {"allowedDomains": ["llm.local:11434"]}}' > "$HOME/.config/coredesktop/sandbox/base.json"
|
||||
run "$SANDBOX/bin/fakeagent"
|
||||
[ "$status" -eq 0 ]
|
||||
jq -e '.network.allowedDomains | index("llm.local:11434") and index("github.com")' "$SETTINGS_COPY"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
# version runs from a fake checkout with fake git, uname, pacman and
|
||||
# checkupdates. The running kernel's modules never exist here, so the kernel
|
||||
# line always also says a newer one is installed.
|
||||
|
||||
setup() {
|
||||
common_setup
|
||||
CD="$HOME/.local/share/coredesktop"
|
||||
mkdir -p "$CD"
|
||||
cp "$REPO/version" "$REPO/utils.sh" "$CD/"
|
||||
|
||||
stub git 'case "$3" in
|
||||
rev-parse) exit 0 ;;
|
||||
log) echo "2026.10.31 (6bc4fdc)" ;;
|
||||
fetch) exit 1 ;;
|
||||
esac'
|
||||
stub uname 'echo "${KERNEL:-9.9.1-arch1-1}"'
|
||||
# pacman -Q <package>: the versions in $INSTALLED, "name version" per line
|
||||
stub pacman 'grep "^$2 " <<< "$INSTALLED" || exit 1'
|
||||
stub checkupdates 'printf "%s" "$PENDING"; [ -n "$PENDING" ] || exit 2'
|
||||
export INSTALLED="mesa 1:26.2.6-1" PENDING=""
|
||||
}
|
||||
|
||||
@test "version shows the kernel and Mesa, with nothing newer to install" {
|
||||
run bash "$CD/version"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"CoreDesktop 2026.10.31 (6bc4fdc)"* ]]
|
||||
[[ "$output" == *"Kernel 9.9.1-arch1-1 (newer kernel installed, reboot to use it)"$'\n'* ]]
|
||||
[[ "$output" == *"Video driver Mesa 26.2.6"$'\n'* ]]
|
||||
[[ "$output" != *"is available"* ]]
|
||||
}
|
||||
|
||||
@test "version suggests update for a newer kernel and video driver" {
|
||||
export PENDING=$'linux 9.9.1.arch1-1 -> 9.9.2.arch1-1\nmesa 1:26.2.6-1 -> 1:26.3.0-1\n'
|
||||
run bash "$CD/version"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Kernel 9.9.1-arch1-1"*", 9.9.2 is available (run update)"* ]]
|
||||
[[ "$output" == *"Video driver Mesa 26.2.6, 26.3.0 is available (run update)"* ]]
|
||||
}
|
||||
|
||||
@test "version shows NVIDIA's driver over Mesa, and ignores a rebuild of the same version" {
|
||||
export INSTALLED=$'mesa 1:26.2.6-1\nnvidia-utils 615.78.08-1'
|
||||
export PENDING=$'nvidia-utils 615.78.08-1 -> 615.78.08-2\n'
|
||||
run bash "$CD/version"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Video driver NVIDIA 615.78.08"$'\n'* ]]
|
||||
[[ "$output" != *"is available"* ]]
|
||||
}
|
||||
|
||||
@test "version checks the kernel package that's running, e.g. linux-lts" {
|
||||
export KERNEL=6.12.50-1-lts
|
||||
export PENDING=$'linux 9.9.1.arch1-1 -> 9.9.2.arch1-1\nlinux-lts 6.12.50-1 -> 6.12.51-1\n'
|
||||
run bash "$CD/version"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"6.12.51 is available"* ]]
|
||||
[[ "$output" != *"9.9.2"* ]]
|
||||
}
|
||||
|
||||
@test "version leaves out what's available when it can't check" {
|
||||
rm "$STUB_BIN/checkupdates"
|
||||
stub checkupdates 'exit 1'
|
||||
export PENDING=$'linux 9.9.1.arch1-1 -> 9.9.2.arch1-1\n'
|
||||
run bash "$CD/version"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Kernel 9.9.1-arch1-1"* ]]
|
||||
[[ "$output" != *"is available"* ]]
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
#
|
||||
# CoreDesktop is a rolling release, so its version is the date of the commit
|
||||
# the local checkout is on, plus the short hash to tell same-day commits apart,
|
||||
# e.g. 2026.10.22 (c5f0651). The kernel is shown separately because it comes
|
||||
# from Arch, not CoreDesktop.
|
||||
# e.g. 2026.10.22 (c5f0651). The kernel and video driver are shown separately
|
||||
# because they come from Arch, not CoreDesktop, along with any newer release of
|
||||
# either that update would install.
|
||||
#
|
||||
# version --check prints the newer version and exits 0 if one is available,
|
||||
# exits 1 if this machine is up to date, or 2 if it couldn't check.
|
||||
@@ -50,11 +51,67 @@ if latest="$(newer_version)"; then
|
||||
version="$version, $latest is available (run update)"
|
||||
fi
|
||||
|
||||
# A version without its epoch and pkgrel, e.g. 1:26.2.6-1 is 26.2.6. Arch's
|
||||
# kernel puts its suffix in the version too (7.2.14.arch1), so drop that.
|
||||
upstream() {
|
||||
local v="${1#*:}"
|
||||
v="${v%-*}"
|
||||
echo "${v%.arch*}"
|
||||
}
|
||||
|
||||
# Packages with a newer version in the repos, as "name old -> new" lines.
|
||||
# checkupdates syncs a copy of the package database, so it needs no root and
|
||||
# doesn't leave a partial upgrade behind. Empty if it couldn't check.
|
||||
pending=""
|
||||
if command -v checkupdates &> /dev/null; then
|
||||
pending="$(timeout 30 checkupdates 2> /dev/null)"
|
||||
fi
|
||||
|
||||
# Print the newer upstream version of package $1 if the repos have one. A
|
||||
# rebuild of the same version (only the pkgrel changed) doesn't count.
|
||||
newer_package() {
|
||||
local name old _ new
|
||||
while read -r name old _ new; do
|
||||
[ "$name" = "$1" ] || continue
|
||||
[ "$(upstream "$old")" != "$(upstream "$new")" ] && upstream "$new"
|
||||
return
|
||||
done <<< "$pending"
|
||||
return 1
|
||||
}
|
||||
|
||||
kernel="$(uname -r)"
|
||||
# The package the running kernel came from (linux, linux-lts, ...)
|
||||
kernel_pkg="$(cat "/usr/lib/modules/$kernel/pkgbase" 2> /dev/null)"
|
||||
case "$kernel" in
|
||||
*-lts) kernel_pkg="${kernel_pkg:-linux-lts}" ;;
|
||||
*-zen) kernel_pkg="${kernel_pkg:-linux-zen}" ;;
|
||||
*-hardened) kernel_pkg="${kernel_pkg:-linux-hardened}" ;;
|
||||
*) kernel_pkg="${kernel_pkg:-linux}" ;;
|
||||
esac
|
||||
|
||||
# If the running kernel's modules are gone, a newer kernel was installed
|
||||
if [ ! -d "/usr/lib/modules/$kernel" ]; then
|
||||
kernel="$kernel (newer kernel installed, reboot to use it)"
|
||||
fi
|
||||
if latest="$(newer_package "$kernel_pkg")"; then
|
||||
kernel="$kernel, $latest is available (run update)"
|
||||
fi
|
||||
|
||||
# NVIDIA's driver if nvidia.sh installed it, otherwise Mesa, which drives
|
||||
# Intel and AMD graphics (vulkan-intel and vulkan-radeon are built with it)
|
||||
driver=""
|
||||
for pkg in nvidia-utils nvidia-580xx-utils mesa; do
|
||||
if installed="$(pacman -Q "$pkg" 2> /dev/null)"; then
|
||||
case "$pkg" in
|
||||
nvidia*) driver="NVIDIA $(upstream "${installed#* }")" ;;
|
||||
*) driver="Mesa $(upstream "${installed#* }")" ;;
|
||||
esac
|
||||
if latest="$(newer_package "$pkg")"; then
|
||||
driver="$driver, $latest is available (run update)"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 10-pacman.sh rewrites this log on every update
|
||||
if [ -f "$PACMAN_UPDATE_LOG" ]; then
|
||||
@@ -65,4 +122,5 @@ fi
|
||||
|
||||
echo "CoreDesktop $version"
|
||||
echo "Kernel $kernel"
|
||||
[ -n "$driver" ] && echo "Video driver $driver"
|
||||
echo "Last update $last_update"
|
||||
|
||||
Reference in New Issue
Block a user