1// +build !windows 2 3package termbox 4 5import "github.com/mattn/go-runewidth" 6import "fmt" 7import "os" 8import "os/signal" 9import "syscall" 10import "runtime" 11import "time" 12 13// public API 14 15// Initializes termbox library. This function should be called before any other functions. 16// After successful initialization, the library must be finalized using 'Close' function. 17// 18// Example usage: 19// err := termbox.Init() 20// if err != nil { 21// panic(err) 22// } 23// defer termbox.Close() 24func Init() error { 25 var err error 26 27 if runtime.GOOS == "openbsd" || runtime.GOOS == "freebsd" { 28 out, err = os.OpenFile("/dev/tty", os.O_RDWR, 0) 29 if err != nil { 30 return err 31 } 32 in = int(out.Fd()) 33 } else { 34 out, err = os.OpenFile("/dev/tty", os.O_WRONLY, 0) 35 if err != nil { 36 return err 37 } 38 in, err = syscall.Open("/dev/tty", syscall.O_RDONLY, 0) 39 if err != nil { 40 return err 41 } 42 } 43 44 err = setup_term() 45 if err != nil { 46 return fmt.Errorf("termbox: error while reading terminfo data: %v", err) 47 } 48 49 signal.Notify(sigwinch, syscall.SIGWINCH) 50 signal.Notify(sigio, syscall.SIGIO) 51 52 _, err = fcntl(in, syscall.F_SETFL, syscall.O_ASYNC|syscall.O_NONBLOCK) 53 if err != nil { 54 return err 55 } 56 _, err = fcntl(in, syscall.F_SETOWN, syscall.Getpid()) 57 if runtime.GOOS != "darwin" && err != nil { 58 return err 59 } 60 err = tcgetattr(out.Fd(), &orig_tios) 61 if err != nil { 62 return err 63 } 64 65 tios := orig_tios 66 tios.Iflag &^= syscall_IGNBRK | syscall_BRKINT | syscall_PARMRK | 67 syscall_ISTRIP | syscall_INLCR | syscall_IGNCR | 68 syscall_ICRNL | syscall_IXON 69 tios.Lflag &^= syscall_ECHO | syscall_ECHONL | syscall_ICANON | 70 syscall_ISIG | syscall_IEXTEN 71 tios.Cflag &^= syscall_CSIZE | syscall_PARENB 72 tios.Cflag |= syscall_CS8 73 tios.Cc[syscall_VMIN] = 1 74 tios.Cc[syscall_VTIME] = 0 75 76 err = tcsetattr(out.Fd(), &tios) 77 if err != nil { 78 return err 79 } 80 81 out.WriteString(funcs[t_enter_ca]) 82 out.WriteString(funcs[t_enter_keypad]) 83 out.WriteString(funcs[t_hide_cursor]) 84 out.WriteString(funcs[t_clear_screen]) 85 86 termw, termh = get_term_size(out.Fd()) 87 back_buffer.init(termw, termh) 88 front_buffer.init(termw, termh) 89 back_buffer.clear() 90 front_buffer.clear() 91 92 go func() { 93 buf := make([]byte, 128) 94 for { 95 select { 96 case <-sigio: 97 for { 98 n, err := syscall.Read(in, buf) 99 if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { 100 break 101 } 102 select { 103 case input_comm <- input_event{buf[:n], err}: 104 ie := <-input_comm 105 buf = ie.data[:128] 106 case <-quit: 107 return 108 } 109 } 110 case <-quit: 111 return 112 } 113 } 114 }() 115 116 IsInit = true 117 return nil 118} 119 120// Interrupt an in-progress call to PollEvent by causing it to return 121// EventInterrupt. Note that this function will block until the PollEvent 122// function has successfully been interrupted. 123func Interrupt() { 124 interrupt_comm <- struct{}{} 125} 126 127// Finalizes termbox library, should be called after successful initialization 128// when termbox's functionality isn't required anymore. 129func Close() { 130 quit <- 1 131 out.WriteString(funcs[t_show_cursor]) 132 out.WriteString(funcs[t_sgr0]) 133 out.WriteString(funcs[t_clear_screen]) 134 out.WriteString(funcs[t_exit_ca]) 135 out.WriteString(funcs[t_exit_keypad]) 136 out.WriteString(funcs[t_exit_mouse]) 137 tcsetattr(out.Fd(), &orig_tios) 138 139 out.Close() 140 syscall.Close(in) 141 142 // reset the state, so that on next Init() it will work again 143 termw = 0 144 termh = 0 145 input_mode = InputEsc 146 out = nil 147 in = 0 148 lastfg = attr_invalid 149 lastbg = attr_invalid 150 lastx = coord_invalid 151 lasty = coord_invalid 152 cursor_x = cursor_hidden 153 cursor_y = cursor_hidden 154 foreground = ColorDefault 155 background = ColorDefault 156 IsInit = false 157} 158 159// Synchronizes the internal back buffer with the terminal. 160func Flush() error { 161 // invalidate cursor position 162 lastx = coord_invalid 163 lasty = coord_invalid 164 165 update_size_maybe() 166 167 for y := 0; y < front_buffer.height; y++ { 168 line_offset := y * front_buffer.width 169 for x := 0; x < front_buffer.width; { 170 cell_offset := line_offset + x 171 back := &back_buffer.cells[cell_offset] 172 front := &front_buffer.cells[cell_offset] 173 if back.Ch < ' ' { 174 back.Ch = ' ' 175 } 176 w := runewidth.RuneWidth(back.Ch) 177 if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) { 178 w = 1 179 } 180 if *back == *front { 181 x += w 182 continue 183 } 184 *front = *back 185 send_attr(back.Fg, back.Bg) 186 187 if w == 2 && x == front_buffer.width-1 { 188 // there's not enough space for 2-cells rune, 189 // let's just put a space in there 190 send_char(x, y, ' ') 191 } else { 192 send_char(x, y, back.Ch) 193 if w == 2 { 194 next := cell_offset + 1 195 front_buffer.cells[next] = Cell{ 196 Ch: 0, 197 Fg: back.Fg, 198 Bg: back.Bg, 199 } 200 } 201 } 202 x += w 203 } 204 } 205 if !is_cursor_hidden(cursor_x, cursor_y) { 206 write_cursor(cursor_x, cursor_y) 207 } 208 return flush() 209} 210 211// Sets the position of the cursor. See also HideCursor(). 212func SetCursor(x, y int) { 213 if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) { 214 outbuf.WriteString(funcs[t_show_cursor]) 215 } 216 217 if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) { 218 outbuf.WriteString(funcs[t_hide_cursor]) 219 } 220 221 cursor_x, cursor_y = x, y 222 if !is_cursor_hidden(cursor_x, cursor_y) { 223 write_cursor(cursor_x, cursor_y) 224 } 225} 226 227// The shortcut for SetCursor(-1, -1). 228func HideCursor() { 229 SetCursor(cursor_hidden, cursor_hidden) 230} 231 232// Changes cell's parameters in the internal back buffer at the specified 233// position. 234func SetCell(x, y int, ch rune, fg, bg Attribute) { 235 if x < 0 || x >= back_buffer.width { 236 return 237 } 238 if y < 0 || y >= back_buffer.height { 239 return 240 } 241 242 back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg} 243} 244 245// Returns a slice into the termbox's back buffer. You can get its dimensions 246// using 'Size' function. The slice remains valid as long as no 'Clear' or 247// 'Flush' function calls were made after call to this function. 248func CellBuffer() []Cell { 249 return back_buffer.cells 250} 251 252// After getting a raw event from PollRawEvent function call, you can parse it 253// again into an ordinary one using termbox logic. That is parse an event as 254// termbox would do it. Returned event in addition to usual Event struct fields 255// sets N field to the amount of bytes used within 'data' slice. If the length 256// of 'data' slice is zero or event cannot be parsed for some other reason, the 257// function will return a special event type: EventNone. 258// 259// IMPORTANT: EventNone may contain a non-zero N, which means you should skip 260// these bytes, because termbox cannot recognize them. 261// 262// NOTE: This API is experimental and may change in future. 263func ParseEvent(data []byte) Event { 264 event := Event{Type: EventKey} 265 status := extract_event(data, &event, false) 266 if status != event_extracted { 267 return Event{Type: EventNone, N: event.N} 268 } 269 return event 270} 271 272// Wait for an event and return it. This is a blocking function call. Instead 273// of EventKey and EventMouse it returns EventRaw events. Raw event is written 274// into `data` slice and Event's N field is set to the amount of bytes written. 275// The minimum required length of the 'data' slice is 1. This requirement may 276// vary on different platforms. 277// 278// NOTE: This API is experimental and may change in future. 279func PollRawEvent(data []byte) Event { 280 if len(data) == 0 { 281 panic("len(data) >= 1 is a requirement") 282 } 283 284 var event Event 285 if extract_raw_event(data, &event) { 286 return event 287 } 288 289 for { 290 select { 291 case ev := <-input_comm: 292 if ev.err != nil { 293 return Event{Type: EventError, Err: ev.err} 294 } 295 296 inbuf = append(inbuf, ev.data...) 297 input_comm <- ev 298 if extract_raw_event(data, &event) { 299 return event 300 } 301 case <-interrupt_comm: 302 event.Type = EventInterrupt 303 return event 304 305 case <-sigwinch: 306 event.Type = EventResize 307 event.Width, event.Height = get_term_size(out.Fd()) 308 return event 309 } 310 } 311} 312 313// Wait for an event and return it. This is a blocking function call. 314func PollEvent() Event { 315 // Constant governing macOS specific behavior. See https://github.com/nsf/termbox-go/issues/132 316 // This is an arbitrary delay which hopefully will be enough time for any lagging 317 // partial escape sequences to come through. 318 const esc_wait_delay = 100 * time.Millisecond 319 320 var event Event 321 var esc_wait_timer *time.Timer 322 var esc_timeout <-chan time.Time 323 324 // try to extract event from input buffer, return on success 325 event.Type = EventKey 326 status := extract_event(inbuf, &event, true) 327 if event.N != 0 { 328 copy(inbuf, inbuf[event.N:]) 329 inbuf = inbuf[:len(inbuf)-event.N] 330 } 331 if status == event_extracted { 332 return event 333 } else if status == esc_wait { 334 esc_wait_timer = time.NewTimer(esc_wait_delay) 335 esc_timeout = esc_wait_timer.C 336 } 337 338 for { 339 select { 340 case ev := <-input_comm: 341 if esc_wait_timer != nil { 342 if !esc_wait_timer.Stop() { 343 <-esc_wait_timer.C 344 } 345 esc_wait_timer = nil 346 } 347 348 if ev.err != nil { 349 return Event{Type: EventError, Err: ev.err} 350 } 351 352 inbuf = append(inbuf, ev.data...) 353 input_comm <- ev 354 status := extract_event(inbuf, &event, true) 355 if event.N != 0 { 356 copy(inbuf, inbuf[event.N:]) 357 inbuf = inbuf[:len(inbuf)-event.N] 358 } 359 if status == event_extracted { 360 return event 361 } else if status == esc_wait { 362 esc_wait_timer = time.NewTimer(esc_wait_delay) 363 esc_timeout = esc_wait_timer.C 364 } 365 case <-esc_timeout: 366 esc_wait_timer = nil 367 368 status := extract_event(inbuf, &event, false) 369 if event.N != 0 { 370 copy(inbuf, inbuf[event.N:]) 371 inbuf = inbuf[:len(inbuf)-event.N] 372 } 373 if status == event_extracted { 374 return event 375 } 376 case <-interrupt_comm: 377 event.Type = EventInterrupt 378 return event 379 380 case <-sigwinch: 381 event.Type = EventResize 382 event.Width, event.Height = get_term_size(out.Fd()) 383 return event 384 } 385 } 386} 387 388// Returns the size of the internal back buffer (which is mostly the same as 389// terminal's window size in characters). But it doesn't always match the size 390// of the terminal window, after the terminal size has changed, the internal 391// back buffer will get in sync only after Clear or Flush function calls. 392func Size() (width int, height int) { 393 return termw, termh 394} 395 396// Clears the internal back buffer. 397func Clear(fg, bg Attribute) error { 398 foreground, background = fg, bg 399 err := update_size_maybe() 400 back_buffer.clear() 401 return err 402} 403 404// Sets termbox input mode. Termbox has two input modes: 405// 406// 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match 407// any known sequence. ESC means KeyEsc. This is the default input mode. 408// 409// 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match 410// any known sequence. ESC enables ModAlt modifier for the next keyboard event. 411// 412// Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will 413// enable mouse button press/release and drag events. 414// 415// If 'mode' is InputCurrent, returns the current input mode. See also Input* 416// constants. 417func SetInputMode(mode InputMode) InputMode { 418 if mode == InputCurrent { 419 return input_mode 420 } 421 if mode&(InputEsc|InputAlt) == 0 { 422 mode |= InputEsc 423 } 424 if mode&(InputEsc|InputAlt) == InputEsc|InputAlt { 425 mode &^= InputAlt 426 } 427 if mode&InputMouse != 0 { 428 out.WriteString(funcs[t_enter_mouse]) 429 } else { 430 out.WriteString(funcs[t_exit_mouse]) 431 } 432 433 input_mode = mode 434 return input_mode 435} 436 437// Sets the termbox output mode. Termbox has four output options: 438// 439// 1. OutputNormal => [1..8] 440// This mode provides 8 different colors: 441// black, red, green, yellow, blue, magenta, cyan, white 442// Shortcut: ColorBlack, ColorRed, ... 443// Attributes: AttrBold, AttrUnderline, AttrReverse 444// 445// Example usage: 446// SetCell(x, y, '@', ColorBlack | AttrBold, ColorRed); 447// 448// 2. Output256 => [1..256] 449// In this mode you can leverage the 256 terminal mode: 450// 0x01 - 0x08: the 8 colors as in OutputNormal 451// 0x09 - 0x10: Color* | AttrBold 452// 0x11 - 0xe8: 216 different colors 453// 0xe9 - 0x1ff: 24 different shades of grey 454// 455// Example usage: 456// SetCell(x, y, '@', 184, 240); 457// SetCell(x, y, '@', 0xb8, 0xf0); 458// 459// 3. Output216 => [1..216] 460// This mode supports the 3rd range of the 256 mode only. 461// But you don't need to provide an offset. 462// 463// 4. OutputGrayscale => [1..26] 464// This mode supports the 4th range of the 256 mode 465// and black and white colors from 3th range of the 256 mode 466// But you don't need to provide an offset. 467// 468// In all modes, 0x00 represents the default color. 469// 470// `go run _demos/output.go` to see its impact on your terminal. 471// 472// If 'mode' is OutputCurrent, it returns the current output mode. 473// 474// Note that this may return a different OutputMode than the one requested, 475// as the requested mode may not be available on the target platform. 476func SetOutputMode(mode OutputMode) OutputMode { 477 if mode == OutputCurrent { 478 return output_mode 479 } 480 481 output_mode = mode 482 return output_mode 483} 484 485// Sync comes handy when something causes desync between termbox's understanding 486// of a terminal buffer and the reality. Such as a third party process. Sync 487// forces a complete resync between the termbox and a terminal, it may not be 488// visually pretty though. 489func Sync() error { 490 front_buffer.clear() 491 err := send_clear() 492 if err != nil { 493 return err 494 } 495 496 return Flush() 497} 498