master ohmu/fastfetch-openbsd / src / common / impl / io_unix.c
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}