aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rwxr-xr-xarchive/bridge-sync.sh.bak31
-rwxr-xr-xarchive/kvm-lab-create.bak110
-rwxr-xr-xarchive/vm-new58
-rwxr-xr-xarchive/vm-rm44
-rwxr-xr-xbackup_shield.sh135
-rwxr-xr-xbackup_usb.sh155
-rwxr-xr-xbashmount855
-rwxr-xr-xbookmarkthis61
-rwxr-xr-xborg-backup.sh24
-rwxr-xr-xbridge-sync88
-rwxr-xr-xcreate-lab206
-rwxr-xr-xdwm-status.sh2
-rwxr-xr-xfirefox4
-rwxr-xr-xfirefox-raw2
-rwxr-xr-xkvm-lab-create160
-rwxr-xr-xkvm-lab-create.bak160
-rwxr-xr-xkvm-lab-destroy52
-rwxr-xr-xkvm-lab-status67
-rwxr-xr-xkvm-promote-to-base87
m---------lib/bashsimplecurses0
-rwxr-xr-xluks-shield142
-rw-r--r--nextcloud1
-rwxr-xr-xofficium214
-rw-r--r--officium_readme.md135
-rwxr-xr-xpassmenu35
-rwxr-xr-xpassmenu-otp24
-rwxr-xr-xproton-bridge-cert-refresh39
-rwxr-xr-xreset-gpt17
l---------rexi1
-rwxr-xr-xrofi-vim-power26
-rwxr-xr-xsharepoint-mount40
-rwxr-xr-xsharepoint-status18
-rwxr-xr-xsharepoint-umount33
-rwxr-xr-xtypebookmarks5
-rwxr-xr-xusb-safe132
l---------yt-dlp1
37 files changed, 3166 insertions, 1 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0f1a254
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/bashsimplecurses"]
+ path = lib/bashsimplecurses
+ url = https://github.com/metal3d/bashsimplecurses
diff --git a/archive/bridge-sync.sh.bak b/archive/bridge-sync.sh.bak
new file mode 100755
index 0000000..7aecb75
--- /dev/null
+++ b/archive/bridge-sync.sh.bak
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CERT_FILE="$HOME/.config/isync/proton-bridge.pem"
+
+echo "[1] Restart Bridge"
+systemctl --user restart protonmail-bridge.service
+
+echo "[2] Wait for Bridge IMAP port"
+for i in {1..30}; do
+ ss -ltn '( sport = :1143 )' | grep -q 1143 && break
+ sleep 1
+done
+
+ss -ltn '( sport = :1143 )' | grep -q 1143 || {
+ echo "Bridge did not start"; exit 1; }
+
+echo "[3] Refresh cert"
+systemctl --user start proton-bridge-cert-refresh.service
+
+echo "[4] Wait for cert"
+for i in {1..10}; do
+ [ -s "$CERT_FILE" ] && break
+ sleep 1
+done
+
+[ -s "$CERT_FILE" ] || { echo "Cert not created"; exit 1; }
+
+echo "[5] Run mbsync"
+mbsync -a -V
+
diff --git a/archive/kvm-lab-create.bak b/archive/kvm-lab-create.bak
new file mode 100755
index 0000000..d0d5865
--- /dev/null
+++ b/archive/kvm-lab-create.bak
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Libvirt context (system)
+URI="qemu:///system"
+
+# Base/template (immutable parent qcow2)
+BASE="/var/lib/libvirt/images/base/debian13-template.qcow2"
+TEMPLATE_DOMAIN="debian13-template"
+
+# Lab overlays directory (dedicated)
+LABDIR="/var/lib/libvirt/images/lab"
+
+usage() {
+ echo "Usage: kvm-lab-create <lab-name> [--start]"
+ echo "Example: kvm-lab-create lab02 --start"
+}
+
+NAME="${1:-}"
+START="${2:-}"
+
+if [[ -z "$NAME" ]]; then
+ usage
+ exit 1
+fi
+
+if [[ "${START:-}" != "" && "${START:-}" != "--start" ]]; then
+ usage
+ exit 1
+fi
+
+DISK="${LABDIR}/${NAME}.qcow2"
+XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")"
+
+cleanup() { rm -f "$XML_TMP"; }
+trap cleanup EXIT
+
+# ---- Preconditions (read-only checks) ----
+if ! sudo test -r "$BASE"; then
+ echo "ERROR: Base image not readable: $BASE"
+ exit 1
+fi
+
+if [[ ! -d "$LABDIR" ]]; then
+ echo "ERROR: Lab directory missing: $LABDIR"
+ exit 1
+fi
+
+if [[ -e "$DISK" ]]; then
+ echo "ERROR: Disk already exists: $DISK"
+ exit 1
+fi
+
+if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then
+ echo "ERROR: Domain already exists: $NAME"
+ exit 1
+fi
+
+if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then
+ echo "ERROR: Template domain not found in $URI: $TEMPLATE_DOMAIN"
+ exit 1
+fi
+
+# ---- Create overlay disk ----
+echo "Creating overlay disk: $DISK"
+sudo qemu-img create -f qcow2 -F qcow2 -b "$BASE" "$DISK" >/dev/null
+
+# Ensure qemu/libvirt can open it
+sudo chown libvirt-qemu:libvirt "$DISK"
+sudo chmod 0660 "$DISK"
+
+# Quick sanity: confirm backing file is correct
+echo "Validating backing file..."
+sudo qemu-img info "$DISK" | grep -E "backing file:|file format:|virtual size:" || true
+
+# ---- Clone domain XML ----
+echo "Cloning domain XML from: $TEMPLATE_DOMAIN"
+virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP"
+
+# Replace <name> and the disk path, and force NAT-only network "default"
+# Notes:
+# - disk path substitution assumes template points at /var/lib/libvirt/images/base/debian13-template.qcow2
+# - network replacement assumes a standard <interface type='network'> exists
+sed -i \
+ -e "s|<name>${TEMPLATE_DOMAIN}</name>|<name>${NAME}</name>|" \
+ -e "s|${BASE}|${DISK}|g" \
+ -e "s|<source network='[^']*'|<source network='default'|g" \
+ "$XML_TMP"
+
+# Extra safety: remove any hostfs passthrough (if any) to prevent host access via mounts
+# (If none exist, this does nothing.)
+perl -0777 -i -pe 's/<filesystem[\s\S]*?<\/filesystem>\n?//g' "$XML_TMP"
+#
+# Remove UUID to avoid collision with template
+sed -i -E '/<uuid>[^<]*<\/uuid>/d' "$XML_TMP"
+# ---- Define domain ----
+echo "Defining domain: $NAME"
+virsh -c "$URI" define "$XML_TMP" >/dev/null
+
+# ---- Optional start ----
+if [[ "$START" == "--start" ]]; then
+ echo "Starting domain: $NAME"
+ virsh -c "$URI" start "$NAME" >/dev/null
+fi
+
+echo "OK: Lab '${NAME}' created (disk: ${DISK})."
+if [[ "$START" != "--start" ]]; then
+ echo "To start: virsh -c $URI start ${NAME}"
+fi
+
diff --git a/archive/vm-new b/archive/vm-new
new file mode 100755
index 0000000..3955c6c
--- /dev/null
+++ b/archive/vm-new
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+NAME="${1:-}"
+RAM="${2:-4096}" # MiB
+VCPUS="${3:-2}"
+
+BASE="/var/lib/libvirt/images/base/work01-base.qcow2"
+DIR="/var/lib/libvirt/images/overlays"
+DISK="${DIR}/${NAME}.qcow2"
+
+usage() {
+ echo "Usage: vm-new <name> [ramMiB=4096] [vcpus=2]" >&2
+ exit 2
+}
+
+[[ -n "$NAME" ]] || usage
+
+case "$NAME" in
+ debian13-template|work01|Whonix-Gateway|Whonix-Workstation)
+ echo "Refusing: protected VM name '$NAME'." >&2
+ exit 1
+ ;;
+esac
+
+if sudo virsh dominfo "$NAME" >/dev/null 2>&1; then
+ echo "Refusing: VM '$NAME' already exists in libvirt." >&2
+ exit 1
+fi
+
+if [[ ! -f "$BASE" ]]; then
+ echo "Base image not found: $BASE" >&2
+ exit 1
+fi
+
+if [[ -e "$DISK" ]]; then
+ echo "Refusing: disk already exists: $DISK" >&2
+ exit 1
+fi
+
+# Create overlay
+sudo qemu-img create -f qcow2 -F qcow2 -b "$BASE" "$DISK" >/dev/null
+
+# Define + install (import existing disk)
+sudo virt-install --connect qemu:///system \
+ --name "$NAME" \
+ --memory "$RAM" --vcpus "$VCPUS" --cpu host-model \
+ --disk "path=$DISK,format=qcow2,bus=virtio" \
+ --network network=default,model=virtio \
+ --graphics none \
+ --import \
+ --osinfo linux2024 \
+ --noautoconsole
+
+echo "Created: $NAME"
+echo "Disk: $DISK"
+echo "Next: sudo virsh start $NAME && sudo virsh domifaddr $NAME"
+
diff --git a/archive/vm-rm b/archive/vm-rm
new file mode 100755
index 0000000..3cbeb7d
--- /dev/null
+++ b/archive/vm-rm
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+NAME="${1:-}"
+usage() { echo "Usage: vm-rm <name>" >&2; exit 2; }
+[[ -n "$NAME" ]] || usage
+
+case "$NAME" in
+ debian13-template|work01|Whonix-Gateway|Whonix-Workstation)
+ echo "Refusing: protected VM name '$NAME'." >&2
+ exit 1
+ ;;
+esac
+
+if ! sudo virsh dominfo "$NAME" >/dev/null 2>&1; then
+ echo "VM not found in libvirt: $NAME" >&2
+ exit 1
+fi
+
+# Find associated disk(s) from XML, delete only qcow2 under overlays/
+mapfile -t DISKS < <(sudo virsh domblklist "$NAME" --details \
+ | awk '$3=="disk" && $4 ~ /^\// {print $4}')
+
+echo "Will remove VM: $NAME"
+for d in "${DISKS[@]}"; do
+ echo "Disk: $d"
+done
+
+sudo virsh destroy "$NAME" >/dev/null 2>&1 || true
+sudo virsh undefine "$NAME" --nvram >/dev/null 2>&1 || sudo virsh undefine "$NAME" >/dev/null
+
+for d in "${DISKS[@]}"; do
+ case "$d" in
+ /var/lib/libvirt/images/overlays/*.qcow2)
+ sudo rm -f -- "$d"
+ ;;
+ *)
+ echo "Skipping non-overlay disk: $d" >&2
+ ;;
+ esac
+done
+
+echo "Removed: $NAME"
+
diff --git a/backup_shield.sh b/backup_shield.sh
new file mode 100755
index 0000000..96f9269
--- /dev/null
+++ b/backup_shield.sh
@@ -0,0 +1,135 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+#################### CONFIG – EDIT THESE ####################
+
+# LUKS2 block device
+LUKS_DEVICE="/dev/disk/by-uuid/6659e638-d52a-4c32-9781-9dcedd44db35"
+
+# Name for the mapper device (will appear as /dev/mapper/${LUKS_NAME})
+LUKS_NAME="shield"
+
+# Mount point for the decrypted filesystem
+LUKS_MOUNTPOINT="/mnt/shield"
+
+# Local folders to back up with rsync → LUKS volume
+# Each folder will be mirrored into ${LUKS_MOUNTPOINT}/<basename-of-folder>
+BACKUP_ITEMS=(
+ "$HOME/"
+ "/media/.secrets/"
+)
+
+# Machine identifier (destination subdir on the LUKS volume)
+MACHINE_DIR="T480"
+
+#################### INTERNALS – NO NEED TO EDIT ############
+
+need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing command: $1" >&2; exit 1; }; }
+
+cleanup() {
+ echo
+ echo ">>> Cleaning up..."
+
+ # Try to unmount, if mounted
+ if mountpoint -q "${LUKS_MOUNTPOINT}"; then
+ echo ">>> Unmounting ${LUKS_MOUNTPOINT}..."
+ sudo umount "${LUKS_MOUNTPOINT}" || true
+ fi
+
+ # Close LUKS mapping if it exists
+ if [ -e "/dev/mapper/${LUKS_NAME}" ]; then
+ echo ">>> Closing LUKS mapping ${LUKS_NAME}..."
+ sudo cryptsetup close "${LUKS_NAME}" || true
+ fi
+}
+trap cleanup EXIT
+
+need_cmd cryptsetup
+need_cmd rclone
+need_cmd rsync
+need_cmd mountpoint
+
+# Cache sudo credentials early to avoid surprises mid-run
+sudo -v
+
+echo ">>> Ensuring mountpoint exists: ${LUKS_MOUNTPOINT}"
+sudo mkdir -p "${LUKS_MOUNTPOINT}"
+
+# Check if already open
+if [ -e "/dev/mapper/${LUKS_NAME}" ]; then
+ echo ">>> WARNING: /dev/mapper/${LUKS_NAME} already exists, assuming already open."
+else
+ echo ">>> Opening LUKS volume (interactive prompt)..."
+ sudo cryptsetup open "${LUKS_DEVICE}" "${LUKS_NAME}"
+fi
+
+# At this point /dev/mapper/${LUKS_NAME} must exist
+if [ ! -e "/dev/mapper/${LUKS_NAME}" ]; then
+ echo "ERROR: /dev/mapper/${LUKS_NAME} not found after cryptsetup open." >&2
+ exit 1
+fi
+
+# Mount if not already mounted
+if mountpoint -q "${LUKS_MOUNTPOINT}"; then
+ echo ">>> WARNING: ${LUKS_MOUNTPOINT} already mounted."
+else
+ echo ">>> Mounting /dev/mapper/${LUKS_NAME} on ${LUKS_MOUNTPOINT}..."
+ sudo mount "/dev/mapper/${LUKS_NAME}" "${LUKS_MOUNTPOINT}"
+fi
+
+echo ">>> LUKS volume mounted at ${LUKS_MOUNTPOINT}"
+
+# Ensure SharePoint destinations exist in root of shield
+sudo mkdir -p \
+ "${LUKS_MOUNTPOINT}/SP_Administration" \
+ "${LUKS_MOUNTPOINT}/SP_Operations" \
+ "${LUKS_MOUNTPOINT}/SP_Skyscale"
+
+echo ">>> Starting rclone syncs (SharePoint -> ${LUKS_MOUNTPOINT}/SP_*)..."
+rclone sync 'SP_Administration:' "${LUKS_MOUNTPOINT}/SP_Administration" --create-empty-src-dirs --fast-list --progress
+rclone sync 'SP_Operations:' "${LUKS_MOUNTPOINT}/SP_Operations" --create-empty-src-dirs --fast-list --progress
+rclone sync 'SP_SkyscaleCommerce:' "${LUKS_MOUNTPOINT}/SP_Skyscale" --create-empty-src-dirs --fast-list --progress
+echo ">>> All rclone syncs finished."
+
+echo ">>> Starting rsync backups of local folders -> ${LUKS_MOUNTPOINT}/${MACHINE_DIR}/..."
+
+# Ensure machine root exists
+sudo mkdir -p "${LUKS_MOUNTPOINT}/${MACHINE_DIR}"
+
+# Explicit destination mapping while keeping your BACKUP_ITEMS sources as-is
+# - "$HOME/" -> /mnt/shield/T480/HOME
+# - "/media/.secrets/"-> /mnt/shield/T480/secrets
+for SRC in "${BACKUP_ITEMS[@]}"; do
+ if [ ! -d "$SRC" ]; then
+ echo ">>> WARNING: source folder does not exist, skipping: $SRC"
+ continue
+ fi
+
+ case "$SRC" in
+ "$HOME/"|"$HOME")
+ RELDEST="${MACHINE_DIR}/HOME"
+ ;;
+ "/media/.secrets/"|"/media/.secrets")
+ RELDEST="${MACHINE_DIR}/secrets"
+ ;;
+ *)
+ # Fallback (shouldn't happen with your current BACKUP_ITEMS)
+ # Note: basename "$SRC" with trailing slash becomes "home"; avoid relying on it.
+ NAME="$(basename "${SRC%/}")"
+ RELDEST="${MACHINE_DIR}/${NAME}"
+ ;;
+ esac
+
+ DEST="${LUKS_MOUNTPOINT}/${RELDEST}"
+ sudo mkdir -p "${DEST}"
+
+ echo ">>> rsync: ${SRC} -> ${DEST}"
+ rsync -a --delete --progress \
+ "${SRC%/}/" \
+ "${DEST}/"
+done
+
+echo ">>> All rsync backups finished."
+echo ">>> Unmounting and closing LUKS (via trap)..."
+# cleanup() will run automatically on script exit
+
diff --git a/backup_usb.sh b/backup_usb.sh
new file mode 100755
index 0000000..8c81b69
--- /dev/null
+++ b/backup_usb.sh
@@ -0,0 +1,155 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+#################### CONFIG ####################
+
+# LUKS2 container UUID (LUKS header UUID)
+LUKS_SOURCE="/dev/disk/by-uuid/bdd2a016-b6db-493d-ad0a-e8dd8a93dfbd"
+
+# Name for /dev/mapper/<name>
+MAPPER_NAME="usb_safe"
+
+# Mountpoint for decrypted filesystem
+MOUNTPOINT="/mnt/usb_safe"
+
+# Optional underlying block device for power-off (set "" to disable)
+RAW_DEVICE=""
+
+# Items to mirror from $HOME to the encrypted drive (files and directories)
+BACKUP_ITEMS=(
+ "$HOME/.abook"
+ "$HOME/.config"
+ "$HOME/Documents"
+ "$HOME/dotfiles"
+ "$HOME/.password-store"
+ "$HOME/secure"
+ "$HOME/vimwiki"
+ "$HOME/.password.tomb"
+ "$HOME/.secrets.tomb"
+)
+
+RSYNC_OPTS=(
+ -a
+ --delete
+ --no-perms
+ --no-owner
+ --no-group
+ --no-xattrs
+ --omit-dir-times
+ --progress
+)
+
+LOGFILE="$HOME/backup_flash_rsync.log"
+
+#################### HELPERS ####################
+
+die() { echo "ERROR: $*" >&2; exit 1; }
+
+is_mapper_open() { [[ -e "/dev/mapper/${MAPPER_NAME}" ]]; }
+is_mounted() { mountpoint -q "${MOUNTPOINT}"; }
+
+busy_report() {
+ echo ">>> Busy report for ${MOUNTPOINT}:"
+ echo ">>> lsof:"
+ sudo lsof +f -- "${MOUNTPOINT}" || true
+ echo ">>> fuser:"
+ sudo fuser -vm "${MOUNTPOINT}" || true
+}
+
+cleanup() {
+ set +e
+
+ if is_mounted; then
+ echo ">>> Unmounting ${MOUNTPOINT}..."
+ sudo umount "${MOUNTPOINT}" || {
+ echo ">>> Unmount failed (device busy)."
+ busy_report
+ echo ">>> Last resort: sudo umount -l '${MOUNTPOINT}'"
+ }
+ fi
+
+ if is_mapper_open; then
+ echo ">>> Closing LUKS mapper ${MAPPER_NAME}..."
+ sudo cryptsetup close "${MAPPER_NAME}" || true
+ fi
+
+ if [[ -n "${RAW_DEVICE}" ]]; then
+ echo ">>> Powering off ${RAW_DEVICE}..."
+ udisksctl power-off -b "${RAW_DEVICE}" >/dev/null 2>&1 || true
+ fi
+}
+
+trap cleanup EXIT
+
+#################### OPEN + MOUNT ####################
+
+echo ">>> Checking LUKS source exists: ${LUKS_SOURCE}"
+[[ -e "${LUKS_SOURCE}" ]] || die "LUKS source not found: ${LUKS_SOURCE}"
+
+echo ">>> Ensuring mountpoint exists: ${MOUNTPOINT}"
+sudo mkdir -p "${MOUNTPOINT}"
+
+is_mounted && die "Mountpoint already in use: ${MOUNTPOINT}"
+is_mapper_open && die "Mapper already open: /dev/mapper/${MAPPER_NAME}"
+
+echo ">>> Opening LUKS2 container (prompt for passphrase)..."
+sudo cryptsetup open --type luks2 "${LUKS_SOURCE}" "${MAPPER_NAME}"
+
+echo ">>> Mounting decrypted filesystem..."
+sudo mount "/dev/mapper/${MAPPER_NAME}" "${MOUNTPOINT}"
+
+echo ">>> Mounted at ${MOUNTPOINT}"
+echo ">>> Logging rsync output to: ${LOGFILE}"
+echo "[$(date)] Backup run started" >> "${LOGFILE}"
+
+#################### RSYNC LOOP ####################
+
+ANY_WARNING=0
+
+for SRC in "${BACKUP_ITEMS[@]}"; do
+ [[ -e "${SRC}" ]] || die "Missing source item: ${SRC}"
+
+ NAME="$(basename "${SRC}")"
+ DEST="${MOUNTPOINT}/${NAME}"
+
+ echo ">>> rsync: ${SRC} -> ${DEST}"
+ echo "[$(date)] rsync: ${SRC} -> ${DEST}" >> "${LOGFILE}"
+
+ set +e
+ if [[ -d "${SRC}" ]]; then
+ rsync "${RSYNC_OPTS[@]}" "${SRC}/" "${DEST}/" 2>> "${LOGFILE}"
+ else
+ # file
+ rsync "${RSYNC_OPTS[@]}" "${SRC}" "${DEST}" 2>> "${LOGFILE}"
+ fi
+ RS=$?
+ set -e
+
+ if [[ "${RS}" -eq 0 ]]; then
+ echo ">>> rsync OK: ${SRC}"
+ echo "[$(date)] rsync OK: ${SRC}" >> "${LOGFILE}"
+ elif [[ "${RS}" -eq 23 || "${RS}" -eq 24 ]]; then
+ echo ">>> WARNING: rsync non-critical issues (code ${RS}) for: ${SRC}"
+ echo ">>> See ${LOGFILE}."
+ echo "[$(date)] WARNING: rsync code ${RS} for: ${SRC}" >> "${LOGFILE}"
+ ANY_WARNING=1
+ else
+ echo "[$(date)] ERROR: rsync failed code ${RS} for: ${SRC}" >> "${LOGFILE}"
+ exit "${RS}"
+ fi
+done
+
+echo ">>> Backup loop finished."
+if [[ "${ANY_WARNING}" -eq 1 ]]; then
+ echo ">>> Backup completed WITH WARNINGS. See ${LOGFILE}."
+ echo "[$(date)] Backup completed WITH WARNINGS" >> "${LOGFILE}"
+else
+ echo ">>> Backup completed OK."
+ echo "[$(date)] Backup completed OK" >> "${LOGFILE}"
+fi
+
+echo ">>> Syncing..."
+sync
+
+echo ">>> Done. (Auto-cleanup will unmount + close mapper.)"
+
diff --git a/bashmount b/bashmount
new file mode 100755
index 0000000..331ebe4
--- /dev/null
+++ b/bashmount
@@ -0,0 +1,855 @@
+#!/bin/bash
+set -u
+
+declare -r VERSION="4.3.2"
+
+#=============================================================================#
+# FILE: bashmount #
+# WEBSITE: https://github.com/jamielinux/bashmount #
+# DESCRIPTION: bashmount is a menu-driven bash script that can use different #
+# backends to easily mount, unmount or eject removable devices #
+# without dependencies on any GUI. An extensive configuration #
+# file allows many aspects of the script to be modified and #
+# custom commands to be run on devices. #
+# LICENSE: GPLv2 #
+# AUTHORS: Jamie Nguyen <j@jamielinux.com> #
+# Lukas B. #
+#=============================================================================#
+
+# Copyright (C) 2013-2020 Jamie Nguyen <j@jamielinux.com>
+# Copyright (C) 2014 Lukas B.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License v2 as published by the
+# Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+if (( $# > 0 )) && [[ "$1" == "-v" || "$1" == "--version" ]]; then
+ printf '%s\n' "$VERSION"
+ exit 0
+fi
+
+declare -ri EXIT_CMDNOTFOUND=127
+declare -ri EXIT_CONFIG=78
+
+# {{{ ··· CONFIGURATION
+# Make sure that user defined options will not interfere with grep.
+unset GREP_OPTIONS
+
+# Set defaults.
+declare udisks="auto"
+declare mount_options=""
+declare -i show_internal=1
+declare -i show_removable=1
+declare -i show_optical=1
+declare -i show_commands=1
+declare -i colourize=1
+declare -i pretty=1
+declare -i custom4_show=0
+declare -i custom5_show=0
+declare -i custom6_show=0
+declare custom4_desc=""
+declare custom5_desc=""
+declare custom6_desc=""
+declare -i run_post_mount=0
+declare -i run_post_unmount=0
+declare -a exclude=()
+
+# Backwards compat
+declare default_mount_options=""
+declare -a blacklist=()
+
+filemanager() {
+ ( cd "$1" && "$SHELL" )
+}
+
+post_mount() {
+ error "No 'post_mount' command specified in bashmount configuration file."
+ return 1
+}
+
+post_unmount() {
+ error "No 'post_unmount' command specified in bashmount configuration file."
+ return 1
+}
+
+declare CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/bashmount/config"
+[[ ! -e "$CONFIG_FILE" ]] && CONFIG_FILE="/etc/bashmount.conf"
+if [[ -e "$CONFIG_FILE" ]]; then
+ if ! . "$CONFIG_FILE"; then
+ printf '%s\n' "bashmount: Failed to source configuration file."
+ exit $EXIT_CONFIG
+ fi
+fi
+
+if [[ "$udisks" != 0 ]]; then
+ if type -p udisksctl >/dev/null 2>&1; then
+ [[ "$udisks" == "auto" ]] && udisks=1
+ elif [[ "$udisks" == "auto" ]]; then
+ udisks=0
+ else
+ printf '%s\n' "bashmount: 'udisksctl': command not found"
+ exit $EXIT_CMDNOTFOUND
+ fi
+fi
+
+if ! type -p lsblk >/dev/null 2>&1; then
+ printf '%s\n' "bashmount: 'lsblk': command not found"
+ exit $EXIT_CMDNOTFOUND
+fi
+
+# Backwards compat
+[[ -n "$default_mount_options" ]] && mount_options="$default_mount_options"
+(( ${#exclude[@]} == 0 )) && exclude=("${blacklist[@]}")
+# }}}
+
+# {{{ ··· PRINTING FUNCTIONS
+if (( colourize )); then
+ if tput setaf 0 >/dev/null 2>&1; then
+ declare -r ALL_OFF="$(tput sgr0)"
+ declare -r BOLD="$(tput bold)"
+ declare -r BLUE="${BOLD}$(tput setaf 4)"
+ declare -r GREEN="${BOLD}$(tput setaf 2)"
+ declare -r RED="${BOLD}$(tput setaf 1)"
+ else
+ declare -r ALL_OFF="\e[1;0m"
+ declare -r BOLD="\e[1;1m"
+ declare -r BLUE="${BOLD}\e[1;34m"
+ declare -r GREEN="${BOLD}\e[1;32m"
+ declare -r RED="${BOLD}\e[1;31m"
+ fi
+else
+ declare -r ALL_OFF="" BOLD="" BLUE="" GREEN="" RED=""
+fi
+declare -r ARROW="==>"
+
+msg() {
+ printf '\n%b\n\n' "${GREEN}${ARROW}${ALL_OFF}${BOLD} ${*}${ALL_OFF}" >&2
+}
+error() {
+ printf '\n%b\n\n' "${RED}${ARROW}${BOLD} ERROR: ${*}${ALL_OFF}" >&2
+ enter_to_continue
+}
+enter_to_continue() {
+ printf '\n'
+ read -r -e -p "Press [${BLUE}enter${ALL_OFF}] to continue: "
+}
+invalid_command() {
+ printf '\n'
+ error "Invalid command. See the help menu."
+}
+
+print_commands() {
+ printf '\n\n'
+ print_separator_commands
+ printf '%s' " ${BLUE}e${ALL_OFF}: eject"
+ printf '%s' " ${BLUE}i${ALL_OFF}: info"
+ printf '%s' " ${BLUE}m${ALL_OFF}: mount"
+ printf '%s' " ${BLUE}o${ALL_OFF}: open"
+ printf '%s' " ${BLUE}u${ALL_OFF}: unmount"
+ printf '\n\n'
+ printf '%s' " ${BLUE}[Enter]${ALL_OFF}: refresh"
+ printf '%s' " ${BLUE}a${ALL_OFF}: unmount all"
+ printf '%s' " ${BLUE}q${ALL_OFF}: quit"
+ printf '%s' " ${BLUE}?${ALL_OFF}: help"
+ printf '\n\n'
+}
+
+print_submenu_commands() {
+ printf '\n\n'
+ print_separator_commands
+ printf '%s' " ${BLUE}e${ALL_OFF}: eject"
+ printf '%s' " ${BLUE}i${ALL_OFF}: info"
+ if check_mounted "$devname"; then
+ printf '%s' " ${BLUE}u${ALL_OFF}: unmount"
+ else
+ printf '%s' " ${BLUE}m${ALL_OFF}: mount "
+ fi
+ printf '%s' " ${BLUE}o${ALL_OFF}: open"
+ printf '\n\n'
+ printf '%s' " ${BLUE}[Enter]${ALL_OFF}: refresh"
+ printf '%s' " ${BLUE}b${ALL_OFF}: back"
+ printf '%s' " ${BLUE}q${ALL_OFF}: quit"
+ printf '%s' " ${BLUE}?${ALL_OFF}: help"
+ printf '\n\n'
+ printf '%s' " ${BLUE}1${ALL_OFF}: luksDump"
+ printf '%s' " ${BLUE}2${ALL_OFF}: luksOpen"
+ printf '%s' " ${BLUE}3${ALL_OFF}: luksClose"
+ printf '\n'
+
+ if (( custom4_show )) || (( custom5_show )) || (( custom6_show )); then
+ local -i col_width=18
+ printf '\n'
+ (( custom4_show )) && [[ -n "$custom4_desc" ]] \
+ && printf '%s' " ${BLUE}4${ALL_OFF}: $custom4_desc"
+ local -i custom4_desc_len="${#custom4_desc}"
+ if (( custom4_desc_len < col_width )); then
+ for (( i=18; i>custom4_desc_len; i-- )); do
+ printf '%s' " "
+ done
+ fi
+ (( custom5_show )) && [[ -n "$custom5_desc" ]] \
+ && printf '%s' " ${BLUE}5${ALL_OFF}: $custom5_desc"
+ local -i custom5_desc_len="${#custom5_desc}"
+ if (( custom5_desc_len < col_width )); then
+ for (( i=18; i>custom5_desc_len; i-- )); do
+ printf '%s' " "
+ done
+ fi
+ (( custom6_show )) && [[ -n "$custom6_desc" ]] \
+ && printf '%s' " ${BLUE}6${ALL_OFF}: $custom6_desc"
+ printf '\n'
+ fi
+}
+
+__print() {
+ printf "${BOLD}"
+ if (( pretty )); then
+ printf '%s\n\n' "$1" | sed -e "s/-/━/g"
+ else
+ printf '%s\n\n' "$1"
+ fi
+ printf "${ALL_OFF}"
+}
+print_separator() {
+ __print "-----------------------------------------------------------------------------"
+}
+print_separator_commands() {
+ __print "-------------------------------- Commands ---------------------------------"
+}
+print_separator_device() {
+ __print "------------------------------- Device Menu -------------------------------"
+}
+print_separator_optical() {
+ __print "------------------------------ Optical Media ------------------------------"
+}
+print_separator_removable() {
+ __print "----------------------------- Removable Media -----------------------------"
+}
+print_separator_internal() {
+ __print "----------------------------- Internal Media ------------------------------"
+}
+
+print_help() {
+ clear
+ print_commands
+ print_separator
+ printf '%b' " ${GREEN}${ARROW}${ALL_OFF} "
+ printf '%s' "${BOLD}To mount the first device, enter ${ALL_OFF}"
+ printf '%s' "${BLUE}1m${ALL_OFF}${BOLD}.${ALL_OFF}"
+ printf '\n\n'
+ printf '%b' " ${GREEN}${ARROW}${ALL_OFF} "
+ printf '%s' "${BOLD}To open the mountpath directory of the first${ALL_OFF}"
+ printf '\n\n'
+ printf '%s' " ${BOLD}device (mounting if required), enter "
+ printf '%s' "${BLUE}1o${ALL_OFF}${BOLD}.${ALL_OFF}"
+ printf '\n\n'
+ printf '%b' " ${GREEN}${ARROW}${ALL_OFF} "
+ printf '%s' "${BOLD}To view a device sub-menu, just enter the number.${ALL_OFF}"
+ printf '\n\n'
+ printf '%b' " ${GREEN}${ARROW}${ALL_OFF} "
+ printf '%s' "${BLUE}[Enter]${ALL_OFF}"
+ printf '%s' "${BOLD}, "
+ printf '%s' "${BLUE}a${ALL_OFF}"
+ printf '%s' "${BOLD}, "
+ printf '%s' "${BLUE}q${ALL_OFF} "
+ printf '%s' "${BOLD}and "
+ printf '%s' "${BLUE}?${ALL_OFF} "
+ printf '%s' "${BOLD}do not require a number.${ALL_OFF}"
+ printf '\n\n'
+ print_separator
+ enter_to_continue
+}
+
+print_help_submenu() {
+ clear
+ print_submenu_commands
+ printf '\n'
+ print_separator
+ printf '%b' " ${GREEN}${ARROW}${ALL_OFF} "
+ printf '%s' "${BOLD}To perform a command, enter a character and press ${ALL_OFF}"
+ printf '%s' "${BLUE}[Enter]${ALL_OFF}${BOLD}.${ALL_OFF}"
+ printf '\n\n'
+ printf '%b' " ${GREEN}${ARROW}${ALL_OFF} "
+ printf '%s' "${BOLD}For example, to mount this device, type ${ALL_OFF}"
+ printf '%s' "${BLUE}m${ALL_OFF} and press ${BLUE}[Enter]${ALL_OFF}"
+ printf '%s' "${BOLD}.${ALL_OFF}"
+ printf '\n\n'
+ print_separator
+ enter_to_continue
+}
+
+print_device() {
+ local -i padding_name=13
+ local -i padding_label=18
+ local -i padding_size=6
+
+ local label="$(info_fslabel "$devname")"
+ local fstype="$(info_fstype "$devname")"
+
+ [[ -z "$label" ]] && (( $# == 1 )) && [[ "$1" == "optical" ]] \
+ && label="$(lsblk -dno MODEL "$devname")"
+ [[ -z "$label" ]] && label="$(info_partlabel "$devname")"
+ [[ -z "$label" ]] && [[ "$fstype" == "crypto_LUKS" ]] && label="crypto_LUKS"
+ [[ -z "$label" ]] && label="-"
+
+ label="$(printf '%s' "$label" | sed -e 's/\\x20/ /g')"
+
+ listed[device_number]="$devname"
+ (( device_number++ ))
+
+ printf '%s' " ${BLUE}${device_number})${ALL_OFF}"
+
+ devname_short="${devname##*/}"
+ label_short="$label"
+ if (( ${#devname_short} > padding_name )); then
+ len=$(( padding_name - 4 ))
+ devname_short="${devname_short:0:len}..."
+ elif (( ${#label} > padding_label )); then
+ label_len=$(( padding_label - 4 ))
+ label_short="${label:0:label_len}..."
+ fi
+
+ printf '%s' " ${devname_short}:"
+ for (( i=padding_name; i>${#devname_short}; i-- )); do
+ printf '%s' " "
+ done
+
+ printf '%s' " $label_short"
+ for (( i=padding_label; i>${#label_short}; i-- )); do
+ printf '%s' " "
+ done
+
+ size="$(info_size "$devname")"
+ printf '%s' " $size"
+ if (( ${#size} < padding_size )); then
+ for (( i=padding_size; i>${#size}; i-- )); do
+ printf '%s' " "
+ done
+ fi
+
+ if [[ "$fstype" == "crypto_LUKS" ]]; then
+ local uuid="$(info_uuid "$devname")"
+ if [[ -n "$uuid" ]]; then
+ for dev in "${all[@]}"; do
+ if [[ "$dev" == "/dev/mapper/luks-$uuid" ]]; then
+ printf '%s' " ${GREEN}decrypted [luks-${uuid:0:4}...]${ALL_OFF}"
+ fi
+ done
+ fi
+ elif check_mounted "$devname"; then
+ mountpath="$(info_mountpath "$devname")"
+ printf '%s' " ${GREEN}[$mountpath]${ALL_OFF}"
+ mounted[${#mounted[*]}]="$devname"
+ fi
+ printf '\n'
+}
+# }}}
+
+# {{{ ··· INFORMATION RETRIEVAL
+# Returns 0 (ie, success) if the device still exists. Otherwise it returns 1.
+check_device() {
+ if [[ ! -b "$1" ]]; then
+ error "$1 is no longer available."
+ return 1
+ fi
+}
+# Returns 0 (ie, success) if the device is mounted. Otherwise it returns 1.
+check_mounted() {
+ findmnt -no TARGET "$1" >/dev/null 2>&1
+}
+# Returns 0 (ie, success) if the device is registered as a removable device in
+# the kernel. Otherwise it returns 1.
+check_removable() {
+ [[ "$(lsblk -drno RM "$1")" == "1" ]]
+}
+
+info_fslabel() {
+ lsblk -drno LABEL "$1" 2>/dev/null
+}
+info_fstype() {
+ lsblk -drno FSTYPE "$1" 2>/dev/null
+}
+info_mountpath() {
+ findmnt -no TARGET "$1" 2>/dev/null
+}
+info_partlabel() {
+ lsblk -drno PARTLABEL "$1" 2>/dev/null
+}
+info_size() {
+ lsblk -drno SIZE "$1" 2>/dev/null
+}
+info_type() {
+ lsblk -drno TYPE "$1" 2>/dev/null
+}
+info_uuid() {
+ lsblk -drno UUID "$1" 2>/dev/null
+}
+info_used() {
+ lsblk -drno FSUSE% "$1" 2>/dev/null
+}
+
+get_luks_child() {
+ local uuid="$(info_uuid "$1")"
+ printf '%s' "/dev/mapper/luks-$uuid"
+}
+# }}}
+
+# {{{ ··· DEVICE MANIPULATION
+__mount() {
+ msg "Mounting $1 ..."
+ if (( udisks )); then
+ udisksctl mount $mount_options --block-device "$1"
+ else
+ read -r -e -p "Choose the mountpoint directory: " dir
+ [[ -z "$dir" ]] && return 1
+ if [[ ! -e "$dir" ]]; then
+ if ! mkdir -p "$dir"; then
+ error "'$dir': Could not create directory."
+ return 1
+ fi
+ fi
+ sudo mount $mount_options "$1" "$dir"
+ fi
+}
+
+__unmount() {
+ msg "Unmounting $1 ..."
+ if (( udisks )); then
+ udisksctl unmount --block-device "$1"
+ else
+ sudo umount "$1"
+ fi
+}
+
+action_eject() {
+ check_device "$1" || return 1
+
+ if [[ "$(info_fstype "$1")" == "crypto_LUKS" ]]; then
+ action_unmount "$1" || return 1
+ else
+ check_mounted "$1" && action_unmount "$1"
+ fi
+
+ local -i retval=0
+ if ! check_mounted "$1"; then
+ msg "Ejecting $1 ..."
+ device_type=$(info_type "$1")
+ if (( udisks )) && [[ "$device_type" != "rom" ]]; then
+ udisksctl power-off -b "$1"
+ retval=$?
+ else
+ sudo eject "$1"
+ retval=$?
+ fi
+ if (( retval == 0 )); then
+ # Give the device some time to eject.
+ sleep 1.5s
+ else
+ enter_to_continue
+ fi
+ fi
+}
+
+action_info() {
+ check_device "$1" || return 1
+ if (( udisks )); then
+ udisksctl info -b "$devname" | less
+ else
+ lsblk -po NAME,FSTYPE,SIZE,FSUSE%,MOUNTPOINT "$1" | less
+ fi
+}
+
+action_mount() {
+ check_device "$1" || return 1
+ if check_mounted "$1"; then
+ error "$1 is already mounted."
+ return 1
+ fi
+
+ if [[ "$(info_fstype "$1")" == "crypto_LUKS" ]]; then
+ luks_child="$(get_luks_child "$1")"
+ if [[ ! -b "$luks_child" ]]; then
+ action_unlock "$1" || return 1
+ fi
+ action_mount "$luks_child"
+ return $?
+ fi
+
+ if __mount "$1"; then
+ msg "$1 mounted successfully."
+ (( run_post_mount )) && post_mount "$1"
+ sleep 0.1s
+ return 0
+ fi
+ error "$1 could not be mounted."
+ return 1
+}
+
+action_open() {
+ check_device "$1" || return 1
+ if [[ "$(info_fstype "$1")" == "crypto_LUKS" ]]; then
+ luks_child="$(get_luks_child "$1")"
+ if [[ ! -b "$luks_child" ]]; then
+ action_mount "$1" || return 1
+ fi
+ action_open "$luks_child"
+ return $?
+ elif ! check_mounted "$1"; then
+ action_mount "$1" || return 1
+ fi
+ msg "Opening $1 ..."
+ filemanager "$(info_mountpath "$1")" || enter_to_continue
+}
+
+action_unmount() {
+ check_device "$1" || return 1
+
+ if [[ "$(info_fstype "$1")" == "crypto_LUKS" ]]; then
+ luks_child="$(get_luks_child "$1")"
+ if [[ -b "$luks_child" ]]; then
+ if check_mounted "$luks_child"; then
+ action_unmount "$luks_child" || return 1
+ fi
+ action_lock "$1" || return 1
+ fi
+ return $?
+ fi
+
+ if ! check_mounted "$1"; then
+ error "$1 is already unmounted."
+ return 1
+ fi
+
+ if __unmount "$1"; then
+ msg "$1 unmounted successfully."
+ (( run_post_unmount )) && post_unmount "$1"
+ sleep 0.1s
+ return 0
+ fi
+ error "$1 could not be unmounted."
+ return 1
+}
+
+action_unlock() {
+ msg "Opening luks volume ..."
+ local -i retval=0
+ if (( udisks )); then
+ udisksctl unlock --block-device "$devname"
+ retval=$?
+ else
+ sudo cryptsetup open --type luks -v "$devname" "luks-${devname##*/}"
+ retval=$?
+ fi
+ (( retval != 0 )) && enter_to_continue
+ return $retval
+}
+
+action_lock() {
+ msg "Closing luks volume ..."
+ local -i retval=0
+ if (( udisks )); then
+ udisksctl lock --block-device "$devname"
+ retval=$?
+ else
+ sudo cryptsetup close --type luks "$devname"
+ retval=$?
+ fi
+ (( retval != 0 )) && enter_to_continue
+ return $retval
+}
+# }}}
+
+# {{{ ··· MENU FUNCTIONS
+list_devices() {
+ all=() # all devices
+ listed=() # all devices that are shown (ie, not hidden)
+ mounted=() # all devices that are shown and mounted
+ device_number=0
+
+ local -a removable=()
+ local -a internal=()
+ local -a optical=()
+
+ while IFS='' read -r device; do
+ all+=( "$device" )
+ done < <(lsblk -plno NAME)
+
+ for devname in "${all[@]}"; do
+ # Hide excluded devices.
+ for string in "${exclude[@]}"; do
+ lsblk -dPno NAME,TYPE,FSTYPE,LABEL,MOUNTPOINT,PARTLABEL,UUID "$devname" \
+ | grep -E "$string" >/dev/null 2>&1
+ (( $? )) || continue 2
+ done
+
+ # Sort devices into arrays removable, internal, and optical.
+ local device_type=$(info_type "$devname")
+ if [[ "$device_type" == "part" ]]; then
+ if check_removable "$devname"; then
+ removable[${#removable[*]}]="$devname"
+ else
+ internal[${#internal[*]}]="$devname"
+ fi
+ elif [[ "$device_type" == "crypt" ]]; then
+ # luks-xxxxx devices are never marked as removable, so we judge
+ # whether they are removable by their parent crypto_LUKS device.
+ local -i parent_found=0
+ for parent_devname in "${all[@]}"; do
+ local parent_uuid="$(info_uuid "$parent_devname")"
+ [[ -z "$parent_uuid" ]] && continue
+ if [[ "/dev/mapper/luks-$parent_uuid" == "$devname" ]]; then
+ if check_removable "$parent_devname"; then
+ removable[${#removable[*]}]="$devname"
+ parent_found=1
+ break
+ else
+ internal[${#internal[*]}]="$devname"
+ parent_found=1
+ break
+ fi
+ fi
+ done
+ (( !parent_found )) && internal[${#internal[*]}]="$devname"
+ # Normally we don't want to see a "disk", but if it has no partitions
+ # (eg, internal storage on some portable media devices) then it should
+ # be visible.
+ elif [[ "$device_type" == "disk" ]]; then
+ for (( i=0; i<${#all[@]}; i++ )); do
+ [[ "${all[$i]}" =~ "$devname".+ ]] && continue 2
+ done
+ if check_removable "$devname"; then
+ removable[${#removable[*]}]="$devname"
+ else
+ internal[${#internal[*]}]="$devname"
+ fi
+ elif [[ "$device_type" == "rom" ]]; then
+ optical[${#optical[*]}]="$devname"
+ else
+ continue
+ fi
+ done
+
+ clear
+ # List internal media.
+ if (( show_internal )) && (( ${#internal[*]} )); then
+ print_separator_internal
+ for devname in "${internal[@]}"; do
+ print_device
+ done
+ printf '\n'
+ fi
+ # List removable media.
+ if (( show_removable )) && (( ${#removable[*]} )); then
+ print_separator_removable
+ for devname in "${removable[@]}"; do
+ print_device
+ done
+ printf '\n'
+ fi
+ # List optical media.
+ if (( show_optical )) && (( ${#optical[*]} )); then
+ print_separator_optical
+ for devname in "${optical[@]}"; do
+ print_device optical
+ done
+ printf '\n'
+ fi
+ (( !device_number )) && printf '%s\n' "No devices."
+}
+
+submenu() {
+ # Make sure device is still valid.
+ check_device "$devname" || return 1
+
+ # Try to use a useful label to identify the device.
+ local label="$(info_fslabel "$devname")"
+ [[ -z "$label" ]] && label="$(info_partlabel "$devname")"
+ [[ -z "$label" ]] && label="-"
+
+ local fstype="$(info_fstype "$devname")"
+ local size="$(info_size "$devname")"
+ local used="$(info_used "$devname")"
+ local uuid="$(info_uuid "$devname")"
+
+ local -i mounted=0
+ check_mounted "$devname" && mounted=1
+
+ # Display the user interface.
+ clear
+ print_separator_device
+ printf '%s\n' " device : $devname"
+ printf '%s\n' " label : $label"
+ if [[ "$fstype" == "crypto_LUKS" ]]; then
+ printf '%s' " status : "
+ local -i unlocked=0
+ if [[ -n "$uuid" ]]; then
+ for dev in "${all[@]}"; do
+ if [[ "$dev" == "/dev/mapper/luks-$uuid" ]]; then
+ printf '%s\n' "${GREEN}decrypted [luks-${uuid:0:4}...]${ALL_OFF}"
+ unlocked=1
+ break
+ fi
+ done
+ fi
+ (( !unlocked )) && printf '%s\n' "encrypted"
+ else
+ printf '%s' " mounted : "
+ if (( mounted )); then
+ printf '%s\n' "${GREEN}yes${ALL_OFF}"
+ printf '%s\n' " mountpath : $(info_mountpath "$devname")"
+ else
+ printf '%s\n' "${RED}no${ALL_OFF}"
+ fi
+ fi
+ printf '%s\n' " fstype : $fstype"
+ printf '%s\n' " uuid : $uuid"
+ printf '%s\n' " size : $size"
+ (( mounted )) && printf '%s\n' " used : $used"
+ if (( show_commands == 1 )); then
+ printf '\n'
+ print_submenu_commands
+ fi
+ printf '\n'
+ print_separator
+
+ # Receive user input.
+ local -i retval
+ if ! read -r -e -p "${BOLD}Command:${ALL_OFF} " action; then
+ # Exit on ctrl-d.
+ printf '\n'
+ exit 0
+ fi
+ case "$action" in
+ "e") action_eject "$devname" || true;;
+ "i") action_info "$devname" || true;;
+ "m") action_mount "$devname" || true;;
+ "o") action_open "$devname" || true;;
+ "u") action_unmount "$devname" || true;;
+ "b") return 1;;
+ ""|"r") return 0;;
+ "q") exit;;
+ "?") print_help_submenu; return 0;;
+ "1") sudo sh -c "cryptsetup luksDump '$devname' | less"; return 0;;
+ "2") action_unlock "$devname"; return 1;;
+ "3") action_lock "$devname"; return 1;;
+ "4")
+ if (( custom4_show )); then
+ msg "Running custom command: $custom4_desc ..."
+ custom4_command "$devname"
+ enter_to_continue
+ else
+ invalid_command
+ fi
+ return 0;;
+ "5")
+ if (( custom5_show )); then
+ msg "Running custom command: $custom5_desc ..."
+ custom5_command "$devname"
+ enter_to_continue
+ else
+ invalid_command
+ fi
+ return 0;;
+ "6")
+ if (( custom6_show )); then
+ msg "Running custom command: $custom6_desc ..."
+ custom6_command "$devname"
+ enter_to_continue
+ else
+ invalid_command
+ fi
+ return 0;;
+ *) invalid_command; return 0;;
+ esac
+}
+
+select_action() {
+ local devname
+ local letter
+ print_separator
+ if ! read -r -e -p "${BOLD}Command:${ALL_OFF} " action; then
+ # Exit on ctrl-d.
+ printf '\n'
+ exit 0
+ fi
+ if [[ "$action" =~ ^[1-9] ]]; then
+ if [[ "$action" =~ ^[1-9][0-9]*$ ]]; then
+ # Zero-based numbering for array elements, so subtract one.
+ local number="$(( action - 1 ))"
+ if (( number >= device_number )); then
+ invalid_command
+ return 1
+ fi
+ devname=${listed[number]}
+ while true; do
+ submenu || break
+ done
+ elif [[ "$action" =~ ^[1-9][0-9]*[eimou]$ ]]; then
+ # Zero-based numbering for array elements, so subtract one.
+ local number="$(( ${action%?} - 1 ))"
+
+ if (( number >= device_number )); then
+ invalid_command
+ return 1
+ fi
+ devname="${listed[number]}"
+
+ letter="${action: -1}"
+ case "$letter" in
+ "e") action_eject "$devname";;
+ "i") action_info "$devname";;
+ "m") action_mount "$devname";;
+ "o") action_open "$devname";;
+ "u") action_unmount "$devname";;
+ *) return 1;;
+ esac
+ return 0
+ else
+ invalid_command
+ return 1
+ fi
+ else
+ case "$action" in
+ "a")
+ if (( ! ${#mounted[*]} )); then
+ error "No devices mounted."
+ return 1
+ fi
+ printf '\n'
+ read -r -e -p "Unmount all devices [y/N]?: " unmount
+ [[ "$unmount" != "y" ]] && [[ "$unmount" != "Y" ]] && return 0
+ clear
+ for devname in "${mounted[@]}"; do
+ action_unmount "$devname" || continue
+ done
+ enter_to_continue
+ return 1;;
+ "r"|"") return 0;;
+ "q"|"b") exit 0;;
+ "?") print_help; return 0;;
+ *) invalid_command; return 1;;
+ esac
+ fi
+}
+# }}}
+
+declare -i device_number=0
+declare -a all=()
+declare -a listed=()
+declare -a mounted=()
+
+while true; do
+ list_devices
+ (( show_commands )) && print_commands
+ select_action
+done
+
diff --git a/bookmarkthis b/bookmarkthis
new file mode 100755
index 0000000..22aff66
--- /dev/null
+++ b/bookmarkthis
@@ -0,0 +1,61 @@
+#!/bin/sh
+set -eu
+
+file="$HOME/.snippets"
+mkdir -p "$(dirname "$file")"
+touch "$file"
+
+# wybierz JEDNO:
+bookmark="$(xclip -o -selection primary 2>/dev/null || true)" # zaznaczenie myszką
+# bookmark="$(xclip -o -selection clipboard 2>/dev/null || true)" # Ctrl+C
+
+# usuń końcowe nowe linie (częste przy kopiowaniu)
+bookmark="$(printf '%s' "$bookmark" | sed -e 's/[[:space:]]\+$//')"
+
+if [ -z "$bookmark" ]; then
+ notify-send "No string highlighted!" "Please select/copy a string to bookmark"
+ exit 1
+fi
+
+# literalne dopasowanie całej linii
+if grep -Fxq -- "$bookmark" "$file"; then
+ notify-send "Oops" "Already Bookmarked!"
+ exit 0
+fi
+
+# edycja w dmenu: domyślnie pokazuje już wartość
+bookmarkEdit="$(printf '%s' "$bookmark" | dmenu -p "Edit bookmark:")"
+
+# jeśli ESC albo pusto -> użyj oryginału
+if [ -z "${bookmarkEdit:-}" ]; then
+ out="$bookmark"
+else
+ out="$bookmarkEdit"
+fi
+
+# zapisz jako nową linię
+printf '%s\n' "$out" >> "$file"
+notify-send "Bookmark added!" "$out is now saved to the file"
+
+# bookmark="$(xclip -o)"
+# file="${HOME}/.snippets"
+#
+# if [ -z "$bookmark" ]
+# then
+# notify-send "No string highlighted!" "Please select a string to bookmark"
+# exit 1
+# else
+# if grep -q "^$bookmark$" "$file"; then
+# notify-send "Oops" "Already Bookmarked!"
+# else
+# bookmarkEdit=$(:|dmenu -p "Make changes to the bookmark: $bookmark" & sleep 0.1 && xdotool type $bookmark)
+# if [ -z "$bookmarkEdit" ]
+# then
+# notify-send "Bookmark added!" "$bookmark is now saved to the file"
+# echo "$bookmark" >> $file
+# else
+# notify-send "Bookmark added!" "$bookmarkEdit is now saved to the file"
+# echo "$bookmarkEdit" >> "$file"
+# fi
+# fi
+# fi
diff --git a/borg-backup.sh b/borg-backup.sh
new file mode 100755
index 0000000..b9572f6
--- /dev/null
+++ b/borg-backup.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+set -euo pipefail
+
+REPO="borg@192.168.33.5:/tank/backups/borg"
+NAME="home-$(date +%F_%H-%M)"
+SRC="/home/lukasz"
+
+export BORG_RSH="ssh -p 57385 -i $HOME/.ssh/sacrum.key -o IdentitiesOnly=yes"
+
+LOGDIR="$HOME/.local/state/borg"
+mkdir -p "$LOGDIR"
+LOGFILE="$LOGDIR/backup-$(date +%F).log"
+
+# Show progress if stdout is a terminal (manual run)
+ARGS=(--compression lz4 --exclude-from "$HOME/.config/borg/excludes")
+if [ -t 1 ]; then
+ ARGS+=(--progress --list --stats)
+else
+ ARGS+=(--stats)
+fi
+
+# Always log; show on screen when interactive
+borg create "${ARGS[@]}" "$REPO"::"$NAME" "$SRC" 2>&1 | tee -a "$LOGFILE"
+
diff --git a/bridge-sync b/bridge-sync
new file mode 100755
index 0000000..b92f849
--- /dev/null
+++ b/bridge-sync
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CERT_FILE="${HOME}/.config/isync/proton-bridge.pem"
+
+BRIDGE_UNIT="protonmail-bridge.service"
+IMAP_PORT="1143"
+
+# These paths match what your ps output shows.
+BRIDGE_CHILD_PATH="/usr/lib/protonmail/bridge/bridge"
+BRIDGE_LAUNCHER_PATH="/usr/bin/protonmail-bridge"
+
+CERT_REFRESH_UNIT="proton-bridge-cert-refresh.service"
+
+port_listening() {
+ ss -ltn "( sport = :${IMAP_PORT} )" | grep -q ":${IMAP_PORT}"
+}
+
+echo "[0] Preconditions"
+command -v ss >/dev/null
+command -v mbsync >/dev/null
+command -v systemctl >/dev/null
+
+echo "[1] Stop Bridge (systemd user service)"
+systemctl --user stop "${BRIDGE_UNIT}" || true
+
+echo "[1.1] Kill any remaining processes in unit cgroup"
+# If stop didn't fully terminate, this forces remaining processes in the unit's cgroup to die.
+systemctl --user kill -s SIGKILL "${BRIDGE_UNIT}" || true
+
+echo "[1.2] Hard kill Bridge child (the one that tends to linger)"
+pkill -u "${USER}" -f "${BRIDGE_CHILD_PATH}" || true
+
+echo "[1.3] Optional: kill launcher if it lingers"
+pkill -u "${USER}" -f "${BRIDGE_LAUNCHER_PATH}" || true
+
+echo "[1.4] Wait until IMAP port ${IMAP_PORT} is closed"
+for i in {1..15}; do
+ port_listening || break
+ sleep 1
+done
+
+if port_listening; then
+ echo "ERROR: Port ${IMAP_PORT} still open after stop/kill."
+ echo "Diagnostics:"
+ ss -ltnp | grep ":${IMAP_PORT}" || true
+ ps -u "${USER}" -o pid,comm,args | grep -E 'protonmail|/usr/lib/protonmail/bridge/bridge' | grep -v grep || true
+ exit 1
+fi
+
+echo "[2] Start Bridge"
+systemctl --user start "${BRIDGE_UNIT}"
+
+echo "[2.1] Wait for Bridge IMAP port ${IMAP_PORT}"
+for i in {1..30}; do
+ port_listening && break
+ sleep 1
+done
+
+if ! port_listening; then
+ echo "ERROR: Bridge did not start (port ${IMAP_PORT} not listening)."
+ echo "Diagnostics:"
+ systemctl --user status "${BRIDGE_UNIT}" --no-pager || true
+ journalctl --user -u "${BRIDGE_UNIT}" -n 80 --no-pager || true
+ exit 1
+fi
+
+echo "[3] Refresh cert"
+systemctl --user start "${CERT_REFRESH_UNIT}"
+
+echo "[3.1] Wait for cert: ${CERT_FILE}"
+for i in {1..10}; do
+ [ -s "${CERT_FILE}" ] && break
+ sleep 1
+done
+
+if [ ! -s "${CERT_FILE}" ]; then
+ echo "ERROR: Cert not created at ${CERT_FILE}"
+ echo "Diagnostics:"
+ systemctl --user status "${CERT_REFRESH_UNIT}" --no-pager || true
+ journalctl --user -u "${CERT_REFRESH_UNIT}" -n 80 --no-pager || true
+ ls -la "$(dirname "${CERT_FILE}")" || true
+ exit 1
+fi
+
+echo "[4] Run mbsync"
+mbsync -a -V
+
diff --git a/create-lab b/create-lab
new file mode 100755
index 0000000..8640ed6
--- /dev/null
+++ b/create-lab
@@ -0,0 +1,206 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+URI="qemu:///system"
+
+IMGROOT="/var/lib/libvirt/images"
+BASEDIR="$IMGROOT/base"
+LABDIR="$IMGROOT/lab"
+
+# Template domains (XML donors)
+TEMPLATE_DOMAIN_DEBIAN="debian13-template"
+TEMPLATE_DOMAIN_NIXOS="nixos-template"
+
+# Base qcow2 images (backing files)
+TEMPLATE_BASE_DEBIAN="$BASEDIR/debian13-template.qcow2"
+TEMPLATE_BASE_NIXOS="$BASEDIR/nixos-base.qcow2"
+
+usage() {
+ cat <<'EOF'
+Usage: kvm-lab-create <lab-name> [--base template|work01|nixos|/abs/path.qcow2] [--start]
+
+Examples:
+ kvm-lab-create lab10 --base template --start
+ kvm-lab-create lab11 --base work01 --start
+ kvm-lab-create lab20 --base nixos --start
+ kvm-lab-create lab12 --base /var/lib/libvirt/images/base/custom.qcow2 --start
+EOF
+}
+
+# --- Parse args ---
+NAME="${1:-}"
+shift || true
+
+BASESEL="template"
+START="no"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --base)
+ BASESEL="${2:-}"
+ shift 2
+ ;;
+ --start)
+ START="yes"
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "ERROR: Unknown argument: $1"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z "${NAME:-}" ]]; then
+ usage
+ exit 1
+fi
+
+DISK="${LABDIR}/${NAME}.qcow2"
+XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")"
+cleanup() { rm -f "$XML_TMP"; }
+trap cleanup EXIT
+
+# --- Resolve base selection (qcow2 backing file) ---
+resolve_base() {
+ local sel="$1"
+
+ if [[ "$sel" == "template" ]]; then
+ echo "$TEMPLATE_BASE_DEBIAN"
+ return 0
+ fi
+
+ if [[ "$sel" == "nixos" ]]; then
+ echo "$TEMPLATE_BASE_NIXOS"
+ return 0
+ fi
+
+ if [[ "$sel" == "work01" ]]; then
+ # Pick newest promoted work01 base (must exist first)
+ local latest
+ latest="$(ls -1t "$BASEDIR"/work01-base-*.qcow2 2>/dev/null | head -n 1 || true)"
+ if [[ -z "$latest" ]]; then
+ echo "ERROR: No work01 base found in $BASEDIR (expected work01-base-*.qcow2)."
+ echo "Action: run your promote script to create one."
+ return 1
+ fi
+ echo "$latest"
+ return 0
+ fi
+
+ # Absolute path base
+ if [[ "$sel" == /* ]]; then
+ echo "$sel"
+ return 0
+ fi
+
+ echo "ERROR: Invalid --base '$sel' (use template|work01|nixos|/abs/path.qcow2)"
+ return 1
+}
+
+# --- Select template domain (XML donor) based on base selection ---
+resolve_template_domain() {
+ local sel="$1"
+
+ if [[ "$sel" == "nixos" ]]; then
+ echo "$TEMPLATE_DOMAIN_NIXOS"
+ return 0
+ fi
+
+ # If user gave an absolute qcow2, we default to Debian template unless they explicitly chose nixos.
+ # (You can change this policy later by adding a separate --os flag.)
+ echo "$TEMPLATE_DOMAIN_DEBIAN"
+ return 0
+}
+
+BASE="$(resolve_base "$BASESEL")"
+TEMPLATE_DOMAIN="$(resolve_template_domain "$BASESEL")"
+
+# --- Preconditions (read-only checks) ---
+if [[ ! -d "$LABDIR" ]]; then
+ echo "ERROR: Lab directory missing: $LABDIR"
+ exit 1
+fi
+
+if ! sudo test -r "$BASE"; then
+ echo "ERROR: Base image not readable (via sudo): $BASE"
+ exit 1
+fi
+
+if [[ -e "$DISK" ]]; then
+ echo "ERROR: Disk already exists: $DISK"
+ exit 1
+fi
+
+if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then
+ echo "ERROR: Domain already exists: $NAME"
+ exit 1
+fi
+
+if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then
+ echo "ERROR: Template domain not found in $URI: $TEMPLATE_DOMAIN"
+ echo "Action:"
+ if [[ "$TEMPLATE_DOMAIN" == "$TEMPLATE_DOMAIN_NIXOS" ]]; then
+ echo " - Define a nixos-template domain (UEFI/OVMF) and keep it powered off."
+ else
+ echo " - Ensure debian13-template exists."
+ fi
+ exit 1
+fi
+
+# --- Create overlay disk ---
+echo "Using template domain: $TEMPLATE_DOMAIN"
+echo "Using base: $BASE"
+echo "Creating overlay disk: $DISK"
+sudo qemu-img create -f qcow2 -F qcow2 -b "$BASE" "$DISK" >/dev/null
+sudo chown libvirt-qemu:libvirt "$DISK"
+sudo chmod 0660 "$DISK"
+
+echo "Validating backing file..."
+sudo qemu-img info "$DISK" | grep -E "file format:|virtual size:|backing file:" || true
+
+# --- Clone domain XML ---
+echo "Cloning domain XML from: $TEMPLATE_DOMAIN"
+virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP"
+
+# Detect original disk path inside the template XML and replace it robustly
+ORIG_DISK_PATH="$(grep -oP "(?<=<source file=')[^']+" "$XML_TMP" | head -n 1 || true)"
+if [[ -z "$ORIG_DISK_PATH" ]]; then
+ echo "ERROR: Could not find <source file='...'> disk path in template XML ($TEMPLATE_DOMAIN)."
+ echo "Action: ensure the template VM uses a file-backed qcow2 disk."
+ exit 1
+fi
+
+# Set name, disk path, and force NAT-only network "default"
+sed -i \
+ -e "s|<name>${TEMPLATE_DOMAIN}</name>|<name>${NAME}</name>|" \
+ -e "s|${ORIG_DISK_PATH}|${DISK}|g" \
+ -e "s|<source network='[^']*'|<source network='default'|g" \
+ "$XML_TMP"
+
+# Remove UUID to avoid collision with template
+sed -i -E '/<uuid>[^<]*<\/uuid>/d' "$XML_TMP"
+
+# Remove any host filesystem passthrough (if present)
+perl -0777 -i -pe 's/<filesystem[\s\S]*?<\/filesystem>\n?//g' "$XML_TMP"
+
+# --- Define domain ---
+echo "Defining domain: $NAME"
+virsh -c "$URI" define "$XML_TMP" >/dev/null
+
+# --- Optional start ---
+if [[ "$START" == "yes" ]]; then
+ echo "Starting domain: $NAME"
+ virsh -c "$URI" start "$NAME" >/dev/null
+fi
+
+echo "OK: Lab '$NAME' created (disk: $DISK)."
+if [[ "$START" != "yes" ]]; then
+ echo "To start: virsh -c $URI start $NAME"
+fi
+
diff --git a/dwm-status.sh b/dwm-status.sh
index b41dc5c..7674482 100755
--- a/dwm-status.sh
+++ b/dwm-status.sh
@@ -1,7 +1,7 @@
#!/bin/sh
while true; do
- datetime="$(date '+%a %m-%d %H:%M')"
+ datetime="$(date '+%d %a %H:%M')"
# Volume
if command -v pamixer >/dev/null 2>&1; then
diff --git a/firefox b/firefox
new file mode 100755
index 0000000..20dac09
--- /dev/null
+++ b/firefox
@@ -0,0 +1,4 @@
+#!/bin/sh
+exec firejail --profile=/etc/firejail/firefox-esr.profile \
+ /usr/bin/firefox-esr --no-remote "$@"
+
diff --git a/firefox-raw b/firefox-raw
new file mode 100755
index 0000000..e29362a
--- /dev/null
+++ b/firefox-raw
@@ -0,0 +1,2 @@
+!/bin/sh
+exec /usr/bin/firefox-esr --no-remote "$@"
diff --git a/kvm-lab-create b/kvm-lab-create
new file mode 100755
index 0000000..43d2926
--- /dev/null
+++ b/kvm-lab-create
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+URI="qemu:///system"
+
+IMGROOT="/var/lib/libvirt/images"
+BASEDIR="$IMGROOT/base"
+LABDIR="$IMGROOT/lab"
+
+TEMPLATE_DOMAIN="debian13-template"
+TEMPLATE_BASE="$BASEDIR/debian13-template.qcow2"
+
+usage() {
+ echo "Usage: kvm-lab-create <lab-name> [--base template|work01|/abs/path.qcow2] [--start]"
+ echo "Examples:"
+ echo " kvm-lab-create lab10 --base template --start"
+ echo " kvm-lab-create lab11 --base work01 --start"
+ echo " kvm-lab-create lab12 --base /var/lib/libvirt/images/base/custom.qcow2 --start"
+}
+
+# --- Parse args ---
+NAME="${1:-}"
+shift || true
+
+BASESEL="template"
+START="no"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --base)
+ BASESEL="${2:-}"
+ shift 2
+ ;;
+ --start)
+ START="yes"
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "ERROR: Unknown argument: $1"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z "$NAME" ]]; then
+ usage
+ exit 1
+fi
+
+DISK="${LABDIR}/${NAME}.qcow2"
+XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")"
+cleanup() { rm -f "$XML_TMP"; }
+trap cleanup EXIT
+
+# --- Resolve base selection ---
+resolve_base() {
+ local sel="$1"
+ if [[ "$sel" == "template" ]]; then
+ echo "$TEMPLATE_BASE"
+ return 0
+ fi
+
+ if [[ "$sel" == "work01" ]]; then
+ # Pick newest promoted work01 base (must exist first)
+ local latest
+ latest="$(ls -1t "$BASEDIR"/work01-base-*.qcow2 2>/dev/null | head -n 1 || true)"
+ if [[ -z "$latest" ]]; then
+ echo "ERROR: No work01 base found in $BASEDIR (expected work01-base-*.qcow2)."
+ echo "Action: run your promote script to create one."
+ return 1
+ fi
+ echo "$latest"
+ return 0
+ fi
+
+ # Absolute path base
+ if [[ "$sel" == /* ]]; then
+ echo "$sel"
+ return 0
+ fi
+
+ echo "ERROR: Invalid --base '$sel' (use template|work01|/abs/path.qcow2)"
+ return 1
+}
+
+BASE="$(resolve_base "$BASESEL")"
+
+# --- Preconditions (read-only checks) ---
+if [[ ! -d "$LABDIR" ]]; then
+ echo "ERROR: Lab directory missing: $LABDIR"
+ exit 1
+fi
+
+if ! sudo test -r "$BASE"; then
+ echo "ERROR: Base image not readable (via sudo): $BASE"
+ exit 1
+fi
+
+if [[ -e "$DISK" ]]; then
+ echo "ERROR: Disk already exists: $DISK"
+ exit 1
+fi
+
+if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then
+ echo "ERROR: Domain already exists: $NAME"
+ exit 1
+fi
+
+if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then
+ echo "ERROR: Template domain not found in $URI: $TEMPLATE_DOMAIN"
+ exit 1
+fi
+
+# --- Create overlay disk ---
+echo "Using base: $BASE"
+echo "Creating overlay disk: $DISK"
+sudo qemu-img create -f qcow2 -F qcow2 -b "$BASE" "$DISK" >/dev/null
+sudo chown libvirt-qemu:libvirt "$DISK"
+sudo chmod 0660 "$DISK"
+
+echo "Validating backing file..."
+sudo qemu-img info "$DISK" | grep -E "file format:|virtual size:|backing file:" || true
+
+# --- Clone domain XML ---
+echo "Cloning domain XML from: $TEMPLATE_DOMAIN"
+virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP"
+
+# Set name, disk path, and force NAT-only network "default"
+sed -i \
+ -e "s|<name>${TEMPLATE_DOMAIN}</name>|<name>${NAME}</name>|" \
+ -e "s|${TEMPLATE_BASE}|${DISK}|g" \
+ -e "s|<source network='[^']*'|<source network='default'|g" \
+ "$XML_TMP"
+
+# Remove UUID to avoid collision with template
+sed -i -E '/<uuid>[^<]*<\/uuid>/d' "$XML_TMP"
+
+# Remove any host filesystem passthrough (if present)
+perl -0777 -i -pe 's/<filesystem[\s\S]*?<\/filesystem>\n?//g' "$XML_TMP"
+
+# --- Define domain ---
+echo "Defining domain: $NAME"
+virsh -c "$URI" define "$XML_TMP" >/dev/null
+
+# --- Optional start ---
+if [[ "$START" == "yes" ]]; then
+ echo "Starting domain: $NAME"
+ virsh -c "$URI" start "$NAME" >/dev/null
+fi
+
+echo "OK: Lab '$NAME' created (disk: $DISK)."
+if [[ "$START" != "yes" ]]; then
+ echo "To start: virsh -c $URI start $NAME"
+fi
+
diff --git a/kvm-lab-create.bak b/kvm-lab-create.bak
new file mode 100755
index 0000000..43d2926
--- /dev/null
+++ b/kvm-lab-create.bak
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+URI="qemu:///system"
+
+IMGROOT="/var/lib/libvirt/images"
+BASEDIR="$IMGROOT/base"
+LABDIR="$IMGROOT/lab"
+
+TEMPLATE_DOMAIN="debian13-template"
+TEMPLATE_BASE="$BASEDIR/debian13-template.qcow2"
+
+usage() {
+ echo "Usage: kvm-lab-create <lab-name> [--base template|work01|/abs/path.qcow2] [--start]"
+ echo "Examples:"
+ echo " kvm-lab-create lab10 --base template --start"
+ echo " kvm-lab-create lab11 --base work01 --start"
+ echo " kvm-lab-create lab12 --base /var/lib/libvirt/images/base/custom.qcow2 --start"
+}
+
+# --- Parse args ---
+NAME="${1:-}"
+shift || true
+
+BASESEL="template"
+START="no"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --base)
+ BASESEL="${2:-}"
+ shift 2
+ ;;
+ --start)
+ START="yes"
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "ERROR: Unknown argument: $1"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z "$NAME" ]]; then
+ usage
+ exit 1
+fi
+
+DISK="${LABDIR}/${NAME}.qcow2"
+XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")"
+cleanup() { rm -f "$XML_TMP"; }
+trap cleanup EXIT
+
+# --- Resolve base selection ---
+resolve_base() {
+ local sel="$1"
+ if [[ "$sel" == "template" ]]; then
+ echo "$TEMPLATE_BASE"
+ return 0
+ fi
+
+ if [[ "$sel" == "work01" ]]; then
+ # Pick newest promoted work01 base (must exist first)
+ local latest
+ latest="$(ls -1t "$BASEDIR"/work01-base-*.qcow2 2>/dev/null | head -n 1 || true)"
+ if [[ -z "$latest" ]]; then
+ echo "ERROR: No work01 base found in $BASEDIR (expected work01-base-*.qcow2)."
+ echo "Action: run your promote script to create one."
+ return 1
+ fi
+ echo "$latest"
+ return 0
+ fi
+
+ # Absolute path base
+ if [[ "$sel" == /* ]]; then
+ echo "$sel"
+ return 0
+ fi
+
+ echo "ERROR: Invalid --base '$sel' (use template|work01|/abs/path.qcow2)"
+ return 1
+}
+
+BASE="$(resolve_base "$BASESEL")"
+
+# --- Preconditions (read-only checks) ---
+if [[ ! -d "$LABDIR" ]]; then
+ echo "ERROR: Lab directory missing: $LABDIR"
+ exit 1
+fi
+
+if ! sudo test -r "$BASE"; then
+ echo "ERROR: Base image not readable (via sudo): $BASE"
+ exit 1
+fi
+
+if [[ -e "$DISK" ]]; then
+ echo "ERROR: Disk already exists: $DISK"
+ exit 1
+fi
+
+if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then
+ echo "ERROR: Domain already exists: $NAME"
+ exit 1
+fi
+
+if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then
+ echo "ERROR: Template domain not found in $URI: $TEMPLATE_DOMAIN"
+ exit 1
+fi
+
+# --- Create overlay disk ---
+echo "Using base: $BASE"
+echo "Creating overlay disk: $DISK"
+sudo qemu-img create -f qcow2 -F qcow2 -b "$BASE" "$DISK" >/dev/null
+sudo chown libvirt-qemu:libvirt "$DISK"
+sudo chmod 0660 "$DISK"
+
+echo "Validating backing file..."
+sudo qemu-img info "$DISK" | grep -E "file format:|virtual size:|backing file:" || true
+
+# --- Clone domain XML ---
+echo "Cloning domain XML from: $TEMPLATE_DOMAIN"
+virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP"
+
+# Set name, disk path, and force NAT-only network "default"
+sed -i \
+ -e "s|<name>${TEMPLATE_DOMAIN}</name>|<name>${NAME}</name>|" \
+ -e "s|${TEMPLATE_BASE}|${DISK}|g" \
+ -e "s|<source network='[^']*'|<source network='default'|g" \
+ "$XML_TMP"
+
+# Remove UUID to avoid collision with template
+sed -i -E '/<uuid>[^<]*<\/uuid>/d' "$XML_TMP"
+
+# Remove any host filesystem passthrough (if present)
+perl -0777 -i -pe 's/<filesystem[\s\S]*?<\/filesystem>\n?//g' "$XML_TMP"
+
+# --- Define domain ---
+echo "Defining domain: $NAME"
+virsh -c "$URI" define "$XML_TMP" >/dev/null
+
+# --- Optional start ---
+if [[ "$START" == "yes" ]]; then
+ echo "Starting domain: $NAME"
+ virsh -c "$URI" start "$NAME" >/dev/null
+fi
+
+echo "OK: Lab '$NAME' created (disk: $DISK)."
+if [[ "$START" != "yes" ]]; then
+ echo "To start: virsh -c $URI start $NAME"
+fi
+
diff --git a/kvm-lab-destroy b/kvm-lab-destroy
new file mode 100755
index 0000000..3548ef9
--- /dev/null
+++ b/kvm-lab-destroy
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+URI="qemu:///system"
+LABDIR="/var/lib/libvirt/images/lab"
+
+NAME="${1:-}"
+
+if [[ -z "$NAME" ]]; then
+ echo "Usage: kvm-lab-destroy <lab-name>"
+ exit 1
+fi
+
+if [[ "$NAME" == "work01" || "$NAME" == "debian13-template" ]]; then
+ echo "Refusing to destroy protected VM: $NAME"
+ exit 1
+fi
+
+if ! virsh -c "$URI" dominfo "$NAME" &>/dev/null; then
+ echo "Domain not found: $NAME"
+ exit 1
+fi
+
+DISK="$(virsh -c "$URI" domblklist "$NAME" --details | awk '$3=="vda"{print $4}')"
+
+if [[ -z "$DISK" || "$DISK" != "$LABDIR/"* ]]; then
+ echo "Refusing to destroy: disk not under $LABDIR"
+ echo "Detected disk: $DISK"
+ exit 1
+fi
+
+STATE="$(virsh -c "$URI" domstate "$NAME")"
+
+if [[ "$STATE" == "running" ]]; then
+ echo "Shutting down $NAME..."
+ virsh -c "$URI" shutdown "$NAME"
+ sleep 3
+fi
+
+if [[ "$(virsh -c "$URI" domstate "$NAME")" == "running" ]]; then
+ echo "Force stopping $NAME..."
+ virsh -c "$URI" destroy "$NAME"
+fi
+
+echo "Undefining $NAME..."
+virsh -c "$URI" undefine "$NAME"
+
+echo "Deleting disk: $DISK"
+sudo rm -v -- "$DISK"
+
+echo "Lab '$NAME' destroyed."
+
diff --git a/kvm-lab-status b/kvm-lab-status
new file mode 100755
index 0000000..2303995
--- /dev/null
+++ b/kvm-lab-status
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+URI="qemu:///system"
+IMGROOT="/var/lib/libvirt/images"
+BASEDIR="$IMGROOT/base"
+LABDIR="$IMGROOT/lab"
+
+hr() { printf '%*s\n' "${COLUMNS:-80}" '' | tr ' ' '-'; }
+
+echo "KVM LAB STATUS"
+hr
+echo "Time: $(date -Is)"
+echo "Host: $(hostname)"
+echo
+
+echo "Libvirt domains ($URI):"
+hr
+virsh -c "$URI" list --all || true
+echo
+
+echo "Lab VMs (names starting with 'lab'):"
+hr
+virsh -c "$URI" list --all | awk 'NR==1||NR==2{print;next} $2 ~ /^lab/ {print}' || true
+echo
+
+echo "Lab disk inventory ($LABDIR):"
+hr
+if [[ -d "$LABDIR" ]]; then
+ sudo ls -lh --time-style=long-iso "$LABDIR" 2>/dev/null || ls -lh "$LABDIR"
+else
+ echo "Missing: $LABDIR"
+fi
+echo
+
+echo "Base images ($BASEDIR):"
+hr
+if [[ -d "$BASEDIR" ]]; then
+ ls -lh --time-style=long-iso "$BASEDIR" 2>/dev/null || ls -lh "$BASEDIR"
+else
+ echo "Missing: $BASEDIR"
+fi
+echo
+
+echo "Selected base candidates:"
+hr
+TEMPLATE="$BASEDIR/debian13-template.qcow2"
+LATEST_WORK01="$(ls -1t "$BASEDIR"/work01-base-*.qcow2 2>/dev/null | head -n 1 || true)"
+
+if [[ -f "$TEMPLATE" ]]; then
+ echo "template: $TEMPLATE"
+else
+ echo "template: MISSING ($TEMPLATE)"
+fi
+
+if [[ -n "$LATEST_WORK01" ]]; then
+ echo "work01 (latest): $LATEST_WORK01"
+else
+ echo "work01 (latest): none found ($BASEDIR/work01-base-*.qcow2)"
+fi
+echo
+
+echo "Libvirt networks:"
+hr
+virsh -c "$URI" net-list --all || true
+echo
+
diff --git a/kvm-promote-to-base b/kvm-promote-to-base
new file mode 100755
index 0000000..93e703e
--- /dev/null
+++ b/kvm-promote-to-base
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+URI="qemu:///system"
+IMGROOT="/var/lib/libvirt/images"
+BASEDIR="$IMGROOT/base"
+SNAPROOT="$HOME/.backup/kvm-labs"
+
+usage() {
+ echo "Usage: kvm-promote-to-base <vm-name> [--disk vda]"
+ echo "Creates: $BASEDIR/<vm-name>-base-YYYY-MM-DD_HHMMSS.qcow2"
+ exit 1
+}
+
+VM="${1:-}"
+DISKTGT="vda"
+
+shift || true
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --disk)
+ DISKTGT="${2:-}"
+ shift 2
+ ;;
+ -h|--help)
+ usage
+ ;;
+ *)
+ echo "ERROR: Unknown arg: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ -z "$VM" ]]; then
+ usage
+fi
+
+if [[ ! -d "$BASEDIR" ]]; then
+ echo "ERROR: Base directory missing: $BASEDIR"
+ exit 1
+fi
+
+state="$(virsh -c "$URI" domstate "$VM" 2>/dev/null | tr -d '\r' || true)"
+if [[ "$state" != "shut off" ]]; then
+ echo "ERROR: VM must be shut off for consistent promotion."
+ echo "VM: $VM"
+ echo "State: ${state:-unknown}"
+ exit 1
+fi
+
+DISK="$(virsh -c "$URI" domblklist "$VM" --details | awk -v t="$DISKTGT" '$3==t{print $4}')"
+if [[ -z "$DISK" || ! -f "$DISK" ]]; then
+ echo "ERROR: Cannot locate disk for $VM target=$DISKTGT"
+ echo "Detected: '$DISK'"
+ exit 1
+fi
+
+TS="$(date +%F_%H%M%S)"
+SNAPDIR="$SNAPROOT/${TS}-promote-${VM}"
+NEWBASE="$BASEDIR/${VM}-base-${TS}.qcow2"
+
+mkdir -p "$SNAPDIR"
+
+# Snapshot metadata (read-only)
+virsh -c "$URI" dumpxml "$VM" > "$SNAPDIR/${VM}.xml"
+sudo qemu-img info --backing-chain "$DISK" > "$SNAPDIR/${VM}.${DISKTGT}.backing-chain.txt" || true
+sudo ls -l "$DISK" > "$SNAPDIR/${VM}.${DISKTGT}.ls.txt" || true
+
+echo "Creating base image:"
+echo " VM: $VM"
+echo " Disk: $DISK (target: $DISKTGT)"
+echo " Target: $NEWBASE"
+
+sudo qemu-img convert -O qcow2 "$DISK" "$NEWBASE"
+sudo chown libvirt-qemu:libvirt "$NEWBASE"
+sudo chmod 0660 "$NEWBASE"
+
+echo "Verifying new base has no backing file..."
+if sudo qemu-img info "$NEWBASE" | grep -q '^backing file:'; then
+ echo "ERROR: New base unexpectedly has a backing file. Aborting."
+ exit 1
+fi
+
+echo "SUCCESS: $NEWBASE"
+echo "Snapshot: $SNAPDIR"
+
diff --git a/lib/bashsimplecurses b/lib/bashsimplecurses
new file mode 160000
+Subproject 006b9cafd4d381b693c5e4f55140f2834cf3087
diff --git a/luks-shield b/luks-shield
new file mode 100755
index 0000000..9080f35
--- /dev/null
+++ b/luks-shield
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+UUID="6659e638-d52a-4c32-9781-9dcedd44db35"
+NAME="shield"
+MNT="/mnt/shield"
+
+DEV_BY_UUID="/dev/disk/by-uuid/${UUID}"
+MAPPER="/dev/mapper/${NAME}"
+
+die() { echo "Error: $*" >&2; exit 1; }
+
+need_cmd() {
+ command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"
+}
+
+resolve_blockdev() {
+ # Resolve the UUID symlink to an absolute /dev/... node
+ local target
+ target="$(readlink -f "$DEV_BY_UUID")" || return 1
+ [[ -b "$target" ]] || return 1
+ echo "$target"
+}
+
+is_mapped() {
+ [[ -e "$MAPPER" ]]
+}
+
+is_mounted() {
+ mountpoint -q "$MNT"
+}
+
+show_busy() {
+ echo "Mount appears busy. Processes using $MNT:"
+ # lsof is optional but useful; show what we can.
+ if command -v lsof >/dev/null 2>&1; then
+ sudo lsof +f -- "$MNT" || true
+ else
+ echo "(Install lsof: sudo apt install lsof)"
+ sudo fuser -vm "$MNT" || true
+ fi
+}
+
+do_open() {
+ need_cmd cryptsetup
+ need_cmd mount
+ need_cmd mountpoint
+ need_cmd readlink
+
+ [[ -e "$DEV_BY_UUID" ]] || die "UUID device not found: $DEV_BY_UUID (is the drive plugged in?)"
+
+ local dev
+ dev="$(resolve_blockdev)" || die "Could not resolve block device for UUID: $UUID"
+
+ if is_mapped; then
+ echo "Mapper already exists: $MAPPER"
+ else
+ sudo cryptsetup open "$dev" "$NAME"
+ fi
+
+ if [[ ! -d "$MNT" ]]; then
+ sudo mkdir -p "$MNT"
+ fi
+
+ if is_mounted; then
+ echo "Already mounted: $MNT"
+ else
+ sudo mount "$MAPPER" "$MNT"
+ fi
+
+ sudo sync
+ echo "OK: unlocked and mounted at $MNT"
+}
+
+do_close() {
+ need_cmd cryptsetup
+ need_cmd umount
+ need_cmd mountpoint
+ need_cmd readlink
+
+ if is_mounted; then
+ sudo sync
+ if ! sudo umount "$MNT"; then
+ show_busy
+ die "Unmount failed"
+ fi
+ fi
+
+ if is_mapped; then
+ sudo cryptsetup close "$NAME"
+ fi
+
+ # Best-effort power-off
+ if [[ -e "$DEV_BY_UUID" ]]; then
+ local dev
+ dev="$(resolve_blockdev)" || true
+ if [[ -n "${dev:-}" ]]; then
+ if command -v udisksctl >/dev/null 2>&1; then
+ sudo udisksctl power-off -b "$dev" || true
+ fi
+ fi
+ fi
+
+ echo "OK: unmounted, closed, and powered off (if supported)"
+}
+
+do_status() {
+ echo "UUID device: $DEV_BY_UUID"
+ if [[ -e "$DEV_BY_UUID" ]]; then
+ echo "Resolved: $(readlink -f "$DEV_BY_UUID")"
+ else
+ echo "Resolved: (not present)"
+ fi
+
+ echo -n "Mapper: "
+ if is_mapped; then echo "OPEN ($MAPPER)"; else echo "CLOSED"; fi
+
+ echo -n "Mount: "
+ if is_mounted; then echo "MOUNTED ($MNT)"; else echo "NOT MOUNTED"; fi
+}
+
+usage() {
+ cat <<EOF
+Usage: $0 {open|close|status}
+
+open - unlock LUKS by UUID and mount to $MNT
+close - sync, unmount, close mapper, power-off block device (best-effort)
+status - show current state
+EOF
+}
+
+main() {
+ case "${1:-}" in
+ open) do_open ;;
+ close) do_close ;;
+ status) do_status ;;
+ *) usage; exit 2 ;;
+ esac
+}
+
+main "$@"
+
diff --git a/nextcloud b/nextcloud
new file mode 100644
index 0000000..fae9bdb
--- /dev/null
+++ b/nextcloud
@@ -0,0 +1 @@
+aHR0cHM6Ly9jbG91ZC5sYWJ1bml4Lnh5ejo5NDQzCmxvZ2luOiBhZGFtCm1haWw6IGFkYW0uZ29tdWxrYTk0QGdtYWlsLmNvbSAKaGFzxYJvOiAgYk9PP2BAZD15S1Q5XmNWYAoKdXN0YXcgc29iaWUgMkZBIHBvdGVtIGRsYSBiZXpwaWVjemXFhHN0d2E=
diff --git a/officium b/officium
new file mode 100755
index 0000000..fae202f
--- /dev/null
+++ b/officium
@@ -0,0 +1,214 @@
+#!/bin/bash
+
+# Scriptum ad preces e Divinum Officium extrahendas
+# Auctor: Adam Kasprzak
+# Data: 2026-01-13
+
+set -o pipefail
+
+if [ $# -eq 0 ]; then
+ echo "Usus: $0 [laudes|prima|tertia|sexta|nona|vesperae]"
+ exit 1
+fi
+
+HORA_INPUT="$1"
+HORA="$(echo "${HORA_INPUT:0:1}" | tr '[:lower:]' '[:upper:]')$(echo "${HORA_INPUT:1}" | tr '[:upper:]' '[:lower:]')"
+
+case "$HORA" in
+ Laudes|Prima|Tertia|Sexta|Nona|Vesperae) ;;
+ *)
+ echo "Error: Argumentum invalidum: '$HORA_INPUT'"
+ echo "Valida: laudes, prima, tertia, sexta, nona, vesperae"
+ exit 1
+ ;;
+esac
+
+BASE_URL="https://www.divinumofficium.com/cgi-bin/horas/officium.pl"
+URL="${BASE_URL}?command=pray${HORA}"
+
+USE_COLOR=1
+if [ ! -t 1 ] || [ "${NO_COLOR:-0}" = "1" ]; then
+ USE_COLOR=0
+fi
+
+WRAP="${WRAP:-80}" # word-wrap width (no justify)
+
+echo
+echo
+echo " ?8888P"
+echo " \`88'"
+echo " 8b,_ 88 _,d8"
+echo " 88888SEAL88888"
+echo " 8P~ 88 ~?8"
+echo " ,88."
+echo " d8888b"
+
+echo "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-"
+echo "Hora: $HORA"
+echo "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-"
+echo
+
+HTML="$(curl -s "$URL")"
+if [ $? -ne 0 ] || [ -z "$HTML" ]; then
+ echo "Error: Pagina recipi non potuit (curl error vel responsum vacuum)."
+ exit 1
+fi
+
+echo "$HTML" | awk '
+ /<FONT SIZE=.+1. COLOR="red"><B><I>Incipit<\/I><\/B><\/FONT>/ { printing=1 }
+ printing {
+ if (/<TD VALIGN=.TOP. WIDTH=.50%.><DIV ALIGN=.right.><FONT SIZE=.1. COLOR=.green.>[0-9]+/) in_english=1
+ if (/<\/TR>/) { in_english=0; print "" }
+ if (!in_english) print
+ }
+ /<\/TABLE>/ && printing { exit }
+' | \
+ sed 's/<[^>]*>//g' | \
+ sed -E 's/^[0-9]+:[0-9]+[[:space:]]*//' | \
+ sed 's/&nbsp;/ /g' | \
+ sed 's/&ensp;/ /g' | \
+ sed 's/&lt;/</g' | \
+ sed 's/&gt;/>/g' | \
+ sed 's/&amp;/\&/g' | \
+ sed 's/&#[0-9]*;//g' | \
+ # remove parentheses ranges after Psalm number: Psalmus 54(17-24) -> Psalmus 54
+ sed -E 's/^(Psalmus[[:space:]]+[0-9]+)\([^)]*\)/\1/' | \
+ cat -s | \
+ grep -v "^Top" | \
+ grep -v "^Next" | \
+ grep -v "^[0-9]$" | \
+ # blank lines between antiphon and psalm title; and before closing antiphon after V/. R/.
+ awk '
+ function is_ant(s) { return (s ~ /^Ant\./) }
+ function is_psalm(s) { return (s ~ /^Psalmus[[:space:]]+[0-9]+/) }
+ function is_vr(s) { return (s ~ /^[VR]\.\//) }
+
+ {
+ cur = $0
+ if (is_psalm(cur) && is_ant(prev) && prev != "") {
+ print prev
+ print ""
+ print cur
+ prev=""
+ next
+ }
+ if (is_ant(cur) && is_vr(prev) && prev != "") {
+ print prev
+ print ""
+ print cur
+ prev=""
+ next
+ }
+ if (prev != "") print prev
+ prev = cur
+ }
+ END { if (prev != "") print prev }
+ ' | \
+ # word-wrap (no justify), preserve indentation
+ awk -v wrap="$WRAP" '
+ function ltrim(s){ sub(/^[[:space:]]+/, "", s); return s }
+ function rtrim(s){ sub(/[[:space:]]+$/, "", s); return s }
+ function trim(s){ return rtrim(ltrim(s)) }
+
+ function wrap_line(indent, text, limit, n,i,words,line,tmp){
+ text = trim(text)
+ if (text == "") { print ""; return }
+ gsub(/[[:space:]]+/, " ", text)
+
+ n = split(text, words, " ")
+ line = ""
+ for (i=1; i<=n; i++){
+ tmp = (line == "" ? words[i] : line " " words[i])
+ if (length(tmp) <= limit) {
+ line = tmp
+ } else {
+ print indent line
+ line = words[i]
+ }
+ }
+ if (line != "") print indent line
+ }
+
+ {
+ raw = $0
+ if (raw ~ /^[[:space:]]*$/) { print ""; next }
+
+ # keep existing indentation
+ indent = ""
+ match(raw, /^[[:space:]]*/)
+ indent = substr(raw, RSTART, RLENGTH)
+ text = substr(raw, RLENGTH+1)
+
+ # wrap only if needed
+ if (length(raw) > wrap) {
+ lim = wrap - length(indent)
+ if (lim < 20) lim = 20
+ wrap_line(indent, text, lim)
+ } else {
+ print raw
+ }
+ }
+ ' | \
+ # pretty print (colors + indentation unchanged; grey for [] and {})
+ awk -v use_color="$USE_COLOR" '
+ BEGIN{
+ reset="\033[0m"; bold="\033[1m"; dim="\033[2m"
+ red="\033[31m"; green="\033[32m"; yellow="\033[33m"
+ blue="\033[34m"; mag="\033[35m"; cyan="\033[36m"
+ gray="\033[90m"
+ if (!use_color) { reset=bold=dim=red=green=yellow=blue=mag=cyan=gray="" }
+ }
+
+ function trim(s){ gsub(/^[[:space:]]+|[[:space:]]+$/, "", s); return s }
+
+ function gray_blocks(s, cur, rep){
+ rep = gray "&" cur
+ gsub(/\[[^]]+\]/, rep, s)
+ gsub(/\{[^}]*\}/, rep, s)
+ return s
+ }
+
+ function blank_before_if_needed(t){
+ if (t ~ /^(Oremus\.?|Conclusio)$/ && prev_blank == 0) print ""
+ }
+
+ {
+ raw = $0
+
+ if (raw ~ /^[[:space:]]*$/) { print ""; prev_blank=1; next }
+
+ t = trim(raw)
+ blank_before_if_needed(t)
+ prev_blank=0
+
+ prefix=""
+
+ # headings in RED
+ if (t == "Incipit" || t == "Hymnus") {
+ prefix = bold red
+ } else if (t ~ /^Psalmus[[:space:]]+[0-9]+/) {
+ prefix = bold red
+ } else if (t ~ /^Psalmi[[:space:]]*\{/) {
+ prefix = dim gray
+ } else if (t == "Conclusio") {
+ prefix = bold red
+ } else if (t ~ /^Ant\./) {
+ prefix = mag
+ } else if (t ~ /^[VR]\.\//) {
+ prefix = green
+ } else if (t ~ /^Gl(ó|o)ria Patri,/) {
+ prefix = blue
+ } else if (t ~ /^Sicut erat/) {
+ prefix = blue
+ }
+
+ line = gray_blocks(raw, prefix)
+ print prefix line reset
+ }
+ '
+
+echo
+echo "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-"
+echo "Finis Horae: $HORA"
+echo "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-"
+
diff --git a/officium_readme.md b/officium_readme.md
new file mode 100644
index 0000000..b4e00a5
--- /dev/null
+++ b/officium_readme.md
@@ -0,0 +1,135 @@
+# Log Projektu: Skrypt Bash do Divinumofficium.com
+
+## Zapytanie użytkownika:
+Utworzyć skrypt bash który:
+- Przyjmuje jeden argument ze zbioru: [Prima, Tertia, Sexta, Nona, Vesperae]
+- Pobiera dane ze strony https://www.divinumofficium.com/cgi-bin/horas/officium.pl
+- Znajduje linki ukryte pod hover dla wybranej modlitwy (Laudes, Prima, Tertia, Sexta, Nona, Vesperae)
+- Pobiera tekst od części "Incipit" aż do końca części "Conclusio"
+- Wyświetla zawartość w terminalu
+
+## Krok 1: Analiza struktury strony
+Data: 2026-01-13 22:12
+
+Pobieram główną stronę aby zrozumieć strukturę HTML i znaleźć linki do poszczególnych modlitw.
+
+### Wyniki analizy:
+- Strona używa parametru `command=pray{NazwaModlitwy}` w URL
+- Przykład: `officium.pl?command=prayPrima`
+- Treść modlitwy znajduje się w tabeli HTML
+- Sekcja "Incipit" to początek (oznaczona znacznikiem `<FONT SIZE='+1' COLOR="red"><B><I>Incipit</I></B></FONT>`)
+- Sekcja "Conclusio" to koniec (analogicznie oznaczona)
+- Treść jest w formacie HTML z tagami FONT, BR, itd.
+
+## Krok 2: Tworzenie skryptu bash
+Data: 2026-01-13 22:12
+
+Tworzę skrypt który:
+1. Waliduje argument wejściowy
+2. Buduje URL z odpowiednim parametrem command
+3. Pobiera stronę za pomocą curl
+4. Ekstrahuje treść od "Incipit" do końca "Conclusio"
+5. Czyści HTML i formatuje dla terminala
+
+### Pierwsze podejście:
+- Utworzono plik `divinumofficium.sh`
+- Użyto sed do ekstrakcji, ale nie działało prawidłowo
+
+### Poprawki:
+- Zmieniono na awk, który czyta od Incipit do zamknięcia </TABLE>
+- Dodano filtry do usunięcia linków nawigacyjnych (Top, Next)
+- Dodano dekodowanie encji HTML
+
+## Krok 3: Testowanie skryptu
+Data: 2026-01-13 22:12
+
+### Testy przeprowadzone:
+1. ✅ `./divinumofficium.sh Prima` - działa poprawnie
+2. ✅ `./divinumofficium.sh Vesperae` - działa poprawnie
+3. ✅ `./divinumofficium.sh Sexta` - działa poprawnie
+4. ✅ Brak argumentu - wyświetla instrukcję użycia
+5. ✅ Nieprawidłowy argument (Laudes) - wyświetla błąd
+
+### Wnioski:
+Skrypt działa zgodnie z wymaganiami:
+- Przyjmuje argumenty: Prima, Tertia, Sexta, Nona, Vesperae
+- Pobiera treść od "Incipit" do końca "Conclusio"
+- Wyświetla czytelnie w terminalu (z tekstem łacińskim i angielskim)
+- Waliduje dane wejściowe
+- Obsługuje błędy połączenia
+
+## Podsumowanie
+Data: 2026-01-13 22:13
+
+Utworzono działający skrypt bash `divinumofficium.sh`, który:
+- Lokalizacja: `/home/adam/copilot_workspace/kasprzak_project/divinumofficium.sh`
+- Rozmiar: ~1.8KB
+- Uprawnienia: wykonywalne (chmod +x)
+- Status: UKOŃCZONE ✅
+
+## Krok 4: Modyfikacja obsługi argumentów
+Data: 2026-01-13 22:17
+
+### Zapytanie użytkownika:
+"popraw aby argumenty pisać małymi literami"
+
+### Zmiany wprowadzone:
+- Argumenty przyjmowane są teraz małymi literami: prima, tertia, sexta, nona, vesperae
+- Dodano automatyczną konwersję: pierwsza litera na wielką (bo API wymaga Prima, Tertia, etc.)
+- Zaktualizowano komunikaty pomocy i błędów
+- Zachowano zgodność z API (nadal wysyła "Prima", "Tertia", etc.)
+
+### Test:
+Teraz użytkownik może pisać: `./divinumofficium.sh prima` zamiast `./divinumofficium.sh Prima`
+
+## Krok 5: Filtrowanie tylko tekstu łacińskiego
+Data: 2026-01-13 22:22
+
+### Zapytanie użytkownika:
+"wynik jest po łacinie i angielsku. Ma być tylko część po łacinie"
+
+### Analiza problemu:
+Strona HTML zawiera tabelę z dwiema kolumnami:
+- Pierwsza kolumna (`<TD WIDTH='50%'>`): tekst łaciński
+- Druga kolumna (`<TD WIDTH='50%'>`): tekst angielski
+
+### Zmiany wprowadzone:
+- Zmodyfikowano skrypt awk aby pomijać drugą kolumnę (angielską)
+- Dodano wykrywanie początku drugiej kolumny (po numerze sekcji)
+- Dodano filtr usuwający pojedyncze cyfry (numery sekcji)
+- Zachowano tylko tekst łaciński w wyniku
+
+### Test:
+```bash
+./divinumofficium.sh prima # Wyświetla tylko tekst łaciński
+./divinumofficium.sh vesperae # Wyświetla tylko tekst łaciński
+```
+
+Wynik zawiera teraz wyłącznie modlitwy w języku łacińskim.
+
+## Krok 6: Dodanie odstępów między sekcjami
+Data: 2026-01-13 22:28
+
+### Zapytanie użytkownika:
+"może dodaj jeszcze wolną linię między częściami <td>"
+
+### Zmiany wprowadzone:
+- Dodano pustą linię po każdym `</TR>` (koniec sekcji w tabeli)
+- Użyto `cat -s` aby zredukować wielokrotne puste linie do maksymalnie dwóch
+- Sekcje są teraz lepiej wizualnie oddzielone
+
+### Efekt:
+Każda sekcja modlitwy (Incipit, Hymnus, Psalmi, Lectio brevis, Conclusio, etc.) jest oddzielona pustą linią, co poprawia czytelność w terminalu.
+
+Przykład:
+```
+Incipit
+
+℣. Deus ✠ in adiutórium...
+
+Hymnus
+
+Iam lucis orto sídere...
+
+Psalmi {ex Psalterio...
+```
diff --git a/passmenu b/passmenu
new file mode 100755
index 0000000..76d92ab
--- /dev/null
+++ b/passmenu
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+shopt -s nullglob globstar
+
+typeit=0
+if [[ $1 == "--type" ]]; then
+ typeit=1
+ shift
+fi
+
+if [[ -n $WAYLAND_DISPLAY ]]; then
+ dmenu=dmenu-wl
+ xdotool="ydotool type --file -"
+elif [[ -n $DISPLAY ]]; then
+ dmenu=dmenu
+ xdotool="xdotool type --clearmodifiers --file -"
+else
+ echo "Error: No Wayland or X11 display detected" >&2
+ exit 1
+fi
+
+prefix=${PASSWORD_STORE_DIR-~/.password-store}
+password_files=( "$prefix"/**/*.gpg )
+password_files=( "${password_files[@]#"$prefix"/}" )
+password_files=( "${password_files[@]%.gpg}" )
+
+password=$(printf '%s\n' "${password_files[@]}" | "$dmenu" "$@")
+
+[[ -n $password ]] || exit
+
+if [[ $typeit -eq 0 ]]; then
+ pass show -c "$password" 2>/dev/null
+else
+ pass show "$password" | { IFS= read -r pass; printf %s "$pass"; } | $xdotool
+fi
diff --git a/passmenu-otp b/passmenu-otp
new file mode 100755
index 0000000..d511944
--- /dev/null
+++ b/passmenu-otp
@@ -0,0 +1,24 @@
+#!/bin/sh
+set -eu
+
+: "${PASSWORD_STORE_DIR:=$HOME/.password-store}"
+DMENU="${DMENU:-dmenu}"
+
+# Build list of pass entry paths (relative to PASSWORD_STORE_DIR) that contain an otpauth:// line.
+entries=$(
+ find "$PASSWORD_STORE_DIR" -type f -name '*.gpg' ! -path '*/.git/*' -print 2>/dev/null |
+ while IFS= read -r f; do
+ entry="${f#$PASSWORD_STORE_DIR/}"
+ entry="${entry%.gpg}"
+ if pass show "$entry" 2>/dev/null | grep -q '^otpauth://'; then
+ printf '%s\n' "$entry"
+ fi
+ done
+)
+
+[ -n "${entries:-}" ] || exit 0
+
+choice=$(printf '%s\n' "$entries" | "$DMENU" -i -p "OTP:")
+[ -n "${choice:-}" ] || exit 0
+
+pass otp -c "$choice"
diff --git a/proton-bridge-cert-refresh b/proton-bridge-cert-refresh
new file mode 100755
index 0000000..dc3be73
--- /dev/null
+++ b/proton-bridge-cert-refresh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CERT_DIR="$HOME/.config/isync"
+CERT_FILE="$CERT_DIR/proton-bridge.pem"
+
+mkdir -p "$CERT_DIR"
+chmod 700 "$CERT_DIR"
+
+# Ensure Bridge port is up (wait up to 30s)
+for i in $(seq 1 30); do
+ if ss -ltn '( sport = :1143 )' | grep -q 1143; then
+ break
+ fi
+ sleep 1
+done
+
+if ! ss -ltn '( sport = :1143 )' | grep -q 1143; then
+ echo "Bridge not listening on 127.0.0.1:1143" >&2
+ exit 1
+fi
+
+tmp="$(mktemp)"
+
+openssl s_client -starttls imap -connect 127.0.0.1:1143 -showcerts </dev/null 2>/dev/null \
+| awk '
+ /BEGIN CERTIFICATE/ {c++}
+ c==1 {print}
+ /END CERTIFICATE/ && c==1 {exit}
+' > "$tmp"
+
+# sanity check
+openssl x509 -in "$tmp" -noout >/dev/null 2>&1
+
+mv "$tmp" "$CERT_FILE"
+chmod 600 "$CERT_FILE"
+
+echo "Updated: $CERT_FILE"
+
diff --git a/reset-gpt b/reset-gpt
new file mode 100755
index 0000000..69131a7
--- /dev/null
+++ b/reset-gpt
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Close ChatGPT tabs only
+pkill -f "firefox.*chatgpt.com" 2>/dev/null
+
+PROFILE=$(ls -d ~/.mozilla/firefox/*.default*)
+
+# Delete all ChatGPT site storage
+rm -rf "$PROFILE/storage/default/https+++chatgpt.com"
+rm -rf "$PROFILE/storage/default/https+++chat.openai.com"
+
+# Clear Firefox HTTP cache
+rm -rf "$PROFILE/cache2/*"
+
+# Relaunch Firefox
+nohup firefox-esr >/dev/null 2>&1 &
+
diff --git a/rexi b/rexi
new file mode 120000
index 0000000..946761f
--- /dev/null
+++ b/rexi
@@ -0,0 +1 @@
+/home/lukasz/.local/share/pipx/venvs/rexi/bin/rexi \ No newline at end of file
diff --git a/rofi-vim-power b/rofi-vim-power
new file mode 100755
index 0000000..3673119
--- /dev/null
+++ b/rofi-vim-power
@@ -0,0 +1,26 @@
+#!/bin/sh
+set -eu
+
+DB="$HOME/.local/share/cheat-sheets/vim-power.tsv"
+
+[ -f "$DB" ] || exit 1
+
+choice="$(
+ cat "$DB" |
+ rofi -dmenu -i -p 'Vim power ›'
+)"
+
+[ -n "${choice:-}" ] || exit 0
+
+# First column = keystrokes
+keys="$(printf '%s' "$choice" | cut -f1)"
+
+# Clipboard (Wayland/X11)
+if command -v wl-copy >/dev/null 2>&1; then
+ printf '%s' "$keys" | wl-copy
+elif command -v xclip >/dev/null 2>&1; then
+ printf '%s' "$keys" | xclip -selection clipboard
+else
+ printf '%s\n' "$keys"
+fi
+
diff --git a/sharepoint-mount b/sharepoint-mount
new file mode 100755
index 0000000..cd894cf
--- /dev/null
+++ b/sharepoint-mount
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BASE="/mnt/sharepoint"
+
+declare -A REMOTES=(
+ ["Administration"]="SP_Administration:"
+ ["Operations"]="SP_Operations:"
+ ["SkyscaleCommerce"]="SP_SkyscaleCommerce:"
+)
+
+RCLONE_FLAGS=(
+ --vfs-cache-mode writes
+ --dir-cache-time 1h
+ --poll-interval 1m
+ --umask 022
+ --log-level INFO
+)
+
+is_mounted() { findmnt -rn "$1" >/dev/null 2>&1; }
+
+# Ensure mountpoints exist
+sudo mkdir -p "$BASE"/{Administration,Operations,SkyscaleCommerce}
+sudo chown -R "$USER:$USER" "$BASE"
+
+for name in "${!REMOTES[@]}"; do
+ mp="$BASE/$name"
+ remote="${REMOTES[$name]}"
+
+ if is_mounted "$mp"; then
+ echo "Already mounted: $mp"
+ continue
+ fi
+
+ echo "Mounting: $remote -> $mp"
+ nohup rclone mount "$remote" "$mp" "${RCLONE_FLAGS[@]}" \
+ --log-file "${XDG_RUNTIME_DIR:-/run/user/$UID}/rclone-${name}.log" \
+ >/dev/null 2>&1 &
+done
+
diff --git a/sharepoint-status b/sharepoint-status
new file mode 100755
index 0000000..0404959
--- /dev/null
+++ b/sharepoint-status
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BASE="/mnt/sharepoint"
+MPS=(
+ "$BASE/Administration"
+ "$BASE/Operations"
+ "$BASE/SkyscaleCommerce"
+)
+
+for mp in "${MPS[@]}"; do
+ if findmnt -rn "$mp" >/dev/null 2>&1; then
+ echo "MOUNTED $mp"
+ else
+ echo "OFF $mp"
+ fi
+done
+
diff --git a/sharepoint-umount b/sharepoint-umount
new file mode 100755
index 0000000..ccac033
--- /dev/null
+++ b/sharepoint-umount
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BASE="/mnt/sharepoint"
+MPS=(
+ "$BASE/Administration"
+ "$BASE/Operations"
+ "$BASE/SkyscaleCommerce"
+)
+
+is_mounted() { findmnt -rn "$1" >/dev/null 2>&1; }
+
+# Do not stay inside any mount
+cd /
+
+for mp in "${MPS[@]}"; do
+ if ! is_mounted "$mp"; then
+ echo "Not mounted: $mp"
+ continue
+ fi
+
+ echo "Unmounting: $mp"
+ if ! fusermount3 -u "$mp" 2>/dev/null; then
+ fusermount3 -uz "$mp" 2>/dev/null || sudo umount -l "$mp" 2>/dev/null || {
+ echo "ERROR: failed to unmount $mp"
+ exit 1
+ }
+ fi
+done
+
+# Optional: kill any leftover rclone mount processes still referencing /mnt/sharepoint
+pkill -f "rclone mount.*${BASE}" 2>/dev/null || true
+
diff --git a/typebookmarks b/typebookmarks
new file mode 100755
index 0000000..559075a
--- /dev/null
+++ b/typebookmarks
@@ -0,0 +1,5 @@
+#! /bin/sh
+
+# map this script as a shortcut key for dwm in config.h
+
+xdotool type "$(grep -v '^#' ${HOME}/.snippets | dmenu -i -l 50 | sed 's/\s*#.*//')"
diff --git a/usb-safe b/usb-safe
new file mode 100755
index 0000000..e983360
--- /dev/null
+++ b/usb-safe
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+UUID="bdd2a016-b6db-493d-ad0a-e8dd8a93dfbd"
+NAME="usb_safe"
+MNT="/mnt/usb_safe"
+
+DEV_BY_UUID="/dev/disk/by-uuid/${UUID}"
+MAPPER="/dev/mapper/${NAME}"
+
+die() { echo "Error: $*" >&2; exit 1; }
+
+need_cmd() {
+ command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"
+}
+
+resolve_blockdev() {
+ local target
+ target="$(readlink -f "$DEV_BY_UUID")" || return 1
+ [[ -b "$target" ]] || return 1
+ echo "$target"
+}
+
+is_mapped() { [[ -e "$MAPPER" ]]; }
+is_mounted() { mountpoint -q "$MNT"; }
+
+show_busy() {
+ echo "Mount appears busy. Processes using $MNT:"
+ if command -v lsof >/dev/null 2>&1; then
+ sudo lsof +f -- "$MNT" || true
+ else
+ echo "(Install lsof: sudo apt install lsof)"
+ sudo fuser -vm "$MNT" || true
+ fi
+}
+
+do_open() {
+ need_cmd cryptsetup
+ need_cmd mount
+ need_cmd mountpoint
+ need_cmd readlink
+
+ [[ -e "$DEV_BY_UUID" ]] || die "UUID device not found: $DEV_BY_UUID (is the drive plugged in?)"
+
+ local dev
+ dev="$(resolve_blockdev)" || die "Could not resolve block device for UUID: $UUID"
+
+ if is_mapped; then
+ echo "Mapper already exists: $MAPPER"
+ else
+ sudo cryptsetup open "$dev" "$NAME"
+ fi
+
+ if [[ ! -d "$MNT" ]]; then
+ sudo mkdir -p "$MNT"
+ fi
+
+ if is_mounted; then
+ echo "Already mounted: $MNT"
+ else
+ sudo mount "$MAPPER" "$MNT"
+ fi
+
+ sudo sync
+ echo "OK: unlocked and mounted at $MNT"
+}
+
+do_close() {
+ need_cmd cryptsetup
+ need_cmd umount
+ need_cmd mountpoint
+ need_cmd readlink
+
+ if is_mounted; then
+ sudo sync
+ if ! sudo umount "$MNT"; then
+ show_busy
+ die "Unmount failed"
+ fi
+ fi
+
+ if is_mapped; then
+ sudo cryptsetup close "$NAME"
+ fi
+
+ if [[ -e "$DEV_BY_UUID" ]]; then
+ local dev
+ dev="$(resolve_blockdev)" || true
+ if [[ -n "${dev:-}" ]] && command -v udisksctl >/dev/null 2>&1; then
+ sudo udisksctl power-off -b "$dev" || true
+ fi
+ fi
+
+ echo "OK: unmounted, closed, and powered off (if supported)"
+}
+
+do_status() {
+ echo "UUID device: $DEV_BY_UUID"
+ if [[ -e "$DEV_BY_UUID" ]]; then
+ echo "Resolved: $(readlink -f "$DEV_BY_UUID")"
+ else
+ echo "Resolved: (not present)"
+ fi
+
+ echo -n "Mapper: "
+ if is_mapped; then echo "OPEN ($MAPPER)"; else echo "CLOSED"; fi
+
+ echo -n "Mount: "
+ if is_mounted; then echo "MOUNTED ($MNT)"; else echo "NOT MOUNTED"; fi
+}
+
+usage() {
+ cat <<EOF
+Usage: $0 {open|close|status}
+
+open - unlock LUKS by UUID and mount to $MNT
+close - sync, unmount, close mapper, power-off block device (best-effort)
+status - show current state
+EOF
+}
+
+main() {
+ case "${1:-}" in
+ open) do_open ;;
+ close) do_close ;;
+ status) do_status ;;
+ *) usage; exit 2 ;;
+ esac
+}
+
+main "$@"
+
diff --git a/yt-dlp b/yt-dlp
new file mode 120000
index 0000000..42d0728
--- /dev/null
+++ b/yt-dlp
@@ -0,0 +1 @@
+/home/lukasz/.local/share/pipx/venvs/yt-dlp/bin/yt-dlp \ No newline at end of file