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:
commit
5d2f0f4d04
5014
_examples/image_pixel.go
Normal file
5014
_examples/image_pixel.go
Normal file
File diff suppressed because it is too large
Load Diff
7
block.go
7
block.go
@ -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
|
||||
}
|
||||
|
35
render.go
35
render.go
@ -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
19
widgets/exp/HACKING.md
Normal 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
0
widgets/exp/README.md
Normal file
36
widgets/exp/aaa_init.go
Normal file
36
widgets/exp/aaa_init.go
Normal 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)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
60
widgets/exp/drawer_iterm2.go
Normal file
60
widgets/exp/drawer_iterm2.go
Normal 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
102
widgets/exp/drawer_kitty.go
Normal 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
|
63
widgets/exp/drawer_sixel.go
Normal file
63
widgets/exp/drawer_sixel.go
Normal 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")
|
||||
}
|
90
widgets/exp/drawer_urxvt.go
Normal file
90
widgets/exp/drawer_urxvt.go
Normal 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
107
widgets/exp/image.go
Normal 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
|
||||
}
|
28
widgets/exp/multiplexer.go
Normal file
28
widgets/exp/multiplexer.go
Normal 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 {}
|
||||
*/
|
177
widgets/exp/terminal_query.go
Normal file
177
widgets/exp/terminal_query.go
Normal 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
|
||||
}
|
51
widgets/exp/terminal_scan.go
Normal file
51
widgets/exp/terminal_scan.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
123
widgets/image.go
123
widgets/image.go
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user