diff --git a/DOXA_ROADMAP.md b/DOXA_ROADMAP.md index 4857c98..d7eae1e 100644 --- a/DOXA_ROADMAP.md +++ b/DOXA_ROADMAP.md @@ -103,31 +103,9 @@ Tento dokument obsahuje návrhy na rozšíření kolekce DOXA aplikací. Všechn --- -### 5. ✅ DOXA Habit Tracker 🎯 -**Popis**: Tracker návyků pro terminál - -- **Priorita**: Střední -- **Integrace**: Motivace a sledování osobního růstu -- **Cílová platforma**: Linux, FreeBSD -- **Technologie**: Go, Bubble Tea - -**Plánované funkce**: -- [ ] Vytváření návyků s cíli (denní, týdenní) -- [ ] Denní sledování splnění -- [ ] Statistiky a grafy (ASCII) -- [ ] Motivující zprávy a upozornění -- [ ] Export dat do CSV -- [ ] Přehled streaku (počet po sobě jdoucích dnů) -- [ ] Dvoujazyčné rozhraní (CZ/EN) - -**Odhadovaná složitost**: Střední -**Odhadovaný čas**: 2 týdny - ---- - ## 🌟 Bonusové nápady -### 6. DOXA Finance 💰 +### 5. DOXA Finance 💰 **Popis**: Správa výdajů a příjmů - **Funkce**: Evidence transakcí, kategorie, statistiky, rozpočty @@ -137,7 +115,7 @@ Tento dokument obsahuje návrhy na rozšíření kolekce DOXA aplikací. Všechn --- -### 7. DOXA Bookmarks 🔖 +### 6. DOXA Bookmarks 🔖 **Popis**: Správce záložek z příkazové řádky - **Funkce**: Ukládání URL, tagování, vyhledávání, otevírání v prohlížeči @@ -147,17 +125,7 @@ Tento dokument obsahuje návrhy na rozšíření kolekce DOXA aplikací. Všechn --- -### 8. DOXA Timer ⏱️ -**Popis**: Pomodoro timer pro terminál - -- **Funkce**: Nastavení časových intervalů, sledování produktivity, statistiky -- **Technologie**: Go, Bubble Tea -- **Složitost**: Nízká -- **Integrace**: Spuštění společně s doxa-todo - ---- - -### 9. DOXA Sync 🔄 +### 7. DOXA Sync 🔄 **Popis**: Lokální synchronizace mezi zařízeními - **Funkce**: Synchronizace dat přes SSH/SCP, konflikt resolution @@ -167,7 +135,7 @@ Tento dokument obsahuje návrhy na rozšíření kolekce DOXA aplikací. Všechn --- -### 10. DOXA Backup 💾 +### 8. DOXA Backup 💾 **Popis**: Nástroj pro zálohování DOXA dat - **Funkce**: Automatické zálohování, obnovení, verze, komprese @@ -180,17 +148,15 @@ Tento dokument obsahuje návrhy na rozšíření kolekce DOXA aplikací. Všechn ## 📊 Prioritizace | Pořadí | Aplikace | Priorita | Složitost | Čas | Integrace s existujícími | -|--------|----------|----------|-----------|------|--------------------------| +|--------|----------|----------|-----------|------|-----------------------------| | 1 | DOXA Calendar | Vysoká | Střední | 2-3 týdny | ✅ Vysoká (doxa-todo) | | 2 | DOXA Contacts | Vysoká | Nízká | 1-2 týdny | ✅ Střední | | 3 | DOXA Password | Vysoká | Vysoká | 3-4 týdny | ✅ Střední (doxa-secret) | | 4 | DOXA Journal | Střední | Nízká | 1-2 týdny | ⚠️ Nízká | -| 5 | DOXA Habit Tracker | Střední | Střední | 2 týdny | ⚠️ Nízká | -| 6 | DOXA Finance | Nízká | Vysoká | 3-4 týdny | ❌ Žádná | -| 7 | DOXA Bookmarks | Nízká | Nízká | 1 týden | ❌ Žádná | -| 8 | DOXA Timer | Nízká | Nízká | 1 týden | ✅ Střední (doxa-todo) | -| 9 | DOXA Sync | Nízká | Vysoká | 4+ týdny | ✅ Vysoká (všechny) | -| 10 | DOXA Backup | Nízká | Střední | 2-3 týdny | ✅ Vysoká (všechny) | +| 5 | DOXA Finance | Nízká | Vysoká | 3-4 týdny | ❌ Žádná | +| 6 | DOXA Bookmarks | Nízká | Nízká | 1 týden | ❌ Žádná | +| 7 | DOXA Sync | Nízká | Vysoká | 4+ týdny | ✅ Vysoká (všechny) | +| 8 | DOXA Backup | Nízká | Střední | 2-3 týdny | ✅ Vysoká (všechny) | --- @@ -204,13 +170,9 @@ Tento dokument obsahuje návrhy na rozšíření kolekce DOXA aplikací. Všechn 3. **DOXA Password** - Bezpečný správce hesel 4. **DOXA Journal** - Osobní deník -### Fáze 3: Pokročilé funkce (3-4 měsíce) -5. **DOXA Habit Tracker** - Sledování návyků -6. **DOXA Timer** - Pomodoro timer - -### Fáze 4: Systémové nástroje (4+ měsíce) -7. **DOXA Sync** - Synchronizace mezi zařízeními -8. **DOXA Backup** - Zálohovací nástroj +### Fáze 3: Systémové nástroje (3+ měsíce) +5. **DOXA Sync** - Synchronizace mezi zařízeními +6. **DOXA Backup** - Zálohovací nástroj --- @@ -232,12 +194,13 @@ Všechny nové aplikace by měly: - [x] DOXA Notes - ✅ Kompletní - [x] DOXA CLI - ✅ Kompletní - [x] DOXA Secret - ✅ Kompletní -- [x] DOXA TODO - ✅ Kompletní (nově přidáno označování splněných úkolů) +- [x] DOXA TODO - ✅ Kompletní +- [x] DOXA Timer - ✅ Kompletní (nově přidáno) +- [x] DOXA Habit - ✅ Kompletní (nově přidáno) - [ ] DOXA Calendar - ⏳ Plánováno - [ ] DOXA Contacts - ⏳ Plánováno - [ ] DOXA Password - ⏳ Plánováno - [ ] DOXA Journal - ⏳ Plánováno -- [ ] DOXA Habit Tracker - ⏳ Plánováno --- @@ -247,4 +210,4 @@ Pokud máte nápady na další aplikace nebo chcete upřednostnit některý z n --- -*Poslední aktualizace: 9. srpna 2026* +*Poslední aktualizace: 16. srpna 2026* diff --git a/README.md b/README.md index 76f1fc1..132464c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MIT license Go JavaScript - Platforms + Platforms

