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
|
.DS_Store
|
||||||
gomodoro
|
gomodoro
|
||||||
coverage.out
|
coverage.out
|
||||||
|
dist/
|
||||||
|
2
Makefile
2
Makefile
@ -2,7 +2,7 @@ VERSION ?= $(shell git describe --tags --dirty)
|
|||||||
GOFILES = *.go go.mod go.sum
|
GOFILES = *.go go.mod go.sum
|
||||||
APP_NAME = gomodoro
|
APP_NAME = gomodoro
|
||||||
# Multi-arch targets are generated from this
|
# 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))
|
TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
|
||||||
#
|
#
|
||||||
# Default make target will run tests
|
# 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
|
go 1.21.4
|
||||||
|
|
||||||
require (
|
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/bubbles v0.20.0
|
||||||
github.com/charmbracelet/bubbletea v1.1.1
|
github.com/charmbracelet/bubbletea v1.1.1
|
||||||
github.com/charmbracelet/harmonica v0.2.0
|
|
||||||
github.com/charmbracelet/lipgloss v0.13.0
|
github.com/charmbracelet/lipgloss v0.13.0
|
||||||
github.com/urfave/cli/v2 v2.27.5
|
github.com/urfave/cli/v2 v2.27.5
|
||||||
)
|
)
|
||||||
|
96
main.go
96
main.go
@ -1,14 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
shellrunner "git.iamthefij.com/iamthefij/go-shell-runner"
|
||||||
|
"git.iamthefij.com/iamthefij/slog"
|
||||||
"github.com/charmbracelet/bubbles/progress"
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@ -18,7 +20,9 @@ import (
|
|||||||
|
|
||||||
type screen int
|
type screen int
|
||||||
|
|
||||||
var version = "dev"
|
var (
|
||||||
|
version = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
inputScreen screen = iota
|
inputScreen screen = iota
|
||||||
@ -31,13 +35,12 @@ type model struct {
|
|||||||
progressBar progress.Model
|
progressBar progress.Model
|
||||||
state string
|
state string
|
||||||
intervalNum int
|
intervalNum int
|
||||||
timer *time.Timer
|
|
||||||
remaining time.Duration
|
remaining time.Duration
|
||||||
totalTime time.Duration
|
|
||||||
currentScreen screen
|
currentScreen screen
|
||||||
intervals int
|
intervals int
|
||||||
breakTime time.Duration
|
breakTime time.Duration
|
||||||
focusTime time.Duration
|
focusTime time.Duration
|
||||||
|
startTime time.Time
|
||||||
onFocusStart []string
|
onFocusStart []string
|
||||||
onFocusEnd []string
|
onFocusEnd []string
|
||||||
onIntervalEnd []string
|
onIntervalEnd []string
|
||||||
@ -46,6 +49,7 @@ type model struct {
|
|||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
err error
|
err error
|
||||||
|
shellrunner *shellrunner.ShellRunner
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel(fullscreen bool, colorLeft string, colorRight string) model {
|
func initialModel(fullscreen bool, colorLeft string, colorRight string) model {
|
||||||
@ -92,10 +96,10 @@ func initialModel(fullscreen bool, colorLeft string, colorRight string) model {
|
|||||||
state: "stopped",
|
state: "stopped",
|
||||||
intervalNum: 1,
|
intervalNum: 1,
|
||||||
remaining: 0,
|
remaining: 0,
|
||||||
totalTime: 0,
|
|
||||||
currentScreen: inputScreen, // Start on input screen
|
currentScreen: inputScreen, // Start on input screen
|
||||||
isFocus: true,
|
isFocus: true,
|
||||||
fullscreen: fullscreen,
|
fullscreen: fullscreen,
|
||||||
|
shellrunner: shellrunner.NewShellRunner(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,13 +110,24 @@ func (m model) Init() tea.Cmd {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-c
|
<-c
|
||||||
|
m.shellrunner.KillWithTimeout(10 * time.Second)
|
||||||
fmt.Println("\nExiting...")
|
fmt.Println("\nExiting...")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
m.shellrunner.Start()
|
||||||
|
|
||||||
return tea.Batch(tea.WindowSize(), textinput.Blink)
|
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) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
@ -166,13 +181,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.focusTime = focusTime
|
m.focusTime = focusTime
|
||||||
m.breakTime = breakTime
|
m.breakTime = breakTime
|
||||||
m.remaining = focusTime
|
m.remaining = focusTime
|
||||||
m.totalTime = focusTime
|
|
||||||
m.state = "Focus"
|
m.state = "Focus"
|
||||||
m.timer = time.NewTimer(time.Second)
|
|
||||||
m.currentScreen = timerScreen
|
m.currentScreen = timerScreen
|
||||||
|
m.startTime = time.Now()
|
||||||
|
|
||||||
// Run onFocusStart commands
|
// Run onFocusStart commands
|
||||||
m.runCommands(m.onFocusStart)
|
m.startCommands(m.onFocusStart)
|
||||||
|
|
||||||
return m, tick()
|
return m, tick()
|
||||||
} else {
|
} else {
|
||||||
@ -186,34 +200,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case timeMsg:
|
case timeMsg:
|
||||||
// Handle timer update for each second
|
// Handle timer update for each second
|
||||||
// TODO: Use absolute times to tick down remaining time rather than incrementing seconds
|
// 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.remaining < 0 {
|
||||||
if m.isFocus {
|
if m.isFocus {
|
||||||
// Focus period ends, switch to break
|
// Focus period ends, switch to break
|
||||||
m.isFocus = false
|
m.isFocus = false
|
||||||
m.remaining = m.remaining + m.breakTime
|
|
||||||
m.totalTime = m.breakTime
|
|
||||||
m.state = "Break"
|
m.state = "Break"
|
||||||
|
m.startTime = time.Now()
|
||||||
|
|
||||||
// Run onFocusEnd commands
|
// Run onFocusEnd commands
|
||||||
m.runCommands(m.onFocusEnd)
|
m.startCommands(m.onFocusEnd)
|
||||||
} else {
|
} else {
|
||||||
// Break ends, switch back to focus or next interval
|
// Break ends, switch back to focus or next interval
|
||||||
m.isFocus = true
|
m.isFocus = true
|
||||||
m.intervalNum++
|
m.intervalNum++
|
||||||
m.runCommands(m.onIntervalEnd)
|
m.startCommands(m.onIntervalEnd)
|
||||||
|
|
||||||
if m.intervalNum > m.intervals {
|
if m.intervalNum > m.intervals {
|
||||||
// All intervals completed
|
// All intervals completed
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
m.remaining = m.remaining + m.focusTime
|
|
||||||
m.totalTime = m.focusTime
|
|
||||||
m.state = "Focus"
|
m.state = "Focus"
|
||||||
|
m.startTime = time.Now()
|
||||||
|
|
||||||
// Run onFocusStart commands
|
// Run onFocusStart commands
|
||||||
m.runCommands(m.onFocusStart)
|
m.startCommands(m.onFocusStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, tick()
|
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)
|
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...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,10 +317,10 @@ func (m model) timerScreenView() string {
|
|||||||
m.progressBar.Width = m.width - progressBarPadding
|
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))
|
status := lipgloss.NewStyle().Bold(true).Render(fmt.Sprintf("State: %s", m.state))
|
||||||
intervalInfo := fmt.Sprintf("Interval: %d / %d", m.intervalNum, m.intervals)
|
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)
|
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
|
// Helper to update input focus
|
||||||
func (m *model) updateFocus() {
|
/*
|
||||||
for i := range m.inputs {
|
* func (m *model) updateFocus() {
|
||||||
if i == m.focusIndex {
|
* for i := range m.inputs {
|
||||||
m.inputs[i].Focus()
|
* if i == m.focusIndex {
|
||||||
} else {
|
* m.inputs[i].Focus()
|
||||||
m.inputs[i].Blur()
|
* } else {
|
||||||
}
|
* m.inputs[i].Blur()
|
||||||
}
|
* }
|
||||||
}
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
// Helper to run shell commands
|
// Helper to run shell commands
|
||||||
func (m *model) runCommands(commands []string) {
|
func (m *model) startCommands(commands []string) {
|
||||||
for _, cmdStr := range commands {
|
for _, cmdStr := range commands {
|
||||||
cmd := exec.Command("sh", "-c", cmdStr)
|
if err := m.shellrunner.AddCommand(cmdStr, nil); err != nil {
|
||||||
cmd.Stdout = os.Stdout
|
m.err = fmt.Errorf("error adding command: %w", err)
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf("Error running command: %v\n", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,9 +425,6 @@ func main() {
|
|||||||
return nil
|
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"))
|
m := initialModel(c.Bool("fullscreen"), c.String("color-left"), c.String("color-right"))
|
||||||
|
|
||||||
// Collect command flags
|
// Collect command flags
|
||||||
|
56
main_test.go
56
main_test.go
@ -1,16 +1,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
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.
|
// 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) {
|
func assertEqual(t *testing.T, actual, expected interface{}, msg string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@ -54,37 +60,12 @@ func TestParseDuration(t *testing.T) {
|
|||||||
|
|
||||||
// TestRunCommands tests the runCommands function
|
// TestRunCommands tests the runCommands function
|
||||||
func TestRunCommands(t *testing.T) {
|
func TestRunCommands(t *testing.T) {
|
||||||
r, w, err := os.Pipe()
|
m := initialModel(false, "#ffdd57", "#57ddff")
|
||||||
if err != nil {
|
m.shellrunner.Start()
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect stdout and stderr to the pipe
|
m.startCommands([]string{"echo Hello, World!"})
|
||||||
oldStdout := os.Stdout
|
|
||||||
oldStderr := os.Stderr
|
|
||||||
os.Stdout = w
|
|
||||||
os.Stderr = w
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
m.shellrunner.Stop()
|
||||||
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!'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestInputView tests the Update method of the model for the input view
|
// 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")
|
assertEqual(t, m.err, nil, "Expected no error")
|
||||||
assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil")
|
assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil")
|
||||||
assertEqual(t, m.currentScreen, timerScreen, "Expected currentScreen to be timerScreen")
|
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
|
// Test timer view
|
||||||
m.View()
|
m.View()
|
||||||
@ -136,21 +117,18 @@ func TestInputView(t *testing.T) {
|
|||||||
m = updatedModel.(model)
|
m = updatedModel.(model)
|
||||||
|
|
||||||
assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'")
|
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
|
// Test switch to break time
|
||||||
m.remaining = 0
|
m.startTime = m.startTime.Add(-10 * time.Minute)
|
||||||
updatedModel, _ = m.Update(oneSec)
|
updatedModel, _ = m.Update(oneSec)
|
||||||
m = updatedModel.(model)
|
m = updatedModel.(model)
|
||||||
|
|
||||||
assertEqual(t, m.state, "Break", "Expected state to be 'Break'")
|
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
|
// Switch back to focus time
|
||||||
m.remaining = 0
|
m.startTime = m.startTime.Add(-10 * time.Minute)
|
||||||
updatedModel, _ = m.Update(oneSec)
|
updatedModel, _ = m.Update(oneSec)
|
||||||
m = updatedModel.(model)
|
m = updatedModel.(model)
|
||||||
|
|
||||||
assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'")
|
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