1 /* vifm
2  * Copyright (C) 2014 xaizek.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
17  */
18 
19 #include "statusline.h"
20 #include "private/statusline.h"
21 
22 #include <curses.h> /* mvwin() werase() */
23 
24 #include <assert.h> /* assert() */
25 #include <ctype.h> /* isdigit() */
26 #include <stddef.h> /* NULL size_t */
27 #include <stdlib.h> /* RAND_MAX free() rand() */
28 #include <string.h> /* strcat() strdup() strlen() */
29 #include <time.h> /* time() */
30 #include <unistd.h>
31 
32 #include "../cfg/config.h"
33 #include "../compat/pthread.h"
34 #include "../compat/reallocarray.h"
35 #include "../engine/mode.h"
36 #include "../engine/parsing.h"
37 #include "../engine/var.h"
38 #include "../modes/modes.h"
39 #include "../utils/fs.h"
40 #include "../utils/log.h"
41 #include "../utils/macros.h"
42 #include "../utils/path.h"
43 #include "../utils/str.h"
44 #include "../utils/string_array.h"
45 #include "../utils/test_helpers.h"
46 #include "../utils/utf8.h"
47 #include "../utils/utils.h"
48 #include "../background.h"
49 #include "../filelist.h"
50 #include "color_manager.h"
51 #include "color_scheme.h"
52 #include "colored_line.h"
53 #include "ui.h"
54 
55 static void update_stat_window_old(view_t *view, int lazy_redraw);
56 static void refresh_window(WINDOW *win, int lazily);
57 TSTATIC cline_t expand_status_line_macros(view_t *view, const char format[]);
58 static cline_t parse_view_macros(view_t *view, const char **format,
59 		const char macros[], int opt);
60 static int expand_num(char buf[], size_t buf_len, int val);
61 static const char * get_tip(void);
62 static void check_expanded_str(const char buf[], int skip, int *nexpansions);
63 static pthread_spinlock_t * get_job_bar_changed_lock(void);
64 static void init_job_bar_changed_lock(void);
65 static int is_job_bar_visible(void);
66 static const char * format_job_bar(void);
67 static char ** take_job_descr_snapshot(void);
68 
69 /* Number of background jobs. */
70 static size_t nbar_jobs;
71 /* Array of jobs. */
72 static bg_op_t **bar_jobs;
73 /* Whether list of jobs needs to be redrawn. */
74 static int job_bar_changed;
75 /* Protects accesses to job_bar_changed variable. */
76 static pthread_spinlock_t job_bar_changed_lock;
77 
78 void
ui_stat_update(view_t * view,int lazy_redraw)79 ui_stat_update(view_t *view, int lazy_redraw)
80 {
81 	if(!cfg.display_statusline || view != curr_view)
82 	{
83 		return;
84 	}
85 
86 	/* Don't redraw anything until :restart command is finished. */
87 	if(curr_stats.restart_in_progress)
88 	{
89 		return;
90 	}
91 
92 	ui_stat_job_bar_check_for_updates();
93 
94 	if(cfg.status_line[0] == '\0')
95 	{
96 		update_stat_window_old(view, lazy_redraw);
97 		return;
98 	}
99 
100 	const int width = getmaxx(stdscr);
101 
102 	wresize(stat_win, 1, width);
103 	ui_set_bg(stat_win, &cfg.cs.color[STATUS_LINE_COLOR],
104 			cfg.cs.pair[STATUS_LINE_COLOR]);
105 	werase(stat_win);
106 	checked_wmove(stat_win, 0, 0);
107 
108 	cline_t result = expand_status_line_macros(view, cfg.status_line);
109 	assert(strlen(result.attrs) == utf8_strsw(result.line) && "Broken attrs!");
110 	result.line = break_in_two(result.line, width, "%=");
111 	result.attrs = break_in_two(result.attrs, width, "=");
112 	cline_print(&result, stat_win, &cfg.cs.color[STATUS_LINE_COLOR]);
113 	cline_dispose(&result);
114 
115 	refresh_window(stat_win, lazy_redraw);
116 }
117 
118 /* Formats status line in the "old way" (before introduction of 'statusline'
119  * option). */
120 static void
update_stat_window_old(view_t * view,int lazy_redraw)121 update_stat_window_old(view_t *view, int lazy_redraw)
122 {
123 	const dir_entry_t *const curr = get_current_entry(view);
124 	char name_buf[160*2 + 1];
125 	char perm_buf[26];
126 	char size_buf[64];
127 	char id_buf[52];
128 	int x;
129 	int cur_x;
130 	size_t print_width;
131 	char *filename;
132 
133 	if(curr == NULL || fentry_is_fake(curr))
134 	{
135 		werase(stat_win);
136 		refresh_window(stat_win, lazy_redraw);
137 		return;
138 	}
139 
140 	x = getmaxx(stdscr);
141 	wresize(stat_win, 1, x);
142 
143 	ui_set_bg(stat_win, &cfg.cs.color[STATUS_LINE_COLOR],
144 			cfg.cs.pair[STATUS_LINE_COLOR]);
145 
146 	filename = get_current_file_name(view);
147 	print_width = utf8_strsnlen(filename, 20 + MAX(0, x - 83));
148 	copy_str(name_buf, MIN(sizeof(name_buf), print_width + 1), filename);
149 	friendly_size_notation(fentry_get_size(view, curr), sizeof(size_buf),
150 			size_buf);
151 
152 	get_uid_string(curr, 0, sizeof(id_buf), id_buf);
153 	if(id_buf[0] != '\0')
154 		strcat(id_buf, ":");
155 	get_gid_string(curr, 0, sizeof(id_buf) - strlen(id_buf),
156 			id_buf + strlen(id_buf));
157 #ifndef _WIN32
158 	get_perm_string(perm_buf, sizeof(perm_buf), curr->mode);
159 #else
160 	copy_str(perm_buf, sizeof(perm_buf), attr_str_long(curr->attrs));
161 #endif
162 
163 	werase(stat_win);
164 	cur_x = 2;
165 	checked_wmove(stat_win, 0, cur_x);
166 	wprint(stat_win, name_buf);
167 	cur_x += 22;
168 	if(x > 83)
169 		cur_x += x - 83;
170 	mvwaddstr(stat_win, 0, cur_x, size_buf);
171 	cur_x += 12;
172 	mvwaddstr(stat_win, 0, cur_x, perm_buf);
173 	cur_x += 11;
174 
175 	snprintf(name_buf, sizeof(name_buf), "%d %s filtered", view->filtered,
176 			(view->filtered == 1) ? "file" : "files");
177 	if(view->filtered > 0)
178 		mvwaddstr(stat_win, 0, x - (strlen(name_buf) + 2), name_buf);
179 
180 	if(cur_x + strlen(id_buf) + 1 > x - (strlen(name_buf) + 2))
181 		break_at(id_buf, ':');
182 	if(cur_x + strlen(id_buf) + 1 > x - (strlen(name_buf) + 2))
183 		id_buf[0] = '\0';
184 	mvwaddstr(stat_win, 0, cur_x, id_buf);
185 
186 	refresh_window(stat_win, lazy_redraw);
187 }
188 
189 /* Refreshes given window, possibly lazily. */
190 static void
refresh_window(WINDOW * win,int lazily)191 refresh_window(WINDOW *win, int lazily)
192 {
193 	if(lazily)
194 	{
195 		wnoutrefresh(stat_win);
196 	}
197 	else
198 	{
199 		ui_refresh_win(stat_win);
200 	}
201 }
202 
203 /* Expands view macros to be displayed on the status line according to the
204  * format string.  Returns colored line. */
205 TSTATIC cline_t
expand_status_line_macros(view_t * view,const char format[])206 expand_status_line_macros(view_t *view, const char format[])
207 {
208 	const dir_entry_t *const curr = get_current_entry(view);
209 	if(curr == NULL || fentry_is_fake(curr))
210 	{
211 		/* Fake entries don't have valid information. */
212 		return cline_make();
213 	}
214 
215 	return parse_view_macros(view, &format, "tTfaAugsEdD-xlLSz%[]{*", 0);
216 }
217 
218 /* Expands possibly limited set of view macros.  Returns newly allocated string,
219  * which should be freed by the caller. */
220 char *
expand_view_macros(view_t * view,const char format[],const char macros[])221 expand_view_macros(view_t *view, const char format[], const char macros[])
222 {
223 	cline_t result = parse_view_macros(view, &format, macros, 0);
224 	free(result.attrs);
225 	return result.line;
226 }
227 
228 /* Expands macros in the *format string advancing the pointer as it goes.  The
229  * opt represents conditional expression state, should be zero for non-recursive
230  * calls.  Returns colored line. */
231 static cline_t
parse_view_macros(view_t * view,const char ** format,const char macros[],int opt)232 parse_view_macros(view_t *view, const char **format, const char macros[],
233 		int opt)
234 {
235 	const dir_entry_t *const curr = get_current_entry(view);
236 	cline_t result = cline_make();
237 	char c;
238 	int nexpansions = 0;
239 	int has_expander = 0;
240 
241 	if(curr == NULL)
242 	{
243 		return result;
244 	}
245 
246 	while((c = **format) != '\0')
247 	{
248 		size_t width = 0;
249 		int left_align = 0;
250 		char buf[PATH_MAX + 1];
251 		const char *const next = ++*format;
252 		int skip, ok;
253 
254 		if(c != '%' ||
255 				(!char_is_one_of(macros, *next) && !isdigit(*next) &&
256 				 (*next != '=' || has_expander)))
257 		{
258 			if(strappendch(&result.line, &result.line_len, c) != 0)
259 			{
260 				break;
261 			}
262 			continue;
263 		}
264 
265 		if(*next == '=')
266 		{
267 			(void)cline_sync(&result, 0);
268 
269 			if(strappend(&result.line, &result.line_len, "%=") != 0 ||
270 					strappendch(&result.attrs, &result.attrs_len, '=') != 0)
271 			{
272 				break;
273 			}
274 			++*format;
275 			has_expander = 1;
276 			continue;
277 		}
278 
279 		if(*next == '-')
280 		{
281 			left_align = 1;
282 			++*format;
283 		}
284 
285 		while(isdigit(**format))
286 		{
287 			width = width*10 + *(*format)++ - '0';
288 		}
289 		c = *(*format)++;
290 
291 		skip = 0;
292 		ok = 1;
293 		buf[0] = '\0';
294 		switch(c)
295 		{
296 			char path[PATH_MAX + 1];
297 			char *escaped;
298 
299 			case 'a':
300 				friendly_size_notation(get_free_space(curr_view->curr_dir), sizeof(buf),
301 						buf);
302 				break;
303 			case 't':
304 			case 'f':
305 				if(c == 't')
306 				{
307 					format_entry_name(curr, NF_FULL, sizeof(path), path);
308 				}
309 				else
310 				{
311 					get_short_path_of(view, curr, NF_FULL, 0, sizeof(path), path);
312 				}
313 				escaped = escape_unreadable(path);
314 				copy_str(buf, sizeof(buf), escaped);
315 				free(escaped);
316 				break;
317 			case 'T':
318 				if(curr->type == FT_LINK)
319 				{
320 					char full_path[PATH_MAX + 1];
321 					get_full_path_of(curr, sizeof(full_path), full_path);
322 					if(get_link_target(full_path, buf, sizeof(buf)) != 0)
323 					{
324 						copy_str(buf, sizeof(buf), "Failed to resolve link");
325 					}
326 				}
327 				break;
328 			case 'A':
329 #ifndef _WIN32
330 				get_perm_string(buf, sizeof(buf), curr->mode);
331 #else
332 				copy_str(buf, sizeof(buf), attr_str_long(curr->attrs));
333 #endif
334 				break;
335 			case 'u':
336 				get_uid_string(curr, 0, sizeof(buf), buf);
337 				break;
338 			case 'g':
339 				get_gid_string(curr, 0, sizeof(buf), buf);
340 				break;
341 			case 's':
342 				friendly_size_notation(fentry_get_size(view, curr), sizeof(buf), buf);
343 				break;
344 			case 'E':
345 				{
346 					uint64_t size = 0U;
347 
348 					typedef int (*iter_f)(view_t *view, dir_entry_t **entry);
349 					/* No current element for visual mode, since it can contain truly
350 					 * empty selection when cursor is on ../ directory. */
351 					iter_f iter = vle_mode_is(VISUAL_MODE) ? &iter_selected_entries
352 					                                       : &iter_selection_or_current;
353 
354 					dir_entry_t *entry = NULL;
355 					while(iter(view, &entry))
356 					{
357 						size += fentry_get_size(view, entry);
358 					}
359 
360 					friendly_size_notation(size, sizeof(buf), buf);
361 				}
362 				break;
363 			case 'd':
364 				{
365 					struct tm *tm_ptr = localtime(&curr->mtime);
366 					strftime(buf, sizeof(buf), cfg.time_format, tm_ptr);
367 				}
368 				break;
369 			case '-':
370 			case 'x':
371 				skip = expand_num(buf, sizeof(buf), view->filtered);
372 				break;
373 			case 'l':
374 				skip = expand_num(buf, sizeof(buf), view->list_pos + 1);
375 				break;
376 			case 'L':
377 				skip = expand_num(buf, sizeof(buf), view->list_rows + view->filtered);
378 				break;
379 			case 'S':
380 				skip = expand_num(buf, sizeof(buf), view->list_rows);
381 				break;
382 			case '%':
383 				copy_str(buf, sizeof(buf), "%");
384 				break;
385 			case 'z':
386 				copy_str(buf, sizeof(buf), get_tip());
387 				break;
388 			case 'D':
389 				if(curr_stats.number_of_windows == 1)
390 				{
391 					view_t *const other = (view == curr_view) ? other_view : curr_view;
392 					copy_str(buf, sizeof(buf), replace_home_part(other->curr_dir));
393 				}
394 				break;
395 			case '[':
396 				{
397 					cline_t opt = parse_view_macros(view, format, macros, 1);
398 					copy_str(buf, sizeof(buf), opt.line);
399 					free(opt.line);
400 
401 					cline_splice_attrs(&result, &opt);
402 					break;
403 				}
404 			case ']':
405 				if(opt)
406 				{
407 					if(nexpansions == 0)
408 					{
409 						cline_clear(&result);
410 					}
411 					cline_finish(&result);
412 					return result;
413 				}
414 
415 				LOG_INFO_MSG("Unmatched %%]");
416 				ok = 0;
417 				break;
418 			case '{':
419 				{
420 					/* Try to find matching closing bracket
421 					 * TODO: implement the way to escape it, so that the expr may contain
422 					 * closing brackets */
423 					const char *e = strchr(*format, '}');
424 					char *expr = NULL, *resstr = NULL;
425 					var_t res = var_false();
426 					ParsingErrors parsing_error;
427 
428 					/* If there's no matching closing bracket, just add the opening one
429 					 * literally */
430 					if(e == NULL)
431 					{
432 						ok = 0;
433 						break;
434 					}
435 
436 					/* Create a NULL-terminated copy of the given expr.
437 					 * TODO: we could temporarily use buf for that, to avoid extra
438 					 * allocation, but explicitly named variable reads better. */
439 					expr = calloc(e - (*format) + 1 /* NUL-term */, 1);
440 					memcpy(expr, *format, e - (*format));
441 
442 					/* Try to parse expr, and convert the res to string if succeed. */
443 					parsing_error = parse(expr, 0, &res);
444 					if(parsing_error == PE_NO_ERROR)
445 					{
446 						resstr = var_to_str(res);
447 					}
448 
449 					if(resstr != NULL)
450 					{
451 						copy_str(buf, sizeof(buf), resstr);
452 					}
453 					else
454 					{
455 						copy_str(buf, sizeof(buf), "<Invalid expr>");
456 					}
457 
458 					var_free(res);
459 					free(resstr);
460 					free(expr);
461 
462 					*format = e + 1 /* closing bracket */;
463 				}
464 				break;
465 			case '*':
466 				if(width > 9)
467 				{
468 					snprintf(buf, sizeof(buf), "%%%d*", (int)width);
469 					width = 0;
470 					break;
471 				}
472 				cline_set_attr(&result, '0' + width);
473 				width = 0;
474 				break;
475 
476 			default:
477 				LOG_INFO_MSG("Unexpected %%-sequence: %%%c", c);
478 				ok = 0;
479 				break;
480 		}
481 
482 		if(char_is_one_of("tTAugsEd", c) && fentry_is_fake(curr))
483 		{
484 			buf[0] = '\0';
485 		}
486 
487 		if(!ok)
488 		{
489 			*format = next;
490 			if(strappendch(&result.line, &result.line_len, '%') != 0)
491 			{
492 				break;
493 			}
494 			continue;
495 		}
496 
497 		check_expanded_str(buf, skip, &nexpansions);
498 		stralign(buf, width, ' ', left_align);
499 
500 		if(strappend(&result.line, &result.line_len, buf) != 0)
501 		{
502 			break;
503 		}
504 	}
505 
506 	/* Unmatched %[. */
507 	if(opt)
508 	{
509 		(void)strprepend(&result.line, &result.line_len, "%[");
510 		(void)strprepend(&result.attrs, &result.attrs_len, "  ");
511 	}
512 
513 	cline_finish(&result);
514 	return result;
515 }
516 
517 /* Prints number into the buffer.  Returns non-zero if numeric value is
518  * "empty" (zero). */
519 static int
expand_num(char buf[],size_t buf_len,int val)520 expand_num(char buf[], size_t buf_len, int val)
521 {
522 	snprintf(buf, buf_len, "%d", val);
523 	return (val == 0);
524 }
525 
526 /* Picks tip to be displayed.  Returns pointer to the tip. */
527 static const char *
get_tip(void)528 get_tip(void)
529 {
530 	enum { SECS_PER_MIN = 60 };
531 
532 	static time_t last_time;
533 	static int need_to_shuffle = 1;
534 	static unsigned int last_item = 0;
535 	static const char *tips[] = {
536 	  "Space after :command name is sometimes optional: :chmod+x",
537 	  "[count]h moves several directories up, e.g. for `cd ../..`: 2h",
538 	  ":set options without completions are completed with current value",
539 	  ":help command completes matches by substring match",
540 	  "'classify' can be used to decorate names with chars/icons",
541 	  "You can rename files recursively via `:rename!`",
542 	  "t key toggles selection of current file",
543 	  ":sync command navigates to the same location on the other panel",
544 	  "Enter key leaves visual mode preserving selection",
545 	  "Enable 'runexec' option to run executable files via l/Enter",
546 	  "Various FUSE file-systems can be used to extend functionality",
547 	  "Trailing slash in :file[x]type, :filter and = matches directories only",
548 	  ":edit forwards arguments to editor, e.g.: :edit +set\\ filetype=sh script",
549 	  "Use :nmap/:vmap/etc. for short information on available mappings",
550 	  "c key in :file menu puts command into command-line to edit before running",
551 	  ":copen reopens the last list of files found by a :grep-like command",
552 	  "Up/down arrows on command-line load history entries with identical prefix",
553 	};
554 
555 	if(need_to_shuffle)
556 	{
557 		/* Fisher-Yates ("Knuth") shuffle algorithm implementation that mildly
558 		 * considers potential biases (not important here, but why not). */
559 		unsigned int i;
560 		for(i = 0U; i < ARRAY_LEN(tips) - 1U; ++i)
561 		{
562 			const unsigned int j =
563 				i + (rand()/(RAND_MAX + 1.0))*(ARRAY_LEN(tips) - i);
564 			const char *const t = tips[i];
565 			tips[i] = tips[j];
566 			tips[j] = t;
567 		}
568 		need_to_shuffle = 0;
569 	}
570 
571 	time_t now = time(NULL);
572 	if(now > last_time + SECS_PER_MIN)
573 	{
574 		last_time = now;
575 		last_item = (last_item + 1U)%ARRAY_LEN(tips);
576 	}
577 	return tips[last_item];
578 }
579 
580 /* Examines expansion buffer to check whether expansion took place.  Updates
581  * *nexpansions accordingly. */
582 static void
check_expanded_str(const char buf[],int skip,int * nexpansions)583 check_expanded_str(const char buf[], int skip, int *nexpansions)
584 {
585 	if(buf[0] != '\0' && !skip)
586 	{
587 		++*nexpansions;
588 	}
589 }
590 
591 int
ui_stat_reposition(int statusbar_height,int force_stat_win)592 ui_stat_reposition(int statusbar_height, int force_stat_win)
593 {
594 	const int stat_line_height = (force_stat_win || cfg.display_statusline)
595 	                           ? getmaxy(stat_win)
596 	                           : 0;
597 	const int job_bar_height = ui_stat_job_bar_height();
598 	const int y = getmaxy(stdscr)
599 	            - statusbar_height
600 	            - stat_line_height
601 	            - job_bar_height;
602 
603 	mvwin(job_bar, y, 0);
604 	if(job_bar_height != 0)
605 	{
606 		wresize(job_bar, job_bar_height, getmaxx(job_bar));
607 	}
608 
609 	if(force_stat_win || cfg.display_statusline)
610 	{
611 		mvwin(stat_win, y + job_bar_height, 0);
612 		return 1;
613 	}
614 	return 0;
615 }
616 
617 void
ui_stat_refresh(void)618 ui_stat_refresh(void)
619 {
620 	ui_stat_job_bar_check_for_updates();
621 
622 	ui_refresh_win(job_bar);
623 	ui_refresh_win(stat_win);
624 }
625 
626 int
ui_stat_job_bar_height(void)627 ui_stat_job_bar_height(void)
628 {
629 	return (nbar_jobs != 0U) ? 1 : 0;
630 }
631 
632 void
ui_stat_job_bar_add(bg_op_t * bg_op)633 ui_stat_job_bar_add(bg_op_t *bg_op)
634 {
635 	const int prev_height = ui_stat_job_bar_height();
636 
637 	bg_op_t **p = reallocarray(bar_jobs, nbar_jobs + 1, sizeof(*bar_jobs));
638 	if(p == NULL)
639 	{
640 		return;
641 	}
642 	bar_jobs = p;
643 
644 	bar_jobs[nbar_jobs] = bg_op;
645 	++nbar_jobs;
646 
647 	if(!is_job_bar_visible())
648 	{
649 		return;
650 	}
651 
652 	ui_stat_job_bar_redraw();
653 
654 	if(ui_stat_job_bar_height() != prev_height)
655 	{
656 		stats_redraw_later();
657 	}
658 }
659 
660 void
ui_stat_job_bar_remove(bg_op_t * bg_op)661 ui_stat_job_bar_remove(bg_op_t *bg_op)
662 {
663 	size_t i;
664 	const int prev_height = ui_stat_job_bar_height();
665 
666 	for(i = 0U; i < nbar_jobs; ++i)
667 	{
668 		if(bar_jobs[i] == bg_op)
669 		{
670 			memmove(&bar_jobs[i], &bar_jobs[i + 1],
671 					sizeof(*bar_jobs)*(nbar_jobs - 1 - i));
672 			break;
673 		}
674 	}
675 
676 	--nbar_jobs;
677 
678 	if(ui_stat_job_bar_height() != 0)
679 	{
680 		ui_stat_job_bar_redraw();
681 	}
682 	else if(prev_height != 0)
683 	{
684 		stats_redraw_later();
685 	}
686 }
687 
688 void
ui_stat_job_bar_changed(bg_op_t * bg_op)689 ui_stat_job_bar_changed(bg_op_t *bg_op)
690 {
691 	pthread_spinlock_t *const lock = get_job_bar_changed_lock();
692 
693 	pthread_spin_lock(lock);
694 	job_bar_changed = 1;
695 	pthread_spin_unlock(lock);
696 }
697 
698 void
ui_stat_job_bar_check_for_updates(void)699 ui_stat_job_bar_check_for_updates(void)
700 {
701 	static int prev_width;
702 
703 	pthread_spinlock_t *const lock = get_job_bar_changed_lock();
704 	int job_bar_changed_value;
705 
706 	pthread_spin_lock(lock);
707 	job_bar_changed_value = job_bar_changed;
708 	job_bar_changed = 0;
709 	pthread_spin_unlock(lock);
710 
711 	if(job_bar_changed_value || getmaxx(job_bar) != prev_width)
712 	{
713 		ui_stat_job_bar_redraw();
714 	}
715 
716 	prev_width = getmaxx(job_bar);
717 }
718 
719 /* Gets spinlock for the job_bar_changed variable in thread-safe way.  Returns
720  * the lock. */
721 static pthread_spinlock_t *
get_job_bar_changed_lock(void)722 get_job_bar_changed_lock(void)
723 {
724 	static pthread_once_t once = PTHREAD_ONCE_INIT;
725 	pthread_once(&once, &init_job_bar_changed_lock);
726 	return &job_bar_changed_lock;
727 }
728 
729 /* Performs spinlock initialization at most once. */
730 static void
init_job_bar_changed_lock(void)731 init_job_bar_changed_lock(void)
732 {
733 	int ret = pthread_spin_init(&job_bar_changed_lock, PTHREAD_PROCESS_PRIVATE);
734 	assert(ret == 0 && "Failed to initialize spinlock!");
735 	(void)ret;
736 }
737 
738 /* Checks whether job bar is visible.  Returns non-zero if so, and zero
739  * otherwise. */
740 static int
is_job_bar_visible(void)741 is_job_bar_visible(void)
742 {
743 	/* Pretend that bar isn't visible in tests. */
744 	return curr_stats.load_stage >= 2
745 	    && ui_stat_job_bar_height() != 0 && !is_in_menu_like_mode();
746 }
747 
748 void
ui_stat_job_bar_redraw(void)749 ui_stat_job_bar_redraw(void)
750 {
751 	if(!is_job_bar_visible())
752 	{
753 		return;
754 	}
755 
756 	werase(job_bar);
757 	checked_wmove(job_bar, 0, 0);
758 	wprint(job_bar, format_job_bar());
759 
760 	wnoutrefresh(job_bar);
761 	/* Update status_bar after job_bar just to ensure that it owns the cursor.
762 	 * Don't know a cleaner way of doing this. */
763 	wnoutrefresh(status_bar);
764 	doupdate();
765 }
766 
767 /* Formats contents of the job bar.  Returns pointer to statically allocated
768  * storage. */
769 static const char *
format_job_bar(void)770 format_job_bar(void)
771 {
772 	enum { MAX_UTF_CHAR_LEN = 4 };
773 
774 	static char bar_text[512*MAX_UTF_CHAR_LEN + 1];
775 
776 	size_t i;
777 	size_t text_width;
778 	size_t width_used;
779 	size_t max_width;
780 	char **descrs;
781 
782 	descrs = take_job_descr_snapshot();
783 
784 	bar_text[0] = '\0';
785 	text_width = 0U;
786 
787 	/* The check of stage is for tests. */
788 	max_width = (curr_stats.load_stage < 2) ? 80 : getmaxx(job_bar);
789 	width_used = 0U;
790 	for(i = 0U; i < nbar_jobs; ++i)
791 	{
792 		const int progress = bar_jobs[i]->progress;
793 		const unsigned int reserved = (progress == -1) ? 0U : 5U;
794 		char item_text[max_width*MAX_UTF_CHAR_LEN + 1U];
795 
796 		const size_t width = (i == nbar_jobs - 1U)
797 		                   ? (max_width - width_used)
798 		                   : (max_width/nbar_jobs);
799 
800 		char *const ellipsed = left_ellipsis(descrs[i], width - 2U - reserved,
801 				curr_stats.ellipsis);
802 
803 		if(progress == -1)
804 		{
805 			snprintf(item_text, sizeof(item_text), "[%s]", ellipsed);
806 		}
807 		else
808 		{
809 			snprintf(item_text, sizeof(item_text), "[%s %3d%%]", ellipsed, progress);
810 		}
811 
812 		free(ellipsed);
813 
814 		(void)sstrappend(bar_text, &text_width, sizeof(bar_text), item_text);
815 
816 		width_used += width;
817 	}
818 
819 	free_string_array(descrs, nbar_jobs);
820 
821 	return bar_text;
822 }
823 
824 /* Makes snapshot of current job descriptions.  Returns array of length
825  * nbar_jobs which should be freed via free_string_array(). */
826 static char **
take_job_descr_snapshot(void)827 take_job_descr_snapshot(void)
828 {
829 	size_t i;
830 	char **descrs;
831 
832 	descrs = reallocarray(NULL, nbar_jobs, sizeof(*descrs));
833 	for(i = 0U; i < nbar_jobs; ++i)
834 	{
835 		const char *descr;
836 
837 		bg_op_lock(bar_jobs[i]);
838 		descr = bar_jobs[i]->descr;
839 		descrs[i] = strdup((descr == NULL) ? "UNKNOWN" : descr);
840 		bg_op_unlock(bar_jobs[i]);
841 	}
842 
843 	return descrs;
844 }
845 
846 void
ui_stat_draw_popup_line(WINDOW * win,const char item[],const char descr[],size_t max_width)847 ui_stat_draw_popup_line(WINDOW *win, const char item[], const char descr[],
848 		size_t max_width)
849 {
850 	char *left, *right, *line;
851 	const size_t text_width = utf8_strsw(item);
852 	const size_t win_width = getmaxx(win);
853 	const int align_columns = (max_width <= win_width/4);
854 	const char *const fmt = align_columns ? "%-*s  %-*s" : "%-*s  %*s";
855 	size_t width_left;
856 	size_t item_width;
857 
858 	if(text_width >= win_width)
859 	{
860 		char *const whole = right_ellipsis(item, win_width, curr_stats.ellipsis);
861 		wprint(win, whole);
862 		free(whole);
863 		return;
864 	}
865 
866 	left = right_ellipsis(item, win_width - 3, curr_stats.ellipsis);
867 
868 	item_width = align_columns ? max_width : utf8_strsw(left);
869 	width_left = win_width - 2 - MAX(item_width, utf8_strsw(left));
870 
871 	right = right_ellipsis(descr, width_left, curr_stats.ellipsis);
872 
873 	line = format_str(fmt, (int)item_width, left, (int)width_left, right);
874 	free(left);
875 	free(right);
876 
877 	wprint(win, line);
878 
879 	free(line);
880 }
881 
882 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
883 /* vim: set cinoptions+=t0 filetype=c : */
884