feat: install.sh — arbre catégories, SPACE=action, TAB=plier, v=viewer bat

- Bug fix: {1} au lieu de {n} — action s'applique bien au skill sélectionné
- SPACE cycle l'action (local→global→ignorer), TAB plie/déplie la catégorie
- Arbre par catégorie : en-têtes ▼/▶, repliage auto si >3 skills
- Touche v : viewer avec coloration syntaxique (bat/batcat ou cat en fallback)
- Preview caché par défaut (v pour afficher), 50% droite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 05:42:55 +02:00
parent 436578968e
commit 5088ec0189
+136 -56
View File
@@ -37,6 +37,7 @@ REPO_URL="https://gitea.maison43.duckdns.org/gilles/mes_skills.git"
REPO_DIR="/tmp/mes_skills_$$"
_CLONED_REPO_DIR=""
STATE_FILE="/tmp/skills_state_$$"
COLLAPSED_FILE="/tmp/skills_collapsed_$$"
SKILLS_DEBUG="${SKILLS_DEBUG:-0}"
SKILLS_DRY_RUN="${SKILLS_DRY_RUN:-0}"
@@ -56,7 +57,8 @@ header() { echo -e "\n${GRV_PURPLE}╔══ $* ══╗${RESET}\n"; }
cleanup() {
debug "Nettoyage $_CLONED_REPO_DIR et $STATE_FILE"
[[ -n "$_CLONED_REPO_DIR" && -d "$_CLONED_REPO_DIR" ]] && rm -rf "$_CLONED_REPO_DIR"
[[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE"
[[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE"
[[ -f "$COLLAPSED_FILE" ]] && rm -f "$COLLAPSED_FILE"
}
trap cleanup EXIT
@@ -311,9 +313,9 @@ state_cycle() {
sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE"
}
# ── Formatage ligne menu ──────────────────────────────────────────
# ── Formatage ligne skill (préfixe caché s:IDX\t pour fzf) ───────
format_skill_line() {
local entry="$1"
local entry="$1" idx="$2"
local cat skill agent etat repo_ver local_ver desc tags
IFS='|' read -r cat skill agent etat repo_ver local_ver desc tags <<< "$entry"
local key; key=$(make_key "$entry")
@@ -340,41 +342,69 @@ format_skill_line() {
[[ "$etat" == "upd" ]] && ver_info=" (${local_ver}${repo_ver})"
[[ "$etat" == "new" ]] && ver_info=" (v${repo_ver})"
printf "${color_etat}%s${RESET} %-28s ${GRV_GRAY}[%s]${RESET} ${color_action}%s${RESET}%s ${GRV_FG}%s${RESET} ${GRV_PURPLE}%s${RESET}\n" \
"$ico_etat" "${cat}/${skill}" "$agent" "$ico_action" "$ver_info" "$desc" "$tags"
printf "s:%d\t ${color_etat}%s${RESET} %-24s ${GRV_GRAY}[%s]${RESET} ${color_action}%s${RESET}%s ${GRV_FG}%s${RESET} ${GRV_PURPLE}%s${RESET}\n" \
"$idx" "$ico_etat" "${skill}" "$agent" "$ico_action" "$ver_info" "$desc" "$tags"
}
# ── Formatage en-tête catégorie (préfixe caché h:CAT\t) ──────────
format_category_header() {
local cat="$1" count="$2" collapsed="$3"
local arrow color_arrow
if [[ "$collapsed" == "1" ]]; then
arrow="▶"; color_arrow="$GRV_YELLOW"
else
arrow="▼"; color_arrow="$GRV_BLUE"
fi
printf "h:%s\t${color_arrow}%s${RESET} ${GRV_BLUE}%s/${RESET} ${GRV_GRAY}(%d skill(s))${RESET}\n" \
"$cat" "$arrow" "$cat" "$count"
}
# ── Menu fzf principal ────────────────────────────────────────────
run_menu() {
header "Sélection des skills"
echo -e " ${GRV_FG}Navigation :${RESET} ${GRV_YELLOW}↑↓${RESET} se déplacer ${GRV_GREEN}TAB${RESET} changer l'action ${GRV_GREEN}ENTER${RESET} confirmer ${GRV_RED}ESC${RESET} annuler"
echo -e " ${GRV_GRAY}Taper du texte filtre les skills par nom ou description.${RESET}\n"
echo -e " ${GRV_FG}Navigation :${RESET} ${GRV_YELLOW}↑↓${RESET} déplacer ${GRV_GREEN}SPACE${RESET} changer action ${GRV_GREEN}TAB${RESET} plier/déplier ${GRV_GREEN}v${RESET} voir skill ${GRV_GREEN}ENTER${RESET} confirmer ${GRV_RED}ESC${RESET} annuler"
echo -e " ${GRV_GRAY}Taper du texte filtre par nom ou description.${RESET}\n"
state_init
local cycle_script="/tmp/skills_cycle_$$.sh"
# Catégories avec >3 skills repliées par défaut
rm -f "$COLLAPSED_FILE"
declare -A _cat_count
for entry in "${SKILLS_LIST[@]}"; do
local _cat; _cat="${entry%%|*}"
_cat_count[$_cat]=$(( ${_cat_count[$_cat]:-0} + 1 ))
done
for _cat in "${!_cat_count[@]}"; do
[[ ${_cat_count[$_cat]} -gt 3 ]] && echo "$_cat" >> "$COLLAPSED_FILE"
done
local space_script="/tmp/skills_space_$$.sh"
local tab_script="/tmp/skills_tab_$$.sh"
local list_script="/tmp/skills_list_$$.sh"
local fns_file="/tmp/skills_fns_$$.sh"
local preview_script="/tmp/skills_preview_$$.sh"
# Script de cycle d'état (appelé par fzf via execute-silent)
cat > "$cycle_script" << 'CYCLE_EOF'
# Exporter fonctions et données dans un fichier source partagé
{
declare -f format_skill_line format_category_header state_get make_key
declare -p GRV_GREEN GRV_YELLOW GRV_AQUA GRV_GRAY GRV_BLUE GRV_FG GRV_PURPLE RESET \
ICO_OK ICO_UPD ICO_NEW ICO_NA ICO_LOCAL ICO_GLOBAL ICO_SKIP
echo "STATE_FILE='$STATE_FILE'"
echo "REPO_DIR='$REPO_DIR'"
echo "COLLAPSED_FILE='$COLLAPSED_FILE'"
echo "SKILLS_LIST=($(printf '"%s" ' "${SKILLS_LIST[@]}"))"
} > "$fns_file"
# Script SPACE : cycle action sur une ligne skill (s:IDX)
cat > "$space_script" << 'SPACE_EOF'
#!/usr/bin/env bash
STATE_FILE="$1"
SKILLS_FNS="$2"
LINE_NUM="$3" # numéro de ligne fzf (1-based)
source "$SKILLS_FNS"
# Récupère l'entrée correspondante (0-based dans SKILLS_LIST)
idx=$(( LINE_NUM - 1 ))
entry="${SKILLS_LIST[$idx]:-}"
source "FNSFILE"
td="$1"; type="${td%%:*}"; value="${td#*:}"
[[ "$type" != "s" ]] && exit 0
entry="${SKILLS_LIST[$value]:-}"
[[ -z "$entry" ]] && exit 0
key=$(make_key "$entry")
etat=$(echo "$entry" | cut -d'|' -f4)
current=$(grep "^${key}=" "$STATE_FILE" 2>/dev/null | cut -d'=' -f2)
case "$current" in
local) next="global" ;;
@@ -384,56 +414,106 @@ case "$current" in
*) next="local" ;;
esac
sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE"
CYCLE_EOF
chmod +x "$cycle_script"
SPACE_EOF
sed -i "s|FNSFILE|$fns_file|" "$space_script"
chmod +x "$space_script"
# Exporter fonctions et variables dans un fichier source
{
declare -f format_skill_line state_get make_key
declare -p GRV_GREEN GRV_YELLOW GRV_AQUA GRV_GRAY GRV_BLUE GRV_FG GRV_PURPLE RESET \
ICO_OK ICO_UPD ICO_NEW ICO_NA ICO_LOCAL ICO_GLOBAL ICO_SKIP
echo "STATE_FILE='$STATE_FILE'"
echo "REPO_DIR='$REPO_DIR'"
echo "SKILLS_LIST=($(printf '"%s" ' "${SKILLS_LIST[@]}"))"
} > "$fns_file"
# Script de prévisualisation du skill (appelé par fzf --preview)
cat > "$preview_script" << 'PREVIEW_EOF'
# Script TAB : plier/déplier une catégorie
cat > "$tab_script" << 'TAB_EOF'
#!/usr/bin/env bash
source "$1"
idx=$(( $2 - 1 ))
entry="${SKILLS_LIST[$idx]:-}"
[[ -z "$entry" ]] && exit 0
IFS='|' read -r cat skill agent _ <<< "$entry"
skill_file="${REPO_DIR}/skills/${cat}/${skill}/${agent}.md"
[[ -f "$skill_file" ]] && cat "$skill_file" || echo "Fichier introuvable : $skill_file"
PREVIEW_EOF
sed -i "s|\"\$1\"|\"$fns_file\"|" "$preview_script"
chmod +x "$preview_script"
source "FNSFILE"
td="$1"; type="${td%%:*}"; value="${td#*:}"
if [[ "$type" == "h" ]]; then
cat_name="$value"
elif [[ "$type" == "s" ]]; then
entry="${SKILLS_LIST[$value]:-}"
[[ -z "$entry" ]] && exit 0
cat_name="${entry%%|*}"
else
exit 0
fi
if grep -qx "$cat_name" "$COLLAPSED_FILE" 2>/dev/null; then
sed -i "/^${cat_name}$/d" "$COLLAPSED_FILE"
else
echo "$cat_name" >> "$COLLAPSED_FILE"
fi
TAB_EOF
sed -i "s|FNSFILE|$fns_file|" "$tab_script"
chmod +x "$tab_script"
# Script générateur de liste pour fzf --reload
cat > "$list_script" << LIST_EOF
# Script LIST : générateur d'arbre pour fzf --reload
cat > "$list_script" << 'LIST_EOF'
#!/usr/bin/env bash
source "$fns_file"
for entry in "\${SKILLS_LIST[@]}"; do
format_skill_line "\$entry"
source "FNSFILE"
declare -A cat_map
declare -a cat_order
for i in "${!SKILLS_LIST[@]}"; do
cat="${SKILLS_LIST[$i]%%|*}"
[[ -z "${cat_map[$cat]+x}" ]] && { cat_order+=("$cat"); cat_map[$cat]=""; }
cat_map[$cat]+=" $i"
done
for cat in "${cat_order[@]}"; do
indices=(${cat_map[$cat]})
collapsed=0
grep -qx "$cat" "$COLLAPSED_FILE" 2>/dev/null && collapsed=1
format_category_header "$cat" "${#indices[@]}" "$collapsed"
[[ "$collapsed" == "1" ]] && continue
for idx in "${indices[@]}"; do
format_skill_line "${SKILLS_LIST[$idx]}" "$idx"
done
done
LIST_EOF
sed -i "s|FNSFILE|$fns_file|" "$list_script"
chmod +x "$list_script"
# Script PREVIEW : affiche le skill avec coloration (bat si dispo)
cat > "$preview_script" << 'PREVIEW_EOF'
#!/usr/bin/env bash
source "FNSFILE"
td="$1"; type="${td%%:*}"; value="${td#*:}"
if [[ "$type" == "s" ]]; then
entry="${SKILLS_LIST[$value]:-}"
[[ -z "$entry" ]] && exit 0
IFS='|' read -r cat skill agent _ <<< "$entry"
skill_file="${REPO_DIR}/skills/${cat}/${skill}/${agent}.md"
if [[ ! -f "$skill_file" ]]; then echo "Fichier introuvable : $skill_file"; exit 0; fi
if command -v bat &>/dev/null; then
bat --style=numbers,header --color=always --language=markdown "$skill_file"
elif command -v batcat &>/dev/null; then
batcat --style=numbers,header --color=always --language=markdown "$skill_file"
else
cat "$skill_file"
fi
elif [[ "$type" == "h" ]]; then
echo "=== Catégorie : ${value} ==="
for entry in "${SKILLS_LIST[@]}"; do
[[ "${entry%%|*}" == "$value" ]] || continue
IFS='|' read -r _ skill agent _ _ _ desc tags <<< "$entry"
echo " • ${skill} [${agent}] ${desc} ${tags}"
done
fi
PREVIEW_EOF
sed -i "s|FNSFILE|$fns_file|" "$preview_script"
chmod +x "$preview_script"
local legend
legend=$(echo -e "${GRV_GRAY}État: ${GRV_GREEN} installé ${GRV_YELLOW} MAJ ${GRV_AQUA}+ nouveau Action: ${GRV_GREEN}●L local ${GRV_BLUE}●G global ${GRV_GRAY}ignorer TAB=changer ENTER=confirmer ESC=quitter${RESET}")
legend=$(echo -e "${GRV_GRAY}État: ${GRV_GREEN}${GRV_YELLOW}${GRV_AQUA}+ Action: ${GRV_GREEN}●L ${GRV_BLUE}●G ${GRV_GRAY} SPACE=action TAB=plier v=voir ENTER=ok ESC=quitter${RESET}")
fzf \
--ansi \
--delimiter='\t' \
--with-nth=2.. \
--nth=2.. \
--prompt="Skills > " \
--header="$legend" \
--preview="bash $preview_script {n}" \
--preview-window="right:45%:wrap" \
--bind="tab:execute-silent($cycle_script '$STATE_FILE' '$fns_file' {n})+reload($list_script)" \
--preview="bash $preview_script {1}" \
--preview-window="right:50%:wrap:hidden" \
--bind="space:execute-silent(bash $space_script {1})+reload(bash $list_script)" \
--bind="tab:execute-silent(bash $tab_script {1})+reload(bash $list_script)" \
--bind="v:toggle-preview" \
< <(bash "$list_script") > /dev/null || true
rm -f "$cycle_script" "$list_script" "$fns_file" "$preview_script"
rm -f "$space_script" "$tab_script" "$list_script" "$fns_file" "$preview_script"
}
# ── Installation ──────────────────────────────────────────────────