Skip to content

Instantly share code, notes, and snippets.

@balajeerc
Created August 4, 2026 01:17
Show Gist options
  • Select an option

  • Save balajeerc/5b43bb3c89d8d515fe4ae3bbe31b6123 to your computer and use it in GitHub Desktop.

Select an option

Save balajeerc/5b43bb3c89d8d515fe4ae3bbe31b6123 to your computer and use it in GitHub Desktop.
Yeah, I save scum in FTL. Sue me.
#!/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