feat: use bubbletea framework
- implement TUI in bubbletea - split components into separate files - remove now unused functions - restructure files
This commit is contained in:
parent
9656db5335
commit
8a0ef32c91
14 changed files with 269 additions and 187 deletions
92
cmd/client/app.go
Normal file
92
cmd/client/app.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/charmbracelet/log"
|
||||
|
||||
"git.smsvc.net/pomodoro/ChronoTomato/internal/helper"
|
||||
"git.smsvc.net/pomodoro/ChronoTomato/internal/websocket"
|
||||
ChronoTomato "git.smsvc.net/pomodoro/ChronoTomato/pkg/models"
|
||||
GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
var (
|
||||
config ChronoTomato.Config
|
||||
client websocket.Client
|
||||
)
|
||||
|
||||
type app struct {
|
||||
keys keyMap
|
||||
help help.Model
|
||||
channel chan GoTomato.ServerMessage
|
||||
pomodoro GoTomato.ServerMessage
|
||||
}
|
||||
|
||||
func myApp() app {
|
||||
return app{
|
||||
keys: keys,
|
||||
help: help.New(),
|
||||
channel: make(chan GoTomato.ServerMessage),
|
||||
pomodoro: GoTomato.ServerMessage{},
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for channel signal and return the ServerMessage as a tea.Msg.
|
||||
// Return tea.Quit() if the channel has been closed.
|
||||
// Excapsulate all this in a tea.Cmd() because bubbletea demands it.
|
||||
func (a app) waitForChannelSignal() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
content, open := <-a.channel
|
||||
if !open {
|
||||
return tea.Quit()
|
||||
}
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
func (a app) Init() tea.Cmd {
|
||||
client = websocket.Connect(config.URL)
|
||||
client.Password = config.Password
|
||||
go client.ProcessServerMessages(a.channel)
|
||||
|
||||
return tea.Batch(
|
||||
tea.ClearScreen,
|
||||
a.waitForChannelSignal(),
|
||||
)
|
||||
}
|
||||
|
||||
func Start() {
|
||||
var (
|
||||
defaultConfigFile = "~/.config/ChronoTomato.yml"
|
||||
|
||||
parameter_url = flag.String("url", "", "GoTomato Server URL (eg ws://localhost:8080/ws)")
|
||||
parameter_password = flag.String("password", "", "Control password for pomodoro session")
|
||||
configfile = flag.String("config", "", "Path to config file")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// read passed config file or try to use default config
|
||||
if *configfile != "" {
|
||||
config = helper.ParseConfig(*configfile)
|
||||
} else {
|
||||
if helper.FileExists(defaultConfigFile) {
|
||||
config = helper.ParseConfig(defaultConfigFile)
|
||||
}
|
||||
}
|
||||
|
||||
// cli parameters always supersede config file
|
||||
if *parameter_url != "" {
|
||||
config.URL = *parameter_url
|
||||
}
|
||||
if *parameter_password != "" {
|
||||
config.Password = *parameter_password
|
||||
}
|
||||
|
||||
_, err := tea.NewProgram(myApp()).Run()
|
||||
if err != nil {
|
||||
log.Fatal("Could not start program:", "msg", err)
|
||||
}
|
||||
}
|
39
cmd/client/helper/notification.go
Normal file
39
cmd/client/helper/notification.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gen2brain/beeep"
|
||||
|
||||
GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||
)
|
||||
|
||||
// Send desktop notifications on the start of each segment
|
||||
func DesktopNotifications(pomodoro GoTomato.ServerMessage) {
|
||||
var (
|
||||
duration int
|
||||
notification string
|
||||
)
|
||||
|
||||
mode := pomodoro.Mode
|
||||
session := pomodoro.Session
|
||||
sessions := pomodoro.Settings.Sessions
|
||||
|
||||
switch mode {
|
||||
case "Work":
|
||||
duration = pomodoro.Settings.Work
|
||||
notification = fmt.Sprintf("Session %d/%d: %s %0.f minutes", session, sessions, mode, float32(duration)/60)
|
||||
case "ShortBreak":
|
||||
duration = pomodoro.Settings.ShortBreak
|
||||
notification = fmt.Sprintf("Session %d/%d: Take a %0.f minute break", session, sessions, float32(duration)/60)
|
||||
case "LongBreak":
|
||||
duration = pomodoro.Settings.LongBreak
|
||||
notification = fmt.Sprintf("Long Break: Take a %0.f minute break", float32(duration)/60)
|
||||
case "End":
|
||||
duration = 0
|
||||
notification = fmt.Sprintf("Pomodoro sessions complete! Great job! 🎉")
|
||||
}
|
||||
|
||||
if pomodoro.TimeLeft == duration { // start of segment
|
||||
beeep.Alert("🍅 Pomodoro Timer", notification, "")
|
||||
}
|
||||
}
|
51
cmd/client/helper/terminal.go
Normal file
51
cmd/client/helper/terminal.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/alecthomas/colour"
|
||||
|
||||
GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||
)
|
||||
|
||||
// Return terminal output based on the passed ServerMessage
|
||||
func TerminalOutput(pomodoro GoTomato.ServerMessage) string {
|
||||
var (
|
||||
output string
|
||||
|
||||
modePrefix string
|
||||
timerOutput string
|
||||
)
|
||||
|
||||
// header
|
||||
output += colour.Sprintf("^D^4Work: %d ◊ Break: %d ◊ Longbreak: %d^R\n\n",
|
||||
pomodoro.Settings.Work/60,
|
||||
pomodoro.Settings.ShortBreak/60,
|
||||
pomodoro.Settings.LongBreak/60,
|
||||
)
|
||||
|
||||
//body
|
||||
switch pomodoro.Mode {
|
||||
case "Idle":
|
||||
modePrefix = " "
|
||||
timerOutput = ""
|
||||
default:
|
||||
modePrefix = " "
|
||||
if pomodoro.Paused {
|
||||
modePrefix = " "
|
||||
}
|
||||
minutes := pomodoro.TimeLeft / 60
|
||||
seconds := pomodoro.TimeLeft % 60
|
||||
|
||||
timerOutput = fmt.Sprintf("⏳ %02d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
output += fmt.Sprintf("Session: %d/%d\n",
|
||||
pomodoro.Session,
|
||||
pomodoro.Settings.Sessions,
|
||||
)
|
||||
output += fmt.Sprintf("%s %s\n", modePrefix, pomodoro.Mode)
|
||||
output += fmt.Sprintf(timerOutput)
|
||||
|
||||
return output
|
||||
}
|
44
cmd/client/keys.go
Normal file
44
cmd/client/keys.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package client
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
// keyMap defines a set of keybindings. To work for help it must satisfy
|
||||
// key.Map. It could also very easily be a map[string]key.Binding.
|
||||
type keyMap struct {
|
||||
Start key.Binding
|
||||
Stop key.Binding
|
||||
Reset key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
Start: key.NewBinding(
|
||||
key.WithKeys(" "),
|
||||
key.WithHelp("space", "start/pause/resume"),
|
||||
),
|
||||
Stop: key.NewBinding(
|
||||
key.WithKeys("s"),
|
||||
key.WithHelp("s", "stop"),
|
||||
),
|
||||
Reset: key.NewBinding(
|
||||
key.WithKeys("r"),
|
||||
key.WithHelp("r", "reset"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q"),
|
||||
key.WithHelp("q", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
func (k keyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{
|
||||
keys.Start,
|
||||
keys.Stop,
|
||||
keys.Reset,
|
||||
keys.Quit,
|
||||
}
|
||||
}
|
||||
|
||||
func (k keyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"atomicgo.dev/cursor"
|
||||
"flag"
|
||||
|
||||
"git.smsvc.net/pomodoro/ChronoTomato/internal/frontend"
|
||||
"git.smsvc.net/pomodoro/ChronoTomato/internal/helper"
|
||||
"git.smsvc.net/pomodoro/ChronoTomato/internal/websocket"
|
||||
|
||||
ChronoTomato "git.smsvc.net/pomodoro/ChronoTomato/pkg/models"
|
||||
GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||
)
|
||||
|
||||
var (
|
||||
config ChronoTomato.Config
|
||||
|
||||
defaultConfigFile = "~/.config/ChronoTomato.yml"
|
||||
|
||||
parameter_url = flag.String("url", "", "GoTomato Server URL (eg ws://localhost:8080/ws)")
|
||||
parameter_password = flag.String("password", "", "Control password for pomodoro session")
|
||||
configfile = flag.String("config", "", "Path to config file")
|
||||
)
|
||||
|
||||
func Start() {
|
||||
flag.Parse()
|
||||
|
||||
// show/hide cursor for nicer interface
|
||||
cursor.Hide()
|
||||
defer cursor.Show()
|
||||
|
||||
// read passed config file or try to use default config
|
||||
if *configfile != "" {
|
||||
config = helper.ParseConfig(*configfile)
|
||||
} else {
|
||||
if helper.FileExists(defaultConfigFile) {
|
||||
config = helper.ParseConfig(defaultConfigFile)
|
||||
}
|
||||
}
|
||||
|
||||
// cli parameters always supersede config file
|
||||
if *parameter_url != "" {
|
||||
config.URL = *parameter_url
|
||||
}
|
||||
if *parameter_password != "" {
|
||||
config.Password = *parameter_password
|
||||
}
|
||||
|
||||
// Create a client
|
||||
client := websocket.Connect(config.URL)
|
||||
client.Password = config.Password
|
||||
|
||||
// Receive server messages und update the terminal
|
||||
channel := make(chan GoTomato.ServerMessage)
|
||||
go client.ProcessServerMessages(channel)
|
||||
frontend.UpdateLoop(client, config, channel)
|
||||
|
||||
// disconnect from server
|
||||
client.Disconnect()
|
||||
}
|
45
cmd/client/update.go
Normal file
45
cmd/client/update.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// sends start/pause/resume based on the state of the pomodoro
|
||||
func start_pause_resume(message GoTomato.ServerMessage) string {
|
||||
if !message.Ongoing {
|
||||
return "start"
|
||||
}
|
||||
if message.Paused {
|
||||
return "resume"
|
||||
} else {
|
||||
return "pause"
|
||||
}
|
||||
}
|
||||
|
||||
func (a app) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case GoTomato.ServerMessage:
|
||||
a.pomodoro = msg
|
||||
return a, a.waitForChannelSignal()
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, a.keys.Start):
|
||||
cmd := start_pause_resume(a.pomodoro)
|
||||
client.SendCmd(cmd)
|
||||
case key.Matches(msg, a.keys.Stop):
|
||||
client.SendCmd("stop")
|
||||
case key.Matches(msg, a.keys.Reset):
|
||||
if config.PomodoroConfig != (GoTomato.PomodoroConfig{}) {
|
||||
client.SendSettingsUpdate(config.PomodoroConfig)
|
||||
}
|
||||
case key.Matches(msg, a.keys.Quit):
|
||||
client.Disconnect()
|
||||
return a, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
24
cmd/client/view.go
Normal file
24
cmd/client/view.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.smsvc.net/pomodoro/ChronoTomato/cmd/client/helper"
|
||||
GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||
)
|
||||
|
||||
func (a app) View() string {
|
||||
var body string
|
||||
|
||||
if a.pomodoro != (GoTomato.ServerMessage{}) {
|
||||
body = helper.TerminalOutput(a.pomodoro)
|
||||
helper.DesktopNotifications(a.pomodoro)
|
||||
} else {
|
||||
body = "Waiting for first server message..."
|
||||
}
|
||||
|
||||
helpView := a.help.View(a.keys)
|
||||
height := 8 - strings.Count(body, "\n") - strings.Count(helpView, "\n")
|
||||
|
||||
return body + strings.Repeat("\n", height) + helpView
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue