From 1598ec4af2a664327965e4366bdea6c2f88fe2a7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Aug 2024 18:49:03 +0800 Subject: [PATCH 001/122] Merge `setvar` and set_options handlers into one function --- src/clitheme/_generator/__init__.py | 11 ++--------- src/clitheme/_generator/_dataclass.py | 11 +++++++++++ src/clitheme/_generator/_entries_parser.py | 7 +------ src/clitheme/_generator/_header_parser.py | 7 +------ src/clitheme/_generator/_manpage_parser.py | 7 +------ src/clitheme/_generator/_substrules_parser.py | 7 +------ 6 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 618b624..b2b34b1 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -34,17 +34,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info global path obj=_dataclass.GeneratorObject(file_content=file_content, custom_infofile_name=custom_infofile_name, filename=filename, path=path, silence_warn=silence_warn) - ## Main code while obj.goto_next_line(): first_phrase=obj.lines_data[obj.lineindex].split()[0] - # process header and main sections here - if first_phrase=="set_options": - obj.check_enough_args(obj.lines_data[obj.lineindex].split(), 2) - obj.handle_set_global_options(obj.subst_variable_content(_globalvar.splitarray_to_string(obj.lines_data[obj.lineindex].split()[1:])).split(), really_really_global=True) - elif first_phrase.startswith("setvar:"): - obj.check_enough_args(obj.lines_data[obj.lineindex].split(), 2) - obj.handle_set_variable(obj.lines_data[obj.lineindex], really_really_global=True) - elif first_phrase=="begin_header" or first_phrase==r"{header_section}": + if first_phrase=="begin_header" or first_phrase==r"{header_section}": _header_parser.handle_header_section(obj, first_phrase) elif first_phrase=="begin_main" or first_phrase==r"{entries_section}": _entries_parser.handle_entries_section(obj, first_phrase) @@ -52,6 +44,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info _substrules_parser.handle_substrules_section(obj, first_phrase) elif first_phrase==r"{manpage_section}": _manpage_parser.handle_manpage_section(obj, first_phrase) + elif obj.handle_setters(really_really_global=True): pass else: obj.handle_invalid_phrase(first_phrase) def is_content_parsed() -> bool: diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 7c939d1..a12ffc1 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -189,6 +189,17 @@ class GeneratorObject(_handlers.DataHandlers): if "substesc" in self.global_options.keys() and self.global_options['substesc']==True: target_content=self.handle_substesc(target_content) return target_content + def handle_setters(self, really_really_global: bool=False) -> bool: + # Handle set_options and setvar + phrases=self.lines_data[self.lineindex].split() + if phrases[0]=="set_options": + self.check_enough_args(phrases, 2) + self.handle_set_global_options(self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split(), really_really_global) + elif phrases[0].startswith("setvar:"): + self.check_enough_args(phrases, 2) + self.handle_set_variable(self.lines_data[self.lineindex], really_really_global) + else: return False + return True ## sub-block processing functions diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index 1faae2a..718a716 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -46,12 +46,7 @@ def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.check_enough_args(phrases, 2) entry_name=_globalvar.extract_content(obj.lines_data[obj.lineindex]) obj.handle_entry(entry_name, start_phrase=phrases[0], end_phrase="[/entry]" if phrases[0]=="[entry]" else "end_entry") - elif phrases[0]=="set_options": - obj.check_enough_args(phrases, 2) - obj.handle_set_global_options(obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) - elif phrases[0].startswith("setvar:"): - obj.check_enough_args(phrases, 2) - obj.handle_set_variable(obj.lines_data[obj.lineindex]) + elif obj.handle_setters(): pass elif phrases[0]==end_phrase: obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("entries") diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index 94dc8e2..93fddbc 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -51,12 +51,7 @@ def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ file_name,\ content,obj.lineindex+1,re.sub(r'_block$','',phrases[0])) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 - elif phrases[0]=="set_options": - obj.check_enough_args(phrases, 2) - obj.handle_set_global_options(obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) - elif phrases[0].startswith("setvar:"): - obj.check_enough_args(phrases, 2) - obj.handle_set_variable(obj.lines_data[obj.lineindex]) + elif obj.handle_setters(): pass elif phrases[0]==end_phrase: obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("header") diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 85e802c..1a1a40e 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -89,12 +89,7 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.check_extra_args(p, 1, use_exact_count=True) break else: obj.handle_invalid_phrase(phrases[0]) - elif phrases[0]=="set_options": - obj.check_enough_args(phrases, 2) - obj.handle_set_global_options(obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) - elif phrases[0].startswith("setvar:"): - obj.check_enough_args(phrases, 2) - obj.handle_set_variable(obj.lines_data[obj.lineindex]) + elif obj.handle_setters(): pass elif phrases[0]==end_phrase: obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("manpage") diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index c5ce821..9e9e719 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -84,12 +84,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness, "foreground_only": command_filter_foreground_only} match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) obj.handle_entry(match_pattern, start_phrase=phrases[0], end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) - elif phrases[0]=="set_options": - obj.check_enough_args(phrases, 2) - obj.handle_set_global_options(obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) - elif phrases[0].startswith("setvar:"): - obj.check_enough_args(phrases, 2) - obj.handle_set_variable(obj.lines_data[obj.lineindex]) + elif obj.handle_setters(): pass elif phrases[0]==end_phrase: obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("substrules") -- Gitee From 21ac29005250ef625c36365e9012f7533658989d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Aug 2024 19:59:20 +0800 Subject: [PATCH 002/122] Properly handle multiple read fragments in one output line --- src/clitheme/exec/output_handler_posix.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 08fe8cc..a23ff6c 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -160,14 +160,17 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): orig_data=unfinished_output orig_line=orig_data[0] if unfinished_output[3]==foreground_pid: - output_lines.append((orig_line+line,is_stderr,do_subst_operation, foreground_pid)) + # Modify existing line data instead of directly pushing it + # to better handle multiple fragments in a single line + line=orig_line+line else: + # Shouldn't join them together in this case output_lines.append(unfinished_output) - output_lines.append((line,is_stderr,do_subst_operation, foreground_pid)) + # Don't push the current line just yet; leave it for newline check unfinished_output=None output_handled=True # if last line of output did not end with newlines, leave for next iteration - elif x==len(lines)-1 and not line.endswith(newlines): + if x==len(lines)-1 and not line.endswith(newlines): unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid) output_handled=True else: -- Gitee From 664441037e7f3f5390c26b7e0103cda8227c8348 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Aug 2024 20:44:17 +0800 Subject: [PATCH 003/122] Ignore backspace outputs from processing in input content Match common backspace patterns when comparing `last_input_content` --- src/clitheme/exec/output_handler_posix.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index a23ff6c..9ee3dba 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -241,11 +241,15 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): line: bytes=line_data[0] # check if the output is user input. if yes, skip # print(last_input_content, line) # DEBUG - if line==last_input_content: line_data=(line_data[0],line_data[1],False, line_data[3]); last_input_content=None - elif last_input_content!=None and last_input_content.startswith(line): - line_data=(line_data[0],line_data[1],False, line_data[3]) - last_input_content=last_input_content[len(line):] - else: last_input_content=None + if last_input_content!=None: + input_match_expression: bytes=re.escape(last_input_content).replace(b'\x7f', rb"(\x08 \x08|\x08\x1b\[K)") # type: ignore + input_startswith=b'^'+input_match_expression + input_equals=input_startswith+b'$' + if re.search(input_equals, line)!=None: line_data=(line_data[0],line_data[1],False, line_data[3]); last_input_content=None + elif re.search(input_startswith, line)!=None: + line_data=(line_data[0],line_data[1],False, line_data[3]) + last_input_content=last_input_content[len(line):] + else: last_input_content=None # subst operation and print output os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) except: -- Gitee From b5c36c7b94ed10d3fc4207a2c7db4d1343446d16 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Aug 2024 22:18:59 +0800 Subject: [PATCH 004/122] Improving handling of exceptions in output reader thread - Add thread exception handling testing flag - Remove an outdated comment in code --- src/clitheme/exec/output_handler_posix.py | 133 +++++++++++++--------- 1 file changed, 78 insertions(+), 55 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 9ee3dba..4dd0eab 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -23,6 +23,7 @@ import re import sqlite3 import time import threading +from typing import Optional from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print @@ -56,7 +57,6 @@ def _process_debug(lines: list, debug_mode: list, is_stderr: bool=False, matched except UnicodeDecodeError: line=re.sub(bytes(match_pattern, 'utf-8'), bytes(sub_pattern, 'utf-8'), line) line+=b'\x1b[0m' if "normal" in debug_mode: - # e.g. o{ ; o> line=bytes(f"\x1b[0;1;{'31' if is_stderr else '32'}{';47' if matched else ''}{';37;41' if failed else ''}m"+('e' if is_stderr else 'o')+'\x1b[0;1m'+(">")+"\x1b[0m ",'utf-8')+line+b"\x1b[0m" final_lines.append(line) return final_lines @@ -129,68 +129,93 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): message=f"\x1b[1m! \x1b[{'32' if foreground_pid==process.pid else '31'}mForeground: \x1b[4m{'True' if foreground_pid==process.pid else 'False'} ({foreground_pid})\x1b[0m\n" os.write(sys.stdout.fileno(), bytes(message, 'utf-8')) last_tcgetpgrp=foreground_pid + thread_exception_handled=False + def handle_exception(exc: Optional[Exception]=None): + nonlocal thread_exception_handled; thread_exception_handled=True + if prev_attrs!=None: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes + print("\x1b[0m\x1b[?1;1000;1001;1002;1003;1005;1006;1015;1016l", end='') # reset color and mouse reporting + _labeled_print(fd.reof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) + if exc!=None: raise exc + else: raise + thread_debug=0 + def thread_debug_handle(sig, frame): + nonlocal thread_debug + if sig==signal.SIGUSR1: thread_debug=1 + elif sig==signal.SIGUSR2: thread_debug=2 + signal.signal(signal.SIGUSR1, thread_debug_handle) + signal.signal(signal.SIGUSR2, thread_debug_handle) def output_read_loop(): nonlocal last_input_content, output_lines unfinished_output=None - while True: - readsize=io.DEFAULT_BUFFER_SIZE - fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.002)[0] - # Handle user input from stdin - if sys.stdin in fds: - data=os.read(sys.stdin.fileno(), readsize) - # if input from last iteration did not end with newlines, append new content - if last_input_content!=None: last_input_content+=data - else: last_input_content=data - try: os.write(stdout_fd, data) - except OSError: pass # Handle input/output error that might occur after program terminates - # Handle output from stdout and stderr - output_handled=False - def handle_output(is_stderr: bool): - nonlocal unfinished_output, output_lines, output_handled + try: + while True: + # Testing thread exception handling + nonlocal thread_debug + if thread_debug==1: raise Exception + elif thread_debug==2: break + + readsize=io.DEFAULT_BUFFER_SIZE + fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.002)[0] + # Handle user input from stdin + if sys.stdin in fds: + data=os.read(sys.stdin.fileno(), readsize) + # if input from last iteration did not end with newlines, append new content + if last_input_content!=None: last_input_content+=data + else: last_input_content=data + try: os.write(stdout_fd, data) + except OSError: pass # Handle input/output error that might occur after program terminates + # Handle output from stdout and stderr + output_handled=False + def handle_output(is_stderr: bool): + nonlocal unfinished_output, output_lines, output_handled - data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) - foreground_pid=os.tcgetpgrp(stdout_fd) - do_subst_operation=True - lines=data.splitlines(keepends=True) - for x in range(len(lines)): - line=lines[x] - # if unfinished output exists, append new content to it - if x==0 and unfinished_output!=None: - orig_data=unfinished_output - orig_line=orig_data[0] - if unfinished_output[3]==foreground_pid: - # Modify existing line data instead of directly pushing it - # to better handle multiple fragments in a single line - line=orig_line+line + data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) + foreground_pid=os.tcgetpgrp(stdout_fd) + do_subst_operation=True + lines=data.splitlines(keepends=True) + for x in range(len(lines)): + line=lines[x] + # if unfinished output exists, append new content to it + if x==0 and unfinished_output!=None: + orig_data=unfinished_output + orig_line=orig_data[0] + if unfinished_output[3]==foreground_pid: + # Modify existing line data instead of directly pushing it + # to better handle multiple fragments in a single line + line=orig_line+line + else: + # Shouldn't join them together in this case + output_lines.append(unfinished_output) + # Don't push the current line just yet; leave it for newline check + unfinished_output=None + output_handled=True + # if last line of output did not end with newlines, leave for next iteration + if x==len(lines)-1 and not line.endswith(newlines): + unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid) + output_handled=True else: - # Shouldn't join them together in this case - output_lines.append(unfinished_output) - # Don't push the current line just yet; leave it for newline check - unfinished_output=None - output_handled=True - # if last line of output did not end with newlines, leave for next iteration - if x==len(lines)-1 and not line.endswith(newlines): - unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid) - output_handled=True - else: - output_lines.append((line,is_stderr,do_subst_operation, foreground_pid)) + output_lines.append((line,is_stderr,do_subst_operation, foreground_pid)) - if stdout_fd in fds: handle_output(is_stderr=False) - if stderr_fd in fds: handle_output(is_stderr=True) - # if no unfinished_output is handled by handle_output, append the unfinished output if exists - if not output_handled and unfinished_output!=None: - output_lines.append(unfinished_output) - unfinished_output=None + if stdout_fd in fds: handle_output(is_stderr=False) + if stderr_fd in fds: handle_output(is_stderr=True) + # if no unfinished_output is handled by handle_output, append the unfinished output if exists + if not output_handled and unfinished_output!=None: + output_lines.append(unfinished_output) + unfinished_output=None - if process.poll()!=None: break + if process.poll()!=None: break + except: handle_exception() - thread=threading.Thread(target=output_read_loop, daemon=True) + thread=threading.Thread(target=output_read_loop, name="output-reader", daemon=True) thread.start() while True: try: if process.poll()!=None and len(output_lines)==0: break - + if not thread.is_alive(): + if not thread_exception_handled: handle_exception(RuntimeError("Output read loop terminated unexpectedly")) + else: return 1 + if thread_exception_handled: continue # Prevent conflict with setting terminal attributes # update terminal attributes from what the program sets try: attrs=termios.tcgetattr(stdout_fd) @@ -252,11 +277,9 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): else: last_input_content=None # subst operation and print output os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) - except: - if prev_attrs!=None: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes - print("\x1b[0m\x1b[?1;1000;1001;1002;1003;1005;1006;1015;1016l", end='') # reset color and mouse reporting - _labeled_print(fd.reof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) - raise + except: + if not thread_exception_handled: handle_exception() + else: raise if prev_attrs!=None: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes exit_code=process.poll() try: -- Gitee From e7806564372cc4811182645442ebe4278894b3a6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Aug 2024 22:40:04 +0800 Subject: [PATCH 005/122] Add SIGUSR to cspell dictionary --- cspell.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index f1a7d2a..d82ea61 100644 --- a/cspell.json +++ b/cspell.json @@ -19,7 +19,7 @@ "substvar", "substesc", "PKGBUILD", "MANPATH", "tcgetattr", "tcsetattr", "getpid", "setsid", "setitimer", "tcgetpgrp", "tcsetpgrp", - "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", "SIGALRM", "ITIMER", "SIGTSTP", "SIGSTOP", "SIGCONT", + "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", "SIGALRM", "ITIMER", "SIGTSTP", "SIGSTOP", "SIGCONT", "SIGUSR1", "SIGUSR2", "showchars", "keepends", "appname", "domainapp", "sanitycheck", "debugmode", "splitarray", "disablelang" -- Gitee From 2971b7cfb4a5556cb1ab21f37f97325f403442f7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Aug 2024 23:45:15 +0800 Subject: [PATCH 006/122] Process lines ending with carriage return altogether Minimize visible cursor blinks --- src/clitheme/exec/output_handler_posix.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 4dd0eab..df61f87 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -174,6 +174,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): foreground_pid=os.tcgetpgrp(stdout_fd) do_subst_operation=True lines=data.splitlines(keepends=True) + unfinished_cr_lines=None for x in range(len(lines)): line=lines[x] # if unfinished output exists, append new content to it @@ -190,12 +191,26 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): # Don't push the current line just yet; leave it for newline check unfinished_output=None output_handled=True + if unfinished_cr_lines!=None: line=unfinished_cr_lines+line + # If line ends with carriage return ('\r') and is not end of content, process them together + # to minimize visible cursor blinks due to delay in unfinished output processing + if line.endswith(b'\r') and x!=len(lines)-1: + unfinished_cr_lines=line + continue + else: unfinished_cr_lines=None # if last line of output did not end with newlines, leave for next iteration if x==len(lines)-1 and not line.endswith(newlines): unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid) output_handled=True else: - output_lines.append((line,is_stderr,do_subst_operation, foreground_pid)) + last_index=0 + for x in range(len(line)): + if line[x]==ord(b'\r') or x==len(line)-1: + try: + if line[x+1]==ord(b'\n'): continue + except IndexError: pass + output_lines.append((line[last_index:x+1], is_stderr, do_subst_operation, foreground_pid)) + last_index=x+1 if stdout_fd in fds: handle_output(is_stderr=False) if stderr_fd in fds: handle_output(is_stderr=True) -- Gitee From 187630f2d0a798565d50f6f179e700fa4d066c2c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 3 Aug 2024 09:40:23 +0800 Subject: [PATCH 007/122] Improve output cleanup handling in `handle_exception` --- src/clitheme/exec/output_handler_posix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index df61f87..39eac54 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -133,7 +133,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): def handle_exception(exc: Optional[Exception]=None): nonlocal thread_exception_handled; thread_exception_handled=True if prev_attrs!=None: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes - print("\x1b[0m\x1b[?1;1000;1001;1002;1003;1005;1006;1015;1016l", end='') # reset color and mouse reporting + print("\x1b[0m\x1b[?1;1000;1001;1002;1003;1005;1006;1015;1016l\n\x1b[J", end='') # reset color, mouse reporting, and clear the rest of the screen _labeled_print(fd.reof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) if exc!=None: raise exc else: raise -- Gitee From 6294cfda8d7e7444760932b5be787941c83fe472 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 3 Aug 2024 09:44:41 +0800 Subject: [PATCH 008/122] Don't join unfinished output if not on same output stream If unfinished output is not on the same stdout/stderr stream as the current output, don't join them just like if they have different foreground pid. --- src/clitheme/exec/output_handler_posix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 39eac54..01ccb23 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -181,7 +181,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): if x==0 and unfinished_output!=None: orig_data=unfinished_output orig_line=orig_data[0] - if unfinished_output[3]==foreground_pid: + if unfinished_output[3]==foreground_pid and unfinished_output[1]==is_stderr: # Modify existing line data instead of directly pushing it # to better handle multiple fragments in a single line line=orig_line+line -- Gitee From 00a1770fc03bff942311b90ce9fe060e57e894f9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 3 Aug 2024 13:46:19 +0800 Subject: [PATCH 009/122] Update version (v2.0-dev20240802) --- PKGBUILD | 2 +- debian/changelog | 6 ++++++ src/clitheme/_version.py | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 570264f..41c4371 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_beta2 +pkgver=2.0_dev20240802 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 90b611f..e8df609 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +clitheme (2.0-dev20240802-1) unstable; urgency=low + + * In development, see commit logs for changes + + -- swiftycode <3291929745@qq.com> Sat, Aug 3 2024 13:44:00 +0800 + clitheme (2.0-beta2-1) unstable; urgency=low New features diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 80fc14d..59ce8cb 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -5,11 +5,11 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-beta2" +__version__="2.0-dev20240802" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_beta2" +version_main="2.0_dev20240802" version_buildnumber=1 \ No newline at end of file -- Gitee From 8519de47cc16df59f2e20bd82861ca500f7ad6c0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 3 Aug 2024 15:00:09 +0800 Subject: [PATCH 010/122] Renaming "Generating data" to "Processing files" --- README.en.md | 6 +++--- README.md | 6 +++--- src/clitheme/cli.py | 4 ++-- src/clitheme/strings/cli-strings.clithemedef.txt | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.en.md b/README.en.md index 44c3b08..0515d7d 100644 --- a/README.en.md +++ b/README.en.md @@ -19,9 +19,9 @@ test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int ``` ```plaintext $ clitheme apply-theme clang-theme.clithemedef.txt -==> Generating data... -Successfully generated data -==> Applying theme...Success +==> Processing files... +Successfully processed files +==> Applying theme... Theme applied successfully ``` ```plaintext diff --git a/README.md b/README.md index dd6ddda..502fd16 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int ``` ```plaintext $ clitheme apply-theme clang-theme.clithemedef.txt -==> Generating data... -Successfully generated data -==> Applying theme...Success +==> Processing files... +Successfully processed files +==> Applying theme... Theme applied successfully ``` ```plaintext diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 3a7fe8b..0eae6c8 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -60,7 +60,7 @@ def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_te if not (inp=="y" or inp=="yes"): return 1 if overlay: print(f.reof("overlay-msg", "Overlay specified")) - print(f.reof("generating-data", "==> Generating data...")) + print(f.reof("processing-files", "==> Processing files...")) index=1 generate_path=True if overlay: @@ -116,7 +116,7 @@ def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_te finally: sys.stdout=orig_stdout # failsafe just in case something didn't work if print_progress: print(line_prefix+f.reof("all-finished", "> All finished")) - print(f.reof("generate-data-success", "Successfully generated data")) + print(f.reof("process-files-success", "Successfully processed files")) global last_data_path; last_data_path=final_path if preserve_temp or generate_only: if os.name=="nt": diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 8b91fa5..5b4947a 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -69,9 +69,9 @@ in_domainapp swiftycode clitheme # locale:default Overlay specified locale:zh_CN 已使用数据叠加模式 [/entry] - [entry] generating-data - # locale:default ==> Generating data... - locale:zh_CN ==> 正在生成数据... + [entry] processing-files + # locale:default ==> Processing files... + locale:zh_CN ==> 正在处理文件... [/entry] [entry] processing-file # locale:default > Processing file {filename}... @@ -81,9 +81,9 @@ in_domainapp swiftycode clitheme # locale:default > All finished locale:zh_CN > 已处理全部文件 [/entry] - [entry] generate-data-success - # locale:default Successfully generated data - locale:zh_CN 已成功生成数据 + [entry] process-files-success + # locale:default Successfully processed files + locale:zh_CN 已成功处理文件 [/entry] [entry] view-temp-dir # locale:default View at {path} -- Gitee From 1fc920f7965703b59f9e97b865abb7afbb6c0239 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 3 Aug 2024 18:58:12 +0800 Subject: [PATCH 011/122] Fix item order in theme-info processing functions Compare their integer values if possible --- src/clitheme/_globalvar.py | 11 ++++++++++- src/clitheme/cli.py | 5 +++-- src/clitheme/exec/__init__.py | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index b99c5e5..15c8764 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -209,4 +209,13 @@ def handle_set_themedef(fr, debug_name: str): fr.global_debugmode=prev_mode if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1]), file=sys.__stdout__) handle_exception() - finally: sys.stdout=orig_stdout \ No newline at end of file + finally: sys.stdout=orig_stdout +def result_sort_cmp(obj1,obj2) -> int: + cmp1='';cmp2='' + try: + cmp1=int(obj1); cmp2=int(obj2) + except ValueError: + cmp1=obj1; cmp2=obj2 + if cmp1>cmp2: return 1 + elif cmp1==cmp2: return 0 + else: return -1 \ No newline at end of file diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 0eae6c8..220d86c 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -16,6 +16,7 @@ import sys import shutil import re import io +import functools from . import _globalvar, _generator, frontend from ._globalvar import make_printable as fmt # A shorter alias of the function @@ -182,7 +183,7 @@ def get_current_theme_info(): print(f.reof("no-theme", "No theme currently set")) return 1 lsdir_result=os.listdir(search_path) - lsdir_result.sort(reverse=True) # sort by latest installed + lsdir_result.sort(reverse=True, key=functools.cmp_to_key(_globalvar.result_sort_cmp)) # sort by latest installed lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): @@ -254,7 +255,7 @@ def update_theme(): if not os.path.isdir(search_path): print(fi.reof("no-theme-err", "Error: no theme currently set")) return 1 - lsdir_result=os.listdir(search_path); lsdir_result.sort() + lsdir_result=os.listdir(search_path); lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 0da7ec3..bce713d 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -15,6 +15,7 @@ import os import re import io import shutil +import functools def _labeled_print(msg: str): print("[clitheme-exec] "+msg) @@ -46,7 +47,7 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) # gather files search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname if not os.path.isdir(search_path): raise Exception(search_path+" not directory") - lsdir_result=os.listdir(search_path); lsdir_result.sort() + lsdir_result=os.listdir(search_path); lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 -- Gitee From e5b7b03b7d2bb894867fc2c4399086a4291b8a1d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 3 Aug 2024 19:51:16 +0800 Subject: [PATCH 012/122] Separate each output in `get-current-theme-info` with newline Make the output more readable --- src/clitheme/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 220d86c..782ac7f 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -238,6 +238,8 @@ def get_current_theme_info(): print(f.reof("supported-apps-str", "Supported apps:")) for app in supported_apps.split(): print(f.feof("list-item", "• {content}", content=fmt(app.strip()))) + + print() # Separate each entry with an empty line return 0 def update_theme(): -- Gitee From 35495e3a721263bdd34bf76e6bc986953fa9ea22 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 5 Aug 2024 10:29:54 +0800 Subject: [PATCH 013/122] Move input content checking into `output_read_loop` --- src/clitheme/exec/output_handler_posix.py | 26 ++++++++++++----------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 01ccb23..aad8a6c 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -168,11 +168,24 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): # Handle output from stdout and stderr output_handled=False def handle_output(is_stderr: bool): - nonlocal unfinished_output, output_lines, output_handled + nonlocal unfinished_output, output_lines, output_handled, last_input_content data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) foreground_pid=os.tcgetpgrp(stdout_fd) do_subst_operation=True + # check if the output is user input. if yes, skip + if last_input_content!=None: + input_match_expression: bytes=re.escape(last_input_content).replace(b'\x7f', rb"(\x08 \x08|\x08\x1b\[K)") # type: ignore + input_startswith=b'^'+input_match_expression + input_equals=input_startswith+b'$' + # print(last_input_content, data, re.search(input_equals, data)!=None) # DEBUG + if re.search(input_equals, data)!=None: + do_subst_operation=False + last_input_content=None + # elif re.search(input_startswith, data)!=None: + # do_subst_operation=False + # last_input_content=last_input_content[len(data):] + else: last_input_content=None lines=data.splitlines(keepends=True) unfinished_cr_lines=None for x in range(len(lines)): @@ -279,17 +292,6 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): while not len(output_lines)==0: line_data=output_lines.pop(0) line: bytes=line_data[0] - # check if the output is user input. if yes, skip - # print(last_input_content, line) # DEBUG - if last_input_content!=None: - input_match_expression: bytes=re.escape(last_input_content).replace(b'\x7f', rb"(\x08 \x08|\x08\x1b\[K)") # type: ignore - input_startswith=b'^'+input_match_expression - input_equals=input_startswith+b'$' - if re.search(input_equals, line)!=None: line_data=(line_data[0],line_data[1],False, line_data[3]); last_input_content=None - elif re.search(input_startswith, line)!=None: - line_data=(line_data[0],line_data[1],False, line_data[3]) - last_input_content=last_input_content[len(line):] - else: last_input_content=None # subst operation and print output os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) except: -- Gitee From 2db003610793fde069cd079ca4378f068e5c74f6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 5 Aug 2024 12:16:08 +0800 Subject: [PATCH 014/122] Poll terminal attributes from stdout Works even if standard input is piped --- src/clitheme/exec/output_handler_posix.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index aad8a6c..d234839 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -73,7 +73,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): # Prevent apps from using "less" or "more" as pager, as it won't work here env['PAGER']="cat" prev_attrs=None - try: prev_attrs=termios.tcgetattr(sys.stdin) + try: prev_attrs=termios.tcgetattr(sys.stdout) except termios.error: pass main_pid=os.getpid() process: subprocess.Popen @@ -132,7 +132,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): thread_exception_handled=False def handle_exception(exc: Optional[Exception]=None): nonlocal thread_exception_handled; thread_exception_handled=True - if prev_attrs!=None: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes + if prev_attrs!=None: termios.tcsetattr(sys.stdout, termios.TCSADRAIN, prev_attrs) # restore previous attributes print("\x1b[0m\x1b[?1;1000;1001;1002;1003;1005;1006;1015;1016l\n\x1b[J", end='') # reset color, mouse reporting, and clear the rest of the screen _labeled_print(fd.reof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) if exc!=None: raise exc @@ -249,7 +249,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): attrs=termios.tcgetattr(stdout_fd) # disable canonical and echo mode (enable cbreak) no matter what attrs[3] &= ~(termios.ICANON | termios.ECHO) - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, attrs) + termios.tcsetattr(sys.stdout, termios.TCSADRAIN, attrs) except termios.error: pass # update terminal size try: @@ -297,7 +297,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): except: if not thread_exception_handled: handle_exception() else: raise - if prev_attrs!=None: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes + if prev_attrs!=None: termios.tcsetattr(sys.stdout, termios.TCSADRAIN, prev_attrs) # restore previous attributes exit_code=process.poll() try: if exit_code!=None and exit_code<0: # Terminated by signal -- Gitee From 25d618cea7db52593381ddd14d0a45b5dff74db9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 5 Aug 2024 14:14:23 +0800 Subject: [PATCH 015/122] Properly handle pipe redirection in `clitheme-exec` --- src/clitheme/exec/output_handler_posix.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index d234839..87858bf 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -15,6 +15,7 @@ import io import pty import select import termios +import stat import fcntl import signal import struct @@ -97,6 +98,13 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): elif sig==signal.SIGINT: os.write(stdout_fd, b'\x03') # '^C' character try: + # Detect if stdin is piped (e.g. cat file|clitheme-exec grep content) + stdin_fd=stdout_slave + if stat.S_ISFIFO(os.stat(sys.stdin.fileno()).st_mode): + r,w=os.pipe() + os.write(w, open(sys.stdin.fileno(), 'rb').read()) + os.close(w) + stdin_fd=r def child_init(): # Must start new session or some programs might not work properly os.setsid() @@ -107,7 +115,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): tmp_fd = os.open(os.ttyname(stdout_slave), os.O_RDWR) tmp_fd2 = os.open(os.ttyname(stderr_slave), os.O_RDWR) os.close(tmp_fd);os.close(tmp_fd2) - process=subprocess.Popen(command, stdin=stdout_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env, preexec_fn=child_init) + process=subprocess.Popen(command, stdin=stdin_fd, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env, preexec_fn=child_init) except: _labeled_print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=_globalvar.make_printable(str(sys.exc_info()[1])))) _globalvar.handle_exception() @@ -156,7 +164,8 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): elif thread_debug==2: break readsize=io.DEFAULT_BUFFER_SIZE - fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.002)[0] + try: fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.002)[0] + except OSError: fds=select.select([stdout_fd, stderr_fd], [], [], 0.002)[0] # Handle user input from stdin if sys.stdin in fds: data=os.read(sys.stdin.fileno(), readsize) -- Gitee From 46c075f6b1705a283fbb5986cc19059e1f2e5434 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 5 Aug 2024 23:32:15 +0800 Subject: [PATCH 016/122] Update version (v2.0-dev20240805) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 41c4371..fae640e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240802 +pkgver=2.0_dev20240805 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index e8df609..eb5f837 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240802-1) unstable; urgency=low +clitheme (2.0-dev20240805-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, Aug 3 2024 13:44:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, Aug 5 2024 23:28:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 59ce8cb..a9333c1 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -5,11 +5,11 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240802" +__version__="2.0-dev20240805" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240802" +version_main="2.0_dev20240805" version_buildnumber=1 \ No newline at end of file -- Gitee From 459dcd0fdb7167ee88291e1cfc2b600f4271e8f2 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 7 Aug 2024 19:22:21 +0800 Subject: [PATCH 017/122] Support content variables in specifying options --- src/clitheme/_generator/_dataclass.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index a12ffc1..6e6b821 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -82,6 +82,7 @@ class GeneratorObject(_handlers.DataHandlers): final_options={} if merge_global_options!=0: final_options=copy.copy(self.global_options if merge_global_options==1 else self.really_really_global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options + options_data=self.subst_variable_content(_globalvar.splitarray_to_string(options_data)).split() for each_option in options_data: option_name=re.sub(r"^(no)?(?P.+?)(:.+)?$", r"\g", each_option) option_name_preserve_no=re.sub(r"^(?P.+?)(:.+)?$", r"\g", each_option) -- Gitee From cc8385904b4a05e98bcaad0fcc11333739ab080c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 7 Aug 2024 19:30:17 +0800 Subject: [PATCH 018/122] Change whitespace in `extract_content` regex to `\s` Better safe than sorry --- src/clitheme/_globalvar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 15c8764..edb06b1 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -129,7 +129,7 @@ def splitarray_to_string(split_content) -> str: final+=phrase+" " return final.strip() def extract_content(line_content: str, begin_phrase_count: int=1) -> str: - results=re.search(r"(?:[ \t]*.+?[ \t]+){"+str(begin_phrase_count)+r"}(?P.+)", line_content.strip()) + results=re.search(r"(?:\s*.+?\s+){"+str(begin_phrase_count)+r"}(?P.+)", line_content.strip()) if results==None: raise ValueError("Match content failed (no matches)") else: return results.groupdict()['content'] def make_printable(content: str) -> str: -- Gitee From d35bc483bb904ab8ecdb7b4d3a0b73a4fc7c525e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 7 Aug 2024 20:52:04 +0800 Subject: [PATCH 019/122] Rename `substall` option to `substallstreams` --- cspell.json | 2 +- src/clitheme/_generator/_dataclass.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index d82ea61..3c0e3f7 100644 --- a/cspell.json +++ b/cspell.json @@ -15,7 +15,7 @@ "exactcmdmatch", "smartcmdmatch", "endmatchhere", "leadtabindents", "leadspaces", "strictcmdmatch", "normalcmdmatch", "exactcmdmatch", "smartcmdmatch", - "subststdoutonly", "subststderronly", "substall", "foregroundonly", + "subststdoutonly", "subststderronly", "substallstreams", "foregroundonly", "substvar", "substesc", "PKGBUILD", "MANPATH", "tcgetattr", "tcsetattr", "getpid", "setsid", "setitimer", "tcgetpgrp", "tcsetpgrp", diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 6e6b821..c1d7734 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -24,7 +24,7 @@ class GeneratorObject(_handlers.DataHandlers): lead_indent_options=["leadtabindents", "leadspaces"] content_subst_options=["substesc","substvar"] command_filter_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"]+["foregroundonly"] - subst_limiting_options=["subststdoutonly", "subststderronly", "substall"]+["endmatchhere"] + subst_limiting_options=["subststdoutonly", "subststderronly", "substallstreams"]+["endmatchhere"] # options used in handle_block_input block_input_options=lead_indent_options+content_subst_options -- Gitee From bf66d7d8b09749ae354ecebe4276df187461195d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 7 Aug 2024 21:58:43 +0800 Subject: [PATCH 020/122] Add message prompts with reading files --- src/clitheme/cli.py | 18 +++++++++++++++++- .../strings/cli-strings.clithemedef.txt | 14 +++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 782ac7f..7fda99c 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -16,6 +16,7 @@ import sys import shutil import re import io +import stat import functools from . import _globalvar, _generator, frontend from ._globalvar import make_printable as fmt # A shorter alias of the function @@ -323,14 +324,29 @@ def _handle_help_message(full_help: bool=False): def _get_file_contents(file_paths: list) -> list: fi=frontend.FetchDescriptor(subsections="cli apply-theme") content_list=[] + line_prefix="\x1b[2K\r" # clear current line content and move cursor to beginning for i in range(len(file_paths)): path=file_paths[i] try: + print(line_prefix+fi.feof("reading-file","==> Reading file {filename}...", filename=f"({i+1}/{len(file_paths)})"), end='') + # Detect standard input + is_stdin=False + try: + if os.stat(path).st_ino==os.stat(sys.stdin.fileno()).st_ino: + is_stdin=True + print("\n"+fi.reof("reading-stdin-note", "Reading from standard input")) + if not stat.S_ISFIFO(os.stat(path).st_mode): + print(fi.feof("stdin-interactive-finish-prompt", "Input file content here and press {shortcut} to finish", shortcut="CTRL-D" if os.name=="posix" else "CTRL-Z+")) + except: pass content_list.append(open(path, 'r', encoding="utf-8").read()) + if is_stdin: print() # Print an extra newline + except KeyboardInterrupt: + print();exit(130) except: - print(fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ + print("\n"+fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ index=str(i+1), message=path+": "+fmt(str(sys.exc_info()[1])))) raise + print(line_prefix, end='') return content_list def main(cli_args: list): diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 5b4947a..602c3b0 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -45,6 +45,18 @@ in_domainapp swiftycode clitheme # apply-theme 和 generate-data 指令 # apply-theme and generate-data commands in_subsection cli apply-theme + [entry] reading-file + # locale:default ==> Reading file {filename}... + locale:zh_CN ==> 正在读取文件{filename}... + [/entry] + [entry] reading-stdin-note + # locale:default Reading from standard input + locale:zh_CN 正在从stdin读取文件 + [/entry] + [entry] stdin-interactive-finish-prompt + # locale:default Input file content here and press {shortcut} to finish + locale:zh_CN 在此输入文件内容,并按下{shortcut}以结束 + [/entry] [entry] generate-data-msg # locale:default The theme data will be generated from the following definition files in the following order: locale:zh_CN 主题定义数据将会从以下顺序的主题定义文件生成: @@ -75,7 +87,7 @@ in_domainapp swiftycode clitheme [/entry] [entry] processing-file # locale:default > Processing file {filename}... - locale:zh_CN > 正在处理文件{filename} + locale:zh_CN > 正在处理文件{filename}... [/entry] [entry] all-finished # locale:default > All finished -- Gitee From 5e16f90a7a3404ea67fa3fe6e3bbdd751bc4abc9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 7 Aug 2024 22:19:25 +0800 Subject: [PATCH 021/122] Raise `SystemExit` in `handle_exception` Exit with the correct return code by doing this --- src/clitheme/_globalvar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index edb06b1..357cbc6 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -189,6 +189,8 @@ def handle_exception(): env_var="CLITHEME_SHOW_TRACEBACK" if env_var in os.environ and os.environ[env_var]=="1": raise + # Let "exit" function calls work + if sys.exc_info()[0]==SystemExit: raise def handle_set_themedef(fr, debug_name: str): prev_mode=False -- Gitee From b714c282829a1e65b750529554ff7d39bc02ebb1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 7 Aug 2024 22:39:02 +0800 Subject: [PATCH 022/122] Change error messages to reflect "Processing files" wording --- src/clitheme/cli.py | 2 +- src/clitheme/strings/cli-strings.clithemedef.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 7fda99c..f8ad7d1 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -106,7 +106,7 @@ def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_te if generator_msgs.getvalue()!='': # end='' because the pipe value already contains a newline due to the print statements print(generator_msgs.getvalue(), end='') - print(f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ + print(f.feof("process-files-error", "[File {index}] An error occurred while processing files:\n{message}", \ index=str(i+1), message=str(sys.exc_info()[1]))) if type(exc)==SyntaxError: _globalvar.handle_exception() else: raise # Always raise exception if other error occurred in _generator diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 602c3b0..f142894 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -139,13 +139,13 @@ in_domainapp swiftycode clitheme 请移除当前数据和重新设定主题后重试 [/locale] [/entry] - [entry] generate-data-error + [entry] process-files-error # [locale] default - # [File {index}] An error occurred while generating the data: + # [File {index}] An error occurred while processing files: # {message} # [/locale] [locale] zh_CN - [文件{index}] 生成数据时发生了错误: + [文件{index}] 处理文件时发生了错误: {message} [/locale] [/entry] -- Gitee From 0fd6039ec02e3ae5ecd4eb1640445803de3c3b12 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 7 Aug 2024 22:54:11 +0800 Subject: [PATCH 023/122] Update version (v2.0-dev20240807) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index fae640e..4458d6b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240805 +pkgver=2.0_dev20240807 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index eb5f837..a2d92ba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240805-1) unstable; urgency=low +clitheme (2.0-dev20240807-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, Aug 5 2024 23:28:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, Aug 7 2024 22:53:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index a9333c1..1cfc328 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -5,11 +5,11 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240805" +__version__="2.0-dev20240807" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240805" +version_main="2.0_dev20240807" version_buildnumber=1 \ No newline at end of file -- Gitee From 8d6ec2d637b00b93b6c182f932bf82e3a65215ab Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Aug 2024 14:32:20 +0800 Subject: [PATCH 024/122] Rename and move README files Move `README.en.md` into `.github/README.md` so English version is displayed at GitHub page, while Chinese version is displayed at Gitee page. --- README-frontend.en.md => .github/README-frontend.en.md | 0 README.en.md => .github/README.md | 2 +- README.md | 2 +- pyproject.toml | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename README-frontend.en.md => .github/README-frontend.en.md (100%) rename README.en.md => .github/README.md (99%) diff --git a/README-frontend.en.md b/.github/README-frontend.en.md similarity index 100% rename from README-frontend.en.md rename to .github/README-frontend.en.md diff --git a/README.en.md b/.github/README.md similarity index 99% rename from README.en.md rename to .github/README.md index 0515d7d..a8285ff 100644 --- a/README.en.md +++ b/.github/README.md @@ -1,6 +1,6 @@ # clitheme - Command line customization utility -[中文](./README.md) | **English** +[中文](../README.md) | **English** --- diff --git a/README.md b/README.md index 502fd16..cb03c61 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # clitheme - 命令行自定义工具 -**中文** | [English](./README.en.md) +**中文** | [English](.github/README.md) --- diff --git a/pyproject.toml b/pyproject.toml index d39afd0..a51adba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ authors = [ { name="swiftycode", email="3291929745@qq.com" }, ] description = "A text theming library for command line applications" -readme = "README.en.md" +readme = ".github/README.md" license = {text = "GNU General Public License v3 (GPLv3)"} requires-python = ">=3.8" classifiers = [ -- Gitee From 5307f24ee2f1e58ab0ef31b4426463867ed92db4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Aug 2024 15:06:11 +0800 Subject: [PATCH 025/122] Rename options in `clitheme-exec` - Rename `--debug-showchars` to `--showchars` - Rename `--debug-foreground` to `--foreground-stat` - Rename `--debug-nosubst` to `--nosubst` - Require `--debug` when using `--debug-newlines` --- .github/README.md | 6 ++--- README.md | 6 ++--- docs/clitheme-exec.1 | 10 ++++----- src/clitheme/exec/__init__.py | 21 +++++++++--------- .../strings/exec-strings.clithemedef.txt | 22 +++++++++++-------- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/.github/README.md b/.github/README.md index a8285ff..38f5326 100644 --- a/.github/README.md +++ b/.github/README.md @@ -64,10 +64,10 @@ Get the command line output, including any terminal control characters: ```plaintext # --debug: Add a marker at the beginning of each line; contains information on whether the output is stdout/stderr ("o>" or "e>") -# --debug-showchars: Show terminal control characters in the output -# --debug-nosubst: Even if a theme is set, do not apply substitution rules (get original output content) +# --showchars: Show terminal control characters in the output +# --nosubst: Even if a theme is set, do not apply substitution rules (get original output content) -$ clitheme-exec --debug --debug-showchars --debug-nosubst clang test.c +$ clitheme-exec --debug --showchars --nosubst clang test.c e> {{ESC}}[1mtest.c:1:1: {{ESC}}[0m{{ESC}}[0;1;31merror: {{ESC}}[0m{{ESC}}[1munknown type name 'bool'{{ESC}}[0m\r\n e> bool *func(int *a) {\r\n e> {{ESC}}[0;1;32m^\r\n diff --git a/README.md b/README.md index cb03c61..aacc91f 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,10 @@ test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者 ```plaintext # --debug:在每一行的输出前添加标记;包含输出是否为stdout或stderr的信息("o>"或"e>") -# --debug-showchars:显示输出中的终端控制符号 -# --debug-nosubst:即使设定了主题,不对输出应用替换规则(获取原始输出) +# --showchars:显示输出中的终端控制符号 +# --nosubst:即使设定了主题,不对输出应用替换规则(获取原始输出) -$ clitheme-exec --debug --debug-showchars --debug-nosubst clang test.c +$ clitheme-exec --debug --showchars --nosubst clang test.c e> {{ESC}}[1mtest.c:1:1: {{ESC}}[0m{{ESC}}[0;1;31merror: {{ESC}}[0m{{ESC}}[1munknown type name 'bool'{{ESC}}[0m\r\n e> bool *func(int *a) {\r\n e> {{ESC}}[0;1;32m^\r\n diff --git a/docs/clitheme-exec.1 b/docs/clitheme-exec.1 index 8016e8c..93e4666 100644 --- a/docs/clitheme-exec.1 +++ b/docs/clitheme-exec.1 @@ -2,7 +2,7 @@ .SH NAME clitheme\-exec \- match and substitute output of a command .SH SYNOPSIS -.B clitheme-exec [--debug] [--debug-color] [--debug-newlines] [--debug-showchars] \fIcommand\fR +.B clitheme-exec [--debug] [--debug-color] [--debug-newlines] [--showchars] \fIcommand\fR .SH DESCRIPTION \fIclitheme-exec\fR substitutes the output of the specified command with substitution rules defined through a theme definition file. The current theme definition on the system is controlled through \fIclitheme(1)\fR. .SH OPTIONS @@ -24,7 +24,7 @@ For stdout, yellow color is applied. For stderr, red color is applied. .B --debug-newlines For output that does not end on a newline, display the output ending with newlines. .TP -.B --debug-showchars +.B --showchars Display various control characters in plain text. The following characters will be displayed as its code name: .P .RS 14 @@ -39,7 +39,7 @@ Display various control characters in plain text. The following characters will - Bell character (\\x07) .RE .TP -.B --debug-foreground +.B --foreground-stat When the foreground status of the main process changes (determined using value of \fItcgetpgrp(3)\fR system call), output a message showing this change. Such change happens when running a shell in \fIclitheme-exec\fR and running another command in that shell. @@ -50,10 +50,10 @@ Such change happens when running a shell in \fIclitheme-exec\fR and running anot - "! Foreground: True ()": Process enters (re-enters) foreground state .RE .TP -.B --debug-nosubst +.B --nosubst Even if a theme is set, do not perform any output substitution operations. -This is useful if you are trying to get the original output of the command with control characters displayed on-screen using \fI--debug-showchars\fR. +This is useful if you are trying to get the original output of the command with control characters displayed on-screen using \fI--showchars\fR. .SH SEE ALSO \fIclitheme(1)\fR \ No newline at end of file diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index bce713d..d009877 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -85,15 +85,15 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) def _handle_help_message(full_help: bool=False): fd2=frontend.FetchDescriptor(subsections="exec help-message") print(fd2.reof("usage-str", "Usage:")) - print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--debug-showchars] [--debug-foreground] [--debug-nosubst] [command]") + print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--showchars] [--foreground-stat] [--nosubst] [command]") if not full_help: return print(fd2.reof("options-str", "Options:")) print("\t"+fd2.reof("options-debug", "--debug: Display indicator at the beginning of each read output by line")) + print("\t\t"+fd2.reof("options-debug-newlines", "--debug-newlines: Use newlines to display output that does not end on a newline")) print("\t"+fd2.reof("options-debug-color", "--debug-color: Apply color on output; used to determine stdout or stderr (BETA: stdout/stderr not implemented)")) - print("\t"+fd2.reof("options-debug-newlines", "--debug-newlines: Use newlines to display output that does not end on a newline")) - print("\t"+fd2.reof("options-debug-showchars", "--debug-showchars: Display various control characters in plain text")) - print("\t"+fd2.reof("options-debug-foreground", "--debug-foreground: Display message when the foreground status of the process changes (value of tcgetpgrp)")) - print("\t"+fd2.reof("options-debug-nosubst", "--debug-nosubst: Do not perform any output substitutions even if a theme is set")) + print("\t"+fd2.reof("options-showchars", "--showchars: Display various control characters in plain text")) + print("\t"+fd2.reof("options-foreground-stat", "--foreground-stat: Display message when the foreground status of the process changes (value of tcgetpgrp)")) + print("\t"+fd2.reof("options-nosubst", "--nosubst: Do not perform any output substitutions even if a theme is set")) def _handle_error(message: str): print(message) @@ -121,24 +121,25 @@ def main(arguments: list): debug_mode.append("color") elif arg=="--debug-newlines": debug_mode.append("newlines") - elif arg=="--debug-showchars": + elif arg in ("--showchars", "--debug-showchars"): debug_mode.append("showchars") - elif arg=="--debug-foreground": + elif arg in ("--foreground-stat", "--debug-foreground"): debug_mode.append("foreground") - elif arg=="--debug-nosubst": + elif arg in ("--nosubst", "--debug-nosubst"): subst=False elif arg=="--help": showhelp=True else: return _handle_error(fd.feof("unknown-option-err", "Error: unknown option \"{phrase}\"", phrase=arg)) + if "newlines" in debug_mode and not "normal" in debug_mode: + return _handle_error(fd.reof("debug-newlines-not-with-debug", "Error: \"--debug-newlines\" must be used with \"--debug\" option")) if len(arguments)<=1+argcount: if showhelp: _handle_help_message(full_help=True) return 0 else: _handle_help_message() - _handle_error(fd.reof("no-command-err", "Error: no command specified")) - return 1 + return _handle_error(fd.reof("no-command-err", "Error: no command specified")) # check database if subst: if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index cff2092..2bf02ba 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -38,17 +38,17 @@ in_domainapp swiftycode clitheme # locale:default --debug-newlines: Use newlines to display output that does not end on a newline locale:zh_CN --debug-newlines:使用新的一行来显示没有新行的输出 [/entry] - [entry] options-debug-showchars - # locale:default --debug-showchars: Display various control characters in plain text - locale:zh_CN --debug-showchars:使用明文显示终端控制符号 + [entry] options-showchars + # locale:default --showchars: Display various control characters in plain text + locale:zh_CN --showchars:使用明文显示终端控制符号 [/entry] - [entry] options-debug-foreground - # locale:default --debug-foreground: Display message when the foreground status of the process changes (value of tcgetpgrp) - locale:zh_CN --debug-foreground: 当进程的前台状态(tcgetpgrp的返回值)变动时,显示提示信息 + [entry] options-foreground-stat + # locale:default --foreground-stat: Display message when the foreground status of the process changes (value of tcgetpgrp) + locale:zh_CN --foreground-stat: 当进程的前台状态(tcgetpgrp的返回值)变动时,显示提示信息 [/entry] - [entry] options-debug-nosubst - # locale:default --debug-nosubst: Do not perform any output substitutions even if a theme is set - locale:zh_CN --debug-nosubst:不进行任何输出替换,即使已设定主题 + [entry] options-nosubst + # locale:default --nosubst: Do not perform any output substitutions even if a theme is set + locale:zh_CN --nosubst:不进行任何输出替换,即使已设定主题 [/entry] in_subsection exec [entry] help-usage-prompt @@ -59,6 +59,10 @@ in_domainapp swiftycode clitheme # locale:default Error: unknown option "{phrase}" locale:zh_CN 错误:未知选项"{phrase}" [/entry] + [entry] debug-newlines-not-with-debug + # locale:default Error: "--debug-newlines" must be used with "--debug" option + locale:zh_CN 错误:"--debug-newlines"选项必须与"--debug"选项同时指定 + [/entry] [entry] no-command-err # locale:default Error: no command specified locale:zh_CN 错误:未指定命令 -- Gitee From 954249f396a2069bb40f78f911d216683a9f71c4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Aug 2024 15:07:00 +0800 Subject: [PATCH 026/122] Add missing options in synopsis section --- docs/clitheme-exec.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/clitheme-exec.1 b/docs/clitheme-exec.1 index 93e4666..047e489 100644 --- a/docs/clitheme-exec.1 +++ b/docs/clitheme-exec.1 @@ -2,7 +2,7 @@ .SH NAME clitheme\-exec \- match and substitute output of a command .SH SYNOPSIS -.B clitheme-exec [--debug] [--debug-color] [--debug-newlines] [--showchars] \fIcommand\fR +.B clitheme-exec [--debug] [--debug-color] [--debug-newlines] [--showchars] [--foreground-stat] [--nosubst] \fIcommand\fR .SH DESCRIPTION \fIclitheme-exec\fR substitutes the output of the specified command with substitution rules defined through a theme definition file. The current theme definition on the system is controlled through \fIclitheme(1)\fR. .SH OPTIONS -- Gitee From 785535a33a205e479c0addf619caf1046394978b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Aug 2024 22:48:19 +0800 Subject: [PATCH 027/122] Use a clearer syntax for matching multiple phrases in parsers Use `phrase in ("phrase1", "phrase2")` instead of `phrase=="phrase1" or phrase=="phrase2"` syntax for more conciseness and clarity --- src/clitheme/_generator/__init__.py | 4 ++-- src/clitheme/_generator/_dataclass.py | 2 +- src/clitheme/_generator/_entries_parser.py | 2 +- src/clitheme/_generator/_header_parser.py | 8 ++++---- src/clitheme/_generator/_substrules_parser.py | 2 +- src/clitheme/cli.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index b2b34b1..71456cc 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -36,9 +36,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info while obj.goto_next_line(): first_phrase=obj.lines_data[obj.lineindex].split()[0] - if first_phrase=="begin_header" or first_phrase==r"{header_section}": + if first_phrase in ("begin_header", r"{header_section}"): _header_parser.handle_header_section(obj, first_phrase) - elif first_phrase=="begin_main" or first_phrase==r"{entries_section}": + elif first_phrase in ("begin_main", r"{entries_section}"): _entries_parser.handle_entries_section(obj, first_phrase) elif first_phrase==r"{substrules_section}": _substrules_parser.handle_substrules_section(obj, first_phrase) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index c1d7734..aa533f6 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -328,7 +328,7 @@ class GeneratorObject(_handlers.DataHandlers): if locale!="default": target_entry+="__"+locale entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) - elif phrases[0]=="locale_block" or phrases[0]=="[locale]": + elif phrases[0] in ("locale_block", "[locale]"): self.check_enough_args(phrases, 2) locales=self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() begin_line_number=self.lineindex+1+1 diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index 718a716..3dcbd4e 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -42,7 +42,7 @@ def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): elif phrases[0]=="unset_subsection": obj.check_extra_args(phrases, 1, use_exact_count=True) obj.in_subsection="" - elif phrases[0]=="entry" or phrases[0]=="[entry]": + elif phrases[0] in ("entry", "[entry]"): obj.check_enough_args(phrases, 2) entry_name=_globalvar.extract_content(obj.lines_data[obj.lineindex]) obj.handle_entry(entry_name, start_phrase=phrases[0], end_phrase="[/entry]" if phrases[0]=="[entry]" else "end_entry") diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index 93fddbc..9b1af4c 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -19,7 +19,7 @@ def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" while obj.goto_next_line(): phrases=obj.lines_data[obj.lineindex].split() - if phrases[0]=="name" or phrases[0]=="version" or phrases[0]=="description": + if phrases[0] in ("name", "version", "description"): obj.check_enough_args(phrases, 2) content=_globalvar.extract_content(obj.lines_data[obj.lineindex]) if phrases[0]=="description": content=obj.handle_singleline_content(content) @@ -28,20 +28,20 @@ def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ _globalvar.generator_info_filename.format(info=phrases[0]),\ content,obj.lineindex+1,phrases[0]) # e.g. [...]/theme-info/1/clithemeinfo_name - elif phrases[0]=="locales" or phrases[0]=="supported_apps": + elif phrases[0] in ("locales", "supported_apps"): obj.check_enough_args(phrases, 2) content=obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() obj.write_infofile_newlines( \ obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ _globalvar.generator_info_v2filename.format(info=phrases[0]),\ content,obj.lineindex+1,phrases[0]) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 - elif phrases[0]=="locales_block" or phrases[0]=="supported_apps_block" or phrases[0]=="description_block" or phrases[0]=="[locales]" or phrases[0]=="[supported_apps]" or phrases[0]=="[description]": + elif phrases[0] in ("locales_block", "supported_apps_block", "description_block", "[locales]", "[supported_apps]", "[description]"): obj.check_extra_args(phrases, 1, use_exact_count=True) # handle block input content=""; file_name="" endphrase="end_block" if not phrases[0].endswith("_block"): endphrase=phrases[0].replace("[", "[/") - if phrases[0]=="description_block" or phrases[0]=="[description]": + if phrases[0] in ("description_block", "[description]"): content=obj.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase=endphrase) file_name=_globalvar.generator_info_filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) else: diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 9e9e719..10b12bd 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -79,7 +79,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str elif phrases[0]=="unset_filter_command": obj.check_extra_args(phrases, 1, use_exact_count=True) command_filters=None - elif phrases[0]=="[substitute_string]" or phrases[0]=="[substitute_regex]": + elif phrases[0] in ("[substitute_string]", "[substitute_regex]"): obj.check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness, "foreground_only": command_filter_foreground_only} match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index f8ad7d1..0bfadd9 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -371,9 +371,9 @@ def main(cli_args: list): if len(cli_args)>count: exit(_handle_usage_error(f.reof("too-many-arguments", "Error: too many arguments"), arg_first)) - if cli_args[1]=="apply-theme" or cli_args[1]=="generate-data" or cli_args[1]=="generate-data-hierarchy": + if cli_args[1] in ("apply-theme", "generate-data", "generate-data-hierarchy"): check_enough_args(3) - generate_only=(cli_args[1]=="generate-data" or cli_args[1]=="generate-data-hierarchy") + generate_only=(cli_args[1] in ("generate-data", "generate-data-hierarchy")) paths=[] overlay=False preserve_temp=False -- Gitee From 3661463a32c67a84e836d47ed5df0e525fd363dd Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Aug 2024 23:15:51 +0800 Subject: [PATCH 028/122] Modify command descriptions in cli help message --- src/clitheme/cli.py | 12 ++++----- .../strings/cli-strings.clithemedef.txt | 26 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 0bfadd9..773c04a 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -313,13 +313,13 @@ def _handle_help_message(full_help: bool=False): if not full_help: return print(fd.reof("options-str", "Options:")) print("\t"+fd.reof("options-apply-theme", - "apply-theme: Applies the given theme definition file(s) into the current system.\nSpecify --overlay to append value definitions in the file(s) onto the current data.\nSpecify --preserve-temp to prevent the temporary directory from removed after the operation. (Debug purposes only)").replace("\n", "\n\t\t")) - print("\t"+fd.reof("options-get-current-theme-info", "get-current-theme-info: Outputs detailed information about the currently applied theme")) + "apply-theme: Apply the given theme definition file(s).\nSpecify --overlay to add file(s) onto the current data.\nSpecify --preserve-temp to preserve the temporary directory after the operation. (Debug purposes only)").replace("\n", "\n\t\t")) + print("\t"+fd.reof("options-get-current-theme-info", "get-current-theme-info: Show information about the currently applied theme(s)")) print("\t"+fd.reof("options-unset-current-theme", "unset-current-theme: Remove the current theme data from the system")) - print("\t"+fd.reof("options-update-theme", "update-theme: Re-applies the theme definition files specified in the previous \"apply-theme\" command (previous commands if --overlay is used)")) - print("\t"+fd.reof("options-generate-data", "generate-data: [Debug purposes only] Generates a data hierarchy from specified theme definition files in a temporary directory")) - print("\t"+fd.reof("options-version", "--version: Outputs the current version of clitheme")) - print("\t"+fd.reof("options-help", "--help: Display this help message")) + print("\t"+fd.reof("options-update-theme", "update-theme: Re-apply the theme definition files specified in the previous \"apply-theme\" command (previous commands if --overlay is used)")) + print("\t"+fd.reof("options-generate-data", "generate-data: [Debug purposes only] Generate a data hierarchy from specified theme definition files in a temporary directory")) + print("\t"+fd.reof("options-version", "--version: Show the current version of clitheme")) + print("\t"+fd.reof("options-help", "--help: Show this help message")) def _get_file_contents(file_paths: list) -> list: fi=frontend.FetchDescriptor(subsections="cli apply-theme") diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index f142894..0ce2e98 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -241,38 +241,38 @@ in_domainapp swiftycode clitheme [/entry] [entry] options-apply-theme # [locale] default - # apply-theme: Applies the given theme definition file(s) into the current system. - # Specify --overlay to append value definitions in the file(s) onto the current data. - # Specify --preserve-temp to prevent the temporary directory from removed after the operation. (Debug purposes only) + # apply-theme: Apply the given theme definition file(s). + # Specify --overlay to add file(s) onto the current data. + # Specify --preserve-temp to preserve the temporary directory after the operation. (Debug purposes only) # [/locale] [locale] zh_CN - apply-theme:将指定的主题定义文件应用到当前系统中 - 指定"--overlay"选项以保留当前主题数据的情况下应用(添加到当前数据中) + apply-theme:应用指定的主题定义文件 + 指定"--overlay"选项以将文件添加到当前数据中 指定"--preserve-temp"以保留该操作生成的临时目录(调试用途) [/locale] [/entry] [entry] options-get-current-theme-info - # locale:default get-current-theme-info: Outputs detailed information about the currently applied theme - locale:zh_CN get-current-theme-info:输出当前主题设定的详细信息 + # locale:default get-current-theme-info: Show information about the currently applied theme(s) + locale:zh_CN get-current-theme-info:显示当前主题设定的详细信息 [/entry] [entry] options-unset-current-theme # locale:default unset-current-theme: Remove the current theme data from the system locale:zh_CN unset-current-theme:取消设定当前主题定义和数据 [/entry] [entry] options-update-theme - # locale:default update-theme: Re-applies the theme definition files specified in the previous "apply-theme" command (previous commands if --overlay is used) + # locale:default update-theme: Re-apply the theme definition files specified in the previous \"apply-theme\" command (previous commands if --overlay is used) locale:zh_CN update-theme:重新应用上一个apply-theme操作中指定的主题定义文件(前几次操作,如果使用了"--overlay") [/entry] [entry] options-generate-data - # locale:default generate-data: [Debug purposes only] Generates a data hierarchy from specified theme definition files in a temporary directory + # locale:default generate-data: [Debug purposes only] Generate a data hierarchy from specified theme definition files in a temporary directory locale:zh_CN generate-data:【仅供调试用途】对于指定的主题定义文件在临时目录中生成一个数据结构 [/entry] [entry] options-version - # locale:default --version: Outputs the current version of clitheme - locale:zh_CN --version:输出clitheme的当前版本信息 + # locale:default --version: Show the current version of clitheme + locale:zh_CN --version:显示clitheme的当前版本信息 [/entry] [entry] options-help - # locale:default --help: Display this help message - locale:zh_CN --help:输出这个帮助提示 + # locale:default --help: Show this help message + locale:zh_CN --help:显示这个帮助提示 [/entry] {/entries_section} -- Gitee From 1303efbed648bb85709499afbf57c94a507b6199 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Aug 2024 23:22:32 +0800 Subject: [PATCH 029/122] Process file paths display in `apply-theme` through make_printable --- src/clitheme/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 773c04a..17d6831 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -50,7 +50,7 @@ def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_te print(f.reof("apply-theme-msg", "The following definition files will be applied in the following order: ")) for i in range(len(filenames)): path=filenames[i] - print("\t{}: {}".format(str(i+1), path)) + print("\t{}: {}".format(str(i+1), fmt(path))) if not generate_only: if os.path.isdir(_globalvar.clitheme_root_data_path) and overlay==False: print(f.reof("overwrite-notice", "The existing theme data will be overwritten if you continue.")) -- Gitee From f96fdde3589e73aee5e5c800b1d7f966e1b00999 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 9 Aug 2024 00:00:16 +0800 Subject: [PATCH 030/122] Optimize progress output on `apply-theme` - Remove redundant `> All finished` prompt - Display `Processing file (1/1)` prompt if one file is specified --- src/clitheme/cli.py | 13 ++++++------- src/clitheme/strings/cli-strings.clithemedef.txt | 4 ---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 17d6831..bb32403 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -83,8 +83,9 @@ def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_te shutil.copytree(_globalvar.clitheme_root_data_path, _generator.path) generate_path=False final_path: str - line_prefix="\x1b[2K\r " # clear current line content and move cursor to beginning - print_progress=len(file_contents)>1 + line_prefix=f"\x1b[2K\r{' '*4}" # clear current line content and move cursor to beginning + print_progress=True #len(file_contents)>1 + newline="\n" if print_progress else "" orig_stdout=sys.stdout # Prevent interference with other code piping stdout for i in range(len(file_contents)): if print_progress: @@ -101,7 +102,7 @@ def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_te index+=1 except Exception as exc: sys.stdout=orig_stdout - print(("\n" if print_progress else ""), end='') + print(newline, end='') # Print any output messages if an error occurs if generator_msgs.getvalue()!='': # end='' because the pipe value already contains a newline due to the print statements @@ -114,11 +115,9 @@ def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_te else: sys.stdout=orig_stdout # restore standard output if generator_msgs.getvalue()!='': - print(("\n" if print_progress else "")+generator_msgs.getvalue(), end='') + print(newline+generator_msgs.getvalue(), end='') finally: sys.stdout=orig_stdout # failsafe just in case something didn't work - if print_progress: - print(line_prefix+f.reof("all-finished", "> All finished")) - print(f.reof("process-files-success", "Successfully processed files")) + print((line_prefix.rstrip(' ') if print_progress else "")+f.reof("process-files-success", "Successfully processed files")) global last_data_path; last_data_path=final_path if preserve_temp or generate_only: if os.name=="nt": diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 0ce2e98..90bc1d1 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -89,10 +89,6 @@ in_domainapp swiftycode clitheme # locale:default > Processing file {filename}... locale:zh_CN > 正在处理文件{filename}... [/entry] - [entry] all-finished - # locale:default > All finished - locale:zh_CN > 已处理全部文件 - [/entry] [entry] process-files-success # locale:default Successfully processed files locale:zh_CN 已成功处理文件 -- Gitee From f6b5cc06d294da5a0259050a583b66738120279b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 9 Aug 2024 00:04:01 +0800 Subject: [PATCH 031/122] Update version (v2.0-dev20240808) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 4458d6b..7651a5d 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240807 +pkgver=2.0_dev20240808 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index a2d92ba..72912b8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240807-1) unstable; urgency=low +clitheme (2.0-dev20240808-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, Aug 7 2024 22:53:00 +0800 + -- swiftycode <3291929745@qq.com> Fri, Aug 9 2024 00:03:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 1cfc328..5956609 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -5,11 +5,11 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240807" +__version__="2.0-dev20240808" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240807" +version_main="2.0_dev20240808" version_buildnumber=1 \ No newline at end of file -- Gitee From 62da39862db2377884f46bab40a097acf7c5e745 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 10 Aug 2024 12:36:44 +0800 Subject: [PATCH 032/122] Fix tuple index reference in sanity check error handling --- src/clitheme/_generator/_dataclass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index aa533f6..f18a554 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -378,7 +378,7 @@ class GeneratorObject(_handlers.DataHandlers): else: # Prevent leading . & prevent /,\ in entry name if _globalvar.sanity_check(match_pattern)==False: - self.handle_error(self.fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(entry[5]), sanitycheck_msg=_globalvar.sanity_check_error_message)) + self.handle_error(self.fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(entry[4]), sanitycheck_msg=_globalvar.sanity_check_error_message)) encountered_ids.add(entry[3]) if is_substrules: try: -- Gitee From 57c99415278e249de39a66f131b1da2f53c865a9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 10 Aug 2024 22:22:50 +0800 Subject: [PATCH 033/122] Continuously read piped standard input in clitheme-exec --- src/clitheme/exec/output_handler_posix.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 87858bf..c030176 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -102,8 +102,17 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): stdin_fd=stdout_slave if stat.S_ISFIFO(os.stat(sys.stdin.fileno()).st_mode): r,w=os.pipe() - os.write(w, open(sys.stdin.fileno(), 'rb').read()) - os.close(w) + def pipe_forward(): + # Background thread to forward stdin to subprocess pipe + nonlocal r,w + while True: + select.select([sys.stdin], [], []) + d=os.read(sys.stdin.fileno(), io.DEFAULT_BUFFER_SIZE) + if d==b'': # stdin is closed + os.close(w); break + os.write(w,d) + t=threading.Thread(target=pipe_forward, daemon=True) + t.start() stdin_fd=r def child_init(): # Must start new session or some programs might not work properly -- Gitee From 364f21b8f48e5570cd69f0057702797bfa67c500 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 10 Aug 2024 22:31:05 +0800 Subject: [PATCH 034/122] Use stdout for get_terminal_size Make it work if stdin is piped --- src/clitheme/exec/output_handler_posix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index c030176..7e45111 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -134,7 +134,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): signal.signal(signal.SIGCONT, signal_handler) signal.signal(signal.SIGINT, signal_handler) output_lines=[] # (line_content, is_stderr, do_subst_operation) - def get_terminal_size(): return fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) + def get_terminal_size(): return fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder # this mechanism prevents user input from being processed through substrules last_input_content=None -- Gitee From a83a4c3706c36e7e6ad81a81e1ba549ef5b3129b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 10 Aug 2024 22:36:16 +0800 Subject: [PATCH 035/122] Move location of readsize variable Other places need to access this variable (for easier modification) --- src/clitheme/exec/output_handler_posix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 7e45111..b5ce161 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -69,6 +69,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): except FileNotFoundError: pass stdout_fd, stdout_slave=pty.openpty() stderr_fd, stderr_slave=pty.openpty() + readsize=io.DEFAULT_BUFFER_SIZE env=copy.copy(os.environ) # Prevent apps from using "less" or "more" as pager, as it won't work here @@ -107,7 +108,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): nonlocal r,w while True: select.select([sys.stdin], [], []) - d=os.read(sys.stdin.fileno(), io.DEFAULT_BUFFER_SIZE) + d=os.read(sys.stdin.fileno(), readsize) if d==b'': # stdin is closed os.close(w); break os.write(w,d) @@ -172,7 +173,6 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): if thread_debug==1: raise Exception elif thread_debug==2: break - readsize=io.DEFAULT_BUFFER_SIZE try: fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.002)[0] except OSError: fds=select.select([stdout_fd, stderr_fd], [], [], 0.002)[0] # Handle user input from stdin -- Gitee From e8a83a21a39d42223fefeb9a3bf6078c5c2b032c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 10 Aug 2024 22:45:45 +0800 Subject: [PATCH 036/122] Remove redundant arguments in `subprocess.Popen` call --- src/clitheme/exec/output_handler_posix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index b5ce161..cd52ffc 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -125,7 +125,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): tmp_fd = os.open(os.ttyname(stdout_slave), os.O_RDWR) tmp_fd2 = os.open(os.ttyname(stderr_slave), os.O_RDWR) os.close(tmp_fd);os.close(tmp_fd2) - process=subprocess.Popen(command, stdin=stdin_fd, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env, preexec_fn=child_init) + process=subprocess.Popen(command, stdin=stdin_fd, stdout=stdout_slave, stderr=stdout_slave, env=env, preexec_fn=child_init) except: _labeled_print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=_globalvar.make_printable(str(sys.exc_info()[1])))) _globalvar.handle_exception() -- Gitee From ac9d79ab0b6de8627620ac95cc3448402b717d6f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 10 Aug 2024 23:37:33 +0800 Subject: [PATCH 037/122] Change sanity check banphrase rules - Add characters not allowed under Windows file systems to banphrase list --- src/clitheme/_globalvar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 357cbc6..5322e6a 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -83,7 +83,7 @@ db_version=3 output_subst_timeout=0.4 ## Sanity check function -entry_banphrases=['/','\\'] +entry_banphrases=['<', '>', ':', '"', '/', '\\', '|', '?', '*'] startswith_banphrases=['.'] banphrase_error_message="cannot contain '{char}'" banphrase_error_message_orig=copy(banphrase_error_message) -- Gitee From 7f2e9200e80cc88404b0ac46a77a5efac73d250a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 10 Aug 2024 23:56:42 +0800 Subject: [PATCH 038/122] Update version (v2.0-dev20240810) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 7651a5d..39dca45 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240808 +pkgver=2.0_dev20240810 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 72912b8..d67f1d2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240808-1) unstable; urgency=low +clitheme (2.0-dev20240810-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Fri, Aug 9 2024 00:03:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, Aug 10 2024 23:55:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 5956609..dc7889f 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -5,11 +5,11 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240808" +__version__="2.0-dev20240810" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240808" +version_main="2.0_dev20240810" version_buildnumber=1 \ No newline at end of file -- Gitee From a233f9a70f043b47697083308e28b17790e947df Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 11 Aug 2024 13:07:28 +0800 Subject: [PATCH 039/122] Use exception handling instead of `exit` for inner functions Make sure that it works properly when invoked in scripts/programmatically --- src/clitheme/_globalvar.py | 2 - src/clitheme/cli.py | 89 +++++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 5322e6a..a23e760 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -189,8 +189,6 @@ def handle_exception(): env_var="CLITHEME_SHOW_TRACEBACK" if env_var in os.environ and os.environ[env_var]=="1": raise - # Let "exit" function calls work - if sys.exc_info()[0]==SystemExit: raise def handle_set_themedef(fr, debug_name: str): prev_mode=False diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index bb32403..39c62f2 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -276,6 +276,7 @@ def update_theme(): if len(file_paths)==0: raise invalid_theme("file_paths empty") # Get file contents try: file_contents=_get_file_contents(file_paths) + except _direct_exit as exc: return exc.code except: _globalvar.handle_exception() return 1 @@ -340,7 +341,7 @@ def _get_file_contents(file_paths: list) -> list: content_list.append(open(path, 'r', encoding="utf-8").read()) if is_stdin: print() # Print an extra newline except KeyboardInterrupt: - print();exit(130) + print();raise _direct_exit(130) except: print("\n"+fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ index=str(i+1), message=path+": "+fmt(str(sys.exc_info()[1])))) @@ -348,6 +349,13 @@ def _get_file_contents(file_paths: list) -> list: print(line_prefix, end='') return content_list +class _direct_exit(Exception): + def __init__(self, code): + """ + Custom exception for handling return code inside another function callback + """ + self.code=code + def main(cli_args: list): """ Use this function invoke 'clitheme' with command line arguments @@ -365,49 +373,52 @@ def main(cli_args: list): for arg in cli_args: if not exclude_options or not _is_option(arg): c+=1 if ccount: - exit(_handle_usage_error(f.reof("too-many-arguments", "Error: too many arguments"), arg_first)) + raise _direct_exit(_handle_usage_error(f.reof("too-many-arguments", "Error: too many arguments"), arg_first)) - if cli_args[1] in ("apply-theme", "generate-data", "generate-data-hierarchy"): - check_enough_args(3) - generate_only=(cli_args[1] in ("generate-data", "generate-data-hierarchy")) - paths=[] - overlay=False - preserve_temp=False - for arg in cli_args[2:]: - if _is_option(arg): - if arg.strip()=="--overlay": overlay=True - elif arg.strip()=="--preserve-temp" and not generate_only: preserve_temp=True - else: return _handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=fmt(arg)), arg_first) - else: - paths.append(arg) - fi=frontend.FetchDescriptor(subsections="cli apply-theme") - content_list: list - try: content_list=_get_file_contents(paths) - except: - _globalvar.handle_exception() - return 1 - return apply_theme(content_list, overlay=overlay, filenames=paths, preserve_temp=preserve_temp, generate_only=generate_only) - elif cli_args[1]=="get-current-theme-info": - check_extra_args(2) # disabled additional options - return get_current_theme_info() - elif cli_args[1]=="unset-current-theme": - check_extra_args(2) - return unset_current_theme() - elif cli_args[1]=="update-theme": - check_extra_args(2) - return update_theme() - elif cli_args[1]=="--version": - check_extra_args(2) - print(f.feof("version-str", "clitheme version {ver}", ver=_globalvar.clitheme_version)) - else: - if cli_args[1]=="--help": + try: + if cli_args[1] in ("apply-theme", "generate-data", "generate-data-hierarchy"): + check_enough_args(3) + generate_only=(cli_args[1] in ("generate-data", "generate-data-hierarchy")) + paths=[] + overlay=False + preserve_temp=False + for arg in cli_args[2:]: + if _is_option(arg): + if arg.strip()=="--overlay": overlay=True + elif arg.strip()=="--preserve-temp" and not generate_only: preserve_temp=True + else: return _handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=fmt(arg)), arg_first) + else: + paths.append(arg) + fi=frontend.FetchDescriptor(subsections="cli apply-theme") + content_list: list + try: content_list=_get_file_contents(paths) + except _direct_exit as exc: return exc.code + except: + _globalvar.handle_exception() + return 1 + return apply_theme(content_list, overlay=overlay, filenames=paths, preserve_temp=preserve_temp, generate_only=generate_only) + elif cli_args[1]=="get-current-theme-info": + check_extra_args(2) # disabled additional options + return get_current_theme_info() + elif cli_args[1]=="unset-current-theme": + check_extra_args(2) + return unset_current_theme() + elif cli_args[1]=="update-theme": check_extra_args(2) - _handle_help_message(full_help=True) + return update_theme() + elif cli_args[1]=="--version": + check_extra_args(2) + print(f.feof("version-str", "clitheme version {ver}", ver=_globalvar.clitheme_version)) else: - return _handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=fmt(cli_args[1])), arg_first) + if cli_args[1]=="--help": + check_extra_args(2) + _handle_help_message(full_help=True) + else: + return _handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=fmt(cli_args[1])), arg_first) + except _direct_exit as exc: return exc.code return 0 def _script_main(): # for script return main(sys.argv) -- Gitee From 2af3784ee60e42319b22471e00eaeb8f689778c0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 11 Aug 2024 13:24:16 +0800 Subject: [PATCH 040/122] Execute `handle_set_themedef` at _globalvar universally - Avoid callback cycle issues - Easier to maintain --- src/clitheme/_generator/__init__.py | 3 +-- src/clitheme/_generator/db_interface.py | 1 - src/clitheme/_globalvar.py | 2 +- src/clitheme/cli.py | 2 -- src/clitheme/exec/__init__.py | 1 - src/clitheme/exec/output_handler_posix.py | 1 - src/clitheme/man.py | 1 - 7 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 71456cc..8aed0ee 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -67,5 +67,4 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # prevent circular import error by placing these statements at the end from .. import _globalvar from . import _dataclass -from . import _header_parser, _entries_parser, _substrules_parser, _manpage_parser -_globalvar.handle_set_themedef(_dataclass.GeneratorObject.frontend, "generator") \ No newline at end of file +from . import _header_parser, _entries_parser, _substrules_parser, _manpage_parser \ No newline at end of file diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 6306262..52e2998 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -22,7 +22,6 @@ from .. import _globalvar, frontend connection=sqlite3.connect(":memory:") # placeholder db_path="" debug_mode=False -_globalvar.handle_set_themedef(frontend, "db_interface") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") class need_db_regenerate(Exception): diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index a23e760..d989bb6 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -102,7 +102,6 @@ def sanity_check(path: str, use_orig: bool=False) -> bool: global msg_retrieved global sanity_check_error_message, banphrase_error_message, startswith_error_message if not msg_retrieved: - handle_set_themedef(frontend, "_globalvar") msg_retrieved=True f=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") banphrase_error_message=f.feof("sanity-check-msg-banphrase-err", banphrase_error_message, char="{char}") @@ -210,6 +209,7 @@ def handle_set_themedef(fr, debug_name: str): if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1]), file=sys.__stdout__) handle_exception() finally: sys.stdout=orig_stdout +handle_set_themedef(frontend, "global") def result_sort_cmp(obj1,obj2) -> int: cmp1='';cmp2='' try: diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 39c62f2..ecc774e 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -27,8 +27,6 @@ frontend.global_domain="swiftycode" frontend.global_appname="clitheme" frontend.global_subsections="cli" -_globalvar.handle_set_themedef(frontend, "cli") - last_data_path="" def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_temp=False, generate_only=False): """ diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index d009877..bc274da 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -24,7 +24,6 @@ from .._generator import db_interface # spell-checker:ignore lsdir showhelp argcount nosubst -_globalvar.handle_set_themedef(frontend, "clitheme-exec") frontend.global_domain="swiftycode" frontend.global_appname="clitheme" fd=frontend.FetchDescriptor(subsections="exec") diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index cd52ffc..3bc6be7 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -31,7 +31,6 @@ from . import _labeled_print # spell-checker:ignore cbreak ICANON readsize splitarray ttyname RDWR preexec pgrp -_globalvar.handle_set_themedef(frontend, "output_handler_posix") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") # https://docs.python.org/3/library/stdtypes.html#str.splitlines newlines=(b'\n',b'\r',b'\r\n',b'\v',b'\f',b'\x1c',b'\x1d',b'\x1e',b'\x85') diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 2a19892..81bdd35 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -20,7 +20,6 @@ from . import _globalvar, frontend def _labeled_print(msg: str): print("[clitheme-man] "+msg) -_globalvar.handle_set_themedef(frontend, "clitheme-man") frontend.global_domain="swiftycode" frontend.global_appname="clitheme" fd=frontend.FetchDescriptor(subsections="man") -- Gitee From f1b27b96551a5462144e49d9315c23bbabe2b6dd Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 11 Aug 2024 13:38:52 +0800 Subject: [PATCH 041/122] Change Gitee wiki URL - The `/pages` URL and the preview page never works properly --- .github/README.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/README.md b/.github/README.md index 38f5326..4eec1a7 100644 --- a/.github/README.md +++ b/.github/README.md @@ -52,7 +52,7 @@ Other characteristics: For more information, please see the project's Wiki documentation page. It can be accessed through the following links: -- https://gitee.com/swiftycode/clitheme/wikis/pages +- https://gitee.com/swiftycode/clitheme/wikis - https://gitee.com/swiftycode/clitheme-wiki-repo - https://github.com/swiftycode256/clitheme-wiki-repo diff --git a/README.md b/README.md index aacc91f..d0fe3f5 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者 - 无需应用程序API也可以访问当前主题中的字符串定义(易懂的数据结构) 更多信息请见本项目的Wiki文档页面。你可以通过以下位置访问这些文档: -- https://gitee.com/swiftycode/clitheme/wikis/pages +- https://gitee.com/swiftycode/clitheme/wikis - https://gitee.com/swiftycode/clitheme-wiki-repo - https://github.com/swiftycode256/clitheme-wiki-repo -- Gitee From ee64d230e31578242becb64be8456565501abbba Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 16 Sep 2024 18:14:33 +0800 Subject: [PATCH 042/122] Change how `foregroundonly` is processed - Use global options instead - Reset `foregroundonly` in global options to previous value after exiting from current command filter - Allow specifying `foregroundonly` at end of `[/substitute_regex]` block --- src/clitheme/_generator/_dataclass.py | 8 +++-- src/clitheme/_generator/_substrules_parser.py | 30 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index f18a554..14ae82f 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -280,6 +280,7 @@ class GeneratorObject(_handlers.DataHandlers): substrules_endmatchhere=False substrules_stdout_stderr_option=0 + substrules_foregroundonly=False def check_valid_pattern(pattern: str, debug_linenumber: Union[str, int]=self.lineindex+1): # check if patterns are valid @@ -345,8 +346,9 @@ class GeneratorObject(_handlers.DataHandlers): elif phrases[0]==end_phrase: got_options=self.parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, \ allowed_options=\ - (self.subst_limiting_options if is_substrules else []) \ + (self.subst_limiting_options if is_substrules else []) +(self.content_subst_options if is_substrules else ["substvar"]) # don't allow substesc in `[entry]` + +(['foregroundonly'] if is_substrules else []) ) for option in got_options: if option=="endmatchhere" and got_options['endmatchhere']==True: @@ -359,6 +361,8 @@ class GeneratorObject(_handlers.DataHandlers): entry_name_substesc=True elif option=="substvar" and got_options['substvar']==True: entry_name_substvar=True + elif option=="foregroundonly" and got_options['foregroundonly']==True: + substrules_foregroundonly=True break else: self.handle_invalid_phrase(phrases[0]) # For silence_warning in subst_variable_content @@ -391,7 +395,7 @@ class GeneratorObject(_handlers.DataHandlers): command_match_strictness=substrules_options['strictness'], \ end_match_here=substrules_endmatchhere, \ stdout_stderr_matchoption=substrules_stdout_stderr_option, \ - foreground_only=substrules_options['foreground_only'], \ + foreground_only=substrules_foregroundonly, \ line_number_debug=entry[4], \ unique_id=entry[3]) except self.db_interface.bad_pattern: self.handle_error(self.fd.feof("bad-subst-pattern-err", "Bad substitute pattern at line {num} ({error_msg})", num=entry[4], error_msg=sys.exc_info()[1])) diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 10b12bd..55cb27e 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -20,7 +20,16 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str end_phrase=r"{/substrules_section}" command_filters: Optional[list]=None command_filter_strictness=0 - command_filter_foreground_only=False + # If True, reset foregroundonly option to beforehand during next command filter + inline_foregroundonly=None + def reset_inline_foregroundonly(): + """ + Set foregroundonly option to false if foregroundonly option is "inline" and not enabled previously + """ + nonlocal inline_foregroundonly + if inline_foregroundonly!=None: + obj.global_options['foregroundonly']=inline_foregroundonly + inline_foregroundonly=None # initialize the database if os.path.exists(obj.path+"/"+_globalvar.db_filename): try: obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) @@ -34,12 +43,12 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str phrases=obj.lines_data[obj.lineindex].split() if phrases[0]=="[filter_commands]": obj.check_extra_args(phrases, 1, use_exact_count=True) + reset_inline_foregroundonly() content=obj.handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=r"[/filter_commands]", disallow_cmdmatch_options=False, disable_substesc=True) # read commands command_strings=content.splitlines() strictness=0 - foreground_only=False # parse strictcmdmatch, exactcmdmatch, and other cmdmatch options here got_options=copy.copy(obj.global_options) if len(obj.lines_data[obj.lineindex].split())>1: @@ -51,19 +60,19 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str strictness=2 elif this_option=="smartcmdmatch" and got_options['smartcmdmatch']==True: strictness=-1 - elif this_option=="foregroundonly" and got_options['foregroundonly']==True: - foreground_only=True + elif this_option=="foregroundonly": + inline_foregroundonly=obj.global_options.get('foregroundonly')==True + obj.global_options['foregroundonly']=got_options['foregroundonly'] command_filters=[] for cmd in command_strings: command_filters.append(cmd.strip()) command_filter_strictness=strictness - command_filter_foreground_only=foreground_only elif phrases[0]=="filter_command": obj.check_enough_args(phrases, 2) + reset_inline_foregroundonly() content=_globalvar.splitarray_to_string(phrases[1:]) content=obj.subst_variable_content(content) strictness=0 - foreground_only=False for this_option in obj.global_options: if this_option=="strictcmdmatch" and obj.global_options['strictcmdmatch']==True: strictness=1 @@ -71,17 +80,18 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str strictness=2 elif this_option=="smartcmdmatch" and obj.global_options['smartcmdmatch']==True: strictness=-1 - elif this_option=="foregroundonly" and obj.global_options['foregroundonly']==True: - foreground_only=True command_filters=[content] command_filter_strictness=strictness - command_filter_foreground_only=foreground_only elif phrases[0]=="unset_filter_command": obj.check_extra_args(phrases, 1, use_exact_count=True) + reset_inline_foregroundonly() command_filters=None elif phrases[0] in ("[substitute_string]", "[substitute_regex]"): obj.check_enough_args(phrases, 2) - options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness, "foreground_only": command_filter_foreground_only} + options={"effective_commands": copy.copy(command_filters), + "is_regex": phrases[0]=="[substitute_regex]", + "strictness": command_filter_strictness, + "foreground_only": obj.global_options.get("foregroundonly")==True} match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) obj.handle_entry(match_pattern, start_phrase=phrases[0], end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) elif obj.handle_setters(): pass -- Gitee From 5ac768917fdc8cf7581854d04440d3cb3049c8aa Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 17 Sep 2024 16:53:39 +0800 Subject: [PATCH 043/122] Remove `foreground_only` option in substrules_options array --- src/clitheme/_generator/_dataclass.py | 2 +- src/clitheme/_generator/_substrules_parser.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 14ae82f..812bee0 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -267,7 +267,7 @@ class GeneratorObject(_handlers.DataHandlers): if is_specified_in_block(): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option))) return blockinput_data def handle_entry(self, entry_name: str, start_phrase: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): - # substrules_options: {effective_commands: list, is_regex: bool, strictness: int, foreground_only: bool} + # substrules_options: {effective_commands: list, is_regex: bool, strictness: int} entry_name_substesc=False; entry_name_substvar=False names_processed=False # Set to True when no more entry names are being specified diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 55cb27e..fff16f6 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -90,8 +90,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str obj.check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", - "strictness": command_filter_strictness, - "foreground_only": obj.global_options.get("foregroundonly")==True} + "strictness": command_filter_strictness} match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) obj.handle_entry(match_pattern, start_phrase=phrases[0], end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) elif obj.handle_setters(): pass -- Gitee From d41ba1e52a2c8a0db61bffb9f9d52a0029dc29c6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 17 Sep 2024 17:23:35 +0800 Subject: [PATCH 044/122] Move `handle_entry` function to another file Makes everything easier to manage --- src/clitheme/_generator/_dataclass.py | 138 +---------------- .../_generator/_entry_block_handler.py | 146 ++++++++++++++++++ 2 files changed, 148 insertions(+), 136 deletions(-) create mode 100644 src/clitheme/_generator/_entry_block_handler.py diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 812bee0..d06d4fa 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -15,7 +15,7 @@ import copy import uuid from typing import Optional, Union from .. import _globalvar -from . import _handlers +from . import _handlers, _entry_block_handler # spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption class GeneratorObject(_handlers.DataHandlers): @@ -266,138 +266,4 @@ class GeneratorObject(_handlers.DataHandlers): elif disallow_cmdmatch_options: if is_specified_in_block(): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option))) return blockinput_data - def handle_entry(self, entry_name: str, start_phrase: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): - # substrules_options: {effective_commands: list, is_regex: bool, strictness: int} - - entry_name_substesc=False; entry_name_substvar=False - names_processed=False # Set to True when no more entry names are being specified - - # For supporting specifying multiple entries at once (0: name, 1: uuid, 2: debug_linenumber) - entryNames: list=[(entry_name, uuid.uuid4(), self.lineindex+1)] - # For substrules_section: (0: match_content, 1: substitute_content, 2: locale, 3: entry_name_uuid, 4: content_linenumber_str, 5: match_content_linenumber) - # For entries_section: (0: target_entry, 1: content, 2: debug_linenumber, 3: entry_name_uuid, 4: entry_name_linenumber) - entries: list=[] - - substrules_endmatchhere=False - substrules_stdout_stderr_option=0 - substrules_foregroundonly=False - - def check_valid_pattern(pattern: str, debug_linenumber: Union[str, int]=self.lineindex+1): - # check if patterns are valid - try: re.compile(pattern) - except: self.handle_error(self.fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(debug_linenumber), error_msg=sys.exc_info()[1])) - while self.goto_next_line(): - phrases=self.lines_data[self.lineindex].split() - line_content=self.lines_data[self.lineindex] - # Support specifying multiple match pattern/entry names in one definition block - if phrases[0]!=start_phrase and not names_processed: - names_processed=True # Prevent specifying it after other definition syntax - # --Process entry names-- - for x in range(len(entryNames)): - each_entry=entryNames[x] - name=each_entry[0] - if not is_substrules: - if self.in_subsection!="": name=self.in_subsection+" "+name - if self.in_domainapp!="": name=self.in_domainapp+" "+name - entryNames[x]=(name, each_entry[1], each_entry[2]) - - if phrases[0]==start_phrase and not names_processed: - self.check_enough_args(phrases, 2) - pattern=_globalvar.extract_content(line_content) - entryNames.append((pattern, uuid.uuid4(), self.lineindex+1)) - elif phrases[0]=="locale" or phrases[0].startswith("locale:"): - content: str - locale: str - if phrases[0].startswith("locale:"): - self.check_enough_args(phrases, 2) - results=re.search(r"locale:(?P.+)", phrases[0]) - if results==None: - self.handle_error(self.fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="locale:", num=str(self.lineindex+1))) - else: - locale=results.groupdict()['locale'] - content=_globalvar.extract_content(line_content) - else: - self.check_enough_args(phrases, 3) - content=_globalvar.extract_content(line_content, begin_phrase_count=2) - locale=phrases[1] - content=self.handle_singleline_content(content) - for each_name in entryNames: - if is_substrules: - entries.append((each_name[0], content, None if locale=="default" else locale, each_name[1], str(self.lineindex+1), each_name[2])) - else: - target_entry=copy.copy(each_name[0]) - if locale!="default": - target_entry+="__"+locale - entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) - elif phrases[0] in ("locale_block", "[locale]"): - self.check_enough_args(phrases, 2) - locales=self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() - begin_line_number=self.lineindex+1+1 - content=self.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/locale]" if phrases[0]=="[locale]" else "end_block") - for this_locale in locales: - for each_name in entryNames: - if is_substrules: - entries.append((each_name[0], content, None if this_locale=="default" else this_locale, each_name[1], self.handle_linenumber_range(begin_line_number, self.lineindex+1-1), each_name[2])) - else: - target_entry=copy.copy(each_name[0]) - if this_locale!="default": - target_entry+="__"+this_locale - entries.append((target_entry, content, begin_line_number, each_name[1], each_name[2])) - elif phrases[0]==end_phrase: - got_options=self.parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, \ - allowed_options=\ - (self.subst_limiting_options if is_substrules else []) - +(self.content_subst_options if is_substrules else ["substvar"]) # don't allow substesc in `[entry]` - +(['foregroundonly'] if is_substrules else []) - ) - for option in got_options: - if option=="endmatchhere" and got_options['endmatchhere']==True: - substrules_endmatchhere=True - elif option=="subststdoutonly" and got_options['subststdoutonly']==True: - substrules_stdout_stderr_option=1 - elif option=="subststderronly" and got_options['subststderronly']==True: - substrules_stdout_stderr_option=2 - elif option=="substesc" and got_options['substesc']==True: - entry_name_substesc=True - elif option=="substvar" and got_options['substvar']==True: - entry_name_substvar=True - elif option=="foregroundonly" and got_options['foregroundonly']==True: - substrules_foregroundonly=True - break - else: self.handle_invalid_phrase(phrases[0]) - # For silence_warning in subst_variable_content - encountered_ids=set() - for x in range(len(entries)): - entry=entries[x] - match_pattern=entry[0] - # substvar MUST come before substesc or "{{ESC}}" in variable content will not be processed - if entry_name_substvar: - match_pattern=self.subst_variable_content(match_pattern, override_check=True, \ - line_number_debug=entry[5] if is_substrules else entry[4], \ - # Don't show warnings for the same match_pattern - silence_warnings=entry[3] in encountered_ids) - if entry_name_substesc: match_pattern=self.handle_substesc(match_pattern) - - if is_substrules: check_valid_pattern(match_pattern, entry[5]) - else: - # Prevent leading . & prevent /,\ in entry name - if _globalvar.sanity_check(match_pattern)==False: - self.handle_error(self.fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(entry[4]), sanitycheck_msg=_globalvar.sanity_check_error_message)) - encountered_ids.add(entry[3]) - if is_substrules: - try: - self.db_interface.add_subst_entry( - match_pattern=match_pattern, \ - substitute_pattern=entry[1], \ - effective_commands=substrules_options['effective_commands'], \ - effective_locale=entry[2], \ - is_regex=substrules_options['is_regex'], \ - command_match_strictness=substrules_options['strictness'], \ - end_match_here=substrules_endmatchhere, \ - stdout_stderr_matchoption=substrules_stdout_stderr_option, \ - foreground_only=substrules_foregroundonly, \ - line_number_debug=entry[4], \ - unique_id=entry[3]) - except self.db_interface.bad_pattern: self.handle_error(self.fd.feof("bad-subst-pattern-err", "Bad substitute pattern at line {num} ({error_msg})", num=entry[4], error_msg=sys.exc_info()[1])) - else: - self.add_entry(self.datapath, match_pattern, entry[1], entry[2]) \ No newline at end of file + handle_entry=_entry_block_handler.handle_entry \ No newline at end of file diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py new file mode 100644 index 0000000..aae4bae --- /dev/null +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -0,0 +1,146 @@ +import sys +import re +import copy +import uuid +from typing import Optional, Union +from .. import _globalvar + + +def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): + # Workaround to circular import issue + from . import _dataclass + self: _dataclass.GeneratorObject=obj + # substrules_options: {effective_commands: list, is_regex: bool, strictness: int} + + entry_name_substesc=False; entry_name_substvar=False + names_processed=False # Set to True when no more entry names are being specified + + # For supporting specifying multiple entries at once (0: name, 1: uuid, 2: debug_linenumber) + entryNames: list=[(entry_name, uuid.uuid4(), self.lineindex+1)] + # For substrules_section: (0: match_content, 1: substitute_content, 2: locale, 3: entry_name_uuid, 4: content_linenumber_str, 5: match_content_linenumber) + # For entries_section: (0: target_entry, 1: content, 2: debug_linenumber, 3: entry_name_uuid, 4: entry_name_linenumber) + entries: list=[] + + substrules_endmatchhere=False + substrules_stdout_stderr_option=0 + substrules_foregroundonly=False + + def check_valid_pattern(pattern: str, debug_linenumber: Union[str, int]=self.lineindex+1): + # check if patterns are valid + try: re.compile(pattern) + except: self.handle_error(self.fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(debug_linenumber), error_msg=sys.exc_info()[1])) + while self.goto_next_line(): + phrases=self.lines_data[self.lineindex].split() + line_content=self.lines_data[self.lineindex] + # Support specifying multiple match pattern/entry names in one definition block + if phrases[0]!=start_phrase and not names_processed: + names_processed=True # Prevent specifying it after other definition syntax + # --Process entry names-- + for x in range(len(entryNames)): + each_entry=entryNames[x] + name=each_entry[0] + if not is_substrules: + if self.in_subsection!="": name=self.in_subsection+" "+name + if self.in_domainapp!="": name=self.in_domainapp+" "+name + entryNames[x]=(name, each_entry[1], each_entry[2]) + + if phrases[0]==start_phrase and not names_processed: + self.check_enough_args(phrases, 2) + pattern=_globalvar.extract_content(line_content) + entryNames.append((pattern, uuid.uuid4(), self.lineindex+1)) + elif phrases[0]=="locale" or phrases[0].startswith("locale:"): + content: str + locale: str + if phrases[0].startswith("locale:"): + self.check_enough_args(phrases, 2) + results=re.search(r"locale:(?P.+)", phrases[0]) + if results==None: + self.handle_error(self.fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="locale:", num=str(self.lineindex+1))) + else: + locale=results.groupdict()['locale'] + content=_globalvar.extract_content(line_content) + else: + self.check_enough_args(phrases, 3) + content=_globalvar.extract_content(line_content, begin_phrase_count=2) + locale=phrases[1] + content=self.handle_singleline_content(content) + for each_name in entryNames: + if is_substrules: + entries.append((each_name[0], content, None if locale=="default" else locale, each_name[1], str(self.lineindex+1), each_name[2])) + else: + target_entry=copy.copy(each_name[0]) + if locale!="default": + target_entry+="__"+locale + entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) + elif phrases[0] in ("locale_block", "[locale]"): + self.check_enough_args(phrases, 2) + locales=self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() + begin_line_number=self.lineindex+1+1 + content=self.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/locale]" if phrases[0]=="[locale]" else "end_block") + for this_locale in locales: + for each_name in entryNames: + if is_substrules: + entries.append((each_name[0], content, None if this_locale=="default" else this_locale, each_name[1], self.handle_linenumber_range(begin_line_number, self.lineindex+1-1), each_name[2])) + else: + target_entry=copy.copy(each_name[0]) + if this_locale!="default": + target_entry+="__"+this_locale + entries.append((target_entry, content, begin_line_number, each_name[1], each_name[2])) + elif phrases[0]==end_phrase: + got_options=self.parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, \ + allowed_options=\ + (self.subst_limiting_options if is_substrules else []) + +(self.content_subst_options if is_substrules else ["substvar"]) # don't allow substesc in `[entry]` + +(['foregroundonly'] if is_substrules else []) + ) + for option in got_options: + if option=="endmatchhere" and got_options['endmatchhere']==True: + substrules_endmatchhere=True + elif option=="subststdoutonly" and got_options['subststdoutonly']==True: + substrules_stdout_stderr_option=1 + elif option=="subststderronly" and got_options['subststderronly']==True: + substrules_stdout_stderr_option=2 + elif option=="substesc" and got_options['substesc']==True: + entry_name_substesc=True + elif option=="substvar" and got_options['substvar']==True: + entry_name_substvar=True + elif option=="foregroundonly" and got_options['foregroundonly']==True: + substrules_foregroundonly=True + break + else: self.handle_invalid_phrase(phrases[0]) + # For silence_warning in subst_variable_content + encountered_ids=set() + for x in range(len(entries)): + entry=entries[x] + match_pattern=entry[0] + # substvar MUST come before substesc or "{{ESC}}" in variable content will not be processed + if entry_name_substvar: + match_pattern=self.subst_variable_content(match_pattern, override_check=True, \ + line_number_debug=entry[5] if is_substrules else entry[4], \ + # Don't show warnings for the same match_pattern + silence_warnings=entry[3] in encountered_ids) + if entry_name_substesc: match_pattern=self.handle_substesc(match_pattern) + + if is_substrules: check_valid_pattern(match_pattern, entry[5]) + else: + # Prevent leading . & prevent /,\ in entry name + if _globalvar.sanity_check(match_pattern)==False: + self.handle_error(self.fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(entry[4]), sanitycheck_msg=_globalvar.sanity_check_error_message)) + encountered_ids.add(entry[3]) + if is_substrules: + try: + self.db_interface.add_subst_entry( + match_pattern=match_pattern, \ + substitute_pattern=entry[1], \ + effective_commands=substrules_options['effective_commands'], \ + effective_locale=entry[2], \ + is_regex=substrules_options['is_regex'], \ + command_match_strictness=substrules_options['strictness'], \ + end_match_here=substrules_endmatchhere, \ + stdout_stderr_matchoption=substrules_stdout_stderr_option, \ + foreground_only=substrules_foregroundonly, \ + line_number_debug=entry[4], \ + unique_id=entry[3]) + except self.db_interface.bad_pattern: self.handle_error(self.fd.feof("bad-subst-pattern-err", "Bad substitute pattern at line {num} ({error_msg})", num=entry[4], error_msg=sys.exc_info()[1])) + else: + self.add_entry(self.datapath, match_pattern, entry[1], entry[2]) \ No newline at end of file -- Gitee From e7adf1cbbd2cfdbf2a951c861e4b560f9bcfe23f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 17 Sep 2024 17:28:16 +0800 Subject: [PATCH 045/122] Move import statements to front in `_generator` Placing them at the end is no longer needed as we don't need to call `handle_set_themedef` anymore. --- src/clitheme/_generator/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 8aed0ee..1473a68 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -11,6 +11,9 @@ import os import string import random from typing import Optional +from .. import _globalvar +from . import _dataclass +from . import _header_parser, _entries_parser, _substrules_parser, _manpage_parser # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent @@ -63,8 +66,3 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info theme_index.write(obj.custom_infofile_name+"\n") path=obj.path return obj.path - -# prevent circular import error by placing these statements at the end -from .. import _globalvar -from . import _dataclass -from . import _header_parser, _entries_parser, _substrules_parser, _manpage_parser \ No newline at end of file -- Gitee From 2a7f17e250faf0acd67ae30c18cbe61aad01a924 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 17 Sep 2024 22:04:32 +0800 Subject: [PATCH 046/122] Update version (v2.0-dev20240917) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 39dca45..e147cf1 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240810 +pkgver=2.0_dev20240917 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index d67f1d2..2f331af 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240810-1) unstable; urgency=low +clitheme (2.0-dev20240917-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, Aug 10 2024 23:55:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, Sep 17 2024 22:03:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index dc7889f..6a254ad 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -5,11 +5,11 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240810" +__version__="2.0-dev20240917" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240810" +version_main="2.0_dev20240917" version_buildnumber=1 \ No newline at end of file -- Gitee From 60d3d87ed3c438285ad5903f62fdea5df7e25775 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 23 Sep 2024 12:57:36 +0800 Subject: [PATCH 047/122] Add `set_local_themedefs` function to frontend - Change `handle_set_themedef` implementation to use this new function --- src/clitheme/_globalvar.py | 19 ++++++++----------- src/clitheme/frontend.py | 24 ++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index d989bb6..a5d0075 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -18,8 +18,6 @@ from . import _version # spell-checker:ignoreRegExp banphrase[s]{0,1} -## Initialization operations - # Enable processing of escape characters in Windows Command Prompt if os.name=="nt": import ctypes @@ -189,20 +187,19 @@ def handle_exception(): if env_var in os.environ and os.environ[env_var]=="1": raise -def handle_set_themedef(fr, debug_name: str): +def handle_set_themedef(fr: frontend, debug_name: str): # type: ignore prev_mode=False # Prevent interference with other code piping stdout orig_stdout=sys.stdout try: files=["strings/generator-strings.clithemedef.txt", "strings/cli-strings.clithemedef.txt", "strings/exec-strings.clithemedef.txt", "strings/man-strings.clithemedef.txt"] - for x in range(len(files)): - filename=files[x] - msg=io.StringIO() - sys.stdout=msg - fr.global_debugmode=True - if not fr.set_local_themedef(_get_resource.read_file(filename), overlay=not x==0): raise RuntimeError("Full log below: \n"+msg.getvalue()) - fr.global_debugmode=prev_mode - sys.stdout=orig_stdout + file_contents=list(map(lambda name: _get_resource.read_file(name), files)) + msg=io.StringIO() + sys.stdout=msg + fr.global_debugmode=True + if not fr.set_local_themedefs(file_contents): raise RuntimeError("Full log below: \n"+msg.getvalue()) + fr.global_debugmode=prev_mode + sys.stdout=orig_stdout except: sys.stdout=orig_stdout fr.global_debugmode=prev_mode diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 5255afb..27d0630 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -18,7 +18,7 @@ import string import re import hashlib import shutil -from typing import Optional +from typing import Optional, List from . import _globalvar # spell-checker:ignore newhash numorig numcur @@ -99,7 +99,6 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: if global_debugmode: print("[Debug] Generator error: "+str(sys.exc_info()[1])) return False finally: global_debugmode=d_copy - # I GIVE UP on solving the callback cycle HELL on _generator.generate_data_hierarchy -> new GeneratorObject -> db_interface import -> set_local_themedef -> [generates data directory] so I'm going to add this CRAP fix if not os.path.exists(path_name): shutil.copytree(return_val, path_name) try: shutil.rmtree(return_val) @@ -109,6 +108,27 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: _alt_path=path_name+"/"+_globalvar.generator_data_pathname _alt_path_dirname=dir_name return True + +def set_local_themedefs(file_contents: List[str], overlay: bool=False): + """ + Sets multiple local theme definition files for the current frontend instance. + When set, the FetchDescriptor functions will try the local definition before falling back to global theme data. + + - Set overlay=True to overlay on top of existing local definition data (if exists) + + WARNING: Pass the file content in str to this function; DO NOT pass the path to the file. + + This function returns True if successful, otherwise returns False. + """ + global _alt_path, _alt_path_hash, _alt_path_dirname + orig=(_alt_path, _alt_path_hash, _alt_path_dirname) + for x in range(len(file_contents)): + content=file_contents[x] + if not set_local_themedef(content, overlay=(x>0 or overlay)): + _alt_path, _alt_path_hash, _alt_path_dirname=orig + return False + return True + def unset_local_themedef(): """ Unset the local theme definition file for the current frontend instance. -- Gitee From f3e8f0bfae3acb8f32a9732a4afc84db9f30b618 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 23 Sep 2024 19:52:55 +0800 Subject: [PATCH 048/122] Fix `handle_set_themedef` error when importing the module --- src/clitheme/__init__.py | 9 ++++++++- src/clitheme/_globalvar.py | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/clitheme/__init__.py b/src/clitheme/__init__.py index 6b724b1..025dd95 100644 --- a/src/clitheme/__init__.py +++ b/src/clitheme/__init__.py @@ -1 +1,8 @@ -__all__=["frontend", "cli", "man", "exec"] \ No newline at end of file +__all__=["frontend", "cli", "man", "exec"] + +# Prevent RuntimeWarning from displaying when running a submodule (e.g. "python3 -m clitheme.exec") +import warnings +warnings.simplefilter("ignore", category=RuntimeWarning) +from . import _globalvar, frontend, cli, man, exec +_globalvar.handle_set_themedef(frontend, "global") # type: ignore +del _globalvar # Don't expose this module by default \ No newline at end of file diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index a5d0075..539254e 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -206,7 +206,6 @@ def handle_set_themedef(fr: frontend, debug_name: str): # type: ignore if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1]), file=sys.__stdout__) handle_exception() finally: sys.stdout=orig_stdout -handle_set_themedef(frontend, "global") def result_sort_cmp(obj1,obj2) -> int: cmp1='';cmp2='' try: -- Gitee From 680e54665d06ccabfca4152a4ce0fffb7de14f6b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 23 Sep 2024 19:53:13 +0800 Subject: [PATCH 049/122] Add a default `overlay=False` option in `apply_theme` --- src/clitheme/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index ecc774e..a5635c0 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -28,7 +28,7 @@ frontend.global_appname="clitheme" frontend.global_subsections="cli" last_data_path="" -def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_temp=False, generate_only=False): +def apply_theme(file_contents: list, filenames: list, overlay: bool=False, preserve_temp=False, generate_only=False): """ Apply the theme using the provided definition file contents and file pathnames in a list object. -- Gitee From 26474cc8a1d78574deeb6452b2e85f9ba4dce0e9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 23 Sep 2024 20:16:21 +0800 Subject: [PATCH 050/122] Add Python 3.8 compatible type annotations for container types --- src/clitheme/_generator/_dataclass.py | 10 +++++----- src/clitheme/_generator/_entry_block_handler.py | 8 ++++---- src/clitheme/_generator/_handlers.py | 6 +++--- src/clitheme/_generator/_manpage_parser.py | 6 +++--- src/clitheme/_generator/db_interface.py | 10 +++++----- src/clitheme/_globalvar.py | 5 +++-- src/clitheme/cli.py | 13 +++++++------ src/clitheme/exec/__init__.py | 3 ++- src/clitheme/exec/output_handler_posix.py | 6 +++--- src/clitheme/man.py | 3 ++- 10 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index d06d4fa..6bd3a64 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -13,7 +13,7 @@ import re import math import copy import uuid -from typing import Optional, Union +from typing import Optional, Union, List, Dict from .. import _globalvar from . import _handlers, _entry_block_handler # spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption @@ -66,10 +66,10 @@ class GeneratorObject(_handlers.DataHandlers): # stop at non-empty or non-comment line if not self.is_ignore_line(): return True else: return False # End of file - def check_enough_args(self, phrases: list, count: int): + def check_enough_args(self, phrases: List[str], count: int): if len(phrases)count @@ -77,7 +77,7 @@ class GeneratorObject(_handlers.DataHandlers): self.handle_error(self.fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=self.fmt(phrases[0]))) def handle_invalid_phrase(self, name: str): self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=self.fmt(name), num=str(self.lineindex+1))) - def parse_options(self, options_data: list, merge_global_options: int, allowed_options: Optional[list]=None) -> dict: + def parse_options(self, options_data: List[str], merge_global_options: int, allowed_options: Optional[list]=None) -> Dict[str, Union[int,bool]]: # merge_global_options: 0 - Don't merge; 1 - Merge self.global_options; 2 - Merge self.really_really_global_options final_options={} if merge_global_options!=0: final_options=copy.copy(self.global_options if merge_global_options==1 else self.really_really_global_options) @@ -116,7 +116,7 @@ class GeneratorObject(_handlers.DataHandlers): if allowed_options!=None and option_name not in allowed_options: self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option_name))) return final_options - def handle_set_global_options(self, options_data: list, really_really_global: bool=False): + def handle_set_global_options(self, options_data: List[str], really_really_global: bool=False): # set options globally if really_really_global: self.really_really_global_options=self.parse_options(options_data, merge_global_options=2) diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index aae4bae..e0ec492 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -2,11 +2,11 @@ import sys import re import copy import uuid -from typing import Optional, Union +from typing import Optional, Union, List, Dict, Any from .. import _globalvar -def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): +def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_substrules: bool=False, substrules_options: Dict[str, Any]={}): # Workaround to circular import issue from . import _dataclass self: _dataclass.GeneratorObject=obj @@ -16,10 +16,10 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su names_processed=False # Set to True when no more entry names are being specified # For supporting specifying multiple entries at once (0: name, 1: uuid, 2: debug_linenumber) - entryNames: list=[(entry_name, uuid.uuid4(), self.lineindex+1)] + entryNames: List[tuple]=[(entry_name, uuid.uuid4(), self.lineindex+1)] # For substrules_section: (0: match_content, 1: substitute_content, 2: locale, 3: entry_name_uuid, 4: content_linenumber_str, 5: match_content_linenumber) # For entries_section: (0: target_entry, 1: content, 2: debug_linenumber, 3: entry_name_uuid, 4: entry_name_linenumber) - entries: list=[] + entries: List[tuple]=[] substrules_endmatchhere=False substrules_stdout_stderr_option=0 diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index 3412fd4..5e14a57 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -10,7 +10,7 @@ Functions for data processing and others (internal module) import os import gzip import re -from typing import Optional +from typing import Optional, List from .. import _globalvar, frontend # spell-checker:ignore datapath @@ -65,7 +65,7 @@ class DataHandlers: num=str(line_number_debug), name=self.fmt(header_name_debug))) f=open(target_path,'w', encoding="utf-8") f.write(content+'\n') - def write_infofile_newlines(self, path: str, filename: str, content_phrases: list, line_number_debug: int, header_name_debug: str): + def write_infofile_newlines(self, path: str, filename: str, content_phrases: List[str], line_number_debug: int, header_name_debug: str): if not os.path.isdir(path): os.makedirs(path) target_path=path+"/"+filename @@ -75,7 +75,7 @@ class DataHandlers: f=open(target_path,'w', encoding="utf-8") for line in content_phrases: f.write(line+"\n") - def write_manpage_file(self, file_path: list, content: str, line_number_debug: int, custom_parent_path: Optional[str]=None): + def write_manpage_file(self, file_path: List[str], content: str, line_number_debug: int, custom_parent_path: Optional[str]=None): parent_path=custom_parent_path if custom_parent_path!=None else self.path+"/"+_globalvar.generator_manpage_pathname parent_path+='/'+os.path.dirname(_globalvar.splitarray_to_string(file_path).replace(" ","/")) # create the parent directory diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 1a1a40e..9e56443 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -9,7 +9,7 @@ substrules_section parser function (internal module) """ import os import sys -from typing import Optional +from typing import Optional, List from .. import _globalvar from . import _dataclass @@ -20,7 +20,7 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): end_phrase="{/manpage_section}" while obj.goto_next_line(): phrases=obj.lines_data[obj.lineindex].split() - def get_file_content(filepath: list) -> str: + def get_file_content(filepath: List[str]) -> str: # determine file path parent_dir="" # if no filename provided, use current working directory as parent path; else, use the directory the file is in as the parent path @@ -35,7 +35,7 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.write_manpage_file(filepath, filecontent, -1, custom_parent_path=obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name+"/manpage_data") return filecontent if phrases[0]=="[file_content]": - def handle(p: list) -> list: + def handle(p: List[str]) -> List[str]: obj.check_enough_args(p, 2) filepath=obj.subst_variable_content(_globalvar.splitarray_to_string(p[1:])).split() # sanity check the file path diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 52e2998..5a14338 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -14,7 +14,7 @@ import sqlite3 import re import copy import uuid -from typing import Optional +from typing import Optional, List, Tuple from .. import _globalvar, frontend # spell-checker:ignore matchoption cmdlist exactmatch rowid pids tcpgrp @@ -67,7 +67,7 @@ def connect_db(path: str=f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_f def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, foreground_only: bool=False, unique_id: uuid.UUID=uuid.UUID(int=0), line_number_debug: str="-1"): if unique_id==uuid.UUID(int=0): unique_id=uuid.uuid4() - cmdlist: list=[] + cmdlist: List[str]=[] try: re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid except: raise bad_pattern(str(sys.exc_info()[1])) # handle condition where no effective_locale is specified ("default") @@ -100,7 +100,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma connection.commit() def _check_strictness(match_cmd: str, strictness: int, target_command: str): - def process_smartcmdmatch_phrases(match_cmd: str) -> list: + def process_smartcmdmatch_phrases(match_cmd: str) -> List[str]: match_cmd_phrases=[] for p in range(len(match_cmd.split())): ph=match_cmd.split()[p] @@ -126,7 +126,7 @@ def _check_strictness(match_cmd: str, strictness: int, target_command: str): if phrase not in target_command.split(): success=False return success -def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False, pids: tuple=(-1,-1)) -> bytes: +def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False, pids: Tuple[int,int]=(-1,-1)) -> bytes: # pids: (main_pid, current_tcpgrp) # Match order: @@ -192,7 +192,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # timeout value for each match operation match_timeout=_globalvar.output_subst_timeout -def _handle_subst(matches: list, content: bytes, is_stderr: bool, pids: tuple, target_command: Optional[str]): +def _handle_subst(matches: List[tuple], content: bytes, is_stderr: bool, pids: Tuple[int,int], target_command: Optional[str]): content_str=copy.copy(content) encountered_ids=set() for match_data in matches: diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 539254e..ca47134 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -15,6 +15,7 @@ import re import string from copy import copy from . import _version +from typing import List # spell-checker:ignoreRegExp banphrase[s]{0,1} @@ -120,7 +121,7 @@ def sanity_check(path: str, use_orig: bool=False) -> bool: ## Convenience functions -def splitarray_to_string(split_content) -> str: +def splitarray_to_string(split_content: List[str]) -> str: final="" for phrase in split_content: final+=phrase+" " @@ -139,7 +140,7 @@ def make_printable(content: str) -> str: exp=re.sub(r"""^(?P['"]?)(?P.+)(?P=quote)$""", r"<\g>", exp) final_str+=exp return final_str -def get_locale(debug_mode: bool=False) -> list: +def get_locale(debug_mode: bool=False) -> List[str]: lang=[] def add_language(target_lang: str): nonlocal lang diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index a5635c0..97a8009 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -20,6 +20,7 @@ import stat import functools from . import _globalvar, _generator, frontend from ._globalvar import make_printable as fmt # A shorter alias of the function +from typing import List # spell-checker:ignore pathnames lsdir inpstr @@ -28,7 +29,7 @@ frontend.global_appname="clitheme" frontend.global_subsections="cli" last_data_path="" -def apply_theme(file_contents: list, filenames: list, overlay: bool=False, preserve_temp=False, generate_only=False): +def apply_theme(file_contents: List[str], filenames: List[str], overlay: bool=False, preserve_temp=False, generate_only=False): """ Apply the theme using the provided definition file contents and file pathnames in a list object. @@ -247,8 +248,8 @@ def update_theme(): (Invokes 'clitheme update-theme') """ class invalid_theme(Exception): pass - file_contents: list - file_paths: list + file_contents: List[str] + file_paths: List[str] fi=frontend.FetchDescriptor(subsections="cli update-theme") try: search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname @@ -319,7 +320,7 @@ def _handle_help_message(full_help: bool=False): print("\t"+fd.reof("options-version", "--version: Show the current version of clitheme")) print("\t"+fd.reof("options-help", "--help: Show this help message")) -def _get_file_contents(file_paths: list) -> list: +def _get_file_contents(file_paths: List[str]) -> List[str]: fi=frontend.FetchDescriptor(subsections="cli apply-theme") content_list=[] line_prefix="\x1b[2K\r" # clear current line content and move cursor to beginning @@ -354,7 +355,7 @@ class _direct_exit(Exception): """ self.code=code -def main(cli_args: list): +def main(cli_args: List[str]): """ Use this function invoke 'clitheme' with command line arguments @@ -391,7 +392,7 @@ def main(cli_args: list): else: paths.append(arg) fi=frontend.FetchDescriptor(subsections="cli apply-theme") - content_list: list + content_list: List[str] try: content_list=_get_file_contents(paths) except _direct_exit as exc: return exc.code except: diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index bc274da..d9336a1 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -21,6 +21,7 @@ def _labeled_print(msg: str): from .. import _globalvar, cli, frontend from .._generator import db_interface +from typing import List # spell-checker:ignore lsdir showhelp argcount nosubst @@ -99,7 +100,7 @@ def _handle_error(message: str): print(fd.reof("help-usage-prompt", "Run \"clitheme-exec --help\" for usage information")) return 1 -def main(arguments: list): +def main(arguments: List[str]): """ Invoke clitheme-exec using the given command line arguments diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 3bc6be7..f4445ce 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -24,7 +24,7 @@ import re import sqlite3 import time import threading -from typing import Optional +from typing import Optional, List from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print @@ -35,7 +35,7 @@ fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subse # https://docs.python.org/3/library/stdtypes.html#str.splitlines newlines=(b'\n',b'\r',b'\r\n',b'\v',b'\f',b'\x1c',b'\x1d',b'\x1e',b'\x85') -def _process_debug(lines: list, debug_mode: list, is_stderr: bool=False, matched: bool=False, failed: bool=False) -> list: +def _process_debug(lines: List[bytes], debug_mode: List[str], is_stderr: bool=False, matched: bool=False, failed: bool=False) -> List[bytes]: final_lines=[] for x in range(len(lines)): line=lines[x] @@ -61,7 +61,7 @@ def _process_debug(lines: list, debug_mode: list, is_stderr: bool=False, matched final_lines.append(line) return final_lines -def handler_main(command: list, debug_mode: list=[], subst: bool=True): +def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True): do_subst=subst if do_subst==True: try: db_interface.connect_db() diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 81bdd35..65deb08 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -17,6 +17,7 @@ import shutil import signal import time from . import _globalvar, frontend +from typing import List def _labeled_print(msg: str): print("[clitheme-man] "+msg) @@ -24,7 +25,7 @@ frontend.global_domain="swiftycode" frontend.global_appname="clitheme" fd=frontend.FetchDescriptor(subsections="man") -def main(args: list): +def main(args: List[str]): """ Invoke clitheme-man using the given command line arguments -- Gitee From 29a7e8e7756f42e90b8d7d2e34a6993a16363af0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 25 Sep 2024 21:38:54 +0800 Subject: [PATCH 051/122] Fix test program --- src/clithemedef-test_testprogram.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index 4271d75..a32bfe6 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -85,10 +85,12 @@ if errorcount>0: exit(1) else: print("Generator test OK") - shutil.rmtree(generator_path) # remove the temp directory print("==> ",end='') if errorcount_frontend>0: print("Frontend test error: "+str(errorcount_frontend)+" errors found") + print("See "+generator_path+" for more details") exit(1) else: - print("Frontend test OK") \ No newline at end of file + print("Frontend test OK") +if errorcount>0 and errorcount_frontend>0: + shutil.rmtree(generator_path) # remove the temp directory \ No newline at end of file -- Gitee From a85701d637f51de0d6dd3a53ec2d2e8d6a11edb2 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 25 Sep 2024 22:18:38 +0800 Subject: [PATCH 052/122] Add copyright notice to more files --- src/clitheme-testblock_testprogram.py | 6 +++++- src/clitheme/__init__.py | 6 ++++++ src/clitheme/_generator/_entry_block_handler.py | 6 ++++++ src/clitheme/_get_resource.py | 6 ++++++ src/clitheme/_version.py | 6 ++++++ src/clithemedef-test_testprogram.py | 6 ++++++ src/db_interface_tests.py | 8 ++++++-- 7 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/clitheme-testblock_testprogram.py b/src/clitheme-testblock_testprogram.py index af3b642..8d5ceb0 100644 --- a/src/clitheme-testblock_testprogram.py +++ b/src/clitheme-testblock_testprogram.py @@ -1,4 +1,8 @@ -#!/usr/bin/python3 +# Copyright © 2023-2024 swiftycode + +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this program. If not, see . # Program for testing multi-line (block) processing of _generator from clitheme import _generator, frontend diff --git a/src/clitheme/__init__.py b/src/clitheme/__init__.py index 025dd95..8ebd7d9 100644 --- a/src/clitheme/__init__.py +++ b/src/clitheme/__init__.py @@ -1,3 +1,9 @@ +# Copyright © 2023-2024 swiftycode + +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this program. If not, see . + __all__=["frontend", "cli", "man", "exec"] # Prevent RuntimeWarning from displaying when running a submodule (e.g. "python3 -m clitheme.exec") diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index e0ec492..315c620 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -1,3 +1,9 @@ +# Copyright © 2023-2024 swiftycode + +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this program. If not, see . + import sys import re import copy diff --git a/src/clitheme/_get_resource.py b/src/clitheme/_get_resource.py index 7941970..83646b8 100644 --- a/src/clitheme/_get_resource.py +++ b/src/clitheme/_get_resource.py @@ -1,3 +1,9 @@ +# Copyright © 2023-2024 swiftycode + +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this program. If not, see . + """ Script to get contents of file inside the module """ diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 6a254ad..6975111 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,3 +1,9 @@ +# Copyright © 2023-2024 swiftycode + +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this program. If not, see . + """ Version information definition file """ diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index a32bfe6..0ab5dc4 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -1,3 +1,9 @@ +# Copyright © 2023-2024 swiftycode + +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this program. If not, see . + import shutil from clitheme import _generator from clitheme import _globalvar diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index abdf989..9f62371 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -1,8 +1,12 @@ +# Copyright © 2023-2024 swiftycode + +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this program. If not, see . + from clitheme._generator import db_interface from clitheme import _generator, _globalvar import shutil -import os -import unittest # sample input for testing sample_inputs=[("rm: missing operand", "rm"), -- Gitee From c88dafa660d4fb308ac2a3f0b139a5acc5dab856 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 27 Sep 2024 10:35:22 +0800 Subject: [PATCH 053/122] Move set Windows terminal mode operation to init --- src/clitheme/__init__.py | 22 +++++++++++++++++++++- src/clitheme/_globalvar.py | 18 +----------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/clitheme/__init__.py b/src/clitheme/__init__.py index 8ebd7d9..40fb812 100644 --- a/src/clitheme/__init__.py +++ b/src/clitheme/__init__.py @@ -9,6 +9,26 @@ __all__=["frontend", "cli", "man", "exec"] # Prevent RuntimeWarning from displaying when running a submodule (e.g. "python3 -m clitheme.exec") import warnings warnings.simplefilter("ignore", category=RuntimeWarning) +del warnings + +# Enable processing of escape characters in Windows Command Prompt +import os +if os.name=="nt": + import ctypes + + try: + handle=ctypes.windll.kernel32.GetStdHandle(-11) # standard output handle + console_mode=ctypes.c_long() + if ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(console_mode))==0: + raise Exception("GetConsoleMode failed: "+str(ctypes.windll.kernel32.GetLastError())) + console_mode.value|=0x0004 # ENABLE_VIRTUAL_TERMINAL_PROCESSING + if ctypes.windll.kernel32.SetConsoleMode(handle, console_mode.value)==0: + raise Exception("SetConsoleMode failed: "+str(ctypes.windll.kernel32.GetLastError())) + except: + pass +del os + +# Expose these modules when "clitheme" is imported from . import _globalvar, frontend, cli, man, exec _globalvar.handle_set_themedef(frontend, "global") # type: ignore -del _globalvar # Don't expose this module by default \ No newline at end of file +del _globalvar # Don't expose this module \ No newline at end of file diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index ca47134..b7510cc 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -5,7 +5,7 @@ # You should have received a copy of the GNU General Public License along with this program. If not, see . """ -Global variable definitions and initialization operations for clitheme +Global variable definitions for clitheme """ import io @@ -19,22 +19,6 @@ from typing import List # spell-checker:ignoreRegExp banphrase[s]{0,1} -# Enable processing of escape characters in Windows Command Prompt -if os.name=="nt": - import ctypes - - try: - handle=ctypes.windll.kernel32.GetStdHandle(-11) # standard output handle - console_mode=ctypes.c_long() - if ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(console_mode))==0: - raise Exception("GetConsoleMode failed: "+str(ctypes.windll.kernel32.GetLastError())) - console_mode.value|=0x0004 # ENABLE_VIRTUAL_TERMINAL_PROCESSING - if ctypes.windll.kernel32.SetConsoleMode(handle, console_mode.value)==0: - raise Exception("SetConsoleMode failed: "+str(ctypes.windll.kernel32.GetLastError())) - except: - pass - - error_msg_str= \ """[clitheme] Error: unable to get your home directory or invalid home directory information. Please make sure that the {var} environment variable is set correctly. -- Gitee From 58f85ba577926ea20dc661562bcca35a01289d0a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 30 Sep 2024 21:17:12 +0800 Subject: [PATCH 054/122] Use function to set default frontend values - The new functions in the `frontend` module allows setting defaults values independently for each code file, preventing conflicts --- frontend_demo.py | 4 +- src/clitheme-testblock_testprogram.py | 2 +- src/clitheme/_globalvar.py | 6 +-- src/clitheme/cli.py | 6 +-- src/clitheme/exec/__init__.py | 4 +- src/clitheme/frontend.py | 69 +++++++++++++++++++++++---- src/clitheme/man.py | 4 +- src/clithemedef-test_testprogram.py | 4 +- 8 files changed, 75 insertions(+), 24 deletions(-) diff --git a/frontend_demo.py b/frontend_demo.py index 713a1c7..48635cf 100755 --- a/frontend_demo.py +++ b/frontend_demo.py @@ -50,8 +50,8 @@ com.example example-app helpmessage unknown-command 错误:未知命令"{}" """ -frontend.global_domain="com.example" -frontend.global_appname="example-app" +frontend.set_domain("com.example") +frontend.set_appname("example-app") f=frontend.FetchDescriptor() if len(sys.argv)>1 and sys.argv[1]=="install-files": diff --git a/src/clitheme-testblock_testprogram.py b/src/clitheme-testblock_testprogram.py index 8d5ceb0..223e3f8 100644 --- a/src/clitheme-testblock_testprogram.py +++ b/src/clitheme-testblock_testprogram.py @@ -57,7 +57,7 @@ begin_main end_main """ -frontend.global_debugmode=True +frontend.set_debugmode(True) if frontend.set_local_themedef(file_data)==False: print("Error: set_local_themedef failed") exit(1) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index b7510cc..05b814f 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -181,13 +181,13 @@ def handle_set_themedef(fr: frontend, debug_name: str): # type: ignore file_contents=list(map(lambda name: _get_resource.read_file(name), files)) msg=io.StringIO() sys.stdout=msg - fr.global_debugmode=True + fr.set_debugmode(True) if not fr.set_local_themedefs(file_contents): raise RuntimeError("Full log below: \n"+msg.getvalue()) - fr.global_debugmode=prev_mode + fr.set_debugmode(prev_mode) sys.stdout=orig_stdout except: sys.stdout=orig_stdout - fr.global_debugmode=prev_mode + fr.set_debugmode(prev_mode) if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1]), file=sys.__stdout__) handle_exception() finally: sys.stdout=orig_stdout diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 97a8009..64ff9ee 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -24,9 +24,9 @@ from typing import List # spell-checker:ignore pathnames lsdir inpstr -frontend.global_domain="swiftycode" -frontend.global_appname="clitheme" -frontend.global_subsections="cli" +frontend.set_domain("swiftycode") +frontend.set_appname("clitheme") +frontend.set_subsections("cli") last_data_path="" def apply_theme(file_contents: List[str], filenames: List[str], overlay: bool=False, preserve_temp=False, generate_only=False): diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index d9336a1..7ca3cf5 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -25,8 +25,8 @@ from typing import List # spell-checker:ignore lsdir showhelp argcount nosubst -frontend.global_domain="swiftycode" -frontend.global_appname="clitheme" +frontend.set_domain("swiftycode") +frontend.set_appname("clitheme") fd=frontend.FetchDescriptor(subsections="exec") # Prevent recursion dead loops and accurately simulate that regeneration is only triggered once diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 27d0630..f222e9f 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -18,13 +18,56 @@ import string import re import hashlib import shutil -from typing import Optional, List +import inspect +from typing import Optional, List, Union, Dict from . import _globalvar # spell-checker:ignore newhash numorig numcur data_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_data_pathname +_setting_defs: Dict[str, type]={ + "domain": str, + "appname": str, + "subsections": str, + "debugmode": bool, + "lang": str, + "disablelang": bool, +} + +_local_settings: Dict[str, Dict[str, Union[None,str,bool]]]={} + +for name in _setting_defs.keys(): + _local_settings[name]={} + +def _get_caller() -> str: + assert len(inspect.stack())>=4, "Cannot determine filename from call stack" + # inspect.stack(): [0: this function, 1: update/get settings, 2: function in frontend module, 3: target calling function] + filename=inspect.stack()[3].filename + return filename + +def _update_local_settings(key: str, value: Union[None,str,bool]): + _local_settings[key][_get_caller()]=value + if _get_setting("debugmode", _get_caller())==True: + print(f"[Debug] Set {key}={value} for file \"{_get_caller()}\"") + + +_desc=\ +""" +Set default value for `{}` option in future FetchDescriptor instances. +This setting is valid for the module/code file that invokes this function. + +- Set value=None to unset the default value and use values defined in global variables +- Change global variables (e.g. global_domain, global_debugmode) to set the default value for all files in an invoking module +""" + +def set_domain(value: Optional[str]): _desc.format("domain_name");_update_local_settings("domain", value) +def set_appname(value: Optional[str]): _desc.format("app_name");_update_local_settings("appname", value) +def set_subsections(value: Optional[str]): _desc.format("subsections");_update_local_settings("subsections", value) +def set_debugmode(value: Optional[bool]): _desc.format("debug_mode");_update_local_settings("debugmode", value) +def set_lang(value: Optional[str]): _desc.format("lang");_update_local_settings("lang", value) +def set_disablelang(value: Optional[bool]): _desc.format("disable_lang");_update_local_settings("disablelang", value) + global_domain="" global_appname="" global_subsections="" @@ -32,6 +75,14 @@ global_debugmode=False global_lang="" # Override locale global_disablelang=False +def _get_setting(key: str, caller: Optional[str]=None) -> Union[str,bool]: + # Get local settings + value=_local_settings[key].get(caller if caller!=None else _get_caller()) + if value!=None: return value + # Get global settings if not found + else: + return eval(f"global_{key}") + _alt_path=None _alt_path_dirname=None _alt_path_hash=None @@ -85,7 +136,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: path_name=_globalvar.clitheme_temp_root+"/"+dir_name if _alt_path_dirname!=None and overlay==True: # overlay if not os.path.exists(path_name): shutil.copytree(_globalvar.clitheme_temp_root+"/"+_alt_path_dirname, _generator.path) - if global_debugmode: print("[Debug] "+path_name) + if _get_setting("debugmode"): print("[Debug] "+path_name) # Generate data hierarchy as needed if not os.path.exists(path_name): _generator.silence_warn=True @@ -96,7 +147,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: global_debugmode=False return_val=_generator.generate_data_hierarchy(file_content, custom_path_gen=False) except SyntaxError: - if global_debugmode: print("[Debug] Generator error: "+str(sys.exc_info()[1])) + if _get_setting("debugmode"): print("[Debug] Generator error: "+str(sys.exc_info()[1])) return False finally: global_debugmode=d_copy if not os.path.exists(path_name): @@ -155,37 +206,37 @@ class FetchDescriptor(): # Leave domain and app names blank for global reference if domain_name==None: - self.domain_name=global_domain.strip() + self.domain_name: str=_get_setting("domain").strip() #type:ignore else: self.domain_name=domain_name.strip() if len(self.domain_name.split())>1: raise SyntaxError("Only one phrase is allowed for domain_name") if app_name==None: - self.app_name=global_appname.strip() + self.app_name: str=_get_setting("appname").strip() #type:ignore else: self.app_name=app_name.strip() if len(self.app_name.split())>1: raise SyntaxError("Only one phrase is allowed for app_name") if subsections==None: - self.subsections=global_subsections.strip() + self.subsections: str=_get_setting("subsections").strip() #type:ignore else: self.subsections=subsections.strip() self.subsections=re.sub(" {2,}", " ", self.subsections) if lang==None: - self.lang=global_lang.strip() + self.lang=_get_setting("lang").strip() #type:ignore else: self.lang=lang.strip() if debug_mode==None: - self.debug_mode=global_debugmode + self.debug_mode: bool=_get_setting("debugmode") #type:ignore else: self.debug_mode=debug_mode if disable_lang==None: - self.disable_lang=global_disablelang + self.disable_lang: bool=_get_setting("disablelang") #type:ignore else: self.disable_lang=disable_lang diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 65deb08..1c6b41c 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -21,8 +21,8 @@ from typing import List def _labeled_print(msg: str): print("[clitheme-man] "+msg) -frontend.global_domain="swiftycode" -frontend.global_appname="clitheme" +frontend.set_domain("swiftycode") +frontend.set_appname("clitheme") fd=frontend.FetchDescriptor(subsections="man") def main(args: List[str]): diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index 0ab5dc4..1dde998 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -50,8 +50,8 @@ for line in expected_data.splitlines(): # Test frontend print("Testing frontend...") from clitheme import frontend -frontend.global_lang="en_US.UTF-8" -frontend.global_debugmode=True +frontend.set_debugmode(True) +frontend.set_lang("en_US.UTF-8") frontend.data_path=generator_path+"/"+_globalvar.generator_data_pathname expected_data_frontend=open(root_directory+"/testprogram-data/clithemedef-test_expected-frontend.txt", 'r', encoding="utf-8").read() current_path_frontend="" -- Gitee From 9ba1fbe80bd070c3bdf40cbe0f9ba1512ccec910 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 30 Sep 2024 23:14:12 +0800 Subject: [PATCH 055/122] Reset `silence_warn` in set_local_themedef Avoid potential problems with it --- src/clitheme/frontend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index f222e9f..5c2c75a 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -141,7 +141,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: if not os.path.exists(path_name): _generator.silence_warn=True return_val: str - d_copy=global_debugmode + d_copy=(global_debugmode, _generator.silence_warn) try: # Set this to prevent extra messages from being displayed global_debugmode=False @@ -149,7 +149,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: except SyntaxError: if _get_setting("debugmode"): print("[Debug] Generator error: "+str(sys.exc_info()[1])) return False - finally: global_debugmode=d_copy + finally: global_debugmode, _generator.silence_warn=d_copy if not os.path.exists(path_name): shutil.copytree(return_val, path_name) try: shutil.rmtree(return_val) -- Gitee From ab6a1ee6379b5cc7f679679bb49c0780585ca53e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 30 Sep 2024 23:34:43 +0800 Subject: [PATCH 056/122] Update version (v2.0-dev20240930) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index e147cf1..feece63 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240917 +pkgver=2.0_dev20240930 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 2f331af..383770d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240917-1) unstable; urgency=low +clitheme (2.0-dev20240930-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Tue, Sep 17 2024 22:03:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, Sep 30 2024 23:32:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 6975111..0bdc815 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,11 +11,11 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240917" +__version__="2.0-dev20240930" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240917" +version_main="2.0_dev20240930" version_buildnumber=1 \ No newline at end of file -- Gitee From cc22f39dd414982027fd6b7287ae85a516c85f3c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 1 Oct 2024 22:21:34 +0800 Subject: [PATCH 057/122] Add `!require_version` phrase support --- src/clitheme/_generator/__init__.py | 14 +++++++++++++- src/clitheme/_generator/_dataclass.py | 19 ++++++++++++++++++- src/clitheme/_version.py | 1 + .../strings/generator-strings.clithemedef.txt | 12 ++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 1473a68..82e8819 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -37,8 +37,11 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info global path obj=_dataclass.GeneratorObject(file_content=file_content, custom_infofile_name=custom_infofile_name, filename=filename, path=path, silence_warn=silence_warn) + before_content_lines=True while obj.goto_next_line(): - first_phrase=obj.lines_data[obj.lineindex].split()[0] + phrases=obj.lines_data[obj.lineindex].split() + first_phrase=phrases[0] + is_content=True if first_phrase in ("begin_header", r"{header_section}"): _header_parser.handle_header_section(obj, first_phrase) elif first_phrase in ("begin_main", r"{entries_section}"): @@ -48,8 +51,17 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif first_phrase==r"{manpage_section}": _manpage_parser.handle_manpage_section(obj, first_phrase) elif obj.handle_setters(really_really_global=True): pass + elif first_phrase=="!require_version": + is_content=False + obj.check_enough_args(phrases, 2) + obj.check_extra_args(phrases, 2, use_exact_count=True) + if not before_content_lines: + obj.handle_error(obj.fd.feof("phrase-precedence-err", "Line {num}: header macro \"{phrase}\" must be specified before other lines", num=str(obj.lineindex+1), phrase=first_phrase)) + obj.check_version(phrases[1]) else: obj.handle_invalid_phrase(first_phrase) + if is_content: before_content_lines=False + def is_content_parsed() -> bool: content_sections=["entries", "substrules", "manpage"] for section in content_sections: diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 6bd3a64..6cb537b 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -14,7 +14,7 @@ import math import copy import uuid from typing import Optional, Union, List, Dict -from .. import _globalvar +from .. import _globalvar, _version from . import _handlers, _entry_block_handler # spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption @@ -75,6 +75,23 @@ class GeneratorObject(_handlers.DataHandlers): else: not_pass=len(phrases)>count if not_pass: self.handle_error(self.fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=self.fmt(phrases[0]))) + def check_version(self, version_str: str): + match_result=re.match(r"^(?P\d+)\.(?P\d+)(\.(?P\d+))?(-beta(?P\d+))?$", version_str) + def invalid_version(): self.handle_error(self.fd.feof("invalid-version-err", "Invalid version information \"{ver}\" on line {num}", ver=self.fmt(version_str), num=str(self.lineindex+1))) + if match_result==None: invalid_version() + elif int(match_result.groupdict()['major'])<2: invalid_version() + else: + version_ok= int(match_result.groupdict()['major'])<=_version.major \ + and int(match_result.groupdict()['minor'])<=_version.minor \ + and (int(match_result.groupdict()['bugfix']) if match_result.groupdict().get("bugfix")!=None else -1)<=_version.release + if match_result.groupdict().get("beta_release")!=None and _version.beta_release!=None: + version_ok=version_ok and int(match_result.groupdict()['beta_release'])<=_version.beta_release + + if not version_ok: + self.handle_error(self.fd.feof("unsupported-version-err", "Current version of clitheme ({cur_ver}) does not support this file (requires {req_ver} or higher)", + cur_ver=_version.__version__+ \ + (f" [beta{_version.beta_release}]" if _version.beta_release!=None and not "beta" in _version.__version__ else ""), + req_ver=self.fmt(version_str))) def handle_invalid_phrase(self, name: str): self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=self.fmt(name), num=str(self.lineindex+1))) def parse_options(self, options_data: List[str], merge_global_options: int, allowed_options: Optional[list]=None) -> Dict[str, Union[int,bool]]: diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 0bdc815..bfb6c55 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -15,6 +15,7 @@ __version__="2.0-dev20240930" major=2 minor=0 release=-1 # -1 stands for "dev" +beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead version_main="2.0_dev20240930" diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index bea4f0a..36c624c 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -56,6 +56,18 @@ in_domainapp swiftycode clitheme # locale:default Failed to migrate existing substrules database; try performing the operation without using "--overlay" locale:zh_CN 无法升级当前的substrules的数据库;请尝试不使用"--overlay"再次执行此操作 [/entry] + [entry] invalid-version-err + # locale:default Invalid version information "{ver}" on line {num} + locale:zh_CN 第{num}行:无效版本信息"{ver}" + [/entry] + [entry] unsupported-version-err + # locale:default Current version of clitheme ({cur_ver}) does not support this file (requires {req_ver} or higher) + locale:zh_CN 当前版本的clitheme({cur_ver})不支持此文件(需要 {req_ver} 或更高版本) + [/entry] + [entry] phrase-precedence-err + # locale:default Line {num}: header macro "{phrase}" must be specified before other lines + locale:zh_CN 第{num}行:头定义"{phrase}"必须在其他行之前声明 + [/entry] # 选项错误提示信息 # Options error messages [entry] option-not-allowed-err -- Gitee From 2171ee4575de47c199a5610c19fc8db1decf244c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 1 Oct 2024 22:30:49 +0800 Subject: [PATCH 058/122] Fix processing of `foregroundonly` option when specified inline Only restore `foregroundonly` option to previous when the option is specified after `[/filter_commands]` --- src/clitheme/_generator/_substrules_parser.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index fff16f6..48d0664 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -21,15 +21,15 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str command_filters: Optional[list]=None command_filter_strictness=0 # If True, reset foregroundonly option to beforehand during next command filter - inline_foregroundonly=None - def reset_inline_foregroundonly(): + outline_foregroundonly=None + def reset_outline_foregroundonly(): """ Set foregroundonly option to false if foregroundonly option is "inline" and not enabled previously """ - nonlocal inline_foregroundonly - if inline_foregroundonly!=None: - obj.global_options['foregroundonly']=inline_foregroundonly - inline_foregroundonly=None + nonlocal outline_foregroundonly + if outline_foregroundonly!=None: + obj.global_options['foregroundonly']=outline_foregroundonly + outline_foregroundonly=None # initialize the database if os.path.exists(obj.path+"/"+_globalvar.db_filename): try: obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) @@ -43,7 +43,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str phrases=obj.lines_data[obj.lineindex].split() if phrases[0]=="[filter_commands]": obj.check_extra_args(phrases, 1, use_exact_count=True) - reset_inline_foregroundonly() + reset_outline_foregroundonly() content=obj.handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=r"[/filter_commands]", disallow_cmdmatch_options=False, disable_substesc=True) # read commands command_strings=content.splitlines() @@ -51,8 +51,10 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str strictness=0 # parse strictcmdmatch, exactcmdmatch, and other cmdmatch options here got_options=copy.copy(obj.global_options) + inline_options={} if len(obj.lines_data[obj.lineindex].split())>1: got_options=obj.parse_options(obj.lines_data[obj.lineindex].split()[1:], merge_global_options=True, allowed_options=obj.block_input_options+obj.command_filter_options) + inline_options=obj.parse_options(obj.lines_data[obj.lineindex].split()[1:], merge_global_options=False, allowed_options=obj.block_input_options+obj.command_filter_options) for this_option in got_options: if this_option=="strictcmdmatch" and got_options['strictcmdmatch']==True: strictness=1 @@ -60,16 +62,16 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str strictness=2 elif this_option=="smartcmdmatch" and got_options['smartcmdmatch']==True: strictness=-1 - elif this_option=="foregroundonly": - inline_foregroundonly=obj.global_options.get('foregroundonly')==True - obj.global_options['foregroundonly']=got_options['foregroundonly'] + elif this_option=="foregroundonly" and "foregroundonly" in inline_options.keys(): + outline_foregroundonly=obj.global_options.get('foregroundonly')==True + obj.global_options['foregroundonly']=inline_options['foregroundonly'] command_filters=[] for cmd in command_strings: command_filters.append(cmd.strip()) command_filter_strictness=strictness elif phrases[0]=="filter_command": obj.check_enough_args(phrases, 2) - reset_inline_foregroundonly() + reset_outline_foregroundonly() content=_globalvar.splitarray_to_string(phrases[1:]) content=obj.subst_variable_content(content) strictness=0 @@ -84,7 +86,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str command_filter_strictness=strictness elif phrases[0]=="unset_filter_command": obj.check_extra_args(phrases, 1, use_exact_count=True) - reset_inline_foregroundonly() + reset_outline_foregroundonly() command_filters=None elif phrases[0] in ("[substitute_string]", "[substitute_regex]"): obj.check_enough_args(phrases, 2) -- Gitee From 9478a4b762198caf3d4e0c84d579714dddb209f8 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 1 Oct 2024 22:47:34 +0800 Subject: [PATCH 059/122] Don't display "Syntax error" for some errors --- src/clitheme/_generator/_dataclass.py | 2 +- src/clitheme/_generator/_handlers.py | 8 ++++---- src/clitheme/_generator/_manpage_parser.py | 2 +- src/clitheme/_generator/_substrules_parser.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 6cb537b..d93e6fc 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -91,7 +91,7 @@ class GeneratorObject(_handlers.DataHandlers): self.handle_error(self.fd.feof("unsupported-version-err", "Current version of clitheme ({cur_ver}) does not support this file (requires {req_ver} or higher)", cur_ver=_version.__version__+ \ (f" [beta{_version.beta_release}]" if _version.beta_release!=None and not "beta" in _version.__version__ else ""), - req_ver=self.fmt(version_str))) + req_ver=self.fmt(version_str)), not_syntax_error=True) def handle_invalid_phrase(self, name: str): self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=self.fmt(name), num=str(self.lineindex+1))) def parse_options(self, options_data: List[str], merge_global_options: int, allowed_options: Optional[list]=None) -> Dict[str, Union[int,bool]]: diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index 5e14a57..605504e 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -26,8 +26,8 @@ class DataHandlers: if not os.path.exists(self.datapath): os.mkdir(self.datapath) self.fd=self.frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") self.fmt=_globalvar.make_printable # alias for the make_printable function - def handle_error(self, message: str): - output=self.fd.feof("error-str", "Syntax error: {msg}", msg=message) + def handle_error(self, message: str, not_syntax_error: bool=False): + output=message if not_syntax_error else self.fd.feof("error-str", "Syntax error: {msg}", msg=message) raise SyntaxError(output) def handle_warning(self, message: str): output=self.fd.feof("warning-str", "Warning: {msg}", msg=message) @@ -81,7 +81,7 @@ class DataHandlers: # create the parent directory try: os.makedirs(parent_path, exist_ok=True) except (FileExistsError, NotADirectoryError): - self.handle_error(self.fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) + self.handle_error(self.fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug)), not_syntax_error=True) full_path=parent_path+"/"+file_path[-1] if os.path.isfile(full_path): if line_number_debug!=-1: self.handle_warning(self.fd.feof("repeated-manpage-warn","Line {num}: repeated manpage file, overwriting", num=str(line_number_debug))) @@ -90,4 +90,4 @@ class DataHandlers: open(full_path, "w", encoding="utf-8").write(content) open(full_path+".gz", "wb").write(gzip.compress(bytes(content, "utf-8"))) except IsADirectoryError: - self.handle_error(self.fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) \ No newline at end of file + self.handle_error(self.fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug)), not_syntax_error=True) \ No newline at end of file diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 9e56443..41dc9b9 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -30,7 +30,7 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): # get file content filecontent: str try: filecontent=open(file_dir, 'r', encoding="utf-8").read() - except: obj.handle_error(obj.fd.feof("include-file-read-error", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(obj.lineindex+1), filepath=obj.fmt(file_dir), error_msg=sys.exc_info()[1])) + except: obj.handle_error(obj.fd.feof("include-file-read-error", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(obj.lineindex+1), filepath=obj.fmt(file_dir), error_msg=sys.exc_info()[1]), not_syntax_error=True) # write manpage files in theme-info for db migration feature to work successfully obj.write_manpage_file(filepath, filecontent, -1, custom_parent_path=obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name+"/manpage_data") return filecontent diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 48d0664..163a794 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -35,7 +35,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str try: obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) except obj.db_interface.need_db_regenerate: from ..exec import _check_regenerate_db - if not _check_regenerate_db(obj.path): raise RuntimeError(obj.fd.reof("db-regenerate-fail-err", "Failed to migrate existing substrules database; try performing the operation without using \"--overlay\"")) + if not _check_regenerate_db(obj.path): obj.handle_error(obj.fd.reof("db-regenerate-fail-err", "Failed to migrate existing substrules database; try performing the operation without using \"--overlay\""), not_syntax_error=True) obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) else: obj.db_interface.init_db(obj.path+"/"+_globalvar.db_filename) obj.db_interface.debug_mode=not obj.silence_warn -- Gitee From 3835242ee32f2b6ba0421a102a40100b89157486 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 1 Oct 2024 23:22:20 +0800 Subject: [PATCH 060/122] Display prompt when reading stdin from `manpage_section` --- src/clitheme/_generator/_manpage_parser.py | 7 ++++++- src/clitheme/_globalvar.py | 13 +++++++++++++ src/clitheme/cli.py | 9 +-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 41dc9b9..a39293b 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -28,9 +28,14 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): parent_dir+=os.path.dirname(obj.filename) file_dir=parent_dir+("/" if parent_dir!="" else "")+_globalvar.splitarray_to_string(filepath).replace(" ","/") # get file content + orig_stdout=sys.stdout + sys.stdout=sys.__stdout__ + is_stdin=_globalvar.handle_stdin_prompt(file_dir) filecontent: str try: filecontent=open(file_dir, 'r', encoding="utf-8").read() - except: obj.handle_error(obj.fd.feof("include-file-read-error", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(obj.lineindex+1), filepath=obj.fmt(file_dir), error_msg=sys.exc_info()[1]), not_syntax_error=True) + except: obj.handle_error(obj.fd.feof("include-file-read-err", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(obj.lineindex+1), filepath=obj.fmt(file_dir), error_msg=sys.exc_info()[1]), not_syntax_error=True) + if is_stdin: print() + sys.stdout=orig_stdout # write manpage files in theme-info for db migration feature to work successfully obj.write_manpage_file(filepath, filecontent, -1, custom_parent_path=obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name+"/manpage_data") return filecontent diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 05b814f..a8e66ed 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -13,6 +13,7 @@ import os import sys import re import string +import stat from copy import copy from . import _version from typing import List @@ -172,6 +173,18 @@ def handle_exception(): if env_var in os.environ and os.environ[env_var]=="1": raise +def handle_stdin_prompt(path: str) -> bool: + fi=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="cli apply-theme") + is_stdin=False + try: + if os.stat(path).st_ino==os.stat(sys.stdin.fileno()).st_ino: + is_stdin=True + print("\n"+fi.reof("reading-stdin-note", "Reading from standard input")) + if not stat.S_ISFIFO(os.stat(path).st_mode): + print(fi.feof("stdin-interactive-finish-prompt", "Input file content here and press {shortcut} to finish", shortcut="CTRL-D" if os.name=="posix" else "CTRL-Z+")) + except: pass + return is_stdin + def handle_set_themedef(fr: frontend, debug_name: str): # type: ignore prev_mode=False # Prevent interference with other code piping stdout diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 64ff9ee..8e69e9c 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -329,14 +329,7 @@ def _get_file_contents(file_paths: List[str]) -> List[str]: try: print(line_prefix+fi.feof("reading-file","==> Reading file {filename}...", filename=f"({i+1}/{len(file_paths)})"), end='') # Detect standard input - is_stdin=False - try: - if os.stat(path).st_ino==os.stat(sys.stdin.fileno()).st_ino: - is_stdin=True - print("\n"+fi.reof("reading-stdin-note", "Reading from standard input")) - if not stat.S_ISFIFO(os.stat(path).st_mode): - print(fi.feof("stdin-interactive-finish-prompt", "Input file content here and press {shortcut} to finish", shortcut="CTRL-D" if os.name=="posix" else "CTRL-Z+")) - except: pass + is_stdin=_globalvar.handle_stdin_prompt(path) content_list.append(open(path, 'r', encoding="utf-8").read()) if is_stdin: print() # Print an extra newline except KeyboardInterrupt: -- Gitee From 686c1dd6be0ed863583ee5742a5aa675332853e2 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 3 Oct 2024 13:30:34 +0800 Subject: [PATCH 061/122] Sync `global_options` with `really_really_global_options` Also change `global_options` when changing `really_really_global_options` to reduce potential issues --- src/clitheme/_generator/_dataclass.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index d93e6fc..b4bc95f 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -137,8 +137,7 @@ class GeneratorObject(_handlers.DataHandlers): # set options globally if really_really_global: self.really_really_global_options=self.parse_options(options_data, merge_global_options=2) - else: - self.global_options=self.parse_options(options_data, merge_global_options=1) + self.global_options=self.parse_options(options_data, merge_global_options=1) def handle_setup_global_options(self): # reset global_options to contents of really_really_global_options self.global_options=copy.copy(self.really_really_global_options) @@ -182,12 +181,10 @@ class GeneratorObject(_handlers.DataHandlers): var_content=_globalvar.extract_content(line_content) # subst variable references - check_list=self.really_really_global_options if really_really_global else self.global_options - if "substvar" in check_list and check_list["substvar"]==True: - var_content=self.subst_variable_content(var_content, override_check=True) + var_content=self.subst_variable_content(var_content) # set variable if really_really_global: self.really_really_global_variables[var_name]=var_content - else: self.global_variables[var_name]=var_content + self.global_variables[var_name]=var_content def handle_begin_section(self, section_name: str): if section_name in self.parsed_sections: self.handle_error(self.fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(self.lineindex+1), section=section_name)) -- Gitee From 271c1f9ebaa5c0f992e8979e05cb160c4f40a791 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 4 Oct 2024 15:44:45 +0800 Subject: [PATCH 062/122] Remove extra substvar operation in `handle_setters` --- src/clitheme/_generator/_dataclass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index b4bc95f..7ffa5dc 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -209,7 +209,7 @@ class GeneratorObject(_handlers.DataHandlers): phrases=self.lines_data[self.lineindex].split() if phrases[0]=="set_options": self.check_enough_args(phrases, 2) - self.handle_set_global_options(self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split(), really_really_global) + self.handle_set_global_options(_globalvar.splitarray_to_string(phrases[1:]).split(), really_really_global) elif phrases[0].startswith("setvar:"): self.check_enough_args(phrases, 2) self.handle_set_variable(self.lines_data[self.lineindex], really_really_global) -- Gitee From 8f1681d30285d40ec2f98ea55b5344d8de7325a0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 3 Oct 2024 13:18:43 +0800 Subject: [PATCH 063/122] Implement substvar warning Show warning when trying to access a defined variable without enabling the `substvar` option --- src/clitheme/_generator/_dataclass.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 7ffa5dc..17cce0b 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -37,9 +37,11 @@ class GeneratorObject(_handlers.DataHandlers): switch_options=[command_filter_options[:4]] # Disable these options for now (BETA) # switch_options+=[subst_limiting_options[:3]] + substvar_banphrases=['{', '}', '[', ']', '(', ')'] def __init__(self, file_content: str, custom_infofile_name: str, filename: str, path: str, silence_warn: bool): # data to keep track of + self.substvar_warning=True self.section_parsing=False self.parsed_sections=[] self.lines_data=file_content.splitlines() @@ -138,17 +140,30 @@ class GeneratorObject(_handlers.DataHandlers): if really_really_global: self.really_really_global_options=self.parse_options(options_data, merge_global_options=2) self.global_options=self.parse_options(options_data, merge_global_options=1) + # if manually disabled, show substvar warning again next time + if self.global_options.get("substvar")!=True: self.substvar_warning=True def handle_setup_global_options(self): # reset global_options to contents of really_really_global_options self.global_options=copy.copy(self.really_really_global_options) + # if manually disabled, show substvar warning again next time + if self.global_options.get("substvar")!=True: self.substvar_warning=True self.global_variables=copy.copy(self.really_really_global_variables) def subst_variable_content(self, content: str, override_check: bool=False, line_number_debug: Optional[str]=None, silence_warnings: bool=False) -> str: - if not override_check and (not "substvar" in self.global_options or self.global_options["substvar"]==False): return content + pattern=r"{{([^\s]+?)??}}" + if not override_check and self.global_options.get("substvar")!=True: + # Handle substvar warning + if self.substvar_warning: + for match in re.finditer(pattern, content): + if self.global_variables.get(match.group(1))!=None: + self.handle_warning(self.fd.feof("set-substvar-warn", "Line {num}: Attempted to reference a defined variable, but \"substvar\" option is not enabled", num=line_number_debug)) + self.substvar_warning=False + break + return content # get all variables used in content new_content=copy.copy(content) encountered_variables=set() offset=0 - for match in re.finditer(r"{{([^\s]+?)??}}", content): + for match in re.finditer(pattern, content): var_name=match.group(1) if var_name==None or var_name.strip()=='': continue if var_name=="ESC": continue # skip {{ESC}}; leave it for substesc @@ -175,8 +190,7 @@ class GeneratorObject(_handlers.DataHandlers): # sanity check var_name def bad_var(): self.handle_error(self.fd.feof("bad-var-name-err", "Line {num}: \"{name}\" is not a valid variable name", name=self.fmt(var_name), num=str(self.lineindex+1))) if var_name=='ESC': bad_var() - banphrases=['{', '}', '[', ']', '(', ')'] - for char in banphrases: + for char in self.substvar_banphrases: if char in var_name: bad_var() var_content=_globalvar.extract_content(line_content) @@ -276,8 +290,11 @@ class GeneratorObject(_handlers.DataHandlers): # substitute {{ESC}} with escape literal if got_options['substesc']==True and not disable_substesc: blockinput_data=self.handle_substesc(blockinput_data) elif option=="substvar": - if got_options['substvar']==True: blockinput_data=self.subst_variable_content(blockinput_data, True, line_number_debug=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1)) + if got_options['substvar']==True: blockinput_data=self.subst_variable_content(blockinput_data, override_check=True, line_number_debug=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1)) elif disallow_cmdmatch_options: if is_specified_in_block(): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option))) + if "substvar" not in specified_options: + # Let the function show the substvar warning + self.subst_variable_content(blockinput_data, override_check=False, line_number_debug=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1)) return blockinput_data handle_entry=_entry_block_handler.handle_entry \ No newline at end of file -- Gitee From 780eab38f8651d2060b2b1c68c5bb53b905839ed Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 4 Oct 2024 17:21:13 +0800 Subject: [PATCH 064/122] Change wording on processing files error message --- src/clitheme/cli.py | 2 +- src/clitheme/strings/cli-strings.clithemedef.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 8e69e9c..d77839a 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -106,7 +106,7 @@ def apply_theme(file_contents: List[str], filenames: List[str], overlay: bool=Fa if generator_msgs.getvalue()!='': # end='' because the pipe value already contains a newline due to the print statements print(generator_msgs.getvalue(), end='') - print(f.feof("process-files-error", "[File {index}] An error occurred while processing files:\n{message}", \ + print(f.feof("process-files-error", "[File {index}] An error occurred while processing the file:\n{message}", \ index=str(i+1), message=str(sys.exc_info()[1]))) if type(exc)==SyntaxError: _globalvar.handle_exception() else: raise # Always raise exception if other error occurred in _generator diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 90bc1d1..3dd2286 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -137,7 +137,7 @@ in_domainapp swiftycode clitheme [/entry] [entry] process-files-error # [locale] default - # [File {index}] An error occurred while processing files: + # [File {index}] An error occurred while processing the file: # {message} # [/locale] [locale] zh_CN -- Gitee From b5aa6dea780bed0b148b9ac12d1d82773e2c78e1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 5 Oct 2024 22:54:25 +0800 Subject: [PATCH 065/122] Update version (v2.0-dev20241005) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index feece63..efca91f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240930 +pkgver=2.0_dev20241005 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 383770d..84b1101 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240930-1) unstable; urgency=low +clitheme (2.0-dev20241005-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, Sep 30 2024 23:32:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 05 Oct 2024 22:52:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index bfb6c55..b479dbf 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240930" +__version__="2.0-dev20241005" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240930" +version_main="2.0_dev20241005" version_buildnumber=1 \ No newline at end of file -- Gitee From c3e3b190290e2569b1ac4bde24add9300bde9fa0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 14 Oct 2024 08:58:59 +0800 Subject: [PATCH 066/122] Ignore hidden files in `listdir` processing It interferes with data processing operations when processing info --- src/clitheme/_globalvar.py | 7 +++++++ src/clitheme/cli.py | 4 ++-- src/clitheme/exec/__init__.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index a8e66ed..d3f71e9 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -115,6 +115,13 @@ def extract_content(line_content: str, begin_phrase_count: int=1) -> str: results=re.search(r"(?:\s*.+?\s+){"+str(begin_phrase_count)+r"}(?P.+)", line_content.strip()) if results==None: raise ValueError("Match content failed (no matches)") else: return results.groupdict()['content'] +def list_directory(dirname: str): + lsdir_result=os.listdir(dirname) + final_result=[] + for name in lsdir_result: + if not name.startswith('.'): + final_result.append(name) + return final_result def make_printable(content: str) -> str: final_str="" for character in content: diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index d77839a..b0d608d 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -181,7 +181,7 @@ def get_current_theme_info(): if not os.path.isdir(search_path): print(f.reof("no-theme", "No theme currently set")) return 1 - lsdir_result=os.listdir(search_path) + lsdir_result=_globalvar.list_directory(search_path) lsdir_result.sort(reverse=True, key=functools.cmp_to_key(_globalvar.result_sort_cmp)) # sort by latest installed lsdir_num=0 for x in lsdir_result: @@ -256,7 +256,7 @@ def update_theme(): if not os.path.isdir(search_path): print(fi.reof("no-theme-err", "Error: no theme currently set")) return 1 - lsdir_result=os.listdir(search_path); lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) + lsdir_result=_globalvar.list_directory(search_path); lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 7ca3cf5..87f1eef 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -47,7 +47,7 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) # gather files search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname if not os.path.isdir(search_path): raise Exception(search_path+" not directory") - lsdir_result=os.listdir(search_path); lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) + lsdir_result=_globalvar.list_directory(search_path); lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 -- Gitee From 78fa321203346af636f1cce23858a5cb2eaeb084 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 24 Oct 2024 11:22:42 +0800 Subject: [PATCH 067/122] Don't retry clitheme-man if exit code is SIGINT Happens when ^C is pressed in the viewer --- src/clitheme/man.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 1c6b41c..dc1e8c5 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -64,7 +64,7 @@ def main(args: List[str]): except KeyboardInterrupt: process.send_signal(signal.SIGINT) return process.poll() # type: ignore returncode=run_process(env) - if returncode!=0 and theme_set: + if returncode!=0 and returncode != -signal.SIGINT and theme_set: _labeled_print(fd.reof("prev-command-fail", "Executing \"man\" with custom path failed, trying execution with normal settings")) env["MANPATH"]=prev_manpath if prev_manpath!=None else '' returncode=run_process(os.environ) -- Gitee From 6ca878794508b4ce80cdf0e929fbc26ad2b14070 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 24 Oct 2024 18:24:51 +0800 Subject: [PATCH 068/122] Small fix for substvar warning handling --- src/clitheme/_generator/_dataclass.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 17cce0b..b2725e8 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -141,7 +141,9 @@ class GeneratorObject(_handlers.DataHandlers): self.really_really_global_options=self.parse_options(options_data, merge_global_options=2) self.global_options=self.parse_options(options_data, merge_global_options=1) # if manually disabled, show substvar warning again next time - if self.global_options.get("substvar")!=True: self.substvar_warning=True + if self.global_options.get("substvar")!=True \ + and "substvar" in self.parse_options(options_data, merge_global_options=False): + self.substvar_warning=True def handle_setup_global_options(self): # reset global_options to contents of really_really_global_options self.global_options=copy.copy(self.really_really_global_options) @@ -155,7 +157,7 @@ class GeneratorObject(_handlers.DataHandlers): if self.substvar_warning: for match in re.finditer(pattern, content): if self.global_variables.get(match.group(1))!=None: - self.handle_warning(self.fd.feof("set-substvar-warn", "Line {num}: Attempted to reference a defined variable, but \"substvar\" option is not enabled", num=line_number_debug)) + self.handle_warning(self.fd.feof("set-substvar-warn", "Line {num}: Attempted to reference a defined variable, but \"substvar\" option is not enabled", num=line_number_debug if line_number_debug!=None else str(self.lineindex+1))) self.substvar_warning=False break return content -- Gitee From e4402015e85ab9b51d6df04d1d36c21efaea838e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 25 Oct 2024 07:25:14 +0800 Subject: [PATCH 069/122] Require `name` info in header section --- .github/README.md | 2 +- README.md | 2 +- src/clitheme/_generator/_header_parser.py | 6 +++++- src/clitheme/strings/generator-strings.clithemedef.txt | 4 ++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/README.md b/.github/README.md index 4eec1a7..efe1b13 100644 --- a/.github/README.md +++ b/.github/README.md @@ -82,7 +82,7 @@ Write theme definition file and substitution rules based on the output: ```plaintext # Define basic information for this theme in header_section; required {header_section} - # It is recommended to include name and description at the minimum + # `name` is a required entry in header_section name clang example theme [description] An example theme for clang (for demonstration purposes) diff --git a/README.md b/README.md index d0fe3f5..b835383 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ e> {{ESC}}[0m2 errors generated.\r\n ```plaintext # 在header_section中定义一些关于该主题定义的基本信息;必须包括 {header_section} - # 这里建议至少包括name和description信息 + # 在header_section中必须定义`name`条目 name clang样例主题 [description] 一个为clang打造的的样例主题,为了演示作用 diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index 9b1af4c..d47c1be 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -17,6 +17,7 @@ from . import _dataclass def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.handle_begin_section("header") end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" + specified_info=[] while obj.goto_next_line(): phrases=obj.lines_data[obj.lineindex].split() if phrases[0] in ("name", "version", "description"): @@ -54,6 +55,9 @@ def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): elif obj.handle_setters(): pass elif phrases[0]==end_phrase: obj.check_extra_args(phrases, 1, use_exact_count=True) + if not "name" in specified_info: + obj.handle_error(obj.fd.feof("missing-info-err", "{sect_name} section missing required entries: {entries}", sect_name="header", entries="name")) obj.handle_end_section("header") break - else: obj.handle_invalid_phrase(phrases[0]) \ No newline at end of file + else: obj.handle_invalid_phrase(phrases[0]) + specified_info.append(phrases[0]) \ No newline at end of file diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 36c624c..07e6b50 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -120,6 +120,10 @@ in_domainapp swiftycode clitheme # locale:default Missing "as " phrase on next line of line {num} locale:zh_CN 第{num}行:在下一行缺少"as <文件名>"语句 [/entry] + [entry] missing-info-err + # locale:default {sect_name} section missing required entries: {entries} + locale:zh_CN {sect_name}段落缺少必要条目:{entries} + [/entry] # 警告提示 # Warning messages [entry] warning-str -- Gitee From 61e539844aa01f6cf0c5ac8529496e572fbf6ca5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 25 Oct 2024 10:38:14 +0800 Subject: [PATCH 070/122] Don't import too many modules during init Speed things up and avoid potential problems --- src/clitheme/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/clitheme/__init__.py b/src/clitheme/__init__.py index 40fb812..1573821 100644 --- a/src/clitheme/__init__.py +++ b/src/clitheme/__init__.py @@ -1,10 +1,13 @@ +""" +Command line customization toolkit +""" # Copyright © 2023-2024 swiftycode # This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along with this program. If not, see . -__all__=["frontend", "cli", "man", "exec"] +__all__=["frontend"] # Prevent RuntimeWarning from displaying when running a submodule (e.g. "python3 -m clitheme.exec") import warnings @@ -29,6 +32,6 @@ if os.name=="nt": del os # Expose these modules when "clitheme" is imported -from . import _globalvar, frontend, cli, man, exec +from . import _globalvar, frontend _globalvar.handle_set_themedef(frontend, "global") # type: ignore del _globalvar # Don't expose this module \ No newline at end of file -- Gitee From 40b5877514fd861ef2c595e843a69f00f02b53ab Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 26 Oct 2024 08:53:48 +0800 Subject: [PATCH 071/122] Allow specifying file_contents as None in `apply_theme` function --- src/clitheme/cli.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index b0d608d..405e27d 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -20,7 +20,7 @@ import stat import functools from . import _globalvar, _generator, frontend from ._globalvar import make_printable as fmt # A shorter alias of the function -from typing import List +from typing import List, Optional # spell-checker:ignore pathnames lsdir inpstr @@ -29,16 +29,23 @@ frontend.set_appname("clitheme") frontend.set_subsections("cli") last_data_path="" -def apply_theme(file_contents: List[str], filenames: List[str], overlay: bool=False, preserve_temp=False, generate_only=False): +def apply_theme(file_contents: Optional[List[str]], filenames: List[str], overlay: bool=False, preserve_temp=False, generate_only=False): """ Apply the theme using the provided definition file contents and file pathnames in a list object. (Invokes 'clitheme apply-theme') + - Set file_contents=None to read file contents from specified filenames - Set overlay=True to overlay the theme on top of existing theme[s] - Set preserve_temp=True to preserve the temp directory (debugging purposes) - Set generate_only=True to generate the data hierarchy only (invokes 'clitheme generate-data' instead) """ + if file_contents==None: + try: file_contents=_get_file_contents(filenames) + except _direct_exit as exc: return exc.code + except: + _globalvar.handle_exception() + return 1 if len(filenames)>0 and len(file_contents)!=len(filenames): # unlikely to happen raise ValueError("file_contents and filenames have different lengths") f=frontend.FetchDescriptor(subsections="cli apply-theme") @@ -248,7 +255,6 @@ def update_theme(): (Invokes 'clitheme update-theme') """ class invalid_theme(Exception): pass - file_contents: List[str] file_paths: List[str] fi=frontend.FetchDescriptor(subsections="cli update-theme") try: @@ -273,12 +279,6 @@ def update_theme(): except: raise invalid_theme("Read error: "+str(sys.exc_info()[1])) file_paths.append(got_path) if len(file_paths)==0: raise invalid_theme("file_paths empty") - # Get file contents - try: file_contents=_get_file_contents(file_paths) - except _direct_exit as exc: return exc.code - except: - _globalvar.handle_exception() - return 1 except invalid_theme: print(fi.reof("not-available-err", "update-theme cannot be used with the current theme setting\nPlease re-apply the current theme and try again")) _globalvar.handle_exception() @@ -287,7 +287,7 @@ def update_theme(): print(fi.feof("other-err", "An error occurred while processing file path information: {msg}\nPlease re-apply the current theme and try again", msg=fmt(str(sys.exc_info()[1])))) _globalvar.handle_exception() return 1 - return apply_theme(file_contents, file_paths, overlay=False) + return apply_theme(None, file_paths, overlay=False) def _is_option(arg): return arg.strip()[0:1]=="-" @@ -384,14 +384,7 @@ def main(cli_args: List[str]): else: return _handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=fmt(arg)), arg_first) else: paths.append(arg) - fi=frontend.FetchDescriptor(subsections="cli apply-theme") - content_list: List[str] - try: content_list=_get_file_contents(paths) - except _direct_exit as exc: return exc.code - except: - _globalvar.handle_exception() - return 1 - return apply_theme(content_list, overlay=overlay, filenames=paths, preserve_temp=preserve_temp, generate_only=generate_only) + return apply_theme(file_contents=None, overlay=overlay, filenames=paths, preserve_temp=preserve_temp, generate_only=generate_only) elif cli_args[1]=="get-current-theme-info": check_extra_args(2) # disabled additional options return get_current_theme_info() -- Gitee From 4f0c0f438f9457b15487bcc53001ba9cecb3e619 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 26 Oct 2024 09:10:05 +0800 Subject: [PATCH 072/122] Fix info file writing in `set_local_themedef` Although it doesn't matter that much, but it's still nice to fix it. --- src/clitheme/frontend.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 5c2c75a..f0359bb 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -86,6 +86,7 @@ def _get_setting(key: str, caller: Optional[str]=None) -> Union[str,bool]: _alt_path=None _alt_path_dirname=None _alt_path_hash=None +_alt_info_index: int=1 # Support for setting a local definition file # - Generate the data in a temporary directory named after content hash # - First try alt_path then data_path @@ -131,7 +132,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: else: local_path_hash=d # else, use generated hash dir_name=f"clitheme-data-{local_path_hash}" _generator.generate_custom_path() # prepare _generator.path - global _alt_path_dirname + global _alt_path_dirname, _alt_info_index global global_debugmode path_name=_globalvar.clitheme_temp_root+"/"+dir_name if _alt_path_dirname!=None and overlay==True: # overlay @@ -145,7 +146,8 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: try: # Set this to prevent extra messages from being displayed global_debugmode=False - return_val=_generator.generate_data_hierarchy(file_content, custom_path_gen=False) + return_val=_generator.generate_data_hierarchy(file_content, custom_path_gen=False, custom_infofile_name=str(_alt_info_index)) + _alt_info_index+=1 except SyntaxError: if _get_setting("debugmode"): print("[Debug] Generator error: "+str(sys.exc_info()[1])) return False @@ -188,6 +190,7 @@ def unset_local_themedef(): global _alt_path; _alt_path=None global _alt_path_dirname; _alt_path_dirname=None global _alt_path_hash; _alt_path_hash=None + global _alt_info_index; _alt_info_index=1 class FetchDescriptor(): """ -- Gitee From 72f1ececc30ab34760339c9d07577667bb0097ea Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 28 Oct 2024 14:36:49 +0800 Subject: [PATCH 073/122] Fix high idle CPU usage in `output_handler_posix` --- src/clitheme/exec/output_handler_posix.py | 64 ++++++++++++++--------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index f4445ce..735faa2 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -24,12 +24,13 @@ import re import sqlite3 import time import threading +import queue from typing import Optional, List from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print -# spell-checker:ignore cbreak ICANON readsize splitarray ttyname RDWR preexec pgrp +# spell-checker:ignore cbreak ICANON readsize splitarray ttyname RDWR preexec pgrp pids fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") # https://docs.python.org/3/library/stdtypes.html#str.splitlines @@ -133,7 +134,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) signal.signal(signal.SIGTSTP, signal_handler) signal.signal(signal.SIGCONT, signal_handler) signal.signal(signal.SIGINT, signal_handler) - output_lines=[] # (line_content, is_stderr, do_subst_operation) + output_lines=queue.Queue() # (line_content, is_stderr, do_subst_operation) def get_terminal_size(): return fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder # this mechanism prevents user input from being processed through substrules @@ -172,8 +173,11 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) if thread_debug==1: raise Exception elif thread_debug==2: break - try: fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.002)[0] - except OSError: fds=select.select([stdout_fd, stderr_fd], [], [], 0.002)[0] + # Set a short timeout value if there are unfinished outputs + # Else, don't timeout and wait for data + timeout=0.002 if unfinished_output!=None else None + try: fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], timeout)[0] + except OSError: fds=select.select([stdout_fd, stderr_fd], [], [], timeout)[0] # Handle user input from stdin if sys.stdin in fds: data=os.read(sys.stdin.fileno(), readsize) @@ -217,7 +221,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) line=orig_line+line else: # Shouldn't join them together in this case - output_lines.append(unfinished_output) + output_lines.put(unfinished_output) # Don't push the current line just yet; leave it for newline check unfinished_output=None output_handled=True @@ -239,25 +243,42 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) try: if line[x+1]==ord(b'\n'): continue except IndexError: pass - output_lines.append((line[last_index:x+1], is_stderr, do_subst_operation, foreground_pid)) + output_lines.put((line[last_index:x+1], is_stderr, do_subst_operation, foreground_pid)) last_index=x+1 if stdout_fd in fds: handle_output(is_stderr=False) if stderr_fd in fds: handle_output(is_stderr=True) # if no unfinished_output is handled by handle_output, append the unfinished output if exists if not output_handled and unfinished_output!=None: - output_lines.append(unfinished_output) + output_lines.put(unfinished_output) unfinished_output=None - if process.poll()!=None: break + if process.poll()!=None: + # Send termination signal + output_lines.put(None) + break except: handle_exception() thread=threading.Thread(target=output_read_loop, name="output-reader", daemon=True) thread.start() + + def update_window_size(*args): + # update terminal size + nonlocal last_terminal_size + try: + new_term_size=get_terminal_size() + if new_term_size!=last_terminal_size: + last_terminal_size=new_term_size + fcntl.ioctl(stdout_fd, termios.TIOCSWINSZ, new_term_size) + fcntl.ioctl(stderr_fd, termios.TIOCSWINSZ, new_term_size) + process.send_signal(signal.SIGWINCH) + except: pass + signal.signal(signal.SIGWINCH, update_window_size) + # Call this function for the first time to set initial window size + update_window_size() while True: try: - if process.poll()!=None and len(output_lines)==0: break - if not thread.is_alive(): + if not thread.is_alive() and not process.poll()!=None: if not thread_exception_handled: handle_exception(RuntimeError("Output read loop terminated unexpectedly")) else: return 1 if thread_exception_handled: continue # Prevent conflict with setting terminal attributes @@ -268,15 +289,6 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) attrs[3] &= ~(termios.ICANON | termios.ECHO) termios.tcsetattr(sys.stdout, termios.TCSADRAIN, attrs) except termios.error: pass - # update terminal size - try: - new_term_size=get_terminal_size() - if new_term_size!=last_terminal_size: - last_terminal_size=new_term_size - fcntl.ioctl(stdout_fd, termios.TIOCSWINSZ, new_term_size) - fcntl.ioctl(stderr_fd, termios.TIOCSWINSZ, new_term_size) - process.send_signal(signal.SIGWINCH) - except: pass # Process outputs def process_line(line: bytes, line_data): @@ -303,14 +315,14 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) signal.setitimer(signal.ITIMER_REAL, 0) if line_data[2]==True: subst_line=_process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line, failed=failed)[0] return subst_line - time.sleep(0.001) # Prevent high CPU usage - if len(output_lines)==0: + if output_lines.empty(): handle_debug_pgrp(os.tcgetpgrp(stdout_fd)) - while not len(output_lines)==0: - line_data=output_lines.pop(0) - line: bytes=line_data[0] - # subst operation and print output - os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) + line_data=output_lines.get(block=True) + # None: termination signal + if line_data==None: break + line: bytes=line_data[0] + # subst operation and print output + os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) except: if not thread_exception_handled: handle_exception() else: raise -- Gitee From 533e89ec87670cde984f16c62d9eaea58b905732 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 1 Nov 2024 21:25:49 +0800 Subject: [PATCH 074/122] Rename data and parser handler code files Use a more straightforward and precise file name instead of the ambiguous `dataclass` and `handlers` --- src/clitheme/_generator/__init__.py | 4 ++-- .../_generator/{_handlers.py => _data_handlers.py} | 2 +- src/clitheme/_generator/_entries_parser.py | 4 ++-- src/clitheme/_generator/_entry_block_handler.py | 4 ++-- src/clitheme/_generator/_header_parser.py | 4 ++-- src/clitheme/_generator/_manpage_parser.py | 4 ++-- .../_generator/{_dataclass.py => _parser_handlers.py} | 8 ++++---- src/clitheme/_generator/_substrules_parser.py | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) rename src/clitheme/_generator/{_handlers.py => _data_handlers.py} (98%) rename src/clitheme/_generator/{_dataclass.py => _parser_handlers.py} (98%) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 82e8819..71a8730 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -12,7 +12,7 @@ import string import random from typing import Optional from .. import _globalvar -from . import _dataclass +from . import _parser_handlers from . import _header_parser, _entries_parser, _substrules_parser, _manpage_parser # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent @@ -35,7 +35,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if custom_path_gen: generate_custom_path() global path - obj=_dataclass.GeneratorObject(file_content=file_content, custom_infofile_name=custom_infofile_name, filename=filename, path=path, silence_warn=silence_warn) + obj=_parser_handlers.GeneratorObject(file_content=file_content, custom_infofile_name=custom_infofile_name, filename=filename, path=path, silence_warn=silence_warn) before_content_lines=True while obj.goto_next_line(): diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_data_handlers.py similarity index 98% rename from src/clitheme/_generator/_handlers.py rename to src/clitheme/_generator/_data_handlers.py index 605504e..7df9730 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_data_handlers.py @@ -5,7 +5,7 @@ # You should have received a copy of the GNU General Public License along with this program. If not, see . """ -Functions for data processing and others (internal module) +Functions for data processing and error handling (internal module) """ import os import gzip diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index 3dcbd4e..393b893 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -9,11 +9,11 @@ entries_section parser function (internal module) """ from typing import Optional from .. import _globalvar -from . import _dataclass +from . import _parser_handlers # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent -def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): +def handle_entries_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): obj.handle_begin_section("entries") end_phrase="end_main" if first_phrase=="begin_main" else r"{/entries_section}" if first_phrase=="begin_main": diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index 315c620..68f97af 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -14,8 +14,8 @@ from .. import _globalvar def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_substrules: bool=False, substrules_options: Dict[str, Any]={}): # Workaround to circular import issue - from . import _dataclass - self: _dataclass.GeneratorObject=obj + from . import _parser_handlers + self: _parser_handlers.GeneratorObject=obj # substrules_options: {effective_commands: list, is_regex: bool, strictness: int} entry_name_substesc=False; entry_name_substvar=False diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index d47c1be..9414867 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -10,11 +10,11 @@ header_section parser function (internal module) import re from typing import Optional from .. import _globalvar -from . import _dataclass +from . import _parser_handlers # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent -def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): +def handle_header_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): obj.handle_begin_section("header") end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" specified_info=[] diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index a39293b..ab1aa27 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -11,11 +11,11 @@ import os import sys from typing import Optional, List from .. import _globalvar -from . import _dataclass +from . import _parser_handlers # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent -def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): +def handle_manpage_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): obj.handle_begin_section("manpage") end_phrase="{/manpage_section}" while obj.goto_next_line(): diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_parser_handlers.py similarity index 98% rename from src/clitheme/_generator/_dataclass.py rename to src/clitheme/_generator/_parser_handlers.py index b2725e8..a71f958 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -5,7 +5,7 @@ # You should have received a copy of the GNU General Public License along with this program. If not, see . """ -Class object for sharing data between section parsers (internal module) +Functions used by various parsers (internal module) """ import sys @@ -15,10 +15,10 @@ import copy import uuid from typing import Optional, Union, List, Dict from .. import _globalvar, _version -from . import _handlers, _entry_block_handler +from . import _data_handlers, _entry_block_handler # spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption -class GeneratorObject(_handlers.DataHandlers): +class GeneratorObject(_data_handlers.DataHandlers): ## Defined option groups lead_indent_options=["leadtabindents", "leadspaces"] @@ -57,7 +57,7 @@ class GeneratorObject(_handlers.DataHandlers): self.custom_infofile_name=custom_infofile_name self.filename=filename self.file_content=file_content - _handlers.DataHandlers.__init__(self, path, silence_warn) + _data_handlers.DataHandlers.__init__(self, path, silence_warn) from . import db_interface self.db_interface=db_interface def is_ignore_line(self) -> bool: diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 163a794..6d005ad 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -11,11 +11,11 @@ import os import copy from typing import Optional from .. import _globalvar -from . import _dataclass +from . import _parser_handlers # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent -def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str): +def handle_substrules_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): obj.handle_begin_section("substrules") end_phrase=r"{/substrules_section}" command_filters: Optional[list]=None -- Gitee From 0115987fd1afe0c6a83e67aeb1d209be9e2578c5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 1 Nov 2024 21:19:25 +0800 Subject: [PATCH 075/122] Update version (v2.0-dev20241101) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index efca91f..153581e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241005 +pkgver=2.0_dev20241101 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 84b1101..d5522b5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241005-1) unstable; urgency=low +clitheme (2.0-dev20241101-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 05 Oct 2024 22:52:00 +0800 + -- swiftycode <3291929745@qq.com> Fri, 01 Nov 2024 21:12:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index b479dbf..d27ec6e 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241005" +__version__="2.0-dev20241101" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241005" +version_main="2.0_dev20241101" version_buildnumber=1 \ No newline at end of file -- Gitee From 3882b8bbf615d58f3aa45c93f8756bee89890e29 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 2 Nov 2024 23:29:06 +0800 Subject: [PATCH 076/122] Use 0.5s timeout on idle for proper error handling --- src/clitheme/exec/output_handler_posix.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 735faa2..b2ba415 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -174,8 +174,8 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) elif thread_debug==2: break # Set a short timeout value if there are unfinished outputs - # Else, don't timeout and wait for data - timeout=0.002 if unfinished_output!=None else None + # Else, wait longer to reduce CPU usage + timeout=0.002 if unfinished_output!=None else 0.5 try: fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], timeout)[0] except OSError: fds=select.select([stdout_fd, stderr_fd], [], [], timeout)[0] # Handle user input from stdin @@ -317,7 +317,8 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) return subst_line if output_lines.empty(): handle_debug_pgrp(os.tcgetpgrp(stdout_fd)) - line_data=output_lines.get(block=True) + try: line_data=output_lines.get(block=True, timeout=0.5) + except queue.Empty: continue # None: termination signal if line_data==None: break line: bytes=line_data[0] -- Gitee From 2af48599f31650aa201a4c1373ea0d3d64374a51 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 4 Nov 2024 13:31:26 +0800 Subject: [PATCH 077/122] Fix last input content processing --- src/clitheme/exec/output_handler_posix.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index b2ba415..618508d 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -175,7 +175,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) # Set a short timeout value if there are unfinished outputs # Else, wait longer to reduce CPU usage - timeout=0.002 if unfinished_output!=None else 0.5 + timeout=0.002 if unfinished_output!=None or last_input_content!=None else 0.5 try: fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], timeout)[0] except OSError: fds=select.select([stdout_fd, stderr_fd], [], [], timeout)[0] # Handle user input from stdin @@ -252,6 +252,9 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) if not output_handled and unfinished_output!=None: output_lines.put(unfinished_output) unfinished_output=None + # Reset last input content if no output is made within timeout + if not sys.stdin in fds and last_input_content!=None: + last_input_content=None if process.poll()!=None: # Send termination signal -- Gitee From 029a54e20237296ac0b08f4169264acf195ff15b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 7 Nov 2024 16:46:30 +0800 Subject: [PATCH 078/122] Cache database fetches and match results Don't repeatedly do database queries and only do it when database file mtime or exist state changes --- src/clitheme/_generator/db_interface.py | 110 ++++++++++++++++-------- 1 file changed, 72 insertions(+), 38 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 5a14338..5ed4c44 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -14,7 +14,8 @@ import sqlite3 import re import copy import uuid -from typing import Optional, List, Tuple +import gc +from typing import Optional, List, Tuple, Dict from .. import _globalvar, frontend # spell-checker:ignore matchoption cmdlist exactmatch rowid pids tcpgrp @@ -99,44 +100,38 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern, substitute_pattern, cmd, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption, str(unique_id), foreground_only)) connection.commit() -def _check_strictness(match_cmd: str, strictness: int, target_command: str): - def process_smartcmdmatch_phrases(match_cmd: str) -> List[str]: - match_cmd_phrases=[] - for p in range(len(match_cmd.split())): - ph=match_cmd.split()[p] - results=re.search(r"^-([^-]+)$",ph) - if p>0 and results!=None: - for character in results.groups()[0]: match_cmd_phrases.append("-"+character) - else: match_cmd_phrases.append(ph) - return match_cmd_phrases - success=True - if strictness==1: # must start with pattern in terms of space-separated phrases - condition=len(match_cmd.split())<=len(target_command.split()) and target_command.split()[:len(match_cmd.split())]==match_cmd.split() - if not condition==True: success=False - elif strictness==2: # must equal to pattern - if not re.sub(r" {2,}", " ", target_command).strip()==match_cmd: success=False - elif strictness==-1: # smartcmdmatch: split phrases starting with one '-' and split them. Then, perform strictness==0 operation - # process both phrases - match_cmd_phrases=process_smartcmdmatch_phrases(match_cmd) - command_phrases=process_smartcmdmatch_phrases(target_command) - for phrase in match_cmd_phrases: - if phrase not in command_phrases: success=False - else: # implying strictness==0; must contain all phrases in pattern - for phrase in match_cmd.split(): - if phrase not in target_command.split(): success=False - return success +## Database fetch caching -def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False, pids: Tuple[int,int]=(-1,-1)) -> bytes: - # pids: (main_pid, current_tcpgrp) +_db_last_state: Optional[float]=None +_matches_cache: Dict[Optional[str],List[tuple]]={} - # Match order: - # 1. Match rules with exactcmdmatch option set - # 2. Match rules with command filter having the same first phrase - # - Command filters with greater number of phrases are prioritized over others - # 3. Match rules without command filter +def _is_db_updated() -> bool: + global _db_last_state + cur_state: Optional[float] + try: + # Check modification time + cur_state=os.stat(db_path).st_mtime + except FileNotFoundError: cur_state=None + updated=False + if cur_state!=_db_last_state: + updated=True + _db_last_state=cur_state + return updated - # retrieve a list of effective commands matching first argument - if not os.path.exists(db_path): raise sqlite3.OperationalError("file at db_path does not exist") +def _fetch_matches(command: Optional[str]) -> List[tuple]: + global _matches_cache + updated=_is_db_updated() + if updated or _matches_cache.get(command)==None: + if updated: + _matches_cache.clear() + gc.collect() # Reduce memory leak + _matches_cache[command]=_get_matches(command) + if _db_last_state==None: raise sqlite3.OperationalError("file at db_path does not exist") + return _matches_cache[command] + +## Output processing and matching + +def _get_matches(command: Optional[str]) -> List[tuple]: _connection=sqlite3.connect(db_path) final_cmdlist=[] final_cmdlist_exactmatch=[] @@ -163,8 +158,6 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F if match_cmd not in final_cmdlist: final_cmdlist.append(match_cmd) final_cmdlist_exactmatch.append(strictness==2) - - content_str=copy.copy(content) matches=[] def fetch_matches_by_locale(filter_condition: str, filter_data: tuple=tuple()): fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only", "unique_id", "foreground_only", "effective_command", "command_match_strictness"] @@ -186,6 +179,47 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # also append matches with other strictness fetch_matches_by_locale("effective_command=? AND command_match_strictness!=2", (cmd,)) fetch_matches_by_locale("typeof(effective_command)=typeof(null)") + return matches + +def _check_strictness(match_cmd: str, strictness: int, target_command: str): + def process_smartcmdmatch_phrases(match_cmd: str) -> List[str]: + match_cmd_phrases=[] + for p in range(len(match_cmd.split())): + ph=match_cmd.split()[p] + results=re.search(r"^-([^-]+)$",ph) + if p>0 and results!=None: + for character in results.groups()[0]: match_cmd_phrases.append("-"+character) + else: match_cmd_phrases.append(ph) + return match_cmd_phrases + success=True + if strictness==1: # must start with pattern in terms of space-separated phrases + condition=len(match_cmd.split())<=len(target_command.split()) and target_command.split()[:len(match_cmd.split())]==match_cmd.split() + if not condition==True: success=False + elif strictness==2: # must equal to pattern + if not re.sub(r" {2,}", " ", target_command).strip()==match_cmd: success=False + elif strictness==-1: # smartcmdmatch: split phrases starting with one '-' and split them. Then, perform strictness==0 operation + # process both phrases + match_cmd_phrases=process_smartcmdmatch_phrases(match_cmd) + command_phrases=process_smartcmdmatch_phrases(target_command) + for phrase in match_cmd_phrases: + if phrase not in command_phrases: success=False + else: # implying strictness==0; must contain all phrases in pattern + for phrase in match_cmd.split(): + if phrase not in target_command.split(): success=False + return success + +def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False, pids: Tuple[int,int]=(-1,-1)) -> bytes: + # pids: (main_pid, current_tcpgrp) + + # Match order: + # 1. Match rules with exactcmdmatch option set + # 2. Match rules with command filter having the same first phrase + # - Command filters with greater number of phrases are prioritized over others + # 3. Match rules without command filter + + # retrieve a list of effective commands matching first argument + matches=_fetch_matches(command) + content_str=copy.copy(content) content_str=_handle_subst(matches, content_str, is_stderr, pids, command) return content_str -- Gitee From 099d344d2f877ac6c7b9b7a7c397b00a42c9ede6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 7 Nov 2024 20:15:59 +0800 Subject: [PATCH 079/122] Fix substesc and substvar handling order in `handle_block_input` --- src/clitheme/_generator/_parser_handlers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index a71f958..3db1c23 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -217,7 +217,7 @@ class GeneratorObject(_data_handlers.DataHandlers): def handle_singleline_content(self, content: str) -> str: target_content=copy.copy(content) target_content=self.subst_variable_content(target_content) - if "substesc" in self.global_options.keys() and self.global_options['substesc']==True: + if self.global_options.get("substesc")==True: target_content=self.handle_substesc(target_content) return target_content def handle_setters(self, really_really_global: bool=False) -> bool: @@ -287,12 +287,15 @@ class GeneratorObject(_data_handlers.DataHandlers): check_whether_explicitly_specified(pass_condition=preserve_indents) # insert spaces at start of each line if preserve_indents: blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) - elif option=="substesc": + elif option=="substesc" and not "substvar" in got_options: + # Only handle substesc independently if substvar is not specified check_whether_explicitly_specified(pass_condition=not disable_substesc) # substitute {{ESC}} with escape literal if got_options['substesc']==True and not disable_substesc: blockinput_data=self.handle_substesc(blockinput_data) elif option=="substvar": if got_options['substvar']==True: blockinput_data=self.subst_variable_content(blockinput_data, override_check=True, line_number_debug=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1)) + # If substvar, substesc must be handled after that, or "{{ESC}}" in variable content will be ignored + if got_options.get('substesc')==True and not disable_substesc: blockinput_data=self.handle_substesc(blockinput_data) elif disallow_cmdmatch_options: if is_specified_in_block(): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option))) if "substvar" not in specified_options: -- Gitee From f4ab26bc3b7ee116924d673f4b1912d9369a6c5e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 7 Nov 2024 23:02:01 +0800 Subject: [PATCH 080/122] Properly handle and relay SIGQUIT signals --- src/clitheme/exec/output_handler_posix.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 618508d..3ec7db3 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -98,6 +98,8 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) os.kill(main_pid, signal.SIGTSTP) # Suspend itself elif sig==signal.SIGINT: os.write(stdout_fd, b'\x03') # '^C' character + elif sig==signal.SIGQUIT: + os.write(stdout_fd, b'\x1c') # '^\' character try: # Detect if stdin is piped (e.g. cat file|clitheme-exec grep content) stdin_fd=stdout_slave @@ -131,9 +133,9 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) _globalvar.handle_exception() return 1 else: - signal.signal(signal.SIGTSTP, signal_handler) - signal.signal(signal.SIGCONT, signal_handler) - signal.signal(signal.SIGINT, signal_handler) + handle_signals=[signal.SIGTSTP, signal.SIGCONT, signal.SIGINT, signal.SIGQUIT] + for sig in handle_signals: + signal.signal(sig, signal_handler) output_lines=queue.Queue() # (line_content, is_stderr, do_subst_operation) def get_terminal_size(): return fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder @@ -334,6 +336,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) exit_code=process.poll() try: if exit_code!=None and exit_code<0: # Terminated by signal + signal.signal(signal.SIGQUIT, signal.SIG_DFL) # Unset SIGQUIT custom handler os.kill(os.getpid(), abs(exit_code)) except: pass return exit_code -- Gitee From 53ecc43707d5e887a83a659b1ab6cf6736541d52 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 10 Nov 2024 22:19:19 +0800 Subject: [PATCH 081/122] Update version (v2.0-dev20241110) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 153581e..482d654 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241101 +pkgver=2.0_dev20241110 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index d5522b5..bb77de1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241101-1) unstable; urgency=low +clitheme (2.0-dev20241110-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Fri, 01 Nov 2024 21:12:00 +0800 + -- swiftycode <3291929745@qq.com> Sun, 10 Nov 2024 22:16:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index d27ec6e..4a8dd24 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241101" +__version__="2.0-dev20241110" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241101" +version_main="2.0_dev20241110" version_buildnumber=1 \ No newline at end of file -- Gitee From 3bd43cb476012b195234e8d17fbdb14ff6fbae82 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 11 Nov 2024 13:28:51 +0800 Subject: [PATCH 082/122] Support variable substitution in `locale:` syntax --- .../_generator/_entry_block_handler.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index 68f97af..5b29ca5 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -69,15 +69,17 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su self.check_enough_args(phrases, 3) content=_globalvar.extract_content(line_content, begin_phrase_count=2) locale=phrases[1] + locales=self.subst_variable_content(locale).split() content=self.handle_singleline_content(content) - for each_name in entryNames: - if is_substrules: - entries.append((each_name[0], content, None if locale=="default" else locale, each_name[1], str(self.lineindex+1), each_name[2])) - else: - target_entry=copy.copy(each_name[0]) - if locale!="default": - target_entry+="__"+locale - entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) + for this_locale in locales: + for each_name in entryNames: + if is_substrules: + entries.append((each_name[0], content, None if this_locale=="default" else this_locale, each_name[1], str(self.lineindex+1), each_name[2])) + else: + target_entry=copy.copy(each_name[0]) + if this_locale!="default": + target_entry+="__"+this_locale + entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) elif phrases[0] in ("locale_block", "[locale]"): self.check_enough_args(phrases, 2) locales=self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() -- Gitee From 2efa5f2362ea6020ed9fe19e970f0636ad75423c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 11 Nov 2024 17:58:07 +0800 Subject: [PATCH 083/122] Cleanup some code in last input content processing --- src/clitheme/exec/output_handler_posix.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 3ec7db3..0f68df0 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -199,16 +199,11 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) # check if the output is user input. if yes, skip if last_input_content!=None: input_match_expression: bytes=re.escape(last_input_content).replace(b'\x7f', rb"(\x08 \x08|\x08\x1b\[K)") # type: ignore - input_startswith=b'^'+input_match_expression - input_equals=input_startswith+b'$' + input_equals=b'^'+input_match_expression+b'$' # print(last_input_content, data, re.search(input_equals, data)!=None) # DEBUG if re.search(input_equals, data)!=None: do_subst_operation=False - last_input_content=None - # elif re.search(input_startswith, data)!=None: - # do_subst_operation=False - # last_input_content=last_input_content[len(data):] - else: last_input_content=None + last_input_content=None lines=data.splitlines(keepends=True) unfinished_cr_lines=None for x in range(len(lines)): -- Gitee From 050bdd03baffa8d0ae851c577978d30f5bf27c10 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 11 Nov 2024 20:58:16 +0800 Subject: [PATCH 084/122] Add unfinished output continuous timeout If the outputs are all unfinished outputs for 100ms, stop processing that block --- src/clitheme/exec/output_handler_posix.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 0f68df0..7940468 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -167,7 +167,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) signal.signal(signal.SIGUSR2, thread_debug_handle) def output_read_loop(): nonlocal last_input_content, output_lines - unfinished_output=None + unfinished_output=None # (line,is_stderr,do_subst_operation,foreground_pid,initial_time) try: while True: # Testing thread exception handling @@ -206,16 +206,18 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) last_input_content=None lines=data.splitlines(keepends=True) unfinished_cr_lines=None + unfinished_output_time=time.perf_counter() for x in range(len(lines)): line=lines[x] # if unfinished output exists, append new content to it if x==0 and unfinished_output!=None: orig_data=unfinished_output orig_line=orig_data[0] - if unfinished_output[3]==foreground_pid and unfinished_output[1]==is_stderr: + if unfinished_output[3]==foreground_pid and unfinished_output[1]==is_stderr and time.perf_counter()-unfinished_output[4]<=0.1: # Modify existing line data instead of directly pushing it # to better handle multiple fragments in a single line line=orig_line+line + unfinished_output_time=unfinished_output[4] else: # Shouldn't join them together in this case output_lines.put(unfinished_output) @@ -231,7 +233,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) else: unfinished_cr_lines=None # if last line of output did not end with newlines, leave for next iteration if x==len(lines)-1 and not line.endswith(newlines): - unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid) + unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid, unfinished_output_time) output_handled=True else: last_index=0 -- Gitee From b7093dc0da1e348b82fa5ededd4bb9d40f2bf7c1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 12 Nov 2024 08:48:07 +0800 Subject: [PATCH 085/122] Overhaul output processing pipeline and mechanism --- src/clitheme/exec/output_handler_posix.py | 110 +++++++++++----------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 7940468..7324e4e 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -25,7 +25,7 @@ import sqlite3 import time import threading import queue -from typing import Optional, List +from typing import Optional, List, Union from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print @@ -136,7 +136,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) handle_signals=[signal.SIGTSTP, signal.SIGCONT, signal.SIGINT, signal.SIGQUIT] for sig in handle_signals: signal.signal(sig, signal_handler) - output_lines=queue.Queue() # (line_content, is_stderr, do_subst_operation) + output_lines=queue.Queue() # (line_content, is_stderr, do_subst_operation, foreground_pid, term_attrs) def get_terminal_size(): return fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder # this mechanism prevents user input from being processed through substrules @@ -167,7 +167,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) signal.signal(signal.SIGUSR2, thread_debug_handle) def output_read_loop(): nonlocal last_input_content, output_lines - unfinished_output=None # (line,is_stderr,do_subst_operation,foreground_pid,initial_time) + unfinished_output=None # (line,is_stderr,do_subst_operation,foreground_pid,term_attrs,initial_time) try: while True: # Testing thread exception handling @@ -189,67 +189,67 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) try: os.write(stdout_fd, data) except OSError: pass # Handle input/output error that might occur after program terminates # Handle output from stdout and stderr - output_handled=False + unfinished_output_handled=False + def process_block(data: tuple): + output_lines.put(data) + # lines=data[0].splitlines(keepends=True) + # for line in lines: + # output_lines.put((line,)+data[1:]) def handle_output(is_stderr: bool): - nonlocal unfinished_output, output_lines, output_handled, last_input_content + nonlocal unfinished_output, output_lines, unfinished_output_handled, last_input_content data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) + # If pipe closed and returns empty data, ignore + if data==b'': return + try: + term_attrs=termios.tcgetattr(stdout_fd) + # disable canonical and echo mode (enable cbreak) no matter what + term_attrs[3] &= ~(termios.ICANON | termios.ECHO) + except termios.error: term_attrs=None foreground_pid=os.tcgetpgrp(stdout_fd) + do_subst_operation=True # check if the output is user input. if yes, skip - if last_input_content!=None: + if last_input_content!=None and not (unfinished_output!=None and unfinished_output[2]==True): input_match_expression: bytes=re.escape(last_input_content).replace(b'\x7f', rb"(\x08 \x08|\x08\x1b\[K)") # type: ignore input_equals=b'^'+input_match_expression+b'$' # print(last_input_content, data, re.search(input_equals, data)!=None) # DEBUG if re.search(input_equals, data)!=None: do_subst_operation=False last_input_content=None - lines=data.splitlines(keepends=True) - unfinished_cr_lines=None unfinished_output_time=time.perf_counter() - for x in range(len(lines)): - line=lines[x] - # if unfinished output exists, append new content to it - if x==0 and unfinished_output!=None: - orig_data=unfinished_output - orig_line=orig_data[0] - if unfinished_output[3]==foreground_pid and unfinished_output[1]==is_stderr and time.perf_counter()-unfinished_output[4]<=0.1: + if unfinished_output!=None: + orig_data=unfinished_output[0] + if unfinished_output[3]==foreground_pid and unfinished_output[1]==is_stderr: + # If exceeds maximum time or differing terminal attributes, append first line of data into unfinished output and process it + if time.perf_counter()-unfinished_output[5]>0.05 or term_attrs!=unfinished_output[4]: + lines=data.splitlines(keepends=True) + process_block((unfinished_output[0]+lines[0],)+unfinished_output[1:]) + data=data[len(lines[0]):] # Remove first line from data + else: # Modify existing line data instead of directly pushing it # to better handle multiple fragments in a single line - line=orig_line+line - unfinished_output_time=unfinished_output[4] - else: - # Shouldn't join them together in this case - output_lines.put(unfinished_output) - # Don't push the current line just yet; leave it for newline check - unfinished_output=None - output_handled=True - if unfinished_cr_lines!=None: line=unfinished_cr_lines+line - # If line ends with carriage return ('\r') and is not end of content, process them together - # to minimize visible cursor blinks due to delay in unfinished output processing - if line.endswith(b'\r') and x!=len(lines)-1: - unfinished_cr_lines=line - continue - else: unfinished_cr_lines=None - # if last line of output did not end with newlines, leave for next iteration - if x==len(lines)-1 and not line.endswith(newlines): - unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid, unfinished_output_time) - output_handled=True + data=orig_data+data + unfinished_output_time=unfinished_output[5] else: - last_index=0 - for x in range(len(line)): - if line[x]==ord(b'\r') or x==len(line)-1: - try: - if line[x+1]==ord(b'\n'): continue - except IndexError: pass - output_lines.put((line[last_index:x+1], is_stderr, do_subst_operation, foreground_pid)) - last_index=x+1 + # Shouldn't join them together in this case + process_block(unfinished_output) + # Don't push the current line just yet; leave it for newline check + unfinished_output=None + unfinished_output_handled=True + # If all data was appended to previous unfinished output and pushed, don't do anything + if data==b'': return + # if last line of output did not end with newlines, leave for next iteration + if not data.endswith(newlines): + unfinished_output=(data,is_stderr,do_subst_operation, foreground_pid, term_attrs, unfinished_output_time) + unfinished_output_handled=True + else: process_block((data, is_stderr, do_subst_operation, foreground_pid, term_attrs)) if stdout_fd in fds: handle_output(is_stderr=False) if stderr_fd in fds: handle_output(is_stderr=True) # if no unfinished_output is handled by handle_output, append the unfinished output if exists - if not output_handled and unfinished_output!=None: - output_lines.put(unfinished_output) + if not unfinished_output_handled and unfinished_output!=None: + process_block(unfinished_output) unfinished_output=None # Reset last input content if no output is made within timeout if not sys.stdin in fds and last_input_content!=None: @@ -284,13 +284,6 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) if not thread_exception_handled: handle_exception(RuntimeError("Output read loop terminated unexpectedly")) else: return 1 if thread_exception_handled: continue # Prevent conflict with setting terminal attributes - # update terminal attributes from what the program sets - try: - attrs=termios.tcgetattr(stdout_fd) - # disable canonical and echo mode (enable cbreak) no matter what - attrs[3] &= ~(termios.ICANON | termios.ECHO) - termios.tcsetattr(sys.stdout, termios.TCSADRAIN, attrs) - except termios.error: pass # Process outputs def process_line(line: bytes, line_data): @@ -299,8 +292,6 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) subst_line=copy.copy(line) failed=False foreground_pid=line_data[3] - # Print message if foreground process changed - if line_data[2]==True: handle_debug_pgrp(foreground_pid) if do_subst and line_data[2]==True: def operation(): nonlocal subst_line, failed, foreground_pid @@ -323,9 +314,18 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) except queue.Empty: continue # None: termination signal if line_data==None: break - line: bytes=line_data[0] + # Process output line by line + output=b'' + for line in line_data[0].splitlines(keepends=True): + output+=process_line(line, line_data) + # Print message if foreground process changed and not user input + if line_data[2]==True: handle_debug_pgrp(line_data[3]) + # update terminal attributes from what the program sets + if line_data[4]!=None: + try: termios.tcsetattr(sys.stdout, termios.TCSADRAIN, line_data[4]) + except termios.error: pass # subst operation and print output - os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) + os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(),output) except: if not thread_exception_handled: handle_exception() else: raise -- Gitee From 76392744afc03b5435ca35f104f8d627b871e998 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 13 Nov 2024 21:58:44 +0800 Subject: [PATCH 086/122] Update version (v2.0-dev20241113) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 482d654..18b5401 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241110 +pkgver=2.0_dev20241113 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index bb77de1..0b7260a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241110-1) unstable; urgency=low +clitheme (2.0-dev20241113-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Sun, 10 Nov 2024 22:16:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 13 Nov 2024 21:57:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 4a8dd24..a1b5507 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241110" +__version__="2.0-dev20241113" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241110" +version_main="2.0_dev20241113" version_buildnumber=1 \ No newline at end of file -- Gitee From 15c2a741296b11148f5a46f4d6c5008013786d55 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 16 Nov 2024 14:18:31 +0800 Subject: [PATCH 087/122] Move `_direct_exit` to `_globalvar` --- src/clitheme/_globalvar.py | 6 ++++++ src/clitheme/cli.py | 8 +------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index d3f71e9..592a107 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -106,6 +106,12 @@ def sanity_check(path: str, use_orig: bool=False) -> bool: ## Convenience functions +class _direct_exit(Exception): + def __init__(self, code): + """ + Custom exception for handling return code inside another function callback + """ + self.code=code def splitarray_to_string(split_content: List[str]) -> str: final="" for phrase in split_content: diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 405e27d..6bbb263 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -20,6 +20,7 @@ import stat import functools from . import _globalvar, _generator, frontend from ._globalvar import make_printable as fmt # A shorter alias of the function +from ._globalvar import _direct_exit from typing import List, Optional # spell-checker:ignore pathnames lsdir inpstr @@ -341,13 +342,6 @@ def _get_file_contents(file_paths: List[str]) -> List[str]: print(line_prefix, end='') return content_list -class _direct_exit(Exception): - def __init__(self, code): - """ - Custom exception for handling return code inside another function callback - """ - self.code=code - def main(cli_args: List[str]): """ Use this function invoke 'clitheme' with command line arguments -- Gitee From 7aa61a37cda2522ac70961b2ef719b45ce183faa Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 16 Nov 2024 15:16:21 +0800 Subject: [PATCH 088/122] Properly handle SIGINT after command exit --- src/clitheme/exec/output_handler_posix.py | 25 ++++++++++++++----- .../strings/exec-strings.clithemedef.txt | 4 +++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 7324e4e..8c45879 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -25,9 +25,10 @@ import sqlite3 import time import threading import queue -from typing import Optional, List, Union +from typing import Optional, List from .._generator import db_interface from .. import _globalvar, frontend +from .._globalvar import _direct_exit from . import _labeled_print # spell-checker:ignore cbreak ICANON readsize splitarray ttyname RDWR preexec pgrp pids @@ -91,15 +92,24 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) signal.signal(signal.SIGTSTP, signal_handler) # Reset signal handler elif sig==signal.SIGTSTP: # suspend signal if os.tcgetpgrp(stdout_fd)!=process.pid: # e.g. A shell running another process - os.write(stdout_fd, b'\x1a') # Send '^Z' character; don't suspend the entire shell + if process.poll()==None: # Process is running + os.write(stdout_fd, b'\x1a') # Send '^Z' character; don't suspend the entire shell else: process.send_signal(signal.SIGSTOP) # Stop the process signal.signal(signal.SIGTSTP, signal.SIG_DFL) # Unset signal handler to prevent deadlock os.kill(main_pid, signal.SIGTSTP) # Suspend itself elif sig==signal.SIGINT: - os.write(stdout_fd, b'\x03') # '^C' character + if process.poll()==None: + os.write(stdout_fd, b'\x03') # '^C' character + else: + reset_terminal() + _labeled_print(fd.reof("output-interrupted-exit", "Output interrupted after command exit")) + # Prevent message being triggered multiple times + signal.signal(signal.SIGINT, signal.SIG_IGN) + raise _direct_exit(130) # Will be raised in main processing loop elif sig==signal.SIGQUIT: - os.write(stdout_fd, b'\x1c') # '^\' character + if process.poll()==None: + os.write(stdout_fd, b'\x1c') # '^\' character try: # Detect if stdin is piped (e.g. cat file|clitheme-exec grep content) stdin_fd=stdout_slave @@ -143,6 +153,9 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) last_input_content=None last_tcgetpgrp=os.tcgetpgrp(stdout_fd) + def reset_terminal(): + if prev_attrs!=None: termios.tcsetattr(sys.stdout, termios.TCSADRAIN, prev_attrs) # restore previous attributes + print("\x1b[0m\x1b[?1;1000;1001;1002;1003;1005;1006;1015;1016l\n\x1b[J", end='') # reset color, mouse reporting, and clear the rest of the screen def handle_debug_pgrp(foreground_pid: int): nonlocal last_tcgetpgrp if "foreground" in debug_mode and foreground_pid!=last_tcgetpgrp: @@ -152,8 +165,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) thread_exception_handled=False def handle_exception(exc: Optional[Exception]=None): nonlocal thread_exception_handled; thread_exception_handled=True - if prev_attrs!=None: termios.tcsetattr(sys.stdout, termios.TCSADRAIN, prev_attrs) # restore previous attributes - print("\x1b[0m\x1b[?1;1000;1001;1002;1003;1005;1006;1015;1016l\n\x1b[J", end='') # reset color, mouse reporting, and clear the rest of the screen + reset_terminal() _labeled_print(fd.reof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) if exc!=None: raise exc else: raise @@ -326,6 +338,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) except termios.error: pass # subst operation and print output os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(),output) + except _direct_exit: break except: if not thread_exception_handled: handle_exception() else: raise diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index 2bf02ba..6dde6bf 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -101,4 +101,8 @@ in_domainapp swiftycode clitheme # locale:default Error: an internal error has occurred while executing the command (execution halted): locale:zh_CN 错误:执行命令时发生内部错误(执行已终止): [/entry] + [entry] output-interrupted-exit + # locale:default Output interrupted after command exit + locale:zh_CN 输出在命令退出后被中断 + [/entry] {/entries_section} \ No newline at end of file -- Gitee From 8fe0195274944261c842a13f2ebab2f66655b8e0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 19 Nov 2024 10:46:38 +0800 Subject: [PATCH 089/122] Improve error messages in `check_regenerate_db` --- src/clitheme/exec/__init__.py | 10 +++++++--- src/clitheme/strings/exec-strings.clithemedef.txt | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 87f1eef..fe387cd 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -17,7 +17,8 @@ import io import shutil import functools def _labeled_print(msg: str): - print("[clitheme-exec] "+msg) + for line in msg.splitlines(): + print("[clitheme-exec] "+line) from .. import _globalvar, cli, frontend from .._generator import db_interface @@ -76,8 +77,11 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) _globalvar.handle_exception() return False except FileNotFoundError: pass - except: - _labeled_print(fd.feof("db-migration-err", "An error occurred while migrating the database: {msg}\nPlease re-apply the theme and try again", msg=str(sys.exc_info()[1]))) + except Exception as exc: + msg=fd.reof("db-invalid-version", "Invalid database version information")\ + if type(exc) in (ValueError, TypeError) else str(sys.exc_info()[1]) + # ValueError: value is not an integer; TypeError: fetched value is None + _labeled_print(fd.feof("db-read-err", "An error occurred while reading the database: {msg}\nPlease re-apply the theme and try again", msg=msg)) _globalvar.handle_exception() return False return True diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index 6dde6bf..368d7c1 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -89,6 +89,20 @@ in_domainapp swiftycode clitheme 请重新应用当前主题,然后重试 [/locale] [/entry] + [entry] db-read-err + # [locale] default + # An error occurred while reading the database: {msg} + # Please re-apply the theme and try again + # [/locale] + [locale] zh_CN + 读取数据库时发生了错误:{msg} + 请重新应用当前主题,然后重试 + [/locale] + [/entry] + [entry] db-invalid-version + # locale:default Invalid database version information + locale:zh_CN 无效数据库版本信息 + [/entry] [entry] db-migrate-success-msg # locale:default Successfully completed migration, proceeding execution locale:zh_CN 数据库迁移完成,继续执行命令 -- Gitee From a8c7c86eea4b25f394d9da0228e42119d05ec007 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 20 Nov 2024 16:22:46 +0800 Subject: [PATCH 090/122] Make `endmatchhere` apply only to rules defined in a single file Only the other substitution rules in the corresponding file are skipped, instead of everything else in the database --- .../_generator/_entry_block_handler.py | 1 + src/clitheme/_generator/_parser_handlers.py | 1 + src/clitheme/_generator/db_interface.py | 18 +++++++++++------- src/clitheme/_globalvar.py | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index 5b29ca5..b5217dd 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -148,6 +148,7 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su stdout_stderr_matchoption=substrules_stdout_stderr_option, \ foreground_only=substrules_foregroundonly, \ line_number_debug=entry[4], \ + file_id=self.file_id, \ unique_id=entry[3]) except self.db_interface.bad_pattern: self.handle_error(self.fd.feof("bad-subst-pattern-err", "Bad substitute pattern at line {num} ({error_msg})", num=entry[4], error_msg=sys.exc_info()[1])) else: diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index 3db1c23..a9e9dbf 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -57,6 +57,7 @@ class GeneratorObject(_data_handlers.DataHandlers): self.custom_infofile_name=custom_infofile_name self.filename=filename self.file_content=file_content + self.file_id=uuid.uuid4() _data_handlers.DataHandlers.__init__(self, path, silence_warn) from . import db_interface self.db_interface=db_interface diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 5ed4c44..376d51d 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -44,6 +44,7 @@ def init_db(file_path: str): substitute_pattern TEXT NOT NULL, \ is_regex INTEGER NOT NULL, \ unique_id TEXT NOT NULL, \ + file_id TEXT NOT NULL, \ effective_command TEXT, \ effective_locale TEXT, \ command_match_strictness INTEGER NOT NULL, \ @@ -66,14 +67,14 @@ def connect_db(path: str=f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_f if version!=_globalvar.db_version: raise need_db_regenerate -def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, foreground_only: bool=False, unique_id: uuid.UUID=uuid.UUID(int=0), line_number_debug: str="-1"): +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, foreground_only: bool=False, unique_id: uuid.UUID=uuid.UUID(int=0), file_id: uuid.UUID=uuid.UUID(int=0), line_number_debug: str="-1"): if unique_id==uuid.UUID(int=0): unique_id=uuid.uuid4() cmdlist: List[str]=[] try: re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid except: raise bad_pattern(str(sys.exc_info()[1])) # handle condition where no effective_locale is specified ("default") locale_condition="AND effective_locale=?" if effective_locale!=None else "AND typeof(effective_locale)=typeof(?)" - insert_values=["match_pattern", "substitute_pattern", "effective_command", "is_regex", "command_match_strictness", "end_match_here", "effective_locale", "stdout_stderr_only", "unique_id", "foreground_only"] + insert_values=["match_pattern", "substitute_pattern", "effective_command", "is_regex", "command_match_strictness", "end_match_here", "effective_locale", "stdout_stderr_only", "unique_id", "foreground_only", "file_id"] if effective_commands!=None and len(effective_commands)>0: for cmd in effective_commands: # remove extra spaces in the command @@ -86,7 +87,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma _handle_warning(fd.feof("repeated-substrules-warn", "Repeated substrules entry at line {num}, overwriting", num=line_number_debug)) connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern, substitute_pattern, None, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption, str(unique_id), foreground_only)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern, substitute_pattern, None, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption, str(unique_id), foreground_only, str(file_id))) for cmd in cmdlist: # remove any existing values with the same match_pattern and effective_command strictness_condition="" @@ -97,7 +98,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma _handle_warning(fd.feof("repeated-substrules-warn", "Repeated substrules entry at line {num}, overwriting", num=line_number_debug)) connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern, substitute_pattern, cmd, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption, str(unique_id), foreground_only)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern, substitute_pattern, cmd, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption, str(unique_id), foreground_only, str(file_id))) connection.commit() ## Database fetch caching @@ -160,7 +161,7 @@ def _get_matches(command: Optional[str]) -> List[tuple]: final_cmdlist_exactmatch.append(strictness==2) matches=[] def fetch_matches_by_locale(filter_condition: str, filter_data: tuple=tuple()): - fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only", "unique_id", "foreground_only", "effective_command", "command_match_strictness"] + fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only", "unique_id", "foreground_only", "effective_command", "command_match_strictness", "file_id"] # get locales locales=_globalvar.get_locale() nonlocal matches @@ -226,13 +227,16 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # timeout value for each match operation match_timeout=_globalvar.output_subst_timeout -def _handle_subst(matches: List[tuple], content: bytes, is_stderr: bool, pids: Tuple[int,int], target_command: Optional[str]): +def _handle_subst(matches: List[tuple], content: bytes, is_stderr: bool, pids: Tuple[int,int], target_command: Optional[str]) -> bytes: content_str=copy.copy(content) encountered_ids=set() + skipped_files=set() # File ids skipped with endmatchhere option for match_data in matches: if match_data[4]!=0 and is_stderr+1!=match_data[4]: continue # check stdout/stderr constraint if match_data[5] in encountered_ids: continue # check uuid else: encountered_ids.add(match_data[5]) + # check if corresponding file is skipped due to endmatchhere + if match_data[9] in skipped_files: continue # Check strictness if target_command!=None and match_data[7]!=None and \ _check_strictness(match_data[7], match_data[8], \ @@ -257,5 +261,5 @@ def _handle_subst(matches: List[tuple], content: bytes, is_stderr: bool, pids: T matched=bytes(match_data[0], 'utf-8') in content_str content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) if match_data[3]==True and matched: # endmatchhere is set - break + skipped_files.add(match_data[9]) return content_str diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 592a107..968ebd2 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -61,7 +61,7 @@ generator_info_v2filename=generator_info_filename+"_v2" # e.g. [...]/theme-info/ ## _generator.db_interface file and table names db_data_tablename="clitheme_subst_data" db_filename="subst-data.db" # e.g. ~/.local/share/clitheme/subst-data.db -db_version=3 +db_version=4 ## clitheme-exec timeout value for each output substitution operation output_subst_timeout=0.4 -- Gitee From 1b0cd7578f8cd8e006d1282db80b7fbc0fb4a3ec Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 20 Nov 2024 18:03:05 +0800 Subject: [PATCH 091/122] Change message wording of database update mechanism --- src/clitheme/exec/__init__.py | 8 ++++---- .../strings/exec-strings.clithemedef.txt | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index fe387cd..d6005de 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -42,7 +42,7 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) raise db_interface.need_db_regenerate("Forced database regeneration with $CLITHEME_REGENERATE_DB=1") else: db_interface.connect_db() except db_interface.need_db_regenerate: - _labeled_print(fd.reof("substrules-migrate-msg", "Migrating substrules database...")) + _labeled_print(fd.reof("substrules-update-msg", "Updating database...")) orig_stdout=sys.stdout try: # gather files @@ -65,15 +65,15 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) cli_msg=io.StringIO() sys.stdout=cli_msg if not cli.apply_theme(file_contents, filenames=paths, overlay=False, generate_only=True, preserve_temp=True)==0: - raise Exception(fd.reof("db-migration-generator-err", "Failed to generate data (full log below):")+"\n"+cli_msg.getvalue()+"\n") + raise Exception(fd.reof("db-update-generator-err", "Failed to generate data (full log below):")+"\n"+cli_msg.getvalue()+"\n") sys.stdout=orig_stdout try: os.remove(dest_root_path+"/"+_globalvar.db_filename) except FileNotFoundError: raise shutil.copy(cli.last_data_path+"/"+_globalvar.db_filename, dest_root_path+"/"+_globalvar.db_filename) - _labeled_print(fd.reof("db-migrate-success-msg", "Successfully completed migration, proceeding execution")) + _labeled_print(fd.reof("db-update-success-msg", "Successfully updated database, proceeding execution")) except: sys.stdout=orig_stdout - _labeled_print(fd.feof("db-migration-err", "An error occurred while migrating the database: {msg}\nPlease re-apply the theme and try again", msg=str(sys.exc_info()[1]))) + _labeled_print(fd.feof("db-update-err", "An error occurred while updating the database: {msg}\nPlease re-apply the theme and try again", msg=str(sys.exc_info()[1]))) _globalvar.handle_exception() return False except FileNotFoundError: pass diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index 368d7c1..efee0c8 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -71,21 +71,21 @@ in_domainapp swiftycode clitheme # locale:default Warning: no theme set or theme does not have substrules locale:zh_CN 警告:没有设定主题或当前主题没有substrules定义 [/entry] - [entry] substrules-migrate-msg - # locale:default Migrating substrules database... - locale:zh_CN 正在迁移substrules数据库... + [entry] substrules-update-msg + # locale:default Updating database... + locale:zh_CN 正在更新数据库... [/entry] - [entry] db-migration-generator-err + [entry] db-update-generator-err # locale:default Failed to generate data (full log below): locale:zh_CN 无法生成数据(完整日志在此): [/entry] - [entry] db-migration-err + [entry] db-update-err # [locale] default - # An error occurred while migrating the database: {msg} + # An error occurred while updating the database: {msg} # Please re-apply the theme and try again # [/locale] [locale] zh_CN - 迁移数据库时发生了错误:{msg} + 更新数据库时发生了错误:{msg} 请重新应用当前主题,然后重试 [/locale] [/entry] @@ -103,9 +103,9 @@ in_domainapp swiftycode clitheme # locale:default Invalid database version information locale:zh_CN 无效数据库版本信息 [/entry] - [entry] db-migrate-success-msg - # locale:default Successfully completed migration, proceeding execution - locale:zh_CN 数据库迁移完成,继续执行命令 + [entry] db-update-success-msg + # locale:default Successfully updated database, proceeding execution + locale:zh_CN 数据库更新完成,继续执行命令 [/entry] [entry] command-fail-err # locale:default Error: failed to run command: {msg} -- Gitee From 4d98e7222f816c9d909fb383569cc22e0ece7e98 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 21 Nov 2024 22:31:47 +0800 Subject: [PATCH 092/122] Update version (v2.0-dev20241121) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 18b5401..812b344 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241113 +pkgver=2.0_dev20241121 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 0b7260a..f24bb2d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241113-1) unstable; urgency=low +clitheme (2.0-dev20241121-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 13 Nov 2024 21:57:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 21 Nov 2024 22:30:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index a1b5507..0d94626 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241113" +__version__="2.0-dev20241121" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241113" +version_main="2.0_dev20241121" version_buildnumber=1 \ No newline at end of file -- Gitee From c25352fbf40f9c20376f3e9a37228bdd762f53c6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 22 Nov 2024 07:54:02 +0800 Subject: [PATCH 093/122] Add instructions on Python installation in README --- .github/README.md | 16 +++++++++++++--- README.md | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/README.md b/.github/README.md index efe1b13..abd19f0 100644 --- a/.github/README.md +++ b/.github/README.md @@ -169,11 +169,21 @@ Please see [this article](./README-frontend.en.md) `clitheme` can be installed through pip package, Debian package, and Arch Linux package. -### Install using pip package +### Install using Python/pip package + +First, ensure that Python 3 is installed on the system. `clitheme` requires Python 3.8 or higher. + +- On Linux distributions, you can use relevant package manager to install +- On macOS, you can install Python through Xcode command line developer tools (use `xcode-select --install` command), or through Python website ( https://www.python.org/downloads ) +- On Windows, you can install Python through Microsoft Store ([Python 3.13 link](https://apps.microsoft.com/detail/9pnrbtzxmb4z)), or through Python website ( https://www.python.org/downloads ) + +Then, ensure that `pip` is installed within Python. The following command will perform an offline install of `pip` if it's not detected. + + $ python3 -m ensurepip Download the `.whl` file from latest distribution page and install it using `pip`: - $ pip install ./clitheme--py3-none-any.whl + $ python3 -m pip install ./clitheme--py3-none-any.whl ### Install using Arch Linux package @@ -195,7 +205,7 @@ You can build the package from the repository source code, which includes any la First, install `setuptools`, `build`, and `wheel` packages. You can use the packages provided by your Linux distribution, or install using `pip`: - $ pip install --upgrade setuptools build wheel + $ python3 -m pip install --upgrade setuptools build wheel Then, switch to project directory and use the following command to build the package: diff --git a/README.md b/README.md index b835383..0954cd8 100644 --- a/README.md +++ b/README.md @@ -168,11 +168,21 @@ $ clitheme-man ls 安装`clitheme`非常简单,您可以通过pip软件包,Arch Linux软件包,或者Debian软件包安装。 -### 通过pip软件包安装 +### 通过Python/pip软件包安装 + +首先,确保Python 3已安装在系统中。`clitheme`需要Python 3.8或更高版本。 + +- 在Linux发行版上,你可以通过对应的软件包管理器安装Python +- 在macOS上,你可以通过Xcode命令行开发者工具安装Python(使用`xcode-select --install`命令),或者通过Python官网( https://www.python.org/downloads )下载 +- 在Windows上,你可以通过Microsoft Store安装Python([Python 3.13链接](https://apps.microsoft.com/detail/9pnrbtzxmb4z)),或者通过Python官网( https://www.python.org/downloads )下载 + +然后,确保`pip`软件包管理器已安装在Python中。以下命令将会通过本地安装`pip`,如果检测到没有安装。 + + $ python3 -m ensurepip 从最新发行版页面下载`.whl`文件,使用`pip`直接安装即可: - $ pip install ./clitheme--py3-none-any.whl + $ python3 -m pip install ./clitheme--py3-none-any.whl ### 通过Arch Linux软件包安装 @@ -194,7 +204,7 @@ $ clitheme-man ls 首先,安装`setuptools`、`build`、和`wheel`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: - $ pip install --upgrade setuptools build wheel + $ python3 -m pip install --upgrade setuptools build wheel 然后,切换到项目目录,使用以下命令构建软件包: -- Gitee From c3139cae9af85c6a0416a9b10ad522614772ab7c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 27 Nov 2024 15:48:38 +0800 Subject: [PATCH 094/122] Unify substesc and substvar into `parse_content` function - Change all `subst_variable_content` to `parse_content` without substesc operation --- src/clitheme/_generator/_entries_parser.py | 4 ++-- src/clitheme/_generator/_entry_block_handler.py | 6 +++--- src/clitheme/_generator/_header_parser.py | 5 ++--- src/clitheme/_generator/_manpage_parser.py | 10 +++++----- src/clitheme/_generator/_parser_handlers.py | 10 +++++----- src/clitheme/_generator/_substrules_parser.py | 2 +- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index 393b893..da7e15a 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -23,7 +23,7 @@ def handle_entries_section(obj: _parser_handlers.GeneratorObject, first_phrase: while obj.goto_next_line(): phrases=obj.lines_data[obj.lineindex].split() if phrases[0]=="in_domainapp": - this_phrases=obj.subst_variable_content(obj.lines_data[obj.lineindex].strip()).split() + this_phrases=obj.parse_content(obj.lines_data[obj.lineindex].strip(), pure_name=True).split() obj.check_enough_args(this_phrases, 3) obj.check_extra_args(this_phrases, 3, use_exact_count=False) obj.in_domainapp=this_phrases[1]+" "+this_phrases[2] @@ -33,7 +33,7 @@ def handle_entries_section(obj: _parser_handlers.GeneratorObject, first_phrase: elif phrases[0]=="in_subsection": obj.check_enough_args(phrases, 2) obj.in_subsection=_globalvar.splitarray_to_string(phrases[1:]) - obj.in_subsection=obj.subst_variable_content(obj.in_subsection) + obj.in_subsection=obj.parse_content(obj.in_subsection, pure_name=True) if _globalvar.sanity_check(obj.in_subsection)==False: obj.handle_error(obj.fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) elif phrases[0]=="unset_domainapp": diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index b5217dd..459fe75 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -69,8 +69,8 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su self.check_enough_args(phrases, 3) content=_globalvar.extract_content(line_content, begin_phrase_count=2) locale=phrases[1] - locales=self.subst_variable_content(locale).split() - content=self.handle_singleline_content(content) + locales=self.parse_content(locale, pure_name=True).split() + content=self.parse_content(content) for this_locale in locales: for each_name in entryNames: if is_substrules: @@ -82,7 +82,7 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) elif phrases[0] in ("locale_block", "[locale]"): self.check_enough_args(phrases, 2) - locales=self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() + locales=self.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() begin_line_number=self.lineindex+1+1 content=self.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/locale]" if phrases[0]=="[locale]" else "end_block") for this_locale in locales: diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index 9414867..d40f189 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -23,15 +23,14 @@ def handle_header_section(obj: _parser_handlers.GeneratorObject, first_phrase: s if phrases[0] in ("name", "version", "description"): obj.check_enough_args(phrases, 2) content=_globalvar.extract_content(obj.lines_data[obj.lineindex]) - if phrases[0]=="description": content=obj.handle_singleline_content(content) - else: content=obj.subst_variable_content(content) + content=obj.parse_content(content, pure_name=phrases[0]!="description") obj.write_infofile( \ obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ _globalvar.generator_info_filename.format(info=phrases[0]),\ content,obj.lineindex+1,phrases[0]) # e.g. [...]/theme-info/1/clithemeinfo_name elif phrases[0] in ("locales", "supported_apps"): obj.check_enough_args(phrases, 2) - content=obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() + content=obj.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() obj.write_infofile_newlines( \ obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ _globalvar.generator_info_v2filename.format(info=phrases[0]),\ diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index ab1aa27..aca50ce 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -42,7 +42,7 @@ def handle_manpage_section(obj: _parser_handlers.GeneratorObject, first_phrase: if phrases[0]=="[file_content]": def handle(p: List[str]) -> List[str]: obj.check_enough_args(p, 2) - filepath=obj.subst_variable_content(_globalvar.splitarray_to_string(p[1:])).split() + filepath=obj.parse_content(_globalvar.splitarray_to_string(p[1:]), pure_name=True).split() # sanity check the file path if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: obj.handle_error(obj.fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) @@ -63,14 +63,14 @@ def handle_manpage_section(obj: _parser_handlers.GeneratorObject, first_phrase: obj.write_manpage_file(filepath, content, obj.lineindex+1) elif phrases[0]=="include_file": obj.check_enough_args(phrases, 2) - filepath=obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() + filepath=obj.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: obj.handle_error(obj.fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) filecontent=get_file_content(filepath) # expect "as" clause on next line if obj.goto_next_line() and len(obj.lines_data[obj.lineindex].split())>0 and obj.lines_data[obj.lineindex].split()[0]=="as": - target_file=obj.subst_variable_content(_globalvar.splitarray_to_string(obj.lines_data[obj.lineindex].split()[1:])).split() + target_file=obj.parse_content(_globalvar.splitarray_to_string(obj.lines_data[obj.lineindex].split()[1:]), pure_name=True).split() if _globalvar.sanity_check(_globalvar.splitarray_to_string(target_file))==False: obj.handle_error(obj.fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) obj.write_manpage_file(target_file, filecontent, obj.lineindex+1) @@ -78,7 +78,7 @@ def handle_manpage_section(obj: _parser_handlers.GeneratorObject, first_phrase: obj.handle_error(obj.fd.feof("include-file-missing-phrase-err", "Missing \"as \" phrase on next line of line {num}", num=str(obj.lineindex+1-1))) elif phrases[0]=="[include_file]": obj.check_enough_args(phrases, 2) - filepath=obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() + filepath=obj.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: obj.handle_error(obj.fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) filecontent=get_file_content(filepath) @@ -86,7 +86,7 @@ def handle_manpage_section(obj: _parser_handlers.GeneratorObject, first_phrase: p=obj.lines_data[obj.lineindex].split() if p[0]=="as": obj.check_enough_args(p, 2) - target_file=obj.subst_variable_content(_globalvar.splitarray_to_string(obj.lines_data[obj.lineindex].split()[1:])).split() + target_file=obj.parse_content(_globalvar.splitarray_to_string(obj.lines_data[obj.lineindex].split()[1:]), pure_name=True).split() if _globalvar.sanity_check(_globalvar.splitarray_to_string(target_file))==False: obj.handle_error(obj.fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) obj.write_manpage_file(target_file, filecontent, obj.lineindex+1) diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index a9e9dbf..3d36b5a 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -102,7 +102,7 @@ class GeneratorObject(_data_handlers.DataHandlers): final_options={} if merge_global_options!=0: final_options=copy.copy(self.global_options if merge_global_options==1 else self.really_really_global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options - options_data=self.subst_variable_content(_globalvar.splitarray_to_string(options_data)).split() + options_data=self.parse_content(_globalvar.splitarray_to_string(options_data), pure_name=True).split() for each_option in options_data: option_name=re.sub(r"^(no)?(?P.+?)(:.+)?$", r"\g", each_option) option_name_preserve_no=re.sub(r"^(?P.+?)(:.+)?$", r"\g", each_option) @@ -197,8 +197,8 @@ class GeneratorObject(_data_handlers.DataHandlers): if char in var_name: bad_var() var_content=_globalvar.extract_content(line_content) - # subst variable references - var_content=self.subst_variable_content(var_content) + # Parse content without substesc (subst variable content) + var_content=self.parse_content(var_content, pure_name=True) # set variable if really_really_global: self.really_really_global_variables[var_name]=var_content self.global_variables[var_name]=var_content @@ -215,10 +215,10 @@ class GeneratorObject(_data_handlers.DataHandlers): def handle_linenumber_range(self, begin: int, end: int) -> str: if begin==end: return str(end) else: return f"{begin}-{end}" - def handle_singleline_content(self, content: str) -> str: + def parse_content(self, content: str, pure_name: bool=False) -> str: target_content=copy.copy(content) target_content=self.subst_variable_content(target_content) - if self.global_options.get("substesc")==True: + if pure_name==False and self.global_options.get("substesc")==True: target_content=self.handle_substesc(target_content) return target_content def handle_setters(self, really_really_global: bool=False) -> bool: diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 6d005ad..6dc0e78 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -73,7 +73,7 @@ def handle_substrules_section(obj: _parser_handlers.GeneratorObject, first_phras obj.check_enough_args(phrases, 2) reset_outline_foregroundonly() content=_globalvar.splitarray_to_string(phrases[1:]) - content=obj.subst_variable_content(content) + content=obj.parse_content(content, pure_name=True) strictness=0 for this_option in obj.global_options: if this_option=="strictcmdmatch" and obj.global_options['strictcmdmatch']==True: -- Gitee From acd5508c547ef7324314839a5eca02c1fbcd479c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 28 Nov 2024 12:29:09 +0800 Subject: [PATCH 095/122] Reset variables to global variables after section exit - Fix the variable scope logic here --- src/clitheme/_generator/_parser_handlers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index 3d36b5a..29710a8 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -210,6 +210,7 @@ class GeneratorObject(_data_handlers.DataHandlers): def handle_end_section(self, section_name: str): self.parsed_sections.append(section_name) self.section_parsing=False + self.handle_setup_global_options() def handle_substesc(self, content: str) -> str: return content.replace("{{ESC}}", "\x1b") def handle_linenumber_range(self, begin: int, end: int) -> str: -- Gitee From 16231d29d6a34268b0d1eb547487288146076689 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 28 Nov 2024 13:19:20 +0800 Subject: [PATCH 096/122] Improve substesc and substvar handling to process warnings - Always call `substesc` with a condition to handle the warning - Change `substvar_warning` into `warnings` dict - Add missing localization strings --- .../_generator/_entry_block_handler.py | 5 +- src/clitheme/_generator/_parser_handlers.py | 47 ++++++++++++------- .../strings/generator-strings.clithemedef.txt | 9 ++++ 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index 459fe75..74b1d5d 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -122,12 +122,13 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su entry=entries[x] match_pattern=entry[0] # substvar MUST come before substesc or "{{ESC}}" in variable content will not be processed + debug_linenumber=entry[5] if is_substrules else entry[4] if entry_name_substvar: match_pattern=self.subst_variable_content(match_pattern, override_check=True, \ - line_number_debug=entry[5] if is_substrules else entry[4], \ + line_number_debug=debug_linenumber, \ # Don't show warnings for the same match_pattern silence_warnings=entry[3] in encountered_ids) - if entry_name_substesc: match_pattern=self.handle_substesc(match_pattern) + match_pattern=self.handle_substesc(match_pattern, condition=entry_name_substesc==True, line_number_debug=debug_linenumber) if is_substrules: check_valid_pattern(match_pattern, entry[5]) else: diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index 29710a8..3e14ff7 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -41,7 +41,7 @@ class GeneratorObject(_data_handlers.DataHandlers): def __init__(self, file_content: str, custom_infofile_name: str, filename: str, path: str, silence_warn: bool): # data to keep track of - self.substvar_warning=True + self.warnings: Dict[str, bool]={} self.section_parsing=False self.parsed_sections=[] self.lines_data=file_content.splitlines() @@ -141,25 +141,30 @@ class GeneratorObject(_data_handlers.DataHandlers): if really_really_global: self.really_really_global_options=self.parse_options(options_data, merge_global_options=2) self.global_options=self.parse_options(options_data, merge_global_options=1) + specified_options=self.parse_options(options_data, merge_global_options=False) # if manually disabled, show substvar warning again next time - if self.global_options.get("substvar")!=True \ - and "substvar" in self.parse_options(options_data, merge_global_options=False): - self.substvar_warning=True + for option in ("substvar", "substesc"): + if self.global_options.get(option)!=True \ + and option in specified_options: + self.warnings[option]=True def handle_setup_global_options(self): + prev_options=copy.copy(self.global_options) # reset global_options to contents of really_really_global_options self.global_options=copy.copy(self.really_really_global_options) - # if manually disabled, show substvar warning again next time - if self.global_options.get("substvar")!=True: self.substvar_warning=True + # if manually disabled, show warnings again next time + for option in ("substvar", "substesc"): + if self.global_options.get(option)!=True and prev_options.get(option)==True: + self.warnings[option]=True self.global_variables=copy.copy(self.really_really_global_variables) def subst_variable_content(self, content: str, override_check: bool=False, line_number_debug: Optional[str]=None, silence_warnings: bool=False) -> str: pattern=r"{{([^\s]+?)??}}" if not override_check and self.global_options.get("substvar")!=True: # Handle substvar warning - if self.substvar_warning: + if self.warnings.get('substvar') in (True,None): for match in re.finditer(pattern, content): if self.global_variables.get(match.group(1))!=None: - self.handle_warning(self.fd.feof("set-substvar-warn", "Line {num}: Attempted to reference a defined variable, but \"substvar\" option is not enabled", num=line_number_debug if line_number_debug!=None else str(self.lineindex+1))) - self.substvar_warning=False + self.handle_warning(self.fd.feof("set-substvar-warn", "Line {num}: attempted to reference a defined variable, but \"substvar\" option is not enabled", num=line_number_debug if line_number_debug!=None else str(self.lineindex+1))) + self.warnings['substvar']=False break return content # get all variables used in content @@ -211,16 +216,23 @@ class GeneratorObject(_data_handlers.DataHandlers): self.parsed_sections.append(section_name) self.section_parsing=False self.handle_setup_global_options() - def handle_substesc(self, content: str) -> str: - return content.replace("{{ESC}}", "\x1b") + def handle_substesc(self, content: str, condition: bool, line_number_debug: Optional[str]=None) -> str: + if condition==True: + return content.replace("{{ESC}}", "\x1b") + else: + # Handle substesc warning + if self.warnings.get("substesc") in (True,None) and "{{ESC}}" in content: + self.handle_warning(self.fd.feof("set-substesc-warn", "Line {num}: attempted to use \"{{{{ESC}}}}\", but \"substesc\" option is not enabled", num=line_number_debug if line_number_debug!=None else str(self.lineindex+1))) + self.warnings['substesc']=False + return content def handle_linenumber_range(self, begin: int, end: int) -> str: if begin==end: return str(end) else: return f"{begin}-{end}" def parse_content(self, content: str, pure_name: bool=False) -> str: target_content=copy.copy(content) target_content=self.subst_variable_content(target_content) - if pure_name==False and self.global_options.get("substesc")==True: - target_content=self.handle_substesc(target_content) + if pure_name==False: + target_content=self.handle_substesc(target_content, condition=pure_name==False and self.global_options.get("substesc")==True) return target_content def handle_setters(self, really_really_global: bool=False) -> bool: # Handle set_options and setvar @@ -277,6 +289,7 @@ class GeneratorObject(_data_handlers.DataHandlers): if len(self.lines_data[self.lineindex].split())>1: got_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=True) specified_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=False) + debug_linenumber=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1) for option in got_options.keys(): def is_specified_in_block() -> bool: return option in specified_options.keys() def check_whether_explicitly_specified(pass_condition: bool): @@ -293,15 +306,15 @@ class GeneratorObject(_data_handlers.DataHandlers): # Only handle substesc independently if substvar is not specified check_whether_explicitly_specified(pass_condition=not disable_substesc) # substitute {{ESC}} with escape literal - if got_options['substesc']==True and not disable_substesc: blockinput_data=self.handle_substesc(blockinput_data) + blockinput_data=self.handle_substesc(blockinput_data, condition=got_options['substesc']==True and not disable_substesc, line_number_debug=debug_linenumber) elif option=="substvar": - if got_options['substvar']==True: blockinput_data=self.subst_variable_content(blockinput_data, override_check=True, line_number_debug=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1)) + if got_options['substvar']==True: blockinput_data=self.subst_variable_content(blockinput_data, override_check=True, line_number_debug=debug_linenumber) # If substvar, substesc must be handled after that, or "{{ESC}}" in variable content will be ignored - if got_options.get('substesc')==True and not disable_substesc: blockinput_data=self.handle_substesc(blockinput_data) + blockinput_data=self.handle_substesc(blockinput_data, condition=got_options.get('substesc')==True and not disable_substesc, line_number_debug=debug_linenumber) elif disallow_cmdmatch_options: if is_specified_in_block(): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option))) if "substvar" not in specified_options: # Let the function show the substvar warning - self.subst_variable_content(blockinput_data, override_check=False, line_number_debug=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1)) + self.subst_variable_content(blockinput_data, override_check=False, line_number_debug=debug_linenumber) return blockinput_data handle_entry=_entry_block_handler.handle_entry \ No newline at end of file diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 07e6b50..e8d14f5 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -175,6 +175,15 @@ in_domainapp swiftycode clitheme # locale:default Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories locale:zh_CN 第{num}行:manpage路径{sanitycheck_msg};使用空格以指定子路径 [/entry] + [entry] set-substvar-warn + # locale:default Line {num}: attempted to reference a defined variable, but "substvar" option is not enabled + locale:zh_CN 第{num}行:尝试引用定义的变量,但"substvar"选项未被启用 + [/entry] + [entry] set-substesc-warn + # Must use "{{{{ESC}}}}" to represent "{{ESC}}" in the message + # locale:default Line {num}: attempted to use "{{{{ESC}}}}", but "substesc" option is not enabled + locale:zh_CN Line {num}: 尝试引用"{{{{ESC}}}}",但"substesc"选项未被启用 + [/entry] # 以下提示会是上方定义中"{sanitycheck_msg}"的内容 # The following messages are the contents of "{sanitycheck_msg}" in the above entries [entry] sanity-check-msg-banphrase-err -- Gitee From 63335666ee85b329bc6a6fe76abb59575a42ce7e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 30 Nov 2024 22:24:52 +0800 Subject: [PATCH 097/122] Rewrite option parsing in `handle_block_input` - Use if statements instead of loop and use `allowed_options` parameter to enforce option restrictions - Extra: rename `disallow_cmdmatch_options` to `disallow_other_options` - Extra: Fix type annotations in `parse_options` --- src/clitheme/_generator/_parser_handlers.py | 48 ++++++++----------- src/clitheme/_generator/_substrules_parser.py | 2 +- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index 3e14ff7..1efe613 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -16,7 +16,7 @@ import uuid from typing import Optional, Union, List, Dict from .. import _globalvar, _version from . import _data_handlers, _entry_block_handler -# spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption +# spell-checker:ignore lineindex banphrases minspaces blockinput optline datapath matchoption class GeneratorObject(_data_handlers.DataHandlers): @@ -97,7 +97,7 @@ class GeneratorObject(_data_handlers.DataHandlers): req_ver=self.fmt(version_str)), not_syntax_error=True) def handle_invalid_phrase(self, name: str): self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=self.fmt(name), num=str(self.lineindex+1))) - def parse_options(self, options_data: List[str], merge_global_options: int, allowed_options: Optional[list]=None) -> Dict[str, Union[int,bool]]: + def parse_options(self, options_data: List[str], merge_global_options: int, allowed_options: Optional[List[str]]=None) -> Dict[str, Union[int,bool]]: # merge_global_options: 0 - Don't merge; 1 - Merge self.global_options; 2 - Merge self.really_really_global_options final_options={} if merge_global_options!=0: final_options=copy.copy(self.global_options if merge_global_options==1 else self.really_really_global_options) @@ -248,7 +248,7 @@ class GeneratorObject(_data_handlers.DataHandlers): ## sub-block processing functions - def handle_block_input(self, preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True, disable_substesc: bool=False) -> str: + def handle_block_input(self, preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_other_options: bool=True, disable_substesc: bool=False) -> str: minspaces=math.inf blockinput_data="" begin_line_number=self.lineindex+1+1 @@ -283,36 +283,28 @@ class GeneratorObject(_data_handlers.DataHandlers): if preserve_indents: pattern=r"(?P\n|^)[ ]{"+str(minspaces)+"}" blockinput_data=re.sub(pattern,r"\g", blockinput_data, flags=re.MULTILINE) - # parse leadtabindents leadspaces, and substesc options + # parse leadtabindents, leadspaces, substesc, and substvar options here got_options=copy.copy(self.global_options) specified_options={} if len(self.lines_data[self.lineindex].split())>1: - got_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=True) + got_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], + merge_global_options=True, + allowed_options=\ + None if not disallow_other_options else\ + (self.lead_indent_options if preserve_indents else []+\ + ["substesc"] if not disable_substesc else []+\ + ["substvar"]) + ) specified_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=False) debug_linenumber=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1) - for option in got_options.keys(): - def is_specified_in_block() -> bool: return option in specified_options.keys() - def check_whether_explicitly_specified(pass_condition: bool): - if not pass_condition and is_specified_in_block(): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option))) - if option=="leadtabindents": - check_whether_explicitly_specified(pass_condition=preserve_indents) - # insert tabs at start of each line - if preserve_indents: blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) - elif option=="leadspaces": - check_whether_explicitly_specified(pass_condition=preserve_indents) - # insert spaces at start of each line - if preserve_indents: blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) - elif option=="substesc" and not "substvar" in got_options: - # Only handle substesc independently if substvar is not specified - check_whether_explicitly_specified(pass_condition=not disable_substesc) - # substitute {{ESC}} with escape literal - blockinput_data=self.handle_substesc(blockinput_data, condition=got_options['substesc']==True and not disable_substesc, line_number_debug=debug_linenumber) - elif option=="substvar": - if got_options['substvar']==True: blockinput_data=self.subst_variable_content(blockinput_data, override_check=True, line_number_debug=debug_linenumber) - # If substvar, substesc must be handled after that, or "{{ESC}}" in variable content will be ignored - blockinput_data=self.handle_substesc(blockinput_data, condition=got_options.get('substesc')==True and not disable_substesc, line_number_debug=debug_linenumber) - elif disallow_cmdmatch_options: - if is_specified_in_block(): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option))) + if preserve_indents and got_options.get("leadtabindents")!=None: + blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) + if preserve_indents and got_options.get("leadspaces")!=None: + blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) + if got_options.get("substvar")==True: + blockinput_data=self.subst_variable_content(blockinput_data, override_check=True, line_number_debug=debug_linenumber) + if not disable_substesc: # Must come after substvar + blockinput_data=self.handle_substesc(blockinput_data, condition=got_options.get("substesc")==True, line_number_debug=debug_linenumber) if "substvar" not in specified_options: # Let the function show the substvar warning self.subst_variable_content(blockinput_data, override_check=False, line_number_debug=debug_linenumber) diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 6dc0e78..ebd10cd 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -44,7 +44,7 @@ def handle_substrules_section(obj: _parser_handlers.GeneratorObject, first_phras if phrases[0]=="[filter_commands]": obj.check_extra_args(phrases, 1, use_exact_count=True) reset_outline_foregroundonly() - content=obj.handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=r"[/filter_commands]", disallow_cmdmatch_options=False, disable_substesc=True) + content=obj.handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=r"[/filter_commands]", disallow_other_options=False, disable_substesc=True) # read commands command_strings=content.splitlines() -- Gitee From a8cde22b54711854292c9648474dc1256ecbd3d9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 30 Nov 2024 22:52:19 +0800 Subject: [PATCH 098/122] Optimize `subst_variable_content` condition specification Allow specifying a custom condition in `subst_variable_content` to better handle the warning and make things easier. --- src/clitheme/_generator/_entry_block_handler.py | 9 ++++----- src/clitheme/_generator/_parser_handlers.py | 13 ++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index 74b1d5d..08b01c7 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -123,11 +123,10 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su match_pattern=entry[0] # substvar MUST come before substesc or "{{ESC}}" in variable content will not be processed debug_linenumber=entry[5] if is_substrules else entry[4] - if entry_name_substvar: - match_pattern=self.subst_variable_content(match_pattern, override_check=True, \ - line_number_debug=debug_linenumber, \ - # Don't show warnings for the same match_pattern - silence_warnings=entry[3] in encountered_ids) + match_pattern=self.subst_variable_content(match_pattern, custom_condition=entry_name_substvar, \ + line_number_debug=debug_linenumber, \ + # Don't show warnings for the same match_pattern + silence_warnings=entry[3] in encountered_ids) match_pattern=self.handle_substesc(match_pattern, condition=entry_name_substesc==True, line_number_debug=debug_linenumber) if is_substrules: check_valid_pattern(match_pattern, entry[5]) diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index 1efe613..6b22837 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -156,9 +156,11 @@ class GeneratorObject(_data_handlers.DataHandlers): if self.global_options.get(option)!=True and prev_options.get(option)==True: self.warnings[option]=True self.global_variables=copy.copy(self.really_really_global_variables) - def subst_variable_content(self, content: str, override_check: bool=False, line_number_debug: Optional[str]=None, silence_warnings: bool=False) -> str: + def subst_variable_content(self, content: str, custom_condition: Optional[bool]=None, line_number_debug: Optional[str]=None, silence_warnings: bool=False) -> str: pattern=r"{{([^\s]+?)??}}" - if not override_check and self.global_options.get("substvar")!=True: + # Check the condition here instead of respective if statements to better handle the warning + condition=self.global_options.get("substvar")==True if custom_condition==None else custom_condition + if condition==False: # Handle substvar warning if self.warnings.get('substvar') in (True,None): for match in re.finditer(pattern, content): @@ -301,12 +303,9 @@ class GeneratorObject(_data_handlers.DataHandlers): blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) if preserve_indents and got_options.get("leadspaces")!=None: blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) - if got_options.get("substvar")==True: - blockinput_data=self.subst_variable_content(blockinput_data, override_check=True, line_number_debug=debug_linenumber) + # Process substvar + blockinput_data=self.subst_variable_content(blockinput_data, custom_condition=got_options.get("substvar")==True, line_number_debug=debug_linenumber) if not disable_substesc: # Must come after substvar blockinput_data=self.handle_substesc(blockinput_data, condition=got_options.get("substesc")==True, line_number_debug=debug_linenumber) - if "substvar" not in specified_options: - # Let the function show the substvar warning - self.subst_variable_content(blockinput_data, override_check=False, line_number_debug=debug_linenumber) return blockinput_data handle_entry=_entry_block_handler.handle_entry \ No newline at end of file -- Gitee From f353990f671b9477dbd9dffe80e873112868d67e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 30 Nov 2024 23:11:07 +0800 Subject: [PATCH 099/122] Update version (v2.0-dev20241130) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 812b344..8bb7b37 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241121 +pkgver=2.0_dev20241130 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index f24bb2d..1af0ea1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241121-1) unstable; urgency=low +clitheme (2.0-dev20241130-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Thu, 21 Nov 2024 22:30:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 30 Nov 2024 23:08:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 0d94626..e5baea1 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241121" +__version__="2.0-dev20241130" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241121" +version_main="2.0_dev20241130" version_buildnumber=1 \ No newline at end of file -- Gitee From e149256dede36c3ddbe546f86e2567aa7390169c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 16 Dec 2024 22:58:15 +0800 Subject: [PATCH 100/122] Support using `[subst_regex]` and `[subst_string]` syntax Allow using a shorter syntax in definition files --- .github/README.md | 8 ++++---- README.md | 8 ++++---- src/clitheme/_generator/_substrules_parser.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/README.md b/.github/README.md index abd19f0..a6edc0c 100644 --- a/.github/README.md +++ b/.github/README.md @@ -99,14 +99,14 @@ Write theme definition file and substitution rules based on the output: gcc g++ [/filter_commands] - [substitute_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)warning: (?P({{ESC}}.*?m)*)incompatible pointer types assigning to '(?P.+)' from '(?P.+)' + [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)warning: (?P({{ESC}}.*?m)*)incompatible pointer types assigning to '(?P.+)' from '(?P.+)' # Use "locale:en_US" if you only want the substitution rule to applied when the system locale setting is English (en_US) # Use "locale:default" to not apply any locale filters locale:default \gnote: \gincompatible pointer types '\g' and '\g', they're so……so incompatible!~ - [/substitute_regex] - [substitute_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)error: (?P({{ESC}}.*?m)*)unknown type name '(?P.+)' + [/subst_regex] + [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)error: (?P({{ESC}}.*?m)*)unknown type name '(?P.+)' locale:default \gError! : \gunknown type name '\g', you forgot to d……define it!~ಥ_ಥ - [/substitute_regex] + [/subst_regex] {/substrules_section} ``` diff --git a/README.md b/README.md index 0954cd8..377c60a 100644 --- a/README.md +++ b/README.md @@ -98,14 +98,14 @@ e> {{ESC}}[0m2 errors generated.\r\n gcc g++ [/filter_commands] - [substitute_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)warning: (?P({{ESC}}.*?m)*)incompatible pointer types assigning to '(?P.+)' from '(?P.+)' + [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)warning: (?P({{ESC}}.*?m)*)incompatible pointer types assigning to '(?P.+)' from '(?P.+)' # 如果你想仅在系统语言设定为中文(zh_CN)时应用这个替换规则,你可以使用"locale:zh_CN" # 使用"locale:default"时不会添加系统语言限制 locale:default \g提示: \g'\g'从不兼容的指针类型赋值为'\g',两者怎么都……都说不过去!^^; - [/substitute_regex] - [substitute_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)error: (?P({{ESC}}.*?m)*)unknown type name '(?P.+)' + [/subst_regex] + [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)error: (?P({{ESC}}.*?m)*)unknown type name '(?P.+)' locale:default \g错误!: \g未知的类型名'\g',忘记定义了~ಥ_ಥ - [/substitute_regex] + [/subst_regex] {/substrules_section} ``` diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index ebd10cd..b6fd8c1 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -88,13 +88,13 @@ def handle_substrules_section(obj: _parser_handlers.GeneratorObject, first_phras obj.check_extra_args(phrases, 1, use_exact_count=True) reset_outline_foregroundonly() command_filters=None - elif phrases[0] in ("[substitute_string]", "[substitute_regex]"): + elif phrases[0] in ("[subst_string]", "[substitute_string]", "[subst_regex]", "[substitute_regex]"): obj.check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), - "is_regex": phrases[0]=="[substitute_regex]", + "is_regex": phrases[0] in ("[subst_regex]", "[substitute_regex]"), "strictness": command_filter_strictness} match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) - obj.handle_entry(match_pattern, start_phrase=phrases[0], end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) + obj.handle_entry(match_pattern, start_phrase=phrases[0], end_phrase=phrases[0].replace('[', '[/'), is_substrules=True, substrules_options=options) elif obj.handle_setters(): pass elif phrases[0]==end_phrase: obj.check_extra_args(phrases, 1, use_exact_count=True) -- Gitee From 858d76db11bd042e254a8113054f902161f04109 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 16 Dec 2024 23:54:15 +0800 Subject: [PATCH 101/122] Only catch `re.error` in pattern validity checking - Only display the error message only when the error comes from the `re` module --- src/clitheme/_generator/_entry_block_handler.py | 2 +- src/clitheme/_generator/db_interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index 08b01c7..bb59907 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -34,7 +34,7 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su def check_valid_pattern(pattern: str, debug_linenumber: Union[str, int]=self.lineindex+1): # check if patterns are valid try: re.compile(pattern) - except: self.handle_error(self.fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(debug_linenumber), error_msg=sys.exc_info()[1])) + except re.error: self.handle_error(self.fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(debug_linenumber), error_msg=sys.exc_info()[1])) while self.goto_next_line(): phrases=self.lines_data[self.lineindex].split() line_content=self.lines_data[self.lineindex] diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 376d51d..1e70c71 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -71,7 +71,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma if unique_id==uuid.UUID(int=0): unique_id=uuid.uuid4() cmdlist: List[str]=[] try: re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid - except: raise bad_pattern(str(sys.exc_info()[1])) + except re.error: raise bad_pattern(str(sys.exc_info()[1])) # handle condition where no effective_locale is specified ("default") locale_condition="AND effective_locale=?" if effective_locale!=None else "AND typeof(effective_locale)=typeof(?)" insert_values=["match_pattern", "substitute_pattern", "effective_command", "is_regex", "command_match_strictness", "end_match_here", "effective_locale", "stdout_stderr_only", "unique_id", "foreground_only", "file_id"] -- Gitee From 19acf21eca5c0d20b07189f26892d67412491640 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 17 Dec 2024 15:19:47 +0800 Subject: [PATCH 102/122] Improve `get-current-theme-info` command - Add `--name` and `--file-path` options to simplify the outputs - Themes are no longer displayed in reverse order --- docs/clitheme.1 | 11 ++++- src/clitheme/cli.py | 43 ++++++++++++------- .../strings/cli-strings.clithemedef.txt | 20 ++++++--- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/docs/clitheme.1 b/docs/clitheme.1 index c64e757..dde2cb0 100644 --- a/docs/clitheme.1 +++ b/docs/clitheme.1 @@ -1,4 +1,4 @@ -.TH clitheme 1 2024-06-17 +.TH clitheme 1 2024-12-17 .SH NAME clitheme \- frontend to customize output of applications .SH SYNOPSIS @@ -14,8 +14,15 @@ Specify \fB--overlay\fR to append value definitions in the file(s) onto the curr Specify \fB--preserve-temp\fR to prevent the temporary directory from removed after the operation. .TP -.B get-current-theme-info +.B get-current-theme-info [--name] [--file-path] Outputs detailed information about the currently applied theme. If multiple themes are applied using the \fB--overlay\fR option, outputs detailed information for each applied theme. + +Specify \fB--name\fR to only display the name of each theme. + +Specify \fB--file-path\fR to only display the source file path of each theme. + +(Both will be displayed when both specified) + .TP .B unset-current-theme Removes the current theme data from the system. Supported applications will immediately stop using the defined values after this operation. diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 6bbb263..7a4be7f 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -178,9 +178,13 @@ def unset_current_theme(): print(f.reof("remove-data-success", "Successfully removed the current theme data")) return 0 -def get_current_theme_info(): +def get_current_theme_info(name: bool=False, file_path=False): """ - Get the current theme info + Displays the current theme info + + - Set name=True to only display the name of each theme + - Set file_path=True to only display the source file path of each theme + - Both information are displayed when both options are set to True (Invokes 'clitheme get-current-theme-info') """ @@ -190,23 +194,28 @@ def get_current_theme_info(): print(f.reof("no-theme", "No theme currently set")) return 1 lsdir_result=_globalvar.list_directory(search_path) - lsdir_result.sort(reverse=True, key=functools.cmp_to_key(_globalvar.result_sort_cmp)) # sort by latest installed + lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 - if lsdir_num<=1: - print(f.reof("current-theme-msg", "Currently installed theme:")) - else: - print(f.reof("overlay-history-msg", "Overlay history (sorted by latest installed):")) + print(f.reof("current-theme-msg", "Currently installed theme(s):")) + minimal_info: bool=name==True or file_path==True for theme_pathname in lsdir_result: target_path=search_path+"/"+theme_pathname.strip() if (not os.path.isdir(target_path)) or re.search(r"^\d+$", theme_pathname.strip())==None: continue # skip current_theme_index file # name - name="(Unknown)" - if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="name")): - name=open(target_path+"/"+_globalvar.generator_info_filename.format(info="name"), 'r', encoding="utf-8").read().strip() - print("[{}]: {}".format(theme_pathname, name)) + if minimal_info==False or (minimal_info==True and name==True): + theme_name="(Unknown)" + if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="name")): + theme_name=open(target_path+"/"+_globalvar.generator_info_filename.format(info="name"), 'r', encoding="utf-8").read().strip() + print("[{}]: {}".format(theme_pathname, theme_name)) + if minimal_info==True and file_path==True: + theme_filepath="(Unknown)" + if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="filepath")): + theme_filepath=open(target_path+"/"+_globalvar.generator_info_filename.format(info="filepath"), 'r', encoding="utf-8").read().strip() + print(theme_filepath) + if minimal_info==True: continue # --Stop here if either parameters are specified-- # version version="(Unknown)" if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="version")): @@ -303,7 +312,7 @@ def _handle_help_message(full_help: bool=False): print(fd.reof("usage-str", "Usage:")) print( """\t{0} apply-theme [themedef-file] [--overlay] [--preserve-temp] -\t{0} get-current-theme-info +\t{0} get-current-theme-info [--name] [--file-path] \t{0} unset-current-theme \t{0} update-theme \t{0} generate-data [themedef-file] [--overlay] @@ -314,7 +323,7 @@ def _handle_help_message(full_help: bool=False): print(fd.reof("options-str", "Options:")) print("\t"+fd.reof("options-apply-theme", "apply-theme: Apply the given theme definition file(s).\nSpecify --overlay to add file(s) onto the current data.\nSpecify --preserve-temp to preserve the temporary directory after the operation. (Debug purposes only)").replace("\n", "\n\t\t")) - print("\t"+fd.reof("options-get-current-theme-info", "get-current-theme-info: Show information about the currently applied theme(s)")) + print("\t"+fd.reof("options-get-current-theme-info", "get-current-theme-info: Show information about the currently applied theme(s)\nSpecify --name to only display the name of each theme\nSpecify --file-path to only display the source file path of each theme\n(Both will be displayed when both specified)").replace("\n", "\n\t\t")) print("\t"+fd.reof("options-unset-current-theme", "unset-current-theme: Remove the current theme data from the system")) print("\t"+fd.reof("options-update-theme", "update-theme: Re-apply the theme definition files specified in the previous \"apply-theme\" command (previous commands if --overlay is used)")) print("\t"+fd.reof("options-generate-data", "generate-data: [Debug purposes only] Generate a data hierarchy from specified theme definition files in a temporary directory")) @@ -380,8 +389,12 @@ def main(cli_args: List[str]): paths.append(arg) return apply_theme(file_contents=None, overlay=overlay, filenames=paths, preserve_temp=preserve_temp, generate_only=generate_only) elif cli_args[1]=="get-current-theme-info": - check_extra_args(2) # disabled additional options - return get_current_theme_info() + name=False; file_path=False + for arg in cli_args[2:]: + if arg.strip()=="--name": name=True + elif arg.strip()=="--file-path": file_path=True + else: return _handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=fmt(arg)), arg_first) + return get_current_theme_info(name=name, file_path=file_path) elif cli_args[1]=="unset-current-theme": check_extra_args(2) return unset_current_theme() diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 3dd2286..6fc03e6 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -174,13 +174,9 @@ in_domainapp swiftycode clitheme locale:zh_CN 当前没有设定任何主题 [/entry] [entry] current-theme-msg - # locale:default Currently installed theme: + # locale:default Currently installed theme(s): locale:zh_CN 当前设定的主题: [/entry] - [entry] overlay-history-msg - # locale:default Overlay history (sorted by latest installed): - locale:zh_CN 叠加历史记录(根据最新安装的主题排序): - [/entry] [entry] version-str # locale:default Version: {ver} locale:zh_CN 版本:{ver} @@ -248,8 +244,18 @@ in_domainapp swiftycode clitheme [/locale] [/entry] [entry] options-get-current-theme-info - # locale:default get-current-theme-info: Show information about the currently applied theme(s) - locale:zh_CN get-current-theme-info:显示当前主题设定的详细信息 + # [locale] default + # get-current-theme-info: Show information about the currently applied theme(s) + # Specify --name to only display the name of each theme + # Specify --file-path to only display the source file path of each theme + # (Both will be displayed when both specified) + # [/locale] + [locale] zh_CN + get-current-theme-info:显示当前主题设定的详细信息 + 指定"--name"以仅显示每个主题的名称 + 指定"--file-path"以仅显示每个主题的源文件路径 + (同时指定时,两者都会显示) + [/locale] [/entry] [entry] options-unset-current-theme # locale:default unset-current-theme: Remove the current theme data from the system -- Gitee From 716ae7ad5cee4ef2327e07c8ab2d1cbe0bff4623 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 17 Dec 2024 23:10:00 +0800 Subject: [PATCH 103/122] Update version (v2.0-dev20241217) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 8bb7b37..7b3b9d5 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241130 +pkgver=2.0_dev20241217 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 1af0ea1..2bcb66e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241130-1) unstable; urgency=low +clitheme (2.0-dev20241217-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 30 Nov 2024 23:08:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 17 Dec 2024 23:08:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index e5baea1..6648c83 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241130" +__version__="2.0-dev20241217" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241130" +version_main="2.0_dev20241217" version_buildnumber=1 \ No newline at end of file -- Gitee From 9b2e5d3bb7d519d09c04bbd03542c22972c88d51 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 18 Dec 2024 20:35:06 +0800 Subject: [PATCH 104/122] Fix user input reading in piped processes --- src/clitheme/exec/output_handler_posix.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 8c45879..1508c51 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -122,7 +122,11 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) select.select([sys.stdin], [], []) d=os.read(sys.stdin.fileno(), readsize) if d==b'': # stdin is closed - os.close(w); break + os.close(w) + # Duplicate stdout terminal onto stdin to read user input + if os.isatty(sys.stdout.fileno()): + os.dup2(sys.stdout.fileno(), sys.stdin.fileno()) + break os.write(w,d) t=threading.Thread(target=pipe_forward, daemon=True) t.start() -- Gitee From 162f32fb1b0b8445cd1193832ab441d370f68118 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 19 Dec 2024 00:19:01 +0800 Subject: [PATCH 105/122] Set initial terminal attributes before handling any output --- src/clitheme/exec/output_handler_posix.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 1508c51..f3dfbf1 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -294,6 +294,13 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) signal.signal(signal.SIGWINCH, update_window_size) # Call this function for the first time to set initial window size update_window_size() + # Initially set terminal attributes + try: + term_attrs=termios.tcgetattr(stdout_fd) + # disable canonical and echo mode (enable cbreak) no matter what + term_attrs[3] &= ~(termios.ICANON | termios.ECHO) + termios.tcsetattr(sys.stdout, termios.TCSADRAIN, term_attrs) + except termios.error: pass while True: try: if not thread.is_alive() and not process.poll()!=None: -- Gitee From 58300792dfb83dcfce0092fe1c6cc7875618d183 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 19 Dec 2024 10:23:39 +0800 Subject: [PATCH 106/122] Unset all signal handlers before exit Prevent repeated signal handling operations --- src/clitheme/exec/output_handler_posix.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index f3dfbf1..fa74f6f 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -110,6 +110,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) elif sig==signal.SIGQUIT: if process.poll()==None: os.write(stdout_fd, b'\x1c') # '^\' character + handle_signals=[signal.SIGTSTP, signal.SIGCONT, signal.SIGINT, signal.SIGQUIT] try: # Detect if stdin is piped (e.g. cat file|clitheme-exec grep content) stdin_fd=stdout_slave @@ -147,7 +148,6 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) _globalvar.handle_exception() return 1 else: - handle_signals=[signal.SIGTSTP, signal.SIGCONT, signal.SIGINT, signal.SIGQUIT] for sig in handle_signals: signal.signal(sig, signal_handler) output_lines=queue.Queue() # (line_content, is_stderr, do_subst_operation, foreground_pid, term_attrs) @@ -357,7 +357,9 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) exit_code=process.poll() try: if exit_code!=None and exit_code<0: # Terminated by signal - signal.signal(signal.SIGQUIT, signal.SIG_DFL) # Unset SIGQUIT custom handler + # Block signal handlers before the kill operation to prevent unexpected behavior + for sig in handle_signals: + signal.signal(sig, signal.SIG_IGN) os.kill(os.getpid(), abs(exit_code)) except: pass return exit_code -- Gitee From dd64588f5a28ef767ff6992be1474a69f9cdd856 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 19 Dec 2024 12:06:41 +0800 Subject: [PATCH 107/122] Minimize delay in `--foreground-stat` output when no output arrives in time --- src/clitheme/exec/output_handler_posix.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index fa74f6f..cc60cd2 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -301,6 +301,8 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) term_attrs[3] &= ~(termios.ICANON | termios.ECHO) termios.tcsetattr(sys.stdout, termios.TCSADRAIN, term_attrs) except termios.error: pass + # If had output on the previous run, use shorter timeout to minimize delay in --foreground-stat output + had_output=False while True: try: if not thread.is_alive() and not process.poll()!=None: @@ -333,8 +335,12 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) return subst_line if output_lines.empty(): handle_debug_pgrp(os.tcgetpgrp(stdout_fd)) - try: line_data=output_lines.get(block=True, timeout=0.5) - except queue.Empty: continue + try: line_data=output_lines.get(block=True, timeout=0.05 if had_output else 0.5) + except queue.Empty: + had_output=False + continue + # --Output processing-- + had_output=True # None: termination signal if line_data==None: break # Process output line by line -- Gitee From e1fff65b47d25953154e2b2da28709224b7b5746 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 19 Dec 2024 12:59:01 +0800 Subject: [PATCH 108/122] Support specifying `--yes` in supported commands Allows the user to skip the confirmation prompt --- docs/clitheme.1 | 12 +++-- src/clitheme/cli.py | 52 +++++++++++-------- .../strings/cli-strings.clithemedef.txt | 4 ++ 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/clitheme.1 b/docs/clitheme.1 index dde2cb0..e866fd4 100644 --- a/docs/clitheme.1 +++ b/docs/clitheme.1 @@ -1,4 +1,4 @@ -.TH clitheme 1 2024-12-17 +.TH clitheme 1 2024-12-19 .SH NAME clitheme \- frontend to customize output of applications .SH SYNOPSIS @@ -7,12 +7,15 @@ clitheme \- frontend to customize output of applications \fIclitheme\fR is a framework for applications that allows users to customize its output and messages through theme definition files. This CLI interface allows the user to control their current settings and theme definition on the system. .SH OPTIONS .TP -.B apply-theme [themedef-file(s)] [--overlay] [--preserve-temp] +.B apply-theme [themedef-file(s)] [--overlay] [--preserve-temp] [--yes] Applies the given theme definition file(s) into the current system. Supported applications will immediately start using the defined values after performing this operation. Specify \fB--overlay\fR to append value definitions in the file(s) onto the current data. Specify \fB--preserve-temp\fR to prevent the temporary directory from removed after the operation. + +Specify \fB--yes\fR to skip the confirmation prompt. + .TP .B get-current-theme-info [--name] [--file-path] Outputs detailed information about the currently applied theme. If multiple themes are applied using the \fB--overlay\fR option, outputs detailed information for each applied theme. @@ -27,10 +30,13 @@ Specify \fB--file-path\fR to only display the source file path of each theme. .B unset-current-theme Removes the current theme data from the system. Supported applications will immediately stop using the defined values after this operation. .TP -.B update-theme +.B update-theme [--yes] Re-applies the theme definition files specified in the previous "apply-theme" command (previous commands if \fB--overlay\fR is used) Calling this command is equivalent to calling "clitheme apply-theme" with previously-specified files. + +Specify \fB--yes\fR to skip the confirmation prompt. + .TP .B generate-data [themedef-file(s)] [--overlay] Generates the data hierarchy for the given theme definition file(s). This operation generates the same data as \fBapply-theme\fR, but does not apply it onto the system. diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 7a4be7f..aae1c7a 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -30,7 +30,7 @@ frontend.set_appname("clitheme") frontend.set_subsections("cli") last_data_path="" -def apply_theme(file_contents: Optional[List[str]], filenames: List[str], overlay: bool=False, preserve_temp=False, generate_only=False): +def apply_theme(file_contents: Optional[List[str]], filenames: List[str], overlay: bool=False, preserve_temp=False, generate_only=False, no_confirm=False): """ Apply the theme using the provided definition file contents and file pathnames in a list object. @@ -40,6 +40,7 @@ def apply_theme(file_contents: Optional[List[str]], filenames: List[str], overla - Set overlay=True to overlay the theme on top of existing theme[s] - Set preserve_temp=True to preserve the temp directory (debugging purposes) - Set generate_only=True to generate the data hierarchy only (invokes 'clitheme generate-data' instead) + - Set no_confirm=True to skip the user confirmation prompt """ if file_contents==None: try: file_contents=_get_file_contents(filenames) @@ -50,19 +51,20 @@ def apply_theme(file_contents: Optional[List[str]], filenames: List[str], overla if len(filenames)>0 and len(file_contents)!=len(filenames): # unlikely to happen raise ValueError("file_contents and filenames have different lengths") f=frontend.FetchDescriptor(subsections="cli apply-theme") - if len(filenames)>1 or True: # currently set to True for now - if generate_only: - print(f.reof("generate-data-msg", "The theme data will be generated from the following definition files in the following order:")) - else: - print(f.reof("apply-theme-msg", "The following definition files will be applied in the following order: ")) - for i in range(len(filenames)): - path=filenames[i] - print("\t{}: {}".format(str(i+1), fmt(path))) - if not generate_only: - if os.path.isdir(_globalvar.clitheme_root_data_path) and overlay==False: - print(f.reof("overwrite-notice", "The existing theme data will be overwritten if you continue.")) - if overlay==True: - print(f.reof("overlay-notice", "The definition files will be appended on top of the existing theme data.")) + # Display information and confirmation prompt + if generate_only: + print(f.reof("generate-data-msg", "The theme data will be generated from the following definition files in the following order:")) + else: + print(f.reof("apply-theme-msg", "The following definition files will be applied in the following order: ")) + for i in range(len(filenames)): + path=filenames[i] + print("\t{}: {}".format(str(i+1), fmt(path))) + if not generate_only: + if os.path.isdir(_globalvar.clitheme_root_data_path) and overlay==False: + print(f.reof("overwrite-notice", "The existing theme data will be overwritten if you continue.")) + if overlay==True: + print(f.reof("overlay-notice", "The definition files will be appended on top of the existing theme data.")) + if not no_confirm: inpstr=f.reof("confirm-prompt", "Do you want to continue? [y/n]") try: inp=input(inpstr+" ").strip().lower() except (KeyboardInterrupt, EOFError): print();return 130 @@ -258,10 +260,12 @@ def get_current_theme_info(name: bool=False, file_path=False): print() # Separate each entry with an empty line return 0 -def update_theme(): +def update_theme(no_confirm=False): """ Re-applies theme files from file paths specified in the previous apply-theme command (including all related apply-theme commands if --overlay is used) + - Set no_confirm=True to skip the user confirmation prompt + (Invokes 'clitheme update-theme') """ class invalid_theme(Exception): pass @@ -297,7 +301,7 @@ def update_theme(): print(fi.feof("other-err", "An error occurred while processing file path information: {msg}\nPlease re-apply the current theme and try again", msg=fmt(str(sys.exc_info()[1])))) _globalvar.handle_exception() return 1 - return apply_theme(None, file_paths, overlay=False) + return apply_theme(None, file_paths, overlay=False, no_confirm=no_confirm) def _is_option(arg): return arg.strip()[0:1]=="-" @@ -311,10 +315,10 @@ def _handle_help_message(full_help: bool=False): fd=frontend.FetchDescriptor(subsections="cli help-message") print(fd.reof("usage-str", "Usage:")) print( -"""\t{0} apply-theme [themedef-file] [--overlay] [--preserve-temp] +"""\t{0} apply-theme [themedef-file] [--overlay] [--preserve-temp] [--yes] \t{0} get-current-theme-info [--name] [--file-path] \t{0} unset-current-theme -\t{0} update-theme +\t{0} update-theme [--yes] \t{0} generate-data [themedef-file] [--overlay] \t{0} --version \t{0} --help""".format(arg_first) @@ -327,6 +331,7 @@ def _handle_help_message(full_help: bool=False): print("\t"+fd.reof("options-unset-current-theme", "unset-current-theme: Remove the current theme data from the system")) print("\t"+fd.reof("options-update-theme", "update-theme: Re-apply the theme definition files specified in the previous \"apply-theme\" command (previous commands if --overlay is used)")) print("\t"+fd.reof("options-generate-data", "generate-data: [Debug purposes only] Generate a data hierarchy from specified theme definition files in a temporary directory")) + print("\t"+fd.reof("options-yes", "[For supported commands, specify --yes to skip the confirmation prompt]")) print("\t"+fd.reof("options-version", "--version: Show the current version of clitheme")) print("\t"+fd.reof("options-help", "--help: Show this help message")) @@ -380,14 +385,16 @@ def main(cli_args: List[str]): paths=[] overlay=False preserve_temp=False + no_confirm=False for arg in cli_args[2:]: if _is_option(arg): if arg.strip()=="--overlay": overlay=True elif arg.strip()=="--preserve-temp" and not generate_only: preserve_temp=True + elif arg.strip()=="--yes" and not generate_only: no_confirm=True else: return _handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=fmt(arg)), arg_first) else: paths.append(arg) - return apply_theme(file_contents=None, overlay=overlay, filenames=paths, preserve_temp=preserve_temp, generate_only=generate_only) + return apply_theme(file_contents=None, overlay=overlay, filenames=paths, preserve_temp=preserve_temp, generate_only=generate_only, no_confirm=no_confirm) elif cli_args[1]=="get-current-theme-info": name=False; file_path=False for arg in cli_args[2:]: @@ -399,8 +406,11 @@ def main(cli_args: List[str]): check_extra_args(2) return unset_current_theme() elif cli_args[1]=="update-theme": - check_extra_args(2) - return update_theme() + no_confirm=False + for arg in cli_args[2:]: + if arg.strip()=="--yes": no_confirm=True + else: return _handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=fmt(arg)), arg_first) + return update_theme(no_confirm=no_confirm) elif cli_args[1]=="--version": check_extra_args(2) print(f.feof("version-str", "clitheme version {ver}", ver=_globalvar.clitheme_version)) diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 6fc03e6..314c8f8 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -269,6 +269,10 @@ in_domainapp swiftycode clitheme # locale:default generate-data: [Debug purposes only] Generate a data hierarchy from specified theme definition files in a temporary directory locale:zh_CN generate-data:【仅供调试用途】对于指定的主题定义文件在临时目录中生成一个数据结构 [/entry] + [entry] options-yes + # locale:default [For supported commands, specify --yes to skip the confirmation prompt] + locale:zh_CN 【在支持的指令中,指定"--yes"以跳过确认提示】 + [/entry] [entry] options-version # locale:default --version: Show the current version of clitheme locale:zh_CN --version:显示clitheme的当前版本信息 -- Gitee From 8416156b90545867d17fe6ae9813ed4635891e58 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 19 Dec 2024 13:04:02 +0800 Subject: [PATCH 109/122] Small grammar fixes in manpage documentation --- docs/clitheme.1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/clitheme.1 b/docs/clitheme.1 index e866fd4..14ad49a 100644 --- a/docs/clitheme.1 +++ b/docs/clitheme.1 @@ -7,12 +7,12 @@ clitheme \- frontend to customize output of applications \fIclitheme\fR is a framework for applications that allows users to customize its output and messages through theme definition files. This CLI interface allows the user to control their current settings and theme definition on the system. .SH OPTIONS .TP -.B apply-theme [themedef-file(s)] [--overlay] [--preserve-temp] [--yes] +.B apply-theme [themedef-file] [--overlay] [--preserve-temp] [--yes] Applies the given theme definition file(s) into the current system. Supported applications will immediately start using the defined values after performing this operation. -Specify \fB--overlay\fR to append value definitions in the file(s) onto the current data. +Specify \fB--overlay\fR to to add file(s) onto the current data. -Specify \fB--preserve-temp\fR to prevent the temporary directory from removed after the operation. +Specify \fB--preserve-temp\fR to preserve the temporary directory after the operation. (Debug purposes only) Specify \fB--yes\fR to skip the confirmation prompt. @@ -38,7 +38,7 @@ Calling this command is equivalent to calling "clitheme apply-theme" with previo Specify \fB--yes\fR to skip the confirmation prompt. .TP -.B generate-data [themedef-file(s)] [--overlay] +.B generate-data [themedef-file] [--overlay] Generates the data hierarchy for the given theme definition file(s). This operation generates the same data as \fBapply-theme\fR, but does not apply it onto the system. This command is for debug purposes only. -- Gitee From cf57e246026d771033b797d146c429649ba76de0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 19 Dec 2024 22:32:19 +0800 Subject: [PATCH 110/122] Fix get caller mechanism in frontend Handle situations where `stack[3]` or more may be some function in frontend --- src/clitheme/frontend.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index f0359bb..5c88d91 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -44,6 +44,11 @@ def _get_caller() -> str: assert len(inspect.stack())>=4, "Cannot determine filename from call stack" # inspect.stack(): [0: this function, 1: update/get settings, 2: function in frontend module, 3: target calling function] filename=inspect.stack()[3].filename + # Find the first function in the stack OUTSIDE of frontend module + # (The stack[3] may also be some function in frontend) + for s in inspect.stack()[3:]: + filename=s.filename + if filename!=__file__: break return filename def _update_local_settings(key: str, value: Union[None,str,bool]): @@ -137,7 +142,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: path_name=_globalvar.clitheme_temp_root+"/"+dir_name if _alt_path_dirname!=None and overlay==True: # overlay if not os.path.exists(path_name): shutil.copytree(_globalvar.clitheme_temp_root+"/"+_alt_path_dirname, _generator.path) - if _get_setting("debugmode"): print("[Debug] "+path_name) + if _get_setting("debugmode"): print("[Debug] set_local_themedef data path: "+path_name) # Generate data hierarchy as needed if not os.path.exists(path_name): _generator.silence_warn=True -- Gitee From b4008f45b7e1805f28b32dfc77393204201f4594 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 19 Dec 2024 22:38:28 +0800 Subject: [PATCH 111/122] Optimize error handling and displaying in `handle_set_themedef` --- src/clitheme/_globalvar.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 968ebd2..eb83891 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -214,8 +214,10 @@ def handle_set_themedef(fr: frontend, debug_name: str): # type: ignore except: sys.stdout=orig_stdout fr.set_debugmode(prev_mode) - if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1]), file=sys.__stdout__) - handle_exception() + # If pre-release build or manual environment variable flag set, display error + if _version.release<0 or os.environ.get("CLITHEME_SHOW_TRACEBACK")=='1': + print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1]), file=sys.__stdout__) + handle_exception() finally: sys.stdout=orig_stdout def result_sort_cmp(obj1,obj2) -> int: cmp1='';cmp2='' -- Gitee From e5e5af02e2389cb00e304ed85c90e332dc84f11e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 23 Dec 2024 23:35:13 +0800 Subject: [PATCH 112/122] Update version (v2.0-dev20241223) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 7b3b9d5..b6e2c5c 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241217 +pkgver=2.0_dev20241223 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 2bcb66e..5d9cb81 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241217-1) unstable; urgency=low +clitheme (2.0-dev20241223-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Tue, 17 Dec 2024 23:08:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, 23 Dec 2024 23:29:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 6648c83..91e0f2e 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241217" +__version__="2.0-dev20241223" major=2 minor=0 release=-1 # -1 stands for "dev" beta_release=2 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241217" +version_main="2.0_dev20241223" version_buildnumber=1 \ No newline at end of file -- Gitee From 903917e762c1ef9b653f8213f156a59f2d588f66 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 26 Dec 2024 14:09:09 +0800 Subject: [PATCH 113/122] Improve signal return code handling --- src/clitheme/exec/output_handler_posix.py | 2 ++ src/clitheme/man.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index cc60cd2..fb2d36c 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -367,5 +367,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) for sig in handle_signals: signal.signal(sig, signal.SIG_IGN) os.kill(os.getpid(), abs(exit_code)) + # Properly return exit code for corresponding signals + return 128+abs(exit_code) except: pass return exit_code diff --git a/src/clitheme/man.py b/src/clitheme/man.py index dc1e8c5..941fdc3 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -64,11 +64,13 @@ def main(args: List[str]): except KeyboardInterrupt: process.send_signal(signal.SIGINT) return process.poll() # type: ignore returncode=run_process(env) - if returncode!=0 and returncode != -signal.SIGINT and theme_set: + # Return code is negative when exited due to signal + if returncode>0 and theme_set: _labeled_print(fd.reof("prev-command-fail", "Executing \"man\" with custom path failed, trying execution with normal settings")) env["MANPATH"]=prev_manpath if prev_manpath!=None else '' returncode=run_process(os.environ) - return returncode + # If return code is a signal, handle the exit code properly + return 128+abs(returncode) if returncode<0 else returncode def _script_main(): # for script return main(sys.argv) -- Gitee From 0678e4ff76d2c2caca3af0d64fc15f179ebe069f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 26 Dec 2024 23:45:06 +0800 Subject: [PATCH 114/122] Improve no database file handling - Check db existence before attempting to update cache - Use dedicated exception --- src/clitheme/_generator/db_interface.py | 9 ++++----- src/clitheme/exec/output_handler_posix.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 1e70c71..c585c99 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -25,10 +25,9 @@ db_path="" debug_mode=False fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") -class need_db_regenerate(Exception): - pass -class bad_pattern(Exception): - pass +class need_db_regenerate(Exception): pass +class bad_pattern(Exception): pass +class db_not_found(Exception): pass def _handle_warning(message: str): if debug_mode: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) @@ -122,12 +121,12 @@ def _is_db_updated() -> bool: def _fetch_matches(command: Optional[str]) -> List[tuple]: global _matches_cache updated=_is_db_updated() + if _db_last_state==None: raise db_not_found("file at db_path does not exist") if updated or _matches_cache.get(command)==None: if updated: _matches_cache.clear() gc.collect() # Reduce memory leak _matches_cache[command]=_get_matches(command) - if _db_last_state==None: raise sqlite3.OperationalError("file at db_path does not exist") return _matches_cache[command] ## Output processing and matching diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index fb2d36c..7283823 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -324,7 +324,7 @@ def handler_main(command: List[str], debug_mode: List[str]=[], subst: bool=True) subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1], pids=(process.pid, foreground_pid)) except TimeoutError: failed=True # Happens when no theme is set/no subst-data.db - except sqlite3.OperationalError: pass + except db_interface.db_not_found: pass def raise_error(sig_num, frame): raise TimeoutError("Execution time out") signal.signal(signal.SIGALRM, raise_error) signal.setitimer(signal.ITIMER_REAL, db_interface.match_timeout) -- Gitee From bd4b9d615cd7591b2dbe455e9009f34daa453944 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 27 Dec 2024 11:33:02 +0800 Subject: [PATCH 115/122] Catch all exceptions in regex pattern testing --- src/clitheme/_generator/_entry_block_handler.py | 2 +- src/clitheme/_generator/db_interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_entry_block_handler.py b/src/clitheme/_generator/_entry_block_handler.py index bb59907..08b01c7 100644 --- a/src/clitheme/_generator/_entry_block_handler.py +++ b/src/clitheme/_generator/_entry_block_handler.py @@ -34,7 +34,7 @@ def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_su def check_valid_pattern(pattern: str, debug_linenumber: Union[str, int]=self.lineindex+1): # check if patterns are valid try: re.compile(pattern) - except re.error: self.handle_error(self.fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(debug_linenumber), error_msg=sys.exc_info()[1])) + except: self.handle_error(self.fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(debug_linenumber), error_msg=sys.exc_info()[1])) while self.goto_next_line(): phrases=self.lines_data[self.lineindex].split() line_content=self.lines_data[self.lineindex] diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index c585c99..157196c 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -70,7 +70,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma if unique_id==uuid.UUID(int=0): unique_id=uuid.uuid4() cmdlist: List[str]=[] try: re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid - except re.error: raise bad_pattern(str(sys.exc_info()[1])) + except: raise bad_pattern(str(sys.exc_info()[1])) # handle condition where no effective_locale is specified ("default") locale_condition="AND effective_locale=?" if effective_locale!=None else "AND typeof(effective_locale)=typeof(?)" insert_values=["match_pattern", "substitute_pattern", "effective_command", "is_regex", "command_match_strictness", "end_match_here", "effective_locale", "stdout_stderr_only", "unique_id", "foreground_only", "file_id"] -- Gitee From d5305c6bb26eb8374b3de12ce60bb318f201fe6a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 31 Dec 2024 11:55:03 +0800 Subject: [PATCH 116/122] Update version (v2.0-dev20241227) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index b6e2c5c..fccd8ec 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241223 +pkgver=2.0_dev20241227 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 5d9cb81..d49a15e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20241223-1) unstable; urgency=low +clitheme (2.0-dev20241227-1) unstable; urgency=low * In development, see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 23 Dec 2024 23:29:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 31 Dec 2024 11:54:00 +0800 clitheme (2.0-beta2-1) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 91e0f2e..ce260a8 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,7 +11,7 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241223" +__version__="2.0-dev20241227" major=2 minor=0 release=-1 # -1 stands for "dev" -- Gitee From 303c7fff32e1541a94809750839f325e47ce5e44 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 Jan 2025 13:39:44 +0800 Subject: [PATCH 117/122] Fix allowed options processing in `handle_block_input` --- src/clitheme/_generator/_parser_handlers.py | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index 6b22837..24ba1b3 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -97,8 +97,10 @@ class GeneratorObject(_data_handlers.DataHandlers): req_ver=self.fmt(version_str)), not_syntax_error=True) def handle_invalid_phrase(self, name: str): self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=self.fmt(name), num=str(self.lineindex+1))) - def parse_options(self, options_data: List[str], merge_global_options: int, allowed_options: Optional[List[str]]=None) -> Dict[str, Union[int,bool]]: + def parse_options(self, options_data: List[str], merge_global_options: int, allowed_options: Optional[List[str]]=None, ban_options: Optional[List[str]]=None) -> Dict[str, Union[int,bool]]: # merge_global_options: 0 - Don't merge; 1 - Merge self.global_options; 2 - Merge self.really_really_global_options + assert not (allowed_options!=None and ban_options!=None), "Cannot specify allowed and banned options at the same time" + final_options={} if merge_global_options!=0: final_options=copy.copy(self.global_options if merge_global_options==1 else self.really_really_global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options @@ -133,7 +135,8 @@ class GeneratorObject(_data_handlers.DataHandlers): break else: # executed when no break occurs self.handle_error(self.fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=self.fmt(option_name_preserve_no))) - if allowed_options!=None and option_name not in allowed_options: + if (allowed_options!=None and option_name not in allowed_options) or\ + (ban_options!=None and option_name in ban_options): self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=self.fmt(option_name))) return final_options def handle_set_global_options(self, options_data: List[str], really_really_global: bool=False): @@ -250,7 +253,7 @@ class GeneratorObject(_data_handlers.DataHandlers): ## sub-block processing functions - def handle_block_input(self, preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_other_options: bool=True, disable_substesc: bool=False) -> str: + def handle_block_input(self, preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str, disallow_other_options: bool=True, disable_substesc: bool=False) -> str: minspaces=math.inf blockinput_data="" begin_line_number=self.lineindex+1+1 @@ -287,17 +290,21 @@ class GeneratorObject(_data_handlers.DataHandlers): blockinput_data=re.sub(pattern,r"\g", blockinput_data, flags=re.MULTILINE) # parse leadtabindents, leadspaces, substesc, and substvar options here got_options=copy.copy(self.global_options) - specified_options={} if len(self.lines_data[self.lineindex].split())>1: + # Process allowed/banned options + ban_options=None; allowed_options=None + if not disallow_other_options: + ban_options=[] + if not preserve_indents: ban_options+=self.lead_indent_options + if disable_substesc: ban_options+=["substesc"] + else: + allowed_options=[] + if preserve_indents: allowed_options+=self.lead_indent_options + if not disable_substesc: allowed_options+=["substesc"] + allowed_options+=["substvar"] got_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=True, - allowed_options=\ - None if not disallow_other_options else\ - (self.lead_indent_options if preserve_indents else []+\ - ["substesc"] if not disable_substesc else []+\ - ["substvar"]) - ) - specified_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=False) + allowed_options=allowed_options, ban_options=ban_options) debug_linenumber=self.handle_linenumber_range(begin_line_number, self.lineindex+1-1) if preserve_indents and got_options.get("leadtabindents")!=None: blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) -- Gitee From 5812666978107c082f63d7935c52085186f3d0ef Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 Jan 2025 16:57:14 +0800 Subject: [PATCH 118/122] Disallow specifying bugfix info in `!require_version` - Improve interoperability with other release variants where incompatible bugfix revisions are used --- src/clitheme/_generator/_parser_handlers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/_parser_handlers.py b/src/clitheme/_generator/_parser_handlers.py index 24ba1b3..420e8ca 100644 --- a/src/clitheme/_generator/_parser_handlers.py +++ b/src/clitheme/_generator/_parser_handlers.py @@ -79,7 +79,9 @@ class GeneratorObject(_data_handlers.DataHandlers): if not_pass: self.handle_error(self.fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=self.fmt(phrases[0]))) def check_version(self, version_str: str): - match_result=re.match(r"^(?P\d+)\.(?P\d+)(\.(?P\d+))?(-beta(?P\d+))?$", version_str) + # allow_bugfix is disabled to allow interoperability with other release variants + allow_bugfix: bool=False # Whether to allow specifying bugfix releases in version info + match_result=re.match(rf"^(?P\d+)\.(?P\d+)(\.(?P\d+)){{,{int(allow_bugfix)}}}(-beta(?P\d+))?$", version_str) def invalid_version(): self.handle_error(self.fd.feof("invalid-version-err", "Invalid version information \"{ver}\" on line {num}", ver=self.fmt(version_str), num=str(self.lineindex+1))) if match_result==None: invalid_version() elif int(match_result.groupdict()['major'])<2: invalid_version() @@ -93,6 +95,7 @@ class GeneratorObject(_data_handlers.DataHandlers): if not version_ok: self.handle_error(self.fd.feof("unsupported-version-err", "Current version of clitheme ({cur_ver}) does not support this file (requires {req_ver} or higher)", cur_ver=_version.__version__+ \ + # For "dev" versions: output corresponding beta milestone (f" [beta{_version.beta_release}]" if _version.beta_release!=None and not "beta" in _version.__version__ else ""), req_ver=self.fmt(version_str)), not_syntax_error=True) def handle_invalid_phrase(self, name: str): -- Gitee From 74f5a868ac9db5556182cf939d0b47d763c4c0b8 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 Jan 2025 17:11:04 +0800 Subject: [PATCH 119/122] Add disclaimer in README --- .github/README.md | 2 ++ README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/README.md b/.github/README.md index a6edc0c..59547f3 100644 --- a/.github/README.md +++ b/.github/README.md @@ -2,6 +2,8 @@ [中文](../README.md) | **English** +**Disclaimer:** Please do not use this tool to create harmful or illegal content. The author of this software does not take any responsibility for content and definition files created by others. + --- `clitheme` allows you to customize the output of command line applications, giving them the style and personality you want. diff --git a/README.md b/README.md index 377c60a..09be97d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ **中文** | [English](.github/README.md) +**免责声明:** 请不要利用该软件传播有害或违法内容。本软件作者对其他人制作的内容和定义文件不负任何责任。 + --- `clitheme`允许你对命令行输出进行个性化定制,给它们一个你想要的风格和个性。 -- Gitee From cbebaa2d1a45b2090f4e5ef6e8d87f5b8524db2f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 Jan 2025 16:59:26 +0800 Subject: [PATCH 120/122] Update version (`v2.0-beta3`) --- PKGBUILD | 2 +- debian/changelog | 42 ++++++++++++++++++++++++++++++++++------ src/clitheme/_version.py | 6 +++--- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index fccd8ec..dad4485 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20241227 +pkgver=2.0_beta3 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index d49a15e..00f5e8a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,15 +1,45 @@ -clitheme (2.0-dev20241227-1) unstable; urgency=low +clitheme (2.0-beta3-1) unstable; urgency=medium - * In development, see commit logs for changes + New features + + * Support using content variables in option definitions + * Show prompt when reading files and message when reading from standard input in CLI interface + * CLI interface: `get-current-theme-info` supports only displaying theme name and source file path for a more concise output + * Rename certain options in `clitheme-exec` + * A warning is now displayed when trying to reference a content variable without enabling the `substvar` option + * Add new functions for modifying global default settings only for current file in `frontend` module, keeping separate settings for different files + * Add `set_local_themedefs` function in `frontend` module, supporting specifying multiple files at once + * Add `!require_version` definition in theme definition file, requiring minimum `clitheme` version + * Support specifying content variables in `locale:` syntax + * Support using more concise `[subst_regex]` and `[subst_string]` syntax + * Support `--name` and `--file-path` options in `clitheme get-current-theme-info` command + * Support using `--yes` option to skip confirmation prompt in `clitheme apply-theme` and `clitheme update-theme` commands - -- swiftycode <3291929745@qq.com> Tue, 31 Dec 2024 11:54:00 +0800 + Bug fixes and improvements -clitheme (2.0-beta2-1) unstable; urgency=low + * clitheme-exec: Specifying `--debug-newlines` option requires `--debug` option to be specified at the same time + * Add more restricted characters to string entry pathnames + * Change how the `foregroundonly` option work: it now applies to individual substitution rules + - If specified in a command filter rule (specified after `[/filter_commands]`), it will reset to previous global setting after exiting current rule + * Fix an issue where read segments repeatedly occur at non-newline positions + * `clitheme-exec`: Ignore backspace characters with identical user input from output substitution processing + * Rename "Generating data" prompt to "Processing files" + * Fix an issue where outputs of `clitheme get-current-theme-info` and `clitheme update-theme` command have incorrect file ordering + * `clitheme-exec`: Properly handle piped standard input + * `{header_section}` now requires defining `name` entry + * `clitheme-exec`: Fix high idle CPU usage + * `clitheme-exec`: Database fetches are now cached in memory, significantly improving performance + * Fix an issue when in multi-line block content and `substesc` option is enabled, `{{ESC}}` in content variables might not be processed + * Important change: `endmatchhere` option only applies to substitution rules in the current file; other files are not affected + + -- swiftycode <3291929745@qq.com> Wed, 01 Jan 2025 11:54:00 +0800 + +clitheme (2.0-beta2-1) unstable; urgency=medium New features - * Support specifying multiple `[file_content]` phrases and filenames in `{manpage_section}`: - * New `[include_file]` syntax in `{manpage_section}`, supporting specifying multiple target file paths at once: + * Support specifying multiple `[file_content]` phrases and filenames in `{manpage_section}` + * New `[include_file]` syntax in `{manpage_section}`, supporting specifying multiple target file paths at once * New `foregroundonly` option for `filter_command` and `[filter_commands]` in `{substrules_section}`; only apply substitution rules if process is in foreground state * Applicable for shell and other command interpreter applications that changes foreground state when executing processes * `clitheme-exec`: new `--debug-foreground` option; outputs message when foreground state changes (shown as `! Foreground: False` and `! Foreground: True`) diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index ce260a8..6036010 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -11,12 +11,12 @@ Version information definition file # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20241227" +__version__="2.0-beta3" major=2 minor=0 release=-1 # -1 stands for "dev" -beta_release=2 # None if not beta +beta_release=3 # None if not beta # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20241223" +version_main="2.0_beta3" version_buildnumber=1 \ No newline at end of file -- Gitee From facb7ef8e2f3a8722a1b5580d49f85a2359e47e5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 Jan 2025 17:15:53 +0800 Subject: [PATCH 121/122] Update frontend_fallback.py --- frontend_fallback.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend_fallback.py b/frontend_fallback.py index f07bb0a..0b4f1fb 100644 --- a/frontend_fallback.py +++ b/frontend_fallback.py @@ -1,7 +1,7 @@ """ clitheme fallback frontend for version 2.0 (returns fallback values for all functions) """ -from typing import Optional +from typing import Optional, List data_path="" @@ -12,6 +12,13 @@ global_debugmode=False global_lang="" global_disablelang=False +def set_domain(value: Optional[str]): pass +def set_appname(value: Optional[str]): pass +def set_subsections(value: Optional[str]): pass +def set_debugmode(value: Optional[bool]): pass +def set_lang(value: Optional[str]): pass +def set_disablelang(value: Optional[bool]): pass + alt_path=None alt_path_dirname=None alt_path_hash=None @@ -19,6 +26,9 @@ alt_path_hash=None def set_local_themedef(file_content: str, overlay: bool=False) -> bool: """Fallback set_local_themedef function (always returns False)""" return False +def set_local_themedefs(file_contents: List[str], overlay: bool=False): + """Fallback set_local_themedefs function (always returns False)""" + return False def unset_local_themedef(): """Fallback unset_local_themedef function""" return -- Gitee From 65021816ce9069638ff5935c467a2915632cd061 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 2 Jan 2025 14:45:42 +0800 Subject: [PATCH 122/122] Fix pycache folder removal in deb build script - Remove the `__pycache__` folder in subdirectories, not just the parent directory --- debian/rules | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/rules b/debian/rules index fa827b5..dba9095 100755 --- a/debian/rules +++ b/debian/rules @@ -10,3 +10,4 @@ override_dh_auto_install: dh_auto_install # remove __pycache__ -rm -r debian/$(PYBUILD_NAME)/usr/lib/python3/dist-packages/$(PYBUILD_NAME)/__pycache__ + -rm -r debian/$(PYBUILD_NAME)/usr/lib/python3/dist-packages/$(PYBUILD_NAME)/*/__pycache__ -- Gitee