summaryrefslogtreecommitdiffstats
path: root/src/common/impl/io_unix.c
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/common/impl/io_unix.c
Add the files
Diffstat (limited to 'src/common/impl/io_unix.c')
-rw-r--r--src/common/impl/io_unix.c333
1 files changed, 333 insertions, 0 deletions
diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c
new file mode 100644
index 0000000..e603186
--- /dev/null
+++ b/src/common/impl/io_unix.c
@@ -0,0 +1,333 @@
+#include "common/io.h"
+#include "fastfetch.h"
+#include "common/strutil.h"
+#include "common/time.h"
+
+#include <fcntl.h>
+#include <termios.h>
+#include <dirent.h>
+#include <errno.h>
+#ifndef __APPLE__
+ #include <poll.h>
+#else
+ #include <sys/select.h>
+#endif
+
+#if FF_HAVE_WORDEXP
+ #include <wordexp.h>
+#else
+ #include <glob.h>
+#endif
+
+static void createSubfolders(const char* fileName) {
+ FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
+
+ const char* token = NULL;
+ while ((token = strchr(fileName, '/')) != NULL) {
+ ffStrbufAppendNS(&path, (uint32_t) (token - fileName + 1), fileName);
+ mkdir(path.chars, S_IRWXU | S_IRGRP | S_IROTH);
+ fileName = token + 1;
+ }
+}
+
+bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
+ int openFlagsModes = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
+ mode_t openFlagsRights = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
+
+ int FF_AUTO_CLOSE_FD fd = open(fileName, openFlagsModes, openFlagsRights);
+ if (fd == -1) {
+ if (errno == ENOENT) {
+ createSubfolders(fileName);
+ fd = open(fileName, openFlagsModes, openFlagsRights);
+ if (fd == -1) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return write(fd, data, dataSize) > 0;
+}
+
+static inline void readWithLength(int fd, FFstrbuf* buffer, uint32_t length) {
+ ffStrbufEnsureFixedLengthFree(buffer, length);
+ ssize_t bytesRead = 0;
+ while (
+ length > 0 && (bytesRead = read(fd, buffer->chars + buffer->length, length)) > 0) {
+ buffer->length += (uint32_t) bytesRead;
+ length -= (uint32_t) bytesRead;
+ }
+}
+
+static inline void readUntilEOF(int fd, FFstrbuf* buffer) {
+ ffStrbufEnsureFree(buffer, 31);
+ uint32_t available = ffStrbufGetFree(buffer);
+ ssize_t bytesRead = 0;
+ while (
+ (bytesRead = read(fd, buffer->chars + buffer->length, available)) > 0) {
+ buffer->length += (uint32_t) bytesRead;
+ if ((uint32_t) bytesRead == available) {
+ ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte.
+ }
+ available = ffStrbufGetFree(buffer);
+ }
+}
+
+bool ffAppendFDBuffer(int fd, FFstrbuf* buffer) {
+ struct stat fileInfo;
+ if (fstat(fd, &fileInfo) != 0) {
+ return false;
+ }
+
+ if (fileInfo.st_size > 0) {
+ readWithLength(fd, buffer, (uint32_t) fileInfo.st_size);
+ } else {
+ readUntilEOF(fd, buffer);
+ }
+
+ buffer->chars[buffer->length] = '\0';
+
+ return buffer->length > 0;
+}
+
+bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
+ bool result = false;
+
+#if FF_HAVE_WORDEXP
+
+ wordexp_t exp;
+ if (wordexp(in, &exp, 0) != 0) { // WARN: 0 = no safety flags; command substitution allowed
+ return false;
+ }
+
+ if (exp.we_wordc >= 1) {
+ result = true;
+ ffStrbufSetS(out, exp.we_wordv[exp.we_wordc > 1 ? ffTimeGetNow() % exp.we_wordc : 0]);
+ }
+
+ wordfree(&exp);
+
+#else
+
+ glob_t gb;
+ if (glob(in, GLOB_NOSORT
+ #ifdef GLOB_TILDE
+ | GLOB_TILDE
+ #endif
+ #ifdef GLOB_BRACE
+ | GLOB_BRACE
+ #endif
+ ,
+ NULL,
+ &gb) != 0)
+ return false;
+
+ if (gb.gl_pathc >= 1) {
+ result = true;
+ ffStrbufSetS(out, gb.gl_pathv[gb.gl_pathc > 1 ? ffTimeGetNow() % (unsigned) gb.gl_pathc : 0]);
+ }
+
+ globfree(&gb);
+
+#endif
+
+ return result;
+}
+
+static int ftty = -1;
+static struct termios oldTerm;
+void restoreTerm(void) {
+ tcsetattr(ftty, TCSAFLUSH, &oldTerm);
+}
+
+const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
+ if (ftty < 0) {
+ ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
+ if (ftty < 0) {
+ return "open(\"/dev/tty\", O_RDWR | O_NOCTTY | O_CLOEXEC) failed";
+ }
+
+ if (tcgetattr(ftty, &oldTerm) == -1) {
+ return "tcgetattr(STDIN_FILENO, &oldTerm) failed";
+ }
+
+ struct termios newTerm = oldTerm;
+ newTerm.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
+ if (tcsetattr(ftty, TCSAFLUSH, &newTerm) == -1) {
+ return "tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm)";
+ }
+ atexit(restoreTerm);
+ }
+
+ ffWriteFDData(ftty, strlen(request), request);
+
+// Give the terminal some time to respond
+#ifndef __APPLE__
+ if (poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS) <= 0) {
+ return "poll(/dev/tty) timeout or failed";
+ }
+#else
+ {
+ // On macOS, poll(/dev/tty) always returns immediately
+ // See also https://nathancraddock.com/blog/macos-dev-tty-polling/
+ fd_set rd;
+ FD_ZERO(&rd);
+ FD_SET(ftty, &rd);
+ if (select(ftty + 1, &rd, NULL, NULL, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }) <= 0) {
+ return "select(/dev/tty) timeout or failed";
+ }
+ }
+#endif
+
+ char buffer[1024];
+ size_t bytesRead = 0;
+
+ va_list args;
+ va_start(args, format);
+
+ while (true) {
+ ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1);
+
+ if (nRead <= 0) {
+ va_end(args);
+ return "read(STDIN_FILENO, buffer, sizeof(buffer) - 1) failed";
+ }
+
+ bytesRead += (size_t) nRead;
+ buffer[bytesRead] = '\0';
+
+ va_list cargs;
+ va_copy(cargs, args);
+ int ret = vsscanf(buffer, format, cargs);
+ va_end(cargs);
+
+ if (ret <= 0) {
+ va_end(args);
+ return "vsscanf(buffer, format, args) failed";
+ }
+ if (ret >= nParams) {
+ break;
+ }
+ }
+
+ va_end(args);
+
+ return NULL;
+}
+
+bool ffSuppressIO(bool suppress) {
+#ifndef NDEBUG
+ if (instance.config.display.debugMode) {
+ return false;
+ }
+#endif
+
+ static bool init = false;
+ static int origOut = -1;
+ static int origErr = -1;
+ static int nullFile = -1;
+
+ if (!init) {
+ if (!suppress) {
+ return true;
+ }
+
+ origOut = dup(STDOUT_FILENO);
+ origErr = dup(STDERR_FILENO);
+ nullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
+ init = true;
+ }
+
+ if (nullFile == -1) {
+ return false;
+ }
+
+ fflush(stdout);
+ fflush(stderr);
+
+ dup2(suppress ? nullFile : origOut, STDOUT_FILENO);
+ dup2(suppress ? nullFile : origErr, STDERR_FILENO);
+ return true;
+}
+
+void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) {
+ int dfd = open(folder->chars, O_RDONLY | O_CLOEXEC | O_DIRECTORY); // Ownership of dfd will be transformed to dir
+ if (dfd < 0) {
+ return;
+ }
+
+ FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd);
+ if (dir == NULL) {
+ close(dfd);
+ return; // Should not happen
+ }
+
+ uint32_t folderLength = folder->length;
+
+ if (pretty && folderName != NULL) {
+ for (uint8_t i = 0; i < indentation - 1; i++) {
+ fputs(" | ", stdout);
+ }
+ printf("%s/\n", folderName);
+ }
+
+ struct dirent* entry;
+
+ while ((entry = readdir(dir)) != NULL) {
+ if (entry->d_name[0] == '.') { // skip hidden files
+ continue;
+ }
+
+ bool isDir = false;
+#if !defined(__sun) && !defined(__HAIKU__)
+ if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK) {
+ isDir = entry->d_type == DT_DIR;
+ } else
+#endif
+ {
+ struct stat stbuf;
+ if (fstatat(dfd, entry->d_name, &stbuf, 0) < 0) {
+ isDir = false;
+ } else {
+ isDir = S_ISDIR(stbuf.st_mode);
+ }
+ }
+ if (isDir) {
+ ffStrbufAppendS(folder, entry->d_name);
+ ffStrbufAppendC(folder, '/');
+ listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry->d_name, pretty);
+ ffStrbufSubstrBefore(folder, folderLength);
+ continue;
+ }
+
+ if (pretty) {
+ for (uint8_t i = 0; i < indentation; i++) {
+ fputs(" | ", stdout);
+ }
+ } else {
+ fputs(folder->chars + baseLength, stdout);
+ }
+
+ puts(entry->d_name);
+ }
+}
+
+void ffListFilesRecursively(const char* path, bool pretty) {
+ FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path);
+ ffStrbufEnsureEndsWithC(&folder, '/');
+ listFilesRecursively(folder.length, &folder, 0, NULL, pretty);
+}
+
+FFNativeFD ffGetNullFD(void) {
+ static FFNativeFD hNullFile = -1;
+ if (hNullFile != -1) {
+ return hNullFile;
+ }
+ hNullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
+ return hNullFile;
+}
+
+bool ffRemoveFile(const char* fileName) {
+ return unlink(fileName) == 0;
+}