// +build ncurses // +build !windows // +build !tcell package tui /* #include #include #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 FILE* c_tty() { return fopen("/dev/tty", "r"); } SCREEN* c_newterm(FILE* tty) { return newterm(NULL, stderr, tty); } int c_getcurx(WINDOW* win) { return getcurx(win); } */ import "C" import ( "os" "strconv" "strings" "time" "unicode/utf8" ) func HasFullscreenRenderer() bool { return true } type Attr C.uint type CursesWindow struct { impl *C.WINDOW top int left int width int height int } func (w *CursesWindow) Top() int { return w.top } func (w *CursesWindow) Left() int { return w.left } func (w *CursesWindow) Width() int { return w.width } func (w *CursesWindow) Height() int { return w.height } func (w *CursesWindow) Refresh() { C.wnoutrefresh(w.impl) } func (w *CursesWindow) FinishFill() { // NO-OP } const ( Bold Attr = C.A_BOLD Dim = C.A_DIM Blink = C.A_BLINK Reverse = C.A_REVERSE Underline = C.A_UNDERLINE ) var Italic Attr = C.A_VERTICAL << 1 // FIXME const ( AttrRegular Attr = 0 ) var ( _screen *C.SCREEN _colorMap map[int]int16 _colorFn func(ColorPair, Attr) (C.short, C.int) ) func init() { _colorMap = make(map[int]int16) if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") { Italic = C.A_NORMAL } } func (a Attr) Merge(b Attr) Attr { return a | b } func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if C.tigetnum(C.CString("colors")) >= 256 { return Dark256 } return Default16 } func (r *FullscreenRenderer) Init() { C.setlocale(C.LC_ALL, C.CString("")) tty := C.c_tty() if tty == nil { errorExit("Failed to open /dev/tty") } _screen = C.c_newterm(tty) if _screen == nil { errorExit("Invalid $TERM: " + os.Getenv("TERM")) } C.set_term(_screen) if r.mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mouseinterval(0) } C.noecho() C.raw() // stty dsusp undef C.nonl() C.keypad(C.stdscr, true) delay := 50 delayEnv := os.Getenv("ESCDELAY") if len(delayEnv) > 0 { num, err := strconv.Atoi(delayEnv) if err == nil && num >= 0 { delay = num } } C.set_escdelay(C.int(delay)) if r.theme != nil { C.start_color() initTheme(r.theme, r.defaultTheme(), r.forceBlack) initPairs(r.theme) C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) _colorFn = attrColored } else { initTheme(r.theme, nil, r.forceBlack) _colorFn = attrMono } C.nodelay(C.stdscr, true) ch := C.getch() if ch != C.ERR { C.ungetch(ch) } C.nodelay(C.stdscr, false) } func initPairs(theme *ColorTheme) { C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) for _, pair := range []ColorPair{ ColNormal, ColPrompt, ColMatch, ColCurrent, ColCurrentMatch, ColSpinner, ColInfo, ColCursor, ColSelected, ColHeader, ColBorder} { C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg())) } } func (r *FullscreenRenderer) Pause() { C.endwin() } func (r *FullscreenRenderer) Resume() bool { return false } func (r *FullscreenRenderer) Close() { C.endwin() C.delscreen(_screen) } func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) if r.theme != nil { C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) } // FIXME Does not implement BorderHorizontal if borderStyle != BorderNone { pair, attr := _colorFn(ColBorder, 0) C.wcolor_set(win, pair, nil) C.wattron(win, attr) C.box(win, 0, 0) C.wattroff(win, attr) C.wcolor_set(win, 0, nil) } return &CursesWindow{ impl: win, top: top, left: left, width: width, height: height, } } func attrColored(color ColorPair, a Attr) (C.short, C.int) { return C.short(color.index()), C.int(a) } func attrMono(color ColorPair, a Attr) (C.short, C.int) { return 0, C.int(attrFor(color, a)) } func (r *FullscreenRenderer) MaxX() int { return int(C.COLS) } func (r *FullscreenRenderer) MaxY() int { return int(C.LINES) } func (w *CursesWindow) Close() { C.delwin(w.impl) } func (w *CursesWindow) Enclose(y int, x int) bool { return bool(C.wenclose(w.impl, C.int(y), C.int(x))) } func (w *CursesWindow) Move(y int, x int) { C.wmove(w.impl, C.int(y), C.int(x)) } func (w *CursesWindow) MoveAndClear(y int, x int) { w.Move(y, x) C.wclrtoeol(w.impl) } func (w *CursesWindow) Print(text string) { C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune { if r < 32 { return -1 } return r }, text))) } func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) { p, a := _colorFn(color, attr) C.wcolor_set(w.impl, p, nil) C.wattron(w.impl, a) w.Print(text) C.wattroff(w.impl, a) C.wcolor_set(w.impl, 0, nil) } func (r *FullscreenRenderer) Clear() { C.clear() C.endwin() } func (r *FullscreenRenderer) Refresh() { C.refresh() } func (w *CursesWindow) Erase() { C.werase(w.impl) } func (w *CursesWindow) X() int { return int(C.c_getcurx(w.impl)) } func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } func (r *FullscreenRenderer) IsOptimized() bool { return true } func (w *CursesWindow) Fill(str string) FillReturn { if C.waddstr(w.impl, C.CString(str)) == C.OK { return FillContinue } return FillSuspend } func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn { index := ColorPair{fg, bg, -1}.index() C.wcolor_set(w.impl, C.short(index), nil) C.wattron(w.impl, C.int(attr)) ret := w.Fill(str) C.wattroff(w.impl, C.int(attr)) C.wcolor_set(w.impl, 0, nil) return ret } func (r *FullscreenRenderer) RefreshWindows(windows []Window) { for _, w := range windows { w.Refresh() } C.doupdate() } func (p ColorPair) index() int16 { if p.id >= 0 { return p.id } // ncurses does not support 24-bit colors if p.is24() { return ColDefault.index() } key := p.key() if found, prs := _colorMap[key]; prs { return found } id := int16(len(_colorMap)) + ColUser.id C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg())) _colorMap[key] = id return id } func consume(expects ...rune) bool { for _, r := range expects { if int(C.getch()) != int(r) { return false } } return true } func escSequence() Event { C.nodelay(C.stdscr, true) defer func() { C.nodelay(C.stdscr, false) }() c := C.getch() switch c { case C.ERR: return Event{ESC, 0, nil} case CtrlM: return Event{CtrlAltM, 0, nil} case '/': return Event{AltSlash, 0, nil} case ' ': return Event{AltSpace, 0, nil} case 127, C.KEY_BACKSPACE: return Event{AltBS, 0, nil} case '[': // Bracketed paste mode (printf "\e[?2004h") // \e[200~ TEXT \e[201~ if consume('2', '0', '0', '~') { return Event{Invalid, 0, nil} } } if c >= 'a' && c <= 'z' { return Event{AltA + int(c) - 'a', 0, nil} } if c >= '0' && c <= '9' { return Event{Alt0 + int(c) - '0', 0, nil} } // Don't care. Ignore the rest. for ; c != C.ERR; c = C.getch() { } return Event{Invalid, 0, nil} } func (r *FullscreenRenderer) GetChar() Event { c := C.getch() switch c { case C.ERR: // Unexpected error from blocking read r.Close() errorExit("Failed to read /dev/tty") case C.KEY_UP: return Event{Up, 0, nil} case C.KEY_DOWN: return Event{Down, 0, nil} case C.KEY_LEFT: return Event{Left, 0, nil} case C.KEY_RIGHT: return Event{Right, 0, nil} case C.KEY_HOME: return Event{Home, 0, nil} case C.KEY_END: return Event{End, 0, nil} case C.KEY_BACKSPACE: return Event{BSpace, 0, nil} case C.KEY_F0 + 1: return Event{F1, 0, nil} case C.KEY_F0 + 2: return Event{F2, 0, nil} case C.KEY_F0 + 3: return Event{F3, 0, nil} case C.KEY_F0 + 4: return Event{F4, 0, nil} case C.KEY_F0 + 5: return Event{F5, 0, nil} case C.KEY_F0 + 6: return Event{F6, 0, nil} case C.KEY_F0 + 7: return Event{F7, 0, nil} case C.KEY_F0 + 8: return Event{F8, 0, nil} case C.KEY_F0 + 9: return Event{F9, 0, nil} case C.KEY_F0 + 10: return Event{F10, 0, nil} case C.KEY_F0 + 11: return Event{F11, 0, nil} case C.KEY_F0 + 12: return Event{F12, 0, nil} case C.KEY_DC: return Event{Del, 0, nil} case C.KEY_PPAGE: return Event{PgUp, 0, nil} case C.KEY_NPAGE: return Event{PgDn, 0, nil} case C.KEY_BTAB: return Event{BTab, 0, nil} case C.KEY_ENTER: return Event{CtrlM, 0, nil} case C.KEY_SLEFT: return Event{SLeft, 0, nil} case C.KEY_SRIGHT: return Event{SRight, 0, nil} case C.KEY_MOUSE: var me C.MEVENT if C.getmouse(&me) != C.ERR { mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0 x := int(me.x) y := int(me.y) /* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */ if (me.bstate & C.BUTTON1_PRESSED) > 0 { now := time.Now() if now.Sub(r.prevDownTime) < doubleClickDuration { r.clickY = append(r.clickY, y) } else { r.clickY = []int{y} r.prevDownTime = now } return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}} } else if (me.bstate & C.BUTTON1_RELEASED) > 0 { double := false if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}} } else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 { return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}} } else if (me.bstate & C.BUTTON4_PRESSED) > 0 { return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}} } } return Event{Invalid, 0, nil} case C.KEY_RESIZE: return Event{Resize, 0, nil} case ESC: return escSequence() case 127: return Event{BSpace, 0, nil} case 0: return Event{CtrlSpace, 0, nil} } // CTRL-A ~ CTRL-Z if c >= CtrlA && c <= CtrlZ { return Event{int(c), 0, nil} } // Multi-byte character buffer := []byte{byte(c)} for { r, _ := utf8.DecodeRune(buffer) if r != utf8.RuneError { return Event{Rune, r, nil} } c := C.getch() if c == C.ERR { break } if c >= C.KEY_CODE_YES { C.ungetch(c) break } buffer = append(buffer, byte(c)) } return Event{Invalid, 0, nil} }