refactor(diary): share the console types through internal/consolekit so diary no longer links bonfire (IN-07, quick-261028-rqd)
This commit is contained in:
@@ -125,9 +125,8 @@ if catalog.Has("blog:import") {
|
||||
| `bonfire.RootOption` | An option of `bonfire.NewRoot` and `bonfire.NewRootIO`. |
|
||||
| `bonfire.WithVersion` | Sets the `bonfire.Version` shown in the list header; without it the root reads `bonfire.BuildVersion`. |
|
||||
| `bonfire.OnGlobals` | Registers a function that receives the parsed `bonfire.Globals` before every command of the root runs. |
|
||||
| `bonfire.Globals` | The global options: `Verbosity` (0 to 3), `Quiet`, `Color` (a `bonfire.ColorMode`), `NoInteraction` and `Env`. |
|
||||
| `bonfire.Globals` | The global options: `Verbosity` (0 to 3), `Quiet`, `Color` (a `bonfire.ColorMode`), `NoInteraction` and `Env`. Its method `Args` turns them back into flags (`-vv`, `-q`, `-n`, `--ansi` or `--no-ansi`, `--env=<value>`) for forwarding. `bonfire.Globals`, `bonfire.ColorMode` and `bonfire.Palette` are aliases of the types in `internal/consolekit`, which diary shares without linking bonfire. |
|
||||
| `bonfire.PeekGlobals` | Reads `bonfire.Globals` from raw arguments without failing, `=value` forms included; stops at `--`. |
|
||||
| `bonfire.Globals.Args` | Turns `bonfire.Globals` back into flags (`-vv`, `-q`, `-n`, `--ansi` or `--no-ansi`, `--env=<value>`) for forwarding. |
|
||||
| `bonfire.Version` | Application and framework versions of a binary (`App`, `Framework`, `Local`). |
|
||||
| `bonfire.BuildVersion` | Reads the `bonfire.Version` from the binary's build information. |
|
||||
| `bonfire.Version.Header` | The list header: `SummerCMS acme v1.4.0 (framework v0.3.2)`. |
|
||||
@@ -192,7 +191,7 @@ Without these variables, color is enabled only when stdout is a terminal. `--ans
|
||||
|
||||
## Dependencies
|
||||
|
||||
- SummerCMS modules: `sunscreen` (scrubs error messages); the framework-internal package `internal/argmask` (masks secret flag values in the Try line, shared with summer's delegate errors).
|
||||
- SummerCMS modules: `sunscreen` (scrubs error messages); the framework-internal packages `internal/argmask` (masks secret flag values in the Try line, shared with summer's delegate errors) and `internal/consolekit` (the color mode, palette, color detection, sanitizer, global options and the print-above hook, shared with diary).
|
||||
- Third-party: `github.com/spf13/cobra`, `github.com/spf13/pflag`, `golang.org/x/term`, `charm.land/lipgloss/v2` (with `lipgloss/v2/table`), `charm.land/bubbletea/v2`, `charm.land/bubbles/v2` (spinner, progress, key), `charm.land/huh/v2` and `github.com/charmbracelet/colorprofile` (decided in `.planning/notes/2026-10-21-console-charm-stack.md`).
|
||||
- Standard library: `bufio`, `context`, `encoding/json`, `errors`, `fmt`, `io`, `os`, `path/filepath`, `runtime/debug`, `slices`, `strconv`, `strings`, `sync`, `time`, `unicode/utf8`.
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.golem15.com/golem15/summercms/internal/argmask"
|
||||
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||
"git.golem15.com/golem15/summercms/modules/sunscreen"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -134,30 +133,7 @@ func Reported(err error) error {
|
||||
// as 0x9b, CSI) cannot reach a terminal that honours it, and the result is
|
||||
// always valid UTF-8. A clean, valid string is returned unchanged.
|
||||
func Sanitize(s string) string {
|
||||
clean := utf8.ValidString(s)
|
||||
if clean {
|
||||
for _, r := range s {
|
||||
if r != '\t' && unicode.IsControl(r) {
|
||||
clean = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if clean {
|
||||
return s
|
||||
}
|
||||
s = strings.ToValidUTF8(s, "\uFFFD")
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r == '\n' || r == '\r':
|
||||
b.WriteByte(' ')
|
||||
case r != '\t' && unicode.IsControl(r):
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
return consolekit.Sanitize(s)
|
||||
}
|
||||
|
||||
// sanitizeLines applies Sanitize to every line of s, keeping the line
|
||||
|
||||
@@ -2,6 +2,8 @@ package bonfire
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -9,24 +11,16 @@ import (
|
||||
)
|
||||
|
||||
// maxVerbosity caps -v, -vv and -vvv (and repeated --verbose).
|
||||
const maxVerbosity = 3
|
||||
const maxVerbosity = consolekit.MaxVerbosity
|
||||
|
||||
// Globals are the global console options every command accepts: -v, -vv and
|
||||
// -vvv (--verbose), -q (--quiet), --ansi and --no-ansi, -n
|
||||
// (--no-interaction) and --env.
|
||||
type Globals struct {
|
||||
// Verbosity is 0 by default and 1 to 3 for -v, -vv and -vvv.
|
||||
Verbosity int
|
||||
// Quiet suppresses decoration and info lines; it implies NoInteraction.
|
||||
Quiet bool
|
||||
// Color is ColorAlways for --ansi, ColorNever for --no-ansi and
|
||||
// ColorAuto otherwise; the last of the two flags wins.
|
||||
Color ColorMode
|
||||
// NoInteraction makes prompts return their defaults without asking.
|
||||
NoInteraction bool
|
||||
// Env is the --env value: the environment the command runs under.
|
||||
Env string
|
||||
}
|
||||
// (--no-interaction) and --env. The fields are Verbosity (0, or 1 to 3 for
|
||||
// -v to -vvv), Quiet (implies NoInteraction), Color (ColorAlways for
|
||||
// --ansi, ColorNever for --no-ansi, ColorAuto otherwise; the last of the
|
||||
// two flags wins), NoInteraction and Env. Its Args method re-serialises
|
||||
// them as flags for forwarding to another program.
|
||||
type Globals = consolekit.Globals
|
||||
|
||||
// Global option names and shorthands, reserved on every command.
|
||||
const (
|
||||
@@ -76,7 +70,7 @@ func PeekGlobals(args []string) Globals {
|
||||
}
|
||||
switch {
|
||||
case a == "--":
|
||||
return g.normalize()
|
||||
return normalize(g)
|
||||
case a == "--"+flagVerbose:
|
||||
g.Verbosity++
|
||||
case a == "-q" || a == "--"+flagQuiet:
|
||||
@@ -93,10 +87,10 @@ func PeekGlobals(args []string) Globals {
|
||||
i++
|
||||
}
|
||||
case isShorthandCluster(a):
|
||||
g.peekShorthands(a[1:])
|
||||
peekShorthands(&g, a[1:])
|
||||
}
|
||||
}
|
||||
return g.normalize()
|
||||
return normalize(g)
|
||||
}
|
||||
|
||||
// peekCount applies a --verbose=N or -v=N value the way pflag's count flag
|
||||
@@ -138,7 +132,7 @@ func isShorthandCluster(a string) bool {
|
||||
// -q -n, -vvq is -v -v -q, -v=3 sets the count, -qn=false turns -n off). It
|
||||
// stops at the first letter that is not a global shorthand: that may be a
|
||||
// command flag whose value is the rest of the token.
|
||||
func (g *Globals) peekShorthands(cluster string) {
|
||||
func peekShorthands(g *Globals, cluster string) {
|
||||
for i := 0; i < len(cluster); i++ {
|
||||
r := cluster[i]
|
||||
if i+1 < len(cluster) && cluster[i+1] == '=' {
|
||||
@@ -167,38 +161,9 @@ func (g *Globals) peekShorthands(cluster string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (g Globals) normalize() Globals {
|
||||
g.Verbosity = min(max(g.Verbosity, 0), maxVerbosity)
|
||||
if g.Quiet {
|
||||
g.NoInteraction = true
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// Args re-serialises the globals as command-line flags, for forwarding to
|
||||
// another program: -v, -vv or -vvv, -q, -n, --ansi or --no-ansi and
|
||||
// --env=<value>. The zero value gives no arguments.
|
||||
func (g Globals) Args() []string {
|
||||
var out []string
|
||||
if v := min(g.Verbosity, maxVerbosity); v > 0 {
|
||||
out = append(out, "-"+strings.Repeat("v", v))
|
||||
}
|
||||
if g.Quiet {
|
||||
out = append(out, "-q")
|
||||
}
|
||||
if g.NoInteraction && !g.Quiet {
|
||||
out = append(out, "-n")
|
||||
}
|
||||
switch g.Color {
|
||||
case ColorAlways:
|
||||
out = append(out, "--"+flagANSI)
|
||||
case ColorNever:
|
||||
out = append(out, "--"+flagNoANSI)
|
||||
}
|
||||
if g.Env != "" {
|
||||
out = append(out, "--"+flagEnv+"="+g.Env)
|
||||
}
|
||||
return out
|
||||
// normalize caps the verbosity and lets Quiet imply NoInteraction.
|
||||
func normalize(g Globals) Globals {
|
||||
return consolekit.Normalize(g)
|
||||
}
|
||||
|
||||
// globalFlags holds the parsed persistent flags of one root.
|
||||
@@ -250,13 +215,13 @@ func (gf *globalFlags) register(fs *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func (gf *globalFlags) globals() Globals {
|
||||
return Globals{
|
||||
return normalize(Globals{
|
||||
Verbosity: gf.verbosity,
|
||||
Quiet: gf.quiet,
|
||||
Color: gf.color,
|
||||
NoInteraction: gf.noInteraction,
|
||||
Env: gf.env,
|
||||
}.normalize()
|
||||
})
|
||||
}
|
||||
|
||||
// resetFlags puts every persistent flag of root back to its default, so a
|
||||
|
||||
@@ -101,8 +101,8 @@ func TestGlobalsArgs(t *testing.T) {
|
||||
if got := strings.Join(args, " "); got != tc.want {
|
||||
t.Errorf("%+v.Args() = %q, want %q", tc.g, got, tc.want)
|
||||
}
|
||||
if back := PeekGlobals(args); back != tc.g.normalize() {
|
||||
t.Errorf("PeekGlobals(%q) = %+v, want %+v", args, back, tc.g.normalize())
|
||||
if back := PeekGlobals(args); back != normalize(tc.g) {
|
||||
t.Errorf("PeekGlobals(%q) = %+v, want %+v", args, back, normalize(tc.g))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||
)
|
||||
|
||||
// Progress bar colors: the cyan to green gradient of the earlier bonfire
|
||||
@@ -558,6 +559,12 @@ func raise(sig os.Signal) {
|
||||
}
|
||||
}
|
||||
|
||||
// bonfire's live view printer serves consolekit.PrintAbove, which diary's
|
||||
// console channel calls without linking bonfire.
|
||||
func init() {
|
||||
consolekit.SetPrintAbove(printAboveLines)
|
||||
}
|
||||
|
||||
// printAboveLines prints s, one or more lines, above the running live view,
|
||||
// whichever console started it (bonfire.PrintAbove). It reports false when
|
||||
// no view runs.
|
||||
|
||||
@@ -2,61 +2,37 @@ package bonfire
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
)
|
||||
|
||||
// ColorMode selects how console output decides on color: from the terminal
|
||||
// and the environment, always, or never.
|
||||
type ColorMode int
|
||||
type ColorMode = consolekit.ColorMode
|
||||
|
||||
const (
|
||||
// ColorAuto colors output when the writer is a terminal and the
|
||||
// environment allows it (NO_COLOR and TERM=dumb disable color,
|
||||
// FORCE_COLOR enables it on any writer).
|
||||
ColorAuto ColorMode = iota
|
||||
ColorAuto = consolekit.ColorAuto
|
||||
// ColorAlways colors output whatever the writer is (--ansi).
|
||||
ColorAlways
|
||||
ColorAlways = consolekit.ColorAlways
|
||||
// ColorNever writes no escape codes at all (--no-ansi).
|
||||
ColorNever
|
||||
ColorNever = consolekit.ColorNever
|
||||
)
|
||||
|
||||
// Palette is the shared console look: the status styles of list, help,
|
||||
// widgets and log lines. Render through a writer that applies the color
|
||||
// profile (DetectProfile), because lipgloss always emits ANSI codes.
|
||||
type Palette struct {
|
||||
// Success marks completed work (green).
|
||||
Success lipgloss.Style
|
||||
// Current marks the item in progress and informational lines (cyan).
|
||||
Current lipgloss.Style
|
||||
// Warning marks warnings (yellow).
|
||||
Warning lipgloss.Style
|
||||
// Error marks failures (red).
|
||||
Error lipgloss.Style
|
||||
// ErrorBold marks fatal failures and error levels (bold red).
|
||||
ErrorBold lipgloss.Style
|
||||
// Muted marks secondary detail such as times and keys (gray).
|
||||
Muted lipgloss.Style
|
||||
// Header marks headings and names (bold).
|
||||
Header lipgloss.Style
|
||||
}
|
||||
// widgets and log lines (Success, Current, Warning, Error, ErrorBold, Muted
|
||||
// and Header). Render through a writer that applies the color profile
|
||||
// (DetectProfile), because lipgloss always emits ANSI codes.
|
||||
type Palette = consolekit.Palette
|
||||
|
||||
// DefaultPalette returns the console palette, ported from SSU's status
|
||||
// styles: green success, cyan current, yellow warning, red and bold red
|
||||
// errors, gray (240) muted text and bold headers.
|
||||
func DefaultPalette() Palette {
|
||||
return Palette{
|
||||
Success: lipgloss.NewStyle().Foreground(lipgloss.Color("2")),
|
||||
Current: lipgloss.NewStyle().Foreground(lipgloss.Color("6")),
|
||||
Warning: lipgloss.NewStyle().Foreground(lipgloss.Color("3")),
|
||||
Error: lipgloss.NewStyle().Foreground(lipgloss.Color("1")),
|
||||
ErrorBold: lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true),
|
||||
Muted: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
|
||||
Header: lipgloss.NewStyle().Bold(true),
|
||||
}
|
||||
return consolekit.DefaultPalette()
|
||||
}
|
||||
|
||||
// DetectProfile returns the color profile for console output written to w.
|
||||
@@ -66,35 +42,5 @@ func DefaultPalette() Palette {
|
||||
// dumb, and otherwise detects the profile from w and environ, which honours
|
||||
// NO_COLOR and TERM=dumb. A nil environ means os.Environ().
|
||||
func DetectProfile(w io.Writer, mode ColorMode, environ []string) colorprofile.Profile {
|
||||
if mode == ColorNever {
|
||||
return colorprofile.NoTTY
|
||||
}
|
||||
if environ == nil {
|
||||
environ = os.Environ()
|
||||
}
|
||||
if mode == ColorAuto && forceColor(environ) {
|
||||
mode = ColorAlways
|
||||
}
|
||||
p := colorprofile.Detect(w, environ)
|
||||
if mode == ColorAlways && p < colorprofile.ANSI256 {
|
||||
p = colorprofile.ANSI256
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func forceColor(environ []string) bool {
|
||||
if envValue(environ, "NO_COLOR") != "" || envValue(environ, "TERM") == "dumb" {
|
||||
return false
|
||||
}
|
||||
return envValue(environ, "FORCE_COLOR") != ""
|
||||
}
|
||||
|
||||
func envValue(environ []string, key string) string {
|
||||
for i := len(environ) - 1; i >= 0; i-- {
|
||||
k, v, ok := strings.Cut(environ[i], "=")
|
||||
if ok && k == key {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return consolekit.DetectProfile(w, mode, environ)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user