-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpreview.c
More file actions
349 lines (319 loc) · 11.4 KB
/
preview.c
File metadata and controls
349 lines (319 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#pragma once
/*
Disclaimer:
Most of the content in this file is "vibe coded" and probably overly complex
The initial manually written version was blocking, so it was terrible when previewing large files
Eventually this should be re-written and simplified
Here be dragons
*/
#include "color.c"
#include "global.h"
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
static pthread_t g_preview_thread;
static bool g_preview_thread_started = false;
static pthread_mutex_t g_preview_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_preview_cond = PTHREAD_COND_INITIALIZER;
static volatile pid_t g_preview_child_pid = -1;
static unsigned int g_preview_generation = 0;
static bool g_preview_has_request = false;
static int g_preview_req_offset_x = 0;
static char g_preview_req_file_name[PATH_MAX];
static void preview_stop_previous(void) {
pid_t pid = g_preview_child_pid;
if (pid > 0) {
// Atomically clear the PID to prevent double-kill
g_preview_child_pid = -1;
kill(pid, SIGTERM);
for (int i = 0; i < 50; i++) {
int status;
pid_t r = waitpid(pid, &status, WNOHANG);
if (r == pid) {
return;
}
usleep(10000);
}
kill(pid, SIGKILL);
int status;
(void)waitpid(pid, &status, 0);
}
}
static void *preview_worker(void *arg) {
(void)arg;
unsigned int my_generation = 0;
int current_offset_x = 0;
char current_file[PATH_MAX];
for (;;) {
pthread_mutex_lock(&g_preview_mutex);
while (!g_preview_has_request) {
pthread_cond_wait(&g_preview_cond, &g_preview_mutex);
}
my_generation = g_preview_generation;
current_offset_x = g_preview_req_offset_x;
strlcpy(current_file, g_preview_req_file_name, sizeof(current_file));
g_preview_has_request = false;
pthread_mutex_unlock(&g_preview_mutex);
preview_stop_previous();
int pipefd[2];
if (pipe(pipefd) == -1) {
continue;
}
pid_t pid = fork();
if (pid == -1) {
close(pipefd[0]);
close(pipefd[1]);
continue;
}
if (pid == 0) {
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[0]);
close(pipefd[1]);
char width_str[16];
char height_str[16];
snprintf(width_str, sizeof(width_str), "%d", tb_width() - current_offset_x);
snprintf(height_str, sizeof(height_str), "%d", tb_height());
execlp(CMD_PREVIEW, CMD_PREVIEW, current_file, width_str, height_str, (char *)NULL);
_exit(127);
}
close(pipefd[1]);
g_preview_child_pid = pid;
int flags = fcntl(pipefd[0], F_GETFL, 0);
fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK);
const int preview_x = current_offset_x + 3;
const int preview_width = MAX(tb_width() - preview_x, 1);
const size_t read_cap = (size_t)MAX(4096, preview_width * 4);
char *read_buf = malloc(read_cap);
if (!read_buf) {
close(pipefd[0]);
continue;
}
const size_t line_cap = (size_t)preview_width + 1;
char *line_buf = malloc(line_cap);
if (!line_buf) {
free(read_buf);
close(pipefd[0]);
continue;
}
size_t line_len = 0;
int y = 1;
bool truncated = false;
int lines_since_present = 0;
// Clear preview area once at the start (without immediate present)
pthread_mutex_lock(&g_tb_mutex);
for (int cy = 1; cy < tb_height() - 1; cy++) {
for (int cx = preview_x; cx < tb_width(); cx++) {
tb_set_cell(cx, cy, ' ', 0, 0);
}
}
pthread_mutex_unlock(&g_tb_mutex);
for (;;) {
// Check generation before any processing
pthread_mutex_lock(&g_preview_mutex);
bool should_continue = (g_preview_generation == my_generation);
pthread_mutex_unlock(&g_preview_mutex);
if (!should_continue) {
preview_stop_previous();
break;
}
bool eof = false;
struct pollfd pfd = {.fd = pipefd[0], .events = POLLIN | POLLHUP};
const int pr = poll(&pfd, 1, 50);
if (pr > 0 && (pfd.revents & (POLLIN | POLLHUP))) {
for (;;) {
ssize_t n = read(pipefd[0], read_buf, read_cap);
if (n > 0) {
for (ssize_t i = 0; i < n; i++) {
// Check generation more frequently to abort quickly
pthread_mutex_lock(&g_preview_mutex);
bool should_abort = (g_preview_generation != my_generation);
pthread_mutex_unlock(&g_preview_mutex);
if (should_abort) {
preview_stop_previous();
goto cleanup_and_exit;
}
char ch = read_buf[i];
if (ch == '\n') {
// Print the accumulated line with soft wrapping to preview_width
pthread_mutex_lock(&g_tb_mutex);
if (line_len == 0) {
// Preserve empty line: print a blank padded row
tb_printf(preview_x, y, COLOR_DEFAULT, COLOR_DEFAULT, "%-*s", preview_width, "");
if (y == tb_height() - 2) {
truncated = true;
tb_present();
pthread_mutex_unlock(&g_tb_mutex);
goto cleanup_and_exit;
}
y++;
lines_since_present++;
if (lines_since_present >= 3 || y == 2) {
tb_present();
lines_since_present = 0;
}
} else {
size_t start = 0;
while (start < line_len) {
size_t chunk = MIN((size_t)preview_width, line_len - start);
tb_printf(preview_x, y, COLOR_DEFAULT, COLOR_DEFAULT, "%-*.*s", preview_width, (int)chunk,
line_buf + start);
if (y == tb_height() - 2) {
truncated = true;
tb_present();
pthread_mutex_unlock(&g_tb_mutex);
goto cleanup_and_exit;
}
y++;
lines_since_present++;
if (lines_since_present >= 3 || y == 2) {
tb_present();
lines_since_present = 0;
}
start += chunk;
}
}
// Reset line buffer after printing newline-terminated line
line_len = 0;
pthread_mutex_unlock(&g_tb_mutex);
} else if (ch == '\t') {
// Expand tabs into spaces based on TAB_WIDTH and current column
for (;;) {
size_t col_in_line = line_len % (size_t)preview_width;
size_t spaces_to_next_tab = TAB_WIDTH - (col_in_line % TAB_WIDTH);
size_t space_chunk = MIN(spaces_to_next_tab, (size_t)preview_width - col_in_line);
// Append as many spaces as fit in this line chunk
for (size_t s = 0; s < space_chunk; s++) {
line_buf[line_len++] = ' ';
}
// If we filled the line, flush it and continue if there are remaining spaces
if (line_len >= (size_t)preview_width) {
pthread_mutex_lock(&g_tb_mutex);
tb_printf(preview_x, y, COLOR_DEFAULT, COLOR_DEFAULT, "%-*.*s", preview_width, preview_width,
line_buf);
if (y == tb_height() - 2) {
truncated = true;
tb_present();
pthread_mutex_unlock(&g_tb_mutex);
goto cleanup_and_exit;
}
y++;
lines_since_present++;
if (lines_since_present >= 3 || y == 2) {
tb_present();
lines_since_present = 0;
}
pthread_mutex_unlock(&g_tb_mutex);
line_len = 0;
// Continue loop only if we didn't finish all spaces toward the next tab stop
if (space_chunk < spaces_to_next_tab) {
continue;
}
}
break;
}
} else {
// Append character and soft-wrap on the fly when reaching width
if (line_len < (size_t)preview_width) {
line_buf[line_len++] = ch;
} else {
// Flush current full-width line chunk
pthread_mutex_lock(&g_tb_mutex);
tb_printf(preview_x, y, COLOR_DEFAULT, COLOR_DEFAULT, "%-*.*s", preview_width, preview_width,
line_buf);
if (y == tb_height() - 2) {
truncated = true;
tb_present();
pthread_mutex_unlock(&g_tb_mutex);
goto cleanup_and_exit;
}
y++;
lines_since_present++;
if (lines_since_present >= 3 || y == 2) {
tb_present();
lines_since_present = 0;
}
pthread_mutex_unlock(&g_tb_mutex);
// Start new line buffer with current char
line_len = 0;
line_buf[line_len++] = ch;
}
}
}
} else if (n == 0) {
eof = true;
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
eof = true;
break;
}
}
}
int status;
(void)waitpid(pid, &status, WNOHANG);
if (eof) {
break;
}
}
cleanup_and_exit:
// Present any remaining buffered content
if (lines_since_present > 0) {
pthread_mutex_lock(&g_tb_mutex);
tb_present();
pthread_mutex_unlock(&g_tb_mutex);
}
if (!truncated && line_len > 0) {
size_t start = 0;
pthread_mutex_lock(&g_tb_mutex);
while (start < line_len) {
size_t chunk = MIN((size_t)preview_width, line_len - start);
tb_printf(preview_x, y, COLOR_DEFAULT, COLOR_DEFAULT, "%-*.*s", preview_width, (int)chunk, line_buf + start);
if (y == tb_height() - 2) {
truncated = true;
tb_present();
pthread_mutex_unlock(&g_tb_mutex);
break;
}
y++;
}
if (!truncated) {
tb_present();
pthread_mutex_unlock(&g_tb_mutex);
}
}
if (truncated) {
// Stop child to avoid blocking on a full pipe
preview_stop_previous();
} else {
int status;
(void)waitpid(pid, &status, 0);
g_preview_child_pid = -1;
}
close(pipefd[0]);
free(read_buf);
free(line_buf);
}
preview_stop_previous();
return NULL;
}
void preview_start(const int offset_x, const char *file_name) {
pthread_mutex_lock(&g_preview_mutex);
if (!g_preview_thread_started) {
if (pthread_create(&g_preview_thread, NULL, preview_worker, NULL) == 0) {
pthread_detach(g_preview_thread);
g_preview_thread_started = true;
}
}
g_preview_req_offset_x = offset_x;
strlcpy(g_preview_req_file_name, file_name, sizeof(g_preview_req_file_name));
g_preview_has_request = true;
g_preview_generation++;
pthread_cond_signal(&g_preview_cond);
pthread_mutex_unlock(&g_preview_mutex);
}