mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
refactor: add terminal output and forms package (#1124)
- `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>
This commit is contained in:
1 parent
88b91faa54
commit
faea86cd94
27 files changed
+1941
-1
No files matched your search
@@ -0,0 +1,124 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/itchyny/gojq"
|
||||
)
|
||||
|
||||
// WriteJSON projects explicit result fields and applies an in-process jq filter.
|
||||
func WriteJSON(w io.Writer, value any, fields, query string) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var normalized any
|
||||
if err := json.Unmarshal(data, &normalized); err != nil {
|
||||
return err
|
||||
}
|
||||
if names := fieldNames(fields); names != nil {
|
||||
selectFields := func(item any) (any, error) {
|
||||
object, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("field selection requires resource objects")
|
||||
}
|
||||
selected := map[string]any{}
|
||||
for _, name := range names {
|
||||
v, exists := object[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unknown JSON field %q", name)
|
||||
}
|
||||
selected[name] = v
|
||||
}
|
||||
return selected, nil
|
||||
}
|
||||
switch v := normalized.(type) {
|
||||
case []any:
|
||||
for i := range v {
|
||||
v[i], err = selectFields(v[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
normalized, err = selectFields(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if query == "" {
|
||||
return encoder.Encode(normalized)
|
||||
}
|
||||
parsed, err := parseQuery(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iter := parsed.Run(normalized)
|
||||
for {
|
||||
value, ok := iter.Next()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err, ok := value.(error); ok {
|
||||
return fmt.Errorf("evaluate --jq: %w", err)
|
||||
}
|
||||
if str, ok := value.(string); ok {
|
||||
if _, err := fmt.Fprintln(w, str); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := encoder.Encode(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateOutput catches invalid projection/filter syntax before a service mutates data.
|
||||
func ValidateOutput(fields, query string, allowed []string) error {
|
||||
if names := fieldNames(fields); names != nil && allowed != nil {
|
||||
known := map[string]bool{}
|
||||
for _, name := range allowed {
|
||||
known[name] = true
|
||||
}
|
||||
for _, name := range names {
|
||||
if !known[name] {
|
||||
return fmt.Errorf("unknown JSON field %q", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if query != "" {
|
||||
if _, err := parseQuery(query); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fieldNames returns the selected JSON fields, or nil when every field is selected.
|
||||
func fieldNames(fields string) []string {
|
||||
if fields == "" || fields == "*" {
|
||||
return nil
|
||||
}
|
||||
names := strings.Split(fields, ",")
|
||||
for i := range names {
|
||||
names[i] = strings.TrimSpace(names[i])
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func parseQuery(query string) (*gojq.Query, error) {
|
||||
parsed, err := gojq.Parse(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid --jq expression: %w", err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
Reference in new issue
Block a user