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