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