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

Add termenv.Output #86

Merged
merged 23 commits into from Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
93a76d0
Add termenv.Output
muesli Feb 4, 2022
81e352d
Move Profile into a separate file
muesli Feb 4, 2022
3d9f492
Use new Termenv API in tests
muesli Feb 4, 2022
51b33bc
Support Profile setting in Style
muesli Feb 4, 2022
3543338
Fix test
muesli Feb 4, 2022
6d2f51c
Add screen command tests
muesli Feb 4, 2022
23fa0a4
fix: swap order of profiles
muesli Jun 1, 2022
f08d5d2
feat: use interfaces and simplify Output
aymanbagabas Apr 29, 2022
ec1a7f9
feat: add support for osc52 system command
aymanbagabas Apr 29, 2022
b49145d
Add ssh example
aymanbagabas May 27, 2022
6a725b8
fix: gitignore line ending
muesli Jun 1, 2022
37a2cd6
feat: use functional options to initialize Output
muesli Jun 2, 2022
cb53485
feat: cache fore- & background colors after first query
muesli Jun 4, 2022
f402de5
feat: make termenv interoperable with plain io.Writers
muesli Jun 4, 2022
c9d980f
feat: add Output.WriteString
muesli Jun 6, 2022
f79efd2
fix: not a formatted string
muesli Jun 6, 2022
2371ca4
docs: mark deprecated methods
muesli Jun 8, 2022
7b47bb6
feat: add WithCache to initialize an Output with cached color informa…
muesli Jun 15, 2022
8e7e9ce
fix: DefaultOutput returns an Output without a pre-set profile
muesli Jun 15, 2022
bc0f503
fix: check whether output is a TTY on Windows
muesli Aug 30, 2022
f52749e
fix: update ssh example to use tty instead of pty
aymanbagabas Jun 15, 2022
52a183e
chore: add bracketed paste methods to Output
muesli Aug 30, 2022
d729275
chore: rename WithCache to the more appropriate WithColorCache
muesli Sep 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
174 changes: 93 additions & 81 deletions README.md

Large diffs are not rendered by default.

