| // +build !windows |
| |
| package termbox |
| |
| import "github.com/mattn/go-runewidth" |
| import "fmt" |
| import "os" |
| import "os/signal" |
| import "syscall" |
| import "runtime" |
| |
| // 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.Oflag &^= syscall_OPOST |
| 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} |
| ok := extract_event(data, &event) |
| if !ok { |
| 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 { |
| var event Event |
| |
| // try to extract event from input buffer, return on success |
| event.Type = EventKey |
| ok := extract_event(inbuf, &event) |
| if event.N != 0 { |
| copy(inbuf, inbuf[event.N:]) |
| inbuf = inbuf[:len(inbuf)-event.N] |
| } |
| if ok { |
| 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 |
| ok := extract_event(inbuf, &event) |
| if event.N != 0 { |
| copy(inbuf, inbuf[event.N:]) |
| inbuf = inbuf[:len(inbuf)-event.N] |
| } |
| if ok { |
| 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 |
| } |
| } |
| panic("unreachable") |
| } |
| |
| // 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() (int, 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 click 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&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: |
| // 0x00 - 0x07: the 8 colors as in OutputNormal |
| // 0x08 - 0x0f: Color* | AttrBold |
| // 0x10 - 0xe7: 216 different colors |
| // 0xe8 - 0xff: 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 dont need to provide an offset. |
| // |
| // 4. OutputGrayscale => [1..24] |
| // This mode supports the 4th range of the 256 mode only. |
| // But you dont need to provide an offset. |
| // |
| // In all modes, 0 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() |
| } |