diff options
| author | Paul Buetow <paul@buetow.org> | 2026-01-02 22:39:11 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-01-02 22:40:19 +0200 |
| commit | 96f92ddd8ea1554766b358333e911f607c3e5d6c (patch) | |
| tree | afc26a32d4c42c752165e4975ab74b597ac4c09f | |
| parent | 7afe4c2c1afe9d8f4ba8887758523999fde12b7b (diff) | |
- Add 4 new ASCII art fonts (mono12, rebel, ansi, ansiShadow) adapted from pomo project
- Implement random font selection on startup when no font is specified
- Add 'f' hotkey in live mode to cycle through fonts
- Update LICENSE with MIT attribution for pomo fonts
- Update README with comprehensive font documentation
- Bump version to v0.3.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| -rw-r--r-- | LICENSE | 34 | ||||
| -rw-r--r-- | README.md | 55 | ||||
| -rw-r--r-- | cmd/timr/main.go | 17 | ||||
| -rwxr-xr-x | install-fish-integration.fish | 6 | ||||
| -rw-r--r-- | internal/ascii/fonts.go | 439 | ||||
| -rw-r--r-- | internal/ascii/render.go | 47 | ||||
| -rw-r--r-- | internal/live/live.go | 52 |
7 files changed, 639 insertions, 11 deletions
@@ -7,4 +7,36 @@ Redistribution and use in source and binary forms, with or without modification, 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +Third-Party Components +====================== + +This project includes code adapted from the following third-party projects: + +1. ASCII Art Fonts (internal/ascii package) + - Source: https://github.com/Bahaaio/pomo + - Copyright (c) 2025 Bahaa El Deen Mohamed + - License: MIT License + + MIT License + + 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.
\ No newline at end of file @@ -10,10 +10,24 @@ It has been vibe coded! ## Installation -To build and install `timr`, you need to have Go installed on your system. You can then build the executable by running the following command in the project's root directory: +To build and install `timr`, you need to have Go installed on your system. The project uses [Mage](https://magefile.org/) for building. + +### Using Mage (Recommended) + +```bash +# Install mage if you haven't already +go install github.com/magefile/mage@latest + +# Build the project +mage build +``` + +### Direct Go Build + +Alternatively, you can build directly with Go: ```bash -go build ./... +go build -o timr ./cmd/timr ``` This will create a `timr` executable in the current directory. To make it accessible from anywhere, you can move it to a directory in your system's `PATH`, such as `/usr/local/bin`: @@ -33,7 +47,42 @@ sudo mv timr /usr/local/bin/ * `timr status raw`: Shows the current elapsed time in seconds, in a raw format. * `timr status rawm`: Shows the current elapsed time in minutes, in a raw format. * `timr reset`: Resets the timer. This will set the elapsed time to zero. -* `timr live`: Shows a live, full-screen timer with keyboard controls (q: quit, s: start/stop, r: reset). +* `timr track <description>`: Saves the current elapsed time with a description and resets the timer. +* `timr live [-f|--font <font>]`: Shows a live, full-screen timer with keyboard controls and optional font selection. +* `timr prompt`: Shows a compact status for shell prompt integration. + +## Live Mode + +The live mode provides an interactive, full-screen timer display with the following features: + +### Keyboard Controls + +* `s`: Start/Stop the timer +* `r`: Reset the timer to zero +* `f`: Cycle through different display fonts +* `q` or `Ctrl+C`: Quit live mode + +### Font Options + +When you run `timr live` without specifying a font, it randomly selects from one of the available fonts. You can also specify a font explicitly: + +```bash +# Random font (different each time) +timr live + +# Specific fonts +timr live -f doom # Classic figlet style +timr live -f mono12 # Clean monospace blocks +timr live -f rebel # Stylized shaded blocks +timr live -f ansi # Simple block numerals +timr live -f ansiShadow # Box-drawing characters +``` + +While in live mode, press `f` to cycle through all available fonts in real-time. + +### Font Credits + +The ASCII art fonts (mono12, rebel, ansi, ansiShadow) are adapted from the [pomo](https://github.com/Bahaaio/pomo) project by Bahaa El Deen Mohamed, licensed under the MIT License. ## Fish Shell Integration diff --git a/cmd/timr/main.go b/cmd/timr/main.go index 0c4df06..e3bbe03 100644 --- a/cmd/timr/main.go +++ b/cmd/timr/main.go @@ -2,10 +2,13 @@ package main import ( "fmt" + "math/rand" "os" "strconv" "strings" + "time" + "codeberg.org/snonux/timr/internal/ascii" "codeberg.org/snonux/timr/internal/live" "codeberg.org/snonux/timr/internal/timer" tea "github.com/charmbracelet/bubbletea" @@ -85,7 +88,18 @@ func runCommand(args []string) (string, error) { description := strings.Join(args[2:], " ") output, err = timer.TrackTime(description) case "live": - p := tea.NewProgram(live.NewModel()) + // Parse optional font flag + var font string + if len(args) > 2 && (args[2] == "--font" || args[2] == "-f") { + if len(args) > 3 { + font = args[3] + } + } else { + // Select a random font if no argument is given + rand.Seed(time.Now().UnixNano()) + font = ascii.AllFonts[rand.Intn(len(ascii.AllFonts))] + } + p := tea.NewProgram(live.NewModel(font)) if err := p.Start(); err != nil { return "", err } @@ -103,4 +117,5 @@ func runCommand(args []string) (string, error) { func printUsage() { fmt.Println("Usage: timr <start|continue|stop|pause|status|reset|live|prompt|track <description>>") + fmt.Println(" live [-f|--font <font>] : Show live timer with optional font (doom, mono12, rebel, ansi, ansiShadow)") } diff --git a/install-fish-integration.fish b/install-fish-integration.fish index 53d864e..b90be24 100755 --- a/install-fish-integration.fish +++ b/install-fish-integration.fish @@ -35,10 +35,16 @@ end complete -c timr -n __fish_use_subcommand -a start -d \"Start the timer\" complete -c timr -n __fish_use_subcommand -a stop -d \"Stop the timer\" complete -c timr -n __fish_use_subcommand -a pause -d \"Pause the timer\" +complete -c timr -n __fish_use_subcommand -a continue -d \"Continue the timer\" +complete -c timr -n __fish_use_subcommand -a cont -d \"Continue the timer\" complete -c timr -n __fish_use_subcommand -a status -d \"Show the timer status\" complete -c timr -n __fish_use_subcommand -a reset -d \"Reset the timer\" +complete -c timr -n __fish_use_subcommand -a track -d \"Save time with description\" complete -c timr -n __fish_use_subcommand -a live -d \"Show the live timer\" complete -c timr -n __fish_use_subcommand -a prompt -d \"Show the prompt status\" + +# Font completions for live mode +complete -c timr -n \"__fish_seen_subcommand_from live\" -s f -l font -d \"Font style\" -a \"doom mono12 rebel ansi ansiShadow\" " > "$integration_file" echo "Created: $integration_file" diff --git a/internal/ascii/fonts.go b/internal/ascii/fonts.go new file mode 100644 index 0000000..daea4bb --- /dev/null +++ b/internal/ascii/fonts.go @@ -0,0 +1,439 @@ +// Package ascii provides ASCII art fonts for timer display. +// This code is adapted from https://github.com/Bahaaio/pomo +// Copyright (c) 2025 Bahaa El Deen Mohamed +// Licensed under the MIT License +package ascii + +type Font [11]string + +const ( + Mono12 = "mono12" + Rebel = "rebel" + Ansi = "ansi" + AnsiShadow = "ansiShadow" + Doom = "doom" // figlet doom font + DefaultFont = Mono12 +) + +// AllFonts returns a list of all available font names. +// This includes both ASCII art fonts and the figlet doom font. +var AllFonts = []string{Doom, Mono12, Rebel, Ansi, AnsiShadow} + +var fonts = map[string]Font{ + Mono12: { + ` + ▄▄▄▄ + ██▀▀██ + ██ ██ + ██ ██ ██ + ██ ██ + ██▄▄██ + ▀▀▀▀ +`, + ` + ▄▄▄ + █▀██ + ██ + ██ + ██ + ▄▄▄██▄▄▄ + ▀▀▀▀▀▀▀▀ +`, + ` + ▄▄▄▄▄ + █▀▀▀▀██▄ + ██ + ▄█▀ + ▄█▀ + ▄██▄▄▄▄▄ + ▀▀▀▀▀▀▀▀ +`, + ` + ▄▄▄▄▄ + █▀▀▀▀██▄ + ▄██ + █████ + ▀██ + █▄▄▄▄██▀ + ▀▀▀▀▀ +`, + ` + ▄▄▄ + ▄███ + █▀ ██ + ▄█▀ ██ + ████████ + ██ + ▀▀ +`, + ` + ▄▄▄▄▄▄▄ + ██▀▀▀▀▀ + ██▄▄▄▄ + █▀▀▀▀██▄ + ██ + █▄▄▄▄██▀ + ▀▀▀▀▀ +`, + ` + ▄▄▄▄ + ██▀▀▀█ + ██ ▄▄▄ + ███▀▀██▄ + ██ ██ + ▀██▄▄██▀ + ▀▀▀▀ +`, + ` + ▄▄▄▄▄▄▄▄ + ▀▀▀▀▀███ + ▄██ + ██ + ██ + ██ + ▀▀ +`, + ` + ▄▄▄▄ + ▄██▀▀██▄ + ██▄ ▄██ + ██████ + ██▀ ▀██ + ▀██▄▄██▀ + ▀▀▀▀ +`, + ` + ▄▄▄▄ + ▄██▀▀██▄ + ██ ██ + ▀██▄▄███ + ▀▀▀ ██ + █▄▄▄██ + ▀▀▀▀ +`, + ` + + ▄▄ + ██ + + ██ + ▀▀ + +`, + }, + + Rebel: { + ` + █████ + ███▒▒▒███ + ███ ▒▒███ +▒███ ▒███ +▒███ ▒███ +▒▒███ ███ + ▒▒▒█████▒ + ▒▒▒▒▒▒ +`, + + ` + ████ +▒▒███ + ▒███ + ▒███ + ▒███ + ▒███ + █████ +▒▒▒▒▒ +`, + + ` + ████████ + ███▒▒▒▒███ +▒▒▒ ▒███ + ███████ + ███▒▒▒▒ + ███ █ +▒██████████ +▒▒▒▒▒▒▒▒▒▒ +`, + + ` + ████████ + ███▒▒▒▒███ +▒▒▒ ▒███ + ██████▒ + ▒▒▒▒▒▒███ + ███ ▒███ +▒▒████████ + ▒▒▒▒▒▒▒▒ +`, + + ` + █████ █████ +▒▒███ ▒▒███ + ▒███ ▒███ █ + ▒███████████ + ▒▒▒▒▒▒▒███▒█ + ▒███▒ + █████ + ▒▒▒▒▒ +`, + + ` + ██████████ +▒███▒▒▒▒▒▒█ +▒███ ▒ +▒█████████ +▒▒▒▒▒▒▒▒███ + ███ ▒███ +▒▒████████ + ▒▒▒▒▒▒▒▒ +`, + + ` + ████████ + ███▒▒▒▒███ +▒███ ▒▒▒ +▒█████████ +▒███▒▒▒▒███ +▒███ ▒███ +▒▒████████ + ▒▒▒▒▒▒▒▒ +`, + + ` + ██████████ +▒███▒▒▒▒███ +▒▒▒ ███ + ███ + ███ + ███ + ███ + ▒▒▒ +`, + + ` + ████████ + ███▒▒▒▒███ +▒███ ▒███ +▒▒████████ + ███▒▒▒▒███ +▒███ ▒███ +▒▒████████ + ▒▒▒▒▒▒▒▒ +`, + + ` + ████████ + ███▒▒▒▒███ +▒███ ▒███ +▒▒█████████ + ▒▒▒▒▒▒▒███ + ███ ▒███ +▒▒████████ + ▒▒▒▒▒▒▒▒ +`, + + ` + + ██ + ▒▒ + + + ██ + ▒▒ + + +`, + }, + + Ansi: { + ` + ██████ +██ ████ +██ ██ ██ +████ ██ + ██████ +`, + + ` + ██ +███ + ██ + ██ + ██ +`, + + ` +██████ + ██ + █████ +██ +███████ +`, + + ` +██████ + ██ + █████ + ██ +██████ +`, + + ` +██ ██ +██ ██ +███████ + ██ + ██ +`, + + ` +███████ +██ +███████ + ██ +███████ +`, + + ` + ██████ +██ +███████ +██ ██ + ██████ +`, + + ` +███████ + ██ + ██ + ██ + ██ +`, + + ` + █████ +██ ██ + █████ +██ ██ + █████ +`, + + ` + █████ +██ ██ + ██████ + ██ + █████ +`, + + ` + + ██ + + ██ + +`, + }, + + AnsiShadow: { + ` + ██████╗ +██╔═████╗ +██║██╔██║ +████╔╝██║ +╚██████╔╝ + ╚═════╝ +`, + + ` + ██╗ +███║ +╚██║ + ██║ + ██║ + ╚═╝ +`, + + ` +██████╗ +╚════██╗ + █████╔╝ +██╔═══╝ +███████╗ +╚══════╝ +`, + + ` +██████╗ +╚════██╗ + █████╔╝ + ╚═══██╗ +██████╔╝ +╚═════╝ +`, + + ` +██╗ ██╗ +██║ ██║ +███████║ +╚════██║ + ██║ + ╚═╝ +`, + + ` +███████╗ +██╔════╝ +███████╗ +╚════██║ +███████║ +╚══════╝ +`, + + ` + ██████╗ +██╔════╝ +███████╗ +██╔═══██╗ +╚██████╔╝ + ╚═════╝ +`, + + ` +███████╗ +╚════██║ + ██╔╝ + ██╔╝ + ██║ + ╚═╝ +`, + + ` + █████╗ +██╔══██╗ +╚█████╔╝ +██╔══██╗ +╚█████╔╝ + ╚════╝ +`, + + ` + █████╗ +██╔══██╗ +╚██████║ + ╚═══██║ + █████╔╝ + ╚════╝ +`, + + ` + + ██╗ + ╚═╝ + ██╗ + ╚═╝ + +`, + }, +} diff --git a/internal/ascii/render.go b/internal/ascii/render.go new file mode 100644 index 0000000..eb41dd1 --- /dev/null +++ b/internal/ascii/render.go @@ -0,0 +1,47 @@ +// Package ascii provides ASCII art rendering for timer display. +// This code is adapted from https://github.com/Bahaaio/pomo +// Copyright (c) 2025 Bahaa El Deen Mohamed +// Licensed under the MIT License +package ascii + +import ( + "github.com/charmbracelet/lipgloss" +) + +// RenderNumber renders a string of digits as ASCII art using the specified font. +// The number parameter should contain only digits (0-9) and colons (:). +// Returns the ASCII art representation as a single string with properly aligned characters. +func RenderNumber(number string, font Font) string { + digits := make([]string, 0, len(number)) + + for _, digit := range number { + digits = append(digits, renderDigit(digit, font)) + } + + asciiDigits := lipgloss.JoinHorizontal(lipgloss.Top, digits...) + return asciiDigits +} + +// GetFont returns the font with the given name. +// If the font doesn't exist, it returns the default font. +func GetFont(fontName string) Font { + if font, exists := fonts[fontName]; exists { + return font + } + + return fonts[DefaultFont] +} + +// renderDigit converts a single digit character to its ASCII art representation. +// Supports digits 0-9 and colon (:). Returns empty string for unsupported characters. +func renderDigit(digit rune, font Font) string { + if digit == ':' { + return font[len(font)-1] + } + + if digit < '0' || digit > '9' { + return "" + } + + return font[digit-'0'] +} diff --git a/internal/live/live.go b/internal/live/live.go index 60a4ce7..9590622 100644 --- a/internal/live/live.go +++ b/internal/live/live.go @@ -1,11 +1,13 @@ package live import ( + "fmt" "time" "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/common-nighthawk/go-figure" + "codeberg.org/snonux/timr/internal/ascii" timrTimer "codeberg.org/snonux/timr/internal/timer" ) @@ -26,20 +28,35 @@ type Model struct { statusStyle lipgloss.Style width int height int + font string // Font selection: "doom" for figlet, or ASCII font names + fontIndex int // Current index in the AllFonts slice for cycling } -// NewModel creates a new Model. -func NewModel() Model { +// NewModel creates a new Model with the specified font. +// The font parameter can be "doom" for the original figlet font, +// or one of the ASCII font names: "mono12", "rebel", "ansi", "ansiShadow". +func NewModel(font string) Model { state, err := timrTimer.LoadState() if err != nil { panic(err) // Or handle more gracefully } + // Find the current font index in AllFonts for cycling support + fontIndex := 0 + for i, f := range ascii.AllFonts { + if f == font { + fontIndex = i + break + } + } + return Model{ state: state, helpStyle: lipgloss.NewStyle().Faint(true), timerStyle: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#00BFFF")), statusStyle: lipgloss.NewStyle().Italic(true), + font: font, + fontIndex: fontIndex, } } @@ -93,6 +110,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // handle error } return m, nil // Stop ticking + + case "f": + // Cycle to the next font + m.fontIndex = (m.fontIndex + 1) % len(ascii.AllFonts) + m.font = ascii.AllFonts[m.fontIndex] + return m, nil } } @@ -100,6 +123,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the model's state to the terminal. +// Supports both figlet-style fonts (doom) and large ASCII art fonts (mono12, rebel, ansi, ansiShadow). func (m Model) View() string { if m.quitting { return "" @@ -117,12 +141,27 @@ func (m Model) View() string { status = "Running" } - // Large timer text - figure := figure.NewFigure(totalElapsed.String(), "doom", true) - timerStr := m.timerStyle.Render(figure.String()) + // Render timer based on selected font + var timerStr string + if m.font == "doom" { + // Use original figlet font + figure := figure.NewFigure(totalElapsed.String(), "doom", true) + timerStr = m.timerStyle.Render(figure.String()) + } else { + // Use large ASCII art fonts + font := ascii.GetFont(m.font) + // Format time as HH:MM:SS for better ASCII display + hours := int(totalElapsed.Hours()) + minutes := int(totalElapsed.Minutes()) % 60 + seconds := int(totalElapsed.Seconds()) % 60 + timeStr := fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) + asciiTime := ascii.RenderNumber(timeStr, font) + timerStr = m.timerStyle.Render(asciiTime) + } statusStr := m.statusStyle.Render(status) - helpStr := m.helpStyle.Render("q: quit, s: start/stop, r: reset") + fontInfo := m.helpStyle.Render(fmt.Sprintf("Font: %s", m.font)) + helpStr := m.helpStyle.Render("q: quit, s: start/stop, r: reset, f: change font") // Centering return lipgloss.Place( @@ -135,6 +174,7 @@ func (m Model) View() string { timerStr, statusStr, "", // spacer + fontInfo, helpStr, ), ) |
