// +build !windows // +build !tcell package tui /* #include <ncurses.h> #include <locale.h> #cgo !static LDFLAGS: -lncurses #cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl #cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch SCREEN *c_newterm () { return newterm(NULL, stderr, stdin); } */ import "C" import ( "fmt" "os" "strings" "syscall" "time" "unicode/utf8" ) type ColorPair int16 type Attr C.int type WindowImpl C.WINDOW const ( Bold = C.A_BOLD Dim = C.A_DIM Blink = C.A_BLINK Reverse = C.A_REVERSE Underline = C.A_UNDERLINE ) const ( AttrRegular Attr = 0 ) // Pallete const ( ColDefault ColorPair = iota ColNormal ColPrompt ColMatch ColCurrent ColCurrentMatch ColSpinner ColInfo ColCursor ColSelected ColHeader ColBorder ColUser // Should be the last entry ) var ( _in *os.File _screen *C.SCREEN _colorMap map[int]ColorPair _colorFn func(ColorPair, Attr) C.int ) func init() { _colorMap = make(map[int]ColorPair) } func (a Attr) Merge(b Attr) Attr { return a | b } func DefaultTheme() *ColorTheme { if C.tigetnum(C.CString("colors")) >= 256 { return Dark256 } return Default16 } func Init(theme *ColorTheme, black bool, mouse bool) { { in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) if err != nil { panic("Failed to open /dev/tty") } _in = in // Break STDIN // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) } C.setlocale(C.LC_ALL, C.CString("")) _screen = C.c_newterm() if _screen == nil { fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) os.Exit(2) } C.set_term(_screen) if mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) } C.noecho() C.raw() // stty dsusp undef _color = theme != nil if _color { C.start_color() InitTheme(theme, black) initPairs(theme) C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) _colorFn = attrColored } else { _colorFn = attrMono } } func initPairs(theme *ColorTheme) { C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) initPair := func(group ColorPair, fg Color, bg Color) { C.init_pair(C.short(group), C.short(fg), C.short(bg)) } initPair(ColNormal, theme.Fg, theme.Bg) initPair(ColPrompt, theme.Prompt, theme.Bg) initPair(ColMatch, theme.Match, theme.Bg) initPair(ColCurrent, theme.Current, theme.DarkBg) initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg) initPair(ColSpinner, theme.Spinner, theme.Bg) initPair(ColInfo, theme.Info, theme.Bg) initPair(ColCursor, theme.Cursor, theme.DarkBg) initPair(ColSelected, theme.Selected, theme.DarkBg) initPair(ColHeader, theme.Header, theme.Bg) initPair(ColBorder, theme.Border, theme.Bg) } func Pause() { C.endwin() } func Resume() bool { return false } func Close() { C.endwin() C.delscreen(_screen) } func NewWindow(top int, left int, width int, height int, border bool) *Window { win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) if _color { C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) } if border { attr := _colorFn(ColBorder, 0) C.wattron(win, attr) C.box(win, 0, 0) C.wattroff(win, attr) } return &Window{ impl: (*WindowImpl)(win), Top: top, Left: left, Width: width, Height: height, } } func attrColored(pair ColorPair, a Attr) C.int { var attr C.int if pair > 0 { attr = C.COLOR_PAIR(C.int(pair)) } return attr | C.int(a) } func attrMono(pair ColorPair, a Attr) C.int { var attr C.int switch pair { case ColCurrent: if C.int(a)&C.A_BOLD == C.A_BOLD { attr = C.A_REVERSE } case ColMatch: attr = C.A_UNDERLINE case ColCurrentMatch: attr = C.A_UNDERLINE | C.A_REVERSE } if C.int(a)&C.A_BOLD == C.A_BOLD { attr = attr | C.A_BOLD } return attr } func MaxX() int { return int(C.COLS) } func MaxY() int { return int(C.LINES) } func (w *Window) win() *C.WINDOW { return (*C.WINDOW)(w.impl) } func (w *Window) Close() { C.delwin(w.win()) } func (w *Window) Enclose(y int, x int) bool { return bool(C.wenclose(w.win(), C.int(y), C.int(x))) } func (w *Window) Move(y int, x int) { C.wmove(w.win(), C.int(y), C.int(x)) } func (w *Window) MoveAndClear(y int, x int) { w.Move(y, x) C.wclrtoeol(w.win()) } func (w *Window) Print(text string) { C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune { if r < 32 { return -1 } return r }, text))) } func (w *Window) CPrint(pair ColorPair, a Attr, text string) { attr := _colorFn(pair, a) C.wattron(w.win(), attr) w.Print(text) C.wattroff(w.win(), attr) } func Clear() { C.clear() C.endwin() } func Refresh() { C.refresh() } func (w *Window) Erase() { C.werase(w.win()) } func (w *Window) Fill(str string) bool { return C.waddstr(w.win(), C.CString(str)) == C.OK } func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { attr := _colorFn(PairFor(fg, bg), a) C.wattron(w.win(), attr) ret := w.Fill(str) C.wattroff(w.win(), attr) return ret } func RefreshWindows(windows []*Window) { for _, w := range windows { C.wnoutrefresh(w.win()) } C.doupdate() } func PairFor(fg Color, bg Color) ColorPair { key := (int(fg) << 8) + int(bg) if found, prs := _colorMap[key]; prs { return found } id := ColorPair(len(_colorMap) + int(ColUser)) C.init_pair(C.short(id), C.short(fg), C.short(bg)) _colorMap[key] = id return id } func getch(nonblock bool) int { b := make([]byte, 1) syscall.SetNonblock(int(_in.Fd()), nonblock) _, err := _in.Read(b) if err != nil { return -1 } return int(b[0]) } func GetBytes() []byte { c := getch(false) _buf = append(_buf, byte(c)) for { c = getch(true) if c == -1 { break } _buf = append(_buf, byte(c)) } return _buf } // 27 (91 79) 77 type x y func mouseSequence(sz *int) Event { if len(_buf) < 6 { return Event{Invalid, 0, nil} } *sz = 6 switch _buf[3] { case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl mod := _buf[3] >= 36 down := _buf[3]%2 == 0 x := int(_buf[4] - 33) y := int(_buf[5] - 33) double := false if down { now := time.Now() if now.Sub(_prevDownTime) < doubleClickDuration { _clickY = append(_clickY, y) } else { _clickY = []int{y} } _prevDownTime = now } else { if len(_clickY) > 1 && _clickY[0] == _clickY[1] && time.Now().Sub(_prevDownTime) < doubleClickDuration { double = true } } return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl mod := _buf[3] >= 100 s := 1 - int(_buf[3]%2)*2 x := int(_buf[4] - 33) y := int(_buf[5] - 33) return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} } return Event{Invalid, 0, nil} } func escSequence(sz *int) Event { if len(_buf) < 2 { return Event{ESC, 0, nil} } *sz = 2 switch _buf[1] { case 13: return Event{AltEnter, 0, nil} case 32: return Event{AltSpace, 0, nil} case 47: return Event{AltSlash, 0, nil} case 98: return Event{AltB, 0, nil} case 100: return Event{AltD, 0, nil} case 102: return Event{AltF, 0, nil} case 127: return Event{AltBS, 0, nil} case 91, 79: if len(_buf) < 3 { return Event{Invalid, 0, nil} } *sz = 3 switch _buf[2] { case 68: return Event{Left, 0, nil} case 67: return Event{Right, 0, nil} case 66: return Event{Down, 0, nil} case 65: return Event{Up, 0, nil} case 90: return Event{BTab, 0, nil} case 72: return Event{Home, 0, nil} case 70: return Event{End, 0, nil} case 77: return mouseSequence(sz) case 80: return Event{F1, 0, nil} case 81: return Event{F2, 0, nil} case 82: return Event{F3, 0, nil} case 83: return Event{F4, 0, nil} case 49, 50, 51, 52, 53, 54: if len(_buf) < 4 { return Event{Invalid, 0, nil} } *sz = 4 switch _buf[2] { case 50: if len(_buf) == 5 && _buf[4] == 126 { *sz = 5 switch _buf[3] { case 48: return Event{F9, 0, nil} case 49: return Event{F10, 0, nil} } } // Bracketed paste mode \e[200~ / \e[201 if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 { *sz = 6 return Event{Invalid, 0, nil} } return Event{Invalid, 0, nil} // INS case 51: return Event{Del, 0, nil} case 52: return Event{End, 0, nil} case 53: return Event{PgUp, 0, nil} case 54: return Event{PgDn, 0, nil} case 49: switch _buf[3] { case 126: return Event{Home, 0, nil} case 53, 55, 56, 57: if len(_buf) == 5 && _buf[4] == 126 { *sz = 5 switch _buf[3] { case 53: return Event{F5, 0, nil} case 55: return Event{F6, 0, nil} case 56: return Event{F7, 0, nil} case 57: return Event{F8, 0, nil} } } return Event{Invalid, 0, nil} case 59: if len(_buf) != 6 { return Event{Invalid, 0, nil} } *sz = 6 switch _buf[4] { case 50: switch _buf[5] { case 68: return Event{Home, 0, nil} case 67: return Event{End, 0, nil} } case 53: switch _buf[5] { case 68: return Event{SLeft, 0, nil} case 67: return Event{SRight, 0, nil} } } // _buf[4] } // _buf[3] } // _buf[2] } // _buf[2] } // _buf[1] if _buf[1] >= 'a' && _buf[1] <= 'z' { return Event{AltA + int(_buf[1]) - 'a', 0, nil} } return Event{Invalid, 0, nil} } func GetChar() Event { if len(_buf) == 0 { _buf = GetBytes() } if len(_buf) == 0 { panic("Empty _buffer") } sz := 1 defer func() { _buf = _buf[sz:] }() switch _buf[0] { case CtrlC: return Event{CtrlC, 0, nil} case CtrlG: return Event{CtrlG, 0, nil} case CtrlQ: return Event{CtrlQ, 0, nil} case 127: return Event{BSpace, 0, nil} case ESC: return escSequence(&sz) } // CTRL-A ~ CTRL-Z if _buf[0] <= CtrlZ { return Event{int(_buf[0]), 0, nil} } r, rsz := utf8.DecodeRune(_buf) if r == utf8.RuneError { return Event{ESC, 0, nil} } sz = rsz return Event{Rune, r, nil} }