termbox.go

  1// +build !windows
  2
  3package termbox
  4
  5import "unicode/utf8"
  6import "bytes"
  7import "syscall"
  8import "unsafe"
  9import "strings"
 10import "strconv"
 11import "os"
 12import "io"
 13
 14// private API
 15
 16const (
 17	t_enter_ca = iota
 18	t_exit_ca
 19	t_show_cursor
 20	t_hide_cursor
 21	t_clear_screen
 22	t_sgr0
 23	t_underline
 24	t_bold
 25	t_blink
 26	t_reverse
 27	t_enter_keypad
 28	t_exit_keypad
 29	t_enter_mouse
 30	t_exit_mouse
 31	t_max_funcs
 32)
 33
 34const (
 35	coord_invalid = -2
 36	attr_invalid  = Attribute(0xFFFF)
 37)
 38
 39type input_event struct {
 40	data []byte
 41	err  error
 42}
 43
 44type extract_event_res int
 45
 46const (
 47	event_not_extracted extract_event_res = iota
 48	event_extracted
 49	esc_wait
 50)
 51
 52var (
 53	// term specific sequences
 54	keys  []string
 55	funcs []string
 56
 57	// termbox inner state
 58	orig_tios      syscall_Termios
 59	back_buffer    cellbuf
 60	front_buffer   cellbuf
 61	termw          int
 62	termh          int
 63	input_mode     = InputEsc
 64	output_mode    = OutputNormal
 65	out            *os.File
 66	in             int
 67	lastfg         = attr_invalid
 68	lastbg         = attr_invalid
 69	lastx          = coord_invalid
 70	lasty          = coord_invalid
 71	cursor_x       = cursor_hidden
 72	cursor_y       = cursor_hidden
 73	foreground     = ColorDefault
 74	background     = ColorDefault
 75	inbuf          = make([]byte, 0, 64)
 76	outbuf         bytes.Buffer
 77	sigwinch       = make(chan os.Signal, 1)
 78	sigio          = make(chan os.Signal, 1)
 79	quit           = make(chan int)
 80	input_comm     = make(chan input_event)
 81	interrupt_comm = make(chan struct{})
 82	intbuf         = make([]byte, 0, 16)
 83
 84	// grayscale indexes
 85	grayscale = []Attribute{
 86		0, 17, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244,
 87		245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 232,
 88	}
 89)
 90
 91func write_cursor(x, y int) {
 92	outbuf.WriteString("\033[")
 93	outbuf.Write(strconv.AppendUint(intbuf, uint64(y+1), 10))
 94	outbuf.WriteString(";")
 95	outbuf.Write(strconv.AppendUint(intbuf, uint64(x+1), 10))
 96	outbuf.WriteString("H")
 97}
 98
 99func write_sgr_fg(a Attribute) {
100	switch output_mode {
101	case Output256, Output216, OutputGrayscale:
102		outbuf.WriteString("\033[38;5;")
103		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
104		outbuf.WriteString("m")
105	default:
106		outbuf.WriteString("\033[3")
107		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
108		outbuf.WriteString("m")
109	}
110}
111
112func write_sgr_bg(a Attribute) {
113	switch output_mode {
114	case Output256, Output216, OutputGrayscale:
115		outbuf.WriteString("\033[48;5;")
116		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
117		outbuf.WriteString("m")
118	default:
119		outbuf.WriteString("\033[4")
120		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
121		outbuf.WriteString("m")
122	}
123}
124
125func write_sgr(fg, bg Attribute) {
126	switch output_mode {
127	case Output256, Output216, OutputGrayscale:
128		outbuf.WriteString("\033[38;5;")
129		outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10))
130		outbuf.WriteString("m")
131		outbuf.WriteString("\033[48;5;")
132		outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10))
133		outbuf.WriteString("m")
134	default:
135		outbuf.WriteString("\033[3")
136		outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10))
137		outbuf.WriteString(";4")
138		outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10))
139		outbuf.WriteString("m")
140	}
141}
142
143type winsize struct {
144	rows    uint16
145	cols    uint16
146	xpixels uint16
147	ypixels uint16
148}
149
150func get_term_size(fd uintptr) (int, int) {
151	var sz winsize
152	_, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
153		fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
154	return int(sz.cols), int(sz.rows)
155}
156
157func send_attr(fg, bg Attribute) {
158	if fg == lastfg && bg == lastbg {
159		return
160	}
161
162	outbuf.WriteString(funcs[t_sgr0])
163
164	var fgcol, bgcol Attribute
165
166	switch output_mode {
167	case Output256:
168		fgcol = fg & 0x1FF
169		bgcol = bg & 0x1FF
170	case Output216:
171		fgcol = fg & 0xFF
172		bgcol = bg & 0xFF
173		if fgcol > 216 {
174			fgcol = ColorDefault
175		}
176		if bgcol > 216 {
177			bgcol = ColorDefault
178		}
179		if fgcol != ColorDefault {
180			fgcol += 0x10
181		}
182		if bgcol != ColorDefault {
183			bgcol += 0x10
184		}
185	case OutputGrayscale:
186		fgcol = fg & 0x1F
187		bgcol = bg & 0x1F
188		if fgcol > 26 {
189			fgcol = ColorDefault
190		}
191		if bgcol > 26 {
192			bgcol = ColorDefault
193		}
194		if fgcol != ColorDefault {
195			fgcol = grayscale[fgcol]
196		}
197		if bgcol != ColorDefault {
198			bgcol = grayscale[bgcol]
199		}
200	default:
201		fgcol = fg & 0x0F
202		bgcol = bg & 0x0F
203	}
204
205	if fgcol != ColorDefault {
206		if bgcol != ColorDefault {
207			write_sgr(fgcol, bgcol)
208		} else {
209			write_sgr_fg(fgcol)
210		}
211	} else if bgcol != ColorDefault {
212		write_sgr_bg(bgcol)
213	}
214
215	if fg&AttrBold != 0 {
216		outbuf.WriteString(funcs[t_bold])
217	}
218	if bg&AttrBold != 0 {
219		outbuf.WriteString(funcs[t_blink])
220	}
221	if fg&AttrUnderline != 0 {
222		outbuf.WriteString(funcs[t_underline])
223	}
224	if fg&AttrReverse|bg&AttrReverse != 0 {
225		outbuf.WriteString(funcs[t_reverse])
226	}
227
228	lastfg, lastbg = fg, bg
229}
230
231func send_char(x, y int, ch rune) {
232	var buf [8]byte
233	n := utf8.EncodeRune(buf[:], ch)
234	if x-1 != lastx || y != lasty {
235		write_cursor(x, y)
236	}
237	lastx, lasty = x, y
238	outbuf.Write(buf[:n])
239}
240
241func flush() error {
242	_, err := io.Copy(out, &outbuf)
243	outbuf.Reset()
244	return err
245}
246
247func send_clear() error {
248	send_attr(foreground, background)
249	outbuf.WriteString(funcs[t_clear_screen])
250	if !is_cursor_hidden(cursor_x, cursor_y) {
251		write_cursor(cursor_x, cursor_y)
252	}
253
254	// we need to invalidate cursor position too and these two vars are
255	// used only for simple cursor positioning optimization, cursor
256	// actually may be in the correct place, but we simply discard
257	// optimization once and it gives us simple solution for the case when
258	// cursor moved
259	lastx = coord_invalid
260	lasty = coord_invalid
261
262	return flush()
263}
264
265func update_size_maybe() error {
266	w, h := get_term_size(out.Fd())
267	if w != termw || h != termh {
268		termw, termh = w, h
269		back_buffer.resize(termw, termh)
270		front_buffer.resize(termw, termh)
271		front_buffer.clear()
272		return send_clear()
273	}
274	return nil
275}
276
277func tcsetattr(fd uintptr, termios *syscall_Termios) error {
278	r, _, e := syscall.Syscall(syscall.SYS_IOCTL,
279		fd, uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(termios)))
280	if r != 0 {
281		return os.NewSyscallError("SYS_IOCTL", e)
282	}
283	return nil
284}
285
286func tcgetattr(fd uintptr, termios *syscall_Termios) error {
287	r, _, e := syscall.Syscall(syscall.SYS_IOCTL,
288		fd, uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(termios)))
289	if r != 0 {
290		return os.NewSyscallError("SYS_IOCTL", e)
291	}
292	return nil
293}
294
295func parse_mouse_event(event *Event, buf string) (int, bool) {
296	if strings.HasPrefix(buf, "\033[M") && len(buf) >= 6 {
297		// X10 mouse encoding, the simplest one
298		// \033 [ M Cb Cx Cy
299		b := buf[3] - 32
300		switch b & 3 {
301		case 0:
302			if b&64 != 0 {
303				event.Key = MouseWheelUp
304			} else {
305				event.Key = MouseLeft
306			}
307		case 1:
308			if b&64 != 0 {
309				event.Key = MouseWheelDown
310			} else {
311				event.Key = MouseMiddle
312			}
313		case 2:
314			event.Key = MouseRight
315		case 3:
316			event.Key = MouseRelease
317		default:
318			return 6, false
319		}
320		event.Type = EventMouse // KeyEvent by default
321		if b&32 != 0 {
322			event.Mod |= ModMotion
323		}
324
325		// the coord is 1,1 for upper left
326		event.MouseX = int(buf[4]) - 1 - 32
327		event.MouseY = int(buf[5]) - 1 - 32
328		return 6, true
329	} else if strings.HasPrefix(buf, "\033[<") || strings.HasPrefix(buf, "\033[") {
330		// xterm 1006 extended mode or urxvt 1015 extended mode
331		// xterm: \033 [ < Cb ; Cx ; Cy (M or m)
332		// urxvt: \033 [ Cb ; Cx ; Cy M
333
334		// find the first M or m, that's where we stop
335		mi := strings.IndexAny(buf, "Mm")
336		if mi == -1 {
337			return 0, false
338		}
339
340		// whether it's a capital M or not
341		isM := buf[mi] == 'M'
342
343		// whether it's urxvt or not
344		isU := false
345
346		// buf[2] is safe here, because having M or m found means we have at
347		// least 3 bytes in a string
348		if buf[2] == '<' {
349			buf = buf[3:mi]
350		} else {
351			isU = true
352			buf = buf[2:mi]
353		}
354
355		s1 := strings.Index(buf, ";")
356		s2 := strings.LastIndex(buf, ";")
357		// not found or only one ';'
358		if s1 == -1 || s2 == -1 || s1 == s2 {
359			return 0, false
360		}
361
362		n1, err := strconv.ParseInt(buf[0:s1], 10, 64)
363		if err != nil {
364			return 0, false
365		}
366		n2, err := strconv.ParseInt(buf[s1+1:s2], 10, 64)
367		if err != nil {
368			return 0, false
369		}
370		n3, err := strconv.ParseInt(buf[s2+1:], 10, 64)
371		if err != nil {
372			return 0, false
373		}
374
375		// on urxvt, first number is encoded exactly as in X10, but we need to
376		// make it zero-based, on xterm it is zero-based already
377		if isU {
378			n1 -= 32
379		}
380		switch n1 & 3 {
381		case 0:
382			if n1&64 != 0 {
383				event.Key = MouseWheelUp
384			} else {
385				event.Key = MouseLeft
386			}
387		case 1:
388			if n1&64 != 0 {
389				event.Key = MouseWheelDown
390			} else {
391				event.Key = MouseMiddle
392			}
393		case 2:
394			event.Key = MouseRight
395		case 3:
396			event.Key = MouseRelease
397		default:
398			return mi + 1, false
399		}
400		if !isM {
401			// on xterm mouse release is signaled by lowercase m
402			event.Key = MouseRelease
403		}
404
405		event.Type = EventMouse // KeyEvent by default
406		if n1&32 != 0 {
407			event.Mod |= ModMotion
408		}
409
410		event.MouseX = int(n2) - 1
411		event.MouseY = int(n3) - 1
412		return mi + 1, true
413	}
414
415	return 0, false
416}
417
418func parse_escape_sequence(event *Event, buf []byte) (int, bool) {
419	bufstr := string(buf)
420	for i, key := range keys {
421		if strings.HasPrefix(bufstr, key) {
422			event.Ch = 0
423			event.Key = Key(0xFFFF - i)
424			return len(key), true
425		}
426	}
427
428	// if none of the keys match, let's try mouse sequences
429	return parse_mouse_event(event, bufstr)
430}
431
432func extract_raw_event(data []byte, event *Event) bool {
433	if len(inbuf) == 0 {
434		return false
435	}
436
437	n := len(data)
438	if n == 0 {
439		return false
440	}
441
442	n = copy(data, inbuf)
443	copy(inbuf, inbuf[n:])
444	inbuf = inbuf[:len(inbuf)-n]
445
446	event.N = n
447	event.Type = EventRaw
448	return true
449}
450
451func extract_event(inbuf []byte, event *Event, allow_esc_wait bool) extract_event_res {
452	if len(inbuf) == 0 {
453		event.N = 0
454		return event_not_extracted
455	}
456
457	if inbuf[0] == '\033' {
458		// possible escape sequence
459		if n, ok := parse_escape_sequence(event, inbuf); n != 0 {
460			event.N = n
461			if ok {
462				return event_extracted
463			} else {
464				return event_not_extracted
465			}
466		}
467
468		// possible partially read escape sequence; trigger a wait if appropriate
469		if enable_wait_for_escape_sequence() && allow_esc_wait {
470			event.N = 0
471			return esc_wait
472		}
473
474		// it's not escape sequence, then it's Alt or Esc, check input_mode
475		switch {
476		case input_mode&InputEsc != 0:
477			// if we're in escape mode, fill Esc event, pop buffer, return success
478			event.Ch = 0
479			event.Key = KeyEsc
480			event.Mod = 0
481			event.N = 1
482			return event_extracted
483		case input_mode&InputAlt != 0:
484			// if we're in alt mode, set Alt modifier to event and redo parsing
485			event.Mod = ModAlt
486			status := extract_event(inbuf[1:], event, false)
487			if status == event_extracted {
488				event.N++
489			} else {
490				event.N = 0
491			}
492			return status
493		default:
494			panic("unreachable")
495		}
496	}
497
498	// if we're here, this is not an escape sequence and not an alt sequence
499	// so, it's a FUNCTIONAL KEY or a UNICODE character
500
501	// first of all check if it's a functional key
502	if Key(inbuf[0]) <= KeySpace || Key(inbuf[0]) == KeyBackspace2 {
503		// fill event, pop buffer, return success
504		event.Ch = 0
505		event.Key = Key(inbuf[0])
506		event.N = 1
507		return event_extracted
508	}
509
510	// the only possible option is utf8 rune
511	if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError {
512		event.Ch = r
513		event.Key = 0
514		event.N = n
515		return event_extracted
516	}
517
518	return event_not_extracted
519}
520
521func fcntl(fd int, cmd int, arg int) (val int, err error) {
522	r, _, e := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), uintptr(cmd),
523		uintptr(arg))
524	val = int(r)
525	if e != 0 {
526		err = e
527	}
528	return
529}