# Shared helpers for floppy-utils. Source from src/floppy only. [[ -n "${_FLOPPY_COMMON_LOADED:-}" ]] && return 0 _FLOPPY_COMMON_LOADED=1 FLOPPY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FLOPPY_SCRIPT_DIR="$(cd "$FLOPPY_LIB_DIR/.." && pwd)" floppy_load_config() { local cfg="${XDG_CONFIG_HOME:-$HOME/.config}/floppy-utils/config" if [[ -f "$cfg" ]]; then # shellcheck source=/dev/null source "$cfg" fi : "${FLOPPY_DISKDIR:=$HOME/Retro/BLANKS}" : "${FLOPPY_MEDIADIR:=/media/${USER:-root}}" : "${FLOPPY_DEFAULT_SIZE_KB:=1440}" # USB floppy drives (e.g. Mitsumi UFDD) often appear as /dev/sda with 0B until media is inserted. : "${FLOPPY_DEVICE_MATCH:=mitsumi|ufdd|fdd|floppy}" } floppy_die() { echo "floppy: $*" >&2 exit 1 } floppy_info() { # Must use stderr: this is called from functions whose stdout is captured (e.g. pick_device). echo ">>> $*" >&2 } floppy_warn() { echo "floppy: warning: $*" >&2 } floppy_require_cmd() { local cmd for cmd in "$@"; do command -v "$cmd" >/dev/null 2>&1 || floppy_die "required command not found: $cmd" done } floppy_require_root() { [[ "$(id -u)" -eq 0 ]] && return 0 command -v sudo >/dev/null 2>&1 || floppy_die "root privileges required (install sudo or run as root)" } floppy_run_root() { floppy_require_root if [[ "$(id -u)" -eq 0 ]]; then "$@" else sudo -n "$@" 2>/dev/null || sudo "$@" fi } floppy_ensure_diskdir() { if [[ ! -d "$FLOPPY_DISKDIR" ]]; then mkdir -p "$FLOPPY_DISKDIR" || floppy_die "cannot create disk directory: $FLOPPY_DISKDIR" fi } floppy_validate_size_kb() { local size="$1" case "$size" in 360|720|1440) return 0 ;; *) floppy_die "size must be one of: 360, 720, 1440 (got: $size)" ;; esac } floppy_random_name() { local rando rando="$(od -An -N4 -tu4 < /dev/urandom | tr -d ' ')" echo "floppy${rando}" } # Sanitize volume label / user text for use as a filename. floppy_sanitize_name() { local raw="$1" local clean clean="$(echo "$raw" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '_' | sed 's/^_//;s/_$//')" [[ -n "$clean" ]] || clean="floppy" echo "${clean:0:32}" } # Resolve argument to an absolute .img path. # Like floppy_resolve_image but returns 1 instead of exiting. floppy_try_resolve_image() { local arg="${1:-}" local path base [[ -n "$arg" ]] || return 1 if [[ -f "$arg" ]]; then printf '%s\n' "$(readlink -f "$arg")" return 0 fi base="${arg%.img}" path="$FLOPPY_DISKDIR/${base}.img" if [[ -f "$path" ]]; then printf '%s\n' "$(readlink -f "$path")" return 0 fi return 1 } floppy_resolve_image() { local arg="${1:-}" local path base [[ -n "$arg" ]] || floppy_die "image name or path required" if [[ -f "$arg" ]]; then printf '%s\n' "$(readlink -f "$arg")" return 0 fi base="${arg%.img}" path="$FLOPPY_DISKDIR/${base}.img" if [[ -f "$path" ]]; then printf '%s\n' "$(readlink -f "$path")" return 0 fi floppy_die "image not found: $arg (also tried $path)" } floppy_image_basename() { local image="$1" basename "${image%.img}" } floppy_image_path_in_diskdir() { local name="${1%.img}" printf '%s/%s.img\n' "$FLOPPY_DISKDIR" "$name" } floppy_create_blank() { local name="$1" local size_kb="$2" local image floppy_validate_size_kb "$size_kb" floppy_ensure_diskdir image="$(floppy_image_path_in_diskdir "$name")" if [[ -f "$image" ]]; then floppy_warn "image already exists: $image" printf '%s\n' "$image" return 0 fi floppy_info "creating ${size_kb}K image: $image" dd if=/dev/zero of="$image" bs=1K count="$size_kb" status=progress 2>/dev/null \ || dd if=/dev/zero of="$image" bs=1K count="$size_kb" printf '%s\n' "$(readlink -f "$image")" } # Per-device lsblk field (avoids broken parsing when LABEL is empty). floppy_device_field() { local dev="$1" local field="$2" lsblk -dno "$field" "$dev" 2>/dev/null | head -1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' } # Volume label, or empty if unknown / invalid. floppy_device_label() { local dev="$1" local label label="$(floppy_device_field "$dev" LABEL)" if [[ -z "$label" || "$label" == "/"* ]]; then label="$(blkid -s LABEL -o value "$dev" 2>/dev/null | head -1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" fi if [[ -n "$label" && "$label" != "/"* ]]; then printf '%s\n' "$label" return 0 fi return 1 } # Score how likely a block device is a USB floppy drive (higher = better match). floppy_device_score() { local dev="$1" local model size tran label local score=0 model="$(floppy_device_field "$dev" MODEL | tr '[:upper:]' '[:lower:]')" size="$(floppy_device_field "$dev" SIZE)" tran="$(floppy_device_field "$dev" TRAN | tr '[:lower:]' '[:upper:]')" label="$(floppy_device_label "$dev" 2>/dev/null || true)" if [[ "$model" =~ ${FLOPPY_DEVICE_MATCH} ]]; then score=$((score + 100)) fi if [[ "$tran" == "USB" ]]; then score=$((score + 40)) fi case "$size" in 0|0B) score=$((score + 5)) ;; 360K|720K|1.0M|1.2M|1.4M|1.44M) score=$((score + 30)) ;; esac [[ -n "$label" ]] && score=$((score + 10)) printf '%s\n' "$score" } # List whole-disk block devices likely to be USB/removable drives (floppy, USB FDD, etc.) floppy_list_devices() { local verbose="${1:-0}" local name rm type size tran model label fstype mnt floppy_require_cmd lsblk while IFS= read -r name; do [[ -z "$name" ]] && continue rm="$(floppy_device_field "$name" RM)" type="$(floppy_device_field "$name" TYPE)" [[ "$type" == "disk" && "$rm" == "1" ]] || continue [[ "$name" =~ ^/dev/(sd|fd) ]] || continue size="$(floppy_device_field "$name" SIZE)" tran="$(floppy_device_field "$name" TRAN)" model="$(floppy_device_field "$name" MODEL)" label="$(floppy_device_label "$name" 2>/dev/null || echo "")" fstype="$(floppy_device_field "$name" FSTYPE)" mnt="$(floppy_device_field "$name" MOUNTPOINT)" if [[ "$verbose" -eq 1 ]]; then printf '%s rm=%s size=%s tran=%s model=%s label=%s fstype=%s mount=%s score=%s\n' \ "$name" "$rm" "$size" "$tran" "$model" "${label:-—}" "${fstype:-—}" "${mnt:-—}" \ "$(floppy_device_score "$name")" else local extra="" [[ -n "$label" ]] && extra=" label=$label" [[ -n "$mnt" ]] && extra="$extra mount=$mnt" printf '%s %s %s %s%s\n' "$name" "$size" "${tran:-?}" "${model:-unknown}" "$extra" fi done < <(lsblk -dpno NAME,RM,TYPE 2>/dev/null | awk '$2==1 && $3=="disk" && $1 ~ /^\/dev\/(sd|fd)/ {print $1}') } floppy_device_has_media() { local dev="$1" local size size="$(floppy_device_field "$dev" SIZE)" [[ -n "$size" && "$size" != "0" && "$size" != "0B" ]] } floppy_device_show() { local dev="$1" lsblk -d -o NAME,SIZE,MODEL,TRAN,LABEL,FSTYPE,MOUNTPOINT "$dev" 2>/dev/null || true } floppy_device_sectors() { local dev="$1" local sectors floppy_require_cmd blockdev sectors="$(floppy_run_root blockdev --getsz "$dev" 2>/dev/null)" || floppy_die "cannot read size of $dev" if [[ -z "$sectors" || "$sectors" -eq 0 ]]; then if ! floppy_device_has_media "$dev"; then floppy_die "no media detected in $dev (insert a floppy and retry)" fi floppy_die "device $dev reports 0 sectors" fi printf '%s\n' "$sectors" } floppy_device_default_name() { local dev="$1" local label mnt base if label="$(floppy_device_label "$dev" 2>/dev/null)"; then floppy_sanitize_name "$label" return 0 fi # Unlabeled FAT volumes: udisks often mounts as $FLOPPY_MEDIADIR/ mnt="$(floppy_device_field "$dev" MOUNTPOINT)" if [[ -n "$mnt" ]]; then base="$(basename "$mnt")" if [[ -n "$base" && "$base" != "/"* ]]; then floppy_sanitize_name "$base" return 0 fi fi floppy_random_name } floppy_unmount_device() { local dev="$1" local mp unmounted=0 if command -v udisksctl >/dev/null 2>&1 && floppy_device_mounted "$dev"; then if udisksctl unmount -b "$dev" 2>/dev/null; then floppy_info "unmounted $dev via udisks" return 0 fi fi while read -r mp; do [[ -n "$mp" ]] || continue floppy_info "unmounting $mp" floppy_run_root umount "$mp" && unmounted=1 done < <(lsblk -rn -o MOUNTPOINT "$dev" 2>/dev/null | sed '/^$/d') [[ "$unmounted" -eq 1 ]] || floppy_warn "$dev does not appear to be mounted" } # After swapping floppies in the same USB drive: drop stale mounts and re-probe media. floppy_cmd_refresh() { local device="${1:-}" local do_eject="${2:-0}" local do_mount="${3:-0}" local wait_secs="${4:-2}" floppy_require_cmd lsblk blockdev device="$(floppy_resolve_burn_device "$device")" floppy_info "refreshing $device (unmount stale state, re-probe media)" echo "Before:" floppy_device_show "$device" echo "" if floppy_device_mounted "$device"; then floppy_unmount_device "$device" fi floppy_run_root blockdev --flushbufs "$device" 2>/dev/null || true if [[ "$do_eject" -eq 1 ]]; then if command -v eject >/dev/null 2>&1; then floppy_info "ejecting $device (helps some USB FDD firmware notice media change)" floppy_run_root eject "$device" 2>/dev/null \ || floppy_warn "eject failed — swap the disk and continue anyway" else floppy_warn "eject command not found; install eject(1) or omit --eject" fi fi if command -v udevadm >/dev/null 2>&1; then udevadm settle --timeout=5 2>/dev/null || true fi if [[ "$wait_secs" -gt 0 ]]; then floppy_info "waiting ${wait_secs}s for drive/kernel to settle" sleep "$wait_secs" fi # Reading size nudges usb-storage to re-check capacity on many drives. floppy_run_root blockdev --getsz "$device" >/dev/null 2>&1 || true echo "After:" floppy_device_show "$device" echo "" if floppy_device_has_media "$device"; then local label mnt label="$(floppy_device_label "$device" 2>/dev/null || true)" mnt="$(floppy_device_field "$device" MOUNTPOINT)" floppy_info "media detected${label:+ (label: $label)}" if [[ "$do_mount" -eq 1 ]] && [[ -z "$mnt" ]]; then if command -v udisksctl >/dev/null 2>&1; then if udisksctl mount -b "$device" 2>/dev/null; then mnt="$(lsblk -dno MOUNTPOINT "$device" 2>/dev/null)" floppy_info "mounted at ${mnt:-?}" else floppy_warn "udisks mount failed — run: udisksctl mount -b $device" fi else floppy_warn "udisksctl not found; mount manually or use a file manager" fi elif [[ -n "$mnt" ]]; then floppy_info "already mounted at $mnt" fi else floppy_warn "no media detected (size 0B) — insert a disk and run: floppy refresh" fi } # Unmount, flush, and power off USB floppy — safe to unplug the cable afterward. floppy_cmd_disconnect() { local device="${1:-}" local powered_off=0 floppy_require_cmd blockdev device="$(floppy_resolve_burn_device "$device")" floppy_info "preparing $device for safe USB removal" floppy_device_show "$device" echo "" if floppy_device_mounted "$device"; then floppy_unmount_device "$device" else floppy_info "no filesystem mounted on $device" fi floppy_run_root blockdev --flushbufs "$device" 2>/dev/null || true sync if command -v udisksctl >/dev/null 2>&1; then if udisksctl power-off -b "$device" 2>/dev/null; then powered_off=1 floppy_info "powered off $device via udisks" else floppy_warn "udisks power-off failed for $device" fi else floppy_warn "udisksctl not found — install udisks2 for USB power-off" fi if [[ "$powered_off" -eq 0 ]] && command -v eject >/dev/null 2>&1; then if floppy_run_root eject "$device" 2>/dev/null; then powered_off=1 floppy_info "ejected $device (fallback)" fi fi if command -v udevadm >/dev/null 2>&1; then udevadm settle --timeout=5 2>/dev/null || true fi echo "" if [[ "$powered_off" -eq 1 ]]; then echo "Safe to unplug the USB floppy drive now." else echo "Unmount and cache flush completed." echo "Wait a few seconds, then unplug the USB floppy drive." echo "If the device is busy, close file managers and retry: floppy disconnect" fi if [[ -b "$device" ]]; then floppy_info "($device still visible until the cable is removed)" fi } floppy_pick_device() { local -a entries=() local line dev score best_dev="" best_score=-1 while IFS= read -r line; do [[ -n "$line" ]] || continue dev="${line%% *}" score="$(floppy_device_score "$dev")" entries+=("$score $dev") if (( score > best_score )); then best_score=$score best_dev="$dev" fi done < <(floppy_list_devices 0) if [[ ${#entries[@]} -eq 0 ]]; then floppy_die "no removable block devices found (is the USB floppy connected?)" fi # Auto-select a clear floppy match (e.g. Mitsumi UFDD at /dev/sda). if [[ ${#entries[@]} -eq 1 ]]; then floppy_info "using removable device: ${entries[0]#* }" printf '%s\n' "${entries[0]#* }" return 0 fi if (( best_score >= 100 )); then local floppy_like=0 d s for entry in "${entries[@]}"; do s="${entry%% *}" if (( s >= 100 )); then floppy_like=$((floppy_like + 1)) best_dev="${entry#* }" fi done if (( floppy_like == 1 )); then floppy_info "using USB floppy drive: $best_dev" printf '%s\n' "$best_dev" return 0 fi fi echo "Multiple removable devices found:" local i=1 entry s d for entry in "${entries[@]}"; do s="${entry%% *}" d="${entry#* }" echo " $i) $d $(lsblk -dno SIZE,MODEL,LABEL,TRAN "$d" 2>/dev/null | tr -s ' ') (score=$s)" ((i++)) || true done echo -n "Select device number [1-${#entries[@]}]: " read -r choice if [[ ! "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#entries[@]} )); then floppy_die "invalid selection" fi entry="${entries[$((choice - 1))]}" printf '%s\n' "${entry#* }" } floppy_device_mounted() { local dev="$1" lsblk -n -o MOUNTPOINT "$dev" 2>/dev/null | grep -q . } floppy_resolve_burn_device() { local device="${1:-}" if [[ -n "$device" ]]; then [[ -b "$device" ]] || floppy_die "not a block device: $device" printf '%s\n' "$device" return 0 fi if [[ -n "${FLOPPY_DEVICE:-}" ]]; then [[ -b "$FLOPPY_DEVICE" ]] || floppy_die "FLOPPY_DEVICE is not a block device: $FLOPPY_DEVICE" printf '%s\n' "$FLOPPY_DEVICE" return 0 fi floppy_pick_device } floppy_confirm_burn() { local image="$1" local device="$2" local yes="${3:-0}" echo "" echo " image: $image ($(du -h "$image" | cut -f1))" echo " target: $device" lsblk -d -o NAME,SIZE,MODEL,TRAN,MOUNTPOINT "$device" 2>/dev/null || true echo "" if [[ "$yes" -eq 1 ]]; then return 0 fi if floppy_device_mounted "$device"; then floppy_warn "$device is mounted — it will be unmounted before writing" fi echo -n "Write image to $device? This will ERASE the floppy. [y/N] " read -r ans [[ "$ans" =~ ^[Yy]$ ]] || floppy_die "aborted" } # Find loop device backing an image, if any. floppy_loop_for_image() { local image="$1" losetup -j "$image" 2>/dev/null | awk -F: '{print $1; exit}' } floppy_mountpoint_for_loop() { local loop="$1" lsblk -n -o MOUNTPOINT "$loop" 2>/dev/null | grep -m1 . } floppy_cmd_attach() { local image_arg="${1:-}" local size_kb="${2:-$FLOPPY_DEFAULT_SIZE_KB}" local do_format="${3:-auto}" local image name mountpoint loop need_format floppy_require_cmd losetup mount blkid floppy_ensure_diskdir if [[ -z "$image_arg" ]]; then name="$(floppy_random_name)" image="$(floppy_create_blank "$name" "$size_kb")" need_format=1 else if [[ -f "$image_arg" ]]; then image="$(readlink -f "$image_arg")" name="$(floppy_image_basename "$image")" elif [[ -f "$(floppy_image_path_in_diskdir "$image_arg")" ]]; then image="$(floppy_resolve_image "$image_arg")" name="$(floppy_image_basename "$image")" else name="${image_arg%.img}" image="$(floppy_create_blank "$name" "$size_kb")" need_format=1 fi fi if [[ "$do_format" == "yes" ]]; then need_format=1 elif [[ "$do_format" == "no" ]]; then need_format=0 fi loop="$(floppy_loop_for_image "$image")" if [[ -n "$loop" ]]; then floppy_warn "already attached on $loop" else floppy_info "attaching loop device for $image" loop="$(floppy_run_root losetup --find --show "$image")" || floppy_die "losetup failed" floppy_info "loop device: $loop" fi if [[ "${need_format:-0}" -eq 1 ]] || ! floppy_run_root blkid "$loop" &>/dev/null; then if [[ "$do_format" == "no" ]]; then floppy_warn "no filesystem on $loop; mount may fail (use --format to create vfat)" else floppy_info "formatting $loop as vfat" floppy_run_root mkfs -t vfat -n "${name:0:11}" "$loop" || floppy_die "mkfs failed" need_format=1 fi fi mountpoint="$FLOPPY_MEDIADIR/$name" if [[ ! -d "$mountpoint" ]]; then floppy_info "creating mount point: $mountpoint" floppy_run_root mkdir -p "$mountpoint" fi if mountpoint -q "$mountpoint" 2>/dev/null; then floppy_info "already mounted at $mountpoint" else floppy_info "mounting $loop at $mountpoint" floppy_run_root mount "$loop" "$mountpoint" || floppy_die "mount failed" fi if [[ "${need_format:-0}" -eq 1 ]]; then local info_file="$mountpoint/${name}.txt" floppy_info "saving partition info to $info_file" floppy_run_root fdisk -l "$loop" >"${info_file}.tmp" 2>/dev/null \ && floppy_run_root chown "${USER:-root}:${USER:-root}" "${info_file}.tmp" 2>/dev/null \ && mv "${info_file}.tmp" "$info_file" \ || rm -f "${info_file}.tmp" fi echo "" echo " image: $image" echo " loop: $loop" echo " mountpoint: $mountpoint" } floppy_cmd_detach() { local target="${1:-}" local image loop mountpoint [[ -n "$target" ]] || floppy_die "specify image name, mount point, or loop device" if [[ -b "$target" && "$target" =~ ^/dev/loop ]]; then loop="$target" mountpoint="$(floppy_mountpoint_for_loop "$loop")" elif mountpoint -q "$target" 2>/dev/null; then mountpoint="$target" loop="$(findmnt -n -o SOURCE "$mountpoint" 2>/dev/null)" elif [[ -f "$target" ]] || [[ -f "$(floppy_image_path_in_diskdir "$target")" ]]; then image="$(floppy_resolve_image "$target")" loop="$(floppy_loop_for_image "$image")" [[ -n "$loop" ]] || floppy_die "image is not attached: $image" mountpoint="$(floppy_mountpoint_for_loop "$loop")" else # Physical floppy: udisks often mounts at $FLOPPY_MEDIADIR/