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

fixed xterm (added TIOCGWINSZ querying), vendored go-tty temporarily

This commit is contained in:
Simon Lehn 2019-04-01 23:08:52 +02:00
parent 78c8702461
commit 6f80c95d3c
11 changed files with 5892 additions and 40 deletions

5013
_examples/image_snake.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,11 @@ type Drawable interface {
}
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)
@ -35,12 +39,35 @@ func Render(items ...Drawable) {
)
}
}
}
tb.Flush()
// draw the image over the already filled cells
// 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)
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()
}

21
vendor/github.com/mattn/go-tty/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
vendor/github.com/mattn/go-tty/README.md generated vendored Normal file
View File

@ -0,0 +1,49 @@
# go-tty
Simple tty utility
## Usage
```go
tty, err := tty.Open()
if err != nil {
log.Fatal(err)
}
defer tty.Close()
for {
r, err := tty.ReadRune()
if err != nil {
log.Fatal(err)
}
// handle key event
}
```
if you are on windows and want to display ANSI colors, use <a href="https://github.com/mattn/go-colorable">go-colorable</a>.
```go
tty, err := tty.Open()
if err != nil {
log.Fatal(err)
}
defer tty.Close()
out := colorable.NewColorable(tty.Output())
fmt.Fprintln(out, "\x1b[2J")
```
## Installation
```
$ go get github.com/mattn/go-tty
```
## License
MIT
## Author
Yasuhiro Matsumoto (a.k.a mattn)

125
vendor/github.com/mattn/go-tty/tty.go generated vendored Normal file
View File

@ -0,0 +1,125 @@
package tty
import (
"os"
"strings"
"unicode"
)
func Open() (*TTY, error) {
return open()
}
func (tty *TTY) Raw() (func() error, error) {
return tty.raw()
}
func (tty *TTY) MustRaw() func() error {
f, err := tty.raw()
if err != nil {
panic(err.Error())
}
return f
}
func (tty *TTY) Buffered() bool {
return tty.buffered()
}
func (tty *TTY) ReadRune() (rune, error) {
return tty.readRune()
}
func (tty *TTY) Close() error {
return tty.close()
}
func (tty *TTY) Size() (int, int, error) {
x, y, err := tty.size()
return x, y, err
}
func (tty *TTY) SizePixel() (int, int, int, int, error) {
return tty.sizePixel()
}
func (tty *TTY) Input() *os.File {
return tty.input()
}
func (tty *TTY) Output() *os.File {
return tty.output()
}
// Display types.
const (
displayNone = iota
displayRune
displayMask
)
func (tty *TTY) readString(displayType int) (string, error) {
rs := []rune{}
loop:
for {
r, err := tty.readRune()
if err != nil {
return "", err
}
switch r {
case 13:
break loop
case 8, 127:
if len(rs) > 0 {
rs = rs[:len(rs)-1]
if displayType != displayNone {
tty.Output().WriteString("\b \b")
}
}
default:
if unicode.IsPrint(r) {
rs = append(rs, r)
switch displayType {
case displayRune:
tty.Output().WriteString(string(r))
case displayMask:
tty.Output().WriteString("*")
}
}
}
}
return string(rs), nil
}
func (tty *TTY) ReadString() (string, error) {
defer tty.Output().WriteString("\n")
return tty.readString(displayRune)
}
func (tty *TTY) ReadPassword() (string, error) {
defer tty.Output().WriteString("\n")
return tty.readString(displayMask)
}
func (tty *TTY) ReadPasswordNoEcho() (string, error) {
defer tty.Output().WriteString("\n")
return tty.readString(displayNone)
}
func (tty *TTY) ReadPasswordClear() (string, error) {
s, err := tty.readString(displayMask)
tty.Output().WriteString(
strings.Repeat("\b", len(s)) +
strings.Repeat(" ", len(s)) +
strings.Repeat("\b", len(s)))
return s, err
}
type WINSIZE struct {
W int
H int
}
func (tty *TTY) SIGWINCH() chan WINSIZE {
return tty.sigwinch()
}

12
vendor/github.com/mattn/go-tty/tty_bsd.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// +build darwin dragonfly freebsd netbsd openbsd
package tty
import (
"syscall"
)
const (
ioctlReadTermios = syscall.TIOCGETA
ioctlWriteTermios = syscall.TIOCSETA
)

8
vendor/github.com/mattn/go-tty/tty_linux.go generated vendored Normal file
View File

