ohmu
·
2026-10-15
1#include "common/io.h"
2#include "fastfetch.h"
3#include "common/debug.h"
4#include "common/strutil.h"
5#include "common/time.h"
6
7#include <fcntl.h>
8#include <termios.h>
9#include <dirent.h>
10#include <errno.h>
11#ifndef __APPLE__
12 #include <poll.h>
13#else
14 #include <sys/select.h>
15 #define st_mtim st_mtimespec // `struct stat` spells it `st_mtimespec` on Apple
16#endif
17
18#if FF_HAVE_WORDEXP
19 #include <wordexp.h>
20#elif FF_HAVE_GLOB
21 #include <glob.h>
22#endif
23
24static void createSubfolders(const char* fileName) {
25 FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
26
27 const char* token = nullptr;
28 while ((token = strchr(fileName, '/')) != nullptr) {
29 ffStrbufAppendNS(&path, (uint32_t) (token - fileName + 1), fileName);
30 mkdir(path.chars, S_IRWXU | S_IRGRP | S_IROTH);
31 fileName = token + 1;
32 }
33}
34
35bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
36 int openFlagsModes = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
37 mode_t openFlagsRights = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
38
39 FF_AUTO_CLOSE_FD int fd = open(fileName, openFlagsModes, openFlagsRights);
40 if (fd == -1) {
41 if (errno == ENOENT) {
42 createSubfolders(fileName);
43 fd = open(fileName, openFlagsModes, openFlagsRights);
44 if (fd == -1) {
45 FF_DEBUG("open(%s) failed after creating the parent folders: %s", fileName, strerror(errno));
46 return false;
47 }
48 } else {
49 FF_DEBUG("open(%s) failed: %s", fileName, strerror(errno));
50 return false;
51 }
52 }
53
54 return write(fd, data, dataSize) > 0;
55}
56
57static inline void readWithLength(int fd, FFstrbuf* buffer, uint32_t length) {
58 ffStrbufEnsureFixedLengthFree(buffer, length);
59 ssize_t bytesRead = 0;
60 while (
61 length > 0 && (bytesRead = read(fd, buffer->chars + buffer->length, length)) > 0) {
62 buffer->length += (uint32_t) bytesRead;
63 length -= (uint32_t) bytesRead;
64 }
65}
66
67static inline void readUntilEOF(int fd, FFstrbuf* buffer) {
68 ffStrbufEnsureFree(buffer, 31);
69 uint32_t available = ffStrbufGetFree(buffer);
70 ssize_t bytesRead = 0;
71 while (
72 (bytesRead = read(fd, buffer->chars + buffer->length, available)) > 0) {
73 buffer->length += (uint32_t) bytesRead;
74 if ((uint32_t) bytesRead == available) {
75 ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte.
76 }
77 available = ffStrbufGetFree(buffer);
78 }
79}
80
81bool ffAppendFDBuffer(int fd, FFstrbuf* buffer) {
82 struct stat fileInfo;
83 if (fstat(fd, &fileInfo) != 0) {
84 return false;
85 }
86
87 if (fileInfo.st_size > 0) {
88 readWithLength(fd, buffer, (uint32_t) fileInfo.st_size);
89 } else {
90 readUntilEOF(fd, buffer);
91 }
92
93 buffer->chars[buffer->length] = '\0';
94
95 return buffer->length > 0;
96}
97
98bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
99 bool result = false;
100
101#if FF_HAVE_WORDEXP
102
103 wordexp_t exp;
104 if (wordexp(in, &exp, 0) != 0) { // WARN: 0 = no safety flags; command substitution allowed
105 return false;
106 }
107
108 if (exp.we_wordc >= 1) {
109 result = true;
110 ffStrbufSetS(out, exp.we_wordv[exp.we_wordc > 1 ? ffTimeGetNow() % exp.we_wordc : 0]);
111 }
112
113 wordfree(&exp);
114
115#elif FF_HAVE_GLOB
116
117 glob_t gb;
118 if (glob(in, GLOB_NOSORT
119 #ifdef GLOB_TILDE
120 | GLOB_TILDE
121 #endif
122 #ifdef GLOB_BRACE
123 | GLOB_BRACE
124 #endif
125 ,
126 nullptr,
127 &gb) != 0)
128 return false;
129
130 if (gb.gl_pathc >= 1) {
131 result = true;
132 ffStrbufSetS(out, gb.gl_pathv[gb.gl_pathc > 1 ? ffTimeGetNow() % (unsigned) gb.gl_pathc : 0]);
133 }
134
135 globfree(&gb);
136
137#else
138
139 FF_UNUSED(in, out)
140
141#endif
142
143 return result;
144}
145
146static int ftty = -1;
147static struct termios oldTerm;
148void restoreTerm(void) {
149 tcsetattr(ftty, TCSAFLUSH, &oldTerm);
150}
151
152const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
153 if (ftty < 0) {
154 ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
155 if (ftty < 0) {
156 FF_DEBUG("open(/dev/tty) failed: %s", strerror(errno));
157 return "open(\"/dev/tty\", O_RDWR | O_NOCTTY | O_CLOEXEC) failed";
158 }
159
160 if (tcgetattr(ftty, &oldTerm) == -1) {
161 FF_DEBUG("tcgetattr(/dev/tty) failed: %s", strerror(errno));
162 return "tcgetattr(STDIN_FILENO, &oldTerm) failed";
163 }
164
165 struct termios newTerm = oldTerm;
166 newTerm.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
167 if (tcsetattr(ftty, TCSAFLUSH, &newTerm) == -1) {
168 FF_DEBUG("tcsetattr(/dev/tty) failed: %s", strerror(errno));
169 return "tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm)";
170 }
171 atexit(restoreTerm);
172 }
173
174 ffWriteFDData(ftty, strlen(request), request);
175
176// Give the terminal some time to respond
177#ifndef __APPLE__
178 const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS);
179 if (pollResult <= 0) {
180 FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s",
181 pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno));
182 return "poll(/dev/tty) timeout or failed";
183 }
184#else
185 {
186 // On macOS, poll(/dev/tty) always returns immediately
187 // See also https://nathancraddock.com/blog/macos-dev-tty-polling/
188 fd_set rd;
189 FD_ZERO(&rd);
190 FD_SET(ftty, &rd);
191 const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 });
192 if (selectResult <= 0) {
193 FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s",
194 selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno));
195 return "select(/dev/tty) timeout or failed";
196 }
197 }
198#endif
199
200 char buffer[1024];
201 size_t bytesRead = 0;
202
203 va_list args;
204 va_start(args, format);
205
206 while (true) {
207 ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1);
208
209 if (nRead <= 0) {
210 FF_DEBUG("read(/dev/tty) returned %zd after %zu bytes: %s",
211 nRead, bytesRead, nRead == 0 ? "the terminal closed the connection" : strerror(errno));
212 va_end(args);
213 return "read(STDIN_FILENO, buffer, sizeof(buffer) - 1) failed";
214 }
215
216 bytesRead += (size_t) nRead;
217 buffer[bytesRead] = '\0';
218
219 va_list cargs;
220 va_copy(cargs, args);
221 int ret = vsscanf(buffer, format, cargs);
222 va_end(cargs);
223
224 if (ret <= 0) {
225 FF_DEBUG("vsscanf(\"%s\") matched %d of %d parameters", buffer, ret, nParams);
226 va_end(args);
227 return "vsscanf(buffer, format, args) failed";
228 }
229 if (ret >= nParams) {
230 break;
231 }
232 }
233
234 va_end(args);
235
236 return nullptr;
237}
238
239bool ffSuppressIO(bool suppress) {
240#ifndef NDEBUG
241 if (instance.config.display.debugMode) {
242 return false;
243 }
244#endif
245
246 static bool init = false;
247 static int origOut = -1;
248 static int origErr = -1;
249 static int nullFile = -1;
250
251 if (!init) {
252 if (!suppress) {
253 return true;
254 }
255
256 origOut = dup(STDOUT_FILENO);
257 origErr = dup(STDERR_FILENO);
258 nullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
259 init = true;
260 }
261
262 if (nullFile == -1) {
263 return false;
264 }
265
266 fflush(stdout);
267 fflush(stderr);
268
269 dup2(suppress ? nullFile : origOut, STDOUT_FILENO);
270 dup2(suppress ? nullFile : origErr, STDERR_FILENO);
271 return true;
272}
273
274void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) {
275 int dfd = open(folder->chars, O_RDONLY | O_CLOEXEC | O_DIRECTORY); // Ownership of dfd will be transformed to dir
276 if (dfd < 0) {
277 return;
278 }
279
280 FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd);
281 if (dir == nullptr) {
282 close(dfd);
283 return; // Should not happen
284 }
285
286 uint32_t folderLength = folder->length;
287
288 if (pretty && folderName != nullptr) {
289 for (uint8_t i = 0; i < indentation - 1; i++) {
290 fputs(" | ", stdout);
291 }
292 printf("%s/\n", folderName);
293 }
294
295 struct dirent* entry;
296
297 while ((entry = readdir(dir)) != nullptr) {
298 if (entry->d_name[0] == '.') { // skip hidden files
299 continue;
300 }
301
302 bool isDir = false;
303#if !defined(__sun) && !defined(__HAIKU__)
304 if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK) {
305 isDir = entry->d_type == DT_DIR;
306 } else
307#endif
308 {
309 struct stat stbuf;
310 if (fstatat(dfd, entry->d_name, &stbuf, 0) < 0) {
311 isDir = false;
312 } else {
313 isDir = S_ISDIR(stbuf.st_mode);
314 }
315 }
316 if (isDir) {
317 ffStrbufAppendS(folder, entry->d_name);
318 ffStrbufAppendC(folder, '/');
319 listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry->d_name, pretty);
320 ffStrbufSubstrBefore(folder, folderLength);
321 continue;
322 }
323
324 if (pretty) {
325 for (uint8_t i = 0; i < indentation; i++) {
326 fputs(" | ", stdout);
327 }
328 } else {
329 fputs(folder->chars + baseLength, stdout);
330 }
331
332 puts(entry->d_name);
333 }
334}
335
336void ffListFilesRecursively(const char* path, bool pretty) {
337 FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path);
338 ffStrbufEnsureEndsWithC(&folder, '/');
339 listFilesRecursively(folder.length, &folder, 0, nullptr, pretty);
340}
341
342FFNativeFD ffGetNullFD(void) {
343 static FFNativeFD hNullFile = -1;
344 if (hNullFile != -1) {
345 return hNullFile;
346 }
347 hNullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
348 return hNullFile;
349}
350
351bool ffIsTerminal(int fd) {
352 return isatty(fd) != 0;
353}
354
355bool ffRemoveFile(const char* fileName) {
356 return unlink(fileName) == 0;
357}
358
359uint64_t ffPathGetMtime(const char* path) {
360 struct stat st;
361 if (stat(path, &st) != 0) {
362 return 0;
363 }
364
365 // An mtime at or before the Unix epoch means the filesystem did not fill it in. Reporting it
366 // as-is would hand the caller a value indistinguishable from "unknown", so treat it as such.
367 if (st.st_mtim.tv_sec <= 0) {
368 return 0;
369 }
370
371 return (uint64_t) st.st_mtim.tv_sec * 1000ull + (uint64_t) st.st_mtim.tv_nsec / 1000000ull;
372}