@@ -16,7 +16,7 @@ ## 🌍 DOXA -**DOXA** is a collection of three minimalist, locally-focused applications for note management, message encryption, and command-line text processing. All projects share a common philosophy: **complete independence, privacy, and control over your data**. No cloud services, no tracking, no external dependencies. Your data belongs only to you. +**DOXA** is a collection of six minimalist, locally-focused applications for note management, message encryption, task management, Pomodoro productivity and habit tracking. All projects share a common philosophy: **complete independence, privacy, and control over your data**. No cloud services, no tracking, no external dependencies. Your data belongs only to you. --- @@ -101,20 +101,66 @@ --- +### 5. ⏱️ [DOXA Timer](doxa-timer/) + +**Pomodoro timer for the terminal with productivity statistics** + +- **Description**: A minimalist Pomodoro timer for the terminal. Helps you focus using the Pomodoro technique — alternating work sessions with short and long breaks. Tracks your daily and total completed Pomodoros. +- **Philosophy**: Local-first productivity. No cloud apps, no subscriptions — just clean focus. Data stored in `~/.doxa-timer/`. +- **Technologies**: Go (Golang), Bubble Tea (TUI framework), Lip Gloss (styling). +- **Platforms**: Linux, FreeBSD. +- **Features**: + - Pomodoro technique: work session (25 min), short break (5 min), long break (15 min). + - Live countdown timer with ASCII progress bar. + - Configurable session lengths. + - Daily and total Pomodoro statistics. + - Bilingual interface (Czech/English) with `l` key toggle. + - Data stored in `~/.doxa-timer/doxa_timer.json`. + +📖 [Read detailed documentation](doxa-timer/README.md) + +--- + +### 6. 🎯 [DOXA Habit](doxa-habit/) + +**Habit tracker for the terminal with streak tracking** + +- **Description**: A minimalist terminal application for building and tracking habits. Monitor your daily and weekly habits, track streaks and visualize progress with ASCII heatmaps. +- **Philosophy**: Local-first personal growth. No gamification traps, no notification manipulation — just your data and your progress. Data stored in `~/.doxa-habit/`. +- **Technologies**: Go (Golang), Bubble Tea (TUI framework), Lip Gloss (styling). +- **Platforms**: Linux, FreeBSD. +- **Features**: + - Daily and weekly habits with completion tracking. + - Streak counter (consecutive days completed). + - ASCII heatmap for the last 7 days. + - Success rate statistics (7/30 days). + - Export data to CSV format. + - Search habits by name. + - Bilingual interface (Czech/English) with `l` key toggle. + - Data stored in `~/.doxa-habit/doxa_habit.json`. + +📖 [Read detailed documentation](doxa-habit/README.md) + +--- + ## 🔗 Common Features of All DOXA Projects -| Feature | DOXA Notes | DOXA CLI | DOXA Secret | DOXA TODO | -|---------|------------|----------|--------------|-----------| -| **Local Data Storage** | ✅ | ✅ | ✅ | ✅ | -| **Open Format (JSON)** | ✅ | ✅ | ✅ | ✅ | -| **No Cloud Dependencies** | ✅ | ✅ | ✅ | ✅ | -| **Open Source (MIT)** | ✅ | ✅ | ✅ | ✅ | -| **Minimalist Design** | ✅ | ✅ | ✅ | ✅ | -| **Tag Support** | ✅ | ✅ | ❌ | ✅ | -| **Search and Filtering** | ✅ | ✅ | ❌ | ✅ | -| **Encryption** | ❌ | ❌ | ✅ | ❌ | -| **Priority Levels** | ❌ | ❌ | ❌ | ✅ | -| **Categories** | ❌ | ❌ | ❌ | ✅ | +| Feature | DOXA Notes | DOXA CLI | DOXA Secret | DOXA TODO | DOXA Timer | DOXA Habit | +|---------|------------|----------|-------------|-----------|------------|------------| +| **Local Data Storage** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Open Format (JSON)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **No Cloud Dependencies** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Open Source (MIT)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Minimalist Design** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Bilingual (CZ/EN)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Tag Support** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| **Search & Filtering** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | +| **Encryption** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| **Priority Levels** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| **Categories** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| **Pomodoro Timer** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | +| **Streak / Habit Stats** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| **Data Export** | ✅ JSON | ✅ TXT | ✅ TXT | ✅ TXT | ✅ JSON | ✅ CSV | --- @@ -126,6 +172,8 @@ Each project has its own installation process. Detailed guides can be found in t - [DOXA CLI – Installation](doxa-cli/README.md#installation) - [DOXA Secret – Installation](doxa-secret/README_EN.md#installation) - [DOXA TODO – Installation](doxa-todo/README.md#installation) +- [DOXA Timer – Installation](doxa-timer/README.md#installation) +- [DOXA Habit – Installation](doxa-habit/README.md#installation) --- @@ -170,7 +218,7 @@ DOXA believes in: ## 🌍 DOXA -**DOXA** je sbírka tří minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv a práci s textem z příkazové řádky. Všechny projekty sdílejí společnou filozofii: **úplná nezávislost, soukromí a kontrola nad vašimi daty**. Žádné cloudové služby, žádné sledování, žádné externí závislosti. Vaše data patří pouze vám. +**DOXA** je sbírka šesti minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv, správu úkolů, Pomodoro produktivitu a sledování návyků. Všechny projekty sdílejí společnou filozofii: **úplná nezávislost, soukromí a kontrola nad vašimi daty**. Žádné cloudové služby, žádné sledování, žádné externí závislosti. Vaše data patří pouze vám. --- @@ -255,20 +303,66 @@ DOXA believes in: --- +### 5. ⏱️ [DOXA Timer](doxa-timer/) + +**Pomodoro timer pro terminál se statistikami produktivity** + +- **Popis**: Minimalistický Pomodoro timer pro terminál. Pomáhá vám se soustředit pomocí Pomodoro techniky — střídání pracovních sezení s krátkými a dlouhými přestávkami. Sleduje denní a celkový počet dokončených Pomodorů. +- **Filozofie**: Lokální produktivita. Žádné cloudové aplikace, žádné předplatné — jen čistý fokus na vaši práci. Data uložena v `~/.doxa-timer/`. +- **Technologie**: Go (Golang), Bubble Tea (TUI framework), Lip Gloss (stylování). +- **Platformy**: Linux, FreeBSD. +- **Vlastnosti**: + - Pomodoro technika: pracovní sezení (25 min), krátká přestávka (5 min), dlouhá přestávka (15 min). + - Live odpočítávání s ASCII progress barem. + - Konfigurovatelné délky sezení. + - Denní a celkové statistiky Pomodorů. + - Dvoujazyčné rozhraní (česky/anglicky) s přepínáním klávesou `l`. + - Data uložena v `~/.doxa-timer/doxa_timer.json`. + +📖 [Přečtěte si podrobnější dokumentaci](doxa-timer/README_CZ.md) + +--- + +### 6. 🎯 [DOXA Habit](doxa-habit/) + +**Tracker návyků pro terminál se sledováním streaku** + +- **Popis**: Minimalistická terminálová aplikace pro budování a sledování návyků. Sledujte denní a týdenní návyky, streaky a vizualizujte pokrok pomocí ASCII heatmap. +- **Filozofie**: Lokální osobní rozvoj. Žádné gamifikační pasti, žádná manipulace s notifikacemi — jen vaše data a váš pokrok. Data uložena v `~/.doxa-habit/`. +- **Technologie**: Go (Golang), Bubble Tea (TUI framework), Lip Gloss (stylování). +- **Platformy**: Linux, FreeBSD. +- **Vlastnosti**: + - Denní a týdenní návyky se sledováním splnění. + - Streak čítač (po sobě jdoucí dny splnění). + - ASCII heatmap posledních 7 dní. + - Statistiky úspěšnosti (7/30 dní). + - Export dat do CSV formátu. + - Vyhledávání návyků dle názvu. + - Dvoujazyčné rozhraní (česky/anglicky) s přepínáním klávesou `l`. + - Data uložena v `~/.doxa-habit/doxa_habit.json`. + +📖 [Přečtěte si podrobnější dokumentaci](doxa-habit/README_CZ.md) + +--- + ## 🔗 Společné vlastnosti všech projektů DOXA -| Vlastnost | DOXA Notes | DOXA CLI | DOXA Secret | DOXA TODO | -|-----------|------------|----------|--------------|-----------| -| **Lokální uložení dat** | ✅ | ✅ | ✅ | ✅ | -| **Otevřený formát (JSON)** | ✅ | ✅ | ✅ | ✅ | -| **Žádné cloudové závislosti** | ✅ | ✅ | ✅ | ✅ | -| **Open Source (MIT)** | ✅ | ✅ | ✅ | ✅ | -| **Minimalistický design** | ✅ | ✅ | ✅ | ✅ | -| **Podpora tagů** | ✅ | ✅ | ❌ | ✅ | -| **Vyhledávání a filtrování** | ✅ | ✅ | ❌ | ✅ | -| **Šifrování** | ❌ | ❌ | ✅ | ❌ | -| **Úrovně priority** | ❌ | ❌ | ❌ | ✅ | -| **Kategorie** | ❌ | ❌ | ❌ | ✅ | +| Vlastnost | DOXA Notes | DOXA CLI | DOXA Secret | DOXA TODO | DOXA Timer | DOXA Habit | +|-----------|------------|----------|-------------|-----------|------------|------------| +| **Lokální uložení dat** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Otevřený formát (JSON)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Žádné cloudové závislosti** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Open Source (MIT)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Minimalistický design** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Dvoujazyčné rozhraní** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Podpora tagů** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| **Vyhledávání a filtrování** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | +| **Šifrování** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| **Úrovně priority** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| **Kategorie** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| **Pomodoro timer** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | +| **Streak / Statistiky návyků** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| **Export dat** | ✅ JSON | ✅ TXT | ✅ TXT | ✅ TXT | ✅ JSON | ✅ CSV | --- @@ -280,6 +374,8 @@ Každý projekt má vlastní instalační postup. Podrobné návody naleznete v - [DOXA CLI – Instalace](doxa-cli/README_CZ.md#instalace) - [DOXA Secret – Instalace](doxa-secret/README.md#instalace) - [DOXA TODO – Instalace](doxa-todo/README_CZ.md#instalace) +- [DOXA Timer – Instalace](doxa-timer/README_CZ.md#instalace) +- [DOXA Habit – Instalace](doxa-habit/README_CZ.md#instalace) --- diff --git a/doxa-habit/LICENSE b/doxa-habit/LICENSE new file mode 100644 index 0000000..b459e5f --- /dev/null +++ b/doxa-habit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kankys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doxa-habit/MANUAL.md b/doxa-habit/MANUAL.md new file mode 100644 index 0000000..2e965de --- /dev/null +++ b/doxa-habit/MANUAL.md @@ -0,0 +1,41 @@ +# DOXA Habit User Manual 🎯 + +This manual describes how to navigate and manage habits in the DOXA Habit tracker. + +## Getting Started + +Start the application by running: +```bash +doxa-habit +``` +Press `Enter` on the splash screen to enter the main interface. + +## Interface States + +1. **MAIN VIEW**: A list showing all your habits, their current streaks (e.g., `🔥3` for 3 consecutive days, or `❄️0`), and a mini calendar heatmap showing the last 7 days (`▓` for completed, `░` for missed). +2. **DETAIL MODAL**: Displays description, frequency, stats for last 7/30 days, and a detailed heatmap. +3. **STATS VIEW**: Table summarizing all your habits' statistics. +4. **ADD / EDIT FORM**: Text inputs to add or modify habits. + +## Controls + +### Main Screen +- `1`: Add a new habit. +- `2`: Edit the selected habit. +- `3`: Export completion history to `~/.doxa-habit/export/_history.csv`. +- `4`: Delete selected habit. +- `t`: Toggle today's status (Completed ✅ vs Not Completed ⬜). +- `s`: Show statistics of all habits. +- `l`: Switch UI language (English/Czech). +- `/`: Filter habits by search term. +- `Enter`: Open details modal. +- `q` / `Esc`: Exit the program. + +### Form Fields (Add/Edit) +- `Tab`: Jump between the "Name" and "Description" fields. +- `f`: Toggle habit frequency between "Daily" and "Weekly". +- `Enter`: Save and create/modify the habit. +- `q` / `Esc`: Go back to the main list without saving. + +### Modal & Stats Views +- `q` / `Esc`: Return to the main list view. diff --git a/doxa-habit/MANUAL_CZ.md b/doxa-habit/MANUAL_CZ.md new file mode 100644 index 0000000..0608ba9 --- /dev/null +++ b/doxa-habit/MANUAL_CZ.md @@ -0,0 +1,41 @@ +# Uživatelský manuál DOXA Habit 🎯 + +Tento manuál popisuje použití aplikace DOXA Habit pro sledování návyků. + +## Spuštění programu + +Spusťte aplikaci příkazem: +```bash +doxa-habit +``` +Na úvodní obrazovce stiskněte `Enter` pro přechod do hlavního rozhraní. + +## Rozhraní aplikace + +1. **SEZNAM NÁVYKŮ**: Zobrazuje všechny vaše návyky, aktuální streak (např. `🔥3` nebo `❄️0`) a mini heatmapu posledních 7 dní (`▓` splněno, `░` nesplněno). +2. **DETAIL MODAL**: Zobrazí podrobnější popis návyku, frekvenci a úspěšnost v procentech za 7/30 dní. +3. **STATISTIKY**: Tabulkový přehled všech vašich návyků a jejich úspěšnosti. +4. **PŘIDAT / UPRAVIT**: Formulář pro zakládání či editaci návyků. + +## Ovládání + +### Hlavní obrazovka +- `1`: Přidat nový návyk. +- `2`: Upravit vybraný návyk. +- `3`: Exportovat historii splnění do souboru `~/.doxa-habit/export/_history.csv`. +- `4`: Smazat vybraný návyk. +- `t`: Označit návyk jako dnes splněný (střídá ✅ a ⬜). +- `s`: Zobrazit celkové statistiky všech návyků. +- `l`: Přepne jazyk rozhraní (čeština/angličtina). +- `/`: Vyhledat/filtrovat návyky podle názvu. +- `Enter`: Otevřít detail návyku v modálním okně. +- `q` / `Esc`: Ukončit program. + +### Formulář (Přidat / Upravit) +- `Tab`: Přechází mezi poli "Název" a "Popis". +- `f`: Přepíná frekvenci návyku (Denní / Týdenní). +- `Enter`: Uloží data. +- `q` / `Esc`: Zruší změny a vrátí se zpět na hlavní seznam. + +### Okna detailu a statistik +- `q` / `Esc`: Vrátí se na hlavní seznam návyků. diff --git a/doxa-habit/README.md b/doxa-habit/README.md new file mode 100644 index 0000000..27ff9fe --- /dev/null +++ b/doxa-habit/README.md @@ -0,0 +1,36 @@ +# DOXA Habit 🎯 + +**Minimalist habit tracker for the terminal with streak tracking** + +DOXA Habit is a terminal-based tool to organize, record, and maintain daily and weekly habits. It calculates streaks and offers a visual 7-day mini heatmap of your progress. + +## Features + +- **Daily & Weekly Habits**: Setup name, description, and execution frequency. +- **Completion toggle**: Mark today as completed or not. +- **Streak & Heatmap**: Displays your current streak (🔥/❄️) and a mini calendar (▓/░) of the last 7 days. +- **Overall Statistics**: Success rates over 7 and 30 days. +- **Local JSON Storage**: Kept securely offline at `~/.doxa-habit/doxa_habit.json`. +- **Export to CSV**: Export your habit completion log history. +- **Bilingual**: CZ/EN language support via the `l` key. + +## Installation + +```bash +cd doxa-habit +chmod +x install.sh +./install.sh +``` + +## Hotkeys + +- `1` - Add Habit +- `2` - Edit selected Habit +- `3` - Export selected Habit history to CSV +- `4` - Delete Habit +- `t` - Toggle completed state for today +- `s` - View Overall Statistics +- `l` - Toggle language (EN/CS) +- `/` - Search habits by name +- `Enter` - View Habit Details Modal +- `q` / `Esc` - Go back / Quit diff --git a/doxa-habit/README_CZ.md b/doxa-habit/README_CZ.md new file mode 100644 index 0000000..734a8d8 --- /dev/null +++ b/doxa-habit/README_CZ.md @@ -0,0 +1,36 @@ +# DOXA Habit 🎯 + +**Minimalistický tracker návyků pro terminál se sledováním streaku** + +DOXA Habit je terminálová aplikace pro organizaci, zaznamenávání a udržování denních i týdenních návyků. Počítá aktuální sérii splnění (streak) a vizualizuje pokrok. + +## Vlastnosti + +- **Denní a týdenní návyky**: Volitelný název, popis a frekvence plnění. +- **Označení splnění**: Rychlé označení dnešního splnění (toggle). +- **Streak & Heatmap**: Zobrazení aktuální série (🔥/❄️) a 7denního mini kalendáře (▓/░). +- **Celkové statistiky**: Úspěšnost plnění za posledních 7 a 30 dní. +- **Local-first**: Data jsou uložena v offline JSON databázi `~/.doxa-habit/doxa_habit.json`. +- **Export do CSV**: Uložení kompletní historie plnění návyku. +- **Dvojjazyčnost**: Podpora češtiny a angličtiny přepínatelná klávesou `l`. + +## Instalace + +```bash +cd doxa-habit +chmod +x install.sh +./install.sh +``` + +## Klávesové zkratky + +- `1` - Přidat návyk +- `2` - Upravit vybraný návyk +- `3` - Exportovat historii návyku do CSV +- `4` - Smazat návyk +- `t` - Změnit stav splnění pro dnešní den +- `s` - Zobrazit celkové statistiky +- `l` - Přepnout jazyk (CZ/EN) +- `/` - Vyhledávání návyků podle názvu +- `Enter` - Zobrazit detail návyku v modálním okně +- `q` / `Esc` - Zpět / Konec diff --git a/doxa-habit/doxa-habit b/doxa-habit/doxa-habit new file mode 100755 index 0000000..62d18f6 Binary files /dev/null and b/doxa-habit/doxa-habit differ diff --git a/doxa-habit/go.mod b/doxa-habit/go.mod new file mode 100644 index 0000000..28e4d16 --- /dev/null +++ b/doxa-habit/go.mod @@ -0,0 +1,33 @@ +module doxa-habit + +go 1.26.5 + +require ( + github.com/charmbracelet/bubbles v1.0.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/doxa-habit/go.sum b/doxa-habit/go.sum new file mode 100644 index 0000000..e92ed90 --- /dev/null +++ b/doxa-habit/go.sum @@ -0,0 +1,52 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/doxa-habit/install.sh b/doxa-habit/install.sh new file mode 100755 index 0000000..a012370 --- /dev/null +++ b/doxa-habit/install.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# DOXA-HABIT Installation Script + +# --- Nastavení --- + +APP_NAME="doxa-habit" +INSTALL_DIR="$HOME/.local/bin" +CONFIG_DIR="$HOME/.doxa-habit" +CONFIG_FILE="doxa_habit.json" +REPO_URL="https://codeberg.org/Kankys/doxa.git" + +# --- Funkce --- + +error_exit() { + echo -e "\033[1;31mERROR: $1\033[0m" >&2 + exit 1 +} + +success_message() { + echo -e "\033[1;32mSUCCESS: $1\033[0m" +} + +info_message() { + echo -e "\033[1;34mINFO: $1\033[0m" +} + +# --- Hlavní část skriptu --- + +info_message "Kontrola Go instalace..." +if ! command -v go &> /dev/null +then + error_exit "Go není nainstalováno. Prosím, nainstalujte Go před spuštěním tohoto skriptu." +fi +success_message "Go je nainstalováno." + +info_message "Kontrola aktuálního adresáře..." +if [ -f "main.go" ] && [ -f "go.mod" ] && grep -q "module doxa-habit" go.mod; then + info_message "Jste již v adresáři $APP_NAME, používám místní verzi." +else + error_exit "Tento skript spouštějte z adresáře doxa-habit." +fi + +info_message "Stažení závislostí projektu..." +if ! go mod tidy +then + error_exit "Nepodařilo se stáhnout závislosti Go." +fi +success_message "Závislosti projektu úspěšně staženy." + +info_message "Kompilace aplikace..." +APP_BINARY="$APP_NAME" +if ! go build -o "$APP_BINARY" . +then + error_exit "Nepodařilo se zkompilovat aplikaci." +fi +success_message "Aplikace úspěšně zkompilována do './$APP_BINARY'." + +info_message "Instalace aplikace..." + +if ! mkdir -p "$INSTALL_DIR" +then + error_exit "Nepodařilo se vytvořit cílový adresář '$INSTALL_DIR'." +fi + +if ! cp "$APP_BINARY" "$INSTALL_DIR/" +then + error_exit "Nepodařilo se zkopírovat binárku do '$INSTALL_DIR'." +fi + +chmod +x "$INSTALL_DIR/$APP_NAME" +success_message "Aplikace '$APP_NAME' úspěšně nainstalována do '$INSTALL_DIR'." + +info_message "Nastavení konfiguračního adresáře..." + +if ! mkdir -p "$CONFIG_DIR" +then + error_exit "Nepodařilo se vytvořit konfigurační adresář '$CONFIG_DIR'." +fi + +if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ] +then + echo "[]" > "$CONFIG_DIR/$CONFIG_FILE" + if [ $? -ne 0 ] + then + error_exit "Nepodařilo se vytvořit konfigurační soubor '$CONFIG_DIR/$CONFIG_FILE'." + fi + success_message "Konfigurační soubor '$CONFIG_DIR/$CONFIG_FILE' vytvořen." +else + info_message "Konfigurační soubor '$CONFIG_DIR/$CONFIG_FILE' již existuje." +fi + +info_message "Instalace dokončena." +success_message "Nyní můžete spustit aplikaci '$APP_NAME' z jakéhokoli místa v terminálu." +echo "" +echo -e "\033[1;33mPRO TIP:\nPokud \`$APP_NAME\` nefunguje z jakéhokoli místa, přidejte '$INSTALL_DIR' do vašeho PATH.\nexport PATH=\$PATH:$INSTALL_DIR\033[0m" +echo "" + +exit 0 diff --git a/doxa-habit/main.go b/doxa-habit/main.go new file mode 100644 index 0000000..4422e42 --- /dev/null +++ b/doxa-habit/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" +) + +func main() { + m := NewModel() + + p := tea.NewProgram(m) + if _, err := p.Run(); err != nil { + fmt.Printf("Chyba při spuštění programu: %v\n", err) + os.Exit(1) + } +} diff --git a/doxa-habit/storage.go b/doxa-habit/storage.go new file mode 100644 index 0000000..31175be --- /dev/null +++ b/doxa-habit/storage.go @@ -0,0 +1,43 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +func getStoragePath() string { + home, err := os.UserHomeDir() + if err != nil { + return "doxa_habit.json" + } + configDir := filepath.Join(home, ".doxa-habit") + if _, err := os.Stat(configDir); os.IsNotExist(err) { + os.MkdirAll(configDir, 0755) + } + return filepath.Join(configDir, "doxa_habit.json") +} + +func loadHabits() []Habit { + path := getStoragePath() + file, err := os.ReadFile(path) + if err != nil { + return []Habit{} + } + var habits []Habit + if err := json.Unmarshal(file, &habits); err != nil { + return []Habit{} + } + return habits +} + +func saveHabits(habits []Habit) { + path := getStoragePath() + data, err := json.MarshalIndent(habits, "", " ") + if err != nil { + fmt.Println("Chyba při ukládání dat:", err) + return + } + os.WriteFile(path, data, 0644) +} diff --git a/doxa-habit/translations.go b/doxa-habit/translations.go new file mode 100644 index 0000000..c4bf75c --- /dev/null +++ b/doxa-habit/translations.go @@ -0,0 +1,128 @@ +package main + +type Translation struct { + Title string + MenuAdd string + MenuEdit string + MenuExport string + MenuDelete string + MenuToggle string + MenuStats string + MenuLang string + MenuQuit string + ListTitle string + NoHabits string + AddTitle string + EditTitle string + LabelName string + LabelDesc string + LabelFreq string + PlaceholderName string + PlaceholderDesc string + FreqDaily string + FreqWeekly string + HelpForm string + HelpList string + HelpModal string + StatusSaved string + StatusEdited string + StatusDeleted string + StatusExported string + ConfirmDelete string + ConfirmYes string + ConfirmNo string + StatsTitle string + LabelStreak string + LabelSuccess7 string + LabelSuccess30 string + LabelCreated string + LabelCompleted string + LabelNotCompleted string + ErrNameRequired string + HelpStats string +} + +var Translations = map[string]Translation{ + "cs": { + Title: "DOXA-HABIT 🦅", + MenuAdd: "1: Přidat", + MenuEdit: "2: Upravit", + MenuExport: "3: Export CSV", + MenuDelete: "4: Smazat", + MenuToggle: "t: Splnit dnes", + MenuStats: "s: Statistiky", + MenuLang: "l: EN", + MenuQuit: "q: Konec", + ListTitle: "--- SEZNAM NÁVYKŮ ---", + NoHabits: " (Žádné návyky...)", + AddTitle: "PŘIDAT NÁVYK", + EditTitle: "UPRAVIT NÁVYK", + LabelName: "Název:", + LabelDesc: "Popis:", + LabelFreq: "Frekvence:", + PlaceholderName: "Název (povinné)", + PlaceholderDesc: "Popis (nepovinný)", + FreqDaily: "Denní", + FreqWeekly: "Týdenní", + HelpForm: "[Tab] Další pole | [Enter] Uložit | [f] Změnit frekvenci | [q] Zrušit", + HelpList: "↑/↓: Vybrat | Enter: Detail | t: Splnit | s: Statistiky | /: Hledat | q: Konec", + HelpModal: "q: Zavřít | 2: Editovat | ↑/↓: Posun", + StatusSaved: "Návyk byl uložen.", + StatusEdited: "Změny byly uloženy.", + StatusDeleted: "Návyk byl smazán.", + StatusExported: "Statistiky exportovány do CSV.", + ConfirmDelete: "Opravdu chcete smazat tento návyk?", + ConfirmYes: "y: Ano", + ConfirmNo: "n: Ne / Esc", + StatsTitle: "CELKOVÉ STATISTIKY", + LabelStreak: "Aktuální streak:", + LabelSuccess7: "Úspěšnost 7 dní:", + LabelSuccess30: "Úspěšnost 30 dní:", + LabelCreated: "Vytvořeno:", + LabelCompleted: "Splněno", + LabelNotCompleted: "Nesplněno", + ErrNameRequired: "Chyba: Název je povinný!", + HelpStats: "q/Esc: Zpět", + }, + "en": { + Title: "DOXA-HABIT 🦅", + MenuAdd: "1: Add", + MenuEdit: "2: Edit", + MenuExport: "3: Export CSV", + MenuDelete: "4: Delete", + MenuToggle: "t: Complete today", + MenuStats: "s: Stats", + MenuLang: "l: CS", + MenuQuit: "q: Quit", + ListTitle: "--- HABITS LIST ---", + NoHabits: " (No habits...)", + AddTitle: "ADD HABIT", + EditTitle: "EDIT HABIT", + LabelName: "Name:", + LabelDesc: "Description:", + LabelFreq: "Frequency:", + PlaceholderName: "Name (required)", + PlaceholderDesc: "Description (optional)", + FreqDaily: "Daily", + FreqWeekly: "Weekly", + HelpForm: "[Tab] Next field | [Enter] Save | [f] Change frequency | [q] Cancel", + HelpList: "↑/↓: Select | Enter: Detail | t: Toggle Done | s: Stats | /: Search | q: Quit", + HelpModal: "q: Close | 2: Edit | ↑/↓: Scroll", + StatusSaved: "Habit saved.", + StatusEdited: "Changes saved.", + StatusDeleted: "Habit deleted.", + StatusExported: "Stats exported to CSV.", + ConfirmDelete: "Are you sure you want to delete this habit?", + ConfirmYes: "y: Yes", + ConfirmNo: "n: No / Esc", + StatsTitle: "OVERALL STATISTICS", + LabelStreak: "Current streak:", + LabelSuccess7: "Success 7 days:", + LabelSuccess30: "Success 30 days:", + LabelCreated: "Created:", + LabelCompleted: "Done", + LabelNotCompleted: "Not done", + ErrNameRequired: "Error: Name is required!", + HelpStats: "q/Esc: Back", + }, +} diff --git a/doxa-habit/tui.go b/doxa-habit/tui.go new file mode 100644 index 0000000..6b0fe0b --- /dev/null +++ b/doxa-habit/tui.go @@ -0,0 +1,846 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// ─── Styles ────────────────────────────────────────────────────────────────── + +var ( + appStyle = lipgloss.NewStyle(). + Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")) + + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")). + Background(lipgloss.Color("#7D56F4")). + Padding(0, 1). + MarginBottom(1) + + selectedStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#7D56F4")). + Bold(true) + + statusStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("205")) + + completedStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#2ECC71")) + + streakStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#F39C12")) + + labelStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")) + + valueStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")) + + focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + cursorStyle = focusedStyle.Copy() + + confirmBoxStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("205")). + Padding(1, 2).Align(lipgloss.Center) + + helpStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1) +) + +// ─── Data Structure ────────────────────────────────────────────────────────── + +type Habit struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Frequency string `json:"frequency"` // "daily" or "weekly" + CreatedAt time.Time `json:"created_at"` + ModifiedAt time.Time `json:"modified_at"` + CompletedDates []string `json:"completed_dates"` // format "2006-01-02" +} + +type AppState int + +const ( + StateStartup AppState = iota + StateViewHabits + StateViewHabitModal + StateAddHabit + StateEditHabit + StateStats + StateConfirmDelete + StateSearch +) + +// ─── Model ──────────────────────────────────────────────────────────────────── + +type model struct { + habits []Habit + filteredHabits []Habit + cursor int + state AppState + filterText string + textInputs []textinput.Model // 0=name, 1=desc + searchInput textinput.Model + viewport viewport.Model + ready bool + focusIndex int + selectedHabitID int64 + statusMsg string + lang string + width int + height int + editFrequency string // "daily" or "weekly" +} + +func NewModel() model { + inputs := make([]textinput.Model, 2) + for i := range inputs { + t := textinput.New() + t.Cursor.Style = cursorStyle + inputs[i] = t + } + inputs[0].Focus() + inputs[0].PromptStyle = focusedStyle + inputs[0].TextStyle = focusedStyle + + si := textinput.New() + si.Prompt = "🔍 " + si.Placeholder = "Hledat..." + + m := model{ + habits: loadHabits(), + cursor: 0, + state: StateStartup, + textInputs: inputs, + searchInput: si, + focusIndex: 0, + lang: "cs", + editFrequency: "daily", + } + m.applyFilter() + m.updatePlaceholders() + return m +} + +func (m model) Init() tea.Cmd { + return tea.Batch(textinput.Blink, tea.EnterAltScreen) +} + +func (m *model) applyFilter() { + if m.filterText == "" { + m.filteredHabits = m.habits + } else { + m.filteredHabits = []Habit{} + query := strings.ToLower(m.filterText) + for _, h := range m.habits { + if strings.Contains(strings.ToLower(h.Name), query) || + strings.Contains(strings.ToLower(h.Description), query) { + m.filteredHabits = append(m.filteredHabits, h) + } + } + } + if m.cursor >= len(m.filteredHabits) { + m.cursor = 0 + if len(m.filteredHabits) > 0 { + m.cursor = len(m.filteredHabits) - 1 + } + } +} + +func (m *model) updatePlaceholders() { + tr := Translations[m.lang] + m.textInputs[0].Placeholder = tr.PlaceholderName + m.textInputs[1].Placeholder = tr.PlaceholderDesc + m.searchInput.Placeholder = "..." +} + +// ─── Update ─────────────────────────────────────────────────────────────────── + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + tr := Translations[m.lang] + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + + headerHeight := 8 + footerHeight := 3 + vpHeight := msg.Height - headerHeight - footerHeight + if vpHeight < 1 { + vpHeight = 1 + } + + if !m.ready { + m.viewport = viewport.New(msg.Width-6, vpHeight) + m.ready = true + } else { + m.viewport.Width = msg.Width - 6 + m.viewport.Height = vpHeight + } + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + + case "esc": + if m.state == StateConfirmDelete { + m.state = StateViewHabits + m.statusMsg = "" + return m, nil + } + if m.state == StateViewHabits { + if m.filterText != "" { + m.filterText = "" + m.applyFilter() + return m, nil + } + return m, tea.Quit + } + m.resetToView() + return m, nil + + case "q": + if m.state == StateStartup { + return m, tea.Quit + } + if m.state == StateConfirmDelete { + m.state = StateViewHabits + m.statusMsg = "" + return m, nil + } + if m.state == StateViewHabitModal { + m.state = StateViewHabits + return m, nil + } + if m.state == StateViewHabits { + if m.filterText != "" { + m.filterText = "" + m.applyFilter() + return m, nil + } + return m, tea.Quit + } + m.resetToView() + return m, nil + + case "l": + if m.state == StateViewHabits { + if m.lang == "cs" { + m.lang = "en" + } else { + m.lang = "cs" + } + m.updatePlaceholders() + return m, nil + } + + case "/": + if m.state == StateViewHabits { + m.state = StateSearch + m.searchInput.Focus() + m.searchInput.SetValue("") + return m, nil + } + + case "tab", "shift+tab": + if m.state == StateAddHabit || m.state == StateEditHabit { + if msg.String() == "shift+tab" { + m.focusIndex-- + } else { + m.focusIndex++ + } + if m.focusIndex > 1 { + m.focusIndex = 0 + } + if m.focusIndex < 0 { + m.focusIndex = 1 + } + return m.updateFocus() + } + + case "up", "k": + if m.state == StateViewHabits && m.cursor > 0 { + m.cursor-- + m.viewport.LineUp(1) + } + if m.state == StateViewHabitModal { + m.viewport.LineUp(1) + } + + case "down", "j": + if m.state == StateViewHabits { + max := len(m.filteredHabits) + if m.cursor < max-1 { + m.cursor++ + m.viewport.LineDown(1) + } + } + if m.state == StateViewHabitModal { + m.viewport.LineDown(1) + } + + case "f": + if m.state == StateAddHabit || m.state == StateEditHabit { + if m.editFrequency == "daily" { + m.editFrequency = "weekly" + } else { + m.editFrequency = "daily" + } + return m, nil + } + + case "enter": + switch m.state { + case StateStartup: + m.state = StateViewHabits + case StateViewHabits: + if len(m.filteredHabits) > 0 { + m.state = StateViewHabitModal + m.selectedHabitID = m.filteredHabits[m.cursor].ID + m.updateModalContent() + } + case StateSearch: + m.filterText = m.searchInput.Value() + m.applyFilter() + m.state = StateViewHabits + case StateAddHabit: + m.handleAddHabit() + case StateEditHabit: + m.handleEditHabit() + case StateConfirmDelete: + if m.focusIndex == 0 { + m.handleDeleteHabit() + m.state = StateViewHabits + m.statusMsg = tr.StatusDeleted + return m, nil + } else { + m.state = StateViewHabits + m.statusMsg = "" + return m, nil + } + } + + case "1": + if m.state == StateViewHabits { + m.state = StateAddHabit + m.resetInputs() + m.editFrequency = "daily" + return m, nil + } + case "2": + if m.state == StateViewHabits && len(m.filteredHabits) > 0 { + m.state = StateEditHabit + m.selectedHabitID = m.filteredHabits[m.cursor].ID + m.populateInputs(m.filteredHabits[m.cursor]) + return m, nil + } + if m.state == StateViewHabitModal { + for _, h := range m.habits { + if h.ID == m.selectedHabitID { + m.state = StateEditHabit + m.populateInputs(h) + break + } + } + return m, nil + } + case "3": + if m.state == StateViewHabits && len(m.filteredHabits) > 0 { + m.exportCSV(m.filteredHabits[m.cursor]) + } + case "4": + if m.state == StateViewHabits && len(m.filteredHabits) > 0 { + m.state = StateConfirmDelete + m.selectedHabitID = m.filteredHabits[m.cursor].ID + m.focusIndex = 0 + } + case "t": + if m.state == StateViewHabits && len(m.filteredHabits) > 0 { + m.toggleHabitComplete(m.filteredHabits[m.cursor].ID) + } + case "s": + if m.state == StateViewHabits { + m.state = StateStats + return m, nil + } + } + } + + if m.state == StateSearch { + m.searchInput, cmd = m.searchInput.Update(msg) + return m, cmd + } + + if m.state == StateAddHabit || m.state == StateEditHabit { + m.textInputs[m.focusIndex], cmd = m.textInputs[m.focusIndex].Update(msg) + return m, cmd + } + + if m.state == StateConfirmDelete { + if msg, ok := msg.(tea.KeyMsg); ok { + switch msg.String() { + case "y": + m.handleDeleteHabit() + m.state = StateViewHabits + m.statusMsg = tr.StatusDeleted + return m, nil + case "n", "esc": + m.state = StateViewHabits + m.statusMsg = "" + return m, nil + } + } + } + + return m, nil +} + +func (m *model) updateFocus() (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + for i := range m.textInputs { + m.textInputs[i].Blur() + m.textInputs[i].PromptStyle = lipgloss.NewStyle() + } + cmds = append(cmds, m.textInputs[m.focusIndex].Focus()) + m.textInputs[m.focusIndex].PromptStyle = focusedStyle + return m, tea.Batch(cmds...) +} + +func (m *model) resetToView() { + m.state = StateViewHabits + m.statusMsg = "" + m.filterText = "" + m.applyFilter() + m.resetInputs() +} + +func (m *model) resetInputs() { + for i := range m.textInputs { + m.textInputs[i].SetValue("") + m.textInputs[i].Blur() + } + m.focusIndex = 0 + m.updateFocus() +} + +func (m *model) populateInputs(h Habit) { + m.textInputs[0].SetValue(h.Name) + m.textInputs[1].SetValue(h.Description) + m.editFrequency = h.Frequency + m.focusIndex = 0 + m.updateFocus() +} + +func (m *model) toggleHabitComplete(id int64) { + today := time.Now().Format("2006-01-02") + for i, h := range m.habits { + if h.ID == id { + found := -1 + for idx, d := range h.CompletedDates { + if d == today { + found = idx + break + } + } + if found != -1 { + // Remove completion + m.habits[i].CompletedDates = append(h.CompletedDates[:found], h.CompletedDates[found+1:]...) + } else { + // Complete + m.habits[i].CompletedDates = append(h.CompletedDates, today) + } + m.habits[i].ModifiedAt = time.Now() + break + } + } + saveHabits(m.habits) + m.applyFilter() +} + +func (m *model) handleAddHabit() { + tr := Translations[m.lang] + name := m.textInputs[0].Value() + desc := m.textInputs[1].Value() + if name == "" { + m.statusMsg = tr.ErrNameRequired + return + } + now := time.Now() + m.habits = append(m.habits, Habit{ + ID: now.UnixMilli(), + Name: name, + Description: desc, + Frequency: m.editFrequency, + CreatedAt: now, + ModifiedAt: now, + CompletedDates: []string{}, + }) + saveHabits(m.habits) + m.applyFilter() + m.statusMsg = tr.StatusSaved + m.state = StateViewHabits +} + +func (m *model) handleEditHabit() { + tr := Translations[m.lang] + name := m.textInputs[0].Value() + desc := m.textInputs[1].Value() + if name == "" { + m.statusMsg = tr.ErrNameRequired + return + } + for i, h := range m.habits { + if h.ID == m.selectedHabitID { + m.habits[i].Name = name + m.habits[i].Description = desc + m.habits[i].Frequency = m.editFrequency + m.habits[i].ModifiedAt = time.Now() + break + } + } + saveHabits(m.habits) + m.applyFilter() + m.statusMsg = tr.StatusEdited + m.state = StateViewHabits +} + +func (m *model) handleDeleteHabit() { + found := -1 + for i, h := range m.habits { + if h.ID == m.selectedHabitID { + found = i + break + } + } + if found != -1 { + m.habits = append(m.habits[:found], m.habits[found+1:]...) + saveHabits(m.habits) + } + m.applyFilter() +} + +func (m *model) updateModalContent() { + tr := Translations[m.lang] + var h Habit + for _, habit := range m.habits { + if habit.ID == m.selectedHabitID { + h = habit + break + } + } + + freqLabel := tr.FreqDaily + if h.Frequency == "weekly" { + freqLabel = tr.FreqWeekly + } + + streak := getCurrentStreak(h) + success7 := getSuccessRate(h, 7) + success30 := getSuccessRate(h, 30) + + modalContent := fmt.Sprintf("%s\n---\n[%d] %s\n\n%s\n\n%s %s\n%s %d\n%s %.0f%%\n%s %.0f%%\n%s %s\n\n Heatmap (Last 7 Days):\n %s", + tr.HelpModal, + h.ID, h.Name, h.Description, + tr.LabelFreq, freqLabel, + tr.LabelStreak, streak, + tr.LabelSuccess7, success7, + tr.LabelSuccess30, success30, + tr.LabelCreated, h.CreatedAt.Format("2006-01-02 15:04"), + renderMiniCalendar(h), + ) + + m.viewport.Width = m.width - 20 + m.viewport.Height = m.height - 10 + m.viewport.SetContent(modalContent) +} + +func (m *model) exportCSV(h Habit) { + home, err := os.UserHomeDir() + if err != nil { + m.statusMsg = "Chyba zjištění domovského adresáře" + return + } + exportDir := filepath.Join(home, ".doxa-habit", "export") + os.MkdirAll(exportDir, 0755) + + safeName := strings.ReplaceAll(h.Name, " ", "_") + filename := filepath.Join(exportDir, fmt.Sprintf("%s_history.csv", safeName)) + + file, err := os.Create(filename) + if err != nil { + m.statusMsg = "Chyba při zápisu souboru" + return + } + defer file.Close() + + file.WriteString("Date,Completed\n") + // Write all dates + for _, d := range h.CompletedDates { + file.WriteString(fmt.Sprintf("%s,1\n", d)) + } + m.statusMsg = fmt.Sprintf("Exportováno do %s", filename) +} + +// ─── Helper Functions ──────────────────────────────────────────────────────── + +func isCompletedToday(h Habit) bool { + today := time.Now().Format("2006-01-02") + for _, d := range h.CompletedDates { + if d == today { + return true + } + } + return false +} + +func getCurrentStreak(h Habit) int { + if len(h.CompletedDates) == 0 { + return 0 + } + streak := 0 + checkDate := time.Now() + // Parse completed dates into a map for fast lookup + completedMap := make(map[string]bool) + for _, d := range h.CompletedDates { + completedMap[d] = true + } + + // If not completed today, check if it was completed yesterday to maintain streak + todayStr := checkDate.Format("2006-01-02") + if !completedMap[todayStr] { + checkDate = checkDate.AddDate(0, 0, -1) + } + + for { + dateStr := checkDate.Format("2006-01-02") + if completedMap[dateStr] { + streak++ + checkDate = checkDate.AddDate(0, 0, -1) + } else { + break + } + } + return streak +} + +func getSuccessRate(h Habit, days int) float64 { + if len(h.CompletedDates) == 0 { + return 0 + } + count := 0 + now := time.Now() + completedMap := make(map[string]bool) + for _, d := range h.CompletedDates { + completedMap[d] = true + } + + for i := 0; i < days; i++ { + dateStr := now.AddDate(0, 0, -i).Format("2006-01-02") + if completedMap[dateStr] { + count++ + } + } + return (float64(count) / float64(days)) * 100 +} + +func renderProgressBar(percent float64, width int) string { + filled := int(percent / 100 * float64(width)) + if filled > width { + filled = width + } + if filled < 0 { + filled = 0 + } + return "[" + strings.Repeat("█", filled) + strings.Repeat("░", width-filled) + "]" +} + +func renderMiniCalendar(h Habit) string { + var sb strings.Builder + now := time.Now() + completedMap := make(map[string]bool) + for _, d := range h.CompletedDates { + completedMap[d] = true + } + + // Last 7 days in order (oldest to newest) + for i := 6; i >= 0; i-- { + day := now.AddDate(0, 0, -i) + dateStr := day.Format("2006-01-02") + if completedMap[dateStr] { + sb.WriteString("▓") + } else { + sb.WriteString("░") + } + } + return sb.String() +} + +// ─── View ───────────────────────────────────────────────────────────────────── + +func (m model) View() string { + tr := Translations[m.lang] + var b strings.Builder + + b.WriteString(titleStyle.Render(tr.Title) + "\n") + + if m.statusMsg != "" { + b.WriteString(statusStyle.Render(m.statusMsg) + "\n\n") + } + + switch m.state { + case StateStartup: + eagle := ` + _ + ( ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("`") + ` + \ ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("\\") + ` + / / _ + __ / / ( \ + ( "---" / / / + \ / / / + \ /-----' / + \ / / + \ / / + '-------' +` + b.WriteString(lipgloss.Place(m.width-12, 14, lipgloss.Center, lipgloss.Center, + eagle+"\n"+ + lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7D56F4")).Render("DOXA-HABIT 🦅")+"\n\n"+ + "Local-First Habit Tracker\n\n"+ + "Enter: Start | q: Quit\n\n"+ + "License: MIT | © 2026 Kankys")) + + case StateSearch: + b.WriteString("HLEDAT: \n" + m.searchInput.View() + "\n\n" + tr.HelpForm) + + case StateViewHabits: + filterInfo := "" + if m.filterText != "" { + filterInfo = fmt.Sprintf(" [Filtr: %s]", m.filterText) + } + + var listBuilder strings.Builder + if len(m.filteredHabits) == 0 { + listBuilder.WriteString(tr.NoHabits + "\n") + } else { + for i, h := range m.filteredHabits { + status := "⬜" + if isCompletedToday(h) { + status = "✅" + } + streak := getCurrentStreak(h) + streakStr := fmt.Sprintf("🔥%d", streak) + if streak == 0 { + streakStr = "❄️0" + } + + cursor := " " + if m.cursor == i { + cursor = "> " + } + + line := fmt.Sprintf("%s%s %-20s %-5s %s", + cursor, + status, + h.Name, + streakStyle.Render(streakStr), + renderMiniCalendar(h), + ) + if m.cursor == i { + listBuilder.WriteString(selectedStyle.Render(line) + "\n") + } else { + listBuilder.WriteString(line + "\n") + } + } + } + + if m.ready { + m.viewport.SetContent(listBuilder.String()) + } + + b.WriteString(fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s\n\n", + tr.MenuAdd, tr.MenuEdit, tr.MenuExport, tr.MenuDelete, tr.MenuToggle, tr.MenuStats, tr.MenuLang)) + b.WriteString(tr.ListTitle + filterInfo + "\n") + + if m.ready { + b.WriteString(m.viewport.View() + "\n") + } else { + b.WriteString(listBuilder.String()) + } + b.WriteString("\n" + tr.HelpList) + + case StateAddHabit, StateEditHabit: + title := tr.AddTitle + if m.state == StateEditHabit { + title = fmt.Sprintf("%s (%d)", tr.EditTitle, m.selectedHabitID) + } + b.WriteString(title + "\n\n") + + b.WriteString(fmt.Sprintf("%-12s %s\n", tr.LabelName, m.textInputs[0].View())) + b.WriteString(fmt.Sprintf("%-12s %s\n", tr.LabelDesc, m.textInputs[1].View())) + + freqLabel := tr.FreqDaily + if m.editFrequency == "weekly" { + freqLabel = tr.FreqWeekly + } + b.WriteString(fmt.Sprintf("%-12s %s\n", tr.LabelFreq, valueStyle.Render(freqLabel))) + b.WriteString("\n" + tr.HelpForm) + + case StateViewHabitModal: + if m.ready { + b.WriteString(confirmBoxStyle.Render(m.viewport.View())) + } else { + b.WriteString(confirmBoxStyle.Render("Načítání...")) + } + + case StateConfirmDelete: + var hToDelete Habit + for _, h := range m.habits { + if h.ID == m.selectedHabitID { + hToDelete = h + break + } + } + confirmText := fmt.Sprintf("%s\n\n[%d] %s\n\n%s | %s", + tr.ConfirmDelete, + hToDelete.ID, + hToDelete.Name, + tr.ConfirmYes, tr.ConfirmNo, + ) + b.WriteString(confirmBoxStyle.Render(confirmText)) + + case StateStats: + b.WriteString(lipgloss.NewStyle().Bold(true).Render(tr.StatsTitle) + "\n\n") + b.WriteString(fmt.Sprintf(" %-25s %-8s %-12s\n", "Návyk", "Streak", "Úspěšnost (30 dní)")) + b.WriteString(" " + strings.Repeat("-", 50) + "\n") + for _, h := range m.habits { + streak := getCurrentStreak(h) + success30 := getSuccessRate(h, 30) + b.WriteString(fmt.Sprintf(" %-25s 🔥%-7d %.0f%%\n", h.Name, streak, success30)) + } + b.WriteString("\n" + helpStyle.Render(tr.HelpStats)) + } + + content := b.String() + if m.width > 0 && m.height > 0 { + return appStyle.Width(m.width - 6).Height(m.height - 4).Render(content) + } + return appStyle.Render(content) +} diff --git a/doxa-notes/web/assets/locales/cs.json b/doxa-notes/web/assets/locales/cs.json index f89a019..20ef973 100644 --- a/doxa-notes/web/assets/locales/cs.json +++ b/doxa-notes/web/assets/locales/cs.json @@ -6,11 +6,11 @@ "nav.about": "O projektu", "nav.themeToggle": "Přepnout motiv", "footer.copyright": "Doxa Notes © 2026 | Local-First & Svoboda 🦅", - "index.pageTitle": "DOXA Notes — Local-First poznámky a minimalistický Markdown editor", + "index.pageTitle": "DOXA Notes — Local-First poznámky and minimalistický Markdown editor", "index.pageDescription": "DOXA Notes je minimalistická, local‑first poznámková aplikace s podporou Markdownu a otevřeným formátem JSON. Absolutní soukromí pro vaše myšlenky.", - "index.ogTitle": "DOXA Notes — Local-First poznámky a minimalistický Markdown editor", + "index.ogTitle": "DOXA Notes — Local-First poznámky and minimalistický Markdown editor", "index.ogDescription": "DOXA Notes je minimalistická, local‑first poznámková aplikace založená na otevřeném JSON formátu. Data zůstávají plně pod vaší kontrolou.", - "index.twitterTitle": "DOXA Notes — Local-First poznámky a minimalistický Markdown editor", + "index.twitterTitle": "DOXA Notes — Local-First poznámky and minimalistický Markdown editor", "index.twitterDescription": "DOXA Notes je minimalistická, local‑first poznámková aplikace založená na otevřeném JSON formátu.", "index.mainTitle": "DOXA Notes", "index.subtitle": "Local-First Notes & Sovereignty", @@ -81,14 +81,14 @@ "manifest.quote.author": "— Stoický odkaz pro svobodné myslitele", "nav.doxa": "Kolekce DOXA", "doxa.pageTitle": "Kolekce DOXA — Celý přehled projektů", - "doxa.pageDescription": "DOXA je sbírka čtyř minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv a práci s textem z příkazové řádky. Všechny projekty sdílejí společnou filozofii: úplná nezávislost, soukromí a kontrola nad vašimi daty.", + "doxa.pageDescription": "DOXA je sbírka šesti minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv, správu úkolů, Pomodoro timer a sledování návyků. Žádný cloud, žádné sledování – vaše data patří pouze vám.", "doxa.ogTitle": "Kolekce DOXA — Celý přehled projektů", - "doxa.ogDescription": "Přehled všech projektů DOXA: Notes, CLI, Secret a TODO. Úplná lokalita, otevřené formáty a digitální suverenita.", + "doxa.ogDescription": "Přehled všech projektů DOXA: Notes, CLI, Secret, TODO, Timer a Habit. Úplná lokalita, otevřené formáty a digitální suverenita.", "doxa.twitterTitle": "Kolekce DOXA", - "doxa.twitterDescription": "Přehled všech projektů DOXA pro správu poznámek, šifrování a organizaci úkolů.", + "doxa.twitterDescription": "Přehled všech projektů DOXA pro správu poznámek, šifrování, úkolů, Pomodoro časovače a návyků.", "doxa.mainTitle": "Kolekce DOXA", "doxa.subtitle": "Úplná nezávislost, soukromí a kontrola nad daty", - "doxa.intro": "DOXA je sbírka čtyř minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv a práci s textem z příkazové řádky. Všechny projekty sdílejí společnou filozofii: úplná nezávislost, soukromí a kontrola nad vašimi daty. Žádná cloudová služba, žádné sledování, žádné externí závislosti. Vaše data patří pouze vám.", + "doxa.intro": "DOXA je sbírka šesti minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv, správu úkolů, Pomodoro timer a sledování návyků. Všechny projekty sdílejí společnou filozofii: úplná nezávislost, soukromí a kontrola nad vašimi daty. Žádná cloudová služba, žádné sledování, žádné externí závislosti. Vaše data patří pouze vám.", "doxa.philosophy": "Základní myšlenka zůstává nekompromisní: Radikální Local-First přístup, čistá data v JSON formátu a absolutní digitální soukromí bez korporátního cloudu a API.", "doxa.projects.title": "Projekty v kolekci DOXA", "doxa.notes.title": "📝 DOXA Notes", @@ -121,28 +121,49 @@ "doxa.todo.philosophy": "Filozofie: Lokální správa úkolů. Vaše úkoly jsou uloženy lokálně v JSON formátu.", "doxa.todo.dataLocation": "Data uložena v ~/.doxa-todo/doxa_todo.json.", "doxa.todo.link": "Přečtěte si podrobnější dokumentaci", + "doxa.timer.title": "⏱️ DOXA Timer", + "doxa.timer.description": "Pomodoro timer pro terminál se statistikami produktivity", + "doxa.timer.philosophy": "Local-first produktivita. Žádné cloudové aplikace, žádné předplatné – jen čistý fokus na vaši práci. Data uložena v ~/.doxa-timer/.", + "doxa.timer.features": "Funkce: Pomodoro technika (25/5/15 min) · Live countdown · Konfigurovatelné délky sessions · Statistiky pomodorů · Dvoujazyčné rozhraní (CZ/EN)", + "doxa.timer.tech": "Technologie: Go (Golang), Bubble Tea, Lip Gloss", + "doxa.timer.platforms": "Platformy: Linux, FreeBSD", + "doxa.timer.link": "Přečtěte si podrobnější dokumentaci", + "doxa.habit.title": "🎯 DOXA Habit", + "doxa.habit.description": "Tracker návyků pro terminál se sledováním streaku", + "doxa.habit.philosophy": "Sledujte svůj osobní růst lokálně. Bez notifikačních manipulací, bez gamifikačního tlaku – jen vaše data a váš pokrok. Data uložena v ~/.doxa-habit/.", + "doxa.habit.features": "Funkce: Denní/týdenní návyky · Streak tracker · ASCII heatmap · Statistiky úspěšnosti (7/30 dní) · Export do CSV · Dvoujazyčné rozhraní (CZ/EN)", + "doxa.habit.tech": "Technologie: Go (Golang), Bubble Tea, Lip Gloss", + "doxa.habit.platforms": "Platformy: Linux, FreeBSD", + "doxa.habit.link": "Přečtěte si podrobnější dokumentaci", "doxa.commonFeatures.title": "Společné vlastnosti všech projektů DOXA", "doxa.commonFeatures.table.header.feature": "Vlastnost", "doxa.commonFeatures.table.header.notes": "DOXA Notes", "doxa.commonFeatures.table.header.cli": "DOXA CLI", "doxa.commonFeatures.table.header.secret": "DOXA Secret", "doxa.commonFeatures.table.header.todo": "DOXA TODO", + "doxa.commonFeatures.table.header.timer": "DOXA Timer", + "doxa.commonFeatures.table.header.habit": "DOXA Habit", "doxa.commonFeatures.table.row.localStorage": "Lokální uložení dat", "doxa.commonFeatures.table.row.openFormat": "Otevřený formát (JSON)", "doxa.commonFeatures.table.row.noCloud": "Žádné cloudové závislosti", "doxa.commonFeatures.table.row.openSource": "Open Source (MIT)", "doxa.commonFeatures.table.row.minimalist": "Minimalistický design", + "doxa.commonFeatures.table.row.bilingual": "Dvoujazyčné rozhraní (CZ/EN)", "doxa.commonFeatures.table.row.tagSupport": "Podpora tagů", "doxa.commonFeatures.table.row.search": "Vyhledávání a filtrování", "doxa.commonFeatures.table.row.encryption": "Šifrování", "doxa.commonFeatures.table.row.priority": "Úrovně priority", - "doxa.commonFeatures.table.row.categories": "Kategorie", + "doxa.commonFeatures.table.row.timer": "Pomodoro timer", + "doxa.commonFeatures.table.row.streak": "Streak / Statistiky návyků", + "doxa.commonFeatures.table.row.export": "Export dat", "doxa.installation.title": "Instalace", "doxa.installation.text": "Každý projekt má svůj vlastní instalační postup. Podrobné návody naleznete v příslušných souborech README:", "doxa.installation.notes": "DOXA Notes — Instalace", "doxa.installation.cli": "DOXA CLI — Instalace", "doxa.installation.secret": "DOXA Secret — Instalace", "doxa.installation.todo": "DOXA TODO — Instalace", + "doxa.installation.timer": "DOXA Timer — Instalace", + "doxa.installation.habit": "DOXA Habit — Instalace", "doxa.license.title": "Licence", "doxa.license.text": "Všechny projekty v kolekci DOXA jsou open-source a licencovány pod MIT licencí. Podrobnosti naleznete v souborech LICENSE v jednotlivých projektech.", "doxa.philosophy.title": "Filozofie DOXA", diff --git a/doxa-notes/web/assets/locales/en.json b/doxa-notes/web/assets/locales/en.json index aff7102..19811a6 100644 --- a/doxa-notes/web/assets/locales/en.json +++ b/doxa-notes/web/assets/locales/en.json @@ -81,14 +81,14 @@ "manifest.quote.author": "— A Stoic message for free thinkers", "nav.doxa": "DOXA Collection", "doxa.pageTitle": "DOXA Collection — Complete Project Overview", - "doxa.pageDescription": "DOXA is a collection of four minimalist, locally-focused applications for note management, message encryption, and command-line text processing. All projects share a common philosophy: complete independence, privacy, and control over your data.", + "doxa.pageDescription": "DOXA is a collection of six minimalist, locally-focused applications for note management, message encryption, task management, Pomodoro timer and habit tracking. No cloud, no tracking – your data belongs only to you.", "doxa.ogTitle": "DOXA Collection — Complete Project Overview", - "doxa.ogDescription": "Overview of all DOXA projects: Notes, CLI, Secret, and TODO. Full locality, open formats, and digital sovereignty.", + "doxa.ogDescription": "Overview of all DOXA projects: Notes, CLI, Secret, TODO, Timer and Habit. Full locality, open formats, and digital sovereignty.", "doxa.twitterTitle": "DOXA Collection", - "doxa.twitterDescription": "Overview of all DOXA projects for note management, encryption, and task organization.", + "doxa.twitterDescription": "Overview of all DOXA projects for note management, encryption, tasks, Pomodoro timer and habit tracking.", "doxa.mainTitle": "DOXA Collection", "doxa.subtitle": "Complete Independence, Privacy & Data Control", - "doxa.intro": "DOXA is a collection of four minimalist, locally-focused applications for note management, message encryption, and command-line text processing. All projects share a common philosophy: complete independence, privacy, and control over your data. No cloud services, no tracking, no external dependencies. Your data belongs only to you.", + "doxa.intro": "DOXA is a collection of six minimalist, locally-focused applications for note management, message encryption, task management, Pomodoro timer and habit tracking. All projects share a common philosophy: complete independence, privacy, and control over your data. No cloud services, no tracking, no external dependencies. Your data belongs only to you.", "doxa.philosophy": "The core idea remains uncompromising: A radical Local-First approach, clean data in JSON format, and absolute digital privacy without corporate clouds and APIs.", "doxa.projects.title": "Projects in the DOXA Collection", "doxa.notes.title": "📝 DOXA Notes", @@ -121,28 +121,49 @@ "doxa.todo.philosophy": "Philosophy: Local task management. Your todos are stored locally in JSON format.", "doxa.todo.dataLocation": "Data stored in ~/.doxa-todo/doxa_todo.json.", "doxa.todo.link": "Read detailed documentation", + "doxa.timer.title": "⏱️ DOXA Timer", + "doxa.timer.description": "Pomodoro timer for terminal with productivity statistics", + "doxa.timer.philosophy": "Local-first productivity. No cloud apps, no subscriptions – just clean focus on your work. Data stored in ~/.doxa-timer/.", + "doxa.timer.features": "Features: Pomodoro technique (25/5/15 min) · Live countdown · Configurable session lengths · Pomodoro statistics · Bilingual interface (CZ/EN)", + "doxa.timer.tech": "Technologies: Go (Golang), Bubble Tea, Lip Gloss", + "doxa.timer.platforms": "Platforms: Linux, FreeBSD", + "doxa.timer.link": "Read detailed documentation", + "doxa.habit.title": "🎯 DOXA Habit", + "doxa.habit.description": "Habit tracker for terminal with streak tracking", + "doxa.habit.philosophy": "Track your personal growth locally. No notification manipulations, no gamification pressure – just your data and your progress. Data stored in ~/.doxa-habit/.", + "doxa.habit.features": "Features: Daily/weekly habits · Streak tracker · ASCII heatmap · Success rate statistics (7/30 days) · Export to CSV · Bilingual interface (CZ/EN)", + "doxa.habit.tech": "Technologies: Go (Golang), Bubble Tea, Lip Gloss", + "doxa.habit.platforms": "Platforms: Linux, FreeBSD", + "doxa.habit.link": "Read detailed documentation", "doxa.commonFeatures.title": "Common Features of All DOXA Projects", "doxa.commonFeatures.table.header.feature": "Feature", "doxa.commonFeatures.table.header.notes": "DOXA Notes", "doxa.commonFeatures.table.header.cli": "DOXA CLI", "doxa.commonFeatures.table.header.secret": "DOXA Secret", "doxa.commonFeatures.table.header.todo": "DOXA TODO", + "doxa.commonFeatures.table.header.timer": "DOXA Timer", + "doxa.commonFeatures.table.header.habit": "DOXA Habit", "doxa.commonFeatures.table.row.localStorage": "Local Data Storage", "doxa.commonFeatures.table.row.openFormat": "Open Format (JSON)", "doxa.commonFeatures.table.row.noCloud": "No Cloud Dependencies", "doxa.commonFeatures.table.row.openSource": "Open Source (MIT)", "doxa.commonFeatures.table.row.minimalist": "Minimalist Design", + "doxa.commonFeatures.table.row.bilingual": "Bilingual Interface (CZ/EN)", "doxa.commonFeatures.table.row.tagSupport": "Tag Support", "doxa.commonFeatures.table.row.search": "Search and Filtering", "doxa.commonFeatures.table.row.encryption": "Encryption", "doxa.commonFeatures.table.row.priority": "Priority Levels", - "doxa.commonFeatures.table.row.categories": "Categories", + "doxa.commonFeatures.table.row.timer": "Pomodoro timer", + "doxa.commonFeatures.table.row.streak": "Streak / Habit Statistics", + "doxa.commonFeatures.table.row.export": "Data Export", "doxa.installation.title": "Installation", "doxa.installation.text": "Each project has its own installation process. Detailed guides can be found in the respective README files:", "doxa.installation.notes": "DOXA Notes — Installation", "doxa.installation.cli": "DOXA CLI — Installation", "doxa.installation.secret": "DOXA Secret — Installation", "doxa.installation.todo": "DOXA TODO — Installation", + "doxa.installation.timer": "DOXA Timer — Installation", + "doxa.installation.habit": "DOXA Habit — Installation", "doxa.license.title": "License", "doxa.license.text": "All projects in the DOXA collection are open-source and licensed under the MIT License. See the LICENSE files in each project for details.", "doxa.philosophy.title": "DOXA Philosophy", diff --git a/doxa-notes/web/doxa.html b/doxa-notes/web/doxa.html index 438ab5f..5fd340c 100644 --- a/doxa-notes/web/doxa.html +++ b/doxa-notes/web/doxa.html @@ -6,20 +6,20 @@ Kolekce DOXA — Celý přehled projektů + content="DOXA je sbírka šesti minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv, správu úkolů, Pomodoro timer a sledování návyků. Žádný cloud, žádné sledování – vaše data patří pouze vám." /> - + - + @@ -28,8 +28,8 @@ "@context": "https://schema.org", "@type": "SoftwareApplication", "name": "DOXA Collection", - "description": "A collection of four minimalist, locally-focused applications for note management, message encryption, and command-line text processing.", - "operatingSystem": "Windows, Linux, FreeBSD, Web", + "description": "A collection of six minimalist, locally-focused applications for note management, message encryption, task management, Pomodoro timer and habit tracking.", + "operatingSystem": "Linux, FreeBSD, Web", "applicationCategory": "UtilitiesApplication", "offers": { "@type": "Offer", @@ -65,13 +65,13 @@

