diff --git a/examples/img2term/main.go b/examples/img2term/main.go index 4c8919fc..98f2a380 100644 --- a/examples/img2term/main.go +++ b/examples/img2term/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "flag" + "fmt" "image" "io" "log" @@ -13,10 +14,16 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi/sixel" + "github.com/charmbracelet/x/graphics" ) // $ go run . ./../../ansi/fixtures/graphics/JigokudaniMonkeyPark.png func main() { + imageProtocols := graphics.DetectImageProtocols() + fmt.Println("sixel supported:", imageProtocols.Sixel) + fmt.Println("iTerm2 supported:", imageProtocols.ITerm2) + fmt.Println("kitty supported", imageProtocols.Kitty) + flag.Parse() args := flag.Args() if len(args) == 0 { diff --git a/go.work b/go.work index 020c73ee..5dbb0206 100644 --- a/go.work +++ b/go.work @@ -17,6 +17,7 @@ use ( ./exp/strings ./exp/teatest ./exp/teatest/v2 + ./graphics ./input ./json ./mosaic diff --git a/graphics/detect.go b/graphics/detect.go new file mode 100644 index 00000000..b1afebe2 --- /dev/null +++ b/graphics/detect.go @@ -0,0 +1,111 @@ +package graphics + +import ( + "bytes" + "os" + "time" + + "github.com/charmbracelet/x/term" +) + +// TODO: Verify if it's running with tmux for Kitty and ITerm2 +// TODO: Write a func for preferred protocol for the terminal +// TODO: Additional check if the terminal supports cell-size by `[16t` or `[14t` +// TODO: Write tests by mocking a terminal context ? + +const ( + termProgramVariable = "TERM_PROGRAM" + lcTerminalVariable = "LC_TERMINAL" +) + +// Returns the availability of each image protocol. +type ImageProtocols struct { + Sixel bool + ITerm2 bool + Kitty bool + // Mosaic (Halfblocks) should work in all terminals, + // even if the font size could not be detected, with a 4:8 pixel ratio. + Mosaic bool +} + +// Detect all availables image protocols and return as [ImageProtocols]. +func DetectImageProtocols() ImageProtocols { + return ImageProtocols{ + Sixel: detectSixel(), + // TODO: `_Gi=...`: Kitty graphics support. + Kitty: detectKitty(), + // TODO: `[1337n`: iTerm2 (some terminals implement the protocol but sadly not this custom CSI) + ITerm2: detectIterm2() || detectIterm2FromEnv(), + Mosaic: true, + } +} + +func detectKitty() bool { + return false +} + +func detectIterm2() bool { + return false +} + +// This function detects iTerm2 protocol from environment variable. +func detectIterm2FromEnv() bool { + termProgram := os.Getenv(termProgramVariable) + if termProgram == "iTerm" || + termProgram == "WezTerm" || + termProgram == "mintty" || + termProgram == "vscode" || + termProgram == "Tabby" || + termProgram == "Hyper" || + termProgram == "rio" { + return true + } + + lcTerminal := os.Getenv(lcTerminalVariable) + return lcTerminal == "iTerm" +} + +func detectSixel() bool { + sixelSupportedTerminals := []string{ + "\x1b[?62;", // VT240 + "\x1b[?63;", // wsltty + "\x1b[?64;", // mintty + "\x1b[?65;", // RLogin + // NOTE: tmux does not return VT name. + "\x1b[?1;2;4c", // Tmux + } + + if term.IsTerminal(os.Stdout.Fd()) { + return true + } + s, err := term.MakeRaw(1) + if err == nil { + defer term.Restore(1, s) // nolint:errcheck + } + _, err = os.Stdout.Write([]byte("\x1b[c")) + if err != nil { + return false + } + defer os.Stdout.SetReadDeadline(time.Time{}) // nolint:errcheck + + var b [100]byte + n, err := os.Stdout.Read(b[:]) + if err != nil { + return false + } + + for _, t := range bytes.Split(b[6:n], []byte(";")) { + // Check if 4 is present in terminal capabilities. + if len(t) == 1 && t[0] == '4' { + return true + } + } + + for _, supportedTerminal := range sixelSupportedTerminals { + if bytes.HasPrefix(b[:n], []byte(supportedTerminal)) { + return true + } + } + + return false +} diff --git a/graphics/go.mod b/graphics/go.mod new file mode 100644 index 00000000..842da450 --- /dev/null +++ b/graphics/go.mod @@ -0,0 +1,7 @@ +module github.com/charmbracelet/x/graphics + +go 1.21 + +require github.com/charmbracelet/x/term v0.2.1 + +require golang.org/x/sys v0.26.0 // indirect diff --git a/graphics/go.sum b/graphics/go.sum new file mode 100644 index 00000000..0183b87e --- /dev/null +++ b/graphics/go.sum @@ -0,0 +1,4 @@ +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=