Skip to content

Commit 19ca9a3

Browse files
docs: renderer documentation (#175)
* docs(readme): add some context to the examples * docs(readme): revert to render-function-based initial example * docs(readme): drop extraneous stringer usage * docs(readme): copyedits * docs(renderer): minor documentation improvements * docs(readme): edit renderer section to focus on custom outputs * docs(readme): for now just use SetString to illustrate stringer * docs(readme): re-add Ayman's clever stringer example * docs(examples): tidy up wish example * docs(examples): improve wish example * docs(examples): session is an io.Writer Co-authored-by: Ayman Bagabas <[email protected]> * docs(examples): add missing pty argument Co-authored-by: Ayman Bagabas <[email protected]> * docs(example): remove extra space * fix(examples): use termenv output --------- Co-authored-by: Ayman Bagabas <[email protected]>
1 parent b3440ac commit 19ca9a3

File tree

4 files changed

+162
-90
lines changed

4 files changed

+162
-90
lines changed

README.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@ Users familiar with CSS will feel at home with Lip Gloss.
2020
import "github.com/charmbracelet/lipgloss"
2121

2222
var style = lipgloss.NewStyle().
23-
SetString("Hello, kitty.").
2423
Bold(true).
2524
Foreground(lipgloss.Color("#FAFAFA")).
2625
Background(lipgloss.Color("#7D56F4")).
2726
PaddingTop(2).
2827
PaddingLeft(4).
2928
Width(22)
3029

31-
fmt.Println(style)
30+
fmt.Println(style.Render("Hello, kitty"))
3231
```
3332

3433
## Colors
@@ -300,37 +299,40 @@ someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda")
300299
Generally, you just call the `Render(string...)` method on a `lipgloss.Style`:
301300

302301
```go
303-
style := lipgloss.NewStyle(lipgloss.WithString("Hello,")).Bold(true)
302+
style := lipgloss.NewStyle().Bold(true).SetString("Hello,")
304303
fmt.Println(style.Render("kitty.")) // Hello, kitty.
305304
fmt.Println(style.Render("puppy.")) // Hello, puppy.
306305
```
307306

308307
But you could also use the Stringer interface:
309308

310309
```go
311-
var style = lipgloss.NewStyle(lipgloss.WithString("你好,猫咪。")).Bold(true)
312-
313-
fmt.Println(style)
310+
var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true)
311+
fmt.Println(style) // 你好,猫咪。
314312
```
315313

316314
### Custom Renderers
317315

318-
Use custom renderers to enforce rendering your styles in a specific way. You can
319-
specify the color profile to use, True Color, ANSI 256, 8-bit ANSI, or good ol'
320-
ASCII. You can also specify whether or not to assume dark background colors.
316+
Custom renderers allow you to render to a specific outputs. This is
317+
particularly important when you want to render to different outputs and
318+
correctly detect the color profile and dark background status for each, such as
319+
in a server-client situation.
321320

322321
```go
323-
renderer := lipgloss.NewRenderer(
324-
lipgloss.WithColorProfile(termenv.ANSI256),
325-
lipgloss.WithDarkBackground(true),
326-
)
322+
func myLittleHandler(sess ssh.Session) {
323+
// Create a renderer for the client.
324+
renderer := lipgloss.NewRenderer(sess)
325+
326+
// Create a new style on the renderer.
327+
style := renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"})
327328

328-
var style = renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"})
329-
fmt.Println(style.Render("Lip Gloss")) // This will always use the dark background color
329+
// Render. The color profile and dark background state will be correctly detected.
330+
io.WriteString(sess, style.Render("Heyyyyyyy"))
331+
}
330332
```
331333

332-
This is also useful when using lipgloss with an SSH server like [Wish][wish].
333-
See the [ssh example][ssh-example] for more details.
334+
For an example on using a custom renderer over SSH with [Wish][wish] see the
335+
[SSH example][ssh-example].
334336

335337
## Utilities
336338

examples/layout/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package main
22

3+
// This example demonstrates various Lip Gloss style and layout features.
4+
35
import (
46
"fmt"
57
"os"

examples/ssh/main.go

Lines changed: 129 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
package main
22

3+
// This example demonstrates how to use a custom Lip Gloss renderer with Wish,
4+
// a package for building custom SSH servers.
5+
//
6+
// The big advantage to using custom renderers here is that we can accurately
7+
// detect the background color and color profile for each client and render
8+
// against that accordingly.
9+
//
10+
// For details on wish see: https://github.com/charmbracelet/wish/
11+
312
import (
413
"fmt"
514
"log"
@@ -14,6 +23,41 @@ import (
1423
"github.com/muesli/termenv"
1524
)
1625

26+
// Available styles.
27+
type styles struct {
28+
bold lipgloss.Style
29+
faint lipgloss.Style
30+
italic lipgloss.Style
31+
underline lipgloss.Style
32+
strikethrough lipgloss.Style
33+
red lipgloss.Style
34+
green lipgloss.Style
35+
yellow lipgloss.Style
36+
blue lipgloss.Style
37+
magenta lipgloss.Style
38+
cyan lipgloss.Style
39+
gray lipgloss.Style
40+
}
41+
42+
// Create new styles against a given renderer.
43+
func makeStyles(r *lipgloss.Renderer) styles {
44+
return styles{
45+
bold: r.NewStyle().SetString("bold").Bold(true),
46+
faint: r.NewStyle().SetString("faint").Faint(true),
47+
italic: r.NewStyle().SetString("italic").Italic(true),
48+
underline: r.NewStyle().SetString("underline").Underline(true),
49+
strikethrough: r.NewStyle().SetString("strikethrough").Strikethrough(true),
50+
red: r.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")),
51+
green: r.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")),
52+
yellow: r.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")),
53+
blue: r.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")),
54+
magenta: r.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")),
55+
cyan: r.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")),
56+
gray: r.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")),
57+
}
58+
}
59+
60+
// Bridge Wish and Termenv so we can query for a user's terminal capabilities.
1761
type sshOutput struct {
1862
ssh.Session
1963
tty *os.File
@@ -23,6 +67,10 @@ func (s *sshOutput) Write(p []byte) (int, error) {
2367
return s.Session.Write(p)
2468
}
2569

70+
func (s *sshOutput) Read(p []byte) (int, error) {
71+
return s.Session.Read(p)
72+
}
73+
2674
func (s *sshOutput) Fd() uintptr {
2775
return s.tty.Fd()
2876
}
@@ -44,86 +92,104 @@ func (s *sshEnviron) Environ() []string {
4492
return s.environ
4593
}
4694

47-
func outputFromSession(s ssh.Session) *termenv.Output {
48-
sshPty, _, _ := s.Pty()
95+
// Create a termenv.Output from the session.
96+
func outputFromSession(sess ssh.Session) *termenv.Output {
97+
sshPty, _, _ := sess.Pty()
4998
_, tty, err := pty.Open()
5099
if err != nil {
51-
panic(err)
100+
log.Fatal(err)
52101
}
53102
o := &sshOutput{
54-
Session: s,
103+
Session: sess,
55104
tty: tty,
56105
}
57-
environ := s.Environ()
106+
environ := sess.Environ()
58107
environ = append(environ, fmt.Sprintf("TERM=%s", sshPty.Term))
59-
e := &sshEnviron{
60-
environ: environ,
108+
e := &sshEnviron{environ: environ}
109+
// We need to use unsafe mode here because the ssh session is not running
110+
// locally and we already know that the session is a TTY.
111+
return termenv.NewOutput(o, termenv.WithUnsafe(), termenv.WithEnvironment(e))
112+
}
113+
114+
// Handle SSH requests.
115+
func handler(next ssh.Handler) ssh.Handler {
116+
return func(sess ssh.Session) {
117+
// Get client's output.
118+
clientOutput := outputFromSession(sess)
119+
120+
pty, _, active := sess.Pty()
121+
if !active {
122+
next(sess)
123+
return
124+
}
125+
width := pty.Window.Width
126+
127+
// Initialize new renderer for the client.
128+
renderer := lipgloss.NewRenderer(sess)
129+
renderer.SetOutput(clientOutput)
130+
131+
// Initialize new styles against the renderer.
132+
styles := makeStyles(renderer)
133+
134+
str := strings.Builder{}
135+
136+
fmt.Fprintf(&str, "\n\n%s %s %s %s %s",
137+
styles.bold,
138+
styles.faint,
139+
styles.italic,
140+
styles.underline,
141+
styles.strikethrough,
142+
)
143+
144+
fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s",
145+
styles.red,
146+
styles.green,
147+
styles.yellow,
148+
styles.blue,
149+
styles.magenta,
150+
styles.cyan,
151+
styles.gray,
152+
)
153+
154+
fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s\n\n",
155+
styles.red,
156+
styles.green,
157+
styles.yellow,
158+
styles.blue,
159+
styles.magenta,
160+
styles.cyan,
161+
styles.gray,
162+
)
163+
164+
fmt.Fprintf(&str, "%s %t %s\n\n", styles.bold.Copy().UnsetString().Render("Has dark background?"),
165+
renderer.HasDarkBackground(),
166+
renderer.Output().BackgroundColor())
167+
168+
block := renderer.Place(width,
169+
lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String(),
170+
lipgloss.WithWhitespaceChars("/"),
171+
lipgloss.WithWhitespaceForeground(lipgloss.AdaptiveColor{Light: "250", Dark: "236"}),
172+
)
173+
174+
// Render to client.
175+
wish.WriteString(sess, block)
176+
177+
next(sess)
61178
}
62-
return termenv.NewOutput(o, termenv.WithEnvironment(e))
63179
}
64180

65181
func main() {
66-
addr := ":3456"
182+
port := 3456
67183
s, err := wish.NewServer(
68-
wish.WithAddress(addr),
184+
wish.WithAddress(fmt.Sprintf(":%d", port)),
69185
wish.WithHostKeyPath("ssh_example"),
70-
wish.WithMiddleware(
71-
func(sh ssh.Handler) ssh.Handler {
72-
return func(s ssh.Session) {
73-
output := outputFromSession(s)
74-
pty, _, active := s.Pty()
75-
if !active {
76-
sh(s)
77-
return
78-
}
79-
w, _ := pty.Window.Width, pty.Window.Height
80-
81-
renderer := lipgloss.NewRenderer(lipgloss.WithTermenvOutput(output),
82-
lipgloss.WithColorProfile(termenv.TrueColor))
83-
str := strings.Builder{}
84-
fmt.Fprintf(&str, "\n%s %s %s %s %s",
85-
renderer.NewStyle().SetString("bold").Bold(true),
86-
renderer.NewStyle().SetString("faint").Faint(true),
87-
renderer.NewStyle().SetString("italic").Italic(true),
88-
renderer.NewStyle().SetString("underline").Underline(true),
89-
renderer.NewStyle().SetString("crossout").Strikethrough(true),
90-
)
91-
92-
fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s",
93-
renderer.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")),
94-
renderer.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")),
95-
renderer.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")),
96-
renderer.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")),
97-
renderer.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")),
98-
renderer.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")),
99-
renderer.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")),
100-
)
101-
102-
fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s\n\n",
103-
renderer.NewStyle().SetString("red").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#E88388")),
104-
renderer.NewStyle().SetString("green").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#A8CC8C")),
105-
renderer.NewStyle().SetString("yellow").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#DBAB79")),
106-
renderer.NewStyle().SetString("blue").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#71BEF2")),
107-
renderer.NewStyle().SetString("magenta").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#D290E4")),
108-
renderer.NewStyle().SetString("cyan").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#66C2CD")),
109-
renderer.NewStyle().SetString("gray").Foreground(lipgloss.Color("0")).Background(lipgloss.Color("#B9BFCA")),
110-
)
111-
112-
fmt.Fprintf(&str, "%s %t\n", renderer.NewStyle().SetString("Has dark background?").Bold(true), renderer.HasDarkBackground())
113-
fmt.Fprintln(&str)
114-
115-
wish.WriteString(s, renderer.Place(w, lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String()))
116-
117-
sh(s)
118-
}
119-
},
120-
lm.Middleware(),
121-
),
186+
wish.WithMiddleware(handler, lm.Middleware()),
122187
)
123188
if err != nil {
124189
log.Fatal(err)
125190
}
126-
log.Printf("Listening on %s", addr)
191+
log.Printf("SSH server listening on port %d", port)
192+
log.Printf("To connect from your local machine run: ssh localhost -p %d", port)
127193
if err := s.ListenAndServe(); err != nil {
128194
log.Fatal(err)
129195
}

renderer.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Renderer struct {
1515
hasDarkBackground *bool
1616
}
1717

18-
// RendererOption is a function that can be used to configure a Renderer.
18+
// RendererOption is a function that can be used to configure a [Renderer].
1919
type RendererOption func(r *Renderer)
2020

2121
// DefaultRenderer returns the default renderer.
@@ -68,10 +68,10 @@ func ColorProfile() termenv.Profile {
6868
//
6969
// Available color profiles are:
7070
//
71-
// termenv.Ascii (no color, 1-bit)
72-
// termenv.ANSI (16 colors, 4-bit)
73-
// termenv.ANSI256 (256 colors, 8-bit)
74-
// termenv.TrueColor (16,777,216 colors, 24-bit)
71+
// termenv.Ascii // no color, 1-bit
72+
// termenv.ANSI //16 colors, 4-bit
73+
// termenv.ANSI256 // 256 colors, 8-bit
74+
// termenv.TrueColor // 16,777,216 colors, 24-bit
7575
//
7676
// This function is thread-safe.
7777
func (r *Renderer) SetColorProfile(p termenv.Profile) {
@@ -88,10 +88,10 @@ func (r *Renderer) SetColorProfile(p termenv.Profile) {
8888
//
8989
// Available color profiles are:
9090
//
91-
// termenv.Ascii (no color, 1-bit)
92-
// termenv.ANSI (16 colors, 4-bit)
93-
// termenv.ANSI256 (256 colors, 8-bit)
94-
// termenv.TrueColor (16,777,216 colors, 24-bit)
91+
// termenv.Ascii // no color, 1-bit
92+
// termenv.ANSI //16 colors, 4-bit
93+
// termenv.ANSI256 // 256 colors, 8-bit
94+
// termenv.TrueColor // 16,777,216 colors, 24-bit
9595
//
9696
// This function is thread-safe.
9797
func SetColorProfile(p termenv.Profile) {
@@ -103,7 +103,9 @@ func HasDarkBackground() bool {
103103
return renderer.HasDarkBackground()
104104
}
105105

106-
// HasDarkBackground returns whether or not the terminal has a dark background.
106+
// HasDarkBackground returns whether or not the renderer will render to a dark
107+
// background. A dark background can either be auto-detected, or set explicitly
108+
// on the renderer.
107109
func (r *Renderer) HasDarkBackground() bool {
108110
if r.hasDarkBackground != nil {
109111
return *r.hasDarkBackground

0 commit comments

Comments
 (0)