From 5088ec0189bc5e86fd448a09cdbaa88937d7091d Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sat, 16 May 2026 05:42:55 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20install.sh=20=E2=80=94=20arbre=20cat?= =?UTF-8?q?=C3=A9gories,=20SPACE=3Daction,=20TAB=3Dplier,=20v=3Dviewer=20b?= =?UTF-8?q?at?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- install.sh | 192 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 136 insertions(+), 56 deletions(-) diff --git a/install.sh b/install.sh index 01ed0d6..cf376f1 100755 --- a/install.sh +++ b/install.sh @@ -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 ──────────────────────────────────────────────────