mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
- `internal/terminal`: streams and the prompting policy, interactive forms (issue, PR, review, merge, milestone), editor / pager / browser launching, JSON and `--jq` output. - An aligned table renderer and a record renderer for human output; both strip control and escape characters from server-supplied text so titles and bodies cannot inject terminal sequences. Reviewed-on: https://gitea.com/gitea/tea/pulls/1124 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
90 lines
3.1 KiB
Go
90 lines
3.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package terminal
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"charm.land/huh/v2"
|
|
)
|
|
|
|
// IssueEditFormChoices supplies current issue values and selectable options for edit.
|
|
// Current values missing from the options stay selectable.
|
|
type IssueEditFormChoices struct {
|
|
Title, Body string
|
|
Assignees []string
|
|
Milestone *IssueMilestoneChoice
|
|
Labels []IssueLabelChoice
|
|
Deadline *time.Time
|
|
CanPush bool
|
|
AvailableAssignees []string
|
|
Milestones []IssueMilestoneChoice
|
|
AvailableLabels []IssueLabelChoice
|
|
EditBody BodyEditor
|
|
}
|
|
|
|
// IssueEditFormValues holds interactively edited issue fields. Deadline is set
|
|
// only when the due date changed.
|
|
type IssueEditFormValues struct {
|
|
Title, Body string
|
|
Assignees []string
|
|
Milestone int64
|
|
ClearMilestone bool
|
|
Labels []int64
|
|
Deadline *time.Time
|
|
ClearDeadline bool
|
|
Draft Draft
|
|
}
|
|
|
|
// IssueEditForm collects interactive issue edit input.
|
|
type IssueEditForm interface {
|
|
EditIssue(context.Context, IssueEditFormChoices) (IssueEditFormValues, error)
|
|
}
|
|
|
|
// EditIssue runs the interactive issue edit form on stderr, seeded with the current values.
|
|
func (p *FormPrompter) EditIssue(ctx context.Context, choices IssueEditFormChoices) (IssueEditFormValues, error) {
|
|
deadline := ""
|
|
if choices.Deadline != nil && !choices.Deadline.IsZero() {
|
|
// Keep the server's offset: converting to UTC can move the date by a day.
|
|
deadline = choices.Deadline.Format(time.DateOnly)
|
|
}
|
|
fields := issueFields{title: choices.Title, body: choices.Body, deadline: deadline, assignees: choices.Assignees}
|
|
milestones := choices.Milestones
|
|
if choices.Milestone != nil {
|
|
fields.milestone, milestones = choices.Milestone.ID, withCurrent(milestones, *choices.Milestone)
|
|
}
|
|
for _, label := range choices.Labels {
|
|
fields.labels = append(fields.labels, label.ID)
|
|
}
|
|
groups := fields.groups(choices.EditBody, choices.CanPush, withCurrent(choices.AvailableAssignees, choices.Assignees...), milestones, withCurrent(choices.AvailableLabels, choices.Labels...))
|
|
if err := p.runForm(ctx, huh.NewForm(groups...)); err != nil {
|
|
return IssueEditFormValues{}, err
|
|
}
|
|
values := IssueEditFormValues{Title: strings.TrimSpace(fields.title), Assignees: fields.assignees, Milestone: fields.milestone, Labels: fields.labels}
|
|
values.ClearMilestone = fields.milestone == 0 && choices.Milestone != nil
|
|
var err error
|
|
if values.Body, values.Draft, err = editBody(ctx, choices.EditBody, fields.body); err != nil {
|
|
return values, err
|
|
}
|
|
if strings.TrimSpace(fields.deadline) == deadline {
|
|
return values, nil
|
|
}
|
|
values.Deadline, err = parseDueDate(fields.deadline)
|
|
values.ClearDeadline = values.Deadline == nil && deadline != ""
|
|
return values, err
|
|
}
|
|
|
|
// withCurrent appends current values missing from options; selects drop values they do not offer.
|
|
func withCurrent[T comparable](options []T, current ...T) []T {
|
|
for _, value := range current {
|
|
if !slices.Contains(options, value) {
|
|
options = append(slices.Clip(options), value)
|
|
}
|
|
}
|
|
return options
|
|
}
|