From 6a3e21f568a500bfcf504a61cdd791c251359027 Mon Sep 17 00:00:00 2001 From: Andreev Gregory Date: Tue, 1 Apr 2025 16:56:23 +0300 Subject: [PATCH] hail dmenuc --- .gitignore | 2 + Makefile | 11 +-- config.def.h | 8 +- dmenu.c | 19 +++-- dmenu_run.sh | 13 ---- dmenuc.py | 211 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 233 insertions(+), 31 deletions(-) delete mode 100755 dmenu_run.sh create mode 100755 dmenuc.py diff --git a/.gitignore b/.gitignore index 1d9d2f1..14ebb28 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ config.h stest *.tar *.gz +test_collect_options.sh +shell.nix diff --git a/Makefile b/Makefile index d011757..5a21b9f 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ clean: dist: clean mkdir -p dmenu-$(VERSION) - cp LICENSE Makefile dmenu_run.sh README arg.h config.def.h config.mk dmenu.1\ - drw.h util.h stest.1 $(SRC)\ + cp LICENSE Makefile README arg.h config.def.h config.mk dmenu.1\ + drw.h util.h stest.1 dmenuc.py $(SRC)\ dmenu-$(VERSION) tar -cf dmenu-$(VERSION).tar dmenu-$(VERSION) gzip dmenu-$(VERSION).tar @@ -33,9 +33,8 @@ dist: clean install: all mkdir -p $(DESTDIR)$(PREFIX)/bin - cp -f dmenu dmenu_run.sh stest $(DESTDIR)$(PREFIX)/bin + cp -f dmenu stest dmenuc.py $(DESTDIR)$(PREFIX)/bin chmod 755 $(DESTDIR)$(PREFIX)/bin/stest - chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_run.sh chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu mkdir -p $(DESTDIR)$(MANPREFIX)/man1 sed "s/VERSION/$(VERSION)/g" < dmenu.1 > $(DESTDIR)$(MANPREFIX)/man1/dmenu.1 @@ -45,9 +44,7 @@ install: all uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/dmenu\ - $(DESTDIR)$(PREFIX)/bin/dmenu_run\ - $(DESTDIR)$(PREFIX)/bin/dmenu_run.sh\ - $(DESTDIR)$(PREFIX)/bin/dmenu_path\ + $(DESTDIR)$(PREFIX)/bin/dmenuc.py $(DESTDIR)$(PREFIX)/bin/stest\ $(DESTDIR)$(MANPREFIX)/man1/dmenu.1\ $(DESTDIR)$(MANPREFIX)/man1/stest.1 diff --git a/config.def.h b/config.def.h index 5c0a645..b8536bb 100644 --- a/config.def.h +++ b/config.def.h @@ -15,10 +15,10 @@ static const char *prompt = NULL; /* -p option; prompt to the left of static const char *colors[SchemeLast][2] = { /* fg bg */ [SchemeNorm] = { "#dddddd", "#111111" }, - [SchemeSel] = { "#ffffff", "#ff0000" }, - [SchemeOut] = { "#000000", "#bc3d2f" }, - [SchemeCaret] = { "#fe5e5e", "#222222" }, - [SchemeBorder] = { "#dc0000", NULL }, + [SchemeSel] = { "#000000", "#ffd000" }, + [SchemeOut] = { "#000000", "#bcad1f" }, + [SchemeCaret] = { "#fede5e", NULL }, + [SchemeBorder] = { "#dcf000", NULL }, }; /* -l and -g options; controls number of lines and columns in grid if > 0 */ diff --git a/dmenu.c b/dmenu.c index bd55ca4..abd979b 100644 --- a/dmenu.c +++ b/dmenu.c @@ -618,12 +618,14 @@ paste(void) static void readstdin(void) { - char *line = NULL, *p; + char *line = NULL; size_t i, itemsiz = 0, linesiz = 0; ssize_t len; - /* read each line from stdin and add it to the item list */ - for (i = 0; (len = getline(&line, &linesiz, stdin)) != -1; i++) { + /* Reading each line from stdin. If it contains tab, we split it into stext and text. + * Otherwise we don't even add it. stext is what will be show. + * text is what will be put to stdout */ + for (i = 0; (len = getline(&line, &linesiz, stdin)) != -1;) { if (i + 1 >= itemsiz) { itemsiz += 256; if (!(items = realloc(items, itemsiz * sizeof(*items)))) @@ -631,13 +633,16 @@ readstdin(void) } if (line[len - 1] == '\n') line[len - 1] = '\0'; - if (!(items[i].text = strdup(line))) + char* second_part = strchr(line, '\t'); + if (!second_part) + continue; + if (!(items[i].text = strdup(second_part + 1))) die("strdup:"); - if ((p = strchr(line, '\t'))) - *p = '\0'; + *second_part = '\0'; if (!(items[i].stext = strdup(line))) - die("cannot strdup bytes:"); + die("strdup:"); items[i].out = 0; + i++; } free(line); if (items) diff --git a/dmenu_run.sh b/dmenu_run.sh deleted file mode 100755 index 951ed02..0000000 --- a/dmenu_run.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -FILE="$HOME/.config/suckless_desktop/win_r.txt" -if [ ! -f "$FILE" ]; then - echo "File not found: $FILE" >&2 - exit 1 -fi - -sed 's/|/'$'\t''/' "$FILE" | dmenu "$@" | { - IFS=$'\t' read -r col1 col2 - [ -z "$col2" ] && exit 0 - eval "$col2" 2>/dev/null || : -} diff --git a/dmenuc.py b/dmenuc.py new file mode 100755 index 0000000..72a2745 --- /dev/null +++ b/dmenuc.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 + +import re +import sys +import argparse +import os +import stat +from dataclasses import dataclass +from typing import List, Union + +TOKEN_SPECIFICATION = [ + ('IMPORT', r'import'), + ('EVAL', r'eval'), + ('OPTION', r'option'), + ('DMENU', r'dmenu'), + ('LPAREN', r'\('), + ('RPAREN', r'\)'), + ('STRING', r'"(?:\\.|[^"\\])*"|\'[^\']*\'|`[^`]*`'), + ('SKIP', r'[\n \t]+'), + ('MISMATCH', r'.'), +] + +TOKEN_REGEX = '|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPECIFICATION) + + +@dataclass +class Token: + type: str + value: str + pos: int + + def __repr__(self): + return f'Token({self.type}, {self.value}, pos={self.pos})' + + +def tokenize(code): + tokens = [] + for mo in re.finditer(TOKEN_REGEX, code): + kind = mo.lastgroup + value = mo.group() + pos = mo.start() + if kind == 'SKIP': + continue + elif kind == 'MISMATCH': + raise RuntimeError(f'Unexpected token {value!r} at position {pos}') + else: + tokens.append(Token(kind, value, pos)) + return tokens + + +def restore_string(string_tkn_val: str): + if string_tkn_val[0] == '"': + return string_tkn_val[1:-1].replace("\\\"", "\"").replace("\\\\", "\\") + else: + return string_tkn_val[1:-1] + + +class ActionDmenu: + ... + + +@dataclass +class OptionInDmenu: + label: str + action: Union[ActionDmenu, str] + + def __repr__(self): + if isinstance(self.action, ActionDmenu): + return f"option {self.label} {self.action}" + else: + return f"option {self.label} eval {self.action}" + + +@dataclass +class ActionDmenu: + prompt: str + options: List[OptionInDmenu] + + def __repr__(self): + return f"dmenu {self.prompt} ( {' ; '.join(map(str, self.options))} )" + + +class Parser: + def __init__(self, tokens): + self.tokens = tokens + self.pos = 0 + + def current(self): + if self.pos < len(self.tokens): + return self.tokens[self.pos] + return None + + def eat(self, token_type) -> Token: + token = self.current() + if token is None: + raise RuntimeError("Unexpected end of input") + if token.type == token_type: + self.pos += 1 + return token + raise RuntimeError(f"Expected token {token_type} at position {token.pos} but got {token.type}") + + # Grammar production: