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:
226
internal/consolekit/consolekit.go
Normal file
226
internal/consolekit/consolekit.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
// Package consolekit holds the console types and helpers shared by bonfire
|
||||||
|
// and diary without the TUI stack: the color mode, the palette, color
|
||||||
|
// detection, the control-character sanitizer, the global options and the
|
||||||
|
// hook that prints a line above a running live view. It imports lipgloss and
|
||||||
|
// colorprofile only, so diary (and the security modules that log through
|
||||||
|
// it) never link cobra, bubbletea, bubbles or huh. bonfire re-exports every
|
||||||
|
// identifier under its own name.
|
||||||
|
package consolekit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/charmbracelet/colorprofile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColorMode selects how console output decides on color: from the terminal
|
||||||
|
// and the environment, always, or never.
|
||||||
|
type ColorMode int
|
||||||
|
|
||||||
|
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
|
||||||
|
// ColorAlways colors output whatever the writer is (--ansi).
|
||||||
|
ColorAlways
|
||||||
|
// ColorNever writes no escape codes at all (--no-ansi).
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectProfile returns the color profile for console output written to w.
|
||||||
|
// ColorNever gives colorprofile.NoTTY (no escape codes at all). ColorAlways
|
||||||
|
// gives the detected profile raised to at least 256 colors. ColorAuto treats
|
||||||
|
// a non-empty FORCE_COLOR as ColorAlways unless NO_COLOR is set or TERM is
|
||||||
|
// 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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize removes control characters other than tab from s; carriage
|
||||||
|
// returns and line feeds become spaces, so a multi-line value stays on one
|
||||||
|
// line. Text from outside the operator's control (database or remote error
|
||||||
|
// text, names an administrator typed) then cannot move the cursor, clear
|
||||||
|
// the screen or set the window title. Invalid UTF-8 becomes U+FFFD first,
|
||||||
|
// so a raw 8-bit C1 byte (such 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, "<22>")
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxVerbosity caps -v, -vv and -vvv (and repeated --verbose).
|
||||||
|
const MaxVerbosity = 3
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize caps the verbosity to 0..MaxVerbosity and lets Quiet imply
|
||||||
|
// NoInteraction.
|
||||||
|
func Normalize(g Globals) 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, "--ansi")
|
||||||
|
case ColorNever:
|
||||||
|
out = append(out, "--no-ansi")
|
||||||
|
}
|
||||||
|
if g.Env != "" {
|
||||||
|
out = append(out, "--env="+g.Env)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// printAboveHook is the function PrintAbove calls; bonfire registers its
|
||||||
|
// live view printer when it is linked.
|
||||||
|
var printAboveHook atomic.Pointer[func(string) bool]
|
||||||
|
|
||||||
|
// SetPrintAbove registers fn as the printer PrintAbove calls; nil removes
|
||||||
|
// it. bonfire registers its live view printer in an init function.
|
||||||
|
func SetPrintAbove(fn func(string) bool) {
|
||||||
|
if fn == nil {
|
||||||
|
printAboveHook.Store(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printAboveHook.Store(&fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintAbove prints s, one or more lines, above the running live view
|
||||||
|
// through the registered printer and reports whether it did. Without a
|
||||||
|
// printer (bonfire is not linked, so no live view can run) it reports
|
||||||
|
// false and the caller writes s itself.
|
||||||
|
func PrintAbove(s string) bool {
|
||||||
|
if fn := printAboveHook.Load(); fn != nil {
|
||||||
|
return (*fn)(s)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
44
internal/consolekit/consolekit_test.go
Normal file
44
internal/consolekit/consolekit_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package consolekit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPrintAboveHook: without a registered printer PrintAbove reports
|
||||||
|
// false; with one it forwards the text and its answer; SetPrintAbove(nil)
|
||||||
|
// removes it again.
|
||||||
|
func TestPrintAboveHook(t *testing.T) {
|
||||||
|
t.Cleanup(func() { SetPrintAbove(nil) })
|
||||||
|
SetPrintAbove(nil)
|
||||||
|
if PrintAbove("x") {
|
||||||
|
t.Fatal("PrintAbove without a printer reported true")
|
||||||
|
}
|
||||||
|
var got []string
|
||||||
|
SetPrintAbove(func(s string) bool {
|
||||||
|
got = append(got, s)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !PrintAbove("line") || strings.Join(got, ",") != "line" {
|
||||||
|
t.Fatalf("PrintAbove with a printer: got %q", got)
|
||||||
|
}
|
||||||
|
SetPrintAbove(nil)
|
||||||
|
if PrintAbove("again") || len(got) != 1 {
|
||||||
|
t.Fatalf("PrintAbove after SetPrintAbove(nil): got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNormalizeAndArgs: Normalize caps the verbosity and lets Quiet imply
|
||||||
|
// NoInteraction; Args emits the flags bonfire forwards.
|
||||||
|
func TestNormalizeAndArgs(t *testing.T) {
|
||||||
|
if g := Normalize(Globals{Verbosity: 9, Quiet: true}); g.Verbosity != MaxVerbosity || !g.NoInteraction {
|
||||||
|
t.Errorf("Normalize = %+v", g)
|
||||||
|
}
|
||||||
|
if g := Normalize(Globals{Verbosity: -2}); g.Verbosity != 0 {
|
||||||
|
t.Errorf("Normalize negative = %+v", g)
|
||||||
|
}
|
||||||
|
g := Globals{Verbosity: 2, Quiet: true, NoInteraction: true, Color: ColorNever, Env: "dev"}
|
||||||
|
if got := strings.Join(g.Args(), " "); got != "-vv -q --no-ansi --env=dev" {
|
||||||
|
t.Errorf("Args = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -125,9 +125,8 @@ if catalog.Has("blog:import") {
|
|||||||
| `bonfire.RootOption` | An option of `bonfire.NewRoot` and `bonfire.NewRootIO`. |
|
| `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.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.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.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.Version` | Application and framework versions of a binary (`App`, `Framework`, `Local`). |
|
||||||
| `bonfire.BuildVersion` | Reads the `bonfire.Version` from the binary's build information. |
|
| `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)`. |
|
| `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
|
## 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`).
|
- 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`.
|
- 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"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"git.golem15.com/golem15/summercms/internal/argmask"
|
"git.golem15.com/golem15/summercms/internal/argmask"
|
||||||
|
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||||
"git.golem15.com/golem15/summercms/modules/sunscreen"
|
"git.golem15.com/golem15/summercms/modules/sunscreen"
|
||||||
"github.com/charmbracelet/colorprofile"
|
"github.com/charmbracelet/colorprofile"
|
||||||
"github.com/spf13/cobra"
|
"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
|
// 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.
|
// always valid UTF-8. A clean, valid string is returned unchanged.
|
||||||
func Sanitize(s string) string {
|
func Sanitize(s string) string {
|
||||||
clean := utf8.ValidString(s)
|
return consolekit.Sanitize(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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeLines applies Sanitize to every line of s, keeping the line
|
// sanitizeLines applies Sanitize to every line of s, keeping the line
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package bonfire
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -9,24 +11,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// maxVerbosity caps -v, -vv and -vvv (and repeated --verbose).
|
// 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
|
// Globals are the global console options every command accepts: -v, -vv and
|
||||||
// -vvv (--verbose), -q (--quiet), --ansi and --no-ansi, -n
|
// -vvv (--verbose), -q (--quiet), --ansi and --no-ansi, -n
|
||||||
// (--no-interaction) and --env.
|
// (--no-interaction) and --env. The fields are Verbosity (0, or 1 to 3 for
|
||||||
type Globals struct {
|
// -v to -vvv), Quiet (implies NoInteraction), Color (ColorAlways for
|
||||||
// Verbosity is 0 by default and 1 to 3 for -v, -vv and -vvv.
|
// --ansi, ColorNever for --no-ansi, ColorAuto otherwise; the last of the
|
||||||
Verbosity int
|
// two flags wins), NoInteraction and Env. Its Args method re-serialises
|
||||||
// Quiet suppresses decoration and info lines; it implies NoInteraction.
|
// them as flags for forwarding to another program.
|
||||||
Quiet bool
|
type Globals = consolekit.Globals
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global option names and shorthands, reserved on every command.
|
// Global option names and shorthands, reserved on every command.
|
||||||
const (
|
const (
|
||||||
@@ -76,7 +70,7 @@ func PeekGlobals(args []string) Globals {
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case a == "--":
|
case a == "--":
|
||||||
return g.normalize()
|
return normalize(g)
|
||||||
case a == "--"+flagVerbose:
|
case a == "--"+flagVerbose:
|
||||||
g.Verbosity++
|
g.Verbosity++
|
||||||
case a == "-q" || a == "--"+flagQuiet:
|
case a == "-q" || a == "--"+flagQuiet:
|
||||||
@@ -93,10 +87,10 @@ func PeekGlobals(args []string) Globals {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
case isShorthandCluster(a):
|
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
|
// 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
|
// -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
|
// 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.
|
// 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++ {
|
for i := 0; i < len(cluster); i++ {
|
||||||
r := cluster[i]
|
r := cluster[i]
|
||||||
if i+1 < len(cluster) && cluster[i+1] == '=' {
|
if i+1 < len(cluster) && cluster[i+1] == '=' {
|
||||||
@@ -167,38 +161,9 @@ func (g *Globals) peekShorthands(cluster string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g Globals) normalize() Globals {
|
// normalize caps the verbosity and lets Quiet imply NoInteraction.
|
||||||
g.Verbosity = min(max(g.Verbosity, 0), maxVerbosity)
|
func normalize(g Globals) Globals {
|
||||||
if g.Quiet {
|
return consolekit.Normalize(g)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// globalFlags holds the parsed persistent flags of one root.
|
// 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 {
|
func (gf *globalFlags) globals() Globals {
|
||||||
return Globals{
|
return normalize(Globals{
|
||||||
Verbosity: gf.verbosity,
|
Verbosity: gf.verbosity,
|
||||||
Quiet: gf.quiet,
|
Quiet: gf.quiet,
|
||||||
Color: gf.color,
|
Color: gf.color,
|
||||||
NoInteraction: gf.noInteraction,
|
NoInteraction: gf.noInteraction,
|
||||||
Env: gf.env,
|
Env: gf.env,
|
||||||
}.normalize()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetFlags puts every persistent flag of root back to its default, so a
|
// 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 {
|
if got := strings.Join(args, " "); got != tc.want {
|
||||||
t.Errorf("%+v.Args() = %q, want %q", tc.g, got, tc.want)
|
t.Errorf("%+v.Args() = %q, want %q", tc.g, got, tc.want)
|
||||||
}
|
}
|
||||||
if back := PeekGlobals(args); back != tc.g.normalize() {
|
if back := PeekGlobals(args); back != normalize(tc.g) {
|
||||||
t.Errorf("PeekGlobals(%q) = %+v, want %+v", args, back, tc.g.normalize())
|
t.Errorf("PeekGlobals(%q) = %+v, want %+v", args, back, normalize(tc.g))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"charm.land/bubbles/v2/spinner"
|
"charm.land/bubbles/v2/spinner"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Progress bar colors: the cyan to green gradient of the earlier bonfire
|
// 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,
|
// printAboveLines prints s, one or more lines, above the running live view,
|
||||||
// whichever console started it (bonfire.PrintAbove). It reports false when
|
// whichever console started it (bonfire.PrintAbove). It reports false when
|
||||||
// no view runs.
|
// no view runs.
|
||||||
|
|||||||
@@ -2,61 +2,37 @@ package bonfire
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||||
"github.com/charmbracelet/colorprofile"
|
"github.com/charmbracelet/colorprofile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ColorMode selects how console output decides on color: from the terminal
|
// ColorMode selects how console output decides on color: from the terminal
|
||||||
// and the environment, always, or never.
|
// and the environment, always, or never.
|
||||||
type ColorMode int
|
type ColorMode = consolekit.ColorMode
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ColorAuto colors output when the writer is a terminal and the
|
// ColorAuto colors output when the writer is a terminal and the
|
||||||
// environment allows it (NO_COLOR and TERM=dumb disable color,
|
// environment allows it (NO_COLOR and TERM=dumb disable color,
|
||||||
// FORCE_COLOR enables it on any writer).
|
// FORCE_COLOR enables it on any writer).
|
||||||
ColorAuto ColorMode = iota
|
ColorAuto = consolekit.ColorAuto
|
||||||
// ColorAlways colors output whatever the writer is (--ansi).
|
// ColorAlways colors output whatever the writer is (--ansi).
|
||||||
ColorAlways
|
ColorAlways = consolekit.ColorAlways
|
||||||
// ColorNever writes no escape codes at all (--no-ansi).
|
// 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,
|
// Palette is the shared console look: the status styles of list, help,
|
||||||
// widgets and log lines. Render through a writer that applies the color
|
// widgets and log lines (Success, Current, Warning, Error, ErrorBold, Muted
|
||||||
// profile (DetectProfile), because lipgloss always emits ANSI codes.
|
// and Header). Render through a writer that applies the color profile
|
||||||
type Palette struct {
|
// (DetectProfile), because lipgloss always emits ANSI codes.
|
||||||
// Success marks completed work (green).
|
type Palette = consolekit.Palette
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultPalette returns the console palette, ported from SSU's status
|
// DefaultPalette returns the console palette, ported from SSU's status
|
||||||
// styles: green success, cyan current, yellow warning, red and bold red
|
// styles: green success, cyan current, yellow warning, red and bold red
|
||||||
// errors, gray (240) muted text and bold headers.
|
// errors, gray (240) muted text and bold headers.
|
||||||
func DefaultPalette() Palette {
|
func DefaultPalette() Palette {
|
||||||
return Palette{
|
return consolekit.DefaultPalette()
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetectProfile returns the color profile for console output written to w.
|
// 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
|
// dumb, and otherwise detects the profile from w and environ, which honours
|
||||||
// NO_COLOR and TERM=dumb. A nil environ means os.Environ().
|
// NO_COLOR and TERM=dumb. A nil environ means os.Environ().
|
||||||
func DetectProfile(w io.Writer, mode ColorMode, environ []string) colorprofile.Profile {
|
func DetectProfile(w io.Writer, mode ColorMode, environ []string) colorprofile.Profile {
|
||||||
if mode == ColorNever {
|
return consolekit.DetectProfile(w, mode, environ)
|
||||||
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 ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ The application `main` that `summer build` generates reads the console global op
|
|||||||
|
|
||||||
## Role in the framework
|
## Role in the framework
|
||||||
|
|
||||||
The single owner of the process logger. It builds on `compass` (configuration), `sunscreen` (redaction) and `bonfire` (the console palette and color detection). Framework modules and plugins only ask it for a component logger; they never configure handlers themselves. River logs as component `river` (warn by default), GORM as `db` (through `lagoon`), and each framework module under its package name: `cabana`, `lagoon`, `attach`, `conga`, `cooler`, `postcard`, `beachcomber`, `lighthouse`, `centrifugo`, `flare`, `phrasebook`, `wristband` and `bouncer`. Modules that let the application publish its own `*slog.Logger` (`conga`, `cooler`, `postcard`, `beachcomber`, `lighthouse`, `flare`) still prefer it.
|
The single owner of the process logger. It builds on `compass` (configuration) and `sunscreen` (redaction), and shares the console palette, color detection and control-character sanitizer with `bonfire` through the leaf package `internal/consolekit`, so diary, and every module that logs through it, does not link bonfire, cobra or the TUI stack. Framework modules and plugins only ask it for a component logger; they never configure handlers themselves. River logs as component `river` (warn by default), GORM as `db` (through `lagoon`), and each framework module under its package name: `cabana`, `lagoon`, `attach`, `conga`, `cooler`, `postcard`, `beachcomber`, `lighthouse`, `centrifugo`, `flare`, `phrasebook`, `wristband` and `bouncer`. Modules that let the application publish its own `*slog.Logger` (`conga`, `cooler`, `postcard`, `beachcomber`, `lighthouse`, `flare`) still prefer it.
|
||||||
|
|
||||||
Use cases:
|
Use cases:
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ Environment variables use the `SUMMER_LOG__` prefix with a double underscore bet
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- SummerCMS modules: `bonfire` (palette and color detection), `compass` (configuration), `sunscreen` (redaction).
|
- SummerCMS modules: `compass` (configuration), `sunscreen` (redaction), and the internal package `internal/consolekit` (the palette, color detection and sanitizer it shares with `bonfire`, whose `bonfire.Globals` and `bonfire.ColorMode` are the same types).
|
||||||
- Third-party: `charm.land/lipgloss/v2`, `github.com/charmbracelet/colorprofile` (decided in `.planning/notes/2026-10-21-console-charm-stack.md`), `golang.org/x/term` (whether a console channel writes to a terminal, for `diary.HideOnTTY`).
|
- Third-party: `charm.land/lipgloss/v2`, `github.com/charmbracelet/colorprofile` (decided in `.planning/notes/2026-10-21-console-charm-stack.md`), `golang.org/x/term` (whether a console channel writes to a terminal, for `diary.HideOnTTY`).
|
||||||
- Standard library: `context`, `errors`, `fmt`, `io`, `io/fs`, `log`, `log/slog`, `maps`, `os`, `path/filepath`, `runtime`, `slices`, `strconv`, `strings`, `sync`, `sync/atomic`, `time`, `unicode`.
|
- Standard library: `context`, `errors`, `fmt`, `io`, `io/fs`, `log`, `log/slog`, `maps`, `os`, `path/filepath`, `runtime`, `slices`, `strconv`, `strings`, `sync`, `sync/atomic`, `time`, `unicode`.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||||
"git.golem15.com/golem15/summercms/modules/sunscreen"
|
"git.golem15.com/golem15/summercms/modules/sunscreen"
|
||||||
"github.com/charmbracelet/colorprofile"
|
"github.com/charmbracelet/colorprofile"
|
||||||
)
|
)
|
||||||
@@ -103,7 +103,7 @@ func (b *channelBuilder) build(name string) (slog.Handler, error) {
|
|||||||
case FormatLogfmt:
|
case FormatLogfmt:
|
||||||
leaf = slog.NewTextHandler(w, hopts)
|
leaf = slog.NewTextHandler(w, hopts)
|
||||||
default:
|
default:
|
||||||
profile := bonfire.DetectProfile(w, b.opts.Color, b.opts.Environ)
|
profile := consolekit.DetectProfile(w, b.opts.Color, b.opts.Environ)
|
||||||
if profile > colorprofile.NoTTY {
|
if profile > colorprofile.NoTTY {
|
||||||
leaf = newConsoleHandler(w, ConsoleOptions{Level: lv, Profile: profile}, b.out.source)
|
leaf = newConsoleHandler(w, ConsoleOptions{Level: lv, Profile: profile}, b.out.source)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||||
"github.com/charmbracelet/colorprofile"
|
"github.com/charmbracelet/colorprofile"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
@@ -60,7 +60,7 @@ func newConsoleHandler(w io.Writer, opts ConsoleOptions, source *atomic.Bool) *c
|
|||||||
mu: new(sync.Mutex),
|
mu: new(sync.Mutex),
|
||||||
level: level,
|
level: level,
|
||||||
source: source,
|
source: source,
|
||||||
palette: bonfire.DefaultPalette(),
|
palette: consolekit.DefaultPalette(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ type consoleHandler struct {
|
|||||||
mu *sync.Mutex
|
mu *sync.Mutex
|
||||||
level slog.Leveler
|
level slog.Leveler
|
||||||
source *atomic.Bool
|
source *atomic.Bool
|
||||||
palette bonfire.Palette
|
palette consolekit.Palette
|
||||||
// terminal is true when the handler's writer is a terminal, checked
|
// terminal is true when the handler's writer is a terminal, checked
|
||||||
// once when the handler is built: only then are HideOnTTY records
|
// once when the handler is built: only then are HideOnTTY records
|
||||||
// skipped.
|
// skipped.
|
||||||
@@ -148,12 +148,12 @@ func (h *consoleHandler) Handle(ctx context.Context, r slog.Record) error {
|
|||||||
label, style := levelLabel(r.Level, p)
|
label, style := levelLabel(r.Level, p)
|
||||||
b.WriteString(style.Render(label))
|
b.WriteString(style.Render(label))
|
||||||
b.WriteString(strings.Repeat(" ", max(0, 5-len(label))+1))
|
b.WriteString(strings.Repeat(" ", max(0, 5-len(label))+1))
|
||||||
name := bonfire.Sanitize(component)
|
name := consolekit.Sanitize(component)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
b.WriteString(p.Header.Render(name))
|
b.WriteString(p.Header.Render(name))
|
||||||
}
|
}
|
||||||
b.WriteString(strings.Repeat(" ", max(0, componentWidth-len(name))+1))
|
b.WriteString(strings.Repeat(" ", max(0, componentWidth-len(name))+1))
|
||||||
b.WriteString(bonfire.Sanitize(r.Message))
|
b.WriteString(consolekit.Sanitize(r.Message))
|
||||||
for _, list := range [][]consoleAttr{h.attrs, recAttrs} {
|
for _, list := range [][]consoleAttr{h.attrs, recAttrs} {
|
||||||
for _, a := range list {
|
for _, a := range list {
|
||||||
b.WriteByte(' ')
|
b.WriteByte(' ')
|
||||||
@@ -170,7 +170,7 @@ func (h *consoleHandler) Handle(ctx context.Context, r slog.Record) error {
|
|||||||
}
|
}
|
||||||
// While a bonfire live view (spinner, progress, process view) runs,
|
// While a bonfire live view (spinner, progress, process view) runs,
|
||||||
// the line prints above it instead of tearing it.
|
// the line prints above it instead of tearing it.
|
||||||
if bonfire.PrintAbove(b.String()) {
|
if consolekit.PrintAbove(b.String()) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
b.WriteByte('\n')
|
b.WriteByte('\n')
|
||||||
@@ -212,7 +212,7 @@ func isTerminal(w io.Writer) bool {
|
|||||||
return ok && term.IsTerminal(int(f.Fd()))
|
return ok && term.IsTerminal(int(f.Fd()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func levelLabel(l slog.Level, p bonfire.Palette) (string, lipgloss.Style) {
|
func levelLabel(l slog.Level, p consolekit.Palette) (string, lipgloss.Style) {
|
||||||
switch {
|
switch {
|
||||||
case l < slog.LevelInfo:
|
case l < slog.LevelInfo:
|
||||||
return l.String(), p.Muted
|
return l.String(), p.Muted
|
||||||
@@ -254,7 +254,7 @@ func appendAttr(out []consoleAttr, prefix string, a slog.Attr) []consoleAttr {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
return append(out, consoleAttr{key: bonfire.Sanitize(prefix + a.Key), value: formatValue(a.Value)})
|
return append(out, consoleAttr{key: consolekit.Sanitize(prefix + a.Key), value: formatValue(a.Value)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatValue(v slog.Value) string {
|
func formatValue(v slog.Value) string {
|
||||||
@@ -273,7 +273,7 @@ func formatValue(v slog.Value) string {
|
|||||||
default:
|
default:
|
||||||
s = v.String()
|
s = v.String()
|
||||||
}
|
}
|
||||||
s = bonfire.Sanitize(s)
|
s = consolekit.Sanitize(s)
|
||||||
if s == "" || strings.ContainsAny(s, " \t\"=") {
|
if s == "" || strings.ContainsAny(s, " \t\"=") {
|
||||||
return strconv.Quote(s)
|
return strconv.Quote(s)
|
||||||
}
|
}
|
||||||
|
|||||||
31
modules/diary/deps_test.go
Normal file
31
modules/diary/deps_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package diary_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestDiaryDoesNotLinkTheConsoleStack (IN-07): diary, and the security
|
||||||
|
// modules that log through it, link neither bonfire nor the TUI stack
|
||||||
|
// (bubbletea, bubbles, huh) nor cobra. The console types diary shares with
|
||||||
|
// bonfire live in internal/consolekit.
|
||||||
|
func TestDiaryDoesNotLinkTheConsoleStack(t *testing.T) {
|
||||||
|
goTool, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("go tool not on PATH")
|
||||||
|
}
|
||||||
|
const module = "git.golem15.com/golem15/summercms/modules/"
|
||||||
|
out, err := exec.Command(goTool, "list", "-deps", module+"diary", module+"bouncer", module+"wristband").Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("go list -deps: %v", err)
|
||||||
|
}
|
||||||
|
banned := []string{module + "bonfire", "charm.land/bubbletea/v2", "charm.land/bubbles/v2", "charm.land/huh/v2", "github.com/spf13/cobra"}
|
||||||
|
for _, dep := range strings.Split(string(out), "\n") {
|
||||||
|
for _, b := range banned {
|
||||||
|
if dep == b || strings.HasPrefix(dep, b+"/") {
|
||||||
|
t.Errorf("diary, bouncer or wristband links %s", dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
"git.golem15.com/golem15/summercms/internal/consolekit"
|
||||||
"git.golem15.com/golem15/summercms/modules/compass"
|
"git.golem15.com/golem15/summercms/modules/compass"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ type Options struct {
|
|||||||
// Quiet raises the console channels to warn; files keep their levels.
|
// Quiet raises the console channels to warn; files keep their levels.
|
||||||
Quiet bool
|
Quiet bool
|
||||||
// Color decides on styled console output (bonfire.ColorAuto by default).
|
// Color decides on styled console output (bonfire.ColorAuto by default).
|
||||||
Color bonfire.ColorMode
|
Color consolekit.ColorMode
|
||||||
// Root is the application root that relative daily paths resolve
|
// Root is the application root that relative daily paths resolve
|
||||||
// against; empty means the working directory.
|
// against; empty means the working directory.
|
||||||
Root string
|
Root string
|
||||||
@@ -214,8 +214,9 @@ func setVerbosityLocked(v int, quiet bool) {
|
|||||||
|
|
||||||
// FromGlobals returns the Options for the console global options: their
|
// FromGlobals returns the Options for the console global options: their
|
||||||
// verbosity, quiet switch and color mode. The generated main passes it to
|
// verbosity, quiet switch and color mode. The generated main passes it to
|
||||||
// Bootstrap and Setup.
|
// Bootstrap and Setup. g is a bonfire.Globals: bonfire and diary share the
|
||||||
func FromGlobals(g bonfire.Globals) Options {
|
// type through internal/consolekit, so diary does not link bonfire.
|
||||||
|
func FromGlobals(g consolekit.Globals) Options {
|
||||||
return Options{Verbosity: g.Verbosity, Quiet: g.Quiet, Color: g.Color}
|
return Options{Verbosity: g.Verbosity, Quiet: g.Quiet, Color: g.Color}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +226,7 @@ func FromGlobals(g bonfire.Globals) Options {
|
|||||||
// rebuilds the channels from the same configuration. The generated main
|
// rebuilds the channels from the same configuration. The generated main
|
||||||
// registers it with bonfire.OnGlobals. It holds the setup lock for its
|
// registers it with bonfire.OnGlobals. It holds the setup lock for its
|
||||||
// whole run, so concurrent calls rebuild the channels once.
|
// whole run, so concurrent calls rebuild the channels once.
|
||||||
func ApplyGlobals(g bonfire.Globals) {
|
func ApplyGlobals(g consolekit.Globals) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
st := current.Load()
|
st := current.Load()
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ DIARY_TESTS=(
|
|||||||
# Plan 05 (review fixes): daily retry, retired writers (WR-04, WR-06).
|
# Plan 05 (review fixes): daily retry, retired writers (WR-04, WR-06).
|
||||||
TestDailyRetriesAfterFailure TestRetiredDailyWriterKeepsNoFile
|
TestDailyRetriesAfterFailure TestRetiredDailyWriterKeepsNoFile
|
||||||
TestApplyGlobalsRebuildsOnce
|
TestApplyGlobalsRebuildsOnce
|
||||||
# quick 261028-rqd (info findings): IN-01.
|
# quick 261028-rqd (info findings): IN-01, IN-07.
|
||||||
TestConsoleHideOnTTYTerminal
|
TestConsoleHideOnTTYTerminal TestDiaryDoesNotLinkTheConsoleStack
|
||||||
)
|
)
|
||||||
SUNSCREEN_TESTS=(
|
SUNSCREEN_TESTS=(
|
||||||
# Plan 04: the compatibility entry point.
|
# Plan 04: the compatibility entry point.
|
||||||
|
|||||||
Reference in New Issue
Block a user