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

feat(image): basic iTerm image protocol support #196

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion ansi/blockelement.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"bytes"
"io"

"github.com/muesli/reflow/wordwrap"
"github.com/charmbracelet/glamour/ansi/wordwrap"
)

// BlockElement provides a render buffer for children of a block element.
Expand Down
2 changes: 1 addition & 1 deletion ansi/codeblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/quick"
"github.com/alecthomas/chroma/styles"
"github.com/muesli/reflow/indent"
"github.com/charmbracelet/glamour/ansi/indent"
"github.com/muesli/termenv"
)

Expand Down
15 changes: 11 additions & 4 deletions ansi/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ type ElementFinisher interface {
Finish(w io.Writer, ctx RenderContext) error
}

// ElementSkipChildrenChecker is called to check if the depth-first search should skip all children.
type ElementSkipChildrenChecker interface {
CheckShouldSkip(ctx RenderContext) (bool, error)
}

// An Element is used to instruct the renderer how to handle individual markdown
// nodes.
type Element struct {
Entering string
Exiting string
Renderer ElementRenderer
Finisher ElementFinisher
Entering string
Exiting string
Renderer ElementRenderer
Finisher ElementFinisher
SkipChildrenChecker ElementSkipChildrenChecker
}

// NewElement returns the appropriate render Element for a given node.
Expand Down Expand Up @@ -248,6 +254,7 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
BaseURL: ctx.options.BaseURL,
URL: string(n.Destination),
},
SkipChildrenChecker: &ImageSkipChildrenChecker{},
}

// Code
Expand Down
4 changes: 2 additions & 2 deletions ansi/heading.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"bytes"
"io"

"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/wordwrap"
"github.com/charmbracelet/glamour/ansi/indent"
"github.com/charmbracelet/glamour/ansi/wordwrap"
)

// A HeadingElement is used to render headings.
Expand Down
50 changes: 50 additions & 0 deletions ansi/image.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package ansi

import (
"fmt"
"github.com/BourgeoisBear/rasterm"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"os"
)

// An ImageElement is used to render images elements.
Expand All @@ -12,7 +19,50 @@ type ImageElement struct {
Child ElementRenderer // FIXME
}

// ImageSkipChildrenChecker should tell whether the ast walker should skip
// all children based on ctx.options.ImageDisplay
type ImageSkipChildrenChecker struct{}

func (e *ImageSkipChildrenChecker) CheckShouldSkip(ctx RenderContext) (bool, error) {
return ctx.options.ImageDisplay, nil
}

func (e *ImageElement) Render(w io.Writer, ctx RenderContext) error {
handleImageDisplay := func(imageAbsUrl string, w io.Writer) error {
file, err := os.Open(imageAbsUrl)
if err != nil {
return err
}

img, _, err := image.Decode(file)
if err != nil {
return err
}

err = rasterm.Settings{}.ItermWriteImage(w, img)
if err != nil {
return err
}

err = file.Close()
if err != nil {
return err
}
return nil
}

if ctx.options.ImageDisplay && len(e.URL) > 0 && rasterm.IsTermItermWez() {
url := resolveRelativeURL(e.BaseURL, e.URL)
err := handleImageDisplay(url, w)
if err != nil {
fmt.Printf("Warning: failed to display image %v: %v\n", url, err)
// fallback to text display
} else {
// all done
return nil
}
}

if len(e.Text) > 0 {
el := &BaseElement{
Token: e.Text,
Expand Down
132 changes: 132 additions & 0 deletions ansi/indent/indent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/// A modified version of indent from reflow, with support of iTerm2 proprietary image sequences.
/// The code is modified so it will not insert \x1b]0m in an image sequence.

package indent

import (
"bytes"
"io"
"strings"

"github.com/muesli/reflow/ansi"
)

type IndentFunc func(w io.Writer)

type Writer struct {
Indent uint
IndentFunc IndentFunc

ansiWriter *ansi.Writer
buf bytes.Buffer
skipIndent bool
ansi bool
iterm bool
ansiWaitSecondChar bool
}

func NewWriter(indent uint, indentFunc IndentFunc) *Writer {
w := &Writer{
Indent: indent,
IndentFunc: indentFunc,
}
w.ansiWriter = &ansi.Writer{
Forward: &w.buf,
}
return w
}

func NewWriterPipe(forward io.Writer, indent uint, indentFunc IndentFunc) *Writer {
return &Writer{
Indent: indent,
IndentFunc: indentFunc,
ansiWriter: &ansi.Writer{
Forward: forward,
},
}
}

// Bytes is shorthand for declaring a new default indent-writer instance,
// used to immediately indent a byte slice.
func Bytes(b []byte, indent uint) []byte {
f := NewWriter(indent, nil)
_, _ = f.Write(b)

return f.Bytes()
}

// String is shorthand for declaring a new default indent-writer instance,
// used to immediately indent a string.
func String(s string, indent uint) string {
return string(Bytes([]byte(s), indent))
}

// Write is used to write content to the indent buffer.
func (w *Writer) Write(b []byte) (int, error) {
for _, c := range string(b) {
if c == '\x1B' {
// ANSI escape sequence
w.ansi = true
w.ansiWaitSecondChar = true
} else if w.ansi {
if w.ansiWaitSecondChar {
w.iterm = c == ']'
w.ansiWaitSecondChar = false
}
if !w.iterm {
if (c >= 0x40 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) {
// ANSI sequence terminated
w.ansi = false
w.ansiWaitSecondChar = false
w.iterm = false
}
} else {
if c == '\a' {
// ANSI sequence terminated
w.ansi = false
w.ansiWaitSecondChar = false
w.iterm = false
}
}
} else {
if !w.skipIndent {
w.ansiWriter.ResetAnsi()
if w.IndentFunc != nil {
for i := 0; i < int(w.Indent); i++ {
w.IndentFunc(w.ansiWriter)
}
} else {
_, err := w.ansiWriter.Write([]byte(strings.Repeat(" ", int(w.Indent))))
if err != nil {
return 0, err
}
}

w.skipIndent = true
w.ansiWriter.RestoreAnsi()
}

if c == '\n' {
// end of current line
w.skipIndent = false
}
}

_, err := w.ansiWriter.Write([]byte(string(c)))
if err != nil {
return 0, err
}
}

return len(b), nil
}

// Bytes returns the indented result as a byte slice.
func (w *Writer) Bytes() []byte {
return w.buf.Bytes()
}

// String returns the indented result as a string.
func (w *Writer) String() string {
return w.buf.String()
}
4 changes: 2 additions & 2 deletions ansi/margin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package ansi
import (
"io"

"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/padding"
"github.com/charmbracelet/glamour/ansi/indent"
"github.com/charmbracelet/glamour/ansi/padding"
)

// MarginWriter is a Writer that applies indentation and padding around
Expand Down
Loading