Files
spilltea/internal/ui/home/update.go
T
2026-05-19 14:34:48 +02:00

181 lines
4.1 KiB
Go

package home
import (
crypto "crypto/rand"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/spilltea/internal/keys"
"github.com/anotherhadi/spilltea/internal/ui/components/teapot"
)
type teapotTickMsg struct{}
func teapotTick() tea.Cmd {
return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
return teapotTickMsg{}
})
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ws, ok := msg.(tea.WindowSizeMsg); ok {
m.SetSize(ws.Width, ws.Height)
return m, nil
}
if _, ok := msg.(teapotTickMsg); ok {
frames := teapot.TeapotFrames()
m.teapotFrame = (m.teapotFrame + 1) % len(frames)
return m, teapotTick()
}
if m.mode == modeNaming {
if kp, ok := msg.(tea.KeyPressMsg); ok {
return m.updateNaming(kp)
}
return m, nil
}
if kp, ok := msg.(tea.KeyPressMsg); ok {
if !m.list.SettingFilter() {
if key.Matches(kp, keys.Keys.Global.Quit) {
return m, tea.Quit
}
if key.Matches(kp, keys.Keys.Home.Open) {
return m.handleSelection()
}
if key.Matches(kp, keys.Keys.Home.Delete) {
return m.deleteSelected()
}
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m Model) handleSelection() (tea.Model, tea.Cmd) {
item, ok := m.list.SelectedItem().(listItem)
if !ok {
return m, nil
}
switch item.kind {
case kindNew:
m.mode = modeNaming
m.nameInput.SetValue("")
return m, m.nameInput.Focus()
case kindTemp:
dir := tempDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
return m, nil
}
initProjectFiles(dir)
p := &Project{Name: "temporary", Path: filepath.Join(dir, "data.db")}
return m, func() tea.Msg { return ProjectSelectedMsg{Project: p} }
default:
p := &Project{Name: item.name, Path: item.path}
return m, func() tea.Msg { return ProjectSelectedMsg{Project: p} }
}
}
func (m Model) deleteSelected() (tea.Model, tea.Cmd) {
item, ok := m.list.SelectedItem().(listItem)
if !ok || item.kind != kindExisting {
return m, nil
}
dir := filepath.Dir(item.path) // parent dir of data.db
os.RemoveAll(dir)
idx := m.list.GlobalIndex()
m.list.RemoveItem(idx)
if idx > 0 && idx >= len(m.list.Items()) {
m.list.Select(idx - 1)
}
return m, nil
}
func (m Model) updateNaming(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, keys.Keys.Global.Escape):
m.mode = modeSelect
m.nameInput.Blur()
return m, nil
case msg.String() == "enter":
name := m.nameInput.Value()
if name == "" {
return m, nil
}
m.mode = modeSelect
m.nameInput.Blur()
dir := filepath.Join(m.projectDir, name)
if err := os.MkdirAll(dir, 0o755); err != nil {
return m, nil
}
initProjectFiles(dir)
p := &Project{Name: name, Path: filepath.Join(dir, "data.db")}
return m, func() tea.Msg { return ProjectSelectedMsg{Project: p} }
default:
var cmd tea.Cmd
m.nameInput, cmd = m.nameInput.Update(msg)
m.nameInput.SetValue(sanitizeName(m.nameInput.Value()))
return m, cmd
}
}
func sanitizeName(s string) string {
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
b.WriteRune(r)
}
}
return b.String()
}
func IsValidProjectName(s string) bool {
if s == "tmp" {
return true
}
return s != "" && s == sanitizeName(s)
}
func OpenProject(projectDir, name string) (*Project, error) {
if name == "tmp" {
dir := tempDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
initProjectFiles(dir)
return &Project{Name: "temporary", Path: filepath.Join(dir, "data.db")}, nil
}
dir := filepath.Join(projectDir, name)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
initProjectFiles(dir)
return &Project{Name: name, Path: filepath.Join(dir, "data.db")}, nil
}
func tempDir() string {
b := make([]byte, 4)
_, _ = crypto.Read(b)
return filepath.Join(os.TempDir(), "spilltea", fmt.Sprintf("%08x", b))
}
func initProjectFiles(dir string) {
for _, name := range []string{"data.db", "logs.log"} {
p := filepath.Join(dir, name)
if _, err := os.Stat(p); os.IsNotExist(err) {
f, err := os.Create(p)
if err == nil {
f.Close()
}
}
}
}