490 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			490 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // +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()
 | |
| }
 |