package views import ( "fmt" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "task/internal/project" ) // ProjectEdit field indices. const ( FieldRemoteURL = iota FieldToken FieldProjectKey ProjectEditFieldCount ) // ProjectEditStyles styles the project edit form. type ProjectEditStyles struct { Label lipgloss.Style Focused lipgloss.Style Blurred lipgloss.Style Hint lipgloss.Style Title lipgloss.Style } // DefaultProjectEditStyles returns sensible defaults. func DefaultProjectEditStyles() ProjectEditStyles { return ProjectEditStyles{ Label: lipgloss.NewStyle().Foreground(lipgloss.Color("245")), Focused: lipgloss.NewStyle().Foreground(lipgloss.Color("205")), Blurred: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), Hint: lipgloss.NewStyle().Foreground(lipgloss.Color("241")), Title: lipgloss.NewStyle().Bold(true), } } // ProjectEditForm is a multi-field editor for YouTrack project settings. type ProjectEditForm struct { ProjectID string Inputs []textinput.Model Focus int Styles ProjectEditStyles Width int hadToken bool tokenChanged bool } // NewProjectEditForm builds a form prefilled from meta. func NewProjectEditForm(projectID string, meta project.Meta, width int) ProjectEditForm { styles := DefaultProjectEditStyles() inputs := make([]textinput.Model, ProjectEditFieldCount) mk := func(placeholder string, charLimit int, password bool) textinput.Model { ti := textinput.New() ti.Placeholder = placeholder ti.CharLimit = charLimit ti.Width = max(20, width-4) if password { ti.EchoMode = textinput.EchoPassword ti.EchoCharacter = '•' } return ti } inputs[FieldRemoteURL] = mk("https://youtrack.example.com", 256, false) inputs[FieldToken] = mk("permanent token", 512, true) inputs[FieldProjectKey] = mk("PROJ", 64, false) hadToken := false if meta.YouTrack != nil { yt := meta.YouTrack inputs[FieldRemoteURL].SetValue(yt.BaseURL) inputs[FieldProjectKey].SetValue(yt.ProjectKey) if yt.Token != "" { hadToken = true inputs[FieldToken].Placeholder = "(unchanged — type to replace)" } } f := ProjectEditForm{ ProjectID: projectID, Inputs: inputs, Focus: FieldRemoteURL, Styles: styles, Width: width, hadToken: hadToken, } f.focusIndex(FieldRemoteURL) return f } func (f *ProjectEditForm) focusIndex(i int) { f.Focus = i for idx := range f.Inputs { if idx == i { f.Inputs[idx].Focus() f.Inputs[idx].PromptStyle = f.Styles.Focused f.Inputs[idx].TextStyle = f.Styles.Focused } else { f.Inputs[idx].Blur() f.Inputs[idx].PromptStyle = f.Styles.Blurred f.Inputs[idx].TextStyle = f.Styles.Blurred } } } // NextField moves focus to the next field. func (f *ProjectEditForm) NextField() { f.focusIndex((f.Focus + 1) % ProjectEditFieldCount) } // PrevField moves focus to the previous field. func (f *ProjectEditForm) PrevField() { f.focusIndex((f.Focus - 1 + ProjectEditFieldCount) % ProjectEditFieldCount) } // Update forwards a message to the focused text input. func (f *ProjectEditForm) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd f.Inputs[f.Focus], cmd = f.Inputs[f.Focus].Update(msg) if f.Focus == FieldToken && f.Inputs[FieldToken].Value() != "" { f.tokenChanged = true } return cmd } // Meta builds the Meta to persist. Empty token keeps the previous token. func (f ProjectEditForm) Meta(previous project.Meta) project.Meta { baseURL := strings.TrimSpace(f.Inputs[FieldRemoteURL].Value()) token := strings.TrimSpace(f.Inputs[FieldToken].Value()) key := strings.TrimSpace(f.Inputs[FieldProjectKey].Value()) if baseURL == "" && token == "" && key == "" { return project.Meta{YouTrack: nil} } yt := &project.YouTrack{ BaseURL: baseURL, ProjectKey: key, Token: token, } if token == "" && previous.YouTrack != nil { yt.Token = previous.YouTrack.Token } if previous.YouTrack != nil && previous.YouTrack.Query != "" { yt.Query = previous.YouTrack.Query } return project.Meta{YouTrack: yt} } // Validate ensures partial YouTrack configs are rejected. func (f ProjectEditForm) Validate(previous project.Meta) error { meta := f.Meta(previous) if meta.YouTrack == nil { return nil } yt := meta.YouTrack any := yt.BaseURL != "" || yt.Token != "" || yt.ProjectKey != "" if any && !yt.Configured() { missing := []string{} if yt.BaseURL == "" { missing = append(missing, "remote URL") } if yt.Token == "" { missing = append(missing, "token") } if yt.ProjectKey == "" { missing = append(missing, "project key") } return fmt.Errorf("incomplete YouTrack config: missing %s", strings.Join(missing, ", ")) } return nil } // View renders the form. func (f ProjectEditForm) View() string { labels := []string{"Remote URL", "Token", "Project key"} var b strings.Builder b.WriteString(f.Styles.Title.Render(fmt.Sprintf("Edit project %s", f.ProjectID))) b.WriteString("\n") b.WriteString(f.Styles.Hint.Render("YouTrack sync settings · tab/↑↓ fields · enter save · esc cancel")) b.WriteString("\n\n") for i, label := range labels { b.WriteString(f.Styles.Label.Render(label + ":")) b.WriteString("\n") b.WriteString(f.Inputs[i].View()) b.WriteString("\n\n") } if f.hadToken && !f.tokenChanged { b.WriteString(f.Styles.Hint.Render("Token is set; leave blank to keep.")) b.WriteString("\n") } return b.String() }