Úplná nezávislost, soukromí a kontrola nad daty

- DOXA je sbírka čtyř minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv a práci s textem z příkazové řádky. + DOXA je sbírka šesti minimalistických, lokálně orientovaných aplikací pro správu poznámek, šifrování zpráv, správu úkolů, Pomodoro timer a sledování návyků. Všechny projekty sdílejí společnou filozofii: úplná nezávislost, soukromí a kontrola nad vašimi daty. Žádná cloudová služba, žádné sledování, žádné externí závislosti. Vaše data patří pouze vám.

- „Základní myšlenka zůstává nekompromisní: Radikální Local-First přístup, čistá data v JSON formátu a absolutní digitální soukromí bez korporátního cloudu a API.“ + „Základní myšlenka zůstává nekompromisní: Radikální Local-First přístup, čistá data v JSON formátu a absolutní digitální soukromí bez korporátního cloudu a API."

@@ -130,10 +130,37 @@

Přečtěte si podrobnější dokumentaci

+
+ + +
+

⏱️ DOXA Timer

+

Pomodoro timer pro terminál se statistikami produktivity

+

Local-first produktivita. Žádné cloudové aplikace, žádné předplatné – jen čistý fokus na vaši práci. Data uložena v ~/.doxa-timer/.

+

Funkce: Pomodoro technika (25/5/15 min) · Live countdown · Konfigurovatelné délky sessions · Statistiky pomodorů · Dvoujazyčné rozhraní (CZ/EN)