@ -0,0 +1,8 @@
// +build linux
package tty
const (
ioctlReadTermios = 0x5401 // syscall.TCGETS
ioctlWriteTermios = 0x5402 // syscall.TCSETS
)

69
vendor/github.com/mattn/go-tty/tty_plan9.go generated vendored Normal file
View File

@ -0,0 +1,69 @@
package tty
import (
"bufio"
"os"
"syscall"
"errors"
)
s
type TTY struct {
in *os.File
bin *bufio.Reader
out *os.File
}
func open() (*TTY, error) {
tty := new(TTY)
in, err := os.Open("/dev/cons")
if err != nil {
return nil, err
}
tty.in = in
tty.bin = bufio.NewReader(in)
out, err := os.OpenFile("/dev/cons", syscall.O_WRONLY, 0)
if err != nil {
return nil, err
}
tty.out = out
return tty, nil
}
func (tty *TTY) buffered() bool {
return tty.bin.Buffered() > 0
}
func (tty *TTY) readRune() (rune, error) {
r, _, err := tty.bin.ReadRune()
return r, err
}
func (tty *TTY) close() (err error) {
if err2 := tty.in.Close(); err2 != nil {
err = err2
}
if err2 := tty.out.Close(); err2 != nil {
err = err2
}
return
}
func (tty *TTY) size() (int, int, error) {
return 80, 24, nil
}
func (tty *TTY) sizePixel() (int, int, int, int, error) {
x, y, _ := tty.size()
return x, y, -1, -1, errors.New("no implemented method for querying size in pixels on Plan 9")
}
func (tty *TTY) input() *os.File {
return tty.in
}
func (tty *TTY) output() *os.File {
return tty.out
}

137
vendor/github.com/mattn/go-tty/tty_unix.go generated vendored Normal file
View File

@ -0,0 +1,137 @@
// +build !windows
// +build !plan9
package tty
import (
"bufio"
"os"
"os/signal"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
type TTY struct {
in *os.File
bin *bufio.Reader
out *os.File
termios syscall.Termios
ws chan WINSIZE
ss chan os.Signal
}
func open() (*TTY, error) {
tty := new(TTY)
in, err := os.Open("/dev/tty")
if err != nil {
return nil, err
}
tty.in = in
tty.bin = bufio.NewReader(in)
out, err := os.OpenFile("/dev/tty", syscall.O_WRONLY, 0)
if err != nil {
return nil, err
}
tty.out = out
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(tty.in.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&tty.termios)), 0, 0, 0); err != 0 {
return nil, err
}
newios := tty.termios
newios.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF
newios.Lflag &^= syscall.ECHO | syscall.ICANON /*| syscall.ISIG*/
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(tty.in.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newios)), 0, 0, 0); err != 0 {
return nil, err
}
tty.ws = make(chan WINSIZE)
tty.ss = make(chan os.Signal, 1)
signal.Notify(tty.ss, syscall.SIGWINCH)
go func() {
defer close(tty.ws)
for sig := range tty.ss {
switch sig {
case syscall.SIGWINCH:
if w, h, err := tty.size(); err == nil {
tty.ws <- WINSIZE{
W: w,
H: h,
}
}
default:
}
}
}()
return tty, nil
}
func (tty *TTY) buffered() bool {
return tty.bin.Buffered() > 0
}
func (tty *TTY) readRune() (rune, error) {
r, _, err := tty.bin.ReadRune()
return r, err
}
func (tty *TTY) close() error {
signal.Stop(tty.ss)
close(tty.ss)
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(tty.in.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&tty.termios)), 0, 0, 0)
return err
}
func (tty *TTY) size() (int, int, error) {
x, y, _, _, err := tty.sizePixel()
return x, y, err
}
func (tty *TTY) sizePixel() (int, int, int, int, error) {
var dim [4]uint16
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(tty.out.Fd()), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dim)), 0, 0, 0); err != 0 {
return -1, -1, -1, -1, err
}
return int(dim[1]), int(dim[0]), int(dim[2]), int(dim[3]), nil
}
func (tty *TTY) input() *os.File {
return tty.in
}
func (tty *TTY) output() *os.File {
return tty.out
}
func (tty *TTY) raw() (func() error, error) {
termios, err := unix.IoctlGetTermios(int(tty.in.Fd()), ioctlReadTermios)
if err != nil {
return nil, err
}
backup := *termios
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
termios.Oflag &^= unix.OPOST
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
termios.Cflag &^= unix.CSIZE | unix.PARENB
termios.Cflag |= unix.CS8
termios.Cc[unix.VMIN] = 1
termios.Cc[unix.VTIME] = 0
if err := unix.IoctlSetTermios(int(tty.in.Fd()), ioctlWriteTermios, termios); err != nil {
return nil, err
}
return func() error {
if err := unix.IoctlSetTermios(int(tty.in.Fd()), ioctlWriteTermios, &backup); err != nil {
return err
}
return nil
}, nil
}
func (tty *TTY) sigwinch() chan WINSIZE {
return tty.ws
}

