feat: implement state management and input handling for tier system
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/StevanFreeborn/term-tier/internal/core"
|
||||
"github.com/StevanFreeborn/term-tier/internal/styles"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
NavigationMode Mode = iota
|
||||
InputMode
|
||||
HelpMode
|
||||
)
|
||||
|
||||
type State interface {
|
||||
Mode() Mode
|
||||
Tiers() []core.Tier
|
||||
CursorRow() int
|
||||
CursorCol() int
|
||||
Dragging() bool
|
||||
SetStatus(msg string)
|
||||
StatusMsg() string
|
||||
WindowWidth() int
|
||||
WindowHeight() int
|
||||
SetWindowSize(width int, height int)
|
||||
InputTitle() string
|
||||
Input() textinput.Model
|
||||
UpdateInput(msg tea.Msg) tea.Cmd
|
||||
StartInputMode(title string, callback func(string, State) tea.Cmd) tea.Cmd
|
||||
ExitInputMode()
|
||||
SubmitInput() tea.Cmd
|
||||
DeleteItem() tea.Cmd
|
||||
ClampCursor()
|
||||
AddItem() tea.Cmd
|
||||
AddItemToPool(item core.Item)
|
||||
UpdateItem() tea.Cmd
|
||||
UpdateCurrentItem(newItem core.Item)
|
||||
SelectItem()
|
||||
MoveItem()
|
||||
MoveRight()
|
||||
MoveUp()
|
||||
MoveDown()
|
||||
MoveLeft()
|
||||
Save() core.SaveState
|
||||
Load(data core.SaveState)
|
||||
ToggleHelp()
|
||||
}
|
||||
|
||||
type state struct {
|
||||
tiers []core.Tier
|
||||
|
||||
cursorRow int
|
||||
cursorCol int
|
||||
dragging bool
|
||||
SourceRow int
|
||||
SourceCol int
|
||||
dragBuffer core.Item
|
||||
|
||||
windowWidth int
|
||||
windowHeight int
|
||||
|
||||
mode Mode
|
||||
input textinput.Model
|
||||
InputCallback func(string, State) tea.Cmd
|
||||
inputTitle string
|
||||
|
||||
statusMsg string
|
||||
}
|
||||
|
||||
type option func(*state)
|
||||
|
||||
func New(opts ...option) State {
|
||||
ti := textinput.New()
|
||||
ti.CharLimit = 50
|
||||
ti.Width = 30
|
||||
|
||||
state := &state{
|
||||
tiers: []core.Tier{
|
||||
{Name: "S Tier", Color: styles.TierColors[0], Items: []core.Item{}},
|
||||
{Name: "A Tier", Color: styles.TierColors[1], Items: []core.Item{}},
|
||||
{Name: "B Tier", Color: styles.TierColors[2], Items: []core.Item{}},
|
||||
{Name: "C Tier", Color: styles.TierColors[3], Items: []core.Item{}},
|
||||
{Name: "D Tier", Color: styles.TierColors[4], Items: []core.Item{}},
|
||||
{Name: "Pool", Color: styles.TierColors[5], Items: []core.Item{}},
|
||||
},
|
||||
cursorRow: 5,
|
||||
cursorCol: 0,
|
||||
mode: NavigationMode,
|
||||
input: ti,
|
||||
statusMsg: "Ready.",
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(state)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
func WithTiers(tiers []core.Tier) option {
|
||||
return func(s *state) {
|
||||
s.tiers = tiers
|
||||
}
|
||||
}
|
||||
|
||||
func WithCursor(row, col int) option {
|
||||
return func(s *state) {
|
||||
s.cursorRow = row
|
||||
s.cursorCol = col
|
||||
}
|
||||
}
|
||||
|
||||
func WithMode(mode Mode) option {
|
||||
return func(s *state) {
|
||||
s.mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
func WithInput(input textinput.Model, title string) option {
|
||||
return func(s *state) {
|
||||
s.input = input
|
||||
s.inputTitle = title
|
||||
}
|
||||
}
|
||||
|
||||
func WithWindowWidth(width int) option {
|
||||
return func(s *state) {
|
||||
s.windowWidth = width
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) Mode() Mode {
|
||||
return s.mode
|
||||
}
|
||||
|
||||
func (s *state) InputTitle() string {
|
||||
return s.inputTitle
|
||||
}
|
||||
|
||||
func (s *state) Input() textinput.Model {
|
||||
return s.input
|
||||
}
|
||||
|
||||
func (s *state) UpdateInput(msg tea.Msg) tea.Cmd {
|
||||
var cmd tea.Cmd
|
||||
s.input, cmd = s.input.Update(msg)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (s *state) Tiers() []core.Tier {
|
||||
return s.tiers
|
||||
}
|
||||
|
||||
func (s *state) CursorRow() int {
|
||||
return s.cursorRow
|
||||
}
|
||||
|
||||
func (s *state) CursorCol() int {
|
||||
return s.cursorCol
|
||||
}
|
||||
|
||||
func (s *state) Dragging() bool {
|
||||
return s.dragging
|
||||
}
|
||||
|
||||
func (s *state) WindowHeight() int {
|
||||
return s.windowHeight
|
||||
}
|
||||
|
||||
func (s *state) WindowWidth() int {
|
||||
return s.windowWidth
|
||||
}
|
||||
|
||||
func (s *state) SetWindowSize(width int, height int) {
|
||||
s.windowHeight = height
|
||||
s.windowWidth = width
|
||||
}
|
||||
|
||||
func (s *state) StatusMsg() string {
|
||||
return s.statusMsg
|
||||
}
|
||||
|
||||
func (s *state) SetStatus(msg string) {
|
||||
s.statusMsg = msg
|
||||
}
|
||||
|
||||
func (s *state) ToggleHelp() {
|
||||
if s.mode == HelpMode {
|
||||
s.mode = NavigationMode
|
||||
} else {
|
||||
s.mode = HelpMode
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) StartInputMode(title string, callback func(string, State) tea.Cmd) tea.Cmd {
|
||||
s.mode = InputMode
|
||||
s.inputTitle = title
|
||||
s.InputCallback = callback
|
||||
s.input.Reset()
|
||||
s.input.Focus()
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (s *state) ExitInputMode() {
|
||||
s.mode = NavigationMode
|
||||
s.input.Reset()
|
||||
}
|
||||
|
||||
func (s *state) SubmitInput() tea.Cmd {
|
||||
var cmd tea.Cmd
|
||||
|
||||
val := s.input.Value()
|
||||
|
||||
if s.InputCallback != nil {
|
||||
cmd = s.InputCallback(val, s)
|
||||
}
|
||||
|
||||
s.mode = NavigationMode
|
||||
s.input.Reset()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (s *state) DeleteItem() tea.Cmd {
|
||||
tier := &s.tiers[s.cursorRow]
|
||||
|
||||
if len(tier.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tier.Items = append(tier.Items[:s.cursorCol], tier.Items[s.cursorCol+1:]...)
|
||||
s.ClampCursor()
|
||||
s.SetStatus("Item Deleted")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *state) ClampCursor() {
|
||||
rowLen := len(s.tiers[s.cursorRow].Items)
|
||||
if s.cursorCol >= rowLen {
|
||||
if rowLen == 0 {
|
||||
s.cursorCol = 0
|
||||
return
|
||||
}
|
||||
s.cursorCol = rowLen - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) AddItem() tea.Cmd {
|
||||
return s.StartInputMode("Add new item", func(val string, s State) tea.Cmd {
|
||||
if val != "" {
|
||||
s.AddItemToPool(core.Item(val))
|
||||
s.SetStatus("Added: " + val)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *state) AddItemToPool(item core.Item) {
|
||||
poolIndex := len(s.tiers) - 1
|
||||
s.tiers[poolIndex].Items = append(s.tiers[poolIndex].Items, item)
|
||||
}
|
||||
|
||||
func (s *state) UpdateItem() tea.Cmd {
|
||||
if len(s.tiers[s.cursorRow].Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentItem := string(s.tiers[s.cursorRow].Items[s.cursorCol])
|
||||
|
||||
cmd := s.StartInputMode("Rename Item", func(val string, m State) tea.Cmd {
|
||||
if val != "" {
|
||||
m.UpdateCurrentItem(core.Item(val))
|
||||
m.SetStatus("Renamed to: " + val)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
s.input.SetValue(currentItem)
|
||||
|
||||
s.input.CursorEnd()
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (s *state) UpdateCurrentItem(newItem core.Item) {
|
||||
s.tiers[s.cursorRow].Items[s.cursorCol] = newItem
|
||||
}
|
||||
|
||||
func (s *state) SelectItem() {
|
||||
if s.dragging {
|
||||
s.dragging = false
|
||||
} else {
|
||||
if len(s.tiers[s.cursorRow].Items) > 0 {
|
||||
s.dragging = true
|
||||
s.SourceRow = s.cursorRow
|
||||
s.SourceCol = s.cursorCol
|
||||
s.dragBuffer = s.tiers[s.cursorRow].Items[s.cursorCol]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) MoveItem() {
|
||||
sourceRow := s.SourceRow
|
||||
sourceCol := s.SourceCol
|
||||
item := s.dragBuffer
|
||||
|
||||
if sourceRow >= len(s.tiers) || sourceCol >= len(s.tiers[sourceRow].Items) {
|
||||
return
|
||||
}
|
||||
|
||||
oldRow := s.tiers[sourceRow].Items
|
||||
s.tiers[sourceRow].Items = append(oldRow[:sourceCol], oldRow[sourceCol+1:]...)
|
||||
|
||||
targetRow := s.tiers[s.cursorRow].Items
|
||||
|
||||
if s.cursorCol > len(targetRow) {
|
||||
s.cursorCol = len(targetRow)
|
||||
}
|
||||
|
||||
s.SourceRow = s.cursorRow
|
||||
s.SourceCol = s.cursorCol
|
||||
|
||||
newRow := make([]core.Item, len(targetRow)+1)
|
||||
copy(newRow, targetRow[:s.cursorCol])
|
||||
|
||||
newRow[s.cursorCol] = item
|
||||
copy(newRow[s.cursorCol+1:], targetRow[s.cursorCol:])
|
||||
|
||||
s.tiers[s.cursorRow].Items = newRow
|
||||
s.SetStatus(fmt.Sprintf("Moved '%s' from %s to %s", item, s.tiers[sourceRow].Name, s.tiers[s.cursorRow].Name))
|
||||
}
|
||||
|
||||
func (s *state) MoveRight() {
|
||||
limit := len(s.tiers[s.cursorRow].Items) - 1
|
||||
|
||||
if s.dragging {
|
||||
limit = len(s.tiers[s.cursorRow].Items)
|
||||
}
|
||||
|
||||
if s.cursorCol < limit {
|
||||
s.cursorCol++
|
||||
} else if !s.dragging && len(s.tiers[s.cursorRow].Items) == 0 {
|
||||
s.cursorCol = 0
|
||||
}
|
||||
|
||||
if s.dragging {
|
||||
s.MoveItem()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) MoveUp() {
|
||||
if s.cursorRow > 0 {
|
||||
s.cursorRow--
|
||||
s.ClampCursor()
|
||||
|
||||
if s.dragging {
|
||||
s.MoveItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) MoveDown() {
|
||||
if s.cursorRow < len(s.tiers)-1 {
|
||||
s.cursorRow++
|
||||
s.ClampCursor()
|
||||
|
||||
if s.dragging {
|
||||
s.MoveItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) MoveLeft() {
|
||||
if s.cursorCol > 0 {
|
||||
s.cursorCol--
|
||||
|
||||
if s.dragging {
|
||||
s.MoveItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) Save() core.SaveState {
|
||||
state := core.SaveState{}
|
||||
|
||||
for _, t := range s.tiers {
|
||||
state.Tiers = append(state.Tiers, struct {
|
||||
Name string `json:"name"`
|
||||
Items []core.Item `json:"items"`
|
||||
}{Name: t.Name, Items: t.Items})
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
func (s *state) Load(data core.SaveState) {
|
||||
if len(data.Tiers) == len(s.tiers) {
|
||||
for i, savedTier := range data.Tiers {
|
||||
s.tiers[i].Items = savedTier.Items
|
||||
}
|
||||
|
||||
s.cursorRow = 0
|
||||
s.cursorCol = 0
|
||||
s.dragging = false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user