+

Technologie: Go (Golang), Bubble Tea, Lip Gloss

+

Platformy: Linux, FreeBSD

+

Přečtěte si podrobnější dokumentaci

+
+ +
+ + +
+

🎯 DOXA Habit

+

Tracker návyků pro terminál se sledováním streaku

+

Sledujte svůj osobní růst lokálně. Bez notifikačních manipulací, bez gamifikačního tlaku – jen vaše data a váš pokrok. Data uložena v ~/.doxa-habit/.

+

Funkce: Denní/týdenní návyky · Streak tracker · ASCII heatmap · Statistiky úspěšnosti (7/30 dní) · Export do CSV · Dvoujazyčné rozhraní (CZ/EN)

+

Technologie: Go (Golang), Bubble Tea, Lip Gloss

+

Platformy: Linux, FreeBSD

+

Přečtěte si podrobnější dokumentaci

+
+

Společné vlastnosti všech projektů DOXA

+
@@ -142,6 +169,8 @@ + + @@ -151,6 +180,8 @@ + + @@ -158,6 +189,8 @@ + + @@ -165,6 +198,8 @@ + + @@ -172,6 +207,8 @@ + + @@ -179,6 +216,17 @@ + + + + + + + + + + + @@ -186,6 +234,8 @@ + + @@ -193,6 +243,8 @@ + + @@ -200,6 +252,8 @@ + + @@ -207,26 +261,50 @@ + + - + + + + + + + + + + + + + + + + + + + + +
DOXA CLI DOXA Secret DOXA TODODOXA TimerDOXA Habit
✅ ✅ ✅✅✅
Otevřený formát (JSON)✅ ✅ ✅✅✅
Žádné cloudové závislosti✅ ✅ ✅✅✅
Open Source (MIT)✅ ✅ ✅✅✅
Minimalistický design✅ ✅ ✅✅✅
Dvoujazyčné rozhraní (CZ/EN)✅✅✅✅✅✅
Podpora tagů✅ ❌ ✅❌❌
Vyhledávání a filtrování✅ ❌ ✅❌✅
Šifrování❌ ✅ ❌❌❌
Úrovně priority❌ ❌ ✅❌❌
KategoriePomodoro timer❌❌❌❌✅❌
Streak / Statistiky návyků❌❌ ❌ ❌ ❌ ✅
Export dat✅ JSON✅ TXT✅ TXT✅ TXT✅ JSON✅ CSV
+

