New dmenu version
This commit is contained in:
parent
bd0806de08
commit
3ce8459771
@ -4,7 +4,7 @@
|
|||||||
static int topbar = 1; /* -b option; if 0, dmenu appears at bottom */
|
static int topbar = 1; /* -b option; if 0, dmenu appears at bottom */
|
||||||
static const unsigned int alpha = 0xe3; /* Amount of opacity. 0xff is opaque */
|
static const unsigned int alpha = 0xe3; /* Amount of opacity. 0xff is opaque */
|
||||||
static int centered = 1; /* -c option; centers dmenu on screen */
|
static int centered = 1; /* -c option; centers dmenu on screen */
|
||||||
static int min_width = 1000; /* minimum width when centered */
|
static int min_width = 1020; /* minimum width when centered */
|
||||||
|
|
||||||
/* -fn option overrides fonts[0]; default X11 font or font set */
|
/* -fn option overrides fonts[0]; default X11 font or font set */
|
||||||
static const char *fonts[] = {
|
static const char *fonts[] = {
|
||||||
@ -23,7 +23,7 @@ static const char *colors[SchemeLast][2] = {
|
|||||||
|
|
||||||
/* -l and -g options; controls number of lines and columns in grid if > 0 */
|
/* -l and -g options; controls number of lines and columns in grid if > 0 */
|
||||||
static unsigned int lines = 9;
|
static unsigned int lines = 9;
|
||||||
static unsigned int columns = 2;
|
static unsigned int columns = 1;
|
||||||
|
|
||||||
static const unsigned int alphas[SchemeLast][2] = {
|
static const unsigned int alphas[SchemeLast][2] = {
|
||||||
[SchemeNorm] = { OPAQUE, alpha },
|
[SchemeNorm] = { OPAQUE, alpha },
|
||||||
|
126
dmenuc.py
126
dmenuc.py
@ -8,8 +8,22 @@ import stat
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
|
def find_line_column(code, pos):
|
||||||
|
last_newline = code.rfind('\n', 0, pos)
|
||||||
|
line = code.count('\n', 0, pos)
|
||||||
|
if last_newline == -1:
|
||||||
|
column = pos
|
||||||
|
else:
|
||||||
|
column = pos - last_newline - 1
|
||||||
|
return (line, column)
|
||||||
|
|
||||||
|
def stringify_code_pos(code, pos):
|
||||||
|
line, column = find_line_column(code, pos)
|
||||||
|
return f"{line+1}:{column+1}"
|
||||||
|
|
||||||
TOKEN_SPECIFICATION = [
|
TOKEN_SPECIFICATION = [
|
||||||
('IMPORT', r'import'),
|
('IMPORT', r'import'),
|
||||||
|
('OPTIONS', r'options'),
|
||||||
('EVAL', r'eval'),
|
('EVAL', r'eval'),
|
||||||
('OPTION', r'option'),
|
('OPTION', r'option'),
|
||||||
('DMENU', r'dmenu'),
|
('DMENU', r'dmenu'),
|
||||||
@ -22,7 +36,6 @@ TOKEN_SPECIFICATION = [
|
|||||||
|
|
||||||
TOKEN_REGEX = '|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPECIFICATION)
|
TOKEN_REGEX = '|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPECIFICATION)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Token:
|
class Token:
|
||||||
type: str
|
type: str
|
||||||
@ -33,7 +46,7 @@ class Token:
|
|||||||
return f'Token({self.type}, {self.value}, pos={self.pos})'
|
return f'Token({self.type}, {self.value}, pos={self.pos})'
|
||||||
|
|
||||||
|
|
||||||
def tokenize(code):
|
def tokenize(code, filename):
|
||||||
tokens = []
|
tokens = []
|
||||||
for mo in re.finditer(TOKEN_REGEX, code):
|
for mo in re.finditer(TOKEN_REGEX, code):
|
||||||
kind = mo.lastgroup
|
kind = mo.lastgroup
|
||||||
@ -42,7 +55,7 @@ def tokenize(code):
|
|||||||
if kind == 'SKIP':
|
if kind == 'SKIP':
|
||||||
continue
|
continue
|
||||||
elif kind == 'MISMATCH':
|
elif kind == 'MISMATCH':
|
||||||
raise RuntimeError(f'Unexpected token {value!r} at position {pos}')
|
raise RuntimeError(f'Unexpected token {value!r} at position {stringify_code_pos(code, pos)}. File {filename}')
|
||||||
else:
|
else:
|
||||||
tokens.append(Token(kind, value, pos))
|
tokens.append(Token(kind, value, pos))
|
||||||
return tokens
|
return tokens
|
||||||
@ -80,10 +93,20 @@ class ActionDmenu:
|
|||||||
return f"dmenu {self.prompt} ( {' ; '.join(map(str, self.options))} )"
|
return f"dmenu {self.prompt} ( {' ; '.join(map(str, self.options))} )"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_path_relative_to_file(path_a, path_b):
|
||||||
|
if os.path.isabs(path_b):
|
||||||
|
return path_b
|
||||||
|
a_dir = os.path.dirname(os.path.abspath(path_a))
|
||||||
|
path_c = os.path.normpath(os.path.join(a_dir, path_b))
|
||||||
|
return path_c
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def __init__(self, tokens):
|
def __init__(self, tokens, filename, code):
|
||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
self.pos = 0
|
self.pos = 0
|
||||||
|
self.filename = filename
|
||||||
|
self.code = code
|
||||||
|
|
||||||
def current(self):
|
def current(self):
|
||||||
if self.pos < len(self.tokens):
|
if self.pos < len(self.tokens):
|
||||||
@ -97,29 +120,46 @@ class Parser:
|
|||||||
if token.type == token_type:
|
if token.type == token_type:
|
||||||
self.pos += 1
|
self.pos += 1
|
||||||
return token
|
return token
|
||||||
raise RuntimeError(f"Expected token {token_type} at position {token.pos} but got {token.type}")
|
raise RuntimeError(f"Expected token {token_type} at position {stringify_code_pos(self.code, self.pos)} but got {token.type}. File {self.filename}")
|
||||||
|
|
||||||
# Grammar production: <option> ::= option <string> ( eval <string> | <dmenu> )
|
# Grammar production: <option> ::= option <string> ( eval <string> | <dmenu> ) | import options <string (filepath)>
|
||||||
def parse_option(self):
|
def parse_option_and_append(self, targ_arr):
|
||||||
self.eat('OPTION')
|
if self.current().type != None and self.current().type == 'OPTION':
|
||||||
label_tkn = self.eat('STRING')
|
self.eat('OPTION')
|
||||||
label = restore_string(label_tkn.value)
|
label_tkn = self.eat('STRING')
|
||||||
if '\t' in label or '\n' in label or '\000' in label:
|
label = restore_string(label_tkn.value)
|
||||||
raise RuntimeError(f"Label of option at {label_tkn.pos} contains illegal character")
|
if '\t' in label or '\n' in label or '\000' in label:
|
||||||
current = self.current()
|
raise RuntimeError(f"Label of option at {stringify_code_pos(self.code, label_tkn.pos)} contains illegal character. File {self.filename}")
|
||||||
if current is None:
|
current = self.current()
|
||||||
raise RuntimeError("Expected end-of-file after option")
|
if current is None:
|
||||||
if current.type == 'EVAL':
|
raise RuntimeError("Expected end-of-file after option")
|
||||||
self.eat('EVAL')
|
if current.type == 'EVAL':
|
||||||
action_command_tkn = self.eat('STRING').value
|
self.eat('EVAL')
|
||||||
action = restore_string(action_command_tkn)
|
action_command_tkn = self.eat('STRING')
|
||||||
if '\n' in action or '\000' in action:
|
action = restore_string(action_command_tkn.value)
|
||||||
raise RuntimeError(f"Command at {action_command_tkn.pos} contains illegal character")
|
if '\n' in action or '\000' in action:
|
||||||
elif current.type == 'DMENU':
|
raise RuntimeError(f"Command at {stringify_code_pos(self.code, action_command_tkn.pos)} contains illegal character. File {self.filename}")
|
||||||
action = self.parse_dmenu()
|
elif current.type == 'DMENU':
|
||||||
|
action = self.parse_dmenu()
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unexpected token {current.type} in option at position {stringify_code_pos(self.code, current.pos)}. File {self.filename}")
|
||||||
|
targ_arr.append(OptionInDmenu(label, action))
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unexpected token {current.type} in option at position {current.pos}")
|
self.eat('IMPORT')
|
||||||
return OptionInDmenu(label, action)
|
self.eat('OPTIONS')
|
||||||
|
filepath_tkn = self.eat('STRING')
|
||||||
|
imported_file_path = restore_string(filepath_tkn.value)
|
||||||
|
child_file = resolve_path_relative_to_file(self.filename, imported_file_path)
|
||||||
|
|
||||||
|
print(f"Importing dmenul file: {child_file}")
|
||||||
|
with open(child_file, 'r') as f:
|
||||||
|
child_file_code = f.read()
|
||||||
|
|
||||||
|
child_file_tokens = tokenize(child_file_code, self.filename)
|
||||||
|
parsed_child = Parser(child_file_tokens, child_file, child_file_code).parse_file_as_dmenul()
|
||||||
|
|
||||||
|
targ_arr += parsed_child
|
||||||
|
|
||||||
|
|
||||||
# Grammar production: <dmenu> ::= dmenu <string> \( <option> * \)
|
# Grammar production: <dmenu> ::= dmenu <string> \( <option> * \)
|
||||||
def parse_dmenu(self):
|
def parse_dmenu(self):
|
||||||
@ -128,16 +168,22 @@ class Parser:
|
|||||||
self.eat('LPAREN')
|
self.eat('LPAREN')
|
||||||
options = []
|
options = []
|
||||||
while self.current() is not None and self.current().type != 'RPAREN':
|
while self.current() is not None and self.current().type != 'RPAREN':
|
||||||
options.append(self.parse_option())
|
self.parse_option_and_append(options)
|
||||||
self.eat('RPAREN')
|
self.eat('RPAREN')
|
||||||
return ActionDmenu(prompt, options)
|
return ActionDmenu(prompt, options)
|
||||||
|
|
||||||
def parse_file_as_dmenu(self):
|
def parse_file_as_dmenu(self):
|
||||||
node = self.parse_dmenu()
|
node = self.parse_dmenu()
|
||||||
if self.current() is not None:
|
if self.current() is not None:
|
||||||
raise RuntimeError("Extra tokens after valid dmenu")
|
raise RuntimeError("Extra tokens after valid dmenu root. File {self.filename}")
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
def parse_file_as_dmenul(self):
|
||||||
|
options = []
|
||||||
|
while self.current() is not None:
|
||||||
|
self.parse_option_and_append(options)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
def shell_escape(s: str) -> str:
|
def shell_escape(s: str) -> str:
|
||||||
return "'" + s.replace("'", "'\\''") + "'"
|
return "'" + s.replace("'", "'\\''") + "'"
|
||||||
@ -158,8 +204,7 @@ def generate_tsv_file(dpath: str, cur_tsv_name, options: List[OptionInDmenu]):
|
|||||||
if isinstance(opt.action, ActionDmenu):
|
if isinstance(opt.action, ActionDmenu):
|
||||||
child_tsv_name = f"{cur_tsv_name}-{i}"
|
child_tsv_name = f"{cur_tsv_name}-{i}"
|
||||||
generate_tsv_file(dpath, child_tsv_name, opt.action.options)
|
generate_tsv_file(dpath, child_tsv_name, opt.action.options)
|
||||||
return f"{opt.label}\t<{shell_escape(os.path.join(dpath, child_tsv_name))} dmenu " + \
|
return f"{opt.label}\t<{shell_escape(os.path.join(dpath, child_tsv_name))} dmenu -p {shell_escape(opt.action.prompt)} | tail -n 1 | sh"
|
||||||
f"-p {shell_escape(opt.action.prompt)} | tail -n 1 | sh"
|
|
||||||
else:
|
else:
|
||||||
return f"{opt.label}\t{opt.action}"
|
return f"{opt.label}\t{opt.action}"
|
||||||
|
|
||||||
@ -169,22 +214,24 @@ def generate_tsv_file(dpath: str, cur_tsv_name, options: List[OptionInDmenu]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def compile_dmenuctree(dpath: str, parsed_tree: ActionDmenu):
|
def compile_dmenuctree(dpath: str, parsed_tree: ActionDmenu, init_shell_file_path: str):
|
||||||
print(f"dmenu tabfiles root is {dpath}")
|
print(f"dmenu tabfiles root is {dpath}")
|
||||||
main_script = os.path.join(dpath, "invoke.sh")
|
main_script = os.path.join(dpath, "invoke.sh")
|
||||||
cur_tsv_name = "tsv"
|
cur_tsv_name = "tsv"
|
||||||
invoke_snippet = f"#!/bin/sh\n" + \
|
invoke_snippet = "#!/bin/sh\n"
|
||||||
f"<{shell_escape(os.path.join(dpath, cur_tsv_name))} dmenu -p {shell_escape(parsed_tree.prompt)} " + \
|
if len(init_shell_file_path) > 0:
|
||||||
f"| tail -n 1 | sh"
|
invoke_snippet += f"[ -f {shell_escape(init_shell_file_path)} ] && source {shell_escape(init_shell_file_path)} \n"
|
||||||
|
invoke_snippet += f"<{shell_escape(os.path.join(dpath, cur_tsv_name))} dmenu -p {shell_escape(parsed_tree.prompt)} | tail -n 1 | sh\n"
|
||||||
write_to_file(main_script, invoke_snippet)
|
write_to_file(main_script, invoke_snippet)
|
||||||
make_file_executable(main_script)
|
make_file_executable(main_script)
|
||||||
generate_tsv_file(dpath, cur_tsv_name, parsed_tree.options)
|
generate_tsv_file(dpath, cur_tsv_name, parsed_tree.options)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser_arg = argparse.ArgumentParser(description="Parse a file using custom dmenu syntax")
|
parser_arg = argparse.ArgumentParser(description="Compile .dmenu file into directory of executable scripts")
|
||||||
parser_arg.add_argument("input_file", help="Path to the input file")
|
parser_arg.add_argument("input_file", help="Path to the input file")
|
||||||
parser_arg.add_argument("dest_dir", help="Destination directory path")
|
parser_arg.add_argument("dest_dir", help="Destination directory path")
|
||||||
|
parser_arg.add_argument("init_shell_file_path", help="Invoke.sh script will source this shell script before running this dmenu conglomeration")
|
||||||
args = parser_arg.parse_args()
|
args = parser_arg.parse_args()
|
||||||
|
|
||||||
if not os.path.exists(args.input_file):
|
if not os.path.exists(args.input_file):
|
||||||
@ -192,20 +239,21 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not os.path.isdir(args.dest_dir):
|
if not os.path.isdir(args.dest_dir):
|
||||||
os.mkdir(args.dest_dir)
|
os.mkdir(args.dest_dir)
|
||||||
|
init_shell_file_path = args.init_shell_file_path if args.init_shell_file_path else ""
|
||||||
|
|
||||||
with open(args.input_file, 'r') as f:
|
with open(args.input_file, 'r') as f:
|
||||||
code = f.read()
|
code = f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tokens = tokenize(code)
|
tokens = tokenize(code, args.input_file)
|
||||||
parsed_tree = Parser(tokens).parse_file_as_dmenu()
|
parsed_tree = Parser(tokens, args.input_file, code).parse_file_as_dmenu()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during parsing: {e}", file=sys.stderr)
|
print(f"Parsing error: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
compile_dmenuctree(os.path.realpath(args.dest_dir), parsed_tree)
|
compile_dmenuctree(os.path.realpath(args.dest_dir), parsed_tree, resolve_path_relative_to_file(args.input_file, init_shell_file_path))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user