dotfiles

Repositório dotfiles
git clone git://git.sivaldodavi.com/dotfiles.git
Log | Files | Refs | README

culto (15877B)


      1 #!/usr/bin/env bash
      2 
      3 # ==============================================================================
      4 # Configuração
      5 # ==============================================================================
      6 
      7 CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}"
      8 BIBLE_FILE="$CACHE_DIR/biblia.json"
      9 MUSICAS_FILE="$CACHE_DIR/musicas.json"
     10 PIX_FILE="$CACHE_DIR/pix-igreja.png"
     11 AGENDA_FILE="$CACHE_DIR/agenda.jpeg"
     12 SENT_FILE="/tmp/sent_presentation"
     13 
     14 : "${EDITOR:=vim}"
     15 
     16 LITURGIA_FIXA=(
     17     "T: oração inicial"
     18     "T: oração de contrição"
     19     "T: oração pelas crianças"
     20     "T: oração pela palavra"
     21     "T: ceia"
     22     "T: ofertório"
     23     "T: oração final"
     24     "T: boas vindas"
     25     "AG: agenda"
     26     "C: limpar tela"
     27 )
     28 
     29 AGENDA_TEXTO="Terça (19:30)   - Culto de oração
     30 Quinta (19:30)  - Culto de doutrina
     31 Domingo (08:30) - EBD
     32 Domingo (18:00) - Culto solene"
     33 
     34 # ==============================================================================
     35 # Utilitários
     36 # ==============================================================================
     37 
     38 if ! command -v xrandr >/dev/null 2>&1; then
     39     echo "'xrandr' não encontrado. Instalando..." >&2
     40     doas apk add xrandr || { echo "ERRO: Falha ao instalar xrandr." >&2; exit 1; }
     41 fi
     42 
     43 # ==============================================================================
     44 # Bíblia Local
     45 # ==============================================================================
     46 
     47 _ensure_bible() {
     48     if [ ! -f "$BIBLE_FILE" ]; then
     49         echo "ERRO: Arquivo de bíblia não encontrado em: $BIBLE_FILE" >&2
     50         return 1
     51     fi
     52 }
     53 
     54 _bible_python() {
     55     BIBLE_FILE="$BIBLE_FILE" python3 - "$@" <<'PYEOF'
     56 import os, re, sys, signal, json
     57 
     58 # Ignora erro de Broken Pipe quando pipeamos pro fzf ou less
     59 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
     60 
     61 BIBLE_FILE  = os.environ["BIBLE_FILE"]
     62 SLIDE_WIDTH = 180
     63 
     64 USE_COLOR = sys.stdout.isatty()
     65 def _c(code, t): return f"\033[{code}m{t}\033[0m" if USE_COLOR else t
     66 def azul(t):     return _c("1;34", t)
     67 def amarelo(t):  return _c("1;33", t)
     68 def verde(t):    return _c("1;32", t)
     69 def vermelho(t): return _c("1;31", t)
     70 
     71 def load_bible():
     72     verses = []
     73     with open(BIBLE_FILE, encoding="utf-8") as f:
     74         data = json.load(f)
     75         for book_obj in data:
     76             book_name = book_obj.get("name", "")
     77             book_abbrev = book_obj.get("abbrev", "")
     78 
     79             for cap_idx, chapter in enumerate(book_obj.get("chapters", [])):
     80                 cap_num = cap_idx + 1
     81 
     82                 for vers_idx, text in enumerate(chapter):
     83                     vers_num = vers_idx + 1
     84 
     85                     verses.append({
     86                         "book": book_name,
     87                         "abbrev": book_abbrev,
     88                         "chapter": cap_num,
     89                         "verse": vers_num,
     90                         "text": text
     91                     })
     92     return verses
     93 
     94 def parse_ref(query, verses):
     95     query = query.strip()
     96     parts = query.rpartition(" ")
     97     book, num = parts[0].strip(), parts[2].strip()
     98     if not book:
     99         book, num = num, ""
    100     try:
    101         if ":" in num:
    102             cap_str, vrs_part = num.split(":", 1)
    103             cap_num = int(cap_str)
    104             if "-" in vrs_part:
    105                 v1, v2 = (int(x) for x in vrs_part.split("-", 1))
    106             else:
    107                 v1 = v2 = int(vrs_part)
    108             return [v for v in verses
    109                     if (v["book"].lower().startswith(book.lower()) or v["abbrev"].lower() == book.lower())
    110                     and v["chapter"] == cap_num
    111                     and v1 <= v["verse"] <= v2]
    112         else:
    113             cap_num = int(num) if num.isdigit() else 0
    114             return [v for v in verses
    115                     if (v["book"].lower().startswith(book.lower()) or v["abbrev"].lower() == book.lower())
    116                     and (cap_num == 0 or v["chapter"] == cap_num)]
    117     except ValueError:
    118         return []
    119 
    120 def _chunks(text, width):
    121     words, chunks, current = text.split(), [], ""
    122     for word in words:
    123         if len(current) + len(word) + 1 > width:
    124             chunks.append(current.rstrip())
    125             current = word + " "
    126         else:
    127             current += word + " "
    128     if current.strip():
    129         chunks.append(current.rstrip())
    130     return chunks
    131 
    132 def slides_read(rows, ref_label, no_cover, resp):
    133     out = []
    134     if not no_cover:
    135         out += [ref_label.upper(), ""]
    136     if resp:
    137         for i, row in enumerate(rows):
    138             label = "DIRIGENTE" if i % 2 == 0 else "IGREJA"
    139             header = f"{row['book']} {row['chapter']} ({label})"
    140             out += [f"{header}\n{row['verse']}. {row['text']}", ""]
    141     else:
    142         full_text = " ".join(f"{r['verse']}. {r['text']}" for r in rows)
    143         chunks = _chunks(full_text, SLIDE_WIDTH)
    144         for i, chunk in enumerate(chunks):
    145             pre = "... " if i > 0 else ""
    146             pos = " ..." if i < len(chunks) - 1 else ""
    147             out += [f"{ref_label}\n{pre}{chunk}{pos}", ""]
    148     return "\n".join(out)
    149 
    150 def terminal_read(rows, ref_label):
    151     print(azul(ref_label))
    152     print()
    153     for r in rows:
    154         num = verde(f"{r['verse']:3}.")
    155         print(f"  {num} {r['text']}")
    156     print()
    157 
    158 cmd  = sys.argv[1]
    159 rest = sys.argv[2:]
    160 
    161 if cmd == "read":
    162     no_cover = "--no-cover" in rest
    163     resp     = "--resp"     in rest
    164     query    = " ".join(a for a in rest if not a.startswith("--"))
    165     verses   = load_bible()
    166     rows     = parse_ref(query, verses)
    167     if not rows:
    168         print(vermelho(f"Nenhuma referência encontrada para '{query}'."), file=sys.stderr)
    169         sys.exit(1)
    170     num_part  = query.rpartition(" ")[2]
    171     ref_label = f"{rows[0]['book']} {num_part}" if num_part else rows[0]["book"]
    172     print(slides_read(rows, ref_label, no_cover, resp))
    173 
    174 elif cmd == "terminal":
    175     query  = " ".join(rest)
    176     verses = load_bible()
    177     rows   = parse_ref(query, verses)
    178     if not rows:
    179         print(vermelho(f"Nenhuma referência encontrada para '{query}'."), file=sys.stderr)
    180         sys.exit(1)
    181     num_part  = query.rpartition(" ")[2]
    182     ref_label = f"{rows[0]['book']} {num_part}" if num_part else rows[0]["book"]
    183     terminal_read(rows, ref_label)
    184 
    185 elif cmd == "search":
    186     word    = " ".join(rest)
    187     verses  = load_bible()
    188     results = [v for v in verses if word.lower() in v["text"].lower()]
    189     pattern = re.compile(re.escape(word), re.IGNORECASE)
    190     print(azul(f"{len(results)} ocorrência(s) de '{amarelo(word)}':"))
    191     print()
    192     for r in results:
    193         ref         = f"{r['book']} {r['chapter']}:{r['verse']}"
    194         highlighted = pattern.sub(lambda m: vermelho(m.group(0)), r["text"])
    195         print(f"{verde(f'{ref:25}')} {highlighted}")
    196 
    197 PYEOF
    198 }
    199 
    200 # ==============================================================================
    201 # Integração ao 'sent'
    202 # ==============================================================================
    203 
    204 format_sent() {
    205     awk '{ print toupper($0) }' \
    206         | fold -s -w 40 \
    207         | sed 's/[[:space:]]*$//' \
    208         > "$SENT_FILE"
    209 
    210     if pgrep -x "sent" >/dev/null; then
    211         xdotool search --class sent key r
    212     else
    213         nohup sent "$SENT_FILE" >/dev/null 2>&1 &
    214     fi
    215 }
    216 
    217 clear_sent() {
    218     printf ' ' > "$SENT_FILE"
    219     pgrep -x "sent" >/dev/null && xdotool search --class sent key r
    220 }
    221 
    222 # ==============================================================================
    223 # Músicas
    224 # ==============================================================================
    225 
    226 _ensure_musicas() {
    227     if [ ! -f "$MUSICAS_FILE" ]; then
    228         # Cria vazio, já com as categorias, se não existir
    229         printf '{\n  "Hinos": {},\n  "Salmos": {},\n  "Cânticos Espirituais": {}\n}\n' > "$MUSICAS_FILE"
    230     fi
    231 }
    232 
    233 _render_json_song() {
    234     local title="$1"
    235     MUSICAS_FILE="$MUSICAS_FILE" python3 - "$title" <<'PYEOF'
    236 import sys, json, os, re, signal
    237 
    238 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    239 
    240 musicas_file = os.environ["MUSICAS_FILE"]
    241 target_title = sys.argv[1]
    242 
    243 with open(musicas_file, encoding='utf-8') as f:
    244     categorias = json.load(f)
    245 
    246 # Procura o título em qualquer categoria (Hinos / Salmos / Cânticos Espirituais)
    247 stanzas = None
    248 for cat_songs in categorias.values():
    249     if target_title in cat_songs:
    250         stanzas = cat_songs[target_title]
    251         break
    252 
    253 if stanzas is not None:
    254     m = re.match(r'^(\d+(?:-[A-Za-z])?)\s*-\s*.+$', target_title)
    255     if m:
    256         print(f"HINO {m.group(1)}")
    257         print("\n\n")
    258     elif re.search(r'salmo', target_title, re.IGNORECASE):
    259         print(target_title)
    260         print("\n\n")
    261 
    262     for stanza in stanzas:
    263         print(stanza)
    264         print("\n")
    265 else:
    266     print(f"Erro: Música '{target_title}' não encontrada.", file=sys.stderr)
    267     sys.exit(1)
    268 PYEOF
    269 }
    270 
    271 pick_song() {
    272     _ensure_musicas || return 1
    273 
    274     local chosen
    275     chosen=$(python3 -c "
    276 import json, signal
    277 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    278 data = json.load(open('$MUSICAS_FILE'))
    279 titulos = sorted(t for cat_songs in data.values() for t in cat_songs)
    280 print('\n'.join(titulos))
    281 " | fzf --reverse --prompt="Música: ")
    282 
    283     [ -z "$chosen" ] && return 0
    284 
    285     _render_json_song "$chosen" | format_sent
    286 }
    287 
    288 # ==============================================================================
    289 # Adicionar / Editar Músicas (JSON <-> MD temporário)
    290 # ==============================================================================
    291 
    292 _update_json_from_md() {
    293     local action="$1"
    294     local title="$2"
    295     local tmp_md="$3"
    296 
    297     MUSICAS_FILE="$MUSICAS_FILE" python3 - "$action" "$title" "$tmp_md" <<'PYEOF'
    298 import json, sys, os, re
    299 
    300 musicas_file = os.environ["MUSICAS_FILE"]
    301 action = sys.argv[1]
    302 title = sys.argv[2]
    303 tmp_md = sys.argv[3]
    304 
    305 CATEGORIAS = ["Hinos", "Salmos", "Cânticos Espirituais"]
    306 
    307 if os.path.exists(musicas_file):
    308     with open(musicas_file, 'r', encoding='utf-8') as f:
    309         try:
    310             songs = json.load(f)
    311         except json.JSONDecodeError:
    312             songs = {}
    313 else:
    314     songs = {}
    315 
    316 # Garante que as categorias existam
    317 for c in CATEGORIAS:
    318     songs.setdefault(c, {})
    319 
    320 def find_category(title):
    321     """Procura em qual categoria o título já existe. None se for novo."""
    322     for cat in CATEGORIAS:
    323         if title in songs.get(cat, {}):
    324             return cat
    325     return None
    326 
    327 def infer_category(title):
    328     """Para músicas novas: decide a categoria pelo padrão do título."""
    329     if re.match(r'^\d+(?:-[A-Za-z])?\s*-\s*.+$', title):
    330         return "Hinos"
    331     if re.search(r'salmo', title, re.IGNORECASE):
    332         return "Salmos"
    333     return "Cânticos Espirituais"
    334 
    335 if action == "extract":
    336     cat = find_category(title)
    337     text = "\n\n".join(songs.get(cat, {}).get(title, [])) if cat else ""
    338     with open(tmp_md, 'w', encoding='utf-8') as f:
    339         f.write(f"# {title}\n\n{text}\n")
    340 
    341 elif action == "save":
    342     with open(tmp_md, 'r', encoding='utf-8') as f:
    343         content = f.read()
    344 
    345     # Remove o título em markdown (ex: # Nome da música)
    346     content = re.sub(r'^#.*\n?', '', content, flags=re.MULTILINE)
    347 
    348     # Separa os blocos (estrofes) por linhas em branco
    349     stanzas = [b.strip() for b in re.split(r'\n{2,}', content) if b.strip()]
    350 
    351     if stanzas:
    352         # Se já existe em alguma categoria, atualiza lá; senão, infere a categoria
    353         cat = find_category(title) or infer_category(title)
    354         songs[cat][title] = stanzas
    355         with open(musicas_file, 'w', encoding='utf-8') as f:
    356             json.dump(songs, f, ensure_ascii=False, indent=2)
    357         print(f"✅ Música salva com sucesso em '{cat}'!")
    358     else:
    359         print("⚠️  Nenhuma estrofe encontrada. Alteração descartada.")
    360 PYEOF
    361 }
    362 
    363 add_song() {
    364     _ensure_musicas
    365     echo -n "Digite o título da nova música: "
    366     read -r title
    367     [ -z "$title" ] && return 1
    368 
    369     local tmp_md="/tmp/culto_musica_$$.md"
    370     echo -e "# $title\n\nEstrofe 1...\n\nEstrofe 2..." > "$tmp_md"
    371     
    372     # Abre no Vim forçando o modo markdown
    373     ${EDITOR} -c "set filetype=markdown" "$tmp_md"
    374     
    375     # Salva o resultado no json
    376     _update_json_from_md "save" "$title" "$tmp_md"
    377     rm -f "$tmp_md"
    378 }
    379 
    380 edit_song() {
    381     _ensure_musicas || return 1
    382 
    383     local chosen
    384     chosen=$(python3 -c "
    385 import json, signal
    386 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    387 data = json.load(open('$MUSICAS_FILE'))
    388 titulos = sorted(t for cat_songs in data.values() for t in cat_songs)
    389 print('\n'.join(titulos))
    390 " | fzf --reverse --prompt="Editar Música: ")
    391 
    392     [ -z "$chosen" ] && return 0
    393 
    394     local tmp_md="/tmp/culto_musica_$$.md"
    395     
    396     # Extrai o array do JSON e converte em texto pra editar
    397     _update_json_from_md "extract" "$chosen" "$tmp_md"
    398     
    399     # Abre no Vim forçando o modo markdown
    400     ${EDITOR} -c "set filetype=markdown" "$tmp_md"
    401     
    402     # Pega o arquivo editado e devolve pro JSON
    403     _update_json_from_md "save" "$chosen" "$tmp_md"
    404     rm -f "$tmp_md"
    405 }
    406 
    407 # ==============================================================================
    408 # Bíblia — wrappers para sent
    409 # ==============================================================================
    410 
    411 get_bible() {
    412     local query="$1"
    413     local no_cover="$2"
    414     local resp="$3"
    415 
    416     local flags=""
    417     [ "$no_cover" = "true" ] && flags="$flags --no-cover"
    418     [ "$resp"     = "true" ] && flags="$flags --resp"
    419 
    420     _ensure_bible || exit 1
    421     _bible_python read $flags "$query" | format_sent
    422 }
    423 
    424 # ==============================================================================
    425 # Liturgia
    426 # ==============================================================================
    427 
    428 show_menu() {
    429     while true; do
    430         local sel
    431         sel=$(printf '%s\n' "${LITURGIA_FIXA[@]}" \
    432             | fzf --reverse --prompt="Liturgia: ")
    433         [ -z "$sel" ] && break
    434 
    435         local tp val
    436         tp="${sel%%:*}"
    437         val=$(printf '%s' "${sel#*:}" | sed 's/^[[:space:]]*//')
    438 
    439         case "$tp" in
    440             T)  printf '%s' "$val" | format_sent ;;
    441             AG) printf '%s' "$AGENDA_TEXTO" | format_sent ;;
    442             C)  clear_sent ;;
    443             *)  echo "Tipo desconhecido: $tp" >&2 ;;
    444         esac
    445     done
    446 }
    447 
    448 # ==============================================================================
    449 # Lógica Principal
    450 # ==============================================================================
    451 
    452 case "${1:-}" in
    453     m|menu)           show_menu ;;
    454     
    455     cm)               pick_song ;;
    456     as|add-song)         add_song ;;
    457     es|edit-song)        edit_song ;;
    458 
    459     b|bible)          get_bible "$2" "false" "false" ;;
    460     bd|bible-clean)   get_bible "$2" "true"  "false" ;;
    461     r|resp)           get_bible "$2" "false" "true"  ;;
    462 
    463     v|ver)
    464         _ensure_bible || exit 1
    465         _bible_python terminal "$2" | less -R -i
    466         ;;
    467     s|search)
    468         _ensure_bible || exit 1
    469         _bible_python search "$2" | less -R -i
    470         ;;
    471 
    472     t|text)           printf '%s' "$2" | format_sent ;;
    473     ag|agenda)        printf '%s' "$AGENDA_TEXTO" | format_sent ;;
    474     x|clear)          clear_sent ;;
    475 
    476     i|index)
    477         awk 'BEGIN{RS="\n\n+"; c=1} {
    478             gsub(/^\s+|\s+$/, "")
    479             if (length($0) > 0) printf "[%02d] %.40s...\n", c++, $0
    480         }' "$SENT_FILE"
    481         ;;
    482 
    483     p|pix)
    484         if [ ! -f "$PIX_FILE" ]; then
    485             echo "Erro: Arquivo PIX não encontrado em $PIX_FILE" >&2
    486             exit 1
    487         fi
    488         nohup nsxiv "$PIX_FILE" >/dev/null 2>&1 &
    489         ;;
    490 
    491     *)
    492         cat <<'USAGE'
    493 culto <comando> [arg]
    494 
    495   m / menu          Liturgia (fzf)
    496   
    497   cm                Selecionar música (fzf)
    498   as / add-song     Adicionar nova música (via Vim)
    499   es / edit-song    Editar música existente (via Vim)
    500   
    501   b / bible <ref>   Bíblia (com capa)
    502   bd        <ref>   Bíblia (sem capa)
    503   r / resp  <ref>   Responsivo (DIRIGENTE / IGREJA)
    504   v / ver   <ref>   Bíblia no terminal
    505   s / search <pal>  Buscar na bíblia
    506   
    507   t / text  <texto> Texto livre
    508   ag / agenda       Agenda da semana
    509   i / index         Índice dos slides atuais
    510   x / clear         Limpar tela do sent
    511   p / pix           Abrir QR code PIX
    512 USAGE
    513         ;;
    514 esac