Compare commits
2 Commits
b7ddd54cf6
...
0476f1e227
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0476f1e227 | ||
|
|
1187ae0046 |
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -13,6 +14,16 @@ import (
|
||||
|
||||
const version = "0.1.0"
|
||||
|
||||
// TickMsg is sent for transition animation frames
|
||||
type TickMsg time.Time
|
||||
|
||||
// Transition settings
|
||||
const (
|
||||
transitionDuration = 200 * time.Millisecond
|
||||
transitionFPS = 60
|
||||
)
|
||||
|
||||
|
||||
var (
|
||||
styleTitle = lipgloss.NewStyle().Foreground(lipgloss.Color("62")).Bold(true)
|
||||
styleSubtitle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||
@@ -22,6 +33,13 @@ var (
|
||||
)
|
||||
|
||||
type model struct {
|
||||
transitionFrom screens.Screen
|
||||
transitionTo screens.Screen
|
||||
transitionProgress float64
|
||||
transitionAnimating bool
|
||||
transitionStartTime time.Time
|
||||
transitionType screens.TransitionType
|
||||
transitionDuration time.Duration
|
||||
currentScreen screens.Screen
|
||||
previousScreen screens.Screen
|
||||
quitting bool
|
||||
@@ -38,6 +56,8 @@ func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Handle initialization on first update after Init
|
||||
if !m.initialized {
|
||||
@@ -46,92 +66,114 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.currentScreen.Init()
|
||||
}
|
||||
|
||||
// Handle transition animation updates
|
||||
if m.transitionAnimating {
|
||||
switch msg.(type) {
|
||||
case TickMsg:
|
||||
// Update transition progress
|
||||
elapsed := time.Since(m.transitionStartTime)
|
||||
m.transitionProgress = float64(elapsed) / float64(m.transitionDuration)
|
||||
|
||||
// Ease-out cubic function for smoother animation
|
||||
easeProgress := 1 - ((1 - m.transitionProgress) * (1 - m.transitionProgress) * (1 - m.transitionProgress))
|
||||
|
||||
if m.transitionProgress >= 1 || easeProgress >= 1 {
|
||||
// Transition complete
|
||||
m.transitionAnimating = false
|
||||
m.currentScreen = m.transitionTo
|
||||
m.previousScreen = m.transitionFrom
|
||||
m.transitionFrom = nil
|
||||
m.transitionTo = nil
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Continue animation
|
||||
return m, tea.Tick(time.Second/time.Duration(transitionFPS), func(t time.Time) tea.Msg {
|
||||
return TickMsg(t)
|
||||
})
|
||||
}
|
||||
// During transition, don't process other messages
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
// Only quit on 'q' or ctrl+c when on list screen
|
||||
// Other screens handle their own navigation keys
|
||||
if msg.String() == "q" || msg.String() == "ctrl+c" {
|
||||
if _, ok := m.currentScreen.(*screens.ListScreen); ok {
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
// For other screens, let them handle the key
|
||||
}
|
||||
switch msg.String() {
|
||||
case "?":
|
||||
// Switch to help screen
|
||||
m.previousScreen = m.currentScreen
|
||||
m.currentScreen = screens.NewHelpScreen(m.previousScreen)
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewHelpScreen(m.previousScreen), screens.TransitionFade)
|
||||
return m, cmd
|
||||
case "l":
|
||||
// Switch to list screen
|
||||
m.currentScreen = screens.NewListScreen()
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewListScreen(), screens.TransitionFade)
|
||||
return m, cmd
|
||||
case "a":
|
||||
// Switch to add screen
|
||||
m.previousScreen = m.currentScreen
|
||||
m.currentScreen = screens.NewAddScreen()
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewAddScreen(), screens.TransitionSlideLeft)
|
||||
return m, cmd
|
||||
}
|
||||
case screens.ClientSelectedMsg:
|
||||
// User selected a client - show detail screen
|
||||
m.previousScreen = m.currentScreen
|
||||
m.currentScreen = screens.NewDetailScreen(msg.Client.Client)
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewDetailScreen(msg.Client.Client), screens.TransitionSlideLeft)
|
||||
return m, cmd
|
||||
case screens.ClientDeletedMsg:
|
||||
// Client was deleted - show success message and return to list
|
||||
m.currentScreen = screens.NewListScreen()
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewListScreen(), screens.TransitionFade)
|
||||
return m, cmd
|
||||
case screens.ClientCreatedMsg:
|
||||
// Client was created - return to list screen
|
||||
m.currentScreen = screens.NewListScreen()
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewListScreen(), screens.TransitionFade)
|
||||
return m, cmd
|
||||
case screens.RestoreCompletedMsg:
|
||||
// Restore completed - return to list screen to refresh clients
|
||||
m.currentScreen = screens.NewListScreen()
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewListScreen(), screens.TransitionFade)
|
||||
return m, cmd
|
||||
case screens.CloseDetailScreenMsg:
|
||||
// Detail screen closed - go back to previous screen
|
||||
if m.previousScreen != nil {
|
||||
m.currentScreen = m.previousScreen
|
||||
m.previousScreen = nil
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(m.previousScreen, screens.TransitionSlideRight)
|
||||
return m, cmd
|
||||
}
|
||||
m.currentScreen = screens.NewListScreen()
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(screens.NewListScreen(), screens.TransitionFade)
|
||||
return m, cmd
|
||||
case screens.ErrMsg:
|
||||
// An error occurred - show error screen
|
||||
m.previousScreen = m.currentScreen
|
||||
m.errorScreen = screens.NewErrorScreen(msg.Err)
|
||||
m.currentScreen = m.errorScreen
|
||||
return m, m.currentScreen.Init()
|
||||
cmd := m.startTransition(m.errorScreen, screens.TransitionFade)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// Pass messages to current screen
|
||||
if m.currentScreen != nil {
|
||||
newScreen, cmd := m.currentScreen.Update(msg)
|
||||
// If screen returns nil, go back to previous screen
|
||||
if newScreen == nil {
|
||||
if m.previousScreen != nil {
|
||||
m.currentScreen = m.previousScreen
|
||||
m.previousScreen = nil
|
||||
transitionCmd := m.startTransition(m.previousScreen, screens.TransitionSlideRight)
|
||||
return m, tea.Batch(cmd, transitionCmd)
|
||||
}
|
||||
} else if newScreen != m.currentScreen {
|
||||
// Screen is switching to a different screen
|
||||
m.previousScreen = m.currentScreen
|
||||
m.currentScreen = newScreen
|
||||
transitionCmd := m.startTransition(newScreen, screens.TransitionSlideLeft)
|
||||
return m, tea.Batch(cmd, transitionCmd)
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return "\nGoodbye!\n"
|
||||
}
|
||||
|
||||
// If transitioning, render the transition animation
|
||||
if m.transitionAnimating {
|
||||
return renderTransition(m)
|
||||
}
|
||||
|
||||
if m.currentScreen != nil && m.initialized {
|
||||
return m.currentScreen.View()
|
||||
}
|
||||
@@ -142,6 +184,7 @@ func (m model) View() string {
|
||||
|
||||
return title + " " + subtitle + "\n\nInitializing...\n\n" + bar
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
p := tea.NewProgram(model{}, tea.WithAltScreen())
|
||||
@@ -150,3 +193,193 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// startTransition begins a screen transition animation
|
||||
func (m *model) startTransition(toScreen screens.Screen, ttype screens.TransitionType) tea.Cmd {
|
||||
// Initialize new screen
|
||||
var initCmd tea.Cmd
|
||||
if toScreen != nil {
|
||||
initCmd = toScreen.Init()
|
||||
}
|
||||
|
||||
// Set up transition
|
||||
m.transitionFrom = m.currentScreen
|
||||
m.transitionTo = toScreen
|
||||
m.transitionType = ttype
|
||||
m.transitionDuration = transitionDuration
|
||||
m.transitionProgress = 0
|
||||
m.transitionAnimating = true
|
||||
m.transitionStartTime = time.Now()
|
||||
|
||||
// Return initialization command and tick command
|
||||
tickCmd := tea.Tick(time.Second/time.Duration(transitionFPS), func(t time.Time) tea.Msg {
|
||||
return TickMsg(t)
|
||||
})
|
||||
|
||||
if initCmd != nil {
|
||||
return tea.Batch(initCmd, tickCmd)
|
||||
}
|
||||
return tickCmd
|
||||
}
|
||||
|
||||
func renderTransition(m model) string {
|
||||
if m.transitionFrom == nil || m.transitionTo == nil {
|
||||
return m.transitionTo.View()
|
||||
}
|
||||
|
||||
fromView := m.transitionFrom.View()
|
||||
toView := m.transitionTo.View()
|
||||
|
||||
// Apply easing to progress
|
||||
easeProgress := 1 - ((1 - m.transitionProgress) * (1 - m.transitionProgress) * (1 - m.transitionProgress))
|
||||
|
||||
switch m.transitionType {
|
||||
case screens.TransitionFade:
|
||||
return renderFadeTransition(fromView, toView, easeProgress)
|
||||
case screens.TransitionSlideLeft:
|
||||
return renderSlideTransition(fromView, toView, easeProgress, true)
|
||||
case screens.TransitionSlideRight:
|
||||
return renderSlideTransition(fromView, toView, easeProgress, false)
|
||||
default:
|
||||
return toView
|
||||
}
|
||||
}
|
||||
|
||||
func renderFadeTransition(fromView, toView string, progress float64) string {
|
||||
fromLines := splitLines(fromView)
|
||||
toLines := splitLines(toView)
|
||||
|
||||
maxLines := max(len(fromLines), len(toLines))
|
||||
for len(fromLines) < maxLines {
|
||||
fromLines = append(fromLines, "")
|
||||
}
|
||||
for len(toLines) < maxLines {
|
||||
toLines = append(toLines, "")
|
||||
}
|
||||
|
||||
var result []string
|
||||
for i := 0; i < maxLines; i++ {
|
||||
threshold := progress * float64(maxLines)
|
||||
if float64(i) < threshold {
|
||||
result = append(result, toLines[i])
|
||||
} else {
|
||||
result = append(result, fromLines[i])
|
||||
}
|
||||
}
|
||||
|
||||
return joinLines(result)
|
||||
}
|
||||
|
||||
func renderSlideTransition(fromView, toView string, progress float64, slideLeft bool) string {
|
||||
fromLines := splitLines(fromView)
|
||||
toLines := splitLines(toView)
|
||||
|
||||
width := 80
|
||||
|
||||
for i := range fromLines {
|
||||
fromLines[i] = padLine(fromLines[i], width)
|
||||
}
|
||||
for i := range toLines {
|
||||
toLines[i] = padLine(toLines[i], width)
|
||||
}
|
||||
|
||||
offset := int(float64(width) * progress)
|
||||
|
||||
var result []string
|
||||
maxLines := max(len(fromLines), len(toLines))
|
||||
|
||||
for i := 0; i < maxLines; i++ {
|
||||
var fromLine, toLine string
|
||||
if i < len(fromLines) {
|
||||
fromLine = fromLines[i]
|
||||
} else {
|
||||
fromLine = ""
|
||||
}
|
||||
if i < len(toLines) {
|
||||
toLine = toLines[i]
|
||||
} else {
|
||||
toLine = ""
|
||||
}
|
||||
|
||||
if slideLeft {
|
||||
fromOffset := -offset
|
||||
toOffset := width - offset
|
||||
combined := combineSlidingLines(fromLine, toLine, fromOffset, toOffset, width)
|
||||
result = append(result, combined)
|
||||
} else {
|
||||
fromOffset := offset
|
||||
toOffset := -width + offset
|
||||
combined := combineSlidingLines(fromLine, toLine, fromOffset, toOffset, width)
|
||||
result = append(result, combined)
|
||||
}
|
||||
}
|
||||
|
||||
return joinLines(result)
|
||||
}
|
||||
|
||||
func combineSlidingLines(from, to string, fromOffset, toOffset, width int) string {
|
||||
result := make([]byte, width)
|
||||
for i := range result {
|
||||
result[i] = ' '
|
||||
}
|
||||
|
||||
if fromOffset >= -len(from) && fromOffset < width {
|
||||
for i := 0; i < len(from) && i+fromOffset < width && i+fromOffset >= 0; i++ {
|
||||
if i+fromOffset >= 0 && i+fromOffset < width {
|
||||
result[i+fromOffset] = from[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if toOffset >= -len(to) && toOffset < width {
|
||||
for i := 0; i < len(to) && i+toOffset < width && i+toOffset >= 0; i++ {
|
||||
if i+toOffset >= 0 && i+toOffset < width {
|
||||
result[i+toOffset] = to[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
lines := make([]string, 0)
|
||||
current := ""
|
||||
for _, c := range s {
|
||||
if c == '\n' {
|
||||
lines = append(lines, current)
|
||||
current = ""
|
||||
} else {
|
||||
current += string(c)
|
||||
}
|
||||
}
|
||||
if current != "" {
|
||||
lines = append(lines, current)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func joinLines(lines []string) string {
|
||||
result := ""
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
result += "\n"
|
||||
}
|
||||
result += line
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func padLine(line string, width int) string {
|
||||
if len(line) >= width {
|
||||
return line[:width]
|
||||
}
|
||||
return fmt.Sprintf("%-*s", width, line)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -4,6 +4,16 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// TransitionType defines the type of transition animation
|
||||
type TransitionType int
|
||||
|
||||
const (
|
||||
TransitionNone TransitionType = iota
|
||||
TransitionFade
|
||||
TransitionSlideLeft
|
||||
TransitionSlideRight
|
||||
)
|
||||
|
||||
// Screen represents a UI screen (list, add, detail, etc.)
|
||||
type Screen interface {
|
||||
Init() tea.Cmd
|
||||
|
||||
Reference in New Issue
Block a user