mirror of
https://github.com/lukaszraczylo/lolcathost.git
synced 2026-07-05 04:05:07 +00:00
Initial commit.
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
// Package tui provides the preset picker component.
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lukaszraczylo/lolcathost/internal/protocol"
|
||||
)
|
||||
|
||||
// PresetMode represents the preset view mode.
|
||||
type PresetMode int
|
||||
|
||||
const (
|
||||
PresetModeSelect PresetMode = iota
|
||||
PresetModeAdd
|
||||
PresetModeEdit
|
||||
PresetModeConfirmDelete
|
||||
)
|
||||
|
||||
// PresetFormField represents a form field index.
|
||||
type PresetFormField int
|
||||
|
||||
const (
|
||||
PresetFieldName PresetFormField = iota
|
||||
PresetFieldEnable
|
||||
PresetFieldDisable
|
||||
PresetFieldCount
|
||||
)
|
||||
|
||||
// PresetPicker handles the preset selection and management UI.
|
||||
type PresetPicker struct {
|
||||
presets []protocol.PresetInfo
|
||||
cursor int
|
||||
width int
|
||||
height int
|
||||
mode PresetMode
|
||||
fields []textinput.Model
|
||||
focus PresetFormField
|
||||
editName string // Original name when editing
|
||||
}
|
||||
|
||||
// NewPresetPicker creates a new preset picker.
|
||||
func NewPresetPicker() *PresetPicker {
|
||||
fields := make([]textinput.Model, PresetFieldCount)
|
||||
|
||||
// Name field
|
||||
fields[PresetFieldName] = textinput.New()
|
||||
fields[PresetFieldName].Placeholder = "preset-name"
|
||||
fields[PresetFieldName].CharLimit = 63
|
||||
|
||||
// Enable field
|
||||
fields[PresetFieldEnable] = textinput.New()
|
||||
fields[PresetFieldEnable].Placeholder = "alias1,alias2,alias3"
|
||||
fields[PresetFieldEnable].CharLimit = 500
|
||||
|
||||
// Disable field
|
||||
fields[PresetFieldDisable] = textinput.New()
|
||||
fields[PresetFieldDisable].Placeholder = "alias1,alias2,alias3"
|
||||
fields[PresetFieldDisable].CharLimit = 500
|
||||
|
||||
return &PresetPicker{
|
||||
fields: fields,
|
||||
mode: PresetModeSelect,
|
||||
}
|
||||
}
|
||||
|
||||
// SetPresets updates the available presets (legacy method for compatibility).
|
||||
func (p *PresetPicker) SetPresets(presets []string) {
|
||||
p.presets = make([]protocol.PresetInfo, len(presets))
|
||||
for i, name := range presets {
|
||||
p.presets[i] = protocol.PresetInfo{Name: name}
|
||||
}
|
||||
if p.cursor >= len(presets) {
|
||||
p.cursor = max(0, len(presets)-1)
|
||||
}
|
||||
}
|
||||
|
||||
// SetPresetsWithInfo updates the available presets with full info.
|
||||
func (p *PresetPicker) SetPresetsWithInfo(presets []protocol.PresetInfo) {
|
||||
p.presets = presets
|
||||
if p.cursor >= len(presets) {
|
||||
p.cursor = max(0, len(presets)-1)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSize sets the picker dimensions.
|
||||
func (p *PresetPicker) SetSize(width, height int) {
|
||||
p.width = width
|
||||
p.height = height
|
||||
|
||||
inputWidth := min(60, width-10)
|
||||
for i := range p.fields {
|
||||
p.fields[i].Width = inputWidth
|
||||
}
|
||||
}
|
||||
|
||||
// MoveUp moves the cursor up.
|
||||
func (p *PresetPicker) MoveUp() {
|
||||
if p.cursor > 0 {
|
||||
p.cursor--
|
||||
}
|
||||
}
|
||||
|
||||
// MoveDown moves the cursor down.
|
||||
func (p *PresetPicker) MoveDown() {
|
||||
if p.cursor < len(p.presets)-1 {
|
||||
p.cursor++
|
||||
}
|
||||
}
|
||||
|
||||
// Selected returns the currently selected preset name.
|
||||
func (p *PresetPicker) Selected() string {
|
||||
if p.cursor >= 0 && p.cursor < len(p.presets) {
|
||||
return p.presets[p.cursor].Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SelectedInfo returns the currently selected preset info.
|
||||
func (p *PresetPicker) SelectedInfo() *protocol.PresetInfo {
|
||||
if p.cursor >= 0 && p.cursor < len(p.presets) {
|
||||
return &p.presets[p.cursor]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the number of presets.
|
||||
func (p *PresetPicker) Len() int {
|
||||
return len(p.presets)
|
||||
}
|
||||
|
||||
// Mode returns the current mode.
|
||||
func (p *PresetPicker) Mode() PresetMode {
|
||||
return p.mode
|
||||
}
|
||||
|
||||
// SetMode sets the mode.
|
||||
func (p *PresetPicker) SetMode(mode PresetMode) {
|
||||
p.mode = mode
|
||||
}
|
||||
|
||||
// InitAdd initializes the form for adding a new preset.
|
||||
func (p *PresetPicker) InitAdd() {
|
||||
p.mode = PresetModeAdd
|
||||
p.editName = ""
|
||||
for i := range p.fields {
|
||||
p.fields[i].Reset()
|
||||
}
|
||||
p.focus = PresetFieldName
|
||||
p.fields[PresetFieldName].Focus()
|
||||
}
|
||||
|
||||
// InitEdit initializes the form for editing an existing preset.
|
||||
func (p *PresetPicker) InitEdit() {
|
||||
preset := p.SelectedInfo()
|
||||
if preset == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.mode = PresetModeEdit
|
||||
p.editName = preset.Name
|
||||
|
||||
p.fields[PresetFieldName].SetValue(preset.Name)
|
||||
p.fields[PresetFieldEnable].SetValue(strings.Join(preset.Enable, ","))
|
||||
p.fields[PresetFieldDisable].SetValue(strings.Join(preset.Disable, ","))
|
||||
|
||||
p.focus = PresetFieldName
|
||||
p.fields[PresetFieldName].Focus()
|
||||
}
|
||||
|
||||
// InitDelete starts delete confirmation.
|
||||
func (p *PresetPicker) InitDelete() {
|
||||
if p.SelectedInfo() == nil {
|
||||
return
|
||||
}
|
||||
p.mode = PresetModeConfirmDelete
|
||||
}
|
||||
|
||||
// CancelForm cancels the current form operation.
|
||||
func (p *PresetPicker) CancelForm() {
|
||||
p.mode = PresetModeSelect
|
||||
p.editName = ""
|
||||
for i := range p.fields {
|
||||
p.fields[i].Reset()
|
||||
p.fields[i].Blur()
|
||||
}
|
||||
}
|
||||
|
||||
// Update handles input events for form mode.
|
||||
func (p *PresetPicker) Update(msg tea.KeyMsg) tea.Cmd {
|
||||
switch msg.String() {
|
||||
case "tab", "down":
|
||||
p.nextField()
|
||||
return nil
|
||||
case "shift+tab", "up":
|
||||
p.prevField()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update the focused field
|
||||
var cmd tea.Cmd
|
||||
p.fields[p.focus], cmd = p.fields[p.focus].Update(msg)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (p *PresetPicker) nextField() {
|
||||
p.fields[p.focus].Blur()
|
||||
p.focus = (p.focus + 1) % PresetFieldCount
|
||||
p.fields[p.focus].Focus()
|
||||
}
|
||||
|
||||
func (p *PresetPicker) prevField() {
|
||||
p.fields[p.focus].Blur()
|
||||
p.focus = (p.focus - 1 + PresetFieldCount) % PresetFieldCount
|
||||
p.fields[p.focus].Focus()
|
||||
}
|
||||
|
||||
// FormValues returns the form values (name, enable list, disable list).
|
||||
func (p *PresetPicker) FormValues() (name string, enable, disable []string) {
|
||||
name = strings.TrimSpace(p.fields[PresetFieldName].Value())
|
||||
|
||||
enableStr := strings.TrimSpace(p.fields[PresetFieldEnable].Value())
|
||||
if enableStr != "" {
|
||||
for _, s := range strings.Split(enableStr, ",") {
|
||||
if trimmed := strings.TrimSpace(s); trimmed != "" {
|
||||
enable = append(enable, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disableStr := strings.TrimSpace(p.fields[PresetFieldDisable].Value())
|
||||
if disableStr != "" {
|
||||
for _, s := range strings.Split(disableStr, ",") {
|
||||
if trimmed := strings.TrimSpace(s); trimmed != "" {
|
||||
disable = append(disable, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return name, enable, disable
|
||||
}
|
||||
|
||||
// EditName returns the original name when editing.
|
||||
func (p *PresetPicker) EditName() string {
|
||||
return p.editName
|
||||
}
|
||||
|
||||
// IsEdit returns true if in edit mode.
|
||||
func (p *PresetPicker) IsEdit() bool {
|
||||
return p.mode == PresetModeEdit
|
||||
}
|
||||
|
||||
// ValidateForm validates the form values.
|
||||
func (p *PresetPicker) ValidateForm() string {
|
||||
name, enable, disable := p.FormValues()
|
||||
|
||||
if name == "" {
|
||||
return "Preset name is required"
|
||||
}
|
||||
if len(enable) == 0 && len(disable) == 0 {
|
||||
return "At least one alias to enable or disable is required"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// View renders the preset picker.
|
||||
func (p *PresetPicker) View() string {
|
||||
switch p.mode {
|
||||
case PresetModeAdd, PresetModeEdit:
|
||||
return p.formView()
|
||||
case PresetModeConfirmDelete:
|
||||
return p.deleteView()
|
||||
default:
|
||||
return p.selectView()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PresetPicker) selectView() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(titleStyle.Render("Presets"))
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
if len(p.presets) == 0 {
|
||||
sb.WriteString(helpDescStyle.Render("No presets configured."))
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString(helpDescStyle.Render("Press 'n' to create one"))
|
||||
} else {
|
||||
for i, preset := range p.presets {
|
||||
if i == p.cursor {
|
||||
sb.WriteString(presetSelectedStyle.Render("▸ " + preset.Name))
|
||||
} else {
|
||||
sb.WriteString(presetItemStyle.Render(" " + preset.Name))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString(helpDescStyle.Render("↑↓ navigate • Enter apply • n new • e edit • d delete • Esc cancel"))
|
||||
|
||||
return dialogStyle.Render(sb.String())
|
||||
}
|
||||
|
||||
func (p *PresetPicker) formView() string {
|
||||
var sb strings.Builder
|
||||
|
||||
title := "Add New Preset"
|
||||
if p.mode == PresetModeEdit {
|
||||
title = "Edit Preset"
|
||||
}
|
||||
|
||||
sb.WriteString(titleStyle.Render(title))
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
labels := []string{"Name:", "Enable aliases (comma-separated):", "Disable aliases (comma-separated):"}
|
||||
|
||||
for i, label := range labels {
|
||||
sb.WriteString(inputLabelStyle.Render(label))
|
||||
sb.WriteString("\n")
|
||||
|
||||
style := inputStyle
|
||||
if PresetFormField(i) == p.focus {
|
||||
style = inputFocusStyle
|
||||
}
|
||||
|
||||
sb.WriteString(style.Render(p.fields[i].View()))
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(helpDescStyle.Render("Tab/↓ next • Shift+Tab/↑ prev • Enter save • Esc cancel"))
|
||||
|
||||
return dialogStyle.Render(sb.String())
|
||||
}
|
||||
|
||||
func (p *PresetPicker) deleteView() string {
|
||||
var sb strings.Builder
|
||||
|
||||
preset := p.SelectedInfo()
|
||||
presetName := ""
|
||||
if preset != nil {
|
||||
presetName = preset.Name
|
||||
}
|
||||
|
||||
sb.WriteString(titleStyle.Render("Delete Preset"))
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString(errorMsgStyle.Render("Are you sure you want to delete preset '" + presetName + "'?"))
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString(helpDescStyle.Render("y confirm • n/Esc cancel"))
|
||||
|
||||
return dialogStyle.Render(sb.String())
|
||||
}
|
||||
Reference in New Issue
Block a user