1 /***************************************************************************
2  *   Copyright (C) 2008-2021 by Andrzej Rybczak                            *
3  *   andrzej@rybczak.net                                                   *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.              *
19  ***************************************************************************/
20 
21 #include <algorithm>
22 #include <cstring>
23 #include <cstdio>
24 #include <cstdlib>
25 #include <iostream>
26 #include <sys/select.h>
27 #include <unistd.h>
28 
29 #include "utility/readline.h"
30 #include "utility/string.h"
31 #include "utility/wide_string.h"
32 #include "window.h"
33 
34 namespace {
35 
36 // In a DirectColor setup, COLORS as returned by ncurses (via terminfo) can
37 // run as high as 2^24. We only work with up to 256.
38 int maxColor;
39 
40 namespace rl {
41 
42 bool aborted;
43 
44 NC::Window *w;
45 size_t start_x;
46 size_t start_y;
47 size_t width;
48 bool encrypted;
49 const char *base;
50 
read_key(FILE *)51 int read_key(FILE *)
52 {
53 	size_t x;
54 	bool done;
55 	int result;
56 	do
57 	{
58 		x = w->getX();
59 		if (w->runPromptHook(rl_line_buffer, &done))
60 		{
61 			// Do not end if readline is in one of its commands, e.g. searching
62 			// through history, as it doesn't actually make readline exit and it
63 			// becomes stuck in a loop.
64 			if (!RL_ISSTATE(RL_STATE_DISPATCHING) && done)
65 			{
66 				rl_done = 1;
67 				return EOF;
68 			}
69 			w->goToXY(x, start_y);
70 		}
71 		w->refresh();
72 		result = w->readKey();
73 		if (!w->FDCallbacksListEmpty())
74 		{
75 			w->goToXY(x, start_y);
76 			w->refresh();
77 		}
78 	}
79 	while (result == ERR);
80 	return result;
81 }
82 
display_string()83 void display_string()
84 {
85 	auto print_char = [](wchar_t wc) {
86 		if (encrypted)
87 			*w << '*';
88 		else
89 			*w << wc;
90 	};
91 	auto print_string = [](wchar_t *ws, size_t len) {
92 		if (encrypted)
93 			for (size_t i = 0; i < len; ++i)
94 				*w << '*';
95 		else
96 			*w << ws;
97 	};
98 	auto narrow_to_wide = [](wchar_t *dest, const char *src, size_t n) {
99 		size_t result = 0;
100 		// convert the string and substitute invalid multibyte chars with dots.
101 		for (size_t i = 0; i < n;)
102 		{
103 			int ret = mbtowc(&dest[result], &src[i], n-i);
104 			if (ret > 0)
105 			{
106 				i += ret;
107 				++result;
108 			}
109 			else if (ret == -1)
110 			{
111 				dest[result] = L'.';
112 				++i;
113 				++result;
114 			}
115 			else
116 				throw std::runtime_error("mbtowc: unexpected return value");
117 		}
118 		return result;
119 	};
120 
121 	// copy the part of the string that is before the cursor to pre_pos
122 	char pt = rl_line_buffer[rl_point];
123 	rl_line_buffer[rl_point] = 0;
124 	wchar_t pre_pos[rl_point+1];
125 	pre_pos[narrow_to_wide(pre_pos, rl_line_buffer, rl_point)] = 0;
126 	rl_line_buffer[rl_point] = pt;
127 
128 	int pos = wcswidth(pre_pos, rl_point);
129 	if (pos < 0)
130 		pos = rl_point;
131 
132 	// clear the area for the string
133 	mvwhline(w->raw(), start_y, start_x, ' ', width+1);
134 
135 	w->goToXY(start_x, start_y);
136 	if (size_t(pos) <= width)
137 	{
138 		// if the current position in the string is not bigger than allowed
139 		// width, print the part of the string before cursor position...
140 
141 		print_string(pre_pos, pos);
142 
143 		// ...and then print the rest char-by-char until there is no more area
144 		wchar_t post_pos[rl_end-rl_point+1];
145 		post_pos[narrow_to_wide(post_pos, rl_line_buffer+rl_point, rl_end-rl_point)] = 0;
146 
147 		size_t cpos = pos;
148 		for (wchar_t *c = post_pos; *c != 0; ++c)
149 		{
150 			int n = wcwidth(*c);
151 			if (n < 0)
152 			{
153 				print_char(L'.');
154 				++cpos;
155 			}
156 			else
157 			{
158 				if (cpos+n > width)
159 					break;
160 				cpos += n;
161 				print_char(*c);
162 			}
163 		}
164 	}
165 	else
166 	{
167 		// if the current position in the string is bigger than allowed
168 		// width, we always keep the cursor at the end of the line (it
169 		// would be nice to have more flexible scrolling, but for now
170 		// let's stick to that) by cutting the beginning of the part
171 		// of the string before the cursor until it fits the area.
172 
173 		wchar_t *mod_pre_pos = pre_pos;
174 		while (*mod_pre_pos != 0)
175 		{
176 			++mod_pre_pos;
177 			int n = wcwidth(*mod_pre_pos);
178 			if (n < 0)
179 				--pos;
180 			else
181 				pos -= n;
182 			if (size_t(pos) <= width)
183 				break;
184 		}
185 		print_string(mod_pre_pos, pos);
186 	}
187 	w->goToXY(start_x+pos, start_y);
188 }
189 
add_base()190 int add_base()
191 {
192 	rl_insert_text(base);
193 	return 0;
194 }
195 
196 }
197 
198 int color_pair_counter;
199 std::vector<int> color_pair_map;
200 
201 }
202 
203 namespace NC {
204 
205 const short Color::transparent = -1;
206 const short Color::current = -2;
207 
208 Color Color::Default(0, 0, true, false);
209 Color Color::Black(COLOR_BLACK, Color::current);
210 Color Color::Red(COLOR_RED, Color::current);
211 Color Color::Green(COLOR_GREEN, Color::current);
212 Color Color::Yellow(COLOR_YELLOW, Color::current);
213 Color Color::Blue(COLOR_BLUE, Color::current);
214 Color Color::Magenta(COLOR_MAGENTA, Color::current);
215 Color Color::Cyan(COLOR_CYAN, Color::current);
216 Color Color::White(COLOR_WHITE, Color::current);
217 Color Color::End(0, 0, false, true);
218 
pairNumber() const219 int Color::pairNumber() const
220 {
221 	// If colors are disabled, return default pair value.
222 	if (color_pair_map.empty())
223 		return 0;
224 
225 	int result = 0;
226 	if (isEnd())
227 		throw std::logic_error("'end' doesn't have a corresponding pair number");
228 	else if (!isDefault())
229 	{
230 		if (!currentBackground())
231 			result = (background() + 1) % colorCount();
232 		result *= 256;
233 		result += foreground() % colorCount();
234 
235 		assert(result < int(color_pair_map.size()));
236 
237 		// NCurses allows for a limited number of color pairs to be registered, so
238 		// in order to be able to support all the combinations we want to, we need
239 		// to dynamically register only pairs of colors we're actually using.
240 		if (!color_pair_map[result])
241 		{
242 			// Check if there are any unused pairs left and either register the one
243 			// that was requested or return a default one if there is no space left.
244 			if (color_pair_counter >= COLOR_PAIRS)
245 				result = 0;
246 			else
247 			{
248 				init_pair(color_pair_counter, foreground(), background());
249 				color_pair_map[result] = color_pair_counter;
250 				++color_pair_counter;
251 			}
252 		}
253 		result = color_pair_map[result];
254 	}
255 	return result;
256 }
257 
operator >>(std::istream & is,Color & c)258 std::istream &operator>>(std::istream &is, Color &c)
259 {
260 	const short invalid_color_value = -1337;
261 	auto get_single_color = [](const std::string &s, bool background) {
262 		short result = invalid_color_value;
263 		if (s == "black")
264 			result = COLOR_BLACK;
265 		else if (s == "red")
266 			result = COLOR_RED;
267 		else if (s == "green")
268 			result = COLOR_GREEN;
269 		else if (s == "yellow")
270 			result = COLOR_YELLOW;
271 		else if (s == "blue")
272 			result = COLOR_BLUE;
273 		else if (s == "magenta")
274 			result = COLOR_MAGENTA;
275 		else if (s == "cyan")
276 			result = COLOR_CYAN;
277 		else if (s == "white")
278 			result = COLOR_WHITE;
279 		else if (background && s == "transparent")
280 			result = NC::Color::transparent;
281 		else if (background && s == "current")
282 			result = NC::Color::current;
283 		else if (std::all_of(s.begin(), s.end(), isdigit))
284 		{
285 			result = atoi(s.c_str());
286 			if (result < (background ? 0 : 1) || result > 256)
287 				result = invalid_color_value;
288 			else
289 				--result;
290 		}
291 		return result;
292 	};
293 
294 	auto get_color = [](std::istream &is_) {
295 		std::string result;
296 		while (!is_.eof() && isalnum(is_.peek()))
297 			result.push_back(is_.get());
298 		return result;
299 	};
300 
301 	std::string sc = get_color(is);
302 
303 	if (sc == "default")
304 		c = Color::Default;
305 	else if (sc == "end")
306 		c = Color::End;
307 	else
308 	{
309 		short fg = get_single_color(sc, false);
310 		if (fg == invalid_color_value)
311 			is.setstate(std::ios::failbit);
312 		// Check if there is background color
313 		else if (!is.eof() && is.peek() == '_')
314 		{
315 			is.get();
316 			sc = get_color(is);
317 			short bg = get_single_color(sc, true);
318 			if (bg == invalid_color_value)
319 				is.setstate(std::ios::failbit);
320 			else
321 				c = Color(fg, bg);
322 		}
323 		else
324 			c = Color(fg, NC::Color::current);
325 	}
326 	return is;
327 }
328 
reverseFormat(NC::Format fmt)329 NC::Format reverseFormat(NC::Format fmt)
330 {
331 	switch (fmt)
332 	{
333 	case NC::Format::Bold:
334 		return NC::Format::NoBold;
335 	case NC::Format::NoBold:
336 		return NC::Format::Bold;
337 	case NC::Format::Underline:
338 		return NC::Format::NoUnderline;
339 	case NC::Format::NoUnderline:
340 		return NC::Format::Underline;
341 	case NC::Format::Reverse:
342 		return NC::Format::NoReverse;
343 	case NC::Format::NoReverse:
344 		return NC::Format::Reverse;
345 	case NC::Format::AltCharset:
346 		return NC::Format::NoAltCharset;
347 	case NC::Format::NoAltCharset:
348 		return NC::Format::AltCharset;
349 	}
350 	// Unreachable, silence GCC.
351 	return fmt;
352 }
353 
354 namespace Mouse {
355 
356 namespace {
357 
358 bool supportEnabled = false;
359 
360 }
361 
enable()362 void enable()
363 {
364 	if (!supportEnabled)
365 		return;
366 	// save old highlight mouse tracking
367 	std::printf("\e[?1001s");
368 	// enable mouse tracking
369 	std::printf("\e[?1000h");
370 	// try to enable extended (urxvt) mouse tracking
371 	std::printf("\e[?1015h");
372 	// send the above to the terminal immediately
373 	std::fflush(stdout);
374 }
375 
disable()376 void disable()
377 {
378 	if (!supportEnabled)
379 		return;
380 	// disable extended (urxvt) mouse tracking
381 	std::printf("\e[?1015l");
382 	// disable mouse tracking
383 	std::printf("\e[?1000l");
384 	// restore old highlight mouse tracking
385 	std::printf("\e[?1001r");
386 	// send the above to the terminal immediately
387 	std::fflush(stdout);
388 }
389 
390 }
391 
colorCount()392 int colorCount()
393 {
394   return maxColor;
395 }
396 
initScreen(bool enable_colors,bool enable_mouse)397 void initScreen(bool enable_colors, bool enable_mouse)
398 {
399 	initscr();
400 	if (has_colors() && enable_colors)
401 	{
402 		start_color();
403 		use_default_colors();
404 		maxColor = COLORS;
405 		if (maxColor > 256)
406 		{
407 			maxColor = 256;
408 		}
409 		color_pair_map.resize(256 * 256, 0);
410 
411 		// Predefine pairs for colors with transparent background, all the other
412 		// ones will be dynamically registered in Color::pairNumber when they're
413 		// used.
414 		color_pair_counter = 1;
415 		for (int fg = 0; fg < colorCount(); ++fg, ++color_pair_counter)
416 		{
417 			init_pair(color_pair_counter, fg, -1);
418 			color_pair_map[fg] = color_pair_counter;
419 		}
420 	}
421 	raw();
422 	nonl();
423 	noecho();
424 	timeout(0);
425 	curs_set(0);
426 
427 	// setup mouse
428 	Mouse::supportEnabled = enable_mouse;
429 	Mouse::enable();
430 
431 	// initialize readline (needed, otherwise we get segmentation
432 	// fault on SIGWINCH). also, initialize first as doing this
433 	// later erases keys bound with rl_bind_key for some users.
434 	rl_initialize();
435 	// disable autocompletion
436 	rl_attempted_completion_function = [](const char *, int, int) -> char ** {
437 		rl_attempted_completion_over = 1;
438 		return nullptr;
439 	};
440 	auto abort_prompt = [](int, int) -> int {
441 		rl::aborted = true;
442 		rl_done = 1;
443 		return 0;
444 	};
445 	// if ctrl-c or ctrl-g is pressed, abort the prompt
446 	rl_bind_key('\3', abort_prompt);
447 	rl_bind_key('\7', abort_prompt);
448 	// do not change the state of the terminal
449 	rl_prep_term_function = nullptr;
450 	rl_deprep_term_function = nullptr;
451 	// do not catch signals
452 	rl_catch_signals = 0;
453 	rl_catch_sigwinch = 0;
454 	// overwrite readline callbacks
455 	rl_getc_function = rl::read_key;
456 	rl_redisplay_function = rl::display_string;
457 	rl_startup_hook = rl::add_base;
458 }
459 
pauseScreen()460 void pauseScreen()
461 {
462 	if (Mouse::supportEnabled)
463 		Mouse::disable();
464 	def_prog_mode();
465 	endwin();
466 }
467 
unpauseScreen()468 void unpauseScreen()
469 {
470 	if (Mouse::supportEnabled)
471 		Mouse::enable();
472 	refresh();
473 }
474 
destroyScreen()475 void destroyScreen()
476 {
477 	Mouse::disable();
478 	curs_set(1);
479 	endwin();
480 }
481 
Window(size_t startx,size_t starty,size_t width,size_t height,std::string title,Color color,Border border)482 Window::Window(size_t startx, size_t starty, size_t width, size_t height,
483                std::string title, Color color, Border border)
484 	: m_window(nullptr),
485 	  m_start_x(startx),
486 	  m_start_y(starty),
487 	  m_width(width),
488 	  m_height(height),
489 	  m_window_timeout(-1),
490 	  m_border(std::move(border)),
491 	  m_prompt_hook(0),
492 	  m_title(std::move(title)),
493 	  m_escape_terminal_sequences(true),
494 	  m_bold_counter(0),
495 	  m_underline_counter(0),
496 	  m_reverse_counter(0),
497 	  m_alt_charset_counter(0)
498 {
499 	if (m_start_x > size_t(COLS)
500 	    ||  m_start_y > size_t(LINES)
501 	    ||  m_width+m_start_x > size_t(COLS)
502 	    ||  m_height+m_start_y > size_t(LINES))
503 		throw std::logic_error("constructed window doesn't fit into the terminal");
504 
505 	if (m_border)
506 	{
507 		++m_start_x;
508 		++m_start_y;
509 		m_width -= 2;
510 		m_height -= 2;
511 	}
512 	if (!m_title.empty())
513 	{
514 		m_start_y += 2;
515 		m_height -= 2;
516 	}
517 
518 	m_window = newpad(m_height, m_width);
519 	wtimeout(m_window, 0);
520 
521 	setBaseColor(color);
522 	setColor(m_base_color);
523 }
524 
Window(const Window & rhs)525 Window::Window(const Window &rhs)
526 : m_window(dupwin(rhs.m_window))
527 , m_start_x(rhs.m_start_x)
528 , m_start_y(rhs.m_start_y)
529 , m_width(rhs.m_width)
530 , m_height(rhs.m_height)
531 , m_window_timeout(rhs.m_window_timeout)
532 , m_color(rhs.m_color)
533 , m_base_color(rhs.m_base_color)
534 , m_border(rhs.m_border)
535 , m_prompt_hook(rhs.m_prompt_hook)
536 , m_title(rhs.m_title)
537 , m_color_stack(rhs.m_color_stack)
538 , m_input_queue(rhs.m_input_queue)
539 , m_fds(rhs.m_fds)
540 , m_escape_terminal_sequences(rhs.m_escape_terminal_sequences)
541 , m_bold_counter(rhs.m_bold_counter)
542 , m_underline_counter(rhs.m_underline_counter)
543 , m_reverse_counter(rhs.m_reverse_counter)
544 , m_alt_charset_counter(rhs.m_alt_charset_counter)
545 {
546 	setColor(m_color);
547 }
548 
Window(Window && rhs)549 Window::Window(Window &&rhs)
550 : m_window(rhs.m_window)
551 , m_start_x(rhs.m_start_x)
552 , m_start_y(rhs.m_start_y)
553 , m_width(rhs.m_width)
554 , m_height(rhs.m_height)
555 , m_window_timeout(rhs.m_window_timeout)
556 , m_color(rhs.m_color)
557 , m_base_color(rhs.m_base_color)
558 , m_border(rhs.m_border)
559 , m_prompt_hook(rhs.m_prompt_hook)
560 , m_title(std::move(rhs.m_title))
561 , m_color_stack(std::move(rhs.m_color_stack))
562 , m_input_queue(std::move(rhs.m_input_queue))
563 , m_fds(std::move(rhs.m_fds))
564 , m_escape_terminal_sequences(rhs.m_escape_terminal_sequences)
565 , m_bold_counter(rhs.m_bold_counter)
566 , m_underline_counter(rhs.m_underline_counter)
567 , m_reverse_counter(rhs.m_reverse_counter)
568 , m_alt_charset_counter(rhs.m_alt_charset_counter)
569 {
570 	rhs.m_window = nullptr;
571 }
572 
operator =(Window rhs)573 Window &Window::operator=(Window rhs)
574 {
575 	std::swap(m_window, rhs.m_window);
576 	std::swap(m_start_x, rhs.m_start_x);
577 	std::swap(m_start_y, rhs.m_start_y);
578 	std::swap(m_width, rhs.m_width);
579 	std::swap(m_height, rhs.m_height);
580 	std::swap(m_window_timeout, rhs.m_window_timeout);
581 	std::swap(m_color, rhs.m_color);
582 	std::swap(m_base_color, rhs.m_base_color);
583 	std::swap(m_border, rhs.m_border);
584 	std::swap(m_prompt_hook, rhs.m_prompt_hook);
585 	std::swap(m_title, rhs.m_title);
586 	std::swap(m_color_stack, rhs.m_color_stack);
587 	std::swap(m_input_queue, rhs.m_input_queue);
588 	std::swap(m_fds, rhs.m_fds);
589 	std::swap(m_escape_terminal_sequences, rhs.m_escape_terminal_sequences);
590 	std::swap(m_bold_counter, rhs.m_bold_counter);
591 	std::swap(m_underline_counter, rhs.m_underline_counter);
592 	std::swap(m_reverse_counter, rhs.m_reverse_counter);
593 	std::swap(m_alt_charset_counter, rhs.m_alt_charset_counter);
594 	return *this;
595 }
596 
~Window()597 Window::~Window()
598 {
599 	delwin(m_window);
600 }
601 
setColor(Color c)602 void Window::setColor(Color c)
603 {
604 	if (c.isDefault())
605 		c = m_base_color;
606 	if (c != Color::Default)
607 	{
608 		assert(!c.currentBackground());
609 		wcolor_set(m_window, c.pairNumber(), nullptr);
610 	}
611 	else
612 		wcolor_set(m_window, m_base_color.pairNumber(), nullptr);
613 	m_color = std::move(c);
614 }
615 
setBaseColor(const Color & color)616 void Window::setBaseColor(const Color &color)
617 {
618 	if (color.currentBackground())
619 		m_base_color = Color(color.foreground(), Color::transparent);
620 	else
621 		m_base_color = color;
622 }
623 
setBorder(Border border)624 void Window::setBorder(Border border)
625 {
626 	if (!border && m_border)
627 	{
628 		--m_start_x;
629 		--m_start_y;
630 		m_height += 2;
631 		m_width += 2;
632 		recreate(m_width, m_height);
633 	}
634 	else if (border && !m_border)
635 	{
636 		++m_start_x;
637 		++m_start_y;
638 		m_height -= 2;
639 		m_width -= 2;
640 		recreate(m_width, m_height);
641 	}
642 	m_border = border;
643 }
644 
setTitle(const std::string & new_title)645 void Window::setTitle(const std::string &new_title)
646 {
647 	if (!new_title.empty() && m_title.empty())
648 	{
649 		m_start_y += 2;
650 		m_height -= 2;
651 		recreate(m_width, m_height);
652 	}
653 	else if (new_title.empty() && !m_title.empty())
654 	{
655 		m_start_y -= 2;
656 		m_height += 2;
657 		recreate(m_width, m_height);
658 	}
659 	m_title = new_title;
660 }
661 
recreate(size_t width,size_t height)662 void Window::recreate(size_t width, size_t height)
663 {
664 	delwin(m_window);
665 	m_window = newpad(height, width);
666 	wtimeout(m_window, 0);
667 	setColor(m_color);
668 }
669 
moveTo(size_t new_x,size_t new_y)670 void Window::moveTo(size_t new_x, size_t new_y)
671 {
672 	m_start_x = new_x;
673 	m_start_y = new_y;
674 	if (m_border)
675 	{
676 		++m_start_x;
677 		++m_start_y;
678 	}
679 	if (!m_title.empty())
680 		m_start_y += 2;
681 }
682 
adjustDimensions(size_t width,size_t height)683 void Window::adjustDimensions(size_t width, size_t height)
684 {
685 	if (m_border)
686 	{
687 		width -= 2;
688 		height -= 2;
689 	}
690 	if (!m_title.empty())
691 		height -= 2;
692 	m_height = height;
693 	m_width = width;
694 }
695 
resize(size_t new_width,size_t new_height)696 void Window::resize(size_t new_width, size_t new_height)
697 {
698 	adjustDimensions(new_width, new_height);
699 	recreate(m_width, m_height);
700 }
701 
refreshBorder() const702 void Window::refreshBorder() const
703 {
704 	if (m_border)
705 	{
706 		size_t start_x = getStartX(), start_y = getStarty();
707 		size_t width = getWidth(), height = getHeight();
708 		color_set(m_border->pairNumber(), nullptr);
709 		attron(A_ALTCHARSET);
710 		// corners
711 		mvaddch(start_y, start_x, 'l');
712 		mvaddch(start_y, start_x+width-1, 'k');
713 		mvaddch(start_y+height-1, start_x, 'm');
714 		mvaddch(start_y+height-1, start_x+width-1, 'j');
715 		// lines
716 		mvhline(start_y, start_x+1, 'q', width-2);
717 		mvhline(start_y+height-1, start_x+1, 'q', width-2);
718 		mvvline(start_y+1, start_x, 'x', height-2);
719 		mvvline(start_y+1, start_x+width-1, 'x', height-2);
720 		if (!m_title.empty())
721 		{
722 			mvaddch(start_y+2, start_x, 't');
723 			mvaddch(start_y+2, start_x+width-1, 'u');
724 		}
725 		attroff(A_ALTCHARSET);
726 	}
727 	else
728 		color_set(m_base_color.pairNumber(), nullptr);
729 	if (!m_title.empty())
730 	{
731 		// clear title line
732 		mvhline(m_start_y-2, m_start_x, ' ', m_width);
733 		attron(A_BOLD);
734 		mvaddstr(m_start_y-2, m_start_x, m_title.c_str());
735 		attroff(A_BOLD);
736 		// add separator
737 		mvhline(m_start_y-1, m_start_x, 0, m_width);
738 	}
739 	standend();
740 	::refresh();
741 }
742 
display()743 void Window::display()
744 {
745 	refreshBorder();
746 	refresh();
747 }
748 
refresh()749 void Window::refresh()
750 {
751 	prefresh(m_window, 0, 0, m_start_y, m_start_x, m_start_y+m_height-1, m_start_x+m_width-1);
752 }
753 
clear()754 void Window::clear()
755 {
756 	werase(m_window);
757 	setColor(m_base_color);
758 }
759 
bold(bool bold_state) const760 void Window::bold(bool bold_state) const
761 {
762 	(bold_state ? wattron : wattroff)(m_window, A_BOLD);
763 }
764 
underline(bool underline_state) const765 void Window::underline(bool underline_state) const
766 {
767 	(underline_state ? wattron : wattroff)(m_window, A_UNDERLINE);
768 }
769 
reverse(bool reverse_state) const770 void Window::reverse(bool reverse_state) const
771 {
772 	(reverse_state ? wattron : wattroff)(m_window, A_REVERSE);
773 }
774 
altCharset(bool altcharset_state) const775 void Window::altCharset(bool altcharset_state) const
776 {
777 	(altcharset_state ? wattron : wattroff)(m_window, A_ALTCHARSET);
778 }
779 
setTimeout(int timeout)780 void Window::setTimeout(int timeout)
781 {
782 	m_window_timeout = timeout;
783 }
784 
addFDCallback(int fd,void (* callback)())785 void Window::addFDCallback(int fd, void (*callback)())
786 {
787 	m_fds.push_back(std::make_pair(fd, callback));
788 }
789 
clearFDCallbacksList()790 void Window::clearFDCallbacksList()
791 {
792 	m_fds.clear();
793 }
794 
FDCallbacksListEmpty() const795 bool Window::FDCallbacksListEmpty() const
796 {
797 	return m_fds.empty();
798 }
799 
getInputChar(int key)800 Key::Type Window::getInputChar(int key)
801 {
802 	if (!m_escape_terminal_sequences || key != Key::Escape)
803 		return key;
804 	auto define_mouse_event = [this](int type) {
805 		switch (type & ~28)
806 		{
807 		case 32:
808 			m_mouse_event.bstate = BUTTON1_PRESSED;
809 			break;
810 		case 33:
811 			m_mouse_event.bstate = BUTTON2_PRESSED;
812 			break;
813 		case 34:
814 			m_mouse_event.bstate = BUTTON3_PRESSED;
815 			break;
816 		case 96:
817 			m_mouse_event.bstate = BUTTON4_PRESSED;
818 			break;
819 		case 97:
820 			m_mouse_event.bstate = BUTTON5_PRESSED;
821 			break;
822 		default:
823 			return Key::None;
824 		}
825 		if (type & 4)
826 			m_mouse_event.bstate |= BUTTON_SHIFT;
827 		if (type & 8)
828 			m_mouse_event.bstate |= BUTTON_ALT;
829 		if (type & 16)
830 			m_mouse_event.bstate |= BUTTON_CTRL;
831 		if (m_mouse_event.x < 0 || m_mouse_event.x >= COLS)
832 			return Key::None;
833 		if (m_mouse_event.y < 0 || m_mouse_event.y >= LINES)
834 			return Key::None;
835 		return Key::Mouse;
836 	};
837 	auto get_xterm_modifier_key = [](int ch) {
838 		Key::Type modifier;
839 		switch (ch)
840 		{
841 		case '2':
842 			modifier = Key::Shift;
843 			break;
844 		case '3':
845 			modifier = Key::Alt;
846 			break;
847 		case '4':
848 			modifier = Key::Alt | Key::Shift;
849 			break;
850 		case '5':
851 			modifier = Key::Ctrl;
852 			break;
853 		case '6':
854 			modifier = Key::Ctrl | Key::Shift;
855 			break;
856 		case '7':
857 			modifier = Key::Alt | Key::Ctrl;
858 			break;
859 		case '8':
860 			modifier = Key::Alt | Key::Ctrl | Key::Shift;
861 			break;
862 		default:
863 			modifier = Key::None;
864 		}
865 		return modifier;
866 	};
867 	auto parse_number = [this](int &result) {
868 		int x;
869 		while (true)
870 		{
871 			x = wgetch(m_window);
872 			if (!isdigit(x))
873 				return x;
874 			result = result*10 + x - '0';
875 		}
876 	};
877 	key = wgetch(m_window);
878 	switch (key)
879 	{
880 	case '\t': // tty
881 		return Key::Shift | Key::Tab;
882 	case 'O':
883 		key = wgetch(m_window);
884 		switch (key)
885 		{
886 		// eterm
887 		case 'A':
888 			return Key::Up;
889 		case 'B':
890 			return Key::Down;
891 		case 'C':
892 			return Key::Right;
893 		case 'D':
894 			return Key::Left;
895 		// terminator
896 		case 'F':
897 			return Key::End;
898 		case 'H':
899 			return Key::Home;
900 		// rxvt
901 		case 'a':
902 			return Key::Ctrl | Key::Up;
903 		case 'b':
904 			return Key::Ctrl | Key::Down;
905 		case 'c':
906 			return Key::Ctrl | Key::Right;
907 		case 'd':
908 			return Key::Ctrl | Key::Left;
909 		// xterm
910 		case 'P':
911 			return Key::F1;
912 		case 'Q':
913 			return Key::F2;
914 		case 'R':
915 			return Key::F3;
916 		case 'S':
917 			return Key::F4;
918 		default:
919 			return Key::None;
920 		}
921 	case '[':
922 		key = wgetch(m_window);
923 		switch (key)
924 		{
925 		case 'a':
926 			return Key::Shift | Key::Up;
927 		case 'b':
928 			return Key::Shift | Key::Down;
929 		case 'c':
930 			return Key::Shift | Key::Right;
931 		case 'd':
932 			return Key::Shift | Key::Left;
933 		case 'A':
934 			return Key::Up;
935 		case 'B':
936 			return Key::Down;
937 		case 'C':
938 			return Key::Right;
939 		case 'D':
940 			return Key::Left;
941 		case 'F': // xterm
942 			return Key::End;
943 		case 'H': // xterm
944 			return Key::Home;
945 		case 'M': // standard mouse event
946 		{
947 			key = wgetch(m_window);
948 			int raw_x = wgetch(m_window);
949 			int raw_y = wgetch(m_window);
950 			// support coordinates up to 255
951 			m_mouse_event.x = (raw_x - 33) & 0xff;
952 			m_mouse_event.y = (raw_y - 33) & 0xff;
953 			return define_mouse_event(key);
954 		}
955 		case 'P': // st
956 			return Key::Delete;
957 		case 'Z':
958 			return Key::Shift | Key::Tab;
959 		case '[': // F1 to F5 in tty
960 			key = wgetch(m_window);
961 			switch (key)
962 			{
963 			case 'A':
964 				return Key::F1;
965 			case 'B':
966 				return Key::F2;
967 			case 'C':
968 				return Key::F3;
969 			case 'D':
970 				return Key::F4;
971 			case 'E':
972 				return Key::F5;
973 			default:
974 				return Key::None;
975 			}
976 		case '1': case '2': case '3':
977 		case '4': case '5': case '6':
978 		case '7': case '8': case '9':
979 		{
980 			key -= '0';
981 			int delim = parse_number(key);
982 			if (key >= 2 && key <= 8)
983 			{
984 				Key::Type modifier;
985 				switch (delim)
986 				{
987 				case '~':
988 					modifier = Key::Null;
989 					break;
990 				case '^':
991 					modifier = Key::Ctrl;
992 					break;
993 				case '$':
994 					modifier = Key::Shift;
995 					break;
996 				case '@':
997 					modifier = Key::Ctrl | Key::Shift;
998 					break;
999 				case ';': // xterm insert/delete/page up/page down
1000 				{
1001 					int local_key = wgetch(m_window);
1002 					modifier = get_xterm_modifier_key(local_key);
1003 					local_key = wgetch(m_window);
1004 					if (local_key != '~' || (key != 2 && key != 3 && key != 5 && key != 6))
1005 						return Key::None;
1006 					break;
1007 				}
1008 				default:
1009 					return Key::None;
1010 				}
1011 				switch (key)
1012 				{
1013 				case 2:
1014 					return modifier | Key::Insert;
1015 				case 3:
1016 					return modifier | Key::Delete;
1017 				case 4:
1018 					return modifier | Key::End;
1019 				case 5:
1020 					return modifier | Key::PageUp;
1021 				case 6:
1022 					return modifier | Key::PageDown;
1023 				case 7:
1024 					return modifier | Key::Home;
1025 				case 8:
1026 					return modifier | Key::End;
1027 				default:
1028 					std::cerr << "Unreachable code, aborting.\n";
1029 					std::terminate();
1030 				}
1031 			}
1032 			switch (delim)
1033 			{
1034 			case '~':
1035 			{
1036 				switch (key)
1037 				{
1038 				case 1: // tty
1039 					return Key::Home;
1040 				case 11:
1041 					return Key::F1;
1042 				case 12:
1043 					return Key::F2;
1044 				case 13:
1045 					return Key::F3;
1046 				case 14:
1047 					return Key::F4;
1048 				case 15:
1049 					return Key::F5;
1050 				case 17: // not a typo
1051 					return Key::F6;
1052 				case 18:
1053 					return Key::F7;
1054 				case 19:
1055 					return Key::F8;
1056 				case 20:
1057 					return Key::F9;
1058 				case 21:
1059 					return Key::F10;
1060 				case 23: // not a typo
1061 					return Key::F11;
1062 				case 24:
1063 					return Key::F12;
1064 				default:
1065 					return Key::None;
1066 				}
1067 			}
1068 			case ';':
1069 				switch (key)
1070 				{
1071 				case 1: // xterm
1072 				{
1073 					key = wgetch(m_window);
1074 					Key::Type modifier = get_xterm_modifier_key(key);
1075 					if (modifier == Key::None)
1076 						return Key::None;
1077 					key = wgetch(m_window);
1078 					switch (key)
1079 					{
1080 					case 'A':
1081 						return modifier | Key::Up;
1082 					case 'B':
1083 						return modifier | Key::Down;
1084 					case 'C':
1085 						return modifier | Key::Right;
1086 					case 'D':
1087 						return modifier | Key::Left;
1088 					case 'F':
1089 						return modifier | Key::End;
1090 					case 'H':
1091 						return modifier | Key::Home;
1092 					default:
1093 						return Key::None;
1094 					}
1095 				}
1096 				default: // urxvt mouse
1097 					m_mouse_event.x = 0;
1098 					delim = parse_number(m_mouse_event.x);
1099 					if (delim != ';')
1100 						return Key::None;
1101 					m_mouse_event.y = 0;
1102 					delim = parse_number(m_mouse_event.y);
1103 					if (delim != 'M')
1104 						return Key::None;
1105 					--m_mouse_event.x;
1106 					--m_mouse_event.y;
1107 					return define_mouse_event(key);
1108 				}
1109 			default:
1110 				return Key::None;
1111 			}
1112 		}
1113 		default:
1114 			return Key::None;
1115 		}
1116 	case ERR:
1117 		return Key::Escape;
1118 	default: // alt + something
1119 	{
1120 		auto key_prim = getInputChar(key);
1121 		if (key_prim != Key::None)
1122 			return Key::Alt | key_prim;
1123 		return Key::None;
1124 	}
1125 	}
1126 }
1127 
readKey()1128 Key::Type Window::readKey()
1129 {
1130 	Key::Type result;
1131 	// if there are characters in input queue,
1132 	// get them and return immediately.
1133 	if (!m_input_queue.empty())
1134 	{
1135 		result = m_input_queue.front();
1136 		m_input_queue.pop();
1137 		return result;
1138 	}
1139 
1140 	fd_set fds_read;
1141 	FD_ZERO(&fds_read);
1142 	FD_SET(STDIN_FILENO, &fds_read);
1143 	timeval timeout = { m_window_timeout/1000, (m_window_timeout%1000)*1000 };
1144 
1145 	int fd_max = STDIN_FILENO;
1146 	for (const auto &fd : m_fds)
1147 	{
1148 		if (fd.first > fd_max)
1149 			fd_max = fd.first;
1150 		FD_SET(fd.first, &fds_read);
1151 	}
1152 
1153 	auto tv_addr = m_window_timeout < 0 ? nullptr : &timeout;
1154 	int res = select(fd_max+1, &fds_read, nullptr, nullptr, tv_addr);
1155 	if (res > 0)
1156 	{
1157 		if (FD_ISSET(STDIN_FILENO, &fds_read))
1158 		{
1159 			int key = wgetch(m_window);
1160 			if (key == EOF)
1161 				result = Key::EoF;
1162 			else
1163 				result = getInputChar(key);
1164 		}
1165 		else
1166 			result = Key::None;
1167 
1168 		for (const auto &fd : m_fds)
1169 			if (FD_ISSET(fd.first, &fds_read))
1170 				fd.second();
1171 	}
1172 	else
1173 		result = Key::None;
1174 	return result;
1175 }
1176 
pushChar(const Key::Type ch)1177 void Window::pushChar(const Key::Type ch)
1178 {
1179 	m_input_queue.push(ch);
1180 }
1181 
prompt(const std::string & base,size_t width,bool encrypted)1182 std::string Window::prompt(const std::string &base, size_t width, bool encrypted)
1183 {
1184 	std::string result;
1185 
1186 	rl::aborted = false;
1187 	rl::w = this;
1188 	getyx(m_window, rl::start_y, rl::start_x);
1189 	rl::width = std::min(m_width-rl::start_x-1, width-1);
1190 	rl::encrypted = encrypted;
1191 	rl::base = base.c_str();
1192 
1193 	curs_set(1);
1194 	Mouse::disable();
1195 	m_escape_terminal_sequences = false;
1196 	char *input = readline(nullptr);
1197 	m_escape_terminal_sequences = true;
1198 	Mouse::enable();
1199 	curs_set(0);
1200 	if (input != nullptr)
1201 	{
1202 #ifdef HAVE_READLINE_HISTORY_H
1203 		if (!encrypted && input[0] != 0)
1204 			add_history(input);
1205 #endif // HAVE_READLINE_HISTORY_H
1206 		result = input;
1207 		free(input);
1208 	}
1209 
1210 	if (rl::aborted)
1211 		throw PromptAborted(std::move(result));
1212 
1213 	return result;
1214 }
1215 
goToXY(int x,int y)1216 void Window::goToXY(int x, int y)
1217 {
1218 	wmove(m_window, y, x);
1219 }
1220 
getX()1221 int Window::getX()
1222 {
1223 	return getcurx(m_window);
1224 }
1225 
getY()1226 int Window::getY()
1227 {
1228 	return getcury(m_window);
1229 }
1230 
hasCoords(int & x,int & y)1231 bool Window::hasCoords(int &x, int &y)
1232 {
1233 	return wmouse_trafo(m_window, &y, &x, 0);
1234 }
1235 
runPromptHook(const char * arg,bool * done) const1236 bool Window::runPromptHook(const char *arg, bool *done) const
1237 {
1238 	if (m_prompt_hook)
1239 	{
1240 		bool continue_ = m_prompt_hook(arg);
1241 		if (done != nullptr)
1242 			*done = !continue_;
1243 		return true;
1244 	}
1245 	else
1246 		return false;
1247 }
1248 
getWidth() const1249 size_t Window::getWidth() const
1250 {
1251 	if (m_border)
1252 		return m_width+2;
1253 	else
1254 		return m_width;
1255 }
1256 
getHeight() const1257 size_t Window::getHeight() const
1258 {
1259 	size_t height = m_height;
1260 	if (m_border)
1261 		height += 2;
1262 	if (!m_title.empty())
1263 		height += 2;
1264 	return height;
1265 }
1266 
getStartX() const1267 size_t Window::getStartX() const
1268 {
1269 	if (m_border)
1270 		return m_start_x-1;
1271 	else
1272 		return m_start_x;
1273 }
1274 
getStarty() const1275 size_t Window::getStarty() const
1276 {
1277 	size_t starty = m_start_y;
1278 	if (m_border)
1279 		--starty;
1280 	if (!m_title.empty())
1281 		starty -= 2;
1282 	return starty;
1283 }
1284 
getTitle() const1285 const std::string &Window::getTitle() const
1286 {
1287 	return m_title;
1288 }
1289 
getColor() const1290 const Color &Window::getColor() const
1291 {
1292 	return m_color;
1293 }
1294 
getBorder() const1295 const Border &Window::getBorder() const
1296 {
1297 	return m_border;
1298 }
1299 
getTimeout() const1300 int Window::getTimeout() const
1301 {
1302 	return m_window_timeout;
1303 }
1304 
getMouseEvent()1305 const MEVENT &Window::getMouseEvent()
1306 {
1307 	return m_mouse_event;
1308 }
1309 
scroll(Scroll where)1310 void Window::scroll(Scroll where)
1311 {
1312 	idlok(m_window, 1);
1313 	scrollok(m_window, 1);
1314 	switch (where)
1315 	{
1316 		case Scroll::Up:
1317 			wscrl(m_window, 1);
1318 			break;
1319 		case Scroll::Down:
1320 			wscrl(m_window, -1);
1321 			break;
1322 		case Scroll::PageUp:
1323 			wscrl(m_window, m_width);
1324 			break;
1325 		case Scroll::PageDown:
1326 			wscrl(m_window, -m_width);
1327 			break;
1328 		default:
1329 			break;
1330 	}
1331 	idlok(m_window, 0);
1332 	scrollok(m_window, 0);
1333 }
1334 
1335 
operator <<(const Color & c)1336 Window &Window::operator<<(const Color &c)
1337 {
1338 	if (c.isDefault())
1339 	{
1340 		while (!m_color_stack.empty())
1341 			m_color_stack.pop();
1342 		setColor(m_base_color);
1343 	}
1344 	else if (c.isEnd())
1345 	{
1346 		if (!m_color_stack.empty())
1347 			m_color_stack.pop();
1348 		if (!m_color_stack.empty())
1349 			setColor(m_color_stack.top());
1350 		else
1351 			setColor(m_base_color);
1352 	}
1353 	else
1354 	{
1355 		if (c.currentBackground())
1356 		{
1357 			short background = m_color.isDefault()
1358 				? Color::transparent
1359 				: m_color.background();
1360 			Color cc = Color(c.foreground(), background);
1361 			setColor(cc);
1362 			m_color_stack.push(cc);
1363 		}
1364 		else
1365 		{
1366 			setColor(c);
1367 			m_color_stack.push(c);
1368 		}
1369 	}
1370 	return *this;
1371 }
1372 
operator <<(Format format)1373 Window &Window::operator<<(Format format)
1374 {
1375 	auto increase_flag = [](Window &w, int &flag, auto set) {
1376 		++flag;
1377 		(w.*set)(true);
1378 	};
1379 	auto decrease_flag = [](Window &w, int &flag, auto set) {
1380 		if (flag > 0)
1381 		{
1382 			--flag;
1383 			if (flag == 0)
1384 				(w.*set)(false);
1385 		}
1386 	};
1387 	switch (format)
1388 	{
1389 		case Format::Bold:
1390 			increase_flag(*this, m_bold_counter, &Window::bold);
1391 			break;
1392 		case Format::NoBold:
1393 			decrease_flag(*this, m_bold_counter, &Window::bold);
1394 			break;
1395 		case Format::Underline:
1396 			increase_flag(*this, m_underline_counter, &Window::underline);
1397 			break;
1398 		case Format::NoUnderline:
1399 			decrease_flag(*this, m_underline_counter, &Window::underline);
1400 			break;
1401 		case Format::Reverse:
1402 			increase_flag(*this, m_reverse_counter, &Window::reverse);
1403 			break;
1404 		case Format::NoReverse:
1405 			decrease_flag(*this, m_reverse_counter, &Window::reverse);
1406 			break;
1407 		case Format::AltCharset:
1408 			increase_flag(*this, m_alt_charset_counter, &Window::altCharset);
1409 			break;
1410 		case Format::NoAltCharset:
1411 			decrease_flag(*this, m_alt_charset_counter, &Window::altCharset);
1412 			break;
1413 	}
1414 	return *this;
1415 }
1416 
operator <<(TermManip tm)1417 Window &Window::operator<<(TermManip tm)
1418 {
1419 	switch (tm)
1420 	{
1421 		case TermManip::ClearToEOL:
1422 		{
1423 			auto x = getX(), y = getY();
1424 			mvwhline(m_window, y, x, ' ', m_width-x);
1425 			goToXY(x, y);
1426 		}
1427 		break;
1428 	}
1429 	return *this;
1430 }
1431 
operator <<(const XY & coords)1432 Window &Window::operator<<(const XY &coords)
1433 {
1434 	goToXY(coords.x, coords.y);
1435 	return *this;
1436 }
1437 
operator <<(const char * s)1438 Window &Window::operator<<(const char *s)
1439 {
1440 	waddstr(m_window, s);
1441 	return *this;
1442 }
1443 
operator <<(char c)1444 Window &Window::operator<<(char c)
1445 {
1446 	// Might cause problem similar to
1447 	// https://github.com/arybczak/ncmpcpp/issues/21, enable for testing as the
1448 	// code in the ticket supposed to be culprit was rewritten.
1449 	waddnstr(m_window, &c, 1);
1450 	//wprintw(m_window, "%c", c);
1451 	return *this;
1452 }
1453 
operator <<(const wchar_t * ws)1454 Window &Window::operator<<(const wchar_t *ws)
1455 {
1456 	waddwstr(m_window, ws);
1457 	return *this;
1458 }
1459 
operator <<(wchar_t wc)1460 Window &Window::operator<<(wchar_t wc)
1461 {
1462 	waddnwstr(m_window, &wc, 1);
1463 	return *this;
1464 }
1465 
operator <<(int i)1466 Window &Window::operator<<(int i)
1467 {
1468 	wprintw(m_window, "%d", i);
1469 	return *this;
1470 }
1471 
operator <<(double d)1472 Window &Window::operator<<(double d)
1473 {
1474 	wprintw(m_window, "%f", d);
1475 	return *this;
1476 }
1477 
operator <<(const std::string & s)1478 Window &Window::operator<<(const std::string &s)
1479 {
1480 	waddnstr(m_window, s.c_str(), s.length());
1481 	return *this;
1482 }
1483 
operator <<(const std::wstring & ws)1484 Window &Window::operator<<(const std::wstring &ws)
1485 {
1486 	waddnwstr(m_window, ws.c_str(), ws.length());
1487 	return *this;
1488 }
1489 
operator <<(size_t s)1490 Window &Window::operator<<(size_t s)
1491 {
1492 	wprintw(m_window, "%zu", s);
1493 	return *this;
1494 }
1495 
1496 }
1497