Created
August 4, 2026 01:17
-
-
Save balajeerc/5b43bb3c89d8d515fe4ae3bbe31b6123 to your computer and use it in GitHub Desktop.
Yeah, I save scum in FTL. Sue me.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| SRC="$HOME/.local/share/FasterThanLight/continue.sav" | |
| DEFAULT_DEST_DIR="$HOME/Documents/ftl_scum_saves" | |
| CONFIG_FILE="$HOME/.config/ftl_save_scum.conf" | |
| DEST_DIR="" | |
| usage() { | |
| cat <<EOF | |
| Usage: $(basename "$0") [save|restore] | |
| save Save a snapshot of the current FTL game state (default). | |
| restore Browse and restore a previously saved snapshot. | |
| EOF | |
| } | |
| # command -> "apt package : one-line explanation of what it's used for" | |
| declare -A REQUIRED_CMDS=( | |
| [fzf]="fzf : interactive fuzzy picker used by the restore command" | |
| [find]="findutils : locates saved snapshots on disk" | |
| [sort]="coreutils : orders snapshots by save time" | |
| [date]="coreutils : timestamps snapshots and formats dates" | |
| [cp]="coreutils : copies save files to/from the backup directory" | |
| [sed]="sed : parses metadata companion files" | |
| [mkdir]="coreutils : creates the backup directory" | |
| [readlink]="coreutils : resolves this script's own path for the fzf preview" | |
| ) | |
| check_deps() { | |
| local missing=() | |
| local cmd | |
| for cmd in "${!REQUIRED_CMDS[@]}"; do | |
| command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") | |
| done | |
| if [[ ${#missing[@]} -gt 0 ]]; then | |
| echo "Error: missing required dependencies:" >&2 | |
| local pkgs=() | |
| for cmd in "${missing[@]}"; do | |
| local info="${REQUIRED_CMDS[$cmd]}" | |
| echo " - $cmd (${info#* : }) -- package: ${info%% :*}" >&2 | |
| pkgs+=("${info%% :*}") | |
| done | |
| echo >&2 | |
| echo "Install with: sudo apt-get install $(printf '%s\n' "${pkgs[@]}" | sort -u | tr '\n' ' ')" >&2 | |
| exit 1 | |
| fi | |
| } | |
| # Reads $CONFIG_FILE for a snapshot directory. If it's missing or the path | |
| # in it isn't usable, prompts for one (prefilled with DEFAULT_DEST_DIR), | |
| # creates it, and writes it back to $CONFIG_FILE. Sets $DEST_DIR either way. | |
| resolve_dest_dir() { | |
| local configured="" | |
| if [[ -f "$CONFIG_FILE" ]]; then | |
| configured=$(grep -m1 -v '^[[:space:]]*#' "$CONFIG_FILE" 2>/dev/null | grep -m1 -v '^[[:space:]]*$' || true) | |
| configured="${configured#"${configured%%[![:space:]]*}"}" | |
| configured="${configured%"${configured##*[![:space:]]}"}" | |
| configured="${configured/#\~/$HOME}" | |
| fi | |
| if [[ -n "$configured" ]] && mkdir -p "$configured" 2>/dev/null; then | |
| DEST_DIR="$configured" | |
| return | |
| fi | |
| local entered | |
| read -e -i "$DEFAULT_DEST_DIR" -p "Save snapshots to directory: " entered | |
| entered="${entered:-$DEFAULT_DEST_DIR}" | |
| entered="${entered/#\~/$HOME}" | |
| entered="${entered%/}" | |
| if ! mkdir -p "$entered"; then | |
| echo "Error: cannot create or access directory: $entered" >&2 | |
| exit 1 | |
| fi | |
| mkdir -p "$(dirname "$CONFIG_FILE")" | |
| echo "$entered" > "$CONFIG_FILE" | |
| DEST_DIR="$entered" | |
| } | |
| show_meta() { | |
| local base="$1" | |
| local f="$DEST_DIR/$base" | |
| printf "File: %s\n\n" "$base" | |
| if [[ -f "$f.meta" ]]; then | |
| cat "$f.meta" | |
| else | |
| echo "(no description)" | |
| fi | |
| } | |
| do_save() { | |
| if [[ ! -f "$SRC" ]]; then | |
| echo "Error: FTL save not found at $SRC" >&2 | |
| exit 1 | |
| fi | |
| suggestion="continue_$(date +%Y%m%d_%H%M%S)" | |
| read -e -i "$suggestion" -p "Save snapshot as: " filename | |
| if [[ -z "$filename" ]]; then | |
| echo "Error: filename cannot be empty" >&2 | |
| exit 1 | |
| fi | |
| # Ensure it ends with .sav | |
| [[ "$filename" == *.sav ]] || filename="${filename}.sav" | |
| dest_path="$DEST_DIR/$filename" | |
| meta_path="${dest_path}.meta" | |
| if [[ -e "$dest_path" ]]; then | |
| read -e -p "$dest_path already exists. Overwrite? [y/N] " confirm | |
| if [[ ! "$confirm" =~ ^[Yy]$ ]]; then | |
| echo "Aborted." | |
| exit 1 | |
| fi | |
| fi | |
| read -e -p "Description (optional): " description | |
| cp -v "$SRC" "$dest_path" | |
| { | |
| echo "Description: $description" | |
| echo "Saved: $(date '+%Y-%m-%d %H:%M:%S')" | |
| } > "$meta_path" | |
| } | |
| do_restore() { | |
| mkdir -p "$DEST_DIR" | |
| saves=() | |
| while IFS=$'\t' read -r _ base; do | |
| saves+=("$base") | |
| done < <(find "$DEST_DIR" -maxdepth 1 -name '*.sav' -printf '%T@\t%f\n' | sort -rn) | |
| if [[ ${#saves[@]} -eq 0 ]]; then | |
| echo "No saved snapshots found in $DEST_DIR" >&2 | |
| exit 1 | |
| fi | |
| local script_path | |
| script_path="$(readlink -f "$0")" | |
| preview_cmd="$(printf '%q' "$script_path") --show-meta {}" | |
| choice=$(printf '%s\n' "${saves[@]}" | fzf \ | |
| --height=70% --layout=reverse --border \ | |
| --prompt="Restore > " \ | |
| --header="Select a snapshot to restore (Enter: confirm, Esc: cancel)" \ | |
| --preview="$preview_cmd" \ | |
| --preview-window=right:55%:wrap) | |
| if [[ -z "${choice:-}" ]]; then | |
| echo "Aborted." | |
| exit 1 | |
| fi | |
| echo | |
| echo "Selected: $choice" | |
| if [[ -f "$DEST_DIR/$choice.meta" ]]; then | |
| cat "$DEST_DIR/$choice.meta" | |
| fi | |
| echo | |
| read -e -p "Restore this snapshot over the current FTL save? This will overwrite $SRC [y/N] " confirm | |
| if [[ ! "$confirm" =~ ^[Yy]$ ]]; then | |
| echo "Aborted." | |
| exit 1 | |
| fi | |
| mkdir -p "$(dirname "$SRC")" | |
| cp -v "$DEST_DIR/$choice" "$SRC" | |
| echo "Restored '$choice' to $SRC" | |
| } | |
| # --show-meta is an internal hidden mode used by the fzf preview pane; it | |
| # skips the dependency check so the preview stays fast. By the time it runs, | |
| # the parent restore invocation has already resolved/written the config, so | |
| # this should just read it back without prompting. | |
| if [[ "${1:-}" == "--show-meta" ]]; then | |
| resolve_dest_dir | |
| show_meta "${2:-}" | |
| exit 0 | |
| fi | |
| check_deps | |
| mode="save" | |
| case "${1:-}" in | |
| save|-s|--save) mode="save" ;; | |
| restore|-r|--restore) mode="restore" ;; | |
| -h|--help) usage; exit 0 ;; | |
| "") mode="save" ;; | |
| *) echo "Unknown option: $1" >&2; usage; exit 1 ;; | |
| esac | |
| resolve_dest_dir | |
| case "$mode" in | |
| save) do_save ;; | |
| restore) do_restore ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment