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