From 64ea47f42b22450f1bb5d847af5e473765e84a04 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Thu, 17 Oct 2024 16:21:08 -0700 Subject: [PATCH] Totally new with a TUI and no json files --- .gitignore | 2 + Makefile | 50 ++++++ go.mod | 33 ++++ go.sum | 51 ++++++ main.go | 452 +++++++++++++++++++++++++++++++++++++++-------------- 5 files changed, 472 insertions(+), 116 deletions(-) create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore index ed6a967..1e25190 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ _testmain.go *.prof .DS_Store +gomodoro +coverage.out diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3834be7 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +VERSION ?= $(shell git describe --tags --dirty) +GOFILES = *.go go.mod go.sum +APP_NAME = gomodoro +# Multi-arch targets are generated from this +TARGET_ALIAS = $(APP_NAME)-linux-amd64 $(APP_NAME)-linux-arm $(APP_NAME)-linux-arm64 $(APP_NAME)-darwin-amd64 +TARGETS = $(addprefix dist/,$(TARGET_ALIAS)) +# +# Default make target will run tests +.DEFAULT_GOAL = test + +# Build all static Minitor binaries +.PHONY: all +all: $(TARGETS) + +# Build app for the current machine +$(APP_NAME): $(GOFILES) + @echo Version: $(VERSION) + go build -ldflags '-X "main.version=${VERSION}"' -o $(APP_NAME) + +.PHONY: build +build: $(APP_NAME) + +# Run app for the current machine +.PHONY: run +run: $(APP_NAME) + ./$(APP_NAME) + +# Run all tests +.PHONY: test +test: + go test -coverprofile=coverage.out + go tool cover -func=coverage.out + @go tool cover -func=coverage.out | awk -v target=80.0% \ + '/^total:/ { print "Total coverage: " $$3 " Minimum coverage: " target; if ($$3+0.0 >= target+0.0) print "ok"; else { print "fail"; exit 1; } }' + +# Installs pre-commit hooks +.PHONY: install-hooks +install-hooks: + pre-commit install --install-hooks + +# Runs pre-commit checks on files +.PHONY: check +check: + pre-commit run --all-files + +.PHONY: clean +clean: + rm -f ./$(APP_NAME) + rm -f ./coverage.out + rm -fr ./dist diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dbc5ab5 --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module git.iamthefij.com/iamthefij/gomodoro + +go 1.21.4 + +require ( + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.1.1 + github.com/charmbracelet/harmonica v0.2.0 + github.com/charmbracelet/lipgloss v0.13.0 + github.com/urfave/cli/v2 v2.27.5 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.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.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7978ff9 --- /dev/null +++ b/go.sum @@ -0,0 +1,51 @@ +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 v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/main.go b/main.go index 66583c2..5bf6945 100644 --- a/main.go +++ b/main.go @@ -1,144 +1,364 @@ package main import ( - "encoding/json" - "flag" "fmt" - "log" - "net" "os" + "os/exec" "os/signal" - "path" - "syscall" + "strconv" "time" + + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/urfave/cli/v2" ) +type screen int + +var version = "dev" + const ( - ConfigDir = ".config/gomodoro" - LogFileName = "gomodoro.json" + inputScreen screen = iota + timerScreen ) -// Gomodoro holds the config for the current timer -type TimerConfig struct { - StartTime time.Time - Duration time.Duration - Interval time.Duration +type model struct { + focusIndex int + inputs []textinput.Model + progressBar progress.Model + state string + intervalNum int + timer *time.Timer + remaining time.Duration + totalTime time.Duration + currentScreen screen + intervals int + breakTime time.Duration + focusTime time.Duration + onFocusStart []string + onFocusEnd []string + onIntervalEnd []string + isFocus bool + fullscreen bool + width int + height int } -// PomoStatus Represents the status of the Gomodor timer at a given moment -type PomoStatus struct { - Status string - StartTime int64 - DurationSeconds float64 - RemainingSeconds float64 - Done bool -} +func initialModel(fullscreen bool, colorLeft string, colorRight string) model { + inputs := make([]textinput.Model, 3) -// timeRemaining returns the remaining duration rounded to the nearest interval -func timeRemaining(start time.Time, duration time.Duration, interval time.Duration) time.Duration { - return (duration - time.Since(start)).Round(interval) -} - -// fmtJSON returns a JSON string for a given interface and safely returns an err -func fmtJSON(obj interface{}) (string, error) { - bstring, err := json.Marshal(obj) - if err != nil { - return "", err - } - return string(bstring[:]), err -} - -// logRemaining prints a JSON formatted string logging the time remaining in the timer -func logRemaining(logFile *os.File, timerConfig TimerConfig) error { - timeRemaining := timeRemaining(timerConfig.StartTime, timerConfig.Duration, timerConfig.Interval) - err := logTextStatus(logFile, timerConfig, timeRemaining.String(), false) - return err -} - -// logTextStatus logs the status of a timer with a given string -func logTextStatus(logFile *os.File, timerConfig TimerConfig, status string, done bool) error { - // TODO: Find a better way than calculating this twice - timeRemaining := timeRemaining(timerConfig.StartTime, timerConfig.Duration, timerConfig.Interval) - jstring, err := fmtJSON(&PomoStatus{ - Status: status, - StartTime: timerConfig.StartTime.Unix(), - DurationSeconds: timerConfig.Duration.Seconds(), - RemainingSeconds: timeRemaining.Seconds(), - Done: done, - }) - if err == nil { - if logFile != nil { - logFile.WriteString(jstring + "\n") + // Set up text input models for interval length, break length, and total intervals + for i := range inputs { + inputs[i] = textinput.New() + inputs[i].CharLimit = 10 // Increase char limit to allow duration strings + if i == 0 { + inputs[i].Focus() // Start focus on first input } - fmt.Printf("%s / %s\n", status, timerConfig.Duration.String()) - } - return err + + inputs[0].Placeholder = "Interval length (minutes or duration)" + inputs[1].Placeholder = "Break length (minutes or duration)" + inputs[2].Placeholder = "Number of intervals" + + return model{ + inputs: inputs, + progressBar: progress.New(progress.WithScaledGradient(colorLeft, colorRight)), + state: "stopped", + intervalNum: 1, + remaining: 0, + totalTime: 0, + currentScreen: inputScreen, // Start on input screen + isFocus: true, + fullscreen: fullscreen, + } } -func tryLock() (net.Listener, error) { - return net.Listen("unix", "/tmp/gomodoro") +func (m model) Init() tea.Cmd { + // Handle Ctrl+C for graceful exit + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + <-c + fmt.Println("\nExiting...") + os.Exit(0) + }() + + return textinput.Blink } -func openLogFile() (*os.File, error) { - dir := path.Join(os.Getenv("HOME"), ConfigDir) - os.MkdirAll(dir, os.ModePerm) - logFile, err := os.OpenFile(path.Join(dir, LogFileName), os.O_APPEND|os.O_RDWR|os.O_CREATE, os.ModePerm) - return logFile, err +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + // Gracefully exit + return m, tea.Quit + + case "tab": + // Move to the next input field + m.focusIndex = (m.focusIndex + 1) % len(m.inputs) + m.updateFocus() + return m, nil + + case "shift+tab": + // Move to the previous input field + m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs) + m.updateFocus() + return m, nil + + case "enter": + if m.currentScreen == inputScreen { + // Handle inputs and move to the timer screen + if m.focusIndex == len(m.inputs)-1 { + focusTime, err := parseDuration(m.inputs[0].Value()) + if err != nil { + focusTime, _ = time.ParseDuration(m.inputs[0].Value() + "m") + } + + breakTime, err := parseDuration(m.inputs[1].Value()) + if err != nil { + breakTime, _ = time.ParseDuration(m.inputs[1].Value() + "m") + } + + m.intervals = atoi(m.inputs[2].Value()) + m.focusTime = focusTime + m.breakTime = breakTime + m.remaining = focusTime + m.totalTime = focusTime + m.state = "Focus" + m.timer = time.NewTimer(time.Second) + m.currentScreen = timerScreen + + // Run onFocusStart commands + m.runCommands(m.onFocusStart) + + return m, tick() + } else { + // Move to next input field + m.focusIndex++ + m.updateFocus() + } + } + } + + case timeMsg: + // Handle timer update for each second + m.remaining -= time.Second + if m.remaining < 0 { + m.runCommands(m.onIntervalEnd) + + if m.isFocus { + // Focus period ends, switch to break + m.isFocus = false + m.remaining = m.remaining + m.breakTime + m.totalTime = m.breakTime + m.state = "Break" + + // Run onFocusEnd commands + m.runCommands(m.onFocusEnd) + } else { + // Break ends, switch back to focus or next interval + m.isFocus = true + m.intervalNum++ + if m.intervalNum > m.intervals { + // All intervals completed + return m, tea.Quit + } + m.remaining = m.remaining + m.focusTime + m.totalTime = m.focusTime + m.state = "Focus" + + // Run onFocusStart commands + m.runCommands(m.onFocusStart) + } + return m, tick() + } + return m, tick() + + case tea.WindowSizeMsg: + if m.fullscreen { + m.width = msg.Width + m.height = msg.Height + } + } + + // Update text inputs + cmds := make([]tea.Cmd, len(m.inputs)) + for i := range m.inputs { + m.inputs[i], cmds[i] = m.inputs[i].Update(msg) + } + + return m, tea.Batch(cmds...) +} + +// Tick every second +type timeMsg time.Time + +func tick() tea.Cmd { + return tea.Tick(time.Second, func(t time.Time) tea.Msg { + return timeMsg(t) + }) +} + +// View rendering for input and timer screens +func (m model) View() string { + switch m.currentScreen { + case inputScreen: + return m.inputScreenView() + case timerScreen: + return m.timerScreenView() + } + return "" +} + +// View for input screen +func (m model) inputScreenView() string { + var builder string + builder = "Enter your Pomodoro settings:\n\n" + for i := range m.inputs { + builder += m.inputs[i].View() + "\n" + } + builder += "\nUse TAB to navigate, ENTER to start." + return builder +} + +// View for timer screen with optional fullscreen centering +func (m model) timerScreenView() string { + progressView := m.progressBar.ViewAs(float64(m.totalTime-m.remaining) / float64(m.totalTime)) + + status := lipgloss.NewStyle().Bold(true).Render(fmt.Sprintf("State: %s", m.state)) + intervalInfo := fmt.Sprintf("Interval: %d / %d", m.intervalNum, m.intervals) + timeLeft := fmt.Sprintf("Time left: %s", m.remaining.String()) + + timerView := fmt.Sprintf("%s\n%s\n%s\n\n%s", status, intervalInfo, timeLeft, progressView) + + if m.fullscreen { + return lipgloss.NewStyle().Width(m.width).Height(m.height).Align(lipgloss.Center, lipgloss.Center).Render(timerView) + } + return timerView +} + +// Helper to update input focus +func (m *model) updateFocus() { + for i := range m.inputs { + if i == m.focusIndex { + m.inputs[i].Focus() + } else { + m.inputs[i].Blur() + } + } +} + +// Helper to run shell commands +func (m *model) runCommands(commands []string) { + for _, cmdStr := range commands { + cmd := exec.Command("sh", "-c", cmdStr) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf("Error running command: %v\n", err) + } + } +} + +// Helper to parse duration or treat an integer as minutes +func parseDuration(input string) (time.Duration, error) { + if minutes, err := strconv.Atoi(input); err == nil { + return time.Duration(minutes) * time.Minute, nil + } + return time.ParseDuration(input) +} + +// Helper to convert string to int +func atoi(s string) int { + n, _ := strconv.Atoi(s) + return n } func main() { - var duration time.Duration - flag.DurationVar(&duration, "duration", 25*time.Minute, "Gomodoro duration") - var interval time.Duration - flag.DurationVar(&interval, "interval", 1*time.Second, "Gomodoro tick interval") - /* Haven't found a good way to tail a file in golang yet - * var status bool - * flag.BoolVar(&status, "status", false, "Only display current Gomodoro status") - */ - flag.Parse() + // Set up CLI app + app := &cli.App{ + Name: "Gomodoro", + Usage: "A Pomodoro timer with customizable shell commands", + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "on-focus-start", + Usage: "Command(s) to run when focus starts", + }, + &cli.StringSliceFlag{ + Name: "on-focus-end", + Usage: "Command(s) to run when focus ends", + }, + &cli.StringSliceFlag{ + Name: "on-interval-end", + Usage: "Command(s) to run when any interval ends", + }, + &cli.BoolFlag{ + Name: "fullscreen", + Usage: "Enable fullscreen mode", + }, + &cli.DurationFlag{ + Name: "focus", + Usage: "Focus time duration (default prompt for input)", + }, + &cli.DurationFlag{ + Name: "break", + Usage: "Break time duration (default prompt for input)", + }, + &cli.IntFlag{ + Name: "intervals", + Usage: "Number of intervals (default prompt for input)", + }, + &cli.StringFlag{ + Name: "color-left", + Usage: "Left color for progress bar", + DefaultText: "#ffdd57", + }, + &cli.StringFlag{ + Name: "color-right", + Usage: "Right color for progress bar", + DefaultText: "#57ddff", + }, + &cli.BoolFlag{ + Name: "version", + Usage: "Show version", + }, + }, + Action: func(c *cli.Context) error { + if c.Bool("version") { + fmt.Println(version) + return nil + } - // Ensure obtain lock - lock, err := tryLock() + m := initialModel(c.Bool("fullscreen"), c.String("color-left"), c.String("color-right")) + + // Collect command flags + m.onFocusStart = c.StringSlice("on-focus-start") + m.onFocusEnd = c.StringSlice("on-focus-end") + m.onIntervalEnd = c.StringSlice("on-interval-end") + + // Set focus, break, and interval values if provided + if c.Duration("focus").String() != "0s" { + m.inputs[0].SetValue(c.String("focus")) + } + if c.Duration("break").String() != "0s" { + m.inputs[1].SetValue(c.String("break")) + } + if c.Int("intervals") != 0 { + m.inputs[2].SetValue(c.String("intervals")) + } + + // Start tea program + p := tea.NewProgram(m, tea.WithAltScreen()) + _, err := p.Run() + return err + }, + } + + err := app.Run(os.Args) if err != nil { - // TODO: More helpful message - log.Fatal(err) - return - } - defer lock.Close() - - // Open file for logging - logFile, err := openLogFile() - if err != nil { - lock.Close() - log.Fatal(err) - return - } - defer logFile.Close() - - // Start timer - start := time.Now() - timerConfig := TimerConfig{ - StartTime: start, - Duration: duration, - Interval: interval, - } - tick := time.Tick(interval) - end := time.After(duration) - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, os.Kill, syscall.SIGINT, syscall.SIGTERM) - for { - select { - case <-tick: - logRemaining(logFile, timerConfig) - case <-end: - logTextStatus(logFile, timerConfig, "Complete", true) - return - case <-stop: - logTextStatus(logFile, timerConfig, "Interrupted", true) - return - default: - time.Sleep(50 * time.Millisecond) - } + fmt.Println("Error:", err) } }