fe

terminal file explorer and picker

git clone https://9o.is/git/fe.git

commit 8a09021efed59ad30725c012c26d2557c4f5bb60
Author: Jul <jul@9o.is>
Date:   Thu,  7 Aug 2025 07:36:40 -0400

init

Diffstat:
A.gitignore | 4++++
AMakefile | 43+++++++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 26++++++++++++++++++++++++++
Aentries.c | 331+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aentries.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Afe.c | 37+++++++++++++++++++++++++++++++++++++
Aoptions.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aoptions.h | 25+++++++++++++++++++++++++
Aspawn.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspawn.h | 2++
Atty.c | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atty.h | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atty_interface.c | 344+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atty_interface.h | 28++++++++++++++++++++++++++++
14 files changed, 1344 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +*.d +*.o +config.h +fe diff --git a/Makefile b/Makefile @@ -0,0 +1,43 @@ +VERSION=1.1 + +CPPFLAGS=-DVERSION=\"${VERSION}\" -D_GNU_SOURCE +CFLAGS+=-MD -Wall -Wextra -g -std=c99 -O3 -pedantic -Werror=vla +PREFIX?=/usr/local +MANDIR?=$(PREFIX)/share/man +BINDIR?=$(PREFIX)/bin +DEBUGGER?= + +INSTALL=install +INSTALL_PROGRAM=$(INSTALL) +INSTALL_DATA=${INSTALL} -m 644 + +OBJECTS=fe.o entries.o options.o spawn.o tty_interface.o tty.o + +all: fe + +fe: $(OBJECTS) + $(CC) $(CFLAGS) $(CCFLAGS) -o $@ $(OBJECTS) + +%.o: %.c config.h + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< + +config.h: config.def.h + cp config.def.h config.h + +install: fe + mkdir -p $(DESTDIR)$(BINDIR) + cp fe $(DESTDIR)$(BINDIR)/ + chmod 755 ${DESTDIR}${BINDIR}/fe + mkdir -p $(DESTDIR)$(MANDIR)/man1 + cp fe.1 $(DESTDIR)$(MANDIR)/man1/ + chmod 644 ${DESTDIR}${MANDIR}/man1/fe.1 + +clean: + rm -f fe *.o *.d + +veryclean: clean + rm -f config.h + +.PHONY: test check all clean veryclean install fmt acceptance + +-include $(OBJECTS:.o=.d) diff --git a/config.def.h b/config.def.h @@ -0,0 +1,26 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#include "tty.h" + +#define COLOR_DIRECTORY TTY_COLOR_BLUE +#define COLOR_LINK TTY_COLOR_CYAN +#define COLOR_SOCK TTY_COLOR_YELLOW +#define COLOR_FIFO TTY_COLOR_MAGENTA +#define COLOR_EXEC TTY_COLOR_GREEN +#define COLOR_REGULAR TTY_COLOR_NORMAL + +#define DEFAULT_TTY "/dev/tty" +#define DEFAULT_NUM_FILES 999 +#define KEYTIMEOUT 25 + +#define SORT_DIR 1; +#define SORT_ICASE 1; +#define SORT_MTIME 0; +#define SHOW_HIDDEN 1; + +#ifdef __cplusplus +} +#endif + diff --git a/entries.c b/entries.c @@ -0,0 +1,331 @@ +#include <sys/stat.h> +#include <sys/types.h> + +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <libgen.h> +#include <limits.h> +#include <locale.h> +#include <regex.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "entries.h" +#include "config.h" + +static int sort_dir; +static int sort_icase; +static int sort_mtime; +static int show_hidden; + +void *xrealloc(void *p, size_t size) { + p = realloc(p, size); + if (p == NULL) + exit(1); + return p; +} + +char *xdirname(const char *path) { + static char out[PATH_MAX]; + char tmp[PATH_MAX], *p; + + strlcpy(tmp, path, sizeof(tmp)); + p = dirname(tmp); + if (p == NULL) + exit(1); + strlcpy(out, p, sizeof(out)); + return out; +} + +int dircmp(mode_t a, mode_t b) { + if (S_ISDIR(a) && S_ISDIR(b)) + return 0; + if (!S_ISDIR(a) && !S_ISDIR(b)) + return 0; + if (S_ISDIR(a)) + return -1; + else + return 1; +} + +int entrycmp(const void *va, const void *vb) { + const struct entry *a = va, *b = vb; + + if (sort_dir && dircmp(a->mode, b->mode) != 0) + return dircmp(a->mode, b->mode); + if (sort_mtime) + return b->t - a->t; + if (sort_icase) + return strcasecmp(a->name, b->name); + return strcmp(a->name, b->name); +} + +int canopendir(char *path) { + DIR *dirp; + + dirp = opendir(path); + if (dirp == NULL) { + printf("Error opening directory '%s': %s\n", path, strerror(errno)); + return 0; + } + closedir(dirp); + return 1; +} + +char *mkpath(char *dir, char *name, char *out, size_t n) { + if (name[0] == '/') { + strlcpy(out, name, n); + } else { + if (strcmp(dir, "/") == 0) { + strlcpy(out, "/", n); + strlcat(out, name, n); + } else { + strlcpy(out, dir, n); + strlcat(out, "/", n); + strlcat(out, name, n); + } + } + return out; +} + +int dentfill(char *path, struct entry **dents) { + char newpath[PATH_MAX]; + DIR *dirp; + struct dirent *dp; + struct stat sb; + int r, n = 0; + + dirp = opendir(path); + if (dirp == NULL) + return 0; + + while ((dp = readdir(dirp)) != NULL) { + if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) + continue; + if (!show_hidden && dp->d_name[0] == '.') + continue; + *dents = xrealloc(*dents, (n + 1) * sizeof(**dents)); + strlcpy((*dents)[n].name, dp->d_name, sizeof((*dents)[n].name)); + mkpath(path, dp->d_name, newpath, sizeof(newpath)); + r = lstat(newpath, &sb); + if (r == -1) + exit(1); + + (*dents)[n].mode = sb.st_mode; + (*dents)[n].t = sb.st_mtime; + + if (S_ISDIR(sb.st_mode)) { + (*dents)[n].color = COLOR_DIRECTORY; + (*dents)[n].cm = '/'; + } else if (S_ISSOCK(sb.st_mode)) { + (*dents)[n].color = COLOR_SOCK; + (*dents)[n].cm = '='; + } else if (S_ISFIFO(sb.st_mode)) { + (*dents)[n].color = COLOR_FIFO; + (*dents)[n].cm = '|'; + } else if (S_ISLNK(sb.st_mode)) { + (*dents)[n].color = COLOR_LINK; + (*dents)[n].cm = '@'; + } else if (sb.st_mode & S_IXUSR) { + (*dents)[n].color = COLOR_EXEC; + (*dents)[n].cm = '*'; + } else { + (*dents)[n].color = COLOR_REGULAR; + (*dents)[n].cm = '\0'; + } + + n++; + } + + r = closedir(dirp); + if (r == -1) + exit(1); + return n; +} + +int dentfind(struct entry *dents, int n, char *cwd, char *path) { + char tmp[PATH_MAX]; + int i; + + if (path == NULL) + return 0; + for (i = 0; i < n; i++) { + mkpath(cwd, dents[i].name, tmp, sizeof(tmp)); + if (strcmp(tmp, path) == 0) + return i; + } + return 0; +} + +int filetype(char *path) { + int fd = open(path, O_RDONLY | O_NONBLOCK); + if (fd == -1) return 0; + + struct stat sb; + int r = fstat(fd, &sb); + + close(fd); + if (r == -1) return 0; + + return sb.st_mode & S_IFMT; +} + +int set_directory(entries_t *entries, char *path) { + if (canopendir(path) == 0) + return -1; + + char oldpath[PATH_MAX]; + + free(entries->dents); + entries->dents = NULL; + + strlcpy(oldpath, entries->path, sizeof(oldpath)); + strlcpy(entries->path, path, sizeof(entries->path)); + entries->size = dentfill(entries->path, &entries->dents); + + if (entries->size > 0) { + qsort(entries->dents, entries->size, sizeof(*entries->dents), entrycmp); + entries->selection = dentfind(entries->dents, entries->size, entries->path, oldpath); + } + + return 0; +} + +void set_path(entries_t *entries, char *path) { + if (filetype(path) == S_IFDIR) { + set_directory(entries, path); + } else { + char *dir = xdirname(path); + + if (filetype(dir) == 0) { + perror("Invalid argument"); + exit(1); + } + + strlcpy(entries->path, path, sizeof(entries->path)); + set_directory(entries, dir); + } +} + +void truncate_at_newline(char *buffer) { + char *newline_pos = strchr(buffer, '\n'); + if (newline_pos != NULL) { + *newline_pos = '\0'; + } +} + +void entries_init(entries_t *entries, options_t *options) { + strcpy(entries->path, ""); + entries->dents = NULL; + entries->size = 0; + entries->selection = 0; + + sort_dir = options->sort_dir; + sort_mtime = options->sort_mtime; + sort_icase = options->sort_icase; + show_hidden = options->show_hidden; +} + +void entries_init_path(entries_t *entries, const char *path) { + char p[PATH_MAX]; + if (path != NULL) { + strlcpy(p, path, sizeof(p)); + } else if (getcwd(p, sizeof(p)) == NULL) { + perror("Failed to get current working directory"); + exit(1); + } + + set_path(entries, p); +} + +void entries_init_stdinpath(entries_t *entries) { + int c; + char p[PATH_MAX]; + size_t i = 0; + + while ((c = fgetc(stdin)) != EOF && i < PATH_MAX - 1) { + if (c == '\n') { + break; + } + p[i++] = c; + } + + p[i] = '\0'; + set_path(entries, p); +} + +void entries_destroy(entries_t *entries) { + free(entries->dents); + + entries->dents = NULL; + entries->size = 0; + entries->selection = 0; +} + +void entries_parent(entries_t *entries) { + if (strcmp(entries->path, "/") == 0 || + strcmp(entries->path, ".") == 0 || + strchr(entries->path, '/') == NULL) { + return; + } + char *newpath = xdirname(entries->path); + set_directory(entries, newpath); +} + +void entries_setpath(entries_t *entries, char *path) { + if (!path || !path[0]) return; + truncate_at_newline(path); + set_path(entries, path); +} + +int entries_select(entries_t *entries) { + char newpath[PATH_MAX]; + + if (entries->size == 0) + return 0; + + mkpath(entries->path, entries->dents[entries->selection].name, newpath, sizeof(newpath)); + + switch (filetype(newpath)) { + case S_IFREG: return 1; + case S_IFDIR: return set_directory(entries, newpath); + } + + return 0; +} + +void entries_position(entries_t *entries, size_t position) { + if (entries->size > 0 && position < entries->size) + entries->selection = position; +} + +void entries_prev(entries_t *entries) { + if (entries->size > 0) + entries->selection = (entries->selection + entries->size - 1) % entries->size; +} + +void entries_next(entries_t *entries) { + if (entries->size > 0) + entries->selection = (entries->selection + 1) % entries->size; +} + +void entries_togglehidden() { + show_hidden = show_hidden == 0 ? 1 : 0; +} + +struct entry *entries_item(entries_t *entries, size_t n) { + if (n < entries->size) { + return &entries->dents[n]; + } else { + return NULL; + } +} + +struct entry *entries_selected(entries_t *entries) { + return entries_item(entries, entries->selection); +} diff --git a/entries.h b/entries.h @@ -0,0 +1,46 @@ +#ifndef ENTRIES_H +#define ENTRIES_H ENTRIES_H + +#include <dirent.h> +#include "options.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct entry { + char name[PATH_MAX]; + mode_t mode; + time_t t; + unsigned int color; + char cm; +}; + +typedef struct { + char path[PATH_MAX]; + struct entry *dents; + size_t size; + size_t selection; +} entries_t; + +void entries_init(entries_t *entries, options_t *options); +void entries_init_path(entries_t *entries, const char *path); +void entries_init_stdinpath(entries_t *entries); +void entries_destroy(entries_t *entries); +void entries_parent(entries_t *entries); +void entries_prev(entries_t *entries); +void entries_next(entries_t *entries); +void entries_position(entries_t *entries, size_t position); +void entries_setpath(entries_t *entries, char *path); +void entries_togglehidden(); +int entries_select(entries_t *entries); +struct entry *entries_item(entries_t *entries, size_t n); +struct entry *entries_selected(entries_t *entries); + +#ifdef __cplusplus +} +#endif + +#endif + + diff --git a/fe.c b/fe.c @@ -0,0 +1,37 @@ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> +#include <limits.h> +#include <unistd.h> + +#include "tty.h" +#include "entries.h" +#include "options.h" +#include "tty_interface.h" + +#include "config.h" + +int main(int argc, char *argv[]) { + options_t options; + options_parse(&options, argc, argv); + + entries_t entries; + entries_init(&entries, &options); + + if (!isatty(STDIN_FILENO)) + entries_init_stdinpath(&entries); + else + entries_init_path(&entries, options.path); + + tty_t tty; + tty_init(&tty, options.tty_filename); + + tty_interface_t tty_interface; + tty_interface_init(&tty_interface, &tty, &entries, &options); + int ret = tty_interface_run(&tty_interface); + + entries_destroy(&entries); + return ret; +} + diff --git a/options.c b/options.c @@ -0,0 +1,96 @@ +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#include "options.h" +#include "config.h" + +static const char *usage_str = + "" + "Usage: fe [OPTION] [path]\n" + " -n, --num-files=LINES Specify how many files to display\n" + " -t, --tty=TTY Specify file to use as TTY device (default /dev/tty)\n" + " -d, --sort-directory Sort by directories first\n" + " -m, --sort-mtime Sort by time modified\n" + " -i, --sort-icase Sort case insensitively\n" + " -a, --all Show all hidden files\n" + " -h, --help Display this help and exit\n" + " -v, --version Output version information and exit\n"; + +static void usage(const char *argv0) { + fprintf(stderr, usage_str, argv0); +} + +static struct option longopts[] = { + {"num-files", required_argument, NULL, 'l'}, + {"tty", required_argument, NULL, 't'}, + {"sort-directory", no_argument, NULL, 'd'}, + {"sort-mtime", no_argument, NULL, 'm'}, + {"sort-icase", no_argument, NULL, 'i'}, + {"all", no_argument, NULL, 'a'}, + {"version", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} +}; + +void options_parse(options_t *options, int argc, char *argv[]) { + options->tty_filename = DEFAULT_TTY; + options->num_files = DEFAULT_NUM_FILES; + options->sort_dir = SORT_DIR; + options->sort_mtime = SORT_MTIME; + options->sort_icase = SORT_ICASE; + options->show_hidden = SHOW_HIDDEN; + options->path = NULL; + + int c; + while ((c = getopt_long(argc, argv, "vhadmi:n:t:", longopts, NULL)) != -1) { + switch (c) { + case 't': + options->tty_filename = optarg; + break; + case 'n': { + int n; + if (!strcmp(optarg, "max")) { + n = INT_MAX; + } else if (sscanf(optarg, "%d", &n) != 1 || n < 3) { + fprintf(stderr, "Invalid format for --num-files: %s\n", optarg); + fprintf(stderr, "Must be integer in range 3..\n"); + usage(argv[0]); + exit(EXIT_FAILURE); + } + options->num_files = n; + } break; + case 'd': + options->sort_dir = 1; + break; + case 'm': + options->sort_mtime = 1; + break; + case 'i': + options->sort_icase = 1; + break; + case 'a': + options->show_hidden = 1; + break; + case 'v': + printf("%s " VERSION " © 2025 Jul\n", argv[0]); + exit(EXIT_SUCCESS); + case 'h': + default: + usage(argv[0]); + exit(EXIT_SUCCESS); + } + } + + if (argc - optind > 1) { + usage(argv[0]); + exit(EXIT_FAILURE); + } + + if (optind < argc) { + options->path = argv[optind]; + } +} + diff --git a/options.h b/options.h @@ -0,0 +1,25 @@ +#ifndef OPTIONS_H +#define OPTIONS_H OPTIONS_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + unsigned int num_files; + const char *tty_filename; + const char *path; + int sort_dir; + int sort_icase; + int sort_mtime; + int show_hidden; +} options_t; + +void options_init(options_t *options); +void options_parse(options_t *options, int argc, char *argv[]); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/spawn.c b/spawn.c @@ -0,0 +1,82 @@ +#include <sys/types.h> +#include <sys/wait.h> + +#include <errno.h> +#include <stdarg.h> +#include <stdlib.h> +#include <unistd.h> +#include <stdio.h> +#include <string.h> + +int execute_command(const char *command) { + pid_t pid; + + pid = fork(); + if (pid == -1) { + perror("fork"); + return -1; + } + + if (pid == 0) { + char *command_copy = strdup(command); + char *const argv[] = {"sh", "-c", command_copy, NULL}; + + execvp(argv[0], argv); + + perror("execvp"); + free(command_copy); + exit(EXIT_FAILURE); + } else { + wait(NULL); + } + + return 0; +} + +int execute_command_output(const char *command, char *output_buffer, size_t buffer_size) { + int pipefd[2]; + pid_t pid; + + if (pipe(pipefd) == -1) { + perror("pipe"); + return -1; + } + + pid = fork(); + if (pid == -1) { + perror("fork"); + return -1; + } + + if (pid == 0) { + close(pipefd[0]); + + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + + char *command_copy = strdup(command); + char *const argv[] = {"sh", "-c", command_copy, NULL}; + + execvp(argv[0], argv); + + free(command_copy); + perror("execvp"); + exit(EXIT_FAILURE); + } else { + close(pipefd[1]); + + ssize_t bytes_read = read(pipefd[0], output_buffer, buffer_size - 1); + if (bytes_read >= 0) { + output_buffer[bytes_read] = '\0'; + } else { + perror("read"); + close(pipefd[0]); + return -1; + } + + wait(NULL); + close(pipefd[0]); + } + + return 0; +} diff --git a/spawn.h b/spawn.h @@ -0,0 +1,2 @@ +int execute_command_output(const char *command, char *output_buffer, size_t buffer_size); +int execute_command(const char *command); diff --git a/tty.c b/tty.c @@ -0,0 +1,207 @@ +#include <unistd.h> +#include <fcntl.h> +#include <stdlib.h> +#include <stdarg.h> +#include <termios.h> +#include <sys/ioctl.h> +#include <sys/select.h> +#include <signal.h> +#include <errno.h> + +#include "tty.h" +#include "config.h" + +void tty_reset(tty_t *tty) { + tcsetattr(tty->fdin, TCSANOW, &tty->original_termios); +} + +void tty_close(tty_t *tty) { + tty_reset(tty); + fclose(tty->fout); + close(tty->fdin); +} + +static void handle_sigwinch(int sig){ + (void)sig; +} + +void tty_init(tty_t *tty, const char *tty_filename) { + tty->fdin = open(tty_filename, O_RDONLY); + if (tty->fdin < 0) { + perror("Failed to open tty"); + exit(EXIT_FAILURE); + } + + tty->fout = fopen(tty_filename, "w"); + if (!tty->fout) { + perror("Failed to open tty"); + exit(EXIT_FAILURE); + } + + if (setvbuf(tty->fout, NULL, _IOFBF, 16384)) { + perror("setvbuf"); + exit(EXIT_FAILURE); + } + + if (tcgetattr(tty->fdin, &tty->original_termios)) { + perror("tcgetattr"); + exit(EXIT_FAILURE); + } + + struct termios new_termios = tty->original_termios; + + /* + * Disable all of + * ICANON Canonical input (erase and kill processing). + * ECHO Echo. + * ISIG Signals from control characters + * ICRNL Conversion of CR characters into NL + */ + new_termios.c_iflag &= ~(ICRNL); + new_termios.c_lflag &= ~(ICANON | ECHO | ISIG); + + if (tcsetattr(tty->fdin, TCSANOW, &new_termios)) + perror("tcsetattr"); + + tty_getwinsz(tty); + + tty_setnormal(tty); + + signal(SIGWINCH, handle_sigwinch); +} + +void tty_getwinsz(tty_t *tty) { + struct winsize ws; + if (ioctl(fileno(tty->fout), TIOCGWINSZ, &ws) == -1) { + tty->maxwidth = 80; + tty->maxheight = 25; + } else { + tty->maxwidth = ws.ws_col; + tty->maxheight = ws.ws_row; + } +} + +char tty_getchar(tty_t *tty) { + char ch; + int size = read(tty->fdin, &ch, 1); + if (size < 0) { + perror("error reading from tty"); + exit(EXIT_FAILURE); + } else if (size == 0) { + /* EOF */ + exit(EXIT_FAILURE); + } else { + return ch; + } +} + +int tty_input_ready(tty_t *tty, long int timeout, int return_on_signal) { + fd_set readfs; + FD_ZERO(&readfs); + FD_SET(tty->fdin, &readfs); + + struct timespec ts = {timeout / 1000, (timeout % 1000) * 1000000}; + + sigset_t mask; + sigemptyset(&mask); + if (!return_on_signal) + sigaddset(&mask, SIGWINCH); + + int err = pselect( + tty->fdin + 1, + &readfs, + NULL, + NULL, + timeout < 0 ? NULL : &ts, + return_on_signal ? NULL : &mask); + + if (err < 0) { + if (errno == EINTR) { + return 0; + } else { + perror("select"); + exit(EXIT_FAILURE); + } + } else { + return FD_ISSET(tty->fdin, &readfs); + } +} + +static void tty_sgr(tty_t *tty, int code) { + tty_printf(tty, "%c%c%im", 0x1b, '[', code); +} + +void tty_setfg(tty_t *tty, int fg) { + if (tty->fgcolor != fg) { + tty_sgr(tty, 30 + fg); + tty->fgcolor = fg; + } +} + +void tty_setinvert(tty_t *tty) { + tty_sgr(tty, 7); +} + +void tty_setunderline(tty_t *tty) { + tty_sgr(tty, 4); +} + +void tty_setnormal(tty_t *tty) { + tty_sgr(tty, 0); + tty->fgcolor = 9; +} + +void tty_setnowrap(tty_t *tty) { + tty_printf(tty, "%c%c?7l", 0x1b, '['); +} + +void tty_setwrap(tty_t *tty) { + tty_printf(tty, "%c%c?7h", 0x1b, '['); +} + +void tty_newline(tty_t *tty) { + tty_printf(tty, "%c%cK\n", 0x1b, '['); +} + +void tty_clearline(tty_t *tty) { + tty_printf(tty, "%c%cK\r", 0x1b, '['); +} + +void tty_setcol(tty_t *tty, int col) { + tty_printf(tty, "%c%c%iG", 0x1b, '[', col + 1); +} + +void tty_moveup(tty_t *tty, int i) { + tty_printf(tty, "%c%c%iA", 0x1b, '[', i); +} + +void tty_printf(tty_t *tty, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + vfprintf(tty->fout, fmt, args); + va_end(args); +} + +void tty_putc(tty_t *tty, char c) { + fputc(c, tty->fout); +} + +void tty_flush(tty_t *tty) { + fflush(tty->fout); +} + +size_t tty_getwidth(tty_t *tty) { + return tty->maxwidth; +} + +size_t tty_getheight(tty_t *tty) { + return tty->maxheight; +} + +void tty_hide_cursor(tty_t *tty) { + fputs("\x1b[?25l", tty->fout); +} + +void tty_unhide_cursor(tty_t *tty) { + fputs("\x1b[?25h", tty->fout); +} diff --git a/tty.h b/tty.h @@ -0,0 +1,73 @@ +#ifndef TTY_H +#define TTY_H TTY_H + +#include <stddef.h> +#include <stdio.h> +#include <termios.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + int fdin; + FILE *fout; + struct termios original_termios; + int fgcolor; + size_t maxwidth; + size_t maxheight; +} tty_t; + +void tty_reset(tty_t *tty); +void tty_close(tty_t *tty); +void tty_init(tty_t *tty, const char *tty_filename); +void tty_getwinsz(tty_t *tty); +char tty_getchar(tty_t *tty); +int tty_input_ready(tty_t *tty, long int timeout, int return_on_signal); + +void tty_setfg(tty_t *tty, int fg); +void tty_setinvert(tty_t *tty); +void tty_setunderline(tty_t *tty); +void tty_setnormal(tty_t *tty); +void tty_setnowrap(tty_t *tty); +void tty_setwrap(tty_t *tty); + +#define TTY_COLOR_BLACK 0 +#define TTY_COLOR_RED 1 +#define TTY_COLOR_GREEN 2 +#define TTY_COLOR_YELLOW 3 +#define TTY_COLOR_BLUE 4 +#define TTY_COLOR_MAGENTA 5 +#define TTY_COLOR_CYAN 6 +#define TTY_COLOR_WHITE 7 +#define TTY_COLOR_NORMAL 9 + +/* tty_newline + * Move cursor to the beginning of the next line, clearing to the end of the + * current line + */ +void tty_newline(tty_t *tty); + +/* tty_clearline + * Clear to the end of the current line without advancing the cursor. + */ +void tty_clearline(tty_t *tty); + +void tty_moveup(tty_t *tty, int i); +void tty_setcol(tty_t *tty, int col); + +void tty_printf(tty_t *tty, const char *fmt, ...); +void tty_putc(tty_t *tty, char c); +void tty_flush(tty_t *tty); + +size_t tty_getwidth(tty_t *tty); +size_t tty_getheight(tty_t *tty); + +void tty_hide_cursor(tty_t *tty); +void tty_unhide_cursor(tty_t *tty); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/tty_interface.c b/tty_interface.c @@ -0,0 +1,344 @@ +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> + +#include "tty_interface.h" +#include "spawn.h" +#include "config.h" + +static void clear(tty_interface_t *state) { + tty_t *tty = state->tty; + + tty_setcol(tty, 0); + size_t line = 0; + while (line++ < state->options->num_files) { + tty_newline(tty); + } + tty_clearline(tty); + if (state->options->num_files > 0) { + tty_moveup(tty, line - 1); + } + tty_unhide_cursor(tty); + tty_flush(tty); +} + +static void draw(tty_interface_t *state) { + tty_t *tty = state->tty; + entries_t *entries = state->entries; + options_t *options = state->options; + + unsigned int num_header = 1; + unsigned int num_footer = 1; + unsigned int num_files = options->num_files; + + size_t start = 0; + size_t scroll_threshold = (num_files / 2) + 1; + + if (entries->selection >= scroll_threshold && entries->size > num_files) { + start = entries->selection - scroll_threshold + num_header; + + if (start + num_files >= entries->size && entries->size > 0) { + start = entries->size - num_files; + } + } + + tty_clearline(tty); + tty_setcol(tty, 0); + tty_printf(tty, "%s", entries->path); + tty_clearline(tty); + tty_setcol(tty, 0); + tty_printf(tty, "\n"); + + for (size_t i = start; i < start + num_files; i++) { + tty_clearline(tty); + tty_setcol(tty, 0); + tty_printf(tty, "\n"); + + struct entry *entry = entries_item(entries, i); + if (entry) { + tty_clearline(tty); + tty_setcol(tty, 2); + tty_setfg(tty, entry->color); + + if (i == entries->selection) tty_setinvert(tty); + tty_printf(tty, "%s%c", entry->name, entry->cm); + tty_setnormal(tty); + } + } + + tty_clearline(tty); + tty_setcol(tty, 0); + tty_printf(tty, "\n"); + tty_moveup(tty, num_files + num_header + num_footer); + tty_flush(tty); +} + +static void action_ignore(tty_interface_t *state, char *argv) { + (void)state; + (void)argv; +} + +static void action_select(tty_interface_t *state, char *argv) { + (void)argv; + int done = entries_select(state->entries); + + if (!done) { + draw(state); + } else { + clear(state); + tty_close(state->tty); + struct entry *selection = entries_selected(state->entries); + printf("%s/%s\n", state->entries->path, selection->name); + state->exit = EXIT_SUCCESS; + } +} + +static void action_parent(tty_interface_t *state, char *argv) { + (void)argv; + entries_parent(state->entries); + draw(state); +} + +static void action_prev(tty_interface_t *state, char *argv) { + (void)argv; + entries_prev(state->entries); + draw(state); +} + +static void action_next(tty_interface_t *state, char *argv) { + (void)argv; + entries_next(state->entries); + draw(state); +} + +static void action_halfpageup(tty_interface_t *state, char *argv) { + (void)argv; + for (size_t i = 0; i < (state->options->num_files / 2) && state->entries->selection > 0; i++) + entries_prev(state->entries); + + draw(state); +} + +static void action_halfpagedown(tty_interface_t *state, char *argv) { + (void)argv; + for (size_t i = 0; i < (state->options->num_files / 2) && state->entries->selection < state->entries->size - 1; i++) + entries_next(state->entries); + + draw(state); +} + +static void action_pageup(tty_interface_t *state, char *argv) { + (void)argv; + for (size_t i = 0; i < state->options->num_files && state->entries->selection > 0; i++) + entries_prev(state->entries); + + draw(state); +} + +static void action_pagedown(tty_interface_t *state, char *argv) { + (void)argv; + for (size_t i = 0; i < state->options->num_files && state->entries->selection < state->entries->size - 1; i++) + entries_next(state->entries); + + draw(state); +} + +static void action_first(tty_interface_t *state, char *argv) { + (void)argv; + entries_position(state->entries, 0); + draw(state); +} + +static void action_last(tty_interface_t *state, char *argv) { + (void)argv; + entries_position(state->entries, state->entries->size - 1); + draw(state); +} + +static void action_home(tty_interface_t *state, char *argv) { + (void)argv; + entries_setpath(state->entries, getenv("HOME")); + draw(state); +} + +static void action_togglehidden(tty_interface_t *state, char *argv) { + (void)argv; + entries_togglehidden(); + draw(state); +} + +static void action_run(tty_interface_t *state, char *argv) { + (void)argv; + + struct entry *entry = entries_item(state->entries, state->entries->selection); + if (!entry) return; + + size_t input_len = strlen(argv) + PATH_MAX; + char *input = malloc(input_len); + + if (input == NULL) { + perror("Failed to allocate memory"); + return; + } + + snprintf(input, input_len, argv, state->entries->path, entry->name); + + clear(state); + execute_command(input); + + free(input); + tty_hide_cursor(state->tty); + draw(state); +} + +static void action_setpath(tty_interface_t *state, char *argv) { + size_t input_len = strlen(argv) + PATH_MAX; + char *input = malloc(input_len); + + if (input == NULL) { + perror("Failed to allocate memory"); + return; + } + + snprintf(input, input_len, argv, state->entries->path); + + char output[PATH_MAX]; + clear(state); + + if (0 == execute_command_output(input, output, sizeof(output))) + entries_setpath(state->entries, output); + + free(input); + tty_hide_cursor(state->tty); + draw(state); +} + +static void action_exit(tty_interface_t *state, char *argv) { + (void)argv; + clear(state); + tty_close(state->tty); + state->exit = EXIT_FAILURE; +} + +void tty_interface_init(tty_interface_t *state, tty_t *tty, entries_t *entries, options_t *options) { + if (options->num_files + 1 > tty_getheight(tty)) { + options->num_files = tty_getheight(tty) - 3; + } + + state->tty = tty; + state->entries = entries; + state->options = options; + state->ambiguous_key_pending = 0; + state->exit = -1; + + strcpy(state->input, ""); + + tty_hide_cursor(tty); + draw(state); +} + +typedef struct { + const char *key; + void (*action)(tty_interface_t *, char *); + char *argv; +} keybinding_t; + +#define KEY(key) ((const char[]){key, '\0'}) +#define KEY_CTRL(key) ((const char[]){((key) - ('@')), '\0'}) + +static const keybinding_t keybindings[] = { + {"\x1b", action_exit, NULL}, /* ESC */ + {KEY_CTRL('C'), action_exit, NULL}, /* C-C */ + {KEY_CTRL('M'), action_select, NULL}, /* CR */ + {KEY_CTRL('P'), action_prev,NULL}, /* C-P */ + {KEY_CTRL('N'), action_next,NULL}, /* C-N */ + {KEY_CTRL('K'), action_prev,NULL}, /* C-K */ + {KEY_CTRL('J'), action_next,NULL}, /* C-J */ + {KEY_CTRL('U'), action_halfpageup,NULL}, /* C-U */ + {KEY_CTRL('D'), action_halfpagedown,NULL}, /* C-D */ + {KEY('g'), action_first,NULL}, /* g */ + {KEY('G'), action_last,NULL}, /* G */ + {KEY('~'), action_home,NULL}, /* ~ */ + {KEY('.'), action_togglehidden,NULL}, /* . */ + {KEY('p'), action_run, "less %s/%s"}, /* p (preview) */ + {KEY('t'), action_run, "echo \"create 'vis %s/%s'\" > $DVTM_CMD_FIFO"}, /* edit new tab */ + {KEY('f'), action_setpath, "find %s | fzy -l 999"}, /* s (search) */ + {KEY('-'), action_parent,NULL}, /* - */ + {"\x1bOD", action_parent,NULL}, /* LEFT */ + {"\x1b[D", action_parent,NULL}, /* LEFT */ + {"\x1bOC", action_select,NULL}, /* RIGHT */ + {"\x1b[C", action_select,NULL}, /* RIGHT */ + {"\x1b[A", action_prev,NULL}, /* UP */ + {"\x1bOA", action_prev,NULL}, /* UP */ + {"\x1b[B", action_next,NULL}, /* DOWN */ + {"\x1bOB", action_next,NULL}, /* DOWN */ + {"\x1b[5~", action_pageup,NULL}, + {"\x1b[6~", action_pagedown,NULL}, + {"\x1b[200~", action_ignore,NULL}, + {"\x1b[201~", action_ignore,NULL}, + {NULL, NULL, NULL}}; + +#undef KEY_CTRL + +static void handle_input(tty_interface_t *state, const char *s, int handle_ambiguous_key) { + state->ambiguous_key_pending = 0; + + char *input = state->input; + strcat(state->input, s); + + /* Figure out if we have completed a keybinding and whether we're in the + * middle of one (both can happen, because of Esc). */ + int found_keybinding = -1; + int in_middle = 0; + for (int i = 0; keybindings[i].key; i++) { + if (!strcmp(input, keybindings[i].key)) + found_keybinding = i; + else if (!strncmp(input, keybindings[i].key, strlen(state->input))) + in_middle = 1; + } + + if (found_keybinding != -1 && (!in_middle || handle_ambiguous_key)) { + keybindings[found_keybinding].action(state, keybindings[found_keybinding].argv); + strcpy(input, ""); + return; + } + + if (in_middle) { + state->ambiguous_key_pending = 1; + return; + } + + strcpy(input, ""); +} + +int tty_interface_run(tty_interface_t *state) { + for (;;) { + do { + while(!tty_input_ready(state->tty, -1, 1)) { + draw(state); + } + + char s[2] = {tty_getchar(state->tty), '\0'}; + handle_input(state, s, 0); + + if (state->ambiguous_key_pending == 1) + continue; + + if (state->exit >= 0) + return state->exit; + + } while (tty_input_ready(state->tty, state->ambiguous_key_pending ? KEYTIMEOUT : 0, 0)); + + if (state->ambiguous_key_pending) { + char s[1] = ""; + handle_input(state, s, 1); + + if (state->exit >= 0) + return state->exit; + } + } + + return state->exit; +} diff --git a/tty_interface.h b/tty_interface.h @@ -0,0 +1,28 @@ +#ifndef TTY_INTERFACE_H +#define TTY_INTERFACE_H TTY_INTERFACE_H + +#include "entries.h" +#include "options.h" +#include "tty.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + tty_t *tty; + entries_t *entries; + options_t *options; + int ambiguous_key_pending; + char input[32]; + int exit; +} tty_interface_t; + +void tty_interface_init(tty_interface_t *state, tty_t *tty, entries_t *entries, options_t *options); +int tty_interface_run(tty_interface_t *state); + +#ifdef __cplusplus +} +#endif + +#endif