refactor: rename xray-sub to xtui, optimize selection tracking with index

- Rename project from xray-sub to xtui (app name, state dir, user-agent)
- Add selected_index to config for O(1) saved server lookup
- Add migrate_saved_index() for backward compatibility
- Replace fingerprint-based comparison with index-based in menu and apply
This commit is contained in:
2026-06-15 15:43:01 +03:00
parent 8c0aa5b8b3
commit 0553ef7493
2 changed files with 47 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
# xray-sub # xtui
Небольшой TUI-скрипт для выбора сервера из JSON-подписки Xray. Небольшой TUI-скрипт для выбора сервера из JSON-подписки Xray.
@@ -21,7 +21,7 @@ chmod +x main.sh
По желанию установите как команду: По желанию установите как команду:
```bash ```bash
sudo install -m 755 main.sh /usr/local/bin/xray-sub sudo install -m 755 main.sh /usr/local/bin/xtui
``` ```
## Запуск ## Запуск
@@ -35,7 +35,7 @@ sudo install -m 755 main.sh /usr/local/bin/xray-sub
Если установлено в систему: Если установлено в систему:
```bash ```bash
xray-sub xtui
``` ```
Первый запуск попросит ссылку на JSON-подписку, путь к конфигу Xray и имя systemd-службы. Первый запуск попросит ссылку на JSON-подписку, путь к конфигу Xray и имя systemd-службы.
@@ -43,9 +43,9 @@ xray-sub
Полезные команды: Полезные команды:
```bash ```bash
xray-sub --setup # заново пройти настройку xtui --setup # заново пройти настройку
xray-sub --refresh # обновить подписку xtui --refresh # обновить подписку
xray-sub --help # справка xtui --help # справка
``` ```
## Что делает ## Что делает
@@ -56,7 +56,7 @@ xray-sub --help # справка
## Где хранит файлы ## Где хранит файлы
Пользовательские файлы хранятся в `~/.xray-sub/`: Пользовательские файлы хранятся в `~/.xtui/`:
- `config` — настройки скрипта и последний выбранный сервер. - `config` — настройки скрипта и последний выбранный сервер.
- `sub.json` — кэш скачанной подписки. - `sub.json` — кэш скачанной подписки.

