Use shell runner
This commit is contained in:
parent
4e05f18091
commit
10c345bc96
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,3 +27,4 @@ _testmain.go
|
||||
.DS_Store
|
||||
gomodoro
|
||||
coverage.out
|
||||
dist/
|
||||
|
2
Makefile
2
Makefile
@ -2,7 +2,7 @@ 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
|
||||
TARGET_ALIAS = $(APP_NAME)-linux-amd64 $(APP_NAME)-linux-arm $(APP_NAME)-linux-arm64 $(APP_NAME)-darwin-amd64 $(APP_NAME)-darwin-arm64
|
||||
TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
|
||||
#
|
||||
# Default make target will run tests
|
||||
|
3
go.mod
3
go.mod
@ -3,9 +3,10 @@ module git.iamthefij.com/iamthefij/gomodoro
|
||||
go 1.21.4
|
||||
|
||||
require (
|
||||
git.iamthefij.com/iamthefij/go-shell-runner v0.1.0
|
||||
git.iamthefij.com/iamthefij/slog v1.3.0
|
||||
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
|
||||
)
|
||||
|
96
main.go
96
main.go
@ -1,14 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
shellrunner "git.iamthefij.com/iamthefij/go-shell-runner"
|
||||
"git.iamthefij.com/iamthefij/slog"
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@ -18,7 +20,9 @@ import (
|
||||
|
||||
type screen int
|
||||
|
||||
var version = "dev"
|
||||
var (
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
const (
|
||||
inputScreen screen = iota
|
||||
@ -31,13 +35,12 @@ type model struct {
|
||||
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
|
||||
startTime time.Time
|
||||
onFocusStart []string
|
||||
onFocusEnd []string
|
||||
onIntervalEnd []string
|
||||
@ -46,6 +49,7 @@ type model struct {
|
||||
width int
|
||||
height int
|
||||
err error
|
||||
shellrunner *shellrunner.ShellRunner
|
||||
}
|
||||
|
||||
func initialModel(fullscreen bool, colorLeft string, colorRight string) model {
|
||||
@ -92,10 +96,10 @@ func initialModel(fullscreen bool, colorLeft string, colorRight string) model {
|
||||
state: "stopped",
|
||||
intervalNum: 1,
|
||||
remaining: 0,
|
||||
totalTime: 0,
|
||||
currentScreen: inputScreen, // Start on input screen
|
||||
isFocus: true,
|
||||
fullscreen: fullscreen,
|
||||
shellrunner: shellrunner.NewShellRunner(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,13 +110,24 @@ func (m model) Init() tea.Cmd {
|
||||
|
||||
go func() {
|
||||
<-c
|
||||
m.shellrunner.KillWithTimeout(10 * time.Second)
|
||||
fmt.Println("\nExiting...")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
m.shellrunner.Start()
|
||||
|
||||
return tea.Batch(tea.WindowSize(), textinput.Blink)
|
||||
}
|
||||
|
||||
func (m model) totalTime() time.Duration {
|
||||
if m.isFocus {
|
||||
return m.focusTime
|
||||
}
|
||||
|
||||
return m.breakTime
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
@ -166,13 +181,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.focusTime = focusTime
|
||||
m.breakTime = breakTime
|
||||
m.remaining = focusTime
|
||||
m.totalTime = focusTime
|
||||
m.state = "Focus"
|
||||
m.timer = time.NewTimer(time.Second)
|
||||
m.currentScreen = timerScreen
|
||||
m.startTime = time.Now()
|
||||
|
||||
// Run onFocusStart commands
|
||||
m.runCommands(m.onFocusStart)
|
||||
m.startCommands(m.onFocusStart)
|
||||
|
||||
return m, tick()
|
||||
} else {
|
||||
@ -186,34 +200,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case timeMsg:
|
||||
// Handle timer update for each second
|
||||
// TODO: Use absolute times to tick down remaining time rather than incrementing seconds
|
||||
m.remaining -= time.Second
|
||||
m.remaining = m.totalTime() - time.Since(m.startTime)
|
||||
if m.remaining < 0 {
|
||||
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"
|
||||
m.startTime = time.Now()
|
||||
|
||||
// Run onFocusEnd commands
|
||||
m.runCommands(m.onFocusEnd)
|
||||
m.startCommands(m.onFocusEnd)
|
||||
} else {
|
||||
// Break ends, switch back to focus or next interval
|
||||
m.isFocus = true
|
||||
m.intervalNum++
|
||||
m.runCommands(m.onIntervalEnd)
|
||||
m.startCommands(m.onIntervalEnd)
|
||||
|
||||
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"
|
||||
m.startTime = time.Now()
|
||||
|
||||
// Run onFocusStart commands
|
||||
m.runCommands(m.onFocusStart)
|
||||
m.startCommands(m.onFocusStart)
|
||||
}
|
||||
|
||||
return m, tick()
|
||||
@ -232,6 +244,21 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
||||
}
|
||||
|
||||
for {
|
||||
result := m.shellrunner.GetResults()
|
||||
if result == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if result.ReturnCode != 0 {
|
||||
m.err = errors.Join(m.err, fmt.Errorf("error running command: %s", result.Command))
|
||||
// TODO: Create an error view
|
||||
slog.Errorf("Error running command: %s", result.Command)
|
||||
slog.Errorf("Error: %s", result.Output)
|
||||
slog.Errorf("Error: %s", result.ErrOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@ -290,10 +317,10 @@ func (m model) timerScreenView() string {
|
||||
m.progressBar.Width = m.width - progressBarPadding
|
||||
}
|
||||
|
||||
progressView := m.progressBar.ViewAs(float64(m.totalTime-m.remaining) / float64(m.totalTime))
|
||||
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())
|
||||
timeLeft := fmt.Sprintf("Time left: %s", m.remaining.Round(time.Second).String())
|
||||
|
||||
timerView := fmt.Sprintf("%s\n%s\n%s\n\n%s", status, intervalInfo, timeLeft, progressView)
|
||||
|
||||
@ -305,25 +332,23 @@ func (m model) timerScreenView() string {
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 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) {
|
||||
func (m *model) startCommands(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)
|
||||
if err := m.shellrunner.AddCommand(cmdStr, nil); err != nil {
|
||||
m.err = fmt.Errorf("error adding command: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -400,9 +425,6 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("color-left: %s\n", c.String("color-left"))
|
||||
fmt.Printf("color-right: %s\n", c.String("color-right"))
|
||||
|
||||
m := initialModel(c.Bool("fullscreen"), c.String("color-left"), c.String("color-right"))
|
||||
|
||||
// Collect command flags
|
||||
|
56
main_test.go
56
main_test.go
@ -1,16 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type compareFunc[T comparable] func(T, T) bool
|
||||
|
||||
func assertFunc[T comparable](t *testing.T, compare compareFunc[T], actual, expected T, msg string) {
|
||||
t.Helper()
|
||||
|
||||
if !compare(actual, expected) {
|
||||
t.Errorf("%s: expected %v, got %v", msg, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// assertEqual checks if two values are equal and reports an error if they are not.
|
||||
func assertEqual(t *testing.T, actual, expected interface{}, msg string) {
|
||||
t.Helper()
|
||||
@ -54,37 +60,12 @@ func TestParseDuration(t *testing.T) {
|
||||
|
||||
// TestRunCommands tests the runCommands function
|
||||
func TestRunCommands(t *testing.T) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := initialModel(false, "#ffdd57", "#57ddff")
|
||||
m.shellrunner.Start()
|
||||
|
||||
// Redirect stdout and stderr to the pipe
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
os.Stdout = w
|
||||
os.Stderr = w
|
||||
m.startCommands([]string{"echo Hello, World!"})
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
|
||||
w.Close()
|
||||
})
|
||||
|
||||
m := model{}
|
||||
m.runCommands([]string{"echo Hello, World!"})
|
||||
|
||||
w.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if _, err = io.Copy(&buf, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
assertEqual(t, strings.Contains(output, "Hello, World!"), true, "Expected output to contain 'Hello, World!'")
|
||||
m.shellrunner.Stop()
|
||||
}
|
||||
|
||||
// TestInputView tests the Update method of the model for the input view
|
||||
@ -126,7 +107,7 @@ func TestInputView(t *testing.T) {
|
||||
assertEqual(t, m.err, nil, "Expected no error")
|
||||
assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil")
|
||||
assertEqual(t, m.currentScreen, timerScreen, "Expected currentScreen to be timerScreen")
|
||||
assertEqual(t, m.remaining, 10*time.Minute, "Expected remaining to be 10 minutes")
|
||||
assertEqual(t, m.remaining.Round(time.Second), 10*time.Minute, "Expected remaining to be 10 minutes")
|
||||
|
||||
// Test timer view
|
||||
m.View()
|
||||
@ -136,21 +117,18 @@ func TestInputView(t *testing.T) {
|
||||
m = updatedModel.(model)
|
||||
|
||||
assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'")
|
||||
assertEqual(t, m.remaining, 9*time.Minute+59*time.Second, "Expected remaining to be 9 minutes 59 seconds")
|
||||
|
||||
// Test switch to break time
|
||||
m.remaining = 0
|
||||
m.startTime = m.startTime.Add(-10 * time.Minute)
|
||||
updatedModel, _ = m.Update(oneSec)
|
||||
m = updatedModel.(model)
|
||||
|
||||
assertEqual(t, m.state, "Break", "Expected state to be 'Break'")
|
||||
assertEqual(t, m.remaining, 9*time.Minute+59*time.Second, "Expected remaining to be 9 minutes 59 seconds")
|
||||
|
||||
// Switch back to focus time
|
||||
m.remaining = 0
|
||||
m.startTime = m.startTime.Add(-10 * time.Minute)
|
||||
updatedModel, _ = m.Update(oneSec)
|
||||
m = updatedModel.(model)
|
||||
|
||||
assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'")
|
||||
assertEqual(t, m.remaining, 9*time.Minute+59*time.Second, "Expected remaining to be 9 minutes 59 seconds")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user