Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation #175

Merged
merged 14 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ 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")).
PaddingTop(2).
PaddingLeft(4).
Width(22)

fmt.Println(style)
fmt.Println(style.Render("Hello, kitty"))
```

## Colors
Expand Down Expand Up @@ -300,37 +299,40 @@ 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.
```

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

Expand Down
2 changes: 2 additions & 0 deletions examples/layout/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package main

// This example demonstrates various Lip Gloss style and layout features.

import (
"fmt"
"os"
Expand Down
192 changes: 129 additions & 63 deletions examples/ssh/main.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 {
aymanbagabas marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -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()
}
Expand All @@ -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)
}
Expand Down
22 changes: 12 additions & 10 deletions renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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
Expand Down