summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKrow Savcik <krow@savcik.xyz>2025-08-31 15:11:01 +0200
committerKrow Savcik <krow@savcik.xyz>2025-08-31 15:11:01 +0200
commit0fa7537ce5006a9c99bc7e295a4ae4ff0b920fda (patch)
tree057a91b30ab61417970c5defe825ce16a094ac39
initial commitHEADmaster
-rw-r--r--Makefile34
-rw-r--r--README60
-rw-r--r--config.def.h2
-rw-r--r--config.mk6
-rw-r--r--debug.c18
-rw-r--r--debug.h3
-rw-r--r--renu.133
-rw-r--r--renu.c387
-rw-r--r--util.c88
-rw-r--r--util.h3
10 files changed, 634 insertions, 0 deletions
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 <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+
+#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 *);