333
vendor/github.com/mattn/go-tty/tty_windows.go generated vendored Normal file
View File

@ -0,0 +1,333 @@
// +build windows
package tty
import (
"os"
"errors"
"syscall"
"unsafe"
"github.com/mattn/go-isatty"
)
const (
rightAltPressed = 1
leftAltPressed = 2
rightCtrlPressed = 4
leftCtrlPressed = 8
shiftPressed = 0x0010
ctrlPressed = rightCtrlPressed | leftCtrlPressed
altPressed = rightAltPressed | leftAltPressed
)
const (
enableProcessedInput = 0x1
enableLineInput = 0x2
enableEchoInput = 0x4
enableWindowInput = 0x8
enableMouseInput = 0x10
enableInsertMode = 0x20
enableQuickEditMode = 0x40
enableExtendedFlag = 0x80
enableProcessedOutput = 1
enableWrapAtEolOutput = 2
keyEvent = 0x1
mouseEvent = 0x2
windowBufferSizeEvent = 0x4
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procAllocConsole = kernel32.NewProc("AllocConsole")
procSetStdHandle = kernel32.NewProc("SetStdHandle")
procGetStdHandle = kernel32.NewProc("GetStdHandle")
procSetConsoleScreenBufferSize = kernel32.NewProc("SetConsoleScreenBufferSize")
procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer")
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
procWriteConsoleOutputCharacter = kernel32.NewProc("WriteConsoleOutputCharacterW")
procWriteConsoleOutputAttribute = kernel32.NewProc("WriteConsoleOutputAttribute")
procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW")
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
procScrollConsoleScreenBuffer = kernel32.NewProc("ScrollConsoleScreenBufferW")
)
type wchar uint16
type short int16
type dword uint32
type word uint16
type coord struct {
x short
y short
}
type smallRect struct {
left short
top short
right short
bottom short
}
type consoleScreenBufferInfo struct {
size coord
cursorPosition coord
attributes word
window smallRect
maximumWindowSize coord
}
type consoleCursorInfo struct {
size dword
visible int32
}
type inputRecord struct {
eventType word
_ [2]byte
event [16]byte
}
type keyEventRecord struct {
keyDown int32
repeatCount word
virtualKeyCode word
virtualScanCode word
unicodeChar wchar
controlKeyState dword
}
type windowBufferSizeRecord struct {
size coord
}
type mouseEventRecord struct {
mousePos coord
buttonState dword
controlKeyState dword
eventFlags dword
}
type charInfo struct {
unicodeChar wchar
attributes word
}
type TTY struct {
in *os.File
out *os.File
st uint32
rs []rune
ws chan WINSIZE
}
func readConsoleInput(fd uintptr, record *inputRecord) (err error) {
var w uint32
r1, _, err := procReadConsoleInput.Call(fd, uintptr(unsafe.Pointer(record)), 1, uintptr(unsafe.Pointer(&w)))
if r1 == 0 {
return err
}
return nil
}
func open() (*TTY, error) {
tty := new(TTY)
if false && isatty.IsTerminal(os.Stdin.Fd()) {
tty.in = os.Stdin
} else {
in, err := syscall.Open("CONIN$", syscall.O_RDWR, 0)
if err != nil {
return nil, err
}
tty.in = os.NewFile(uintptr(in), "/dev/tty")
}
if isatty.IsTerminal(os.Stdout.Fd()) {
tty.out = os.Stdout
} else {
procAllocConsole.Call()
out, err := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
if err != nil {
return nil, err
}
tty.out = os.NewFile(uintptr(out), "/dev/tty")
}
h := tty.in.Fd()
var st uint32
r1, _, err := procGetConsoleMode.Call(h, uintptr(unsafe.Pointer(&st)))
if r1 == 0 {
return nil, err
}
tty.st = st
st &^= enableEchoInput
st &^= enableInsertMode
st &^= enableLineInput
st &^= enableMouseInput
st &^= enableWindowInput
st &^= enableExtendedFlag
st &^= enableQuickEditMode
// ignore error
procSetConsoleMode.Call(h, uintptr(st))
tty.ws = make(chan WINSIZE)
return tty, nil
}
func (tty *TTY) buffered() bool {
return len(tty.rs) > 0
}
func (tty *TTY) readRune() (rune, error) {
if len(tty.rs) > 0 {
r := tty.rs[0]
tty.rs = tty.rs[1:]
return r, nil
}
var ir inputRecord
err := readConsoleInput(tty.in.Fd(), &ir)
if err != nil {
return 0, err
}
switch ir.eventType {
case windowBufferSizeEvent:
wr := (*windowBufferSizeRecord)(unsafe.Pointer(&ir.event))
tty.ws <- WINSIZE{
W: int(wr.size.x),
H: int(wr.size.y),
}
case keyEvent:
kr := (*keyEventRecord)(unsafe.Pointer(&ir.event))
if kr.keyDown != 0 {
if kr.controlKeyState&altPressed != 0 && kr.unicodeChar > 0 {
tty.rs = []rune{rune(kr.unicodeChar)}
return rune(0x1b), nil
}
if kr.unicodeChar > 0 {
if kr.controlKeyState&shiftPressed != 0 {
switch kr.unicodeChar {
case 0x09:
tty.rs = []rune{0x5b, 0x5a}
return rune(0x1b), nil
}
}
return rune(kr.unicodeChar), nil
}
vk := kr.virtualKeyCode
switch vk {
case 0x21: // page-up
tty.rs = []rune{0x5b, 0x35, 0x7e}
return rune(0x1b), nil
case 0x22: // page-down
tty.rs = []rune{0x5b, 0x36, 0x7e}
return rune(0x1b), nil
case 0x23: // end
tty.rs = []rune{0x5b, 0x46}
return rune(0x1b), nil
case 0x24: // home
tty.rs = []rune{0x5b, 0x48}
return rune(0x1b), nil
case 0x25: // left
tty.rs = []rune{0x5b, 0x44}
return rune(0x1b), nil
case 0x26: // up
tty.rs = []rune{0x5b, 0x41}
return rune(0x1b), nil
case 0x27: // right
tty.rs = []rune{0x5b, 0x43}
return rune(0x1b), nil
case 0x28: // down
tty.rs = []rune{0x5b, 0x42}
return rune(0x1b), nil
case 0x2e: // delete
tty.rs = []rune{0x5b, 0x33, 0x7e}
return rune(0x1b), nil
case 0x70, 0x71, 0x72, 0x73: // F1,F2,F3,F4
tty.rs = []rune{0x5b, 0x4f, rune(vk) - 0x20}
return rune(0x1b), nil
case 0x074, 0x75, 0x76, 0x77: // F5,F6,F7,F8
tty.rs = []rune{0x5b, 0x31, rune(vk) - 0x3f, 0x7e}
return rune(0x1b), nil
case 0x78, 0x79: // F9,F10
tty.rs = []rune{0x5b, 0x32, rune(vk) - 0x48, 0x7e}
return rune(0x1b), nil
case 0x7a, 0x7b: // F11,F12
tty.rs = []rune{0x5b, 0x32, rune(vk) - 0x47, 0x7e}
return rune(0x1b), nil
}
return 0, nil
}
}
return 0, nil
}
func (tty *TTY) close() error {
close(tty.ws)
procSetConsoleMode.Call(tty.in.Fd(), uintptr(tty.st))
return nil
}
func (tty *TTY) size() (int, int, error) {
var csbi consoleScreenBufferInfo
r1, _, err := procGetConsoleScreenBufferInfo.Call(tty.out.Fd(), uintptr(unsafe.Pointer(&csbi)))
if r1 == 0 {
return 0, 0, err
}
return int(csbi.window.right - csbi.window.left + 1), int(csbi.window.bottom - csbi.window.top + 1), nil
}
func (tty *TTY) sizePixel() (int, int, int, int, error) {
x, y, err := tty.size()
if err != nil {
x = -1
y = -1
}
return x, y, -1, -1, errors.New("no implemented method for querying size in pixels on Windows")
}
func (tty *TTY) input() *os.File {
return tty.in
}
func (tty *TTY) output() *os.File {
return tty.out
}
func (tty *TTY) raw() (func() error, error) {
var st uint32
r1, _, err := procGetConsoleMode.Call(tty.in.Fd(), uintptr(unsafe.Pointer(&st)))
if r1 == 0 {
return nil, err
}
mode := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput)
r1, _, err = procSetConsoleMode.Call(tty.in.Fd(), uintptr(mode))
if r1 == 0 {
return nil, err
}
return func() error {
r1, _, err := procSetConsoleMode.Call(tty.in.Fd(), uintptr(st))
if r1 == 0 {
return err
}
return nil
}, nil
}
func (tty *TTY) sigwinch() chan WINSIZE {
return tty.ws
}

