// +build !windows package termbox import "github.com/mattn/go-runewidth" import "fmt" import "os" import "os/signal" import "syscall" import "runtime" import "time" // public API // Initializes termbox library. This function should be called before any other functions. // After successful initialization, the library must be finalized using 'Close' function. // // Example usage: // err := termbox.Init() // if err != nil { // panic(err) // } // defer termbox.Close() func Init() error { var err error out, err = os.OpenFile("/dev/tty", syscall.O_WRONLY, 0) if err != nil { return err } in, err = syscall.Open("/dev/tty", syscall.O_RDONLY, 0) if err != nil { return err } err = setup_term() if err != nil { return fmt.Errorf("termbox: error while reading terminfo data: %v", err) } signal.Notify(sigwinch, syscall.SIGWINCH) signal.Notify(sigio, syscall.SIGIO) _, err = fcntl(in, syscall.F_SETFL, syscall.O_ASYNC|syscall.O_NONBLOCK) if err != nil { return err } _, err = fcntl(in, syscall.F_SETOWN, syscall.Getpid()) if runtime.GOOS != "darwin" && err != nil { return err } err = tcgetattr(out.Fd(), &orig_tios) if err != nil { return err } tios := orig_tios tios.Iflag &^= syscall_IGNBRK | syscall_BRKINT | syscall_PARMRK | syscall_ISTRIP | syscall_INLCR | syscall_IGNCR | syscall_ICRNL | syscall_IXON tios.Lflag &^= syscall_ECHO | syscall_ECHONL | syscall_ICANON | syscall_ISIG | syscall_IEXTEN tios.Cflag &^= syscall_CSIZE | syscall_PARENB tios.Cflag |= syscall_CS8 tios.Cc[syscall_VMIN] = 1 tios.Cc[syscall_VTIME] = 0 err = tcsetattr(out.Fd(), &tios) if err != nil { return err } out.WriteString(funcs[t_enter_ca]) out.WriteString(funcs[t_enter_keypad]) out.WriteString(funcs[t_hide_cursor]) out.WriteString(funcs[t_clear_screen]) termw, termh = get_term_size(out.Fd()) back_buffer.init(termw, termh) front_buffer.init(termw, termh) back_buffer.clear() front_buffer.clear() go func() { buf := make([]byte, 128) for { select { case <-sigio: for { n, err := syscall.Read(in, buf) if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { break } select { case input_comm <- input_event{buf[:n], err}: ie := <-input_comm buf = ie.data[:128] case <-quit: return } } case <-quit: return } } }() IsInit = true return nil } // Interrupt an in-progress call to PollEvent by causing it to return // EventInterrupt. Note that this function will block until the PollEvent // function has successfully been interrupted. func Interrupt() { interrupt_comm <- struct{}{} } // Finalizes termbox library, should be called after successful initialization // when termbox's functionality isn't required anymore. func Close() { quit <- 1 out.WriteString(funcs[t_show_cursor]) out.WriteString(funcs[t_sgr0]) out.WriteString(funcs[t_clear_screen]) out.WriteString(funcs[t_exit_ca]) out.WriteString(funcs[t_exit_keypad]) out.WriteString(funcs[t_exit_mouse]) tcsetattr(out.Fd(), &orig_tios) out.Close() syscall.Close(in) // reset the state, so that on next Init() it will work again termw = 0 termh = 0 input_mode = InputEsc out = nil in = 0 lastfg = attr_invalid lastbg = attr_invalid lastx = coord_invalid lasty = coord_invalid cursor_x = cursor_hidden cursor_y = cursor_hidden foreground = ColorDefault background = ColorDefault IsInit = false } // Synchronizes the internal back buffer with the terminal. func Flush() error { // invalidate cursor position lastx = coord_invalid lasty = coord_invalid update_size_maybe() for y := 0; y < front_buffer.height; y++ { line_offset := y * front_buffer.width for x := 0; x < front_buffer.width; { cell_offset := line_offset + x back := &back_buffer.cells[cell_offset] front := &front_buffer.cells[cell_offset] if back.Ch < ' ' { back.Ch = ' ' } w := runewidth.RuneWidth(back.Ch) if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) { w = 1 } if *back == *front { x += w continue } *front = *back send_attr(back.Fg, back.Bg) if w == 2 && x == front_buffer.width-1 { // there's not enough space for 2-cells rune, // let's just put a space in there send_char(x, y, ' ') } else { send_char(x, y, back.Ch) if w == 2 { next := cell_offset + 1 front_buffer.cells[next] = Cell{ Ch: 0, Fg: back.Fg, Bg: back.Bg, } } } x += w } } if !is_cursor_hidden(cursor_x, cursor_y) { write_cursor(cursor_x, cursor_y) } return flush() } // Sets the position of the cursor. See also HideCursor(). func SetCursor(x, y int) { if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) { outbuf.WriteString(funcs[t_show_cursor]) } if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) { outbuf.WriteString(funcs[t_hide_cursor]) } cursor_x, cursor_y = x, y if !is_cursor_hidden(cursor_x, cursor_y) { write_cursor(cursor_x, cursor_y) } } // The shortcut for SetCursor(-1, -1). func HideCursor() { SetCursor(cursor_hidden, cursor_hidden) } // Changes cell's parameters in the internal back buffer at the specified // position. func SetCell(x, y int, ch rune, fg, bg Attribute) { if x < 0 || x >= back_buffer.width { return } if y < 0 || y >= back_buffer.height { return } back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg} } // Returns a slice into the termbox's back buffer. You can get its dimensions // using 'Size' function. The slice remains valid as long as no 'Clear' or // 'Flush' function calls were made after call to this function. func CellBuffer() []Cell { return back_buffer.cells } // After getting a raw event from PollRawEvent function call, you can parse it // again into an ordinary one using termbox logic. That is parse an event as // termbox would do it. Returned event in addition to usual Event struct fields // sets N field to the amount of bytes used within 'data' slice. If the length // of 'data' slice is zero or event cannot be parsed for some other reason, the // function will return a special event type: EventNone. // // IMPORTANT: EventNone may contain a non-zero N, which means you should skip // these bytes, because termbox cannot recognize them. // // NOTE: This API is experimental and may change in future. func ParseEvent(data []byte) Event { event := Event{Type: EventKey} status := extract_event(data, &event, false) if status != event_extracted { return Event{Type: EventNone, N: event.N} } return event } // Wait for an event and return it. This is a blocking function call. Instead // of EventKey and EventMouse it returns EventRaw events. Raw event is written // into `data` slice and Event's N field is set to the amount of bytes written. // The minimum required length of the 'data' slice is 1. This requirement may // vary on different platforms. // // NOTE: This API is experimental and may change in future. func PollRawEvent(data []byte) Event { if len(data) == 0 { panic("len(data) >= 1 is a requirement") } var event Event if extract_raw_event(data, &event) { return event } for { select { case ev := <-input_comm: if ev.err != nil { return Event{Type: EventError, Err: ev.err} } inbuf = append(inbuf, ev.data...) input_comm <- ev if extract_raw_event(data, &event) { return event } case <-interrupt_comm: event.Type = EventInterrupt return event case <-sigwinch: event.Type = EventResize event.Width, event.Height = get_term_size(out.Fd()) return event } } } // Wait for an event and return it. This is a blocking function call. func PollEvent() Event { // Constant governing macOS specific behavior. See https://github.com/nsf/termbox-go/issues/132 // This is an arbitrary delay which hopefully will be enough time for any lagging // partial escape sequences to come through. const esc_wait_delay = 50 * time.Millisecond var event Event var esc_wait_timer *time.Timer var esc_timeout <-chan time.Time // try to extract event from input buffer, return on success event.Type = EventKey status := extract_event(inbuf, &event, true) if event.N != 0 { copy(inbuf, inbuf[event.N:]) inbuf = inbuf[:len(inbuf)-event.N] } if status == event_extracted { return event } else if status == esc_wait { esc_wait_timer = time.NewTimer(esc_wait_delay) esc_timeout = esc_wait_timer.C } for { select { case ev := <-input_comm: if esc_wait_timer != nil { if !esc_wait_timer.Stop() { <-esc_wait_timer.C } esc_wait_timer = nil } if ev.err != nil { return Event{Type: EventError, Err: ev.err} } inbuf = append(inbuf, ev.data...) input_comm <- ev status := extract_event(inbuf, &event, true) if event.N != 0 { copy(inbuf, inbuf[event.N:]) inbuf = inbuf[:len(inbuf)-event.N] } if status == event_extracted { return event } else if status == esc_wait { esc_wait_timer = time.NewTimer(esc_wait_delay) esc_timeout = esc_wait_timer.C } case <-esc_timeout: esc_wait_timer = nil status := extract_event(inbuf, &event, false) if event.N != 0 { copy(inbuf, inbuf[event.N:]) inbuf = inbuf[:len(inbuf)-event.N] } if status == event_extracted { return event } case <-interrupt_comm: event.Type = EventInterrupt return event case <-sigwinch: event.Type = EventResize event.Width, event.Height = get_term_size(out.Fd()) return event } } } // Returns the size of the internal back buffer (which is mostly the same as // terminal's window size in characters). But it doesn't always match the size // of the terminal window, after the terminal size has changed, the internal // back buffer will get in sync only after Clear or Flush function calls. func Size() (width int, height int) { return termw, termh } // Clears the internal back buffer. func Clear(fg, bg Attribute) error { foreground, background = fg, bg err := update_size_maybe() back_buffer.clear() return err } // Sets termbox input mode. Termbox has two input modes: // // 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match // any known sequence. ESC means KeyEsc. This is the default input mode. // // 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match // any known sequence. ESC enables ModAlt modifier for the next keyboard event. // // Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will // enable mouse button press/release and drag events. // // If 'mode' is InputCurrent, returns the current input mode. See also Input* // constants. func SetInputMode(mode InputMode) InputMode { if mode == InputCurrent { return input_mode } if mode&(InputEsc|InputAlt) == 0 { mode |= InputEsc } if mode&(InputEsc|InputAlt) == InputEsc|InputAlt { mode &^= InputAlt } if mode&InputMouse != 0 { out.WriteString(funcs[t_enter_mouse]) } else { out.WriteString(funcs[t_exit_mouse]) } input_mode = mode return input_mode } // Sets the termbox output mode. Termbox has four output options: // // 1. OutputNormal => [1..8] // This mode provides 8 different colors: // black, red, green, yellow, blue, magenta, cyan, white // Shortcut: ColorBlack, ColorRed, ... // Attributes: AttrBold, AttrUnderline, AttrReverse // // Example usage: // SetCell(x, y, '@', ColorBlack | AttrBold, ColorRed); // // 2. Output256 => [1..256] // In this mode you can leverage the 256 terminal mode: // 0x01 - 0x08: the 8 colors as in OutputNormal // 0x09 - 0x10: Color* | AttrBold // 0x11 - 0xe8: 216 different colors // 0xe9 - 0x1ff: 24 different shades of grey // // Example usage: // SetCell(x, y, '@', 184, 240); // SetCell(x, y, '@', 0xb8, 0xf0); // // 3. Output216 => [1..216] // This mode supports the 3rd range of the 256 mode only. // But you don't need to provide an offset. // // 4. OutputGrayscale => [1..26] // This mode supports the 4th range of the 256 mode // and black and white colors from 3th range of the 256 mode // But you don't need to provide an offset. // // In all modes, 0x00 represents the default color. // // `go run _demos/output.go` to see its impact on your terminal. // // If 'mode' is OutputCurrent, it returns the current output mode. // // Note that this may return a different OutputMode than the one requested, // as the requested mode may not be available on the target platform. func SetOutputMode(mode OutputMode) OutputMode { if mode == OutputCurrent { return output_mode } output_mode = mode return output_mode } // Sync comes handy when something causes desync between termbox's understanding // of a terminal buffer and the reality. Such as a third party process. Sync // forces a complete resync between the termbox and a terminal, it may not be // visually pretty though. func Sync() error { front_buffer.clear() err := send_clear() if err != nil { return err } return Flush() }