code / bb

Lines2.7K C1.8K Shell331 YAML273 Markdown197 make44
(381 lines)
1 //
2 // draw.c - This file contains logic for drawing columns.
3 //
5 #include <limits.h>
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <sys/ioctl.h>
10 #include <sys/stat.h>
11 #include <time.h>
13 #include "draw.h"
14 #include "terminal.h"
15 #include "types.h"
16 #include "utils.h"
18 column_t column_info[255] = {
19 ['*'] = {.name = "*", .render = col_selected}, ['n'] = {.name = "Name", .render = col_name, .stretchy = 1},
20 ['s'] = {.name = " Size", .render = col_size}, ['p'] = {.name = "Perm", .render = col_perm},
21 ['m'] = {.name = " Modified", .render = col_mreltime}, ['M'] = {.name = " Modified ", .render = col_mtime},
22 ['a'] = {.name = " Accessed", .render = col_areltime}, ['A'] = {.name = " Accessed ", .render = col_atime},
23 ['c'] = {.name = " Created", .render = col_creltime}, ['C'] = {.name = " Created ", .render = col_ctime},
24 ['r'] = {.name = "Random", .render = col_random},
25 };
27 //
28 // Left-pad a string with spaces.
29 //
30 static void lpad(char *buf, int width) {
31 int len = strlen(buf);
32 if (len < width) {
33 int pad = width - len;
34 memmove(&buf[pad], buf, (size_t)len + 1);
35 while (pad > 0)
36 buf[--pad] = ' ';
40 //
41 // Append a string to an existing string, but with escape sequences made explicit.
42 //
43 static char *stpcpy_escaped(char *buf, const char *str, const char *color) {
44 static const char *escapes = " abtnvfr e";
45 for (const char *c = str; *c; ++c) {
46 if (*c > 0 && *c <= '\x1b' && escapes[(int)*c] != ' ') { // "\n", etc.
47 buf += sprintf(buf, "\033[31m\\%c%s", escapes[(int)*c], color);
48 } else if (*c >= 0 && !(' ' <= *c && *c <= '~')) { // "\x02", etc.
49 buf += sprintf(buf, "\033[31m\\x%02X%s", (unsigned int)*c, color);
50 } else {
51 *(buf++) = *c;
54 *buf = '\0';
55 return buf;
58 //
59 // Print a string, but replacing bytes like '\n' with a red-colored "\n".
60 // The color argument is what color to put back after the red.
61 // Returns the number of bytes that were escaped.
62 //
63 static int fputs_escaped(FILE *f, const char *str, const char *color) {
64 static const char *escapes = " abtnvfr e";
65 int escaped = 0;
66 for (const char *c = str; *c; ++c) {
67 if (*c > 0 && *c <= '\x1b' && escapes[(int)*c] != ' ') { // "\n", etc.
68 fprintf(f, "\033[31m\\%c%s", escapes[(int)*c], color);
69 ++escaped;
70 } else if (*c >= 0 && !(' ' <= *c && *c <= '~')) { // "\x02", etc.
71 fprintf(f, "\033[31m\\x%02X%s", (unsigned int)*c, color);
72 ++escaped;
73 } else {
74 fputc(*c, f);
77 return escaped;
80 //
81 // Return a human-readable string representing how long ago a time was.
82 //
83 static void timeago(char *buf, time_t t) {
84 const int SECOND = 1;
85 const int MINUTE = 60 * SECOND;
86 const int HOUR = 60 * MINUTE;
87 const int DAY = 24 * HOUR;
88 const int MONTH = 30 * DAY;
89 const int YEAR = 365 * DAY;
91 time_t now = time(0);
92 double delta = difftime(now, t);
94 if (delta < 1.5) sprintf(buf, "a second");
95 else if (delta < 1 * MINUTE) sprintf(buf, "%d seconds", (int)delta);
96 else if (delta < 2 * MINUTE) sprintf(buf, "a minute");
97 else if (delta < 1 * HOUR) sprintf(buf, "%d minutes", (int)delta / MINUTE);
98 else if (delta < 2 * HOUR) sprintf(buf, "an hour");
99 else if (delta < 1 * DAY) sprintf(buf, "%d hours", (int)delta / HOUR);
100 else if (delta < 2 * DAY) sprintf(buf, "yesterday");
101 else if (delta < 1 * MONTH) sprintf(buf, "%d days", (int)delta / DAY);
102 else if (delta < 2 * MONTH) sprintf(buf, "a month");
103 else if (delta < 1 * YEAR) sprintf(buf, "%d months", (int)delta / MONTH);
104 else if (delta < 2 * YEAR) sprintf(buf, "a year");
105 else sprintf(buf, "%d years", (int)delta / YEAR);
108 void col_mreltime(entry_t *entry, const char *color, char *buf, int width) {
109 (void)color;
110 timeago(buf, entry->info.st_mtime);
111 lpad(buf, width);
114 void col_areltime(entry_t *entry, const char *color, char *buf, int width) {
115 (void)color;
116 timeago(buf, entry->info.st_atime);
117 lpad(buf, width);
120 void col_creltime(entry_t *entry, const char *color, char *buf, int width) {
121 (void)color;
122 timeago(buf, entry->info.st_ctime);
123 lpad(buf, width);
126 void col_mtime(entry_t *entry, const char *color, char *buf, int width) {
127 (void)color;
128 strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_mtime)));
131 void col_atime(entry_t *entry, const char *color, char *buf, int width) {
132 (void)color;
133 strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_atime)));
136 void col_ctime(entry_t *entry, const char *color, char *buf, int width) {
137 (void)color;
138 strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_ctime)));
141 void col_selected(entry_t *entry, const char *color, char *buf, int width) {
142 (void)width;
143 buf = stpcpy(buf, IS_SELECTED(entry) ? SELECTED_INDICATOR : NOT_SELECTED_INDICATOR);
144 buf = stpcpy(buf, color);
147 void col_perm(entry_t *entry, const char *color, char *buf, int width) {
148 (void)color;
149 (void)width;
150 sprintf(buf, " %03o", entry->info.st_mode & 0777);
153 void col_random(entry_t *entry, const char *color, char *buf, int width) {
154 (void)color;
155 sprintf(buf, "%*d", width, entry->shufflepos);
158 void col_size(entry_t *entry, const char *color, char *buf, int width) {
159 (void)color;
160 (void)width;
161 int mag = 0;
162 const char *units = "BKMGTPEZY";
163 double bytes = (double)entry->info.st_size;
164 while (bytes > 1024 && units[mag + 1]) {
165 bytes /= 1024;
166 mag++;
168 // Add 1 extra digit of precision if it would be nonzero:
169 sprintf(buf, "%5.*f%c ", ((int)(bytes * 10.0 + 0.5) % 10 >= 1) ? 1 : 0, bytes, units[mag]);
172 void col_name(entry_t *entry, const char *color, char *buf, int width) {
173 (void)width;
174 if (entry->no_esc) buf = stpcpy(buf, entry->name);
175 else buf = stpcpy_escaped(buf, entry->name, color);
177 if (E_ISDIR(entry)) buf = stpcpy(buf, "/");
179 if (!entry->linkname) return;
181 buf = stpcpy(buf, "\033[2m -> \033[3m");
182 buf = stpcpy(buf, color);
183 if (entry->link_no_esc) buf = stpcpy(buf, entry->linkname);
184 else buf = stpcpy_escaped(buf, entry->linkname, color);
186 if (S_ISDIR(entry->linkedmode)) buf = stpcpy(buf, "/");
188 buf = stpcpy(buf, "\033[22;23m");
192 // Calculate the column widths.
194 int *get_column_widths(char columns[], int width) {
195 // TODO: maybe memoize
196 static int colwidths[16] = {0};
197 int space = width, nstretchy = 0;
198 for (int c = 0; columns[c]; c++) {
199 column_t col = column_info[(int)columns[c]];
200 if (!col.name) continue;
201 if (col.stretchy) {
202 ++nstretchy;
203 } else {
204 colwidths[c] = strlen(col.name) + 1;
205 space -= colwidths[c];
207 if (c > 0) --space;
209 for (int c = 0; columns[c]; c++)
210 if (column_info[(int)columns[c]].stretchy) colwidths[c] = space / nstretchy;
211 return colwidths;
215 // Draw the column header labels.
217 void draw_column_labels(FILE *out, char columns[], char *sort, int width) {
218 int *colwidths = get_column_widths(columns, width);
219 fputs("\033[0;44;30m\033[K", out);
220 int x = 0;
221 for (int c = 0; columns[c]; c++) {
222 column_t col = column_info[(int)columns[c]];
223 if (!col.name) continue;
224 const char *title = col.name;
225 move_cursor_col(out, x);
226 if (c > 0) {
227 fputs("┃\033[K", out);
228 x += 1;
230 const char *indicator = " ";
231 if (columns[c] == sort[1]) indicator = sort[0] == '-' ? RSORT_INDICATOR : SORT_INDICATOR;
232 move_cursor_col(out, x);
233 fputs(indicator, out);
234 if (title) fputs(title, out);
235 x += colwidths[c];
237 fputs(" \033[K\033[0m", out);
241 // Draw a row (one file).
243 void draw_row(FILE *out, char columns[], entry_t *entry, const char *color, int width) {
244 int *colwidths = get_column_widths(columns, width);
245 fputs(color, out);
246 int x = 0;
247 for (int c = 0; columns[c]; c++) {
248 column_t col = column_info[(int)columns[c]];
249 if (!col.name) continue;
250 move_cursor_col(out, x);
251 if (c > 0) { // Separator |
252 fprintf(out, "\033[37;2m┃\033[22m%s", color);
253 x += 1;
255 char buf[PATH_MAX * 2] = {0};
256 col.render(entry, color, buf, colwidths[c]);
257 fprintf(out, "%s\033[K", buf);
258 x += colwidths[c];
260 fputs("\033[0m", out);
264 // Draw everything to the screen.
265 // If `bb->dirty` is false, then use terminal scrolling to move the file
266 // listing around and only update the files that have changed.
268 void render(FILE *out, bb_t *bb) {
269 static int lastcursor = -1, lastscroll = -1;
270 static struct winsize oldsize = {0};
272 struct winsize winsize;
273 ioctl(STDIN_FILENO, TIOCGWINSZ, &winsize);
274 int onscreen = winsize.ws_row - 3;
276 bb->dirty |= (winsize.ws_row != oldsize.ws_row) || (winsize.ws_col != oldsize.ws_col);
277 oldsize = winsize;
279 if (!bb->dirty) {
280 // Use terminal scrolling:
281 if (lastscroll > bb->scroll) {
282 fprintf(out, "\033[3;%dr\033[%dT\033[1;%dr", winsize.ws_row - 1, lastscroll - bb->scroll, winsize.ws_row);
283 } else if (lastscroll < bb->scroll) {
284 fprintf(out, "\033[3;%dr\033[%dS\033[1;%dr", winsize.ws_row - 1, bb->scroll - lastscroll, winsize.ws_row);
288 if (bb->dirty) {
289 // Path
290 move_cursor(out, 0, 0);
291 const char *color = TITLE_COLOR;
292 fputs(color, out);
294 char *home = getenv("HOME");
295 if (home && strncmp(bb->path, home, strlen(home)) == 0) {
296 fputs("~", out);
297 fputs_escaped(out, bb->path + strlen(home), color);
298 } else {
299 fputs_escaped(out, bb->path, color);
301 fprintf(out, "\033[0;2m[%s]", bb->globpats);
302 fputs(" \033[K\033[0m", out);
304 static const char *help = "Press '?' to see key bindings ";
305 move_cursor(out, MAX(0, winsize.ws_col - (int)strlen(help)), 0);
306 fputs(help, out);
307 fputs("\033[K\033[0m", out);
309 // Columns
310 move_cursor(out, 0, 1);
311 fputs("\033[0;44;30m\033[K", out);
312 draw_column_labels(out, bb->columns, bb->sort, winsize.ws_col - 1);
315 if (bb->nfiles == 0) {
316 move_cursor(out, 0, 2);
317 fputs("\033[37;2m ...no files here... \033[0m\033[J", out);
318 } else {
319 entry_t **files = bb->files;
320 for (int i = bb->scroll; i < bb->scroll + onscreen && i < bb->nfiles; i++) {
321 if (!(bb->dirty || i == bb->cursor || i == lastcursor || i < lastscroll || i >= lastscroll + onscreen)) {
322 continue;
325 entry_t *entry = files[i];
326 const char *color = NORMAL_COLOR;
327 if (i == bb->cursor) color = CURSOR_COLOR;
328 else if (S_ISDIR(entry->info.st_mode)) color = DIR_COLOR;
329 else if (S_ISLNK(entry->info.st_mode)) color = LINK_COLOR;
330 else if (entry->info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) color = EXECUTABLE_COLOR;
332 int x = 0, y = i - bb->scroll + 2;
333 move_cursor(out, x, y);
334 draw_row(out, bb->columns, entry, color, winsize.ws_col - 1);
336 move_cursor(out, 0, MIN(bb->nfiles - bb->scroll, onscreen) + 2);
337 fputs("\033[J", out);
340 // Scrollbar:
341 if (bb->nfiles > onscreen) {
342 int height = (onscreen * onscreen + (bb->nfiles - 1)) / bb->nfiles;
343 int start = 2 + (bb->scroll * onscreen) / bb->nfiles;
344 for (int i = 2; i < 2 + onscreen; i++) {
345 move_cursor(out, winsize.ws_col - 1, i);
346 fprintf(out, "%s\033[0m", (i >= start && i < start + height) ? SCROLLBAR_FG : SCROLLBAR_BG);
350 // Bottom Line:
351 move_cursor(out, winsize.ws_col / 2, winsize.ws_row - 1);
352 fputs("\033[0m\033[K", out);
353 int x = winsize.ws_col;
354 if (bb->selected) { // Number of selected files
355 int n = 0;
356 for (entry_t *s = bb->selected; s; s = s->selected.next)
357 ++n;
358 x -= 14;
359 for (int k = n; k; k /= 10)
360 x--;
361 move_cursor(out, MAX(0, x), winsize.ws_row - 1);
362 fprintf(out, "\033[41;30m %d Selected \033[0m", n);
364 int nprocs = 0;
365 for (proc_t *p = bb->running_procs; p; p = p->running.next)
366 ++nprocs;
367 if (nprocs > 0) { // Number of suspended processes
368 x -= 13;
369 for (int k = nprocs; k; k /= 10)
370 x--;
371 move_cursor(out, MAX(0, x), winsize.ws_row - 1);
372 fprintf(out, "\033[44;30m %d Suspended \033[0m", nprocs);
374 move_cursor(out, winsize.ws_col / 2, winsize.ws_row - 1);
376 lastcursor = bb->cursor;
377 lastscroll = bb->scroll;
378 fflush(out);
379 bb->dirty = 0;
381 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0