Add files via upload

This commit is contained in:
公明
2026-07-11 11:41:00 +08:00
committed by GitHub
parent 9ac5fd33ec
commit 62efc81993
4 changed files with 324 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
package termout
import (
"fmt"
"os"
"strings"
)
// StartupWebUIOptions configures the startup Web UI banner.
type StartupWebUIOptions struct {
Scheme string
Port int
SelfSigned bool
HTTPRedirect bool
}
// PrintConfigCreated prints a short notice when config.yaml is bootstrapped.
func PrintConfigCreated() {
s := New(os.Stdout)
s.Println("")
s.Println(s.Green("✔ ") + s.Bold("已创建 config.yaml") + s.Dim("(来自 config.example.yaml"))
s.BlankLine()
}
// PrintStartupWebUI prints a colored startup banner for the Web UI.
func PrintStartupWebUI(opts StartupWebUIOptions) {
s := New(os.Stdout)
scheme := opts.Scheme
if scheme == "" {
scheme = "http"
}
port := opts.Port
if port <= 0 {
port = 8080
}
url := fmt.Sprintf("%s://127.0.0.1:%d/", scheme, port)
s.BlankLine()
s.Println(s.Bold(s.Cyan("CYBERSTRIKE AI")) + s.Dim(" / secure workspace"))
s.Println(s.Dim(strings.Repeat("─", 60)))
s.Println(s.Green("● ONLINE") + " " + s.Bold(s.White(url)))
if opts.SelfSigned {
s.Println(s.Dim(" TLS ") + s.Yellow("self-signed") + s.Dim(" · accept the browser warning once"))
}
if opts.HTTPRedirect {
s.Println(s.Dim(" Redirect ") + fmt.Sprintf("http://127.0.0.1:%d/ → HTTPS", port))
}
s.BlankLine()
}
// PrintBootstrapAdminCredentials prints the initial admin password banner.
func PrintBootstrapAdminCredentials(password string) {
password = strings.TrimSpace(password)
if password == "" {
return
}
s := New(os.Stdout)
s.Println(s.Bold(s.Yellow("ADMIN SETUP REQUIRED")))
s.Println(s.Dim(strings.Repeat("─", 60)))
s.Println(s.Dim(" Username ") + s.Bold(s.White("admin")))
s.Println(s.Dim(" Password ") + s.Bold(s.Yellow(password)))
s.BlankLine()
s.Println(s.Yellow(" ! ") + s.White("Store this password securely. It is shown only once."))
s.Println(s.Dim(" Change it in Settings immediately after signing in."))
s.BlankLine()
}
+76
View File
@@ -0,0 +1,76 @@
package termout
import (
"strings"
"testing"
)
func TestDisplayWidthEmoji(t *testing.T) {
if got := displayWidth("🚀"); got != 2 {
t.Fatalf("displayWidth(emoji) = %d, want 2", got)
}
if got := displayWidth("ab"); got != 2 {
t.Fatalf("displayWidth(ab) = %d, want 2", got)
}
}
func TestDisplayWidthIgnoresANSI(t *testing.T) {
s := New(nil)
colored := s.Bold("admin")
if got := displayWidth(colored); got != 5 {
t.Fatalf("displayWidth colored = %d, want 5", got)
}
}
func TestPadRightDisplay(t *testing.T) {
got := padRightDisplay("pwd", 10)
if displayWidth(got) != 10 {
t.Fatalf("padded width = %d, want 10", displayWidth(got))
}
}
func TestColorDisabledWithoutTTY(t *testing.T) {
s := New(nil)
if s.enabled {
t.Fatal("expected colors disabled for nil writer")
}
if got := s.Cyan("x"); got != "x" {
t.Fatalf("Cyan without TTY = %q, want plain text", got)
}
}
func TestPrintBootstrapAdminCredentialsEmpty(t *testing.T) {
PrintBootstrapAdminCredentials(" ")
}
func TestPrintStartupWebUIOptions(t *testing.T) {
PrintStartupWebUI(StartupWebUIOptions{
Scheme: "https",
Port: 8080,
SelfSigned: true,
HTTPRedirect: true,
})
}
func TestBoxRowAlignedWidth(t *testing.T) {
s := New(nil)
rows := []string{
s.Bold("CyberStrikeAI") + s.White(" is ready"),
s.Dim("Web UI ") + s.Bold("https://127.0.0.1:8080/"),
}
inner := maxDisplayWidth(rows...)
for _, row := range rows {
line := s.boxRow(inner, row)
if !strings.Contains(line, "│") {
t.Fatalf("box row missing border: %q", line)
}
}
}
func TestMaxDisplayWidth(t *testing.T) {
short := "abc"
long := "https://127.0.0.1:8080/"
if got := maxDisplayWidth(short, long); got != displayWidth(long) {
t.Fatalf("maxDisplayWidth = %d, want %d", got, displayWidth(long))
}
}
+108
View File
@@ -0,0 +1,108 @@
package termout
import (
"fmt"
"io"
"os"
"strings"
)
const (
codeReset = "\033[0m"
codeBold = "\033[1m"
codeDim = "\033[2m"
codeRed = "\033[31m"
codeGreen = "\033[32m"
codeYellow = "\033[33m"
codeBlue = "\033[34m"
codeCyan = "\033[36m"
codeWhite = "\033[97m"
)
// Style wraps ANSI styling with TTY / NO_COLOR awareness.
type Style struct {
out io.Writer
enabled bool
}
// New creates a Style writing to out (typically os.Stdout).
func New(out io.Writer) *Style {
return &Style{out: out, enabled: colorEnabled(out)}
}
func colorEnabled(w io.Writer) bool {
if strings.TrimSpace(os.Getenv("NO_COLOR")) != "" {
return false
}
force := strings.TrimSpace(os.Getenv("FORCE_COLOR"))
if force == "1" || strings.EqualFold(force, "true") || strings.EqualFold(force, "yes") {
return true
}
f, ok := w.(*os.File)
if !ok {
return false
}
stat, err := f.Stat()
if err != nil {
return false
}
return stat.Mode()&os.ModeCharDevice != 0
}
func (s *Style) paint(code, text string) string {
if !s.enabled || text == "" {
return text
}
return code + text + codeReset
}
func (s *Style) Bold(text string) string { return s.paint(codeBold, text) }
func (s *Style) Dim(text string) string { return s.paint(codeDim, text) }
func (s *Style) Red(text string) string { return s.paint(codeRed, text) }
func (s *Style) Green(text string) string { return s.paint(codeGreen, text) }
func (s *Style) Yellow(text string) string { return s.paint(codeYellow, text) }
func (s *Style) Blue(text string) string { return s.paint(codeBlue, text) }
func (s *Style) Cyan(text string) string { return s.paint(codeCyan, text) }
func (s *Style) White(text string) string { return s.paint(codeWhite, text) }
func (s *Style) Println(text string) {
_, _ = fmt.Fprintln(s.out, text)
}
func (s *Style) Printf(format string, args ...interface{}) {
_, _ = fmt.Fprintf(s.out, format, args...)
}
func (s *Style) BlankLine() {
s.Println("")
}
func (s *Style) boxTop(innerWidth int) string {
return s.Cyan("╭" + strings.Repeat("─", innerWidth+2) + "╮")
}
func (s *Style) boxBottom(innerWidth int) string {
return s.Cyan("╰" + strings.Repeat("─", innerWidth+2) + "╯")
}
func (s *Style) boxRow(innerWidth int, content string) string {
return s.Cyan("│ ") + padRightDisplay(content, innerWidth) + s.Cyan(" │")
}
func (s *Style) printBox(rows []string, minInner, maxInner int) {
inner := maxDisplayWidth(rows...)
if inner < minInner {
inner = minInner
}
if maxInner > 0 && inner > maxInner {
inner = maxInner
}
s.BlankLine()
s.Println(s.boxTop(inner))
for _, row := range rows {
s.Println(s.boxRow(inner, row))
}
s.Println(s.boxBottom(inner))
s.BlankLine()
}
+73
View File
@@ -0,0 +1,73 @@
package termout
import (
"regexp"
"strings"
"unicode/utf8"
"golang.org/x/text/width"
)
var ansiEscapeRe = regexp.MustCompile(`\x1b\[[0-9;]*m`)
// displayWidth returns the terminal display width of text, ignoring ANSI codes.
func displayWidth(text string) int {
plain := ansiEscapeRe.ReplaceAllString(text, "")
w := 0
for _, r := range plain {
w += runeDisplayWidth(r)
}
return w
}
func runeDisplayWidth(r rune) int {
if r == utf8.RuneError {
return 0
}
// Most emoji / symbols render as double-width in modern terminals.
if isEmojiLikeRune(r) {
return 2
}
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
default:
return 1
}
}
func isEmojiLikeRune(r rune) bool {
switch {
case r >= 0x1F300 && r <= 0x1FAFF: // pictographs / emoji
return true
case r >= 0x2600 && r <= 0x27BF: // misc symbols
return true
case r >= 0x2300 && r <= 0x23FF: // misc technical (⌚ etc.)
return true
case r >= 0x2B50 && r <= 0x2B55:
return true
default:
return false
}
}
func padRightDisplay(text string, target int) string {
if target <= 0 {
return ""
}
gap := target - displayWidth(text)
if gap <= 0 {
return text
}
return text + strings.Repeat(" ", gap)
}
func maxDisplayWidth(rows ...string) int {
max := 0
for _, row := range rows {
if w := displayWidth(row); w > max {
max = w
}
}
return max
}