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 } }