Add tm_server with SQLite, Svelte UI, and task-master TUI sync.
Introduces the web backend (auth, RBAC, project tokens, revisions), YouTrack-or-taskmaster backend choice in the TUI, and a first-pass embedded Svelte UI. The frontend is functional but needs a major rework. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+44
-10
@@ -19,6 +19,7 @@ import (
|
||||
"task/internal/ticket"
|
||||
"task/internal/ui/views"
|
||||
"task/internal/version"
|
||||
"task/internal/tmsync"
|
||||
"task/internal/youtrack"
|
||||
"task/internal/ytsync"
|
||||
)
|
||||
@@ -826,6 +827,14 @@ func (m Model) updateEditProject(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.projectEdit.PrevField()
|
||||
return m, textinput.Blink
|
||||
|
||||
case msg.Type == tea.KeyLeft:
|
||||
m.projectEdit.CycleBackend(-1)
|
||||
return m, nil
|
||||
|
||||
case msg.Type == tea.KeyRight:
|
||||
m.projectEdit.CycleBackend(1)
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.Enter):
|
||||
if err := m.projectEdit.Validate(m.projectEditMeta); err != nil {
|
||||
m.setError(err)
|
||||
@@ -842,10 +851,10 @@ func (m Model) updateEditProject(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
m.mode = ModeProjects
|
||||
if meta.YouTrack != nil && meta.YouTrack.Configured() {
|
||||
m.setStatus(fmt.Sprintf("saved YouTrack settings for %s", id))
|
||||
if meta.SyncConfigured() {
|
||||
m.setStatus(fmt.Sprintf("saved %s sync for %s", meta.EffectiveBackend(), id))
|
||||
} else {
|
||||
m.setStatus(fmt.Sprintf("cleared YouTrack settings for %s", id))
|
||||
m.setStatus(fmt.Sprintf("cleared sync settings for %s", id))
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -866,12 +875,21 @@ func (m *Model) openProjectEdit(projectID string) error {
|
||||
|
||||
func (m *Model) openSyncMenu(projectID string) {
|
||||
m.syncProjectID = projectID
|
||||
label := "Sync"
|
||||
if meta, err := m.projects.LoadMeta(projectID); err == nil {
|
||||
switch meta.EffectiveBackend() {
|
||||
case project.BackendTaskMaster:
|
||||
label = "task-master"
|
||||
case project.BackendYouTrack:
|
||||
label = "YouTrack"
|
||||
}
|
||||
}
|
||||
m.syncMenu = views.NewSyncMenu(m.width, views.StatusMenuStyles{
|
||||
Box: m.styles.StatusMenuBox,
|
||||
Title: m.styles.StatusMenuTitle,
|
||||
Selected: m.styles.StatusMenuSelected,
|
||||
Normal: m.styles.StatusMenuNormal,
|
||||
})
|
||||
}, label)
|
||||
m.mode = ModeSync
|
||||
}
|
||||
|
||||
@@ -925,11 +943,27 @@ func (m Model) startSyncAction(projectID string, action views.SyncAction) (tea.M
|
||||
return m, func() tea.Msg {
|
||||
var pull, push ytsync.Result
|
||||
var err error
|
||||
switch action {
|
||||
case views.SyncActionPush:
|
||||
push, err = ytsync.PushProject(context.Background(), projects, store, timeStore, projectID)
|
||||
default:
|
||||
pull, err = ytsync.PullProject(context.Background(), projects, store, timeStore, projectID)
|
||||
meta, merr := projects.LoadMeta(projectID)
|
||||
if merr != nil {
|
||||
err = merr
|
||||
} else {
|
||||
switch meta.EffectiveBackend() {
|
||||
case project.BackendTaskMaster:
|
||||
syncer := &tmsync.Syncer{Store: store, TimeStore: timeStore}
|
||||
switch action {
|
||||
case views.SyncActionPush:
|
||||
push, err = syncer.Push(context.Background(), projectID, *meta.TaskMaster)
|
||||
default:
|
||||
pull, err = syncer.Pull(context.Background(), projectID, *meta.TaskMaster)
|
||||
}
|
||||
default:
|
||||
switch action {
|
||||
case views.SyncActionPush:
|
||||
push, err = ytsync.PushProject(context.Background(), projects, store, timeStore, projectID)
|
||||
default:
|
||||
pull, err = ytsync.PullProject(context.Background(), projects, store, timeStore, projectID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return syncFinishedMsg{projectID: projectID, action: action, pull: pull, push: push, err: err}
|
||||
}
|
||||
@@ -1451,7 +1485,7 @@ func (m *Model) openProjects() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
yt[p.ID] = meta.YouTrack != nil && meta.YouTrack.Configured()
|
||||
yt[p.ID] = meta.SyncConfigured()
|
||||
}
|
||||
m.projectList = views.NewProjectListWithYT(projects, m.cfg.ActiveProject, yt, m.width, bodyH)
|
||||
return nil
|
||||
|
||||
@@ -34,7 +34,7 @@ func (m Model) helpEntries() []helpEntry {
|
||||
{"enter", "select"},
|
||||
{"n", "new project"},
|
||||
{"e", "edit"},
|
||||
{"Y", "YouTrack"},
|
||||
{"Y", "Sync"},
|
||||
{"x", "delete"},
|
||||
{"X", "force delete"},
|
||||
{"esc", "cancel"},
|
||||
@@ -121,7 +121,7 @@ func (m Model) helpEntries() []helpEntry {
|
||||
{"t/T", "timer/log"},
|
||||
{"m", "entries"},
|
||||
{"r", "report"},
|
||||
{"Y", "YouTrack"},
|
||||
{"Y", "Sync"},
|
||||
{"u/U", "undo/redo"},
|
||||
{"/", "search"},
|
||||
{"p", "project"},
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"task/internal/project"
|
||||
)
|
||||
|
||||
// ProjectEdit field indices.
|
||||
// ProjectEdit field indices (backend picker is separate).
|
||||
const (
|
||||
FieldRemoteURL = iota
|
||||
FieldToken
|
||||
@@ -19,6 +19,12 @@ const (
|
||||
ProjectEditFieldCount
|
||||
)
|
||||
|
||||
var backendOptions = []string{
|
||||
project.BackendNone,
|
||||
project.BackendYouTrack,
|
||||
project.BackendTaskMaster,
|
||||
}
|
||||
|
||||
// ProjectEditStyles styles the project edit form.
|
||||
type ProjectEditStyles struct {
|
||||
Label lipgloss.Style
|
||||
@@ -39,9 +45,12 @@ func DefaultProjectEditStyles() ProjectEditStyles {
|
||||
}
|
||||
}
|
||||
|
||||
// ProjectEditForm is a multi-field editor for YouTrack project settings.
|
||||
// ProjectEditForm is a multi-field editor for sync backend settings.
|
||||
type ProjectEditForm struct {
|
||||
ProjectID string
|
||||
Backend string
|
||||
BackendIdx int
|
||||
FocusBackend bool // true when selecting backend
|
||||
Inputs []textinput.Model
|
||||
Focus int
|
||||
Styles ProjectEditStyles
|
||||
@@ -67,34 +76,82 @@ func NewProjectEditForm(projectID string, meta project.Meta, width int) ProjectE
|
||||
return ti
|
||||
}
|
||||
|
||||
inputs[FieldRemoteURL] = mk("https://youtrack.example.com", 256, false)
|
||||
inputs[FieldToken] = mk("permanent token", 512, true)
|
||||
inputs[FieldProjectKey] = mk("PROJ", 64, false)
|
||||
backend := meta.EffectiveBackend()
|
||||
if backend == "" {
|
||||
backend = project.BackendNone
|
||||
}
|
||||
bIdx := 0
|
||||
for i, b := range backendOptions {
|
||||
if b == backend {
|
||||
bIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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)"
|
||||
switch backend {
|
||||
case project.BackendTaskMaster:
|
||||
inputs[FieldRemoteURL] = mk("http://127.0.0.1:8080", 256, false)
|
||||
inputs[FieldToken] = mk("tm_… project token", 512, true)
|
||||
inputs[FieldProjectKey] = mk("remote project UUID", 64, false)
|
||||
if meta.TaskMaster != nil {
|
||||
tm := meta.TaskMaster
|
||||
inputs[FieldRemoteURL].SetValue(tm.BaseURL)
|
||||
inputs[FieldProjectKey].SetValue(tm.ProjectID)
|
||||
if tm.Token != "" {
|
||||
hadToken = true
|
||||
inputs[FieldToken].Placeholder = "(unchanged — type to replace)"
|
||||
}
|
||||
}
|
||||
default:
|
||||
inputs[FieldRemoteURL] = mk("https://youtrack.example.com", 256, false)
|
||||
inputs[FieldToken] = mk("permanent token", 512, true)
|
||||
inputs[FieldProjectKey] = mk("PROJ", 64, 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,
|
||||
ProjectID: projectID,
|
||||
Backend: backend,
|
||||
BackendIdx: bIdx,
|
||||
FocusBackend: true,
|
||||
Inputs: inputs,
|
||||
Focus: FieldRemoteURL,
|
||||
Styles: styles,
|
||||
Width: width,
|
||||
hadToken: hadToken,
|
||||
}
|
||||
f.focusIndex(FieldRemoteURL)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *ProjectEditForm) applyBackendPlaceholders() {
|
||||
f.Backend = backendOptions[f.BackendIdx]
|
||||
switch f.Backend {
|
||||
case project.BackendTaskMaster:
|
||||
f.Inputs[FieldRemoteURL].Placeholder = "http://127.0.0.1:8080"
|
||||
f.Inputs[FieldToken].Placeholder = "tm_… project token"
|
||||
f.Inputs[FieldProjectKey].Placeholder = "remote project UUID"
|
||||
case project.BackendYouTrack:
|
||||
f.Inputs[FieldRemoteURL].Placeholder = "https://youtrack.example.com"
|
||||
f.Inputs[FieldToken].Placeholder = "permanent token"
|
||||
f.Inputs[FieldProjectKey].Placeholder = "PROJ"
|
||||
default:
|
||||
f.Inputs[FieldRemoteURL].Placeholder = "(unused)"
|
||||
f.Inputs[FieldToken].Placeholder = "(unused)"
|
||||
f.Inputs[FieldProjectKey].Placeholder = "(unused)"
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ProjectEditForm) focusIndex(i int) {
|
||||
f.FocusBackend = false
|
||||
f.Focus = i
|
||||
for idx := range f.Inputs {
|
||||
if idx == i {
|
||||
@@ -111,16 +168,52 @@ func (f *ProjectEditForm) focusIndex(i int) {
|
||||
|
||||
// NextField moves focus to the next field.
|
||||
func (f *ProjectEditForm) NextField() {
|
||||
if f.FocusBackend {
|
||||
if f.Backend == project.BackendNone {
|
||||
return
|
||||
}
|
||||
f.focusIndex(FieldRemoteURL)
|
||||
return
|
||||
}
|
||||
f.focusIndex((f.Focus + 1) % ProjectEditFieldCount)
|
||||
}
|
||||
|
||||
// PrevField moves focus to the previous field.
|
||||
func (f *ProjectEditForm) PrevField() {
|
||||
if f.FocusBackend {
|
||||
return
|
||||
}
|
||||
if f.Focus == FieldRemoteURL {
|
||||
f.blurInputs()
|
||||
f.FocusBackend = true
|
||||
return
|
||||
}
|
||||
f.focusIndex((f.Focus - 1 + ProjectEditFieldCount) % ProjectEditFieldCount)
|
||||
}
|
||||
|
||||
func (f *ProjectEditForm) blurInputs() {
|
||||
for idx := range f.Inputs {
|
||||
f.Inputs[idx].Blur()
|
||||
f.Inputs[idx].PromptStyle = f.Styles.Blurred
|
||||
f.Inputs[idx].TextStyle = f.Styles.Blurred
|
||||
}
|
||||
}
|
||||
|
||||
// CycleBackend changes the selected backend when focusing the picker.
|
||||
func (f *ProjectEditForm) CycleBackend(delta int) {
|
||||
if !f.FocusBackend {
|
||||
return
|
||||
}
|
||||
n := len(backendOptions)
|
||||
f.BackendIdx = (f.BackendIdx + delta + n) % n
|
||||
f.applyBackendPlaceholders()
|
||||
}
|
||||
|
||||
// Update forwards a message to the focused text input.
|
||||
func (f *ProjectEditForm) Update(msg tea.Msg) tea.Cmd {
|
||||
if f.FocusBackend || f.Backend == project.BackendNone {
|
||||
return nil
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
f.Inputs[f.Focus], cmd = f.Inputs[f.Focus].Update(msg)
|
||||
if f.Focus == FieldToken && f.Inputs[FieldToken].Value() != "" {
|
||||
@@ -131,60 +224,103 @@ func (f *ProjectEditForm) Update(msg tea.Msg) tea.Cmd {
|
||||
|
||||
// Meta builds the Meta to persist. Empty token keeps the previous token.
|
||||
func (f ProjectEditForm) Meta(previous project.Meta) project.Meta {
|
||||
backend := backendOptions[f.BackendIdx]
|
||||
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}
|
||||
switch backend {
|
||||
case project.BackendNone:
|
||||
return project.Meta{Backend: project.BackendNone}
|
||||
case project.BackendTaskMaster:
|
||||
tm := &project.TaskMaster{BaseURL: baseURL, ProjectID: key, Token: token}
|
||||
if token == "" && previous.TaskMaster != nil {
|
||||
tm.Token = previous.TaskMaster.Token
|
||||
}
|
||||
if baseURL == "" && token == "" && key == "" {
|
||||
return project.Meta{Backend: project.BackendNone}
|
||||
}
|
||||
return project.Meta{Backend: project.BackendTaskMaster, TaskMaster: tm}
|
||||
default:
|
||||
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
|
||||
}
|
||||
if baseURL == "" && token == "" && key == "" {
|
||||
return project.Meta{Backend: project.BackendNone}
|
||||
}
|
||||
return project.Meta{Backend: project.BackendYouTrack, YouTrack: yt}
|
||||
}
|
||||
|
||||
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.
|
||||
// Validate ensures partial configs are rejected.
|
||||
func (f ProjectEditForm) Validate(previous project.Meta) error {
|
||||
meta := f.Meta(previous)
|
||||
if meta.YouTrack == nil {
|
||||
switch meta.EffectiveBackend() {
|
||||
case project.BackendNone:
|
||||
return nil
|
||||
case project.BackendTaskMaster:
|
||||
tm := meta.TaskMaster
|
||||
if tm == nil {
|
||||
return nil
|
||||
}
|
||||
if !tm.Configured() {
|
||||
return fmt.Errorf("incomplete taskmaster config: need base URL, token, project id")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
yt := meta.YouTrack
|
||||
if yt == nil {
|
||||
return nil
|
||||
}
|
||||
if !yt.Configured() {
|
||||
return fmt.Errorf("incomplete YouTrack config: need remote URL, token, project key")
|
||||
}
|
||||
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(f.Styles.Hint.Render("Sync backend · ←/→ change · tab fields · enter save · esc cancel"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
backendLine := "Backend: "
|
||||
for i, opt := range backendOptions {
|
||||
label := opt
|
||||
if i == f.BackendIdx {
|
||||
if f.FocusBackend {
|
||||
label = f.Styles.Focused.Render("[" + opt + "]")
|
||||
} else {
|
||||
label = f.Styles.Focused.Render(opt)
|
||||
}
|
||||
} else {
|
||||
label = f.Styles.Blurred.Render(opt)
|
||||
}
|
||||
if i > 0 {
|
||||
backendLine += " "
|
||||
}
|
||||
backendLine += label
|
||||
}
|
||||
b.WriteString(backendLine)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if f.Backend == project.BackendNone {
|
||||
b.WriteString(f.Styles.Hint.Render("No remote sync for this project."))
|
||||
b.WriteString("\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
labels := []string{"Remote URL", "Token", "Project key"}
|
||||
if f.Backend == project.BackendTaskMaster {
|
||||
labels = []string{"Server URL", "Project token", "Remote project ID"}
|
||||
}
|
||||
for i, label := range labels {
|
||||
b.WriteString(f.Styles.Label.Render(label + ":"))
|
||||
b.WriteString("\n")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
func TestProjectEditMetaKeepsToken(t *testing.T) {
|
||||
prev := project.Meta{
|
||||
Backend: project.BackendYouTrack,
|
||||
YouTrack: &project.YouTrack{
|
||||
BaseURL: "https://yt.example",
|
||||
Token: "secret",
|
||||
@@ -31,10 +32,27 @@ func TestProjectEditMetaKeepsToken(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProjectEditValidateIncomplete(t *testing.T) {
|
||||
f := views.NewProjectEditForm("inbox", project.Meta{}, 80)
|
||||
f := views.NewProjectEditForm("inbox", project.Meta{Backend: project.BackendYouTrack}, 80)
|
||||
f.BackendIdx = 1 // youtrack
|
||||
f.Backend = project.BackendYouTrack
|
||||
f.Inputs[views.FieldRemoteURL].SetValue("https://yt.example")
|
||||
// missing token + key
|
||||
if err := f.Validate(project.Meta{}); err == nil {
|
||||
t.Fatal("expected incomplete error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectEditTaskMaster(t *testing.T) {
|
||||
prev := project.Meta{
|
||||
Backend: project.BackendTaskMaster,
|
||||
TaskMaster: &project.TaskMaster{
|
||||
BaseURL: "http://127.0.0.1:8080", Token: "tm_secret", ProjectID: "abc",
|
||||
},
|
||||
}
|
||||
f := views.NewProjectEditForm("inbox", prev, 80)
|
||||
f.Inputs[views.FieldToken].SetValue("")
|
||||
meta := f.Meta(prev)
|
||||
if meta.TaskMaster == nil || meta.TaskMaster.Token != "tm_secret" {
|
||||
t.Fatalf("expected kept tm token, got %+v", meta.TaskMaster)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// SyncAction is a YouTrack sync menu choice.
|
||||
// SyncAction is a sync menu choice.
|
||||
type SyncAction string
|
||||
|
||||
const (
|
||||
@@ -21,27 +21,30 @@ type SyncOption struct {
|
||||
Shortcut rune
|
||||
}
|
||||
|
||||
// AllSyncOptions is the ordered sync menu.
|
||||
var AllSyncOptions = []SyncOption{
|
||||
{Action: SyncActionPull, Label: "Pull from YouTrack", Shortcut: 'p'},
|
||||
{Action: SyncActionPush, Label: "Push to YouTrack", Shortcut: 'u'},
|
||||
}
|
||||
|
||||
// SyncMenu renders a pull/push picker overlay.
|
||||
type SyncMenu struct {
|
||||
Cursor int
|
||||
Width int
|
||||
Styles StatusMenuStyles
|
||||
Cursor int
|
||||
Width int
|
||||
Title string
|
||||
Options []SyncOption
|
||||
Styles StatusMenuStyles
|
||||
}
|
||||
|
||||
// NewSyncMenu creates a sync action menu.
|
||||
func NewSyncMenu(width int, styles StatusMenuStyles) SyncMenu {
|
||||
return SyncMenu{Cursor: 0, Width: width, Styles: styles}
|
||||
// NewSyncMenu creates a sync action menu for the given backend label.
|
||||
func NewSyncMenu(width int, styles StatusMenuStyles, backendLabel string) SyncMenu {
|
||||
if backendLabel == "" {
|
||||
backendLabel = "Sync"
|
||||
}
|
||||
opts := []SyncOption{
|
||||
{Action: SyncActionPull, Label: "Pull from " + backendLabel, Shortcut: 'p'},
|
||||
{Action: SyncActionPush, Label: "Push to " + backendLabel, Shortcut: 'u'},
|
||||
}
|
||||
return SyncMenu{Cursor: 0, Width: width, Title: backendLabel, Options: opts, Styles: styles}
|
||||
}
|
||||
|
||||
// MoveDown moves the selection down.
|
||||
func (m *SyncMenu) MoveDown() {
|
||||
if m.Cursor < len(AllSyncOptions)-1 {
|
||||
if m.Cursor < len(m.Options)-1 {
|
||||
m.Cursor++
|
||||
}
|
||||
}
|
||||
@@ -56,7 +59,7 @@ func (m *SyncMenu) MoveUp() {
|
||||
// JumpToShortcut selects the option whose shortcut matches r.
|
||||
func (m *SyncMenu) JumpToShortcut(r rune) bool {
|
||||
r = unicode.ToLower(r)
|
||||
for i, opt := range AllSyncOptions {
|
||||
for i, opt := range m.Options {
|
||||
if opt.Shortcut == r {
|
||||
m.Cursor = i
|
||||
return true
|
||||
@@ -67,17 +70,17 @@ func (m *SyncMenu) JumpToShortcut(r rune) bool {
|
||||
|
||||
// Selected returns the highlighted action.
|
||||
func (m SyncMenu) Selected() SyncAction {
|
||||
if m.Cursor < 0 || m.Cursor >= len(AllSyncOptions) {
|
||||
if m.Cursor < 0 || m.Cursor >= len(m.Options) {
|
||||
return SyncActionPull
|
||||
}
|
||||
return AllSyncOptions[m.Cursor].Action
|
||||
return m.Options[m.Cursor].Action
|
||||
}
|
||||
|
||||
// View renders the sync menu.
|
||||
func (m SyncMenu) View() string {
|
||||
var lines []string
|
||||
lines = append(lines, m.Styles.Title.Render("YouTrack"))
|
||||
for i, opt := range AllSyncOptions {
|
||||
lines = append(lines, m.Styles.Title.Render(m.Title))
|
||||
for i, opt := range m.Options {
|
||||
entry := fmt.Sprintf("%c %s", opt.Shortcut, opt.Label)
|
||||
if i == m.Cursor {
|
||||
lines = append(lines, m.Styles.Selected.Render("▸ "+entry))
|
||||
|
||||
Reference in New Issue
Block a user