Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up| // +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 = 100 * 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() | |
| } |