Files
eko/internal/client/ui/core/core.go

325 lines
8.2 KiB
Go

package core
import (
"crypto/ed25519"
"fmt"
"log"
"slices"
"time"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/timer"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/kyren223/eko/internal/client/gateway"
"github.com/kyren223/eko/internal/client/ui"
"github.com/kyren223/eko/internal/client/ui/core/chat"
"github.com/kyren223/eko/internal/client/ui/core/frequencycreation"
"github.com/kyren223/eko/internal/client/ui/core/frequencylist"
"github.com/kyren223/eko/internal/client/ui/core/networkcreation"
"github.com/kyren223/eko/internal/client/ui/core/networklist"
"github.com/kyren223/eko/internal/client/ui/core/state"
"github.com/kyren223/eko/internal/client/ui/loadscreen"
"github.com/kyren223/eko/internal/data"
"github.com/kyren223/eko/internal/packet"
"github.com/kyren223/eko/pkg/assert"
"github.com/kyren223/eko/pkg/snowflake"
)
var (
connectingToServer = "Connecting to server.."
connectionFailed = "Connection failed - retrying in %d sec..."
connectionTimeout = 5 * time.Second
initialTimeout = 3750 * time.Millisecond
timerInterval = 50 * time.Millisecond
)
const (
FocusNetworkList = iota
FocusFrequencyList
FocusChat
FocusMax
)
type Model struct {
name string
privKey ed25519.PrivateKey
id snowflake.ID
loading loadscreen.Model
timer timer.Model
timeout time.Duration
connected bool
networkCreationPopup *networkcreation.Model
frequencyCreationPopup *frequencycreation.Model
networkList networklist.Model
frequencyList frequencylist.Model
chat chat.Model
focus int
}
func New(privKey ed25519.PrivateKey, name string) Model {
m := Model{
name: name,
privKey: privKey,
id: 0,
loading: loadscreen.New(connectingToServer),
timer: newTimer(initialTimeout),
timeout: initialTimeout,
connected: false,
networkCreationPopup: nil,
frequencyCreationPopup: nil,
networkList: networklist.New(),
frequencyList: frequencylist.New(),
chat: chat.New(),
focus: FocusNetworkList,
}
m.move(0) // Update focus
return m
}
func (m Model) Init() tea.Cmd {
return tea.Batch(gateway.Connect(m.privKey, connectionTimeout), m.loading.Init())
}
func (m Model) View() string {
if !m.connected {
return m.loading.View()
}
networkList := m.networkList.View()
frequencyList := m.frequencyList.View()
chat := m.chat.View()
result := lipgloss.JoinHorizontal(lipgloss.Top, networkList, frequencyList, chat)
result = lipgloss.Place(
ui.Width, ui.Height,
lipgloss.Left, lipgloss.Top,
result,
)
var popup string
if m.networkCreationPopup != nil {
popup = m.networkCreationPopup.View()
} else if m.frequencyCreationPopup != nil {
popup = m.frequencyCreationPopup.View()
}
if popup != "" {
x := (ui.Width - lipgloss.Width(popup)) / 2
y := (ui.Height - lipgloss.Height(popup)) / 2
result = ui.PlaceOverlay(x, y, popup, result)
}
return result
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.connected {
cmd := m.updateConnected(msg)
return m, cmd
} else {
cmd := m.updateNotConnected(msg)
return m, cmd
}
}
func (m *Model) updateNotConnected(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case gateway.ConnectionEstablished:
m.id = snowflake.ID(msg)
m.connected = true
m.timeout = initialTimeout
return m.timer.Stop()
case gateway.ConnectionFailed:
m.timer = newTimer(m.timeout)
m.updateLoadScreenContent()
return m.timer.Start()
case timer.TimeoutMsg:
m.timeout = min(m.timeout*2, time.Minute)
m.loading.SetContent(connectingToServer)
return gateway.Connect(m.privKey, connectionTimeout)
case timer.StartStopMsg:
var cmd tea.Cmd
m.timer, cmd = m.timer.Update(msg)
return cmd
case timer.TickMsg:
m.updateLoadScreenContent()
var cmd tea.Cmd
m.timer, cmd = m.timer.Update(msg)
return cmd
case spinner.TickMsg:
var loadscreenCmd tea.Cmd
m.loading, loadscreenCmd = m.loading.Update(msg)
return loadscreenCmd
default:
return nil
}
}
func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case ui.QuitMsg:
gateway.Disconnect()
case gateway.ConnectionLost:
m.connected = false
m.timeout = initialTimeout
return tea.Batch(gateway.Connect(m.privKey, connectionTimeout), m.loading.Init())
case *packet.NetworksInfo:
if msg.Set {
state.State.Networks = msg.Networks
} else {
networks := state.State.Networks
networks = append(networks, msg.Networks...)
networks = slices.DeleteFunc(networks, func(network packet.FullNetwork) bool {
return slices.Contains(msg.RemoveNetworks, network.ID)
})
slices.SortFunc(networks, func(a, b packet.FullNetwork) int {
return a.Position - b.Position
})
log.Println(networks)
state.State.Networks = networks
}
case *packet.FrequenciesInfo:
var network *packet.FullNetwork
for i, fullNetwork := range state.State.Networks {
if fullNetwork.ID == msg.Network {
network = &state.State.Networks[i]
}
}
if msg.Set {
network.Frequencies = msg.Frequencies
} else {
frequencies := network.Frequencies
frequencies = append(frequencies, msg.Frequencies...)
frequencies = slices.DeleteFunc(frequencies, func(frequency data.Frequency) bool {
return slices.Contains(msg.RemoveFrequencies, frequency.ID)
})
slices.SortFunc(frequencies, func(a, b data.Frequency) int {
return int(a.Position - b.Position)
})
log.Println(frequencies)
network.Frequencies = frequencies
}
case tea.KeyMsg:
key := msg.Type
switch key {
case tea.KeyCtrlN:
switch m.focus {
case FocusNetworkList:
if m.networkCreationPopup == nil {
popup := networkcreation.New()
m.networkCreationPopup = &popup
}
case FocusFrequencyList:
if m.frequencyCreationPopup == nil {
index := m.networkList.Index()
network := state.State.Networks[index]
popup := frequencycreation.New(network.ID)
m.frequencyCreationPopup = &popup
}
}
case tea.KeyEscape:
if m.networkCreationPopup != nil {
m.networkCreationPopup = nil
} else if m.frequencyCreationPopup != nil {
m.frequencyCreationPopup = nil
}
case tea.KeyEnter:
if m.networkCreationPopup != nil {
cmd := m.networkCreationPopup.Select()
if cmd != nil {
m.networkCreationPopup = nil
}
return cmd
} else if m.frequencyCreationPopup != nil {
cmd := m.frequencyCreationPopup.Select()
if cmd != nil {
m.frequencyCreationPopup = nil
}
return cmd
}
default:
if m.networkCreationPopup != nil {
popup, cmd := m.networkCreationPopup.Update(msg)
m.networkCreationPopup = &popup
return cmd
} else if m.frequencyCreationPopup != nil {
popup, cmd := m.frequencyCreationPopup.Update(msg)
m.frequencyCreationPopup = &popup
return cmd
}
left := msg.String() == "H"
right := msg.String() == "L"
direction := 0
if left {
direction = -1
} else if right {
direction = 1
}
m.move(direction)
}
}
inPopup := m.networkCreationPopup != nil || m.frequencyCreationPopup != nil
if inPopup {
return nil
}
var cmd tea.Cmd
switch m.focus {
case FocusNetworkList:
m.networkList, cmd = m.networkList.Update(msg)
m.frequencyList.Set(m.networkList.Index())
case FocusFrequencyList:
m.frequencyList, cmd = m.frequencyList.Update(msg)
case FocusChat:
m.chat, cmd = m.chat.Update(msg)
}
return cmd
}
func (m *Model) updateLoadScreenContent() {
seconds := m.timer.Timeout.Round(time.Second) / time.Second
m.loading.SetContent(fmt.Sprintf(connectionFailed, seconds))
}
func newTimer(timeout time.Duration) timer.Model {
return timer.NewWithInterval(timeout.Truncate(time.Second)+(time.Second/2), timerInterval)
}
func (m *Model) move(direction int) {
focus := m.focus + direction
m.focus = max(0, min(FocusMax-1, focus))
m.networkList.Blur()
m.frequencyList.Blur()
m.chat.Blur()
switch m.focus {
case FocusNetworkList:
m.networkList.Focus()
case FocusFrequencyList:
m.frequencyList.Focus()
case FocusChat:
m.chat.Focus()
default:
assert.Never("missing switch statement field in move", "focus", m.focus)
}
}