- commit
- 8c41a5c017d7ce15bb25a34b1de6e224127c27b8
- parent
- 47eccbc190abca038664f8d5bbb528762b4d8c99
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 44 | +++++----- |
| M | select-mate.sh | 133 | +++++++++++++++++++++++------- |
@@ -6,7 +6,8 @@
66 77 - `bash`88 - `sqlite3`9-- `gum`9+- `curl` if you want to run it directly from `https://select-mate.sh`10+- `gum` for the richer picker UI, optional1011 1112 On macOS with Homebrew:1213 @@ -14,8 +15,18 @@ On macOS with Homebrew:
1415 brew install sqlite gum1516 ```1617 18+If you skip `gum`, the game falls back to plain terminal prompts and numbered menus.19+1720 ## Quick Start1821 22+Run the hosted script in one shot:23+24+```bash25+curl -fsSL https://select-mate.sh | bash26+```27+28+The URL should serve the actual released `select-mate.sh` script. When launched this way, the script reattaches to your terminal for prompts, asks for your identity, and then lets you choose between a multiplayer game and a local-only game.29+1930 Make sure the script is executable:2031 2132 ```bash@@ -71,6 +82,8 @@ Show CLI help:
7182 ./select-mate.sh --help7283 ```7384 85+If you are hosting the curl entrypoint yourself, serve the exact released `select-mate.sh` script at `https://select-mate.sh`.86+7487 For a shared live-test setup with two terminals:7588 7689 ```bash@@ -94,16 +107,16 @@ Config format is a single key/value entry:
94107 identity=alice95108 ```96109 97-Game state is stored here by default:110+If you do not pipe a board URI on stdin, game state is stored next to the config file by default:98111 99112 ```bash100-${XDG_STATE_HOME:-$HOME/.local/state}/select-mate/game.db113+${XDG_CONFIG_HOME:-$HOME/.config}/game.db101114 ```102115 103-If you want an isolated game state for testing, point `XDG_STATE_HOME` somewhere else:116+If you want an isolated game state for testing, point `XDG_CONFIG_HOME` somewhere else:104117 105118 ```bash106-XDG_STATE_HOME="$(mktemp -d)" ./select-mate.sh119+XDG_CONFIG_HOME="$(mktemp -d)" ./select-mate.sh107120 ```108121 109122 ## Multiplayer@@ -163,19 +176,11 @@ Helper targets:
163176 - `make test-board-uri` prints the shared board URI.164177 - `make test-reset` removes the shared `/tmp/select-mate-live` test area.165178 166-Example using a shared local folder:167-168-```bash169-mkdir -p /tmp/select-mate-shared170-export XDG_STATE_HOME=/tmp/select-mate-shared171-./select-mate.sh172-```173-174-On a second terminal or second machine with access to the same shared directory, choose another identity:179+For multiplayer with separate config homes or identities, pipe the shared board URI on stdin:175180 176181 ```bash177-export XDG_STATE_HOME=/tmp/select-mate-shared178-./select-mate.sh --identity bob182+printf 'file:/tmp/select-mate-shared/game.db?mode=rwc\n' | ./select-mate.sh --identity alice183+printf 'file:/tmp/select-mate-shared/game.db?mode=rwc\n' | ./select-mate.sh --identity bob179184 ```180185 181186 Recommended flow:@@ -186,13 +191,6 @@ Recommended flow:
186191 - The other identity sees a read-only waiting view and can still refresh and inspect moves.187192 - Choose the player identity up front with `--identity NAME` before joining a multiplayer board.188193 189-If you prefer a URI-based board target instead of `XDG_STATE_HOME`, pipe the board URI on stdin:190-191-```bash192-printf 'file:/tmp/select-mate-shared/game.db?mode=rwc\n' | ./select-mate.sh --identity alice193-printf 'file:/tmp/select-mate-shared/game.db?mode=rwc\n' | ./select-mate.sh --identity bob194-```195-196194 Current limitations:197195 198196 - Avoid trying to make moves at the same time from two terminals.@@ -4,14 +4,59 @@ set -euo pipefail
44 55 APP_NAME="select-mate"66 APP_TITLE="select-mate.sh"7-STATE_ROOT="${XDG_STATE_HOME:-$HOME/.local/state}"8-APP_STATE_DIR="$STATE_ROOT/$APP_NAME"9-DEFAULT_DB_PATH="$APP_STATE_DIR/game.db"10-DB_PATH="$DEFAULT_DB_PATH"117 CONFIG_PATH="${XDG_CONFIG_HOME:-$HOME/.config}/select-mate.conf"8+DEFAULT_DB_PATH="$(dirname "$CONFIG_PATH")/game.db"9+DB_PATH="$DEFAULT_DB_PATH"1210 SESSION_IDENTITY_ID=""1311 SESSION_IDENTITY_NAME=""1412 13+has_gum() {14+ command -v gum >/dev/null 2>&115+}16+17+prepare_interactive_stdin() {18+ if [[ -t 0 || -r /dev/tty ]]; then19+ return 020+ fi21+ printf 'An interactive terminal is required for prompts.\n' >&222+ return 123+}24+25+read_user_line() {26+ local __reply_var=$127+28+ if [[ -t 0 ]]; then29+ IFS= read -r "$__reply_var" || return 130+ return 031+ fi32+33+ IFS= read -r "$__reply_var" </dev/tty || return 134+}35+36+gum_confirm_interactive() {37+ if [[ -t 0 ]]; then38+ gum confirm "$@"39+ return $?40+ fi41+ gum confirm "$@" </dev/tty42+}43+44+gum_input_interactive() {45+ if [[ -t 0 ]]; then46+ gum input "$@"47+ return $?48+ fi49+ gum input "$@" </dev/tty50+}51+52+gum_choose_interactive() {53+ if [[ -t 0 ]]; then54+ gum choose "$@"55+ return $?56+ fi57+ gum choose "$@" </dev/tty58+}59+1560 usage() {1661 cat <<'EOF'1762 Usage: select-mate.sh [--identity NAME] [--new | --local | --moves | --whoami | --help]@@ -27,6 +72,7 @@ Commands:
2772 Startup defaults:2873 - Identity config: ${XDG_CONFIG_HOME:-$HOME/.config}/select-mate.conf2974 Example: identity=alice75+ - Board path when stdin does not provide a URI: ${XDG_CONFIG_HOME:-$HOME/.config}/game.db3076 - Board URI from stdin when stdin is not a TTY3177 Example: printf 'file:/tmp/select-mate.db?mode=rwc\n' | ./select-mate.sh --whoami3278 EOF@@ -55,13 +101,6 @@ require_sqlite() {
55101 fi56102 }57103 58-require_gum() {59- if ! command -v gum >/dev/null 2>&1; then60- printf 'gum is required to play interactively. Install it from https://github.com/charmbracelet/gum.\n' >&261- exit 162- fi63-}64-65104 has_full_tty() {66105 [[ -t 0 && -t 1 && "${TERM:-}" != "dumb" ]]67106 }@@ -383,12 +422,14 @@ count_lines() {
383422 384423 confirm_action() {385424 local prompt=$1386- if command -v gum >/dev/null 2>&1; then387- gum confirm "$prompt"425+ local reply426+427+ if has_gum; then428+ gum_confirm_interactive "$prompt"388429 return $?389430 fi390- printf '%s [y/N] ' "$prompt"391- read -r reply431+ printf '%s [y/N] ' "$prompt" >&2432+ read_user_line reply || return 1392433 [[ "$reply" == "y" || "$reply" == "Y" ]]393434 }394435 @@ -572,18 +613,18 @@ prompt_text_input() {
572613 local default_value=${2:-}573614 local reply574615 575- if command -v gum >/dev/null 2>&1; then616+ if has_gum; then576617 if [[ -n "$default_value" ]]; then577- reply=$(gum input --prompt "$prompt: " --value "$default_value") || return 1618+ reply=$(gum_input_interactive --prompt "$prompt: " --value "$default_value") || return 1578619 else579- reply=$(gum input --prompt "$prompt: ") || return 1620+ reply=$(gum_input_interactive --prompt "$prompt: ") || return 1580621 fi581622 printf '%s' "$reply"582623 return 0583624 fi584625 585- printf '%s: ' "$prompt"586- read -r reply || return 1626+ printf '%s: ' "$prompt" >&2627+ read_user_line reply || return 1587628 if [[ -z "$reply" && -n "$default_value" ]]; then588629 reply=$default_value589630 fi@@ -592,11 +633,41 @@ prompt_text_input() {
592633 593634 pick_option() {594635 local header=$1636+ local options=()637+ local option_count reply i option595638 shift596- if [[ "$#" -eq 0 ]]; then639+ options=("$@")640+ option_count=${#options[@]}641+642+ if [[ "$option_count" -eq 0 ]]; then597643 return 1598644 fi599- printf '%s\n' "$@" | gum choose --header "$header"645+646+ if has_gum; then647+ printf '%s\n' "${options[@]}" | gum_choose_interactive --header "$header"648+ return 0649+ fi650+651+ printf '%s\n' "$header" >&2652+ for ((i = 0; i < option_count; i += 1)); do653+ printf ' %d) %s\n' "$((i + 1))" "${options[$i]}" >&2654+ done655+656+ while :; do657+ reply=$(prompt_text_input "Choose 1-$option_count") || return 1658+ reply=$(trim_string "$reply")659+ if [[ "$reply" =~ ^[0-9]+$ ]] && ((reply >= 1 && reply <= option_count)); then660+ printf '%s' "${options[$((reply - 1))]}"661+ return 0662+ fi663+ for option in "${options[@]}"; do664+ if [[ "$reply" == "$option" ]]; then665+ printf '%s' "$option"666+ return 0667+ fi668+ done669+ printf 'Choose one of the listed options.\n' >&2670+ done600671 }601672 602673 prompt_for_identity_name() {@@ -605,7 +676,7 @@ prompt_for_identity_name() {
605676 local choice name606677 local options=()607678 608- if command -v gum >/dev/null 2>&1; then679+ if has_gum; then609680 while IFS= read -r line; do610681 [[ -n "$line" ]] || continue611682 options[${#options[@]}]=$line@@ -1573,7 +1644,7 @@ interactive_game() {
15731644 local destination_options destination_choice target_square action status15741645 local piece_file piece_rank allow_switch_identity15751646 1576- require_gum1647+ prepare_interactive_stdin || return 115771648 ensure_selected_identity_for_interactive || return 015781649 15791650 while :; do@@ -1593,8 +1664,12 @@ interactive_game() {
15931664 fi15941665 15951666 clear_screen1596- gum style --border rounded --padding "1 2" --margin "1 0" --border-foreground 212 "$(printf '%s\n' "$APP_TITLE")"1597- gum style --border normal --padding "1 2" "$screen"1667+ if has_gum; then1668+ gum style --border rounded --padding "1 2" --margin "1 0" --border-foreground 212 "$(printf '%s\n' "$APP_TITLE")"1669+ gum style --border normal --padding "1 2" "$screen"1670+ else1671+ printf '%s\n\n%s\n' "$APP_TITLE" "$screen"1672+ fi15981673 15991674 if [[ -z "$game_id" ]]; then16001675 action=$(pick_option "No active game." "new game" "new local game" "switch identity" "quit") || return 0@@ -1826,7 +1901,7 @@ main() {
18261901 fi18271902 ;;18281903 --new)1829- require_gum1904+ prepare_interactive_stdin || exit 118301905 ensure_selected_identity_for_interactive || exit 018311906 if confirm_action "Start a new game?"; then18321907 start_new_game_flow "multiplayer"@@ -1834,7 +1909,7 @@ main() {
18341909 interactive_game18351910 ;;18361911 --local)1837- require_gum1912+ prepare_interactive_stdin || exit 118381913 ensure_selected_identity_for_interactive || exit 018391914 if confirm_action "Start a new local game?"; then18401915 start_new_game_flow "local"@@ -1852,6 +1927,6 @@ main() {
18521927 esac18531928 }18541929 1855-if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then1930+if [[ "${BASH_SOURCE[0]-$0}" == "$0" ]]; then18561931 main "$@"18571932 fi