Instalace


@@ -256,18 +334,16 @@

Hlavní návrhy

Bonusové nápady

@@ -296,7 +372,7 @@ diff --git a/doxa-timer/LICENSE b/doxa-timer/LICENSE new file mode 100644 index 0000000..b459e5f --- /dev/null +++ b/doxa-timer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kankys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doxa-timer/MANUAL.md b/doxa-timer/MANUAL.md new file mode 100644 index 0000000..3268034 --- /dev/null +++ b/doxa-timer/MANUAL.md @@ -0,0 +1,43 @@ +# DOXA Timer User Manual ⏱️ + +This manual explains how to use the DOXA Timer application. + +## Getting Started + +Run the program with: +```bash +doxa-timer +``` + +When you first launch the app, you will see a startup screen. Press `Enter` to access the main interface. + +## Interface States + +1. **READY**: The timer is set and waiting for you to start. +2. **FOCUS**: Work interval. Focus on your tasks. +3. **SHORT BREAK / LONG BREAK**: Relax. The countdown signals the break duration. +4. **SETTINGS**: Configuration menu to set durations. +5. **STATS**: Historical data. + +## Controls + +### Main Screen +- `Space`: Starts or pauses the countdown. +- `r`: Resets the countdown back to the configured focus or break time. +- `s`: Opens Settings. +- `t`: Opens Stats. +- `l`: Toggles UI translation (English/Czech). +- `q` / `Esc`: Exits the application. + +### Settings Screen +- `up`/`down` or `k`/`j`: Navigate between different settings options: + - Focus time + - Short break time + - Long break time + - Long break after X pomodoros +- `+` / `-`: Increase or decrease the value of the active option. +- `Enter`: Save settings and return to the main screen. +- `q` / `Esc`: Go back to the main screen without saving changes. + +### Stats Screen +- `q` / `Esc`: Return to the main screen. diff --git a/doxa-timer/MANUAL_CZ.md b/doxa-timer/MANUAL_CZ.md new file mode 100644 index 0000000..37ba8f9 --- /dev/null +++ b/doxa-timer/MANUAL_CZ.md @@ -0,0 +1,43 @@ +# Uživatelský manuál DOXA Timer ⏱️ + +Tento manuál popisuje použití aplikace DOXA Timer. + +## Spuštění programu + +Spusťte aplikaci příkazem: +```bash +doxa-timer +``` + +Při prvním spuštění se zobrazí úvodní logo a text. Stiskněte `Enter` pro přechod do hlavního rozhraní. + +## Stavy aplikace + +1. **PŘIPRAVEN (READY)**: Časovač čeká na spuštění. +2. **FOKUS (FOCUS)**: Pracovní interval. Soustřeďte se na práci. +3. **KRÁTKÁ / DLOUHÁ PŘESTÁVKA**: Čas na relaxaci a odpočinek. +4. **NASTAVENÍ (SETTINGS)**: Konfigurace délek časovačů. +5. **STATISTIKY (STATS)**: Přehled o dokončených sezeních. + +## Ovládání + +### Hlavní obrazovka +- `Mezerník (Space)`: Spustí nebo pozastaví aktuální odpočet. +- `r`: Obnoví (resetuje) aktuální časovač zpět na plnou délku. +- `s`: Přejde do Nastavení. +- `t`: Přejde do Statistik. +- `l`: Přepne jazyk mezi češtinou a angličtinou. +- `q` / `Esc`: Ukončí program. + +### Obrazovka Nastavení +- `šipka nahoru/dolů` nebo `k`/`j`: Pohyb mezi položkami nastavení: + - Délka fokusu + - Krátká přestávka + - Dlouhá přestávka + - Dlouhá přestávka po X pomodorech +- `+` / `-`: Zvýší nebo sníží hodnotu zvolené položky. +- `Enter`: Uloží konfiguraci a vrátí se na hlavní obrazovku. +- `q` / `Esc`: Vrátí se na hlavní obrazovku bez uložení změn. + +### Obrazovka Statistiky +- `q` / `Esc`: Vrátí se na hlavní obrazovku. diff --git a/doxa-timer/README.md b/doxa-timer/README.md new file mode 100644 index 0000000..8e78b1b --- /dev/null +++ b/doxa-timer/README.md @@ -0,0 +1,32 @@ +# DOXA Timer ⏱️ + +**Minimalist Pomodoro timer for the terminal with local statistics** + +DOXA Timer is a terminal-based Pomodoro app. It alternates focus work sessions with short and long breaks to optimize concentration, while tracking your sessions locally. + +## Features + +- **Pomodoro Technique**: Standard Focus (25m), Short Break (5m), and Long Break (15m). +- **Live Countdown & Progress**: Interactive ASCII progress bar. +- **Persistent Local Stats**: Tracking of today's and total Pomodoros. +- **Customizable Intervals**: Adjust times inside the app settings. +- **Sovereign & Offline**: Everything is saved locally at `~/.doxa-timer/doxa_timer.json`. +- **Bilingual TUI**: Switch between English and Czech using the `l` key. + +## Installation + +Go to the `doxa-timer` folder and run the install script: + +```bash +chmod +x install.sh +./install.sh +``` + +## Hotkeys + +- `Space` - Start / Pause / Resume +- `r` - Reset current timer +- `s` - Open Settings (press `+`/`-` to adjust times, `Enter` to Save, `q` to Cancel) +- `t` - Show Statistics +- `l` - Toggle language (EN/CS) +- `q` or `Esc` - Go back / Quit diff --git a/doxa-timer/README_CZ.md b/doxa-timer/README_CZ.md new file mode 100644 index 0000000..3a8ea86 --- /dev/null +++ b/doxa-timer/README_CZ.md @@ -0,0 +1,32 @@ +# DOXA Timer ⏱️ + +**Minimalistický Pomodoro časovač do terminálu s lokální statistikou** + +DOXA Timer je terminálová Pomodoro aplikace. Střídá intervaly plného soustředění na práci s krátkými a dlouhými přestávkami pro zvýšení produktivity. + +## Vlastnosti + +- **Metoda Pomodoro**: Fokus (25 min), Krátká přestávka (5 min) a Dlouhá přestávka (15 min). +- **Živý odpočet**: ASCII progress bar s procentuálním vyjádřením pokroku. +- **Lokální statistika**: Počítadlo celkových a dnešních dokončených Pomodorů. +- **Nastavitelnost**: Změna délek jednotlivých fází v menu Nastavení. +- **Local-first**: Data se ukládají lokálně do `~/.doxa-timer/doxa_timer.json`. +- **Dvojjazyčnost**: Přepínání češtiny a angličtiny pomocí klávesy `l`. + +## Instalace + +Přejděte do složky `doxa-timer` a spusťte instalační skript: + +```bash +chmod +x install.sh +./install.sh +``` + +## Klávesové zkratky + +- `Space` - Start / Pauza / Pokračovat +- `r` - Reset aktuálního odpočtu +- `s` - Otevřít Nastavení (klávesy `+`/`-` mění hodnotu, `Enter` uloží, `q` zruší změny) +- `t` - Zobrazit Statistiky +- `l` - Přepnutí jazyka (CZ/EN) +- `q` nebo `Esc` - Zpět / Konec diff --git a/doxa-timer/doxa-timer b/doxa-timer/doxa-timer new file mode 100755 index 0000000..f460a8e Binary files /dev/null and b/doxa-timer/doxa-timer differ diff --git a/doxa-timer/go.mod b/doxa-timer/go.mod new file mode 100644 index 0000000..a0e365b --- /dev/null +++ b/doxa-timer/go.mod @@ -0,0 +1,31 @@ +module doxa-timer + +go 1.26.5 + +require ( + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/doxa-timer/go.sum b/doxa-timer/go.sum new file mode 100644 index 0000000..b7000fb --- /dev/null +++ b/doxa-timer/go.sum @@ -0,0 +1,48 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/doxa-timer/install.sh b/doxa-timer/install.sh new file mode 100755 index 0000000..0f70e15 --- /dev/null +++ b/doxa-timer/install.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# DOXA-TIMER Installation Script + +# --- Nastavení --- + +APP_NAME="doxa-timer" +INSTALL_DIR="$HOME/.local/bin" +CONFIG_DIR="$HOME/.doxa-timer" +CONFIG_FILE="doxa_timer.json" +REPO_URL="https://codeberg.org/Kankys/doxa.git" + +# --- Funkce --- + +error_exit() { + echo -e "\033[1;31mERROR: $1\033[0m" >&2 + exit 1 +} + +success_message() { + echo -e "\033[1;32mSUCCESS: $1\033[0m" +} + +info_message() { + echo -e "\033[1;34mINFO: $1\033[0m" +} + +# --- Hlavní část skriptu --- + +info_message "Kontrola Go instalace..." +if ! command -v go &> /dev/null +then + error_exit "Go není nainstalováno. Prosím, nainstalujte Go před spuštěním tohoto skriptu." +fi +success_message "Go je nainstalováno." + +info_message "Kontrola aktuálního adresáře..." +if [ -f "main.go" ] && [ -f "go.mod" ] && grep -q "module doxa-timer" go.mod; then + info_message "Jste již v adresáři $APP_NAME, používám místní verzi." +else + error_exit "Tento skript spouštějte z adresáře doxa-timer." +fi + +info_message "Stažení závislostí projektu..." +if ! go mod tidy +then + error_exit "Nepodařilo se stáhnout závislosti Go." +fi +success_message "Závislosti projektu úspěšně staženy." + +info_message "Kompilace aplikace..." +APP_BINARY="$APP_NAME" +if ! go build -o "$APP_BINARY" . +then + error_exit "Nepodařilo se zkompilovat aplikaci." +fi +success_message "Aplikace úspěšně zkompilována do './$APP_BINARY'." + +info_message "Instalace aplikace..." + +if ! mkdir -p "$INSTALL_DIR" +then + error_exit "Nepodařilo se vytvořit cílový adresář '$INSTALL_DIR'." +fi + +if ! cp "$APP_BINARY" "$INSTALL_DIR/" +then + error_exit "Nepodařilo se zkopírovat binárku do '$INSTALL_DIR'." +fi + +chmod +x "$INSTALL_DIR/$APP_NAME" +success_message "Aplikace '$APP_NAME' úspěšně nainstalována do '$INSTALL_DIR'." + +info_message "Nastavení konfiguračního adresáře..." + +if ! mkdir -p "$CONFIG_DIR" +then + error_exit "Nepodařilo se vytvořit konfigurační adresář '$CONFIG_DIR'." +fi + +if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ] +then + echo "{}" > "$CONFIG_DIR/$CONFIG_FILE" + if [ $? -ne 0 ] + then + error_exit "Nepodařilo se vytvořit konfigurační soubor '$CONFIG_DIR/$CONFIG_FILE'." + fi + success_message "Konfigurační soubor '$CONFIG_DIR/$CONFIG_FILE' vytvořen." +else + info_message "Konfigurační soubor '$CONFIG_DIR/$CONFIG_FILE' již existuje." +fi + +info_message "Instalace dokončena." +success_message "Nyní můžete spustit aplikaci '$APP_NAME' z jakéhokoli místa v terminálu." +echo "" +echo -e "\033[1;33mPRO TIP:\nPokud \`$APP_NAME\` nefunguje z jakéhokoli místa, přidejte '$INSTALL_DIR' do vašeho PATH.\nexport PATH=\$PATH:$INSTALL_DIR\033[0m" +echo "" + +exit 0 diff --git a/doxa-timer/main.go b/doxa-timer/main.go new file mode 100644 index 0000000..4422e42 --- /dev/null +++ b/doxa-timer/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" +) + +func main() { + m := NewModel() + + p := tea.NewProgram(m) + if _, err := p.Run(); err != nil { + fmt.Printf("Chyba při spuštění programu: %v\n", err) + os.Exit(1) + } +} diff --git a/doxa-timer/storage.go b/doxa-timer/storage.go new file mode 100644 index 0000000..327edc3 --- /dev/null +++ b/doxa-timer/storage.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" +) + +// TimerStats holds persistent statistics and settings +type TimerStats struct { + TotalPomodoros int `json:"total_pomodoros"` + TodayPomodoros int `json:"today_pomodoros"` + LastDate string `json:"last_date"` + FocusDuration int `json:"focus_duration"` // minutes + ShortBreak int `json:"short_break"` // minutes + LongBreak int `json:"long_break"` // minutes + LongBreakAfter int `json:"long_break_after"` // after how many pomodoros +} + +func getStoragePath() string { + home, err := os.UserHomeDir() + if err != nil { + return "doxa_timer.json" + } + configDir := filepath.Join(home, ".doxa-timer") + if _, err := os.Stat(configDir); os.IsNotExist(err) { + os.MkdirAll(configDir, 0755) + } + return filepath.Join(configDir, "doxa_timer.json") +} + +func loadStats() TimerStats { + path := getStoragePath() + file, err := os.ReadFile(path) + if err != nil { + return defaultStats() + } + var stats TimerStats + if err := json.Unmarshal(file, &stats); err != nil { + return defaultStats() + } + // Reset today's count if it's a new day + today := time.Now().Format("2006-01-02") + if stats.LastDate != today { + stats.TodayPomodoros = 0 + stats.LastDate = today + } + // Ensure sane defaults if values are zero + if stats.FocusDuration == 0 { + stats.FocusDuration = 25 + } + if stats.ShortBreak == 0 { + stats.ShortBreak = 5 + } + if stats.LongBreak == 0 { + stats.LongBreak = 15 + } + if stats.LongBreakAfter == 0 { + stats.LongBreakAfter = 4 + } + return stats +} + +func saveStats(stats TimerStats) { + path := getStoragePath() + data, err := json.MarshalIndent(stats, "", " ") + if err != nil { + fmt.Println("Chyba při ukládání dat:", err) + return + } + os.WriteFile(path, data, 0644) +} + +func defaultStats() TimerStats { + return TimerStats{ + TotalPomodoros: 0, + TodayPomodoros: 0, + LastDate: time.Now().Format("2006-01-02"), + FocusDuration: 25, + ShortBreak: 5, + LongBreak: 15, + LongBreakAfter: 4, + } +} diff --git a/doxa-timer/translations.go b/doxa-timer/translations.go new file mode 100644 index 0000000..8a473d9 --- /dev/null +++ b/doxa-timer/translations.go @@ -0,0 +1,111 @@ +package main + +// Translation holds all UI strings for one language +type Translation struct { + Title string + MenuStart string + MenuPause string + MenuReset string + MenuSettings string + MenuStats string + MenuLang string + MenuQuit string + StateIdle string + StateRunning string + StatePaused string + StateBreak string + StateLongBreak string + StateDone string + LabelFocus string + LabelShortBreak string + LabelLongBreak string + LabelLongAfter string + LabelTodayCount string + LabelTotalCount string + LabelNextBreak string + HelpIdle string + HelpRunning string + HelpSettings string + HelpStats string + SettingsTitle string + StatsTitle string + SessionComplete string + BreakComplete string + PomodorosToday string + PomodorosTotal string + NextLongBreakIn string + MinutesShort string +} + +var Translations = map[string]Translation{ + "cs": { + Title: "DOXA-TIMER 🦅", + MenuStart: "Space: Start", + MenuPause: "Space: Pauza", + MenuReset: "r: Reset", + MenuSettings: "s: Nastavení", + MenuStats: "t: Statistiky", + MenuLang: "l: EN", + MenuQuit: "q: Konec", + StateIdle: "PŘIPRAVEN", + StateRunning: "FOKUS", + StatePaused: "PAUZA", + StateBreak: "KRÁTKÁ PŘESTÁVKA", + StateLongBreak: "DLOUHÁ PŘESTÁVKA", + StateDone: "DOKONČENO!", + LabelFocus: "Fokus:", + LabelShortBreak: "Krátká přestávka:", + LabelLongBreak: "Dlouhá přestávka:", + LabelLongAfter: "Dlouhá přestávka po:", + LabelTodayCount: "Dnes:", + LabelTotalCount: "Celkem:", + LabelNextBreak: "Příští dlouhá přestávka za:", + HelpIdle: "Space: Spustit | r: Reset | s: Nastavení | t: Statistiky | l: EN | q: Konec", + HelpRunning: "Space: Pauza | r: Reset | q: Konec", + HelpSettings: "+/-: Změnit fokus | b/n: Změnit přestávky | Enter: Uložit | q: Zpět", + HelpStats: "q/Esc: Zpět", + SettingsTitle: "NASTAVENÍ ČASOVAČŮ", + StatsTitle: "STATISTIKY", + SessionComplete: "🎉 Sezení dokončeno! Čas na přestávku.", + BreakComplete: "⏰ Přestávka skončila! Čas na práci.", + PomodorosToday: "Dnes dokončeno:", + PomodorosTotal: "Celkem dokončeno:", + NextLongBreakIn: "Dlouhá přestávka za:", + MinutesShort: "min", + }, + "en": { + Title: "DOXA-TIMER 🦅", + MenuStart: "Space: Start", + MenuPause: "Space: Pause", + MenuReset: "r: Reset", + MenuSettings: "s: Settings", + MenuStats: "t: Stats", + MenuLang: "l: CS", + MenuQuit: "q: Quit", + StateIdle: "READY", + StateRunning: "FOCUS", + StatePaused: "PAUSED", + StateBreak: "SHORT BREAK", + StateLongBreak: "LONG BREAK", + StateDone: "DONE!", + LabelFocus: "Focus:", + LabelShortBreak: "Short break:", + LabelLongBreak: "Long break:", + LabelLongAfter: "Long break after:", + LabelTodayCount: "Today:", + LabelTotalCount: "Total:", + LabelNextBreak: "Next long break in:", + HelpIdle: "Space: Start | r: Reset | s: Settings | t: Stats | l: CS | q: Quit", + HelpRunning: "Space: Pause | r: Reset | q: Quit", + HelpSettings: "+/-: Change focus | b/n: Change breaks | Enter: Save | q: Back", + HelpStats: "q/Esc: Back", + SettingsTitle: "TIMER SETTINGS", + StatsTitle: "STATISTICS", + SessionComplete: "🎉 Session complete! Time for a break.", + BreakComplete: "⏰ Break over! Time to focus.", + PomodorosToday: "Completed today:", + PomodorosTotal: "Completed total:", + NextLongBreakIn: "Long break in:", + MinutesShort: "min", + }, +} diff --git a/doxa-timer/tui.go b/doxa-timer/tui.go new file mode 100644 index 0000000..581746a --- /dev/null +++ b/doxa-timer/tui.go @@ -0,0 +1,526 @@ +package main + +import ( + "fmt" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// ─── Styles ────────────────────────────────────────────────────────────────── + +var ( + appStyle = lipgloss.NewStyle(). + Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")) + + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")). + Background(lipgloss.Color("#7D56F4")). + Padding(0, 1). + MarginBottom(1) + + statusStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("205")) + + timerFocusStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")) + + timerPauseStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("205")) + + timerBreakStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#2ECC71")) + + timerDoneStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#F39C12")) + + labelStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")) + + valueStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")) + + settingActiveStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")) + + helpStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1) +) + +// ─── State ──────────────────────────────────────────────────────────────────── + +type AppState int + +const ( + StateStartup AppState = iota + StateIdle + StateRunning + StatePaused + StateBreak + StateLongBreak + StateSettings + StateStats +) + +// ─── Messages ───────────────────────────────────────────────────────────────── + +type tickMsg time.Time + +func tickCmd() tea.Cmd { + return tea.Tick(time.Second, func(t time.Time) tea.Msg { + return tickMsg(t) + }) +} + +// ─── Model ──────────────────────────────────────────────────────────────────── + +type model struct { + state AppState + stats TimerStats + lang string + width int + height int + statusMsg string + // Timer countdown + remaining int // seconds remaining + totalSeconds int // total seconds for current session + // Settings edit fields (temporary values while editing) + editFocus int + editShort int + editLong int + editAfter int + settingCursor int // 0=focus,1=short,2=long,3=after +} + +func NewModel() model { + stats := loadStats() + return model{ + state: StateStartup, + stats: stats, + lang: "cs", + remaining: stats.FocusDuration * 60, + totalSeconds: stats.FocusDuration * 60, + editFocus: stats.FocusDuration, + editShort: stats.ShortBreak, + editLong: stats.LongBreak, + editAfter: stats.LongBreakAfter, + } +} + +func (m model) Init() tea.Cmd { + return tea.EnterAltScreen +} + +// ─── Update ─────────────────────────────────────────────────────────────────── + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + tr := Translations[m.lang] + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + + case tickMsg: + if m.state == StateRunning || m.state == StateBreak || m.state == StateLongBreak { + m.remaining-- + if m.remaining <= 0 { + return m.handleTimerComplete() + } + return m, tickCmd() + } + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + + case "q", "esc": + switch m.state { + case StateStartup: + return m, tea.Quit + case StateIdle: + return m, tea.Quit + case StateRunning, StatePaused: + return m, tea.Quit + case StateSettings, StateStats: + m.state = StateIdle + m.statusMsg = "" + return m, nil + default: + return m, tea.Quit + } + + case "l": + if m.state != StateSettings { + if m.lang == "cs" { + m.lang = "en" + } else { + m.lang = "cs" + } + } + return m, nil + + case "enter": + switch m.state { + case StateStartup: + m.state = StateIdle + m.remaining = m.stats.FocusDuration * 60 + m.totalSeconds = m.stats.FocusDuration * 60 + return m, nil + case StateSettings: + // Save settings + m.stats.FocusDuration = m.editFocus + m.stats.ShortBreak = m.editShort + m.stats.LongBreak = m.editLong + m.stats.LongBreakAfter = m.editAfter + saveStats(m.stats) + m.state = StateIdle + m.remaining = m.stats.FocusDuration * 60 + m.totalSeconds = m.stats.FocusDuration * 60 + m.statusMsg = "" + return m, nil + } + + case " ": + switch m.state { + case StateIdle: + m.state = StateRunning + m.statusMsg = "" + return m, tickCmd() + case StateRunning: + m.state = StatePaused + return m, nil + case StatePaused: + m.state = StateRunning + return m, tickCmd() + case StateBreak, StateLongBreak: + m.state = StateRunning + return m, tickCmd() + } + + case "r": + if m.state == StateRunning || m.state == StatePaused || m.state == StateIdle { + m.state = StateIdle + m.remaining = m.stats.FocusDuration * 60 + m.totalSeconds = m.stats.FocusDuration * 60 + m.statusMsg = "" + return m, nil + } + if m.state == StateBreak || m.state == StateLongBreak { + m.state = StateIdle + m.remaining = m.stats.FocusDuration * 60 + m.totalSeconds = m.stats.FocusDuration * 60 + m.statusMsg = "" + return m, nil + } + + case "s": + if m.state == StateIdle || m.state == StatePaused { + m.state = StateSettings + m.editFocus = m.stats.FocusDuration + m.editShort = m.stats.ShortBreak + m.editLong = m.stats.LongBreak + m.editAfter = m.stats.LongBreakAfter + m.settingCursor = 0 + return m, nil + } + + case "t": + if m.state == StateIdle || m.state == StatePaused { + m.state = StateStats + return m, nil + } + + // Settings navigation + case "up", "k": + if m.state == StateSettings { + m.settingCursor-- + if m.settingCursor < 0 { + m.settingCursor = 3 + } + } + case "down", "j": + if m.state == StateSettings { + m.settingCursor++ + if m.settingCursor > 3 { + m.settingCursor = 0 + } + } + case "+", "=": + if m.state == StateSettings { + switch m.settingCursor { + case 0: + if m.editFocus < 60 { m.editFocus++ } + case 1: + if m.editShort < 30 { m.editShort++ } + case 2: + if m.editLong < 60 { m.editLong++ } + case 3: + if m.editAfter < 10 { m.editAfter++ } + } + } + case "-": + if m.state == StateSettings { + switch m.settingCursor { + case 0: + if m.editFocus > 1 { m.editFocus-- } + case 1: + if m.editShort > 1 { m.editShort-- } + case 2: + if m.editLong > 1 { m.editLong-- } + case 3: + if m.editAfter > 1 { m.editAfter-- } + } + } + } + _ = tr + } + + return m, nil +} + +func (m model) handleTimerComplete() (tea.Model, tea.Cmd) { + tr := Translations[m.lang] + + if m.state == StateRunning { + // Pomodoro complete + m.stats.TotalPomodoros++ + m.stats.TodayPomodoros++ + m.stats.LastDate = time.Now().Format("2006-01-02") + saveStats(m.stats) + m.statusMsg = tr.SessionComplete + + // Decide: long break or short break + if m.stats.TodayPomodoros%m.stats.LongBreakAfter == 0 { + m.state = StateLongBreak + m.remaining = m.stats.LongBreak * 60 + m.totalSeconds = m.stats.LongBreak * 60 + } else { + m.state = StateBreak + m.remaining = m.stats.ShortBreak * 60 + m.totalSeconds = m.stats.ShortBreak * 60 + } + return m, tickCmd() + } + + // Break complete → go idle + m.statusMsg = tr.BreakComplete + m.state = StateIdle + m.remaining = m.stats.FocusDuration * 60 + m.totalSeconds = m.stats.FocusDuration * 60 + return m, nil +} + +// ─── View ───────────────────────────────────────────────────────────────────── + +func (m model) View() string { + tr := Translations[m.lang] + var b strings.Builder + + b.WriteString(titleStyle.Render(tr.Title) + "\n") + + if m.statusMsg != "" { + b.WriteString(statusStyle.Render(m.statusMsg) + "\n\n") + } + + switch m.state { + case StateStartup: + eagle := ` + _ + ( ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("`") + ` + \ ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("\\") + ` + / / _ + __ / / ( \ + ( "---" / / / + \ / / / + \ /-----' / + \ / / + \ / / + '-------' +` + b.WriteString(lipgloss.Place(m.width-12, 14, lipgloss.Center, lipgloss.Center, + eagle+"\n"+ + lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7D56F4")).Render("DOXA-TIMER 🦅")+"\n\n"+ + "Local-First Pomodoro Timer\n\n"+ + "Enter: Start | q: Quit\n\n"+ + "License: MIT | © 2026 Kankys")) + + case StateIdle, StateRunning, StatePaused, StateBreak, StateLongBreak: + // State label + stateLabel := m.getStateLabel() + b.WriteString(lipgloss.NewStyle().Bold(true).Render(stateLabel) + "\n\n") + + // Big timer + timerStr := formatDuration(m.remaining) + timerRendered := m.renderTimer(timerStr) + b.WriteString(lipgloss.PlaceHorizontal(m.width-10, lipgloss.Center, timerRendered) + "\n\n") + + // Progress bar + progressBar := renderProgressBar(m.remaining, m.totalSeconds, 40) + b.WriteString(lipgloss.PlaceHorizontal(m.width-10, lipgloss.Center, progressBar) + "\n\n") + + // Stats row + pomLine := fmt.Sprintf(" %s %s %s %s", + labelStyle.Render(tr.LabelTodayCount), + valueStyle.Render(fmt.Sprintf("%d", m.stats.TodayPomodoros)), + labelStyle.Render(tr.LabelTotalCount), + valueStyle.Render(fmt.Sprintf("%d", m.stats.TotalPomodoros)), + ) + b.WriteString(pomLine + "\n\n") + + // Next long break + remaining := m.stats.LongBreakAfter - (m.stats.TodayPomodoros % m.stats.LongBreakAfter) + if remaining == m.stats.LongBreakAfter { + remaining = 0 + } + if remaining > 0 { + b.WriteString(labelStyle.Render(fmt.Sprintf(" %s %d pomodoro(s)", tr.LabelNextBreak, remaining)) + "\n\n") + } + + // Help + helpText := tr.HelpIdle + if m.state == StateRunning { + helpText = tr.HelpRunning + } else if m.state == StatePaused { + helpText = tr.HelpIdle + } + b.WriteString(helpStyle.Render(helpText)) + + case StateSettings: + b.WriteString(lipgloss.NewStyle().Bold(true).Render(tr.SettingsTitle) + "\n\n") + + settings := []struct { + label string + value int + unit string + }{ + {tr.LabelFocus, m.editFocus, tr.MinutesShort}, + {tr.LabelShortBreak, m.editShort, tr.MinutesShort}, + {tr.LabelLongBreak, m.editLong, tr.MinutesShort}, + {tr.LabelLongAfter, m.editAfter, "pomodorů"}, + } + if m.lang == "en" { + settings[3].unit = "pomodoros" + } + + for i, s := range settings { + cursor := " " + lStyle := labelStyle + vStyle := valueStyle + if i == m.settingCursor { + cursor = "> " + lStyle = settingActiveStyle + vStyle = settingActiveStyle + } + b.WriteString(fmt.Sprintf("%s%-22s %s %s\n", + cursor, + lStyle.Render(s.label), + vStyle.Render(fmt.Sprintf("%d", s.value)), + labelStyle.Render(s.unit), + )) + } + b.WriteString("\n" + helpStyle.Render(tr.HelpSettings)) + + case StateStats: + b.WriteString(lipgloss.NewStyle().Bold(true).Render(tr.StatsTitle) + "\n\n") + b.WriteString(fmt.Sprintf(" %-28s %s\n", + labelStyle.Render(tr.PomodorosToday), + valueStyle.Render(fmt.Sprintf("%d 🍅", m.stats.TodayPomodoros)), + )) + b.WriteString(fmt.Sprintf(" %-28s %s\n", + labelStyle.Render(tr.PomodorosTotal), + valueStyle.Render(fmt.Sprintf("%d 🍅", m.stats.TotalPomodoros)), + )) + b.WriteString(fmt.Sprintf("\n %-28s %d %s\n", + labelStyle.Render(tr.LabelFocus), + m.stats.FocusDuration, + tr.MinutesShort, + )) + b.WriteString(fmt.Sprintf(" %-28s %d %s\n", + labelStyle.Render(tr.LabelShortBreak), + m.stats.ShortBreak, + tr.MinutesShort, + )) + b.WriteString(fmt.Sprintf(" %-28s %d %s\n", + labelStyle.Render(tr.LabelLongBreak), + m.stats.LongBreak, + tr.MinutesShort, + )) + b.WriteString("\n" + helpStyle.Render(tr.HelpStats)) + } + + content := b.String() + if m.width > 0 && m.height > 0 { + return appStyle.Width(m.width - 6).Height(m.height - 4).Render(content) + } + return appStyle.Render(content) +} + +func (m model) getStateLabel() string { + tr := Translations[m.lang] + switch m.state { + case StateIdle: + return tr.StateIdle + case StateRunning: + return tr.StateRunning + case StatePaused: + return tr.StatePaused + case StateBreak: + return tr.StateBreak + case StateLongBreak: + return tr.StateLongBreak + } + return "" +} + +func (m model) renderTimer(s string) string { + large := lipgloss.NewStyle().Bold(true).PaddingLeft(1).PaddingRight(1) + switch m.state { + case StateRunning: + return timerFocusStyle.Copy().Inherit(large).Render(s) + case StatePaused: + return timerPauseStyle.Copy().Inherit(large).Render(s) + case StateBreak, StateLongBreak: + return timerBreakStyle.Copy().Inherit(large).Render(s) + default: + return timerPauseStyle.Copy().Inherit(large).Render(s) + } +} + +// renderProgressBar renders an ASCII progress bar +func renderProgressBar(remaining, total, width int) string { + if total == 0 { + return "" + } + elapsed := total - remaining + filled := int(float64(elapsed) / float64(total) * float64(width)) + if filled > width { + filled = width + } + bar := "[" + strings.Repeat("█", filled) + strings.Repeat("░", width-filled) + "]" + pct := int(float64(elapsed) / float64(total) * 100) + return fmt.Sprintf("%s %3d%%", bar, pct) +} + +// formatDuration formats seconds as MM:SS +func formatDuration(secs int) string { + if secs < 0 { + secs = 0 + } + m := secs / 60 + s := secs % 60 + return fmt.Sprintf("%02d:%02d", m, s) +}