1
0
mirror of https://github.com/gizak/termui.git synced 2025-04-26 13:48:54 +08:00

Merge 5330efee94e37e98c0e363b724d5fc1168bc6660 into 2b8f0c7960e9553acea6d579a740713066da5e13

This commit is contained in:
Simon Lehn 2024-01-30 02:58:06 -08:00 committed by GitHub
commit 5d2f0f4d04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 5890 additions and 22 deletions

5014
_examples/image_pixel.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,8 @@ type Block struct {
Title string
TitleStyle Style
ANSIString string
sync.Mutex
}
@ -103,3 +105,8 @@ func (self *Block) SetRect(x1, y1, x2, y2 int) {
func (self *Block) GetRect() image.Rectangle {
return self.Rectangle
}
// GetANSIString implements the Drawable interface.
func (self *Block) GetANSIString() string {
return self.ANSIString
}

View File

@ -5,6 +5,7 @@
package termui
import (
"fmt"
"image"
"sync"
@ -16,10 +17,44 @@ type Drawable interface {
SetRect(int, int, int, int)
Draw(*Buffer)
sync.Locker
GetANSIString() string
}
func Render(items ...Drawable) {
// draw background, etc for items with ANSI escape strings
for _, item := range items {
if len(item.GetANSIString()) > 0 {
continue
}
buf := NewBuffer(item.GetRect())
item.Lock()
item.Draw(buf)
item.Unlock()
for point, cell := range buf.CellMap {
if point.In(buf.Rectangle) {
tb.SetCell(
point.X, point.Y,
cell.Rune,
tb.Attribute(cell.Style.Fg+1)|tb.Attribute(cell.Style.Modifier), tb.Attribute(cell.Style.Bg+1),
)
}
}
}
tb.Flush()
// draw images, etc over the already filled cells with ANSI escape strings (sixel, ...)
for _, item := range items {
if ansiString := item.GetANSIString(); len(ansiString) > 0 {
fmt.Printf("%s", ansiString)
continue
}
}
// draw items without ANSI strings last in case the ANSI escape strings ended messed up
for _, item := range items {
if len(item.GetANSIString()) == 0 {
continue
}
buf := NewBuffer(item.GetRect())
item.Lock()
item.Draw(buf)

19
widgets/exp/HACKING.md Normal file
View File

@ -0,0 +1,19 @@
possible enhancements:
kitty https://sw.kovidgoyal.net/kitty/graphics-protocol.html
Terminology (from Enlightenment) https://www.enlightenment.org/docs/apps/terminology.md#tycat https://github.com/billiob/terminology
urxvt pixbuf / ...
Tektronix 4014
ReGis
reimplement 23imgdisplay:
check for X11 and not Alacritty (https://github.com/jwilm/alacritty/issues/1021)
---
if i remember correctly xterm has a size limit for sixel images - pixel width???

0
widgets/exp/README.md Normal file
View File

36
widgets/exp/aaa_init.go Normal file
View File

@ -0,0 +1,36 @@
// <Copyright> 2018,2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
// the file name should appear at the top when alphabetically sorted (start with "aaa")
// because the init() functions are executed in alphabetic file order
func init() {
scanTerminal()
var drawFallback func(*widgets.Image, *Buffer) (error)
if drbl, ok := widgets.GetDrawers()["block"]; ok {
drawFallback = drbl.Draw
}
widgets.RegisterDrawer(
"block",
widgets.Drawer{
Remote: true,
IsEscapeString: false,
Available: func() bool {return true},
Draw: func(img *widgets.Image, buf *Buffer) (err error) {
// possible reattachments of the terminal multiplexer?
if isMuxed {
scanTerminal()
}
return drawFallback(img, buf)
},
},
)
}

View File

@ -0,0 +1,60 @@
// <Copyright> 2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"fmt"
"bytes"
"encoding/base64"
"errors"
"image/png"
. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func init() {
widgets.RegisterDrawer(
"iterm2",
widgets.Drawer{
Remote: true,
IsEscapeString: true,
Available: func() bool {return isIterm2 || isMacTerm},
Draw: drawITerm2,
},
)
}
func drawITerm2(wdgt *widgets.Image, buf *Buffer) (err error) {
wdgt.Block.Draw(buf)
img, changed, err := resizeImage(wdgt, buf)
if !changed || err != nil {
return err
}
imageDimensions := wdgt.GetVisibleArea()
// https://www.iterm2.com/documentation-images.html
if isIterm2 || isMacTerm {
buf := new(bytes.Buffer)
if err = png.Encode(buf, img); err != nil {
goto skipIterm2
}
imgBase64 := base64.StdEncoding.EncodeToString(buf.Bytes())
nameBase64 := base64.StdEncoding.EncodeToString([]byte(wdgt.Block.Title))
// 0 for stretching - 1 for no stretching
noStretch := 0
iterm2String := wrap(fmt.Sprintf("\033]1337;File=name=%s;inline=1;height=%d;width=%d;preserveAspectRatio=%d:%s\a", nameBase64, imageDimensions.Max.Y, nameBase64, imageDimensions.Max.X, noStretch, imgBase64))
// for width, height: "auto" || N: N character cells || Npx: N pixels || N%: N percent of terminal width/height
wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH%s", imageDimensions.Min.Y, imageDimensions.Min.X, iterm2String)
return nil
}
skipIterm2:
return errors.New("no method applied for ANSI drawing")
}

102
widgets/exp/drawer_kitty.go Normal file
View File

@ -0,0 +1,102 @@
// <Copyright> 2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"fmt"
"errors"
"bytes"
"encoding/base64"
"image/png"
. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
const (
kittyLimit = 4096
)
var (
// TODO: for numbering of ids
kittyImageCount int
)
func init() {
widgets.RegisterDrawer(
"kitty",
widgets.Drawer{
Remote: true,
IsEscapeString: true,
Available: func() bool {return isKitty},
Draw: drawKitty,
},
)
}
func drawKitty(wdgt *widgets.Image, buf *Buffer) (err error) {
if !isKitty {
return errors.New("method not supported for this terminal type")
}
wdgt.Block.Draw(buf)
// TODO: FIX THIS
termWidth, termHeight := getTermSizeInChars(true)
var _ = termWidth
/*
if termWidth == 0 || termHeight == 0 {
return errors.New("could not query terminal dimensions")
}
*/
img, changed, err := resizeImage(wdgt, buf)
if !changed || err != nil {
return err
}
var imgHeight int
imageDimensions := wdgt.GetVisibleArea()
if wdgt.Inner.Max.Y < termHeight {
imgHeight = wdgt.Inner.Dy()
} else {
imgHeight = termHeight-1
}
imgHeight = wdgt.Inner.Dy() // TODO: REMOVE THIS CRUTCH
// https://sw.kovidgoyal.net/kitty/graphics-protocol.html#remote-client
// https://sw.kovidgoyal.net/kitty/graphics-protocol.html#png-data
// https://sw.kovidgoyal.net/kitty/graphics-protocol.html#controlling-displayed-image-layout
bytBuf := new(bytes.Buffer)
if err = png.Encode(bytBuf, img); err != nil {
return err
}
imgBase64 := base64.StdEncoding.EncodeToString(bytBuf.Bytes())
lenImgB64 := len([]byte(imgBase64))
// a=T action
// t=d payload is (base64 encoded) data itself not a file location
// f=100 format: 100 = PNG payload
// o=z data compression
// X=...,Y=,,, Upper left image corner in cell coordinates (starting with 1, 1)
// c=...,r=... image size in cell columns and rows
// w=...,h=... width & height (in pixels) of the image area to display // TODO: Use this to let Kitty handle cropping!
// z=0 z-index vertical stacking order of the image
// m=[01] 0 last escape code chunk - 1 for all except the last
var kittyString string
var zIndex = 2 // draw over text
settings := fmt.Sprintf("a=T,t=d,f=100,X=%d,Y=%d,c=%d,r=%d,z=%d,", imageDimensions.Min.X, imageDimensions.Min.Y, wdgt.Inner.Dx(), imgHeight, zIndex)
i := 0
for ; i < (lenImgB64-1)/kittyLimit; i++ {
kittyString += wrap(fmt.Sprintf("\033_G%sm=1;%s\033\\", settings, imgBase64[i*kittyLimit:(i+1)*kittyLimit]))
settings = ""
}
kittyString += wrap(fmt.Sprintf("\033_G%sm=0;%s\033\\", settings, imgBase64[i*kittyLimit:lenImgB64]))
wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH%s", imageDimensions.Min.Y, imageDimensions.Min.X, kittyString)
return nil
}
// TODO:
// store images with ids in Kitty

View File

@ -0,0 +1,63 @@
// <Copyright> 2018,2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"fmt"
"bytes"
"errors"
"github.com/mattn/go-sixel"
. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
func init() {
widgets.RegisterDrawer(
"sixel",
widgets.Drawer{
Remote: true,
IsEscapeString: true,
Available: func() bool {return sixelCapable},
Draw: drawSixel,
},
)
}
func drawSixel(wdgt *widgets.Image, buf *Buffer) (err error) {
wdgt.Block.Draw(buf)
img, changed, err := resizeImage(wdgt, buf)
if !changed || err != nil {
return err
}
// sixel
// https://vt100.net/docs/vt3xx-gp/chapter14.html
if sixelCapable {
byteBuf := new(bytes.Buffer)
enc := sixel.NewEncoder(byteBuf)
enc.Dither = true
if err := enc.Encode(img); err != nil {
return err
}
sixelString := wrap("\033[?8452h" + byteBuf.String())
// position where the image should appear (upper left corner) + sixel
// https://github.com/mintty/mintty/wiki/CtrlSeqs#sixel-graphics-end-position
// "\033[?8452h" sets the cursor next right to the bottom of the image instead of below
// this prevents vertical scrolling when the image fills the last line.
// horizontal scrolling because of this did not happen in my test cases.
// "\033[?80l" disables sixel scrolling if it isn't already.
wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH%s", wdgt.Inner.Min.Y + 1, wdgt.Inner.Min.X + 1, sixelString)
// test string "HI"
// wdgt.Block.ANSIString = fmt.Sprintf("\033[%d;%dH\033[?8452h%s", wdgt.Inner.Min.Y+1, wdgt.Inner.Min.X+1, "\033Pq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\033\\")
return nil
}
return errors.New("no method applied for ANSI drawing")
}

View File

@ -0,0 +1,90 @@
// <Copyright> 2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"fmt"
"bytes"
"errors"
"image/png"
"crypto/md5"
"io/ioutil"
"os"
"path/filepath"
. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
var (
tempdir string
)
func init() {
widgets.RegisterDrawer(
"urxvt",
widgets.Drawer{
Remote: false,
IsEscapeString: true,
Available: func() bool {return isUrxvt},
Draw: drawUrxvt,
},
)
}
func drawUrxvt(wdgt *widgets.Image, buf *Buffer) (err error) {
if !isUrxvt {
return errors.New("method not supported for this terminal type")
}
// wdgt.Block.Draw(buf)
var widthPercentage, heightPercentage, CenterPosXPercentage, CenterPosYPercentage int
termWidth, termHeight := getTermSizeInChars(true)
if termWidth == 0 || termHeight == 0 {
return errors.New("could not query terminal dimensions")
}
widthPercentage = (100*wdgt.Inner.Dx())/termWidth
heightPercentage = (100*wdgt.Inner.Dy())/termHeight
maxX := wdgt.Inner.Max.X
maxY := wdgt.Inner.Max.Y
if termWidth < maxX {
maxX = termWidth
}
if termHeight < maxY {
maxY = termHeight
}
CenterPosXPercentage = 50*(wdgt.Inner.Min.X+maxX)/termWidth
CenterPosYPercentage = 50*(wdgt.Inner.Min.Y+maxY)/termHeight
img, changed, err := resizeImage(wdgt, buf)
if !changed || err != nil {
return err
}
bytBuf := new(bytes.Buffer)
if err = png.Encode(bytBuf, img); err != nil {
return errors.New("image encoding failed")
}
if fi, err := os.Stat(tempdir); err != nil || !fi.IsDir() {
if tempdir, err = ioutil.TempDir("", "termui."); err != nil {
return err
}
}
// defer os.RemoveAll(dir) // clean up
filename := filepath.Join(tempdir, fmt.Sprintf("urxvt-%x", md5.Sum(bytBuf.Bytes())) + ".png")
if err := ioutil.WriteFile(filename, bytBuf.Bytes(), 0644); err != nil {
return err
}
// "op=keep-aspect" maintains the image aspect ratio when scaling
wdgt.Block.ANSIString = wrap(fmt.Sprintf("\033]20;%s;%dx%d+%d+%d:op=keep-aspect\a", filename, widthPercentage, heightPercentage, CenterPosXPercentage, CenterPosYPercentage))
return nil
}

107
widgets/exp/image.go Normal file
View File

@ -0,0 +1,107 @@
// <Copyright> 2018,2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"fmt"
"errors"
"image"
"github.com/disintegration/imaging"
. "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
var (
charBoxWidthInPixels, charBoxHeightInPixels float64
charBoxWidthColumns, charBoxHeightRows int
)
func resizeImage(wdgt *widgets.Image, buf *Buffer) (img image.Image, changed bool, err error) {
img = wdgt.Image
// img = image.NRGBA{}
// get dimensions //
// terminal size measured in cells
imageWidthInColumns := wdgt.Inner.Dx()
imageHeightInRows := wdgt.Inner.Dy()
// terminal size in cells and pixels and calculated terminal character box size in pixels
var termWidthInColumns, termHeightInRows int
var charBoxWidthInPixelsTemp, charBoxHeightInPixelsTemp float64
termWidthInColumns, termHeightInRows, _, _, charBoxWidthInPixelsTemp, charBoxHeightInPixelsTemp, err = getTermSize()
if err != nil {
return img, true, err
}
// update if value is more precise
if termWidthInColumns > charBoxWidthColumns {
charBoxWidthInPixels = charBoxWidthInPixelsTemp
}
if termHeightInRows > charBoxHeightRows {
charBoxHeightInPixels = charBoxHeightInPixelsTemp
}
if isTmux {charBoxWidthInPixels, charBoxHeightInPixels = 10, 19} // mlterm settings (temporary)
// calculate image size in pixels
// subtract 1 pixel for small deviations from char box size (float64)
imageWidthInPixels := int(float64(imageWidthInColumns) * charBoxWidthInPixels) - 1
imageHeightInPixels := int(float64(imageHeightInRows) * charBoxHeightInPixels) - 1
if imageWidthInPixels == 0 || imageHeightInPixels == 0 {
return img, true, errors.New("could not calculate the image size in pixels")
}
// handle only partially displayed image
// otherwise we get scrolling
var needsCropX, needsCropY bool
var imgCroppedWidth, imgCroppedHeight int
imgCroppedWidth = imageWidthInPixels
imgCroppedHeight = imageHeightInPixels
if wdgt.Max.Y >= int(termHeightInRows) {
var scrollExtraRows int
// remove last 2 rows for xterm when cropped vertically to prevent scrolling
if isXterm {
scrollExtraRows = 2
}
/*
if isKitty {
scrollExtraRows = 1
}
*/
// subtract 1 pixel for small deviations from char box size (float64)
imgCroppedHeight = int(float64(int(termHeightInRows) - wdgt.Inner.Min.Y - scrollExtraRows) * charBoxHeightInPixels) - 1
needsCropY = true
}
if wdgt.Max.X >= int(termWidthInColumns) {
var scrollExtraColumns int
imgCroppedWidth = int(float64(int(termWidthInColumns) - wdgt.Inner.Min.X - scrollExtraColumns) * charBoxWidthInPixels) - 1
needsCropX = true
}
lastImageDimensions := wdgt.GetVisibleArea()
// this is meant for comparison and for positioning in the ANSI string
// the Min values are in cells while the Max values are in pixels
// imageDimensions := image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: imgCroppedWidth, Y: imgCroppedHeight}}
imageDimensions := image.Rectangle{Min: image.Point{X: wdgt.Inner.Min.X + 1, Y: wdgt.Inner.Min.Y + 1}, Max: image.Point{X: imgCroppedWidth, Y: imgCroppedHeight}}
wdgt.SetVisibleArea(imageDimensions)
// print saved ANSI string if image size and position didn't change
if imageDimensions.Min.X == lastImageDimensions.Min.X && imageDimensions.Min.Y == lastImageDimensions.Min.Y && imageDimensions.Max.X == lastImageDimensions.Max.X && imageDimensions.Max.Y == lastImageDimensions.Max.Y {
// reuse last encoded image because of unchanged image dimensions
return img, false, nil
}
lastImageDimensions = imageDimensions
// resize and crop the image //
img = imaging.Resize(wdgt.Image, imageWidthInPixels, imageHeightInPixels, imaging.Lanczos)
if needsCropX || needsCropY {
img = imaging.Crop(img, image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: imgCroppedWidth, Y: imgCroppedHeight}})
}
if img.Bounds().Dx() == 0 || img.Bounds().Dy() == 0 {
return img, true, fmt.Errorf("image size in pixels is 0")
}
return img, true, err
}

View File

@ -0,0 +1,28 @@
// <Copyright> 2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"strings"
)
func wrap(s string) string {
if !isMuxed {
return s
}
if isTmux {
return tmuxWrap(s)
}
return s
}
func tmuxWrap(s string) string {
return "\033Ptmux;" + strings.Replace(s, "\033", "\033\033", -1) + "\033\\"
}
/*
// https://savannah.gnu.org/bugs/index.php?56063
func screenWrap(s string) string {}
*/

View File

@ -0,0 +1,177 @@
// <Copyright> 2018,2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"fmt"
"strconv"
"time"
"github.com/mattn/go-tty"
)
func getTermSize() (termWidthInColumns, termHeightInRows, termWidthInPixels, termHeightInPixels int, charBoxWidthInPixels, charBoxHeightInPixels float64, err error) {
// this uses a combination of TIOCGWINSZ and \033[14t , \033[18t
// the syscall to TIOCGWINSZ only works locally
var cx, cy, px, py int
err = nil
t, err := tty.Open()
defer t.Close()
cx, cy, px, py, err = t.SizePixel()
// if isMuxed {
if false { // temporary
// in case of split view it is better to query from the same source
/*if cx <= 0 || cy <= 0 || px <= 0 || py <= 0 {
cx, cy = getTermSizeInChars(true)
px, py = getTermSizeInPixels(true)
if cx <= 0 || cy <= 0 || px <= 0 || py <= 0 {
cx, cy = getTermSizeInChars(false)
px, py = getTermSizeInPixels(false)
if cx <= 0 || cy <= 0 || px <= 0 || py <= 0 {
return
}
}
}*/
} else if err == nil {
if cx > 0 && cy > 0 {
if px <= 0 || py <= 0 {
px, py = getTermSizeInPixels(true)
}
} else {
if cx, cy = getTermSizeInChars(true); cx != 0 && cy != 0 {
if px <= 0 || py <= 0 {
px, py = getTermSizeInPixels(true)
}
} else {
return
}
}
}
termWidthInColumns = cx
termHeightInRows = cy
termWidthInPixels = px
termHeightInPixels = py
charBoxWidthInPixels = float64(px) / float64(cx)
charBoxHeightInPixels = float64(py) / float64(cy)
return
}
func getTermSizeInChars(needsWrap bool) (x, y int) {
// query terminal size in character boxes
// answer: <termHeightInRows>;<termWidthInColumns>t
s := "\033[18t"
if needsWrap {
s = wrap(s)
}
q := queryTerm(s)
if len(q) != 3 {
return
}
if yy, err := strconv.Atoi(string(q[1])); err == nil {
if xx, err := strconv.Atoi(string(q[2])); err == nil {
x = xx
y = yy
} else {
return
}
} else {
return
}
return
}
func getTermSizeInPixels(needsWrap bool) (x, y int) {
// query terminal size in pixels
// answer: <termHeightInPixels>;<termWidthInPixels>t
s := "\033[14t"
if needsWrap {
s = wrap(s)
}
q := queryTerm(s)
if len(q) != 3 {
return
}
if yy, err := strconv.Atoi(string(q[1])); err == nil {
if xx, err := strconv.Atoi(string(q[2])); err == nil {
x = xx
y = yy
} else {
return
}
} else {
return
}
return
}
func queryTerm(qs string) (ret [][]rune) {
// temporary fix for xterm - not completely sure if still needed
// otherwise TUI wouldn't react to any further events
// resizing still works though
if isXterm && qs != "\033[0c" && qs != wrap("\033[0c") {
return
}
var b []rune
t, err := tty.Open()
if err != nil {
return
}
ch := make(chan bool, 1)
go func() {
defer t.Close()
// query terminal
fmt.Printf(qs)
for {
r, err := t.ReadRune()
if err != nil {
return
}
// handle key event
switch r {
case 'c', 't':
ret = append(ret, b)
goto afterLoop
case '?', ';':
ret = append(ret, b)
b = []rune{}
default:
b = append(b, r)
}
}
afterLoop:
ch <- true
}()
var timer *time.Timer
if isTmux {
// tmux needs a bit more time
timer = time.NewTimer(50000 * time.Microsecond)
} else {
// on my system the terminals mlterm, xterm need at least around 100 microseconds
timer = time.NewTimer(500 * time.Microsecond)
}
defer timer.Stop()
select {
case <-ch:
defer close(ch)
case <-timer.C:
}
return
}

View File

@ -0,0 +1,51 @@
// <Copyright> 2018,2019 Simon Robin Lehn. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package exp
import (
"os"
"strings"
)
var (
sixelCapable, isXterm, isMlterm, isMintty, isIterm2, isUrxvt, isAlacritty, isYaft, isKitty, isMacTerm, isTmux, isScreen, isMuxed bool
)
func scanTerminal() {
if len(os.Getenv("XTERM_VERSION")) > 0 { isXterm = true } else { isXterm = false }
if os.Getenv("TERM_PROGRAM") == "iTerm.app" { isIterm2 = true } else { isIterm2 = false } // https://superuser.com/a/683971
if os.Getenv("TERM_PROGRAM") == "MacTerm" { isMacTerm = true } else { isMacTerm = false } // https://github.com/kmgrant/macterm/issues/3#issuecomment-458387953
if strings.HasPrefix(os.Getenv("TERM"), "rxvt-unicode") { isUrxvt = true } else { isUrxvt = false }
if os.Getenv("TERM") == "xterm-kitty" ||len(os.Getenv("KITTY_WINDOW_ID")) > 0 { isKitty = true } else { isKitty = false }
if len(os.Getenv("MLTERM")) > 0 { isMlterm = true } else { isMlterm = false }
if len(os.Getenv("MINTTY_SHORTCUT")) > 0 { isMintty = true } else { isMintty = false }
if len(os.Getenv("ALACRITTY_LOG")) > 0 { isAlacritty = true } else { isAlacritty = false }
if os.Getenv("TERM") == "yaft-256color" { isYaft = true } else { isYaft = false } // https://github.com/uobikiemukot/yaft/blob/21b69124a2907ad6ede8f45ca96c390615e3dc0c/conf.h#L26
if strings.HasPrefix(os.Getenv("TERM"), "screen") && len(os.Getenv("STY")) > 0 { isScreen = true } else { isScreen = false }
if (strings.HasPrefix(os.Getenv("TERM"), "screen") || strings.HasPrefix(os.Getenv("TERM"), "tmux")) &&
len(os.Getenv("TMUX")) > 0 || len(os.Getenv("TMUX_PANE")) > 0 { isTmux = true } else { isTmux = false }
if isTmux || isScreen { isMuxed = true } else { isMuxed = false }
if isYaft {
sixelCapable = true
} else {
sixelCapable = false
}
// example query: "\033[0c"
// possible answer from the terminal (here xterm): "\033[[?63;1;2;4;6;9;15;22c", vte(?): ...62,9;c
// the "4" signals that the terminal is capable of sixel
// conhost.exe knows this sequence.
if !sixelCapable {
termCapabilities := queryTerm(wrap("\033[0c"))
for i, cap := range termCapabilities {
if i == 0 || i == len(termCapabilities) - 1 {
continue
}
if string(cap) == `4` {
sixelCapable = true
}
}
}
}

View File

@ -7,39 +7,107 @@ package widgets
import (
"image"
"image/color"
"sync"
"sync/atomic"
. "github.com/gizak/termui/v3"
)
type Drawer struct {
Remote bool // if the drawer can be used for remote terminals
IsEscapeString bool
Available func() (bool)
Draw func(*Image, *Buffer) (error)
}
var (
drawersMu sync.Mutex
atomicDrawers atomic.Value
)
// from https://golang.org/src/image/format.go?s=1069:1193#L31
func RegisterDrawer(nameNew string, drawerNew Drawer) {
drawersMu.Lock()
// drawers, _ := atomicDrawers.Load().([]Drawer)
// atomicDrawers.Store(append(drawers, dr))
drawers, _ := atomicDrawers.Load().(map[string]Drawer)
drawersNew := make(map[string]Drawer)
for name, drawer := range drawers {
drawersNew[name] = drawer
}
drawersNew[nameNew] = drawerNew
atomicDrawers.Store(drawersNew)
drawersMu.Unlock()
}
func GetDrawers() map[string]Drawer {
if drawers, ok := atomicDrawers.Load().(map[string]Drawer); ok {
return drawers
}
return map[string]Drawer{}
}
func init() {
RegisterDrawer(
"block",
Drawer{
Remote: true,
IsEscapeString: false,
Available: func() bool {return true},
Draw: drawBlocks,
},
)
}
type Image struct {
Block
Image image.Image
Monochrome bool
MonochromeThreshold uint8
MonochromeInvert bool
Image image.Image
Monochrome bool
MonochromeThreshold uint8
MonochromeInvert bool
visibleSubImagePixels image.Rectangle
}
func NewImage(img image.Image) *Image {
return &Image{
Block: *NewBlock(),
MonochromeThreshold: 128,
Image: img,
Block: *NewBlock(),
MonochromeThreshold: 128,
Image: img,
visibleSubImagePixels: image.Rectangle{},
}
}
func (self *Image) Draw(buf *Buffer) {
self.Block.Draw(buf)
drawers := GetDrawers()
if self.Image == nil {
// fall back - draw with box characters atomicDrawers.Load().(map[string]Drawer)]["blocks"]
// possible enhancement: make use of further box characters like chafa:
// https://hpjansson.org/chafa/
// https://github.com/hpjansson/chafa/
if drbl, ok := drawers["block"]; ok {
drbl.Draw(self, buf)
}
for name, dr := range drawers {
if name != "block" && dr.Available() {
dr.Draw(self, buf)
}
}
}
func drawBlocks(img *Image, buf *Buffer) (err error) {
img.Block.Draw(buf)
if img.Image == nil {
return
}
bufWidth := self.Inner.Dx()
bufHeight := self.Inner.Dy()
imageWidth := self.Image.Bounds().Dx()
imageHeight := self.Image.Bounds().Dy()
bufWidth := img.Inner.Dx()
bufHeight := img.Inner.Dy()
imageWidth := img.Image.Bounds().Dx()
imageHeight := img.Image.Bounds().Dy()
if self.Monochrome {
if img.Monochrome {
if bufWidth > imageWidth/2 {
bufWidth = imageWidth / 2
}
@ -48,33 +116,33 @@ func (self *Image) Draw(buf *Buffer) {
}
for bx := 0; bx < bufWidth; bx++ {
for by := 0; by < bufHeight; by++ {
ul := self.colorAverage(
ul := img.colorAverage(
2*bx*imageWidth/bufWidth/2,
(2*bx+1)*imageWidth/bufWidth/2,
2*by*imageHeight/bufHeight/2,
(2*by+1)*imageHeight/bufHeight/2,
)
ur := self.colorAverage(
ur := img.colorAverage(
(2*bx+1)*imageWidth/bufWidth/2,
(2*bx+2)*imageWidth/bufWidth/2,
2*by*imageHeight/bufHeight/2,
(2*by+1)*imageHeight/bufHeight/2,
)
ll := self.colorAverage(
ll := img.colorAverage(
2*bx*imageWidth/bufWidth/2,
(2*bx+1)*imageWidth/bufWidth/2,
(2*by+1)*imageHeight/bufHeight/2,
(2*by+2)*imageHeight/bufHeight/2,
)
lr := self.colorAverage(
lr := img.colorAverage(
(2*bx+1)*imageWidth/bufWidth/2,
(2*bx+2)*imageWidth/bufWidth/2,
(2*by+1)*imageHeight/bufHeight/2,
(2*by+2)*imageHeight/bufHeight/2,
)
buf.SetCell(
NewCell(blocksChar(ul, ur, ll, lr, self.MonochromeThreshold, self.MonochromeInvert)),
image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by),
NewCell(blocksChar(ul, ur, ll, lr, img.MonochromeThreshold, img.MonochromeInvert)),
image.Pt(img.Inner.Min.X+bx, img.Inner.Min.Y+by),
)
}
}
@ -87,7 +155,7 @@ func (self *Image) Draw(buf *Buffer) {
}
for bx := 0; bx < bufWidth; bx++ {
for by := 0; by < bufHeight; by++ {
c := self.colorAverage(
c := img.colorAverage(
bx*imageWidth/bufWidth,
(bx+1)*imageWidth/bufWidth,
by*imageHeight/bufHeight,
@ -95,11 +163,22 @@ func (self *Image) Draw(buf *Buffer) {
)
buf.SetCell(
NewCell(c.ch(), NewStyle(c.fgColor(), ColorBlack)),
image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by),
image.Pt(img.Inner.Min.X+bx, img.Inner.Min.Y+by),
)
}
}
}
return
}
// measured in pixels
func (self *Image) SetVisibleArea(area image.Rectangle) {
self.visibleSubImagePixels = area
}
// measured in pixels
func (self *Image) GetVisibleArea() image.Rectangle {
return self.visibleSubImagePixels
}
func (self *Image) colorAverage(x0, x1, y0, y1 int) colorAverager {