View File

@ -35,24 +35,22 @@ type Image struct {
var (
sixelCapable, isIterm2 bool
charBoxWidthInPixels, charBoxHeightInPixels float64
charBoxWidthColumns, charBoxHeightRows int
lastImageDimensions image.Rectangle
)
func init() {
// example query: "\033[0c"
// possible answer from the terminal (here xterm): "\033[[?63;1;2;4;6;9;15;22c"
// 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.
termCapabilities := queryTerm("\033[0c")
for i, cap := range termCapabilities {
if i == 0 || i == len(termCapabilities)-1 {
if i == 0 || i == len(termCapabilities) - 1 {
continue
}
if string(cap) == `4` {
sixelCapable = true
// terminal character box size measured in pixels
charBoxWidthInPixels, charBoxHeightInPixels = getTermCharBoxSize()
}
}
// # https://superuser.com/a/683971
@ -79,9 +77,10 @@ func (self *Image) Draw(buf *Buffer) {
}
}
// urxvt pixbuf / ...
// fall back - draw with box characters
// possible enhancement: make use of further box characters like chafa:
// https://hpjansson.org/chafa/
// https://github.com/hpjansson/chafa/
self.drawFallBack(buf)
}
@ -269,27 +268,49 @@ func (self *Image) drawANSI(buf *Buffer) (err error) {
imageWidthInColumns := self.Inner.Dx()
imageHeightInRows := self.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 err
}
// update if value is more precise
if termWidthInColumns > charBoxWidthColumns {
charBoxWidthInPixels = charBoxWidthInPixelsTemp
}
if termHeightInRows > charBoxHeightRows {
charBoxHeightInPixels = charBoxHeightInPixelsTemp
}
// calculate image size in pixels
imageWidthInPixels := int(float64(imageWidthInColumns) * charBoxWidthInPixels)
imageHeightInPixels := int(float64(imageHeightInRows) * charBoxHeightInPixels)
// 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 fmt.Errorf("could not calculate the image size in pixels")
}
termWidthInColumns, termHeightInRows := getTermSizeInChars()
// handle only partially displayed image
// otherwise we get scrolling
var needsCrop bool
imgCroppedWidth := imageWidthInPixels
imgCroppedHeight := imageHeightInPixels
if self.Max.X > int(termWidthInColumns)+1 {
imgCroppedWidth = int(float64(int(termWidthInColumns)-self.Inner.Min.X) * charBoxWidthInPixels)
needsCrop = true
var needsCropX, needsCropY bool
var imgCroppedWidth, imgCroppedHeight int
imgCroppedWidth = imageWidthInPixels
imgCroppedHeight = imageHeightInPixels
if self.Max.Y >= int(termHeightInRows) {
var scrollExtraRows int
// remove last 2 rows for xterm when cropped vertically to prevent scrolling
if len(os.Getenv("XTERM_VERSION")) > 0 {
scrollExtraRows = 2
}
// subtract 1 pixel for small deviations from char box size (float64)
imgCroppedHeight = int(float64(int(termHeightInRows) - self.Inner.Min.Y - scrollExtraRows) * charBoxHeightInPixels) - 1
needsCropY = true
}
if self.Max.Y > int(termHeightInRows)+1 {
imgCroppedHeight = int(float64(int(termHeightInRows)-self.Inner.Min.Y) * charBoxHeightInPixels)
needsCrop = true
if self.Max.X >= int(termWidthInColumns) {
var scrollExtraColumns int
imgCroppedWidth = int(float64(int(termWidthInColumns) - self.Inner.Min.X - scrollExtraColumns) * charBoxWidthInPixels) - 1
needsCropX = true
}
// this is meant for comparison and for positioning in the ANSI string
@ -304,7 +325,7 @@ func (self *Image) drawANSI(buf *Buffer) (err error) {
// resize and crop the image //
img := imaging.Resize(self.Image, imageWidthInPixels, imageHeightInPixels, imaging.Lanczos)
if needsCrop {
if needsCropX || needsCropY {
img = imaging.Crop(img, image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: imgCroppedWidth, Y: imgCroppedHeight}})
}
@ -330,7 +351,15 @@ func (self *Image) drawANSI(buf *Buffer) (err error) {
}
skipIterm2:
// 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
// sixel
// https://vt100.net/docs/vt3xx-gp/chapter14.html
if sixelCapable {
byteBuf := new(bytes.Buffer)
enc := sixel.NewEncoder(byteBuf)
@ -338,12 +367,12 @@ func (self *Image) drawANSI(buf *Buffer) (err error) {
if err := enc.Encode(img); err != nil {
return err
}
// 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.
self.Block.ANSIString = fmt.Sprintf("\033[%d;%dH\033[?8452h%s", imageDimensions.Min.Y, imageDimensions.Min.X, byteBuf.String())
// test string "HI"
// self.Block.ANSIString = fmt.Sprintf("\033[%d;%dH\033[?8452h%s", self.Inner.Min.Y+1, self.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\\")
@ -354,16 +383,44 @@ func (self *Image) drawANSI(buf *Buffer) (err error) {
return errors.New("no method applied for ANSI drawing")
}
func getTermCharBoxSize() (x, y float64) {
if cx, cy := getTermSizeInChars(); cx != 0 && cy != 0 {
px, py := getTermSizeInPixels()
x = float64(px) / float64(cx)
y = float64(py) / float64(cy)
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 err == nil {
if cx > 0 && cy > 0 {
if px <= 0 || py <= 0 {
px, py = getTermSizeInPixels()
}
} else {
if cx, cy = getTermSizeInChars(); cx != 0 && cy != 0 {
if px <= 0 || py <= 0 {
px, py = getTermSizeInPixels()
}
} else {
return
}
}
}
termWidthInColumns = cx
termHeightInRows = cy
termWidthInPixels = px
termHeightInPixels = py
charBoxWidthInPixels = float64(px) / float64(cx)
charBoxHeightInPixels = float64(py) / float64(cy)
return
}
func getTermSizeInChars() (x, y uint) {
func getTermSizeInChars() (x, y int) {
// query terminal size in character boxes
// answer: <termHeightInRows>;<termWidthInColumns>t
q := queryTerm("\033[18t")
@ -374,8 +431,8 @@ func getTermSizeInChars() (x, y uint) {
if yy, err := strconv.Atoi(string(q[1])); err == nil {
if xx, err := strconv.Atoi(string(q[2])); err == nil {
x = uint(xx)
y = uint(yy)
x = xx
y = yy
} else {
return
}
@ -386,7 +443,7 @@ func getTermSizeInChars() (x, y uint) {
return
}
func getTermSizeInPixels() (x, y uint) {
func getTermSizeInPixels() (x, y int) {
// query terminal size in pixels
// answer: <termHeightInPixels>;<termWidthInPixels>t
q := queryTerm("\033[14t")
@ -397,8 +454,8 @@ func getTermSizeInPixels() (x, y uint) {
if yy, err := strconv.Atoi(string(q[1])); err == nil {
if xx, err := strconv.Atoi(string(q[2])); err == nil {
x = uint(xx)
y = uint(yy)
x = xx
y = yy
} else {
return
}
@ -410,29 +467,29 @@ func getTermSizeInPixels() (x, y uint) {
}
func queryTerm(qs string) (ret [][]rune) {
// temporary fix for xterm
// temporary fix for xterm - not completely sure if still needed
// otherwise TUI wouldn't react to any further events
// resizing still works though
if len(os.Getenv("XTERM_VERSION")) > 0 {
if len(os.Getenv("XTERM_VERSION")) > 0 && qs != "\033[0c" {
return
}
var b []rune
tty, err := tty.Open()
t, err := tty.Open()
if err != nil {
return
}
defer tty.Close()
ch := make(chan bool, 1)
go func() {
defer t.Close()
// query terminal
fmt.Printf(qs)
for {
r, err := tty.ReadRune()
r, err := t.ReadRune()
if err != nil {
return
}
@ -452,7 +509,8 @@ func queryTerm(qs string) (ret [][]rune) {
ch <- true
}()
timer := time.NewTimer(50 * time.Millisecond)
// on my system the terminals mlterm, xterm need at least around 100 microseconds
timer := time.NewTimer(500 * time.Microsecond)
defer timer.Stop()
select {