63
main.sh
View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
APP_NAME="xray-sub" APP_NAME="xtui"
STATE_DIR="${HOME}/.xray-sub" STATE_DIR="${HOME}/.xtui"
CONFIG_FILE="${STATE_DIR}/config" CONFIG_FILE="${STATE_DIR}/config"
SUB_JSON="${STATE_DIR}/sub.json" SUB_JSON="${STATE_DIR}/sub.json"
BACKUP_DIR="${STATE_DIR}/backups" BACKUP_DIR="${STATE_DIR}/backups"
@@ -135,7 +135,7 @@ write_cfg() {
--arg sub_url "$sub_url" \ --arg sub_url "$sub_url" \
--arg xray_config "$xray_config" \ --arg xray_config "$xray_config" \
--arg service "$service" \ --arg service "$service" \
'{sub_url:$sub_url, xray_config:$xray_config, service:$service, selected_fp:"", selected_name:""}' >"$tmp" '{sub_url:$sub_url, xray_config:$xray_config, service:$service, selected_index:"", selected_fp:"", selected_name:""}' >"$tmp"
install -m 600 "$tmp" "$CONFIG_FILE" install -m 600 "$tmp" "$CONFIG_FILE"
rm -f "$tmp" rm -f "$tmp"
} }
@@ -163,7 +163,7 @@ fetch_subscription() {
info "Скачиваю подписку…" info "Скачиваю подписку…"
if ! curl -fLsS --connect-timeout 10 --max-time 45 --retry 2 --retry-delay 1 \ if ! curl -fLsS --connect-timeout 10 --max-time 45 --retry 2 --retry-delay 1 \
-H 'Accept: application/json, */*' \ -H 'Accept: application/json, */*' \
-A 'xray-sub-selector/1.2' \ -A 'xtui/1.2' \
"$url" -o "$tmp"; then "$url" -o "$tmp"; then
rm -f "$tmp" rm -f "$tmp"
return 1 return 1
@@ -332,21 +332,36 @@ profile_fingerprint() {
} }
find_saved_index() { find_saved_index() {
local saved_fp count i fp local saved_index saved_fp count i fp
saved_index="$(cfg_get selected_index)"
saved_fp="$(cfg_get selected_fp)" saved_fp="$(cfg_get selected_fp)"
count="$(profile_count)" count="$(profile_count)"
[[ -n "$saved_fp" ]] || { echo 0; return; }
for ((i=0; i<count; i++)); do if [[ "$saved_index" =~ ^[0-9]+$ ]] && (( saved_index < count )); then
fp="$(profile_fingerprint "$i")" echo "$saved_index"
if [[ "$fp" == "$saved_fp" ]]; then return
echo "$i" fi
return
fi if [[ -n "$saved_fp" ]]; then
done for ((i=0; i<count; i++)); do
fp="$(profile_fingerprint "$i")"
if [[ "$fp" == "$saved_fp" ]]; then
cfg_set selected_index "$i"
echo "$i"
return
fi
done
fi
echo 0 echo 0
} }
migrate_saved_index() {
[[ -s "$SUB_JSON" ]] || return 0
validate_subscription_file "$SUB_JSON" || return 0
find_saved_index >/dev/null
}
build_selected_config() { build_selected_config() {
local idx="$1" out="$2" local idx="$1" out="$2"
jq -e --argjson i "$idx" "${PROFILES_DEF}"' jq -e --argjson i "$idx" "${PROFILES_DEF}"'
@@ -418,10 +433,10 @@ restart_xray_service() {
} }
apply_selection() { apply_selection() {
local idx="$1" dest service tmp fp old_fp name proto addr port changed=1 service_state local idx="$1" dest service tmp fp old_index name proto addr port changed=1 service_state
dest="$(cfg_get xray_config)" dest="$(cfg_get xray_config)"
service="$(cfg_get service)" service="$(cfg_get service)"
old_fp="$(cfg_get selected_fp)" old_index="$(cfg_get selected_index)"
fp="$(profile_fingerprint "$idx")" fp="$(profile_fingerprint "$idx")"
IFS=$'\t' read -r name proto addr port < <(profile_summary "$idx") IFS=$'\t' read -r name proto addr port < <(profile_summary "$idx")
@@ -444,12 +459,13 @@ apply_selection() {
ok "В ${dest} уже записан этот конфиг: ${name}" ok "В ${dest} уже записан этот конфиг: ${name}"
fi fi
cfg_set selected_index "$idx"
cfg_set selected_fp "$fp" cfg_set selected_fp "$fp"
cfg_set selected_name "$name" cfg_set selected_name "$name"
rm -f "$tmp" rm -f "$tmp"
service_state="$(service_state_raw "$service")" service_state="$(service_state_raw "$service")"
if (( changed )) || [[ "$old_fp" != "$fp" ]]; then if (( changed )) || [[ "$old_index" != "$idx" ]]; then
if [[ "$service_state" == "active" || "$service_state" == "activating" ]]; then if [[ "$service_state" == "active" || "$service_state" == "activating" ]]; then
restart_xray_service "$service" restart_xray_service "$service"
else else
@@ -622,17 +638,16 @@ render_table_header() {
} }
render_profile_row() { render_profile_row() {
local idx="$1" cursor="$2" saved_fp="$3" local idx="$1" cursor="$2" saved_index="$3"
local fp name proto addr port pointer mark row_style name_short addr_short local name proto addr port pointer mark row_style name_short addr_short
IFS=$'\t' read -r name proto addr port < <(profile_summary "$idx") IFS=$'\t' read -r name proto addr port < <(profile_summary "$idx")
fp="$(profile_fingerprint "$idx")"
pointer=" " pointer=" "
mark=" " mark=" "
row_style="" row_style=""
[[ "$idx" -eq "$cursor" ]] && { pointer="➤"; row_style="$C_INV"; } [[ "$idx" -eq "$cursor" ]] && { pointer="➤"; row_style="$C_INV"; }
[[ -n "$saved_fp" && "$fp" == "$saved_fp" ]] && mark="✓" [[ "$saved_index" =~ ^[0-9]+$ && "$idx" -eq "$saved_index" ]] && mark="✓"
name_short="$(shorten "$name" 31)" name_short="$(shorten "$name" 31)"
addr_short="$(shorten "$addr" 20)" addr_short="$(shorten "$addr" 20)"
@@ -646,11 +661,11 @@ render_profile_row() {
} }
render_menu() { render_menu() {
local cursor="$1" count saved_fp i cache_info selected_name local cursor="$1" count saved_index i cache_info selected_name
local service service_state service_enabled xray_config socks_endpoint local service service_state service_enabled xray_config socks_endpoint
count="$(profile_count)" count="$(profile_count)"
saved_fp="$(cfg_get selected_fp)" saved_index="$(cfg_get selected_index)"
selected_name="$(cfg_get selected_name)" selected_name="$(cfg_get selected_name)"
service="$(cfg_get service)" service="$(cfg_get service)"
xray_config="$(cfg_get xray_config)" xray_config="$(cfg_get xray_config)"
@@ -676,7 +691,7 @@ render_menu() {
render_table_header render_table_header
for ((i=0; i<count; i++)); do for ((i=0; i<count; i++)); do
render_profile_row "$i" "$cursor" "$saved_fp" render_profile_row "$i" "$cursor" "$saved_index"
done done
} }
@@ -777,6 +792,7 @@ main() {
;; ;;
--refresh) --refresh)
cfg_exists || wizard cfg_exists || wizard
migrate_saved_index
fetch_subscription "$(cfg_get sub_url)" || die "Не удалось обновить подписку." fetch_subscription "$(cfg_get sub_url)" || die "Не удалось обновить подписку."
exit 0 exit 0
;; ;;
@@ -789,6 +805,7 @@ main() {
;; ;;
esac esac
migrate_saved_index
maybe_refresh_subscription "$(cfg_get sub_url)" maybe_refresh_subscription "$(cfg_get sub_url)"
validate_subscription_file "$SUB_JSON" || die "Кэш подписки повреждён: ${SUB_JSON}. Запустите ${0##*/} --refresh" validate_subscription_file "$SUB_JSON" || die "Кэш подписки повреждён: ${SUB_JSON}. Запустите ${0##*/} --refresh"
menu menu