1 /* vifm
2  * Copyright (C) 2012 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 "column_view.h"
20 
21 #include <assert.h> /* assert() */
22 #include <stddef.h> /* NULL size_t */
23 #include <stdlib.h> /* malloc() free() */
24 #include <string.h> /* memmove() memset() strcpy() strlen() */
25 
26 #include "../compat/reallocarray.h"
27 #include "../utils/macros.h"
28 #include "../utils/str.h"
29 #include "../utils/utf8.h"
30 #include "../utils/utils.h"
31 
32 /* Character used to fill gaps in lines. */
33 #define GAP_FILL_CHAR ' '
34 
35 /* Holds general column information. */
36 typedef struct
37 {
38 	int column_id;    /* Unique column id. */
39 	column_func func; /* Function, which prints column value. */
40 }
41 column_desc_t;
42 
43 /* Column information including calculated values. */
44 typedef struct
45 {
46 	column_info_t info; /* Column properties specified by the client. */
47 	size_t start;       /* Start position of the column. */
48 	size_t width;       /* Calculated width of the column. */
49 	size_t print_width; /* Print width (less or equal to the width field). */
50 	column_func func;   /* Cached column print function of column_desc_t. */
51 }
52 column_t;
53 
54 /* Column view description structure.  Typedef is in the header file. */
55 struct columns_t
56 {
57 	size_t max_width; /* Maximum width of one line of the view. */
58 	size_t count;     /* Number of columns in the list. */
59 	column_t *list;   /* Array of columns of count length. */
60 };
61 
62 static int extend_column_desc_list(void);
63 static void init_new_column_desc(column_desc_t *desc, int column_id,
64 		column_func func);
65 static int column_id_present(int column_id);
66 static int extend_column_list(columns_t *cols);
67 static void init_new_column(column_t *col, column_info_t info);
68 static void mark_for_recalculation(columns_t *cols);
69 static column_func get_column_func(int column_id);
70 static AlignType decorate_output(const column_t *col, char buf[],
71 		size_t buf_len, size_t max_line_width);
72 static size_t calculate_max_width(const column_t *col, size_t len,
73 		size_t max_line_width);
74 static size_t calculate_start_pos(const column_t *col, const char buf[],
75 		AlignType align);
76 static void fill_gap_pos(const void *data, size_t from, size_t to);
77 static size_t get_width_on_screen(const char str[]);
78 static void recalculate_if_needed(columns_t *cols, size_t max_width);
79 static void recalculate(columns_t *cols, size_t max_width);
80 static void update_widths(columns_t *cols, size_t max_width);
81 static int update_abs_and_rel_widths(columns_t *cols, size_t *max_width);
82 static void update_auto_widths(columns_t *cols, size_t auto_count,
83 		size_t max_width);
84 static void update_start_positions(columns_t *cols);
85 
86 /* Number of registered column descriptors. */
87 static size_t col_desc_count;
88 /* List of column descriptors. */
89 static column_desc_t *col_descs;
90 /* Column print function. */
91 static column_line_print_func print_func;
92 /* String to be used in place of ellipsis. */
93 static const char *ellipsis = "...";
94 
95 void
columns_set_line_print_func(column_line_print_func func)96 columns_set_line_print_func(column_line_print_func func)
97 {
98 	print_func = func;
99 }
100 
101 void
columns_set_ellipsis(const char ell[])102 columns_set_ellipsis(const char ell[])
103 {
104 	ellipsis = ell;
105 }
106 
107 int
columns_add_column_desc(int column_id,column_func func)108 columns_add_column_desc(int column_id, column_func func)
109 {
110 	if(!column_id_present(column_id) && extend_column_desc_list() == 0)
111 	{
112 		init_new_column_desc(&col_descs[col_desc_count - 1], column_id, func);
113 		return 0;
114 	}
115 	return 1;
116 }
117 
118 /* Extends column descriptors list by one element, but doesn't initialize it at
119  * all.  Returns zero on success. */
120 static int
extend_column_desc_list(void)121 extend_column_desc_list(void)
122 {
123 	column_desc_t *mem_ptr;
124 	mem_ptr = reallocarray(col_descs, col_desc_count + 1, sizeof(*mem_ptr));
125 	if(mem_ptr == NULL)
126 	{
127 		return 1;
128 	}
129 	col_descs = mem_ptr;
130 	col_desc_count++;
131 	return 0;
132 }
133 
134 /* Fills column description structure with initial values. */
135 static void
init_new_column_desc(column_desc_t * desc,int column_id,column_func func)136 init_new_column_desc(column_desc_t *desc, int column_id, column_func func)
137 {
138 	desc->column_id = column_id;
139 	desc->func = func;
140 }
141 
142 columns_t *
columns_create(void)143 columns_create(void)
144 {
145 	columns_t *const result = malloc(sizeof(*result));
146 	if(result == NULL)
147 	{
148 		return NULL;
149 	}
150 	result->count = 0;
151 	result->list = NULL;
152 	mark_for_recalculation(result);
153 	return result;
154 }
155 
156 void
columns_free(columns_t * cols)157 columns_free(columns_t *cols)
158 {
159 	if(cols != NULL)
160 	{
161 		columns_clear(cols);
162 		free(cols);
163 	}
164 }
165 
166 void
columns_clear(columns_t * cols)167 columns_clear(columns_t *cols)
168 {
169 	free(cols->list);
170 	cols->list = NULL;
171 	cols->count = 0;
172 }
173 
174 void
columns_clear_column_descs(void)175 columns_clear_column_descs(void)
176 {
177 	free(col_descs);
178 	col_descs = NULL;
179 	col_desc_count = 0;
180 }
181 
182 void
columns_add_column(columns_t * cols,column_info_t info)183 columns_add_column(columns_t *cols, column_info_t info)
184 {
185 	assert(info.text_width <= info.full_width &&
186 			"Text width should be bigger than full width.");
187 	assert(column_id_present(info.column_id) && "Unknown column id.");
188 	if(extend_column_list(cols) == 0)
189 	{
190 		init_new_column(&cols->list[cols->count - 1], info);
191 		mark_for_recalculation(cols);
192 	}
193 }
194 
195 /* Checks whether a column with such column_id is already in the list of column
196  * descriptors. Returns non-zero if yes. */
197 static int
column_id_present(int column_id)198 column_id_present(int column_id)
199 {
200 	size_t i;
201 	/* Validate column_id. */
202 	for(i = 0; i < col_desc_count; i++)
203 	{
204 		if(col_descs[i].column_id == column_id)
205 		{
206 			return 1;
207 		}
208 	}
209 	return 0;
210 }
211 
212 /* Extends columns list by one element, but doesn't initialize it at all.
213  * Returns zero on success. */
214 static int
extend_column_list(columns_t * cols)215 extend_column_list(columns_t *cols)
216 {
217 	column_t *mem_ptr;
218 	mem_ptr = reallocarray(cols->list, cols->count + 1, sizeof(*mem_ptr));
219 	if(mem_ptr == NULL)
220 	{
221 		return 1;
222 	}
223 	cols->list = mem_ptr;
224 	cols->count++;
225 	return 0;
226 }
227 
228 /* Marks columns structure as one that need to be recalculated. */
229 static void
mark_for_recalculation(columns_t * cols)230 mark_for_recalculation(columns_t *cols)
231 {
232 	cols->max_width = -1UL;
233 }
234 
235 /* Fills column structure with initial values. */
236 static void
init_new_column(column_t * col,column_info_t info)237 init_new_column(column_t *col, column_info_t info)
238 {
239 	col->info = info;
240 	col->start = -1UL;
241 	col->width = -1UL;
242 	col->print_width = -1UL;
243 	col->func = get_column_func(info.column_id);
244 }
245 
246 /* Returns a pointer to column formatting function by the column id or NULL on
247  * unknown column_id. */
248 static column_func
get_column_func(int column_id)249 get_column_func(int column_id)
250 {
251 	size_t i;
252 	for(i = 0; i < col_desc_count; i++)
253 	{
254 		column_desc_t *col_desc = &col_descs[i];
255 		if(col_desc->column_id == column_id)
256 		{
257 			return col_desc->func;
258 		}
259 	}
260 	assert(0 && "Unknown column id");
261 	return NULL;
262 }
263 
264 void
columns_format_line(columns_t * cols,const void * data,size_t max_line_width)265 columns_format_line(columns_t *cols, const void *data, size_t max_line_width)
266 {
267 	char prev_col_buf[1024 + 1];
268 	size_t prev_col_start = 0UL;
269 	prev_col_buf[0] = '\0';
270 
271 	size_t i;
272 	size_t prev_col_end = 0;
273 
274 	recalculate_if_needed(cols, max_line_width);
275 
276 	for(i = 0U; i < cols->count; ++i)
277 	{
278 		/* Use big buffer to hold whole item so there will be no issues with right
279 		 * aligned fields. */
280 		char col_buffer[sizeof(prev_col_buf)];
281 		char full_column[sizeof(prev_col_buf)];
282 		size_t cur_col_start;
283 		AlignType align;
284 		const column_t *const col = &cols->list[i];
285 
286 		col->func(col->info.column_id, data, sizeof(col_buffer), col_buffer);
287 		strcpy(full_column, col_buffer);
288 		align = decorate_output(col, col_buffer, sizeof(col_buffer),
289 				max_line_width);
290 		cur_col_start = calculate_start_pos(col, col_buffer, align);
291 
292 		/* Ensure that we are not trying to draw current column in the middle of a
293 		 * character inside previous column. */
294 		if(prev_col_end > cur_col_start)
295 		{
296 			const size_t prev_col_max_width = (cur_col_start > prev_col_start)
297 			                                ? (cur_col_start - prev_col_start)
298 			                                : 0UL;
299 			const size_t break_point = utf8_strsnlen(prev_col_buf,
300 					prev_col_max_width);
301 			prev_col_buf[break_point] = '\0';
302 			fill_gap_pos(data, prev_col_start + get_width_on_screen(prev_col_buf),
303 					cur_col_start);
304 		}
305 		else
306 		{
307 			fill_gap_pos(data, prev_col_end, cur_col_start);
308 		}
309 
310 		print_func(data, col->info.column_id, col_buffer, cur_col_start, align,
311 				full_column);
312 
313 		prev_col_end = cur_col_start + get_width_on_screen(col_buffer);
314 
315 		/* Store information about the current column for usage on the next
316 		 * iteration. */
317 		strcpy(prev_col_buf, col_buffer);
318 		prev_col_start = cur_col_start;
319 	}
320 
321 	fill_gap_pos(data, prev_col_end, max_line_width);
322 }
323 
324 /* Adds decorations like ellipsis to the output.  Returns actual align type used
325  * for the column (might not match col->info.align). */
326 static AlignType
decorate_output(const column_t * col,char buf[],size_t buf_len,size_t max_line_width)327 decorate_output(const column_t *col, char buf[], size_t buf_len,
328 		size_t max_line_width)
329 {
330 	const size_t len = get_width_on_screen(buf);
331 	const size_t max_col_width = calculate_max_width(col, len, max_line_width);
332 	const int too_long = len > max_col_width;
333 	AlignType result;
334 	const char *const ell = (col->info.cropping == CT_ELLIPSIS ? ellipsis : "");
335 	char *ellipsed;
336 
337 	if(!too_long)
338 	{
339 		return (col->info.align == AT_RIGHT ? AT_RIGHT : AT_LEFT);
340 	}
341 
342 	if(col->info.align == AT_LEFT ||
343 			(col->info.align == AT_DYN && len <= max_col_width))
344 	{
345 		ellipsed = right_ellipsis(buf, max_col_width, ell);
346 		result = AT_LEFT;
347 	}
348 	else
349 	{
350 		ellipsed = left_ellipsis(buf, max_col_width, ell);
351 		result = AT_RIGHT;
352 	}
353 
354 	copy_str(buf, buf_len, ellipsed);
355 	free(ellipsed);
356 
357 	return result;
358 }
359 
360 /* Calculates maximum width for outputting content of the col. */
361 static size_t
calculate_max_width(const column_t * col,size_t len,size_t max_line_width)362 calculate_max_width(const column_t *col, size_t len, size_t max_line_width)
363 {
364 	if(col->info.cropping == CT_NONE)
365 	{
366 		const size_t left_bound = (col->info.align == AT_LEFT) ? col->start : 0;
367 		const size_t max_col_width = max_line_width - left_bound;
368 		return MIN(len, max_col_width);
369 	}
370 	else
371 	{
372 		return col->print_width;
373 	}
374 }
375 
376 /* Calculates start position for outputting content of the col. */
377 static size_t
calculate_start_pos(const column_t * col,const char buf[],AlignType align)378 calculate_start_pos(const column_t *col, const char buf[], AlignType align)
379 {
380 	if(align == AT_LEFT)
381 	{
382 		return col->start;
383 	}
384 	else
385 	{
386 		const size_t end = col->start + col->width;
387 		const size_t len = get_width_on_screen(buf);
388 		return (end > len && align == AT_RIGHT) ? (end - len) : 0;
389 	}
390 }
391 
392 /* Prints gap filler (GAP_FILL_CHAR) in place of gaps.  Does nothing if to less
393  * or equal to from. */
394 static void
fill_gap_pos(const void * data,size_t from,size_t to)395 fill_gap_pos(const void *data, size_t from, size_t to)
396 {
397 	if(to > from)
398 	{
399 		char gap[to - from + 1];
400 		memset(gap, GAP_FILL_CHAR, to - from);
401 		gap[to - from] = '\0';
402 		print_func(data, FILL_COLUMN_ID, gap, from, AT_LEFT, gap);
403 	}
404 }
405 
406 /* Returns number of character positions allocated by the string on the
407  * screen.  On issues will try to do the best, to make visible at least
408  * something. */
409 static size_t
get_width_on_screen(const char str[])410 get_width_on_screen(const char str[])
411 {
412 	size_t length = (size_t)-1;
413 	wchar_t *const wide = to_wide(str);
414 	if(wide != NULL)
415 	{
416 		length = vifm_wcswidth(wide, (size_t)-1);
417 		free(wide);
418 	}
419 	if(length == (size_t)-1)
420 	{
421 		length = utf8_nstrlen(str);
422 	}
423 	return length;
424 }
425 
426 /* Checks if recalculation is needed and runs it if yes. */
427 static void
recalculate_if_needed(columns_t * cols,size_t max_width)428 recalculate_if_needed(columns_t *cols, size_t max_width)
429 {
430 	if(!columns_matches_width(cols, max_width))
431 	{
432 		recalculate(cols, max_width);
433 	}
434 }
435 
436 int
columns_matches_width(const columns_t * cols,size_t max_width)437 columns_matches_width(const columns_t *cols, size_t max_width)
438 {
439 	return cols->max_width == max_width;
440 }
441 
442 /* Recalculates column widths and start offsets. */
443 static void
recalculate(columns_t * cols,size_t max_width)444 recalculate(columns_t *cols, size_t max_width)
445 {
446 	update_widths(cols, max_width);
447 	update_start_positions(cols);
448 
449 	cols->max_width = max_width;
450 }
451 
452 /* Recalculates column widths. */
453 static void
update_widths(columns_t * cols,size_t max_width)454 update_widths(columns_t *cols, size_t max_width)
455 {
456 	size_t width_left = max_width;
457 	const size_t auto_count = update_abs_and_rel_widths(cols, &width_left);
458 	update_auto_widths(cols, auto_count, width_left);
459 }
460 
461 /* Recalculates widths of columns with absolute or percent widths.  Returns
462  * number of columns with auto size type. */
463 static int
update_abs_and_rel_widths(columns_t * cols,size_t * max_width)464 update_abs_and_rel_widths(columns_t *cols, size_t *max_width)
465 {
466 	size_t i;
467 	size_t auto_count = 0;
468 	size_t percent_count = 0;
469 	size_t width_left = *max_width;
470 	for(i = 0; i < cols->count; i++)
471 	{
472 		size_t effective_width;
473 		column_t *col = &cols->list[i];
474 		if(col->info.sizing == ST_ABSOLUTE)
475 		{
476 			effective_width = MIN(col->info.full_width, width_left);
477 		}
478 		else if(col->info.sizing == ST_PERCENT)
479 		{
480 			++percent_count;
481 			effective_width = MIN(col->info.full_width**max_width/100, width_left);
482 		}
483 		else
484 		{
485 			auto_count++;
486 			continue;
487 		}
488 
489 		width_left -= effective_width;
490 		col->width = effective_width;
491 		if(col->info.sizing == ST_ABSOLUTE)
492 		{
493 			col->print_width = MIN(effective_width, col->info.text_width);
494 		}
495 		else
496 		{
497 			col->print_width = col->width;
498 		}
499 	}
500 
501 	/* When there is no columns with automatic size, give unused size to last
502 	 * percent column if one exists.  This removes right "margin", which otherwise
503 	 * present. */
504 	if(auto_count == 0U && percent_count != 0U)
505 	{
506 		int i;
507 		for(i = cols->count - 1; i >= 0; --i)
508 		{
509 			column_t *col = &cols->list[i];
510 			if(col->info.sizing == ST_PERCENT)
511 			{
512 				col->width += width_left;
513 				col->print_width += width_left;
514 				width_left = 0U;
515 			}
516 		}
517 	}
518 
519 	*max_width = width_left;
520 	return auto_count;
521 }
522 
523 /* Recalculates widths of columns with automatic widths. */
524 static void
update_auto_widths(columns_t * cols,size_t auto_count,size_t max_width)525 update_auto_widths(columns_t *cols, size_t auto_count, size_t max_width)
526 {
527 	size_t i;
528 	size_t auto_left = auto_count;
529 	size_t left = max_width;
530 	for(i = 0; i < cols->count; i++)
531 	{
532 		column_t *col = &cols->list[i];
533 		if(col->info.sizing == ST_AUTO)
534 		{
535 			auto_left--;
536 
537 			col->width = (auto_left > 0) ? max_width/auto_count : left;
538 			col->print_width = col->width;
539 
540 			left -= col->width;
541 		}
542 	}
543 }
544 
545 /* Should be used after updating column widths. */
546 static void
update_start_positions(columns_t * cols)547 update_start_positions(columns_t *cols)
548 {
549 	size_t i;
550 	for(i = 0; i < cols->count; i++)
551 	{
552 		if(i == 0)
553 		{
554 			cols->list[i].start = 0;
555 		}
556 		else
557 		{
558 			column_t *prev_col = &cols->list[i - 1];
559 			cols->list[i].start = prev_col->start + prev_col->width;
560 		}
561 	}
562 }
563 
564 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
565 /* vim: set cinoptions+=t0 filetype=c : */
566