From 0fa7537ce5006a9c99bc7e295a4ae4ff0b920fda Mon Sep 17 00:00:00 2001 From: Krow Savcik Date: Sun, 31 Aug 2025 15:11:01 +0200 Subject: initial commit --- Makefile | 34 ++++++ README | 60 +++++++++ config.def.h | 2 + config.mk | 6 + debug.c | 18 +++ debug.h | 3 + renu.1 | 33 +++++ renu.c | 387 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ util.c | 88 ++++++++++++++ util.h | 3 + 10 files changed, 634 insertions(+) create mode 100644 Makefile create mode 100644 README create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 debug.c create mode 100644 debug.h create mode 100644 renu.1 create mode 100644 renu.c create mode 100644 util.c create mode 100644 util.h diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b4990a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +include config.mk + +all: renu + +config.h: + cp config.def.h $@ + +renu: renu.c config.h + ${CC} renu.c util.c debug.c -o renu + +install: renu + mkdir -p ${PREFIX}/bin + cp -f renu ${PREFIX}/bin + chmod 755 ${PREFIX}/bin/renu + mkdir -p ${MANPREFIX}/man1 + sed "s/VERSION/${VERSION}/g" < renu.1 > ${MANPREFIX}/man1/renu.1 + chmod 644 ${MANPREFIX}/man1/renu.1 + +uninstall: + rm -f ${PREFIX}/bin/renu\ + ${MANPREFIX}/man1/renu.1 + +dist: clean + mkdir -p renu-${VERSION} + cp -R Makefile README config.def.h config.mk\ + renu.c util.c util.h debug.c debug.h renu.1 renu-${VERSION} + tar -cf renu-${VERSION}.tar renu-${VERSION} + gzip renu-${VERSION}.tar + rm -rf renu-${VERSION} + +clean: + rm -f renu renu-${VERSION}.tar.gz + +.PHONY: all dist install uninstall clean diff --git a/README b/README new file mode 100644 index 0000000..e585a3e --- /dev/null +++ b/README @@ -0,0 +1,60 @@ +renu - recursive menu +===================== +renu uses a line selection program to choose and exectue options specified in a file. + + +Requirements +------------ +To run renu you will need a program like dmenu or rofi. + + +Installation +------------ +Enter the following command to compile and install: +```sh +make install +``` +By default, everything is installed in /usr/local. To change the +install directory, edit config.mk. + + +Writing a menu file +------------------- +renu reads a file, to determine how the menu(s) should be constructed +and what should they do. Each line represents an option: + +- Menu (m name) + This line represents the start of a menu. All the lines that follow, + until another menu line is found, represent the entries of this menu. + The default name the program will open is 'def'. + +- Empty line and Comment (# ...) + If the line starts with a # or is empty, it is ignored and regarded as + a comment. + +- Submenu (s entry name) + The submenu line will open the menu with the specified name, once + selected. Entry is the displayed name. The line takes two arguments. + +- Print (p entry [value]) + This option will print the value once selected. The value is an + optional argument and, if empty, entry will be printed. + +- Run (r entry command) + This line takes two arguments. The option will run the command as if + it were run with sh -c "command". + +- Arguments + Each line uses one or more arguments. The arguments are separated by + spaces, thus if you want to have spaces in the arguments, you should + either escape the space or use double quotes. You can escape a + character using backslash ('\'). Note that the double quotes ('"') and + backslashes ('\') inside the argument always have to be escaped. + +Configuartion +------------- +The settings for compiling and installing renu are in config.mk + +By default renu uses dmenu for selecting the options. To change the +program, edit the config.h file and recompile. If you don't see the +config.h file, try building renu. diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..b636888 --- /dev/null +++ b/config.def.h @@ -0,0 +1,2 @@ +static const char *default_menu = "def"; +static const char *menu_cmd[] = {"dmenu", NULL}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..2472f2e --- /dev/null +++ b/config.mk @@ -0,0 +1,6 @@ +VERSION = alpha + +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +CC = cc diff --git a/debug.c b/debug.c new file mode 100644 index 0000000..81d173c --- /dev/null +++ b/debug.c @@ -0,0 +1,18 @@ +#include +#include +#include + +#include "debug.h" + +void +_error(const char *file, const int line, const char *fmt, ...) +{ + va_list args; + fprintf(stderr, "%s:%d: ", file, line); + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + + exit(1); +} diff --git a/debug.h b/debug.h new file mode 100644 index 0000000..03f41d3 --- /dev/null +++ b/debug.h @@ -0,0 +1,3 @@ +#define error(...) _error(__FILE__, __LINE__, __VA_ARGS__) + +void _error(const char *, const int, const char *, ...); diff --git a/renu.1 b/renu.1 new file mode 100644 index 0000000..ec0ad21 --- /dev/null +++ b/renu.1 @@ -0,0 +1,33 @@ +.TH RENU 1 renu\-VERSION +.SH NAME +renu \- recursive menu +.SH SYNOPSIS +.B renu +.RB [FILE] +.SH DESCRIPTION +Create a recursive menu described in FILE. +.PP +The FILE should have a specific format, each line representing an option. +.SS Menu (m name) +This line represents the start of a menu. +All the lines that follow, until another menu line is found, represent the entries of this menu. +The default name the program will open is 'def'. +.SS Empty line +.SS Comment (# ...) +If the line starts with a # or is empty, it is ignored and regarded as a comment. +.SS Submenu (s entry name) +The submenu line will open the menu with the specified name, once selected. +Entry is the displayed name. +The line takes two arguments. +.SS Print (p entry [value]) +This option will print the value once selected. +The value is an optional argument and, if empty, entry will be printed. +.SS Run (r entry command) +This line takes two arguments. +The option will run the command as if it were run with sh -c "command". +.SS Arguments +Each line uses one or more arguments. +The arguments are separated by spaces, thus if you want to have spaces +in the arguments, you should either escape the space or use double quotes. +You can escape a character using backslash ('\\'). +Note that the double quotes ('"') and backslashes ('\\') inside the argument always have to be escaped. diff --git a/renu.c b/renu.c new file mode 100644 index 0000000..742d0c2 --- /dev/null +++ b/renu.c @@ -0,0 +1,387 @@ +#include +#include +#include +#include +#include + +#include "util.h" +#include "debug.h" +#include "config.h" + +#define MAXLINE 128 + +static char menu[MAXLINE]; + +void +usage() +{ + printf("remu [FILE]\n"); + exit(0); +} + +int +launch_menu(int *pfd, char *prompt) +{ + int pipefd[4]; + pid_t pid; + if (pipe(pipefd)) + return 1; + if (pipe(&pipefd[2])) { + close(pipefd[0]); + close(pipefd[1]); + return 1; + } + + pid = fork(); + + switch (pid) { + case -1: + close(pipefd[0]); + close(pipefd[1]); + close(pipefd[2]); + close(pipefd[3]); + return 1; + case 0: + break; + default: + pfd[0] = pipefd[0]; + pfd[1] = pipefd[3]; + close(pipefd[1]); + close(pipefd[2]); + return 0; + } + + /* Child process */ + close(pipefd[0]); + close(pipefd[3]); + + if (dup2(pipefd[1], STDOUT_FILENO) == -1) + exit(1); + if (dup2(pipefd[2], STDIN_FILENO) == -1) + exit(1); + + execvp(menu_cmd[0], (char **)menu_cmd); + close(pipefd[1]); + close(pipefd[2]); + exit(0); +} + +char * +nextline(char *c) +{ + while (*c != '\n') + c++; + + return ++c; +} + +char * +setline(char *p) +{ + char *end; + end = nextline(p); + end[-1] = 0; + return end; +} + +void +resetline(char *end) +{ + end[-1] = '\n'; +} + +char * +nextarg(char *c) +{ + while (*c == '\0') + c++; + + return c; +} + +char * +process_quoted(char *pnt) +{ + size_t off = 0; + char *ret = NULL; + pnt[0] = 0; + pnt++; + while (pnt[off] != '"') { + if (pnt[off] == '\0' || pnt[off] == '\n') + return NULL; + + if (pnt[off] == '\\') { + if (pnt[off+1] == '\0' || pnt[off+1] == '\n') + return NULL; + off++; + } + pnt[0] = pnt[off]; + pnt++; + } + + for (ret = &(pnt[off+1]); pnt != ret; pnt++) + pnt[0] = 0; + + return ret; +} + +char * +process_unquoted(char *pnt) +{ + size_t off = 0; + char *ret = NULL; + while (pnt[off] && pnt[off] != '\n') { + if (pnt[off] == ' ') { + ret = &pnt[off+1]; + break; + } + + if (pnt[off] == '"') + return NULL; + + if (pnt[off] == '\\') { + if (pnt[off+1] == '\0' || pnt[off+1] == '\n') + return NULL; + off++; + } + pnt[0] = pnt[off]; + pnt++; + } + + ret = ret ? ret : &pnt[off]; + + for (; pnt != ret; pnt++) + pnt[0] = 0; + + return ret; +} + +char * +process_line(char *pnt) +{ + while (*pnt && *pnt != '\n') { + if (*pnt == '"') + pnt = process_quoted(pnt); + else + pnt = process_unquoted(pnt); + + if (pnt == NULL) + return NULL; + } + + return pnt; +} + +unsigned int +preprocess(char *buf) +{ + char *pnt = buf; + unsigned int line = 0; + while (*pnt) { + line++; + if (*pnt == '#' || *pnt == '\n') { + pnt = nextline(pnt); + continue; + } + + if (pnt[1] != ' ') + return line; + + pnt[1] = 0; + pnt = process_line(pnt+2); + if (pnt == NULL) + return line; + pnt = nextline(pnt); + } + + return 0; +} + +int +submenu(char *line) +{ + char *e, *arg, *ea; + arg = nextarg(line+2); + ea = &arg[strlen(arg) - 1]; + if (*ea == '\n') + return 1; + + arg = nextarg(ea+1); + e = setline(line); + strncpy(menu, arg, MAXLINE); + menu[MAXLINE-1] = 0; + resetline(e); + return 0; +} + +int +print(char *line) +{ + char *e, *arg, *ea; + arg = nextarg(line+2); + + ea = &arg[strlen(arg) - 1]; + if (*ea != '\n' && *nextarg(ea+1) != '\n') + arg = nextarg(ea+1); + + e = setline(line); + printf("%s\n", arg); + resetline(e); + return 0; +} + +int +run(char *line) +{ + char *e, *arg, *ea; + arg = nextarg(line+2); + + ea = &arg[strlen(arg) - 1]; + if (*ea == '\n') + return 1; + + arg = nextarg(ea+1); + e = setline(line); + if (execl("/bin/sh", "sh", "-c", arg, NULL)) + error("Error executing process: %s\n", strerror(errno)); + resetline(e); + return 0; +} + +int +main(int argc, char *argv[]) +{ + char *buf, *stop, *pnt, *end, *mpnt, *line; + unsigned int i; + int pfd[2]; + FILE *f[2]; + char option[MAXLINE]; + + strcpy(menu, default_menu); + + if (argc != 2) + usage(); + + buf = f_read(argv[1]); + if (buf == NULL) { + fprintf(stderr, "Couldn't read file %s\n", argv[1]); + return 1; + } + + stop = &buf[strlen(buf)]; + if (stop == buf) { + fprintf(stderr, "Wrong format in %s: Empty file\n", argv[1]); + return 1; + } else if (stop[-1] != '\n') { + fprintf(stderr, "Wrong format in %s: Missing EOL at the end of the file\n", argv[1]); + return 1; + } + + if (i = preprocess(buf)) { + fprintf(stderr, "Wrong format in %s:%u\n", argv[1], i); + return 1; + } + + + while (1) { + for (pnt = buf; pnt != stop; pnt = nextline(pnt)) { + if (*pnt != 'm') + continue; + pnt = nextarg(pnt+1); + end = setline(pnt); + + if (strcmp(menu, pnt) == 0) + break; + resetline(end); + } + + if (pnt == stop) { + fprintf(stderr, "Couldn't find menu %s in file %s\n", menu, argv[1]); + return 1; + } + + resetline(end); + + /* Query option */ + if (launch_menu(pfd, NULL)) { + fprintf(stderr, "Couldn't open the menu\n"); + return 1; + } + + f[0] = fdopen(pfd[0], "r"); + f[1] = fdopen(pfd[1], "w"); + + if (f[0] == NULL || f[1] == NULL) { + fprintf(stderr, "Couldn't open the menu\n"); + return 1; + } + + mpnt = nextline(pnt); + for (pnt = mpnt; pnt != stop; pnt = nextline(pnt)) { + if (*pnt == 'm') + break; + + if (*pnt == '\n' || *pnt == '#') + continue; + + pnt = nextarg(pnt+1); + end = setline(pnt); + fprintf(f[1], "%s\n", pnt); + resetline(end); + } + + fclose(f[1]); + close(pfd[1]); + + if (fgets(option, MAXLINE, f[0]) == NULL) { + fprintf(stderr, "Couldn't open the menu\n"); + return 1; + } + option[strlen(option)-1] = 0; + + for (line = mpnt; line != stop; line = nextline(line)) { + if (*line == 'm') + break; + + if (*line == '\n' || *line == '#') + continue; + + pnt = nextarg(line+1); + + if (*pnt == '\n') { + fprintf(stderr, "Wrong format in %s: Not enough arguments\n", argv[1]); + return 1; + } + + end = setline(pnt); + if (strcmp(pnt, option) == 0) + break; + resetline(end); + } + + /* No option selected */ + if (*line == 'm' || line == stop) + break; + + resetline(end); + switch (line[0]) { + case 's': + if (submenu(line)) { + fprintf(stderr, "Wrong format in %s: Not enough arguments\n", argv[1]); + return 1; + } + break; + case 'p': + print(line); + return 0; + case 'r': + if (run(line)) { + fprintf(stderr, "Wrong format in %s: Not enough arguments\n", argv[1]); + return 1; + } + return 0; + default: + return 0; + } + } +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..3232168 --- /dev/null +++ b/util.c @@ -0,0 +1,88 @@ +#include +#include +#include +#include + +#include "util.h" +#include "debug.h" /* Probably shouldn't have debug in utils */ + +char * +s_cpy(const char *str) +{ + if (str == NULL) + return NULL; + + char *ret = malloc(strlen(str) + 1); + if (ret == NULL) + error("Not enough memory\n"); + + strcpy(ret, str); + return ret; +} + +char * +s_con(const char *str, ...) +{ + size_t len; + va_list args; + const char *ar; + char *ret, *p; + + len = strlen(str) + 1; + va_start(args, str); + while (ar = va_arg(args, const char*)) + len += strlen(ar); + va_end(args); + + ret = malloc(len); + if (ret == NULL) + error("Not enough memory\n"); + p = stpcpy(ret, str); + + va_start(args, str); + while (ar = va_arg(args, const char*)) + p = stpcpy(p, ar); + va_end(args); + + return ret; +} + +char * +f_read(const char *path) +{ + FILE *f; + char *buf; + long bsize; + buf = NULL; + if ((f = fopen(path, "r")) == NULL) + goto fread_deffer; + + if (fseek(f, 0L, SEEK_END)) + goto fread_deffer; + + if ((bsize = ftell(f)) == -1) + goto fread_deffer; + + buf = calloc(sizeof(*buf), bsize + 1); + + if (buf == NULL) + goto fread_deffer; + + if (fseek(f, 0L, SEEK_SET)) + goto fread_deffer; + + bsize = (long) fread(buf, sizeof(*buf), bsize, f); + if (ferror(f) != 0) + goto fread_deffer; + + buf[bsize] = '\0'; + fclose(f); + + return buf; +fread_deffer: + if (f) + fclose(f); + if (buf) + free(buf); + return NULL; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..e42c7ad --- /dev/null +++ b/util.h @@ -0,0 +1,3 @@ +char * s_cpy(const char *); +char * s_con(const char *, ...); +char * f_read(const char *); -- cgit v1.2.3