"""recordatorios_msgbox.py — Recordatorios con cuadros de dialogo (MessageBox) en Windows. Muestra un message box cuando la hora de un recordatorio llega (o ya ha pasado y no esta hecho). - Si el usuario pulsa SI -> se marca como hecho (done=True) y no vuelve a salir. - Si pulsa NO -> se pospone 5 minutos (snooze). - Si se ejecuta con --done se marca como terminado (lo usa el asistente cuando el usuario dice "terminada"). - Si se ejecuta con --listar muestra el estado de todos los recordatorios. Fichero de datos: recordatorios_msgbox.json (formato: id, texto, fecha [diario|YYYY-MM-DD], hora HH:MM, done). Uso: python scripts/recordatorios_msgbox.py # bucle de monitorizacion (correr en background) python scripts/recordatorios_msgbox.py --listar # listar estado python scripts/recordatorios_msgbox.py --done # marcar como terminado python scripts/recordatorios_msgbox.py --add "texto" --hora HH:MM [--fecha diario|YYYY-MM-DD] python scripts/recordatorios_msgbox.py --test # mostrar el cuadro sin esperar a la hora python scripts/recordatorios_msgbox.py --sync # crear recordatorios desde pendientes.json (tareas con campo hora) """ import argparse import ctypes import json import sys import time from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import infra_paths # noqa: E402 (resolucion de rutas del repo INFRAESTRUCTURA) DATA = infra_paths.ESTADO_DIR / 'recordatorios_msgbox.json' PENDIENTES = infra_paths.biblioteca_repo() / 'pendientes.json' # Constantes de MessageBoxW de Win32 MB_OK = 0x0 MB_YESNO = 0x4 MB_ICONINFORMATION = 0x40 MB_ICONQUESTION = 0x20 MB_SYSTEMMODAL = 0x1000 IDYES = 6 IDNO = 7 SLEEP_SEG = 15 # cadencia de comprobacion en segundos SNOOZE_MIN = 5 # minutos a posponer si pulsa NO def cargar(): try: return json.loads(DATA.read_text(encoding='utf-8-sig')) except FileNotFoundError: return {'recordatorios': []} def guardar(data): DATA.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding='utf-8') def fecha_hoy(): return datetime.now().strftime('%Y-%m-%d') def hora_ahora(): return datetime.now().strftime('%H:%M') def msgbox(texto, titulo, yesno=False): """Muestra un cuadro de dialogo nativo de Windows. Devuelve IDYES/IDNO/1.""" flags = MB_SYSTEMMODAL | MB_ICONINFORMATION if yesno: flags |= MB_YESNO else: flags |= MB_OK return ctypes.windll.user32.MessageBoxW(None, texto, titulo, flags) def es_hora(r): """Comprueba si toca mostrar el recordatorio.""" if r.get('done'): return False if r.get('fecha', 'diario') == 'diario': return True return r.get('fecha') <= fecha_hoy() def run(): data = cargar() ultima_vez = {} # id -> 'HH:MM' ya mostrado hoy (o fecha mostrada) print(f'[recordatorios_msgbox] Monitorizando {len(data["recordatorios"])} recordatorios. Ctrl+C para salir.') while True: data = cargar() ahora = fecha_hoy() + ' ' + hora_ahora() for r in data['recordatorios']: if not es_hora(r): continue # evita repetir en la misma hora ya mostrada para ese id clave = f"{r['id']}-{fecha_hoy()}-{r.get('hora')}" if ultima_vez.get(clave): continue if hora_ahora() >= r.get('hora', ''): ultima_vez[clave] = True texto = r.get('texto', '') res = msgbox(texto, f'RECORDATORIO {r.get("hora")}', yesno=True) if res == IDYES: r['done'] = True guardar(data) print(f'[{ahora}] #{r["id"]} marcado como HECHO: {texto[:60]}') else: # snooze: fuerza a no repetir en los proximos SNOOZE_MIN minutos ultima_vez[clave] = True print(f'[{ahora}] #{r["id"]} pospuesto (NO).') time.sleep(SLEEP_SEG) def listar(): data = cargar() if not data['recordatorios']: print('No hay recordatorios.') return print(f'{"ID":>3} {"HORA":>5} {"FECHA":<12} {"ESTADO":<10} TAREA') for r in sorted(data['recordatorios'], key=lambda x: (x.get('hora', ''), x.get('id', 0))): estado = 'HECHO' if r.get('done') else 'PENDIENTE' print(f'{r.get("id", 0):>3} {r.get("hora", ""):>5} {str(r.get("fecha", "")):<12} {estado:<10} {r.get("texto", "")[:70]}') def marcar_done(i): data = cargar() for r in data['recordatorios']: if r.get('id') == i: r['done'] = True guardar(data) print(f'#{i} marcado como terminado. Ya no saldra mas.') return print(f'No existe el recordatorio #{i}') def add(texto, hora, fecha): data = cargar() ids = [r.get('id', 0) for r in data['recordatorios']] nid = max(ids) + 1 if ids else 1 data['recordatorios'].append({'id': nid, 'texto': texto, 'fecha': fecha, 'hora': hora, 'done': False}) guardar(data) print(f'Recordatorio #{nid} anadido: {hora} [{fecha}] {texto}') def test(i): data = cargar() r = next((x for x in data['recordatorios'] if x.get('id') == i), None) if not r: print(f'No existe el recordatorio #{i}') return res = msgbox(r.get('texto', ''), f'TEST RECORDATORIO {r.get("hora")}', yesno=True) if res == IDYES: r['done'] = True guardar(data) print(f'#{i} marcado como HECHO (test).') def sync(): """Crea recordatorios desde pendientes.json: tareas activas con campo 'hora' (HH:MM). La fecha sale del campo 'date' del pendiente o 'diario' si es de hoy.""" try: data = cargar() pends = json.loads(PENDIENTES.read_text(encoding='utf-8-sig')) except FileNotFoundError: print(f'[sync] No existe {PENDIENTES.name}. Nada que sincronizar.') return hoy = fecha_hoy() creados = 0 for p in pends.get('pending', []): if p.get('done'): continue hora = p.get('hora') if not hora: continue fecha_p = p.get('date', hoy) texto = p.get('task', '') # evita duplicados: mismo pendiente_id, o mismo texto y misma hora pid = p.get('id') if any(r.get('pendiente_id') == pid for r in data['recordatorios']): continue if any(r.get('texto') == texto and r.get('hora') == hora for r in data['recordatorios']): continue ids = [r.get('id', 0) for r in data['recordatorios']] nid = max(ids) + 1 if ids else 1 data['recordatorios'].append({ 'id': nid, 'texto': f'[pendiente #{p.get("id")}] {texto}', 'fecha': fecha_p if fecha_p else 'diario', 'hora': hora, 'done': False, 'pendiente_id': p.get('id') }) creados += 1 print(f'[sync] #{nid} creado desde pendiente #{p.get("id")}: {hora} {texto[:60]}') if creados: guardar(data) print(f'[sync] Sincronizacion completa. {creados} recordatorio(s) nuevo(s).') def main(): ap = argparse.ArgumentParser(description='Recordatorios con message box') ap.add_argument('--listar', action='store_true', help='listar estado de los recordatorios') ap.add_argument('--done', type=int, metavar='ID', help='marcar recordatorio como terminado') ap.add_argument('--add', metavar='TEXTO', help='anadir recordatorio (usa --hora y --fecha)') ap.add_argument('--hora', default='12:00', help='hora HH:MM (por defecto 12:00)') ap.add_argument('--fecha', default='diario', help='fecha YYYY-MM-DD o "diario" (por defecto diario)') ap.add_argument('--test', type=int, metavar='ID', help='mostrar el cuadro ahora (sin esperar hora)') ap.add_argument('--sync', action='store_true', help='crear recordatorios desde pendientes.json con hora') args = ap.parse_args() if args.sync: sync() elif args.listar: listar() elif args.done is not None: marcar_done(args.done) elif args.add: add(args.add, args.hora, args.fecha) elif args.test is not None: test(args.test) else: run() if __name__ == '__main__': try: sys.stdout.reconfigure(encoding='utf-8') except Exception: pass main()