habit+timer

This commit is contained in:
kankys committed 2026-08-16 21:13:50 +02:00
1 parent 6356981825
commit f19af63d91
31 files changed
+2767 -106

No files matched your search

+16 -53
View File
@@ -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*
+123 -27
View File
@@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT license">
<img src="https://img.shields.io/badge/language-Go-00ADD8?style=flat-square&logo=go&logoColor=white" alt="Go">
<img src="https://img.shields.io/badge/language-JavaScript-F7DF1E?style=flat-square&logo=javascript&logoColor=black" alt="JavaScript">
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20Windows%20%7C%20FreeBSD-6DA55F?style=flat-square" alt="Platforms">
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20MacOS%20%7C%20FreeBSD-6DA55F?style=flat-square" alt="Platforms">
</p>
<p align="center">
@@ -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)
---
+21
View File
@@ -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.
+41
View File
@@ -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/<habit_name>_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.
+41
View File
@@ -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/<název>_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ů.
+36
View File
@@ -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
+36
View File
@@ -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
BIN
View File
Binary file not shown.
+33
View File
@@ -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
)
+52
View File
@@ -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=
+99
View File
@@ -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
+18
View File
@@ -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)
}
}
+43
View File
@@ -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)
}
+128
View File
@@ -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",
},
}
+846
View File
@@ -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)
}
+29 -8
View File
@@ -6,11 +6,11 @@
"nav.about": "O projektu",
"nav.themeToggle": "Přepnout motiv",
"footer.copyright": "Doxa Notes &copy; 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",
+26 -5
View File
@@ -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",
+89 -13
View File
@@ -6,20 +6,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title data-i18n-key="doxa.pageTitle">Kolekce DOXA — Celý přehled projektů</title>
<meta name="description" data-i18n-key="doxa.pageDescription" data-i18n-attr="content"
content="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." />
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." />
<link rel="canonical" href="https://doxa-notes.org/doxa.html" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://doxa-notes.org/doxa.html" />
<meta property="og:title" data-i18n-key="doxa.ogTitle" data-i18n-attr="content" content="Kolekce DOXA — Celý přehled projektů" />
<meta property="og:description" data-i18n-key="doxa.ogDescription" data-i18n-attr="content" content="Přehled všech projektů DOXA: Notes, CLI, Secret a TODO. Úplná lokalita, otevřené formáty a digitální suverenita." />
<meta property="og:description" data-i18n-key="doxa.ogDescription" data-i18n-attr="content" content="Přehled všech projektů DOXA: Notes, CLI, Secret, TODO, Timer a Habit. Úplná lokalita, otevřené formáty a digitální suverenita." />
<meta property="og:image" content="https://doxa-notes.org/assets/img/doxa.png" />
<meta property="og:image:alt" content="DOXA Collection - Complete Project Overview" />
<meta property="og:locale" data-i18n-key="ogLocale" data-i18n-attr="content" content="cs_CZ" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" data-i18n-key="doxa.twitterTitle" data-i18n-attr="content" content="Kolekce DOXA" />
<meta name="twitter:description" data-i18n-key="doxa.twitterDescription" data-i18n-attr="content" content="Přehled všech projektů DOXA pro správu poznámek, šifrování a organizaci úkolů." />
<meta name="twitter:description" data-i18n-key="doxa.twitterDescription" data-i18n-attr="content" content="Přehled všech projektů DOXA pro správu poznámek, šifrování, organizaci úkolů, Pomodoro timer a sledování návyků." />
<meta name="twitter:image" content="https://doxa-notes.org/assets/img/doxa.png" />
<meta name="twitter:image:alt" content="DOXA Collection - Complete Project Overview" />
@@ -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 @@
<p style="color: var(--muted); margin-bottom: 30px; margin-top: 2px; font-size: 0.95rem;" data-i18n-key="doxa.subtitle">Úplná nezávislost, soukromí a kontrola nad daty</p>
<p data-i18n-key="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.
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.
</p>
<div class="philosophy" data-i18n-key="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.“
„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."
</div>
<hr id="lineHR" style="margin: 30px 0;">
@@ -130,10 +130,37 @@
<p><a id="links" href="https://codeberg.org/Kankys/doxa-todo" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.todo.link">Přečtěte si podrobnější dokumentaci</a></p>
</section>
<hr id="lineHR" style="margin: 20px 0;">
<!-- DOXA Timer -->
<section style="margin-bottom: 40px; text-align: left;">
<h3 data-i18n-key="doxa.timer.title">⏱️ DOXA Timer</h3>
<p><strong data-i18n-key="doxa.timer.description">Pomodoro timer pro terminál se statistikami produktivity</strong></p>
<p><em data-i18n-key="doxa.timer.philosophy">Local-first produktivita. Žádné cloudové aplikace, žádné předplatné – jen čistý fokus na vaši práci. Data uložena v <code>~/.doxa-timer/</code>.</em></p>
<p><strong data-i18n-key="doxa.timer.features">Funkce: Pomodoro technika (25/5/15 min) · Live countdown · Konfigurovatelné délky sessions · Statistiky pomodorů · Dvoujazyčné rozhraní (CZ/EN)</strong></p>
<p><strong data-i18n-key="doxa.timer.tech">Technologie: Go (Golang), Bubble Tea, Lip Gloss</strong></p>
<p><strong data-i18n-key="doxa.timer.platforms">Platformy: Linux, FreeBSD</strong></p>
<p><a id="links" href="https://codeberg.org/Kankys/doxa-timer" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.timer.link">Přečtěte si podrobnější dokumentaci</a></p>
</section>
<hr id="lineHR" style="margin: 20px 0;">
<!-- DOXA Habit -->
<section style="margin-bottom: 40px; text-align: left;">
<h3 data-i18n-key="doxa.habit.title">🎯 DOXA Habit</h3>
<p><strong data-i18n-key="doxa.habit.description">Tracker návyků pro terminál se sledováním streaku</strong></p>
<p><em data-i18n-key="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 <code>~/.doxa-habit/</code>.</em></p>
<p><strong data-i18n-key="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)</strong></p>
<p><strong data-i18n-key="doxa.habit.tech">Technologie: Go (Golang), Bubble Tea, Lip Gloss</strong></p>
<p><strong data-i18n-key="doxa.habit.platforms">Platformy: Linux, FreeBSD</strong></p>
<p><a id="links" href="https://codeberg.org/Kankys/doxa-habit" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.habit.link">Přečtěte si podrobnější dokumentaci</a></p>
</section>
<hr id="lineHR" style="margin: 30px 0;">
<!-- Common Features Table -->
<h2 data-i18n-key="doxa.commonFeatures.title">Společné vlastnosti všech projektů DOXA</h2>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<thead>
<tr>
@@ -142,6 +169,8 @@
<th style="border: 1px solid var(--note-border); padding: 12px; text-align: center; background: var(--button-bg);" data-i18n-key="doxa.commonFeatures.table.header.cli">DOXA CLI</th>
<th style="border: 1px solid var(--note-border); padding: 12px; text-align: center; background: var(--button-bg);" data-i18n-key="doxa.commonFeatures.table.header.secret">DOXA Secret</th>
<th style="border: 1px solid var(--note-border); padding: 12px; text-align: center; background: var(--button-bg);" data-i18n-key="doxa.commonFeatures.table.header.todo">DOXA TODO</th>
<th style="border: 1px solid var(--note-border); padding: 12px; text-align: center; background: var(--button-bg);" data-i18n-key="doxa.commonFeatures.table.header.timer">DOXA Timer</th>
<th style="border: 1px solid var(--note-border); padding: 12px; text-align: center; background: var(--button-bg);" data-i18n-key="doxa.commonFeatures.table.header.habit">DOXA Habit</th>
</tr>
</thead>
<tbody>
@@ -151,6 +180,8 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.openFormat">Otevřený formát (JSON)</td>
@@ -158,6 +189,8 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.noCloud">Žádné cloudové závislosti</td>
@@ -165,6 +198,8 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.openSource">Open Source (MIT)</td>
@@ -172,6 +207,8 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.minimalist">Minimalistický design</td>
@@ -179,6 +216,17 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.bilingual">Dvoujazyčné rozhraní (CZ/EN)</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.tagSupport">Podpora tagů</td>
@@ -186,6 +234,8 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.search">Vyhledávání a filtrování</td>
@@ -193,6 +243,8 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.encryption">Šifrování</td>
@@ -200,6 +252,8 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.priority">Úrovně priority</td>
@@ -207,26 +261,50 @@
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.categories">Kategorie</td>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.timer">Pomodoro timer</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.streak">Streak / Statistiky návyků</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">❌</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅</td>
</tr>
<tr>
<td style="border: 1px solid var(--note-border); padding: 12px;" data-i18n-key="doxa.commonFeatures.table.row.export">Export dat</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅ JSON</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅ TXT</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅ TXT</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅ TXT</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅ JSON</td>
<td style="border: 1px solid var(--note-border); padding: 12px; text-align: center;">✅ CSV</td>
</tr>
</tbody>
</table>
</div>
<hr id="lineHR" style="margin: 30px 0;">
<h2 data-i18n-key="doxa.installation.title">Instalace</h2>
<p data-i18n-key="doxa.installation.text"></p>
<ul style="text-align: left;">
<!--<li><a id="links" href="../doxa-notes/03_install.md" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.installation.notes">DOXA Notes — Instalace</a></li>-->
<li><a id="links" href="https://codeberg.org/Kankys/doxa-cli" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.installation.cli">DOXA CLI — Instalace</a></li>
<li><a id="links" href="https://codeberg.org/Kankys/doxa-secret" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.installation.secret">DOXA Secret — Instalace</a></li>
<li><a id="links" href="https://codeberg.org/Kankys/doxa-todo" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.installation.todo">DOXA TODO — Instalace</a></li>
<li><a id="links" href="https://codeberg.org/Kankys/doxa-timer" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.installation.timer">DOXA Timer — Instalace</a></li>
<li><a id="links" href="https://codeberg.org/Kankys/doxa-habit" target="_blank" rel="noopener noreferrer" data-i18n-key="doxa.installation.habit">DOXA Habit — Instalace</a></li>
</ul>
<hr id="lineHR" style="margin: 30px 0;">
@@ -256,18 +334,16 @@
<h3 data-i18n-key="doxa.roadmap.mainTitle">Hlavní návrhy</h3>
<ul style="text-align: left;">
<li><strong>📅 DOXA Calendar</strong> - <span data-i18n-key="doxa.roadmap.calendar">Termínový kalendář s událostmi a integrací s DOXA TODO</span></li>
<li><strong>📅 DOXA Calendar</strong> - <span data-i18n-key="doxa.roadmap.calendar">Terminálový kalendář s událostmi a integrací s DOXA TODO</span></li>
<li><strong>📇 DOXA Contacts</strong> - <span data-i18n-key="doxa.roadmap.contacts">Správce kontaktů v terminálu</span></li>
<li><strong>🔑 DOXA Password</strong> - <span data-i18n-key="doxa.roadmap.password">Bezpečný správce hesel s místním šifrováním</span></li>
<li><strong>📖 DOXA Journal</strong> - <span data-i18n-key="doxa.roadmap.journal">Deníkový systém pro terminál</span></li>
<li><strong>🎯 DOXA Habit Tracker</strong> - <span data-i18n-key="doxa.roadmap.habit">Tracker návyků pro sledování osobního růstu</span></li>
</ul>
<h3 data-i18n-key="doxa.roadmap.bonusTitle">Bonusové nápady</h3>
<ul style="text-align: left;">
<li><strong>💰 DOXA Finance</strong> - <span data-i18n-key="doxa.roadmap.finance">Správa výdajů a příjmů</span></li>
<li><strong>🔖 DOXA Bookmarks</strong> - <span data-i18n-key="doxa.roadmap.bookmarks">Správce záložek z příkazové řádky</span></li>
<li><strong>⏱️ DOXA Timer</strong> - <span data-i18n-key="doxa.roadmap.timer">Pomodoro timer pro terminál</span></li>
<li><strong>🔄 DOXA Sync</strong> - <span data-i18n-key="doxa.roadmap.sync">Lokální synchronizace mezi zařízeními</span></li>
<li><strong>💾 DOXA Backup</strong> - <span data-i18n-key="doxa.roadmap.backup">Nástroj pro zálohování DOXA dat</span></li>
</ul>
@@ -296,7 +372,7 @@
</div>
<footer>
<p class="copyright" data-i18n-key="footer.copyright">&copy; 2026 Doxa Notes | Local-First & Freedom 🦅</p>
<p class="copyright" data-i18n-key="footer.copyright">&copy; 2026 Doxa Notes | Local-First &amp; Freedom 🦅</p>
</footer>
<script src="assets/js/i18n.js"></script>
<script src="assets/js/script.js"></script>
+21
View File
@@ -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.
+43
View File
@@ -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.
+43
View File
@@ -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.
+32
View File
@@ -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
+32
View File
@@ -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
BIN
View File
Binary file not shown.
+31
View File
@@ -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
)
+48
View File
@@ -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=
+99
View File
@@ -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
+18
View File
@@ -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)
}
}
+86
View File
@@ -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,
}
}
+111
View File
@@ -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",
},
}
+526
View File
@@ -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)
}