From 96f92ddd8ea1554766b358333e911f607c3e5d6c Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 2 Jan 2026 22:39:11 +0200 Subject: add large ASCII fonts and font cycling to live mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- internal/ascii/fonts.go | 439 +++++++++++++++++++++++++++++++++++++++++++++++ internal/ascii/render.go | 47 +++++ internal/live/live.go | 52 +++++- 3 files changed, 532 insertions(+), 6 deletions(-) create mode 100644 internal/ascii/fonts.go create mode 100644 internal/ascii/render.go (limited to 'internal') 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, ), ) -- cgit v1.2.3