//go:build !windows // +build !windows package termbox import ( "bytes" "io" "os" "strconv" "strings" "unicode/utf8" "golang.org/x/sys/unix" ) // private API const ( // for future contributors: after adding something here, // you have to add the corresponding index in a terminfo // file to `terminfo.go#ti_funcs`. The values can be taken // from (ncurses) `term.h`. The builtin terminfo at terminfo_builtin.go // also needs adjusting with the new values. t_enter_ca = iota t_exit_ca t_show_cursor t_hide_cursor t_clear_screen t_sgr0 t_underline t_bold t_hidden t_blink t_dim t_cursive t_reverse t_enter_keypad t_exit_keypad t_enter_mouse t_exit_mouse t_max_funcs ) const ( coord_invalid = -2 attr_invalid = Attribute(0xFFFF) ) // ANSI escape sequence constants const ( ansiEscapeStart = "\033[" ansiCursorMove = "H" ansiSemicolon = ";" ansiSGRFg256 = "\033[38;5;" ansiSGRBg256 = "\033[48;5;" ansiSGREnd = "m" ) type input_event struct { data []byte err error } type extract_event_res int const ( event_not_extracted extract_event_res = iota event_extracted esc_wait ) var ( // term specific sequences keys []string funcs []string // termbox inner state orig_tios unix.Termios back_buffer cellbuf front_buffer cellbuf termw int termh int input_mode = InputEsc output_mode = OutputNormal out *os.File outfd uintptr in int lastfg = attr_invalid lastbg = attr_invalid lastx = coord_invalid lasty = coord_invalid cursor_x = cursor_hidden cursor_y = cursor_hidden foreground = ColorDefault background = ColorDefault inbuf = make([]byte, 0, 64) outbuf bytes.Buffer sigwinch = make(chan os.Signal, 1) sigio = make(chan os.Signal, 1) quit = make(chan int) input_comm = make(chan input_event) interrupt_comm = make(chan struct{}) intbuf = make([]byte, 0, 16) // grayscale indexes grayscale = []Attribute{ 0, 17, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 232, } ) func write_cursor(x, y int) { outbuf.WriteString(ansiEscapeStart) outbuf.Write(strconv.AppendUint(intbuf, uint64(y+1), 10)) outbuf.WriteString(ansiSemicolon) outbuf.Write(strconv.AppendUint(intbuf, uint64(x+1), 10)) outbuf.WriteString(ansiCursorMove) } func write_sgr_fg(a Attribute) { switch output_mode { case Output256, Output216, OutputGrayscale: outbuf.WriteString(ansiSGRFg256) outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) outbuf.WriteString(ansiSGREnd) case OutputRGB: r, g, b := AttributeToRGB(a) outbuf.WriteString(escapeRGB(true, r, g, b)) default: if a < ColorDarkGray { outbuf.WriteString("\033[3") outbuf.Write(strconv.AppendUint(intbuf, uint64(a-ColorBlack), 10)) outbuf.WriteString("m") } else { outbuf.WriteString("\033[9") outbuf.Write(strconv.AppendUint(intbuf, uint64(a-ColorDarkGray), 10)) outbuf.WriteString("m") } } } func write_sgr_bg(a Attribute) { switch output_mode { case Output256, Output216, OutputGrayscale: outbuf.WriteString(ansiSGRBg256) outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) outbuf.WriteString(ansiSGREnd) case OutputRGB: r, g, b := AttributeToRGB(a) outbuf.WriteString(escapeRGB(false, r, g, b)) default: if a < ColorDarkGray { outbuf.WriteString("\033[4") outbuf.Write(strconv.AppendUint(intbuf, uint64(a-ColorBlack), 10)) outbuf.WriteString("m") } else { outbuf.WriteString("\033[10") outbuf.Write(strconv.AppendUint(intbuf, uint64(a-ColorDarkGray), 10)) outbuf.WriteString("m") } } } func write_sgr(fg, bg Attribute) { switch output_mode { case Output256, Output216, OutputGrayscale: outbuf.WriteString("\033[38;5;") outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10)) outbuf.WriteString("m") outbuf.WriteString("\033[48;5;") outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10)) outbuf.WriteString("m") case OutputRGB: r, g, b := AttributeToRGB(fg) outbuf.WriteString(escapeRGB(true, r, g, b)) r, g, b = AttributeToRGB(bg) outbuf.WriteString(escapeRGB(false, r, g, b)) default: if fg < ColorDarkGray { outbuf.WriteString("\033[3") outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-ColorBlack), 10)) outbuf.WriteString(";") } else { outbuf.WriteString("\033[9") outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-ColorDarkGray), 10)) outbuf.WriteString(";") } if bg < ColorDarkGray { outbuf.WriteString("4") outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-ColorBlack), 10)) outbuf.WriteString("m") } else { outbuf.WriteString("10") outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-ColorDarkGray), 10)) outbuf.WriteString("m") } } } func escapeRGB(fg bool, r uint8, g uint8, b uint8) string { var builder strings.Builder builder.WriteString("\033[") if fg { builder.WriteString("38") } else { builder.WriteString("48") } builder.WriteString(";2;") builder.WriteString(strconv.FormatUint(uint64(r), 10)) builder.WriteByte(';') builder.WriteString(strconv.FormatUint(uint64(g), 10)) builder.WriteByte(';') builder.WriteString(strconv.FormatUint(uint64(b), 10)) builder.WriteByte('m') return builder.String() } func get_term_size(fd uintptr) (int, int) { sz, _ := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ) return int(sz.Col), int(sz.Row) } func send_attr(fg, bg Attribute) { if fg == lastfg && bg == lastbg { return } outbuf.WriteString(funcs[t_sgr0]) var fgcol, bgcol Attribute switch output_mode { case Output256: fgcol = fg & 0x1FF bgcol = bg & 0x1FF case Output216: fgcol = fg & 0xFF bgcol = bg & 0xFF if fgcol > 216 { fgcol = ColorDefault } if bgcol > 216 { bgcol = ColorDefault } if fgcol != ColorDefault { fgcol += 0x10 } if bgcol != ColorDefault { bgcol += 0x10 } case OutputGrayscale: fgcol = fg & 0x1F bgcol = bg & 0x1F if fgcol > 26 { fgcol = ColorDefault } if bgcol > 26 { bgcol = ColorDefault } if fgcol != ColorDefault { fgcol = grayscale[fgcol] } if bgcol != ColorDefault { bgcol = grayscale[bgcol] } case OutputRGB: fgcol = fg bgcol = bg default: fgcol = fg & 0xFF bgcol = bg & 0xFF } if fgcol != ColorDefault { if bgcol != ColorDefault { write_sgr(fgcol, bgcol) } else { write_sgr_fg(fgcol) } } else if bgcol != ColorDefault { write_sgr_bg(bgcol) } if fg&AttrBold != 0 { outbuf.WriteString(funcs[t_bold]) } /*if bg&AttrBold != 0 { outbuf.WriteString(funcs[t_blink]) }*/ if fg&AttrBlink != 0 { outbuf.WriteString(funcs[t_blink]) } if fg&AttrUnderline != 0 { outbuf.WriteString(funcs[t_underline]) } if fg&AttrCursive != 0 { outbuf.WriteString(funcs[t_cursive]) } if fg&AttrHidden != 0 { outbuf.WriteString(funcs[t_hidden]) } if fg&AttrDim != 0 { outbuf.WriteString(funcs[t_dim]) } if fg&AttrReverse|bg&AttrReverse != 0 { outbuf.WriteString(funcs[t_reverse]) } lastfg, lastbg = fg, bg } func send_char(x, y int, ch rune) { var buf [8]byte n := utf8.EncodeRune(buf[:], ch) if x-1 != lastx || y != lasty { write_cursor(x, y) } lastx, lasty = x, y outbuf.Write(buf[:n]) } func flush() error { _, err := io.Copy(out, &outbuf) outbuf.Reset() return err } func send_clear() error { send_attr(foreground, background) outbuf.WriteString(funcs[t_clear_screen]) if !is_cursor_hidden(cursor_x, cursor_y) { write_cursor(cursor_x, cursor_y) } // we need to invalidate cursor position too and these two vars are // used only for simple cursor positioning optimization, cursor // actually may be in the correct place, but we simply discard // optimization once and it gives us simple solution for the case when // cursor moved lastx = coord_invalid lasty = coord_invalid return flush() } func update_size_maybe() error { w, h := get_term_size(outfd) if w != termw || h != termh { termw, termh = w, h back_buffer.resize(termw, termh) front_buffer.resize(termw, termh) front_buffer.clear() return send_clear() } return nil } func tcsetattr(fd uintptr, termios unix.Termios) error { return unix.IoctlSetTermios(int(fd), tcsets, &termios) } func tcgetattr(fd uintptr) (unix.Termios, error) { tios, err := unix.IoctlGetTermios(int(fd), tcgets) if err != nil { return unix.Termios{}, err } return *tios, nil } func parse_mouse_event(event *Event, buf string) (int, bool) { if strings.HasPrefix(buf, "\033[M") && len(buf) >= 6 { // X10 mouse encoding, the simplest one // \033 [ M Cb Cx Cy b := buf[3] - 32 switch b & 3 { case 0: if b&64 != 0 { event.Key = MouseWheelUp } else { event.Key = MouseLeft } case 1: if b&64 != 0 { event.Key = MouseWheelDown } else { event.Key = MouseMiddle } case 2: event.Key = MouseRight case 3: event.Key = MouseRelease default: return 6, false } event.Type = EventMouse // KeyEvent by default if b&32 != 0 { event.Mod |= ModMotion } // the coord is 1,1 for upper left event.MouseX = int(buf[4]) - 1 - 32 event.MouseY = int(buf[5]) - 1 - 32 return 6, true } else if strings.HasPrefix(buf, "\033[<") || strings.HasPrefix(buf, "\033[") { // xterm 1006 extended mode or urxvt 1015 extended mode // xterm: \033 [ < Cb ; Cx ; Cy (M or m) // urxvt: \033 [ Cb ; Cx ; Cy M // find the first M or m, that's where we stop mi := strings.IndexAny(buf, "Mm") if mi == -1 { return 0, false } // whether it's a capital M or not isM := buf[mi] == 'M' // whether it's urxvt or not isU := false // buf[2] is safe here, because having M or m found means we have at // least 3 bytes in a string if buf[2] == '<' { buf = buf[3:mi] } else { isU = true buf = buf[2:mi] } s1 := strings.Index(buf, ";") s2 := strings.LastIndex(buf, ";") // not found or only one ';' if s1 == -1 || s2 == -1 || s1 == s2 { return 0, false } n1, err := strconv.ParseInt(buf[0:s1], 10, 64) if err != nil { return 0, false } n2, err := strconv.ParseInt(buf[s1+1:s2], 10, 64) if err != nil { return 0, false } n3, err := strconv.ParseInt(buf[s2+1:], 10, 64) if err != nil { return 0, false } // on urxvt, first number is encoded exactly as in X10, but we need to // make it zero-based, on xterm it is zero-based already if isU { n1 -= 32 } switch n1 & 3 { case 0: if n1&64 != 0 { event.Key = MouseWheelUp } else { event.Key = MouseLeft } case 1: if n1&64 != 0 { event.Key = MouseWheelDown } else { event.Key = MouseMiddle } case 2: event.Key = MouseRight case 3: event.Key = MouseRelease default: return mi + 1, false } if !isM { // on xterm mouse release is signaled by lowercase m event.Key = MouseRelease } event.Type = EventMouse // KeyEvent by default if n1&32 != 0 { event.Mod |= ModMotion } event.MouseX = int(n2) - 1 event.MouseY = int(n3) - 1 return mi + 1, true } return 0, false } func parse_escape_sequence(event *Event, buf []byte) (int, bool) { bufstr := string(buf) for i, key := range keys { if strings.HasPrefix(bufstr, key) { event.Ch = 0 event.Key = Key(0xFFFF - i) return len(key), true } } // if none of the keys match, let's try mouse sequences return parse_mouse_event(event, bufstr) } func extract_raw_event(data []byte, event *Event) bool { if len(inbuf) == 0 { return false } n := len(data) if n == 0 { return false } n = copy(data, inbuf) copy(inbuf, inbuf[n:]) inbuf = inbuf[:len(inbuf)-n] event.N = n event.Type = EventRaw return true } func extract_event(inbuf []byte, event *Event, allow_esc_wait bool) extract_event_res { if len(inbuf) == 0 { event.N = 0 return event_not_extracted } if inbuf[0] == '\033' { // possible escape sequence if n, ok := parse_escape_sequence(event, inbuf); n != 0 { event.N = n if ok { return event_extracted } else { return event_not_extracted } } // possible partially read escape sequence; trigger a wait if appropriate if enable_wait_for_escape_sequence() && allow_esc_wait { event.N = 0 return esc_wait } // it's not escape sequence, then it's Alt or Esc, check input_mode switch { case input_mode&InputEsc != 0: // if we're in escape mode, fill Esc event, pop buffer, return success event.Ch = 0 event.Key = KeyEsc event.Mod = 0 event.N = 1 return event_extracted case input_mode&InputAlt != 0: // if we're in alt mode, set Alt modifier to event and redo parsing event.Mod = ModAlt status := extract_event(inbuf[1:], event, false) if status == event_extracted { event.N++ } else { event.N = 0 } return status default: panic("unreachable") } } // if we're here, this is not an escape sequence and not an alt sequence // so, it's a FUNCTIONAL KEY or a UNICODE character // first of all check if it's a functional key if Key(inbuf[0]) <= KeySpace || Key(inbuf[0]) == KeyBackspace2 { // fill event, pop buffer, return success event.Ch = 0 event.Key = Key(inbuf[0]) event.N = 1 return event_extracted } // the only possible option is utf8 rune if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError { event.Ch = r event.Key = 0 event.N = n return event_extracted } return event_not_extracted }