diff --git a/README.md b/README.md index e50e4ce2..a071564e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ Users familiar with CSS will feel at home with Lip Gloss. import "github.com/charmbracelet/lipgloss" var style = lipgloss.NewStyle(). - SetString("Hello, kitty."). Bold(true). Foreground(lipgloss.Color("#FAFAFA")). Background(lipgloss.Color("#7D56F4")). @@ -28,7 +27,7 @@ var style = lipgloss.NewStyle(). PaddingLeft(4). Width(22) -fmt.Println(style) +fmt.Println(style.Render("Hello, kitty")) ``` ## Colors @@ -300,7 +299,7 @@ someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda") Generally, you just call the `Render(string...)` method on a `lipgloss.Style`: ```go -style := lipgloss.NewStyle(lipgloss.WithString("Hello,")).Bold(true) +style := lipgloss.NewStyle().Bold(true).SetString("Hello,") fmt.Println(style.Render("kitty.")) // Hello, kitty. fmt.Println(style.Render("puppy.")) // Hello, puppy. ``` @@ -308,29 +307,32 @@ fmt.Println(style.Render("puppy.")) // Hello, puppy. But you could also use the Stringer interface: ```go -var style = lipgloss.NewStyle(lipgloss.WithString("你好,猫咪。")).Bold(true) - -fmt.Println(style) +var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true) +fmt.Println(style) // 你好,猫咪。 ``` ### Custom Renderers -Use custom renderers to enforce rendering your styles in a specific way. You can -specify the color profile to use, True Color, ANSI 256, 8-bit ANSI, or good ol' -ASCII. You can also specify whether or not to assume dark background colors. +Custom renderers allow you to render to a specific outputs. This is +particularly important when you want to render to different outputs and +correctly detect the color profile and dark background status for each, such as +in a server-client situation. ```go -renderer := lipgloss.NewRenderer( - lipgloss.WithColorProfile(termenv.ANSI256), - lipgloss.WithDarkBackground(true), -) +func myLittleHandler(sess ssh.Session) { + // Create a renderer for the client. + renderer := lipgloss.NewRenderer(sess) + + // Create a new style on the renderer. + style := renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"}) -var style = renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"}) -fmt.Println(style.Render("Lip Gloss")) // This will always use the dark background color + // Render. The color profile and dark background state will be correctly detected. + io.WriteString(sess, style.Render("Heyyyyyyy")) +} ``` -This is also useful when using lipgloss with an SSH server like [Wish][wish]. -See the [ssh example][ssh-example] for more details. +For an example on using a custom renderer over SSH with [Wish][wish] see the +[SSH example][ssh-example]. ## Utilities diff --git a/examples/layout/main.go b/examples/layout/main.go index 8ec832b8..e68cf772 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -1,5 +1,7 @@ package main +// This example demonstrates various Lip Gloss style and layout features. + import ( "fmt" "os" diff --git a/examples/ssh/main.go b/examples/ssh/main.go index 8a451078..cd23b052 100644 --- a/examples/ssh/main.go +++ b/examples/ssh/main.go @@ -1,5 +1,14 @@ package main +// This example demonstrates how to use a custom Lip Gloss renderer with Wish, +// a package for building custom SSH servers. +// +// The big advantage to using custom renderers here is that we can accurately +// detect the background color and color profile for each client and render +// against that accordingly. +// +// For details on wish see: https://github.com/charmbracelet/wish/ + import ( "fmt" "log" @@ -14,6 +23,41 @@ import ( "github.com/muesli/termenv" ) +// Available styles. +type styles struct { + bold lipgloss.Style + faint lipgloss.Style + italic lipgloss.Style + underline lipgloss.Style + strikethrough lipgloss.Style + red lipgloss.Style + green lipgloss.Style + yellow lipgloss.Style + blue lipgloss.Style + magenta lipgloss.Style + cyan lipgloss.Style + gray lipgloss.Style +} + +// Create new styles against a given renderer. +func makeStyles(r *lipgloss.Renderer) styles { + return styles{ + bold: r.NewStyle().SetString("bold").Bold(true), + faint: r.NewStyle().SetString("faint").Faint(true), + italic: r.NewStyle().SetString("italic").Italic(true), + underline: r.NewStyle().SetString("underline").Underline(true), + strikethrough: r.NewStyle().SetString("strikethrough").Strikethrough(true), + red: r.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")), + green: r.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")), + yellow: r.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")), + blue: r.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")), + magenta: r.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")), + cyan: r.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")), + gray: r.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")), + } +} + +// Bridge Wish and Termenv so we can query for a user's terminal capabilities. type sshOutput struct { ssh.Session tty *os.File @@ -23,6 +67,10 @@ func (s *sshOutput) Write(p []byte) (int, error) { return s.Session.Write(p) } +func (s *sshOutput) Read(p []byte) (int, error) { + return s.Session.Read(p) +} + func (s *sshOutput) Fd() uintptr { return s.tty.Fd() } @@ -44,86 +92,104 @@ func (s *sshEnviron) Environ() []string { return s.environ } -func outputFromSession(s ssh.Session) *termenv.Output { - sshPty, _, _ := s.Pty() +// Create a termenv.Output from the session. +func outputFromSession(sess ssh.Session) *termenv.Output { + sshPty, _, _ := sess.Pty() _, tty, err := pty.Open() if err != nil { - panic(err) + log.Fatal(err) } o := &sshOutput{ - Session: s, + Session: sess, tty: tty, } - environ := s.Environ() + environ := sess.Environ() environ = append(environ, fmt.Sprintf("TERM=%s", sshPty.Term)) - e := &sshEnviron{ - environ: environ, + e := &sshEnviron{environ: environ} + // We need to use unsafe mode here because the ssh session is not running + // locally and we already know that the session is a TTY. + return termenv.NewOutput(o, termenv.WithUnsafe(), termenv.WithEnvironment(e)) +} + +// Handle SSH requests. +func handler(next ssh.Handler) ssh.Handler { + return func(sess ssh.Session) { + // Get client's output. + clientOutput := outputFromSession(sess) + + pty, _, active := sess.Pty() + if !active { + next(sess) + return + } + width := pty.Window.Width + + // Initialize new renderer for the client. + renderer := lipgloss.NewRenderer(sess) + renderer.SetOutput(clientOutput) + + // Initialize new styles against the renderer. + styles := makeStyles(renderer) + + str := strings.Builder{} + + fmt.Fprintf(&str, "\n\n%s %s %s %s %s", + styles.bold, + styles.faint, + styles.italic, + styles.underline, + styles.strikethrough, + ) + + fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s", + styles.red, + styles.green, + styles.yellow, + styles.blue, + styles.magenta, + styles.cyan, + styles.gray, + ) + + fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s\n\n", + styles.red, + styles.green, + styles.yellow, + styles.blue, + styles.magenta, + styles.cyan, + styles.gray, + ) + + fmt.Fprintf(&str, "%s %t %s\n\n", styles.bold.Copy().UnsetString().Render("Has dark background?"), + renderer.HasDarkBackground(), + renderer.Output().BackgroundColor()) + + block := renderer.Place(width, + lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String(), + lipgloss.WithWhitespaceChars("/"), + lipgloss.WithWhitespaceForeground(lipgloss.AdaptiveColor{Light: "250", Dark: "236"}), + ) + + // Render to client. + wish.WriteString(sess, block) + + next(sess) } - return termenv.NewOutput(o, termenv.WithEnvironment(e)) } func main() { - addr := ":3456" + port := 3456 s, err := wish.NewServer( - wish.WithAddress(addr), + wish.WithAddress(fmt.Sprintf(":%d", port)), wish.WithHostKeyPath("ssh_example"), - wish.WithMiddleware( - func(sh ssh.Handler) ssh.Handler { - return func(s ssh.Session) { - output := outputFromSession(s) - pty, _, active := s.Pty() - if !active { - sh(s) - return - } - w, _ := pty.Window.Width, pty.Window.Height - - renderer := lipgloss.NewRenderer(lipgloss.WithTermenvOutput(output), - lipgloss.WithColorProfile(termenv.TrueColor)) - str := strings.Builder{} - fmt.Fprintf(&str, "\n%s %s %s %s %s", - renderer.NewStyle().SetString("bold").Bold(true), - renderer.NewStyle().SetString("faint").Faint(true), - renderer.NewStyle().SetString("italic").Italic(true), - renderer.NewStyle().SetString("underline").Underline(true), - renderer.NewStyle().SetString("crossout").Strikethrough(true), - ) - - fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s", - renderer.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")), - renderer.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")), - renderer.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")), - renderer.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")), - renderer.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")), - renderer.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")), - renderer.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")), - ) - - fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s\n\n", - renderer.NewStyle().SetString("red").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#E88388")), - renderer.NewStyle().SetString("green").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#A8CC8C")), - renderer.NewStyle().SetString("yellow").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#DBAB79")), - renderer.NewStyle().SetString("blue").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#71BEF2")), - renderer.NewStyle().SetString("magenta").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#D290E4")), - renderer.NewStyle().SetString("cyan").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#66C2CD")), - renderer.NewStyle().SetString("gray").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#B9BFCA")), - ) - - fmt.Fprintf(&str, "%s %t\n", renderer.NewStyle().SetString("Has dark background?").Bold(true), renderer.HasDarkBackground()) - fmt.Fprintln(&str) - - wish.WriteString(s, renderer.Place(w, lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String())) - - sh(s) - } - }, - lm.Middleware(), - ), + wish.WithMiddleware(handler, lm.Middleware()), ) if err != nil { log.Fatal(err) } - log.Printf("Listening on %s", addr) + log.Printf("SSH server listening on port %d", port) + log.Printf("To connect from your local machine run: ssh localhost -p %d", port) if err := s.ListenAndServe(); err != nil { log.Fatal(err) } diff --git a/renderer.go b/renderer.go index 099e4b42..b5e115d1 100644 --- a/renderer.go +++ b/renderer.go @@ -15,7 +15,7 @@ type Renderer struct { hasDarkBackground *bool } -// RendererOption is a function that can be used to configure a Renderer. +// RendererOption is a function that can be used to configure a [Renderer]. type RendererOption func(r *Renderer) // DefaultRenderer returns the default renderer. @@ -68,10 +68,10 @@ func ColorProfile() termenv.Profile { // // Available color profiles are: // -// termenv.Ascii (no color, 1-bit) -// termenv.ANSI (16 colors, 4-bit) -// termenv.ANSI256 (256 colors, 8-bit) -// termenv.TrueColor (16,777,216 colors, 24-bit) +// termenv.Ascii // no color, 1-bit +// termenv.ANSI //16 colors, 4-bit +// termenv.ANSI256 // 256 colors, 8-bit +// termenv.TrueColor // 16,777,216 colors, 24-bit // // This function is thread-safe. func (r *Renderer) SetColorProfile(p termenv.Profile) { @@ -88,10 +88,10 @@ func (r *Renderer) SetColorProfile(p termenv.Profile) { // // Available color profiles are: // -// termenv.Ascii (no color, 1-bit) -// termenv.ANSI (16 colors, 4-bit) -// termenv.ANSI256 (256 colors, 8-bit) -// termenv.TrueColor (16,777,216 colors, 24-bit) +// termenv.Ascii // no color, 1-bit +// termenv.ANSI //16 colors, 4-bit +// termenv.ANSI256 // 256 colors, 8-bit +// termenv.TrueColor // 16,777,216 colors, 24-bit // // This function is thread-safe. func SetColorProfile(p termenv.Profile) { @@ -103,7 +103,9 @@ func HasDarkBackground() bool { return renderer.HasDarkBackground() } -// HasDarkBackground returns whether or not the terminal has a dark background. +// HasDarkBackground returns whether or not the renderer will render to a dark +// background. A dark background can either be auto-detected, or set explicitly +// on the renderer. func (r *Renderer) HasDarkBackground() bool { if r.hasDarkBackground != nil { return *r.hasDarkBackground