mirror of
https://codeberg.org/Kankys/doxa.git
synced 2026-11-11 06:44:36 +01:00
habit+timer
This commit is contained in:
1 parent
6356981825
commit
f19af63d91
31 files changed
+2767
-106
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
BIN
Binary file not shown.
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
Executable
+99
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user