68 changes: 0 additions & 68 deletions color.go
Expand Up @@ -3,9 +3,7 @@ package termenv
import (
"errors"
"fmt"
"image/color"
"math"
"strconv"
"strings"

"github.com/lucasb-eyer/go-colorful"
Expand Down Expand Up @@ -69,72 +67,6 @@ func ConvertToRGB(c Color) colorful.Color {
return ch
}

// Convert transforms a given Color to a Color supported within the Profile.
func (p Profile) Convert(c Color) Color {
if p == Ascii {
return NoColor{}
}

switch v := c.(type) {
case ANSIColor:
return v

case ANSI256Color:
if p == ANSI {
return ansi256ToANSIColor(v)
}
return v

case RGBColor:
h, err := colorful.Hex(string(v))
if err != nil {
return nil
}
if p < TrueColor {
ac := hexToANSI256Color(h)
if p == ANSI {
return ansi256ToANSIColor(ac)
}
return ac
}
return v
}

return c
}

// Color creates a Color from a string. Valid inputs are hex colors, as well as
// ANSI color codes (0-15, 16-255).
func (p Profile) Color(s string) Color {
if len(s) == 0 {
return nil
}

var c Color
if strings.HasPrefix(s, "#") {
c = RGBColor(s)
} else {
i, err := strconv.Atoi(s)
if err != nil {
return nil
}

if i < 16 {
c = ANSIColor(i)
} else {
c = ANSI256Color(i)
}
}

return p.Convert(c)
}

// FromColor creates a Color from a color.Color.
func (p Profile) FromColor(c color.Color) Color {
col, _ := colorful.MakeColor(c)
return p.Color(col.Hex())
}

// Sequence returns the ANSI Sequence for the color.
func (c NoColor) Sequence(bg bool) string {
return ""
Expand Down
16 changes: 16 additions & 0 deletions copy.go
@@ -0,0 +1,16 @@
package termenv

import (
"github.com/aymanbagabas/go-osc52"
)

// Copy copies text to clipboard using OSC 52 escape sequence.
func (o Output) Copy(str string) {
out := osc52.NewOutput(o.tty, o.environ.Environ())
out.Copy(str)
}

// Copy copies text to clipboard using OSC 52 escape sequence.
func Copy(str string) {
output.Copy(str)
}
9 changes: 9 additions & 0 deletions examples/hello-world/main.go
Expand Up @@ -40,4 +40,13 @@ func main() {
fmt.Printf("\n\t%s %s\n", termenv.String("Has foreground color").Bold(), termenv.ForegroundColor())
fmt.Printf("\t%s %s\n", termenv.String("Has background color").Bold(), termenv.BackgroundColor())
fmt.Printf("\t%s %t\n", termenv.String("Has dark background?").Bold(), termenv.HasDarkBackground())
fmt.Println()

hw := "Hello, world!"
termenv.Copy(hw)
fmt.Printf("\t%q copied to clipboard\n", hw)
fmt.Println()

fmt.Printf("\t%s", termenv.Hyperlink("http://example.com", "This is a link"))
fmt.Println()
}
26 changes: 26 additions & 0 deletions examples/ssh/go.mod
@@ -0,0 +1,26 @@
module github.com/muesli/termenv/examples/ssh

go 1.18

require (
github.com/charmbracelet/wish v0.4.0
github.com/creack/pty v1.1.18
github.com/gliderlabs/ssh v0.3.4
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
)

require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/caarlos0/sshmarshal v0.1.0 // indirect
github.com/charmbracelet/keygen v0.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
)

replace github.com/muesli/termenv => ../../
38 changes: 38 additions & 0 deletions examples/ssh/go.sum
@@ -0,0 +1,38 @@
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
github.com/charmbracelet/wish v0.4.0 h1:MLo8JjyvSK1lYkhPCzpI+8pwpKO8cUDP4GZtEoOKu4c=
github.com/charmbracelet/wish v0.4.0/go.mod h1:jRL2Shd80OlP77bR8x3v8PrLCtkYCc/1nUV1eGexNj0=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
128 changes: 128 additions & 0 deletions examples/ssh/main.go
@@ -0,0 +1,128 @@
package main

import (
"fmt"
"log"
"os"
"strings"

"github.com/charmbracelet/wish"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
"github.com/muesli/termenv"
)

type sshOutput struct {
ssh.Session
tty *os.File
}

func (s *sshOutput) Write(p []byte) (int, error) {
return s.Session.Write(p)
}

func (s *sshOutput) Fd() uintptr {
return s.tty.Fd()
}

type sshEnviron struct {
environ []string
}

func (s *sshEnviron) Getenv(key string) string {
for _, v := range s.environ {
if strings.HasPrefix(v, key+"=") {
return v[len(key)+1:]
}
}
return ""
}

func (s *sshEnviron) Environ() []string {
return s.environ
}

func outputFromSession(s ssh.Session) *termenv.Output {
sshPty, _, _ := s.Pty()
_, tty, err := pty.Open()
if err != nil {
panic(err)
}
o := &sshOutput{
Session: s,
tty: tty,
}
environ := s.Environ()
environ = append(environ, fmt.Sprintf("TERM=%s", sshPty.Term))
e := &sshEnviron{
environ: environ,
}
return termenv.NewOutput(o, termenv.WithEnvironment(e))
}

func main() {
s, err := wish.NewServer(
wish.WithAddress(":2345"),
wish.WithHostKeyPath("termenv"),
wish.WithMiddleware(
func(sh ssh.Handler) ssh.Handler {
return func(s ssh.Session) {
output := outputFromSession(s)

p := output.ColorProfile()
fmt.Fprintf(s, "\tColor Profile: %d\n", p)

fmt.Fprintf(s, "\n\t%s %s %s %s %s",
output.String("bold").Bold(),
output.String("faint").Faint(),
output.String("italic").Italic(),
output.String("underline").Underline(),
output.String("crossout").CrossOut(),
)

fmt.Fprintf(s, "\n\t%s %s %s %s %s %s %s",
output.String("red").Foreground(p.Color("#E88388")),
output.String("green").Foreground(p.Color("#A8CC8C")),
output.String("yellow").Foreground(p.Color("#DBAB79")),
output.String("blue").Foreground(p.Color("#71BEF2")),
output.String("magenta").Foreground(p.Color("#D290E4")),
output.String("cyan").Foreground(p.Color("#66C2CD")),
output.String("gray").Foreground(p.Color("#B9BFCA")),
)

fmt.Fprintf(s, "\n\t%s %s %s %s %s %s %s\n\n",
output.String("red").Foreground(p.Color("0")).Background(p.Color("#E88388")),
output.String("green").Foreground(p.Color("0")).Background(p.Color("#A8CC8C")),
output.String("yellow").Foreground(p.Color("0")).Background(p.Color("#DBAB79")),
output.String("blue").Foreground(p.Color("0")).Background(p.Color("#71BEF2")),
output.String("magenta").Foreground(p.Color("0")).Background(p.Color("#D290E4")),
output.String("cyan").Foreground(p.Color("0")).Background(p.Color("#66C2CD")),
output.String("gray").Foreground(p.Color("0")).Background(p.Color("#B9BFCA")),
)

fmt.Fprintf(s, "\n\t%s %s\n", output.String("Has foreground color").Bold(), output.ForegroundColor())
fmt.Fprintf(s, "\t%s %s\n", output.String("Has background color").Bold(), output.BackgroundColor())
fmt.Fprintf(s, "\t%s %t\n", output.String("Has dark background?").Bold(), output.HasDarkBackground())
fmt.Fprintln(s)

hw := "Hello, world!"
output.Copy(hw)
fmt.Fprintf(s, "\t%q copied to clipboard\n", hw)
fmt.Fprintln(s)

fmt.Fprintf(s, "\t%s", output.Hyperlink("http://example.com", "This is a link"))
fmt.Fprintln(s)

sh(s)
}
},
),
)
if err != nil {
log.Fatal(err)
}
log.Printf("Listening on %s", s.Addr)
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -3,6 +3,7 @@ module github.com/muesli/termenv
go 1.13

require (
github.com/aymanbagabas/go-osc52 v1.0.3
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-runewidth v0.0.13
Expand Down
2 changes: 2 additions & 0 deletions go.sum
@@ -1,3 +1,5 @@
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
Expand Down
15 changes: 15 additions & 0 deletions hyperlink.go
@@ -0,0 +1,15 @@
package termenv

import (
"fmt"
)

// Hyperlink creates a hyperlink using OSC8.
func Hyperlink(link, name string) string {
return output.Hyperlink(link, name)
}

// Hyperlink creates a hyperlink using OSC8.
func (o *Output) Hyperlink(link, name string) string {
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", link, name)
}