From 931b95788d6937607163b5a7ae8aa7befc54c37f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 22 Jan 2024 22:36:09 +0800 Subject: [PATCH 001/354] Update version (v1.2-dev20240122) --- debian/changelog | 6 ++++++ src/clitheme/_version.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index 37f8e4c..c73b314 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +clitheme (1.2-dev20240122-1) UNRELEASED; urgency=low + + * In development, please see commit logs for changes + + -- swiftycode <3291929745@qq.com> Mon, 22 Jan 2024 22:34:00 +0800 + clitheme (1.1-r1-1) unstable; urgency=medium * Version 1.1-r1 in Debian package format diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 2c71ced..41bd985 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.1-r1" +__version__="1.2-dev20240122" major=1 -minor=1 -release=1 # 0 stands for "dev" +minor=2 +release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.1_r1" +version_main="1.2_dev20240122" version_buildnumber=1 \ No newline at end of file -- Gitee From 21d1ed79929a6c36b39a997d0c4cd5e4003b8692 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 1 Feb 2024 18:43:57 +0800 Subject: [PATCH 002/354] Fix subsection parsing issue in frontend --- 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 d71d176..83b2baa 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -174,9 +174,9 @@ class FetchDescriptor(): if self.debug_mode: print(f"[Debug] lang: {lang}\n[Debug] entry_path: {entry_path}") # just being lazy here I don't want to check the variables before using ಥ_ಥ (because it doesn't matter) - path=data_path+"/"+self.domain_name+"/"+self.app_name+"/"+self.subsections + path=data_path+"/"+self.domain_name+"/"+self.app_name+"/"+re.sub(" ",r"/", self.subsections) path2=None - if alt_path!=None: path2=alt_path+"/"+self.domain_name+"/"+self.app_name+"/"+self.subsections + if alt_path!=None: path2=alt_path+"/"+self.domain_name+"/"+self.app_name+"/"+re.sub(" ",r"/", self.subsections) for section in entry_path.split(): path+="/"+section if path2!=None: path2+="/"+section -- Gitee From abdec792de41c8ffb1f50992625a0da12728a72d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 1 Feb 2024 18:44:09 +0800 Subject: [PATCH 003/354] Add zh_CN translation file --- src/clitheme/zh_CN-strings.clithemedef.txt | 135 +++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/clitheme/zh_CN-strings.clithemedef.txt diff --git a/src/clitheme/zh_CN-strings.clithemedef.txt b/src/clitheme/zh_CN-strings.clithemedef.txt new file mode 100644 index 0000000..ed0c480 --- /dev/null +++ b/src/clitheme/zh_CN-strings.clithemedef.txt @@ -0,0 +1,135 @@ +begin_header + name clitheme提示信息中文翻译 + version 1.2 + locales zh_CN + supported_apps clitheme +end_header + +begin_main +in_domainapp swiftycode clitheme + # 命令行指令 + in_subsection cli + entry no-command + locale zh_CN 错误:没有提供指令或选项 + end_entry + entry not-enough-arguments + locale zh_CN 错误:参数不够 + end_entry + entry unknown-option + locale zh_CN 错误:未知选项”{option}“ + end_entry + entry unknown-command + locale zh_CN 错误:未知指令“{cmd}“ + end_entry + entry too-many-arguments + locale zh_CN 错误:参数太多 + end_entry + entry help-usage-prompt + locale zh_CN 使用“clitheme --help”以获取使用方法 + end_entry + entry version-str + locale zh_CN clitheme版本{ver} + end_entry + # apply-theme 和 generate-data 指令 + in_subsection cli apply-theme + entry generate-data-msg + locale zh_CN test + end_entry + entry apply-theme-msg + locale zh_CN 这些主题定义文件将会通过以下顺序被应用: + end_entry + entry overwrite-notice + locale zh_CN 如果继续,当前的主题数据将会被覆盖。 + end_entry + entry overlay-notice + locale zh_CN 这些主题定义文件会被叠加在当前的数据上。 + end_entry + entry confirm-prompt + locale zh_CN 是否继续操作?[y/n] + end_entry + entry overlay-msg + locale zh_CN 已使用数据叠加模式 + end_entry + entry generating-data + locale zh_CN ==> 正在生成数据…… + end_entry + entry processing-file + locale zh_CN > 正在处理文件{filename} + end_entry + entry all-finished + locale zh_CN > 已处理全部文件 + end_entry + entry generate-data-success + locale zh_CN 已成功生成数据 + end_entry + entry view-temp-dir + locale zh_CN 生成的数据可以在“{path}”查看 + end_entry + entry applying-theme + locale zh_CN ==> 正在应用主题…… + end_entry + entry apply-theme-success + locale zh_CN 已成功应用主题 + end_entry + entry read-file-error + locale_block zh_CN + [文件{index}] 读取文件时出现了错误: + {message} + end_block + end_entry + entry overlay-no-data + locale_block zh_CN + 错误:当前没有设定主题或当前数据损坏 + 请尝试设定主题 + end_block + end_entry + entry overlay-data-error + locale_block zh_CN + 错误:当前主题数据损坏 + 请移除当前数据和重新设定主题后重试 + end_block + end_entry + entry generate-data-error + locale_block zh_CN + [文件{index}] 生成数据时发生了错误: + {message} + end_block + end_entry + # unset-current-theme 指令 + in_subsection cli unset-current-theme + entry no-data-found + locale zh_CN 错误:当前没有设定主题 + end_entry + entry remove-data-error + locale_block zh_CN + 移除当前数据时发生了错误: + {message} + end_block + end_entry + entry remove-data-success + locale zh_CN 已成功移除当前主题数据 + end_entry + # get-current-theme-info 指令 + in_subsection cli get-current-theme-info + entry no-theme + locale zh_CN 当前没有设定任何主题 + end_entry + entry current-theme-msg + locale zh_CN 当前设定的主题: + end_entry + entry overlay-history-msg + locale zh_CN 叠加历史记录(根据最新安装的主题排序): + end_entry + entry version-str + locale zh_CN 版本:{ver} + end_entry + entry description-str + locale zh_CN 详细说明: + end_entry + entry locales-str + locale zh_CN 支持的语言: + end_entry + entry supported-apps-str + locale zh_CN 支持的应用程序: + end_entry +end_main \ No newline at end of file -- Gitee From 44a4e7e890c1b4d4f52fe488d01ba2490e06a431 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 1 Feb 2024 20:55:33 +0800 Subject: [PATCH 004/354] Optimize local themedef functions --- src/clitheme/_generator.py | 3 ++- src/clitheme/frontend.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator.py b/src/clitheme/_generator.py index e927ffa..98becea 100644 --- a/src/clitheme/_generator.py +++ b/src/clitheme/_generator.py @@ -13,13 +13,14 @@ except ImportError: # for test program import frontend path="" # to be generated by function +silence_warn=False fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") def handle_error(message): raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) def handle_warning(message): - print(fd.feof("warning-str", "Warning: {msg}", msg=message)) + if not silence_warn: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate directories (excluding file itself) current_path=path current_entry="" # for error output diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 83b2baa..9e38072 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -7,6 +7,7 @@ import random import string import re import hashlib +import shutil from typing import Optional try: from . import _globalvar @@ -23,6 +24,7 @@ global_disablelang=False alt_path=None alt_path_dirname=None +alt_path_hash=None # 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 @@ -42,15 +44,43 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: except ImportError: import _generator # Determine directory name h=hashlib.shake_256(bytes(file_content, "utf-8")) + d=h.hexdigest(6) + global alt_path_hash + # if overlay, update hash with new contents of file + if alt_path_hash!=None and overlay==True: + newhash="" + for x in range(len(alt_path_hash)): + chart=string.ascii_uppercase+string.ascii_lowercase+string.digits + numorig=0 + numcur=0 + if d[x]>='A' and d[x]<='Z': #uppercase letters + numorig=ord(d[x])-ord('A') + elif d[x]>='a' and d[x]<='z': #lowercase letters + numorig=(ord(d[x])-ord('a'))+len(string.ascii_uppercase) + elif d[x]>='0' and d[x]<='9': #digit + numorig=ord(d[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) + if alt_path_hash[x]>='A' and alt_path_hash[x]<='Z': #uppercase letters + numcur=ord(alt_path_hash[x])-ord('A') + elif alt_path_hash[x]>='a' and alt_path_hash[x]<='z': #lowercase letters + numcur=(ord(alt_path_hash[x])-ord('a'))+len(string.ascii_uppercase) + elif alt_path_hash[x]>='0' and alt_path_hash[x]<='9': #digit + numcur=ord(alt_path_hash[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) + newhash+=chart[(numorig+numcur)%len(chart)] + alt_path_hash=newhash + else: alt_path_hash=d # else, use generated hash global alt_path_dirname - dir_name=f"clitheme-data-{h.hexdigest(6)}" # length of 12 (6*2) + dir_name=f"clitheme-data-{alt_path_hash}" # length of 12 (6*2) + overlay_cont=False if alt_path_dirname!=None and overlay==True: # overlay - dir_name=alt_path_dirname + if not os.path.exists(_globalvar.clitheme_temp_root+"/"+dir_name): + overlay_cont=True + shutil.copytree(_globalvar.clitheme_temp_root+"/"+alt_path_dirname, _globalvar.clitheme_temp_root+"/"+dir_name) path_name=_globalvar.clitheme_temp_root+"/"+dir_name if global_debugmode: print("[Debug] "+path_name) # Generate data hierarchy as needed - if not os.path.exists(path_name): + if overlay_cont or not os.path.exists(path_name): _generator.path=path_name + _generator.silence_warn=True try: _generator.generate_data_hierarchy(file_content, custom_path_gen=False) except SyntaxError: @@ -67,6 +97,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 class FetchDescriptor(): """ -- Gitee From 44310506c9539ef05b227599440557037f2c910d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 1 Feb 2024 21:06:08 +0800 Subject: [PATCH 005/354] Change some strings in zh_CN localization --- src/clitheme/zh_CN-strings.clithemedef.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clitheme/zh_CN-strings.clithemedef.txt b/src/clitheme/zh_CN-strings.clithemedef.txt index ed0c480..fccf9b4 100644 --- a/src/clitheme/zh_CN-strings.clithemedef.txt +++ b/src/clitheme/zh_CN-strings.clithemedef.txt @@ -28,7 +28,7 @@ in_domainapp swiftycode clitheme locale zh_CN 使用“clitheme --help”以获取使用方法 end_entry entry version-str - locale zh_CN clitheme版本{ver} + locale zh_CN clitheme 版本:{ver} end_entry # apply-theme 和 generate-data 指令 in_subsection cli apply-theme @@ -51,7 +51,7 @@ in_domainapp swiftycode clitheme locale zh_CN 已使用数据叠加模式 end_entry entry generating-data - locale zh_CN ==> 正在生成数据…… + locale zh_CN ==> 正在生成数据... end_entry entry processing-file locale zh_CN > 正在处理文件{filename} @@ -66,7 +66,7 @@ in_domainapp swiftycode clitheme locale zh_CN 生成的数据可以在“{path}”查看 end_entry entry applying-theme - locale zh_CN ==> 正在应用主题…… + locale zh_CN ==> 正在应用主题... end_entry entry apply-theme-success locale zh_CN 已成功应用主题 -- Gitee From 7f4900cf9204f9a80644cfa1c8a0754c0ec92820 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 1 Feb 2024 21:06:29 +0800 Subject: [PATCH 006/354] Update version (20240201) --- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index c73b314..9512a6b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240122-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240201-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 22 Jan 2024 22:34:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 01 Feb 2024 21:03:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 41bd985..039a7ee 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240122" +__version__="1.2-dev20240201" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240122" +version_main="1.2_dev20240201" version_buildnumber=1 \ No newline at end of file -- Gitee From 691c700d34aa18cee5b16a9c5e1a406b51a706fc Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 09:57:57 +0800 Subject: [PATCH 007/354] Change location and name of string file; Add string file for generator. --- .../cli-strings.clithemedef.txt} | 10 +-- .../strings/generator-strings.clithemedef.txt | 72 +++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) rename src/clitheme/{zh_CN-strings.clithemedef.txt => strings/cli-strings.clithemedef.txt} (93%) create mode 100644 src/clitheme/strings/generator-strings.clithemedef.txt diff --git a/src/clitheme/zh_CN-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt similarity index 93% rename from src/clitheme/zh_CN-strings.clithemedef.txt rename to src/clitheme/strings/cli-strings.clithemedef.txt index fccf9b4..5f2e67e 100644 --- a/src/clitheme/zh_CN-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -1,5 +1,5 @@ begin_header - name clitheme提示信息中文翻译 + name clitheme message text translations (cli) version 1.2 locales zh_CN supported_apps clitheme @@ -16,16 +16,16 @@ in_domainapp swiftycode clitheme locale zh_CN 错误:参数不够 end_entry entry unknown-option - locale zh_CN 错误:未知选项”{option}“ + locale zh_CN 错误:未知选项"{option}" end_entry entry unknown-command - locale zh_CN 错误:未知指令“{cmd}“ + locale zh_CN 错误:未知指令"{cmd}" end_entry entry too-many-arguments locale zh_CN 错误:参数太多 end_entry entry help-usage-prompt - locale zh_CN 使用“clitheme --help”以获取使用方法 + locale zh_CN 使用"clitheme --help"以获取使用方法 end_entry entry version-str locale zh_CN clitheme 版本:{ver} @@ -63,7 +63,7 @@ in_domainapp swiftycode clitheme locale zh_CN 已成功生成数据 end_entry entry view-temp-dir - locale zh_CN 生成的数据可以在“{path}”查看 + locale zh_CN 生成的数据可以在"{path}"查看 end_entry entry applying-theme locale zh_CN ==> 正在应用主题... diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt new file mode 100644 index 0000000..082ab82 --- /dev/null +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -0,0 +1,72 @@ +begin_header + name clitheme message text translations (generator) + version 1.2 + locales zh_CN + supported_apps clitheme +end_header + +begin_main +in_domainapp swiftycode clitheme +in_subsection generator + entry error-str + locale zh_CN 语法错误:{msg} + end_entry + entry subsection-conflict-err + locale zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 + end_entry + entry entry-conflict-err + locale zh_CN 第{num}行:无法创建定义"{name}",因为拥有相同名称的子路径已存在 + end_entry + entry internal-error-blockinput + locale zh_CN 第{num}行:处理多行输入时发生了未知错误;请提交问题反馈 + end_entry + entry extra-arguments-err + locale zh_CN 第{num}行:"{phrase}"后的参数太多 + end_entry + entry repeated-header-err + locale zh_CN 第{num}行:重复的header段落 + end_entry + entry repeated-main-err + locale zh_CN 第{num}行:重复的main段落 + end_entry + entry invalid-phrase-err + locale zh_CN 第{num}行:无效的"{phrase}" + end_entry + entry not-enough-args-err + locale zh_CN 第{num}行:"{phrase}"后参数不够 + end_entry + entry subsection-before-domainapp-err + locale zh_CN 第{num}行:未定义"in_domainapp"时使用"in_subsection" + end_entry + entry incomplete-block-err + locale zh_CN 文件缺少或包含不完整的header或main段落 + end_entry + # warning strings + entry warning-str + 警告:{msg} + end_entry + entry repeated-entry-warn + locale zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 + end_entry + entry repeated-header-warn + locale zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 + end_entry + # sanity check + entry sanity-check-entry-err + locale zh_CN 第{num}行:定义路径名称{sanitycheck_msg} + end_entry + entry sanity-check-domainapp-err + locale zh_CN 第{num}行:开发者和应用程序名称{sanitycheck_msg} + end_entry + entry sanity-check-subsection-err + locale zh_CN 第{num}行:子路径名称{sanitycheck_msg} + end_entry + entry sanity-check-msg-banphrase-err + locale zh_CN 不能包含'{char}' + end_entry + entry sanity-check-msg-startswith-err + locale zh_CN 不能以'{char}'开头 + end_entry +end_main + + \ No newline at end of file -- Gitee From 76aa0c6b03a0e79dfca122c82114ca2939a2b3b3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 09:58:15 +0800 Subject: [PATCH 008/354] Remove in_domainapp before in_subsection requirement --- src/clitheme/_generator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/clitheme/_generator.py b/src/clitheme/_generator.py index 98becea..94c29d7 100644 --- a/src/clitheme/_generator.py +++ b/src/clitheme/_generator.py @@ -244,9 +244,6 @@ def generate_data_hierarchy(file_content, custom_path_gen=True, custom_infofile_ elif phrases[0]=="in_subsection": if len(phrases)<2: handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) - # check if in_domainapp is set - if current_domainapp=="": - handle_error(fd.feof("subsection-before-domainapp-err", "Line {num}: in_subsection used before in_domainapp", num=str(linenumber))) # sanity check if _globalvar.sanity_check(splitarray_to_string(phrases[1:]))==False: handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) -- Gitee From c7628956c8c0fc0bacd279d5175bb490207ff034 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 10:01:31 +0800 Subject: [PATCH 009/354] Remove subsection-before-domainapp definition --- src/clitheme/strings/generator-strings.clithemedef.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 082ab82..1e1675c 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -35,9 +35,6 @@ in_subsection generator entry not-enough-args-err locale zh_CN 第{num}行:"{phrase}"后参数不够 end_entry - entry subsection-before-domainapp-err - locale zh_CN 第{num}行:未定义"in_domainapp"时使用"in_subsection" - end_entry entry incomplete-block-err locale zh_CN 文件缺少或包含不完整的header或main段落 end_entry -- Gitee From fa21cfe06df6dcc7da7878ea85b23795aaedc2f0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 10:08:14 +0800 Subject: [PATCH 010/354] Test in_subsection without in_domainapp definition --- tests/clithemedef-test_mainfile.clithemedef.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/clithemedef-test_mainfile.clithemedef.txt b/tests/clithemedef-test_mainfile.clithemedef.txt index 32be385..33a99f4 100644 --- a/tests/clithemedef-test_mainfile.clithemedef.txt +++ b/tests/clithemedef-test_mainfile.clithemedef.txt @@ -105,7 +105,8 @@ begin_main locale en_US Some text locale zh_CN 一些文本 end_entry - entry global.example global_entry + in_subsection global.example + entry global_entry locale default Global entry in app locale en_US Global entry in app locale zh_CN app内的通用实例 -- Gitee From 034b2c7ecbbfbc86ff8002a7c6f0707b08781f36 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 14:00:42 +0800 Subject: [PATCH 011/354] Fix typo in generator strings file --- src/clitheme/strings/generator-strings.clithemedef.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 1e1675c..08ca037 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -40,7 +40,7 @@ in_subsection generator end_entry # warning strings entry warning-str - 警告:{msg} + locale zh_CN 警告:{msg} end_entry entry repeated-entry-warn locale zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 -- Gitee From 078d19f62b0dee4ced8a5de3800dc3e28a80d51a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 14:01:39 +0800 Subject: [PATCH 012/354] Implement string files in components --- src/clitheme/_generator.py | 14 ++++++++++++++ src/clitheme/_get_resource.py | 11 +++++++++++ src/clitheme/_globalvar.py | 9 +++++++-- src/clitheme/cli.py | 11 +++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/clitheme/_get_resource.py diff --git a/src/clitheme/_generator.py b/src/clitheme/_generator.py index 94c29d7..208ec93 100644 --- a/src/clitheme/_generator.py +++ b/src/clitheme/_generator.py @@ -8,9 +8,13 @@ import re try: from . import _globalvar from . import frontend + from . import _version + from . import _get_resource except ImportError: # for test program import _globalvar import frontend + import _version + import _get_resource path="" # to be generated by function silence_warn=False @@ -84,6 +88,16 @@ def generate_data_hierarchy(file_content, custom_path_gen=True, custom_infofile_ This function should not be invoked directly unless absolutely necessary. """ + global silence_warn + sw=silence_warn + if not silence_warn: # avoid dead loops in set_local_themedef + try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() + except: + if _version.release==0: print("generator set_local_themedef failed") + pass + silence_warn=sw if custom_path_gen: generate_custom_path() if not os.path.exists(path): os.mkdir(path) diff --git a/src/clitheme/_get_resource.py b/src/clitheme/_get_resource.py new file mode 100644 index 0000000..d937d71 --- /dev/null +++ b/src/clitheme/_get_resource.py @@ -0,0 +1,11 @@ +""" +Script to get contents of file inside the module +""" +import os +l=__file__.split(os.sep) +l.pop() +final_str="" # directory where the script files are in +for part in l: + final_str+=part+os.sep +def read_file(path: str) -> str: + return open(final_str+os.sep+path).read() \ No newline at end of file diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 8bcc48e..e4357e6 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -51,10 +51,15 @@ sanity_check_error_message="" msg_retrieved=False def sanity_check(path): # retrieve the entry (only for the first time) - try: from . import frontend - except ImportError: import frontend + try: from . import frontend, _get_resource + except ImportError: import frontend, _get_resource global msg_retrieved if not msg_retrieved: + try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + except: + if _version.release==0: print("set_local_themedef failed") + pass msg_retrieved=True f=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") global banphrase_error_message diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index b9abdc5..ace749c 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -12,10 +12,14 @@ try: from . import _globalvar from . import _generator from . import frontend + from . import _get_resource + from . import _version except ImportError: import _globalvar import _generator import frontend + import _get_resource + import _version usage_description=\ """Usage: {0} apply-theme [themedef-file] [--overlay] [--preserve-temp] @@ -29,6 +33,12 @@ frontend.global_domain="swiftycode" frontend.global_appname="clitheme" frontend.global_subsections="cli" +try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt")): raise RuntimeError() +except: + if _version.release==0: print("set_local_themedef failed") + pass + def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, generate_only=False): """ Apply the theme using the provided definition file contents in a list[str] object. @@ -64,6 +74,7 @@ def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, ge file_content=file_contents[i] # Generate data hierarchy, erase current data, copy it to data path try: + _generator.silence_warn=False _generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index)) generate_path=False # Don't generate another temp folder after first one index+=1 -- Gitee From 8ae318110547eb8740f339b9ff7ec5d99e1bf357 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 14:08:32 +0800 Subject: [PATCH 013/354] Handle keyboard interrupt in confirm prompt --- src/clitheme/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index ace749c..8f7057c 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -243,7 +243,8 @@ def main(cli_args): if overlay==True: print(fi.reof("overlay-notice", "The definition files will be appended on top of the existing theme data.")) inpstr=fi.reof("confirm-prompt", "Do you want to continue? [y/n]") - inp=input(inpstr+" ").strip().lower() + try: inp=input(inpstr+" ").strip().lower() + except KeyboardInterrupt: print();return 1 if not (inp=="y" or inp=="yes"): return 1 content_list=[] -- Gitee From 54751a2a065219e43f952b379dab5cf2114d8801 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 2 Feb 2024 19:56:53 +0800 Subject: [PATCH 014/354] Update version (v1.2-dev20240202) --- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index 9512a6b..6af8650 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240201-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240202-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Thu, 01 Feb 2024 21:03:00 +0800 + -- swiftycode <3291929745@qq.com> Fri, 02 Feb 2024 19:56:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 039a7ee..0799f49 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240201" +__version__="1.2-dev20240202" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240201" +version_main="1.2_dev20240202" version_buildnumber=1 \ No newline at end of file -- Gitee From 22eaed07c85d180ec0423b328b8ca99e9a373b21 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Feb 2024 18:23:47 +0800 Subject: [PATCH 015/354] Move generator file to separate folder --- src/clitheme/{_generator.py => _generator/__init__.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/clitheme/{_generator.py => _generator/__init__.py} (99%) diff --git a/src/clitheme/_generator.py b/src/clitheme/_generator/__init__.py similarity index 99% rename from src/clitheme/_generator.py rename to src/clitheme/_generator/__init__.py index 208ec93..7376b5d 100644 --- a/src/clitheme/_generator.py +++ b/src/clitheme/_generator/__init__.py @@ -6,10 +6,10 @@ import string import random import re try: - from . import _globalvar - from . import frontend - from . import _version - from . import _get_resource + from .. import _globalvar + from .. import frontend + from .. import _version + from .. import _get_resource except ImportError: # for test program import _globalvar import frontend -- Gitee From e08d41b9536f26849eccdf56ba9b1bdf55c1f74a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Feb 2024 19:16:41 +0800 Subject: [PATCH 016/354] Fix _globalvar set_local_themedef not working --- src/clitheme/_globalvar.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index e4357e6..29e617b 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -49,23 +49,11 @@ sanity_check_error_message="" # retrieve the entry only once to avoid dead loop in frontend.FetchDescriptor callbacks msg_retrieved=False +try: from . import frontend, _get_resource +except ImportError: import frontend, _get_resource def sanity_check(path): # retrieve the entry (only for the first time) - try: from . import frontend, _get_resource - except ImportError: import frontend, _get_resource global msg_retrieved - if not msg_retrieved: - try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - except: - if _version.release==0: print("set_local_themedef failed") - pass - msg_retrieved=True - f=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") - global banphrase_error_message - banphrase_error_message=f.feof("sanity-check-msg-banphrase-err", banphrase_error_message, char="{char}") - global startswith_error_message - startswith_error_message=f.feof("sanity-check-msg-startswith-err", startswith_error_message, char="{char}") global sanity_check_error_message for p in path.split(): for b in startswith_banphrases: @@ -77,3 +65,13 @@ def sanity_check(path): sanity_check_error_message=banphrase_error_message.format(char=b) return False return True +if not msg_retrieved: + try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + except RuntimeError: + if _version.release==0: print("_globalvar set_local_themedef failed") + pass + 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}") + startswith_error_message=f.feof("sanity-check-msg-startswith-err", startswith_error_message, char="{char}") \ No newline at end of file -- Gitee From c529d7792c16a0c3bc1ece3aa855c0b62c427c2e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Feb 2024 22:04:10 +0800 Subject: [PATCH 017/354] Add first version of database interface --- PKGBUILD | 2 +- src/clitheme/_generator/db_interface.py | 42 +++++++++++++++++++++++++ src/clitheme/_globalvar.py | 3 ++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/clitheme/_generator/db_interface.py diff --git a/PKGBUILD b/PKGBUILD index 5aa4a15..9ad178f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,7 +6,7 @@ pkgdesc="A text theming library for command line applications" arch=('any') url="https://gitee.com/swiftycode/clitheme" license=('GPL3') -depends=('python>=3.7') +depends=('python>=3.7' 'sqlite>=3') makedepends=('git' 'python-hatch' 'python-installer' 'python-wheel') checkdepends=() optdepends=() diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py new file mode 100644 index 0000000..2b3f6b7 --- /dev/null +++ b/src/clitheme/_generator/db_interface.py @@ -0,0 +1,42 @@ +import sqlite3 +from typing import Optional +try: from .. import _globalvar +except ImportError: import _globalvar + +connection=sqlite3.connect(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) +cursor=connection.cursor() +def init_db(): + global connection, cursor + # create the table + cursor.execute(f"CREATE TABLE IF NOT EXISTS {_globalvar.db_data_tablename} ( \ + match_pattern TEXT PRIMARY KEY, \ + substitute_pattern TEXT NOT NULL \ + );") + # this table stores the effective commands for each match pattern + # Usage: find values based on "command"; obtain substitute pattern from main table using parent_match_pattern of each value + cursor.execute("PRAGMA foreign_keys=ON;") # enable foreign keys support (in case not enabled by default) + cursor.execute(f"CREATE TABLE IF NOT EXISTS {_globalvar.db_data_tablename}_effective_commands ( \ + command TEXT NOT NULL, \ + parent_match_pattern TEXT NOT NULL, \ + FOREIGN KEY (parent_match_pattern) \ + REFERENCES {_globalvar.db_data_tablename} (match_pattern) \ + ON UPDATE CASCADE \ + ON DELETE CASCADE \ + );") + connection.commit() + +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: list[str]): + global cursor + # remove any existing values with the match_pattern, if any + if len(cursor.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=?;", (match_pattern,)).fetchall())>0: + # TODO: print warning message + cursor.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=?;", (match_pattern,)) + # insert the entry into the main table + cursor.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern) VALUES (?, ?);", (match_pattern, substitute_pattern)) + # update effective commands table + for cmd in effective_commands: + cursor.execute(f"INSERT INTO {_globalvar.db_data_tablename}_effective_commands (command, parent_match_pattern) VALUES (?, ?);", (cmd, match_pattern)) + connection.commit() + +def match_content(content: str, command: Optional[str]=None): + pass # TODO \ No newline at end of file diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 29e617b..269b7e0 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -38,6 +38,9 @@ clitheme_version=_version.__version__ generator_info_pathname="theme-info" # e.g. ~/.local/share/clitheme/theme-info generator_data_pathname="theme-data" # e.g. ~/.local/share/clitheme/theme-data generator_index_filename="current_theme_index" +db_data_tablename="clitheme_subst_data" +db_filename="subst-data.db" + entry_banphrases=['/','\\'] startswith_banphrases=['.'] banphrase_error_message="cannot contain '{char}'" -- Gitee From f905204843c26a8c412f66f707484852691bce07 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 8 Feb 2024 22:37:00 +0800 Subject: [PATCH 018/354] Update version (v1.2-20240208) --- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index 6af8650..581c214 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240202-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240208-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Fri, 02 Feb 2024 19:56:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 08 Feb 2024 22:36:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 0799f49..c929188 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240202" +__version__="1.2-dev20240208" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240202" +version_main="1.2_dev20240208" version_buildnumber=1 \ No newline at end of file -- Gitee From ea5129ed9c5a513849512ade965fc22f2b1fca9d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 1 Mar 2024 22:32:58 +0800 Subject: [PATCH 019/354] Finalize db_interface functions --- src/clitheme/_generator/db_interface.py | 85 ++++++++++++++++--------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 2b3f6b7..b02414b 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -1,42 +1,67 @@ +import sys import sqlite3 +import re from typing import Optional try: from .. import _globalvar except ImportError: import _globalvar -connection=sqlite3.connect(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) -cursor=connection.cursor() -def init_db(): - global connection, cursor +connection=sqlite3.connect(":memory:") # placeholder +def init_db(file_path: str): + global connection + connection=sqlite3.connect(file_path) # create the table - cursor.execute(f"CREATE TABLE IF NOT EXISTS {_globalvar.db_data_tablename} ( \ - match_pattern TEXT PRIMARY KEY, \ - substitute_pattern TEXT NOT NULL \ - );") - # this table stores the effective commands for each match pattern - # Usage: find values based on "command"; obtain substitute pattern from main table using parent_match_pattern of each value - cursor.execute("PRAGMA foreign_keys=ON;") # enable foreign keys support (in case not enabled by default) - cursor.execute(f"CREATE TABLE IF NOT EXISTS {_globalvar.db_data_tablename}_effective_commands ( \ - command TEXT NOT NULL, \ - parent_match_pattern TEXT NOT NULL, \ - FOREIGN KEY (parent_match_pattern) \ - REFERENCES {_globalvar.db_data_tablename} (match_pattern) \ - ON UPDATE CASCADE \ - ON DELETE CASCADE \ + connection.execute(f"CREATE TABLE {_globalvar.db_data_tablename} ( \ + match_pattern TEXT NOT NULL, \ + substitute_pattern TEXT NOT NULL, \ + is_regex INTEGER DEFAULT true NOT NULL, \ + effective_command TEXT \ );") connection.commit() -def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: list[str]): +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: list[str], is_regex: bool=True): global cursor - # remove any existing values with the match_pattern, if any - if len(cursor.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=?;", (match_pattern,)).fetchall())>0: - # TODO: print warning message - cursor.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=?;", (match_pattern,)) - # insert the entry into the main table - cursor.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern) VALUES (?, ?);", (match_pattern, substitute_pattern)) - # update effective commands table - for cmd in effective_commands: - cursor.execute(f"INSERT INTO {_globalvar.db_data_tablename}_effective_commands (command, parent_match_pattern) VALUES (?, ?);", (cmd, match_pattern)) + cmdlist=[] + if len(effective_commands)>0: + for cmd in effective_commands: + # remove extra spaces in the command + cmdlist.append(re.sub(r" {2,}", " ", cmd).strip()) + for cmd in cmdlist: + # remove any existing values with the match_pattern and effective_command, if any + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?;", (match_pattern.strip(),cmd)).fetchall())>0: + print("Warning: Repeated entry at line %, overwriting") + connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?;", (match_pattern.strip(),cmd)) + # insert the entry into the main table + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex) VALUES (?,?,?,?);", (match_pattern.strip(), substitute_pattern, cmd, is_regex)) connection.commit() -def match_content(content: str, command: Optional[str]=None): - pass # TODO \ No newline at end of file +def match_content(content: str, command: Optional[str]=None) -> str: + # retrieve a list of effective commands matching first argument + target_command=None + if command!=None and len(command.split())>0: + cmdlist=connection.execute(f"SELECT DISTINCT effective_command FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ?;", (command.split()[0].strip()+" %")).fetchall() + # sort by command length (greatest to least) + cmdlist.sort(key=len, reverse=True) + # attempt to find matching command + for tp in cmdlist: + cmd=tp[0] # extract value from tuple + success=True + for phrase in cmd.split(): + if phrase not in command.split(): + success=False; break + if success: + # if found matching command + target_command=cmd + break + content_str=content + matches=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE typeof(effective_command)=typeof(null) ORDER BY rowid;").fetchall() + if target_command!=None: matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE effective_command=? ORDER BY rowid;").fetchall() + for match_data in matches: + try: + if match_data[2]==True: # is regex + content_str=re.sub(match_data[0], match_data[1], content_str) + else: + content_str=content_str.replace(match_data[0], match_data[1]) + except: + print("Error occured while matching string: ", end="") + print(sys.exc_info()[1]) + return content_str \ No newline at end of file -- Gitee From 232c86dee48ea513b438a4b67842381688ac5e19 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 2 Mar 2024 14:04:41 +0800 Subject: [PATCH 020/354] Skip LANGUAGE if LANG and LC_ALL are set to C --- src/clitheme/frontend.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 9e38072..9d4b0ac 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -170,8 +170,17 @@ class FetchDescriptor(): if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) else: if self.debug_mode: print("[Debug] Locale: Using environment variables") + # Skip $LANGUAGE if both $LANG and $LC_ALL is set to C (treat empty as C also) + skip_LANGUAGE=False + LANG_value="" + if "LANG" not in os.environ or os.environ["LANG"]=='': LANG_value="C" + else: LANG_value=os.environ['LANG'] + LC_ALL_value="" + if "LC_ALL" not in os.environ or os.environ["LC_ALL"]=='': LC_ALL_value="C" + else: LC_ALL_value=os.environ["LC_ALL"] + if (LANG_value=="C" or LANG_value.startswith("C.")) and (LC_ALL_value=="C" or LC_ALL_value.startswith("C.")): skip_LANGUAGE=True # $LANGUAGE (list of languages separated by colons) - if os.environ.__contains__("LANGUAGE"): + if "LANGUAGE" in os.environ and not skip_LANGUAGE: target_str=os.environ['LANGUAGE'] for each_language in target_str.strip().split(":"): # avoid exploit of accessing top-level folders @@ -187,7 +196,7 @@ class FetchDescriptor(): lang+=re.sub(r"(?P.+)[\.].+", r"\g", each_language)+" " lang=lang.strip() # $LC_ALL - elif os.environ.__contains__("LC_ALL"): + elif "LC_ALL" in os.environ: target_str=os.environ["LC_ALL"].strip() if not _globalvar.sanity_check(target_str)==False: lang=target_str+" " @@ -195,7 +204,7 @@ class FetchDescriptor(): else: if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) # $LANG - elif os.environ.__contains__("LANG"): + elif "LANG" in os.environ: target_str=os.environ["LANG"].strip() if not _globalvar.sanity_check(target_str)==False: lang=target_str+" " -- Gitee From adf103d8537873de8614bbaa8c1f5de1e4f98cbc Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 2 Mar 2024 23:13:10 +0800 Subject: [PATCH 021/354] Update version (v1.2-dev20240302) --- PKGBUILD | 4 ++-- debian/changelog | 10 +++++----- src/clitheme/_version.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 9ad178f..7a76445 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -11,12 +11,12 @@ makedepends=('git' 'python-hatch' 'python-installer' 'python-wheel') checkdepends=() optdepends=() provides=($pkgname) -conflicts=($pkgname 'clitheme') +conflicts=($pkgname) replaces=() backup=() options=() install= -changelog= +changelog='debian/changelog' source=("srctmp::git+file://$PWD") noextract=() md5sums=('SKIP') diff --git a/debian/changelog b/debian/changelog index 581c214..43df2dc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,12 +1,12 @@ -clitheme (1.2-dev20240208-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240302-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Thu, 08 Feb 2024 22:36:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 02 Mar 2024 22:59:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium - * Version 1.1-r1 in Debian package format + * Version 1.1 release 1 For more information please see: https://gitee.com/swiftycode/clitheme/releases/tag/v1.1-r1 @@ -14,7 +14,7 @@ clitheme (1.1-r1-1) unstable; urgency=medium clitheme (1.0-r2-1) unstable; urgency=medium - * Version 1.0-r2 in Debian package format + * Version 1.0 release 2 For more information please see: https://gitee.com/swiftycode/clitheme/releases/tag/v1.0-r2 @@ -22,6 +22,6 @@ clitheme (1.0-r2-1) unstable; urgency=medium clitheme (1.0-r1-1) unstable; urgency=medium - * Version 1.0_r1 in Debian package format + * Version 1.0 release 1 -- swiftycode <3291929745@qq.com> Wed, 13 Dec 2023 17:16:33 +0800 diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index c929188..ceadc4a 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240208" +__version__="1.2-dev20240302" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240208" +version_main="1.2_dev20240302" version_buildnumber=1 \ No newline at end of file -- Gitee From dc32043f0ad512d43cea2d99ffaaa8a88735cc05 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 2 Mar 2024 23:21:25 +0800 Subject: [PATCH 022/354] Force UTF-8 encoding in _get_resource --- src/clitheme/_get_resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_get_resource.py b/src/clitheme/_get_resource.py index d937d71..7941970 100644 --- a/src/clitheme/_get_resource.py +++ b/src/clitheme/_get_resource.py @@ -8,4 +8,4 @@ final_str="" # directory where the script files are in for part in l: final_str+=part+os.sep def read_file(path: str) -> str: - return open(final_str+os.sep+path).read() \ No newline at end of file + return open(final_str+os.sep+path, encoding="utf-8").read() \ No newline at end of file -- Gitee From 0a323f9dbeae29c231dd206d98affc544a93e96a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 6 Mar 2024 15:45:20 +0800 Subject: [PATCH 023/354] Fix circular import issue (in _globalvar) --- src/clitheme/_globalvar.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 269b7e0..d8e47b8 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -54,27 +54,31 @@ sanity_check_error_message="" msg_retrieved=False try: from . import frontend, _get_resource except ImportError: import frontend, _get_resource -def sanity_check(path): - # retrieve the entry (only for the first time) - global msg_retrieved +def sanity_check(path: str) -> bool: + def retrieve_entry(): + # retrieve the entry (only for the first time) + global msg_retrieved + global sanity_check_error_message, banphrase_error_message, startswith_error_message + if not msg_retrieved: + try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + except RuntimeError: + if _version.release==0: print("_globalvar set_local_themedef failed") + pass + 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}") + startswith_error_message=f.feof("sanity-check-msg-startswith-err", startswith_error_message, char="{char}") global sanity_check_error_message for p in path.split(): for b in startswith_banphrases: if p.startswith(b): + retrieve_entry() sanity_check_error_message=startswith_error_message.format(char=b) return False for b in entry_banphrases: if p.find(b)!=-1: + retrieve_entry() sanity_check_error_message=banphrase_error_message.format(char=b) return False - return True -if not msg_retrieved: - try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - except RuntimeError: - if _version.release==0: print("_globalvar set_local_themedef failed") - pass - 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}") - startswith_error_message=f.feof("sanity-check-msg-startswith-err", startswith_error_message, char="{char}") \ No newline at end of file + return True \ No newline at end of file -- Gitee From 56d57c2b535f07277ecc23cc0d90b6699b7fd909 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 7 Mar 2024 07:56:14 +0800 Subject: [PATCH 024/354] Add db_interface_tests; work on db_interface --- src/clitheme/_generator/db_interface.py | 29 +++++++++++++++--------- src/db_interface_tests.py | 30 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 src/db_interface_tests.py diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index b02414b..a8c467c 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -18,27 +18,35 @@ def init_db(file_path: str): );") connection.commit() -def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: list[str], is_regex: bool=True): +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], is_regex: bool=True): global cursor cmdlist=[] - if len(effective_commands)>0: + if effective_commands!=None and len(effective_commands)>0: for cmd in effective_commands: # remove extra spaces in the command cmdlist.append(re.sub(r" {2,}", " ", cmd).strip()) + else: + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null);", (match_pattern.strip(),)).fetchall())>0: + print("Warning: Repeated entry at line %, overwriting") + connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null);", (match_pattern.strip(),)) + # insert the entry into the main table + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, is_regex) VALUES (?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), is_regex)) for cmd in cmdlist: # remove any existing values with the match_pattern and effective_command, if any if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?;", (match_pattern.strip(),cmd)).fetchall())>0: print("Warning: Repeated entry at line %, overwriting") connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?;", (match_pattern.strip(),cmd)) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex) VALUES (?,?,?,?);", (match_pattern.strip(), substitute_pattern, cmd, is_regex)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex) VALUES (?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd, is_regex)) connection.commit() -def match_content(content: str, command: Optional[str]=None) -> str: +def match_content(content: bytes, command: Optional[str]=None) -> bytes: # retrieve a list of effective commands matching first argument target_command=None if command!=None and len(command.split())>0: - cmdlist=connection.execute(f"SELECT DISTINCT effective_command FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ?;", (command.split()[0].strip()+" %")).fetchall() + cmdlist=connection.execute(f"SELECT DISTINCT effective_command FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ?;", (command.split()[0].strip()+" %",)).fetchall() + # also include one-phrase effective commands + cmdlist+=connection.execute(f"SELECT DISTINCT effective_command FROM {_globalvar.db_data_tablename} WHERE effective_command=?;", (command.split()[0].strip(),)).fetchall() # sort by command length (greatest to least) cmdlist.sort(key=len, reverse=True) # attempt to find matching command @@ -53,15 +61,16 @@ def match_content(content: str, command: Optional[str]=None) -> str: target_command=cmd break content_str=content - matches=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE typeof(effective_command)=typeof(null) ORDER BY rowid;").fetchall() - if target_command!=None: matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE effective_command=? ORDER BY rowid;").fetchall() + matches=[] + if target_command!=None: matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE effective_command=? ORDER BY rowid;", (target_command,)).fetchall() + matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE typeof(effective_command)=typeof(null) ORDER BY rowid;").fetchall() for match_data in matches: try: if match_data[2]==True: # is regex - content_str=re.sub(match_data[0], match_data[1], content_str) + content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) else: - content_str=content_str.replace(match_data[0], match_data[1]) + content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) except: - print("Error occured while matching string: ", end="") + print("Error occurred while matching string: ", end="") print(sys.exc_info()[1]) return content_str \ No newline at end of file diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py new file mode 100644 index 0000000..4f597e4 --- /dev/null +++ b/src/db_interface_tests.py @@ -0,0 +1,30 @@ +from clitheme._generator import db_interface + +# sample input for testing +sample_inputs=[("rm: missing operand", "rm"), + ("type rm --help for more information", "rm"), + ("rm: /etc/folder: Permission denied", "rm -rf /etc/folder"), + ("cat: /dev/mem: Permission denied","cat /dev/mem"), + ("bash: /etc/secret: Permission denied","cd /etc/secret"), + ("ls: /etc/secret: Permission denied","ls /etc/secret"), + ("ls: /etc/secret: Permission denied","wef ls /etc/secret"), # test first phrase detection + ("ls: unrecognized option '--help'", "ls --help"), + ("Warning: invaild input", "input anything"), + ("Error: invaild input ","input anything"), # test extra spaces +] +# substitute patterns +subst_patterns=[("rm: missing operand", "rm says: missing arguments and options (>﹏<)", False, ["rm"]), + ("type rm --help for more information", "For more information, use rm --help (。ì _ í。)", False, ["rm"]), + (r"(?P.+): (?P.+): Permission denied",r"""\g says: Access denied to \g! ಥ_ಥ""",True, ["rm", "cat", "cd", "ls"]), + (r"(?P.+): unrecognized option '(?P.+)'",r"""wef""",True, ["ls"]), # testing repeated entry detection + (r"(?P.+): unrecognized option '(?P.+)'",r"""\g says: option '\g' not known! (ToT)/~~~'""",True, ["ls"]), + (r"^Warning:( )", r"o(≧v≦)o Note:\g<1>",True , None), + (r"^Error:( )", r"(ToT)/~~~ Error:\g<1>",True, None), + (r"invaild input( ){0,}$", r"input is invaild! ಥ_ಥ", True, None)] + +db_interface.init_db(":memory:") +# record substitute patterns +for dat in subst_patterns: db_interface.add_subst_entry(match_pattern=dat[0], substitute_pattern=dat[1], effective_commands=dat[3], is_regex=dat[2]) +print("Successfully recorded data\nTesting sample outputs: ") +for inp in sample_inputs: + print(db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8')) -- Gitee From 1fb7d88bd33498598215a054e137e91e871b2c6a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 7 Mar 2024 07:59:44 +0800 Subject: [PATCH 025/354] Move tests into src folder --- .../clitheme-testblock_testprogram.py | 2 +- .../clithemedef-test_testprogram.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename clitheme-testblock_testprogram.py => src/clitheme-testblock_testprogram.py (96%) rename clithemedef-test_testprogram.py => src/clithemedef-test_testprogram.py (96%) diff --git a/clitheme-testblock_testprogram.py b/src/clitheme-testblock_testprogram.py similarity index 96% rename from clitheme-testblock_testprogram.py rename to src/clitheme-testblock_testprogram.py index b18aa13..48b441c 100644 --- a/clitheme-testblock_testprogram.py +++ b/src/clitheme-testblock_testprogram.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # Program for testing multi-line (block) processing of _generator -from src.clitheme import _generator, frontend +from clitheme import _generator, frontend file_data=""" begin_header diff --git a/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py similarity index 96% rename from clithemedef-test_testprogram.py rename to src/clithemedef-test_testprogram.py index fb03f2a..e3faa55 100644 --- a/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -1,6 +1,6 @@ import shutil -from src.clitheme import _generator -from src.clitheme import _globalvar +from clitheme import _generator +from clitheme import _globalvar import random import string @@ -34,7 +34,7 @@ for line in expected_data.splitlines(): # Test frontend print("Testing frontend...") -from src.clitheme import frontend +from clitheme import frontend frontend.global_lang="en_US.UTF-8" frontend.global_debugmode=True frontend.data_path=_generator.path+"/"+_globalvar.generator_data_pathname -- Gitee From d8bb7693437da62ddbd29f848ecac1593bdf29de Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 7 Mar 2024 16:36:39 +0800 Subject: [PATCH 026/354] Update db_interface_tests --- src/db_interface_tests.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index 4f597e4..f1598bc 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -3,11 +3,12 @@ from clitheme._generator import db_interface # sample input for testing sample_inputs=[("rm: missing operand", "rm"), ("type rm --help for more information", "rm"), - ("rm: /etc/folder: Permission denied", "rm -rf /etc/folder"), + ("rm: /etc/folder: Permission denied", "rm /etc/folder -rf"), + ("rm: /etc/file: Permission denied", "rm /etc/folder"), # test multiple phrase detection (substitution should not happen) ("cat: /dev/mem: Permission denied","cat /dev/mem"), ("bash: /etc/secret: Permission denied","cd /etc/secret"), ("ls: /etc/secret: Permission denied","ls /etc/secret"), - ("ls: /etc/secret: Permission denied","wef ls /etc/secret"), # test first phrase detection + ("ls: /etc/secret: Permission denied","wef ls /etc/secret"), # test first phrase detection (substitution should not happen) ("ls: unrecognized option '--help'", "ls --help"), ("Warning: invaild input", "input anything"), ("Error: invaild input ","input anything"), # test extra spaces @@ -15,7 +16,7 @@ sample_inputs=[("rm: missing operand", "rm"), # substitute patterns subst_patterns=[("rm: missing operand", "rm says: missing arguments and options (>﹏<)", False, ["rm"]), ("type rm --help for more information", "For more information, use rm --help (。ì _ í。)", False, ["rm"]), - (r"(?P.+): (?P.+): Permission denied",r"""\g says: Access denied to \g! ಥ_ಥ""",True, ["rm", "cat", "cd", "ls"]), + (r"(?P.+): (?P.+): Permission denied",r"""\g says: Access denied to \g! ಥ_ಥ""",True, ["rm -rf", "cat", "cd", "ls"]), (r"(?P.+): unrecognized option '(?P.+)'",r"""wef""",True, ["ls"]), # testing repeated entry detection (r"(?P.+): unrecognized option '(?P.+)'",r"""\g says: option '\g' not known! (ToT)/~~~'""",True, ["ls"]), (r"^Warning:( )", r"o(≧v≦)o Note:\g<1>",True , None), -- Gitee From 7e9a6d11af03b23e05a32b277bc5a9f8ccf13630 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 7 Mar 2024 20:51:20 +0800 Subject: [PATCH 027/354] Optimize set_local_themedef in _generator --- src/clitheme/_generator/__init__.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 7376b5d..c883123 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -88,16 +88,6 @@ def generate_data_hierarchy(file_content, custom_path_gen=True, custom_infofile_ This function should not be invoked directly unless absolutely necessary. """ - global silence_warn - sw=silence_warn - if not silence_warn: # avoid dead loops in set_local_themedef - try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - except: - if _version.release==0: print("generator set_local_themedef failed") - pass - silence_warn=sw if custom_path_gen: generate_custom_path() if not os.path.exists(path): os.mkdir(path) @@ -304,3 +294,9 @@ def generate_data_hierarchy(file_content, custom_path_gen=True, custom_infofile_ theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") theme_index.write(custom_infofile_name+"\n") return True # Everything is successful! :) + +try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() +except: + if _version.release==0: print("generator set_local_themedef failed") + pass \ No newline at end of file -- Gitee From 5a60c3c48e41e34c0a1c9c4d88dbb1c4e2e119f8 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 8 Mar 2024 00:00:19 +0800 Subject: [PATCH 028/354] Update version (v1.2-20240307) --- PKGBUILD | 4 ++-- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 7a76445..37c2281 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=UNKNOWN # to be filled out by pkgver() -pkgrel=1 # to be filled out by pkgver() +pkgver='1.2_dev20240307' +pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') url="https://gitee.com/swiftycode/clitheme" diff --git a/debian/changelog b/debian/changelog index 43df2dc..833523c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240302-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240307-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 02 Mar 2024 22:59:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 07 Mar 2024 23:59:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index ceadc4a..33b3e4b 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240302" +__version__="1.2-dev20240307" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240302" +version_main="1.2_dev20240307" version_buildnumber=1 \ No newline at end of file -- Gitee From 4c175cdd46f60980653361541efce9cfcb228a08 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 5 Apr 2024 21:54:41 +0800 Subject: [PATCH 029/354] Add pybuild-plugin-pyproject to build-depends --- README.en.md | 3 ++- README.md | 3 ++- debian/control | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.en.md b/README.en.md index 7639254..7f6d986 100644 --- a/README.en.md +++ b/README.en.md @@ -249,10 +249,11 @@ The following packages are required prior to building the package: - `dh-python` - `python3-hatchling` - `dpkg-dev` +- `pybuild-plugin-pyproject` They can be installed using this command: - sudo apt install debhelper dh-python python3-hatchling dpkg-dev + sudo apt install debhelper dh-python python3-hatchling dpkg-dev pybuild-plugin-pyproject Run `dpkg-buildpackage -b` to build the package. A `.deb` file will be generated in the upper folder after the build process finishes. diff --git a/README.md b/README.md index bea038a..09fa6e2 100644 --- a/README.md +++ b/README.md @@ -249,10 +249,11 @@ rm -rf buildtmp srctmp - `dh-python` - `python3-hatchling` - `dpkg-dev` +- `pybuild-plugin-pyproject` 你可以使用以下命令安装: - sudo apt install debhelper dh-python python3-hatchling dpkg-dev + sudo apt install debhelper dh-python python3-hatchling dpkg-dev pybuild-plugin-pyproject 安装完后,请在仓库目录中执行`dpkg-buildpackage -b`以构建软件包。完成后,你会在上层目录中获得一个`.deb`的文件。 diff --git a/debian/control b/debian/control index 8227c38..201fc46 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: libs Priority: optional Maintainer: swiftycode <3291929745@qq.com> Rules-Requires-Root: no -Build-Depends: debhelper-compat (= 13), python3, dh-python, python3-hatchling +Build-Depends: debhelper-compat (= 13), python3, dh-python, python3-hatchling, pybuild-plugin-pyproject Standards-Version: 4.6.2 Homepage: https://gitee.com/swiftycode/clitheme Vcs-Git: https://gitee.com/swiftycode/clitheme.git -- Gitee From 86d928e52be19fcab75bbd0010f87dbbe434748b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 6 Apr 2024 22:26:31 +0800 Subject: [PATCH 030/354] Move testprogram data into src folder --- src/clithemedef-test_testprogram.py | 13 +++++++++---- .../clithemedef-test_expected-frontend.txt | 0 .../testprogram-data}/clithemedef-test_expected.txt | 0 .../clithemedef-test_mainfile.clithemedef.txt | 0 4 files changed, 9 insertions(+), 4 deletions(-) rename {tests => src/testprogram-data}/clithemedef-test_expected-frontend.txt (100%) rename {tests => src/testprogram-data}/clithemedef-test_expected.txt (100%) rename {tests => src/testprogram-data}/clithemedef-test_mainfile.clithemedef.txt (100%) diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index e3faa55..322d7f7 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -3,10 +3,15 @@ from clitheme import _generator from clitheme import _globalvar import random import string - +import os +l=__file__.split(os.sep) +l.pop() +root_directory="" # directory where the script files are in +for part in l: + root_directory+=part+os.sep print("Testing generator function...") -mainfile_data=open("tests/clithemedef-test_mainfile.clithemedef.txt",'r', encoding="utf-8").read() -expected_data=open("tests/clithemedef-test_expected.txt",'r', encoding="utf-8").read() +mainfile_data=open(root_directory+"/testprogram-data/clithemedef-test_mainfile.clithemedef.txt",'r', encoding="utf-8").read() +expected_data=open(root_directory+"/testprogram-data/clithemedef-test_expected.txt",'r', encoding="utf-8").read() funcresult=_generator.generate_data_hierarchy(mainfile_data) errorcount=0 @@ -38,7 +43,7 @@ from clitheme import frontend frontend.global_lang="en_US.UTF-8" frontend.global_debugmode=True frontend.data_path=_generator.path+"/"+_globalvar.generator_data_pathname -expected_data_frontend=open("tests/clithemedef-test_expected-frontend.txt", 'r', encoding="utf-8").read() +expected_data_frontend=open(root_directory+"/testprogram-data/clithemedef-test_expected-frontend.txt", 'r', encoding="utf-8").read() current_path_frontend="" errorcount_frontend=0 for line in expected_data_frontend.splitlines(): diff --git a/tests/clithemedef-test_expected-frontend.txt b/src/testprogram-data/clithemedef-test_expected-frontend.txt similarity index 100% rename from tests/clithemedef-test_expected-frontend.txt rename to src/testprogram-data/clithemedef-test_expected-frontend.txt diff --git a/tests/clithemedef-test_expected.txt b/src/testprogram-data/clithemedef-test_expected.txt similarity index 100% rename from tests/clithemedef-test_expected.txt rename to src/testprogram-data/clithemedef-test_expected.txt diff --git a/tests/clithemedef-test_mainfile.clithemedef.txt b/src/testprogram-data/clithemedef-test_mainfile.clithemedef.txt similarity index 100% rename from tests/clithemedef-test_mainfile.clithemedef.txt rename to src/testprogram-data/clithemedef-test_mainfile.clithemedef.txt -- Gitee From 07af68a484934d30b6abe67e7e6e6a5fc33448ba Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 6 Apr 2024 23:06:17 +0800 Subject: [PATCH 031/354] Update PKGBUILD --- PKGBUILD | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 37c2281..c24f834 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -7,17 +7,17 @@ arch=('any') url="https://gitee.com/swiftycode/clitheme" license=('GPL3') depends=('python>=3.7' 'sqlite>=3') -makedepends=('git' 'python-hatch' 'python-installer' 'python-wheel') +makedepends=('git' 'python-hatch' 'python-installer' 'python-wheel' 'gzip') checkdepends=() optdepends=() -provides=($pkgname) +provides=() conflicts=($pkgname) replaces=() backup=() options=() install= changelog='debian/changelog' -source=("srctmp::git+file://$PWD") +source=("srctmp::git+file://$PWD#branch=v1.2_dev") noextract=() md5sums=('SKIP') validpgpkeys=() -- Gitee From 1d5841dfa3abcb26f95e02764d6ea539fe07c628 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 7 Apr 2024 15:54:39 +0800 Subject: [PATCH 032/354] Add generator_v2 --- src/clitheme/_generator/generator_v2.py | 276 ++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 src/clitheme/_generator/generator_v2.py diff --git a/src/clitheme/_generator/generator_v2.py b/src/clitheme/_generator/generator_v2.py new file mode 100644 index 0000000..328006e --- /dev/null +++ b/src/clitheme/_generator/generator_v2.py @@ -0,0 +1,276 @@ +""" +Generator function used in applying themes (should not be invoked directly) +""" +import os +import string +import random +import re +try: + from .. import _globalvar + from .. import frontend + from .. import _version + from .. import _get_resource +except ImportError: # for test program + import _globalvar + import frontend + import _version + import _get_resource + +fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") + +path="" +silence_warn=False + +def handle_error(message): + raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) +def handle_warning(message): + if not silence_warn: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) +def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate directories (excluding file itself) + current_path=path + current_entry="" # for error output + for x in entry_name.split()[:-1]: + current_entry+=x+" " + current_path+="/"+x + if os.path.isfile(current_path): # conflict with entry file + handle_error(fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ + num=str(line_number_debug), name=current_entry)) + elif os.path.isdir(str(current_path))==False: # directory does not exist + os.mkdir(current_path) +def add_entry(path, entry_name, entry_content, line_number_debug): # add entry to where it belongs (assuming recursive_mkdir already completed) + target_path=path + for x in entry_name.split(): + target_path+="/"+x + if os.path.isdir(target_path): + handle_error(fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ + num=str(line_number_debug), name=entry_name)) + elif os.path.isfile(target_path): + handle_warning(fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ + num=str(line_number_debug), name=entry_name)) + f=open(target_path,'w', encoding="utf-8") + f.write(entry_content+"\n") +def splitarray_to_string(split_content): + final="" + for phrase in split_content: + final+=phrase+" " + return final.strip() +def write_infofile(path,filename,content,line_number_debug, header_name_debug): + if not os.path.isdir(path): + os.makedirs(path) + target_path=path+"/"+filename + if os.path.isfile(target_path): + handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ + num=str(line_number_debug), name=header_name_debug)) + f=open(target_path,'w', encoding="utf-8") + f.write(content+'\n') + +def write_infofile_newlines(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 + if os.path.isfile(target_path): + handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ + num=str(line_number_debug), name=header_name_debug)) + f=open(target_path,'w', encoding="utf-8") + for line in content_phrases: + f.write(line+"\n") + +def generate_custom_path(): + # Generate a temporary path + global path + path=_globalvar.clitheme_temp_root+"/clitheme-temp-" + for x in range(8): + path+=random.choice(string.ascii_letters) + +def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1"): + # make directories + if custom_path_gen: + generate_custom_path() + if not os.path.exists(path): os.mkdir(path) + datapath=path+"/"+_globalvar.generator_data_pathname + if not os.path.exists(datapath): os.mkdir(datapath) + + # data to keep track of + headerparsed=False + mainparsed=False + lines_data=file_content.splitlines() + lineindex=-1 # counter extra +1 operation at beginning + + # define check functions + def check_enough_args(phrases: list[str], count: int): + if len(phrases)count + if not_pass: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=phrases[0])) + + # defined sub-processing functions + def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block") -> str: + nonlocal lineindex + minspaces=0 + blockinput_data="" + while lineindexend_block", line.strip()) + # update minspaces + minspaces=min(minspaces, len(leading_whitespace)) + else: # don't preserve whitespaces + line=re.sub(r"^\\([\\]*)end_block", r"\g<1>end_block", line.strip()) + # write to data + blockinput_data+="\n"+line + # remove the extra leading newline + blockinput_data=re.sub(r"\A\n", "", blockinput_data) + # remove all whitespaces except common minspaces (if preserve_indents) + if preserve_indents: + pattern=r"(?P\n|^)[ ]{"+str(minspaces)+"}" + blockinput_data=re.sub(pattern,r"\g", blockinput_data) + return blockinput_data + def handle_entry(entry_name: str): + # expect locale, locale_block, end_entry + nonlocal lineindex + while lineindex Date: Sun, 7 Apr 2024 21:36:59 +0800 Subject: [PATCH 033/354] Move generator_v2 onto __init__ and keep old file --- src/clitheme/_generator/__init__.py | 369 +++++++++++------------ src/clitheme/_generator/generator_old.py | 302 +++++++++++++++++++ src/clitheme/_generator/generator_v2.py | 276 ----------------- 3 files changed, 477 insertions(+), 470 deletions(-) create mode 100644 src/clitheme/_generator/generator_old.py delete mode 100644 src/clitheme/_generator/generator_v2.py diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index c883123..7b52d63 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -16,11 +16,11 @@ except ImportError: # for test program import _version import _get_resource -path="" # to be generated by function -silence_warn=False - fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") +path="" +silence_warn=False + def handle_error(message): raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) def handle_warning(message): @@ -63,7 +63,7 @@ def write_infofile(path,filename,content,line_number_debug, header_name_debug): f=open(target_path,'w', encoding="utf-8") f.write(content+'\n') -def write_infofile_v2(path: str, filename: str, content_phrases: list[str], line_number_debug: int, header_name_debug: str): +def write_infofile_newlines(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 @@ -81,219 +81,200 @@ def generate_custom_path(): for x in range(8): path+=random.choice(string.ascii_letters) -# Returns true for success or error message -def generate_data_hierarchy(file_content, custom_path_gen=True, custom_infofile_name="1"): - """ - Generate the data hierarchy in a temporary directory from a definition file (accessible with _generator.path) - - This function should not be invoked directly unless absolutely necessary. - """ +def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1"): + # make directories if custom_path_gen: generate_custom_path() if not os.path.exists(path): os.mkdir(path) datapath=path+"/"+_globalvar.generator_data_pathname if not os.path.exists(datapath): os.mkdir(datapath) - current_status="" # header, main, entry - linenumber=0 - # To detect repeated blocks + + # data to keep track of headerparsed=False mainparsed=False + lines_data=file_content.splitlines() + lineindex=-1 # counter extra +1 operation at beginning - current_domainapp="" # for in_domainapp and unset_domainapp in main block - current_entry_name="" # for entry - current_subsection="" # for in_subsection - - current_entry_locale="" # for handling locale_block - current_entry_linenumber=-1 + # define check functions + def check_enough_args(phrases: list[str], count: int): + if len(phrases)count + if not_pass: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=phrases[0])) - current_header_entry="" # for block input in header - current_header_linenumber=-1 - - blockinput=False # for multi-line (block) input - blockinput_data="" # data of current block input - blockinput_minspaces=-1 # min number of whitespaces - for line in file_content.splitlines(): - linenumber+=1 - phrases=line.split() - if blockinput==False and (line.strip()=="" or line.strip()[0]=="#"): # if empty line or comment (except in block input mode) - continue - - if blockinput==True: - if len(phrases)>0 and phrases[0]=="end_block": - if blockinput_minspaces!=-1: - # process whitespaces - # trim amount of leading whitespaces on each line - pattern=r"(?P\n|^)[ ]{"+str(blockinput_minspaces)+"}" - blockinput_data=re.sub(pattern,r"\g", blockinput_data) - if current_status=="entry": - for this_locale in current_entry_locale.split(): - target_entry=current_entry_name - if this_locale!="default": - target_entry+="__"+this_locale - add_entry(datapath,target_entry, blockinput_data, current_entry_linenumber) - # clear data - current_entry_locale="" - current_entry_linenumber=-1 - elif current_status=="header": - if current_header_entry!="description": - # trim all leading whitespaces - blockinput_data=re.sub(r"(?P\n|^)[ ]+",r"\g", blockinput_data) - # trim all trailing whitespaces - blockinput_data=re.sub(r"[ ]+(?P\n|$)",r"\g", blockinput_data) - # trim all leading/trailing newlines - blockinput_data=re.sub(r"(\A\n+|\n+\Z)", "", blockinput_data) - filename="clithemeinfo_"+current_header_entry+"_v2" - if current_header_entry=="description": - filename="clithemeinfo_"+current_header_entry - write_infofile( \ - path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ - filename,\ - blockinput_data,current_header_linenumber,current_header_entry) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 - # clear data - current_header_entry="" - current_header_linenumber=-1 - else: # the unlikely case - handle_error(fd.feof("internal-error-blockinput", "Line {num}: internal error while handling block input; please file a bug report", num=str(linenumber))) - # clear data - blockinput=False - blockinput_data="" - else: - if blockinput_data!="": blockinput_data+="\n" - line_content=line.strip() - if line_content=="": # empty line - if blockinput_data=="": blockinput_data+=" " - continue - # Calculate whitespaces - spaces=-1 + # defined sub-processing functions + def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block") -> str: + nonlocal lineindex + minspaces=0 + blockinput_data="" + while lineindex3: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - # sanity check - if _globalvar.sanity_check(phrases[1]+" "+phrases[2])==False: - handle_error(fd.feof("sanity-check-domainapp-err", "Line {num}: domain and app names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) - current_domainapp=phrases[1]+" "+phrases[2] - current_subsection="" - elif phrases[0]=="in_subsection": - if len(phrases)<2: - handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) - # sanity check - if _globalvar.sanity_check(splitarray_to_string(phrases[1:]))==False: - handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) - current_subsection=splitarray_to_string(phrases[1:]) - elif phrases[0]=="unset_domainapp": - if len(phrases)!=1: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - current_domainapp="" - current_subsection="" - elif phrases[0]=="unset_subsection": - if len(phrases)!=1: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - current_subsection="" - elif phrases[0]=="end_main": - if len(phrases)!=1: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - current_status="" - mainparsed=True - else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(linenumber))) - elif current_status=="entry": # expect locale, end_entry + leading_whitespace=ws_match.group() + leading_whitespace=re.sub(r"\t", " "*8, leading_whitespace) + # update line content + # replace \end_block with end_block + line=leading_whitespace+re.sub(r"^\\([\\]*)end_block", r"\g<1>end_block", line.strip()) + # update minspaces + minspaces=min(minspaces, len(leading_whitespace)) + else: # don't preserve whitespaces + line=re.sub(r"^\\([\\]*)end_block", r"\g<1>end_block", line.strip()) + # write to data + blockinput_data+="\n"+line + # remove the extra leading newline + blockinput_data=re.sub(r"\A\n", "", blockinput_data) + # remove all whitespaces except common minspaces (if preserve_indents) + if preserve_indents: + pattern=r"(?P\n|^)[ ]{"+str(minspaces)+"}" + blockinput_data=re.sub(pattern,r"\g", blockinput_data) + return blockinput_data + def handle_entry(entry_name: str): + # expect locale, locale_block, end_entry + nonlocal lineindex + while lineindex0 and phrases[0]=="end_block": + if blockinput_minspaces!=-1: + # process whitespaces + # trim amount of leading whitespaces on each line + pattern=r"(?P\n|^)[ ]{"+str(blockinput_minspaces)+"}" + blockinput_data=re.sub(pattern,r"\g", blockinput_data) + if current_status=="entry": + for this_locale in current_entry_locale.split(): + target_entry=current_entry_name + if this_locale!="default": + target_entry+="__"+this_locale + add_entry(datapath,target_entry, blockinput_data, current_entry_linenumber) + # clear data + current_entry_locale="" + current_entry_linenumber=-1 + elif current_status=="header": + if current_header_entry!="description": + # trim all leading whitespaces + blockinput_data=re.sub(r"(?P\n|^)[ ]+",r"\g", blockinput_data) + # trim all trailing whitespaces + blockinput_data=re.sub(r"[ ]+(?P\n|$)",r"\g", blockinput_data) + # trim all leading/trailing newlines + blockinput_data=re.sub(r"(\A\n+|\n+\Z)", "", blockinput_data) + filename="clithemeinfo_"+current_header_entry+"_v2" + if current_header_entry=="description": + filename="clithemeinfo_"+current_header_entry + write_infofile( \ + path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ + filename,\ + blockinput_data,current_header_linenumber,current_header_entry) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 + # clear data + current_header_entry="" + current_header_linenumber=-1 + else: # the unlikely case + handle_error(fd.feof("internal-error-blockinput", "Line {num}: internal error while handling block input; please file a bug report", num=str(linenumber))) + # clear data + blockinput=False + blockinput_data="" + else: + if blockinput_data!="": blockinput_data+="\n" + line_content=line.strip() + if line_content=="": # empty line + if blockinput_data=="": blockinput_data+=" " + continue + # Calculate whitespaces + spaces=-1 + ws_match=re.search(r"^\s+", line) # match leading whitespaces + if ws_match==None: # no leading spaces + spaces=0 + else: + leading_whitespace=ws_match.group() + # substitute \t with 8 spaces + leading_whitespace=re.sub(r"\t"," "*8, leading_whitespace) + # append it to line_content + line_content=leading_whitespace+line_content + # write line_content to data + blockinput_data+=line_content + spaces=len(leading_whitespace) + # update min count + if spaces!=-1 and (spaces3: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) + # sanity check + if _globalvar.sanity_check(phrases[1]+" "+phrases[2])==False: + handle_error(fd.feof("sanity-check-domainapp-err", "Line {num}: domain and app names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) + current_domainapp=phrases[1]+" "+phrases[2] + current_subsection="" + elif phrases[0]=="in_subsection": + if len(phrases)<2: + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) + # sanity check + if _globalvar.sanity_check(splitarray_to_string(phrases[1:]))==False: + handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) + current_subsection=splitarray_to_string(phrases[1:]) + elif phrases[0]=="unset_domainapp": + if len(phrases)!=1: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) + current_domainapp="" + current_subsection="" + elif phrases[0]=="unset_subsection": + if len(phrases)!=1: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) + current_subsection="" + elif phrases[0]=="end_main": + if len(phrases)!=1: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) + current_status="" + mainparsed=True + else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(linenumber))) + elif current_status=="entry": # expect locale, end_entry + if phrases[0]=="locale": + if len(phrases)<3: + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) + content=splitarray_to_string(phrases[2:]) + target_entry=current_entry_name + if phrases[1]!="default": + target_entry+="__"+phrases[1] + add_entry(datapath,target_entry,content,linenumber) + elif phrases[0]=="locale_block": + if len(phrases)<2: + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) + current_entry_locale=splitarray_to_string(phrases[1:]) + current_entry_linenumber=linenumber + blockinput=True # start block input + elif phrases[0]=="end_entry": + if len(phrases)!=1: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) + current_status="main" + current_entry_name="" + else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(linenumber))) + if not headerparsed or not mainparsed: + handle_error(fd.reof("incomplete-block-err", "Missing or incomplete header or main block")) + # Update current theme index + theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") + theme_index.write(custom_infofile_name+"\n") + return True # Everything is successful! :) + +try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() +except: + if _version.release==0: print("generator set_local_themedef failed") + pass \ No newline at end of file diff --git a/src/clitheme/_generator/generator_v2.py b/src/clitheme/_generator/generator_v2.py deleted file mode 100644 index 328006e..0000000 --- a/src/clitheme/_generator/generator_v2.py +++ /dev/null @@ -1,276 +0,0 @@ -""" -Generator function used in applying themes (should not be invoked directly) -""" -import os -import string -import random -import re -try: - from .. import _globalvar - from .. import frontend - from .. import _version - from .. import _get_resource -except ImportError: # for test program - import _globalvar - import frontend - import _version - import _get_resource - -fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") - -path="" -silence_warn=False - -def handle_error(message): - raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) -def handle_warning(message): - if not silence_warn: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) -def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate directories (excluding file itself) - current_path=path - current_entry="" # for error output - for x in entry_name.split()[:-1]: - current_entry+=x+" " - current_path+="/"+x - if os.path.isfile(current_path): # conflict with entry file - handle_error(fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ - num=str(line_number_debug), name=current_entry)) - elif os.path.isdir(str(current_path))==False: # directory does not exist - os.mkdir(current_path) -def add_entry(path, entry_name, entry_content, line_number_debug): # add entry to where it belongs (assuming recursive_mkdir already completed) - target_path=path - for x in entry_name.split(): - target_path+="/"+x - if os.path.isdir(target_path): - handle_error(fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ - num=str(line_number_debug), name=entry_name)) - elif os.path.isfile(target_path): - handle_warning(fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ - num=str(line_number_debug), name=entry_name)) - f=open(target_path,'w', encoding="utf-8") - f.write(entry_content+"\n") -def splitarray_to_string(split_content): - final="" - for phrase in split_content: - final+=phrase+" " - return final.strip() -def write_infofile(path,filename,content,line_number_debug, header_name_debug): - if not os.path.isdir(path): - os.makedirs(path) - target_path=path+"/"+filename - if os.path.isfile(target_path): - handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) - f=open(target_path,'w', encoding="utf-8") - f.write(content+'\n') - -def write_infofile_newlines(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 - if os.path.isfile(target_path): - handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) - f=open(target_path,'w', encoding="utf-8") - for line in content_phrases: - f.write(line+"\n") - -def generate_custom_path(): - # Generate a temporary path - global path - path=_globalvar.clitheme_temp_root+"/clitheme-temp-" - for x in range(8): - path+=random.choice(string.ascii_letters) - -def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1"): - # make directories - if custom_path_gen: - generate_custom_path() - if not os.path.exists(path): os.mkdir(path) - datapath=path+"/"+_globalvar.generator_data_pathname - if not os.path.exists(datapath): os.mkdir(datapath) - - # data to keep track of - headerparsed=False - mainparsed=False - lines_data=file_content.splitlines() - lineindex=-1 # counter extra +1 operation at beginning - - # define check functions - def check_enough_args(phrases: list[str], count: int): - if len(phrases)count - if not_pass: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=phrases[0])) - - # defined sub-processing functions - def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block") -> str: - nonlocal lineindex - minspaces=0 - blockinput_data="" - while lineindexend_block", line.strip()) - # update minspaces - minspaces=min(minspaces, len(leading_whitespace)) - else: # don't preserve whitespaces - line=re.sub(r"^\\([\\]*)end_block", r"\g<1>end_block", line.strip()) - # write to data - blockinput_data+="\n"+line - # remove the extra leading newline - blockinput_data=re.sub(r"\A\n", "", blockinput_data) - # remove all whitespaces except common minspaces (if preserve_indents) - if preserve_indents: - pattern=r"(?P\n|^)[ ]{"+str(minspaces)+"}" - blockinput_data=re.sub(pattern,r"\g", blockinput_data) - return blockinput_data - def handle_entry(entry_name: str): - # expect locale, locale_block, end_entry - nonlocal lineindex - while lineindex Date: Sun, 7 Apr 2024 21:57:24 +0800 Subject: [PATCH 034/354] Fix missing entry of generate-data-msg --- src/clitheme/strings/cli-strings.clithemedef.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 5f2e67e..948884a 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -33,7 +33,7 @@ in_domainapp swiftycode clitheme # apply-theme 和 generate-data 指令 in_subsection cli apply-theme entry generate-data-msg - locale zh_CN test + locale zh_CN 主题定义数据将会从以下顺序的主题定义文件生成: end_entry entry apply-theme-msg locale zh_CN 这些主题定义文件将会通过以下顺序被应用: @@ -132,4 +132,4 @@ in_domainapp swiftycode clitheme entry supported-apps-str locale zh_CN 支持的应用程序: end_entry -end_main \ No newline at end of file +end_main -- Gitee From 51f8e33b8efeb8307c7d7ea91e42d9f4ba682e89 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 7 Apr 2024 21:57:34 +0800 Subject: [PATCH 035/354] Update version (v1.2-dev20240407) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index c24f834..1ee0ebc 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='1.2_dev20240307' +pkgver='1.2_dev20240407' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 833523c..eb132e0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240307-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240407-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Thu, 07 Mar 2024 23:59:00 +0800 + -- swiftycode <3291929745@qq.com> Sun, 07 Apr 2024 21:41:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 33b3e4b..b6b199e 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240307" +__version__="1.2-dev20240407" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240307" +version_main="1.2_dev20240407" version_buildnumber=1 \ No newline at end of file -- Gitee From b573eda6383a278f025229f6afac14edcc99ff86 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 8 Apr 2024 09:30:05 +0800 Subject: [PATCH 036/354] Merge comment/empty line check into is_ignore_line --- src/clitheme/_generator/__init__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 7b52d63..5a1e453 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -105,6 +105,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: not_pass=len(phrases)>count if not_pass: handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=phrases[0])) + def is_ignore_line() -> bool: + return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') # defined sub-processing functions def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block") -> str: @@ -148,8 +150,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info nonlocal lineindex while lineindex Date: Mon, 8 Apr 2024 09:31:03 +0800 Subject: [PATCH 037/354] Add warning message to generator_old --- src/clitheme/_generator/generator_old.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/generator_old.py b/src/clitheme/_generator/generator_old.py index c883123..3cc3f9e 100644 --- a/src/clitheme/_generator/generator_old.py +++ b/src/clitheme/_generator/generator_old.py @@ -1,5 +1,5 @@ """ -Generator function used in applying themes (should not be invoked directly) +OBSOLETE generator functions; --Do not use-- """ import os import string -- Gitee From d647e186bf0b8ffe7fb0d5847d8d90ee47ba2667 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 8 Apr 2024 13:42:49 +0800 Subject: [PATCH 038/354] Support lead indent and substesc options in block input --- src/clitheme-testblock_testprogram.py | 6 +-- src/clitheme/_generator/__init__.py | 61 ++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/clitheme-testblock_testprogram.py b/src/clitheme-testblock_testprogram.py index 48b441c..19e918f 100644 --- a/src/clitheme-testblock_testprogram.py +++ b/src/clitheme-testblock_testprogram.py @@ -23,7 +23,7 @@ begin_main - end_block + end_block leadtabindents:1 locale_block zh_CN @@ -36,7 +36,7 @@ begin_main should have leading 3 lines and trailing 2 lines - end_block + end_block leadspaces:4 end_entry end_main """ @@ -59,7 +59,7 @@ for lang in ["C", "en", "en_US", "zh_CN"]: f.disable_lang=True name=f"test_entry__{lang}" if f.entry_exists(name): - print(f"{name} OK") + print(f"{name} found") else: print(f"{name} not found") diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 5a1e453..c34e019 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -5,6 +5,7 @@ import os import string import random import re +import math try: from .. import _globalvar from .. import frontend @@ -94,6 +95,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info mainparsed=False lines_data=file_content.splitlines() lineindex=-1 # counter extra +1 operation at beginning + options={} # define check functions def check_enough_args(phrases: list[str], count: int): @@ -109,9 +111,41 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') # defined sub-processing functions - def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block") -> str: + def parse_options(options_data: str, merge_global_options: bool) -> dict: + # value options: leadtabindents, leadspaces + value_options=["leadtabindents", "leadspaces"] + # on/off options: substesc, strictcmdmatch, exactcmdmatch (use no<...> to disable) + bool_options=["substesc", "strictcmdmatch", "exactcmdmatch"] + final_options={} + if merge_global_options: nonlocal options; final_options=options + if len(options_data.split())==0: return {} + for each_option in options_data.split(): + option_name=re.sub(r"^(no)*(?P.+?)(:.+)*$", r"\g", each_option) + option_name_preserve_no=re.sub(r"^(?P.+?)(:.+)*$", r"\g", each_option) + if option_name in value_options: + # must not begin with no + if option_name_preserve_no.startswith("no"): + handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) + # get value + results=re.search(r"^(?P.+?):(?P.+)+$", each_option) + value: int + if results==None: # no value specified + handle_error(fd.feof("option-without-value-err", "No value specified for option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name)) + else: + try: value=int(results.groupdict()['value']) + except ValueError: handle_error(fd.feof("option-value-not-int-err", "The value specified for option \"{phrase}\" is not an integer on line {num}", num=str(lineindex+1), phrase=option_name)) + # set option + final_options[option_name]=value + elif option_name in bool_options: + # if starts with no, set to false; else, set to true + final_options[option_name]=not option_name_preserve_no.startswith("no") + else: + handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) + return final_options + + def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True) -> str: nonlocal lineindex - minspaces=0 + minspaces=math.inf blockinput_data="" while lineindexend_block", line.strip()) + line=leading_whitespace+re.sub(r"^\\([\\]*)"+end_phrase, r"\g<1>"+end_phrase, line.strip()) # update minspaces minspaces=min(minspaces, len(leading_whitespace)) else: # don't preserve whitespaces - line=re.sub(r"^\\([\\]*)end_block", r"\g<1>end_block", line.strip()) + line=re.sub(r"^\\([\\]*)"+end_phrase, r"\g<1>"+end_phrase, line.strip()) # write to data blockinput_data+="\n"+line # remove the extra leading newline @@ -143,7 +177,24 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # remove all whitespaces except common minspaces (if preserve_indents) if preserve_indents: pattern=r"(?P\n|^)[ ]{"+str(minspaces)+"}" - blockinput_data=re.sub(pattern,r"\g", blockinput_data) + blockinput_data=re.sub(pattern,r"\g", blockinput_data, flags=re.MULTILINE) + # parse leadtabindents leadspaces, and substesc options + if len(lines_data[lineindex].split())>1: + got_options=parse_options(splitarray_to_string(lines_data[lineindex].split()[1:]), merge_global_options=True) + for option in got_options.keys(): + if option=="leadtabindents": + if not preserve_indents and option not in options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) + # insert tabs at start of each line + blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) + elif option=="leadspaces": + if not preserve_indents and option not in options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) + # insert spaces at start of each line + blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) + elif option=="substesc": + # substitute {{ESC}} with escape literal + if got_options['substesc']==True: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) + elif disallow_cmdmatch_options: + handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) return blockinput_data def handle_entry(entry_name: str): # expect locale, locale_block, end_entry -- Gitee From 4ec0ebb5336dc4f75f52ce0e27f8ead08998000c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 8 Apr 2024 20:00:58 +0800 Subject: [PATCH 039/354] Change argument type of parse_options to list[str] --- src/clitheme/_generator/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index c34e019..1743285 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -6,6 +6,7 @@ import string import random import re import math +import copy try: from .. import _globalvar from .. import frontend @@ -111,15 +112,15 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') # defined sub-processing functions - def parse_options(options_data: str, merge_global_options: bool) -> dict: + def parse_options(options_data: list[str], merge_global_options: bool) -> dict: # value options: leadtabindents, leadspaces value_options=["leadtabindents", "leadspaces"] # on/off options: substesc, strictcmdmatch, exactcmdmatch (use no<...> to disable) bool_options=["substesc", "strictcmdmatch", "exactcmdmatch"] final_options={} - if merge_global_options: nonlocal options; final_options=options - if len(options_data.split())==0: return {} - for each_option in options_data.split(): + if merge_global_options: nonlocal options; final_options=copy.copy(options) + if len(options_data)==0: return {} + 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) if option_name in value_options: @@ -180,7 +181,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info blockinput_data=re.sub(pattern,r"\g", blockinput_data, flags=re.MULTILINE) # parse leadtabindents leadspaces, and substesc options if len(lines_data[lineindex].split())>1: - got_options=parse_options(splitarray_to_string(lines_data[lineindex].split()[1:]), merge_global_options=True) + got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True) for option in got_options.keys(): if option=="leadtabindents": if not preserve_indents and option not in options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) -- Gitee From f0833a4e63ba49f887440be7ef85214cee06fc96 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 8 Apr 2024 21:10:35 +0800 Subject: [PATCH 040/354] Add set_local_themedef overlay option testing --- src/clitheme-testblock_testprogram.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/clitheme-testblock_testprogram.py b/src/clitheme-testblock_testprogram.py index 19e918f..5ab9bc6 100644 --- a/src/clitheme-testblock_testprogram.py +++ b/src/clitheme-testblock_testprogram.py @@ -24,6 +24,17 @@ begin_main end_block leadtabindents:1 + end_entry +end_main +""" + +file_data_2=""" +begin_header + name untitled +end_header + +begin_main + entry test_entry locale_block zh_CN @@ -45,6 +56,9 @@ frontend.global_debugmode=True if frontend.set_local_themedef(file_data)==False: print("Error: set_local_themedef failed") exit(1) +if frontend.set_local_themedef(file_data_2, overlay=True)==False: # test overlay function + print("Error: set_local_themedef failed") + exit(1) f=frontend.FetchDescriptor() print("Default locale:") f.disable_lang=True @@ -64,9 +78,10 @@ for lang in ["C", "en", "en_US", "zh_CN"]: print(f"{name} not found") import sys -if sys.argv.__contains__("--preserve-temp"): +if "--preserve-temp" in sys.argv: print(f"View generated data at {_generator.path}") exit() import shutil -shutil.rmtree(_generator.path) \ No newline at end of file +try: shutil.rmtree(_generator.path) +except: print("Warning: failed to remove temp directory \"{}\"".format(_generator.path)) \ No newline at end of file -- Gitee From 9768f7087bf92c9d91ca92204d4b47070adf4ae1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 8 Apr 2024 21:46:00 +0800 Subject: [PATCH 041/354] Add substesc support for non-block entry content; Fix and improve global option handling. --- src/clitheme/_generator/__init__.py | 45 +++++++++++++++++------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 1743285..72cd096 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -96,7 +96,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info mainparsed=False lines_data=file_content.splitlines() lineindex=-1 # counter extra +1 operation at beginning - options={} + global_options={} # define check functions def check_enough_args(phrases: list[str], count: int): @@ -118,8 +118,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # on/off options: substesc, strictcmdmatch, exactcmdmatch (use no<...> to disable) bool_options=["substesc", "strictcmdmatch", "exactcmdmatch"] final_options={} - if merge_global_options: nonlocal options; final_options=copy.copy(options) - if len(options_data)==0: return {} + if merge_global_options: nonlocal global_options; final_options=copy.copy(global_options) + if len(options_data)==0: return final_options # return either empty data or pre-existing global options 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) @@ -143,7 +143,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) return final_options - + def handle_set_global_options(options_data: list[str]): + # set options globally + nonlocal global_options; global_options=parse_options(options_data, merge_global_options=True) def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True) -> str: nonlocal lineindex minspaces=math.inf @@ -180,22 +182,23 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info pattern=r"(?P\n|^)[ ]{"+str(minspaces)+"}" blockinput_data=re.sub(pattern,r"\g", blockinput_data, flags=re.MULTILINE) # parse leadtabindents leadspaces, and substesc options + got_options=copy.copy(global_options) if len(lines_data[lineindex].split())>1: got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True) - for option in got_options.keys(): - if option=="leadtabindents": - if not preserve_indents and option not in options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) - # insert tabs at start of each line - blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) - elif option=="leadspaces": - if not preserve_indents and option not in options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) - # insert spaces at start of each line - blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) - elif option=="substesc": - # substitute {{ESC}} with escape literal - if got_options['substesc']==True: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) - elif disallow_cmdmatch_options: - handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) + for option in got_options.keys(): + if option=="leadtabindents": + if not preserve_indents and option not in global_options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) + # insert tabs at start of each line + blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) + elif option=="leadspaces": + if not preserve_indents and option not in global_options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) + # insert spaces at start of each line + blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) + elif option=="substesc": + # substitute {{ESC}} with escape literal + if got_options['substesc']==True: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) + elif disallow_cmdmatch_options: + handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) return blockinput_data def handle_entry(entry_name: str): # expect locale, locale_block, end_entry @@ -208,6 +211,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info check_enough_args(phrases, 3) content=splitarray_to_string(phrases[2:]) target_entry=entry_name + # substesc + if "substesc" in global_options.keys() and global_options['substesc']==True: + content=re.sub(r"{{ESC}}", '\x1b', content) if phrases[1]!="default": target_entry+="__"+phrases[1] add_entry(datapath, target_entry, content, lineindex+1) @@ -315,6 +321,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if domainapp!="": entry_name=domainapp+" "+entry_name recursive_mkdir(datapath, entry_name, lineindex+1) handle_entry(entry_name) + elif phrases[0]=="set_options": + check_enough_args(phrases, 2) + handle_set_global_options(phrases[1:]) elif phrases[0]=="end_main": check_extra_args(phrases, 1, use_exact_count=True) mainparsed=True -- Gitee From 0c48cf0d504ccaf44d6f2546deb147301756f02a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 8 Apr 2024 21:47:55 +0800 Subject: [PATCH 042/354] Add test for global options handling --- src/clitheme-testblock_testprogram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme-testblock_testprogram.py b/src/clitheme-testblock_testprogram.py index 5ab9bc6..4c2a162 100644 --- a/src/clitheme-testblock_testprogram.py +++ b/src/clitheme-testblock_testprogram.py @@ -9,6 +9,7 @@ begin_header end_header begin_main + set_options leadtabindents:1 entry test_entry locale_block default en_US en C @@ -23,7 +24,7 @@ begin_main - end_block leadtabindents:1 + end_block end_entry end_main """ -- Gitee From d30e5e185406458e27fec2ce64495654095c1932 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 8 Apr 2024 21:50:44 +0800 Subject: [PATCH 043/354] Update version (v1.2-dev20240408) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 1ee0ebc..04beb80 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='1.2_dev20240407' +pkgver='1.2_dev20240408' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index eb132e0..edfef58 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240407-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240408-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sun, 07 Apr 2024 21:41:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, 08 Apr 2024 21:49:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index b6b199e..0683db2 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240407" +__version__="1.2-dev20240408" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240407" +version_main="1.2_dev20240408" version_buildnumber=1 \ No newline at end of file -- Gitee From b0de90b0fe2e10becdf1d62c8077e596a8077292 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 9 Apr 2024 21:52:31 +0800 Subject: [PATCH 044/354] Implement strictcmdmatch, exactcmdmatch, and endmatchhere in db_interface --- src/clitheme/_generator/db_interface.py | 78 +++++++++++++++++-------- src/db_interface_tests.py | 25 +++++--- 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index a8c467c..c98c35d 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -1,6 +1,7 @@ import sys import sqlite3 import re +import copy from typing import Optional try: from .. import _globalvar except ImportError: import _globalvar @@ -10,15 +11,18 @@ def init_db(file_path: str): global connection connection=sqlite3.connect(file_path) # create the table + # command_match_strictness: 0:default match options, 1:must start with pattern, 2: must exactly equal pattern connection.execute(f"CREATE TABLE {_globalvar.db_data_tablename} ( \ match_pattern TEXT NOT NULL, \ substitute_pattern TEXT NOT NULL, \ is_regex INTEGER DEFAULT true NOT NULL, \ - effective_command TEXT \ + effective_command TEXT, \ + command_match_strictness INTEGER DEFAULT 0 NOT NULL, \ + end_match_here INTEGER DEFAULT 0 NOT NULL \ );") connection.commit() -def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], is_regex: bool=True): +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, line_number_debug: int=-1): global cursor cmdlist=[] if effective_commands!=None and len(effective_commands)>0: @@ -26,51 +30,79 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma # remove extra spaces in the command cmdlist.append(re.sub(r" {2,}", " ", cmd).strip()) else: + # remove any existing values with the same match_pattern if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null);", (match_pattern.strip(),)).fetchall())>0: - print("Warning: Repeated entry at line %, overwriting") + print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null);", (match_pattern.strip(),)) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, is_regex) VALUES (?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), is_regex)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, is_regex, command_match_strictness, end_match_here) VALUES (?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), is_regex, command_match_strictness, end_match_here)) for cmd in cmdlist: - # remove any existing values with the match_pattern and effective_command, if any - if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?;", (match_pattern.strip(),cmd)).fetchall())>0: - print("Warning: Repeated entry at line %, overwriting") - connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?;", (match_pattern.strip(),cmd)) + # remove any existing values with the same match_pattern and effective_command and command_match_strictness(if ==2) + extra_condition="" + if command_match_strictness==2: extra_condition=" AND command_match_strictness=2" + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?{extra_condition};", (match_pattern.strip(),cmd)).fetchall())>0: + print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") + connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?{extra_condition};", (match_pattern.strip(),cmd)) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex) VALUES (?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd, is_regex)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here) VALUES (?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd, is_regex, command_match_strictness, end_match_here)) connection.commit() def match_content(content: bytes, command: Optional[str]=None) -> bytes: + # 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 - target_command=None + final_cmdlist=[] + final_cmdlist_strictmatch=[] if command!=None and len(command.split())>0: - cmdlist=connection.execute(f"SELECT DISTINCT effective_command FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ?;", (command.split()[0].strip()+" %",)).fetchall() - # also include one-phrase effective commands - cmdlist+=connection.execute(f"SELECT DISTINCT effective_command FROM {_globalvar.db_data_tablename} WHERE effective_command=?;", (command.split()[0].strip(),)).fetchall() - # sort by command length (greatest to least) - cmdlist.sort(key=len, reverse=True) + # obtain a list of effective_command with the same first term + cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ?;", (command.split()[0].strip()+" %",)).fetchall() + # also include one-phrase commands + cmdlist+=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=?;", (command.split()[0].strip(),)).fetchall() + # sort by number of phrases (greatest to least) + def split_len(obj: tuple) -> int: return len(obj[0].split()) + cmdlist.sort(key=split_len, reverse=True) + # prioritize effective_command with exact match requirement + cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2", (command.strip(),)).fetchall()+cmdlist # attempt to find matching command for tp in cmdlist: cmd=tp[0] # extract value from tuple + strictness=tp[1] # strictness setting success=True - for phrase in cmd.split(): - if phrase not in command.split(): - success=False; break + if strictness==1: # must start with pattern + if not command.startswith(cmd): success=False + elif strictness==2: # must equal to pattern + if not command==cmd: success=False + else: # implying strictness==0; must contain all phrases in pattern + for phrase in cmd.split(): + if phrase not in command.split(): + success=False; break if success: # if found matching command - target_command=cmd + if cmd not in final_cmdlist: final_cmdlist.append(cmd) + final_cmdlist_strictmatch.append(strictness==2) break - content_str=content + content_str=copy.copy(content) matches=[] - if target_command!=None: matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE effective_command=? ORDER BY rowid;", (target_command,)).fetchall() - matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex FROM {_globalvar.db_data_tablename} WHERE typeof(effective_command)=typeof(null) ORDER BY rowid;").fetchall() + if len(final_cmdlist)>0: + for x in range(len(final_cmdlist)): + cmd=final_cmdlist[x] + # prioritize exact match + if final_cmdlist_strictmatch[x]==True: matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2 ORDER BY rowid;", (cmd,)).fetchall() + matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE effective_command=? ORDER BY rowid;", (cmd,)).fetchall() + matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE typeof(effective_command)=typeof(null) ORDER BY rowid;").fetchall() for match_data in matches: try: if match_data[2]==True: # is regex content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) - else: + else: # is string content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) except: print("Error occurred while matching string: ", end="") print(sys.exc_info()[1]) + if match_data[3]==True: # endmatchoption is set + break return content_str \ No newline at end of file diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index f1598bc..bc479a1 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -12,20 +12,27 @@ sample_inputs=[("rm: missing operand", "rm"), ("ls: unrecognized option '--help'", "ls --help"), ("Warning: invaild input", "input anything"), ("Error: invaild input ","input anything"), # test extra spaces + ("Error: sample message", "example_app --this install-stuff"), # test strictcmdmatch (substitution should not happen) + ("Error: sample message", "example_app install-stuff --this"), # test strictcmdmatch and endmatchhere options + ("rm: : Permission denied", "rm -rf") # test exactcmdmatch (substitution rule containing this option should be prioritized over previous rules) ] # substitute patterns -subst_patterns=[("rm: missing operand", "rm says: missing arguments and options (>﹏<)", False, ["rm"]), - ("type rm --help for more information", "For more information, use rm --help (。ì _ í。)", False, ["rm"]), - (r"(?P.+): (?P.+): Permission denied",r"""\g says: Access denied to \g! ಥ_ಥ""",True, ["rm -rf", "cat", "cd", "ls"]), - (r"(?P.+): unrecognized option '(?P.+)'",r"""wef""",True, ["ls"]), # testing repeated entry detection - (r"(?P.+): unrecognized option '(?P.+)'",r"""\g says: option '\g' not known! (ToT)/~~~'""",True, ["ls"]), - (r"^Warning:( )", r"o(≧v≦)o Note:\g<1>",True , None), - (r"^Error:( )", r"(ToT)/~~~ Error:\g<1>",True, None), - (r"invaild input( ){0,}$", r"input is invaild! ಥ_ಥ", True, None)] +# (match_pattern, substitute_pattern, is_regex, effective_commands, command_match_strictness, end_match_here) +subst_patterns=[("rm: missing operand", "rm says: missing arguments and options (>﹏<)", False, ["rm"], 0, False), + ("type rm --help for more information", "For more information, use rm --help (。ì _ í。)", False, ["rm"], 0, False), + (r"(?P.+): (?P.+): Permission denied",r"""\g says: Access denied to \g! ಥ_ಥ""",True, ["rm -rf", "cat", "cd", "ls"], 0, False), + (r"(?P.+): unrecognized option '(?P.+)'",r"""wef""",True, ["ls"], 0, False), # testing repeated entry detection + (r"(?P.+): unrecognized option '(?P.+)'",r"""\g says: option '\g' not known! (ToT)/~~~'""",True, ["ls"], 0, False), + (r"^Warning:( )", r"o(≧v≦)o Note:\g<1>",True , None, 0, False), + (r"^Error:( )", r"(ToT)/~~~ Error:\g<1>",True, None, 0, False), + (r"invaild input( ){0,}$", r"input is invaild! ಥ_ಥ", True, None, 0, False), + ("Error: sample message", "Error: sample message! (>﹏<)", False, ["example_app install-stuff"], 1, True), + (r"(?P.+): (?P.+): Permission denied",r"""\g says: Missing argument for operation! ಥ_ಥ""",True, ["rm -rf"], 2, False), +] db_interface.init_db(":memory:") # record substitute patterns -for dat in subst_patterns: db_interface.add_subst_entry(match_pattern=dat[0], substitute_pattern=dat[1], effective_commands=dat[3], is_regex=dat[2]) +for dat in subst_patterns: db_interface.add_subst_entry(match_pattern=dat[0], substitute_pattern=dat[1], effective_commands=dat[3], is_regex=dat[2], command_match_strictness=dat[4], end_match_here=dat[5]) print("Successfully recorded data\nTesting sample outputs: ") for inp in sample_inputs: print(db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8')) -- Gitee From fe8e84df512510e8982354cc5168e95f8960f82f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 9 Apr 2024 22:02:54 +0800 Subject: [PATCH 045/354] Update version (v1.2-dev20240409) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 04beb80..1b90706 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='1.2_dev20240408' +pkgver='1.2_dev20240409' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index edfef58..7451257 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240408-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240409-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 08 Apr 2024 21:49:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 09 Apr 2024 22:01:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 0683db2..1b0d5f5 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240408" +__version__="1.2-dev20240409" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240408" +version_main="1.2_dev20240409" version_buildnumber=1 \ No newline at end of file -- Gitee From 34972a4c1c46be7feffdc499d11f2fc4ed7ccb52 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 13 Apr 2024 21:31:44 +0800 Subject: [PATCH 046/354] apply-theme: properly handle EOF sequence at confirm prompt --- 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 8f7057c..ca051ad 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -244,7 +244,7 @@ def main(cli_args): print(fi.reof("overlay-notice", "The definition files will be appended on top of the existing theme data.")) inpstr=fi.reof("confirm-prompt", "Do you want to continue? [y/n]") try: inp=input(inpstr+" ").strip().lower() - except KeyboardInterrupt: print();return 1 + except (KeyboardInterrupt, EOFError): print();return 130 if not (inp=="y" or inp=="yes"): return 1 content_list=[] -- Gitee From c9dd55b3fa800064b332882ea0d32f8e9c246ebf Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 13 Apr 2024 21:48:28 +0800 Subject: [PATCH 047/354] Improve arguments checking (less/more) in cli --- src/clitheme/cli.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index ca051ad..36d1191 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -214,9 +214,18 @@ def main(cli_args): print(f.reof("no-command", "Error: no command or option specified")) return 1 + def check_enough_args(count: int, exclude_options: bool=True): + c=0 + 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)) + if cli_args[1]=="apply-theme" or cli_args[1]=="generate-data" or cli_args[1]=="generate-data-hierarchy": - if len(cli_args)<3: - return handle_usage_error(f.reof("not-enough-arguments", "Error: not enough arguments"), arg_first) + check_enough_args(3) generate_only=(cli_args[1]=="generate-data" or cli_args[1]=="generate-data-hierarchy") paths=[] overlay=False @@ -258,17 +267,17 @@ def main(cli_args): return 1 return apply_theme(content_list, overlay=overlay, preserve_temp=preserve_temp, generate_only=generate_only) elif cli_args[1]=="get-current-theme-info": - if len(cli_args)>2: # disabled additional options - return handle_usage_error(f.reof("too-many-arguments", "Error: too many arguments"), arg_first) + check_extra_args(2) # disabled additional options return get_current_theme_info() elif cli_args[1]=="unset-current-theme": - if len(cli_args)>2: - return handle_usage_error(f.reof("too-many-arguments", "Error: too many arguments"), arg_first) + check_extra_args(2) return unset_current_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": + check_extra_args(2) print(usage_description.format(arg_first)) else: return handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=cli_args[1]), arg_first) -- Gitee From 0d25e4efc8e16e46b1381ee5d62175705b1f7318 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 13 Apr 2024 22:08:29 +0800 Subject: [PATCH 048/354] Add support for new syntax format --- .../example-theme-textemojis.clithemedef.txt | 101 ++++---- src/clitheme/_generator/__init__.py | 68 ++++-- .../strings/cli-strings.clithemedef.txt | 222 +++++++++--------- .../strings/generator-strings.clithemedef.txt | 117 +++++---- .../clithemedef-test_mainfile.clithemedef.txt | 158 ++++++------- 5 files changed, 343 insertions(+), 323 deletions(-) diff --git a/example-clithemedef/example-theme-textemojis.clithemedef.txt b/example-clithemedef/example-theme-textemojis.clithemedef.txt index afd87ce..b43d20f 100644 --- a/example-clithemedef/example-theme-textemojis.clithemedef.txt +++ b/example-clithemedef/example-theme-textemojis.clithemedef.txt @@ -1,68 +1,67 @@ -begin_header +{header_section} name 颜文字样例主题 version 1.0 # testing block input - locales_block + [locales] Simplified Chinese 简体中文 zh_CN - end_block - supported_apps_block + [/locales] + [supported_apps] clitheme example clitheme 样例应用 clitheme_example - end_block - description_block + [/supported_apps] + [description] 适配项目中提供的example程序的一个颜文字主题,把它的输出变得可爱。 应用这个主题,沉浸在颜文字的世界中吧! 不要小看我的年龄,人家可是非常萌的~! - end_block + [/description] +{/header_section} -end_header - -begin_main +{entries_section} in_domainapp com.example example-app - entry found-file - locale default o(≧v≦)o 太好了,在当前目录找到了{}个文件! - locale zh_CN o(≧v≦)o 太好了,在当前目录找到了{}个文件! - end_entry - entry installing-file - locale default (>^ω^<) 正在安装 "{}"... (>^ω^<) - locale zh_CN (>^ω^<) 正在安装 "{}"... (>^ω^<) - end_entry - entry install-success - locale default o(≧v≦)o 已成功安装{}个文件! - locale zh_CN o(≧v≦)o 已成功安装{}个文件! - end_entry - entry install-success-file - locale default o(≧v≦)o 已成功安装"{}"! - locale zh_CN o(≧v≦)o 已成功安装"{}"! - end_entry - entry file-not-found - locale default ಥ_ಥ 糟糕,出错啦!找不到文件 "{}"!呜呜呜~ - locale zh_CN ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" !呜呜呜~ - end_entry - entry format-error - locale default ಥ_ಥ 糟糕,命令语法不正确!(ToT)/~~~ - locale zh_CN ಥ_ಥ 糟糕,命令语法不正确!(ToT)/~~~ - end_entry - entry directory-empty - locale default ಥ_ಥ 糟糕,当前目录里没有任何文件!呜呜呜~ - locale zh_CN ಥ_ಥ 糟糕,当前目录里没有任何文件!呜呜呜~ - end_entry + [entry] found-file + locale:default o(≧v≦)o 太好了,在当前目录找到了{}个文件! + locale:zh_CN o(≧v≦)o 太好了,在当前目录找到了{}个文件! + [/entry] + [entry] installing-file + locale:default (>^ω^<) 正在安装 "{}"... (>^ω^<) + locale:zh_CN (>^ω^<) 正在安装 "{}"... (>^ω^<) + [/entry] + [entry] install-success + locale:default o(≧v≦)o 已成功安装{}个文件! + locale:zh_CN o(≧v≦)o 已成功安装{}个文件! + [/entry] + [entry] install-success-file + locale:default o(≧v≦)o 已成功安装"{}"! + locale:zh_CN o(≧v≦)o 已成功安装"{}"! + [/entry] + [entry] file-not-found + locale:default ಥ_ಥ 糟糕,出错啦!找不到文件 "{}"!呜呜呜~ + locale:zh_CN ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" !呜呜呜~ + [/entry] + [entry] format-error + locale:default ಥ_ಥ 糟糕,命令语法不正确!(ToT)/~~~ + locale:zh_CN ಥ_ಥ 糟糕,命令语法不正确!(ToT)/~~~ + [/entry] + [entry] directory-empty + locale:default ಥ_ಥ 糟糕,当前目录里没有任何文件!呜呜呜~ + locale:zh_CN ಥ_ಥ 糟糕,当前目录里没有任何文件!呜呜呜~ + [/entry] in_subsection helpmessage - entry description-general - locale default (⊙ω⊙) 文件安装程序样例(不会修改系统中的文件哦~) - locale zh_CN (⊙ω⊙) 文件安装程序样例(不会修改系统中的文件哦~) - end_entry - entry description-usageprompt - locale default (>﹏<) 使用方法:(◐‿◑) - locale zh_CN (>﹏<) 使用方法:(◐‿◑) - end_entry - entry unknown-command - locale default ಥ_ಥ 找不到指令"{}"!呜呜呜~ - locale zh_CN ಥ_ಥ 找不到指令"{}"!呜呜呜~ - end_entry -end_main + [entry] description-general + locale:default (⊙ω⊙) 文件安装程序样例(不会修改系统中的文件哦~) + locale:zh_CN (⊙ω⊙) 文件安装程序样例(不会修改系统中的文件哦~) + [/entry] + [entry] description-usageprompt + locale:default (>﹏<) 使用方法:(◐‿◑) + locale:zh_CN (>﹏<) 使用方法:(◐‿◑) + [/entry] + [entry] unknown-command + locale:default ಥ_ಥ 找不到指令"{}"!呜呜呜~ + locale:zh_CN ಥ_ಥ 找不到指令"{}"!呜呜呜~ + [/entry] +{/entries_section} diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 72cd096..e83d71f 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -200,33 +200,45 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif disallow_cmdmatch_options: handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) return blockinput_data - def handle_entry(entry_name: str): + def handle_entry(entry_name: str, end_phrase: str): # expect locale, locale_block, end_entry nonlocal lineindex while lineindex.+)", phrases[0]) + if results==None: + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="locale:", num=str(lineindex+1))) + else: + locale=results.groupdict()['locale'] + content=splitarray_to_string(phrases[1:]) + else: + check_enough_args(phrases, 3) + content=splitarray_to_string(phrases[2:]) + locale=phrases[1] + target_entry=copy.copy(entry_name) # substesc if "substesc" in global_options.keys() and global_options['substesc']==True: content=re.sub(r"{{ESC}}", '\x1b', content) - if phrases[1]!="default": - target_entry+="__"+phrases[1] + if locale!="default": + target_entry+="__"+locale add_entry(datapath, target_entry, content, lineindex+1) - elif phrases[0]=="locale_block": + elif phrases[0]=="locale_block" or phrases[0]=="[locale]": check_enough_args(phrases, 2) locales=phrases[1:] - content=handle_block_input(preserve_indents=True, preserve_empty_lines=True) + content=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: suffix="" if this_locale!="default": suffix="__"+this_locale add_entry(datapath, entry_name+suffix, content, lineindex+1) - elif phrases[0]=="end_entry": + elif phrases[0]==end_phrase: check_extra_args(phrases, 1, use_exact_count=True) break else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) @@ -236,11 +248,13 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info lineindex+=1 # ignore empty and comment lines if is_ignore_line(): continue + first_phrase=lines_data[lineindex].split()[0] # process header and main sections here - if lines_data[lineindex].split()[0]=="begin_header": + if first_phrase=="begin_header" or first_phrase==r"{header_section}": # avoid repeated block if headerparsed==True: handle_error(fd.feof("repeated-header-err", "Repeated header block at line {num}", num=str(lineindex+1))) + end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" # --Process header block-- while lineindex 正在生成数据... - end_entry - entry processing-file - locale zh_CN > 正在处理文件{filename} - end_entry - entry all-finished - locale zh_CN > 已处理全部文件 - end_entry - entry generate-data-success - locale zh_CN 已成功生成数据 - end_entry - entry view-temp-dir - locale zh_CN 生成的数据可以在"{path}"查看 - end_entry - entry applying-theme - locale zh_CN ==> 正在应用主题... - end_entry - entry apply-theme-success - locale zh_CN 已成功应用主题 - end_entry - entry read-file-error - locale_block zh_CN + [entry] generate-data-msg + locale:zh_CN 主题定义数据将会从以下顺序的主题定义文件生成: + [/entry] + [entry] apply-theme-msg + locale:zh_CN 这些主题定义文件将会通过以下顺序被应用: + [/entry] + [entry] overwrite-notice + locale:zh_CN 如果继续,当前的主题数据将会被覆盖。 + [/entry] + [entry] overlay-notice + locale:zh_CN 这些主题定义文件会被叠加在当前的数据上。 + [/entry] + [entry] confirm-prompt + locale:zh_CN 是否继续操作?[y/n] + [/entry] + [entry] overlay-msg + locale:zh_CN 已使用数据叠加模式 + [/entry] + [entry] generating-data + locale:zh_CN ==> 正在生成数据... + [/entry] + [entry] processing-file + locale:zh_CN > 正在处理文件{filename} + [/entry] + [entry] all-finished + locale:zh_CN > 已处理全部文件 + [/entry] + [entry] generate-data-success + locale:zh_CN 已成功生成数据 + [/entry] + [entry] view-temp-dir + locale:zh_CN 生成的数据可以在"{path}"查看 + [/entry] + [entry] applying-theme + locale:zh_CN ==> 正在应用主题... + [/entry] + [entry] apply-theme-success + locale:zh_CN 已成功应用主题 + [/entry] + [entry] read-file-error + [locale] zh_CN [文件{index}] 读取文件时出现了错误: {message} - end_block - end_entry - entry overlay-no-data - locale_block zh_CN + [/locale] + [/entry] + [entry] overlay-no-data + [locale] zh_CN 错误:当前没有设定主题或当前数据损坏 请尝试设定主题 - end_block - end_entry - entry overlay-data-error - locale_block zh_CN + [/locale] + [/entry] + [entry] overlay-data-error + [locale] zh_CN 错误:当前主题数据损坏 请移除当前数据和重新设定主题后重试 - end_block - end_entry - entry generate-data-error - locale_block zh_CN + [/locale] + [/entry] + [entry] generate-data-error + [locale] zh_CN [文件{index}] 生成数据时发生了错误: {message} - end_block - end_entry + [/locale] + [/entry] # unset-current-theme 指令 in_subsection cli unset-current-theme - entry no-data-found - locale zh_CN 错误:当前没有设定主题 - end_entry - entry remove-data-error - locale_block zh_CN + [entry] no-data-found + locale:zh_CN 错误:当前没有设定主题 + [/entry] + [entry] remove-data-error + [locale] zh_CN 移除当前数据时发生了错误: {message} - end_block - end_entry - entry remove-data-success - locale zh_CN 已成功移除当前主题数据 - end_entry + [/locale] + [/entry] + [entry] remove-data-success + locale:zh_CN 已成功移除当前主题数据 + [/entry] # get-current-theme-info 指令 in_subsection cli get-current-theme-info - entry no-theme - locale zh_CN 当前没有设定任何主题 - end_entry - entry current-theme-msg - locale zh_CN 当前设定的主题: - end_entry - entry overlay-history-msg - locale zh_CN 叠加历史记录(根据最新安装的主题排序): - end_entry - entry version-str - locale zh_CN 版本:{ver} - end_entry - entry description-str - locale zh_CN 详细说明: - end_entry - entry locales-str - locale zh_CN 支持的语言: - end_entry - entry supported-apps-str - locale zh_CN 支持的应用程序: - end_entry -end_main + [entry] no-theme + locale:zh_CN 当前没有设定任何主题 + [/entry] + [entry] current-theme-msg + locale:zh_CN 当前设定的主题: + [/entry] + [entry] overlay-history-msg + locale:zh_CN 叠加历史记录(根据最新安装的主题排序): + [/entry] + [entry] version-str + locale:zh_CN 版本:{ver} + [/entry] + [entry] description-str + locale:zh_CN 详细说明: + [/entry] + [entry] locales-str + locale:zh_CN 支持的语言: + [/entry] + [entry] supported-apps-str + locale:zh_CN 支持的应用程序: + [/entry] +{/entries_section} diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 08ca037..81cd853 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -1,69 +1,68 @@ -begin_header +{header_section} name clitheme message text translations (generator) version 1.2 locales zh_CN supported_apps clitheme -end_header +{/header_section} -begin_main +{entries_section} in_domainapp swiftycode clitheme in_subsection generator - entry error-str - locale zh_CN 语法错误:{msg} - end_entry - entry subsection-conflict-err - locale zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 - end_entry - entry entry-conflict-err - locale zh_CN 第{num}行:无法创建定义"{name}",因为拥有相同名称的子路径已存在 - end_entry - entry internal-error-blockinput - locale zh_CN 第{num}行:处理多行输入时发生了未知错误;请提交问题反馈 - end_entry - entry extra-arguments-err - locale zh_CN 第{num}行:"{phrase}"后的参数太多 - end_entry - entry repeated-header-err - locale zh_CN 第{num}行:重复的header段落 - end_entry - entry repeated-main-err - locale zh_CN 第{num}行:重复的main段落 - end_entry - entry invalid-phrase-err - locale zh_CN 第{num}行:无效的"{phrase}" - end_entry - entry not-enough-args-err - locale zh_CN 第{num}行:"{phrase}"后参数不够 - end_entry - entry incomplete-block-err - locale zh_CN 文件缺少或包含不完整的header或main段落 - end_entry + [entry] error-str + locale:zh_CN 语法错误:{msg} + [/entry] + [entry] subsection-conflict-err + locale:zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 + [/entry] + [entry] entry-conflict-err + locale:zh_CN 第{num}行:无法创建定义"{name}",因为拥有相同名称的子路径已存在 + [/entry] + [entry] internal-error-blockinput + locale:zh_CN 第{num}行:处理多行输入时发生了未知错误;请提交问题反馈 + [/entry] + [entry] extra-arguments-err + locale:zh_CN 第{num}行:"{phrase}"后的参数太多 + [/entry] + [entry] repeated-header-err + locale:zh_CN 第{num}行:重复的header段落 + [/entry] + [entry] repeated-main-err + locale:zh_CN 第{num}行:重复的main段落 + [/entry] + [entry] invalid-phrase-err + locale:zh_CN 第{num}行:无效的"{phrase}" + [/entry] + [entry] not-enough-args-err + locale:zh_CN 第{num}行:"{phrase}"后参数不够 + [/entry] + [entry] incomplete-block-err + locale:zh_CN 文件缺少或包含不完整的header或main段落 + [/entry] # warning strings - entry warning-str - locale zh_CN 警告:{msg} - end_entry - entry repeated-entry-warn - locale zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 - end_entry - entry repeated-header-warn - locale zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 - end_entry + [entry] warning-str + locale:zh_CN 警告:{msg} + [/entry] + [entry] repeated-entry-warn + locale:zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 + [/entry] + [entry] repeated-header-warn + locale:zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 + [/entry] # sanity check - entry sanity-check-entry-err - locale zh_CN 第{num}行:定义路径名称{sanitycheck_msg} - end_entry - entry sanity-check-domainapp-err - locale zh_CN 第{num}行:开发者和应用程序名称{sanitycheck_msg} - end_entry - entry sanity-check-subsection-err - locale zh_CN 第{num}行:子路径名称{sanitycheck_msg} - end_entry - entry sanity-check-msg-banphrase-err - locale zh_CN 不能包含'{char}' - end_entry - entry sanity-check-msg-startswith-err - locale zh_CN 不能以'{char}'开头 - end_entry -end_main - + [entry] sanity-check-entry-err + locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} + [/entry] + [entry] sanity-check-domainapp-err + locale:zh_CN 第{num}行:开发者和应用程序名称{sanitycheck_msg} + [/entry] + [entry] sanity-check-subsection-err + locale:zh_CN 第{num}行:子路径名称{sanitycheck_msg} + [/entry] + [entry] sanity-check-msg-banphrase-err + locale:zh_CN 不能包含'{char}' + [/entry] + [entry] sanity-check-msg-startswith-err + locale:zh_CN 不能以'{char}'开头 + [/entry] +{/entries_section} \ No newline at end of file diff --git a/src/testprogram-data/clithemedef-test_mainfile.clithemedef.txt b/src/testprogram-data/clithemedef-test_mainfile.clithemedef.txt index 33a99f4..bf847c4 100644 --- a/src/testprogram-data/clithemedef-test_mainfile.clithemedef.txt +++ b/src/testprogram-data/clithemedef-test_mainfile.clithemedef.txt @@ -1,7 +1,7 @@ # Sample theme definition file for parser/generator testing # Header information -begin_header +{header_section} name Example theem version 0.1 locales en_US @@ -11,104 +11,104 @@ begin_header version 1.0 name Example theme supported_apps example-app example-app-two another-example shound_unset-app -end_header +{/header_section} # Main block -begin_main - entry com.example example-app text-one - locale default Some example text one - locale en_US Some example text one - locale zh_CN 一些样例文字(一) - end_entry - entry com.example example-app text-two - locale default Some example text two - locale en_US Some example text two - locale zh_CN 一些样例文字(二) - end_entry +{entries_section} + [entry] com.example example-app text-one + locale:default Some example text one + locale:en_US Some example text one + locale:zh_CN 一些样例文字(一) + [/entry] + [entry] com.example example-app text-two + locale:default Some example text two + locale:en_US Some example text two + locale:zh_CN 一些样例文字(二) + [/entry] # Testing in_domainapp in_domainapp com.example example-app-two - entry text_one - locale default Some text - locale en_US Some text - locale zh_CN 一些文本 - end_entry + [entry] text_one + locale:default Some text + locale:en_US Some text + locale:zh_CN 一些文本 + [/entry] # Testing subsections - entry subsection-one text_one - locale default Some text - locale en_US Some text - locale zh_CN 一些文本 - end_entry - entry subsection-one text_two - locale default Some text two - locale en_US Some text two - locale zh_CN 一些文本(二) - end_entry + [entry] subsection-one text_one + locale:default Some text + locale:en_US Some text + locale:zh_CN 一些文本 + [/entry] + [entry] subsection-one text_two + locale:default Some text two + locale:en_US Some text two + locale:zh_CN 一些文本(二) + [/entry] # Testing in_subsection in_subsection subsection-two - entry text_one - locale default Some text - locale en_US Some text - locale zh_CN 一些文本 - end_entry - entry text_two - locale default Some text two - locale en_US Some text two - locale zh_CN 一些文本(二) - end_entry + [entry] text_one + locale:default Some text + locale:en_US Some text + locale:zh_CN 一些文本 + [/entry] + [entry] text_two + locale:default Some text two + locale:en_US Some text two + locale:zh_CN 一些文本(二) + [/entry] # Testing unset_subsection unset_subsection - entry text_two - locale default Some text two - locale en_US Some text two - locale zh_CN 一些文本(二) - end_entry + [entry] text_two + locale:default Some text two + locale:en_US Some text two + locale:zh_CN 一些文本(二) + [/entry] in_domainapp com.example-two another-example # Ignore entries in here - entry repeat_test_text_one - locale default Some text - locale en_US Some text - locale zh_CN 一些文本 - end_entry - entry repeat_test_text_two - locale default Some text two - locale en_US Some text two - locale zh_CN 一些文本(二) - end_entry + [entry] repeat_test_text_one + locale:default Some text + locale:en_US Some text + locale:zh_CN 一些文本 + [/entry] + [entry] repeat_test_text_two + locale:default Some text two + locale:en_US Some text two + locale:zh_CN 一些文本(二) + [/entry] # Testing handling of repeated entries - entry repeat_test_text_one - locale default Some other text - locale en_US Some other text - locale zh_CN 一些其他文本 - end_entry - entry repeat_test_text_two - locale default Some other text two - locale en_US Some other text two - locale zh_CN 一些其他文本(二) - end_entry + [entry] repeat_test_text_one + locale:default Some other text + locale:en_US Some other text + locale:zh_CN 一些其他文本 + [/entry] + [entry] repeat_test_text_two + locale:default Some other text two + locale:en_US Some other text two + locale:zh_CN 一些其他文本(二) + [/entry] # Testing unset_domainapp unset_domainapp - entry should_unset.example should_unset-app text - locale default Should have reset - locale en_US Should have reset - locale zh_CN 应该已经重置 - end_entry + [entry] should_unset.example should_unset-app text + locale:default Should have reset + locale:en_US Should have reset + locale:zh_CN 应该已经重置 + [/entry] # Testing global entries (without domain or/and app) - entry sample_global_entry - locale default Some text - locale en_US Some text - locale zh_CN 一些文本 - end_entry + [entry] sample_global_entry + locale:default Some text + locale:en_US Some text + locale:zh_CN 一些文本 + [/entry] in_subsection global.example - entry global_entry - locale default Global entry in app - locale en_US Global entry in app - locale zh_CN app内的通用实例 - end_entry -end_main \ No newline at end of file + [entry] global_entry + locale:default Global entry in app + locale:en_US Global entry in app + locale:zh_CN app内的通用实例 + [/entry] +{/entries_section} \ No newline at end of file -- Gitee From 96629d101c2b818454fa278597efce4a21e72234 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 13 Apr 2024 22:28:13 +0800 Subject: [PATCH 049/354] Forcefully fix set_local_themedef override issue The issue is that the data of an imported module is shared across all other modules that use it, meaning that the alt_dir and related variables in frontend will be updated across everywhere. I forcefully created a workaround by including all string files in all set_local_themedef uses. --- src/clitheme/_generator/__init__.py | 1 + src/clitheme/_globalvar.py | 1 + src/clitheme/cli.py | 1 + 3 files changed, 3 insertions(+) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index e83d71f..327b5a3 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -360,6 +360,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info try: if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except: if _version.release==0: print("generator set_local_themedef failed") pass \ No newline at end of file diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index d8e47b8..9a66cdf 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -62,6 +62,7 @@ def sanity_check(path: str) -> bool: if not msg_retrieved: try: if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except RuntimeError: if _version.release==0: print("_globalvar set_local_themedef failed") pass diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 36d1191..7992448 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -35,6 +35,7 @@ frontend.global_subsections="cli" try: if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt")): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except: if _version.release==0: print("set_local_themedef failed") pass -- Gitee From 093b90b4aaa660415b26ad53749961bd216eb0a1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 13 Apr 2024 22:33:28 +0800 Subject: [PATCH 050/354] Update version (v1.2-dev20240413) --- PKGBUILD | 4 ++-- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 1b90706..a330397 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='1.2_dev20240409' +pkgver='1.2_dev20240413' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') @@ -17,7 +17,7 @@ backup=() options=() install= changelog='debian/changelog' -source=("srctmp::git+file://$PWD#branch=v1.2_dev") +source=("srctmp::git+file://$PWD#branch=v1.2_dev") # REMOVE "#branch=v1.2_dev" WHEN MERGING WITH STABLE BRANCH!! noextract=() md5sums=('SKIP') validpgpkeys=() diff --git a/debian/changelog b/debian/changelog index 7451257..bde37e8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240409-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240413-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Tue, 09 Apr 2024 22:01:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 13 Apr 2024 22:30:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 1b0d5f5..3630a11 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240409" +__version__="1.2-dev20240413" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240409" +version_main="1.2_dev20240413" version_buildnumber=1 \ No newline at end of file -- Gitee From 7ecbd61c32bee55781404971b4b7007054f09719 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 14 Apr 2024 13:14:24 +0800 Subject: [PATCH 051/354] Organize _globalvar a little bit --- src/clitheme/_globalvar.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 9a66cdf..123b206 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -6,17 +6,19 @@ import os try: from . import _version except ImportError: import _version -clitheme_root_data_path="" -if os.name=="posix": # Linux/macOS only - try: - clitheme_root_data_path=os.environ["XDG_DATA_HOME"]+"/clitheme" - except KeyError: 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. Try restarting your terminal session to fix this issue.""" +clitheme_version=_version.__version__ +## Core data paths +clitheme_root_data_path="" +if os.name=="posix": # Linux/macOS only: Try to get XDG_DATA_HOME if possible + try: + clitheme_root_data_path=os.environ["XDG_DATA_HOME"]+"/clitheme" + except KeyError: pass + if clitheme_root_data_path=="": # prev did not succeed try: if os.name=="nt": # Windows @@ -31,16 +33,16 @@ if clitheme_root_data_path=="": # prev did not succeed var=r"%USERPROFILE%" print(error_msg_str.format(var=var)) exit(1) -clitheme_temp_root="/tmp" -if os.name=="nt": - clitheme_temp_root=os.environ['TEMP'] -clitheme_version=_version.__version__ +clitheme_temp_root="/tmp" if os.name!="nt" else os.environ['TEMP'] +## _generator file and folder names generator_info_pathname="theme-info" # e.g. ~/.local/share/clitheme/theme-info generator_data_pathname="theme-data" # e.g. ~/.local/share/clitheme/theme-data generator_index_filename="current_theme_index" +## _generator.db_interface file and table names db_data_tablename="clitheme_subst_data" db_filename="subst-data.db" +## Sanity check function entry_banphrases=['/','\\'] startswith_banphrases=['.'] banphrase_error_message="cannot contain '{char}'" -- Gitee From 3e36e60c6b24c1ca49c55d7dd4c20343fbfb2c58 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 14 Apr 2024 16:42:11 +0800 Subject: [PATCH 052/354] First version of substrules_section implementation --- src/clitheme/_generator/__init__.py | 138 ++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 27 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 327b5a3..96051b9 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -7,16 +7,13 @@ import random import re import math import copy +from typing import Optional try: - from .. import _globalvar - from .. import frontend - from .. import _version - from .. import _get_resource + from .. import _globalvar, frontend, _version, _get_resource + from . import db_interface except ImportError: # for test program - import _globalvar - import frontend - import _version - import _get_resource + import _globalvar, frontend, _version, _get_resource + import _generator.db_interface as db_interface fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") @@ -92,8 +89,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if not os.path.exists(datapath): os.mkdir(datapath) # data to keep track of - headerparsed=False - mainparsed=False + parsed_sections=[] lines_data=file_content.splitlines() lineindex=-1 # counter extra +1 operation at beginning global_options={} @@ -112,17 +108,22 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') # defined sub-processing functions - def parse_options(options_data: list[str], merge_global_options: bool) -> dict: + def parse_options(options_data: list[str], merge_global_options: bool, allowed_options: Optional[list]=None) -> dict: + nonlocal global_options # value options: leadtabindents, leadspaces value_options=["leadtabindents", "leadspaces"] # on/off options: substesc, strictcmdmatch, exactcmdmatch (use no<...> to disable) - bool_options=["substesc", "strictcmdmatch", "exactcmdmatch"] + bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "endmatchhere"] + # only one of these options can be set to true at the same time + bool_options_unique=["strictcmdmatch", "exactcmdmatch"] final_options={} - if merge_global_options: nonlocal global_options; final_options=copy.copy(global_options) + if merge_global_options: final_options=copy.copy(global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options 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) + if allowed_options!=None and option_name not in allowed_options: + handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option_name)) if option_name in value_options: # must not begin with no if option_name_preserve_no.startswith("no"): @@ -138,6 +139,14 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # set option final_options[option_name]=value elif option_name in bool_options: + # process unique bool options + if option_name_preserve_no in bool_options_unique: + # can't be specified at the same time + for opt in options_data: + if opt!=option_name and opt in bool_options_unique: + handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same with \"{option2}\"", option1=option_name, option2=opt)) + # set all other options to false + for opt in bool_options_unique: final_options[opt]=False # if starts with no, set to false; else, set to true final_options[option_name]=not option_name_preserve_no.startswith("no") else: @@ -200,9 +209,12 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif disallow_cmdmatch_options: handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) return blockinput_data - def handle_entry(entry_name: str, end_phrase: str): + def handle_entry(entry_name: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): + # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int, end_match_here: bool # expect locale, locale_block, end_entry nonlocal lineindex + substrules_entries=[] # (match_content, substitute_content) + substrules_endmatchhere=substrules_options['end_match_here'] if 'end_match_here' in substrules_options else False while lineindex1 else [], merge_global_options=True, allowed_options=["endmatchhere"]) + for option in got_options: + if option=="endmatchhere" and got_options['endmatchhere']==True: + substrules_endmatchhere=True break else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) - + if is_substrules: + for entry in substrules_entries: db_interface.add_subst_entry(match_pattern=entry[0], substitute_pattern=entry[1], effective_commands=substrules_options['effective_commands'], is_regex=substrules_options['is_regex'], command_match_strictness=substrules_options['strictness'], end_match_here=substrules_endmatchhere) + ## Main code while lineindex1: + got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True) + for this_option in got_options: + if this_option=="strictcmdmatch" and got_options['strictcmdmatch']==True: + strictness=1 + elif this_option=="exactcmdmatch" and got_options['exactcmdmatch']==True: + strictness=2 + command_filters=[] + for cmd in command_strings: + command_filters.append(cmd) + command_filter_strictness=strictness + elif phrases[0]=="filter_command": + check_enough_args(phrases, 2) + content=splitarray_to_string(phrases[1:]) + strictness=0 + for this_option in global_options: + if this_option=="strictcmdmatch" and global_options['strictcmdmatch']==True: + strictness=1 + elif this_option=="exactcmdmatch" and global_options['exactcmdmatch']==True: + strictness=2 + command_filters=[content] + command_filter_strictness=strictness + elif phrases[0]=="unset_filter_command": + check_extra_args(phrases, 1, use_exact_count=True) + command_filters=None + elif phrases[0]=="[substitute_string]" or phrases[0]=="[substitute_regex]": + check_enough_args(phrases, 2) + options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} + handle_entry(splitarray_to_string(phrases[1:]), end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) + elif phrases[0]=="set_options": + check_enough_args(phrases, 2) + handle_set_global_options(phrases[1:]) + elif phrases[0]==end_phrase: + check_extra_args(phrases, 1, use_exact_count=True) + parsed_sections.append("substrules") + break + else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) + ## END --Process substrules block-- + else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=first_phrase, num=str(lineindex+1))) + + if not "header" in parsed_sections or (not "entries" in parsed_sections and not "substrules" in parsed_sections): handle_error(fd.reof("incomplete-block-err", "Missing or incomplete header or main block")) # Update current theme index theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") -- Gitee From 1db2ad36123c4ba53a284d92827e7f9bb6bcbbc6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 14 Apr 2024 17:24:32 +0800 Subject: [PATCH 053/354] Update db_interface_tests to use clithemedef --- src/db_interface_tests.py | 75 +++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index bc479a1..6828a7d 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -1,4 +1,5 @@ from clitheme._generator import db_interface +from clitheme import _generator, _globalvar # sample input for testing sample_inputs=[("rm: missing operand", "rm"), @@ -17,22 +18,66 @@ sample_inputs=[("rm: missing operand", "rm"), ("rm: : Permission denied", "rm -rf") # test exactcmdmatch (substitution rule containing this option should be prioritized over previous rules) ] # substitute patterns -# (match_pattern, substitute_pattern, is_regex, effective_commands, command_match_strictness, end_match_here) -subst_patterns=[("rm: missing operand", "rm says: missing arguments and options (>﹏<)", False, ["rm"], 0, False), - ("type rm --help for more information", "For more information, use rm --help (。ì _ í。)", False, ["rm"], 0, False), - (r"(?P.+): (?P.+): Permission denied",r"""\g says: Access denied to \g! ಥ_ಥ""",True, ["rm -rf", "cat", "cd", "ls"], 0, False), - (r"(?P.+): unrecognized option '(?P.+)'",r"""wef""",True, ["ls"], 0, False), # testing repeated entry detection - (r"(?P.+): unrecognized option '(?P.+)'",r"""\g says: option '\g' not known! (ToT)/~~~'""",True, ["ls"], 0, False), - (r"^Warning:( )", r"o(≧v≦)o Note:\g<1>",True , None, 0, False), - (r"^Error:( )", r"(ToT)/~~~ Error:\g<1>",True, None, 0, False), - (r"invaild input( ){0,}$", r"input is invaild! ಥ_ಥ", True, None, 0, False), - ("Error: sample message", "Error: sample message! (>﹏<)", False, ["example_app install-stuff"], 1, True), - (r"(?P.+): (?P.+): Permission denied",r"""\g says: Missing argument for operation! ಥ_ಥ""",True, ["rm -rf"], 2, False), -] +substrules_file=r""" +{header_section} + name test +{/header_section} +{substrules_section} + filter_command rm + [substitute_string] rm: missing operand + locale:default rm says: missing arguments and options (>﹏<) + [/substitute_string] + [substitute_string] type rm --help for more information + locale:default For more information, use rm --help (。ì _ í。) + [/substitute_string] + [filter_commands] + rm -rf + cat + cd + ls + [/filter_commands] + [substitute_regex] (?P.+): (?P.+): Permission denied + locale:default \g says: Access denied to \g! ಥ_ಥ + [/substitute_regex] + filter_command ls + # testing repeated entry detection + [substitute_regex] (?P.+): unrecognized option '(?P.+)' + locale:default wef + [/substitute_regex] + [substitute_regex] (?P.+): unrecognized option '(?P.+)' + locale:default \g says: option '\g' not known! (ToT)/~~~' + [/substitute_regex] + unset_filter_command + [substitute_regex] ^Warning:( ) + locale:default o(≧v≦)o Note:\g<1> + [/substitute_regex] + [substitute_regex] ^Error:( ) + locale:default (ToT)/~~~ Error:\g<1> + [/substitute_regex] + [substitute_regex] invaild input( ){0,}$ + locale:default input is invaild! ಥ_ಥ + [/substitute_regex] + set_options strictcmdmatch + filter_command example_app install-stuff + [substitute_string] Error: sample message + locale:default Error: sample message! (>﹏<) + [/substitute_string] endmatchhere + set_options exactcmdmatch + filter_command rm -rf + [substitute_regex] (?P.+): (?P.+): Permission denied + locale:default \g says: Missing argument for operation! ಥ_ಥ + [/substitute_regex] + set_options noexactcmdmatch nostrictcmdmatch +{/substrules_section} +""" + +# db_interface.init_db(":memory:") +# # record substitute patterns +# for dat in subst_patterns: db_interface.add_subst_entry(match_pattern=dat[0], substitute_pattern=dat[1], effective_commands=dat[3], is_regex=dat[2], command_match_strictness=dat[4], end_match_here=dat[5]) + +_generator.generate_data_hierarchy(substrules_file) +db_interface.connection=db_interface.sqlite3.connect(_generator.path+"/"+_globalvar.db_filename) -db_interface.init_db(":memory:") -# record substitute patterns -for dat in subst_patterns: db_interface.add_subst_entry(match_pattern=dat[0], substitute_pattern=dat[1], effective_commands=dat[3], is_regex=dat[2], command_match_strictness=dat[4], end_match_here=dat[5]) print("Successfully recorded data\nTesting sample outputs: ") for inp in sample_inputs: print(db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8')) -- Gitee From a61739244f5cca568ce74487a5a8b06d99f83d5e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 14 Apr 2024 17:28:11 +0800 Subject: [PATCH 054/354] Change incomplete-section-err string definition --- src/clitheme/_generator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 96051b9..d06d7e1 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -437,7 +437,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=first_phrase, num=str(lineindex+1))) if not "header" in parsed_sections or (not "entries" in parsed_sections and not "substrules" in parsed_sections): - handle_error(fd.reof("incomplete-block-err", "Missing or incomplete header or main block")) + handle_error(fd.reof("incomplete-section-err", "Missing or incomplete header or content sections")) # Update current theme index theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") theme_index.write(custom_infofile_name+"\n") -- Gitee From 7c4daf019de5fae79998251329b71ec4373d6992 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 14 Apr 2024 17:33:58 +0800 Subject: [PATCH 055/354] Implement line_number_debug for add_subst_entry --- src/clitheme/_generator/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index d06d7e1..e874cf5 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -214,6 +214,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # expect locale, locale_block, end_entry nonlocal lineindex substrules_entries=[] # (match_content, substitute_content) + substrules_entries_linenumber=[] substrules_endmatchhere=substrules_options['end_match_here'] if 'end_match_here' in substrules_options else False while lineindex1 else [], merge_global_options=True, allowed_options=["endmatchhere"]) @@ -261,7 +262,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info break else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) if is_substrules: - for entry in substrules_entries: db_interface.add_subst_entry(match_pattern=entry[0], substitute_pattern=entry[1], effective_commands=substrules_options['effective_commands'], is_regex=substrules_options['is_regex'], command_match_strictness=substrules_options['strictness'], end_match_here=substrules_endmatchhere) + for x in range(len(substrules_entries)): + entry=substrules_entries[x] + db_interface.add_subst_entry(match_pattern=entry[0], substitute_pattern=entry[1], effective_commands=substrules_options['effective_commands'], is_regex=substrules_options['is_regex'], command_match_strictness=substrules_options['strictness'], end_match_here=substrules_endmatchhere, line_number_debug=substrules_entries_linenumber[x]) ## Main code while lineindex Date: Sun, 14 Apr 2024 17:36:37 +0800 Subject: [PATCH 056/354] Update version (v1.2-dev20240414) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index a330397..c0297e9 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='1.2_dev20240413' +pkgver='1.2_dev20240414' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index bde37e8..84702ca 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240413-1) UNRELEASED; urgency=low +clitheme (1.2-dev20240414-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 13 Apr 2024 22:30:00 +0800 + -- swiftycode <3291929745@qq.com> Sun, 14 Apr 2024 17:35:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 3630a11..89dc5f4 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240413" +__version__="1.2-dev20240414" major=1 minor=2 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240413" +version_main="1.2_dev20240414" version_buildnumber=1 \ No newline at end of file -- Gitee From a729848d22f64e607e8e59170fc0725e8bdf32dc Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 13:11:19 +0800 Subject: [PATCH 057/354] Fixes some logical errors in db_interface --- src/clitheme/_generator/db_interface.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index c98c35d..63487e0 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -39,10 +39,10 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma for cmd in cmdlist: # remove any existing values with the same match_pattern and effective_command and command_match_strictness(if ==2) extra_condition="" - if command_match_strictness==2: extra_condition=" AND command_match_strictness=2" - if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?{extra_condition};", (match_pattern.strip(),cmd)).fetchall())>0: + if command_match_strictness==2: extra_condition="AND command_match_strictness=2" + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {extra_condition};", (match_pattern.strip(),cmd)).fetchall())>0: print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") - connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=?{extra_condition};", (match_pattern.strip(),cmd)) + connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {extra_condition};", (match_pattern.strip(),cmd)) # insert the entry into the main table connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here) VALUES (?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd, is_regex, command_match_strictness, end_match_here)) connection.commit() @@ -66,14 +66,15 @@ def match_content(content: bytes, command: Optional[str]=None) -> bytes: def split_len(obj: tuple) -> int: return len(obj[0].split()) cmdlist.sort(key=split_len, reverse=True) # prioritize effective_command with exact match requirement - cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2", (command.strip(),)).fetchall()+cmdlist + cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),)).fetchall()+cmdlist # attempt to find matching command for tp in cmdlist: cmd=tp[0] # extract value from tuple strictness=tp[1] # strictness setting success=True - if strictness==1: # must start with pattern - if not command.startswith(cmd): success=False + if strictness==1: # must start with pattern in terms of space-separated phrases + condition=len(cmd.split()) bytes: success=False; break if success: # if found matching command - if cmd not in final_cmdlist: final_cmdlist.append(cmd) - final_cmdlist_strictmatch.append(strictness==2) + if cmd not in final_cmdlist: + final_cmdlist.append(cmd) + final_cmdlist_strictmatch.append(strictness==2) break content_str=copy.copy(content) matches=[] @@ -92,7 +94,8 @@ def match_content(content: bytes, command: Optional[str]=None) -> bytes: cmd=final_cmdlist[x] # prioritize exact match if final_cmdlist_strictmatch[x]==True: matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2 ORDER BY rowid;", (cmd,)).fetchall() - matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE effective_command=? ORDER BY rowid;", (cmd,)).fetchall() + # also append matches with other strictness + matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness!=2 ORDER BY rowid;", (cmd,)).fetchall() matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE typeof(effective_command)=typeof(null) ORDER BY rowid;").fetchall() for match_data in matches: try: -- Gitee From f74d56c9632f13f1ef7279de9722f0a42b3b5b44 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 13:46:41 +0800 Subject: [PATCH 058/354] Remove code that removes an extra newline at end The _generator block parsing sections have changed, so things work differently now --- src/clitheme/frontend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 9d4b0ac..384e432 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -235,8 +235,7 @@ class FetchDescriptor(): f=open(p,'r', encoding="utf-8") dat=f.read() if self.debug_mode: print("Success:\n> "+dat) - # since the generator adds an extra newline in the entry data, we need to remove it - return re.sub(r"\n\Z", "", dat) + return dat except (FileNotFoundError, IsADirectoryError): if self.debug_mode: print("Failed") return fallback_string -- Gitee From 2b6ca70f9a4622176b45d5c9b13a81a679369e99 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 14:18:02 +0800 Subject: [PATCH 059/354] Move code to get locale into _globalvar.get_locale --- src/clitheme/_globalvar.py | 51 ++++++++++++++++++++++++++++++++++++-- src/clitheme/frontend.py | 50 ++++--------------------------------- 2 files changed, 54 insertions(+), 47 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 123b206..0c47175 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -3,6 +3,7 @@ Global variable definitions for clitheme """ import os +import re try: from . import _version except ImportError: import _version @@ -12,6 +13,7 @@ Please make sure that the {var} environment variable is set correctly. Try restarting your terminal session to fix this issue.""" clitheme_version=_version.__version__ + ## Core data paths clitheme_root_data_path="" if os.name=="posix": # Linux/macOS only: Try to get XDG_DATA_HOME if possible @@ -34,14 +36,17 @@ if clitheme_root_data_path=="": # prev did not succeed print(error_msg_str.format(var=var)) exit(1) clitheme_temp_root="/tmp" if os.name!="nt" else os.environ['TEMP'] + ## _generator file and folder names generator_info_pathname="theme-info" # e.g. ~/.local/share/clitheme/theme-info generator_data_pathname="theme-data" # e.g. ~/.local/share/clitheme/theme-data generator_index_filename="current_theme_index" + ## _generator.db_interface file and table names db_data_tablename="clitheme_subst_data" db_filename="subst-data.db" + ## Sanity check function entry_banphrases=['/','\\'] startswith_banphrases=['.'] @@ -51,7 +56,6 @@ startswith_error_message="cannot start with '{char}'" # - cannot start with . # - cannot contain banphrases sanity_check_error_message="" - # retrieve the entry only once to avoid dead loop in frontend.FetchDescriptor callbacks msg_retrieved=False try: from . import frontend, _get_resource @@ -84,4 +88,47 @@ def sanity_check(path: str) -> bool: retrieve_entry() sanity_check_error_message=banphrase_error_message.format(char=b) return False - return True \ No newline at end of file + return True + +## Convenience functions +def get_locale(debug_mode: bool=False): + lang=[] + # Skip $LANGUAGE if both $LANG and $LC_ALL is set to C (treat empty as C also) + skip_LANGUAGE=False + LANG_value=os.environ["LANG"] if "LANG" in os.environ and os.environ["LANG"].strip()!='' else "C" + LC_ALL_value=os.environ["LC_ALL"] if "LC_ALL" in os.environ and os.environ["LC_ALL"].strip()!='' else "C" + if (LANG_value=="C" or LANG_value.startswith("C.")) and (LC_ALL_value=="C" or LC_ALL_value.startswith("C.")): skip_LANGUAGE=True + # $LANGUAGE (list of languages separated by colons) + if "LANGUAGE" in os.environ and not skip_LANGUAGE: + target_str=os.environ['LANGUAGE'] + for language in target_str.split(":"): + each_language=language.strip() + if each_language=="": continue + # avoid exploit of accessing top-level folders + if sanity_check(each_language)==False: continue + # Ignore en and en_US (See https://wiki.archlinux.org/title/Locale#LANGUAGE:_fallback_locales) + if each_language!="en" and each_language!="en_US": + # Treat C as en_US also + if re.sub(r"(?P.+)[\.].+", r"\g", each_language)=="C": + lang.append(re.sub(r".+[\.]", "en_US.", each_language)) + lang.append("en_US") + lang.append(each_language) + # no encoding + lang.append(re.sub(r"(?P.+)[\.].+", r"\g", each_language)) + # $LC_ALL + elif "LC_ALL" in os.environ and os.environ["LC_ALL"].strip()!="": + target_str=os.environ["LC_ALL"].strip() + if not sanity_check(target_str)==False: + lang.append(target_str) + lang.append(re.sub(r"(?P.+)[\.].+", r"\g", target_str)) + else: + if debug_mode: print("[Debug] Locale: sanity check failed ({})".format(sanity_check_error_message)) + # $LANG + elif "LANG" in os.environ and os.environ["LANG"].strip()!="": + target_str=os.environ["LANG"].strip() + if not sanity_check(target_str)==False: + lang.append(target_str) + lang.append(re.sub(r"(?P.+)[\.].+", r"\g", target_str)) + else: + if debug_mode: print("[Debug] Locale: sanity check failed ({})".format(sanity_check_error_message)) + return lang \ No newline at end of file diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 384e432..5d99956 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -159,58 +159,18 @@ class FetchDescriptor(): if _globalvar.sanity_check(entry_path)==False: if self.debug_mode: print("[Debug] Error: entry names/subsections {}".format(_globalvar.sanity_check_error_message)) return fallback_string - lang="" + lang=[] # Language handling: see https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Environment-Variables for more information if not self.disable_lang: if self.lang!="": if self.debug_mode: print("[Debug] Locale: Using defined self.lang") if not _globalvar.sanity_check(self.lang)==False: - lang=self.lang + lang=[self.lang] else: if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) else: if self.debug_mode: print("[Debug] Locale: Using environment variables") - # Skip $LANGUAGE if both $LANG and $LC_ALL is set to C (treat empty as C also) - skip_LANGUAGE=False - LANG_value="" - if "LANG" not in os.environ or os.environ["LANG"]=='': LANG_value="C" - else: LANG_value=os.environ['LANG'] - LC_ALL_value="" - if "LC_ALL" not in os.environ or os.environ["LC_ALL"]=='': LC_ALL_value="C" - else: LC_ALL_value=os.environ["LC_ALL"] - if (LANG_value=="C" or LANG_value.startswith("C.")) and (LC_ALL_value=="C" or LC_ALL_value.startswith("C.")): skip_LANGUAGE=True - # $LANGUAGE (list of languages separated by colons) - if "LANGUAGE" in os.environ and not skip_LANGUAGE: - target_str=os.environ['LANGUAGE'] - for each_language in target_str.strip().split(":"): - # avoid exploit of accessing top-level folders - if _globalvar.sanity_check(each_language)==False: continue - # Ignore en and en_US (See https://wiki.archlinux.org/title/Locale#LANGUAGE:_fallback_locales) - if each_language!="en" and each_language!="en_US": - # Treat C as en_US also - if re.sub(r"(?P.+)[\.].+", r"\g", each_language)=="C": - lang+=re.sub(r".+[\.]", "en_US.", each_language)+" " - lang+="en_US"+" " - lang+=each_language+" " - # no encoding - lang+=re.sub(r"(?P.+)[\.].+", r"\g", each_language)+" " - lang=lang.strip() - # $LC_ALL - elif "LC_ALL" in os.environ: - target_str=os.environ["LC_ALL"].strip() - if not _globalvar.sanity_check(target_str)==False: - lang=target_str+" " - lang+=re.sub(r"(?P.+)[\.].+", r"\g", target_str) - else: - if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) - # $LANG - elif "LANG" in os.environ: - target_str=os.environ["LANG"].strip() - if not _globalvar.sanity_check(target_str)==False: - lang=target_str+" " - lang+=re.sub(r"(?P.+)[\.].+", r"\g", target_str) - else: - if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) + lang=_globalvar.get_locale(debug_mode=self.debug_mode) if self.debug_mode: print(f"[Debug] lang: {lang}\n[Debug] entry_path: {entry_path}") # just being lazy here I don't want to check the variables before using ಥ_ಥ (because it doesn't matter) @@ -222,11 +182,11 @@ class FetchDescriptor(): if path2!=None: path2+="/"+section # path with lang, path with lang but without e.g. .UTF-8, path with no lang possible_paths=[] - for l in lang.split(): + for l in lang: possible_paths.append(path+"__"+l) possible_paths.append(path) if path2!=None: - for l in lang.split(): + for l in lang: possible_paths.append(path2+"__"+l) possible_paths.append(path2) for p in possible_paths: -- Gitee From 9c02dd24930a28e76d44ac8624b260f67b92b186 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 14:25:58 +0800 Subject: [PATCH 060/354] Don't use localized sanity check error message in debug --- src/clitheme/_globalvar.py | 18 ++++++++++-------- src/clitheme/frontend.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 0c47175..e3e2493 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -4,6 +4,7 @@ Global variable definitions for clitheme import os import re +from copy import copy try: from . import _version except ImportError: import _version @@ -46,12 +47,13 @@ generator_index_filename="current_theme_index" db_data_tablename="clitheme_subst_data" db_filename="subst-data.db" - ## Sanity check function entry_banphrases=['/','\\'] startswith_banphrases=['.'] banphrase_error_message="cannot contain '{char}'" +banphrase_error_message_orig=copy(banphrase_error_message) startswith_error_message="cannot start with '{char}'" +startswith_error_message_orig=copy(startswith_error_message) # function to check whether the pathname contains invalid phrases # - cannot start with . # - cannot contain banphrases @@ -60,7 +62,7 @@ sanity_check_error_message="" msg_retrieved=False try: from . import frontend, _get_resource except ImportError: import frontend, _get_resource -def sanity_check(path: str) -> bool: +def sanity_check(path: str, use_orig: bool=False) -> bool: def retrieve_entry(): # retrieve the entry (only for the first time) global msg_retrieved @@ -80,13 +82,13 @@ def sanity_check(path: str) -> bool: for p in path.split(): for b in startswith_banphrases: if p.startswith(b): - retrieve_entry() - sanity_check_error_message=startswith_error_message.format(char=b) + if not use_orig: retrieve_entry() + sanity_check_error_message=startswith_error_message.format(char=b) if not use_orig else startswith_error_message_orig.format(char=b) return False for b in entry_banphrases: if p.find(b)!=-1: - retrieve_entry() - sanity_check_error_message=banphrase_error_message.format(char=b) + if not use_orig: retrieve_entry() + sanity_check_error_message=banphrase_error_message.format(char=b) if not use_orig else banphrase_error_message_orig.format(char=b) return False return True @@ -118,7 +120,7 @@ def get_locale(debug_mode: bool=False): # $LC_ALL elif "LC_ALL" in os.environ and os.environ["LC_ALL"].strip()!="": target_str=os.environ["LC_ALL"].strip() - if not sanity_check(target_str)==False: + if not sanity_check(target_str, use_orig=True)==False: lang.append(target_str) lang.append(re.sub(r"(?P.+)[\.].+", r"\g", target_str)) else: @@ -126,7 +128,7 @@ def get_locale(debug_mode: bool=False): # $LANG elif "LANG" in os.environ and os.environ["LANG"].strip()!="": target_str=os.environ["LANG"].strip() - if not sanity_check(target_str)==False: + if not sanity_check(target_str, use_orig=True)==False: lang.append(target_str) lang.append(re.sub(r"(?P.+)[\.].+", r"\g", target_str)) else: diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 5d99956..9b4014e 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -146,7 +146,7 @@ class FetchDescriptor(): self.disable_lang=disable_lang # sanity check the domain, app, and subsections - if _globalvar.sanity_check(self.domain_name+" "+self.app_name+" "+self.subsections)==False: + if _globalvar.sanity_check(self.domain_name+" "+self.app_name+" "+self.subsections, use_orig=True)==False: raise SyntaxError("Domain, app, or subsection names {}".format(_globalvar.sanity_check_error_message)) def retrieve_entry_or_fallback(self, entry_path: str, fallback_string: str) -> str: """ -- Gitee From 7bd870bc582507cbe6d981364c44c16197b05bc8 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 17:29:40 +0800 Subject: [PATCH 061/354] Implement locale support in _generator and db_interface --- src/clitheme/_generator/__init__.py | 8 ++--- src/clitheme/_generator/db_interface.py | 40 +++++++++++++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index e874cf5..a9b27af 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -213,7 +213,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int, end_match_here: bool # expect locale, locale_block, end_entry nonlocal lineindex - substrules_entries=[] # (match_content, substitute_content) + substrules_entries=[] # (match_content, substitute_content, locale) substrules_entries_linenumber=[] substrules_endmatchhere=substrules_options['end_match_here'] if 'end_match_here' in substrules_options else False while lineindex1 else [], merge_global_options=True, allowed_options=["endmatchhere"]) @@ -264,7 +264,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if is_substrules: for x in range(len(substrules_entries)): entry=substrules_entries[x] - db_interface.add_subst_entry(match_pattern=entry[0], substitute_pattern=entry[1], effective_commands=substrules_options['effective_commands'], is_regex=substrules_options['is_regex'], command_match_strictness=substrules_options['strictness'], end_match_here=substrules_endmatchhere, line_number_debug=substrules_entries_linenumber[x]) + db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x]) ## Main code while lineindex0: for cmd in effective_commands: # remove extra spaces in the command cmdlist.append(re.sub(r" {2,}", " ", cmd).strip()) else: # remove any existing values with the same match_pattern - if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null);", (match_pattern.strip(),)).fetchall())>0: + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition};", (match_pattern.strip(), effective_locale)).fetchall())>0: print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") - connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null);", (match_pattern.strip(),)) + connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition};", (match_pattern.strip(), effective_locale)) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, is_regex, command_match_strictness, end_match_here) VALUES (?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), is_regex, command_match_strictness, end_match_here)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), is_regex, command_match_strictness, end_match_here, effective_locale)) for cmd in cmdlist: # remove any existing values with the same match_pattern and effective_command and command_match_strictness(if ==2) - extra_condition="" - if command_match_strictness==2: extra_condition="AND command_match_strictness=2" - if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {extra_condition};", (match_pattern.strip(),cmd)).fetchall())>0: + strictness_condition="" + if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(),cmd, effective_locale)).fetchall())>0: print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") - connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {extra_condition};", (match_pattern.strip(),cmd)) + connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(),cmd, effective_locale)) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here) VALUES (?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd, is_regex, command_match_strictness, end_match_here)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd, is_regex, command_match_strictness, end_match_here, effective_locale)) connection.commit() def match_content(content: bytes, command: Optional[str]=None) -> bytes: @@ -89,14 +92,27 @@ def match_content(content: bytes, command: Optional[str]=None) -> bytes: break 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" + # get locales + locales=_globalvar.get_locale() + nonlocal matches + # try the ones with locale defined + for this_locale in locales: + fetch_data=connection.execute(f"SELECT {fetch_items} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND effective_locale=? ORDER BY rowid;", filter_data+(this_locale,)).fetchall() + if len(fetch_data)>0: + matches+=fetch_data + return + # else, fetches the ones without locale defined + matches+=connection.execute(f"SELECT {fetch_items} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND typeof(effective_locale)=typeof(null) ORDER BY rowid;", filter_data).fetchall() if len(final_cmdlist)>0: for x in range(len(final_cmdlist)): cmd=final_cmdlist[x] # prioritize exact match - if final_cmdlist_strictmatch[x]==True: matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2 ORDER BY rowid;", (cmd,)).fetchall() + if final_cmdlist_strictmatch[x]==True: fetch_matches_by_locale("effective_command=? AND command_match_strictness=2", (cmd,)) # also append matches with other strictness - matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness!=2 ORDER BY rowid;", (cmd,)).fetchall() - matches+=connection.execute(f"SELECT match_pattern, substitute_pattern, is_regex, end_match_here FROM {_globalvar.db_data_tablename} WHERE typeof(effective_command)=typeof(null) ORDER BY rowid;").fetchall() + fetch_matches_by_locale("effective_command=? AND command_match_strictness!=2", (cmd,)) + fetch_matches_by_locale("typeof(effective_command)=typeof(null)") for match_data in matches: try: if match_data[2]==True: # is regex -- Gitee From 74e33abb0f21338a0751698a806e4b3085cdf41b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 18:09:11 +0800 Subject: [PATCH 062/354] Add zh_CN localization to db_interface_tests --- src/db_interface_tests.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index 6828a7d..ee0d1e3 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -26,9 +26,11 @@ substrules_file=r""" filter_command rm [substitute_string] rm: missing operand locale:default rm says: missing arguments and options (>﹏<) + locale:zh_CN rm 说:缺少参数和选项 (>﹏<) [/substitute_string] [substitute_string] type rm --help for more information locale:default For more information, use rm --help (。ì _ í。) + locale:zh_CN 关于更多信息,请使用rm --help (。ì _ í。) [/substitute_string] [filter_commands] rm -rf @@ -38,6 +40,7 @@ substrules_file=r""" [/filter_commands] [substitute_regex] (?P.+): (?P.+): Permission denied locale:default \g says: Access denied to \g! ಥ_ಥ + locale:zh_CN \g 说:文件"\g"拒绝访问!ಥ_ಥ [/substitute_regex] filter_command ls # testing repeated entry detection @@ -45,27 +48,33 @@ substrules_file=r""" locale:default wef [/substitute_regex] [substitute_regex] (?P.+): unrecognized option '(?P.+)' - locale:default \g says: option '\g' not known! (ToT)/~~~' + locale:default \g says: option "\g" not known! (ToT)/~~~ + locale:zh_CN \g 说:未知选项"\g"!(ToT)/~~~ [/substitute_regex] unset_filter_command [substitute_regex] ^Warning:( ) locale:default o(≧v≦)o Note:\g<1> + locale:zh_CN o(≧v≦)o 提示:\g<1> [/substitute_regex] [substitute_regex] ^Error:( ) locale:default (ToT)/~~~ Error:\g<1> + locale:zh_CN (ToT)/~~~ 错误: [/substitute_regex] - [substitute_regex] invaild input( ){0,}$ + [substitute_regex] invaild input( )*$ locale:default input is invaild! ಥ_ಥ + locale:zh_CN 无效输入!ಥ_ಥ [/substitute_regex] set_options strictcmdmatch filter_command example_app install-stuff [substitute_string] Error: sample message locale:default Error: sample message! (>﹏<) + locale:zh_CN 错误:样例提示!(>﹏<) [/substitute_string] endmatchhere set_options exactcmdmatch filter_command rm -rf [substitute_regex] (?P.+): (?P.+): Permission denied locale:default \g says: Missing argument for operation! ಥ_ಥ + locale:zh_CN \g 说:缺少操作参数!ಥ_ಥ [/substitute_regex] set_options noexactcmdmatch nostrictcmdmatch {/substrules_section} -- Gitee From d4fdb71fccd5058ab48f5709a74520b817609f7a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 18:41:27 +0800 Subject: [PATCH 063/354] Improve allowed options and added some fixes --- src/clitheme/_generator/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index a9b27af..0359221 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -144,7 +144,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # can't be specified at the same time for opt in options_data: if opt!=option_name and opt in bool_options_unique: - handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same with \"{option2}\"", option1=option_name, option2=opt)) + handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\"", option1=option_name, option2=opt)) # set all other options to false for opt in bool_options_unique: final_options[opt]=False # if starts with no, set to false; else, set to true @@ -193,7 +193,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # parse leadtabindents leadspaces, and substesc options got_options=copy.copy(global_options) if len(lines_data[lineindex].split())>1: - got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True) + got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True, allowed_options=(["leadtabindents", "leadspaces"] if preserve_indents else []) if disallow_cmdmatch_options else None) for option in got_options.keys(): if option=="leadtabindents": if not preserve_indents and option not in global_options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) @@ -408,7 +408,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info strictness=2 command_filters=[] for cmd in command_strings: - command_filters.append(cmd) + command_filters.append(cmd.strip()) command_filter_strictness=strictness elif phrases[0]=="filter_command": check_enough_args(phrases, 2) -- Gitee From 4dd3366d59ace481018692a7cd0afb6493cacd50 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 18:45:11 +0800 Subject: [PATCH 064/354] Move splitarray_to_string function to _globalvar --- src/clitheme/_generator/__init__.py | 23 +++++++++-------------- src/clitheme/_globalvar.py | 5 +++++ src/clithemedef-test_testprogram.py | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 0359221..7362bdc 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -47,11 +47,6 @@ def add_entry(path, entry_name, entry_content, line_number_debug): # add entry t num=str(line_number_debug), name=entry_name)) f=open(target_path,'w', encoding="utf-8") f.write(entry_content+"\n") -def splitarray_to_string(split_content): - final="" - for phrase in split_content: - final+=phrase+" " - return final.strip() def write_infofile(path,filename,content,line_number_debug, header_name_debug): if not os.path.isdir(path): os.makedirs(path) @@ -230,10 +225,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="locale:", num=str(lineindex+1))) else: locale=results.groupdict()['locale'] - content=splitarray_to_string(phrases[1:]) + content=_globalvar.splitarray_to_string(phrases[1:]) else: check_enough_args(phrases, 3) - content=splitarray_to_string(phrases[2:]) + content=_globalvar.splitarray_to_string(phrases[2:]) locale=phrases[1] target_entry=copy.copy(entry_name) # substesc @@ -286,7 +281,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # Expect name, description, description_block, version, locales, locales_block, supported_apps, supported_apps_block if phrases[0]=="name" or phrases[0]=="version" or phrases[0]=="description": check_enough_args(phrases, 2) - content=splitarray_to_string(phrases[1:]) + content=_globalvar.splitarray_to_string(phrases[1:]) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ "clithemeinfo_"+phrases[0],\ @@ -344,9 +339,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info subsection="" # clear subsection elif phrases[0]=="in_subsection": check_enough_args(phrases, 2) - if _globalvar.sanity_check(splitarray_to_string(phrases[1:]))==False: + if _globalvar.sanity_check(_globalvar.splitarray_to_string(phrases[1:]))==False: handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) - subsection=splitarray_to_string(phrases[1:]) + subsection=_globalvar.splitarray_to_string(phrases[1:]) elif phrases[0]=="unset_domainapp": check_extra_args(phrases, 1, use_exact_count=True) domainapp=""; subsection="" @@ -356,9 +351,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="entry" or phrases[0]=="[entry]": check_enough_args(phrases, 2) # Prevent leading . & prevent /,\ in entry name - if _globalvar.sanity_check(splitarray_to_string(phrases[1:]))==False: + if _globalvar.sanity_check(_globalvar.splitarray_to_string(phrases[1:]))==False: handle_error(fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) - entry_name=splitarray_to_string(phrases[1:]) + entry_name=_globalvar.splitarray_to_string(phrases[1:]) if subsection!="": entry_name=subsection+" "+entry_name if domainapp!="": entry_name=domainapp+" "+entry_name recursive_mkdir(datapath, entry_name, lineindex+1) @@ -412,7 +407,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info command_filter_strictness=strictness elif phrases[0]=="filter_command": check_enough_args(phrases, 2) - content=splitarray_to_string(phrases[1:]) + content=_globalvar.splitarray_to_string(phrases[1:]) strictness=0 for this_option in global_options: if this_option=="strictcmdmatch" and global_options['strictcmdmatch']==True: @@ -427,7 +422,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="[substitute_string]" or phrases[0]=="[substitute_regex]": check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} - handle_entry(splitarray_to_string(phrases[1:]), end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) + handle_entry(_globalvar.splitarray_to_string(phrases[1:]), end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) elif phrases[0]=="set_options": check_enough_args(phrases, 2) handle_set_global_options(phrases[1:]) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index e3e2493..48f51d2 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -93,6 +93,11 @@ def sanity_check(path: str, use_orig: bool=False) -> bool: return True ## Convenience functions +def splitarray_to_string(split_content): + final="" + for phrase in split_content: + final+=phrase+" " + return final.strip() def get_locale(debug_mode: bool=False): lang=[] # Skip $LANGUAGE if both $LANG and $LC_ALL is set to C (treat empty as C also) diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index 322d7f7..63c0249 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -57,7 +57,7 @@ for line in expected_data_frontend.splitlines(): entry_path=None if len(phrases)>2: descriptor=frontend.FetchDescriptor(domain_name=phrases[0],app_name=phrases[1]) - entry_path=_generator.splitarray_to_string(phrases[2:]) # just being lazy here + entry_path=_globalvar.splitarray_to_string(phrases[2:]) # just being lazy here else: descriptor=frontend.FetchDescriptor() entry_path=current_path_frontend -- Gitee From 467450dc05be47954ec58b97684a23cbfdf9dce5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 18:51:35 +0800 Subject: [PATCH 065/354] Remove unused code in db_interface_tests --- src/db_interface_tests.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index ee0d1e3..a17afb6 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -80,10 +80,6 @@ substrules_file=r""" {/substrules_section} """ -# db_interface.init_db(":memory:") -# # record substitute patterns -# for dat in subst_patterns: db_interface.add_subst_entry(match_pattern=dat[0], substitute_pattern=dat[1], effective_commands=dat[3], is_regex=dat[2], command_match_strictness=dat[4], end_match_here=dat[5]) - _generator.generate_data_hierarchy(substrules_file) db_interface.connection=db_interface.sqlite3.connect(_generator.path+"/"+_globalvar.db_filename) -- Gitee From 1feb5b7f7d4c5683679c09f25ab5a57403b8a646 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 19:20:59 +0800 Subject: [PATCH 066/354] Properly handle bad match and substitute patterns --- src/clitheme/_generator/__init__.py | 8 +++++++- src/clitheme/_generator/db_interface.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 7362bdc..1ae93bf 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -2,6 +2,7 @@ Generator function used in applying themes (should not be invoked directly) """ import os +import sys import string import random import re @@ -211,6 +212,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info substrules_entries=[] # (match_content, substitute_content, locale) substrules_entries_linenumber=[] substrules_endmatchhere=substrules_options['end_match_here'] if 'end_match_here' in substrules_options else False + if is_substrules: + # check if patterns are valid + try: re.compile(entry_name) + except re.error: handle_error(fd.feof("invaild-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(lineindex+1), error_msg=sys.exc_info()[1])) while lineindex0: -- Gitee From f118be3c2bc8d319e65c65f5d783a602c03c8e4e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 19:31:46 +0800 Subject: [PATCH 067/354] Add information about Gitee repo Issues section --- README.en.md | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.en.md b/README.en.md index 7f6d986..f337f23 100644 --- a/README.en.md +++ b/README.en.md @@ -264,5 +264,6 @@ Run `dpkg-buildpackage -b` to build the package. A `.deb` file will be generated - https://gitee.com/swiftycode/clitheme-wiki-repo - https://github.com/swiftycode256/clitheme-wiki-repo - This repository is also synced onto GitHub (using Gitee automatic sync feature): https://github.com/swiftycode256/clitheme +- The latest developments, future plans, and in-development features of this project are detailed in the Issues section of the Gitee repository: https://gitee.com/swiftycode/clitheme/issues - You are welcome to propose suggestions and changes using Issues and Pull Requests - Use the Wiki repositories listed above for Wiki-related suggestions \ No newline at end of file diff --git a/README.md b/README.md index 09fa6e2..c11df66 100644 --- a/README.md +++ b/README.md @@ -264,5 +264,6 @@ rm -rf buildtmp srctmp - https://gitee.com/swiftycode/clitheme-wiki-repo - https://github.com/swiftycode256/clitheme-wiki-repo - 本仓库中的代码也同步在GitHub上(使用Gitee仓库镜像功能自动同步):https://github.com/swiftycode256/clitheme +- 该项目的最新进展、未来计划、和开发中的新功能会在这里Gitee仓库中的Issues里列出:https://gitee.com/swiftycode/clitheme/issues - 欢迎通过Issues和Pull Requests提交建议和改进。 - Wiki页面也可以;你可以在上方列出的仓库中提交Issues和Pull Requests \ No newline at end of file -- Gitee From 04fd9690121d5db9f8d52d922592745c895a7d83 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 22:08:08 +0800 Subject: [PATCH 068/354] Implement smartcmdmatch; Remove offending break statements in db_interface; Fix option-conflict-err typo in _generator. --- src/clitheme/_generator/__init__.py | 14 ++++++--- src/clitheme/_generator/db_interface.py | 42 ++++++++++++++++--------- src/db_interface_tests.py | 30 ++++++++++++++++-- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 1ae93bf..eaf1cfd 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -106,12 +106,12 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # defined sub-processing functions def parse_options(options_data: list[str], merge_global_options: bool, allowed_options: Optional[list]=None) -> dict: nonlocal global_options - # value options: leadtabindents, leadspaces + # value options: options requiring an integer value value_options=["leadtabindents", "leadspaces"] - # on/off options: substesc, strictcmdmatch, exactcmdmatch (use no<...> to disable) - bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "endmatchhere"] + # on/off options (use no<...> to disable) + bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere"] # only one of these options can be set to true at the same time - bool_options_unique=["strictcmdmatch", "exactcmdmatch"] + bool_options_unique=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"] final_options={} if merge_global_options: final_options=copy.copy(global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options @@ -140,7 +140,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # can't be specified at the same time for opt in options_data: if opt!=option_name and opt in bool_options_unique: - handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\"", option1=option_name, option2=opt)) + handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name, option2=opt)) # set all other options to false for opt in bool_options_unique: final_options[opt]=False # if starts with no, set to false; else, set to true @@ -407,6 +407,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info strictness=1 elif this_option=="exactcmdmatch" and got_options['exactcmdmatch']==True: strictness=2 + elif this_option=="smartcmdmatch" and got_options['smartcmdmatch']==True: + strictness=-1 command_filters=[] for cmd in command_strings: command_filters.append(cmd.strip()) @@ -420,6 +422,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info strictness=1 elif this_option=="exactcmdmatch" and global_options['exactcmdmatch']==True: strictness=2 + elif this_option=="smartcmdmatch" and global_options['smartcmdmatch']==True: + strictness=-1 command_filters=[content] command_filter_strictness=strictness elif phrases[0]=="unset_filter_command": diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 3323674..89eb783 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -24,7 +24,7 @@ def init_db(file_path: str): connection.commit() def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, line_number_debug: int=-1): - cmdlist=[] + cmdlist: list[str]=[] re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid # handle condition where no effective_locale is specified ("default") locale_condition="AND effective_locale=?" if effective_locale!=None else "AND typeof(effective_locale)=typeof(?)" @@ -43,11 +43,11 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma # remove any existing values with the same match_pattern and effective_command and command_match_strictness(if ==2) strictness_condition="" if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" - if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(),cmd, effective_locale)).fetchall())>0: + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(), cmd.strip(), effective_locale)).fetchall())>0: print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") - connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(),cmd, effective_locale)) + connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(), cmd.strip(), effective_locale)) # insert the entry into the main table - connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd, is_regex, command_match_strictness, end_match_here, effective_locale)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd.strip(), is_regex, command_match_strictness, end_match_here, effective_locale)) connection.commit() def match_content(content: bytes, command: Optional[str]=None) -> bytes: @@ -70,26 +70,40 @@ def match_content(content: bytes, command: Optional[str]=None) -> bytes: cmdlist.sort(key=split_len, reverse=True) # prioritize effective_command with exact match requirement cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),)).fetchall()+cmdlist + 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"^-([a-zA-z0-9]+)",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 # attempt to find matching command for tp in cmdlist: - cmd=tp[0] # extract value from tuple - strictness=tp[1] # strictness setting + match_cmd: str=tp[0].strip() # extract value from tuple + strictness: int=tp[1] # strictness setting success=True if strictness==1: # must start with pattern in terms of space-separated phrases - condition=len(cmd.split()): Permission denied", "rm -rf") # test exactcmdmatch (substitution rule containing this option should be prioritized over previous rules) + ("rm: : Permission denied", "rm -rf"), # test exactcmdmatch (substitution rule containing this option should be prioritized over previous rules) + ("example_app: using recursive directories", "example_app -rlc"), # test smartcmdmatch + ("example_app: using list options", "example_app -rlc"), # test smartcmdmatch ] # substitute patterns substrules_file=r""" @@ -32,6 +34,7 @@ substrules_file=r""" locale:default For more information, use rm --help (。ì _ í。) locale:zh_CN 关于更多信息,请使用rm --help (。ì _ í。) [/substitute_string] + [filter_commands] rm -rf cat @@ -42,6 +45,7 @@ substrules_file=r""" locale:default \g says: Access denied to \g! ಥ_ಥ locale:zh_CN \g 说:文件"\g"拒绝访问!ಥ_ಥ [/substitute_regex] + filter_command ls # testing repeated entry detection [substitute_regex] (?P.+): unrecognized option '(?P.+)' @@ -52,6 +56,8 @@ substrules_file=r""" locale:zh_CN \g 说:未知选项"\g"!(ToT)/~~~ [/substitute_regex] unset_filter_command + + # global substitutions [substitute_regex] ^Warning:( ) locale:default o(≧v≦)o Note:\g<1> locale:zh_CN o(≧v≦)o 提示:\g<1> @@ -64,19 +70,39 @@ substrules_file=r""" locale:default input is invaild! ಥ_ಥ locale:zh_CN 无效输入!ಥ_ಥ [/substitute_regex] + set_options strictcmdmatch filter_command example_app install-stuff [substitute_string] Error: sample message locale:default Error: sample message! (>﹏<) locale:zh_CN 错误:样例提示!(>﹏<) [/substitute_string] endmatchhere + set_options exactcmdmatch filter_command rm -rf [substitute_regex] (?P.+): (?P.+): Permission denied locale:default \g says: Missing argument for operation! ಥ_ಥ locale:zh_CN \g 说:缺少操作参数!ಥ_ಥ [/substitute_regex] - set_options noexactcmdmatch nostrictcmdmatch + + set_options noexactcmdmatch + filter_command example_app + [substitute_string] example_app: + locale:default o(≧v≦)o example_app says: + locale:zh_CN o(≧v≦)o example_app 说: + [/substitute_string] + set_options smartcmdmatch + filter_command example_app -r + [substitute_string] using recursive directories + locale:default using recursive directories! (。ì _ í。) + locale:zh_CN 正在使用子路径!(。ì _ í。) + [/substitute_string] + filter_command example_app -l + [substitute_string] using list options + locale:default using list options! (⊙ω⊙) + locale:zh_CN 正在使用列表选项!(⊙ω⊙) + [/substitute_string] + set_options nosmartcmdmatch {/substrules_section} """ -- Gitee From 5ab918c29ab4e942bae8ef0dc1c70e4f2b421f6c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Apr 2024 22:24:34 +0800 Subject: [PATCH 069/354] Update version (v2.0-dev20240420) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index c0297e9..d8ee37f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='1.2_dev20240414' +pkgver='2.0_dev20240420' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 84702ca..e07f8cc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (1.2-dev20240414-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240420-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sun, 14 Apr 2024 17:35:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 20 Apr 2024 22:21:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 89dc5f4..ee0ce62 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.2-dev20240414" -major=1 -minor=2 +__version__="2.0-dev20240420" +major=2 +minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="1.2_dev20240414" +version_main="2.0_dev20240420" version_buildnumber=1 \ No newline at end of file -- Gitee From 17977cd1fc5804b1f5b763f0a1e467b62f949bcf Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 21 Apr 2024 13:06:17 +0800 Subject: [PATCH 070/354] Remove extra newline at end of entry content Sorry I didn't realize that the add_entry function in _generator adds and extra newline, so I have to add it back --- src/clitheme/frontend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 9b4014e..acd252c 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -195,7 +195,8 @@ class FetchDescriptor(): f=open(p,'r', encoding="utf-8") dat=f.read() if self.debug_mode: print("Success:\n> "+dat) - return dat + # since the generator adds an extra newline in the entry data, we need to remove it + return re.sub(r"\n\Z", "", dat) except (FileNotFoundError, IsADirectoryError): if self.debug_mode: print("Failed") return fallback_string -- Gitee From 8884b51593e8d0aba3a2b67e306858d579fc239d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 23 Apr 2024 21:36:43 +0800 Subject: [PATCH 071/354] Fix description headerinfo being written to wrong file When using the new [description] syntax, the file is written to clithemeinfo_description_v2 intead of clithemeinfo_description. This is now fixed by adding missing conditional checks. --- src/clitheme/_generator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index eaf1cfd..a080536 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -305,7 +305,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content=""; filename="" endphrase="end_block" if not phrases[0].endswith("_block"): endphrase=phrases[0].replace("[", "[/") - if phrases[0]=="description_block": + if phrases[0]=="description_block" or phrases[0]=="[description]": content=handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase=endphrase) filename=f"clithemeinfo_{re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')}" else: -- Gitee From 89a4521e1929f0a2b29269089972651680c97977 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 23 Apr 2024 21:42:15 +0800 Subject: [PATCH 072/354] Add argument types in _generator functions --- src/clitheme/_generator/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index a080536..f9339da 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -21,11 +21,11 @@ fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subse path="" silence_warn=False -def handle_error(message): +def handle_error(message: str): raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) -def handle_warning(message): +def handle_warning(message: str): if not silence_warn: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) -def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate directories (excluding file itself) +def recursive_mkdir(path: str, entry_name: str, line_number_debug: int): # recursively generate directories (excluding file itself) current_path=path current_entry="" # for error output for x in entry_name.split()[:-1]: @@ -36,7 +36,7 @@ def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate num=str(line_number_debug), name=current_entry)) elif os.path.isdir(str(current_path))==False: # directory does not exist os.mkdir(current_path) -def add_entry(path, entry_name, entry_content, line_number_debug): # add entry to where it belongs (assuming recursive_mkdir already completed) +def add_entry(path: str, entry_name: str, entry_content: str, line_number_debug: int): # add entry to where it belongs (assuming recursive_mkdir already completed) target_path=path for x in entry_name.split(): target_path+="/"+x @@ -48,7 +48,7 @@ def add_entry(path, entry_name, entry_content, line_number_debug): # add entry t num=str(line_number_debug), name=entry_name)) f=open(target_path,'w', encoding="utf-8") f.write(entry_content+"\n") -def write_infofile(path,filename,content,line_number_debug, header_name_debug): +def write_infofile(path: str, filename: str, content: str, line_number_debug: int, header_name_debug: str): if not os.path.isdir(path): os.makedirs(path) target_path=path+"/"+filename -- Gitee From ecbac6e81a2bf19921cb8c782449bbfe8d8d5828 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 23 Apr 2024 22:04:14 +0800 Subject: [PATCH 073/354] Merge theme info filename into _globalvar Makes managing this information more easier --- src/clitheme/_generator/__init__.py | 8 ++++---- src/clitheme/_globalvar.py | 4 +++- src/clitheme/cli.py | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index f9339da..7de9f0d 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -290,14 +290,14 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content=_globalvar.splitarray_to_string(phrases[1:]) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ - "clithemeinfo_"+phrases[0],\ + _globalvar.generator_info_filename.format(info=phrases[0]),\ content,lineindex+1,phrases[0]) # e.g. [...]/theme-info/1/clithemeinfo_name elif phrases[0]=="locales" or phrases[0]=="supported_apps": check_enough_args(phrases, 2) content=phrases[1:] write_infofile_newlines( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ - "clithemeinfo_"+phrases[0]+"_v2",\ + _globalvar.generator_info_v2filename.format(info=phrases[0]),\ content,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]": check_extra_args(phrases, 1, use_exact_count=True) @@ -307,10 +307,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if not phrases[0].endswith("_block"): endphrase=phrases[0].replace("[", "[/") if phrases[0]=="description_block" or phrases[0]=="[description]": content=handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase=endphrase) - filename=f"clithemeinfo_{re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')}" + filename=_globalvar.generator_info_filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) else: content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase) - filename=f"clithemeinfo_{re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')}_v2" + filename=_globalvar.generator_info_v2filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ filename,\ diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 48f51d2..d9cbd5d 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -41,7 +41,9 @@ clitheme_temp_root="/tmp" if os.name!="nt" else os.environ['TEMP'] ## _generator file and folder names generator_info_pathname="theme-info" # e.g. ~/.local/share/clitheme/theme-info generator_data_pathname="theme-data" # e.g. ~/.local/share/clitheme/theme-data -generator_index_filename="current_theme_index" +generator_index_filename="current_theme_index" # e.g. [...]/theme-info/current_theme_index +generator_info_filename="clithemeinfo_{info}" # e.g. [...]/theme-info/1/clithemeinfo_name +generator_info_v2filename=generator_info_filename+"_v2" # e.g. [...]/theme-info/1/clithemeinfo_description_v2 ## _generator.db_interface file and table names db_data_tablename="clitheme_subst_data" diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 7992448..4f275de 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -152,44 +152,44 @@ def get_current_theme_info(): if not os.path.isdir(target_path): continue # skip current_theme_index file # name name="(Unknown)" - if os.path.isfile(target_path+"/"+"clithemeinfo_name"): - name=open(target_path+"/"+"clithemeinfo_name", 'r', encoding="utf-8").read().strip() + 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)) # version version="(Unknown)" - if os.path.isfile(target_path+"/"+"clithemeinfo_version"): - version=open(target_path+"/"+"clithemeinfo_version", 'r', encoding="utf-8").read().strip() + if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="version")): + version=open(target_path+"/"+_globalvar.generator_info_filename.format(info="version"), 'r', encoding="utf-8").read().strip() print(f.feof("version-str", "Version: {ver}", ver=version)) # description description="(Unknown)" - if os.path.isfile(target_path+"/"+"clithemeinfo_description"): - description=open(target_path+"/"+"clithemeinfo_description", 'r', encoding="utf-8").read() + if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="description")): + description=open(target_path+"/"+_globalvar.generator_info_filename.format(info="description"), 'r', encoding="utf-8").read() print(f.reof("description-str", "Description:")) print(description) # locales locales="(Unknown)" # version 2: items are separated by newlines instead of spaces - if os.path.isfile(target_path+"/"+"clithemeinfo_locales_v2"): - locales=open(target_path+"/"+"clithemeinfo_locales_v2", 'r', encoding="utf-8").read().strip() + if os.path.isfile(target_path+"/"+_globalvar.generator_info_v2filename.format(info="locales")): + locales=open(target_path+"/"+_globalvar.generator_info_v2filename.format(info="locales"), 'r', encoding="utf-8").read().strip() print(f.reof("locales-str", "Supported locales:")) for locale in locales.splitlines(): if locale.strip()!="": print(f.feof("list-item", "• {content}", content=locale.strip())) - elif os.path.isfile(target_path+"/"+"clithemeinfo_locales"): - locales=open(target_path+"/"+"clithemeinfo_locales", 'r', encoding="utf-8").read().strip() + elif os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="locales")): + locales=open(target_path+"/"+_globalvar.generator_info_filename.format(info="locales"), 'r', encoding="utf-8").read().strip() print(f.reof("locales-str", "Supported locales:")) for locale in locales.split(): print(f.feof("list-item", "• {content}", content=locale.strip())) # supported_apps supported_apps="(Unknown)" - if os.path.isfile(target_path+"/"+"clithemeinfo_supported_apps_v2"): - supported_apps=open(target_path+"/"+"clithemeinfo_supported_apps_v2", 'r', encoding="utf-8").read().strip() + if os.path.isfile(target_path+"/"+_globalvar.generator_info_v2filename.format(info="supported_apps")): + supported_apps=open(target_path+"/"+_globalvar.generator_info_v2filename.format(info="supported_apps"), 'r', encoding="utf-8").read().strip() print(f.reof("supported-apps-str", "Supported apps:")) for app in supported_apps.splitlines(): if app.strip()!="": print(f.feof("list-item", "• {content}", content=app.strip())) - elif os.path.isfile(target_path+"/"+"clithemeinfo_supported_apps"): - supported_apps=open(target_path+"/"+"clithemeinfo_supported_apps", 'r', encoding="utf-8").read().strip() + elif os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="supported_apps")): + supported_apps=open(target_path+"/"+_globalvar.generator_info_filename.format(info="supported_apps"), 'r', encoding="utf-8").read().strip() print(f.reof("supported-apps-str", "Supported apps:")) for app in supported_apps.split(): print(f.feof("list-item", "• {content}", content=app.strip())) -- Gitee From 03e1e22b43e6d7f4130adecba8825e19e3d79dd3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 23 Apr 2024 22:05:57 +0800 Subject: [PATCH 074/354] Update version (v2.0-dev20240423) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index d8ee37f..ea9c9f3 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240420' +pkgver='2.0_dev20240423' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index e07f8cc..bf73ed5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240420-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240423-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 20 Apr 2024 22:21:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 23 Apr 2024 22:05:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index ee0ce62..ee3ee68 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240420" +__version__="2.0-dev20240423" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240420" +version_main="2.0_dev20240423" version_buildnumber=1 \ No newline at end of file -- Gitee From a636d89279edc30a7adae49b4f08c327ce2404ce Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 23 Apr 2024 22:13:39 +0800 Subject: [PATCH 075/354] Add Arch Linux package files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2e4a816..4d30e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ buildtmp buildtmp/* srctmp srctmp/* +*.pkg.tar.zst .DS_Store \ No newline at end of file -- Gitee From 32050c36bb3c502f9c7eebd0203fde7d8a961b46 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 12:09:56 +0800 Subject: [PATCH 076/354] Add substesc support for match pattern --- src/clitheme/_generator/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 7de9f0d..2fe35aa 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -432,7 +432,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="[substitute_string]" or phrases[0]=="[substitute_regex]": check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} - handle_entry(_globalvar.splitarray_to_string(phrases[1:]), end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) + match_pattern=_globalvar.splitarray_to_string(phrases[1:]) + if "substesc" in global_options.keys() and global_options['substesc']==True: + match_pattern=match_pattern.replace("{{ESC}}", "\x1b") + handle_entry(match_pattern, end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) elif phrases[0]=="set_options": check_enough_args(phrases, 2) handle_set_global_options(phrases[1:]) -- Gitee From 6c69c7ac3a498e4130cc2d1d73a0ce9663b2a276 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 12:17:59 +0800 Subject: [PATCH 077/354] Add debug_mode control in db_interface --- src/clitheme/_generator/__init__.py | 1 + src/clitheme/_generator/db_interface.py | 11 +++++++---- src/db_interface_tests.py | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 2fe35aa..b0a74b7 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -387,6 +387,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if os.path.exists(path+"/"+_globalvar.db_filename): db_interface.connection=db_interface.sqlite3.connect(path+"/"+_globalvar.db_filename) else: db_interface.init_db(path+"/"+_globalvar.db_filename) + db_interface.debug_mode=not silence_warn while lineindex0: - print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") + handle_warning(f"Warning: Repeated entry at line {line_number_debug}, overwriting") connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition};", (match_pattern.strip(), effective_locale)) # insert the entry into the main table connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), is_regex, command_match_strictness, end_match_here, effective_locale)) @@ -44,7 +48,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma strictness_condition="" if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(), cmd.strip(), effective_locale)).fetchall())>0: - print(f"Warning: Repeated entry at line {line_number_debug}, overwriting") + handle_warning(f"Warning: Repeated entry at line {line_number_debug}, overwriting") connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(), cmd.strip(), effective_locale)) # insert the entry into the main table connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd.strip(), is_regex, command_match_strictness, end_match_here, effective_locale)) @@ -134,8 +138,7 @@ def match_content(content: bytes, command: Optional[str]=None) -> bytes: else: # is string content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) except: - print("Error occurred while matching string: ", end="") - print(sys.exc_info()[1]) + handle_warning("Error occurred while matching string: "+str(sys.exc_info()[1])) if match_data[3]==True: # endmatchoption is set break return content_str \ No newline at end of file diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index 07ad088..a63e684 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -106,6 +106,7 @@ substrules_file=r""" {/substrules_section} """ +db_interface.debug_mode=True _generator.generate_data_hierarchy(substrules_file) db_interface.connection=db_interface.sqlite3.connect(_generator.path+"/"+_globalvar.db_filename) -- Gitee From d20f312239e1fb9021de33e44f405231116f1dfc Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 19:06:31 +0800 Subject: [PATCH 078/354] First version of clitheme-exec module --- src/clitheme/_generator/db_interface.py | 6 +++ src/clitheme/exec/__init__.py | 30 ++++++++++++ src/clitheme/exec/__main__.py | 3 ++ src/clitheme/exec/output_handler_posix.py | 56 +++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 src/clitheme/exec/__init__.py create mode 100644 src/clitheme/exec/__main__.py create mode 100644 src/clitheme/exec/output_handler_posix.py diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index b65e6b5..3463154 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -1,4 +1,5 @@ import sys +import os import sqlite3 import re import copy @@ -26,6 +27,11 @@ def init_db(file_path: str): end_match_here INTEGER DEFAULT 0 NOT NULL \ );") connection.commit() +def connect_db(): + if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): + raise FileNotFoundError("No theme set or theme does not contain substrules") + global connection + connection=sqlite3.connect(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}") def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, line_number_debug: int=-1): cmdlist: list[str]=[] diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py new file mode 100644 index 0000000..4ea0976 --- /dev/null +++ b/src/clitheme/exec/__init__.py @@ -0,0 +1,30 @@ +""" +Module used for clitheme-exec (should not be invoked directly) +""" +import sys +import os +try: + from . import output_handler_posix + from .. import _globalvar +except ImportError: + import output_handler_posix + import _globalvar +def main(arguments: list[str]): + # get arguments + if len(arguments)<2: + print("Not enough arguments") + return 1 + # check dataase + if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): + print("No theme set; please set a theme") + return 1 + # determine platform + if os.name=="posix": + output_handler_posix.handler_main(arguments[1:]) + elif os.name=="nt": + print("Windows platform is not currently supported") + return 1 + else: + print("Unsupported platform") + return 1 + return 0 \ No newline at end of file diff --git a/src/clitheme/exec/__main__.py b/src/clitheme/exec/__main__.py new file mode 100644 index 0000000..d5057f0 --- /dev/null +++ b/src/clitheme/exec/__main__.py @@ -0,0 +1,3 @@ +from . import main +import sys +exit(main(sys.argv)) \ No newline at end of file diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py new file mode 100644 index 0000000..49326a7 --- /dev/null +++ b/src/clitheme/exec/output_handler_posix.py @@ -0,0 +1,56 @@ +import subprocess +import sys +import os +import pty +import select +import termios, tty +import time +import copy +try: + from .._generator import db_interface + from .. import _globalvar +except ImportError: + from _generator import db_interface + import _globalvar + + +def handler_main(command: list[str]): + db_interface.connect_db() + stdout_fd, stdout_slave=pty.openpty() + stderr_fd, stderr_slave=pty.openpty() + stdin_fd, stdin_slave=pty.openpty() + + env=copy.copy(os.environ) + process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stderr_slave, bufsize=0, close_fds=True, env=env) + while process.poll()==None: + try: + # update cbreak (realtime stdin) attributes from what the program sets + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) + fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.1)[0] + output_lines=[] # (line_content, is_stderr) + if sys.stdin in fds: + data=os.read(sys.stdin.fileno(), 1024) + if not data: break + os.write(stdin_fd, data) + if stdout_fd in fds: + data=os.read(stdout_fd, 1024) + #data=b'\x1b[33m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[33m')+b'\x1b[0m' # DEBUG purposes + #data=b'\x1b[33m'+data+b'\x1b[0m' # DEBUG purposes + for line in data.splitlines(): + output_lines.append((line,False)) + if stderr_fd in fds: + data=os.read(stderr_fd, 1024) + #data=b'\x1b[31m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[31m')+b'\x1b[0m' # DEBUG purposes + #data=b'\x1b[31m'+data+b'\x1b[0m' # DEBUG purposes + for line in data.splitlines(): + output_lines.append((line,True)) + # Process outputs + for line_data in output_lines: + line=line_data[0] + # TODO: handle is_stderr + # subst operation + subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command)) + os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line+b"\n") + except KeyboardInterrupt: + process.send_signal(2) #SIGINT + #os.write(stdin_fd, b'\x03') -- Gitee From b0f261c2fba6db48f4ea635642feb925e9c681d4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 19:06:51 +0800 Subject: [PATCH 079/354] Fix variable typo in _generator --- src/clitheme/_generator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index b0a74b7..ca3d5ca 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -252,7 +252,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if this_locale!="default": suffix="__"+this_locale if not is_substrules: add_entry(datapath, entry_name+suffix, content, lineindex+1) - else: substrules_entries.append((entry_name, content, None if locale=="default" else locale)); substrules_entries_linenumber.append(lineindex+1) + else: substrules_entries.append((entry_name, content, None if this_locale=="default" else this_locale)); substrules_entries_linenumber.append(lineindex+1) elif phrases[0]==end_phrase: if not is_substrules: check_extra_args(phrases, 1, use_exact_count=True) got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere"]) -- Gitee From fd3d4278f90309ea895d140cc1491bc2ff1af415 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 22:49:01 +0800 Subject: [PATCH 080/354] Implement stdout_only and stderr_only options --- src/clitheme/_generator/__init__.py | 34 +++++++++++--------- src/clitheme/_generator/db_interface.py | 38 ++++++++++++++--------- src/clitheme/exec/output_handler_posix.py | 3 +- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index ca3d5ca..e5e8116 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -109,9 +109,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # value options: options requiring an integer value value_options=["leadtabindents", "leadspaces"] # on/off options (use no<...> to disable) - bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere"] - # only one of these options can be set to true at the same time - bool_options_unique=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"] + bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere", "stdout_only", "stderr_only"] + # only one of these options can be set to true at the same time (specific to groups) + bool_options_unique_groups=[["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"], ["stdout_only", "stderr_only"]] final_options={} if merge_global_options: final_options=copy.copy(global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options @@ -136,13 +136,14 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info final_options[option_name]=value elif option_name in bool_options: # process unique bool options - if option_name_preserve_no in bool_options_unique: - # can't be specified at the same time - for opt in options_data: - if opt!=option_name and opt in bool_options_unique: - handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name, option2=opt)) - # set all other options to false - for opt in bool_options_unique: final_options[opt]=False + for bool_options_unique in bool_options_unique_groups: + if option_name_preserve_no in bool_options_unique: + # can't be specified at the same time + for opt in options_data: + if opt!=option_name and opt in bool_options_unique: + handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name, option2=opt)) + # set all other options to false + for opt in bool_options_unique: final_options[opt]=False # if starts with no, set to false; else, set to true final_options[option_name]=not option_name_preserve_no.startswith("no") else: @@ -206,12 +207,13 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) return blockinput_data def handle_entry(entry_name: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): - # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int, end_match_here: bool + # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int # expect locale, locale_block, end_entry nonlocal lineindex substrules_entries=[] # (match_content, substitute_content, locale) substrules_entries_linenumber=[] substrules_endmatchhere=substrules_options['end_match_here'] if 'end_match_here' in substrules_options else False + substrules_stdout_stderr_option=0 if is_substrules: # check if patterns are valid try: re.compile(entry_name) @@ -255,16 +257,20 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: substrules_entries.append((entry_name, content, None if this_locale=="default" else this_locale)); substrules_entries_linenumber.append(lineindex+1) elif phrases[0]==end_phrase: if not is_substrules: check_extra_args(phrases, 1, use_exact_count=True) - got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere"]) + got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere", "stdout_only", "stderr_only"]) for option in got_options: if option=="endmatchhere" and got_options['endmatchhere']==True: substrules_endmatchhere=True + elif option=="stdout_only" and got_options['stdout_only']==True: + substrules_stdout_stderr_option=1 + elif option=="stderr_only" and got_options['stderr_only']==True: + substrules_stdout_stderr_option=2 break else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) if is_substrules: for x in range(len(substrules_entries)): entry=substrules_entries[x] - try: db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x]) + try: db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x]) except re.error: handle_error(fd.feof("invaild-subst-pattern-err", "Bad substitute pattern at line {num} ({error_msg})", num=str(lineindex+1), error_msg=sys.exc_info()[1])) ## Main code @@ -402,7 +408,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # parse strictcmdmatch, exactcmdmatch, and other cmdmatch options here got_options=copy.copy(global_options) if len(lines_data[lineindex].split())>1: - got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True) + got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True, allowed_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"]) for this_option in got_options: if this_option=="strictcmdmatch" and got_options['strictcmdmatch']==True: strictness=1 diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 3463154..43cdf0b 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -16,15 +16,17 @@ def init_db(file_path: str): global connection connection=sqlite3.connect(file_path) # create the table - # command_match_strictness: 0:default match options, 1:must start with pattern, 2: must exactly equal pattern + # command_match_strictness: 0: default match options, 1: must start with pattern, 2: must exactly equal pattern + # stdout_stderr_only: 0: no limiter, 1: match stdout only, 2: match stderr only connection.execute(f"CREATE TABLE {_globalvar.db_data_tablename} ( \ match_pattern TEXT NOT NULL, \ substitute_pattern TEXT NOT NULL, \ - is_regex INTEGER DEFAULT true NOT NULL, \ + is_regex INTEGER NOT NULL, \ effective_command TEXT, \ effective_locale TEXT, \ - command_match_strictness INTEGER DEFAULT 0 NOT NULL, \ - end_match_here INTEGER DEFAULT 0 NOT NULL \ + command_match_strictness INTEGER NOT NULL, \ + end_match_here INTEGER NOT NULL, \ + stdout_stderr_only INTEGER NOT NULL \ );") connection.commit() def connect_db(): @@ -33,34 +35,39 @@ def connect_db(): global connection connection=sqlite3.connect(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}") -def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, line_number_debug: int=-1): +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, line_number_debug: int=-1): cmdlist: list[str]=[] re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid # 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"] if effective_commands!=None and len(effective_commands)>0: for cmd in effective_commands: # remove extra spaces in the command cmdlist.append(re.sub(r" {2,}", " ", cmd).strip()) else: # remove any existing values with the same match_pattern - if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition};", (match_pattern.strip(), effective_locale)).fetchall())>0: + match_condition=f"match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition} AND stdout_stderr_only=?" + match_params=(match_pattern.strip(), effective_locale, stdout_stderr_matchoption) + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: handle_warning(f"Warning: Repeated entry at line {line_number_debug}, overwriting") - connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition};", (match_pattern.strip(), effective_locale)) + 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} (match_pattern, substitute_pattern, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), is_regex, command_match_strictness, end_match_here, effective_locale)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern.strip(), substitute_pattern.strip(), None, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption)) for cmd in cmdlist: # remove any existing values with the same match_pattern and effective_command and command_match_strictness(if ==2) strictness_condition="" if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" - if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(), cmd.strip(), effective_locale)).fetchall())>0: + match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=?" + match_params=(match_pattern.strip(), cmd.strip(), effective_locale, stdout_stderr_matchoption) + if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: handle_warning(f"Warning: Repeated entry at line {line_number_debug}, overwriting") - connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE match_pattern=? AND effective_command=? {strictness_condition} {locale_condition};", (match_pattern.strip(), cmd.strip(), effective_locale)) + 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} (match_pattern, substitute_pattern, effective_command, is_regex, command_match_strictness, end_match_here, effective_locale) VALUES (?,?,?,?,?,?,?);", (match_pattern.strip(), substitute_pattern.strip(), cmd.strip(), is_regex, command_match_strictness, end_match_here, effective_locale)) + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern.strip(), substitute_pattern.strip(), cmd.strip(), is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption)) connection.commit() -def match_content(content: bytes, command: Optional[str]=None) -> bytes: +def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False) -> bytes: # Match order: # 1. Match rules with exactcmdmatch option set # 2. Match rules with command filter having the same first phrase @@ -117,18 +124,18 @@ def match_content(content: bytes, command: Optional[str]=None) -> bytes: 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" + fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only"] # get locales locales=_globalvar.get_locale() nonlocal matches # try the ones with locale defined for this_locale in locales: - fetch_data=connection.execute(f"SELECT {fetch_items} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND effective_locale=? ORDER BY rowid;", filter_data+(this_locale,)).fetchall() + fetch_data=connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND effective_locale=? ORDER BY rowid;", filter_data+(this_locale,)).fetchall() if len(fetch_data)>0: matches+=fetch_data return # else, fetches the ones without locale defined - matches+=connection.execute(f"SELECT {fetch_items} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND typeof(effective_locale)=typeof(null) ORDER BY rowid;", filter_data).fetchall() + matches+=connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND typeof(effective_locale)=typeof(null) ORDER BY rowid;", filter_data).fetchall() if len(final_cmdlist)>0: for x in range(len(final_cmdlist)): cmd=final_cmdlist[x] @@ -138,6 +145,7 @@ def match_content(content: bytes, command: Optional[str]=None) -> bytes: fetch_matches_by_locale("effective_command=? AND command_match_strictness!=2", (cmd,)) fetch_matches_by_locale("typeof(effective_command)=typeof(null)") for match_data in matches: + if match_data[4]!=0 and is_stderr+1!=match_data[4]: continue # check stdout/stderr constraint try: if match_data[2]==True: # is regex content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 49326a7..fdaa11d 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -47,9 +47,8 @@ def handler_main(command: list[str]): # Process outputs for line_data in output_lines: line=line_data[0] - # TODO: handle is_stderr # subst operation - subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command)) + subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line+b"\n") except KeyboardInterrupt: process.send_signal(2) #SIGINT -- Gitee From ad1cacac57a7dc434d1dfefd17701534cfca6d97 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 23:02:40 +0800 Subject: [PATCH 081/354] Only process unique options if it's being set to True --- src/clitheme/_generator/__init__.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index e5e8116..56b5975 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -136,14 +136,15 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info final_options[option_name]=value elif option_name in bool_options: # process unique bool options - for bool_options_unique in bool_options_unique_groups: - if option_name_preserve_no in bool_options_unique: - # can't be specified at the same time - for opt in options_data: - if opt!=option_name and opt in bool_options_unique: - handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name, option2=opt)) - # set all other options to false - for opt in bool_options_unique: final_options[opt]=False + if not option_name_preserve_no.startswith("no"): + for bool_options_unique in bool_options_unique_groups: + if option_name_preserve_no in bool_options_unique: + # can't be specified at the same time + for opt in options_data: + if opt!=option_name and opt in bool_options_unique: + handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name, option2=opt)) + # set all other options to false + for opt in bool_options_unique: final_options[opt]=False # if starts with no, set to false; else, set to true final_options[option_name]=not option_name_preserve_no.startswith("no") else: -- Gitee From 2fb64871f2c3b0ee4fe52c793ebf553d11cc5e6a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 23:29:33 +0800 Subject: [PATCH 082/354] Don't output the extra newline in theme description _generator adds an extra newline to the contents of theme info data, so we remove that when outputing the description. We are already doing the same thing with strip() with other info data. --- 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 4f275de..587af2f 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -165,7 +165,7 @@ def get_current_theme_info(): if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="description")): description=open(target_path+"/"+_globalvar.generator_info_filename.format(info="description"), 'r', encoding="utf-8").read() print(f.reof("description-str", "Description:")) - print(description) + print(re.sub(r"\n\Z", "", description)) # remove the extra newline added by _generator # locales locales="(Unknown)" # version 2: items are separated by newlines instead of spaces -- Gitee From 61e995398768974fb299ad8042920149533c8c4f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 26 Apr 2024 23:32:13 +0800 Subject: [PATCH 083/354] Update version (v2.0-dev20240426) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index ea9c9f3..a56d3b0 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240423' +pkgver='2.0_dev20240426' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index bf73ed5..3bfbf0e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240423-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240426-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Tue, 23 Apr 2024 22:05:00 +0800 + -- swiftycode <3291929745@qq.com> Fri, 26 Apr 2024 23:30:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index ee3ee68..4dc5487 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240423" +__version__="2.0-dev20240426" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240423" +version_main="2.0_dev20240426" version_buildnumber=1 \ No newline at end of file -- Gitee From 1292393f73463bd342850f33e3bbad54b8cda6c3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 12:12:32 +0800 Subject: [PATCH 084/354] Tell cli applications to not use pager --- src/clitheme/exec/output_handler_posix.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index fdaa11d..00f6759 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -21,6 +21,10 @@ def handler_main(command: list[str]): stdin_fd, stdin_slave=pty.openpty() env=copy.copy(os.environ) + # Tell apps that the terminal aren't designed to handle TUI + env['TERM']="dumb" + # Prevent apps from using "less" or "more" as pager, as it won't work here + env['PAGER']="cat" process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stderr_slave, bufsize=0, close_fds=True, env=env) while process.poll()==None: try: -- Gitee From 1e876c7b85d8be17bc121055bcc4e30eb23a1506 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 12:16:20 +0800 Subject: [PATCH 085/354] Allow clitheme-exec to run if no theme is set --- src/clitheme/exec/__init__.py | 3 +-- src/clitheme/exec/output_handler_posix.py | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 4ea0976..f02706b 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -16,8 +16,7 @@ def main(arguments: list[str]): return 1 # check dataase if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): - print("No theme set; please set a theme") - return 1 + print("Warning: no theme set or theme does not have substrules") # determine platform if os.name=="posix": output_handler_posix.handler_main(arguments[1:]) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 00f6759..16673b9 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -15,7 +15,9 @@ except ImportError: def handler_main(command: list[str]): - db_interface.connect_db() + do_subst=True + try: db_interface.connect_db() + except FileNotFoundError: do_subst=False stdout_fd, stdout_slave=pty.openpty() stderr_fd, stderr_slave=pty.openpty() stdin_fd, stdin_slave=pty.openpty() @@ -52,7 +54,8 @@ def handler_main(command: list[str]): for line_data in output_lines: line=line_data[0] # subst operation - subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) + subst_line=copy.copy(line) + if do_subst: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line+b"\n") except KeyboardInterrupt: process.send_signal(2) #SIGINT -- Gitee From 199fc0ed649f6fa5d86efa4ebdbd69e36fe12281 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 12:21:17 +0800 Subject: [PATCH 086/354] Increase read block size in output_handler --- 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 16673b9..5b4dc9f 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -34,18 +34,19 @@ def handler_main(command: list[str]): termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.1)[0] output_lines=[] # (line_content, is_stderr) + readsize=40960 if sys.stdin in fds: - data=os.read(sys.stdin.fileno(), 1024) + data=os.read(sys.stdin.fileno(), readsize) if not data: break os.write(stdin_fd, data) if stdout_fd in fds: - data=os.read(stdout_fd, 1024) + data=os.read(stdout_fd, readsize) #data=b'\x1b[33m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[33m')+b'\x1b[0m' # DEBUG purposes #data=b'\x1b[33m'+data+b'\x1b[0m' # DEBUG purposes for line in data.splitlines(): output_lines.append((line,False)) if stderr_fd in fds: - data=os.read(stderr_fd, 1024) + data=os.read(stderr_fd, readsize) #data=b'\x1b[31m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[31m')+b'\x1b[0m' # DEBUG purposes #data=b'\x1b[31m'+data+b'\x1b[0m' # DEBUG purposes for line in data.splitlines(): -- Gitee From dc4d75cfbc39528cacf0d14e410fa6b71b21dc77 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 14:48:45 +0800 Subject: [PATCH 087/354] Implement debug mode in clitheme-exec --- src/clitheme/exec/__init__.py | 18 ++++++++-- src/clitheme/exec/output_handler_posix.py | 41 +++++++++++++++++++---- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index f02706b..392eed4 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -14,12 +14,26 @@ def main(arguments: list[str]): if len(arguments)<2: print("Not enough arguments") return 1 - # check dataase + # check database if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): print("Warning: no theme set or theme does not have substrules") + # process debug mode arguments + debug_mode=[] + argcount=0 + for arg in arguments[1:]: + if not arg.startswith('-'): break + argcount+=1 + if arg=="--debug": + debug_mode.append("normal") + elif arg=="--debug-color": + debug_mode.append("color") + elif arg=="--debug-newlines": + debug_mode.append("newlines") + elif arg=="--debug-showchars": + debug_mode.append("showchars") # determine platform if os.name=="posix": - output_handler_posix.handler_main(arguments[1:]) + output_handler_posix.handler_main(arguments[1+argcount:], debug_mode) elif os.name=="nt": print("Windows platform is not currently supported") return 1 diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 5b4dc9f..5ed1c7d 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -3,9 +3,10 @@ import sys import os import pty import select -import termios, tty -import time +import termios import copy +import string +from typing import Optional try: from .._generator import db_interface from .. import _globalvar @@ -14,7 +15,30 @@ except ImportError: import _globalvar -def handler_main(command: list[str]): +def process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=False, matched: bool=False) -> list[bytes]: + # debug_mode: newlines, showchars, color + final_lines=[] + for x in range(len(lines)): + line=lines[x] + if "newlines" in debug_mode: + if not line.endswith(b'\n') and not line.endswith(b'\f') and not line.endswith(b'\x0c'): + line+=b"\n" + if "showchars" in debug_mode: + wrapper=b"\x1b[32m{}\x1b[0m" + if "color" in debug_mode: wrapper+=bytes(f"\x1b[{'31' if is_stderr else '33'}m", 'utf-8') + line=line.replace(b'\x1b', wrapper.replace(b'{}', b'{{ESC}}')) # this must come before anything else + line=line.replace(b'\r', wrapper.replace(b'{}',b'\\r')) + line=line.replace(b'\b', wrapper.replace(b'{}',b'\\x08')) + line=line.replace(b'\a', wrapper.replace(b'{}',b'\\x07')) + if "color" in debug_mode: + line=bytes(f"\x1b[{'31' if is_stderr else '33'}m", 'utf-8')+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 ''}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 + +def handler_main(command: list[str], debug_mode: list[str]=[]): do_subst=True try: db_interface.connect_db() except FileNotFoundError: do_subst=False @@ -34,7 +58,7 @@ def handler_main(command: list[str]): termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.1)[0] output_lines=[] # (line_content, is_stderr) - readsize=40960 + readsize=1000000 if sys.stdin in fds: data=os.read(sys.stdin.fileno(), readsize) if not data: break @@ -43,13 +67,15 @@ def handler_main(command: list[str]): data=os.read(stdout_fd, readsize) #data=b'\x1b[33m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[33m')+b'\x1b[0m' # DEBUG purposes #data=b'\x1b[33m'+data+b'\x1b[0m' # DEBUG purposes - for line in data.splitlines(): + lines=data.splitlines(keepends=True) + for line in lines: output_lines.append((line,False)) if stderr_fd in fds: data=os.read(stderr_fd, readsize) #data=b'\x1b[31m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[31m')+b'\x1b[0m' # DEBUG purposes #data=b'\x1b[31m'+data+b'\x1b[0m' # DEBUG purposes - for line in data.splitlines(): + lines=data.splitlines(keepends=True) + for line in lines: output_lines.append((line,True)) # Process outputs for line_data in output_lines: @@ -57,7 +83,8 @@ def handler_main(command: list[str]): # subst operation subst_line=copy.copy(line) if do_subst: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) - os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line+b"\n") + subst_line=process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line)[0] + os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line) except KeyboardInterrupt: process.send_signal(2) #SIGINT #os.write(stdin_fd, b'\x03') -- Gitee From 8162a24c5f516bac7d443b6fc9f730624a9d0e9c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 19:15:22 +0800 Subject: [PATCH 088/354] Finish reading all output before exiting clitheme-exec --- src/clitheme/exec/__init__.py | 2 +- src/clitheme/exec/output_handler_posix.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 392eed4..983c219 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -33,7 +33,7 @@ def main(arguments: list[str]): debug_mode.append("showchars") # determine platform if os.name=="posix": - output_handler_posix.handler_main(arguments[1+argcount:], debug_mode) + return output_handler_posix.handler_main(arguments[1+argcount:], debug_mode) elif os.name=="nt": print("Windows platform is not currently supported") return 1 diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 5ed1c7d..15e70e4 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -52,10 +52,11 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): # Prevent apps from using "less" or "more" as pager, as it won't work here env['PAGER']="cat" process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stderr_slave, bufsize=0, close_fds=True, env=env) - while process.poll()==None: + while True: try: # update cbreak (realtime stdin) attributes from what the program sets - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) + try: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) + except termios.error: pass fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.1)[0] output_lines=[] # (line_content, is_stderr) readsize=1000000 @@ -77,6 +78,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): lines=data.splitlines(keepends=True) for line in lines: output_lines.append((line,True)) + if process.poll()!=None and len(output_lines)==0: break # Process outputs for line_data in output_lines: line=line_data[0] @@ -88,3 +90,4 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): except KeyboardInterrupt: process.send_signal(2) #SIGINT #os.write(stdin_fd, b'\x03') + return process.poll() -- Gitee From 6cdb46ff59188d42c2708d25dc154d456a47d097 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 23:13:25 +0800 Subject: [PATCH 089/354] Implement database upgrade/regenerate feature --- src/clitheme/_generator/__init__.py | 2 ++ src/clitheme/_generator/db_interface.py | 9 +++++ src/clitheme/_globalvar.py | 1 + src/clitheme/exec/__init__.py | 44 +++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 56b5975..0adfb9b 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -457,6 +457,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if not "header" in parsed_sections or (not "entries" in parsed_sections and not "substrules" in parsed_sections): handle_error(fd.reof("incomplete-section-err", "Missing or incomplete header or content sections")) + # record file content for database migration/upgrade feature + write_infofile(path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, "file_content", file_content, lineindex+1, "") # Update current theme index theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") theme_index.write(custom_infofile_name+"\n") diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 43cdf0b..c6d3ae6 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -10,6 +10,9 @@ except ImportError: import _globalvar connection=sqlite3.connect(":memory:") # placeholder debug_mode=False +class need_db_regenerate(Exception): + pass + def handle_warning(message: str): if debug_mode: print(message) def init_db(file_path: str): @@ -28,12 +31,18 @@ def init_db(file_path: str): end_match_here INTEGER NOT NULL, \ stdout_stderr_only INTEGER NOT NULL \ );") + connection.execute(f"CREATE TABLE {_globalvar.db_data_tablename}_version (value INTEGER NOT NULL);") + connection.execute(f"INSERT INTO {_globalvar.db_data_tablename}_version (value) VALUES (?)", (_globalvar.db_version,)) connection.commit() def connect_db(): if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): raise FileNotFoundError("No theme set or theme does not contain substrules") global connection connection=sqlite3.connect(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}") + # check db version + version=int(connection.execute(f"SELECT value FROM {_globalvar.db_data_tablename}_version").fetchone()[0]) + if version!=_globalvar.db_version: + raise need_db_regenerate def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, line_number_debug: int=-1): cmdlist: list[str]=[] diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index d9cbd5d..b69d3a9 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -48,6 +48,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" +db_version=1 ## Sanity check function entry_banphrases=['/','\\'] diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 983c219..5274a95 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -3,12 +3,50 @@ Module used for clitheme-exec (should not be invoked directly) """ import sys import os +import shutil try: from . import output_handler_posix - from .. import _globalvar + from .. import _globalvar, cli + from .._generator import db_interface except ImportError: import output_handler_posix - import _globalvar + import _globalvar, cli + from _generator import db_interface + +def check_regenerate_db() -> bool: + try: db_interface.connect_db() + except db_interface.need_db_regenerate: + print("Migrating substrules database...") + try: + # gather files + search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname + if not os.path.isdir(search_path): raise Exception + lsdir_result=os.listdir(search_path); lsdir_result.sort() + lsdir_num=0 + for x in lsdir_result: + if os.path.isdir(search_path+"/"+x): lsdir_num+=1 + if lsdir_num<1: raise Exception + + file_contents=[] + for pathname in lsdir_result: + target_path=search_path+"/"+pathname + if not os.path.isdir(target_path): continue + content=open(target_path+"/file_content", encoding="utf-8").read() + file_contents.append(content) + cli.apply_theme(file_contents, overlay=False, generate_only=True, preserve_temp=True) + os.remove(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) + shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, _globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) + print("Done") + except: + print("An error occurred while migrating the database: "+str(sys.exc_info()[1])) + print("Please re-apply the theme and try again") + return False + except: + print("An error occurred while migrating the database: "+str(sys.exc_info()[1])) + print("Please re-apply the theme and try again") + return False + return True + def main(arguments: list[str]): # get arguments if len(arguments)<2: @@ -31,6 +69,8 @@ def main(arguments: list[str]): debug_mode.append("newlines") elif arg=="--debug-showchars": debug_mode.append("showchars") + else: print("Unknown option \"{}\"".format(arg)); return 1 + if not check_regenerate_db(): return 1 # determine platform if os.name=="posix": return output_handler_posix.handler_main(arguments[1+argcount:], debug_mode) -- Gitee From 5a69f74a9a458e5f51576c0d4d88b8392129b5aa Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 23:18:35 +0800 Subject: [PATCH 090/354] Properly handle command init errors --- 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 15e70e4..35ecaf7 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -51,7 +51,11 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): env['TERM']="dumb" # Prevent apps from using "less" or "more" as pager, as it won't work here env['PAGER']="cat" - process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stderr_slave, bufsize=0, close_fds=True, env=env) + process: subprocess.Popen + try: process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stderr_slave, bufsize=0, close_fds=True, env=env) + except: + print("Failed to run command: "+str(sys.exc_info()[1])) + return 1 while True: try: # update cbreak (realtime stdin) attributes from what the program sets -- Gitee From 36b30058d0681be7b992a9b0f366477698556099 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 27 Apr 2024 23:20:22 +0800 Subject: [PATCH 091/354] Update version (v2.0-dev20240427) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index a56d3b0..3499a31 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240426' +pkgver='2.0_dev20240427' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 3bfbf0e..577dc5b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240426-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240427-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Fri, 26 Apr 2024 23:30:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 27 Apr 2024 23:18:00 +0800 clitheme (1.1-r1-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 4dc5487..a89402e 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240426" +__version__="2.0-dev20240427" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240426" +version_main="2.0_dev20240427" version_buildnumber=1 \ No newline at end of file -- Gitee From c228f7f75a67266ebd37c9c4198c4981fdfedcfe Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 13:42:53 +0800 Subject: [PATCH 092/354] Make sure each line isn't separated by each block read --- src/clitheme/exec/output_handler_posix.py | 45 ++++++++++++++++------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 35ecaf7..3d11942 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -1,12 +1,11 @@ import subprocess import sys import os +import io import pty import select import termios import copy -import string -from typing import Optional try: from .._generator import db_interface from .. import _globalvar @@ -14,6 +13,8 @@ except ImportError: from _generator import db_interface import _globalvar +# 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[bytes], debug_mode: list[str], is_stderr: bool=False, matched: bool=False) -> list[bytes]: # debug_mode: newlines, showchars, color @@ -21,7 +22,7 @@ def process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fal for x in range(len(lines)): line=lines[x] if "newlines" in debug_mode: - if not line.endswith(b'\n') and not line.endswith(b'\f') and not line.endswith(b'\x0c'): + if not line.endswith(newlines): line+=b"\n" if "showchars" in debug_mode: wrapper=b"\x1b[32m{}\x1b[0m" @@ -47,8 +48,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): stdin_fd, stdin_slave=pty.openpty() env=copy.copy(os.environ) - # Tell apps that the terminal aren't designed to handle TUI - env['TERM']="dumb" # Prevent apps from using "less" or "more" as pager, as it won't work here env['PAGER']="cat" process: subprocess.Popen @@ -56,14 +55,14 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): except: print("Failed to run command: "+str(sys.exc_info()[1])) return 1 + output_lines=[] # (line_content, is_stderr) while True: try: # update cbreak (realtime stdin) attributes from what the program sets try: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) except termios.error: pass - fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.1)[0] - output_lines=[] # (line_content, is_stderr) - readsize=1000000 + fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.01)[0] + readsize=io.DEFAULT_BUFFER_SIZE if sys.stdin in fds: data=os.read(sys.stdin.fileno(), readsize) if not data: break @@ -73,24 +72,42 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): #data=b'\x1b[33m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[33m')+b'\x1b[0m' # DEBUG purposes #data=b'\x1b[33m'+data+b'\x1b[0m' # DEBUG purposes lines=data.splitlines(keepends=True) - for line in lines: - output_lines.append((line,False)) + for x in range(len(lines)): + line=lines[x] + # if last input did not end with newlines, append new content to it + if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines): + orig_line=output_lines[-1][0] + output_lines.pop() + output_lines.append((orig_line+line,False)) + else: output_lines.append((line,False)) if stderr_fd in fds: data=os.read(stderr_fd, readsize) #data=b'\x1b[31m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[31m')+b'\x1b[0m' # DEBUG purposes #data=b'\x1b[31m'+data+b'\x1b[0m' # DEBUG purposes lines=data.splitlines(keepends=True) - for line in lines: - output_lines.append((line,True)) + for x in range(len(lines)): + line=lines[x] + if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines): + orig_line=output_lines[-1][0] + output_lines.pop() + output_lines.append((orig_line+line,True)) + else: output_lines.append((line,True)) if process.poll()!=None and len(output_lines)==0: break # Process outputs - for line_data in output_lines: - line=line_data[0] + for x in range(len(output_lines)): + line_data=output_lines[x] + line: bytes=line_data[0] + # if does not end with newlines, leave it for the next iteration + if x==len(output_lines)-1 and not line.endswith(newlines): + if not len(line_data)>2: # not from previous iteration + output_lines=[line_data+(True,)] # add another entry to signal it's from previous iteration + break # subst operation subst_line=copy.copy(line) if do_subst: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) subst_line=process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line)[0] os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line) + else: output_lines=[] # happens when no 'break' statement occurs except KeyboardInterrupt: process.send_signal(2) #SIGINT #os.write(stdin_fd, b'\x03') -- Gitee From a85cf3572ff09a9884d8c0008b691c7fd9a39305 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 14:01:30 +0800 Subject: [PATCH 093/354] Disable stdout_only and stderr_only for now (BETA) --- src/clitheme/_generator/__init__.py | 5 ++++- src/clitheme/exec/output_handler_posix.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 0adfb9b..674cc50 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -109,7 +109,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # value options: options requiring an integer value value_options=["leadtabindents", "leadspaces"] # on/off options (use no<...> to disable) - bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere", "stdout_only", "stderr_only"] + bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere"] + # Disable these options for now (BETA) + # bool_options+=["stdout_only", "stderr_only"] + # only one of these options can be set to true at the same time (specific to groups) bool_options_unique_groups=[["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"], ["stdout_only", "stderr_only"]] final_options={} diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 3d11942..cb6085b 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -51,7 +51,9 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): # Prevent apps from using "less" or "more" as pager, as it won't work here env['PAGER']="cat" process: subprocess.Popen - try: process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stderr_slave, bufsize=0, close_fds=True, env=env) + # Redirect stderr to stdout for now (BETA) + # need to find a method to preserve exact order when using separated stdout and stderr pipes + try: process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) except: print("Failed to run command: "+str(sys.exc_info()[1])) return 1 -- Gitee From e689cb0e5fd7ae0176bfcc2d9a4fe0b76b62bffa Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 14:22:47 +0800 Subject: [PATCH 094/354] Fix and improve database migration logic --- src/clitheme/exec/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 5274a95..f1f0b3f 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -3,6 +3,7 @@ Module used for clitheme-exec (should not be invoked directly) """ import sys import os +import io import shutil try: from . import output_handler_posix @@ -33,14 +34,19 @@ def check_regenerate_db() -> bool: if not os.path.isdir(target_path): continue content=open(target_path+"/file_content", encoding="utf-8").read() file_contents.append(content) - cli.apply_theme(file_contents, overlay=False, generate_only=True, preserve_temp=True) + cli_msg=io.StringIO() + sys.stdout=cli_msg + if not cli.apply_theme(file_contents, overlay=False, generate_only=True, preserve_temp=True)==0: + raise Exception("Failed to generate data (full log below):\n"+cli_msg.getvalue()+"\n") + sys.stdout=sys.__stdout__ os.remove(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, _globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) - print("Done") + print("Successfully completed migration, proceeding execution") except: print("An error occurred while migrating the database: "+str(sys.exc_info()[1])) print("Please re-apply the theme and try again") return False + except FileNotFoundError: pass except: print("An error occurred while migrating the database: "+str(sys.exc_info()[1])) print("Please re-apply the theme and try again") -- Gitee From aa809e259ba41d83df2992ee53e2a03382dd0daf Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 15:00:36 +0800 Subject: [PATCH 095/354] Implement help message in clitheme-exec --- src/clitheme/exec/__init__.py | 42 +++++++++++++++++------ src/clitheme/exec/output_handler_posix.py | 2 +- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index f1f0b3f..4c31caa 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -53,17 +53,26 @@ def check_regenerate_db() -> bool: return False return True +def handle_help_message(full_help: bool=False): + print("Usage: ") + print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--debug-showchars] [command]") + if not full_help: return + print("Options: ") + print("\t--debug: Display indicator at the beginning of each read output by line") + print("\t--debug-color: Apply color on output; used to determine stdout or stderr (BETA: stdout/stderr not implemented)") + print("\t--debug-newlines: Use newlines to display output that does not end on a newline") + print("\t--debug-showchars: Display various control characters in plain text") + +def handle_error(message: str): + print(message) + print("Run clitheme-exec --help for usage information") + return 1 + def main(arguments: list[str]): - # get arguments - if len(arguments)<2: - print("Not enough arguments") - return 1 - # check database - if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): - print("Warning: no theme set or theme does not have substrules") # process debug mode arguments debug_mode=[] argcount=0 + showhelp=False for arg in arguments[1:]: if not arg.startswith('-'): break argcount+=1 @@ -75,15 +84,28 @@ def main(arguments: list[str]): debug_mode.append("newlines") elif arg=="--debug-showchars": debug_mode.append("showchars") - else: print("Unknown option \"{}\"".format(arg)); return 1 + elif arg=="--help": + showhelp=True + else: return handle_error("Error: unknown option \"{}\"".format(arg)) + if len(arguments)<=1+argcount: + if showhelp: + handle_help_message(full_help=True) + return 0 + else: + handle_help_message() + print("Error: no command specified") + return 1 + # check database + if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): + print("Warning: no theme set or theme does not have substrules") if not check_regenerate_db(): return 1 # determine platform if os.name=="posix": return output_handler_posix.handler_main(arguments[1+argcount:], debug_mode) elif os.name=="nt": - print("Windows platform is not currently supported") + print("Error: Windows platform is not currently supported") return 1 else: - print("Unsupported platform") + print("Error: Unsupported platform") return 1 return 0 \ No newline at end of file diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index cb6085b..a91cc40 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -55,7 +55,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): # need to find a method to preserve exact order when using separated stdout and stderr pipes try: process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) except: - print("Failed to run command: "+str(sys.exc_info()[1])) + print("Error: failed to run command: "+str(sys.exc_info()[1])) return 1 output_lines=[] # (line_content, is_stderr) while True: -- Gitee From b657af8be38e33a2aaf5bffa2c5337ec71dc7cf6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 15:16:33 +0800 Subject: [PATCH 096/354] Add release notes to changelog --- debian/changelog | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index 577dc5b..e6f2f9b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,10 +3,38 @@ clitheme (2.0-dev20240427-1) UNRELEASED; urgency=low * In development, please see commit logs for changes -- swiftycode <3291929745@qq.com> Sat, 27 Apr 2024 23:18:00 +0800 + +clitheme (1.1-r2-1) unstable; urgency=medium + + Version 1.1 release 2 + + Bug fixes: + * Fixes an issue where setting overlay=True in frontend.set_local_themedef causes the function to not work properly + * Fixes an issue where the subsections in frontend functions are incorrectly parsed + + For more information please see: + https://gitee.com/swiftycode/clitheme/releases/tag/v1.1-r2 + -- swiftycode <3291929745@qq.com> Sat, 02 Mar 2024 23:49:00 +0800 + clitheme (1.1-r1-1) unstable; urgency=medium - * Version 1.1 release 1 + Version 1.1 release 1 + + New features: + * New description header definition for theme definition files + * Add support for multi-line/block input in theme definition files + * Support local deployment of theme definition files + * The CLI interface can be modified using theme definition files + * frontend module: Add format_entry_or_fallback function + * Add support for Windows + * CLI interface: Support specifying multiple files on apply-theme and generate-data-hierarchy commands + + Improvements and bug fixes: + * frontend module: Optimize language detection process + * CLI interface: Rename generate-data-hierarchy command to generate-data (the original command can still be used) + * Fix some grammar and spelling errors in output messages + For more information please see: https://gitee.com/swiftycode/clitheme/releases/tag/v1.1-r1 @@ -14,7 +42,8 @@ clitheme (1.1-r1-1) unstable; urgency=medium clitheme (1.0-r2-1) unstable; urgency=medium - * Version 1.0 release 2 + Version 1.0 release 2 + For more information please see: https://gitee.com/swiftycode/clitheme/releases/tag/v1.0-r2 @@ -22,6 +51,6 @@ clitheme (1.0-r2-1) unstable; urgency=medium clitheme (1.0-r1-1) unstable; urgency=medium - * Version 1.0 release 1 + Version 1.0 release 1 -- swiftycode <3291929745@qq.com> Wed, 13 Dec 2023 17:16:33 +0800 -- Gitee From c45c1050984ebbc31a69a25ddba3e5dd3059698d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 15:55:59 +0800 Subject: [PATCH 097/354] Add _generator string entries to zh_CN string file --- src/clitheme/_generator/__init__.py | 8 ++-- .../strings/generator-strings.clithemedef.txt | 42 ++++++++++++++----- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 674cc50..cd17957 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -221,7 +221,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if is_substrules: # check if patterns are valid try: re.compile(entry_name) - except re.error: handle_error(fd.feof("invaild-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(lineindex+1), error_msg=sys.exc_info()[1])) + except re.error: handle_error(fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(lineindex+1), error_msg=sys.exc_info()[1])) while lineindex Date: Sun, 28 Apr 2024 16:15:36 +0800 Subject: [PATCH 098/354] Add db_interface warning message as string entry --- src/clitheme/_generator/db_interface.py | 17 ++++++++++++----- .../strings/generator-strings.clithemedef.txt | 3 +++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index c6d3ae6..e2e93ff 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -4,17 +4,24 @@ import sqlite3 import re import copy from typing import Optional -try: from .. import _globalvar -except ImportError: import _globalvar +try: from .. import _globalvar, frontend, _get_resource, _version +except ImportError: import _globalvar, frontend, _get_resource, _version connection=sqlite3.connect(":memory:") # placeholder debug_mode=False +try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() +except: + if _version.release==0: print("db_interface set_local_themedef failed") + pass +fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") class need_db_regenerate(Exception): pass def handle_warning(message: str): - if debug_mode: print(message) + if debug_mode: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) def init_db(file_path: str): global connection connection=sqlite3.connect(file_path) @@ -59,7 +66,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma match_condition=f"match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition} AND stdout_stderr_only=?" match_params=(match_pattern.strip(), effective_locale, stdout_stderr_matchoption) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: - handle_warning(f"Warning: Repeated entry at line {line_number_debug}, overwriting") + 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.strip(), substitute_pattern.strip(), None, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption)) @@ -70,7 +77,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=?" match_params=(match_pattern.strip(), cmd.strip(), effective_locale, stdout_stderr_matchoption) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: - handle_warning(f"Warning: Repeated entry at line {line_number_debug}, overwriting") + 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.strip(), substitute_pattern.strip(), cmd.strip(), is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption)) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 72fd7be..960d997 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -62,6 +62,9 @@ in_subsection generator [entry] repeated-entry-warn locale:zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 [/entry] + [entry] repeated-substrules-warn + locale:zh_CN 第{num}行:重复的substrules定义;之前的定义内容将会被覆盖 + [/entry] [entry] repeated-header-warn locale:zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 [/entry] -- Gitee From d778f3a3db4ddf0e6584b18177e4f05c44453558 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 17:48:18 +0800 Subject: [PATCH 099/354] Properly handle regex syntax errors --- src/clitheme/_generator/__init__.py | 4 ++-- src/clitheme/_generator/db_interface.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index cd17957..ddb9b0f 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -221,7 +221,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if is_substrules: # check if patterns are valid try: re.compile(entry_name) - except re.error: handle_error(fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(lineindex+1), error_msg=sys.exc_info()[1])) + except: handle_error(fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(lineindex+1), error_msg=sys.exc_info()[1])) while lineindex Date: Sun, 28 Apr 2024 21:48:55 +0800 Subject: [PATCH 100/354] Add string entries for clitheme-exec --- src/clitheme/exec/__init__.py | 46 ++++++++------ src/clitheme/exec/output_handler_posix.py | 13 +++- .../strings/exec-strings.clithemedef.txt | 60 +++++++++++++++++++ 3 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 src/clitheme/strings/exec-strings.clithemedef.txt diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 4c31caa..9ce8525 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -7,17 +7,25 @@ import io import shutil try: from . import output_handler_posix - from .. import _globalvar, cli + from .. import _globalvar, cli, frontend, _get_resource, _version from .._generator import db_interface except ImportError: import output_handler_posix - import _globalvar, cli + import _globalvar, cli, frontend, _get_resource, _version from _generator import db_interface +try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() +except: + if _version.release==0: print("db_interface set_local_themedef failed") + pass +fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") + def check_regenerate_db() -> bool: try: db_interface.connect_db() except db_interface.need_db_regenerate: - print("Migrating substrules database...") + print(fd.reof("substrules-migrate-msg", "Migrating substrules database...")) try: # gather files search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname @@ -37,35 +45,34 @@ def check_regenerate_db() -> bool: cli_msg=io.StringIO() sys.stdout=cli_msg if not cli.apply_theme(file_contents, overlay=False, generate_only=True, preserve_temp=True)==0: - raise Exception("Failed to generate data (full log below):\n"+cli_msg.getvalue()+"\n") + raise Exception(fd.reof("db-migration-generator-err", "Failed to generate data (full log below):")+"\n"+cli_msg.getvalue()+"\n") sys.stdout=sys.__stdout__ os.remove(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, _globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) - print("Successfully completed migration, proceeding execution") + print(fd.reof("db-migrate-success-msg", "Successfully completed migration, proceeding execution")) except: - print("An error occurred while migrating the database: "+str(sys.exc_info()[1])) - print("Please re-apply the theme and try again") + 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]))) return False except FileNotFoundError: pass except: - print("An error occurred while migrating the database: "+str(sys.exc_info()[1])) - print("Please re-apply the theme and try again") + 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]))) return False return True def handle_help_message(full_help: bool=False): - print("Usage: ") + fd2=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec help-message") + print(fd2.reof("usage-str", "Usage:")) print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--debug-showchars] [command]") if not full_help: return - print("Options: ") - print("\t--debug: Display indicator at the beginning of each read output by line") - print("\t--debug-color: Apply color on output; used to determine stdout or stderr (BETA: stdout/stderr not implemented)") - print("\t--debug-newlines: Use newlines to display output that does not end on a newline") - print("\t--debug-showchars: Display various control characters in plain text") + 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"+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")) def handle_error(message: str): print(message) - print("Run clitheme-exec --help for usage information") + print(fd.reof("help-usage-prompt", "Run clitheme-exec --help for usage information")) return 1 def main(arguments: list[str]): @@ -86,18 +93,19 @@ def main(arguments: list[str]): debug_mode.append("showchars") elif arg=="--help": showhelp=True - else: return handle_error("Error: unknown option \"{}\"".format(arg)) + else: + return handle_error(fd.feof("unknown-option-err", "Error: unknown option \"{phrase}\"", phrase=arg)) if len(arguments)<=1+argcount: if showhelp: handle_help_message(full_help=True) return 0 else: handle_help_message() - print("Error: no command specified") + print(fd.reof("no-command-err", "Error: no command specified")) return 1 # check database if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): - print("Warning: no theme set or theme does not have substrules") + print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) if not check_regenerate_db(): return 1 # determine platform if os.name=="posix": diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index a91cc40..264777b 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -8,11 +8,18 @@ import termios import copy try: from .._generator import db_interface - from .. import _globalvar + from .. import _globalvar, frontend, _get_resource, _version except ImportError: from _generator import db_interface - import _globalvar + import _globalvar, frontend, _get_resource, _version +try: + if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() +except: + if _version.release==0: print("db_interface set_local_themedef failed") + pass +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') @@ -55,7 +62,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): # need to find a method to preserve exact order when using separated stdout and stderr pipes try: process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) except: - print("Error: failed to run command: "+str(sys.exc_info()[1])) + print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) return 1 output_lines=[] # (line_content, is_stderr) while True: diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt new file mode 100644 index 0000000..61080c2 --- /dev/null +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -0,0 +1,60 @@ +{header_section} + name clitheme message text translations (clitheme-exec) + version 2.0 + locales zh_CN + supported_apps clitheme +{/header_section} + +{entries_section} +in_domainapp swiftycode clitheme + in_subsection exec help-message + [entry] usage-str + locale:zh_CN 使用方式: + [/entry] + [entry] options-str + locale:zh_CN 选项: + [/entry] + [entry] options-debug + locale:zh_CN --debug: 在每一行被读取的输出前显示标记 + [/entry] + [entry] options-debug-color + locale:zh_CN --debug-color: 为输出设定颜色;用于区分stdout和stderr(BETA:stdout/stderr未实现) + [/entry] + [entry] options-debug-newlines + locale:zh_CN --debug-newlines: 使用新的一行来显示没有新行的输出 + [/entry] + [entry] options-debug-showchars + locale:zh_CN --debug-showchars: 使用明文显示终端控制符号 + [/entry] + in_subsection exec + [entry] help-usage-prompt + locale:zh_CN 使用"clitheme-exec --help"以获取使用方式 + [/entry] + [entry] unknown-option-err + locale:zh_CN 错误:未知选项"{phrase}" + [/entry] + [entry] no-command-err + locale:zh_CN 错误:未指定命令 + [/entry] + [entry] no-theme-warn + locale:zh_CN 警告:没有设定主题或当前主题没有substrules定义 + [/entry] + [entry] substrules-migrate-msg + locale:zh_CN 正在迁移数据库... + [/entry] + [entry] db-migration-generator-err + locale:zh_CN 无法生成数据(完整日志在此): + [/entry] + [entry] db-migration-err + [locale] zh_CN + 迁移数据库时发生了错误: {msg} + 请重新应用当前主题,然后重试 + [/locale] + [/entry] + [entry] db-migrate-success-msg + locale:zh_CN 数据库迁移完成,继续执行命令 + [/entry] + [entry] command-fail-err + locale:zh_CN 错误: 无法执行命令: {msg} + [/entry] +{/entries_section} \ No newline at end of file -- Gitee From 21319aa0281e7f779ebf74ca067d1e5d5bc22c87 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 21:49:13 +0800 Subject: [PATCH 101/354] Fix version information in cli-strings --- src/clitheme/strings/cli-strings.clithemedef.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 77eaba7..4644236 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -1,6 +1,6 @@ {header_section} name clitheme message text translations (cli) - version 1.2 + version 2.0 locales zh_CN supported_apps clitheme {/header_section} -- Gitee From 9b8b06b198b3f69aeb8eec3aeb9902577e0e8655 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 22:03:07 +0800 Subject: [PATCH 102/354] Add exec-strings set_local_themedef --- src/clitheme/_generator/__init__.py | 8 +++++--- src/clitheme/_generator/db_interface.py | 3 ++- src/clitheme/_globalvar.py | 4 +++- src/clitheme/cli.py | 3 ++- src/clitheme/exec/__init__.py | 3 ++- src/clitheme/exec/output_handler_posix.py | 3 ++- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index ddb9b0f..14e9ee4 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -11,10 +11,8 @@ import copy from typing import Optional try: from .. import _globalvar, frontend, _version, _get_resource - from . import db_interface except ImportError: # for test program import _globalvar, frontend, _version, _get_resource - import _generator.db_interface as db_interface fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") @@ -466,9 +464,13 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") theme_index.write(custom_infofile_name+"\n") +# prevent circular import error +try: from . import db_interface +except ImportError: import db_interface try: if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except: - if _version.release==0: print("generator set_local_themedef failed") + if _version.release==0: print("generator set_local_themedef failed: "+str(sys.exc_info()[1])) pass \ No newline at end of file diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 62b7b73..878d913 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -12,8 +12,9 @@ debug_mode=False try: if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except: - if _version.release==0: print("db_interface set_local_themedef failed") + if _version.release==0: print("db_interface set_local_themedef failed: "+str(sys.exc_info()[1])) pass fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index b69d3a9..f0e95a3 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -3,6 +3,7 @@ Global variable definitions for clitheme """ import os +import sys import re from copy import copy try: from . import _version @@ -74,8 +75,9 @@ def sanity_check(path: str, use_orig: bool=False) -> bool: try: if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except RuntimeError: - if _version.release==0: print("_globalvar set_local_themedef failed") + if _version.release==0: print("_globalvar set_local_themedef failed: "+str(sys.exc_info()[1])) pass msg_retrieved=True f=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 587af2f..6fe8655 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -36,8 +36,9 @@ frontend.global_subsections="cli" try: if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt")): raise RuntimeError() if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt"), overlay=True): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except: - if _version.release==0: print("set_local_themedef failed") + if _version.release==0: print("cli set_local_themedef failed: "+str(sys.exc_info()[1])) pass def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, generate_only=False): diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 9ce8525..0d7018f 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -17,8 +17,9 @@ except ImportError: try: if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except: - if _version.release==0: print("db_interface set_local_themedef failed") + if _version.release==0: print("clitheme-exec set_local_themedef failed: "+str(sys.exc_info()[1])) pass fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 264777b..3fae261 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -16,8 +16,9 @@ except ImportError: try: if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() + if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() except: - if _version.release==0: print("db_interface set_local_themedef failed") + if _version.release==0: print("output_handler_posix set_local_themedef failed: "+str(sys.exc_info()[1])) pass fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") # https://docs.python.org/3/library/stdtypes.html#str.splitlines -- Gitee From 14e873a16c23a6a6a917a382fa5c71112141806c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 22:03:18 +0800 Subject: [PATCH 103/354] Fix colons in exec-strings file --- src/clitheme/strings/exec-strings.clithemedef.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index 61080c2..46c7578 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -15,16 +15,16 @@ in_domainapp swiftycode clitheme locale:zh_CN 选项: [/entry] [entry] options-debug - locale:zh_CN --debug: 在每一行被读取的输出前显示标记 + locale:zh_CN --debug:在每一行被读取的输出前显示标记 [/entry] [entry] options-debug-color - locale:zh_CN --debug-color: 为输出设定颜色;用于区分stdout和stderr(BETA:stdout/stderr未实现) + locale:zh_CN --debug-color:为输出设定颜色;用于区分stdout和stderr(BETA:stdout/stderr未实现) [/entry] [entry] options-debug-newlines - locale:zh_CN --debug-newlines: 使用新的一行来显示没有新行的输出 + locale:zh_CN --debug-newlines:使用新的一行来显示没有新行的输出 [/entry] [entry] options-debug-showchars - locale:zh_CN --debug-showchars: 使用明文显示终端控制符号 + locale:zh_CN --debug-showchars:使用明文显示终端控制符号 [/entry] in_subsection exec [entry] help-usage-prompt @@ -47,7 +47,7 @@ in_domainapp swiftycode clitheme [/entry] [entry] db-migration-err [locale] zh_CN - 迁移数据库时发生了错误: {msg} + 迁移数据库时发生了错误:{msg} 请重新应用当前主题,然后重试 [/locale] [/entry] @@ -55,6 +55,6 @@ in_domainapp swiftycode clitheme locale:zh_CN 数据库迁移完成,继续执行命令 [/entry] [entry] command-fail-err - locale:zh_CN 错误: 无法执行命令: {msg} + locale:zh_CN 错误:无法执行命令:{msg} [/entry] {/entries_section} \ No newline at end of file -- Gitee From b1477859f20b7f62df466971848e49ffe4b7b965 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 28 Apr 2024 22:11:04 +0800 Subject: [PATCH 104/354] Update version (v2.0-dev20240428) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 3499a31..f6a7ebf 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240427' +pkgver='2.0_dev20240428' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index e6f2f9b..c584f57 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240427-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240428-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 27 Apr 2024 23:18:00 +0800 + -- swiftycode <3291929745@qq.com> Sun, 28 Apr 2024 22:09:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index a89402e..5f10f8b 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240427" +__version__="2.0-dev20240428" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240427" +version_main="2.0_dev20240428" version_buildnumber=1 \ No newline at end of file -- Gitee From 9992073b041b962fec081d564b71934272b218cd Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 29 Apr 2024 08:30:30 +0800 Subject: [PATCH 105/354] Show newline character in --debug-showchars --- src/clitheme/exec/output_handler_posix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 3fae261..3a2528b 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -37,6 +37,7 @@ def process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fal if "color" in debug_mode: wrapper+=bytes(f"\x1b[{'31' if is_stderr else '33'}m", 'utf-8') line=line.replace(b'\x1b', wrapper.replace(b'{}', b'{{ESC}}')) # this must come before anything else line=line.replace(b'\r', wrapper.replace(b'{}',b'\\r')) + line=line.replace(b'\n', wrapper.replace(b'{}',b'\\n\n')) line=line.replace(b'\b', wrapper.replace(b'{}',b'\\x08')) line=line.replace(b'\a', wrapper.replace(b'{}',b'\\x07')) if "color" in debug_mode: -- Gitee From d81d9033faf93783c7f673e41bd7b2dfce79a591 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 29 Apr 2024 09:26:44 +0800 Subject: [PATCH 106/354] Fix logic of endmatchhere processing --- src/clitheme/_generator/db_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 878d913..2280b93 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -171,8 +171,8 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) else: # is string content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) + if match_data[3]==True and re.search(bytes(match_data[0], 'utf-8'), content_str)!=None: # endmatchoption is set + break except: handle_warning("Error occurred while matching string: "+str(sys.exc_info()[1])) - if match_data[3]==True: # endmatchoption is set - break return content_str \ No newline at end of file -- Gitee From 57b2e83b57aebcfa2c5a1e54f37b1d4840080552 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 29 Apr 2024 22:26:08 +0800 Subject: [PATCH 107/354] Update version (v2.0-dev20240429) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index f6a7ebf..62698cc 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240428' +pkgver='2.0_dev20240429' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index c584f57..48b6165 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240428-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240429-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sun, 28 Apr 2024 22:09:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, 29 Apr 2024 22:24:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 5f10f8b..a1aabe8 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240428" +__version__="2.0-dev20240429" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240428" +version_main="2.0_dev20240429" version_buildnumber=1 \ No newline at end of file -- Gitee From 19277714f7ee6e51752cd379989336b5491288d3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 30 Apr 2024 23:07:37 +0800 Subject: [PATCH 108/354] Implement variable substitution feature --- src/clitheme/_generator/__init__.py | 52 +++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 14e9ee4..085edf5 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -87,6 +87,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info lines_data=file_content.splitlines() lineindex=-1 # counter extra +1 operation at beginning global_options={} + global_variables={} # define check functions def check_enough_args(phrases: list[str], count: int): @@ -107,7 +108,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # value options: options requiring an integer value value_options=["leadtabindents", "leadspaces"] # on/off options (use no<...> to disable) - bool_options=["substesc", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere"] + bool_options=["substesc", "substvar", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere"] # Disable these options for now (BETA) # bool_options+=["stdout_only", "stderr_only"] @@ -154,6 +155,39 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info def handle_set_global_options(options_data: list[str]): # set options globally nonlocal global_options; global_options=parse_options(options_data, merge_global_options=True) + def subst_variable_content(content: str, override_check: bool=False) -> str: + if not override_check and (not "substvar" in global_options or global_options["substvar"]==False): return content + # get all variables used in content + new_content=copy.copy(content) + variables=re.findall(r"{{(.+?)}}", content) + if len(variables)>0: + for var_name in variables: + if var_name=="ESC": continue # skip {{ESC}}; leave it for substesc + var_content: str + try: + var_content=global_variables[var_name] + except KeyError: + handle_warning(fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\" in content, not performing substitution", num=str(lineindex+1), name=var_name)) + continue + new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) + return new_content + def handle_set_variable(phrases: list[str]): + nonlocal global_variables + if not phrases[0].startswith("setvar:"): return + # match variable name + check_enough_args(phrases, 2) + results=re.search(r"setvar:(?P.+)", phrases[0]) + var_name: str + if results==None: + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="setvar:", num=str(lineindex+1))) + else: var_name=results.groupdict()['name'] + var_content=_globalvar.splitarray_to_string(phrases[1:]) + # subst variable references + if "substvar" in global_options and global_options["substvar"]==True: + var_content=subst_variable_content(var_content) + # set variable + global_variables[var_name]=var_content + def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True) -> str: nonlocal lineindex minspaces=math.inf @@ -205,6 +239,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif option=="substesc": # substitute {{ESC}} with escape literal if got_options['substesc']==True: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) + elif option=="substvar": + if got_options['substvar']==True: blockinput_data=subst_variable_content(blockinput_data, True) elif disallow_cmdmatch_options: handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) return blockinput_data @@ -214,7 +250,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info nonlocal lineindex substrules_entries=[] # (match_content, substitute_content, locale) substrules_entries_linenumber=[] - substrules_endmatchhere=substrules_options['end_match_here'] if 'end_match_here' in substrules_options else False + substrules_endmatchhere=False substrules_stdout_stderr_option=0 if is_substrules: # check if patterns are valid @@ -240,6 +276,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content=_globalvar.splitarray_to_string(phrases[2:]) locale=phrases[1] target_entry=copy.copy(entry_name) + # substvar + content=subst_variable_content(content) # substesc if "substesc" in global_options.keys() and global_options['substesc']==True: content=re.sub(r"{{ESC}}", '\x1b', content) @@ -350,12 +388,14 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if _globalvar.sanity_check(phrases[1]+" "+phrases[2])==False: handle_error(fd.feof("sanity-check-domainapp-err", "Line {num}: domain and app names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) domainapp=phrases[1]+" "+phrases[2] + domainapp=subst_variable_content(domainapp) subsection="" # clear subsection elif phrases[0]=="in_subsection": check_enough_args(phrases, 2) if _globalvar.sanity_check(_globalvar.splitarray_to_string(phrases[1:]))==False: handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) subsection=_globalvar.splitarray_to_string(phrases[1:]) + subsection=subst_variable_content(subsection) elif phrases[0]=="unset_domainapp": check_extra_args(phrases, 1, use_exact_count=True) domainapp=""; subsection="" @@ -368,6 +408,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if _globalvar.sanity_check(_globalvar.splitarray_to_string(phrases[1:]))==False: handle_error(fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) entry_name=_globalvar.splitarray_to_string(phrases[1:]) + entry_name=subst_variable_content(entry_name) if subsection!="": entry_name=subsection+" "+entry_name if domainapp!="": entry_name=domainapp+" "+entry_name recursive_mkdir(datapath, entry_name, lineindex+1) @@ -375,6 +416,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="set_options": check_enough_args(phrases, 2) handle_set_global_options(phrases[1:]) + elif phrases[0].startswith("setvar:"): + check_enough_args(phrases, 2) + handle_set_variable(phrases) elif phrases[0]==end_phrase: check_extra_args(phrases, 1, use_exact_count=True) parsed_sections.append("entries") @@ -442,12 +486,16 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} match_pattern=_globalvar.splitarray_to_string(phrases[1:]) + match_pattern=subst_variable_content(match_pattern) if "substesc" in global_options.keys() and global_options['substesc']==True: match_pattern=match_pattern.replace("{{ESC}}", "\x1b") handle_entry(match_pattern, end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) elif phrases[0]=="set_options": check_enough_args(phrases, 2) handle_set_global_options(phrases[1:]) + elif phrases[0].startswith("setvar:"): + check_enough_args(phrases, 2) + handle_set_variable(phrases) elif phrases[0]==end_phrase: check_extra_args(phrases, 1, use_exact_count=True) parsed_sections.append("substrules") -- Gitee From 3fc6cf754d78e79fd0a0d0f865f3e37b78f4097f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 30 Apr 2024 23:39:53 +0800 Subject: [PATCH 109/354] Properly handle continuous spaces in content --- src/clitheme/_generator/__init__.py | 26 +++++++++++++------------- src/clitheme/_globalvar.py | 4 ++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 085edf5..7259912 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -171,17 +171,17 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info continue new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) return new_content - def handle_set_variable(phrases: list[str]): + def handle_set_variable(line_content: str): nonlocal global_variables - if not phrases[0].startswith("setvar:"): return + if not line_content.split()[0].startswith("setvar:"): return # match variable name - check_enough_args(phrases, 2) - results=re.search(r"setvar:(?P.+)", phrases[0]) + check_enough_args(line_content.split(), 2) + results=re.search(r"setvar:(?P.+)", line_content.split()[0]) var_name: str if results==None: handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="setvar:", num=str(lineindex+1))) else: var_name=results.groupdict()['name'] - var_content=_globalvar.splitarray_to_string(phrases[1:]) + var_content=_globalvar.extract_content(line_content) # subst variable references if "substvar" in global_options and global_options["substvar"]==True: var_content=subst_variable_content(var_content) @@ -270,10 +270,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="locale:", num=str(lineindex+1))) else: locale=results.groupdict()['locale'] - content=_globalvar.splitarray_to_string(phrases[1:]) + content=_globalvar.extract_content(lines_data[lineindex]) else: check_enough_args(phrases, 3) - content=_globalvar.splitarray_to_string(phrases[2:]) + content=_globalvar.extract_content(lines_data[lineindex], begin_phrase_count=2) locale=phrases[1] target_entry=copy.copy(entry_name) # substvar @@ -333,7 +333,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # Expect name, description, description_block, version, locales, locales_block, supported_apps, supported_apps_block if phrases[0]=="name" or phrases[0]=="version" or phrases[0]=="description": check_enough_args(phrases, 2) - content=_globalvar.splitarray_to_string(phrases[1:]) + content=_globalvar.extract_content(lines_data[lineindex]) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ _globalvar.generator_info_filename.format(info=phrases[0]),\ @@ -404,10 +404,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info subsection="" elif phrases[0]=="entry" or phrases[0]=="[entry]": check_enough_args(phrases, 2) + entry_name=_globalvar.extract_content(lines_data[lineindex]) # Prevent leading . & prevent /,\ in entry name - if _globalvar.sanity_check(_globalvar.splitarray_to_string(phrases[1:]))==False: + if _globalvar.sanity_check(entry_name)==False: handle_error(fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) - entry_name=_globalvar.splitarray_to_string(phrases[1:]) entry_name=subst_variable_content(entry_name) if subsection!="": entry_name=subsection+" "+entry_name if domainapp!="": entry_name=domainapp+" "+entry_name @@ -418,7 +418,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_set_global_options(phrases[1:]) elif phrases[0].startswith("setvar:"): check_enough_args(phrases, 2) - handle_set_variable(phrases) + handle_set_variable(lines_data[lineindex]) elif phrases[0]==end_phrase: check_extra_args(phrases, 1, use_exact_count=True) parsed_sections.append("entries") @@ -485,7 +485,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="[substitute_string]" or phrases[0]=="[substitute_regex]": check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} - match_pattern=_globalvar.splitarray_to_string(phrases[1:]) + match_pattern=_globalvar.extract_content(lines_data[lineindex]) match_pattern=subst_variable_content(match_pattern) if "substesc" in global_options.keys() and global_options['substesc']==True: match_pattern=match_pattern.replace("{{ESC}}", "\x1b") @@ -495,7 +495,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_set_global_options(phrases[1:]) elif phrases[0].startswith("setvar:"): check_enough_args(phrases, 2) - handle_set_variable(phrases) + handle_set_variable(lines_data[lineindex]) elif phrases[0]==end_phrase: check_extra_args(phrases, 1, use_exact_count=True) parsed_sections.append("substrules") diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index f0e95a3..074a30d 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -103,6 +103,10 @@ def splitarray_to_string(split_content): for phrase in split_content: 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()) + if results==None: raise ValueError("Match content failed (no matches)") + else: return results.groupdict()['content'] def get_locale(debug_mode: bool=False): lang=[] # Skip $LANGUAGE if both $LANG and $LC_ALL is set to C (treat empty as C also) -- Gitee From c64972360fe7a872a6283c7c53e43c16de55255c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 13:06:52 +0800 Subject: [PATCH 110/354] Add unknown-variable-warn to generator-strings --- src/clitheme/strings/generator-strings.clithemedef.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 960d997..dce9db9 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -71,6 +71,8 @@ in_subsection generator [entry] syntax-phrase-deprecation-warn locale:zh_CN 第{num}行:"{old_phrase}"在当前版本中已被弃用;请使用"{new_phrase}" [/entry] + [entry] unknown-variable-warn + locale:zh_CN 第{num}行:未知变量名称"{name}",不会进行替换 # sanity check [entry] sanity-check-entry-err locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} -- Gitee From de36749dcd67d3f2f39e26bdc33fb031c6d202d0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 13:25:54 +0800 Subject: [PATCH 111/354] Add var_name sanity checking --- src/clitheme/_generator/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 7259912..739b480 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -181,6 +181,13 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if results==None: handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="setvar:", num=str(lineindex+1))) else: var_name=results.groupdict()['name'] + # sanity check var_name + def bad_var(): handle_error(fd.feof("bad-var-name-err", "Line {num}: \"{name}\" is not a valid variable name", name=var_name, num=str(lineindex+1))) + if var_name=='ESC': bad_var() + banphrases=['{', '}', '[', ']', '(', ')'] + for char in banphrases: + if char in var_name: bad_var() + var_content=_globalvar.extract_content(line_content) # subst variable references if "substvar" in global_options and global_options["substvar"]==True: -- Gitee From 49f9dd2593d7518e446af590a461f6a9bc74d24b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 14:19:13 +0800 Subject: [PATCH 112/354] Properly handle incomplete sections --- src/clitheme/_generator/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 739b480..5c3dd2d 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -83,6 +83,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if not os.path.exists(datapath): os.mkdir(datapath) # data to keep track of + section_parsing=False parsed_sections=[] lines_data=file_content.splitlines() lineindex=-1 # counter extra +1 operation at beginning @@ -331,6 +332,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # avoid repeated block if "header" in parsed_sections: handle_error(fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(lineindex+1), section="header")) + section_parsing=True # --Process header block-- end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" while lineindex") -- Gitee From c00c450a6209945dd79175d62feaccd33b79f7f8 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 14:40:44 +0800 Subject: [PATCH 113/354] Small change in unknown-variable-warn wording --- src/clitheme/_generator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 5c3dd2d..f48056a 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -168,7 +168,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info try: var_content=global_variables[var_name] except KeyError: - handle_warning(fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\" in content, not performing substitution", num=str(lineindex+1), name=var_name)) + handle_warning(fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", num=str(lineindex+1), name=var_name)) continue new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) return new_content -- Gitee From 8528b9e3a01b338113f348f9434420340bcb0e1d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 14:48:46 +0800 Subject: [PATCH 114/354] Add bad-var-name-err to generator_strings --- src/clitheme/strings/generator-strings.clithemedef.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index dce9db9..b6a3e6f 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -55,6 +55,9 @@ in_subsection generator [entry] bad-subst-pattern-err locale:zh_CN 第{num}行:无效的替换正则表达式({error_msg}) [/entry] + [entry] bad-var-name-err + locale:zh_CN 第{num}行:"{name}"不是一个有效的变量名称 + [/entry] # warning strings [entry] warning-str locale:zh_CN 警告:{msg} -- Gitee From 921462ce4f8b9c5de57533937567b8d9961d3756 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 15:07:10 +0800 Subject: [PATCH 115/354] Optimize variable content handling in in_domainapp --- src/clitheme/_generator/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index f48056a..569a331 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -394,12 +394,12 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info phrases=lines_data[lineindex].split() # expect entry, in_domainapp, in_subsction, unset_domainapp, unset_subsection if phrases[0]=="in_domainapp": - check_enough_args(phrases, 3) - check_extra_args(phrases, 3, use_exact_count=False) - if _globalvar.sanity_check(phrases[1]+" "+phrases[2])==False: + this_phrases=subst_variable_content(lines_data[lineindex].strip()).split() + check_enough_args(this_phrases, 3) + check_extra_args(this_phrases, 3, use_exact_count=False) + domainapp=this_phrases[1]+" "+this_phrases[2] + if _globalvar.sanity_check(domainapp)==False: handle_error(fd.feof("sanity-check-domainapp-err", "Line {num}: domain and app names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) - domainapp=phrases[1]+" "+phrases[2] - domainapp=subst_variable_content(domainapp) subsection="" # clear subsection elif phrases[0]=="in_subsection": check_enough_args(phrases, 2) -- Gitee From aa69dca558ab2bbd1fe60e3a022f9b7446a79da2 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 15:24:55 +0800 Subject: [PATCH 116/354] Update version (v2.0-dev20240429) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 62698cc..26d3b6e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240429' +pkgver='2.0_dev20240430' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 48b6165..91996cb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240429-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240430-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 29 Apr 2024 22:24:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 01 May 2024 15:22:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index a1aabe8..42a2c11 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240429" +__version__="2.0-dev20240430" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240429" +version_main="2.0_dev20240430" version_buildnumber=1 \ No newline at end of file -- Gitee From c18062d285901bd2440abc0f821a2ca7af0aec6e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 15:36:11 +0800 Subject: [PATCH 117/354] Fix --debug-newlines not working --- 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 3a2528b..3d0d2aa 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -30,7 +30,7 @@ def process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fal for x in range(len(lines)): line=lines[x] if "newlines" in debug_mode: - if not line.endswith(newlines): + if not line.endswith(b'\n'): line+=b"\n" if "showchars" in debug_mode: wrapper=b"\x1b[32m{}\x1b[0m" -- Gitee From 8b90f8c705f6a1a69972d0d0fc41c1ca28c062d3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 15:38:39 +0800 Subject: [PATCH 118/354] Fix --debug-showchars showing newline with --debug-newlines --- 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 3d0d2aa..924952c 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -29,17 +29,17 @@ def process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fal final_lines=[] for x in range(len(lines)): line=lines[x] - if "newlines" in debug_mode: - if not line.endswith(b'\n'): - line+=b"\n" if "showchars" in debug_mode: wrapper=b"\x1b[32m{}\x1b[0m" if "color" in debug_mode: wrapper+=bytes(f"\x1b[{'31' if is_stderr else '33'}m", 'utf-8') line=line.replace(b'\x1b', wrapper.replace(b'{}', b'{{ESC}}')) # this must come before anything else line=line.replace(b'\r', wrapper.replace(b'{}',b'\\r')) - line=line.replace(b'\n', wrapper.replace(b'{}',b'\\n\n')) + line=line.replace(b'\n', wrapper.replace(b'{}',b'\\n')+b'\n') line=line.replace(b'\b', wrapper.replace(b'{}',b'\\x08')) line=line.replace(b'\a', wrapper.replace(b'{}',b'\\x07')) + if "newlines" in debug_mode: + if not line.endswith(b'\n'): + line+=b"\n" if "color" in debug_mode: line=bytes(f"\x1b[{'31' if is_stderr else '33'}m", 'utf-8')+line+b"\x1b[0m" if "normal" in debug_mode: -- Gitee From 1cfab4bd2f087c874841efd4d8b2710844718e4b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 22:12:40 +0800 Subject: [PATCH 119/354] Fix syntax typo in generator-strings --- src/clitheme/strings/generator-strings.clithemedef.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index b6a3e6f..92a2ba2 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -76,6 +76,7 @@ in_subsection generator [/entry] [entry] unknown-variable-warn locale:zh_CN 第{num}行:未知变量名称"{name}",不会进行替换 + [/entry] # sanity check [entry] sanity-check-entry-err locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} -- Gitee From caa7f55c756e45a842655e30f68b49d04e2c4817 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 22:36:55 +0800 Subject: [PATCH 120/354] Merge set_local_themedef uses into _globalvar.handle_set_themedef --- src/clitheme/_generator/__init__.py | 8 +------- src/clitheme/_generator/db_interface.py | 8 +------- src/clitheme/_globalvar.py | 25 +++++++++++++++-------- src/clitheme/cli.py | 8 +------- src/clitheme/exec/__init__.py | 8 +------- src/clitheme/exec/output_handler_posix.py | 8 +------- 6 files changed, 22 insertions(+), 43 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 569a331..e11360c 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -529,10 +529,4 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # prevent circular import error try: from . import db_interface except ImportError: import db_interface -try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() -except: - if _version.release==0: print("generator set_local_themedef failed: "+str(sys.exc_info()[1])) - pass \ No newline at end of file +_globalvar.handle_set_themedef(frontend, "generator") \ No newline at end of file diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 2280b93..57ab1e9 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -9,13 +9,7 @@ except ImportError: import _globalvar, frontend, _get_resource, _version connection=sqlite3.connect(":memory:") # placeholder debug_mode=False -try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() -except: - if _version.release==0: print("db_interface set_local_themedef failed: "+str(sys.exc_info()[1])) - pass +_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 074a30d..0ca394c 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -2,6 +2,7 @@ Global variable definitions for clitheme """ +import io import os import sys import re @@ -72,13 +73,7 @@ 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: - try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - except RuntimeError: - if _version.release==0: print("_globalvar set_local_themedef failed: "+str(sys.exc_info()[1])) - pass + 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}") @@ -147,4 +142,18 @@ def get_locale(debug_mode: bool=False): lang.append(re.sub(r"(?P.+)[\.].+", r"\g", target_str)) else: if debug_mode: print("[Debug] Locale: sanity check failed ({})".format(sanity_check_error_message)) - return lang \ No newline at end of file + return lang + +def handle_set_themedef(fr, debug_name: str): + try: + files=["strings/generator-strings.clithemedef.txt", "strings/cli-strings.clithemedef.txt", "strings/exec-strings.clithemedef.txt"] + for filename in files: + msg=io.StringIO() + sys.stdout=msg + fr.global_debugmode=True + if not fr.set_local_themedef(_get_resource.read_file(filename), overlay=True): raise RuntimeError("Full log below: \n"+msg.getvalue()) + fr.global_debugmode=False + sys.stdout=sys.__stdout__ + except: + if _version.release==0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) + pass \ No newline at end of file diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 6fe8655..a5d63c9 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -33,13 +33,7 @@ frontend.global_domain="swiftycode" frontend.global_appname="clitheme" frontend.global_subsections="cli" -try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt")): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() -except: - if _version.release==0: print("cli set_local_themedef failed: "+str(sys.exc_info()[1])) - pass +_globalvar.handle_set_themedef(frontend, "cli") def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, generate_only=False): """ diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 0d7018f..867a63c 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -14,13 +14,7 @@ except ImportError: import _globalvar, cli, frontend, _get_resource, _version from _generator import db_interface -try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() -except: - if _version.release==0: print("clitheme-exec set_local_themedef failed: "+str(sys.exc_info()[1])) - pass +_globalvar.handle_set_themedef(frontend, "clitheme-exec") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") def check_regenerate_db() -> bool: diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 924952c..0f83bf4 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -13,13 +13,7 @@ except ImportError: from _generator import db_interface import _globalvar, frontend, _get_resource, _version -try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/cli-strings.clithemedef.txt"), overlay=True): raise RuntimeError() - if not frontend.set_local_themedef(_get_resource.read_file("strings/exec-strings.clithemedef.txt"), overlay=True): raise RuntimeError() -except: - if _version.release==0: print("output_handler_posix set_local_themedef failed: "+str(sys.exc_info()[1])) - pass +_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') -- Gitee From b6af2be9330d5f5e9c4b0fb07b372dae211b0a5f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 1 May 2024 22:38:55 +0800 Subject: [PATCH 121/354] Update version (v2.0-dev20240501) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 26d3b6e..159088f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240430' +pkgver='2.0_dev20240501' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 91996cb..dbb3038 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240430-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240501-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 01 May 2024 15:22:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 01 May 2024 22:37:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 42a2c11..3abb238 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240430" +__version__="2.0-dev20240501" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240430" +version_main="2.0_dev20240501" version_buildnumber=1 \ No newline at end of file -- Gitee From 557a60c8b7689790311b5890a23a8889f6315869 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 2 May 2024 12:25:18 +0800 Subject: [PATCH 122/354] Fix handle_set_themedef message not working --- src/clitheme/_globalvar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 0ca394c..4998fab 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -155,5 +155,6 @@ def handle_set_themedef(fr, debug_name: str): fr.global_debugmode=False sys.stdout=sys.__stdout__ except: + sys.stdout=sys.__stdout__ if _version.release==0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) pass \ No newline at end of file -- Gitee From 812d9e76bd02fd74385ae99c2bfa2fe3b1c98d12 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 13:21:27 +0800 Subject: [PATCH 123/354] Handle KeyboardInterrupt during SIGINT send --- src/clitheme/exec/output_handler_posix.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 0f83bf4..57c50bd 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -114,6 +114,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line) else: output_lines=[] # happens when no 'break' statement occurs except KeyboardInterrupt: - process.send_signal(2) #SIGINT + try: process.send_signal(2) #SIGINT + except KeyboardInterrupt: pass #os.write(stdin_fd, b'\x03') return process.poll() -- Gitee From ac28f95ebc55bc32489efe18c3b20d15575e0e3c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 16:03:03 +0800 Subject: [PATCH 124/354] Fix db migration error message not showing --- src/clitheme/exec/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 867a63c..1417abc 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -46,6 +46,7 @@ def check_regenerate_db() -> bool: shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, _globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) print(fd.reof("db-migrate-success-msg", "Successfully completed migration, proceeding execution")) except: + sys.stdout=sys.__stdout__ 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]))) return False except FileNotFoundError: pass -- Gitee From 9e26fd54f0eb12a3490c81f32ad9ca8a14161f55 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 18:26:17 +0800 Subject: [PATCH 125/354] Migrate build backend to setuptools --- pyproject.toml | 22 +++++++++++++++------- src/clitheme/cli.py | 2 +- src/clitheme/exec/__init__.py | 4 +++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a1592f..59c9351 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,16 @@ [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" -[tool.hatch.version] -path = "src/clitheme/_version.py" +[tool.setuptools.dynamic] +version = {attr = "clitheme._version.__version__"} + +[tool.setuptools.packages.find] +where = ["src"] +exclude = ["*test*"] + +[tool.setuptools.package-data] +"*" = ["*"] [project] name = "clitheme" @@ -12,7 +19,7 @@ authors = [ { name="swiftycode", email="3291929745@qq.com" }, ] description = "A text theming library for command line applications" -readme = "README.md" +readme = "README.en.md" license = {file = "LICENSE"} requires-python = ">=3.7" classifiers = [ @@ -23,8 +30,9 @@ classifiers = [ [project.scripts] clitheme = "clitheme:cli.script_main" +clitheme-exec= "clitheme:exec.script_main" [project.urls] -Homepage = "https://gitee.com/swiftycode/clitheme" -Documentation = "https://gitee.com/swiftycode/clitheme/wikis" +Repository = "https://gitee.com/swiftycode/clitheme" +Documentation = "https://gitee.com/swiftycode/clitheme/wikis/pages" Issues = "https://gitee.com/swiftycode/clitheme/issues" \ No newline at end of file diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index a5d63c9..59fa276 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -279,6 +279,6 @@ def main(cli_args): return handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=cli_args[1]), arg_first) return 0 def script_main(): # for script - exit(main(sys.argv)) + return main(sys.argv) if __name__=="__main__": exit(main(sys.argv)) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 1417abc..4432e62 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -112,4 +112,6 @@ def main(arguments: list[str]): else: print("Error: Unsupported platform") return 1 - return 0 \ No newline at end of file + return 0 +def script_main(): # for script + return main(sys.argv) \ No newline at end of file -- Gitee From 5ab122ad367f7028e12d07d6909a6f6bdad1699b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 18:43:59 +0800 Subject: [PATCH 126/354] Improve input checking in frontend --- src/clitheme/frontend.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index acd252c..9b34887 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -119,16 +119,21 @@ class FetchDescriptor(): self.domain_name=global_domain.strip() 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() 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() else: self.subsections=subsections.strip() + self.subsections=re.sub(" {2,}", " ", self.subsections) if lang==None: self.lang=global_lang.strip() @@ -156,15 +161,16 @@ class FetchDescriptor(): # entry_path e.g. "class-a sample_text" # Sanity check the path - if _globalvar.sanity_check(entry_path)==False: - if self.debug_mode: print("[Debug] Error: entry names/subsections {}".format(_globalvar.sanity_check_error_message)) - return fallback_string + if entry_path.strip()=="": + raise SyntaxError("Empty entry name") + if _globalvar.sanity_check(entry_path, use_orig=True)==False: + raise SyntaxError("Entry names and subsections {}".format(_globalvar.sanity_check_error_message)) lang=[] # Language handling: see https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Environment-Variables for more information if not self.disable_lang: if self.lang!="": if self.debug_mode: print("[Debug] Locale: Using defined self.lang") - if not _globalvar.sanity_check(self.lang)==False: + if not _globalvar.sanity_check(self.lang, use_orig=True)==False: lang=[self.lang] else: if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) @@ -190,7 +196,7 @@ class FetchDescriptor(): possible_paths.append(path2+"__"+l) possible_paths.append(path2) for p in possible_paths: - if self.debug_mode: print("Trying "+p, end="...") + if self.debug_mode: print("Trying "+p, end=" ...") try: f=open(p,'r', encoding="utf-8") dat=f.read() @@ -230,6 +236,6 @@ class FetchDescriptor(): fallback_string="" for x in range(30): fallback_string+=random.choice(string.ascii_letters) - recieved_content=self.retrieve_entry_or_fallback(entry_path, fallback_string) - if recieved_content.strip()==fallback_string: return False + received_content=self.retrieve_entry_or_fallback(entry_path, fallback_string) + if received_content.strip()==fallback_string: return False else: return True -- Gitee From f970334fb8ea376206090090e53b23b8682587d8 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 20:13:03 +0800 Subject: [PATCH 127/354] Optimize set_local_themedef; Only write to data directory if generator succeeds --- src/clitheme/frontend.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 9b34887..7b0324e 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -44,8 +44,9 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: except ImportError: import _generator # Determine directory name h=hashlib.shake_256(bytes(file_content, "utf-8")) - d=h.hexdigest(6) + d=h.hexdigest(6) # length of 12 (6*2) global alt_path_hash + local_path_hash=alt_path_hash # if overlay, update hash with new contents of file if alt_path_hash!=None and overlay==True: newhash="" @@ -66,27 +67,31 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: elif alt_path_hash[x]>='0' and alt_path_hash[x]<='9': #digit numcur=ord(alt_path_hash[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) newhash+=chart[(numorig+numcur)%len(chart)] - alt_path_hash=newhash - else: alt_path_hash=d # else, use generated hash - global alt_path_dirname - dir_name=f"clitheme-data-{alt_path_hash}" # length of 12 (6*2) + local_path_hash=newhash + else: local_path_hash=d # else, use generated hash + dir_name=f"clitheme-data-{local_path_hash}" + _generator.generate_custom_path() # prepare _generator.path overlay_cont=False + global alt_path_dirname if alt_path_dirname!=None and overlay==True: # overlay - if not os.path.exists(_globalvar.clitheme_temp_root+"/"+dir_name): + if not os.path.exists(_globalvar.clitheme_temp_root+"/"+dir_name): # check if not already generated before overlay_cont=True - shutil.copytree(_globalvar.clitheme_temp_root+"/"+alt_path_dirname, _globalvar.clitheme_temp_root+"/"+dir_name) + shutil.copytree(_globalvar.clitheme_temp_root+"/"+alt_path_dirname, _generator.path) path_name=_globalvar.clitheme_temp_root+"/"+dir_name if global_debugmode: print("[Debug] "+path_name) # Generate data hierarchy as needed if overlay_cont or not os.path.exists(path_name): - _generator.path=path_name _generator.silence_warn=True try: _generator.generate_data_hierarchy(file_content, custom_path_gen=False) except SyntaxError: if global_debugmode: print("[Debug] Generator error: "+str(sys.exc_info()[1])) return False + shutil.copytree(_generator.path, path_name) + try: shutil.rmtree(_generator.path) + except: pass global alt_path + alt_path_hash=local_path_hash alt_path=path_name+"/"+_globalvar.generator_data_pathname alt_path_dirname=dir_name return True -- Gitee From d57195131c3c071ec9df1d480bf21eb84a69d0c1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 20:16:06 +0800 Subject: [PATCH 128/354] Optimize handle_set_themedef; - Unset global_debugmode if function fails - Only use overlay=True if not first file in list --- 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 4998fab..8712769 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -147,14 +147,16 @@ def get_locale(debug_mode: bool=False): def handle_set_themedef(fr, debug_name: str): try: files=["strings/generator-strings.clithemedef.txt", "strings/cli-strings.clithemedef.txt", "strings/exec-strings.clithemedef.txt"] - for filename in files: + 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=True): raise RuntimeError("Full log below: \n"+msg.getvalue()) + 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=False sys.stdout=sys.__stdout__ except: sys.stdout=sys.__stdout__ + fr.global_debugmode=False if _version.release==0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) pass \ No newline at end of file -- Gitee From bd0a540b462ab121ffe3763c5029e28ca15e6349 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 20:23:59 +0800 Subject: [PATCH 129/354] frontend: remove extra newline for content in debug mode --- 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 7b0324e..69a12f1 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -204,10 +204,10 @@ class FetchDescriptor(): if self.debug_mode: print("Trying "+p, end=" ...") try: f=open(p,'r', encoding="utf-8") - dat=f.read() + dat=re.sub(r"\n\Z", "", f.read()) if self.debug_mode: print("Success:\n> "+dat) # since the generator adds an extra newline in the entry data, we need to remove it - return re.sub(r"\n\Z", "", dat) + return dat except (FileNotFoundError, IsADirectoryError): if self.debug_mode: print("Failed") return fallback_string -- Gitee From b42b93299236525e1ff781b646535898c5359d50 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 20:25:31 +0800 Subject: [PATCH 130/354] Update clitheme_fallback.py --- clitheme_fallback.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/clitheme_fallback.py b/clitheme_fallback.py index 8b94b6b..f07bb0a 100644 --- a/clitheme_fallback.py +++ b/clitheme_fallback.py @@ -1,5 +1,5 @@ """ -clitheme fallback frontend for 1.1 (returns fallback values for all functions) +clitheme fallback frontend for version 2.0 (returns fallback values for all functions) """ from typing import Optional @@ -13,8 +13,10 @@ global_lang="" global_disablelang=False alt_path=None +alt_path_dirname=None +alt_path_hash=None -def set_local_themedef(file_content: str) -> bool: +def set_local_themedef(file_content: str, overlay: bool=False) -> bool: """Fallback set_local_themedef function (always returns False)""" return False def unset_local_themedef(): -- Gitee From 0a3053621c17703683071665a557c9c93e23d0c4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 20:54:35 +0800 Subject: [PATCH 131/354] Update build scripts to use setuptools --- PKGBUILD | 4 ++-- debian/control | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 159088f..3c7918e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -7,7 +7,7 @@ arch=('any') url="https://gitee.com/swiftycode/clitheme" license=('GPL3') depends=('python>=3.7' 'sqlite>=3') -makedepends=('git' 'python-hatch' 'python-installer' 'python-wheel' 'gzip') +makedepends=('git' 'python-setuptools' 'python-build' 'python-installer' 'python-wheel' 'gzip') checkdepends=() optdepends=() provides=() @@ -32,7 +32,7 @@ pkgver(){ build() { cd srctmp - hatch build -t wheel + python3 -m build --wheel --no-isolation } check() { diff --git a/debian/control b/debian/control index 201fc46..91ba9d9 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: libs Priority: optional Maintainer: swiftycode <3291929745@qq.com> Rules-Requires-Root: no -Build-Depends: debhelper-compat (= 13), python3, dh-python, python3-hatchling, pybuild-plugin-pyproject +Build-Depends: debhelper-compat (= 13), python3, dh-python, python3-setuptools, pybuild-plugin-pyproject Standards-Version: 4.6.2 Homepage: https://gitee.com/swiftycode/clitheme Vcs-Git: https://gitee.com/swiftycode/clitheme.git -- Gitee From 6778d523bb0cb567b2c4c505cd181f4a9203bc09 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 20:54:59 +0800 Subject: [PATCH 132/354] Remove generator_old for good --- src/clitheme/_generator/generator_old.py | 302 ----------------------- 1 file changed, 302 deletions(-) delete mode 100644 src/clitheme/_generator/generator_old.py diff --git a/src/clitheme/_generator/generator_old.py b/src/clitheme/_generator/generator_old.py deleted file mode 100644 index 3cc3f9e..0000000 --- a/src/clitheme/_generator/generator_old.py +++ /dev/null @@ -1,302 +0,0 @@ -""" -OBSOLETE generator functions; --Do not use-- -""" -import os -import string -import random -import re -try: - from .. import _globalvar - from .. import frontend - from .. import _version - from .. import _get_resource -except ImportError: # for test program - import _globalvar - import frontend - import _version - import _get_resource - -path="" # to be generated by function -silence_warn=False - -fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") - -def handle_error(message): - raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) -def handle_warning(message): - if not silence_warn: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) -def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate directories (excluding file itself) - current_path=path - current_entry="" # for error output - for x in entry_name.split()[:-1]: - current_entry+=x+" " - current_path+="/"+x - if os.path.isfile(current_path): # conflict with entry file - handle_error(fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ - num=str(line_number_debug), name=current_entry)) - elif os.path.isdir(str(current_path))==False: # directory does not exist - os.mkdir(current_path) -def add_entry(path, entry_name, entry_content, line_number_debug): # add entry to where it belongs (assuming recursive_mkdir already completed) - target_path=path - for x in entry_name.split(): - target_path+="/"+x - if os.path.isdir(target_path): - handle_error(fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ - num=str(line_number_debug), name=entry_name)) - elif os.path.isfile(target_path): - handle_warning(fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ - num=str(line_number_debug), name=entry_name)) - f=open(target_path,'w', encoding="utf-8") - f.write(entry_content+"\n") -def splitarray_to_string(split_content): - final="" - for phrase in split_content: - final+=phrase+" " - return final.strip() -def write_infofile(path,filename,content,line_number_debug, header_name_debug): - if not os.path.isdir(path): - os.makedirs(path) - target_path=path+"/"+filename - if os.path.isfile(target_path): - handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) - f=open(target_path,'w', encoding="utf-8") - f.write(content+'\n') - -def write_infofile_v2(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 - if os.path.isfile(target_path): - handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) - f=open(target_path,'w', encoding="utf-8") - for line in content_phrases: - f.write(line+"\n") - -def generate_custom_path(): - # Generate a temporary path - global path - path=_globalvar.clitheme_temp_root+"/clitheme-temp-" - for x in range(8): - path+=random.choice(string.ascii_letters) - -# Returns true for success or error message -def generate_data_hierarchy(file_content, custom_path_gen=True, custom_infofile_name="1"): - """ - Generate the data hierarchy in a temporary directory from a definition file (accessible with _generator.path) - - This function should not be invoked directly unless absolutely necessary. - """ - if custom_path_gen: - generate_custom_path() - if not os.path.exists(path): os.mkdir(path) - datapath=path+"/"+_globalvar.generator_data_pathname - if not os.path.exists(datapath): os.mkdir(datapath) - current_status="" # header, main, entry - linenumber=0 - # To detect repeated blocks - headerparsed=False - mainparsed=False - - current_domainapp="" # for in_domainapp and unset_domainapp in main block - current_entry_name="" # for entry - current_subsection="" # for in_subsection - - current_entry_locale="" # for handling locale_block - current_entry_linenumber=-1 - - current_header_entry="" # for block input in header - current_header_linenumber=-1 - - blockinput=False # for multi-line (block) input - blockinput_data="" # data of current block input - blockinput_minspaces=-1 # min number of whitespaces - for line in file_content.splitlines(): - linenumber+=1 - phrases=line.split() - if blockinput==False and (line.strip()=="" or line.strip()[0]=="#"): # if empty line or comment (except in block input mode) - continue - - if blockinput==True: - if len(phrases)>0 and phrases[0]=="end_block": - if blockinput_minspaces!=-1: - # process whitespaces - # trim amount of leading whitespaces on each line - pattern=r"(?P\n|^)[ ]{"+str(blockinput_minspaces)+"}" - blockinput_data=re.sub(pattern,r"\g", blockinput_data) - if current_status=="entry": - for this_locale in current_entry_locale.split(): - target_entry=current_entry_name - if this_locale!="default": - target_entry+="__"+this_locale - add_entry(datapath,target_entry, blockinput_data, current_entry_linenumber) - # clear data - current_entry_locale="" - current_entry_linenumber=-1 - elif current_status=="header": - if current_header_entry!="description": - # trim all leading whitespaces - blockinput_data=re.sub(r"(?P\n|^)[ ]+",r"\g", blockinput_data) - # trim all trailing whitespaces - blockinput_data=re.sub(r"[ ]+(?P\n|$)",r"\g", blockinput_data) - # trim all leading/trailing newlines - blockinput_data=re.sub(r"(\A\n+|\n+\Z)", "", blockinput_data) - filename="clithemeinfo_"+current_header_entry+"_v2" - if current_header_entry=="description": - filename="clithemeinfo_"+current_header_entry - write_infofile( \ - path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ - filename,\ - blockinput_data,current_header_linenumber,current_header_entry) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 - # clear data - current_header_entry="" - current_header_linenumber=-1 - else: # the unlikely case - handle_error(fd.feof("internal-error-blockinput", "Line {num}: internal error while handling block input; please file a bug report", num=str(linenumber))) - # clear data - blockinput=False - blockinput_data="" - else: - if blockinput_data!="": blockinput_data+="\n" - line_content=line.strip() - if line_content=="": # empty line - if blockinput_data=="": blockinput_data+=" " - continue - # Calculate whitespaces - spaces=-1 - ws_match=re.search(r"^\s+", line) # match leading whitespaces - if ws_match==None: # no leading spaces - spaces=0 - else: - leading_whitespace=ws_match.group() - # substitute \t with 8 spaces - leading_whitespace=re.sub(r"\t"," "*8, leading_whitespace) - # append it to line_content - line_content=leading_whitespace+line_content - # write line_content to data - blockinput_data+=line_content - spaces=len(leading_whitespace) - # update min count - if spaces!=-1 and (spaces3: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - # sanity check - if _globalvar.sanity_check(phrases[1]+" "+phrases[2])==False: - handle_error(fd.feof("sanity-check-domainapp-err", "Line {num}: domain and app names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) - current_domainapp=phrases[1]+" "+phrases[2] - current_subsection="" - elif phrases[0]=="in_subsection": - if len(phrases)<2: - handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) - # sanity check - if _globalvar.sanity_check(splitarray_to_string(phrases[1:]))==False: - handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) - current_subsection=splitarray_to_string(phrases[1:]) - elif phrases[0]=="unset_domainapp": - if len(phrases)!=1: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - current_domainapp="" - current_subsection="" - elif phrases[0]=="unset_subsection": - if len(phrases)!=1: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - current_subsection="" - elif phrases[0]=="end_main": - if len(phrases)!=1: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - current_status="" - mainparsed=True - else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(linenumber))) - elif current_status=="entry": # expect locale, end_entry - if phrases[0]=="locale": - if len(phrases)<3: - handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) - content=splitarray_to_string(phrases[2:]) - target_entry=current_entry_name - if phrases[1]!="default": - target_entry+="__"+phrases[1] - add_entry(datapath,target_entry,content,linenumber) - elif phrases[0]=="locale_block": - if len(phrases)<2: - handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) - current_entry_locale=splitarray_to_string(phrases[1:]) - current_entry_linenumber=linenumber - blockinput=True # start block input - elif phrases[0]=="end_entry": - if len(phrases)!=1: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) - current_status="main" - current_entry_name="" - else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(linenumber))) - if not headerparsed or not mainparsed: - handle_error(fd.reof("incomplete-block-err", "Missing or incomplete header or main block")) - # Update current theme index - theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") - theme_index.write(custom_infofile_name+"\n") - return True # Everything is successful! :) - -try: - if not frontend.set_local_themedef(_get_resource.read_file("strings/generator-strings.clithemedef.txt")): raise RuntimeError() -except: - if _version.release==0: print("generator set_local_themedef failed") - pass \ No newline at end of file -- Gitee From 8d3c2a14d7af819d282a17ebaf079044fe03e47a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 4 May 2024 23:33:57 +0800 Subject: [PATCH 133/354] Update version (v2.0-dev20240504) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 3c7918e..4dc6d38 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240501' +pkgver='2.0_dev20240504' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index dbb3038..c1cc804 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240501-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240504-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 01 May 2024 22:37:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 04 May 2024 22:37:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 3abb238..258bc06 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240501" +__version__="2.0-dev20240504" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240501" +version_main="2.0_dev20240504" version_buildnumber=1 \ No newline at end of file -- Gitee From 568480eb3980d1a1f43d43a766c4e559e4410b4c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 5 May 2024 15:16:36 +0800 Subject: [PATCH 134/354] Implement global option scopes; - Options defined in sections applies to that block only - Options defined outside sections applies to all sections --- src/clitheme/_generator/__init__.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index e11360c..041ef77 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -88,6 +88,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info lines_data=file_content.splitlines() lineindex=-1 # counter extra +1 operation at beginning global_options={} + really_really_global_options={} # options defined outside any blocks global_variables={} # define check functions @@ -104,7 +105,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') # defined sub-processing functions - def parse_options(options_data: list[str], merge_global_options: bool, allowed_options: Optional[list]=None) -> dict: + def parse_options(options_data: list[str], merge_global_options: int, allowed_options: Optional[list]=None) -> dict: nonlocal global_options # value options: options requiring an integer value value_options=["leadtabindents", "leadspaces"] @@ -116,7 +117,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # only one of these options can be set to true at the same time (specific to groups) bool_options_unique_groups=[["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"], ["stdout_only", "stderr_only"]] final_options={} - if merge_global_options: final_options=copy.copy(global_options) + if merge_global_options!=0: final_options=copy.copy(global_options if merge_global_options==1 else really_really_global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options for each_option in options_data: option_name=re.sub(r"^(no)*(?P.+?)(:.+)*$", r"\g", each_option) @@ -153,9 +154,13 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) return final_options - def handle_set_global_options(options_data: list[str]): + def handle_set_global_options(options_data: list[str], really_really_global: bool=False): # set options globally - nonlocal global_options; global_options=parse_options(options_data, merge_global_options=True) + if really_really_global: nonlocal really_really_global_options; really_really_global_options=parse_options(options_data, merge_global_options=2) + else: nonlocal global_options; global_options=parse_options(options_data, merge_global_options=1) + def handle_setup_global_options(): + # reset global_options to contents of really_really_global_options + nonlocal global_options; global_options=copy.copy(really_really_global_options) def subst_variable_content(content: str, override_check: bool=False) -> str: if not override_check and (not "substvar" in global_options or global_options["substvar"]==False): return content # get all variables used in content @@ -239,11 +244,11 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if option=="leadtabindents": if not preserve_indents and option not in global_options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) # insert tabs at start of each line - blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) + if preserve_indents: blockinput_data=re.sub(r"^", r"\t"*int(got_options['leadtabindents']), blockinput_data, flags=re.MULTILINE) elif option=="leadspaces": if not preserve_indents and option not in global_options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) # insert spaces at start of each line - blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) + if preserve_indents: blockinput_data=re.sub(r"^", " "*int(got_options['leadspaces']), blockinput_data, flags=re.MULTILINE) elif option=="substesc": # substitute {{ESC}} with escape literal if got_options['substesc']==True: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) @@ -328,11 +333,15 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if is_ignore_line(): continue first_phrase=lines_data[lineindex].split()[0] # process header and main sections here - if first_phrase=="begin_header" or first_phrase==r"{header_section}": + if first_phrase=="set_options": + check_enough_args(lines_data[lineindex].split(), 2) + handle_set_global_options(lines_data[lineindex].split()[1:], really_really_global=True) + elif first_phrase=="begin_header" or first_phrase==r"{header_section}": # avoid repeated block if "header" in parsed_sections: handle_error(fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(lineindex+1), section="header")) section_parsing=True + handle_setup_global_options() # --Process header block-- end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" while lineindex Date: Sun, 5 May 2024 15:17:30 +0800 Subject: [PATCH 135/354] Remove extra strip operations in db_interface It interferes with substitute patterns containing lead indents, and other places already strips the string --- src/clitheme/_generator/db_interface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 57ab1e9..a28a8d0 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -62,23 +62,23 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma else: # remove any existing values with the same match_pattern match_condition=f"match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition} AND stdout_stderr_only=?" - match_params=(match_pattern.strip(), effective_locale, stdout_stderr_matchoption) + match_params=(match_pattern, effective_locale, stdout_stderr_matchoption) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: 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.strip(), substitute_pattern.strip(), None, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption)) + 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)) for cmd in cmdlist: # remove any existing values with the same match_pattern and effective_command and command_match_strictness(if ==2) strictness_condition="" if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=?" - match_params=(match_pattern.strip(), cmd.strip(), effective_locale, stdout_stderr_matchoption) + match_params=(match_pattern, cmd, effective_locale, stdout_stderr_matchoption) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: 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.strip(), substitute_pattern.strip(), cmd.strip(), is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption)) + 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)) connection.commit() def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False) -> bytes: -- Gitee From e4de15176231f52db72ca73f224c4051efdc36bd Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 5 May 2024 16:12:52 +0800 Subject: [PATCH 136/354] Merge common section begin&end operations into handler function --- src/clitheme/_generator/__init__.py | 39 +++++++++++++---------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 041ef77..f03445a 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -91,7 +91,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info really_really_global_options={} # options defined outside any blocks global_variables={} - # define check functions + ## check functions def check_enough_args(phrases: list[str], count: int): if len(phrases) bool: return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') - # defined sub-processing functions + ## sub-processing functions and handlers def parse_options(options_data: list[str], merge_global_options: int, allowed_options: Optional[list]=None) -> dict: nonlocal global_options # value options: options requiring an integer value @@ -200,7 +200,17 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info var_content=subst_variable_content(var_content) # set variable global_variables[var_name]=var_content + def handle_begin_section(section_name: str): + nonlocal parsed_sections + if section_name in parsed_sections: + handle_error(fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(lineindex+1), section=section_name)) + nonlocal section_parsing; section_parsing=True + handle_setup_global_options() + def handle_end_section(section_name: str): + nonlocal parsed_sections; parsed_sections.append(section_name) + nonlocal section_parsing; section_parsing=False + ## sub-block processing functions def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True) -> str: nonlocal lineindex minspaces=math.inf @@ -337,11 +347,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info check_enough_args(lines_data[lineindex].split(), 2) handle_set_global_options(lines_data[lineindex].split()[1:], really_really_global=True) elif first_phrase=="begin_header" or first_phrase==r"{header_section}": - # avoid repeated block - if "header" in parsed_sections: - handle_error(fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(lineindex+1), section="header")) - section_parsing=True - handle_setup_global_options() + handle_begin_section("header") # --Process header block-- end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" while lineindex Date: Sun, 5 May 2024 16:18:38 +0800 Subject: [PATCH 137/354] Remove global variables after each section --- src/clitheme/_generator/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index f03445a..0e42f1e 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -206,6 +206,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_error(fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(lineindex+1), section=section_name)) nonlocal section_parsing; section_parsing=True handle_setup_global_options() + nonlocal global_variables; global_variables={} def handle_end_section(section_name: str): nonlocal parsed_sections; parsed_sections.append(section_name) nonlocal section_parsing; section_parsing=False -- Gitee From d8cd434736d7077dca6f1a52627f89ce58a6561e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 5 May 2024 17:17:59 +0800 Subject: [PATCH 138/354] First version of manpage_section implementation --- src/clitheme/_generator/__init__.py | 51 ++++++++++++++++++- src/clitheme/_globalvar.py | 1 + .../strings/generator-strings.clithemedef.txt | 7 +++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 0e42f1e..beb10c6 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -8,6 +8,7 @@ import random import re import math import copy +import gzip from typing import Optional try: from .. import _globalvar, frontend, _version, _get_resource @@ -67,6 +68,26 @@ def write_infofile_newlines(path: str, filename: str, content_phrases: list[str] for line in content_phrases: f.write(line+"\n") +def write_manpage_file(file_path: list[str], content: str, line_number_debug: int): + parent_path=path+"/"+_globalvar.generator_manpage_pathname+"/" + if len(file_path)>1: + for subdir in file_path[:-1]: + parent_path+=subdir+"/" + # create the parent directory + if not os.path.isdir(path): + try: os.makedirs(parent_path) + except NotADirectoryError: + handle_error(fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) + # write the compressed and original version of the file + full_path=parent_path+"/"+file_path[-1] + if os.path.isfile(full_path): + handle_warning(fd.feof("repeated-manpage-warn","Line {num}: repeated manpage file, overwriting", num=str(line_number_debug))) + try: + open(full_path, "w", encoding="utf-8").write(content) + open(full_path+".gz", "wb").write(gzip.compress(bytes(content, "utf-8"))) + except IsADirectoryError: + handle_error(fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) + def generate_custom_path(): # Generate a temporary path global path @@ -525,9 +546,37 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info break else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) ## END --Process substrules block-- + elif first_phrase==r"{manpage_section}": + handle_begin_section("manpage") + ## --Process manpage block-- + end_phrase="{/manpage_section}" + while lineindex") diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 8712769..c206de9 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -43,6 +43,7 @@ clitheme_temp_root="/tmp" if os.name!="nt" else os.environ['TEMP'] ## _generator file and folder names generator_info_pathname="theme-info" # e.g. ~/.local/share/clitheme/theme-info generator_data_pathname="theme-data" # e.g. ~/.local/share/clitheme/theme-data +generator_manpage_pathname="manpages" # e.g. ~/.local/share/clitheme/manpages generator_index_filename="current_theme_index" # e.g. [...]/theme-info/current_theme_index generator_info_filename="clithemeinfo_{info}" # e.g. [...]/theme-info/1/clithemeinfo_name generator_info_v2filename=generator_info_filename+"_v2" # e.g. [...]/theme-info/1/clithemeinfo_description_v2 diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 92a2ba2..f42016d 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -58,6 +58,9 @@ in_subsection generator [entry] bad-var-name-err locale:zh_CN 第{num}行:"{name}"不是一个有效的变量名称 [/entry] + [entry] manpage-subdir-file-conflict-err + locale:zh_CN 第{num}行:子路径和文件有冲突;请检查之前的定义 + [/entry] # warning strings [entry] warning-str locale:zh_CN 警告:{msg} @@ -77,6 +80,8 @@ in_subsection generator [entry] unknown-variable-warn locale:zh_CN 第{num}行:未知变量名称"{name}",不会进行替换 [/entry] + [entry] repeated-manpage-warn + locale:zh_CN 第{num}行:重复的manpage文件;之前的文件内容将会被覆盖 # sanity check [entry] sanity-check-entry-err locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} @@ -87,6 +92,8 @@ in_subsection generator [entry] sanity-check-subsection-err locale:zh_CN 第{num}行:子路径名称{sanitycheck_msg} [/entry] + [entry] sanity-check-manpage-err + locale:zh_CN 第{num}行:manpage路径{sanitycheck_msg};使用空格以指定子路径 [entry] sanity-check-msg-banphrase-err locale:zh_CN 不能包含'{char}' [/entry] -- Gitee From 0f5183ff7d0fdd49ad3c115ad1ad3c33edc591cb Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 5 May 2024 19:15:14 +0800 Subject: [PATCH 139/354] Add generated build files to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4d30e4a..d8c17a6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__ __pycache__/* dist dist/* +build +build/* +src/*.egg-info .vscode .vscode/* # excluded files (ex) -- Gitee From 3e4f400f872232ebe0ee7124cdcacdee8506087c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 5 May 2024 22:10:10 +0800 Subject: [PATCH 140/354] Update version (v2.0-20240505) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 4dc6d38..85582f5 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240504' +pkgver='2.0_dev20240505' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index c1cc804..e1e713c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240504-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240505-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 04 May 2024 22:37:00 +0800 + -- swiftycode <3291929745@qq.com> Sun, 05 May 2024 22:05:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 258bc06..1abd001 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240504" +__version__="2.0-dev20240505" major=2 minor=0 release=0 # 0 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240504" +version_main="2.0_dev20240505" version_buildnumber=1 \ No newline at end of file -- Gitee From fc0bca886bfdc9acfc3eb975d08173862eb5bf77 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 11:51:22 +0800 Subject: [PATCH 141/354] Implement scope in variables --- src/clitheme/_generator/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index beb10c6..015bdae 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -109,8 +109,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info lines_data=file_content.splitlines() lineindex=-1 # counter extra +1 operation at beginning global_options={} - really_really_global_options={} # options defined outside any blocks + really_really_global_options={} # options defined outside any sections global_variables={} + really_really_global_variables={} # variables defined outside any sections ## check functions def check_enough_args(phrases: list[str], count: int): @@ -182,6 +183,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info def handle_setup_global_options(): # reset global_options to contents of really_really_global_options nonlocal global_options; global_options=copy.copy(really_really_global_options) + nonlocal global_variables; global_variables=copy.copy(really_really_global_variables) def subst_variable_content(content: str, override_check: bool=False) -> str: if not override_check and (not "substvar" in global_options or global_options["substvar"]==False): return content # get all variables used in content @@ -198,8 +200,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info continue new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) return new_content - def handle_set_variable(line_content: str): - nonlocal global_variables + def handle_set_variable(line_content: str, really_really_global: bool=False): + nonlocal global_variables, really_really_global_variables if not line_content.split()[0].startswith("setvar:"): return # match variable name check_enough_args(line_content.split(), 2) @@ -220,14 +222,14 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if "substvar" in global_options and global_options["substvar"]==True: var_content=subst_variable_content(var_content) # set variable - global_variables[var_name]=var_content + if really_really_global: really_really_global_variables[var_name]=var_content + else: global_variables[var_name]=var_content def handle_begin_section(section_name: str): nonlocal parsed_sections if section_name in parsed_sections: handle_error(fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(lineindex+1), section=section_name)) nonlocal section_parsing; section_parsing=True handle_setup_global_options() - nonlocal global_variables; global_variables={} def handle_end_section(section_name: str): nonlocal parsed_sections; parsed_sections.append(section_name) nonlocal section_parsing; section_parsing=False @@ -368,6 +370,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if first_phrase=="set_options": check_enough_args(lines_data[lineindex].split(), 2) handle_set_global_options(lines_data[lineindex].split()[1:], really_really_global=True) + elif first_phrase.startswith("setvar:"): + check_enough_args(phrases, 2) + handle_set_variable(lines_data[lineindex], really_really_global=True) elif first_phrase=="begin_header" or first_phrase==r"{header_section}": handle_begin_section("header") # --Process header block-- -- Gitee From e438094ebb4e514be52537559ddef3e690619bc4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 11:52:13 +0800 Subject: [PATCH 142/354] Fix typo in generator-strings --- src/clitheme/strings/generator-strings.clithemedef.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index f42016d..115b758 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -82,6 +82,7 @@ in_subsection generator [/entry] [entry] repeated-manpage-warn locale:zh_CN 第{num}行:重复的manpage文件;之前的文件内容将会被覆盖 + [/entry] # sanity check [entry] sanity-check-entry-err locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} @@ -94,6 +95,7 @@ in_subsection generator [/entry] [entry] sanity-check-manpage-err locale:zh_CN 第{num}行:manpage路径{sanitycheck_msg};使用空格以指定子路径 + [/entry] [entry] sanity-check-msg-banphrase-err locale:zh_CN 不能包含'{char}' [/entry] -- Gitee From de92049cde3c43ea9dc8f413560d910bfa9fcb28 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 11:56:07 +0800 Subject: [PATCH 143/354] Fix global setvar not working --- src/clitheme/_generator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 015bdae..e167c4e 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -371,7 +371,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info check_enough_args(lines_data[lineindex].split(), 2) handle_set_global_options(lines_data[lineindex].split()[1:], really_really_global=True) elif first_phrase.startswith("setvar:"): - check_enough_args(phrases, 2) + check_enough_args(lines_data[lineindex].split(), 2) handle_set_variable(lines_data[lineindex], really_really_global=True) elif first_phrase=="begin_header" or first_phrase==r"{header_section}": handle_begin_section("header") -- Gitee From 53bcc89821945595375a9fa9479ef052c98c69d0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 13:36:41 +0800 Subject: [PATCH 144/354] Implement substvar for all content phrases + other changes: - Fix option processing in handle_block_input - Don't use substesc for non-content sections - Improve sanity check in "in_subsection" when a variable is used --- src/clitheme/_generator/__init__.py | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index e167c4e..604db16 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -235,7 +235,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info nonlocal section_parsing; section_parsing=False ## sub-block processing functions - def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True) -> str: + def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True, disable_substesc: bool=False) -> str: nonlocal lineindex minspaces=math.inf blockinput_data="" @@ -272,20 +272,24 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info blockinput_data=re.sub(pattern,r"\g", blockinput_data, flags=re.MULTILINE) # parse leadtabindents leadspaces, and substesc options got_options=copy.copy(global_options) + specified_options={} if len(lines_data[lineindex].split())>1: got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True, allowed_options=(["leadtabindents", "leadspaces"] if preserve_indents else []) if disallow_cmdmatch_options else None) + specified_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=False, allowed_options=(["leadtabindents", "leadspaces"] if preserve_indents else []) if disallow_cmdmatch_options else None) for option in got_options.keys(): + def is_specified_in_block() -> bool: return option in specified_options.keys() and specified_options[option]==True if option=="leadtabindents": - if not preserve_indents and option not in global_options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) + if not preserve_indents and is_specified_in_block(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) # 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": - if not preserve_indents and option not in global_options.keys(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) + if not preserve_indents and is_specified_in_block(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) # 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": + if disable_substesc and is_specified_in_block(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) # substitute {{ESC}} with escape literal - if got_options['substesc']==True: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) + if got_options['substesc']==True and not disable_substesc: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) elif option=="substvar": if got_options['substvar']==True: blockinput_data=subst_variable_content(blockinput_data, True) elif disallow_cmdmatch_options: @@ -385,6 +389,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if phrases[0]=="name" or phrases[0]=="version" or phrases[0]=="description": check_enough_args(phrases, 2) content=_globalvar.extract_content(lines_data[lineindex]) + content=subst_variable_content(content) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ _globalvar.generator_info_filename.format(info=phrases[0]),\ @@ -392,6 +397,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="locales" or phrases[0]=="supported_apps": check_enough_args(phrases, 2) content=phrases[1:] + for x in range(len(content)): + content[x]=subst_variable_content(content[x]) write_infofile_newlines( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ _globalvar.generator_info_v2filename.format(info=phrases[0]),\ @@ -406,12 +413,19 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content=handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase=endphrase) filename=_globalvar.generator_info_filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) else: - content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase) + content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase, disable_substesc=True) filename=_globalvar.generator_info_v2filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) + content=subst_variable_content(content) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ filename,\ content,lineindex+1,re.sub(r'_block$','',phrases[0])) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 + elif phrases[0]=="set_options": + check_enough_args(phrases, 2) + handle_set_global_options(phrases[1:]) + elif phrases[0].startswith("setvar:"): + check_enough_args(phrases, 2) + handle_set_variable(lines_data[lineindex]) elif phrases[0]==end_phrase: check_extra_args(phrases, 1, use_exact_count=True) handle_end_section("header") @@ -442,10 +456,10 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info subsection="" # clear subsection elif phrases[0]=="in_subsection": check_enough_args(phrases, 2) - if _globalvar.sanity_check(_globalvar.splitarray_to_string(phrases[1:]))==False: - handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) subsection=_globalvar.splitarray_to_string(phrases[1:]) subsection=subst_variable_content(subsection) + if _globalvar.sanity_check(subsection)==False: + handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) elif phrases[0]=="unset_domainapp": check_extra_args(phrases, 1, use_exact_count=True) domainapp=""; subsection="" @@ -495,7 +509,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info phrases=lines_data[lineindex].split() if phrases[0]=="[filter_commands]": check_extra_args(phrases, 1, use_exact_count=True) - content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=r"[/filter_commands]", disallow_cmdmatch_options=False) + content=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() @@ -518,6 +532,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="filter_command": check_enough_args(phrases, 2) content=_globalvar.splitarray_to_string(phrases[1:]) + content=subst_variable_content(content) strictness=0 for this_option in global_options: if this_option=="strictcmdmatch" and global_options['strictcmdmatch']==True: -- Gitee From 79e2834785d0491921c6f7cbb84254cae6e9baed Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 13:38:05 +0800 Subject: [PATCH 145/354] Reset terminal color after displaying description The description in theme definition might not include "{{ESC}}[0m", which messes up terminal output. Therefore, we will add that to prevent this mistake from messing up the terminal. --- 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 59fa276..146bb45 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -160,7 +160,7 @@ def get_current_theme_info(): if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="description")): description=open(target_path+"/"+_globalvar.generator_info_filename.format(info="description"), 'r', encoding="utf-8").read() print(f.reof("description-str", "Description:")) - print(re.sub(r"\n\Z", "", description)) # remove the extra newline added by _generator + print(re.sub(r"\n\Z", "", description)+"\x1b[0m") # remove the extra newline added by _generator # locales locales="(Unknown)" # version 2: items are separated by newlines instead of spaces -- Gitee From 42fb7663ef353cbc188f7eb75f8c408565651b3a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 21:46:52 +0800 Subject: [PATCH 146/354] Implement variable content substitution in manpage file path; - Change syntax of manpage_section --- src/clitheme/_generator/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 604db16..4a89c31 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -11,9 +11,9 @@ import copy import gzip from typing import Optional try: - from .. import _globalvar, frontend, _version, _get_resource + from .. import _globalvar, frontend except ImportError: # for test program - import _globalvar, frontend, _version, _get_resource + import _globalvar, frontend fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") @@ -574,14 +574,13 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info lineindex+=1 if is_ignore_line(): continue phrases=lines_data[lineindex].split() - if phrases[0]=="[manpage]": + if phrases[0]=="[file_content]": check_enough_args(phrases, 2) - check_extra_args(phrases, 2, use_exact_count=True) - filepath=phrases[1:] + filepath=subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() # sanity check the file path if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: handle_error(fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) - content=handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/manpage]") + content=handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/file_content]") write_manpage_file(filepath, content, lineindex+1) elif phrases[0]=="set_options": check_enough_args(phrases, 2) -- Gitee From 31c7df4a6cff4d62b3cfe36c741b97c268ac2269 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 21:46:59 +0800 Subject: [PATCH 147/354] Remove unused imports --- src/clitheme/cli.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 146bb45..bf57063 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -12,14 +12,10 @@ try: from . import _globalvar from . import _generator from . import frontend - from . import _get_resource - from . import _version except ImportError: import _globalvar import _generator import frontend - import _get_resource - import _version usage_description=\ """Usage: {0} apply-theme [themedef-file] [--overlay] [--preserve-temp] -- Gitee From 1a1181c99f8089edd021fa2c276ffb1681f0bd46 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 21:55:58 +0800 Subject: [PATCH 148/354] Change version release scheme; Use "-1" to denote development version in _version.release, because the final version scheme will be changed to 2.x.x (e.g 2.0[.0]) --- src/clitheme/_globalvar.py | 2 +- src/clitheme/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index c206de9..997f0bf 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -159,5 +159,5 @@ def handle_set_themedef(fr, debug_name: str): except: sys.stdout=sys.__stdout__ fr.global_debugmode=False - if _version.release==0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) + if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) pass \ No newline at end of file diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 1abd001..d921d33 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -3,7 +3,7 @@ __version__="2.0-dev20240505" major=2 minor=0 -release=0 # 0 stands for "dev" +release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead version_main="2.0_dev20240505" -- Gitee From e41cfbd169ac4458ec997903677962df60b3d8f3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 21:56:54 +0800 Subject: [PATCH 149/354] Update version (v2.0-dev20240506) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 85582f5..f510599 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240505' +pkgver='2.0_dev20240506' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index e1e713c..d28f278 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240505-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240506-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sun, 05 May 2024 22:05:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, 06 May 2024 21:51:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index d921d33..f2ba416 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240505" +__version__="2.0-dev20240506" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240505" +version_main="2.0_dev20240506" version_buildnumber=1 \ No newline at end of file -- Gitee From 94b9f387d5ad7a1fb3ee9512dddf3c80f796c347 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 22:09:59 +0800 Subject: [PATCH 150/354] Display "use --help" message prompt when no arguments specified --- src/clitheme/cli.py | 3 +-- src/clitheme/exec/__init__.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index bf57063..8fa7c92 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -22,7 +22,6 @@ usage_description=\ {0} get-current-theme-info {0} unset-current-theme {0} generate-data [themedef-file] [--overlay] - {0} --help {0} --version""" frontend.global_domain="swiftycode" @@ -203,7 +202,7 @@ def main(cli_args): arg_first="clitheme" # controls what appears as the command name in messages if len(cli_args)<=1: # no arguments passed print(usage_description.format(arg_first)) - print(f.reof("no-command", "Error: no command or option specified")) + handle_usage_error(f.reof("no-command", "Error: no command or option specified"), arg_first) return 1 def check_enough_args(count: int, exclude_options: bool=True): diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 4432e62..be38abd 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -97,7 +97,7 @@ def main(arguments: list[str]): return 0 else: handle_help_message() - print(fd.reof("no-command-err", "Error: no command specified")) + handle_error(fd.reof("no-command-err", "Error: no command specified")) return 1 # check database if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): -- Gitee From 4206dd308d6d0113915e61eee904ccb6658a2b34 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 22:43:05 +0800 Subject: [PATCH 151/354] Rename clitheme_demo files and add an usage instruction --- clitheme_demo.py | 3 ++- {example-clithemedef => demo-clithemedef}/README.zh-CN.md | 2 +- .../demo-theme-textemojis.clithemedef.txt | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename {example-clithemedef => demo-clithemedef}/README.zh-CN.md (88%) rename example-clithemedef/example-theme-textemojis.clithemedef.txt => demo-clithemedef/demo-theme-textemojis.clithemedef.txt (100%) diff --git a/clitheme_demo.py b/clitheme_demo.py index 533d0f3..713a1c7 100755 --- a/clitheme_demo.py +++ b/clitheme_demo.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 -# This file is originally named clitheme_example.py +# This program is a demo of the clitheme frontend API for applications. Apply a theme definition file in the folder "demo-clithemedef" to see it in action. +# 这个程序展示了clitheme的应用程序frontend API。请应用一个在"demo-clithemedef"文件夹中的任意一个主题定义文件以观察它的效果。 import os import sys diff --git a/example-clithemedef/README.zh-CN.md b/demo-clithemedef/README.zh-CN.md similarity index 88% rename from example-clithemedef/README.zh-CN.md rename to demo-clithemedef/README.zh-CN.md index d05e345..bbf92b3 100644 --- a/example-clithemedef/README.zh-CN.md +++ b/demo-clithemedef/README.zh-CN.md @@ -1,6 +1,6 @@ # 字符串定义说明 -本仓库中的`clitheme_example.py`支持以下字符串定义。默认字符串文本会以blockquote样式显示在路径名称下方。部分定义会包含额外的说明。 +本仓库中的`clitheme_demo.py`支持以下字符串定义。默认字符串文本会以blockquote样式显示在路径名称下方。部分定义会包含额外的说明。 --- diff --git a/example-clithemedef/example-theme-textemojis.clithemedef.txt b/demo-clithemedef/demo-theme-textemojis.clithemedef.txt similarity index 100% rename from example-clithemedef/example-theme-textemojis.clithemedef.txt rename to demo-clithemedef/demo-theme-textemojis.clithemedef.txt -- Gitee From 3b3bed44cdcd602bea1cf9ec10939be8e61e060b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 6 May 2024 22:44:38 +0800 Subject: [PATCH 152/354] Tweak the "use --help" prompt a little bit --- src/clitheme/cli.py | 2 +- src/clitheme/exec/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 8fa7c92..82e5d41 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -190,7 +190,7 @@ def is_option(arg): def handle_usage_error(message, cli_args_first): f=frontend.FetchDescriptor() print(message) - print(f.feof("help-usage-prompt", "Run {clitheme} --help for usage information", clitheme=cli_args_first)) + print(f.feof("help-usage-prompt", "Run \"{clitheme} --help\" for usage information", clitheme=cli_args_first)) return 1 def main(cli_args): """ diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index be38abd..171becb 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -68,7 +68,7 @@ def handle_help_message(full_help: bool=False): def handle_error(message: str): print(message) - print(fd.reof("help-usage-prompt", "Run clitheme-exec --help for usage information")) + print(fd.reof("help-usage-prompt", "Run \"clitheme-exec --help\" for usage information")) return 1 def main(arguments: list[str]): -- Gitee From 81a84f88aad4ba6b9645b62ac5866fc10e2eb08c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 09:23:28 +0800 Subject: [PATCH 153/354] Remove unused imports --- src/clitheme/_generator/db_interface.py | 4 ++-- src/clitheme/exec/__init__.py | 4 ++-- src/clitheme/exec/output_handler_posix.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index a28a8d0..6d7c48f 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -4,8 +4,8 @@ import sqlite3 import re import copy from typing import Optional -try: from .. import _globalvar, frontend, _get_resource, _version -except ImportError: import _globalvar, frontend, _get_resource, _version +try: from .. import _globalvar, frontend +except ImportError: import _globalvar, frontend connection=sqlite3.connect(":memory:") # placeholder debug_mode=False diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 171becb..dee329a 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -7,11 +7,11 @@ import io import shutil try: from . import output_handler_posix - from .. import _globalvar, cli, frontend, _get_resource, _version + from .. import _globalvar, cli, frontend from .._generator import db_interface except ImportError: import output_handler_posix - import _globalvar, cli, frontend, _get_resource, _version + import _globalvar, cli, frontend from _generator import db_interface _globalvar.handle_set_themedef(frontend, "clitheme-exec") diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 57c50bd..001a89c 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -8,10 +8,10 @@ import termios import copy try: from .._generator import db_interface - from .. import _globalvar, frontend, _get_resource, _version + from .. import _globalvar, frontend except ImportError: from _generator import db_interface - import _globalvar, frontend, _get_resource, _version + import _globalvar, frontend _globalvar.handle_set_themedef(frontend, "output_handler_posix") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") -- Gitee From a2496057209b947e4b7c61e9a7af1333b68700c5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 12:47:17 +0800 Subject: [PATCH 154/354] Fix recursive mkdir at write_manpage_file --- src/clitheme/_generator/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 4a89c31..c8aad4a 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -74,10 +74,9 @@ def write_manpage_file(file_path: list[str], content: str, line_number_debug: in for subdir in file_path[:-1]: parent_path+=subdir+"/" # create the parent directory - if not os.path.isdir(path): - try: os.makedirs(parent_path) - except NotADirectoryError: - handle_error(fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) + try: os.makedirs(parent_path, exist_ok=True) + except NotADirectoryError: + handle_error(fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) # write the compressed and original version of the file full_path=parent_path+"/"+file_path[-1] if os.path.isfile(full_path): -- Gitee From c66b3c82b63bde8f21308a5c63df7274eada95e2 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 13:58:01 +0800 Subject: [PATCH 155/354] First version of clitheme-man --- pyproject.toml | 3 ++- src/clitheme/man.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100755 src/clitheme/man.py diff --git a/pyproject.toml b/pyproject.toml index 59c9351..8129ed7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,8 @@ classifiers = [ [project.scripts] clitheme = "clitheme:cli.script_main" -clitheme-exec= "clitheme:exec.script_main" +clitheme-exec = "clitheme:exec.script_main" +clitheme-man = "clitheme:man.script_main" [project.urls] Repository = "https://gitee.com/swiftycode/clitheme" diff --git a/src/clitheme/man.py b/src/clitheme/man.py new file mode 100755 index 0000000..898c339 --- /dev/null +++ b/src/clitheme/man.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +import sys +import os +import subprocess +import shutil +try: from . import _globalvar +except ImportError: import _globalvar + +def main(args: list[str]): + if os.name=="nt": + print("Windows platform not supported") + return 1 + # check if "man" exists on system + man_executable: str=shutil.which("man") # type: ignore + if man_executable==None: + print("Error: \"man\" is not installed on this system") + return 1 + env=os.environ + # check if theme is set + if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.generator_manpage_pathname}"): + print("Warning: no theme set or theme does not contain manpages") + # set MANPATH + env['MANPATH']=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_manpage_pathname+":"+(os.environ['MANPATH'] if 'MANPATH' in os.environ else '') + # invoke man + results=subprocess.run([man_executable]+args[1:], env=env) + return results.returncode + +def script_main(): # for script + return main(sys.argv) +if __name__=="__main__": + exit(main(sys.argv)) \ No newline at end of file -- Gitee From 573aabbba94863f38976e41bd8df01e770eb1e52 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 15:03:24 +0800 Subject: [PATCH 156/354] Add man-db and sqlite3 requirement to build files --- PKGBUILD | 2 +- debian/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index f510599..e1f9bd4 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,7 +6,7 @@ pkgdesc="A text theming library for command line applications" arch=('any') url="https://gitee.com/swiftycode/clitheme" license=('GPL3') -depends=('python>=3.7' 'sqlite>=3') +depends=('python>=3.7' 'sqlite>=3' 'man-db') makedepends=('git' 'python-setuptools' 'python-build' 'python-installer' 'python-wheel' 'gzip') checkdepends=() optdepends=() diff --git a/debian/control b/debian/control index 91ba9d9..5ae09c6 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ Vcs-Git: https://gitee.com/swiftycode/clitheme.git Package: clitheme Architecture: all Multi-Arch: foreign -Depends: ${misc:Depends}, python3 (> 3.7) +Depends: ${misc:Depends}, python3 (> 3.7), man-db, sqlite3 Description: Application framework for text theming clitheme allows users to customize the output of supported programs, such as multi-language support or mimicking your favorite cartoon character. It has an -- Gitee From e09e68700805dab1ee1ecc7a557b86a6246221ae Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 15:04:03 +0800 Subject: [PATCH 157/354] No longer allow repeated patterns with exactcmdmatch --- src/clitheme/_generator/db_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 6d7c48f..2d22de8 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -69,9 +69,9 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma # 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)) for cmd in cmdlist: - # remove any existing values with the same match_pattern and effective_command and command_match_strictness(if ==2) + # remove any existing values with the same match_pattern and effective_command strictness_condition="" - if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" + # if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=?" match_params=(match_pattern, cmd, effective_locale, stdout_stderr_matchoption) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: -- Gitee From 5249ac9f46ea452a39e89931f1bf723575c17e55 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 21:09:39 +0800 Subject: [PATCH 158/354] Add clitheme-exec.1 manpage and update clitheme.1 --- PKGBUILD | 3 +++ debian/clitheme.manpages | 3 ++- docs/clitheme-exec.1 | 42 +++++++++++++++++++++++++++++++++++++++ docs/clitheme.1 | 43 ++++++++++++---------------------------- 4 files changed, 60 insertions(+), 31 deletions(-) create mode 100644 docs/clitheme-exec.1 diff --git a/PKGBUILD b/PKGBUILD index e1f9bd4..ec004a8 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -44,6 +44,8 @@ check() { echo "Ensuring manpage files (in docs directory) exist:" echo -n "docs/clitheme.1 ..." test ! -f docs/clitheme.1 && echo "Error" && return 1 + echo -n "docs/clitheme-exec.1 ..." + test ! -f docs/clitheme-exec.1 && echo "Error" && return 1 echo "OK" } @@ -53,4 +55,5 @@ package() { # install manpage mkdir -p $pkgdir/usr/share/man/man1 gzip -c docs/clitheme.1 > $pkgdir/usr/share/man/man1/clitheme.1.gz + gzip -c docs/clitheme-exec.1 > $pkgdir/usr/share/man/man1/clitheme-exec.1.gz } diff --git a/debian/clitheme.manpages b/debian/clitheme.manpages index 906208a..1e768c6 100644 --- a/debian/clitheme.manpages +++ b/debian/clitheme.manpages @@ -1 +1,2 @@ -docs/clitheme.1 \ No newline at end of file +docs/clitheme.1 +docs/clitheme-exec.1 \ No newline at end of file diff --git a/docs/clitheme-exec.1 b/docs/clitheme-exec.1 new file mode 100644 index 0000000..aed3cc7 --- /dev/null +++ b/docs/clitheme-exec.1 @@ -0,0 +1,42 @@ +.TH clitheme-exec 1 2024-05-07 +.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 +.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 +.TP +.B --debug +Display an indicator at the beginning of each line of output. The indicator contains information about stdout/stderr and whether if substitution happened. +.P +.RS 14 +- \fIo>\fR: stdout output + +- \fIe>\fR: stderr output +.RE +.TP +.B --debug-color +Applies color on the output contents; used to determine whether output is stdout or stderr. + +For stdout, yellow color is applied. For stderr, red color is applied. +.TP +.B --debug-newlines +For output that does not end on a newline, display the output ending with newlines. +.TP +.B --debug-showchars +Display various control characters in plain text. The following characters will be displayed as its code name: +.P +.RS 14 +- ANSI escape character (ESC) + +- Carriage return (\\r) + +- Newline character (\\n) + +- Backspace character (\\x08) + +- Bell character (\\x07) +.RE +.SH SEE ALSO +\fIclitheme(1)\fR \ No newline at end of file diff --git a/docs/clitheme.1 b/docs/clitheme.1 index d9614b2..11d2b37 100644 --- a/docs/clitheme.1 +++ b/docs/clitheme.1 @@ -1,57 +1,40 @@ -.TH clitheme 1 2024-01-20 +.TH clitheme 1 2024-05-07 .SH NAME clitheme \- frontend to customize output of applications .SH SYNOPSIS .B clitheme [COMMAND] [OPTIONS] .SH DESCRIPTION -clitheme 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. +\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 -.P +.TP .B apply-theme [themedef-file(s)] [--overlay] [--preserve-temp] -.RS 7 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. -.RE -.P +.TP .B get-current-theme-info -.RS 7 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. -.RE -.P +.TP .B unset-current-theme -.RS 7 Removes the current theme data from the system. Supported applications will immediately stop using the defined values after this operation. -.RE -.P +.TP .B generate-data [themedef-file(s)] [--overlay] -.RS 7 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. -.RE -.P +.TP .B --help -.RS 7 Outputs a short help message consisting of available commands. -.RE -.P +.TP .B --version -.RS 7 Outputs the current version of the program. -.RE .SH SEE ALSO -For more information, please see the -.UR https://gitee.com/swiftycode/clitheme -project homepage -.UE -and the -.UR https://gitee.com/swiftycode/clitheme/wikis -project wiki: -.UE +For more information, please see the project homepage and the project wiki: .P -https://gitee.com/swiftycode/clitheme +.I https://gitee.com/swiftycode/clitheme .P -https://gitee.com/swiftycode/clitheme/wikis \ No newline at end of file +.I https://gitee.com/swiftycode/clitheme/wikis/pages +or +.I https://gitee.com/swiftycode/clitheme-wiki-repo \ No newline at end of file -- Gitee From 6163c9cdd039bbf52b694026c1129b01973476c1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 21:28:42 +0800 Subject: [PATCH 159/354] Add clitheme-man.1 manpage --- PKGBUILD | 3 +++ debian/clitheme.manpages | 3 ++- docs/clitheme-man.1 | 13 +++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docs/clitheme-man.1 diff --git a/PKGBUILD b/PKGBUILD index ec004a8..b772a93 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -46,6 +46,8 @@ check() { test ! -f docs/clitheme.1 && echo "Error" && return 1 echo -n "docs/clitheme-exec.1 ..." test ! -f docs/clitheme-exec.1 && echo "Error" && return 1 + echo -n "docs/clitheme-man.1 ..." + test ! -f docs/clitheme-man.1 && echo "Error" && return 1 echo "OK" } @@ -56,4 +58,5 @@ package() { mkdir -p $pkgdir/usr/share/man/man1 gzip -c docs/clitheme.1 > $pkgdir/usr/share/man/man1/clitheme.1.gz gzip -c docs/clitheme-exec.1 > $pkgdir/usr/share/man/man1/clitheme-exec.1.gz + gzip -c docs/clitheme-man.1 > $pkgdir/usr/share/man/man1/clitheme-man.1.gz } diff --git a/debian/clitheme.manpages b/debian/clitheme.manpages index 1e768c6..16d56f2 100644 --- a/debian/clitheme.manpages +++ b/debian/clitheme.manpages @@ -1,2 +1,3 @@ docs/clitheme.1 -docs/clitheme-exec.1 \ No newline at end of file +docs/clitheme-exec.1 +docs/clitheme-man.1 \ No newline at end of file diff --git a/docs/clitheme-man.1 b/docs/clitheme-man.1 new file mode 100644 index 0000000..3b20930 --- /dev/null +++ b/docs/clitheme-man.1 @@ -0,0 +1,13 @@ +.TH clitheme-man 1 2024-05-07 +.SH NAME +clitheme\-man \- access manual pages in the current theme +.SH SYNOPSIS +.B clitheme-man [OPTIONS] +.SH DESCRIPTION +\fIclitheme-man\fR is a wrapper for \fIman(1)\fR that accesses the manual pages defined in a theme definition file. The current theme definition on the system is controlled through \fIclitheme(1)\fR. +.P +\fIclitheme-man\fR is designed to be used the same way as \fIman(1)\fR; the same arguments and options will work on \fIclitheme-man\fR. +.SH OPTIONS +For a list of options you can use, please see \fIman(1)\fR. +.SH SEE ALSO +\fIclitheme(1)\fR, \fIman(1)\fR \ No newline at end of file -- Gitee From 9588c36145638ac538d7e43f4639f02bfd05e6d9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 21:39:23 +0800 Subject: [PATCH 160/354] Update version (v2.0-dev20240507) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index b772a93..e4c9b88 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240506' +pkgver='2.0_dev20240507' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index d28f278..a2f49a8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240506-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240507-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 06 May 2024 21:51:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 07 May 2024 21:38:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index f2ba416..97fe82e 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240506" +__version__="2.0-dev20240507" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240506" +version_main="2.0_dev20240507" version_buildnumber=1 \ No newline at end of file -- Gitee From 2ad6557ad4b4be8c3046746ab2fd9e760ec9129d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 7 May 2024 21:45:34 +0800 Subject: [PATCH 161/354] Fix small error in PKGBUILD output --- PKGBUILD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PKGBUILD b/PKGBUILD index e4c9b88..37031b1 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -44,8 +44,10 @@ check() { echo "Ensuring manpage files (in docs directory) exist:" echo -n "docs/clitheme.1 ..." test ! -f docs/clitheme.1 && echo "Error" && return 1 + echo "OK" echo -n "docs/clitheme-exec.1 ..." test ! -f docs/clitheme-exec.1 && echo "Error" && return 1 + echo "OK" echo -n "docs/clitheme-man.1 ..." test ! -f docs/clitheme-man.1 && echo "Error" && return 1 echo "OK" -- Gitee From e675c27135b8887f24733cd79ed11358bc82210e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 9 May 2024 21:48:23 +0800 Subject: [PATCH 162/354] Add "include_file" clause to manpage_section; - Fix variable content being incorrectly parsed in "locales" and "supported_apps" --- src/clitheme/_generator/__init__.py | 35 ++++++++++++++++++++++------- src/clitheme/cli.py | 8 ++++--- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index c8aad4a..fa5acb9 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -70,12 +70,10 @@ def write_infofile_newlines(path: str, filename: str, content_phrases: list[str] def write_manpage_file(file_path: list[str], content: str, line_number_debug: int): parent_path=path+"/"+_globalvar.generator_manpage_pathname+"/" - if len(file_path)>1: - for subdir in file_path[:-1]: - parent_path+=subdir+"/" + parent_path+=os.path.dirname(_globalvar.splitarray_to_string(file_path).replace(" ","/")) # create the parent directory try: os.makedirs(parent_path, exist_ok=True) - except NotADirectoryError: + except (FileExistsError, NotADirectoryError): handle_error(fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) # write the compressed and original version of the file full_path=parent_path+"/"+file_path[-1] @@ -94,7 +92,7 @@ def generate_custom_path(): for x in range(8): path+=random.choice(string.ascii_letters) -def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1"): +def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1", filename: str=""): # make directories if custom_path_gen: generate_custom_path() @@ -395,9 +393,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content,lineindex+1,phrases[0]) # e.g. [...]/theme-info/1/clithemeinfo_name elif phrases[0]=="locales" or phrases[0]=="supported_apps": check_enough_args(phrases, 2) - content=phrases[1:] - for x in range(len(content)): - content[x]=subst_variable_content(content[x]) + content=subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() write_infofile_newlines( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ _globalvar.generator_info_v2filename.format(info=phrases[0]),\ @@ -581,6 +577,29 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_error(fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) content=handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/file_content]") write_manpage_file(filepath, content, lineindex+1) + elif phrases[0]=="include_file": + check_enough_args(phrases, 2) + filepath=subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() + if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: + handle_error(fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) + # get content + # if no filename provided, use current working directory as parent path; else, use the directory the file is in as the parent path + parent_dir="" + if filename.strip()!="": + parent_dir+=os.path.dirname(filename) + file_dir=parent_dir+"/"+_globalvar.splitarray_to_string(filepath).replace(" ","/") + filecontent: str + try: filecontent=open(file_dir, 'r', encoding="utf-8").read() + except: handle_error(fd.feof("include-file-read-error", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(lineindex+1), filepath=file_dir, error_msg=sys.exc_info()[1])) + # expect "as" clause right on next line + lineindex+=1 + if lineindex0 and lines_data[lineindex].split()[0]=="as": + target_file=subst_variable_content(_globalvar.splitarray_to_string(lines_data[lineindex].split()[1:])).split() + if _globalvar.sanity_check(_globalvar.splitarray_to_string(target_file))==False: + handle_error(fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) + write_manpage_file(target_file, filecontent, lineindex+1) + else: + handle_error(fd.feof("include-file-missing-phrase-err", "Missing \"as \" phrase on next line of line {num}", num=str(lineindex+1-1))) elif phrases[0]=="set_options": check_enough_args(phrases, 2) handle_set_global_options(phrases[1:]) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 82e5d41..819bb32 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -30,7 +30,7 @@ frontend.global_subsections="cli" _globalvar.handle_set_themedef(frontend, "cli") -def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, generate_only=False): +def apply_theme(file_contents: list[str], overlay: bool, filenames: list[str]=[], preserve_temp=False, generate_only=False): """ Apply the theme using the provided definition file contents in a list[str] object. @@ -38,6 +38,8 @@ def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, ge - Set preserve_temp=True to preserve the temp directory (debugging purposes) - Set generate_only=True to generate the data hierarchy only (and not apply the theme) """ + 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 overlay: print(f.reof("overlay-msg", "Overlay specified")) print(f.reof("generating-data", "==> Generating data...")) @@ -66,7 +68,7 @@ def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, ge # Generate data hierarchy, erase current data, copy it to data path try: _generator.silence_warn=False - _generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index)) + _generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index), filename=filenames[i] if len(filenames)>0 else "") generate_path=False # Don't generate another temp folder after first one index+=1 except SyntaxError: @@ -256,7 +258,7 @@ def main(cli_args): print(fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ index=str(i+1), message=str(sys.exc_info()[1]))) return 1 - return apply_theme(content_list, overlay=overlay, preserve_temp=preserve_temp, generate_only=generate_only) + 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() -- Gitee From fc860bfabf1f9c80afdc1bcae55f38210eba9629 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 9 May 2024 22:07:02 +0800 Subject: [PATCH 163/354] Add new string definitions to generator-strings --- src/clitheme/strings/generator-strings.clithemedef.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 115b758..a01036b 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -61,6 +61,15 @@ in_subsection generator [entry] manpage-subdir-file-conflict-err locale:zh_CN 第{num}行:子路径和文件有冲突;请检查之前的定义 [/entry] + [entry] include-file-read-err + [locale] zh_CN + 第{num}行:无法读取文件"{filepath}": + {error_msg} + [/locale] + [/entry] + [entry] include-file-missing-phrase-err + locale:zh_CN 第{num}行:在下一行缺少"as <文件名>"语句 + [/entry] # warning strings [entry] warning-str locale:zh_CN 警告:{msg} -- Gitee From 8858a2336616eb2e2e8706836624e2dc744595a1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 9 May 2024 22:12:55 +0800 Subject: [PATCH 164/354] Fix variable conflict issue in [description] --- src/clitheme/_generator/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index fa5acb9..c5e3915 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -401,19 +401,19 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info 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]": check_extra_args(phrases, 1, use_exact_count=True) # handle block input - content=""; filename="" + 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]": content=handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase=endphrase) - filename=_globalvar.generator_info_filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) + file_name=_globalvar.generator_info_filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) else: content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase, disable_substesc=True) - filename=_globalvar.generator_info_v2filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) + file_name=_globalvar.generator_info_v2filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) content=subst_variable_content(content) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ - filename,\ + file_name,\ content,lineindex+1,re.sub(r'_block$','',phrases[0])) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 elif phrases[0]=="set_options": check_enough_args(phrases, 2) @@ -587,7 +587,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info parent_dir="" if filename.strip()!="": parent_dir+=os.path.dirname(filename) - file_dir=parent_dir+"/"+_globalvar.splitarray_to_string(filepath).replace(" ","/") + file_dir=parent_dir+("/" if parent_dir!="" else "")+_globalvar.splitarray_to_string(filepath).replace(" ","/") filecontent: str try: filecontent=open(file_dir, 'r', encoding="utf-8").read() except: handle_error(fd.feof("include-file-read-error", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(lineindex+1), filepath=file_dir, error_msg=sys.exc_info()[1])) -- Gitee From 1996df00380e453295387df06950407ee2b83619 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 9 May 2024 22:17:16 +0800 Subject: [PATCH 165/354] Add a clithemedef file in "docs" folder --- docs/docs.clithemedef.txt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/docs.clithemedef.txt diff --git a/docs/docs.clithemedef.txt b/docs/docs.clithemedef.txt new file mode 100644 index 0000000..11d9b1b --- /dev/null +++ b/docs/docs.clithemedef.txt @@ -0,0 +1,20 @@ +{header_section} + name clitheme manual pages + version 2.0 + [description] + This file contains manual pages for clitheme. + Apply this theme to install the manual pages in the "docs" directory. + After that, use "clitheme-man" to access them. + [/description] +{/header_section} + +{manpage_section} + set_options substvar + setvar:name clitheme + include_file {{name}}.1 + as man1 {{name}}.1 + include_file {{name}}-exec.1 + as man1 {{name}}-exec.1 + include_file {{name}}-man.1 + as man1 {{name}}-man.1 +{/manpage_section} \ No newline at end of file -- Gitee From ad0ad1ba922148861d380e20140d29b77ea6fcb4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 9 May 2024 22:22:02 +0800 Subject: [PATCH 166/354] Update version (v2.0-dev20240509) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 37031b1..908e6d4 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240507' +pkgver='2.0_dev20240509' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index a2f49a8..92cca8e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240507-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240509-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Tue, 07 May 2024 21:38:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 09 May 2024 22:20:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 97fe82e..556548d 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240507" +__version__="2.0-dev20240509" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240507" +version_main="2.0_dev20240509" version_buildnumber=1 \ No newline at end of file -- Gitee From 19f080b05bd98e431d02e8625d8c699a564c9aed Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 10 May 2024 21:58:32 +0800 Subject: [PATCH 167/354] Change syntax of cmdmatch and stdonly options; - New "normalcmdmatch" option - No longer supports setting the options to false "no<...>" - Rename to "subststdoutonly" and "subststderronly" - Add "substall"; used to reset previous two options --- src/clitheme/_generator/__init__.py | 42 ++++++++++++++--------------- src/db_interface_tests.py | 8 ++++-- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index c5e3915..985b68d 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -129,12 +129,12 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # value options: options requiring an integer value value_options=["leadtabindents", "leadspaces"] # on/off options (use no<...> to disable) - bool_options=["substesc", "substvar", "strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "endmatchhere"] + bool_options=["substesc", "substvar", "endmatchhere"] + # only one of these options can be set to true at the same time (specific to groups) + switch_options=[["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"]] # Disable these options for now (BETA) - # bool_options+=["stdout_only", "stderr_only"] + # switch_options+=[["subststdoutonly", "subststderronly", "substall"]] - # only one of these options can be set to true at the same time (specific to groups) - bool_options_unique_groups=[["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"], ["stdout_only", "stderr_only"]] final_options={} if merge_global_options!=0: final_options=copy.copy(global_options if merge_global_options==1 else really_really_global_options) if len(options_data)==0: return final_options # return either empty data or pre-existing global options @@ -143,10 +143,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info option_name_preserve_no=re.sub(r"^(?P.+?)(:.+)*$", r"\g", each_option) if allowed_options!=None and option_name not in allowed_options: handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option_name)) - if option_name in value_options: - # must not begin with no - if option_name_preserve_no.startswith("no"): - handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) + if option_name_preserve_no in value_options: # must not begin with "no" # get value results=re.search(r"^(?P.+?):(?P.+)+$", each_option) value: int @@ -158,20 +155,21 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # set option final_options[option_name]=value elif option_name in bool_options: - # process unique bool options - if not option_name_preserve_no.startswith("no"): - for bool_options_unique in bool_options_unique_groups: - if option_name_preserve_no in bool_options_unique: - # can't be specified at the same time - for opt in options_data: - if opt!=option_name and opt in bool_options_unique: - handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name, option2=opt)) - # set all other options to false - for opt in bool_options_unique: final_options[opt]=False # if starts with no, set to false; else, set to true final_options[option_name]=not option_name_preserve_no.startswith("no") else: - handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) + for option_group in switch_options: + if option_name_preserve_no in option_group: + for opt in options_data: + if opt!=option_name_preserve_no and opt in option_group: + handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name_preserve_no, option2=opt)) + # set all other options to false + for opt in option_group: final_options[opt]=False + # set the option + final_options[option_name_preserve_no]=True + break + else: # executed when no break occurs + handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) return final_options def handle_set_global_options(options_data: list[str], really_really_global: bool=False): # set options globally @@ -345,13 +343,13 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: substrules_entries.append((entry_name, content, None if this_locale=="default" else this_locale)); substrules_entries_linenumber.append(lineindex+1) elif phrases[0]==end_phrase: if not is_substrules: check_extra_args(phrases, 1, use_exact_count=True) - got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere", "stdout_only", "stderr_only"]) + got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere", "subststdoutonly", "subststderronly"]) for option in got_options: if option=="endmatchhere" and got_options['endmatchhere']==True: substrules_endmatchhere=True - elif option=="stdout_only" and got_options['stdout_only']==True: + elif option=="subststdoutonly" and got_options['subststdoutonly']==True: substrules_stdout_stderr_option=1 - elif option=="stderr_only" and got_options['stderr_only']==True: + elif option=="subststderronly" and got_options['subststderronly']==True: substrules_stdout_stderr_option=2 break else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index a63e684..dac18eb 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -1,5 +1,6 @@ from clitheme._generator import db_interface from clitheme import _generator, _globalvar +import shutil # sample input for testing sample_inputs=[("rm: missing operand", "rm"), @@ -85,7 +86,7 @@ substrules_file=r""" locale:zh_CN \g 说:缺少操作参数!ಥ_ಥ [/substitute_regex] - set_options noexactcmdmatch + set_options normalcmdmatch filter_command example_app [substitute_string] example_app: locale:default o(≧v≦)o example_app says: @@ -102,7 +103,7 @@ substrules_file=r""" locale:default using list options! (⊙ω⊙) locale:zh_CN 正在使用列表选项!(⊙ω⊙) [/substitute_string] - set_options nosmartcmdmatch + set_options normalcmdmatch {/substrules_section} """ @@ -113,3 +114,6 @@ db_interface.connection=db_interface.sqlite3.connect(_generator.path+"/"+_global print("Successfully recorded data\nTesting sample outputs: ") for inp in sample_inputs: print(db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8')) + +try: shutil.rmtree(_generator.path) +except: pass \ No newline at end of file -- Gitee From 786e86a112da3b7ae77394dd3475700a12eb5641 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 10 May 2024 22:13:48 +0800 Subject: [PATCH 168/354] Add indents to include_file clause; New syntax convention :) --- docs/docs.clithemedef.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs.clithemedef.txt b/docs/docs.clithemedef.txt index 11d9b1b..43b374b 100644 --- a/docs/docs.clithemedef.txt +++ b/docs/docs.clithemedef.txt @@ -12,9 +12,9 @@ set_options substvar setvar:name clitheme include_file {{name}}.1 - as man1 {{name}}.1 + as man1 {{name}}.1 include_file {{name}}-exec.1 - as man1 {{name}}-exec.1 + as man1 {{name}}-exec.1 include_file {{name}}-man.1 - as man1 {{name}}-man.1 + as man1 {{name}}-man.1 {/manpage_section} \ No newline at end of file -- Gitee From c48383b1deb6967ee49ba252b68d55f5867c210b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 10 May 2024 22:27:06 +0800 Subject: [PATCH 169/354] Allow variable substitution on "set_options" and all header entries; - Fix small sanity check issue with entry name --- src/clitheme/_generator/__init__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 985b68d..df78be5 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -368,7 +368,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # process header and main sections here if first_phrase=="set_options": check_enough_args(lines_data[lineindex].split(), 2) - handle_set_global_options(lines_data[lineindex].split()[1:], really_really_global=True) + handle_set_global_options(subst_variable_content(_globalvar.splitarray_to_string(lines_data[lineindex].split()[1:])).split(), really_really_global=True) elif first_phrase.startswith("setvar:"): check_enough_args(lines_data[lineindex].split(), 2) handle_set_variable(lines_data[lineindex], really_really_global=True) @@ -406,16 +406,15 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content=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: - content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase, disable_substesc=True) + content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase) file_name=_globalvar.generator_info_v2filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) - content=subst_variable_content(content) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ file_name,\ content,lineindex+1,re.sub(r'_block$','',phrases[0])) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 elif phrases[0]=="set_options": check_enough_args(phrases, 2) - handle_set_global_options(phrases[1:]) + handle_set_global_options(subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) elif phrases[0].startswith("setvar:"): check_enough_args(phrases, 2) handle_set_variable(lines_data[lineindex]) @@ -462,17 +461,17 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif phrases[0]=="entry" or phrases[0]=="[entry]": check_enough_args(phrases, 2) entry_name=_globalvar.extract_content(lines_data[lineindex]) + entry_name=subst_variable_content(entry_name) # Prevent leading . & prevent /,\ in entry name if _globalvar.sanity_check(entry_name)==False: handle_error(fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) - entry_name=subst_variable_content(entry_name) if subsection!="": entry_name=subsection+" "+entry_name if domainapp!="": entry_name=domainapp+" "+entry_name recursive_mkdir(datapath, entry_name, lineindex+1) handle_entry(entry_name, end_phrase="[/entry]" if phrases[0]=="[entry]" else "end_entry") elif phrases[0]=="set_options": check_enough_args(phrases, 2) - handle_set_global_options(phrases[1:]) + handle_set_global_options(subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) elif phrases[0].startswith("setvar:"): check_enough_args(phrases, 2) handle_set_variable(lines_data[lineindex]) @@ -549,7 +548,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_entry(match_pattern, end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) elif phrases[0]=="set_options": check_enough_args(phrases, 2) - handle_set_global_options(phrases[1:]) + handle_set_global_options(subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) elif phrases[0].startswith("setvar:"): check_enough_args(phrases, 2) handle_set_variable(lines_data[lineindex]) @@ -600,7 +599,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info handle_error(fd.feof("include-file-missing-phrase-err", "Missing \"as \" phrase on next line of line {num}", num=str(lineindex+1-1))) elif phrases[0]=="set_options": check_enough_args(phrases, 2) - handle_set_global_options(phrases[1:]) + handle_set_global_options(subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) elif phrases[0].startswith("setvar:"): check_enough_args(phrases, 2) handle_set_variable(lines_data[lineindex]) -- Gitee From 1497107477d562b3843624fa2b10b60059ca5b05 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 10 May 2024 22:43:54 +0800 Subject: [PATCH 170/354] Use frontend.globals in clitheme-exec --- src/clitheme/exec/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index dee329a..7357e1e 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -15,7 +15,9 @@ except ImportError: from _generator import db_interface _globalvar.handle_set_themedef(frontend, "clitheme-exec") -fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") +frontend.global_domain="swiftycode" +frontend.global_appname="clitheme" +fd=frontend.FetchDescriptor(subsections="exec") def check_regenerate_db() -> bool: try: db_interface.connect_db() @@ -56,7 +58,7 @@ def check_regenerate_db() -> bool: return True def handle_help_message(full_help: bool=False): - fd2=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec help-message") + fd2=frontend.FetchDescriptor(subsections="exec help-message") print(fd2.reof("usage-str", "Usage:")) print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--debug-showchars] [command]") if not full_help: return -- Gitee From ca1d07f2428989cb79e8d399f979441b40a512c5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 10 May 2024 22:47:26 +0800 Subject: [PATCH 171/354] Make "filename" argument in apply_theme function mandatory --- src/clitheme/cli.py | 4 ++-- src/clitheme/exec/__init__.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 819bb32..cf0deaf 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -30,9 +30,9 @@ frontend.global_subsections="cli" _globalvar.handle_set_themedef(frontend, "cli") -def apply_theme(file_contents: list[str], overlay: bool, filenames: list[str]=[], preserve_temp=False, generate_only=False): +def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, preserve_temp=False, generate_only=False): """ - Apply the theme using the provided definition file contents in a list[str] object. + Apply the theme using the provided definition file contents and file pathnames in a list[str] object. - Set overlay=True to overlay the theme on top of existing theme[s] - Set preserve_temp=True to preserve the temp directory (debugging purposes) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 7357e1e..b117473 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -34,14 +34,16 @@ def check_regenerate_db() -> bool: if lsdir_num<1: raise Exception file_contents=[] + paths=[] for pathname in lsdir_result: target_path=search_path+"/"+pathname if not os.path.isdir(target_path): continue content=open(target_path+"/file_content", encoding="utf-8").read() file_contents.append(content) + paths.append(target_path+"/file_content") cli_msg=io.StringIO() sys.stdout=cli_msg - if not cli.apply_theme(file_contents, overlay=False, generate_only=True, preserve_temp=True)==0: + 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") sys.stdout=sys.__stdout__ os.remove(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) -- Gitee From 5ac673fbfb6146d0bcbee2a6307ff976ba923a83 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 10 May 2024 23:03:15 +0800 Subject: [PATCH 172/354] Write manpage files in theme-info for db migration feature to work successfully Specifically for the "include_file" clause --- src/clitheme/_generator/__init__.py | 8 +++++--- src/clitheme/exec/__init__.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index df78be5..c0644fa 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -68,9 +68,9 @@ def write_infofile_newlines(path: str, filename: str, content_phrases: list[str] for line in content_phrases: f.write(line+"\n") -def write_manpage_file(file_path: list[str], content: str, line_number_debug: int): - parent_path=path+"/"+_globalvar.generator_manpage_pathname+"/" - parent_path+=os.path.dirname(_globalvar.splitarray_to_string(file_path).replace(" ","/")) +def write_manpage_file(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 path+"/"+_globalvar.generator_manpage_pathname + parent_path+='/'+os.path.dirname(_globalvar.splitarray_to_string(file_path).replace(" ","/")) # create the parent directory try: os.makedirs(parent_path, exist_ok=True) except (FileExistsError, NotADirectoryError): @@ -588,6 +588,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info filecontent: str try: filecontent=open(file_dir, 'r', encoding="utf-8").read() except: handle_error(fd.feof("include-file-read-error", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(lineindex+1), filepath=file_dir, error_msg=sys.exc_info()[1])) + # write manpage files in theme-info for db migration feature to work successfully + write_manpage_file(filepath, filecontent, lineindex+1, custom_parent_path=path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name+"/manpage_data") # expect "as" clause right on next line lineindex+=1 if lineindex0 and lines_data[lineindex].split()[0]=="as": diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index b117473..758b72f 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -40,7 +40,7 @@ def check_regenerate_db() -> bool: if not os.path.isdir(target_path): continue content=open(target_path+"/file_content", encoding="utf-8").read() file_contents.append(content) - paths.append(target_path+"/file_content") + paths.append(target_path+"/manpage_data/file_content") # small hack/workaround 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: -- Gitee From 6883d1ece5a193c938eaaeb5272dabdff2803bca Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 11 May 2024 10:30:57 +0800 Subject: [PATCH 173/354] Make internal functions as private --- pyproject.toml | 6 +++--- src/clitheme/cli.py | 20 ++++++++++---------- src/clitheme/exec/__init__.py | 20 ++++++++++---------- src/clitheme/exec/output_handler_posix.py | 6 +++--- src/clitheme/man.py | 2 +- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8129ed7..13348d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,9 @@ classifiers = [ ] [project.scripts] -clitheme = "clitheme:cli.script_main" -clitheme-exec = "clitheme:exec.script_main" -clitheme-man = "clitheme:man.script_main" +clitheme = "clitheme:cli._script_main" +clitheme-exec = "clitheme:exec._script_main" +clitheme-man = "clitheme:man._script_main" [project.urls] Repository = "https://gitee.com/swiftycode/clitheme" diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index cf0deaf..6d2223d 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -187,9 +187,9 @@ def get_current_theme_info(): print(f.feof("list-item", "• {content}", content=app.strip())) return 0 -def is_option(arg): +def _is_option(arg): return arg.strip()[0:1]=="-" -def handle_usage_error(message, cli_args_first): +def _handle_usage_error(message, cli_args_first): f=frontend.FetchDescriptor() print(message) print(f.feof("help-usage-prompt", "Run \"{clitheme} --help\" for usage information", clitheme=cli_args_first)) @@ -204,18 +204,18 @@ def main(cli_args): arg_first="clitheme" # controls what appears as the command name in messages if len(cli_args)<=1: # no arguments passed print(usage_description.format(arg_first)) - handle_usage_error(f.reof("no-command", "Error: no command or option specified"), arg_first) + _handle_usage_error(f.reof("no-command", "Error: no command or option specified"), arg_first) return 1 def check_enough_args(count: int, exclude_options: bool=True): c=0 for arg in cli_args: - if not exclude_options or not is_option(arg): c+=1 + 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)) + 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": check_enough_args(3) @@ -224,10 +224,10 @@ def main(cli_args): overlay=False preserve_temp=False for arg in cli_args[2:]: - if is_option(arg): + 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=arg), arg_first) + else: return _handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=arg), arg_first) else: paths.append(arg) fi=frontend.FetchDescriptor(subsections="cli apply-theme") @@ -273,9 +273,9 @@ def main(cli_args): check_extra_args(2) print(usage_description.format(arg_first)) else: - return handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=cli_args[1]), arg_first) + return _handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=cli_args[1]), arg_first) return 0 -def script_main(): # for script +def _script_main(): # for script return main(sys.argv) if __name__=="__main__": exit(main(sys.argv)) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 758b72f..e1ef672 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -19,7 +19,7 @@ frontend.global_domain="swiftycode" frontend.global_appname="clitheme" fd=frontend.FetchDescriptor(subsections="exec") -def check_regenerate_db() -> bool: +def _check_regenerate_db() -> bool: try: db_interface.connect_db() except db_interface.need_db_regenerate: print(fd.reof("substrules-migrate-msg", "Migrating substrules database...")) @@ -59,7 +59,7 @@ def check_regenerate_db() -> bool: return False return True -def handle_help_message(full_help: bool=False): +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] [command]") @@ -70,7 +70,7 @@ def handle_help_message(full_help: bool=False): 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")) -def handle_error(message: str): +def _handle_error(message: str): print(message) print(fd.reof("help-usage-prompt", "Run \"clitheme-exec --help\" for usage information")) return 1 @@ -94,22 +94,22 @@ def main(arguments: list[str]): elif arg=="--help": showhelp=True else: - return handle_error(fd.feof("unknown-option-err", "Error: unknown option \"{phrase}\"", phrase=arg)) + return _handle_error(fd.feof("unknown-option-err", "Error: unknown option \"{phrase}\"", phrase=arg)) if len(arguments)<=1+argcount: if showhelp: - handle_help_message(full_help=True) + _handle_help_message(full_help=True) return 0 else: - handle_help_message() - handle_error(fd.reof("no-command-err", "Error: no command specified")) + _handle_help_message() + _handle_error(fd.reof("no-command-err", "Error: no command specified")) return 1 # check database if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) - if not check_regenerate_db(): return 1 + if not _check_regenerate_db(): return 1 # determine platform if os.name=="posix": - return output_handler_posix.handler_main(arguments[1+argcount:], debug_mode) + return output_handler_posix._handler_main(arguments[1+argcount:], debug_mode) elif os.name=="nt": print("Error: Windows platform is not currently supported") return 1 @@ -117,5 +117,5 @@ def main(arguments: list[str]): print("Error: Unsupported platform") return 1 return 0 -def script_main(): # for script +def _script_main(): # for script return main(sys.argv) \ No newline at end of file diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 001a89c..1dfa5c7 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -18,7 +18,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[bytes], debug_mode: list[str], is_stderr: bool=False, matched: bool=False) -> list[bytes]: +def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=False, matched: bool=False) -> list[bytes]: # debug_mode: newlines, showchars, color final_lines=[] for x in range(len(lines)): @@ -42,7 +42,7 @@ def process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fal final_lines.append(line) return final_lines -def handler_main(command: list[str], debug_mode: list[str]=[]): +def _handler_main(command: list[str], debug_mode: list[str]=[]): do_subst=True try: db_interface.connect_db() except FileNotFoundError: do_subst=False @@ -110,7 +110,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[]): # subst operation subst_line=copy.copy(line) if do_subst: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) - subst_line=process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line)[0] + subst_line=_process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line)[0] os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line) else: output_lines=[] # happens when no 'break' statement occurs except KeyboardInterrupt: diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 898c339..016592c 100755 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -26,7 +26,7 @@ def main(args: list[str]): results=subprocess.run([man_executable]+args[1:], env=env) return results.returncode -def script_main(): # for script +def _script_main(): # for script return main(sys.argv) if __name__=="__main__": exit(main(sys.argv)) \ No newline at end of file -- Gitee From f3f18eb8cd5e98cc52bfc8d80a6098318b362db3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 11 May 2024 11:03:13 +0800 Subject: [PATCH 174/354] Add and modify module and method descriptions --- src/clitheme/__init__.py | 2 +- src/clitheme/cli.py | 21 +++++++++++++++------ src/clitheme/exec/__init__.py | 11 ++++++++++- src/clitheme/frontend.py | 12 ++++++++---- src/clitheme/man.py | 12 ++++++++++++ 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/clitheme/__init__.py b/src/clitheme/__init__.py index 9629bb6..6b724b1 100644 --- a/src/clitheme/__init__.py +++ b/src/clitheme/__init__.py @@ -1 +1 @@ -__all__=["frontend", "cli"] \ No newline at end of file +__all__=["frontend", "cli", "man", "exec"] \ No newline at end of file diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 6d2223d..b68fcb9 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -1,7 +1,10 @@ #!/usr/bin/python3 """ -clitheme command line utility interface +Module used for the clitheme command line interface + +- You can access 'clitheme' by invoking this module directly: 'python3 -m clitheme' +- You can invoke individual commands in scripts using the functions in this module """ import os @@ -32,11 +35,13 @@ _globalvar.handle_set_themedef(frontend, "cli") def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, preserve_temp=False, generate_only=False): """ - Apply the theme using the provided definition file contents and file pathnames in a list[str] object. + Apply the theme using the provided definition file contents and file pathnames in a list[str] object. + + (Invokes 'clitheme apply-theme') - 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 (and not apply the theme) + - Set generate_only=True to generate the data hierarchy only (invokes 'clitheme generate-data' instead) """ if len(filenames)>0 and len(file_contents)!=len(filenames): # unlikely to happen raise ValueError("file_contents and filenames have different lengths") @@ -108,6 +113,8 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p def unset_current_theme(): """ Delete the current theme data hierarchy from the data path + + (Invokes 'clitheme unset-current-theme') """ f=frontend.FetchDescriptor(subsections="cli unset-current-theme") try: shutil.rmtree(_globalvar.clitheme_root_data_path) @@ -123,6 +130,8 @@ def unset_current_theme(): def get_current_theme_info(): """ Get the current theme info + + (Invokes 'clitheme get-current-theme-info') """ f=frontend.FetchDescriptor(subsections="cli get-current-theme-info") search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname @@ -194,11 +203,11 @@ def _handle_usage_error(message, cli_args_first): print(message) print(f.feof("help-usage-prompt", "Run \"{clitheme} --help\" for usage information", clitheme=cli_args_first)) return 1 -def main(cli_args): +def main(cli_args: list[str]): """ - Use this function for indirect invocation of the interface (e.g. from another function) + Use this function invoke 'clitheme' with command line arguments - Provide a list of command line arguments to this function through cli_args. + Note: the first item in the argument list must be a program name (e.g. ['clitheme', ]) """ f=frontend.FetchDescriptor() arg_first="clitheme" # controls what appears as the command name in messages diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index e1ef672..d649bc7 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -1,5 +1,8 @@ """ -Module used for clitheme-exec (should not be invoked directly) +Module used for clitheme-exec + +- You can access clitheme-exec by invoking this module directly: 'python3 -m clitheme.exec' +- You can also invoke clitheme-exec in scripts using the 'main' function """ import sys import os @@ -76,6 +79,12 @@ def _handle_error(message: str): return 1 def main(arguments: list[str]): + """ + Invoke clitheme-exec using the given command line arguments + + Note: the first item in the argument list must be the program name + (e.g. ['clitheme-exec', ] or ['example-app', ]) + """ # process debug mode arguments debug_mode=[] argcount=0 diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 69a12f1..2f788b3 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -1,5 +1,9 @@ """ -clitheme front-end interface for accessing entries +clitheme frontend interface for accessing entries + +- Create a FetchDescriptor instance and optionally pass information such as domain&app name and subsections +- Use the 'retrieve_entry_or_fallback' or 'reof' function in the instance to retrieve content of an entry definition +- Use the 'format_entry_or_fallback' or 'feof' function in the instance to retrieve and format content of entry definition using str.format """ import os,sys @@ -112,10 +116,10 @@ class FetchDescriptor(): """ Create a new instance of the object. - - Provide domain_name and app_name to automatically append them for retrieval functions. + - Provide domain_name and app_name to automatically append them for retrieval functions - Provide subsections to automatically append them after domain_name+app_name - Provide lang to override the automatically detected system locale information - - Set debug_mode=True to output underlying operations when retrieving entries. + - Set debug_mode=True to output underlying operations when retrieving entries (debug purposes only) - Set disable_lang=True to disable localization detection and use "default" entry for all retrieval operations """ # Leave domain and app names blank for global reference @@ -216,7 +220,7 @@ class FetchDescriptor(): def format_entry_or_fallback(self, entry_path: str, fallback_string: str, *args, **kwargs) -> str: """ - Attempt to retrieve and format the entry based on given entry path and arguments. + Attempt to retrieve and format the entry using str.format based on given entry path and arguments. If the entry does not exist or an error occurs while formatting the entry string, use the provided fallback string instead. """ # retrieve the entry diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 016592c..2ae17e8 100755 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -1,5 +1,11 @@ #!/usr/bin/python3 +""" +Module used for clitheme-man + +- You can access clitheme-man by invoking this module directly: 'python3 -m clitheme.man' +- You can also invoke clitheme-man in scripts using the 'main' function +""" import sys import os import subprocess @@ -8,6 +14,12 @@ try: from . import _globalvar except ImportError: import _globalvar def main(args: list[str]): + """ + Invoke clitheme-man using the given command line arguments + + Note: the first item in the argument list must be the program name + (e.g. ['clitheme-man', ] or ['example-app', ]) + """ if os.name=="nt": print("Windows platform not supported") return 1 -- Gitee From ddba6e4aef89078b65736749152ce31f89d0c319 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 11 May 2024 11:03:26 +0800 Subject: [PATCH 175/354] Add clitheme-exec and clitheme-man reference in "See also" --- docs/clitheme.1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/clitheme.1 b/docs/clitheme.1 index 11d2b37..a51aecd 100644 --- a/docs/clitheme.1 +++ b/docs/clitheme.1 @@ -31,6 +31,8 @@ Outputs a short help message consisting of available commands. .B --version Outputs the current version of the program. .SH SEE ALSO +\fIclitheme-exec(1)\fR, \fIclitheme-man(1)\fR + For more information, please see the project homepage and the project wiki: .P .I https://gitee.com/swiftycode/clitheme -- Gitee From 9c03984200f98269b82f66f2edfdc27ab47ce24b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 11 May 2024 11:41:29 +0800 Subject: [PATCH 176/354] Change help message of clitheme --- src/clitheme/cli.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index b68fcb9..b88378f 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -203,6 +203,28 @@ def _handle_usage_error(message, cli_args_first): print(message) print(f.feof("help-usage-prompt", "Run \"{clitheme} --help\" for usage information", clitheme=cli_args_first)) return 1 +arg_first="clitheme" # controls what appears as the command name in messages +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} get-current-theme-info +\t{0} unset-current-theme +\t{0} generate-data [themedef-file] [--overlay] +\t{0} --version +\t{0} --help""".format(arg_first) + ) + 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.").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")) + print("\t"+fd.reof("options-unset-current-theme", "unset-current-theme: Remove the current theme data from the system")) + print("\t"+fd.reof("options-generate-data", "generate-data: [Debug purposes only] Generates the 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")) + def main(cli_args: list[str]): """ Use this function invoke 'clitheme' with command line arguments @@ -210,9 +232,8 @@ def main(cli_args: list[str]): Note: the first item in the argument list must be a program name (e.g. ['clitheme', ]) """ f=frontend.FetchDescriptor() - arg_first="clitheme" # controls what appears as the command name in messages if len(cli_args)<=1: # no arguments passed - print(usage_description.format(arg_first)) + _handle_help_message() _handle_usage_error(f.reof("no-command", "Error: no command or option specified"), arg_first) return 1 @@ -280,7 +301,7 @@ def main(cli_args: list[str]): else: if cli_args[1]=="--help": check_extra_args(2) - print(usage_description.format(arg_first)) + _handle_help_message(full_help=True) else: return _handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=cli_args[1]), arg_first) return 0 -- Gitee From 457523791c56e8848c3588afb92ab57076101720 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 11 May 2024 12:43:05 +0800 Subject: [PATCH 177/354] Make alt_path variables private in frontend --- src/clitheme/frontend.py | 48 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 2f788b3..9ed0c8d 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -26,9 +26,9 @@ global_debugmode=False global_lang="" # Override locale global_disablelang=False -alt_path=None -alt_path_dirname=None -alt_path_hash=None +_alt_path=None +_alt_path_dirname=None +_alt_path_hash=None # 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 @@ -49,12 +49,12 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: # Determine directory name h=hashlib.shake_256(bytes(file_content, "utf-8")) d=h.hexdigest(6) # length of 12 (6*2) - global alt_path_hash - local_path_hash=alt_path_hash + global _alt_path_hash + local_path_hash=_alt_path_hash # if overlay, update hash with new contents of file - if alt_path_hash!=None and overlay==True: + if _alt_path_hash!=None and overlay==True: newhash="" - for x in range(len(alt_path_hash)): + for x in range(len(_alt_path_hash)): chart=string.ascii_uppercase+string.ascii_lowercase+string.digits numorig=0 numcur=0 @@ -64,23 +64,23 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: numorig=(ord(d[x])-ord('a'))+len(string.ascii_uppercase) elif d[x]>='0' and d[x]<='9': #digit numorig=ord(d[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) - if alt_path_hash[x]>='A' and alt_path_hash[x]<='Z': #uppercase letters - numcur=ord(alt_path_hash[x])-ord('A') - elif alt_path_hash[x]>='a' and alt_path_hash[x]<='z': #lowercase letters - numcur=(ord(alt_path_hash[x])-ord('a'))+len(string.ascii_uppercase) - elif alt_path_hash[x]>='0' and alt_path_hash[x]<='9': #digit - numcur=ord(alt_path_hash[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) + if _alt_path_hash[x]>='A' and _alt_path_hash[x]<='Z': #uppercase letters + numcur=ord(_alt_path_hash[x])-ord('A') + elif _alt_path_hash[x]>='a' and _alt_path_hash[x]<='z': #lowercase letters + numcur=(ord(_alt_path_hash[x])-ord('a'))+len(string.ascii_uppercase) + elif _alt_path_hash[x]>='0' and _alt_path_hash[x]<='9': #digit + numcur=ord(_alt_path_hash[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) newhash+=chart[(numorig+numcur)%len(chart)] local_path_hash=newhash else: local_path_hash=d # else, use generated hash dir_name=f"clitheme-data-{local_path_hash}" _generator.generate_custom_path() # prepare _generator.path overlay_cont=False - global alt_path_dirname - if alt_path_dirname!=None and overlay==True: # overlay + global _alt_path_dirname + if _alt_path_dirname!=None and overlay==True: # overlay if not os.path.exists(_globalvar.clitheme_temp_root+"/"+dir_name): # check if not already generated before overlay_cont=True - shutil.copytree(_globalvar.clitheme_temp_root+"/"+alt_path_dirname, _generator.path) + shutil.copytree(_globalvar.clitheme_temp_root+"/"+_alt_path_dirname, _generator.path) path_name=_globalvar.clitheme_temp_root+"/"+dir_name if global_debugmode: print("[Debug] "+path_name) # Generate data hierarchy as needed @@ -94,19 +94,19 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: shutil.copytree(_generator.path, path_name) try: shutil.rmtree(_generator.path) except: pass - global alt_path - alt_path_hash=local_path_hash - alt_path=path_name+"/"+_globalvar.generator_data_pathname - alt_path_dirname=dir_name + global _alt_path + _alt_path_hash=local_path_hash + _alt_path=path_name+"/"+_globalvar.generator_data_pathname + _alt_path_dirname=dir_name return True def unset_local_themedef(): """ Unsets the local theme definition file for the current frontend instance. After this operation, FetchDescriptor functions will no longer use local definitions. """ - global alt_path; alt_path=None - global alt_path_dirname; alt_path_dirname=None - global alt_path_hash; alt_path_hash=None + global _alt_path; _alt_path=None + global _alt_path_dirname; _alt_path_dirname=None + global _alt_path_hash; _alt_path_hash=None class FetchDescriptor(): """ @@ -191,7 +191,7 @@ class FetchDescriptor(): # just being lazy here I don't want to check the variables before using ಥ_ಥ (because it doesn't matter) path=data_path+"/"+self.domain_name+"/"+self.app_name+"/"+re.sub(" ",r"/", self.subsections) path2=None - if alt_path!=None: path2=alt_path+"/"+self.domain_name+"/"+self.app_name+"/"+re.sub(" ",r"/", self.subsections) + if _alt_path!=None: path2=_alt_path+"/"+self.domain_name+"/"+self.app_name+"/"+re.sub(" ",r"/", self.subsections) for section in entry_path.split(): path+="/"+section if path2!=None: path2+="/"+section -- Gitee From d62a88580e6b39b867f541d10e8b6b6a92a72fca Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 11 May 2024 16:43:25 +0800 Subject: [PATCH 178/354] Update pyproject.toml; - Add "Private :: Do Not Upload" - Change license name --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13348d0..d8d03d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,12 +20,13 @@ authors = [ ] description = "A text theming library for command line applications" readme = "README.en.md" -license = {file = "LICENSE"} +license = {text = "GNU General Public License v3 (GPLv3)"} requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", + "Private :: Do Not Upload" ] [project.scripts] -- Gitee From 17cbeee5bb23210a942c3d7e7f9b8f2312076831 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 11 May 2024 22:20:54 +0800 Subject: [PATCH 179/354] Update version (v2.0-dev20240511) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 908e6d4..b599877 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240509' +pkgver='2.0_dev20240511' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 92cca8e..5835385 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240509-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240511-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Thu, 09 May 2024 22:20:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 11 May 2024 22:20:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 556548d..63dee4d 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240509" +__version__="2.0-dev20240511" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240509" +version_main="2.0_dev20240511" version_buildnumber=1 \ No newline at end of file -- Gitee From 739cd255e07a7572789ba728e87e703a61b91763 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 12 May 2024 18:15:39 +0800 Subject: [PATCH 180/354] Small fix in smartcmdmatch --- src/clitheme/_generator/db_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 2d22de8..f5a7230 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -105,7 +105,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F match_cmd_phrases=[] for p in range(len(match_cmd.split())): ph=match_cmd.split()[p] - results=re.search(r"^-([a-zA-z0-9]+)",ph) + 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) -- Gitee From c2ebe2a4ec1486e5570e2e558ef30d5b9eb0b7c8 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 12 May 2024 18:49:57 +0800 Subject: [PATCH 181/354] Add "is_regex" to condition determining repeated entries --- src/clitheme/_generator/db_interface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index f5a7230..09d7a68 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -61,8 +61,8 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma cmdlist.append(re.sub(r" {2,}", " ", cmd).strip()) else: # remove any existing values with the same match_pattern - match_condition=f"match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition} AND stdout_stderr_only=?" - match_params=(match_pattern, effective_locale, stdout_stderr_matchoption) + match_condition=f"match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition} AND stdout_stderr_only=? AND is_regex=?" + match_params=(match_pattern, effective_locale, stdout_stderr_matchoption, is_regex) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: 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) @@ -72,8 +72,8 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma # remove any existing values with the same match_pattern and effective_command strictness_condition="" # if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" - match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=?" - match_params=(match_pattern, cmd, effective_locale, stdout_stderr_matchoption) + match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=? AND is_regex=?" + match_params=(match_pattern, cmd, effective_locale, stdout_stderr_matchoption, is_regex) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: 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) -- Gitee From 9d45f990cfe94dfd62f2378f75e5f9743820e876 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 12 May 2024 22:47:14 +0800 Subject: [PATCH 182/354] Change output of --debug-color and --debug --- 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 1dfa5c7..fd2fcd8 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -6,6 +6,7 @@ import pty import select import termios import copy +import re try: from .._generator import db_interface from .. import _globalvar, frontend @@ -24,7 +25,7 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa for x in range(len(lines)): line=lines[x] if "showchars" in debug_mode: - wrapper=b"\x1b[32m{}\x1b[0m" + wrapper=b"\x1b[4;32m{}\x1b[0m" if "color" in debug_mode: wrapper+=bytes(f"\x1b[{'31' if is_stderr else '33'}m", 'utf-8') line=line.replace(b'\x1b', wrapper.replace(b'{}', b'{{ESC}}')) # this must come before anything else line=line.replace(b'\r', wrapper.replace(b'{}',b'\\r')) @@ -35,7 +36,8 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa if not line.endswith(b'\n'): line+=b"\n" if "color" in debug_mode: - line=bytes(f"\x1b[{'31' if is_stderr else '33'}m", 'utf-8')+line+b"\x1b[0m" + try: line=bytes(re.sub(r"(\x1b\[.+?[a-zA-Z]|.)", f"\x1b[{'31' if is_stderr else '33'}m\\g<0>", line.decode('utf-8')), 'utf-8') + except UnicodeDecodeError: line=re.sub(bytes(r"(\x1b\[.+?[a-zA-Z]|.)", 'utf-8'), bytes(f"\x1b[{'31' if is_stderr else '33'}m\\g<0>", 'utf-8'), line) 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 ''}m"+('e' if is_stderr else 'o')+'\x1b[0;1m'+(">")+"\x1b[0m ",'utf-8')+line+b"\x1b[0m" -- Gitee From 604d9d222b762d9e4424a463f8911a330f5e8de5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 12 May 2024 22:47:59 +0800 Subject: [PATCH 183/354] Improve handling of Unicode characters in substitution Try to convert the output into UTF-8 first if possible, so it handles multi-byte Unicode characters (e.g. Chinese) better --- src/clitheme/_generator/db_interface.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 09d7a68..90e4547 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -162,9 +162,11 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F if match_data[4]!=0 and is_stderr+1!=match_data[4]: continue # check stdout/stderr constraint try: if match_data[2]==True: # is regex - content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) + try: content_str=bytes(re.sub(match_data[0], match_data[1], content_str.decode('utf-8')), 'utf-8') + except UnicodeDecodeError: content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) else: # is string - content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) + try: content_str=bytes(content_str.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') + except UnicodeDecodeError: content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) if match_data[3]==True and re.search(bytes(match_data[0], 'utf-8'), content_str)!=None: # endmatchoption is set break except: -- Gitee From 3ba2d5a1f09ad0f36a6267b4c41ceacfe87cbb05 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 12 May 2024 22:51:27 +0800 Subject: [PATCH 184/354] Small fix at --debug-color --- 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 fd2fcd8..8561b66 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -36,8 +36,10 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa if not line.endswith(b'\n'): line+=b"\n" if "color" in debug_mode: - try: line=bytes(re.sub(r"(\x1b\[.+?[a-zA-Z]|.)", f"\x1b[{'31' if is_stderr else '33'}m\\g<0>", line.decode('utf-8')), 'utf-8') - except UnicodeDecodeError: line=re.sub(bytes(r"(\x1b\[.+?[a-zA-Z]|.)", 'utf-8'), bytes(f"\x1b[{'31' if is_stderr else '33'}m\\g<0>", 'utf-8'), line) + match_pattern=r"(^|\x1b\[[\d;]*?m)" + sub_pattern=f"\\g<0>\x1b[{'31' if is_stderr else '33'}m" + try: line=bytes(re.sub(match_pattern, sub_pattern, line.decode('utf-8')), 'utf-8') + except UnicodeDecodeError: line=re.sub(bytes(match_pattern, 'utf-8'), bytes(sub_pattern, 'utf-8'), line) 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 ''}m"+('e' if is_stderr else 'o')+'\x1b[0;1m'+(">")+"\x1b[0m ",'utf-8')+line+b"\x1b[0m" -- Gitee From 3deebfb28bf54ff845c318eea37614cd060f53a7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 13 May 2024 10:49:10 +0800 Subject: [PATCH 185/354] Improve endmatchhere handling --- src/clitheme/_generator/db_interface.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 90e4547..61f5037 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -160,14 +160,19 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F fetch_matches_by_locale("typeof(effective_command)=typeof(null)") for match_data in matches: if match_data[4]!=0 and is_stderr+1!=match_data[4]: continue # check stdout/stderr constraint + matched=False try: if match_data[2]==True: # is regex - try: content_str=bytes(re.sub(match_data[0], match_data[1], content_str.decode('utf-8')), 'utf-8') + try: + matched=re.search(match_data[0], content_str.decode('utf-8'))!=None + content_str=bytes(re.sub(match_data[0], match_data[1], content_str.decode('utf-8')), 'utf-8') except UnicodeDecodeError: content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) else: # is string - try: content_str=bytes(content_str.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') + try: + matched=re.search(bytes(match_data[0], 'utf-8'), content_str)!=None + content_str=bytes(content_str.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') except UnicodeDecodeError: content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) - if match_data[3]==True and re.search(bytes(match_data[0], 'utf-8'), content_str)!=None: # endmatchoption is set + if match_data[3]==True and matched: # endmatchoption is set break except: handle_warning("Error occurred while matching string: "+str(sys.exc_info()[1])) -- Gitee From 5d14f27aa6889f06c34018faa3f7d428b090de44 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 13 May 2024 11:02:11 +0800 Subject: [PATCH 186/354] Add unique_id mechanism to avoid repeated match operations --- src/clitheme/_generator/__init__.py | 4 +++- src/clitheme/_generator/db_interface.py | 21 +++++++++++++-------- src/clitheme/_globalvar.py | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index c0644fa..ad13874 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -9,6 +9,7 @@ import re import math import copy import gzip +import uuid from typing import Optional try: from .. import _globalvar, frontend @@ -294,6 +295,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int # expect locale, locale_block, end_entry nonlocal lineindex + unique_id=uuid.uuid4() substrules_entries=[] # (match_content, substitute_content, locale) substrules_entries_linenumber=[] substrules_endmatchhere=False @@ -356,7 +358,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info if is_substrules: for x in range(len(substrules_entries)): entry=substrules_entries[x] - try: db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x]) + try: db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x], unique_id=unique_id) except db_interface.bad_pattern: handle_error(fd.feof("bad-subst-pattern-err", "Bad substitute pattern at line {num} ({error_msg})", num=str(substrules_entries_linenumber[x]), error_msg=sys.exc_info()[1])) ## Main code diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 61f5037..0e2b7b5 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -3,6 +3,7 @@ import os import sqlite3 import re import copy +import uuid from typing import Optional try: from .. import _globalvar, frontend except ImportError: import _globalvar, frontend @@ -29,6 +30,7 @@ def init_db(file_path: str): match_pattern TEXT NOT NULL, \ substitute_pattern TEXT NOT NULL, \ is_regex INTEGER NOT NULL, \ + unique_id TEXT NOT NULL, \ effective_command TEXT, \ effective_locale TEXT, \ command_match_strictness INTEGER NOT NULL, \ @@ -48,13 +50,13 @@ def connect_db(): if version!=_globalvar.db_version: raise need_db_regenerate -def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, line_number_debug: int=-1): +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, unique_id: uuid.UUID=uuid.uuid4(), line_number_debug: int=-1): 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"] + insert_values=["match_pattern", "substitute_pattern", "effective_command", "is_regex", "command_match_strictness", "end_match_here", "effective_locale", "stdout_stderr_only", "unique_id"] if effective_commands!=None and len(effective_commands)>0: for cmd in effective_commands: # remove extra spaces in the command @@ -67,7 +69,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)) + 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))) for cmd in cmdlist: # remove any existing values with the same match_pattern and effective_command strictness_condition="" @@ -78,7 +80,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)) + 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))) connection.commit() def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False) -> bytes: @@ -90,7 +92,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # retrieve a list of effective commands matching first argument final_cmdlist=[] - final_cmdlist_strictmatch=[] + final_cmdlist_exactmatch=[] if command!=None and len(command.split())>0: # obtain a list of effective_command with the same first term cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ?;", (command.split()[0].strip()+" %",)).fetchall() @@ -133,12 +135,12 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # if found matching command if match_cmd not in final_cmdlist: final_cmdlist.append(match_cmd) - final_cmdlist_strictmatch.append(strictness==2) + 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"] + fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only", "unique_id"] # get locales locales=_globalvar.get_locale() nonlocal matches @@ -154,12 +156,15 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F for x in range(len(final_cmdlist)): cmd=final_cmdlist[x] # prioritize exact match - if final_cmdlist_strictmatch[x]==True: fetch_matches_by_locale("effective_command=? AND command_match_strictness=2", (cmd,)) + if final_cmdlist_exactmatch[x]==True: fetch_matches_by_locale("effective_command=? AND command_match_strictness=2", (cmd,)) # 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)") + encountered_ids=set() 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 + else: encountered_ids.add(match_data[5]) matched=False try: if match_data[2]==True: # is regex diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 997f0bf..c34d051 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -51,7 +51,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" -db_version=1 +db_version=2 ## Sanity check function entry_banphrases=['/','\\'] -- Gitee From cf889716e4c33a61806a5cbd9d659767dedf03dd Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 13 May 2024 12:31:09 +0800 Subject: [PATCH 187/354] Fix typo at clitheme-exec.1 --- 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 aed3cc7..058200b 100644 --- a/docs/clitheme-exec.1 +++ b/docs/clitheme-exec.1 @@ -28,7 +28,7 @@ For output that does not end on a newline, display the output ending with newlin Display various control characters in plain text. The following characters will be displayed as its code name: .P .RS 14 -- ANSI escape character (ESC) +- ASCII escape character (ESC) - Carriage return (\\r) -- Gitee From 84d330dc31ea998413d6c9db6407ee3477c0029b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 13 May 2024 22:27:14 +0800 Subject: [PATCH 188/354] Update version (v2.0-dev20240513) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index b599877..e08aa51 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240511' +pkgver='2.0_dev20240513' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 5835385..f27b9a0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240511-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240513-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 11 May 2024 22:20:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, 13 May 2024 22:26:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 63dee4d..8121aa5 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240511" +__version__="2.0-dev20240513" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240511" +version_main="2.0_dev20240513" version_buildnumber=1 \ No newline at end of file -- Gitee From e86c432eedb48b4a44f6e452878c153ba7920c22 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 13 May 2024 22:39:17 +0800 Subject: [PATCH 189/354] Fix implementation of endmatchhere checking --- src/clitheme/_generator/db_interface.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 0e2b7b5..9973510 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -171,13 +171,17 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F try: matched=re.search(match_data[0], content_str.decode('utf-8'))!=None content_str=bytes(re.sub(match_data[0], match_data[1], content_str.decode('utf-8')), 'utf-8') - except UnicodeDecodeError: content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) + except UnicodeDecodeError: + matched=re.search(bytes(match_data[0], 'utf-8'), content_str)!=None + content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) else: # is string try: - matched=re.search(bytes(match_data[0], 'utf-8'), content_str)!=None + matched=match_data[0] in content_str.decode('utf-8') content_str=bytes(content_str.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') - except UnicodeDecodeError: content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) - if match_data[3]==True and matched: # endmatchoption is set + except UnicodeDecodeError: + 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 except: handle_warning("Error occurred while matching string: "+str(sys.exc_info()[1])) -- Gitee From 52b09478d36ec669ca9842cb70ae5014d36b89f4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 14 May 2024 17:32:07 +0800 Subject: [PATCH 190/354] Support terminal resizing in TUI applications --- 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 8561b66..3a773e5 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -5,6 +5,9 @@ import io import pty import select import termios +import fcntl +import signal +import struct import copy import re try: @@ -65,11 +68,23 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) return 1 output_lines=[] # (line_content, is_stderr) + def get_terminal_size(): return fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) + last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder while True: try: # update cbreak (realtime stdin) attributes from what the program sets try: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) 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(stdin_fd, termios.TIOCSWINSZ, 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 fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.01)[0] readsize=io.DEFAULT_BUFFER_SIZE if sys.stdin in fds: @@ -118,7 +133,7 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line) else: output_lines=[] # happens when no 'break' statement occurs except KeyboardInterrupt: - try: process.send_signal(2) #SIGINT + try: process.send_signal(signal.SIGINT) except KeyboardInterrupt: pass #os.write(stdin_fd, b'\x03') return process.poll() -- Gitee From cff52e6faccaee4a046741ac0872e12bf4848516 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 14 May 2024 17:47:10 +0800 Subject: [PATCH 191/354] Remove fallback when relative import is unavailable It's no longer needed anymore and might cause problems in the future --- src/clitheme/_generator/__init__.py | 5 +---- src/clitheme/_generator/db_interface.py | 3 +-- src/clitheme/_globalvar.py | 3 +-- src/clitheme/cli.py | 11 +---------- src/clitheme/exec/__init__.py | 11 +++-------- src/clitheme/exec/output_handler_posix.py | 8 ++------ src/clitheme/frontend.py | 5 +---- src/clitheme/man.py | 5 +---- 8 files changed, 11 insertions(+), 40 deletions(-) mode change 100755 => 100644 src/clitheme/cli.py mode change 100755 => 100644 src/clitheme/man.py diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index ad13874..818d05e 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -11,10 +11,7 @@ import copy import gzip import uuid from typing import Optional -try: - from .. import _globalvar, frontend -except ImportError: # for test program - import _globalvar, frontend +from .. import _globalvar, frontend fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 9973510..a792d5c 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -5,8 +5,7 @@ import re import copy import uuid from typing import Optional -try: from .. import _globalvar, frontend -except ImportError: import _globalvar, frontend +from .. import _globalvar, frontend connection=sqlite3.connect(":memory:") # placeholder debug_mode=False diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index c34d051..2e0bcae 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -7,8 +7,7 @@ import os import sys import re from copy import copy -try: from . import _version -except ImportError: import _version +from . import _version error_msg_str= \ """[clitheme] Error: unable to get your home directory or invalid home directory information. diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py old mode 100755 new mode 100644 index b88378f..37ef18d --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - """ Module used for the clitheme command line interface @@ -11,14 +9,7 @@ import os import sys import shutil import re -try: - from . import _globalvar - from . import _generator - from . import frontend -except ImportError: - import _globalvar - import _generator - import frontend +from . import _globalvar, _generator, frontend usage_description=\ """Usage: {0} apply-theme [themedef-file] [--overlay] [--preserve-temp] diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index d649bc7..872d75a 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -8,14 +8,9 @@ import sys import os import io import shutil -try: - from . import output_handler_posix - from .. import _globalvar, cli, frontend - from .._generator import db_interface -except ImportError: - import output_handler_posix - import _globalvar, cli, frontend - from _generator import db_interface +from . import output_handler_posix +from .. import _globalvar, cli, frontend +from .._generator import db_interface _globalvar.handle_set_themedef(frontend, "clitheme-exec") frontend.global_domain="swiftycode" diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 3a773e5..bc2361c 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -10,12 +10,8 @@ import signal import struct import copy import re -try: - from .._generator import db_interface - from .. import _globalvar, frontend -except ImportError: - from _generator import db_interface - import _globalvar, frontend +from .._generator import db_interface +from .. import _globalvar, frontend _globalvar.handle_set_themedef(frontend, "output_handler_posix") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 9ed0c8d..7615b30 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -13,10 +13,7 @@ import re import hashlib import shutil from typing import Optional -try: - from . import _globalvar -except ImportError: # for test program - import _globalvar +from . import _globalvar data_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_data_pathname global_domain="" diff --git a/src/clitheme/man.py b/src/clitheme/man.py old mode 100755 new mode 100644 index 2ae17e8..9fd75a2 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - """ Module used for clitheme-man @@ -10,8 +8,7 @@ import sys import os import subprocess import shutil -try: from . import _globalvar -except ImportError: import _globalvar +from . import _globalvar def main(args: list[str]): """ -- Gitee From f600498b8c11fcd268f6879388aaa77c78101cc7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 14 May 2024 22:30:00 +0800 Subject: [PATCH 192/354] Handle command basename handling; - Also check for e.g. 'bash' if input command is '/usr/bin/bash' - Return error when substitution fails --- src/clitheme/_generator/db_interface.py | 90 ++++++++++++------------- src/db_interface_tests.py | 2 +- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index a792d5c..0305475 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -93,15 +93,17 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F final_cmdlist=[] final_cmdlist_exactmatch=[] if command!=None and len(command.split())>0: + # command without paths (e.g. /usr/bin/bash -> bash) + stripped_command=os.path.basename(command) # obtain a list of effective_command with the same first term - cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ?;", (command.split()[0].strip()+" %",)).fetchall() + cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ? or effective_command LIKE ?;", (command.split()[0].strip()+" %", stripped_command.split()[0].strip()+" %")).fetchall() # also include one-phrase commands - cmdlist+=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=?;", (command.split()[0].strip(),)).fetchall() + cmdlist+=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? or effective_command=?;", (command.split()[0].strip(),stripped_command.split()[0].strip())).fetchall() # sort by number of phrases (greatest to least) def split_len(obj: tuple) -> int: return len(obj[0].split()) cmdlist.sort(key=split_len, reverse=True) # prioritize effective_command with exact match requirement - cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),)).fetchall()+cmdlist + cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE (effective_command=? OR effective_command=?) AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),re.sub(r" {2,}", " ", stripped_command).strip())).fetchall()+cmdlist def process_smartcmdmatch_phrases(match_cmd: str) -> list[str]: match_cmd_phrases=[] for p in range(len(match_cmd.split())): @@ -112,29 +114,30 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F else: match_cmd_phrases.append(ph) return match_cmd_phrases # attempt to find matching command - for tp in cmdlist: - match_cmd: str=tp[0].strip() # extract value from tuple - strictness: int=tp[1] # strictness setting - success=True - if strictness==1: # must start with pattern in terms of space-separated phrases - condition=len(match_cmd.split()): Permission denied", "rm -rf"), # test exactcmdmatch (substitution rule containing this option should be prioritized over previous rules) ("example_app: using recursive directories", "example_app -rlc"), # test smartcmdmatch - ("example_app: using list options", "example_app -rlc"), # test smartcmdmatch + ("example_app: using list options", "/usr/bin/example_app -rlc"), # test smartcmdmatch and command basename handling ] # substitute patterns substrules_file=r""" -- Gitee From 1cec94263834b07282744a2f0c827cf928b46e7f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 14 May 2024 22:39:18 +0800 Subject: [PATCH 193/354] Update version (v2.0-dev20240514) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index e08aa51..4f0ba12 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240513' +pkgver='2.0_dev20240514' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index f27b9a0..13993fc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240513-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240514-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 13 May 2024 22:26:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 14 May 2024 22:38:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 8121aa5..e89c951 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240513" +__version__="2.0-dev20240514" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240513" +version_main="2.0_dev20240514" version_buildnumber=1 \ No newline at end of file -- Gitee From 46e952a67ff412be8412de990c77a0cb11255856 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 16 May 2024 21:47:19 +0800 Subject: [PATCH 194/354] Remove unused comments in code --- src/clitheme/_generator/__init__.py | 3 --- src/clitheme/exec/output_handler_posix.py | 6 ------ 2 files changed, 9 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 818d05e..5fbabde 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -290,7 +290,6 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info return blockinput_data def handle_entry(entry_name: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int - # expect locale, locale_block, end_entry nonlocal lineindex unique_id=uuid.uuid4() substrules_entries=[] # (match_content, substitute_content, locale) @@ -379,7 +378,6 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info lineindex+=1 if is_ignore_line(): continue phrases=lines_data[lineindex].split() - # Expect name, description, description_block, version, locales, locales_block, supported_apps, supported_apps_block if phrases[0]=="name" or phrases[0]=="version" or phrases[0]=="description": check_enough_args(phrases, 2) content=_globalvar.extract_content(lines_data[lineindex]) @@ -436,7 +434,6 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info lineindex+=1 if is_ignore_line(): continue phrases=lines_data[lineindex].split() - # expect entry, in_domainapp, in_subsction, unset_domainapp, unset_subsection if phrases[0]=="in_domainapp": this_phrases=subst_variable_content(lines_data[lineindex].strip()).split() check_enough_args(this_phrases, 3) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index bc2361c..c66837f 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -19,7 +19,6 @@ fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subse 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[bytes], debug_mode: list[str], is_stderr: bool=False, matched: bool=False) -> list[bytes]: - # debug_mode: newlines, showchars, color final_lines=[] for x in range(len(lines)): line=lines[x] @@ -89,8 +88,6 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): os.write(stdin_fd, data) if stdout_fd in fds: data=os.read(stdout_fd, readsize) - #data=b'\x1b[33m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[33m')+b'\x1b[0m' # DEBUG purposes - #data=b'\x1b[33m'+data+b'\x1b[0m' # DEBUG purposes lines=data.splitlines(keepends=True) for x in range(len(lines)): line=lines[x] @@ -102,8 +99,6 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): else: output_lines.append((line,False)) if stderr_fd in fds: data=os.read(stderr_fd, readsize) - #data=b'\x1b[31m'+data.replace(b'\x1b',b'\x1b[32m{{ESC}}\x1b[31m')+b'\x1b[0m' # DEBUG purposes - #data=b'\x1b[31m'+data+b'\x1b[0m' # DEBUG purposes lines=data.splitlines(keepends=True) for x in range(len(lines)): line=lines[x] @@ -131,5 +126,4 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): except KeyboardInterrupt: try: process.send_signal(signal.SIGINT) except KeyboardInterrupt: pass - #os.write(stdin_fd, b'\x03') return process.poll() -- Gitee From 8a1b38ab54b3c0a3f4e0a69bdd9f313d7635e609 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 16 May 2024 22:01:09 +0800 Subject: [PATCH 195/354] Add name prefix for clitheme-exec and clitheme-man output --- src/clitheme/exec/__init__.py | 17 ++++++++++------- src/clitheme/exec/output_handler_posix.py | 3 ++- src/clitheme/man.py | 8 +++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 872d75a..cfc1f78 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -8,6 +8,9 @@ import sys import os import io import shutil +def _labeled_print(msg: str): + print("[clitheme-exec] "+msg) + from . import output_handler_posix from .. import _globalvar, cli, frontend from .._generator import db_interface @@ -20,7 +23,7 @@ fd=frontend.FetchDescriptor(subsections="exec") def _check_regenerate_db() -> bool: try: db_interface.connect_db() except db_interface.need_db_regenerate: - print(fd.reof("substrules-migrate-msg", "Migrating substrules database...")) + _labeled_print(fd.reof("substrules-migrate-msg", "Migrating substrules database...")) try: # gather files search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname @@ -46,14 +49,14 @@ def _check_regenerate_db() -> bool: sys.stdout=sys.__stdout__ os.remove(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, _globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) - print(fd.reof("db-migrate-success-msg", "Successfully completed migration, proceeding execution")) + _labeled_print(fd.reof("db-migrate-success-msg", "Successfully completed migration, proceeding execution")) except: sys.stdout=sys.__stdout__ - 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-migration-err", "An error occurred while migrating the database: {msg}\nPlease re-apply the theme and try again", msg=str(sys.exc_info()[1]))) return False except FileNotFoundError: pass except: - 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-migration-err", "An error occurred while migrating the database: {msg}\nPlease re-apply the theme and try again", msg=str(sys.exc_info()[1]))) return False return True @@ -109,16 +112,16 @@ def main(arguments: list[str]): return 1 # check database if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): - print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) + _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) if not _check_regenerate_db(): return 1 # determine platform if os.name=="posix": return output_handler_posix._handler_main(arguments[1+argcount:], debug_mode) elif os.name=="nt": - print("Error: Windows platform is not currently supported") + _labeled_print("Error: Windows platform is not currently supported") return 1 else: - print("Error: Unsupported platform") + _labeled_print("Error: Unsupported platform") return 1 return 0 def _script_main(): # for script diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index c66837f..19c892f 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -12,6 +12,7 @@ import copy import re from .._generator import db_interface from .. import _globalvar, frontend +from . import _labeled_print _globalvar.handle_set_themedef(frontend, "output_handler_posix") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") @@ -60,7 +61,7 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): # need to find a method to preserve exact order when using separated stdout and stderr pipes try: process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) except: - print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) + _labeled_print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) return 1 output_lines=[] # (line_content, is_stderr) def get_terminal_size(): return fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 9fd75a2..724033a 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -9,6 +9,8 @@ import os import subprocess import shutil from . import _globalvar +def _labeled_print(msg: str): + print("[clitheme-man] "+msg) def main(args: list[str]): """ @@ -18,17 +20,17 @@ def main(args: list[str]): (e.g. ['clitheme-man', ] or ['example-app', ]) """ if os.name=="nt": - print("Windows platform not supported") + _labeled_print("Windows platform not supported") return 1 # check if "man" exists on system man_executable: str=shutil.which("man") # type: ignore if man_executable==None: - print("Error: \"man\" is not installed on this system") + _labeled_print("Error: \"man\" is not installed on this system") return 1 env=os.environ # check if theme is set if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.generator_manpage_pathname}"): - print("Warning: no theme set or theme does not contain manpages") + _labeled_print("Warning: no theme set or theme does not contain manpages") # set MANPATH env['MANPATH']=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_manpage_pathname+":"+(os.environ['MANPATH'] if 'MANPATH' in os.environ else '') # invoke man -- Gitee From dcae3a09c34af58f9f07297269061eb0170397c4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 16 May 2024 22:39:36 +0800 Subject: [PATCH 196/354] Add missing string entries for clitheme-man and cli --- src/clitheme/_globalvar.py | 7 +++-- src/clitheme/cli.py | 4 +-- src/clitheme/man.py | 13 ++++++--- .../strings/cli-strings.clithemedef.txt | 29 +++++++++++++++++++ .../strings/man-strings.clithemedef.txt | 21 ++++++++++++++ 5 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 src/clitheme/strings/man-strings.clithemedef.txt diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 2e0bcae..b2994da 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -145,18 +145,19 @@ def get_locale(debug_mode: bool=False): return lang def handle_set_themedef(fr, debug_name: str): + prev_mode=False try: - files=["strings/generator-strings.clithemedef.txt", "strings/cli-strings.clithemedef.txt", "strings/exec-strings.clithemedef.txt"] + 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=False + fr.global_debugmode=prev_mode sys.stdout=sys.__stdout__ except: sys.stdout=sys.__stdout__ - fr.global_debugmode=False + fr.global_debugmode=prev_mode if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) pass \ No newline at end of file diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 37ef18d..2340f51 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -209,10 +209,10 @@ 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.").replace("\n", "\n\t\t")) + "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")) print("\t"+fd.reof("options-unset-current-theme", "unset-current-theme: Remove the current theme data from the system")) - print("\t"+fd.reof("options-generate-data", "generate-data: [Debug purposes only] Generates the data hierarchy from specified theme definition files in a temporary directory")) + 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")) diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 724033a..b10e832 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -8,10 +8,15 @@ import sys import os import subprocess import shutil -from . import _globalvar +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") + def main(args: list[str]): """ Invoke clitheme-man using the given command line arguments @@ -20,17 +25,17 @@ def main(args: list[str]): (e.g. ['clitheme-man', ] or ['example-app', ]) """ if os.name=="nt": - _labeled_print("Windows platform not supported") + _labeled_print(fd.reof("win32-not-supported", "Error: Windows platform not supported")) return 1 # check if "man" exists on system man_executable: str=shutil.which("man") # type: ignore if man_executable==None: - _labeled_print("Error: \"man\" is not installed on this system") + _labeled_print(fd.reof("man-not-installed", "Error: \"man\" is not installed on this system")) return 1 env=os.environ # check if theme is set if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.generator_manpage_pathname}"): - _labeled_print("Warning: no theme set or theme does not contain manpages") + _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not contain manpages")) # set MANPATH env['MANPATH']=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_manpage_pathname+":"+(os.environ['MANPATH'] if 'MANPATH' in os.environ else '') # invoke man diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 4644236..b531651 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -132,4 +132,33 @@ in_domainapp swiftycode clitheme [entry] supported-apps-str locale:zh_CN 支持的应用程序: [/entry] + in_subsection cli help-message + [entry] usage-str + locale:zh_CN 使用方式: + [/entry] + [entry] options-str + locale:zh_CN 选项: + [/entry] + [entry] options-apply-theme + [locale] zh_CN + apply-theme:将指定的主题定义文件应用到当前系统中 + 指定"--overlay"选项以保留当前主题数据的情况下应用(添加到当前数据中) + 指定"--preserve-temp"以保留该操作生成的临时目录(调试用途) + [/locale] + [/entry] + [entry] options-get-current-theme-info + locale:zh_CN get-current-theme-info:输出当前主题设定的详细信息 + [/entry] + [entry] options-unset-current-theme + locale:zh_CN unset-current-theme:取消设定当前主题定义和数据 + [/entry] + [entry] options-generate-data + locale:zh_CN generate-data:【仅供调试用途】对于指定的主题定义文件在临时目录中生成一个数据结构 + [/entry] + [entry] options-version + locale:zh_CN --version:输出clitheme的当前版本信息 + [/entry] + [entry] options-help + locale:zh_CN --help:输出这个帮助提示 + [/entry] {/entries_section} diff --git a/src/clitheme/strings/man-strings.clithemedef.txt b/src/clitheme/strings/man-strings.clithemedef.txt new file mode 100644 index 0000000..af6152f --- /dev/null +++ b/src/clitheme/strings/man-strings.clithemedef.txt @@ -0,0 +1,21 @@ +{header_section} + name clitheme message text translations (man) + version 2.0 + locales zh_CN + supported_apps clitheme +{/header_section} + +{entries_section} +in_domainapp swiftycode clitheme + in_subsection man + [entry] man-not-installed + locale:zh_CN 错误:"man"未安装在此系统中 + [/entry] + [entry] no-theme-warn + locale:zh_CN 警告:没有设定主题或当前主题没有manpages定义 + [/entry] + [entry] win32-not-supported + locale:zh_CN 错误:不支持Windows平台 + [/entry] +{/entries_section} + -- Gitee From 1e8811a709b79ce80e480f3ae7d333f94ec4d247 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 16 May 2024 22:43:33 +0800 Subject: [PATCH 197/354] Update version (v2.0-dev20240516) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 4f0ba12..6e2aaeb 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240514' +pkgver='2.0_dev20240516' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 13993fc..98209a9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240514-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240516-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Tue, 14 May 2024 22:38:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 16 May 2024 22:38:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index e89c951..3a39829 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240514" +__version__="2.0-dev20240516" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240514" +version_main="2.0_dev20240516" version_buildnumber=1 \ No newline at end of file -- Gitee From b367efed6f8b929e308a9846d2256e90a225dd48 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 18 May 2024 00:12:26 +0800 Subject: [PATCH 198/354] Reset color at every end of newline in --debug-color --- src/clitheme/exec/output_handler_posix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 19c892f..0fd79f8 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -39,6 +39,7 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa sub_pattern=f"\\g<0>\x1b[{'31' if is_stderr else '33'}m" try: line=bytes(re.sub(match_pattern, sub_pattern, line.decode('utf-8')), 'utf-8') 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 ''}m"+('e' if is_stderr else 'o')+'\x1b[0;1m'+(">")+"\x1b[0m ",'utf-8')+line+b"\x1b[0m" -- Gitee From 4c460c1e3b464e6a452c6815adb141e7f9fbeb00 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 18 May 2024 11:38:21 +0800 Subject: [PATCH 199/354] Rewrite stdin handling in output_handler_posix --- src/clitheme/exec/output_handler_posix.py | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 0fd79f8..e563064 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -2,7 +2,7 @@ import subprocess import sys import os import io -import pty +import pty, tty import select import termios import fcntl @@ -60,17 +60,23 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): process: subprocess.Popen # Redirect stderr to stdout for now (BETA) # need to find a method to preserve exact order when using separated stdout and stderr pipes - try: process=subprocess.Popen(command, stdin=stdin_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) + try: process=subprocess.Popen(command, stdin=stdout_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) except: _labeled_print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) return 1 + prev_attrs=termios.tcgetattr(sys.stdin) output_lines=[] # (line_content, is_stderr) def get_terminal_size(): return fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder while True: try: - # update cbreak (realtime stdin) attributes from what the program sets - try: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdin_fd)) + # update terminal attributes from what the program sets + try: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdout_fd)) + 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) except termios.error: pass # update terminal size try: @@ -86,8 +92,15 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): readsize=io.DEFAULT_BUFFER_SIZE if sys.stdin in fds: data=os.read(sys.stdin.fileno(), readsize) - if not data: break - os.write(stdin_fd, data) + # if child process not in cbreak mode, output the characters + # if termios.tcgetattr(stdin_fd)[3] & termios.ICANON: + # os.write(sys.stdout.fileno(), data) + # # output a new line if return key is pressed and ends on \r + # if data.endswith(b'\r'): os.write(sys.stdout.fileno(), b'\n') + # if not data: break + os.write(stdout_fd, data) + # ^C pressed + # if data==b'\x03' and (not termios.tcgetattr(stdout_fd)[0] & termios.IGNBRK) and termios.tcgetattr(stdout_fd)[0] & termios.BRKINT: process.send_signal(signal.SIGINT) if stdout_fd in fds: data=os.read(stdout_fd, readsize) lines=data.splitlines(keepends=True) @@ -128,4 +141,5 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): except KeyboardInterrupt: try: process.send_signal(signal.SIGINT) except KeyboardInterrupt: pass + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes return process.poll() -- Gitee From b20e5717feb1941dfabb048cb82513af4fe351ed Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 18 May 2024 12:34:09 +0800 Subject: [PATCH 200/354] Omit content from user input from subst processing --- src/clitheme/exec/output_handler_posix.py | 48 +++++++++++++---------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index e563064..bd359be 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -52,7 +52,6 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): except FileNotFoundError: do_subst=False stdout_fd, stdout_slave=pty.openpty() stderr_fd, stderr_slave=pty.openpty() - stdin_fd, stdin_slave=pty.openpty() env=copy.copy(os.environ) # Prevent apps from using "less" or "more" as pager, as it won't work here @@ -65,9 +64,11 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): _labeled_print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) return 1 prev_attrs=termios.tcgetattr(sys.stdin) - output_lines=[] # (line_content, is_stderr) + 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)) 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 while True: try: # update terminal attributes from what the program sets @@ -83,7 +84,6 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): new_term_size=get_terminal_size() if new_term_size!=last_terminal_size: last_terminal_size=new_term_size - fcntl.ioctl(stdin_fd, termios.TIOCSWINSZ, 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) @@ -92,6 +92,9 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): readsize=io.DEFAULT_BUFFER_SIZE 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 # if child process not in cbreak mode, output the characters # if termios.tcgetattr(stdin_fd)[3] & termios.ICANON: # os.write(sys.stdout.fileno(), data) @@ -101,8 +104,13 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): os.write(stdout_fd, data) # ^C pressed # if data==b'\x03' and (not termios.tcgetattr(stdout_fd)[0] & termios.IGNBRK) and termios.tcgetattr(stdout_fd)[0] & termios.BRKINT: process.send_signal(signal.SIGINT) - if stdout_fd in fds: - data=os.read(stdout_fd, readsize) + def handle_output(is_stderr: bool): + data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) + do_subst_operation=True + # nonlocal last_input_content + # print(last_input_content, data, data==last_input_content) + # if data==last_input_content: do_subst_operation=False + # last_input_content=None lines=data.splitlines(keepends=True) for x in range(len(lines)): line=lines[x] @@ -110,18 +118,11 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines): orig_line=output_lines[-1][0] output_lines.pop() - output_lines.append((orig_line+line,False)) - else: output_lines.append((line,False)) - if stderr_fd in fds: - data=os.read(stderr_fd, readsize) - lines=data.splitlines(keepends=True) - for x in range(len(lines)): - line=lines[x] - if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines): - orig_line=output_lines[-1][0] - output_lines.pop() - output_lines.append((orig_line+line,True)) - else: output_lines.append((line,True)) + output_lines.append((orig_line+line,is_stderr,do_subst_operation)) + else: output_lines.append((line,is_stderr,do_subst_operation)) + if stdout_fd in fds: handle_output(is_stderr=False) + if stderr_fd in fds: handle_output(is_stderr=True) + if process.poll()!=None and len(output_lines)==0: break # Process outputs for x in range(len(output_lines)): @@ -129,13 +130,20 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): line: bytes=line_data[0] # if does not end with newlines, leave it for the next iteration if x==len(output_lines)-1 and not line.endswith(newlines): - if not len(line_data)>2: # not from previous iteration + if not len(line_data)>=4: # not from previous iteration output_lines=[line_data+(True,)] # add another entry to signal it's from previous iteration break + # 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); last_input_content=None + elif last_input_content!=None and last_input_content.startswith(line): + line_data=(line_data[0],line_data[1],False) + last_input_content=last_input_content[len(line):] + else: last_input_content=None # subst operation subst_line=copy.copy(line) - if do_subst: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) - subst_line=_process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line)[0] + if do_subst and line_data[2]==True: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) + if line_data[2]==True: subst_line=_process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line)[0] os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line) else: output_lines=[] # happens when no 'break' statement occurs except KeyboardInterrupt: -- Gitee From 5a02bb8e9bf365e7dd42f63b0620949a739e954c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 18 May 2024 15:35:48 +0800 Subject: [PATCH 201/354] Add ignore words for cSpell vscode extension --- cspell.json | 31 +++++++++++++++++++ src/clitheme/_generator/__init__.py | 2 ++ src/clitheme/_generator/db_interface.py | 2 ++ src/clitheme/_globalvar.py | 2 ++ src/clitheme/_version.py | 2 ++ src/clitheme/cli.py | 2 ++ src/clitheme/exec/__init__.py | 2 ++ src/clitheme/exec/output_handler_posix.py | 3 ++ src/clitheme/frontend.py | 3 ++ .../strings/generator-strings.clithemedef.txt | 1 + 10 files changed, 50 insertions(+) create mode 100644 cspell.json diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..dc147e5 --- /dev/null +++ b/cspell.json @@ -0,0 +1,31 @@ +// cSpell Settings +{ + // Version of the setting file. Always 0.2 + "version": "0.2", + // language - current active spelling language + "language": "en", + // words - list of words to be always considered correct + "words": [ + "globalvar", + "swiftycode", + "clitheme", + "feof", "reof", + "themedef", "clithemeinfo", "clithemedef", "infofile", + "substrules", "manpages", + "exactcmdmatch", "smartcmdmatch", "endmatchhere", + "leadtabindents", "leadspaces", + "strictcmdmatch", "normalcmdmatch", "exactcmdmatch", "smartcmdmatch", + "subststdoutonly", "subststderronly", "substall", + "substvar", "substesc", + "PKGBUILD", "MANPATH", + "tcgetattr", "tcsetattr", + "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", + "showchars", "keepends", + "appname", "domainapp", + "sanitycheck", "debugmode", "splitarray", "disablelang" + ], + // flagWords - list of words to be always considered incorrect + // This is useful for offensive words and common spelling errors. + "flagWords": [ + ] +} \ No newline at end of file diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 5fbabde..989affe 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -13,6 +13,8 @@ import uuid from typing import Optional from .. import _globalvar, frontend +# spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent + fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") path="" diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 0305475..1a88d2f 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -7,6 +7,8 @@ import uuid from typing import Optional from .. import _globalvar, frontend +# spell-checker:ignore matchoption cmdlist exactmatch rowid + connection=sqlite3.connect(":memory:") # placeholder debug_mode=False _globalvar.handle_set_themedef(frontend, "db_interface") diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index b2994da..0e27de5 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -9,6 +9,8 @@ import re from copy import copy from . import _version +# spell-checker:ignoreRegExp banphrase[s]{0,1} + 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. diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 3a39829..4053345 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,3 +1,5 @@ +# spell-checker:ignore buildnumber + # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables __version__="2.0-dev20240516" diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 2340f51..3586dae 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -11,6 +11,8 @@ import shutil import re from . import _globalvar, _generator, frontend +# spell-checker:ignore pathnames lsdir inpstr + usage_description=\ """Usage: {0} apply-theme [themedef-file] [--overlay] [--preserve-temp] {0} get-current-theme-info diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index cfc1f78..5bdb8a6 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -15,6 +15,8 @@ from . import output_handler_posix from .. import _globalvar, cli, frontend from .._generator import db_interface +# spell-checker:ignore lsdir showhelp argcount + _globalvar.handle_set_themedef(frontend, "clitheme-exec") frontend.global_domain="swiftycode" frontend.global_appname="clitheme" diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index bd359be..9b165b3 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -14,6 +14,8 @@ from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print +# spell-checker:ignore cbreak ICANON readsize splitarray + _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 @@ -103,6 +105,7 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): # if not data: break os.write(stdout_fd, data) # ^C pressed + # spell-checker:ignore IGNBRK BRKINT # if data==b'\x03' and (not termios.tcgetattr(stdout_fd)[0] & termios.IGNBRK) and termios.tcgetattr(stdout_fd)[0] & termios.BRKINT: process.send_signal(signal.SIGINT) def handle_output(is_stderr: bool): data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 7615b30..e5fe5c2 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -14,6 +14,9 @@ import hashlib import shutil from typing import Optional from . import _globalvar + +# spell-checker:ignore newhash numorig numcur + data_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_data_pathname global_domain="" diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index a01036b..383a2a1 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -1,3 +1,4 @@ +# spell-checker:ignore subdir banphrase startswith {header_section} name clitheme message text translations (generator) version 2.0 -- Gitee From 946617d6403a24958bfe70a319f3eb4da5f24b9c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 18 May 2024 17:51:12 +0800 Subject: [PATCH 202/354] Only process number-named folders in theme-info --- src/clitheme/cli.py | 4 ++-- src/clitheme/exec/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 3586dae..a10c64c 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -142,8 +142,8 @@ def get_current_theme_info(): else: print(f.reof("overlay-history-msg", "Overlay history (sorted by latest installed):")) for theme_pathname in lsdir_result: - target_path=search_path+"/"+theme_pathname - if not os.path.isdir(target_path): continue # skip current_theme_index file + 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")): diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 5bdb8a6..f52eb93 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -6,6 +6,7 @@ Module used for clitheme-exec """ import sys import os +import re import io import shutil def _labeled_print(msg: str): @@ -40,7 +41,7 @@ def _check_regenerate_db() -> bool: paths=[] for pathname in lsdir_result: target_path=search_path+"/"+pathname - if not os.path.isdir(target_path): continue + if (not os.path.isdir(target_path)) or re.search(r"^\d+$", pathname.strip())==None: continue # skip current_theme_index file content=open(target_path+"/file_content", encoding="utf-8").read() file_contents.append(content) paths.append(target_path+"/manpage_data/file_content") # small hack/workaround -- Gitee From afdccaf0e941acf498bcdbef04f713684516048f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 18 May 2024 23:55:07 +0800 Subject: [PATCH 203/354] Update version (v2.0-dev20240518) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 6e2aaeb..8d60612 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240516' +pkgver='2.0_dev20240518' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 98209a9..f889019 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240516-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240518-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Thu, 16 May 2024 22:38:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 18 May 2024 22:55:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 4053345..a40a67c 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -2,11 +2,11 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240516" +__version__="2.0-dev20240518" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240516" +version_main="2.0_dev20240518" version_buildnumber=1 \ No newline at end of file -- Gitee From eb0d711518e91a1802fa2f105b8a8b345d7c8cb1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 29 May 2024 19:10:07 +0800 Subject: [PATCH 204/354] Fix allowed_options in some places; - Merge some operations into handler functions --- src/clitheme/_generator/__init__.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 989affe..bdcc1a9 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -228,6 +228,14 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info def handle_end_section(section_name: str): nonlocal parsed_sections; parsed_sections.append(section_name) nonlocal section_parsing; section_parsing=False + def handle_substesc(content: str) -> str: + return content.replace("{{ESC}}", "\x1b") + def handle_singleline_content(content: str) -> str: + target_content=copy.copy(content) + target_content=subst_variable_content(target_content) + if "substesc" in global_options.keys() and global_options['substesc']==True: + target_content=handle_substesc(target_content) + return target_content ## sub-block processing functions def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True, disable_substesc: bool=False) -> str: @@ -269,8 +277,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info got_options=copy.copy(global_options) specified_options={} if len(lines_data[lineindex].split())>1: - got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True, allowed_options=(["leadtabindents", "leadspaces"] if preserve_indents else []) if disallow_cmdmatch_options else None) - specified_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=False, allowed_options=(["leadtabindents", "leadspaces"] if preserve_indents else []) if disallow_cmdmatch_options else None) + got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True) + specified_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=False) for option in got_options.keys(): def is_specified_in_block() -> bool: return option in specified_options.keys() and specified_options[option]==True if option=="leadtabindents": @@ -284,7 +292,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif option=="substesc": if disable_substesc and is_specified_in_block(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) # substitute {{ESC}} with escape literal - if got_options['substesc']==True and not disable_substesc: blockinput_data=re.sub(r"{{ESC}}", "\x1b", blockinput_data) + if got_options['substesc']==True and not disable_substesc: blockinput_data=handle_substesc(blockinput_data) elif option=="substvar": if got_options['substvar']==True: blockinput_data=subst_variable_content(blockinput_data, True) elif disallow_cmdmatch_options: @@ -322,11 +330,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content=_globalvar.extract_content(lines_data[lineindex], begin_phrase_count=2) locale=phrases[1] target_entry=copy.copy(entry_name) - # substvar - content=subst_variable_content(content) - # substesc - if "substesc" in global_options.keys() and global_options['substesc']==True: - content=re.sub(r"{{ESC}}", '\x1b', content) + content=handle_singleline_content(content) # handle substesc and substvar if locale!="default": target_entry+="__"+locale if not is_substrules: add_entry(datapath, target_entry, content, lineindex+1) @@ -343,7 +347,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: substrules_entries.append((entry_name, content, None if this_locale=="default" else this_locale)); substrules_entries_linenumber.append(lineindex+1) elif phrases[0]==end_phrase: if not is_substrules: check_extra_args(phrases, 1, use_exact_count=True) - got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere", "subststdoutonly", "subststderronly"]) + got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere", "subststdoutonly", "subststderronly", "substall"]) for option in got_options: if option=="endmatchhere" and got_options['endmatchhere']==True: substrules_endmatchhere=True @@ -405,7 +409,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info content=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: - content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase) + content=handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase, disable_substesc=True) file_name=_globalvar.generator_info_v2filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) write_infofile( \ path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ @@ -540,9 +544,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info check_enough_args(phrases, 2) options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} match_pattern=_globalvar.extract_content(lines_data[lineindex]) - match_pattern=subst_variable_content(match_pattern) - if "substesc" in global_options.keys() and global_options['substesc']==True: - match_pattern=match_pattern.replace("{{ESC}}", "\x1b") + match_pattern=handle_singleline_content(match_pattern) # handle substesc and substvar handle_entry(match_pattern, end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) elif phrases[0]=="set_options": check_enough_args(phrases, 2) -- Gitee From b71cf0808914f14258aad9109aa37d63cf45bde3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 29 May 2024 21:10:59 +0800 Subject: [PATCH 205/354] Add --debug-nosubst option in clitheme-exec --- src/clitheme/exec/__init__.py | 17 +++++++++++------ src/clitheme/exec/output_handler_posix.py | 12 ++++++------ .../strings/exec-strings.clithemedef.txt | 3 +++ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index f52eb93..be61f08 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -16,7 +16,7 @@ from . import output_handler_posix from .. import _globalvar, cli, frontend from .._generator import db_interface -# spell-checker:ignore lsdir showhelp argcount +# spell-checker:ignore lsdir showhelp argcount nosubst _globalvar.handle_set_themedef(frontend, "clitheme-exec") frontend.global_domain="swiftycode" @@ -66,13 +66,14 @@ def _check_regenerate_db() -> bool: 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] [command]") + print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--debug-showchars] [--debug-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"+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-nosubst", "--debug-nosubst: Do not perform any output substitutions even if a theme is set")) def _handle_error(message: str): print(message) @@ -90,6 +91,7 @@ def main(arguments: list[str]): debug_mode=[] argcount=0 showhelp=False + subst=True for arg in arguments[1:]: if not arg.startswith('-'): break argcount+=1 @@ -101,6 +103,8 @@ def main(arguments: list[str]): debug_mode.append("newlines") elif arg=="--debug-showchars": debug_mode.append("showchars") + elif arg=="--debug-nosubst": + subst=False elif arg=="--help": showhelp=True else: @@ -114,12 +118,13 @@ def main(arguments: list[str]): _handle_error(fd.reof("no-command-err", "Error: no command specified")) return 1 # check database - if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): - _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) - if not _check_regenerate_db(): return 1 + if subst: + if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): + _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) + if not _check_regenerate_db(): return 1 # determine platform if os.name=="posix": - return output_handler_posix._handler_main(arguments[1+argcount:], debug_mode) + return output_handler_posix._handler_main(arguments[1+argcount:], debug_mode, subst) elif os.name=="nt": _labeled_print("Error: Windows platform is not currently supported") return 1 diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 9b165b3..03120f4 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -2,7 +2,7 @@ import subprocess import sys import os import io -import pty, tty +import pty import select import termios import fcntl @@ -48,10 +48,11 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa final_lines.append(line) return final_lines -def _handler_main(command: list[str], debug_mode: list[str]=[]): - do_subst=True - try: db_interface.connect_db() - except FileNotFoundError: do_subst=False +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() + except FileNotFoundError: do_subst=False stdout_fd, stdout_slave=pty.openpty() stderr_fd, stderr_slave=pty.openpty() @@ -75,7 +76,6 @@ def _handler_main(command: list[str], debug_mode: list[str]=[]): try: # update terminal attributes from what the program sets try: - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, termios.tcgetattr(stdout_fd)) attrs=termios.tcgetattr(stdout_fd) # disable canonical and echo mode (enable cbreak) no matter what attrs[3] &= ~(termios.ICANON | termios.ECHO) diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index 46c7578..b0ed1ae 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -26,6 +26,9 @@ in_domainapp swiftycode clitheme [entry] options-debug-showchars locale:zh_CN --debug-showchars:使用明文显示终端控制符号 [/entry] + [entry] options-debug-nosubst + locale:zh_CN --debug-nosubst:不进行任何输出替换,即使已设定主题 + [/entry] in_subsection exec [entry] help-usage-prompt locale:zh_CN 使用"clitheme-exec --help"以获取使用方式 -- Gitee From f9fb11761a8b889fcae0bbb63dfd7a604bcb074a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 29 May 2024 21:15:54 +0800 Subject: [PATCH 206/354] Fix stripped_command processing in db_interface --- src/clitheme/_generator/db_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 1a88d2f..97445d5 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -96,7 +96,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F final_cmdlist_exactmatch=[] if command!=None and len(command.split())>0: # command without paths (e.g. /usr/bin/bash -> bash) - stripped_command=os.path.basename(command) + stripped_command=os.path.basename(command.split()[0])+" "+(_globalvar.splitarray_to_string(command.split()[1:]) if len(command.split())>1 else '') # obtain a list of effective_command with the same first term cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ? or effective_command LIKE ?;", (command.split()[0].strip()+" %", stripped_command.split()[0].strip()+" %")).fetchall() # also include one-phrase commands -- Gitee From 60f58b6ceca626d2095f481878623a9931de35bf Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 29 May 2024 21:24:57 +0800 Subject: [PATCH 207/354] Update version (v2.0-dev20240529) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 8d60612..2d90395 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240518' +pkgver='2.0_dev20240529' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index f889019..3eddd26 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240518-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240529-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 18 May 2024 22:55:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 29 May 2024 21:24:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index a40a67c..0bb62da 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -2,11 +2,11 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240518" +__version__="2.0-dev20240529" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240518" +version_main="2.0_dev20240529" version_buildnumber=1 \ No newline at end of file -- Gitee From d616959189f44e8a60bdb5c68fb8fe861f3809b3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 29 May 2024 22:12:37 +0800 Subject: [PATCH 208/354] Add copyright and license notice in each file --- src/clitheme/_generator/__init__.py | 6 ++++++ src/clitheme/_generator/db_interface.py | 6 ++++++ src/clitheme/_globalvar.py | 6 ++++++ src/clitheme/cli.py | 6 ++++++ src/clitheme/exec/__init__.py | 6 ++++++ src/clitheme/exec/output_handler_posix.py | 6 ++++++ src/clitheme/frontend.py | 6 ++++++ src/clitheme/man.py | 6 ++++++ src/clitheme/strings/cli-strings.clithemedef.txt | 6 ++++++ src/clitheme/strings/exec-strings.clithemedef.txt | 8 ++++++++ src/clitheme/strings/generator-strings.clithemedef.txt | 6 ++++++ src/clitheme/strings/man-strings.clithemedef.txt | 6 ++++++ 12 files changed, 74 insertions(+) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index bdcc1a9..5305c34 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__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 . + """ Generator function used in applying themes (should not be invoked directly) """ diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 97445d5..af7e720 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.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 os import sqlite3 diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 0e27de5..9bb494f 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.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 . + """ Global variable definitions for clitheme """ diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index a10c64c..2b663c3 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.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 . + """ Module used for the clitheme command line interface diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index be61f08..60d3ca8 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__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 . + """ Module used for clitheme-exec diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 03120f4..82cf940 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.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 subprocess import sys import os diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index e5fe5c2..61a703d 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.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 . + """ clitheme frontend interface for accessing entries diff --git a/src/clitheme/man.py b/src/clitheme/man.py index b10e832..5d19e79 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.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 . + """ Module used for clitheme-man diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index b531651..1850829 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -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 . + {header_section} name clitheme message text translations (cli) version 2.0 diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index b0ed1ae..da6bdb1 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -1,3 +1,11 @@ +# 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 . + +# spell-checker:ignore nosubst + {header_section} name clitheme message text translations (clitheme-exec) version 2.0 diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 383a2a1..46c39d4 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -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 . + # spell-checker:ignore subdir banphrase startswith {header_section} name clitheme message text translations (generator) diff --git a/src/clitheme/strings/man-strings.clithemedef.txt b/src/clitheme/strings/man-strings.clithemedef.txt index af6152f..8d860e2 100644 --- a/src/clitheme/strings/man-strings.clithemedef.txt +++ b/src/clitheme/strings/man-strings.clithemedef.txt @@ -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 . + {header_section} name clitheme message text translations (man) version 2.0 -- Gitee From 769aaed3046e96459542ba55a84e0e1b58d88ae6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 1 Jun 2024 18:48:17 +0800 Subject: [PATCH 209/354] Implement thread pool and half-complete implementation of function timeout --- src/clitheme/_generator/db_interface.py | 93 ++++++++++++++++++----- src/clitheme/exec/output_handler_posix.py | 38 ++++----- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index af7e720..afc628b 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -10,15 +10,19 @@ import sqlite3 import re import copy import uuid +import multiprocessing, threading from typing import Optional from .. import _globalvar, frontend # spell-checker:ignore matchoption cmdlist exactmatch rowid 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") +try: multiprocessing.set_start_method('fork', force=True) +except: pass class need_db_regenerate(Exception): pass @@ -28,7 +32,8 @@ class bad_pattern(Exception): def handle_warning(message: str): if debug_mode: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) def init_db(file_path: str): - global connection + global connection, db_path + db_path=file_path connection=sqlite3.connect(file_path) # create the table # command_match_strictness: 0: default match options, 1: must start with pattern, 2: must exactly equal pattern @@ -50,8 +55,10 @@ def init_db(file_path: str): def connect_db(): if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): raise FileNotFoundError("No theme set or theme does not contain substrules") + global db_path + db_path=f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}" global connection - connection=sqlite3.connect(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}") + connection=sqlite3.connect(db_path) # check db version version=int(connection.execute(f"SELECT value FROM {_globalvar.db_data_tablename}_version").fetchone()[0]) if version!=_globalvar.db_version: @@ -90,6 +97,32 @@ 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))) connection.commit() +_last_result=tuple() +def _exec_re(match_data, content: bytes, ret: list): + matched=None; content_str=None + try: + matched=re.search(match_data[0], content.decode('utf-8'))!=None + content_str=bytes(re.sub(match_data[0], match_data[1], content.decode('utf-8')), 'utf-8') + except UnicodeDecodeError: + matched=re.search(bytes(match_data[0], 'utf-8'), content)!=None + content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content) + assert matched!=None + assert content_str!=None + ret+=[matched, content_str] + return (matched, content_str) +def _exec_str(match_data, content: bytes, ret: list): + matched=None; content_str=None + try: + matched=match_data[0] in content.decode('utf-8') + content_str=bytes(content.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') + except UnicodeDecodeError: + matched=bytes(match_data[0], 'utf-8') in content + content_str=content.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) + assert matched!=None + assert content_str!=None + ret+=[matched, content_str] + return (matched, content_str) + def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False) -> bytes: # Match order: # 1. Match rules with exactcmdmatch option set @@ -98,20 +131,21 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # 3. Match rules without command filter # retrieve a list of effective commands matching first argument + _connection=sqlite3.connect(db_path) final_cmdlist=[] final_cmdlist_exactmatch=[] if command!=None and len(command.split())>0: # command without paths (e.g. /usr/bin/bash -> bash) stripped_command=os.path.basename(command.split()[0])+" "+(_globalvar.splitarray_to_string(command.split()[1:]) if len(command.split())>1 else '') # obtain a list of effective_command with the same first term - cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ? or effective_command LIKE ?;", (command.split()[0].strip()+" %", stripped_command.split()[0].strip()+" %")).fetchall() + cmdlist=_connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ? or effective_command LIKE ?;", (command.split()[0].strip()+" %", stripped_command.split()[0].strip()+" %")).fetchall() # also include one-phrase commands - cmdlist+=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? or effective_command=?;", (command.split()[0].strip(),stripped_command.split()[0].strip())).fetchall() + cmdlist+=_connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? or effective_command=?;", (command.split()[0].strip(),stripped_command.split()[0].strip())).fetchall() # sort by number of phrases (greatest to least) def split_len(obj: tuple) -> int: return len(obj[0].split()) cmdlist.sort(key=split_len, reverse=True) # prioritize effective_command with exact match requirement - cmdlist=connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE (effective_command=? OR effective_command=?) AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),re.sub(r" {2,}", " ", stripped_command).strip())).fetchall()+cmdlist + cmdlist=_connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE (effective_command=? OR effective_command=?) AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),re.sub(r" {2,}", " ", stripped_command).strip())).fetchall()+cmdlist def process_smartcmdmatch_phrases(match_cmd: str) -> list[str]: match_cmd_phrases=[] for p in range(len(match_cmd.split())): @@ -156,12 +190,12 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F nonlocal matches # try the ones with locale defined for this_locale in locales: - fetch_data=connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND effective_locale=? ORDER BY rowid;", filter_data+(this_locale,)).fetchall() + fetch_data=_connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND effective_locale=? ORDER BY rowid;", filter_data+(this_locale,)).fetchall() if len(fetch_data)>0: matches+=fetch_data return # else, fetches the ones without locale defined - matches+=connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND typeof(effective_locale)=typeof(null) ORDER BY rowid;", filter_data).fetchall() + matches+=_connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND typeof(effective_locale)=typeof(null) ORDER BY rowid;", filter_data).fetchall() if len(final_cmdlist)>0: for x in range(len(final_cmdlist)): cmd=final_cmdlist[x] @@ -171,25 +205,46 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F fetch_matches_by_locale("effective_command=? AND command_match_strictness!=2", (cmd,)) fetch_matches_by_locale("typeof(effective_command)=typeof(null)") encountered_ids=set() + # timeout value for each regex match + timeout=0.2 + # Flag to enable creating separate processes for each operation + # Using multiprocessing has SERIOUS performance downsides; currently disabled + enable_multiprocessing=False 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 else: encountered_ids.add(match_data[5]) matched=False if match_data[2]==True: # is regex - try: - matched=re.search(match_data[0], content_str.decode('utf-8'))!=None - content_str=bytes(re.sub(match_data[0], match_data[1], content_str.decode('utf-8')), 'utf-8') - except UnicodeDecodeError: - matched=re.search(bytes(match_data[0], 'utf-8'), content_str)!=None - content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) + if enable_multiprocessing: + with multiprocessing.Manager() as manager: + ret=manager.list() + pr=multiprocessing.Process(target=_exec_re, args=(match_data,content_str,ret)) + pr.start();pr.join(timeout=timeout) + if pr.is_alive(): + pr.terminate() + raise TimeoutError("regex match timeout") + break + else: + matched, content_str=ret + else: + ret=[]; _exec_re(match_data,content_str,ret) + matched, content_str=ret else: # is string - try: - matched=match_data[0] in content_str.decode('utf-8') - content_str=bytes(content_str.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') - except UnicodeDecodeError: - 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 enable_multiprocessing: + with multiprocessing.Manager() as manager: + ret=manager.list() + pr=multiprocessing.Process(target=_exec_str, args=(match_data,content_str, ret)) + pr.start();pr.join(timeout=timeout) + if pr.is_alive(): + pr.terminate() + raise TimeoutError("string match timeout") + break + else: + matched, content_str=ret + else: + ret=[]; _exec_str(match_data,content_str,ret) + matched, content_str=ret if match_data[3]==True and matched: # endmatchhere is set break return content_str \ No newline at end of file diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 82cf940..48c7e29 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -16,6 +16,7 @@ import signal import struct import copy import re +import concurrent.futures from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print @@ -27,7 +28,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[bytes], debug_mode: list[str], is_stderr: bool=False, matched: bool=False) -> list[bytes]: +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] @@ -50,7 +51,7 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa 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 ''}m"+('e' if is_stderr else 'o')+'\x1b[0;1m'+(">")+"\x1b[0m ",'utf-8')+line+b"\x1b[0m" + line=bytes(f"\x1b[0;1;{'31' if is_stderr else '32'}{';47' if matched else ''}{';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 @@ -103,23 +104,10 @@ def _handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True # 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 - # if child process not in cbreak mode, output the characters - # if termios.tcgetattr(stdin_fd)[3] & termios.ICANON: - # os.write(sys.stdout.fileno(), data) - # # output a new line if return key is pressed and ends on \r - # if data.endswith(b'\r'): os.write(sys.stdout.fileno(), b'\n') - # if not data: break os.write(stdout_fd, data) - # ^C pressed - # spell-checker:ignore IGNBRK BRKINT - # if data==b'\x03' and (not termios.tcgetattr(stdout_fd)[0] & termios.IGNBRK) and termios.tcgetattr(stdout_fd)[0] & termios.BRKINT: process.send_signal(signal.SIGINT) def handle_output(is_stderr: bool): data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) do_subst_operation=True - # nonlocal last_input_content - # print(last_input_content, data, data==last_input_content) - # if data==last_input_content: do_subst_operation=False - # last_input_content=None lines=data.splitlines(keepends=True) for x in range(len(lines)): line=lines[x] @@ -133,6 +121,17 @@ def _handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True if stderr_fd in fds: handle_output(is_stderr=True) if process.poll()!=None and len(output_lines)==0: break + def process_line(line: bytes, line_data): + # subst operation + subst_line=copy.copy(line) + failed=False + try: + if do_subst and line_data[2]==True: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) + except TimeoutError: failed=True + 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 + executor=concurrent.futures.ThreadPoolExecutor() + futures=[] # Process outputs for x in range(len(output_lines)): line_data=output_lines[x] @@ -149,12 +148,13 @@ def _handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True line_data=(line_data[0],line_data[1],False) last_input_content=last_input_content[len(line):] else: last_input_content=None + futures.append(executor.submit(process_line, line, line_data)) # subst operation - subst_line=copy.copy(line) - if do_subst and line_data[2]==True: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) - if line_data[2]==True: subst_line=_process_debug([subst_line], debug_mode, is_stderr=line_data[1], matched=not subst_line==line)[0] - os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), subst_line) + # os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) else: output_lines=[] # happens when no 'break' statement occurs + # Print outputs + for thread in futures: + os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), thread.result()) except KeyboardInterrupt: try: process.send_signal(signal.SIGINT) except KeyboardInterrupt: pass -- Gitee From 59eaa5dbe690b2f25b21ce90e6349e82fde2e81f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 1 Jun 2024 22:24:40 +0800 Subject: [PATCH 210/354] Improve multiprocessing handling in db_interface --- src/clitheme/_generator/db_interface.py | 97 +++++++++---------------- 1 file changed, 36 insertions(+), 61 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index afc628b..93120b4 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -97,32 +97,6 @@ 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))) connection.commit() -_last_result=tuple() -def _exec_re(match_data, content: bytes, ret: list): - matched=None; content_str=None - try: - matched=re.search(match_data[0], content.decode('utf-8'))!=None - content_str=bytes(re.sub(match_data[0], match_data[1], content.decode('utf-8')), 'utf-8') - except UnicodeDecodeError: - matched=re.search(bytes(match_data[0], 'utf-8'), content)!=None - content_str=re.sub(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content) - assert matched!=None - assert content_str!=None - ret+=[matched, content_str] - return (matched, content_str) -def _exec_str(match_data, content: bytes, ret: list): - matched=None; content_str=None - try: - matched=match_data[0] in content.decode('utf-8') - content_str=bytes(content.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') - except UnicodeDecodeError: - matched=bytes(match_data[0], 'utf-8') in content - content_str=content.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) - assert matched!=None - assert content_str!=None - ret+=[matched, content_str] - return (matched, content_str) - def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False) -> bytes: # Match order: # 1. Match rules with exactcmdmatch option set @@ -204,47 +178,48 @@ 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)") - encountered_ids=set() # timeout value for each regex match - timeout=0.2 - # Flag to enable creating separate processes for each operation - # Using multiprocessing has SERIOUS performance downsides; currently disabled - enable_multiprocessing=False + timeout=0.5 + # Flag to enable creating a separate process for the match operation + # May impact performance; function timeout not available if disabled + enable_multiprocessing=True + if enable_multiprocessing: + with multiprocessing.Manager() as manager: + ret=manager.list() + pr=multiprocessing.Process(target=_handle_subst, args=(matches, content_str, is_stderr, ret)) + pr.start(); pr.join(timeout=timeout) + if pr.is_alive(): + pr.terminate() + raise TimeoutError("match operation timeout") + else: content_str=ret[0] + else: + content_str=_handle_subst(matches, content_str, is_stderr) + return content_str +def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, ret: Optional[list[bytes]]=None): + content_str=copy.copy(content) + encountered_ids=set() 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 + if match_data[5] in encountered_ids: continue # check uuid else: encountered_ids.add(match_data[5]) matched=False if match_data[2]==True: # is regex - if enable_multiprocessing: - with multiprocessing.Manager() as manager: - ret=manager.list() - pr=multiprocessing.Process(target=_exec_re, args=(match_data,content_str,ret)) - pr.start();pr.join(timeout=timeout) - if pr.is_alive(): - pr.terminate() - raise TimeoutError("regex match timeout") - break - else: - matched, content_str=ret - else: - ret=[]; _exec_re(match_data,content_str,ret) - matched, content_str=ret + try: + ret_val: tuple=re.subn(match_data[0], match_data[1], content_str.decode('utf-8')) + matched=ret_val[1]>0 + content_str=bytes(ret_val[0], 'utf-8') + except UnicodeDecodeError: + ret_val: tuple=re.subn(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) + matched=ret_val[1]>0 + content_str=ret_val[0] else: # is string - if enable_multiprocessing: - with multiprocessing.Manager() as manager: - ret=manager.list() - pr=multiprocessing.Process(target=_exec_str, args=(match_data,content_str, ret)) - pr.start();pr.join(timeout=timeout) - if pr.is_alive(): - pr.terminate() - raise TimeoutError("string match timeout") - break - else: - matched, content_str=ret - else: - ret=[]; _exec_str(match_data,content_str,ret) - matched, content_str=ret + try: + matched=match_data[0] in content_str.decode('utf-8') + content_str=bytes(content_str.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') + except UnicodeDecodeError: + 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 - return content_str \ No newline at end of file + if ret!=None: ret.append(content_str) + return content_str -- Gitee From c27512f5b98fc92a2fb19f2ddd7cf6d31b098f74 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 1 Jun 2024 23:15:38 +0800 Subject: [PATCH 211/354] Further improvements to timeout handling of match operations --- src/clitheme/_generator/db_interface.py | 54 ++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 93120b4..81abb6d 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -10,7 +10,8 @@ import sqlite3 import re import copy import uuid -import multiprocessing, threading +import time +import multiprocessing, concurrent.futures from typing import Optional from .. import _globalvar, frontend @@ -184,17 +185,52 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # May impact performance; function timeout not available if disabled enable_multiprocessing=True if enable_multiprocessing: - with multiprocessing.Manager() as manager: - ret=manager.list() - pr=multiprocessing.Process(target=_handle_subst, args=(matches, content_str, is_stderr, ret)) - pr.start(); pr.join(timeout=timeout) - if pr.is_alive(): - pr.terminate() - raise TimeoutError("match operation timeout") - else: content_str=ret[0] + global _process + if _process==None or (_process!=None and not _process.is_alive()): + _init_process() + result_id=uuid.uuid4() + global _input_values; _input_values.append((matches, content_str, is_stderr, result_id)) + for _ in range(int(timeout*1000)): + time.sleep(0.001) + if result_id in _return_values.keys(): + content_str=_return_values[result_id] + del _return_values[result_id] + break + else: # executed when no "break" happens + _process.terminate() # type: ignore + raise TimeoutError("match operation timeout") else: content_str=_handle_subst(matches, content_str, is_stderr) return content_str + +# --The following implementation is for setting a timeout capacity on content match functions-- + # - A main loop is started for handling substitution requests and returns the corresponding content based on UUID + # - If the main loop times out due to catastrophic backtracking or other issues, match_content terminates the loop + # - The main loop is checked and restored (if needed) every time match_content is called, while preserving input queue and return values (resumes seamlessly) +_manager=multiprocessing.Manager() +_process: Optional[multiprocessing.Process]=None +_input_values=_manager.list() # (matches, content, is_stderr, uuid) +_return_values=_manager.dict() # uuid : content_str +def _init_process(): + global _process, _input_values, _return_values + if _process!=None and _process.is_alive(): _process.terminate() + _process=multiprocessing.Process(target=__process_main_loop, args=(_input_values, _return_values)) + try: _process.start() + except AssertionError: _init_process() # handle "cannot start a process twice" error by trying again +def __process_main_loop(input_vals: list, return_vals: dict): + with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor: + def handler(content): + nonlocal return_vals + return_str=_handle_subst(content[0], content[1], content[2]) + return_vals[content[3]]=return_str + while True: + time.sleep(0.001) + try: + while len(input_vals)>0: + content=input_vals.pop(0) + executor.submit(handler, content) + except: break + def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, ret: Optional[list[bytes]]=None): content_str=copy.copy(content) encountered_ids=set() -- Gitee From 2a8c85d87c2b035d10a8078ccd5672e542594292 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 1 Jun 2024 23:15:46 +0800 Subject: [PATCH 212/354] Small fix in db_interface_tests --- src/db_interface_tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index 5f41d5c..48fb77f 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -16,7 +16,7 @@ sample_inputs=[("rm: missing operand", "rm"), ("Error: invaild input ","input anything"), # test extra spaces ("Error: sample message", "example_app --this install-stuff"), # test strictcmdmatch (substitution should not happen) ("Error: sample message", "example_app install-stuff --this"), # test strictcmdmatch and endmatchhere options - ("rm: : Permission denied", "rm -rf"), # test exactcmdmatch (substitution rule containing this option should be prioritized over previous rules) + ("rm: : Operation not permitted", "rm file.ban"), # test exactcmdmatch ("example_app: using recursive directories", "example_app -rlc"), # test smartcmdmatch ("example_app: using list options", "/usr/bin/example_app -rlc"), # test smartcmdmatch and command basename handling ] @@ -80,9 +80,9 @@ substrules_file=r""" [/substitute_string] endmatchhere set_options exactcmdmatch - filter_command rm -rf - [substitute_regex] (?P.+): (?P.+): Permission denied - locale:default \g says: Missing argument for operation! ಥ_ಥ + filter_command rm file.ban + [substitute_regex] (?P.+): (?P.+): Operation not permitted + locale:default \g says: Operation not permitted! ಥ_ಥ locale:zh_CN \g 说:缺少操作参数!ಥ_ಥ [/substitute_regex] -- Gitee From 8ff6b559c24eff4042def1cc420a8cd801f75921 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 1 Jun 2024 23:21:11 +0800 Subject: [PATCH 213/354] Update version (v2.0-dev20240601) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 2d90395..510f856 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240529' +pkgver='2.0_dev20240601' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 3eddd26..ae867d8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240529-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240601-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 29 May 2024 21:24:00 +0800 + -- swiftycode <3291929745@qq.com> Sat, 01 Jun 2024 23:20:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 0bb62da..0d97258 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -2,11 +2,11 @@ # Version definition file; define the package version here # The __version__ variable must be a literal string; DO NOT use variables -__version__="2.0-dev20240529" +__version__="2.0-dev20240601" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240529" +version_main="2.0_dev20240601" version_buildnumber=1 \ No newline at end of file -- Gitee From eeaaf463af0ef70ff5955d66f4433c30e0723db7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 1 Jun 2024 23:38:04 +0800 Subject: [PATCH 214/354] Add daemon=True to db_interface process Avoid hangs after program exits --- src/clitheme/_generator/db_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 81abb6d..bdd2b1e 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -214,7 +214,7 @@ _return_values=_manager.dict() # uuid : content_str def _init_process(): global _process, _input_values, _return_values if _process!=None and _process.is_alive(): _process.terminate() - _process=multiprocessing.Process(target=__process_main_loop, args=(_input_values, _return_values)) + _process=multiprocessing.Process(target=__process_main_loop, args=(_input_values, _return_values), daemon=True) try: _process.start() except AssertionError: _init_process() # handle "cannot start a process twice" error by trying again def __process_main_loop(input_vals: list, return_vals: dict): -- Gitee From 1e56e5d9c06b26c0c0b8994a40c1e6fabed7d18c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 1 Jun 2024 23:42:54 +0800 Subject: [PATCH 215/354] Optimize debug output of failed matches Make the text color white so that it's more visible --- 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 48c7e29..324ccc6 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -51,7 +51,7 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa 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 ''}{';41' if failed else ''}m"+('e' if is_stderr else 'o')+'\x1b[0;1m'+(">")+"\x1b[0m ",'utf-8')+line+b"\x1b[0m" + 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 -- Gitee From 07194f6a0b67c223324cf153c648842436f891b7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 2 Jun 2024 07:34:52 +0800 Subject: [PATCH 216/354] Further improve multiprocessing handling in db_interface match_content will not start counting time when the operation is queued in ThreadPoolExecutor, so that the chance of accidentally timing out is much lower. --- src/clitheme/_generator/db_interface.py | 31 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index bdd2b1e..d45c8df 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -190,14 +190,20 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F _init_process() result_id=uuid.uuid4() global _input_values; _input_values.append((matches, content_str, is_stderr, result_id)) - for _ in range(int(timeout*1000)): + counter=0 + while counter0: - content=input_vals.pop(0) - executor.submit(handler, content) + executor.submit(handler) except: break def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, ret: Optional[list[bytes]]=None): -- Gitee From ee2e6e550b57d9248082a0815bbae8d6e7b67182 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 2 Jun 2024 10:29:32 +0800 Subject: [PATCH 217/354] Fixes in multiprocessing handling in db_interface --- src/clitheme/_generator/db_interface.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index d45c8df..8eedb92 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -11,6 +11,7 @@ import re import copy import uuid import time +import signal import multiprocessing, concurrent.futures from typing import Optional from .. import _globalvar, frontend @@ -191,6 +192,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F result_id=uuid.uuid4() global _input_values; _input_values.append((matches, content_str, is_stderr, result_id)) counter=0 + watchdog_timer=0 while counter=1.000: + _init_process() + _input_values.append((matches, content_str, is_stderr, result_id)) + watchdog_timer=0 else: # executed when no "break" happens try: del _return_values[result_id] except: pass - if _process.is_alive(): _process.terminate() # type: ignore + _init_process() # restart the process raise TimeoutError("match operation timeout") else: content_str=_handle_subst(matches, content_str, is_stderr) @@ -234,10 +243,12 @@ def __process_main_loop(input_vals: list, return_vals: dict): return_str=_handle_subst(content[0], content[1], content[2]) return_vals[content[3]]=return_str while True: - time.sleep(0.001) + try: time.sleep(0.001) + except KeyboardInterrupt: pass try: while len(input_vals)>0: executor.submit(handler) + except KeyboardInterrupt: pass except: break def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, ret: Optional[list[bytes]]=None): -- Gitee From 55a8f2a797cdcefeff9d8b6ab29815873c1e01c0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 2 Jun 2024 10:30:07 +0800 Subject: [PATCH 218/354] Properly handle internal exceptions in clitheme-exec Reset terminal attributes and color mode, and then raise exception --- src/clitheme/exec/output_handler_posix.py | 5 +++++ src/clitheme/strings/exec-strings.clithemedef.txt | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 324ccc6..03e38a7 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -158,5 +158,10 @@ def _handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True except KeyboardInterrupt: try: process.send_signal(signal.SIGINT) except KeyboardInterrupt: pass + except Exception as exc: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes + print("\x1b[0m", end='') # reset color + _labeled_print(fd.feof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) + raise exc termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes return process.poll() diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index da6bdb1..242f42e 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -68,4 +68,7 @@ in_domainapp swiftycode clitheme [entry] command-fail-err locale:zh_CN 错误:无法执行命令:{msg} [/entry] + [entry] internal-error-err + locale:zh_CN 错误:执行命令时发生内部错误(执行已终止): + [/entry] {/entries_section} \ No newline at end of file -- Gitee From ce2c751fc0ec4a210899c612bb62f9ac5bac29a4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 2 Jun 2024 12:16:03 +0800 Subject: [PATCH 219/354] Add process watchdog in db_interface Ensure that only one subprocess is running at all times --- src/clitheme/_generator/db_interface.py | 36 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 8eedb92..5e9f6b9 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -186,8 +186,8 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # May impact performance; function timeout not available if disabled enable_multiprocessing=True if enable_multiprocessing: - global _process - if _process==None or (_process!=None and not _process.is_alive()): + global _running_processes_ids + if len(_running_processes_ids)==0: _init_process() result_id=uuid.uuid4() global _input_values; _input_values.append((matches, content_str, is_stderr, result_id)) @@ -226,13 +226,35 @@ _manager=multiprocessing.Manager() _process: Optional[multiprocessing.Process]=None _input_values=_manager.list() # (matches, content, is_stderr, uuid) _return_values=_manager.dict() # uuid : content_str (uuid:None means processing) + +_watchdog_process: Optional[multiprocessing.Process]=None +_running_processes_ids=_manager.list() + def _init_process(): - global _process, _input_values, _return_values - if _process!=None and _process.is_alive(): _process.terminate() - _process=multiprocessing.Process(target=__process_main_loop, args=(_input_values, _return_values), daemon=True) + global _process, _input_values, _return_values, _watchdog_process, _running_processes_ids + # if _process!=None and _process.is_alive(): _process.terminate() + if _watchdog_process==None: + _watchdog_process=multiprocessing.Process(name="process_watchdog", target=__process_watchdog, args=(_running_processes_ids,), daemon=True) + _watchdog_process.start() + _process=multiprocessing.Process(name="subst_content_handler", target=__process_main_loop, args=(_input_values, _return_values, _running_processes_ids), daemon=True) try: _process.start() - except AssertionError: _init_process() # handle "cannot start a process twice" error by trying again -def __process_main_loop(input_vals: list, return_vals: dict): + except AssertionError: _init_process();return # handle "cannot start a process twice" error by trying again + # _running_processes_ids.append(_process.pid) +# watchdog to terminate any processes other than the current one +def __process_watchdog(running_ids): + while True: + try: + time.sleep(0.01) + # kill all processes except the most recently started one (last in list) + l=running_ids[:-1] + for pid in l: + try: + os.kill(pid, signal.SIGTERM) + running_ids.remove(pid) + except: pass + except KeyboardInterrupt: pass +def __process_main_loop(input_vals: list, return_vals: dict, process_ids: list): + process_ids.append(os.getpid()) with concurrent.futures.ThreadPoolExecutor() as executor: def handler(): nonlocal return_vals, input_vals -- Gitee From 4b647d7b7fe1e27de8cad34c8adc0e0e73fd27a3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 2 Jun 2024 12:42:33 +0800 Subject: [PATCH 220/354] Fix typo in db_interface_tests --- src/db_interface_tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index 48fb77f..fc976e8 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -12,8 +12,8 @@ sample_inputs=[("rm: missing operand", "rm"), ("ls: /etc/secret: Permission denied","ls /etc/secret"), ("ls: /etc/secret: Permission denied","wef ls /etc/secret"), # test first phrase detection (substitution should not happen) ("ls: unrecognized option '--help'", "ls --help"), - ("Warning: invaild input", "input anything"), - ("Error: invaild input ","input anything"), # test extra spaces + ("Warning: invalid input", "input anything"), + ("Error: invalid input ","input anything"), # test extra spaces ("Error: sample message", "example_app --this install-stuff"), # test strictcmdmatch (substitution should not happen) ("Error: sample message", "example_app install-stuff --this"), # test strictcmdmatch and endmatchhere options ("rm: : Operation not permitted", "rm file.ban"), # test exactcmdmatch @@ -67,8 +67,8 @@ substrules_file=r""" locale:default (ToT)/~~~ Error:\g<1> locale:zh_CN (ToT)/~~~ 错误: [/substitute_regex] - [substitute_regex] invaild input( )*$ - locale:default input is invaild! ಥ_ಥ + [substitute_regex] invalid input( )*$ + locale:default input is invalid! ಥ_ಥ locale:zh_CN 无效输入!ಥ_ಥ [/substitute_regex] -- Gitee From f3c0b0fb57059969c9caf013867e0619fc50d303 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 2 Jun 2024 15:08:44 +0800 Subject: [PATCH 221/354] Further attempts at fixing multiprocessing handling --- src/clitheme/_generator/db_interface.py | 46 ++++++++++++----------- src/clitheme/exec/output_handler_posix.py | 2 +- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 5e9f6b9..d6befef 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -13,6 +13,7 @@ import uuid import time import signal import multiprocessing, concurrent.futures +import queue from typing import Optional from .. import _globalvar, frontend @@ -190,7 +191,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F if len(_running_processes_ids)==0: _init_process() result_id=uuid.uuid4() - global _input_values; _input_values.append((matches, content_str, is_stderr, result_id)) + global _input_values; _input_values.put((matches, content_str, is_stderr, result_id)) counter=0 watchdog_timer=0 while counter=1.000: + if watchdog_timer>=1.500: _init_process() - _input_values.append((matches, content_str, is_stderr, result_id)) + _input_values.put((matches, content_str, is_stderr, result_id)) watchdog_timer=0 else: # executed when no "break" happens try: del _return_values[result_id] @@ -224,7 +225,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # - The main loop is checked and restored (if needed) every time match_content is called, while preserving input queue and return values (resumes seamlessly) _manager=multiprocessing.Manager() _process: Optional[multiprocessing.Process]=None -_input_values=_manager.list() # (matches, content, is_stderr, uuid) +_input_values=multiprocessing.Queue() # (matches, content, is_stderr, uuid) _return_values=_manager.dict() # uuid : content_str (uuid:None means processing) _watchdog_process: Optional[multiprocessing.Process]=None @@ -253,25 +254,26 @@ def __process_watchdog(running_ids): running_ids.remove(pid) except: pass except KeyboardInterrupt: pass -def __process_main_loop(input_vals: list, return_vals: dict, process_ids: list): +def __process_main_loop(input_vals: multiprocessing.Queue, return_vals: dict, process_ids: list): process_ids.append(os.getpid()) - with concurrent.futures.ThreadPoolExecutor() as executor: - def handler(): - nonlocal return_vals, input_vals - # the function might be called extra times if operation is queued, so a check is performed - if len(input_vals)<=0: return - content=input_vals.pop(0) - return_vals[content[3]]=None # Processing - return_str=_handle_subst(content[0], content[1], content[2]) - return_vals[content[3]]=return_str - while True: - try: time.sleep(0.001) - except KeyboardInterrupt: pass - try: - while len(input_vals)>0: - executor.submit(handler) - except KeyboardInterrupt: pass - except: break + #executor=concurrent.futures.ThreadPoolExecutor(max_workers=32) + def handler(): + nonlocal return_vals, input_vals + # the function might be called extra times if operation is queued, so a check is performed + content: tuple + try: content=input_vals.get_nowait() + except queue.Empty: return + return_vals[content[3]]=None # Processing + return_str=_handle_subst(content[0], content[1], content[2]) + return_vals[content[3]]=return_str + while True: + try: time.sleep(0.001) + except KeyboardInterrupt: pass + try: + #executor.submit(handler) + handler() + except KeyboardInterrupt: pass + # except: break def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, ret: Optional[list[bytes]]=None): content_str=copy.copy(content) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 03e38a7..5cd9380 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -79,6 +79,7 @@ def _handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True 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 + executor=concurrent.futures.ThreadPoolExecutor() while True: try: # update terminal attributes from what the program sets @@ -130,7 +131,6 @@ def _handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True except TimeoutError: failed=True 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 - executor=concurrent.futures.ThreadPoolExecutor() futures=[] # Process outputs for x in range(len(output_lines)): -- Gitee From ff853c9e33973fa0d6823c7448fa101a293815d6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 2 Jun 2024 19:27:25 +0800 Subject: [PATCH 222/354] Fix typo at output_handler_posix --- 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 5cd9380..7dba0da 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -161,7 +161,7 @@ def _handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True except Exception as exc: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes print("\x1b[0m", end='') # reset color - _labeled_print(fd.feof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) + _labeled_print(fd.reof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) raise exc termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes return process.poll() -- Gitee From 2b0a725f4520aabafccc0c2d2fb5e1ed365f0107 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 3 Jun 2024 20:04:04 +0800 Subject: [PATCH 223/354] Switch to a working implementation of function timeout --- src/clitheme/_generator/db_interface.py | 25 ++++++++++-------- src/clitheme/exec/output_handler_posix.py | 31 ++++++++++++++++++----- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index d6befef..5b87700 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -181,11 +181,7 @@ 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)") - # timeout value for each regex match - timeout=0.5 - # Flag to enable creating a separate process for the match operation - # May impact performance; function timeout not available if disabled - enable_multiprocessing=True + global enable_multiprocessing, match_timeout if enable_multiprocessing: global _running_processes_ids if len(_running_processes_ids)==0: @@ -194,7 +190,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F global _input_values; _input_values.put((matches, content_str, is_stderr, result_id)) counter=0 watchdog_timer=0 - while counter Date: Mon, 3 Jun 2024 20:04:10 +0800 Subject: [PATCH 224/354] Update cspell words list --- cspell.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index dc147e5..abe81f3 100644 --- a/cspell.json +++ b/cspell.json @@ -18,8 +18,8 @@ "subststdoutonly", "subststderronly", "substall", "substvar", "substesc", "PKGBUILD", "MANPATH", - "tcgetattr", "tcsetattr", - "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", + "tcgetattr", "tcsetattr", "getpid", "setitimer", + "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", "SIGALRM", "ITIMER", "showchars", "keepends", "appname", "domainapp", "sanitycheck", "debugmode", "splitarray", "disablelang" -- Gitee From 5be49826a6c0a8193d22900d214e3e0ad52cf752 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 3 Jun 2024 21:07:34 +0800 Subject: [PATCH 225/354] Optimize logic of option processing; - Small fix in substvar handling in set_variable --- src/clitheme/_generator/__init__.py | 35 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 5305c34..c69dbdc 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -129,17 +129,27 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info def is_ignore_line() -> bool: return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') + ## Defined option groups + lead_indent_options=["leadtabindents", "leadspaces"] + content_subst_options=["substesc","substvar"] + command_filter_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"] + subst_limiting_options=["subststdoutonly", "subststderronly", "substall"] + + # options used in handle_block_input + block_input_options=lead_indent_options+content_subst_options + + # value options: options requiring an integer value + value_options=lead_indent_options + # on/off options (use no<...> to disable) + bool_options=content_subst_options+["endmatchhere"] + # only one of these options can be set to true at the same time (specific to groups) + switch_options=[command_filter_options] + # Disable these options for now (BETA) + # switch_options+=[subst_limiting_options] + ## sub-processing functions and handlers def parse_options(options_data: list[str], merge_global_options: int, allowed_options: Optional[list]=None) -> dict: nonlocal global_options - # value options: options requiring an integer value - value_options=["leadtabindents", "leadspaces"] - # on/off options (use no<...> to disable) - bool_options=["substesc", "substvar", "endmatchhere"] - # only one of these options can be set to true at the same time (specific to groups) - switch_options=[["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"]] - # Disable these options for now (BETA) - # switch_options+=[["subststdoutonly", "subststderronly", "substall"]] final_options={} if merge_global_options!=0: final_options=copy.copy(global_options if merge_global_options==1 else really_really_global_options) @@ -220,8 +230,9 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info var_content=_globalvar.extract_content(line_content) # subst variable references - if "substvar" in global_options and global_options["substvar"]==True: - var_content=subst_variable_content(var_content) + check_list=really_really_global_options if really_really_global else global_options + if "substvar" in check_list and check_list["substvar"]==True: + var_content=subst_variable_content(var_content, override_check=True) # set variable if really_really_global: really_really_global_variables[var_name]=var_content else: global_variables[var_name]=var_content @@ -353,7 +364,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info else: substrules_entries.append((entry_name, content, None if this_locale=="default" else this_locale)); substrules_entries_linenumber.append(lineindex+1) elif phrases[0]==end_phrase: if not is_substrules: check_extra_args(phrases, 1, use_exact_count=True) - got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=["endmatchhere", "subststdoutonly", "subststderronly", "substall"]) + got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=subst_limiting_options+["endmatchhere"]) for option in got_options: if option=="endmatchhere" and got_options['endmatchhere']==True: substrules_endmatchhere=True @@ -517,7 +528,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # parse strictcmdmatch, exactcmdmatch, and other cmdmatch options here got_options=copy.copy(global_options) if len(lines_data[lineindex].split())>1: - got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True, allowed_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch"]) + got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True, allowed_options=block_input_options+command_filter_options) for this_option in got_options: if this_option=="strictcmdmatch" and got_options['strictcmdmatch']==True: strictness=1 -- Gitee From eb9839377236360acbb7b8c87da145147a11e828 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 3 Jun 2024 21:28:11 +0800 Subject: [PATCH 226/354] Add help string for internal modules; Change some function names --- src/clitheme/_generator/__init__.py | 1 + src/clitheme/_generator/db_interface.py | 10 +++++++--- src/clitheme/_globalvar.py | 1 + src/clitheme/_version.py | 3 +++ src/clitheme/exec/__init__.py | 2 +- src/clitheme/exec/output_handler_posix.py | 6 +++++- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index c69dbdc..66b30ad 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -25,6 +25,7 @@ fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subse path="" silence_warn=False +__all__=["generate_data_hierarchy"] def handle_error(message: str): raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 5b87700..597adf1 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -4,6 +4,10 @@ # 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 . +""" +Interface for adding and matching substitution entries in database (internal module) +""" + import sys import os import sqlite3 @@ -32,7 +36,7 @@ class need_db_regenerate(Exception): class bad_pattern(Exception): pass -def handle_warning(message: str): +def _handle_warning(message: str): if debug_mode: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) def init_db(file_path: str): global connection, db_path @@ -83,7 +87,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma match_condition=f"match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition} AND stdout_stderr_only=? AND is_regex=?" match_params=(match_pattern, effective_locale, stdout_stderr_matchoption, is_regex) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: - handle_warning(fd.feof("repeated-substrules-warn", "Repeated substrules entry at line {num}, overwriting", num=line_number_debug)) + _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))) @@ -94,7 +98,7 @@ def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_comma match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=? AND is_regex=?" match_params=(match_pattern, cmd, effective_locale, stdout_stderr_matchoption, is_regex) if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: - handle_warning(fd.feof("repeated-substrules-warn", "Repeated substrules entry at line {num}, overwriting", num=line_number_debug)) + _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))) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 9bb494f..dc8e3c1 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -101,6 +101,7 @@ def sanity_check(path: str, use_orig: bool=False) -> bool: return True ## Convenience functions + def splitarray_to_string(split_content): final="" for phrase in split_content: diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 0d97258..26b5565 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,3 +1,6 @@ +""" +Version information definition file +""" # spell-checker:ignore buildnumber # Version definition file; define the package version here diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 60d3ca8..504eb31 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -130,7 +130,7 @@ def main(arguments: list[str]): if not _check_regenerate_db(): return 1 # determine platform if os.name=="posix": - return output_handler_posix._handler_main(arguments[1+argcount:], debug_mode, subst) + return output_handler_posix.handler_main(arguments[1+argcount:], debug_mode, subst) elif os.name=="nt": _labeled_print("Error: Windows platform is not currently supported") return 1 diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 9e4ce9b..39fc49b 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -4,6 +4,10 @@ # 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 . +""" +Main output processing handler for Unix/Linux systems (internal module) +""" + import subprocess import sys import os @@ -55,7 +59,7 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa final_lines.append(line) return final_lines -def _handler_main(command: list[str], debug_mode: list[str]=[], 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() -- Gitee From ce02bee0047d9f9321d8400157c814af01c12967 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 3 Jun 2024 21:56:20 +0800 Subject: [PATCH 227/354] Update version (v2.0-dev20240603) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 510f856..cbcb946 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240601' +pkgver='2.0_dev20240603' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index ae867d8..3199165 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240601-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240603-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sat, 01 Jun 2024 23:20:00 +0800 + -- swiftycode <3291929745@qq.com> Mon, 03 Jun 2024 21:54:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 26b5565..58f7cfb 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-dev20240601" +__version__="2.0-dev20240603" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240601" +version_main="2.0_dev20240603" version_buildnumber=1 \ No newline at end of file -- Gitee From bb9f0261f027928b376ca16cae16845a54c62ff7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 15:30:27 +0800 Subject: [PATCH 228/354] Fix substrules database migration during --overlay operation --- src/clitheme/_generator/__init__.py | 10 +++++++++- src/clitheme/_generator/db_interface.py | 6 +++--- src/clitheme/exec/__init__.py | 10 ++++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 66b30ad..aed2038 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -103,6 +103,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # make directories if custom_path_gen: generate_custom_path() + global path if not os.path.exists(path): os.mkdir(path) datapath=path+"/"+_globalvar.generator_data_pathname if not os.path.exists(datapath): os.mkdir(datapath) @@ -512,7 +513,14 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info command_filter_strictness=0 # initialize the database if os.path.exists(path+"/"+_globalvar.db_filename): - db_interface.connection=db_interface.sqlite3.connect(path+"/"+_globalvar.db_filename) + try: db_interface.connect_db(path=path+"/"+_globalvar.db_filename) + except db_interface.need_db_regenerate: + # the following import statement changes the path, so we make a copy of it + path_copy=path + from ..exec import _check_regenerate_db + if not _check_regenerate_db(path_copy): exit(1) + path=path_copy + db_interface.connect_db(path=path+"/"+_globalvar.db_filename) else: db_interface.init_db(path+"/"+_globalvar.db_filename) db_interface.debug_mode=not silence_warn while lineindex bool: +def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) -> bool: try: db_interface.connect_db() except db_interface.need_db_regenerate: _labeled_print(fd.reof("substrules-migrate-msg", "Migrating substrules database...")) @@ -56,12 +55,14 @@ def _check_regenerate_db() -> bool: 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") sys.stdout=sys.__stdout__ - os.remove(_globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) - shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, _globalvar.clitheme_root_data_path+"/"+_globalvar.db_filename) + try: os.remove(dest_root_path+"/"+_globalvar.db_filename) + except FileNotFoundError: raise + shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, dest_root_path+"/"+_globalvar.db_filename) _labeled_print(fd.reof("db-migrate-success-msg", "Successfully completed migration, proceeding execution")) except: sys.stdout=sys.__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]))) + raise return False except FileNotFoundError: pass except: @@ -130,6 +131,7 @@ def main(arguments: list[str]): if not _check_regenerate_db(): return 1 # determine platform if os.name=="posix": + from . import output_handler_posix return output_handler_posix.handler_main(arguments[1+argcount:], debug_mode, subst) elif os.name=="nt": _labeled_print("Error: Windows platform is not currently supported") -- Gitee From 5d9bb23f127122c71be2347be5ef2996ccbcce38 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 15:32:42 +0800 Subject: [PATCH 229/354] Add example to db_filename definition --- 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 dc8e3c1..abbf39b 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -57,7 +57,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" +db_filename="subst-data.db" # e.g. ~/.local/share/clitheme/subst-data.db db_version=2 ## Sanity check function -- Gitee From d84b0af6eed2200fc6363a02ddc43c6bd7879e09 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 16:51:11 +0800 Subject: [PATCH 230/354] Add _dataclass and _handlers --- src/clitheme/_generator/_dataclass.py | 297 ++++++++++++++++++++++++++ src/clitheme/_generator/_handlers.py | 93 ++++++++ 2 files changed, 390 insertions(+) create mode 100644 src/clitheme/_generator/_dataclass.py create mode 100644 src/clitheme/_generator/_handlers.py diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py new file mode 100644 index 0000000..3f50dc9 --- /dev/null +++ b/src/clitheme/_generator/_dataclass.py @@ -0,0 +1,297 @@ +# 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 . + +""" +Class object for sharing data between section parsers (internal module) +""" + +import os +import sys +import string +import random +import re +import math +import copy +import gzip +import uuid +from typing import Optional +from .. import _globalvar, frontend +from . import _handlers, db_interface +# spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption + +class GeneratorObject(_handlers.DataHandlers): + ## Defined option groups + lead_indent_options=["leadtabindents", "leadspaces"] + content_subst_options=["substesc","substvar"] + command_filter_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"] + subst_limiting_options=["subststdoutonly", "subststderronly", "substall"] + + # options used in handle_block_input + block_input_options=lead_indent_options+content_subst_options + + # value options: options requiring an integer value + value_options=lead_indent_options + # on/off options (use no<...> to disable) + bool_options=content_subst_options+["endmatchhere"] + # only one of these options can be set to true at the same time (specific to groups) + switch_options=[command_filter_options] + # Disable these options for now (BETA) + # switch_options+=[subst_limiting_options] + + def __init__(self, file_content: str, custom_infofile_name: str, filename: str, path: str, silence_warn: bool): + # data to keep track of + self.section_parsing=False + self.parsed_sections=[] + self.lines_data=file_content.splitlines() + self.lineindex=-1 # counter extra +1 operation at beginning + self.global_options={} + self.really_really_global_options={} # options defined outside any sections + self.global_variables={} + self.really_really_global_variables={} # variables defined outside any sections + + self.custom_infofile_name=custom_infofile_name + self.filename=filename + _handlers.DataHandlers.__init__(self, path, silence_warn) + self.db_interface=db_interface + def is_ignore_line(self) -> bool: + return self.lines_data[self.lineindex].strip()=="" or self.lines_data[self.lineindex].strip().startswith('#') + def check_enough_args(self, phrases: list[str], count: int): + if 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=phrases[0])) + def parse_options(self, options_data: list[str], merge_global_options: int, allowed_options: Optional[list]=None) -> dict: + 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 + 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) + 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=option_name)) + if option_name_preserve_no in self.value_options: # must not begin with "no" + # get value + results=re.search(r"^(?P.+?):(?P.+)+$", each_option) + value: int + if results==None: # no value specified + self.handle_error(self.fd.feof("option-without-value-err", "No value specified for option \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=option_name)) + else: + try: value=int(results.groupdict()['value']) + except ValueError: self.handle_error(self.fd.feof("option-value-not-int-err", "The value specified for option \"{phrase}\" is not an integer on line {num}", num=str(self.lineindex+1), phrase=option_name)) + # set option + final_options[option_name]=value + elif option_name in self.bool_options: + # if starts with no, set to false; else, set to true + final_options[option_name]=not option_name_preserve_no.startswith("no") + else: + for option_group in self.switch_options: + if option_name_preserve_no in option_group: + for opt in options_data: + if opt!=option_name_preserve_no and opt in option_group: + self.handle_error(self.fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(self.lineindex+1), option1=option_name_preserve_no, option2=opt)) + # set all other options to false + for opt in option_group: final_options[opt]=False + # set the option + final_options[option_name_preserve_no]=True + 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=option_name_preserve_no)) + return final_options + 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) + else: + 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) + self.global_variables=copy.copy(self.really_really_global_variables) + def subst_variable_content(self, content: str, override_check: bool=False) -> str: + if not override_check and (not "substvar" in self.global_options or self.global_options["substvar"]==False): return content + # get all variables used in content + new_content=copy.copy(content) + variables=re.findall(r"{{(.+?)}}", content) + if len(variables)>0: + for var_name in variables: + if var_name=="ESC": continue # skip {{ESC}}; leave it for substesc + var_content: str + try: + var_content=self.global_variables[var_name] + except KeyError: + self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", num=str(self.lineindex+1), name=var_name)) + continue + new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) + return new_content + def handle_set_variable(self, line_content: str, really_really_global: bool=False): + if not line_content.split()[0].startswith("setvar:"): return + # match variable name + self.check_enough_args(line_content.split(), 2) + results=re.search(r"setvar:(?P.+)", line_content.split()[0]) + var_name: str + if results==None: + self.handle_error(self.fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="setvar:", num=str(self.lineindex+1))) + else: var_name=results.groupdict()['name'] + # 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=var_name, num=str(self.lineindex+1))) + if var_name=='ESC': bad_var() + banphrases=['{', '}', '[', ']', '(', ')'] + for char in banphrases: + if char in var_name: bad_var() + + 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) + # set variable + if really_really_global: self.really_really_global_variables[var_name]=var_content + else: 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)) + self.section_parsing=True + self.handle_setup_global_options() + def handle_end_section(self, section_name: str): + self.parsed_sections.append(section_name) + self.section_parsing=False + def handle_substesc(self, content: str) -> str: + return content.replace("{{ESC}}", "\x1b") + 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: + target_content=self.handle_substesc(target_content) + return target_content + + ## 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: + minspaces=math.inf + blockinput_data="" + while self.lineindex"+end_phrase, line.strip()) + # update minspaces + minspaces=min(minspaces, len(leading_whitespace)) + else: # don't preserve whitespaces + line=re.sub(r"^\\([\\]*)"+end_phrase, r"\g<1>"+end_phrase, line.strip()) + # write to data + blockinput_data+="\n"+line + # remove the extra leading newline + blockinput_data=re.sub(r"\A\n", "", blockinput_data) + # remove all whitespaces except common minspaces (if preserve_indents) + 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 + 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) + specified_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=False) + for option in got_options.keys(): + def is_specified_in_block() -> bool: return option in specified_options.keys() and specified_options[option]==True + if option=="leadtabindents": + if not preserve_indents 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=option)) + # 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": + if not preserve_indents 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=option)) + # 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": + if disable_substesc 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=option)) + # 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) + elif disallow_cmdmatch_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=option)) + return blockinput_data + def handle_entry(self, entry_name: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): + # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int + unique_id=uuid.uuid4() + substrules_entries=[] # (match_content, substitute_content, locale) + substrules_entries_linenumber=[] + substrules_endmatchhere=False + substrules_stdout_stderr_option=0 + if is_substrules: + # check if patterns are valid + try: re.compile(entry_name) + except: self.handle_error(self.fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(self.lineindex+1), error_msg=sys.exc_info()[1])) + while self.lineindex.+)", 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(self.lines_data[self.lineindex]) + else: + self.check_enough_args(phrases, 3) + content=_globalvar.extract_content(self.lines_data[self.lineindex], begin_phrase_count=2) + locale=phrases[1] + target_entry=copy.copy(entry_name) + content=self.handle_singleline_content(content) # handle substesc and substvar + if locale!="default": + target_entry+="__"+locale + if not is_substrules: self.add_entry(self.datapath, target_entry, content, self.lineindex+1) + else: substrules_entries.append((entry_name, content, None if locale=="default" else locale)); substrules_entries_linenumber.append(self.lineindex+1) + elif phrases[0]=="locale_block" or phrases[0]=="[locale]": + self.check_enough_args(phrases, 2) + locales=phrases[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: + suffix="" + if this_locale!="default": + suffix="__"+this_locale + if not is_substrules: self.add_entry(self.datapath, entry_name+suffix, content, self.lineindex+1) + else: substrules_entries.append((entry_name, content, None if this_locale=="default" else this_locale)); substrules_entries_linenumber.append(self.lineindex+1) + elif phrases[0]==end_phrase: + if not is_substrules: self.check_extra_args(phrases, 1, use_exact_count=True) + got_options=self.parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=self.subst_limiting_options+["endmatchhere"]) + 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 + break + else: self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(self.lineindex+1))) + if is_substrules: + for x in range(len(substrules_entries)): + entry=substrules_entries[x] + try: self.db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x], unique_id=unique_id) + 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=str(substrules_entries_linenumber[x]), error_msg=sys.exc_info()[1])) \ No newline at end of file diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py new file mode 100644 index 0000000..78cd539 --- /dev/null +++ b/src/clitheme/_generator/_handlers.py @@ -0,0 +1,93 @@ +# 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 . + +""" +Functions for data processing and others (internal module) +""" +import os +import sys +import string +import random +import re +import math +import copy +import gzip +import uuid +from typing import Optional +from .. import _globalvar, frontend + +# spell-checker:ignore datapath + +class DataHandlers: + fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") + def __init__(self, path: str, silence_warn: bool): + self.path=path + self.silence_warn=silence_warn + if not os.path.exists(self.path): os.mkdir(self.path) + self.datapath=self.path+"/"+_globalvar.generator_data_pathname + if not os.path.exists(self.datapath): os.mkdir(self.datapath) + def handle_error(self, message: str): + raise SyntaxError(self.fd.feof("error-str", "Syntax error: {msg}", msg=message)) + def handle_warning(self, message: str): + if not self.silence_warn: print(self.fd.feof("warning-str", "Warning: {msg}", msg=message)) + def recursive_mkdir(self, path: str, entry_name: str, line_number_debug: int): # recursively generate directories (excluding file itself) + current_path=path + current_entry="" # for error output + for x in entry_name.split()[:-1]: + current_entry+=x+" " + current_path+="/"+x + if os.path.isfile(current_path): # conflict with entry file + self.handle_error(self.fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ + num=str(line_number_debug), name=current_entry)) + elif os.path.isdir(str(current_path))==False: # directory does not exist + os.mkdir(current_path) + def add_entry(self, path: str, entry_name: str, entry_content: str, line_number_debug: int): # add entry to where it belongs (assuming recursive_mkdir already completed) + target_path=path + for x in entry_name.split(): + target_path+="/"+x + if os.path.isdir(target_path): + self.handle_error(self.fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ + num=str(line_number_debug), name=entry_name)) + elif os.path.isfile(target_path): + self.handle_warning(self.fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ + num=str(line_number_debug), name=entry_name)) + f=open(target_path,'w', encoding="utf-8") + f.write(entry_content+"\n") + def write_infofile(self, path: str, filename: str, content: str, line_number_debug: int, header_name_debug: str): + if not os.path.isdir(path): + os.makedirs(path) + target_path=path+"/"+filename + if os.path.isfile(target_path): + self.handle_warning(self.fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ + num=str(line_number_debug), name=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[str], line_number_debug: int, header_name_debug: str): + if not os.path.isdir(path): + os.makedirs(path) + target_path=path+"/"+filename + if os.path.isfile(target_path): + self.handle_warning(self.fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ + num=str(line_number_debug), name=header_name_debug)) + 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[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 + 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))) + # write the compressed and original version of the file + full_path=parent_path+"/"+file_path[-1] + if os.path.isfile(full_path): + self.handle_warning(self.fd.feof("repeated-manpage-warn","Line {num}: repeated manpage file, overwriting", num=str(line_number_debug))) + try: + 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 -- Gitee From d2866e09ff1a7ac1a976387c91a80c03b5cf60d0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 18:21:33 +0800 Subject: [PATCH 231/354] Add individual parser files --- src/clitheme/_generator/_entries_parser.py | 73 ++++++++++++++ src/clitheme/_generator/_header_parser.py | 67 +++++++++++++ src/clitheme/_generator/_manpage_parser.py | 68 +++++++++++++ src/clitheme/_generator/_substrules_parser.py | 96 +++++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 src/clitheme/_generator/_entries_parser.py create mode 100644 src/clitheme/_generator/_header_parser.py create mode 100644 src/clitheme/_generator/_manpage_parser.py create mode 100644 src/clitheme/_generator/_substrules_parser.py diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py new file mode 100644 index 0000000..42b7ef8 --- /dev/null +++ b/src/clitheme/_generator/_entries_parser.py @@ -0,0 +1,73 @@ +# 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 . + +""" +entries_section parser function (internal module) +""" +from typing import Optional +from .. import _globalvar +from . import _dataclass + +# 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): + obj.handle_begin_section("entries") + # --Process entries/main block-- + end_phrase="end_main" if first_phrase=="begin_main" else r"{/entries_section}" + if first_phrase=="begin_main": + obj.handle_warning(obj.fd.feof("syntax-phrase-deprecation-warn", "Line {num}: phrase \"{old_phrase}\" is deprecated in this version; please use \"{new_phrase}\" instead", num=str(obj.lineindex+1), old_phrase="begin_main", new_phrase=r"{entries_section}")) + domainapp="" + subsection="" + while obj.lineindex. + +""" +header_section parser function (internal module) +""" +import re +from typing import Optional +from .. import _globalvar +from . import _dataclass + +# 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): + obj.handle_begin_section("header") + # --Process header block-- + end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" + while obj.lineindex. + +""" +substrules_section parser function (internal module) +""" +import os +import copy +import sys +from typing import Optional +from .. import _globalvar +from . import _dataclass + +def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): + obj.handle_begin_section("manpage") + ## --Process manpage block-- + end_phrase="{/manpage_section}" + while obj.lineindex0 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() + 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) + else: + 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]=="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 phrases[0]==end_phrase: + obj.check_extra_args(phrases, 1, use_exact_count=True) + obj.handle_end_section("manpage") + break + ## END --Process manpage block-- diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py new file mode 100644 index 0000000..8c3eed0 --- /dev/null +++ b/src/clitheme/_generator/_substrules_parser.py @@ -0,0 +1,96 @@ +# 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 . + +""" +substrules_section parser function (internal module) +""" +import os +import copy +from typing import Optional +from .. import _globalvar +from . import _dataclass + +# 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): + obj.handle_begin_section("substrules") + ## --Process substrules block-- + end_phrase=r"{/substrules_section}" + command_filters: Optional[list[str]]=None + command_filter_strictness=0 + # initialize the database + if os.path.exists(obj.path+"/"+_globalvar.db_filename): + try: obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) + except obj.db_interface.need_db_regenerate: + # the following import statement changes the path, so we make a copy of it + path_copy=obj.path + from ..exec import _check_regenerate_db + if not _check_regenerate_db(path_copy): exit(1) + path=path_copy + obj.db_interface.connect_db(path=path+"/"+_globalvar.db_filename) + else: obj.db_interface.init_db(obj.path+"/"+_globalvar.db_filename) + obj.db_interface.debug_mode=not obj.silence_warn + while obj.lineindex1: + 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) + for this_option in got_options: + if this_option=="strictcmdmatch" and got_options['strictcmdmatch']==True: + strictness=1 + elif this_option=="exactcmdmatch" and got_options['exactcmdmatch']==True: + strictness=2 + elif this_option=="smartcmdmatch" and got_options['smartcmdmatch']==True: + strictness=-1 + 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) + content=_globalvar.splitarray_to_string(phrases[1:]) + content=obj.subst_variable_content(content) + strictness=0 + for this_option in obj.global_options: + if this_option=="strictcmdmatch" and obj.global_options['strictcmdmatch']==True: + strictness=1 + elif this_option=="exactcmdmatch" and obj.global_options['exactcmdmatch']==True: + strictness=2 + elif this_option=="smartcmdmatch" and obj.global_options['smartcmdmatch']==True: + strictness=-1 + command_filters=[content] + command_filter_strictness=strictness + 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]": + obj.check_enough_args(phrases, 2) + options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} + match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) + match_pattern=obj.handle_singleline_content(match_pattern) # handle substesc and substvar + obj.handle_entry(match_pattern, 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 phrases[0]==end_phrase: + obj.check_extra_args(phrases, 1, use_exact_count=True) + obj.handle_end_section("substrules") + break + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(obj.lineindex+1))) + ## END --Process substrules block-- \ No newline at end of file -- Gitee From 72490baa98ad65433296b3b264b18642d7d8643c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 18:24:45 +0800 Subject: [PATCH 232/354] Implement handle_set_themedef in GeneratorObject --- src/clitheme/_generator/__init__.py | 5 ++++- src/clitheme/_generator/_handlers.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index aed2038..3c219a5 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -18,6 +18,8 @@ import gzip import uuid from typing import Optional from .. import _globalvar, frontend +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 @@ -649,4 +651,5 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info # prevent circular import error try: from . import db_interface except ImportError: import db_interface -_globalvar.handle_set_themedef(frontend, "generator") \ No newline at end of file +_globalvar.handle_set_themedef(frontend, "generator") +_globalvar.handle_set_themedef(_dataclass.GeneratorObject.frontend, "generator") \ No newline at end of file diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index 78cd539..cd1f534 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -22,13 +22,15 @@ from .. import _globalvar, frontend # spell-checker:ignore datapath class DataHandlers: - fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") + frontend=frontend + def __init__(self, path: str, silence_warn: bool): self.path=path self.silence_warn=silence_warn if not os.path.exists(self.path): os.mkdir(self.path) self.datapath=self.path+"/"+_globalvar.generator_data_pathname if not os.path.exists(self.datapath): os.mkdir(self.datapath) + self.fd=self.frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") def handle_error(self, message: str): raise SyntaxError(self.fd.feof("error-str", "Syntax error: {msg}", msg=message)) def handle_warning(self, message: str): -- Gitee From dab04e35f17d015887b7ae7c915a79f9af5f8979 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 18:26:08 +0800 Subject: [PATCH 233/354] Remove unused imports in _generator files --- src/clitheme/_generator/_dataclass.py | 6 +----- src/clitheme/_generator/_handlers.py | 7 ------- src/clitheme/_generator/_manpage_parser.py | 1 - 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 3f50dc9..857fc29 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -8,17 +8,13 @@ Class object for sharing data between section parsers (internal module) """ -import os import sys -import string -import random import re import math import copy -import gzip import uuid from typing import Optional -from .. import _globalvar, frontend +from .. import _globalvar from . import _handlers, db_interface # spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index cd1f534..8f70f72 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -8,14 +8,7 @@ Functions for data processing and others (internal module) """ import os -import sys -import string -import random -import re -import math -import copy import gzip -import uuid from typing import Optional from .. import _globalvar, frontend diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index ce118a0..6dcf554 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -8,7 +8,6 @@ substrules_section parser function (internal module) """ import os -import copy import sys from typing import Optional from .. import _globalvar -- Gitee From a84eb5fade7173d7f5945122977ecbd628f4297c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 21:25:39 +0800 Subject: [PATCH 234/354] Change _generator to use newly-created class objects; - Set return value on generate_data_hierarchy and change to using that return value as much as possible - Optimize logic on frontend.set_local_themedef --- src/clitheme-testblock_testprogram.py | 11 +- src/clitheme/_generator/__init__.py | 639 +----------------- src/clitheme/_generator/_dataclass.py | 5 +- src/clitheme/_generator/_substrules_parser.py | 7 +- src/clitheme/cli.py | 13 +- src/clitheme/exec/__init__.py | 2 +- src/clitheme/frontend.py | 18 +- src/clithemedef-test_testprogram.py | 10 +- src/db_interface_tests.py | 6 +- 9 files changed, 62 insertions(+), 649 deletions(-) diff --git a/src/clitheme-testblock_testprogram.py b/src/clitheme-testblock_testprogram.py index 4c2a162..af3b642 100644 --- a/src/clitheme-testblock_testprogram.py +++ b/src/clitheme-testblock_testprogram.py @@ -76,13 +76,4 @@ for lang in ["C", "en", "en_US", "zh_CN"]: if f.entry_exists(name): print(f"{name} found") else: - print(f"{name} not found") - -import sys -if "--preserve-temp" in sys.argv: - print(f"View generated data at {_generator.path}") - exit() - -import shutil -try: shutil.rmtree(_generator.path) -except: print("Warning: failed to remove temp directory \"{}\"".format(_generator.path)) \ No newline at end of file + print(f"{name} not found") \ No newline at end of file diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 3c219a5..edc049e 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -7,649 +7,68 @@ """ Generator function used in applying themes (should not be invoked directly) """ -import os -import sys import string import random -import re -import math -import copy -import gzip -import uuid from typing import Optional -from .. import _globalvar, frontend -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 -fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") - path="" silence_warn=False __all__=["generate_data_hierarchy"] -def handle_error(message: str): - raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) -def handle_warning(message: str): - if not silence_warn: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) -def recursive_mkdir(path: str, entry_name: str, line_number_debug: int): # recursively generate directories (excluding file itself) - current_path=path - current_entry="" # for error output - for x in entry_name.split()[:-1]: - current_entry+=x+" " - current_path+="/"+x - if os.path.isfile(current_path): # conflict with entry file - handle_error(fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ - num=str(line_number_debug), name=current_entry)) - elif os.path.isdir(str(current_path))==False: # directory does not exist - os.mkdir(current_path) -def add_entry(path: str, entry_name: str, entry_content: str, line_number_debug: int): # add entry to where it belongs (assuming recursive_mkdir already completed) - target_path=path - for x in entry_name.split(): - target_path+="/"+x - if os.path.isdir(target_path): - handle_error(fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ - num=str(line_number_debug), name=entry_name)) - elif os.path.isfile(target_path): - handle_warning(fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ - num=str(line_number_debug), name=entry_name)) - f=open(target_path,'w', encoding="utf-8") - f.write(entry_content+"\n") -def write_infofile(path: str, filename: str, content: str, line_number_debug: int, header_name_debug: str): - if not os.path.isdir(path): - os.makedirs(path) - target_path=path+"/"+filename - if os.path.isfile(target_path): - handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) - f=open(target_path,'w', encoding="utf-8") - f.write(content+'\n') - -def write_infofile_newlines(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 - if os.path.isfile(target_path): - handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) - f=open(target_path,'w', encoding="utf-8") - for line in content_phrases: - f.write(line+"\n") - -def write_manpage_file(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 path+"/"+_globalvar.generator_manpage_pathname - parent_path+='/'+os.path.dirname(_globalvar.splitarray_to_string(file_path).replace(" ","/")) - # create the parent directory - try: os.makedirs(parent_path, exist_ok=True) - except (FileExistsError, NotADirectoryError): - handle_error(fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) - # write the compressed and original version of the file - full_path=parent_path+"/"+file_path[-1] - if os.path.isfile(full_path): - handle_warning(fd.feof("repeated-manpage-warn","Line {num}: repeated manpage file, overwriting", num=str(line_number_debug))) - try: - open(full_path, "w", encoding="utf-8").write(content) - open(full_path+".gz", "wb").write(gzip.compress(bytes(content, "utf-8"))) - except IsADirectoryError: - handle_error(fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug))) - -def generate_custom_path(): +def generate_custom_path() -> str: # Generate a temporary path global path path=_globalvar.clitheme_temp_root+"/clitheme-temp-" for x in range(8): path+=random.choice(string.ascii_letters) + return path + -def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1", filename: str=""): +def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1", filename: str="") -> str: # make directories if custom_path_gen: generate_custom_path() global path - if not os.path.exists(path): os.mkdir(path) - datapath=path+"/"+_globalvar.generator_data_pathname - if not os.path.exists(datapath): os.mkdir(datapath) - - # data to keep track of - section_parsing=False - parsed_sections=[] - lines_data=file_content.splitlines() - lineindex=-1 # counter extra +1 operation at beginning - global_options={} - really_really_global_options={} # options defined outside any sections - global_variables={} - really_really_global_variables={} # variables defined outside any sections - - ## check functions - def check_enough_args(phrases: list[str], count: int): - if len(phrases)count - if not_pass: - handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=phrases[0])) - def is_ignore_line() -> bool: - return lines_data[lineindex].strip()=="" or lines_data[lineindex].strip().startswith('#') - - ## Defined option groups - lead_indent_options=["leadtabindents", "leadspaces"] - content_subst_options=["substesc","substvar"] - command_filter_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"] - subst_limiting_options=["subststdoutonly", "subststderronly", "substall"] - - # options used in handle_block_input - block_input_options=lead_indent_options+content_subst_options - - # value options: options requiring an integer value - value_options=lead_indent_options - # on/off options (use no<...> to disable) - bool_options=content_subst_options+["endmatchhere"] - # only one of these options can be set to true at the same time (specific to groups) - switch_options=[command_filter_options] - # Disable these options for now (BETA) - # switch_options+=[subst_limiting_options] - - ## sub-processing functions and handlers - def parse_options(options_data: list[str], merge_global_options: int, allowed_options: Optional[list]=None) -> dict: - nonlocal global_options - - final_options={} - if merge_global_options!=0: final_options=copy.copy(global_options if merge_global_options==1 else really_really_global_options) - if len(options_data)==0: return final_options # return either empty data or pre-existing global options - 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) - if allowed_options!=None and option_name not in allowed_options: - handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option_name)) - if option_name_preserve_no in value_options: # must not begin with "no" - # get value - results=re.search(r"^(?P.+?):(?P.+)+$", each_option) - value: int - if results==None: # no value specified - handle_error(fd.feof("option-without-value-err", "No value specified for option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name)) - else: - try: value=int(results.groupdict()['value']) - except ValueError: handle_error(fd.feof("option-value-not-int-err", "The value specified for option \"{phrase}\" is not an integer on line {num}", num=str(lineindex+1), phrase=option_name)) - # set option - final_options[option_name]=value - elif option_name in bool_options: - # if starts with no, set to false; else, set to true - final_options[option_name]=not option_name_preserve_no.startswith("no") - else: - for option_group in switch_options: - if option_name_preserve_no in option_group: - for opt in options_data: - if opt!=option_name_preserve_no and opt in option_group: - handle_error(fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(lineindex+1), option1=option_name_preserve_no, option2=opt)) - # set all other options to false - for opt in option_group: final_options[opt]=False - # set the option - final_options[option_name_preserve_no]=True - break - else: # executed when no break occurs - handle_error(fd.feof("unknown-option-err", "Unknown option \"{phrase}\" on line {num}", num=str(lineindex+1), phrase=option_name_preserve_no)) - return final_options - def handle_set_global_options(options_data: list[str], really_really_global: bool=False): - # set options globally - if really_really_global: nonlocal really_really_global_options; really_really_global_options=parse_options(options_data, merge_global_options=2) - else: nonlocal global_options; global_options=parse_options(options_data, merge_global_options=1) - def handle_setup_global_options(): - # reset global_options to contents of really_really_global_options - nonlocal global_options; global_options=copy.copy(really_really_global_options) - nonlocal global_variables; global_variables=copy.copy(really_really_global_variables) - def subst_variable_content(content: str, override_check: bool=False) -> str: - if not override_check and (not "substvar" in global_options or global_options["substvar"]==False): return content - # get all variables used in content - new_content=copy.copy(content) - variables=re.findall(r"{{(.+?)}}", content) - if len(variables)>0: - for var_name in variables: - if var_name=="ESC": continue # skip {{ESC}}; leave it for substesc - var_content: str - try: - var_content=global_variables[var_name] - except KeyError: - handle_warning(fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", num=str(lineindex+1), name=var_name)) - continue - new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) - return new_content - def handle_set_variable(line_content: str, really_really_global: bool=False): - nonlocal global_variables, really_really_global_variables - if not line_content.split()[0].startswith("setvar:"): return - # match variable name - check_enough_args(line_content.split(), 2) - results=re.search(r"setvar:(?P.+)", line_content.split()[0]) - var_name: str - if results==None: - handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="setvar:", num=str(lineindex+1))) - else: var_name=results.groupdict()['name'] - # sanity check var_name - def bad_var(): handle_error(fd.feof("bad-var-name-err", "Line {num}: \"{name}\" is not a valid variable name", name=var_name, num=str(lineindex+1))) - if var_name=='ESC': bad_var() - banphrases=['{', '}', '[', ']', '(', ')'] - for char in banphrases: - if char in var_name: bad_var() - - var_content=_globalvar.extract_content(line_content) - # subst variable references - check_list=really_really_global_options if really_really_global else global_options - if "substvar" in check_list and check_list["substvar"]==True: - var_content=subst_variable_content(var_content, override_check=True) - # set variable - if really_really_global: really_really_global_variables[var_name]=var_content - else: global_variables[var_name]=var_content - def handle_begin_section(section_name: str): - nonlocal parsed_sections - if section_name in parsed_sections: - handle_error(fd.feof("repeated-section-err", "Repeated {section} section at line {num}", num=str(lineindex+1), section=section_name)) - nonlocal section_parsing; section_parsing=True - handle_setup_global_options() - def handle_end_section(section_name: str): - nonlocal parsed_sections; parsed_sections.append(section_name) - nonlocal section_parsing; section_parsing=False - def handle_substesc(content: str) -> str: - return content.replace("{{ESC}}", "\x1b") - def handle_singleline_content(content: str) -> str: - target_content=copy.copy(content) - target_content=subst_variable_content(target_content) - if "substesc" in global_options.keys() and global_options['substesc']==True: - target_content=handle_substesc(target_content) - return target_content - - ## sub-block processing functions - def handle_block_input(preserve_indents: bool, preserve_empty_lines: bool, end_phrase: str="end_block", disallow_cmdmatch_options: bool=True, disable_substesc: bool=False) -> str: - nonlocal lineindex - minspaces=math.inf - blockinput_data="" - while lineindex"+end_phrase, line.strip()) - # update minspaces - minspaces=min(minspaces, len(leading_whitespace)) - else: # don't preserve whitespaces - line=re.sub(r"^\\([\\]*)"+end_phrase, r"\g<1>"+end_phrase, line.strip()) - # write to data - blockinput_data+="\n"+line - # remove the extra leading newline - blockinput_data=re.sub(r"\A\n", "", blockinput_data) - # remove all whitespaces except common minspaces (if preserve_indents) - 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 - got_options=copy.copy(global_options) - specified_options={} - if len(lines_data[lineindex].split())>1: - got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True) - specified_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=False) - for option in got_options.keys(): - def is_specified_in_block() -> bool: return option in specified_options.keys() and specified_options[option]==True - if option=="leadtabindents": - if not preserve_indents and is_specified_in_block(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) - # 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": - if not preserve_indents and is_specified_in_block(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) - # 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": - if disable_substesc and is_specified_in_block(): handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) - # substitute {{ESC}} with escape literal - if got_options['substesc']==True and not disable_substesc: blockinput_data=handle_substesc(blockinput_data) - elif option=="substvar": - if got_options['substvar']==True: blockinput_data=subst_variable_content(blockinput_data, True) - elif disallow_cmdmatch_options: - handle_error(fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(lineindex+1), phrase=option)) - return blockinput_data - def handle_entry(entry_name: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): - # substrules_options: effective_commands: list[str], is_regex: bool, strictness: int - nonlocal lineindex - unique_id=uuid.uuid4() - substrules_entries=[] # (match_content, substitute_content, locale) - substrules_entries_linenumber=[] - substrules_endmatchhere=False - substrules_stdout_stderr_option=0 - if is_substrules: - # check if patterns are valid - try: re.compile(entry_name) - except: handle_error(fd.feof("bad-match-pattern-err", "Bad match pattern at line {num} ({error_msg})", num=str(lineindex+1), error_msg=sys.exc_info()[1])) - while lineindex.+)", phrases[0]) - if results==None: - handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="locale:", num=str(lineindex+1))) - else: - locale=results.groupdict()['locale'] - content=_globalvar.extract_content(lines_data[lineindex]) - else: - check_enough_args(phrases, 3) - content=_globalvar.extract_content(lines_data[lineindex], begin_phrase_count=2) - locale=phrases[1] - target_entry=copy.copy(entry_name) - content=handle_singleline_content(content) # handle substesc and substvar - if locale!="default": - target_entry+="__"+locale - if not is_substrules: add_entry(datapath, target_entry, content, lineindex+1) - else: substrules_entries.append((entry_name, content, None if locale=="default" else locale)); substrules_entries_linenumber.append(lineindex+1) - elif phrases[0]=="locale_block" or phrases[0]=="[locale]": - check_enough_args(phrases, 2) - locales=phrases[1:] - content=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: - suffix="" - if this_locale!="default": - suffix="__"+this_locale - if not is_substrules: add_entry(datapath, entry_name+suffix, content, lineindex+1) - else: substrules_entries.append((entry_name, content, None if this_locale=="default" else this_locale)); substrules_entries_linenumber.append(lineindex+1) - elif phrases[0]==end_phrase: - if not is_substrules: check_extra_args(phrases, 1, use_exact_count=True) - got_options=parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=subst_limiting_options+["endmatchhere"]) - 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 - break - else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) - if is_substrules: - for x in range(len(substrules_entries)): - entry=substrules_entries[x] - try: db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x], unique_id=unique_id) - except db_interface.bad_pattern: handle_error(fd.feof("bad-subst-pattern-err", "Bad substitute pattern at line {num} ({error_msg})", num=str(substrules_entries_linenumber[x]), error_msg=sys.exc_info()[1])) + obj=_dataclass.GeneratorObject(file_content=file_content, custom_infofile_name=custom_infofile_name, filename=filename, path=path, silence_warn=silence_warn) ## Main code - while lineindex1: - got_options=parse_options(lines_data[lineindex].split()[1:], merge_global_options=True, allowed_options=block_input_options+command_filter_options) - for this_option in got_options: - if this_option=="strictcmdmatch" and got_options['strictcmdmatch']==True: - strictness=1 - elif this_option=="exactcmdmatch" and got_options['exactcmdmatch']==True: - strictness=2 - elif this_option=="smartcmdmatch" and got_options['smartcmdmatch']==True: - strictness=-1 - command_filters=[] - for cmd in command_strings: - command_filters.append(cmd.strip()) - command_filter_strictness=strictness - elif phrases[0]=="filter_command": - check_enough_args(phrases, 2) - content=_globalvar.splitarray_to_string(phrases[1:]) - content=subst_variable_content(content) - strictness=0 - for this_option in global_options: - if this_option=="strictcmdmatch" and global_options['strictcmdmatch']==True: - strictness=1 - elif this_option=="exactcmdmatch" and global_options['exactcmdmatch']==True: - strictness=2 - elif this_option=="smartcmdmatch" and global_options['smartcmdmatch']==True: - strictness=-1 - command_filters=[content] - command_filter_strictness=strictness - elif phrases[0]=="unset_filter_command": - check_extra_args(phrases, 1, use_exact_count=True) - command_filters=None - elif phrases[0]=="[substitute_string]" or phrases[0]=="[substitute_regex]": - check_enough_args(phrases, 2) - options={"effective_commands": copy.copy(command_filters), "is_regex": phrases[0]=="[substitute_regex]", "strictness": command_filter_strictness} - match_pattern=_globalvar.extract_content(lines_data[lineindex]) - match_pattern=handle_singleline_content(match_pattern) # handle substesc and substvar - handle_entry(match_pattern, end_phrase="[/substitute_string]" if phrases[0]=="[substitute_string]" else "[/substitute_regex]", is_substrules=True, substrules_options=options) - elif phrases[0]=="set_options": - check_enough_args(phrases, 2) - handle_set_global_options(subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) - elif phrases[0].startswith("setvar:"): - check_enough_args(phrases, 2) - handle_set_variable(lines_data[lineindex]) - elif phrases[0]==end_phrase: - check_extra_args(phrases, 1, use_exact_count=True) - handle_end_section("substrules") - break - else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(lineindex+1))) - ## END --Process substrules block-- + # the import statement it might call changes the path, so we make a copy of it + _substrules_parser.handle_substrules_section(obj, first_phrase) elif first_phrase==r"{manpage_section}": - handle_begin_section("manpage") - ## --Process manpage block-- - end_phrase="{/manpage_section}" - while lineindex0 and lines_data[lineindex].split()[0]=="as": - target_file=subst_variable_content(_globalvar.splitarray_to_string(lines_data[lineindex].split()[1:])).split() - if _globalvar.sanity_check(_globalvar.splitarray_to_string(target_file))==False: - handle_error(fd.feof("sanity-check-manpage-err", "Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories", num=str(lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) - write_manpage_file(target_file, filecontent, lineindex+1) - else: - handle_error(fd.feof("include-file-missing-phrase-err", "Missing \"as \" phrase on next line of line {num}", num=str(lineindex+1-1))) - elif phrases[0]=="set_options": - check_enough_args(phrases, 2) - handle_set_global_options(subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split()) - elif phrases[0].startswith("setvar:"): - check_enough_args(phrases, 2) - handle_set_variable(lines_data[lineindex]) - elif phrases[0]==end_phrase: - check_extra_args(phrases, 1, use_exact_count=True) - handle_end_section("manpage") - break - ## END --Process manpage block-- - else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=first_phrase, num=str(lineindex+1))) + _manpage_parser.handle_manpage_section(obj, first_phrase) + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=first_phrase, num=str(obj.lineindex+1))) - if section_parsing or not "header" in parsed_sections or (not "entries" in parsed_sections and not "substrules" in parsed_sections and not "manpage" in parsed_sections): - handle_error(fd.reof("incomplete-section-err", "Missing or incomplete header or content sections")) + if obj.section_parsing or not "header" in obj.parsed_sections or (not "entries" in obj.parsed_sections and not "substrules" in obj.parsed_sections and not "manpage" in obj.parsed_sections): + obj.handle_error(obj.fd.reof("incomplete-section-err", "Missing or incomplete header or content sections")) # record file content for database migration/upgrade feature - write_infofile(path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, "file_content", file_content, lineindex+1, "") + obj.write_infofile(obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, "file_content", obj.file_content, obj.lineindex+1, "") # Update current theme index - theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") - theme_index.write(custom_infofile_name+"\n") + theme_index=open(obj.path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") + theme_index.write(obj.custom_infofile_name+"\n") + path=obj.path + return obj.path # prevent circular import error -try: from . import db_interface -except ImportError: import db_interface -_globalvar.handle_set_themedef(frontend, "generator") +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 diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 857fc29..0eb7fcf 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -15,10 +15,11 @@ import copy import uuid from typing import Optional from .. import _globalvar -from . import _handlers, db_interface +from . import _handlers # spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption class GeneratorObject(_handlers.DataHandlers): + ## Defined option groups lead_indent_options=["leadtabindents", "leadspaces"] content_subst_options=["substesc","substvar"] @@ -50,7 +51,9 @@ 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) + from . import db_interface self.db_interface=db_interface def is_ignore_line(self) -> bool: return self.lines_data[self.lineindex].strip()=="" or self.lines_data[self.lineindex].strip().startswith('#') diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 8c3eed0..93afa58 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -25,12 +25,9 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str if os.path.exists(obj.path+"/"+_globalvar.db_filename): try: obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) except obj.db_interface.need_db_regenerate: - # the following import statement changes the path, so we make a copy of it - path_copy=obj.path from ..exec import _check_regenerate_db - if not _check_regenerate_db(path_copy): exit(1) - path=path_copy - obj.db_interface.connect_db(path=path+"/"+_globalvar.db_filename) + if not _check_regenerate_db(obj.path): exit(1) + 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 while obj.lineindex1: print(" "+f.feof("processing-file", "> Processing file {filename}...", filename=str(i+1))) @@ -72,7 +74,7 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p # Generate data hierarchy, erase current data, copy it to data path try: _generator.silence_warn=False - _generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index), filename=filenames[i] if len(filenames)>0 else "") + final_path=_generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index), filename=filenames[i] if len(filenames)>0 else "") generate_path=False # Don't generate another temp folder after first one index+=1 except SyntaxError: @@ -82,11 +84,12 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p if len(file_contents)>1: print(" "+f.reof("all-finished", "> All finished")) print(f.reof("generate-data-success", "Successfully generated data")) + global last_data_path; last_data_path=final_path if preserve_temp or generate_only: if os.name=="nt": - print(f.feof("view-temp-dir", "View at {path}", path=re.sub(r"/", r"\\", _generator.path))) # make the output look pretty + print(f.feof("view-temp-dir", "View at {path}", path=re.sub(r"/", r"\\", final_path))) # make the output look pretty else: - print(f.feof("view-temp-dir", "View at {path}", path=_generator.path)) + print(f.feof("view-temp-dir", "View at {path}", path=final_path)) if generate_only: return 0 # ---Stop here if generate_only is set--- @@ -99,13 +102,13 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p return 1 try: - shutil.copytree(_generator.path, _globalvar.clitheme_root_data_path) + shutil.copytree(final_path, _globalvar.clitheme_root_data_path) except Exception: print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) return 1 print(f.reof("apply-theme-success", "Theme applied successfully")) if not preserve_temp: - try: shutil.rmtree(_generator.path) + try: shutil.rmtree(final_path) except Exception: pass return 0 diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index f2abf04..3ba3b2d 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -57,7 +57,7 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) sys.stdout=sys.__stdout__ try: os.remove(dest_root_path+"/"+_globalvar.db_filename) except FileNotFoundError: raise - shutil.copy(cli._generator.path+"/"+_globalvar.db_filename, dest_root_path+"/"+_globalvar.db_filename) + 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")) except: sys.stdout=sys.__stdout__ diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 61a703d..11a2d0f 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -81,24 +81,24 @@ 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 - overlay_cont=False global _alt_path_dirname - if _alt_path_dirname!=None and overlay==True: # overlay - if not os.path.exists(_globalvar.clitheme_temp_root+"/"+dir_name): # check if not already generated before - overlay_cont=True - shutil.copytree(_globalvar.clitheme_temp_root+"/"+_alt_path_dirname, _generator.path) path_name=_globalvar.clitheme_temp_root+"/"+dir_name + if _alt_path_dirname!=None and overlay==True: # overlay + shutil.copytree(_globalvar.clitheme_temp_root+"/"+_alt_path_dirname, _generator.path) if global_debugmode: print("[Debug] "+path_name) # Generate data hierarchy as needed - if overlay_cont or not os.path.exists(path_name): + if not os.path.exists(path_name): _generator.silence_warn=True + return_val: str try: - _generator.generate_data_hierarchy(file_content, custom_path_gen=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])) return False - shutil.copytree(_generator.path, path_name) - try: shutil.rmtree(_generator.path) + # 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) except: pass global _alt_path _alt_path_hash=local_path_hash diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index 63c0249..e0c4762 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -12,10 +12,10 @@ for part in l: print("Testing generator function...") mainfile_data=open(root_directory+"/testprogram-data/clithemedef-test_mainfile.clithemedef.txt",'r', encoding="utf-8").read() expected_data=open(root_directory+"/testprogram-data/clithemedef-test_expected.txt",'r', encoding="utf-8").read() -funcresult=_generator.generate_data_hierarchy(mainfile_data) +generator_path=_generator.generate_data_hierarchy(mainfile_data) errorcount=0 -rootpath=_generator.path+"/"+_globalvar.generator_data_pathname +rootpath=generator_path+"/"+_globalvar.generator_data_pathname current_path="" for line in expected_data.splitlines(): if line.strip()=='' or line.strip()[0]=='#': @@ -42,7 +42,7 @@ print("Testing frontend...") from clitheme import frontend frontend.global_lang="en_US.UTF-8" frontend.global_debugmode=True -frontend.data_path=_generator.path+"/"+_globalvar.generator_data_pathname +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="" errorcount_frontend=0 @@ -77,11 +77,11 @@ print("\n\nTest results:") print("==> ",end='') if errorcount>0: print("Generator test error: "+str(errorcount)+" errors found") - print("See "+_generator.path+" for more details") + print("See "+generator_path+" for more details") exit(1) else: print("Generator test OK") - shutil.rmtree(_generator.path) # remove the temp directory + shutil.rmtree(generator_path) # remove the temp directory print("==> ",end='') if errorcount_frontend>0: print("Frontend test error: "+str(errorcount_frontend)+" errors found") diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index fc976e8..fee0415 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -108,12 +108,12 @@ substrules_file=r""" """ db_interface.debug_mode=True -_generator.generate_data_hierarchy(substrules_file) -db_interface.connection=db_interface.sqlite3.connect(_generator.path+"/"+_globalvar.db_filename) +generator_path=_generator.generate_data_hierarchy(substrules_file) +db_interface.connection=db_interface.sqlite3.connect(generator_path+"/"+_globalvar.db_filename) print("Successfully recorded data\nTesting sample outputs: ") for inp in sample_inputs: print(db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8')) -try: shutil.rmtree(_generator.path) +try: shutil.rmtree(generator_path) except: pass \ No newline at end of file -- Gitee From 75a62485101a069a60cfcb57046edfdcb71cef52 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 4 Jun 2024 21:28:42 +0800 Subject: [PATCH 235/354] Add missing spell-checker ignore flags; - Fix some misspellings in code --- src/clitheme/_generator/_manpage_parser.py | 2 ++ src/clitheme/frontend.py | 2 +- src/clithemedef-test_testprogram.py | 9 ++++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 6dcf554..4d1573f 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -13,6 +13,8 @@ from typing import Optional from .. import _globalvar from . import _dataclass +# 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): obj.handle_begin_section("manpage") ## --Process manpage block-- diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 11a2d0f..765b6fc 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -107,7 +107,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: return True def unset_local_themedef(): """ - Unsets the local theme definition file for the current frontend instance. + Unset the local theme definition file for the current frontend instance. After this operation, FetchDescriptor functions will no longer use local definitions. """ global _alt_path; _alt_path=None diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index e0c4762..13fc0b3 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -4,6 +4,9 @@ from clitheme import _globalvar import random import string import os + +# spell-checker:ignore rootpath errorcount mainfile + l=__file__.split(os.sep) l.pop() root_directory="" # directory where the script files are in @@ -65,9 +68,9 @@ for line in expected_data_frontend.splitlines(): fallback_string="" for x in range(30): # reduce inaccuracies fallback_string+=random.choice(string.ascii_letters) - recieved_content=descriptor.retrieve_entry_or_fallback(entry_path, fallback_string) - if expected_content.strip()!=recieved_content.strip(): - if recieved_content.strip()==fallback_string: + received_content=descriptor.retrieve_entry_or_fallback(entry_path, fallback_string) + if expected_content.strip()!=received_content.strip(): + if received_content.strip()==fallback_string: print("[Error] Failed to retrieve entry for \""+current_path_frontend+"\"") else: print("[Content] Content mismatch on path \""+current_path_frontend+"\"") -- Gitee From bd9effe880d097bb083497a909990818f83874ed Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 5 Jun 2024 22:05:36 +0800 Subject: [PATCH 236/354] Update version (v2.0-dev20240605) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index cbcb946..51b6386 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240603' +pkgver='2.0_dev20240605' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 3199165..9200182 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240603-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240605-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 03 Jun 2024 21:54:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 05 Jun 2024 22:03:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 58f7cfb..a6a1507 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-dev20240603" +__version__="2.0-dev20240605" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240603" +version_main="2.0_dev20240605" version_buildnumber=1 \ No newline at end of file -- Gitee From c548bdab16b16237f1c7366279c4edd684b54061 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 6 Jun 2024 11:09:16 +0800 Subject: [PATCH 237/354] handle_block_input: improve logic of checking whether an option is explicitly specified --- src/clitheme/_generator/_dataclass.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 0eb7fcf..a13eb97 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -214,16 +214,18 @@ class GeneratorObject(_handlers.DataHandlers): specified_options=self.parse_options(self.lines_data[self.lineindex].split()[1:], merge_global_options=False) for option in got_options.keys(): def is_specified_in_block() -> bool: return option in specified_options.keys() and specified_options[option]==True + 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=option)) if option=="leadtabindents": - if not preserve_indents 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=option)) + 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": - if not preserve_indents 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=option)) + 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": - if disable_substesc 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=option)) + 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": -- Gitee From f591edd8e3b84b974158ab91bcd3247bc4d510f1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 6 Jun 2024 12:42:35 +0800 Subject: [PATCH 238/354] Fix fetch_matches_by_locale logic where mixed locales won't work --- src/clitheme/_generator/db_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 6fa7cf5..feea97a 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -174,7 +174,6 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F fetch_data=_connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND effective_locale=? ORDER BY rowid;", filter_data+(this_locale,)).fetchall() if len(fetch_data)>0: matches+=fetch_data - return # else, fetches the ones without locale defined matches+=_connection.execute(f"SELECT DISTINCT {','.join(fetch_items)} FROM {_globalvar.db_data_tablename} WHERE {filter_condition} AND typeof(effective_locale)=typeof(null) ORDER BY rowid;", filter_data).fetchall() if len(final_cmdlist)>0: -- Gitee From 8438004dcf554e904f68d670bd4c91d4cc1d219b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 6 Jun 2024 12:54:25 +0800 Subject: [PATCH 239/354] Support for specifying multiple entry names/match patterns at once --- src/clitheme/_generator/_dataclass.py | 57 ++++++++++++------- src/clitheme/_generator/_entries_parser.py | 2 +- src/clitheme/_generator/_substrules_parser.py | 2 +- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index a13eb97..139478c 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -233,22 +233,33 @@ class GeneratorObject(_handlers.DataHandlers): elif disallow_cmdmatch_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=option)) return blockinput_data - def handle_entry(self, entry_name: str, end_phrase: str, is_substrules: bool=False, substrules_options: dict={}): + 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[str], is_regex: bool, strictness: int - unique_id=uuid.uuid4() - substrules_entries=[] # (match_content, substitute_content, locale) + entryNames: list[tuple]=[(entry_name, uuid.uuid4())] # For supporting specifying multiple entries at once (name, uuid) + names_processed=False # Set to True when no more entry names are being specified + substrules_entries=[] # (match_content, substitute_content, locale, uuid) substrules_entries_linenumber=[] substrules_endmatchhere=False substrules_stdout_stderr_option=0 - if is_substrules: + def check_valid_pattern(pattern: str): # check if patterns are valid - try: re.compile(entry_name) + 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(self.lineindex+1), error_msg=sys.exc_info()[1])) + if is_substrules: check_valid_pattern(entry_name) while self.lineindex1 else [], merge_global_options=True, allowed_options=self.subst_limiting_options+["endmatchhere"]) @@ -294,5 +313,5 @@ class GeneratorObject(_handlers.DataHandlers): if is_substrules: for x in range(len(substrules_entries)): entry=substrules_entries[x] - try: self.db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x], unique_id=unique_id) + try: self.db_interface.add_subst_entry(match_pattern=entry[0], 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, line_number_debug=substrules_entries_linenumber[x], 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=str(substrules_entries_linenumber[x]), error_msg=sys.exc_info()[1])) \ No newline at end of file diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index 42b7ef8..cef2c7b 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -55,7 +55,7 @@ def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): if subsection!="": entry_name=subsection+" "+entry_name if domainapp!="": entry_name=domainapp+" "+entry_name obj.recursive_mkdir(obj.datapath, entry_name, obj.lineindex+1) - obj.handle_entry(entry_name, end_phrase="[/entry]" if phrases[0]=="[entry]" else "end_entry") + 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()) diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 93afa58..03d0e62 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -78,7 +78,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} match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) match_pattern=obj.handle_singleline_content(match_pattern) # handle substesc and substvar - obj.handle_entry(match_pattern, 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="[/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()) -- Gitee From bbea6a05139b745322742564a1c2915d22214305 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 6 Jun 2024 13:49:07 +0800 Subject: [PATCH 240/354] Move location of match_timeout variable to _globalvar --- src/clitheme/_generator/db_interface.py | 2 +- src/clitheme/_globalvar.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index feea97a..f115598 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -229,7 +229,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # If False, implementation B is used in output_handler_posix under Unix/Linux enable_multiprocessing=False # timeout value for each match operation -match_timeout=0.4 +match_timeout=_globalvar.output_subst_timeout _manager=multiprocessing.Manager() _process: Optional[multiprocessing.Process]=None diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index abbf39b..2fa7e90 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -60,6 +60,9 @@ db_data_tablename="clitheme_subst_data" db_filename="subst-data.db" # e.g. ~/.local/share/clitheme/subst-data.db db_version=2 +## clitheme-exec timeout value for each output substitution operation +output_subst_timeout=0.4 + ## Sanity check function entry_banphrases=['/','\\'] startswith_banphrases=['.'] -- Gitee From bcd88de7436828668889cd7fc1290067a93c4b8f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 6 Jun 2024 14:15:31 +0800 Subject: [PATCH 241/354] Prevent extra temp directories in set_local_themedef --- src/clitheme/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 765b6fc..917eace 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -84,7 +84,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: global _alt_path_dirname path_name=_globalvar.clitheme_temp_root+"/"+dir_name if _alt_path_dirname!=None and overlay==True: # overlay - shutil.copytree(_globalvar.clitheme_temp_root+"/"+_alt_path_dirname, _generator.path) + 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) # Generate data hierarchy as needed if not os.path.exists(path_name): -- Gitee From 45cfb92906c462efb7acefe19d659924076864b0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 6 Jun 2024 14:36:59 +0800 Subject: [PATCH 242/354] Fix misplaced comment in retrieve_entry_or_fallback --- src/clitheme/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 917eace..5790a3c 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -214,9 +214,9 @@ class FetchDescriptor(): if self.debug_mode: print("Trying "+p, end=" ...") try: f=open(p,'r', encoding="utf-8") + # since the generator adds an extra newline in the entry data, we need to remove it dat=re.sub(r"\n\Z", "", f.read()) if self.debug_mode: print("Success:\n> "+dat) - # since the generator adds an extra newline in the entry data, we need to remove it return dat except (FileNotFoundError, IsADirectoryError): if self.debug_mode: print("Failed") -- Gitee From 7b35184e4c82604acad78bd917db9f5e1b092e4f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 7 Jun 2024 18:25:00 +0800 Subject: [PATCH 243/354] Change where the apply-theme confirmation prompt is placed --- src/clitheme/cli.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index c4ae49f..d41eefd 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -46,6 +46,24 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p 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), 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.")) + inpstr=f.reof("confirm-prompt", "Do you want to continue? [y/n]") + try: inp=input(inpstr+" ").strip().lower() + except (KeyboardInterrupt, EOFError): print();return 130 + 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...")) index=1 @@ -263,24 +281,6 @@ def main(cli_args: list[str]): else: paths.append(arg) fi=frontend.FetchDescriptor(subsections="cli apply-theme") - if len(paths)>1 or True: # currently set to True for now - if generate_only: - print(fi.reof("generate-data-msg", "The theme data will be generated from the following definition files in the following order:")) - else: - print(fi.reof("apply-theme-msg", "The following definition files will be applied in the following order: ")) - for i in range(len(paths)): - path=paths[i] - print("\t{}: {}".format(str(i+1), path)) - if not generate_only: - if os.path.isdir(_globalvar.clitheme_root_data_path) and overlay==False: - print(fi.reof("overwrite-notice", "The existing theme data will be overwritten if you continue.")) - if overlay==True: - print(fi.reof("overlay-notice", "The definition files will be appended on top of the existing theme data.")) - inpstr=fi.reof("confirm-prompt", "Do you want to continue? [y/n]") - try: inp=input(inpstr+" ").strip().lower() - except (KeyboardInterrupt, EOFError): print();return 130 - if not (inp=="y" or inp=="yes"): - return 1 content_list=[] for i in range(len(paths)): path=paths[i] -- Gitee From fbd7be820780b16ca5b4eb4d83344761368e8ca3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 7 Jun 2024 21:19:36 +0800 Subject: [PATCH 244/354] Remove extra raise statement in _check_regenerate_db --- src/clitheme/exec/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 3ba3b2d..ab8545e 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -62,7 +62,6 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) except: sys.stdout=sys.__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]))) - raise return False except FileNotFoundError: pass except: -- Gitee From 30c85f22c3cfdcc8613d9413a7984080d4828c3f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 7 Jun 2024 22:12:03 +0800 Subject: [PATCH 245/354] Implement update-theme feature in cli --- src/clitheme/_generator/__init__.py | 3 + src/clitheme/cli.py | 69 ++++++++++++++++--- .../strings/cli-strings.clithemedef.txt | 14 ++++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index edc049e..7493f06 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -7,6 +7,7 @@ """ Generator function used in applying themes (should not be invoked directly) """ +import os import string import random from typing import Optional @@ -61,6 +62,8 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info obj.handle_error(obj.fd.reof("incomplete-section-err", "Missing or incomplete header or content sections")) # record file content for database migration/upgrade feature obj.write_infofile(obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, "file_content", obj.file_content, obj.lineindex+1, "") + # record *full* file path for update-themes feature + obj.write_infofile(obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, _globalvar.generator_info_filename.format(info="filepath"), os.path.abspath(filename), obj.lineindex+1, "") # Update current theme index theme_index=open(obj.path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") theme_index.write(obj.custom_infofile_name+"\n") diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index d41eefd..4a271e2 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -216,6 +216,47 @@ def get_current_theme_info(): print(f.feof("list-item", "• {content}", content=app.strip())) return 0 +def update_theme(): + """ + Re-applies theme files from file paths specified in the previous apply-theme command (including all related apply-theme commands if --overlay is used) + + (Invokes 'clitheme update-theme') + """ + class some_exc(Exception): pass + 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 + if not os.path.isdir(search_path): raise some_exc(search_path+" not directory") + lsdir_result=os.listdir(search_path); lsdir_result.sort() + lsdir_num=0 + for x in lsdir_result: + if os.path.isdir(search_path+"/"+x): lsdir_num+=1 + if lsdir_num<1: raise some_exc("empty directory") + + # Get file paths from clithemeinfo_filepath files + file_paths=[] + for pathname in lsdir_result: + target_path=search_path+"/"+pathname + if (not os.path.isdir(target_path)) or re.search(r"^\d+$", pathname.strip())==None: continue # skip current_theme_index file + got_path: str + try: + got_path=open(target_path+"/"+_globalvar.generator_info_filename.format(info="filepath"), encoding="utf-8").readline().strip() + except: raise some_exc("Read error: "+str(sys.exc_info()[1])) + file_paths.append(got_path) + if len(file_paths)==0: raise some_exc("file_paths empty") + # Get file contents + try: file_contents=_get_file_contents(file_paths) + except: return 1 + except some_exc: + 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")) + return 1 + except: + print(fi.feof("other-err", "An error occurred while processing file path information: {msg}\nPlease re-apply the current theme and try again")) + return 1 + return apply_theme(file_contents, file_paths, overlay=False) + def _is_option(arg): return arg.strip()[0:1]=="-" def _handle_usage_error(message, cli_args_first): @@ -245,6 +286,19 @@ def _handle_help_message(full_help: bool=False): print("\t"+fd.reof("options-version", "--version: Outputs the current version of clitheme")) print("\t"+fd.reof("options-help", "--help: Display this help message")) +def _get_file_contents(file_paths: list[str]) -> list[str]: + fi=frontend.FetchDescriptor(subsections="cli apply-theme") + content_list=[] + for i in range(len(file_paths)): + path=file_paths[i] + try: + content_list.append(open(path, 'r', encoding="utf-8").read()) + except: + print(fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ + index=str(i+1), message=path+": "+str(sys.exc_info()[1]))) + raise + return content_list + def main(cli_args: list[str]): """ Use this function invoke 'clitheme' with command line arguments @@ -281,15 +335,9 @@ def main(cli_args: list[str]): else: paths.append(arg) fi=frontend.FetchDescriptor(subsections="cli apply-theme") - content_list=[] - for i in range(len(paths)): - path=paths[i] - try: - content_list.append(open(path, 'r', encoding="utf-8").read()) - except Exception: - print(fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ - index=str(i+1), message=str(sys.exc_info()[1]))) - return 1 + content_list: list[str] + try: content_list=_get_file_contents(paths) + except: 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 @@ -297,6 +345,9 @@ def main(cli_args: list[str]): 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)) diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 1850829..4240c36 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -138,6 +138,20 @@ in_domainapp swiftycode clitheme [entry] supported-apps-str locale:zh_CN 支持的应用程序: [/entry] + # update-theme 指令 + in_subsection cli update-theme + [entry] not-available-err + [locale] zh_CN + update-theme在当前主题设定上无法使用 + 请重新应用当前主题后重试 + [/locale] + [/entry] + [entry] other-err + [locale] zh_CN + 处理文件路径信息时发生错误:{msg} + 请重新应用当前主题后重试 + [/locale] + [/entry] in_subsection cli help-message [entry] usage-str locale:zh_CN 使用方式: -- Gitee From c083f2bb6386834486062ca1f673d867b3b3f52b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 7 Jun 2024 22:15:41 +0800 Subject: [PATCH 246/354] Add missing exception messages in _check_regenerate_db --- src/clitheme/exec/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index ab8545e..8c8f29f 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -35,12 +35,12 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) try: # gather files search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname - if not os.path.isdir(search_path): raise Exception + if not os.path.isdir(search_path): raise Exception(search_path+" not directory") lsdir_result=os.listdir(search_path); lsdir_result.sort() lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 - if lsdir_num<1: raise Exception + if lsdir_num<1: raise Exception("empty directory") file_contents=[] paths=[] -- Gitee From 9654f68ec7fe893caa250b645a20ae9d9a31a5d1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 7 Jun 2024 22:31:32 +0800 Subject: [PATCH 247/354] Add help message to update-theme command --- src/clitheme/cli.py | 9 ++------- src/clitheme/strings/cli-strings.clithemedef.txt | 3 +++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 4a271e2..9e450d4 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -19,13 +19,6 @@ from . import _globalvar, _generator, frontend # spell-checker:ignore pathnames lsdir inpstr -usage_description=\ -"""Usage: {0} apply-theme [themedef-file] [--overlay] [--preserve-temp] - {0} get-current-theme-info - {0} unset-current-theme - {0} generate-data [themedef-file] [--overlay] - {0} --version""" - frontend.global_domain="swiftycode" frontend.global_appname="clitheme" frontend.global_subsections="cli" @@ -272,6 +265,7 @@ def _handle_help_message(full_help: bool=False): """\t{0} apply-theme [themedef-file] [--overlay] [--preserve-temp] \t{0} get-current-theme-info \t{0} unset-current-theme +\t{0} update-theme \t{0} generate-data [themedef-file] [--overlay] \t{0} --version \t{0} --help""".format(arg_first) @@ -282,6 +276,7 @@ def _handle_help_message(full_help: bool=False): "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")) 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")) diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 4240c36..e32f6b2 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -172,6 +172,9 @@ in_domainapp swiftycode clitheme [entry] options-unset-current-theme locale:zh_CN unset-current-theme:取消设定当前主题定义和数据 [/entry] + [entry] options-update-theme + locale:zh_CN update-theme:重新应用上一个apply-theme操作中指定的主题定义文件(前几次操作,如果使用了"--overlay") + [/entry] [entry] options-generate-data locale:zh_CN generate-data:【仅供调试用途】对于指定的主题定义文件在临时目录中生成一个数据结构 [/entry] -- Gitee From 43fc123321522408c7afe9e2579436121be0e836 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 7 Jun 2024 22:32:00 +0800 Subject: [PATCH 248/354] Remove extra comments --- src/clitheme/_generator/__init__.py | 1 - src/clitheme/_generator/_entries_parser.py | 2 -- src/clitheme/_generator/_handlers.py | 2 +- src/clitheme/_generator/_header_parser.py | 4 +--- src/clitheme/_generator/_manpage_parser.py | 2 -- src/clitheme/_generator/_substrules_parser.py | 4 +--- 6 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 7493f06..0294865 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -52,7 +52,6 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info elif first_phrase=="begin_main" or first_phrase==r"{entries_section}": _entries_parser.handle_entries_section(obj, first_phrase) elif first_phrase==r"{substrules_section}": - # the import statement it might call changes the path, so we make a copy of it _substrules_parser.handle_substrules_section(obj, first_phrase) elif first_phrase==r"{manpage_section}": _manpage_parser.handle_manpage_section(obj, first_phrase) diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index cef2c7b..686db7e 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -15,7 +15,6 @@ from . import _dataclass def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.handle_begin_section("entries") - # --Process entries/main block-- end_phrase="end_main" if first_phrase=="begin_main" else r"{/entries_section}" if first_phrase=="begin_main": obj.handle_warning(obj.fd.feof("syntax-phrase-deprecation-warn", "Line {num}: phrase \"{old_phrase}\" is deprecated in this version; please use \"{new_phrase}\" instead", num=str(obj.lineindex+1), old_phrase="begin_main", new_phrase=r"{entries_section}")) @@ -70,4 +69,3 @@ def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.handle_warning(obj.fd.feof("syntax-phrase-deprecation-warn", "Line {num}: phrase \"{old_phrase}\" is deprecated in this version; please use \"{new_phrase}\" instead", num=str(obj.lineindex+1), old_phrase="end_main", new_phrase=r"{/entries_section}")) break else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(obj.lineindex+1))) - ## END --Process entries/main block-- diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index 8f70f72..2a1b448 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -77,11 +77,11 @@ class DataHandlers: 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))) - # write the compressed and original version of the file full_path=parent_path+"/"+file_path[-1] if os.path.isfile(full_path): self.handle_warning(self.fd.feof("repeated-manpage-warn","Line {num}: repeated manpage file, overwriting", num=str(line_number_debug))) try: + # write the compressed and original version of the file open(full_path, "w", encoding="utf-8").write(content) open(full_path+".gz", "wb").write(gzip.compress(bytes(content, "utf-8"))) except IsADirectoryError: diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index a755c9f..e67b656 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -16,7 +16,6 @@ from . import _dataclass def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.handle_begin_section("header") - # --Process header block-- end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" while obj.lineindex Date: Fri, 7 Jun 2024 22:57:11 +0800 Subject: [PATCH 249/354] Update version (v2.0-dev20240607) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 51b6386..63b2fc3 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240605' +pkgver='2.0_dev20240607' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 9200182..aef21ee 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240605-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240607-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 05 Jun 2024 22:03:00 +0800 + -- swiftycode <3291929745@qq.com> Fri, 07 Jun 2024 22:55:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index a6a1507..88065db 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-dev20240605" +__version__="2.0-dev20240607" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240605" +version_main="2.0_dev20240607" version_buildnumber=1 \ No newline at end of file -- Gitee From 3dda2fc2f7c8c98b8a49b936b4381e87961bbe8e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 8 Jun 2024 21:44:43 +0800 Subject: [PATCH 250/354] Add new README file --- README-frontend.md | 130 +++++++++++++++++++++++ README-new.md | 255 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 README-frontend.md create mode 100644 README-new.md diff --git a/README-frontend.md b/README-frontend.md new file mode 100644 index 0000000..f7c17f5 --- /dev/null +++ b/README-frontend.md @@ -0,0 +1,130 @@ +# 基本用法 + +## 数据结构和路径名称 + +应用程序是主要通过**路径名称**来指定所需的字符串。这个路径由空格来区别子路径(`subsections`)。大部分时候路径的前两个名称是用来指定开发者和应用名称的。主题文件会通过该路径名称来适配对应的字符串,从而达到自定义输出的效果。 + +比如`com.example example-app example-text`指的是`com.example`开发的`example-app`中的`example-text`字符串。 + +当然,路径名称也可以是全局的(不和任何应用信息关联),如`global-entry`或`global-example global-text`。 + +### 直接访问主题数据结构 + +`clitheme`的核心设计理念之一包括无需使用frontend模块就可以访问主题数据,并且访问方法直观易懂。这一点在使用其他语言编写的程序中尤其重要,因为frontend模块目前只提供Python程序的支持。 + +`clitheme`的数据结构采用了**子文件夹**的结构,意味着路径中的每一段代表着数据结构中的一个文件夹/文件。 + +比如说,`com.example example-app example-text` 的字符串会被存储在`/com.example/example-app/example-text`。在Linux和macOS系统下,``是 `$XDG_DATA_HOME/clitheme/theme-data`或`~/.local/share/clitheme/theme-data`。 + +在Windows系统下,``是`%USERPROFILE%\.local\share\clitheme\theme-data`。(`C:\Users\<用户名称>\.local\share\clitheme\theme-data`) + +如果需要访问该字符串的其他语言,直接在路径的最后添加`__`加上locale名称就可以了。比如:`/com.example/example-app/example-text__zh_CN` + +所以说,如果需要直接访问字符串信息,只需要访问对应的文件路径就可以了。 + +## 前端实施和编写主题文件 + +### 使用内置frontend模块 + +使用`clitheme`的frontend模块非常简单。只需要新建一个`frontend.FetchDescriptor`实例然后调用该实例中的`retrieve_entry_or_fallback`即可。 + +该函数需要提供路径名称和默认字符串。如果当前主题设定没有适配该字符串,则函数会返回提供的默认字符串。 + +如果新建`FetchDescriptor`时提供了`domain_name`,`app-name`,或`subsections`,则调用函数时会自动把它添加到路径名称前。 + +我们拿上面的样例来示范: + +```py +from clitheme import frontend + +# 新建FetchDescriptor实例 +f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") + +# 对应 “在当前目录找到了2个文件” +fcount="[...]" +f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) + +# 对应 “-> 正在安装 "example-file"...” +filename="[...]" +f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) + +# 对应 “已成功安装2个文件” +f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) + +# 对应 “错误:找不到文件 "foo-nonexist"” +filename_err="[...]" +f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) +``` + +### 使用fallback模块 + +应用程序还可以在src中内置本项目提供的fallback模块,以便更好的处理`clitheme`模块不存在时的情况。该fallback模块包括了frontend模块中的所有定义和功能,并且会永远返回失败时的默认值(fallback)。 + +如需使用,请在你的项目文件中导入`clitheme_fallback.py`文件,并且在你的程序中包括以下代码: + +```py +try: + from clitheme import frontend +except (ModuleNotFoundError, ImportError): + import clitheme_fallback as frontend +``` + +本项目提供的fallback文件会随版本更新而更改,所以请定期往你的项目里导入最新的fallback文件以适配最新的功能。 + +### 应用程序应该提供的信息 + +为了让用户更容易编写主题文件,应用程序应该加入输出字符串定义的功能。该输出信息应该包含路径名称和默认字符串。 + +比如说,应用程序可以通过`--clitheme-output-defs`来输出所有的字符串定义: + +``` +$ example-app --clitheme-output-defs +com.example example-app found-file +在当前目录找到了{}个文件 + +com.example example-app installing-file +-> 正在安装"{}"... + +com.example example-app install-success +已成功安装{}个文件 + +com.example example-app file-not-found +错误:找不到文件 "{}" +``` + +应用程序还可以在对应的官方文档中包括此信息。如需样例,请参考本仓库中`example-clithemedef`文件夹的[README文件](example-clithemedef/README.zh-CN.md)。 + +### 编写主题文件 + +关于主题文件的详细语法请见Wiki文档,下面将展示一个样例: + +``` +{header_section} + name 样例主题 + version 1.0 + locales zh_CN + supported_apps clitheme_demo +{/header_section} + +{entries_section} + in_domainapp com.example example-app + [entry] found-file + locale:default o(≧v≦)o 太好了,在当前目录找到了{}个文件! + locale:zh_CN o(≧v≦)o 太好了,在当前目录找到了{}个文件! + [/entry] + [entry] installing-file + locale:default (>^ω^<) 正在安装 "{}"... + locale:zh_CN (>^ω^<) 正在安装 "{}"... + [/entry] + [entry] install-success + locale:default o(≧v≦)o 已成功安装{}个文件! + locale:zh_CN o(≧v≦)o 已成功安装{}个文件! + [/entry] + [entry] file-not-found + locale:default ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" + locale:zh_CN ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" + [/entry] +end_main +``` + +编写好主题文件后,使用 `clitheme apply-theme `来应用主题。应用程序会直接采用主题中适配的字符串。 diff --git a/README-new.md b/README-new.md new file mode 100644 index 0000000..3b6828f --- /dev/null +++ b/README-new.md @@ -0,0 +1,255 @@ +# clitheme - 命令行自定义工具 + +`clitheme`允许你对命令行输出进行个性化定制,给它们一个你想要的风格和个性。 + +样例: +```plaintext +$ clang test.c +test.c:1:1: error: unknown type name 'bool' +bool *haku(int *a) { +^ +test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated +``` +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt +==> Generating data... +Successfully generated data +==> Applying theme...Success +Theme applied successfully +``` +```plaintext +$ clitheme-exec clang test.c +test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. +``` + +## 功能 + +`clitheme`包含以下主要功能: + +- 对任何命令行应用程序的输出通过定义替换规则进行修改和自定义 +- 自定义Unix/Linux文档手册(manpage) +- 包含类似于本地化套件(如GNU gettext)的应用程序API,帮助用户更好的自定义提示信息的内容 + +其他特性: + +- 多语言支持 + - 这意味着你也可以用`clitheme`来为应用程序添加多语言支持 +- 简洁易懂的**主题定义文件**语法 +- 无需应用程序API也可以访问当前主题中的字符串定义(易懂的数据结构) + +更多信息请见本项目的Wiki文档页面。你可以通过以下位置访问这些文档: +- https://gitee.com/swiftycode/clitheme/wikis/pages +- https://gitee.com/swiftycode/clitheme-wiki-repo +- https://github.com/swiftycode256/clitheme-wiki-repo + +# 功能样例和示范 + +## 命令行输出自定义 + +获取包含终端控制符号的原始输出内容: + +```plaintext +# --debug:在每一行的输出前添加标记;包含输出是否为stdout或stderr的信息("o>"或"e>") +# --debug-showchars:显示输出中的终端控制符号 +# --debug-nosubst:即使设定了主题,不对输出应用替换规则(获取原始输出) + +$ clitheme-exec --debug --debug-showchars --debug-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 *haku(int *a) {\r\n +e> {{ESC}}[0;1;32m^\r\n +e> {{ESC}}[0m{{ESC}}[1mtest.c:4:3: {{ESC}}[0m{{ESC}}[0;1;35mwarning: {{ESC}}[0m{{ESC}}[1mincompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types]{{ESC}}[0m\r\n +e> b=a;\r\n +e> {{ESC}}[0;1;32m ^~\r\n +e> {{ESC}}[0m2 errors generated.\r\n +``` + +根据输出内容编写主题定义文件和替换规则: + +```plaintext +# 在header_section中定义一些关于该主题定义的基本信息;必须包括 +{header_section} + # 这里建议至少包括name和description信息 + name clang样例主题 + [description] + 一个为clang打造的的样例主题,为了演示作用 + [/description] +{/header_section} + +{substrules_section} + # 设定"substesc"选项:内容中的"{{ESC}}"字样会被替换成ASCII Escape终端控制符号 + set_options substesc + # 命令限制条件:以下的替换规则仅会在以下命令中被应用。建议设定这个条件,因为可以尽量防止不应该的输出替换。 + [filter_commands] + clang + clang++ + 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.+)' + # 如果你想仅在系统语言设定为中文(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.+)' + locale:default \g错误!: \g未知的类型名'\g',忘记定义了~ಥ_ಥ + [/substitute_regex] +{/substrules_section} +``` + +使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-exec`执行命令以应用这些替换规则: + +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt +$ clitheme-exec clang test.c +test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. +``` + +## 自定义manpage文档 + +编写/编辑manpage文档的源代码,并且保存在一个位置中: + +```plaintext +$ nano man-pages/1/ls-custom.txt +# <编辑文件> +$ nano man-pages/1/cat-custom.txt +# <编辑文件> +``` + +编写主题定义文件: + +```plaintext +{header_section} + name 样例文档手册主题 + description 一个manpage文档手册样例主题 +{/header_section} + +{manpage_section} + # 在"include_file"后添加*由空格分开*的文件路径(以主题定义文件所在的路径为父路径) + # 在"as"后添加*由空格分开*的目标文件路径(放在如`/usr/share/man`文件夹下的文件路径) + include_file man-pages 1 ls-custom.txt + as man1 ls.1 + include_file man-pages 1 cat-custom.txt + as man1 cat.1 +{/manpage_section} +``` + +使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-man`查看这些自定义文档(使用方法和选项和`man`一样): + +```plaintext +$ clitheme apply-theme manpage-theme.clithemedef.txt +$ clitheme-man cat +$ clitheme-man ls +``` + +## 应用程序API和字符串定义 + +请见[此文档](./README-frontend.md) + +# 安装与构建 + +安装`clitheme`非常简单,您可以通过Arch Linux软件包,Debian软件包,或者pip软件包安装。 + +### 通过pip软件包安装 + +从最新发行版页面下载whl文件,使用`pip`直接安装即可: + + $ pip install ./clitheme--py3-none-any.whl + +### 通过Arch Linux软件包安装 + +因为Arch Linux上无法使用`pip`往系统里直接安装pip软件包,所以本项目支持通过Arch Linux软件包安装。 + +因为构建的Arch Linux软件包只兼容特定的Python版本,并且升级Python版本后会导致原软件包失效,本项目仅提供构建软件包的方式,不提供构建好的软件包。详细请见下方的**构建Arch Linux软件包**。 + +### 通过Debian软件包安装 + +因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 + +如需在Debian系统上安装,请从最新发行版页面下载`.deb`文件,使用`apt`安装即可: + + $ sudo apt install ./clitheme__all.deb + +## 构建安装包 + +你也可以从仓库源代码构建安装包,以包括最新或自定义更改。如果需要安装最新的开发版本,则需要通过此方法安装。 + +### 构建pip软件包 + +`clitheme`使用的是`setuptools`构建器,所以构建软件包前需要安装它。 + +首先,安装`setuptools`和`build`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: + + $ pip install setuptools build + +然后,切换到项目目录,使用以下命令构建软件包: + + $ python3 -m build --wheel --no-isolation + +构建完成后,相应的安装包文件可以在当前目录中的`dist`文件夹中找到。 + +### 构建Arch Linux软件包 + +构建Arch Linux软件包前,请确保`base-devel`软件包已安装。如需安装,请使用以下命令: + + $ sudo pacman -S base-devel + +构建软件包前,请先确保任何对仓库文件的更改以被提交(git commit): + + $ git add . + $ git commit + +构建软件包只需要在仓库目录中执行`makepkg`指令就可以了。你可以通过以下一系列命令来完成这些操作: + +```sh +# 如果之前执行过makepkg,请删除之前生成的临时文件夹,否则构建时会出现问题 +rm -rf buildtmp srctmp + +makepkg -si +# -s:自动安装构建时需要的软件包 +# -i:构建完后自动安装生成的软件包 + +# 完成后,你可以删除临时文件夹 +rm -rf buildtmp srctmp +``` + +**注意:** 每次升级Python版本时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 + +### 构建Debian软件包 + +因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 + +构建Debian软件包前,你需要安装以下用于构建的系统组件: + +- `debhelper` +- `dh-python` +- `python3-setuptools` +- `dpkg-dev` +- `pybuild-plugin-pyproject` + +你可以使用以下命令安装: + + sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject + +安装完后,请在仓库目录中执行`dpkg-buildpackage -b`以构建软件包。完成后,你会在上层目录中获得一个`.deb`的文件。 + +## 更多信息 + +- 本仓库中的代码也同步在GitHub上(使用Gitee仓库镜像功能自动同步):https://github.com/swiftycode256/clitheme +- 该项目的最新进展、未来计划、和开发中的新功能会在这里Gitee仓库中的Issues里列出:https://gitee.com/swiftycode/clitheme/issues +- 欢迎通过Issues和Pull Requests提交建议和改进。 + - Wiki页面也可以;你可以在上方列出的仓库中提交Issues和Pull Requests \ No newline at end of file -- Gitee From be8a3cecec14d0d10e843b9388b6335e931235ec Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 10:01:41 +0800 Subject: [PATCH 251/354] Some fixes in README-new.md --- README-new.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/README-new.md b/README-new.md index 3b6828f..73f48bb 100644 --- a/README-new.md +++ b/README-new.md @@ -6,7 +6,7 @@ ```plaintext $ clang test.c test.c:1:1: error: unknown type name 'bool' -bool *haku(int *a) { +bool *func(int *a) { ^ test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] b=a; @@ -64,7 +64,7 @@ test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者 $ clitheme-exec --debug --debug-showchars --debug-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 *haku(int *a) {\r\n +e> bool *func(int *a) {\r\n e> {{ESC}}[0;1;32m^\r\n e> {{ESC}}[0m{{ESC}}[1mtest.c:4:3: {{ESC}}[0m{{ESC}}[0;1;35mwarning: {{ESC}}[0m{{ESC}}[1mincompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types]{{ESC}}[0m\r\n e> b=a;\r\n @@ -87,7 +87,7 @@ e> {{ESC}}[0m2 errors generated.\r\n {substrules_section} # 设定"substesc"选项:内容中的"{{ESC}}"字样会被替换成ASCII Escape终端控制符号 set_options substesc - # 命令限制条件:以下的替换规则仅会在以下命令中被应用。建议设定这个条件,因为可以尽量防止不应该的输出替换。 + # 命令限制条件:以下的替换规则仅会在以下命令被调用时被应用。建议设定这个条件,因为可以尽量防止不应该的输出替换。 [filter_commands] clang clang++ @@ -105,7 +105,7 @@ e> {{ESC}}[0m2 errors generated.\r\n {/substrules_section} ``` -使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-exec`执行命令以应用这些替换规则: +使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-exec`执行命令以对输出应用这些替换规则: ```plaintext $ clitheme apply-theme clang-theme.clithemedef.txt @@ -162,24 +162,20 @@ $ clitheme-man ls # 安装与构建 -安装`clitheme`非常简单,您可以通过Arch Linux软件包,Debian软件包,或者pip软件包安装。 +安装`clitheme`非常简单,您可以通过pip软件包,Arch Linux软件包,或者Debian软件包安装。 ### 通过pip软件包安装 -从最新发行版页面下载whl文件,使用`pip`直接安装即可: +从最新发行版页面下载`.whl`文件,使用`pip`直接安装即可: $ pip install ./clitheme--py3-none-any.whl ### 通过Arch Linux软件包安装 -因为Arch Linux上无法使用`pip`往系统里直接安装pip软件包,所以本项目支持通过Arch Linux软件包安装。 - 因为构建的Arch Linux软件包只兼容特定的Python版本,并且升级Python版本后会导致原软件包失效,本项目仅提供构建软件包的方式,不提供构建好的软件包。详细请见下方的**构建Arch Linux软件包**。 ### 通过Debian软件包安装 -因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 - 如需在Debian系统上安装,请从最新发行版页面下载`.deb`文件,使用`apt`安装即可: $ sudo apt install ./clitheme__all.deb @@ -227,12 +223,10 @@ makepkg -si rm -rf buildtmp srctmp ``` -**注意:** 每次升级Python版本时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 +**注意:** 每次升级Python时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 ### 构建Debian软件包 -因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 - 构建Debian软件包前,你需要安装以下用于构建的系统组件: - `debhelper` @@ -247,7 +241,7 @@ rm -rf buildtmp srctmp 安装完后,请在仓库目录中执行`dpkg-buildpackage -b`以构建软件包。完成后,你会在上层目录中获得一个`.deb`的文件。 -## 更多信息 +# 更多信息 - 本仓库中的代码也同步在GitHub上(使用Gitee仓库镜像功能自动同步):https://github.com/swiftycode256/clitheme - 该项目的最新进展、未来计划、和开发中的新功能会在这里Gitee仓库中的Issues里列出:https://gitee.com/swiftycode/clitheme/issues -- Gitee From 83b1e0613bcf5f915e010f815f423d7bee5e9f43 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 10:06:18 +0800 Subject: [PATCH 252/354] Add English version of README files --- README-frontend.en.md | 130 ++++++++++++++++++++++ README-new.en.md | 250 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 README-frontend.en.md create mode 100644 README-new.en.md diff --git a/README-frontend.en.md b/README-frontend.en.md new file mode 100644 index 0000000..cfca7e5 --- /dev/null +++ b/README-frontend.en.md @@ -0,0 +1,130 @@ +# Frontend API and string entries demo + +## Data hierarchy and path naming + +Applications use **path names** to specify the string definitions they want. Subsections in the path name is separated using spaces. The first two subsections are usually reserved for the developer and application name. Theme definition files will use this path name to adopt corresponding string definitions, achieving the effect of output customization. + +For example, the path name `com.example example-app example-text` refers to the `example-text` string definition for the `example-app` application developed by `com.example`. + +It is not required to always follow this path naming convention and specifying global definitions (not related to any specific application) is allowed. For example, `global-entry` and `global-example global-text` are also valid path names. + +### Directly accessing the theme data hierarchy + +One of the key design principles of `clitheme` is that the use of frontend module is not needed to access the theme data hierarchy, and its method is easy to understand and implement. This is important especially in applications written in languages other than Python because Python is the only language supported by the frontend module. + +The data hierarchy is organized in a **subfolder structure**, meaning that every subsection in the path name represent a file or folder in the data hierarchy. + +For example, the contents of string definition `com.example example-app example-text` is stored in the directory `/com.example/example-app`. `` is `$XDG_DATA_HOME/clitheme/theme-data` or `~/.local/share/clitheme/theme-data` under Linux and macOS systems. + +Under Windows systems, `` is `%USERPROFILE%\.local\share\clitheme\theme-data` or `C:\Users\\.local\share\clitheme\theme-data`. + +To access a specific language of a string definition, add `__` plus the locale name to the end of the directory path. For example: `/com.example/example-app/example-text__en_US` + +In conclusion, to directly access a specific string definition, convert the path name to a directory path and access the file located there. + +## Frontend implementation and writing theme definition files + +### Using the built-in frontend module + +Using the frontend module provided by `clitheme` is very easy and straightforward. To access a string definition in the current theme setting, create a new `frontend.FetchDescriptor` object and use the provided `retrieve_entry_or_fallback` function. + +You need to pass the path name and a fallback string to this function. If the current theme setting does not provide the specified path name and string definition, the function will return the fallback string. + +You can pass the `domain_name`, `app_name`, and `subsections` arguments when creating a new `frontend.FetchDescriptor` object. When specified, these arguments will be automatically appended in front of the path name provided when calling the `retrieve_entry_or_fallback` function. + +Let's demonstrate it using the previous examples: + +```py +from clitheme import frontend + +# Create a new FetchDescriptor object +f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") + +# Corresponds to "Found 2 files in current directory" +fcount="[...]" +f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) + +# Corresponds to "-> Installing "example-file"..." +filename="[...]" +f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) + +# Corresponds to "Successfully installed 2 files" +f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) + +# Corresponds to "Error: File "foo-nonexist" not found" +filename_err="[...]" +f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) +``` + +### Using the fallback frontend module + +You can integrate the fallback frontend module provided by this project to better handle situations when `clitheme` does not exist on the system. This fallback module contains all the functions in the frontend module, and its functions will always return fallback values. + +Import the `clitheme_fallback.py` file from the repository and insert the following code in your project to use it: + +```py +try: + from clitheme import frontend +except (ModuleNotFoundError, ImportError): + import clitheme_fallback as frontend +``` + +The fallback module provided by this project will update accordingly with new versions. Therefore, it is recommended to import the latest version of this module to adopt the latest features. + +### Information your application should provide + +To allow users to write theme definition files of your application, your application should provide information about supported string definitions with its path name and default string. + +For example, your app can implement a feature to output all supported string definitions: + +``` +$ example-app --clitheme-output-defs +com.example example-app found-file +Found {} files in current directory + +com.example example-app installing-file +-> Installing "{}"... + +com.example example-app install-success +Successfully installed {} files + +com.example example-app file-not-found +Error: file "{}" not found +``` + +You can also include this information in your project's official documentation. The demo application in this repository provides an example of it and the corresponding README file is located in the folder `example-clithemedef`. + +### Writing theme definition files + +Consult the Wiki pages and documentation for detailed syntax of theme definition files. An example is provided below: + +``` +begin_header + name Example theme + version 1.0 + locales en_US + supported_apps clitheme_demo +end_header + +begin_main + in_domainapp com.example example-app + entry found-file + locale default o(≧v≦)o Great! Found {} files in current directory! + locale en_US o(≧v≦)o Great! Found {} files in current directory! + end_entry + entry installing-file + locale default (>^ω^<) Installing "{}"... + locale en_US (>^ω^<) Installing "{}"... + end_entry + entry install-success + locale default o(≧v≦)o Successfully installed {} files! + locale en_US o(≧v≦)o Successfully installed {} files! + end_entry + entry file-not-found + locale default ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found + locale en_US ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found + end_entry +end_main +``` + +Use the command `clitheme apply-theme ` to apply the theme definition file onto the system. Supported applications will start using the string definitions listed in this file. diff --git a/README-new.en.md b/README-new.en.md new file mode 100644 index 0000000..9b31f56 --- /dev/null +++ b/README-new.en.md @@ -0,0 +1,250 @@ +# clitheme - Command line customization utility + +`clitheme` allows you to customize the output of command line applications, giving them the style and personality you want. + +Example: +```plaintext +$ clang test.c +test.c:1:1: error: unknown type name 'bool' +bool *func(int *a) { +^ +test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated +``` +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt +==> Generating data... +Successfully generated data +==> Applying theme...Success +Theme applied successfully +``` +```plaintext +$ clitheme-exec clang test.c +test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. +``` + +## Features + +`clitheme` has these main features: + +- Customize and modify the output of any command line application through defining substitution rules +- Customize Unix/Linux manual pages (man pages) +- A frontend API for applications similar to localization toolkits (like GNU gettext), which can help users better customize output messages + +Other characteristics: + +- Multi-language/internalization support + - This means that you can also use `clitheme` to add internalization support for command line applications +- Easy-to-understand **theme definition file** syntax +- The string entries in the current theme setting can be accessed without using the frontend API (easy-to-understand data structure) + +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-wiki-repo +- https://github.com/swiftycode256/clitheme-wiki-repo + +# Feature examples and demos + +## Command line output substitution + +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) + +$ clitheme-exec --debug --debug-showchars --debug-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 +e> {{ESC}}[0m{{ESC}}[1mtest.c:4:3: {{ESC}}[0m{{ESC}}[0;1;35mwarning: {{ESC}}[0m{{ESC}}[1mincompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types]{{ESC}}[0m\r\n +e> b=a;\r\n +e> {{ESC}}[0;1;32m ^~\r\n +e> {{ESC}}[0m2 errors generated.\r\n +``` + +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 clang example theme + [description] + An example theme for clang (for demonstration purposes) + [/description] +{/header_section} + +{substrules_section} + # Set "substesc" option: "{{ESC}}" in content will be replaced with the ASCII Escape terminal control character + set_options substesc + # Command filter: following substitution rules will be applied only if these commands are invoked. It is recommended as it can prevent unwanted output substitutions. + [filter_commands] + clang + clang++ + 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.+)' + # 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.+)' + locale:default \gError! : \gunknown type name '\g', you forgot to d……define it!~ಥ_ಥ + [/substitute_regex] +{/substrules_section} +``` + +After applying the theme with `clitheme apply-theme `, execute the command with `clitheme-exec` to apply the substitution rules onto the output: + +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt +$ clitheme-exec clang test.c +test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. +``` + +## Custom man pages + +Write/edit the source code of the man page and save it into a location: + +```plaintext +$ nano man-pages/1/ls-custom.txt +# +$ nano man-pages/1/cat-custom.txt +# +``` + +Write a theme definition file: + +```plaintext +{header_section} + name Example manual page theme + description An example man page theme +{/header_section} + +{manpage_section} + # Add the file path *separated by spaces* after "include_file" (with the directory the theme definition file is placed as the parent directory) + # Add the target file path (e.g. where the file is placed under `/usr/share/man`) *separated by spaces* after "as" + include_file man-pages 1 ls-custom.txt + as man1 ls.1 + include_file man-pages 1 cat-custom.txt + as man1 cat.1 +{/manpage_section} +``` + +After applying the theme with `clitheme apply-theme `, use `clitheme-man` to view these custom man pages (arguments and options are the same as `man`): + +```plaintext +$ clitheme apply-theme manpage-theme.clithemedef.txt +$ clitheme-man cat +$ clitheme-man ls +``` + +## Application frontend API and string entries + +Please see [this article][todo] + +# Installing and building + +`clitheme` can be installed through pip package, Debian package, and Arch Linux package. + +### Install using pip package + +Download the `.whl` file from latest distribution page and install it using `pip`: + + $ pip install ./clitheme--py3-none-any.whl + +### Install using Arch Linux package + +Because each build of the Arch Linux package only supports a specific Python version and upgrading Python will break the package, pre-built packages are not provided and you need to build the package. Please see **Building Arch Linux package** below. + +### Install using Debian package + +Download the `.deb` file from the latest distribution page and install using `apt`: + + $ sudo apt install ./clitheme__all.deb + +## Building packages + +You can build the package from the repository source code, which includes any latest or custom changes. You can also use this method to install the latest development version. + +### Build pip package + +`clitheme` uses the `setuptools` build system, so it needs to be installed beforehand. + +First, install `setuptools` and `build` packages. You can use the packages provided by your Linux distribution, or install using `pip`: + + $ pip install setuptools build + +Then, switch to project directory and use the following command to build the package: + + $ python3 -m build --wheel --no-isolation + +The package file can be found in the `dist` folder after build finishes. + +### Build Arch Linux package + +Ensure that the `base-devel` package is installed before building. Use the following command to install: + + $ sudo pacman -S base-devel + +Before build the package, make sure that any changes in the repository are committed (git commit): + + $ git add . + $ git commit + +Execute `makepkg` to build the package. Use the following commands to perform these operations: + +```sh +# If makepkg is executed before, delete the temporary directories to prevent issues +rm -rf buildtmp srctmp + +makepkg -si +# -s: Automatically install required build dependencies (e.g. python-setuptools, python-build) +# -i:Automatically install the built package + +# You can delete the temporary directories after it completes +rm -rf buildtmp srctmp +``` + +**Note:** The package must be re-built every time Python is upgraded, because the package only works with the version of Python installed during build + +### Build Debian package + +Install the following packages before building: + +- `debhelper` +- `dh-python` +- `python3-setuptools` +- `dpkg-dev` +- `pybuild-plugin-pyproject` + +You can use the following command to install: + + sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject + +While in the repo directory, execute `dpkg-buildpackage -b` to build the package. A `.deb` file will be generated at the parent directory (`..`) after build completes. + +# More information + +- This repository is also synced onto GitHub (using Gitee automatic sync feature): https://github.com/swiftycode256/clitheme +- The latest developments, future plans, and in-development features of this project are detailed in the Issues section of the Gitee repository: https://gitee.com/swiftycode/clitheme/issues +- Feel free to propose suggestions and changes using Issues and Pull Requests + - Use the Wiki repositories listed above for Wiki-related suggestions -- Gitee From ca8945e39dc48f10717323797672ac69a0079901 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 10:06:25 +0800 Subject: [PATCH 253/354] Fix title in README-frontend --- README-frontend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README-frontend.md b/README-frontend.md index f7c17f5..079c52a 100644 --- a/README-frontend.md +++ b/README-frontend.md @@ -1,4 +1,4 @@ -# 基本用法 +# 应用程序API和字符串定义示范 ## 数据结构和路径名称 -- Gitee From 5f261cb6e6c62a27ea18949209fb9aae061498f2 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 10:07:57 +0800 Subject: [PATCH 254/354] Move README-new onto README.md --- README-new.en.md | 250 ----------------------------------- README-new.md | 249 ---------------------------------- README.en.md | 337 ++++++++++++++++++++++------------------------- README.md | 318 +++++++++++++++++++++----------------------- 4 files changed, 308 insertions(+), 846 deletions(-) delete mode 100644 README-new.en.md delete mode 100644 README-new.md diff --git a/README-new.en.md b/README-new.en.md deleted file mode 100644 index 9b31f56..0000000 --- a/README-new.en.md +++ /dev/null @@ -1,250 +0,0 @@ -# clitheme - Command line customization utility - -`clitheme` allows you to customize the output of command line applications, giving them the style and personality you want. - -Example: -```plaintext -$ clang test.c -test.c:1:1: error: unknown type name 'bool' -bool *func(int *a) { -^ -test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] - b=a; - ^~ -2 errors generated -``` -```plaintext -$ clitheme apply-theme clang-theme.clithemedef.txt -==> Generating data... -Successfully generated data -==> Applying theme...Success -Theme applied successfully -``` -```plaintext -$ clitheme-exec clang test.c -test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ -bool *func(int *a) { -^ -test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] - b=a; - ^~ -2 errors generated. -``` - -## Features - -`clitheme` has these main features: - -- Customize and modify the output of any command line application through defining substitution rules -- Customize Unix/Linux manual pages (man pages) -- A frontend API for applications similar to localization toolkits (like GNU gettext), which can help users better customize output messages - -Other characteristics: - -- Multi-language/internalization support - - This means that you can also use `clitheme` to add internalization support for command line applications -- Easy-to-understand **theme definition file** syntax -- The string entries in the current theme setting can be accessed without using the frontend API (easy-to-understand data structure) - -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-wiki-repo -- https://github.com/swiftycode256/clitheme-wiki-repo - -# Feature examples and demos - -## Command line output substitution - -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) - -$ clitheme-exec --debug --debug-showchars --debug-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 -e> {{ESC}}[0m{{ESC}}[1mtest.c:4:3: {{ESC}}[0m{{ESC}}[0;1;35mwarning: {{ESC}}[0m{{ESC}}[1mincompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types]{{ESC}}[0m\r\n -e> b=a;\r\n -e> {{ESC}}[0;1;32m ^~\r\n -e> {{ESC}}[0m2 errors generated.\r\n -``` - -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 clang example theme - [description] - An example theme for clang (for demonstration purposes) - [/description] -{/header_section} - -{substrules_section} - # Set "substesc" option: "{{ESC}}" in content will be replaced with the ASCII Escape terminal control character - set_options substesc - # Command filter: following substitution rules will be applied only if these commands are invoked. It is recommended as it can prevent unwanted output substitutions. - [filter_commands] - clang - clang++ - 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.+)' - # 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.+)' - locale:default \gError! : \gunknown type name '\g', you forgot to d……define it!~ಥ_ಥ - [/substitute_regex] -{/substrules_section} -``` - -After applying the theme with `clitheme apply-theme `, execute the command with `clitheme-exec` to apply the substitution rules onto the output: - -```plaintext -$ clitheme apply-theme clang-theme.clithemedef.txt -$ clitheme-exec clang test.c -test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ -bool *func(int *a) { -^ -test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] - b=a; - ^~ -2 errors generated. -``` - -## Custom man pages - -Write/edit the source code of the man page and save it into a location: - -```plaintext -$ nano man-pages/1/ls-custom.txt -# -$ nano man-pages/1/cat-custom.txt -# -``` - -Write a theme definition file: - -```plaintext -{header_section} - name Example manual page theme - description An example man page theme -{/header_section} - -{manpage_section} - # Add the file path *separated by spaces* after "include_file" (with the directory the theme definition file is placed as the parent directory) - # Add the target file path (e.g. where the file is placed under `/usr/share/man`) *separated by spaces* after "as" - include_file man-pages 1 ls-custom.txt - as man1 ls.1 - include_file man-pages 1 cat-custom.txt - as man1 cat.1 -{/manpage_section} -``` - -After applying the theme with `clitheme apply-theme `, use `clitheme-man` to view these custom man pages (arguments and options are the same as `man`): - -```plaintext -$ clitheme apply-theme manpage-theme.clithemedef.txt -$ clitheme-man cat -$ clitheme-man ls -``` - -## Application frontend API and string entries - -Please see [this article][todo] - -# Installing and building - -`clitheme` can be installed through pip package, Debian package, and Arch Linux package. - -### Install using pip package - -Download the `.whl` file from latest distribution page and install it using `pip`: - - $ pip install ./clitheme--py3-none-any.whl - -### Install using Arch Linux package - -Because each build of the Arch Linux package only supports a specific Python version and upgrading Python will break the package, pre-built packages are not provided and you need to build the package. Please see **Building Arch Linux package** below. - -### Install using Debian package - -Download the `.deb` file from the latest distribution page and install using `apt`: - - $ sudo apt install ./clitheme__all.deb - -## Building packages - -You can build the package from the repository source code, which includes any latest or custom changes. You can also use this method to install the latest development version. - -### Build pip package - -`clitheme` uses the `setuptools` build system, so it needs to be installed beforehand. - -First, install `setuptools` and `build` packages. You can use the packages provided by your Linux distribution, or install using `pip`: - - $ pip install setuptools build - -Then, switch to project directory and use the following command to build the package: - - $ python3 -m build --wheel --no-isolation - -The package file can be found in the `dist` folder after build finishes. - -### Build Arch Linux package - -Ensure that the `base-devel` package is installed before building. Use the following command to install: - - $ sudo pacman -S base-devel - -Before build the package, make sure that any changes in the repository are committed (git commit): - - $ git add . - $ git commit - -Execute `makepkg` to build the package. Use the following commands to perform these operations: - -```sh -# If makepkg is executed before, delete the temporary directories to prevent issues -rm -rf buildtmp srctmp - -makepkg -si -# -s: Automatically install required build dependencies (e.g. python-setuptools, python-build) -# -i:Automatically install the built package - -# You can delete the temporary directories after it completes -rm -rf buildtmp srctmp -``` - -**Note:** The package must be re-built every time Python is upgraded, because the package only works with the version of Python installed during build - -### Build Debian package - -Install the following packages before building: - -- `debhelper` -- `dh-python` -- `python3-setuptools` -- `dpkg-dev` -- `pybuild-plugin-pyproject` - -You can use the following command to install: - - sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject - -While in the repo directory, execute `dpkg-buildpackage -b` to build the package. A `.deb` file will be generated at the parent directory (`..`) after build completes. - -# More information - -- This repository is also synced onto GitHub (using Gitee automatic sync feature): https://github.com/swiftycode256/clitheme -- The latest developments, future plans, and in-development features of this project are detailed in the Issues section of the Gitee repository: https://gitee.com/swiftycode/clitheme/issues -- Feel free to propose suggestions and changes using Issues and Pull Requests - - Use the Wiki repositories listed above for Wiki-related suggestions diff --git a/README-new.md b/README-new.md deleted file mode 100644 index 73f48bb..0000000 --- a/README-new.md +++ /dev/null @@ -1,249 +0,0 @@ -# clitheme - 命令行自定义工具 - -`clitheme`允许你对命令行输出进行个性化定制,给它们一个你想要的风格和个性。 - -样例: -```plaintext -$ clang test.c -test.c:1:1: error: unknown type name 'bool' -bool *func(int *a) { -^ -test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] - b=a; - ^~ -2 errors generated -``` -```plaintext -$ clitheme apply-theme clang-theme.clithemedef.txt -==> Generating data... -Successfully generated data -==> Applying theme...Success -Theme applied successfully -``` -```plaintext -$ clitheme-exec clang test.c -test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ -bool *func(int *a) { -^ -test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] - b=a; - ^~ -2 errors generated. -``` - -## 功能 - -`clitheme`包含以下主要功能: - -- 对任何命令行应用程序的输出通过定义替换规则进行修改和自定义 -- 自定义Unix/Linux文档手册(manpage) -- 包含类似于本地化套件(如GNU gettext)的应用程序API,帮助用户更好的自定义提示信息的内容 - -其他特性: - -- 多语言支持 - - 这意味着你也可以用`clitheme`来为应用程序添加多语言支持 -- 简洁易懂的**主题定义文件**语法 -- 无需应用程序API也可以访问当前主题中的字符串定义(易懂的数据结构) - -更多信息请见本项目的Wiki文档页面。你可以通过以下位置访问这些文档: -- https://gitee.com/swiftycode/clitheme/wikis/pages -- https://gitee.com/swiftycode/clitheme-wiki-repo -- https://github.com/swiftycode256/clitheme-wiki-repo - -# 功能样例和示范 - -## 命令行输出自定义 - -获取包含终端控制符号的原始输出内容: - -```plaintext -# --debug:在每一行的输出前添加标记;包含输出是否为stdout或stderr的信息("o>"或"e>") -# --debug-showchars:显示输出中的终端控制符号 -# --debug-nosubst:即使设定了主题,不对输出应用替换规则(获取原始输出) - -$ clitheme-exec --debug --debug-showchars --debug-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 -e> {{ESC}}[0m{{ESC}}[1mtest.c:4:3: {{ESC}}[0m{{ESC}}[0;1;35mwarning: {{ESC}}[0m{{ESC}}[1mincompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types]{{ESC}}[0m\r\n -e> b=a;\r\n -e> {{ESC}}[0;1;32m ^~\r\n -e> {{ESC}}[0m2 errors generated.\r\n -``` - -根据输出内容编写主题定义文件和替换规则: - -```plaintext -# 在header_section中定义一些关于该主题定义的基本信息;必须包括 -{header_section} - # 这里建议至少包括name和description信息 - name clang样例主题 - [description] - 一个为clang打造的的样例主题,为了演示作用 - [/description] -{/header_section} - -{substrules_section} - # 设定"substesc"选项:内容中的"{{ESC}}"字样会被替换成ASCII Escape终端控制符号 - set_options substesc - # 命令限制条件:以下的替换规则仅会在以下命令被调用时被应用。建议设定这个条件,因为可以尽量防止不应该的输出替换。 - [filter_commands] - clang - clang++ - 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.+)' - # 如果你想仅在系统语言设定为中文(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.+)' - locale:default \g错误!: \g未知的类型名'\g',忘记定义了~ಥ_ಥ - [/substitute_regex] -{/substrules_section} -``` - -使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-exec`执行命令以对输出应用这些替换规则: - -```plaintext -$ clitheme apply-theme clang-theme.clithemedef.txt -$ clitheme-exec clang test.c -test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ -bool *func(int *a) { -^ -test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] - b=a; - ^~ -2 errors generated. -``` - -## 自定义manpage文档 - -编写/编辑manpage文档的源代码,并且保存在一个位置中: - -```plaintext -$ nano man-pages/1/ls-custom.txt -# <编辑文件> -$ nano man-pages/1/cat-custom.txt -# <编辑文件> -``` - -编写主题定义文件: - -```plaintext -{header_section} - name 样例文档手册主题 - description 一个manpage文档手册样例主题 -{/header_section} - -{manpage_section} - # 在"include_file"后添加*由空格分开*的文件路径(以主题定义文件所在的路径为父路径) - # 在"as"后添加*由空格分开*的目标文件路径(放在如`/usr/share/man`文件夹下的文件路径) - include_file man-pages 1 ls-custom.txt - as man1 ls.1 - include_file man-pages 1 cat-custom.txt - as man1 cat.1 -{/manpage_section} -``` - -使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-man`查看这些自定义文档(使用方法和选项和`man`一样): - -```plaintext -$ clitheme apply-theme manpage-theme.clithemedef.txt -$ clitheme-man cat -$ clitheme-man ls -``` - -## 应用程序API和字符串定义 - -请见[此文档](./README-frontend.md) - -# 安装与构建 - -安装`clitheme`非常简单,您可以通过pip软件包,Arch Linux软件包,或者Debian软件包安装。 - -### 通过pip软件包安装 - -从最新发行版页面下载`.whl`文件,使用`pip`直接安装即可: - - $ pip install ./clitheme--py3-none-any.whl - -### 通过Arch Linux软件包安装 - -因为构建的Arch Linux软件包只兼容特定的Python版本,并且升级Python版本后会导致原软件包失效,本项目仅提供构建软件包的方式,不提供构建好的软件包。详细请见下方的**构建Arch Linux软件包**。 - -### 通过Debian软件包安装 - -如需在Debian系统上安装,请从最新发行版页面下载`.deb`文件,使用`apt`安装即可: - - $ sudo apt install ./clitheme__all.deb - -## 构建安装包 - -你也可以从仓库源代码构建安装包,以包括最新或自定义更改。如果需要安装最新的开发版本,则需要通过此方法安装。 - -### 构建pip软件包 - -`clitheme`使用的是`setuptools`构建器,所以构建软件包前需要安装它。 - -首先,安装`setuptools`和`build`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: - - $ pip install setuptools build - -然后,切换到项目目录,使用以下命令构建软件包: - - $ python3 -m build --wheel --no-isolation - -构建完成后,相应的安装包文件可以在当前目录中的`dist`文件夹中找到。 - -### 构建Arch Linux软件包 - -构建Arch Linux软件包前,请确保`base-devel`软件包已安装。如需安装,请使用以下命令: - - $ sudo pacman -S base-devel - -构建软件包前,请先确保任何对仓库文件的更改以被提交(git commit): - - $ git add . - $ git commit - -构建软件包只需要在仓库目录中执行`makepkg`指令就可以了。你可以通过以下一系列命令来完成这些操作: - -```sh -# 如果之前执行过makepkg,请删除之前生成的临时文件夹,否则构建时会出现问题 -rm -rf buildtmp srctmp - -makepkg -si -# -s:自动安装构建时需要的软件包 -# -i:构建完后自动安装生成的软件包 - -# 完成后,你可以删除临时文件夹 -rm -rf buildtmp srctmp -``` - -**注意:** 每次升级Python时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 - -### 构建Debian软件包 - -构建Debian软件包前,你需要安装以下用于构建的系统组件: - -- `debhelper` -- `dh-python` -- `python3-setuptools` -- `dpkg-dev` -- `pybuild-plugin-pyproject` - -你可以使用以下命令安装: - - sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject - -安装完后,请在仓库目录中执行`dpkg-buildpackage -b`以构建软件包。完成后,你会在上层目录中获得一个`.deb`的文件。 - -# 更多信息 - -- 本仓库中的代码也同步在GitHub上(使用Gitee仓库镜像功能自动同步):https://github.com/swiftycode256/clitheme -- 该项目的最新进展、未来计划、和开发中的新功能会在这里Gitee仓库中的Issues里列出:https://gitee.com/swiftycode/clitheme/issues -- 欢迎通过Issues和Pull Requests提交建议和改进。 - - Wiki页面也可以;你可以在上方列出的仓库中提交Issues和Pull Requests \ No newline at end of file diff --git a/README.en.md b/README.en.md index f337f23..9b31f56 100644 --- a/README.en.md +++ b/README.en.md @@ -1,269 +1,250 @@ -# clitheme - A CLI application framework for output customization - -[中文](README.md) | **English** +# clitheme - Command line customization utility `clitheme` allows you to customize the output of command line applications, giving them the style and personality you want. Example: +```plaintext +$ clang test.c +test.c:1:1: error: unknown type name 'bool' +bool *func(int *a) { +^ +test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated ``` -$ example-app install-files -Found 2 files in current directory --> Installing "example-file"... --> Installing "example-file-2"... -Successfully installed 2 files -$ example-app install-file foo-nonexist -Error: File "foo-nonexist" not found -``` -``` -$ clitheme apply-theme example-app-theme_clithemedef.txt +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt ==> Generating data... Successfully generated data ==> Applying theme...Success Theme applied successfully ``` -``` -$ example-app install-files -o(≧v≦)o Great! Found 2 files in current directory! -(>^ω^<) Installing "example-file"... -(>^ω^<) Installing "example-file-2:"... -o(≧v≦)o Successfully installed 2 files! -$ example-app install-file foo-nonexist -ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found +```plaintext +$ clitheme-exec clang test.c +test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. ``` ## Features -- Multi-language (Internationalization) support -- Supports applying multiple themes simultaneously -- Clear and easy-to-understand theme definition file (`clithemedef`) syntax -- The theme data can be easily accessed without the use of frontend module - -Not only `clitheme` can customize the output of command-line applications, it can also: -- Add support for another language for an application -- Support GUI applications - -# Basic usage - -## Data hierarchy and path naming - -Applications use **path names** to specify the string definitions they want. Subsections in the path name is separated using spaces. The first two subsections are usually reserved for the developer and application name. Theme definition files will use this path name to adopt corresponding string definitions, achieving the effect of output customization. - -For example, the path name `com.example example-app example-text` refers to the `example-text` string definition for the `example-app` application developed by `com.example`. - -It is not required to always follow this path naming convention and specifying global definitions (not related to any specific application) is allowed. For example, `global-entry` and `global-example global-text` are also valid path names. - -### Directly accessing the theme data hierarchy - -One of the key design principles of `clitheme` is that the use of frontend module is not needed to access the theme data hierarchy, and its method is easy to understand and implement. This is important especially in applications written in languages other than Python because Python is the only language supported by the frontend module. - -The data hierarchy is organized in a **subfolder structure**, meaning that every subsection in the path name represent a file or folder in the data hierarchy. - -For example, the contents of string definition `com.example example-app example-text` is stored in the directory `/com.example/example-app`. `` is `$XDG_DATA_HOME/clitheme/theme-data` or `~/.local/share/clitheme/theme-data` under Linux and macOS systems. - -Under Windows systems, `` is `%USERPROFILE%\.local\share\clitheme\theme-data` or `C:\Users\\.local\share\clitheme\theme-data`. - -To access a specific language of a string definition, add `__` plus the locale name to the end of the directory path. For example: `/com.example/example-app/example-text__en_US` - -In conclusion, to directly access a specific string definition, convert the path name to a directory path and access the file located there. - -## Frontend implementation and writing theme definition files +`clitheme` has these main features: -### Using the built-in frontend module +- Customize and modify the output of any command line application through defining substitution rules +- Customize Unix/Linux manual pages (man pages) +- A frontend API for applications similar to localization toolkits (like GNU gettext), which can help users better customize output messages -Using the frontend module provided by `clitheme` is very easy and straightforward. To access a string definition in the current theme setting, create a new `frontend.FetchDescriptor` object and use the provided `retrieve_entry_or_fallback` function. +Other characteristics: -You need to pass the path name and a fallback string to this function. If the current theme setting does not provide the specified path name and string definition, the function will return the fallback string. +- Multi-language/internalization support + - This means that you can also use `clitheme` to add internalization support for command line applications +- Easy-to-understand **theme definition file** syntax +- The string entries in the current theme setting can be accessed without using the frontend API (easy-to-understand data structure) -You can pass the `domain_name`, `app_name`, and `subsections` arguments when creating a new `frontend.FetchDescriptor` object. When specified, these arguments will be automatically appended in front of the path name provided when calling the `retrieve_entry_or_fallback` function. +For more information, please see the project's Wiki documentation page. It can be accessed through the following links: -Let's demonstrate it using the previous examples: +- https://gitee.com/swiftycode/clitheme/wikis/pages +- https://gitee.com/swiftycode/clitheme-wiki-repo +- https://github.com/swiftycode256/clitheme-wiki-repo -```py -from clitheme import frontend +# Feature examples and demos -# Create a new FetchDescriptor object -f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") +## Command line output substitution -# Corresponds to "Found 2 files in current directory" -fcount="[...]" -f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) +Get the command line output, including any terminal control characters: -# Corresponds to "-> Installing "example-file"..." -filename="[...]" -f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) +```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) -# Corresponds to "Successfully installed 2 files" -f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) - -# Corresponds to "Error: File "foo-nonexist" not found" -filename_err="[...]" -f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) +$ clitheme-exec --debug --debug-showchars --debug-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 +e> {{ESC}}[0m{{ESC}}[1mtest.c:4:3: {{ESC}}[0m{{ESC}}[0;1;35mwarning: {{ESC}}[0m{{ESC}}[1mincompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types]{{ESC}}[0m\r\n +e> b=a;\r\n +e> {{ESC}}[0;1;32m ^~\r\n +e> {{ESC}}[0m2 errors generated.\r\n ``` -### Using the fallback frontend module - -You can integrate the fallback frontend module provided by this project to better handle situations when `clitheme` does not exist on the system. This fallback module contains all the functions in the frontend module, and its functions will always return fallback values. - -Import the `clitheme_fallback.py` file from the repository and insert the following code in your project to use it: - -```py -try: - from clitheme import frontend -except (ModuleNotFoundError, ImportError): - import clitheme_fallback as frontend +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 clang example theme + [description] + An example theme for clang (for demonstration purposes) + [/description] +{/header_section} + +{substrules_section} + # Set "substesc" option: "{{ESC}}" in content will be replaced with the ASCII Escape terminal control character + set_options substesc + # Command filter: following substitution rules will be applied only if these commands are invoked. It is recommended as it can prevent unwanted output substitutions. + [filter_commands] + clang + clang++ + 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.+)' + # 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.+)' + locale:default \gError! : \gunknown type name '\g', you forgot to d……define it!~ಥ_ಥ + [/substitute_regex] +{/substrules_section} ``` -The fallback module provided by this project will update accordingly with new versions. Therefore, it is recommended to import the latest version of this module to adopt the latest features. - -### Information your application should provide - -To allow users to write theme definition files of your application, your application should provide information about supported string definitions with its path name and default string. - -For example, your app can implement a feature to output all supported string definitions: - +After applying the theme with `clitheme apply-theme `, execute the command with `clitheme-exec` to apply the substitution rules onto the output: + +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt +$ clitheme-exec clang test.c +test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. ``` -$ example-app --clitheme-output-defs -com.example example-app found-file -Found {} files in current directory -com.example example-app installing-file --> Installing "{}"... +## Custom man pages -com.example example-app install-success -Successfully installed {} files +Write/edit the source code of the man page and save it into a location: -com.example example-app file-not-found -Error: file "{}" not found +```plaintext +$ nano man-pages/1/ls-custom.txt +# +$ nano man-pages/1/cat-custom.txt +# ``` -You can also include this information in your project's official documentation. The demo application in this repository provides an example of it and the corresponding README file is located in the folder `example-clithemedef`. - -### Writing theme definition files +Write a theme definition file: + +```plaintext +{header_section} + name Example manual page theme + description An example man page theme +{/header_section} + +{manpage_section} + # Add the file path *separated by spaces* after "include_file" (with the directory the theme definition file is placed as the parent directory) + # Add the target file path (e.g. where the file is placed under `/usr/share/man`) *separated by spaces* after "as" + include_file man-pages 1 ls-custom.txt + as man1 ls.1 + include_file man-pages 1 cat-custom.txt + as man1 cat.1 +{/manpage_section} +``` -Consult the Wiki pages and documentation for detailed syntax of theme definition files. An example is provided below: +After applying the theme with `clitheme apply-theme `, use `clitheme-man` to view these custom man pages (arguments and options are the same as `man`): -``` -begin_header - name Example theme - version 1.0 - locales en_US - supported_apps clitheme_demo -end_header - -begin_main - in_domainapp com.example example-app - entry found-file - locale default o(≧v≦)o Great! Found {} files in current directory! - locale en_US o(≧v≦)o Great! Found {} files in current directory! - end_entry - entry installing-file - locale default (>^ω^<) Installing "{}"... - locale en_US (>^ω^<) Installing "{}"... - end_entry - entry install-success - locale default o(≧v≦)o Successfully installed {} files! - locale en_US o(≧v≦)o Successfully installed {} files! - end_entry - entry file-not-found - locale default ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found - locale en_US ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found - end_entry -end_main +```plaintext +$ clitheme apply-theme manpage-theme.clithemedef.txt +$ clitheme-man cat +$ clitheme-man ls ``` -Use the command `clitheme apply-theme ` to apply the theme definition file onto the system. Supported applications will start using the string definitions listed in this file. +## Application frontend API and string entries -# Installation +Please see [this article][todo] -Installing `clitheme` is very easy. You can use the provided Arch Linux, Debian, or pip package to install it. +# Installing and building -### Install using pip +`clitheme` can be installed through pip package, Debian package, and Arch Linux package. -Download the whl file from the latest release and install it using `pip`: +### Install using pip package - $ pip install clitheme--py3-none-any.whl +Download the `.whl` file from latest distribution page and install it using `pip`: + + $ pip install ./clitheme--py3-none-any.whl ### Install using Arch Linux package -Because `pip` cannot be used to install Python packages onto an Arch Linux system, this project provides an Arch Linux package. - -Because the built package only supports a specific Python version and will stop working when Python is upgraded, this project only provides files needed to build the package. Please see **Build Arch Linux package** for more information. +Because each build of the Arch Linux package only supports a specific Python version and upgrading Python will break the package, pre-built packages are not provided and you need to build the package. Please see **Building Arch Linux package** below. ### Install using Debian package -Because `pip` cannot be used to install Python packages onto certain Debian Linux distributions, this project provides a Debian package. - -Download the `.deb` file from the latest release and install it using `apt`: +Download the `.deb` file from the latest distribution page and install using `apt`: $ sudo apt install ./clitheme__all.deb -## Building the installation package +## Building packages -You can also build the installation package from source code, which allows you to include the latest or custom changes. This is the only method to install the latest development version of `clitheme`. +You can build the package from the repository source code, which includes any latest or custom changes. You can also use this method to install the latest development version. ### Build pip package -`clitheme` uses the `hatchling` build backend, so installing it is required for building the package. +`clitheme` uses the `setuptools` build system, so it needs to be installed beforehand. -First, install the `hatch` package. You can use the software package provided by your Linux distribution, or install it using `pip`: +First, install `setuptools` and `build` packages. You can use the packages provided by your Linux distribution, or install using `pip`: - $ pip install hatch + $ pip install setuptools build -Next, making sure that you are under the project directory, use `hatch build` to build the package: +Then, switch to project directory and use the following command to build the package: - $ hatch build + $ python3 -m build --wheel --no-isolation -If this command does not work, try using `hatchling build` instead. - -The corresponding pip package (whl file) can be found in the `dist` folder under the working directory. +The package file can be found in the `dist` folder after build finishes. ### Build Arch Linux package -Make sure that the `base-devel` package is installed before building the package. You can install it using the following command: +Ensure that the `base-devel` package is installed before building. Use the following command to install: $ sudo pacman -S base-devel -To build the package, run `makepkg` under the project directory. You can use the following commands: +Before build the package, make sure that any changes in the repository are committed (git commit): + + $ git add . + $ git commit + +Execute `makepkg` to build the package. Use the following commands to perform these operations: ```sh -# Delete the temporary directories if makepkg has been run before. Issues will occur if you do not do so. +# If makepkg is executed before, delete the temporary directories to prevent issues rm -rf buildtmp srctmp makepkg -si -# -s: install dependencies required for building the package -# -i: automatically install the built package +# -s: Automatically install required build dependencies (e.g. python-setuptools, python-build) +# -i:Automatically install the built package -# You can remove the temporary directories after you are finished +# You can delete the temporary directories after it completes rm -rf buildtmp srctmp ``` -**Warning:** You must rebuild the package every time Python is upgraded, because the package only works under the Python version when the package is built. +**Note:** The package must be re-built every time Python is upgraded, because the package only works with the version of Python installed during build ### Build Debian package -Because `pip` cannot be used to install Python packages onto certain Debian Linux distributions, this project provides a Debian package. - -The following packages are required prior to building the package: +Install the following packages before building: - `debhelper` - `dh-python` -- `python3-hatchling` +- `python3-setuptools` - `dpkg-dev` - `pybuild-plugin-pyproject` -They can be installed using this command: +You can use the following command to install: - sudo apt install debhelper dh-python python3-hatchling dpkg-dev pybuild-plugin-pyproject + sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject -Run `dpkg-buildpackage -b` to build the package. A `.deb` file will be generated in the upper folder after the build process finishes. +While in the repo directory, execute `dpkg-buildpackage -b` to build the package. A `.deb` file will be generated at the parent directory (`..`) after build completes. -## More information +# More information -- For more information, please reference the project's Wiki pages: https://gitee.com/swiftycode/clitheme/wikis/pages - - You can also access the pages in these repositories: - - https://gitee.com/swiftycode/clitheme-wiki-repo - - https://github.com/swiftycode256/clitheme-wiki-repo - This repository is also synced onto GitHub (using Gitee automatic sync feature): https://github.com/swiftycode256/clitheme - The latest developments, future plans, and in-development features of this project are detailed in the Issues section of the Gitee repository: https://gitee.com/swiftycode/clitheme/issues -- You are welcome to propose suggestions and changes using Issues and Pull Requests - - Use the Wiki repositories listed above for Wiki-related suggestions \ No newline at end of file +- Feel free to propose suggestions and changes using Issues and Pull Requests + - Use the Wiki repositories listed above for Wiki-related suggestions diff --git a/README.md b/README.md index c11df66..73f48bb 100644 --- a/README.md +++ b/README.md @@ -1,198 +1,181 @@ -# clitheme - 命令行应用文本主题框架 +# clitheme - 命令行自定义工具 -**中文** | [English](README.en.md) - -`clitheme` 允许你定制命令行应用程序的输出,给它们一个你想要的风格和个性。 +`clitheme`允许你对命令行输出进行个性化定制,给它们一个你想要的风格和个性。 样例: +```plaintext +$ clang test.c +test.c:1:1: error: unknown type name 'bool' +bool *func(int *a) { +^ +test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated ``` -$ example-app install-files -在当前目录找到了2个文件 --> 正在安装 "example-file"... --> 正在安装 "example-file-2"... -已成功安装2个文件 -$ example-app install-file foo-nonexist -错误:找不到文件 "foo-nonexist" -``` -``` -$ clitheme apply-theme example-app-theme_clithemedef.txt +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt ==> Generating data... Successfully generated data ==> Applying theme...Success Theme applied successfully ``` -``` -$ example-app install-files -o(≧v≦)o 太好了,在当前目录找到了2个文件! -(>^ω^<) 正在安装 "example-file"... -(>^ω^<) 正在安装 "example-file-2:"... -o(≧v≦)o 已成功安装2个文件! -$ example-app install-file foo-nonexist -ಥ_ಥ 糟糕,出错啦!找不到文件 "foo-nonexist" +```plaintext +$ clitheme-exec clang test.c +test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. ``` ## 功能 -- 多语言支持 -- 支持同时应用多个主题 -- 简洁易懂的主题信息文件(`clithemedef`)语法 -- 无需frontend模块也可访问当前主题数据(易懂的数据结构) - -`clitheme` 不仅可以定制命令行应用的输出,它还可以: -- 为应用程序添加多语言支持 -- 支持图形化应用 - -# 基本用法 - -## 数据结构和路径名称 - -应用程序是主要通过**路径名称**来指定所需的字符串。这个路径由空格来区别子路径(`subsections`)。大部分时候路径的前两个名称是用来指定开发者和应用名称的。主题文件会通过该路径名称来适配对应的字符串,从而达到自定义输出的效果。 - -比如`com.example example-app example-text`指的是`com.example`开发的`example-app`中的`example-text`字符串。 - -当然,路径名称也可以是全局的(不和任何应用信息关联),如`global-entry`或`global-example global-text`。 - -### 直接访问主题数据结构 - -`clitheme`的核心设计理念之一包括无需使用frontend模块就可以访问主题数据,并且访问方法直观易懂。这一点在使用其他语言编写的程序中尤其重要,因为frontend模块目前只提供Python程序的支持。 - -`clitheme`的数据结构采用了**子文件夹**的结构,意味着路径中的每一段代表着数据结构中的一个文件夹/文件。 - -比如说,`com.example example-app example-text` 的字符串会被存储在`/com.example/example-app/example-text`。在Linux和macOS系统下,``是 `$XDG_DATA_HOME/clitheme/theme-data`或`~/.local/share/clitheme/theme-data`。 - -在Windows系统下,``是`%USERPROFILE%\.local\share\clitheme\theme-data`。(`C:\Users\<用户名称>\.local\share\clitheme\theme-data`) - -如果需要访问该字符串的其他语言,直接在路径的最后添加`__`加上locale名称就可以了。比如:`/com.example/example-app/example-text__zh_CN` - -所以说,如果需要直接访问字符串信息,只需要访问对应的文件路径就可以了。 - -## 前端实施和编写主题文件 - -### 使用内置frontend模块 - -使用`clitheme`的frontend模块非常简单。只需要新建一个`frontend.FetchDescriptor`实例然后调用该实例中的`retrieve_entry_or_fallback`即可。 - -该函数需要提供路径名称和默认字符串。如果当前主题设定没有适配该字符串,则函数会返回提供的默认字符串。 - -如果新建`FetchDescriptor`时提供了`domain_name`,`app-name`,或`subsections`,则调用函数时会自动把它添加到路径名称前。 +`clitheme`包含以下主要功能: -我们拿上面的样例来示范: +- 对任何命令行应用程序的输出通过定义替换规则进行修改和自定义 +- 自定义Unix/Linux文档手册(manpage) +- 包含类似于本地化套件(如GNU gettext)的应用程序API,帮助用户更好的自定义提示信息的内容 -```py -from clitheme import frontend +其他特性: -# 新建FetchDescriptor实例 -f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") - -# 对应 “在当前目录找到了2个文件” -fcount="[...]" -f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) - -# 对应 “-> 正在安装 "example-file"...” -filename="[...]" -f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) - -# 对应 “已成功安装2个文件” -f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) - -# 对应 “错误:找不到文件 "foo-nonexist"” -filename_err="[...]" -f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) +- 多语言支持 + - 这意味着你也可以用`clitheme`来为应用程序添加多语言支持 +- 简洁易懂的**主题定义文件**语法 +- 无需应用程序API也可以访问当前主题中的字符串定义(易懂的数据结构) + +更多信息请见本项目的Wiki文档页面。你可以通过以下位置访问这些文档: +- https://gitee.com/swiftycode/clitheme/wikis/pages +- https://gitee.com/swiftycode/clitheme-wiki-repo +- https://github.com/swiftycode256/clitheme-wiki-repo + +# 功能样例和示范 + +## 命令行输出自定义 + +获取包含终端控制符号的原始输出内容: + +```plaintext +# --debug:在每一行的输出前添加标记;包含输出是否为stdout或stderr的信息("o>"或"e>") +# --debug-showchars:显示输出中的终端控制符号 +# --debug-nosubst:即使设定了主题,不对输出应用替换规则(获取原始输出) + +$ clitheme-exec --debug --debug-showchars --debug-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 +e> {{ESC}}[0m{{ESC}}[1mtest.c:4:3: {{ESC}}[0m{{ESC}}[0;1;35mwarning: {{ESC}}[0m{{ESC}}[1mincompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types]{{ESC}}[0m\r\n +e> b=a;\r\n +e> {{ESC}}[0;1;32m ^~\r\n +e> {{ESC}}[0m2 errors generated.\r\n ``` -### 使用fallback模块 - -应用程序还可以在src中内置本项目提供的fallback模块,以便更好的处理`clitheme`模块不存在时的情况。该fallback模块包括了frontend模块中的所有定义和功能,并且会永远返回失败时的默认值(fallback)。 - -如需使用,请在你的项目文件中导入`clitheme_fallback.py`文件,并且在你的程序中包括以下代码: - -```py -try: - from clitheme import frontend -except (ModuleNotFoundError, ImportError): - import clitheme_fallback as frontend +根据输出内容编写主题定义文件和替换规则: + +```plaintext +# 在header_section中定义一些关于该主题定义的基本信息;必须包括 +{header_section} + # 这里建议至少包括name和description信息 + name clang样例主题 + [description] + 一个为clang打造的的样例主题,为了演示作用 + [/description] +{/header_section} + +{substrules_section} + # 设定"substesc"选项:内容中的"{{ESC}}"字样会被替换成ASCII Escape终端控制符号 + set_options substesc + # 命令限制条件:以下的替换规则仅会在以下命令被调用时被应用。建议设定这个条件,因为可以尽量防止不应该的输出替换。 + [filter_commands] + clang + clang++ + 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.+)' + # 如果你想仅在系统语言设定为中文(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.+)' + locale:default \g错误!: \g未知的类型名'\g',忘记定义了~ಥ_ಥ + [/substitute_regex] +{/substrules_section} ``` -本项目提供的fallback文件会随版本更新而更改,所以请定期往你的项目里导入最新的fallback文件以适配最新的功能。 - -### 应用程序应该提供的信息 - -为了让用户更容易编写主题文件,应用程序应该加入输出字符串定义的功能。该输出信息应该包含路径名称和默认字符串。 - -比如说,应用程序可以通过`--clitheme-output-defs`来输出所有的字符串定义: - +使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-exec`执行命令以对输出应用这些替换规则: + +```plaintext +$ clitheme apply-theme clang-theme.clithemedef.txt +$ clitheme-exec clang test.c +test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ +bool *func(int *a) { +^ +test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] + b=a; + ^~ +2 errors generated. ``` -$ example-app --clitheme-output-defs -com.example example-app found-file -在当前目录找到了{}个文件 -com.example example-app installing-file --> 正在安装"{}"... +## 自定义manpage文档 -com.example example-app install-success -已成功安装{}个文件 +编写/编辑manpage文档的源代码,并且保存在一个位置中: -com.example example-app file-not-found -错误:找不到文件 "{}" +```plaintext +$ nano man-pages/1/ls-custom.txt +# <编辑文件> +$ nano man-pages/1/cat-custom.txt +# <编辑文件> ``` -应用程序还可以在对应的官方文档中包括此信息。如需样例,请参考本仓库中`example-clithemedef`文件夹的[README文件](example-clithemedef/README.zh-CN.md)。 - -### 编写主题文件 +编写主题定义文件: + +```plaintext +{header_section} + name 样例文档手册主题 + description 一个manpage文档手册样例主题 +{/header_section} + +{manpage_section} + # 在"include_file"后添加*由空格分开*的文件路径(以主题定义文件所在的路径为父路径) + # 在"as"后添加*由空格分开*的目标文件路径(放在如`/usr/share/man`文件夹下的文件路径) + include_file man-pages 1 ls-custom.txt + as man1 ls.1 + include_file man-pages 1 cat-custom.txt + as man1 cat.1 +{/manpage_section} +``` -关于主题文件的详细语法请见Wiki文档,下面将展示一个样例: +使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-man`查看这些自定义文档(使用方法和选项和`man`一样): +```plaintext +$ clitheme apply-theme manpage-theme.clithemedef.txt +$ clitheme-man cat +$ clitheme-man ls ``` -begin_header - name 样例主题 - version 1.0 - locales zh_CN - supported_apps clitheme_demo -end_header - -begin_main - in_domainapp com.example example-app - entry found-file - locale default o(≧v≦)o 太好了,在当前目录找到了{}个文件! - locale zh_CN o(≧v≦)o 太好了,在当前目录找到了{}个文件! - end_entry - entry installing-file - locale default (>^ω^<) 正在安装 "{}"... - locale zh_CN (>^ω^<) 正在安装 "{}"... - end_entry - entry install-success - locale default o(≧v≦)o 已成功安装{}个文件! - locale zh_CN o(≧v≦)o 已成功安装{}个文件! - end_entry - entry file-not-found - locale default ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" - locale zh_CN ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" - end_entry -end_main -``` -编写好主题文件后,使用 `clitheme apply-theme `来应用主题。应用程序会直接采用主题中适配的字符串。 +## 应用程序API和字符串定义 + +请见[此文档](./README-frontend.md) -# 安装 +# 安装与构建 -安装`clitheme`非常简单,您可以通过Arch Linux软件包,Debian软件包,或者pip软件包安装。 +安装`clitheme`非常简单,您可以通过pip软件包,Arch Linux软件包,或者Debian软件包安装。 ### 通过pip软件包安装 -从最新发行版页面下载whl文件,使用`pip`直接安装即可: +从最新发行版页面下载`.whl`文件,使用`pip`直接安装即可: - $ pip install clitheme--py3-none-any.whl + $ pip install ./clitheme--py3-none-any.whl ### 通过Arch Linux软件包安装 -因为Arch Linux上无法使用`pip`往系统里直接安装pip软件包,所以本项目支持通过Arch Linux软件包安装。 - 因为构建的Arch Linux软件包只兼容特定的Python版本,并且升级Python版本后会导致原软件包失效,本项目仅提供构建软件包的方式,不提供构建好的软件包。详细请见下方的**构建Arch Linux软件包**。 ### 通过Debian软件包安装 -因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 - 如需在Debian系统上安装,请从最新发行版页面下载`.deb`文件,使用`apt`安装即可: $ sudo apt install ./clitheme__all.deb @@ -203,17 +186,15 @@ end_main ### 构建pip软件包 -`clitheme`使用的是`hatchling`构建器,所以构建软件包前需要安装它。 - -首先,安装`hatch`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: +`clitheme`使用的是`setuptools`构建器,所以构建软件包前需要安装它。 - $ pip install hatch +首先,安装`setuptools`和`build`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: -然后,切换到项目目录,使用`hatch build`构建软件包: + $ pip install setuptools build - $ hatch build +然后,切换到项目目录,使用以下命令构建软件包: -如果这个指令无法正常运行,请尝试运行`hatchling build`。 + $ python3 -m build --wheel --no-isolation 构建完成后,相应的安装包文件可以在当前目录中的`dist`文件夹中找到。 @@ -223,6 +204,11 @@ end_main $ sudo pacman -S base-devel +构建软件包前,请先确保任何对仓库文件的更改以被提交(git commit): + + $ git add . + $ git commit + 构建软件包只需要在仓库目录中执行`makepkg`指令就可以了。你可以通过以下一系列命令来完成这些操作: ```sh @@ -237,32 +223,26 @@ makepkg -si rm -rf buildtmp srctmp ``` -**注意:** 每次升级Python版本时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 +**注意:** 每次升级Python时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 ### 构建Debian软件包 -因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 - 构建Debian软件包前,你需要安装以下用于构建的系统组件: - `debhelper` - `dh-python` -- `python3-hatchling` +- `python3-setuptools` - `dpkg-dev` - `pybuild-plugin-pyproject` 你可以使用以下命令安装: - sudo apt install debhelper dh-python python3-hatchling dpkg-dev pybuild-plugin-pyproject + sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject 安装完后,请在仓库目录中执行`dpkg-buildpackage -b`以构建软件包。完成后,你会在上层目录中获得一个`.deb`的文件。 -## 更多信息 +# 更多信息 -- 更多的详细信息和文档请参考本项目Wiki页面:https://gitee.com/swiftycode/clitheme/wikis/pages - - 你也可以通过以下仓库访问这些Wiki页面: - - https://gitee.com/swiftycode/clitheme-wiki-repo - - https://github.com/swiftycode256/clitheme-wiki-repo - 本仓库中的代码也同步在GitHub上(使用Gitee仓库镜像功能自动同步):https://github.com/swiftycode256/clitheme - 该项目的最新进展、未来计划、和开发中的新功能会在这里Gitee仓库中的Issues里列出:https://gitee.com/swiftycode/clitheme/issues - 欢迎通过Issues和Pull Requests提交建议和改进。 -- Gitee From 4d4dce813c4d4921aa3b35eeae94ae87e833f90f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 10:08:29 +0800 Subject: [PATCH 255/354] Fix missing link in README.en.md --- README.en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.en.md b/README.en.md index 9b31f56..5db7109 100644 --- a/README.en.md +++ b/README.en.md @@ -159,7 +159,7 @@ $ clitheme-man ls ## Application frontend API and string entries -Please see [this article][todo] +Please see [this article](./README-frontend.en.md) # Installing and building -- Gitee From 333e4322dc38d622c42a4eec716ae2b9e23016fa Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 10:54:43 +0800 Subject: [PATCH 256/354] Add comments in string files --- .../strings/cli-strings.clithemedef.txt | 5 +- .../strings/exec-strings.clithemedef.txt | 2 + .../strings/generator-strings.clithemedef.txt | 214 +++++++++--------- 3 files changed, 116 insertions(+), 105 deletions(-) diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index e32f6b2..7ab5c66 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -13,7 +13,6 @@ {entries_section} in_domainapp swiftycode clitheme - # 命令行指令 in_subsection cli [entry] no-command locale:zh_CN 错误:没有提供指令或选项 @@ -37,6 +36,7 @@ in_domainapp swiftycode clitheme locale:zh_CN clitheme 版本:{ver} [/entry] # apply-theme 和 generate-data 指令 + # apply-theme and generate-data commands in_subsection cli apply-theme [entry] generate-data-msg locale:zh_CN 主题定义数据将会从以下顺序的主题定义文件生成: @@ -102,6 +102,7 @@ in_domainapp swiftycode clitheme [/locale] [/entry] # unset-current-theme 指令 + # unset-current-theme command in_subsection cli unset-current-theme [entry] no-data-found locale:zh_CN 错误:当前没有设定主题 @@ -116,6 +117,7 @@ in_domainapp swiftycode clitheme locale:zh_CN 已成功移除当前主题数据 [/entry] # get-current-theme-info 指令 + # get-current-theme-info command in_subsection cli get-current-theme-info [entry] no-theme locale:zh_CN 当前没有设定任何主题 @@ -139,6 +141,7 @@ in_domainapp swiftycode clitheme locale:zh_CN 支持的应用程序: [/entry] # update-theme 指令 + # update-theme command in_subsection cli update-theme [entry] not-available-err [locale] zh_CN diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index 242f42e..04e3340 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -15,6 +15,8 @@ {entries_section} in_domainapp swiftycode clitheme + # 用于clitheme-exec --help的字符串定义 + # String entries used for "clitheme-exec --help" in_subsection exec help-message [entry] usage-str locale:zh_CN 使用方式: diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 46c39d4..a046f9a 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -14,109 +14,115 @@ {entries_section} in_domainapp swiftycode clitheme -in_subsection generator - # error strings - [entry] error-str - locale:zh_CN 语法错误:{msg} - [/entry] - [entry] subsection-conflict-err - locale:zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 - [/entry] - [entry] entry-conflict-err - locale:zh_CN 第{num}行:无法创建定义"{name}",因为拥有相同名称的子路径已存在 - [/entry] - [entry] extra-arguments-err - locale:zh_CN 第{num}行:"{phrase}"后的参数太多 - [/entry] - [entry] repeated-section-err - locale:zh_CN 第{num}行:重复的{phrase}段落 - [/entry] - [entry] invalid-phrase-err - locale:zh_CN 第{num}行:无效的"{phrase}" - [/entry] - [entry] not-enough-args-err - locale:zh_CN 第{num}行:"{phrase}"后参数不够 - [/entry] - [entry] incomplete-section-err - locale:zh_CN 文件缺少或包含不完整的header或内容段落 - [/entry] - # Options error strings - [entry] option-not-allowed-err - locale:zh_CN 第{num}行:选项"{phrase}"不允许再这里指定 - [/entry] - [entry] unknown-option-err - locale:zh_CN 第{num}行:未知选项"{phrase}" - [/entry] - [entry] option-without-value-err - locale:zh_CN 第{num}行:选项"{phrase}"未指定数值 - [/entry] - [entry] optional-value-not-int-err - locale:zh_CN 第{num}行:选项"{phrase}"指定的数值不是整数 - [/entry] - [entry] option-conflict-err - locale:zh_CN 第{num}行:选项"{option1}"和"{option2}"不能同时指定 - [/entry] - [entry] bad-match-pattern-err - locale:zh_CN 第{num}行:无效的匹配正则表达式({error_msg}) - [/entry] - [entry] bad-subst-pattern-err - locale:zh_CN 第{num}行:无效的替换正则表达式({error_msg}) - [/entry] - [entry] bad-var-name-err - locale:zh_CN 第{num}行:"{name}"不是一个有效的变量名称 - [/entry] - [entry] manpage-subdir-file-conflict-err - locale:zh_CN 第{num}行:子路径和文件有冲突;请检查之前的定义 - [/entry] - [entry] include-file-read-err - [locale] zh_CN - 第{num}行:无法读取文件"{filepath}": - {error_msg} - [/locale] - [/entry] - [entry] include-file-missing-phrase-err - locale:zh_CN 第{num}行:在下一行缺少"as <文件名>"语句 - [/entry] - # warning strings - [entry] warning-str - locale:zh_CN 警告:{msg} - [/entry] - [entry] repeated-entry-warn - locale:zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 - [/entry] - [entry] repeated-substrules-warn - locale:zh_CN 第{num}行:重复的substrules定义;之前的定义内容将会被覆盖 - [/entry] - [entry] repeated-header-warn - locale:zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 - [/entry] - [entry] syntax-phrase-deprecation-warn - locale:zh_CN 第{num}行:"{old_phrase}"在当前版本中已被弃用;请使用"{new_phrase}" - [/entry] - [entry] unknown-variable-warn - locale:zh_CN 第{num}行:未知变量名称"{name}",不会进行替换 - [/entry] - [entry] repeated-manpage-warn - locale:zh_CN 第{num}行:重复的manpage文件;之前的文件内容将会被覆盖 - [/entry] - # sanity check - [entry] sanity-check-entry-err - locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} - [/entry] - [entry] sanity-check-domainapp-err - locale:zh_CN 第{num}行:开发者和应用程序名称{sanitycheck_msg} - [/entry] - [entry] sanity-check-subsection-err - locale:zh_CN 第{num}行:子路径名称{sanitycheck_msg} - [/entry] - [entry] sanity-check-manpage-err - locale:zh_CN 第{num}行:manpage路径{sanitycheck_msg};使用空格以指定子路径 - [/entry] - [entry] sanity-check-msg-banphrase-err - locale:zh_CN 不能包含'{char}' - [/entry] - [entry] sanity-check-msg-startswith-err - locale:zh_CN 不能以'{char}'开头 - [/entry] + in_subsection generator + # 错误提示 + # error messages + [entry] error-str + locale:zh_CN 语法错误:{msg} + [/entry] + [entry] subsection-conflict-err + locale:zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 + [/entry] + [entry] entry-conflict-err + locale:zh_CN 第{num}行:无法创建定义"{name}",因为拥有相同名称的子路径已存在 + [/entry] + [entry] extra-arguments-err + locale:zh_CN 第{num}行:"{phrase}"后的参数太多 + [/entry] + [entry] repeated-section-err + locale:zh_CN 第{num}行:重复的{phrase}段落 + [/entry] + [entry] invalid-phrase-err + locale:zh_CN 第{num}行:无效的"{phrase}" + [/entry] + [entry] not-enough-args-err + locale:zh_CN 第{num}行:"{phrase}"后参数不够 + [/entry] + [entry] incomplete-section-err + locale:zh_CN 文件缺少或包含不完整的header或内容段落 + [/entry] + # 选项错误提示信息 + # Options error messages + [entry] option-not-allowed-err + locale:zh_CN 第{num}行:选项"{phrase}"不允许再这里指定 + [/entry] + [entry] unknown-option-err + locale:zh_CN 第{num}行:未知选项"{phrase}" + [/entry] + [entry] option-without-value-err + locale:zh_CN 第{num}行:选项"{phrase}"未指定数值 + [/entry] + [entry] optional-value-not-int-err + locale:zh_CN 第{num}行:选项"{phrase}"指定的数值不是整数 + [/entry] + [entry] option-conflict-err + locale:zh_CN 第{num}行:选项"{option1}"和"{option2}"不能同时指定 + [/entry] + [entry] bad-match-pattern-err + locale:zh_CN 第{num}行:无效的匹配正则表达式({error_msg}) + [/entry] + [entry] bad-subst-pattern-err + locale:zh_CN 第{num}行:无效的替换正则表达式({error_msg}) + [/entry] + [entry] bad-var-name-err + locale:zh_CN 第{num}行:"{name}"不是一个有效的变量名称 + [/entry] + [entry] manpage-subdir-file-conflict-err + locale:zh_CN 第{num}行:子路径和文件有冲突;请检查之前的定义 + [/entry] + [entry] include-file-read-err + [locale] zh_CN + 第{num}行:无法读取文件"{filepath}": + {error_msg} + [/locale] + [/entry] + [entry] include-file-missing-phrase-err + locale:zh_CN 第{num}行:在下一行缺少"as <文件名>"语句 + [/entry] + # 警告提示 + # Warning messages + [entry] warning-str + locale:zh_CN 警告:{msg} + [/entry] + [entry] repeated-entry-warn + locale:zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 + [/entry] + [entry] repeated-substrules-warn + locale:zh_CN 第{num}行:重复的substrules定义;之前的定义内容将会被覆盖 + [/entry] + [entry] repeated-header-warn + locale:zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 + [/entry] + [entry] syntax-phrase-deprecation-warn + locale:zh_CN 第{num}行:"{old_phrase}"在当前版本中已被弃用;请使用"{new_phrase}" + [/entry] + [entry] unknown-variable-warn + locale:zh_CN 第{num}行:未知变量名称"{name}",不会进行替换 + [/entry] + [entry] repeated-manpage-warn + locale:zh_CN 第{num}行:重复的manpage文件;之前的文件内容将会被覆盖 + [/entry] + # 路径检查功能提示 + # Sanity check feature messages + [entry] sanity-check-entry-err + locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} + [/entry] + [entry] sanity-check-domainapp-err + locale:zh_CN 第{num}行:开发者和应用程序名称{sanitycheck_msg} + [/entry] + [entry] sanity-check-subsection-err + locale:zh_CN 第{num}行:子路径名称{sanitycheck_msg} + [/entry] + [entry] sanity-check-manpage-err + locale:zh_CN 第{num}行:manpage路径{sanitycheck_msg};使用空格以指定子路径 + [/entry] + # 以下提示会是上方定义中"{sanitycheck_msg}"的内容 + # The following messages are the contents of "{sanitycheck_msg}" in the above entries + [entry] sanity-check-msg-banphrase-err + locale:zh_CN 不能包含'{char}' + [/entry] + [entry] sanity-check-msg-startswith-err + locale:zh_CN 不能以'{char}'开头 + [/entry] {/entries_section} \ No newline at end of file -- Gitee From 1986c8cf3f1c99a4a90753a639462d6e05501ed7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 18:31:56 +0800 Subject: [PATCH 257/354] Add default content as comment in cli-strings --- .../strings/cli-strings.clithemedef.txt | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index 7ab5c66..b5450d4 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -15,87 +15,123 @@ in_domainapp swiftycode clitheme in_subsection cli [entry] no-command + # locale:default Error: no command or option specified locale:zh_CN 错误:没有提供指令或选项 [/entry] [entry] not-enough-arguments + # locale:default Error: not enough arguments locale:zh_CN 错误:参数不够 [/entry] [entry] unknown-option + # locale:default Error: unknown option "{option}" locale:zh_CN 错误:未知选项"{option}" [/entry] [entry] unknown-command + # locale:default Error: unknown command "{cmd}" locale:zh_CN 错误:未知指令"{cmd}" [/entry] [entry] too-many-arguments + # locale:default Error: too many arguments locale:zh_CN 错误:参数太多 [/entry] [entry] help-usage-prompt - locale:zh_CN 使用"clitheme --help"以获取使用方法 + # locale:default Run "{clitheme} --help" for usage information + locale:zh_CN 使用"{clitheme} --help"以获取使用方法 [/entry] [entry] version-str + # locale:default clitheme version {ver} locale:zh_CN clitheme 版本:{ver} [/entry] # apply-theme 和 generate-data 指令 # apply-theme and generate-data commands in_subsection cli apply-theme [entry] generate-data-msg + # locale:default The theme data will be generated from the following definition files in the following order: locale:zh_CN 主题定义数据将会从以下顺序的主题定义文件生成: [/entry] [entry] apply-theme-msg + # locale:default The following definition files will be applied in the following order: locale:zh_CN 这些主题定义文件将会通过以下顺序被应用: [/entry] [entry] overwrite-notice + # locale:default The existing theme data will be overwritten if you continue. locale:zh_CN 如果继续,当前的主题数据将会被覆盖。 [/entry] [entry] overlay-notice + # locale:default The definition files will be appended on top of the existing theme data. locale:zh_CN 这些主题定义文件会被叠加在当前的数据上。 [/entry] [entry] confirm-prompt + # locale:default Do you want to continue? [y/n] locale:zh_CN 是否继续操作?[y/n] [/entry] [entry] overlay-msg + # locale:default Overlay specified locale:zh_CN 已使用数据叠加模式 [/entry] [entry] generating-data + # locale:default ==> Generating data... locale:zh_CN ==> 正在生成数据... [/entry] [entry] processing-file + # locale:default > Processing file {filename}... locale:zh_CN > 正在处理文件{filename} [/entry] [entry] all-finished + # locale:default > All finished locale:zh_CN > 已处理全部文件 [/entry] [entry] generate-data-success + # locale:default Successfully generated data locale:zh_CN 已成功生成数据 [/entry] [entry] view-temp-dir + # locale:default View at {path} locale:zh_CN 生成的数据可以在"{path}"查看 [/entry] [entry] applying-theme + # locale:default ==> Applying theme... locale:zh_CN ==> 正在应用主题... [/entry] [entry] apply-theme-success + # locale:default Theme applied successfully locale:zh_CN 已成功应用主题 [/entry] [entry] read-file-error + # [locale] default + # [File {index}] An error occurred while reading the file: + # {message} + # [/locale] [locale] zh_CN [文件{index}] 读取文件时出现了错误: {message} [/locale] [/entry] [entry] overlay-no-data + # [locale] default + # Error: no theme set or the current data is corrupt + # Try setting a theme first + # [/locale] [locale] zh_CN 错误:当前没有设定主题或当前数据损坏 请尝试设定主题 [/locale] [/entry] [entry] overlay-data-error + # [locale] default + # Error: the current data is corrupt + # Remove the current theme, set the theme, and try again + # [/locale] [locale] zh_CN 错误:当前主题数据损坏 请移除当前数据和重新设定主题后重试 [/locale] [/entry] [entry] generate-data-error + # [locale] default + # [File {index}] An error occurred while generating the data: + # {message} + # [/locale] [locale] zh_CN [文件{index}] 生成数据时发生了错误: {message} @@ -105,64 +141,94 @@ in_domainapp swiftycode clitheme # unset-current-theme command in_subsection cli unset-current-theme [entry] no-data-found + # locale:default Error: No theme data present (no theme was set) locale:zh_CN 错误:当前没有设定主题 [/entry] [entry] remove-data-error + # [locale] default + # An error occurred while removing the data: + # {message} + # [/locale] [locale] zh_CN 移除当前数据时发生了错误: {message} [/locale] [/entry] [entry] remove-data-success + # locale:default Successfully removed the current theme data locale:zh_CN 已成功移除当前主题数据 [/entry] # get-current-theme-info 指令 # get-current-theme-info command in_subsection cli get-current-theme-info [entry] no-theme + # locale:default No theme currently set locale:zh_CN 当前没有设定任何主题 [/entry] [entry] current-theme-msg + # locale:default Currently installed theme: 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} [/entry] [entry] description-str + # locale:default Description: locale:zh_CN 详细说明: [/entry] [entry] locales-str + # locale:default Supported locales: locale:zh_CN 支持的语言: [/entry] [entry] supported-apps-str + # locale:default Supported apps: locale:zh_CN 支持的应用程序: [/entry] # update-theme 指令 # update-theme command in_subsection cli update-theme [entry] not-available-err + # [locale] default + # update-theme cannot be used with the current theme setting + # Please re-apply the current theme and try again + # [/locale] [locale] zh_CN update-theme在当前主题设定上无法使用 请重新应用当前主题后重试 [/locale] [/entry] [entry] other-err + # [locale] default + # An error occurred while processing file path information: {msg} + # Please re-apply the current theme and try again + # [/locale] [locale] zh_CN 处理文件路径信息时发生错误:{msg} 请重新应用当前主题后重试 [/locale] [/entry] + # 用于clitheme --help的字符串定义 + # String entries used for "clitheme --help" in_subsection cli help-message [entry] usage-str + # locale:default Usage: locale:zh_CN 使用方式: [/entry] [entry] options-str + # locale:default Options: locale:zh_CN 选项: [/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) + # [/locale] [locale] zh_CN apply-theme:将指定的主题定义文件应用到当前系统中 指定"--overlay"选项以保留当前主题数据的情况下应用(添加到当前数据中) @@ -170,21 +236,27 @@ in_domainapp swiftycode clitheme [/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:输出当前主题设定的详细信息 [/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: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:zh_CN generate-data:【仅供调试用途】对于指定的主题定义文件在临时目录中生成一个数据结构 [/entry] [entry] options-version + # locale:default --version: Outputs 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:输出这个帮助提示 [/entry] {/entries_section} -- Gitee From 39a41f472f03ff03b5023e371c25dc3da765dff6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 18:55:11 +0800 Subject: [PATCH 258/354] Add default content as comment in generator-strings --- .../strings/generator-strings.clithemedef.txt | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index a046f9a..7acf269 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -18,110 +18,145 @@ in_domainapp swiftycode clitheme # 错误提示 # error messages [entry] error-str + # locale:default Syntax error: {msg} locale:zh_CN 语法错误:{msg} [/entry] [entry] subsection-conflict-err + # locale:default Line {num}: cannot create subsection "{name}" because an entry with the same name already exists locale:zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 [/entry] [entry] entry-conflict-err + # locale:default Line {num}: cannot create entry "{name}" because a subsection with the same name already exists locale:zh_CN 第{num}行:无法创建定义"{name}",因为拥有相同名称的子路径已存在 [/entry] [entry] extra-arguments-err + # locale:default Extra arguments after "{phrase}" on line {num} locale:zh_CN 第{num}行:"{phrase}"后的参数太多 [/entry] [entry] repeated-section-err + # locale:default Repeated {section} section at line {num} locale:zh_CN 第{num}行:重复的{phrase}段落 [/entry] [entry] invalid-phrase-err + # locale:default Unexpected "{phrase}" on line {num} locale:zh_CN 第{num}行:无效的"{phrase}" [/entry] [entry] not-enough-args-err + # locale:default Not enough arguments for "{phrase}" at line {num} locale:zh_CN 第{num}行:"{phrase}"后参数不够 [/entry] [entry] incomplete-section-err + # locale:default Missing or incomplete header or content sections locale:zh_CN 文件缺少或包含不完整的header或内容段落 [/entry] # 选项错误提示信息 # Options error messages [entry] option-not-allowed-err - locale:zh_CN 第{num}行:选项"{phrase}"不允许再这里指定 + # locale:default Option "{phrase}" not allowed here at line {num} + locale:zh_CN 第{num}行:选项"{phrase}"不允许在这里指定 [/entry] [entry] unknown-option-err + # locale:default Unknown option "{phrase}" on line {num} locale:zh_CN 第{num}行:未知选项"{phrase}" [/entry] [entry] option-without-value-err + # locale:default No value specified for option "{phrase}" on line {num} locale:zh_CN 第{num}行:选项"{phrase}"未指定数值 [/entry] [entry] optional-value-not-int-err + # locale:default The value specified for option "{phrase}" is not an integer on line {num} locale:zh_CN 第{num}行:选项"{phrase}"指定的数值不是整数 [/entry] [entry] option-conflict-err + # locale:default The option "{option1}" can't be set at the same time with "{option2}" on line {num} locale:zh_CN 第{num}行:选项"{option1}"和"{option2}"不能同时指定 [/entry] [entry] bad-match-pattern-err + # locale:default Bad match pattern at line {num} ({error_msg}) locale:zh_CN 第{num}行:无效的匹配正则表达式({error_msg}) [/entry] [entry] bad-subst-pattern-err + # locale:default Bad substitute pattern at line {num} ({error_msg}) locale:zh_CN 第{num}行:无效的替换正则表达式({error_msg}) [/entry] [entry] bad-var-name-err + # locale:default Line {num}: "{name}" is not a valid variable name locale:zh_CN 第{num}行:"{name}"不是一个有效的变量名称 [/entry] [entry] manpage-subdir-file-conflict-err + # locale:default Line {num}: conflicting files and subdirectories; please check previous definitions locale:zh_CN 第{num}行:子路径和文件有冲突;请检查之前的定义 [/entry] [entry] include-file-read-err + # [locale] default + # Line {num}: unable to read file "{filepath}": + # {error_msg} + # [/locale] [locale] zh_CN 第{num}行:无法读取文件"{filepath}": {error_msg} [/locale] [/entry] [entry] include-file-missing-phrase-err + # locale:default Missing "as " phrase on next line of line {num} locale:zh_CN 第{num}行:在下一行缺少"as <文件名>"语句 [/entry] # 警告提示 # Warning messages [entry] warning-str + # locale:default Warning: {msg} locale:zh_CN 警告:{msg} [/entry] [entry] repeated-entry-warn + # locale:default Line {num}: repeated entry "{name}", overwriting locale:zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 [/entry] [entry] repeated-substrules-warn + # locale:default Repeated substrules entry at line {num}, overwriting locale:zh_CN 第{num}行:重复的substrules定义;之前的定义内容将会被覆盖 [/entry] [entry] repeated-header-warn + # locale:default Line {num}: repeated header info "{name}", overwriting locale:zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 [/entry] [entry] syntax-phrase-deprecation-warn + # locale:default Line {num}: phrase "{old_phrase}" is deprecated in this version; please use "{new_phrase}" instead locale:zh_CN 第{num}行:"{old_phrase}"在当前版本中已被弃用;请使用"{new_phrase}" [/entry] [entry] unknown-variable-warn + # locale:default Line {num}: unknown variable "{name}", not performing substitution locale:zh_CN 第{num}行:未知变量名称"{name}",不会进行替换 [/entry] [entry] repeated-manpage-warn + # locale:default Line {num}: repeated manpage file, overwriting locale:zh_CN 第{num}行:重复的manpage文件;之前的文件内容将会被覆盖 [/entry] # 路径检查功能提示 # Sanity check feature messages [entry] sanity-check-entry-err + # locale:default Line {num}: entry subsections/names {sanitycheck_msg} locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} [/entry] [entry] sanity-check-domainapp-err + # locale:default Line {num}: domain and app names {sanitycheck_msg} locale:zh_CN 第{num}行:开发者和应用程序名称{sanitycheck_msg} [/entry] [entry] sanity-check-subsection-err + # locale:default Line {num}: subsection names {sanitycheck_msg} locale:zh_CN 第{num}行:子路径名称{sanitycheck_msg} [/entry] [entry] sanity-check-manpage-err + # locale:default Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories locale:zh_CN 第{num}行:manpage路径{sanitycheck_msg};使用空格以指定子路径 [/entry] # 以下提示会是上方定义中"{sanitycheck_msg}"的内容 # The following messages are the contents of "{sanitycheck_msg}" in the above entries [entry] sanity-check-msg-banphrase-err + # locale:default cannot contain '{char}' locale:zh_CN 不能包含'{char}' [/entry] [entry] sanity-check-msg-startswith-err + # locale:default cannot start with '{char}' locale:zh_CN 不能以'{char}'开头 [/entry] {/entries_section} -- Gitee From 1e45b5c2ba592bd2b08d37b04f0618ce4c5910a6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 19:05:18 +0800 Subject: [PATCH 259/354] Add default content as comment in exec-strings --- .../strings/exec-strings.clithemedef.txt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index 04e3340..b476073 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -19,58 +19,78 @@ in_domainapp swiftycode clitheme # String entries used for "clitheme-exec --help" in_subsection exec help-message [entry] usage-str + # locale:default Usage: locale:zh_CN 使用方式: [/entry] [entry] options-str + # locale:default Options: locale:zh_CN 选项: [/entry] [entry] options-debug + # locale:default --debug: Display indicator at the beginning of each read output by line locale:zh_CN --debug:在每一行被读取的输出前显示标记 [/entry] [entry] options-debug-color + # locale:default --debug-color: Apply color on output; used to determine stdout or stderr (BETA: stdout/stderr not implemented) locale:zh_CN --debug-color:为输出设定颜色;用于区分stdout和stderr(BETA:stdout/stderr未实现) [/entry] [entry] options-debug-newlines + # 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] [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] in_subsection exec [entry] help-usage-prompt + # locale:default Run "clitheme-exec --help" for usage information locale:zh_CN 使用"clitheme-exec --help"以获取使用方式 [/entry] [entry] unknown-option-err + # locale:default Error: unknown option "{phrase}" locale:zh_CN 错误:未知选项"{phrase}" [/entry] [entry] no-command-err + # locale:default Error: no command specified locale:zh_CN 错误:未指定命令 [/entry] [entry] no-theme-warn + # locale:default Warning: no theme set or theme does not have substrules locale:zh_CN 警告:没有设定主题或当前主题没有substrules定义 [/entry] [entry] substrules-migrate-msg - locale:zh_CN 正在迁移数据库... + # locale:default Migrating substrules database... + locale:zh_CN 正在迁移substrules数据库... [/entry] [entry] db-migration-generator-err + # locale:default Failed to generate data (full log below): locale:zh_CN 无法生成数据(完整日志在此): [/entry] [entry] db-migration-err + # [locale] default + # An error occurred while migrating the database: {msg} + # Please re-apply the theme and try again + # [/locale] [locale] zh_CN 迁移数据库时发生了错误:{msg} 请重新应用当前主题,然后重试 [/locale] [/entry] [entry] db-migrate-success-msg + # locale:default Successfully completed migration, proceeding execution locale:zh_CN 数据库迁移完成,继续执行命令 [/entry] [entry] command-fail-err + # locale:default Error: failed to run command: {msg} locale:zh_CN 错误:无法执行命令:{msg} [/entry] [entry] internal-error-err + # locale:default Error: an internal error has occurred while executing the command (execution halted): locale:zh_CN 错误:执行命令时发生内部错误(执行已终止): [/entry] {/entries_section} \ No newline at end of file -- Gitee From 4004a61896aa403c4236eec4c8ecd9cc11257def Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 19:09:46 +0800 Subject: [PATCH 260/354] Add default content as comment in man-strings --- src/clitheme/strings/man-strings.clithemedef.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/clitheme/strings/man-strings.clithemedef.txt b/src/clitheme/strings/man-strings.clithemedef.txt index 8d860e2..f9bd130 100644 --- a/src/clitheme/strings/man-strings.clithemedef.txt +++ b/src/clitheme/strings/man-strings.clithemedef.txt @@ -15,12 +15,15 @@ in_domainapp swiftycode clitheme in_subsection man [entry] man-not-installed + # locale:default Error: "man" is not installed on this system locale:zh_CN 错误:"man"未安装在此系统中 [/entry] [entry] no-theme-warn + # locale:default Warning: no theme set or theme does not contain manpages locale:zh_CN 警告:没有设定主题或当前主题没有manpages定义 [/entry] [entry] win32-not-supported + # locale:default Error: Windows platform not supported locale:zh_CN 错误:不支持Windows平台 [/entry] {/entries_section} -- Gitee From 4056ebf69fbe8668f2d7683ecf78b4410ad6ba9e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 19:17:14 +0800 Subject: [PATCH 261/354] Prevent extra messages from being displayed in set_local_themedef --- src/clitheme/frontend.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 5790a3c..b7fde5e 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -82,6 +82,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: dir_name=f"clitheme-data-{local_path_hash}" _generator.generate_custom_path() # prepare _generator.path global _alt_path_dirname + global global_debugmode 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) @@ -90,11 +91,15 @@ 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 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) except SyntaxError: 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) -- Gitee From 8bd65126b0b17bcc5fd0942b5a801454b9cf1971 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 19:21:16 +0800 Subject: [PATCH 262/354] Optimize logic in checking if content sections parsed --- src/clitheme/_generator/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 0294865..66155a5 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -57,7 +57,12 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info _manpage_parser.handle_manpage_section(obj, first_phrase) else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=first_phrase, num=str(obj.lineindex+1))) - if obj.section_parsing or not "header" in obj.parsed_sections or (not "entries" in obj.parsed_sections and not "substrules" in obj.parsed_sections and not "manpage" in obj.parsed_sections): + def is_content_parsed() -> bool: + content_sections=["entries", "substrules", "manpage"] + for section in content_sections: + if section in obj.parsed_sections: return True + return False + if obj.section_parsing or not "header" in obj.parsed_sections or not is_content_parsed(): obj.handle_error(obj.fd.reof("incomplete-section-err", "Missing or incomplete header or content sections")) # record file content for database migration/upgrade feature obj.write_infofile(obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, "file_content", obj.file_content, obj.lineindex+1, "") @@ -69,7 +74,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info path=obj.path return obj.path -# prevent circular import error +# 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 -- Gitee From 982dda255eb1194d7e4dbf6fe4e1ce8c0aecb637 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 21:48:53 +0800 Subject: [PATCH 263/354] Detect when no theme is set in update-theme command --- src/clitheme/cli.py | 4 +++- src/clitheme/strings/cli-strings.clithemedef.txt | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 9e450d4..9e2043b 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -221,7 +221,9 @@ def update_theme(): fi=frontend.FetchDescriptor(subsections="cli update-theme") try: search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname - if not os.path.isdir(search_path): raise some_exc(search_path+" not directory") + 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_num=0 for x in lsdir_result: diff --git a/src/clitheme/strings/cli-strings.clithemedef.txt b/src/clitheme/strings/cli-strings.clithemedef.txt index b5450d4..8b91fa5 100644 --- a/src/clitheme/strings/cli-strings.clithemedef.txt +++ b/src/clitheme/strings/cli-strings.clithemedef.txt @@ -192,6 +192,10 @@ in_domainapp swiftycode clitheme # update-theme 指令 # update-theme command in_subsection cli update-theme + [entry] no-theme-err + # locale:default Error: no theme currently set + locale:zh_CN 错误:当前没有设定主题 + [/entry] [entry] not-available-err # [locale] default # update-theme cannot be used with the current theme setting -- Gitee From dc205874e04132cde40519a1b6e84786aa1eea58 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 9 Jun 2024 21:53:07 +0800 Subject: [PATCH 264/354] Update version (v2.0-dev20240609) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 63b2fc3..27d3279 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240607' +pkgver='2.0_dev20240609' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index aef21ee..b3b91d8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240607-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240609-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Fri, 07 Jun 2024 22:55:00 +0800 + -- swiftycode <3291929745@qq.com> Sun, 09 Jun 2024 21:52:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 88065db..3cc6be1 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-dev20240607" +__version__="2.0-dev20240609" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240607" +version_main="2.0_dev20240609" version_buildnumber=1 \ No newline at end of file -- Gitee From eb876226a5ed51fc1eadac30adf3cb67b838b59e Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 10 Jun 2024 11:43:24 +0800 Subject: [PATCH 265/354] Remove extra import statements --- src/clitheme/_globalvar.py | 3 +-- src/clitheme/frontend.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 2fa7e90..a8e2a19 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -76,8 +76,7 @@ startswith_error_message_orig=copy(startswith_error_message) sanity_check_error_message="" # retrieve the entry only once to avoid dead loop in frontend.FetchDescriptor callbacks msg_retrieved=False -try: from . import frontend, _get_resource -except ImportError: import frontend, _get_resource +from . import frontend, _get_resource def sanity_check(path: str, use_orig: bool=False) -> bool: def retrieve_entry(): # retrieve the entry (only for the first time) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index b7fde5e..5255afb 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -50,8 +50,7 @@ def set_local_themedef(file_content: str, overlay: bool=False) -> bool: This function returns True if successful, otherwise returns False. """ - try: from . import _generator - except ImportError: import _generator + from . import _generator # Determine directory name h=hashlib.shake_256(bytes(file_content, "utf-8")) d=h.hexdigest(6) # length of 12 (6*2) -- Gitee From 82bf75c82a613bd26f5c5414c6e77b27c5ace126 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 10 Jun 2024 13:23:52 +0800 Subject: [PATCH 266/354] Properly handle executing apply-theme inside clitheme-exec --- src/clitheme/_generator/db_interface.py | 4 ++-- src/clitheme/cli.py | 13 +++++++++---- src/clitheme/exec/output_handler_posix.py | 9 ++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index f115598..18eb19e 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -60,10 +60,10 @@ def init_db(file_path: str): connection.execute(f"INSERT INTO {_globalvar.db_data_tablename}_version (value) VALUES (?)", (_globalvar.db_version,)) connection.commit() def connect_db(path: str=f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): - if not os.path.exists(path): - raise FileNotFoundError("No theme set or theme does not contain substrules") global db_path db_path=path + if not os.path.exists(path): + raise FileNotFoundError("No theme set or theme does not contain substrules") global connection connection=sqlite3.connect(db_path) # check db version diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 9e2043b..f62e5a7 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -90,7 +90,7 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p index+=1 except SyntaxError: print(f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ - index=str(i+1), message=str(sys.exc_info()[1]) )) + index=str(i+1), message=str(sys.exc_info()[1]))) return 1 if len(file_contents)>1: print(" "+f.reof("all-finished", "> All finished")) @@ -106,21 +106,26 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p print(f.reof("applying-theme", "==> Applying theme...")) # remove the current data, ignoring directory not found error - try: shutil.rmtree(_globalvar.clitheme_root_data_path) + try: + try: shutil.rmtree(_globalvar.clitheme_root_data_path) + except OSError as exc: + # Prevent errors when executing "clitheme apply-theme" in clitheme-exec + if exc.errno==66: pass # Directory not empty error when rmtree executes os.rmdir after removing files + else: raise except FileNotFoundError: pass except Exception: print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) return 1 try: - shutil.copytree(final_path, _globalvar.clitheme_root_data_path) + shutil.copytree(final_path, _globalvar.clitheme_root_data_path, dirs_exist_ok=True) except Exception: print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) return 1 print(f.reof("apply-theme-success", "Theme applied successfully")) if not preserve_temp: try: shutil.rmtree(final_path) - except Exception: pass + except: pass return 0 def unset_current_theme(): diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 39fc49b..94dc265 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -20,6 +20,7 @@ import signal import struct import copy import re +import sqlite3 import concurrent.futures from .._generator import db_interface from .. import _globalvar, frontend @@ -63,7 +64,7 @@ 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() - except FileNotFoundError: do_subst=False + except FileNotFoundError: pass stdout_fd, stdout_slave=pty.openpty() stderr_fd, stderr_slave=pty.openpty() @@ -136,6 +137,8 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) try: subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) except TimeoutError: failed=True + # Happens when no theme is set/no subst-data.db + except sqlite3.OperationalError: pass if db_interface.enable_multiprocessing: # First implementation (A): use the separate process in db_interface # No additional actions required @@ -179,10 +182,10 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) except KeyboardInterrupt: try: process.send_signal(signal.SIGINT) except KeyboardInterrupt: pass - except Exception as exc: + except: 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 exc + raise termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes return process.poll() -- Gitee From 0d3be454b38f4fa1cec2e5c311f337f297091ccf Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 11 Jun 2024 08:45:37 +0800 Subject: [PATCH 267/354] Support showing traceback in errors with env var flag --- src/clitheme/_globalvar.py | 7 ++++++- src/clitheme/cli.py | 25 ++++++++++++++++------- src/clitheme/exec/__init__.py | 2 ++ src/clitheme/exec/output_handler_posix.py | 1 + 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index a8e2a19..82b44b0 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -155,6 +155,11 @@ def get_locale(debug_mode: bool=False): if debug_mode: print("[Debug] Locale: sanity check failed ({})".format(sanity_check_error_message)) return lang +def handle_exception(): + env_var="CLITHEME_SHOW_TRACEBACK" + if env_var in os.environ and os.environ[env_var]=="1": + raise + def handle_set_themedef(fr, debug_name: str): prev_mode=False try: @@ -171,4 +176,4 @@ def handle_set_themedef(fr, debug_name: str): sys.stdout=sys.__stdout__ fr.global_debugmode=prev_mode if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) - pass \ No newline at end of file + handle_exception() \ No newline at end of file diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index f62e5a7..da3f955 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -72,6 +72,7 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p except ValueError: print(f.reof("overlay-data-error", \ "Error: the current data is corrupt\nRemove the current theme, set the theme, and try again")) + _globalvar.handle_exception() return 1 # copy the current data into the temp directory _generator.generate_custom_path() @@ -91,6 +92,7 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p except SyntaxError: print(f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ index=str(i+1), message=str(sys.exc_info()[1]))) + _globalvar.handle_exception() return 1 if len(file_contents)>1: print(" "+f.reof("all-finished", "> All finished")) @@ -115,12 +117,14 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p except FileNotFoundError: pass except Exception: print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) + _globalvar.handle_exception() return 1 try: shutil.copytree(final_path, _globalvar.clitheme_root_data_path, dirs_exist_ok=True) except Exception: print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) + _globalvar.handle_exception() return 1 print(f.reof("apply-theme-success", "Theme applied successfully")) if not preserve_temp: @@ -141,6 +145,7 @@ def unset_current_theme(): return 1 except Exception: print(f.feof("remove-data-error", "An error occurred while removing the data:\n{message}", message=str(sys.exc_info()[1]))) + _globalvar.handle_exception() return 1 print(f.reof("remove-data-success", "Successfully removed the current theme data")) return 0 @@ -220,7 +225,7 @@ def update_theme(): (Invokes 'clitheme update-theme') """ - class some_exc(Exception): pass + class invalid_theme(Exception): pass file_contents: list[str] file_paths: list[str] fi=frontend.FetchDescriptor(subsections="cli update-theme") @@ -233,7 +238,7 @@ def update_theme(): lsdir_num=0 for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 - if lsdir_num<1: raise some_exc("empty directory") + if lsdir_num<1: raise invalid_theme("empty directory") # Get file paths from clithemeinfo_filepath files file_paths=[] @@ -243,17 +248,21 @@ def update_theme(): got_path: str try: got_path=open(target_path+"/"+_globalvar.generator_info_filename.format(info="filepath"), encoding="utf-8").readline().strip() - except: raise some_exc("Read error: "+str(sys.exc_info()[1])) + except: raise invalid_theme("Read error: "+str(sys.exc_info()[1])) file_paths.append(got_path) - if len(file_paths)==0: raise some_exc("file_paths empty") + if len(file_paths)==0: raise invalid_theme("file_paths empty") # Get file contents try: file_contents=_get_file_contents(file_paths) - except: return 1 - except some_exc: + 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() return 1 except: print(fi.feof("other-err", "An error occurred while processing file path information: {msg}\nPlease re-apply the current theme and try again")) + _globalvar.handle_exception() return 1 return apply_theme(file_contents, file_paths, overlay=False) @@ -339,7 +348,9 @@ def main(cli_args: list[str]): fi=frontend.FetchDescriptor(subsections="cli apply-theme") content_list: list[str] try: content_list=_get_file_contents(paths) - except: return 1 + 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 diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 8c8f29f..0310e09 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -62,10 +62,12 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) except: sys.stdout=sys.__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]))) + _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]))) + _globalvar.handle_exception() return False return True diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 94dc265..0a33538 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -78,6 +78,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) try: process=subprocess.Popen(command, stdin=stdout_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) except: _labeled_print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) + _globalvar.handle_exception() return 1 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)) -- Gitee From 3f00658b967bab50f449b18bd027b42c2592afb7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 11 Jun 2024 09:56:38 +0800 Subject: [PATCH 268/354] Make specified locales after [locale] support substvar --- 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 139478c..ea95dba 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -286,7 +286,7 @@ class GeneratorObject(_handlers.DataHandlers): self.add_entry(self.datapath, target_entry, content, self.lineindex+1) elif phrases[0]=="locale_block" or phrases[0]=="[locale]": self.check_enough_args(phrases, 2) - locales=phrases[1:] + locales=self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() 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: -- Gitee From d69453742736644894480cffc3d8e8abae958e18 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 11 Jun 2024 16:08:25 +0800 Subject: [PATCH 269/354] Use connect_db function in db_interface_tests --- src/db_interface_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index fee0415..eb1f174 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -109,7 +109,7 @@ substrules_file=r""" db_interface.debug_mode=True generator_path=_generator.generate_data_hierarchy(substrules_file) -db_interface.connection=db_interface.sqlite3.connect(generator_path+"/"+_globalvar.db_filename) +db_interface.connect_db(generator_path+"/"+_globalvar.db_filename) print("Successfully recorded data\nTesting sample outputs: ") for inp in sample_inputs: -- Gitee From af0e8b578067982deb1e86fffeb5aff36ad2c2e4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 11 Jun 2024 16:30:57 +0800 Subject: [PATCH 270/354] Implement output checking in db_interface_tests --- src/db_interface_tests.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/db_interface_tests.py b/src/db_interface_tests.py index eb1f174..636e56d 100644 --- a/src/db_interface_tests.py +++ b/src/db_interface_tests.py @@ -1,6 +1,8 @@ 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"), @@ -20,6 +22,24 @@ sample_inputs=[("rm: missing operand", "rm"), ("example_app: using recursive directories", "example_app -rlc"), # test smartcmdmatch ("example_app: using list options", "/usr/bin/example_app -rlc"), # test smartcmdmatch and command basename handling ] +expected_outputs=[ + ("rm says: missing arguments and options (>﹏<)", "rm 说:缺少参数和选项 (>﹏<)"), + ("For more information, use rm --help (。ì _ í。)", "关于更多信息,请使用rm --help (。ì _ í。)"), + ("rm says: Access denied to /etc/folder! ಥ_ಥ", "rm 说:文件\"/etc/folder\"拒绝访问!ಥ_ಥ"), + ("rm: /etc/file: Permission denied",), + ("cat says: Access denied to /dev/mem! ಥ_ಥ", "cat 说:文件\"/dev/mem\"拒绝访问!ಥ_ಥ"), + ("bash says: Access denied to /etc/secret! ಥ_ಥ", "bash 说:文件\"/etc/secret\"拒绝访问!ಥ_ಥ"), + ("ls says: Access denied to /etc/secret! ಥ_ಥ", "ls 说:文件\"/etc/secret\"拒绝访问!ಥ_ಥ"), + ("ls: /etc/secret: Permission denied",), + ("ls says: option \"--help\" not known! (ToT)/~~~", "ls 说:未知选项\"--help\"!(ToT)/~~~"), + ("o(≧v≦)o Note: input is invalid! ಥ_ಥ", "o(≧v≦)o 提示: 无效输入!ಥ_ಥ"), + ("(ToT)/~~~ Error: input is invalid! ಥ_ಥ", "(ToT)/~~~ 错误:无效输入!ಥ_ಥ"), + ("(ToT)/~~~ Error: sample message", "(ToT)/~~~ 错误:sample message"), + ("Error: sample message! (>﹏<)", "错误:样例提示!(>﹏<)"), + ("rm says: Operation not permitted! ಥ_ಥ", "rm 说:不允许的操作!ಥ_ಥ"), + ("o(≧v≦)o example_app says: using recursive directories! (。ì _ í。)", "o(≧v≦)o example_app 说: 正在使用子路径!(。ì _ í。)"), + ("o(≧v≦)o example_app says: using list options! (⊙ω⊙)", "o(≧v≦)o example_app 说: 正在使用列表选项!(⊙ω⊙)"), +] # substitute patterns substrules_file=r""" {header_section} @@ -83,7 +103,7 @@ substrules_file=r""" filter_command rm file.ban [substitute_regex] (?P.+): (?P.+): Operation not permitted locale:default \g says: Operation not permitted! ಥ_ಥ - locale:zh_CN \g 说:缺少操作参数!ಥ_ಥ + locale:zh_CN \g 说:不允许的操作!ಥ_ಥ [/substitute_regex] set_options normalcmdmatch @@ -112,8 +132,14 @@ generator_path=_generator.generate_data_hierarchy(substrules_file) db_interface.connect_db(generator_path+"/"+_globalvar.db_filename) print("Successfully recorded data\nTesting sample outputs: ") -for inp in sample_inputs: - print(db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8')) +for x in range(len(sample_inputs)): + inp=sample_inputs[x] + expected=expected_outputs[x] + content=db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8') + if content in expected: + print("\x1b[1;32mOK\x1b[0m: "+content) + else: + print("\x1b[1;31mMismatch\x1b[0m: "+content) try: shutil.rmtree(generator_path) except: pass \ No newline at end of file -- Gitee From 4f7019c190a5ac8e21ddf6e0ca42056f86beedfe Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 11 Jun 2024 17:33:56 +0800 Subject: [PATCH 271/354] Add links to README for other language version --- README.en.md | 4 ++++ README.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.en.md b/README.en.md index 5db7109..f010728 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,9 @@ # clitheme - Command line customization utility +[**中文**](./README.md) | **English** + +--- + `clitheme` allows you to customize the output of command line applications, giving them the style and personality you want. Example: diff --git a/README.md b/README.md index 73f48bb..8d76d7a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # clitheme - 命令行自定义工具 +**中文** | [**English**](./README.en.md) + +--- + `clitheme`允许你对命令行输出进行个性化定制,给它们一个你想要的风格和个性。 样例: -- Gitee From 7dbc1c8921bc26ad48dd7d4ba70d53606c993db6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 11 Jun 2024 21:52:15 +0800 Subject: [PATCH 272/354] Update version (v2.0-dev20240611) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 27d3279..0fabf12 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240609' +pkgver='2.0_dev20240611' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index b3b91d8..5616f93 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240609-1) UNRELEASED; urgency=low +clitheme (2.0-dev20240611-1) UNRELEASED; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sun, 09 Jun 2024 21:52:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 11 Jun 2024 21:52:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 3cc6be1..320e1e5 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-dev20240609" +__version__="2.0-dev20240611" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240609" +version_main="2.0_dev20240611" version_buildnumber=1 \ No newline at end of file -- Gitee From fb215606983849ab495c0ea4b37cae9f27bee158 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 12 Jun 2024 19:03:55 +0800 Subject: [PATCH 273/354] Fix substvar and substesc not processed in multiple entry name input --- src/clitheme/_generator/_dataclass.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index ea95dba..dbffb77 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -257,6 +257,8 @@ class GeneratorObject(_handlers.DataHandlers): if phrases[0]==start_phrase and not names_processed: self.check_enough_args(phrases, 2) pattern=_globalvar.extract_content(line_content) + if is_substrules: pattern=self.handle_singleline_content(pattern) + else: pattern=self.subst_variable_content(pattern) check_valid_pattern(pattern) entryNames.append((pattern, uuid.uuid4())) elif phrases[0]=="locale" or phrases[0].startswith("locale:"): -- Gitee From 965f297cf66dde0bd377b7e73ebc73c16125ce8a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 12 Jun 2024 19:41:38 +0800 Subject: [PATCH 274/354] Add wheel to pip build dependencies --- README.en.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.en.md b/README.en.md index f010728..9662ac4 100644 --- a/README.en.md +++ b/README.en.md @@ -193,9 +193,9 @@ You can build the package from the repository source code, which includes any la `clitheme` uses the `setuptools` build system, so it needs to be installed beforehand. -First, install `setuptools` and `build` packages. You can use the packages provided by your Linux distribution, or install using `pip`: +First, install `setuptools`, `build`, and `wheel` packages. You can use the packages provided by your Linux distribution, or install using `pip`: - $ pip install setuptools build + $ pip install 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 8d76d7a..eea8df0 100644 --- a/README.md +++ b/README.md @@ -192,9 +192,9 @@ $ clitheme-man ls `clitheme`使用的是`setuptools`构建器,所以构建软件包前需要安装它。 -首先,安装`setuptools`和`build`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: +首先,安装`setuptools`、`build`、和`wheel`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: - $ pip install setuptools build + $ pip install setuptools build wheel 然后,切换到项目目录,使用以下命令构建软件包: -- Gitee From bb1bf27a308feb5ab7ba185867785314ec48d412 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 12 Jun 2024 20:08:02 +0800 Subject: [PATCH 275/354] Add venv (virtual environment) to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d8c17a6..87faa43 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ dist dist/* build build/* +venv +venv/* src/*.egg-info .vscode .vscode/* -- Gitee From 99a287d00c423b7c5ea64dac15c515ff55966323 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 13 Jun 2024 08:47:28 +0800 Subject: [PATCH 276/354] Update version (v2.0-beta1) --- PKGBUILD | 2 +- debian/changelog | 21 ++++++++++++++++++--- src/clitheme/_version.py | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 0fabf12..01fcd7c 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_dev20240611' +pkgver='2.0_beta1' pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 5616f93..7566cf2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,23 @@ -clitheme (2.0-dev20240611-1) UNRELEASED; urgency=low +clitheme (2.0-beta1-1) unstable; urgency=low - * In development, please see commit logs for changes + Known issues (Beta version) - -- swiftycode <3291929745@qq.com> Tue, 11 Jun 2024 21:52:00 +0800 + * Because redirecting stdout and stderr to separate streams cannot guarantee original output order as of now: + * The `subststdoutonly` and `subststderronly` options in theme definition file is currently unavailable + * The output marker will always show that output is stdout (`o>`) when using `clitheme-exec --debug` + * When executing certain commands using `clitheme-exec`, error messages such as `No access to TTY` or `No job control` might appear + * When executing `fish`, the error message `Inappropriate ioctl for device` will appear and will not run properly + * When executing `vim` and suspending the program (using `:suspend` or `^Z`), terminal settings/attributes will not be restored to normal + * Command line output substitution feature does not currently support Windows systems + * This feature might be delayed for version `v2.1` + + Code repository and latest code changes: https://gitee.com/swiftycode/clitheme/tree/v1.2_dev + + Documentation: https://gitee.com/swiftycode/clitheme-wiki-repo/tree/v1.2_dev + + Full release notes for v2.0: [please see v2.0-beta1 release page] + + -- swiftycode <3291929745@qq.com> Thu, 13 Jun 2024 08:44:00 +0800 clitheme (1.1-r2-1) unstable; urgency=medium diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 320e1e5..bb30aec 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-dev20240611" +__version__="2.0-beta1" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240611" +version_main="2.0_beta1" version_buildnumber=1 \ No newline at end of file -- Gitee From 4f60ac504f300330ebf3e7ccb0a7b79769bfcb69 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 13 Jun 2024 14:27:10 +0800 Subject: [PATCH 277/354] Add --upgrade to pip install instruction --- README.en.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index 9662ac4..7bd86ef 100644 --- a/README.en.md +++ b/README.en.md @@ -195,7 +195,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 setuptools build wheel + $ 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 eea8df0..c56b217 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ $ clitheme-man ls 首先,安装`setuptools`、`build`、和`wheel`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: - $ pip install setuptools build wheel + $ pip install --upgrade setuptools build wheel 然后,切换到项目目录,使用以下命令构建软件包: -- Gitee From 090bbf5d8b329f02b99d88742b7bf906e6ca865f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 15 Jun 2024 23:51:56 +0800 Subject: [PATCH 278/354] Execute "man" with MANPATH set only as the data path; If that fails, try executing "man" with normal path settings. --- src/clitheme/man.py | 16 +++++++++++++++- src/clitheme/strings/man-strings.clithemedef.txt | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 5d19e79..62e122a 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -39,13 +39,27 @@ def main(args: list[str]): _labeled_print(fd.reof("man-not-installed", "Error: \"man\" is not installed on this system")) return 1 env=os.environ + prev_manpath=env.get('MANPATH') # check if theme is set + theme_set=True if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.generator_manpage_pathname}"): _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not contain manpages")) + theme_set=False # set MANPATH - env['MANPATH']=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_manpage_pathname+":"+(os.environ['MANPATH'] if 'MANPATH' in os.environ else '') + if theme_set: env['MANPATH']=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_manpage_pathname + # Only try "man" with fallback settings if content arguments are specified + for x in range(1,len(args)): + arg=args[x] + # Specified '--' and contains following content arguments + if arg=='--' and len(args)>x+1: break + if not arg.startswith('-'): break + else: theme_set=False # invoke man results=subprocess.run([man_executable]+args[1:], env=env) + if results.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 '' + results=subprocess.run([man_executable]+args[1:], env=os.environ) return results.returncode def _script_main(): # for script diff --git a/src/clitheme/strings/man-strings.clithemedef.txt b/src/clitheme/strings/man-strings.clithemedef.txt index f9bd130..7d447ff 100644 --- a/src/clitheme/strings/man-strings.clithemedef.txt +++ b/src/clitheme/strings/man-strings.clithemedef.txt @@ -26,5 +26,8 @@ in_domainapp swiftycode clitheme # locale:default Error: Windows platform not supported locale:zh_CN 错误:不支持Windows平台 [/entry] + [entry] prev-command-fail + locale:zh_CN 设定自定义路径执行"man"时发生错误,正在尝试以正常设定执行 + [/entry] {/entries_section} -- Gitee From 1af06fcffced6726e6ef43899f72629d2b20c8a3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 16 Jun 2024 00:06:25 +0800 Subject: [PATCH 279/354] Update version (v2.0-beta1.r2) --- PKGBUILD | 2 +- debian/changelog | 6 ++++++ src/clitheme/_version.py | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 01fcd7c..c20bfbe 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' pkgver='2.0_beta1' -pkgrel=1 +pkgrel=2 pkgdesc="A text theming library for command line applications" arch=('any') url="https://gitee.com/swiftycode/clitheme" diff --git a/debian/changelog b/debian/changelog index 7566cf2..1ff61d9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +clitheme (2.0-beta1-2) unstable; urgency=low + + * Fixed an issue where clitheme-man might use system man pages instead of custom defined man pages + + -- swiftycode <3291929745@qq.com> Sat, 15 Jun 2024 23:55:00 +0800 + clitheme (2.0-beta1-1) unstable; urgency=low Known issues (Beta version) diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index bb30aec..b934845 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-beta1" +__version__="2.0-beta1.r2" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead version_main="2.0_beta1" -version_buildnumber=1 \ No newline at end of file +version_buildnumber=2 \ No newline at end of file -- Gitee From a6c500994441c9db1e6a939c807e20d9e2f9c531 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 13:12:06 +0800 Subject: [PATCH 280/354] Print "Processing file" outputs on a single line Use escape sequences in "Processing file" outputs in "clitheme apply-theme" to make sure that the outputs only take up one line in total. Also, pipe generator warning outputs and output a newline before showing the outputs to make it look pretty. --- src/clitheme/_globalvar.py | 11 +++++++---- src/clitheme/cli.py | 22 +++++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 82b44b0..44bd88a 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -162,6 +162,8 @@ def handle_exception(): def handle_set_themedef(fr, debug_name: str): 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)): @@ -171,9 +173,10 @@ def handle_set_themedef(fr, debug_name: str): 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=sys.__stdout__ + sys.stdout=orig_stdout except: - sys.stdout=sys.__stdout__ + sys.stdout=orig_stdout fr.global_debugmode=prev_mode - if _version.release<0: print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1])) - handle_exception() \ No newline at end of file + 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 diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index da3f955..7c78a72 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -15,6 +15,7 @@ import os import sys import shutil import re +import io from . import _globalvar, _generator, frontend # spell-checker:ignore pathnames lsdir inpstr @@ -79,23 +80,34 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p 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 + orig_stdout=sys.stdout # Prevent interference with other code piping stdout for i in range(len(file_contents)): - if len(file_contents)>1: - print(" "+f.feof("processing-file", "> Processing file {filename}...", filename=str(i+1))) + if print_progress: + print(line_prefix+f.feof("processing-file", "> Processing file {filename}...", filename=f"({i+1}/{len(file_contents)})"), end='') file_content=file_contents[i] # Generate data hierarchy, erase current data, copy it to data path try: _generator.silence_warn=False + # Output the warning messages correctly (make sure that they start on new line if any exists) + generator_msgs=io.StringIO() + sys.stdout=generator_msgs final_path=_generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index), filename=filenames[i] if len(filenames)>0 else "") generate_path=False # Don't generate another temp folder after first one index+=1 + sys.stdout=orig_stdout # restore standard output + if generator_msgs.getvalue()!='': + print(("\n" if print_progress else "")+generator_msgs.getvalue(), end='') except SyntaxError: - print(f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ + sys.stdout=orig_stdout + print(("\n" if print_progress else "")+f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ index=str(i+1), message=str(sys.exc_info()[1]))) _globalvar.handle_exception() return 1 - if len(file_contents)>1: - print(" "+f.reof("all-finished", "> All finished")) + finally: sys.stdout=orig_stdout + if print_progress: + print(line_prefix+f.reof("all-finished", "> All finished")) print(f.reof("generate-data-success", "Successfully generated data")) global last_data_path; last_data_path=final_path if preserve_temp or generate_only: -- Gitee From f9e5313367b67911387d643488b958ad6fe02b7d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 13:43:19 +0800 Subject: [PATCH 281/354] Enable processing of escape characters in Windows Command Prompt --- src/clitheme/_globalvar.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 44bd88a..3d20dfc 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 for clitheme +Global variable definitions and initialization operations for clitheme """ import io @@ -17,6 +17,24 @@ 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 + + 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 427ca9184f27158e3c7489f18b23c1409b25bab1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 21:24:20 +0800 Subject: [PATCH 282/354] Fix "option not allowed" message incorrectly showing at unknown option --- src/clitheme/_generator/_dataclass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index dbffb77..296b32f 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -73,8 +73,6 @@ class GeneratorObject(_handlers.DataHandlers): 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) - 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=option_name)) if option_name_preserve_no in self.value_options: # must not begin with "no" # get value results=re.search(r"^(?P.+?):(?P.+)+$", each_option) @@ -102,6 +100,8 @@ class GeneratorObject(_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=option_name_preserve_no)) + 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=option_name)) return final_options def handle_set_global_options(self, options_data: list[str], really_really_global: bool=False): # set options globally -- Gitee From 8dd8c73c36ca93418109c937d676c8c9f4699f7c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 21:17:59 +0800 Subject: [PATCH 283/354] Improve option specification restriction in block input --- 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 296b32f..08a2ce1 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -213,7 +213,7 @@ class GeneratorObject(_handlers.DataHandlers): 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) for option in got_options.keys(): - def is_specified_in_block() -> bool: return option in specified_options.keys() and specified_options[option]==True + 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=option)) if option=="leadtabindents": -- Gitee From 58e5ee5565d021cfab5f73277388859e59d9fbc7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 21:53:25 +0800 Subject: [PATCH 284/354] Improve line number display in error and warning messages for block input --- src/clitheme/_generator/_dataclass.py | 36 +++++++++++++++++-------- src/clitheme/_generator/db_interface.py | 2 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 08a2ce1..2a4c1d2 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -113,7 +113,7 @@ class GeneratorObject(_handlers.DataHandlers): # reset global_options to contents of really_really_global_options self.global_options=copy.copy(self.really_really_global_options) self.global_variables=copy.copy(self.really_really_global_variables) - def subst_variable_content(self, content: str, override_check: bool=False) -> str: + def subst_variable_content(self, content: str, override_check: bool=False, line_number_debug: Optional[str]=None) -> str: if not override_check and (not "substvar" in self.global_options or self.global_options["substvar"]==False): return content # get all variables used in content new_content=copy.copy(content) @@ -125,7 +125,8 @@ class GeneratorObject(_handlers.DataHandlers): try: var_content=self.global_variables[var_name] except KeyError: - self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", num=str(self.lineindex+1), name=var_name)) + self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", \ + num=line_number_debug if line_number_debug!=None else str(self.lineindex+1), name=var_name)) continue new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) return new_content @@ -163,6 +164,9 @@ class GeneratorObject(_handlers.DataHandlers): self.section_parsing=False def handle_substesc(self, content: str) -> str: return content.replace("{{ESC}}", "\x1b") + 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: target_content=copy.copy(content) target_content=self.subst_variable_content(target_content) @@ -175,6 +179,7 @@ class GeneratorObject(_handlers.DataHandlers): 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: minspaces=math.inf blockinput_data="" + begin_line_number=self.lineindex+1+1 while self.lineindex Date: Mon, 17 Jun 2024 22:13:18 +0800 Subject: [PATCH 285/354] Handle Ctrl-C signal properly in clitheme-man --- src/clitheme/man.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/clitheme/man.py b/src/clitheme/man.py index 62e122a..b781075 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -14,6 +14,8 @@ import sys import os import subprocess import shutil +import signal +import time from . import _globalvar, frontend def _labeled_print(msg: str): print("[clitheme-man] "+msg) @@ -55,12 +57,18 @@ def main(args: list[str]): if not arg.startswith('-'): break else: theme_set=False # invoke man - results=subprocess.run([man_executable]+args[1:], env=env) - if results.returncode!=0 and theme_set: + def run_process(env) -> int: + process=subprocess.Popen([man_executable]+args[1:], env=env) + while process.poll()==None: + try: time.sleep(0.001) + except KeyboardInterrupt: process.send_signal(signal.SIGINT) + return process.poll() # type: ignore + returncode=run_process(env) + 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 '' - results=subprocess.run([man_executable]+args[1:], env=os.environ) - return results.returncode + returncode=run_process(os.environ) + return returncode def _script_main(): # for script return main(sys.argv) -- Gitee From d4e2e9d817afcfe872d177bf2112cb2cbe01569c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 22:41:42 +0800 Subject: [PATCH 286/354] Fix regular expression matching in parse_options --- src/clitheme/_generator/_dataclass.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 2a4c1d2..a797970 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -71,11 +71,11 @@ class GeneratorObject(_handlers.DataHandlers): 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 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) + option_name=re.sub(r"^(no)?(?P.+?)(:.+)?$", r"\g", each_option) + option_name_preserve_no=re.sub(r"^(?P.+?)(:.+)?$", r"\g", each_option) if option_name_preserve_no in self.value_options: # must not begin with "no" # get value - results=re.search(r"^(?P.+?):(?P.+)+$", each_option) + results=re.search(r"^(?P.+?):(?P.+)$", each_option) value: int if results==None: # no value specified self.handle_error(self.fd.feof("option-without-value-err", "No value specified for option \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=option_name)) -- Gitee From a56f95bdacf47307deb016d946ca97d30edd8e49 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 22:51:40 +0800 Subject: [PATCH 287/354] Update version (v2.0-dev20240617) --- PKGBUILD | 6 +++--- debian/changelog | 6 ++++++ src/clitheme/_version.py | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index c20bfbe..67606e3 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver='2.0_beta1' -pkgrel=2 +pkgver=2.0_dev20240617 +pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') url="https://gitee.com/swiftycode/clitheme" @@ -17,7 +17,7 @@ backup=() options=() install= changelog='debian/changelog' -source=("srctmp::git+file://$PWD#branch=v1.2_dev") # REMOVE "#branch=v1.2_dev" WHEN MERGING WITH STABLE BRANCH!! +source=("srctmp::git+file://$PWD") # Commit any active changes before building the package! noextract=() md5sums=('SKIP') validpgpkeys=() diff --git a/debian/changelog b/debian/changelog index 1ff61d9..f53881b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +clitheme (2.0-dev20240617-1) unstable; urgency=low + + * In development, please see commit logs for changes + + -- swiftycode <3291929745@qq.com> Mon, 17 Jun 2024 22:44:00 +0800 + clitheme (2.0-beta1-2) unstable; urgency=low * Fixed an issue where clitheme-man might use system man pages instead of custom defined man pages diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index b934845..f3c3612 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-beta1.r2" +__version__="2.0-dev20240617" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_beta1" -version_buildnumber=2 \ No newline at end of file +version_main="2.0_dev20240617" +version_buildnumber=1 \ No newline at end of file -- Gitee From 38eb037a86644d2718658eb77665d40e173ad895 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 17 Jun 2024 23:45:32 +0800 Subject: [PATCH 288/354] Add update-theme information to clitheme.1 man page --- docs/clitheme.1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/clitheme.1 b/docs/clitheme.1 index a51aecd..c64e757 100644 --- a/docs/clitheme.1 +++ b/docs/clitheme.1 @@ -1,4 +1,4 @@ -.TH clitheme 1 2024-05-07 +.TH clitheme 1 2024-06-17 .SH NAME clitheme \- frontend to customize output of applications .SH SYNOPSIS @@ -20,6 +20,11 @@ Outputs detailed information about the currently applied theme. If multiple them .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 +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. +.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. -- Gitee From e4b17f32b2efb07528676d47f77535fd6bf3bd48 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 18 Jun 2024 13:00:38 +0800 Subject: [PATCH 289/354] Fix strictcmdmatch not working if input command and command in filter are equal --- src/clitheme/_generator/db_interface.py | 2 +- src/db_interface_tests.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 5ca7058..fb8af34 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -143,7 +143,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F strictness: int=tp[1] # strictness setting success=True if strictness==1: # must start with pattern in terms of space-separated phrases - condition=len(match_cmd.split()): Operation not permitted", "rm file.ban"), # test exactcmdmatch ("example_app: using recursive directories", "example_app -rlc"), # test smartcmdmatch ("example_app: using list options", "/usr/bin/example_app -rlc"), # test smartcmdmatch and command basename handling @@ -36,6 +37,7 @@ expected_outputs=[ ("(ToT)/~~~ Error: input is invalid! ಥ_ಥ", "(ToT)/~~~ 错误:无效输入!ಥ_ಥ"), ("(ToT)/~~~ Error: sample message", "(ToT)/~~~ 错误:sample message"), ("Error: sample message! (>﹏<)", "错误:样例提示!(>﹏<)"), + ("Error: sample message! (>﹏<)", "错误:样例提示!(>﹏<)"), ("rm says: Operation not permitted! ಥ_ಥ", "rm 说:不允许的操作!ಥ_ಥ"), ("o(≧v≦)o example_app says: using recursive directories! (。ì _ í。)", "o(≧v≦)o example_app 说: 正在使用子路径!(。ì _ í。)"), ("o(≧v≦)o example_app says: using list options! (⊙ω⊙)", "o(≧v≦)o example_app 说: 正在使用列表选项!(⊙ω⊙)"), @@ -137,9 +139,9 @@ for x in range(len(sample_inputs)): expected=expected_outputs[x] content=db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8') if content in expected: - print("\x1b[1;32mOK\x1b[0m: "+content) + print("\x1b[1;32mOK\x1b[0;1m:\x1b[0m "+content) else: - print("\x1b[1;31mMismatch\x1b[0m: "+content) + print("\x1b[1;31mMismatch\x1b[0;1m:\x1b[0m "+content) try: shutil.rmtree(generator_path) except: pass \ No newline at end of file -- Gitee From ab494153716c7e89345ed1d295e90628ed7a0a4a Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 18 Jun 2024 15:09:48 +0800 Subject: [PATCH 290/354] Improve locale detection (get_locale function) --- src/clitheme/_globalvar.py | 45 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 3d20dfc..41845a7 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -122,7 +122,7 @@ def sanity_check(path: str, use_orig: bool=False) -> bool: ## Convenience functions -def splitarray_to_string(split_content): +def splitarray_to_string(split_content) -> str: final="" for phrase in split_content: final+=phrase+" " @@ -131,46 +131,47 @@ 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()) if results==None: raise ValueError("Match content failed (no matches)") else: return results.groupdict()['content'] -def get_locale(debug_mode: bool=False): +def get_locale(debug_mode: bool=False) -> list[str]: lang=[] + def add_language(target_lang: str): + nonlocal lang + if not sanity_check(target_lang, use_orig=True)==False: + no_encoding=re.sub(r"^(?P.+)[\.].+$", r"\g", target_lang) + lang.append(target_lang) + if no_encoding!=target_lang: lang.append(no_encoding) + else: + if debug_mode: print("[Debug] Locale \"{0}\": sanity check failed ({1})".format(target_lang, sanity_check_error_message)) + # Skip $LANGUAGE if both $LANG and $LC_ALL is set to C (treat empty as C also) - skip_LANGUAGE=False LANG_value=os.environ["LANG"] if "LANG" in os.environ and os.environ["LANG"].strip()!='' else "C" LC_ALL_value=os.environ["LC_ALL"] if "LC_ALL" in os.environ and os.environ["LC_ALL"].strip()!='' else "C" - if (LANG_value=="C" or LANG_value.startswith("C.")) and (LC_ALL_value=="C" or LC_ALL_value.startswith("C.")): skip_LANGUAGE=True + skip_LANGUAGE=(LANG_value=="C" or LANG_value.startswith("C.")) and (LC_ALL_value=="C" or LC_ALL_value.startswith("C.")) # $LANGUAGE (list of languages separated by colons) if "LANGUAGE" in os.environ and not skip_LANGUAGE: + if debug_mode: print("[Debug] Using LANGUAGE variable") target_str=os.environ['LANGUAGE'] for language in target_str.split(":"): each_language=language.strip() if each_language=="": continue - # avoid exploit of accessing top-level folders - if sanity_check(each_language)==False: continue # Ignore en and en_US (See https://wiki.archlinux.org/title/Locale#LANGUAGE:_fallback_locales) if each_language!="en" and each_language!="en_US": # Treat C as en_US also - if re.sub(r"(?P.+)[\.].+", r"\g", each_language)=="C": - lang.append(re.sub(r".+[\.]", "en_US.", each_language)) - lang.append("en_US") - lang.append(each_language) - # no encoding - lang.append(re.sub(r"(?P.+)[\.].+", r"\g", each_language)) + if re.sub(r"^(?P.+)[\.].+$", r"\g", each_language)=="C": + for item in ["en_US", "en"]: + with_encoding=re.subn(r"^.+[\.]", f"{item}.", each_language) # (content, num_of_substitutions) + if with_encoding[1]>0: add_language(with_encoding[0]) + else: add_language(item) + add_language(each_language) # $LC_ALL elif "LC_ALL" in os.environ and os.environ["LC_ALL"].strip()!="": + if debug_mode: print("[Debug] Using LC_ALL variable") target_str=os.environ["LC_ALL"].strip() - if not sanity_check(target_str, use_orig=True)==False: - lang.append(target_str) - lang.append(re.sub(r"(?P.+)[\.].+", r"\g", target_str)) - else: - if debug_mode: print("[Debug] Locale: sanity check failed ({})".format(sanity_check_error_message)) + add_language(target_str) # $LANG elif "LANG" in os.environ and os.environ["LANG"].strip()!="": + if debug_mode: print("[Debug] Using LANG variable") target_str=os.environ["LANG"].strip() - if not sanity_check(target_str, use_orig=True)==False: - lang.append(target_str) - lang.append(re.sub(r"(?P.+)[\.].+", r"\g", target_str)) - else: - if debug_mode: print("[Debug] Locale: sanity check failed ({})".format(sanity_check_error_message)) + add_language(target_str) return lang def handle_exception(): -- Gitee From a049adcfd23f7aeda47685bcb7be88c37917d86b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 00:22:57 +0800 Subject: [PATCH 291/354] Support specifying substvar and substesc in `handle_entry` block --- src/clitheme/_generator/_dataclass.py | 57 ++++++++++++------- src/clitheme/_generator/_substrules_parser.py | 1 - 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index a797970..1429072 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 +from typing import Optional, Union from .. import _globalvar from . import _handlers # spell-checker:ignore lineindex banphrases cmdmatch minspaces blockinput optline datapath matchoption @@ -66,7 +66,8 @@ 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=phrases[0])) - def parse_options(self, options_data: list[str], 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) if len(options_data)==0: return final_options # return either empty data or pre-existing global options @@ -113,7 +114,7 @@ class GeneratorObject(_handlers.DataHandlers): # reset global_options to contents of really_really_global_options self.global_options=copy.copy(self.really_really_global_options) 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) -> str: + 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 # get all variables used in content new_content=copy.copy(content) @@ -125,7 +126,7 @@ class GeneratorObject(_handlers.DataHandlers): try: var_content=self.global_variables[var_name] except KeyError: - self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", \ + if not silence_warnings: self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", \ num=line_number_debug if line_number_debug!=None else str(self.lineindex+1), name=var_name)) continue new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) @@ -240,9 +241,12 @@ class GeneratorObject(_handlers.DataHandlers): 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[str], is_regex: bool, strictness: int - entryNames: list[tuple]=[(entry_name, uuid.uuid4())] # For supporting specifying multiple entries at once (name, uuid) + entryNames: list[tuple]=[(entry_name, uuid.uuid4(), self.lineindex+1)] # For supporting specifying multiple entries at once (name, uuid, debug_linenumber) + entry_name_substesc=False; entry_name_substvar=False names_processed=False # Set to True when no more entry names are being specified - substrules_entries=[] # (match_content, substitute_content, locale, uuid, linenumber_str) + # 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[tuple]=[] substrules_endmatchhere=False substrules_stdout_stderr_option=0 def check_valid_pattern(pattern: str): @@ -261,10 +265,8 @@ class GeneratorObject(_handlers.DataHandlers): if phrases[0]==start_phrase and not names_processed: self.check_enough_args(phrases, 2) pattern=_globalvar.extract_content(line_content) - if is_substrules: pattern=self.handle_singleline_content(pattern) - else: pattern=self.subst_variable_content(pattern) check_valid_pattern(pattern) - entryNames.append((pattern, uuid.uuid4())) + entryNames.append((pattern, uuid.uuid4(), self.lineindex+1)) elif phrases[0]=="locale" or phrases[0].startswith("locale:"): content: str locale: str @@ -283,12 +285,12 @@ class GeneratorObject(_handlers.DataHandlers): content=self.handle_singleline_content(content) # handle substesc and substvar for each_name in entryNames: if is_substrules: - substrules_entries.append((each_name[0], content, None if locale=="default" else locale, each_name[1], str(self.lineindex+1))) + 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 - self.add_entry(self.datapath, target_entry, content, self.lineindex+1) + entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) elif phrases[0]=="locale_block" or phrases[0]=="[locale]": self.check_enough_args(phrases, 2) locales=self.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).split() @@ -297,15 +299,18 @@ class GeneratorObject(_handlers.DataHandlers): for this_locale in locales: for each_name in entryNames: if is_substrules: - 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))) + 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 - self.add_entry(self.datapath, target_entry, content, self.lineindex+1) + entries.append((target_entry, content, begin_line_number, each_name[1], each_name[2])) elif phrases[0]==end_phrase: - if not is_substrules: self.check_extra_args(phrases, 1, use_exact_count=True) - got_options=self.parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, allowed_options=self.subst_limiting_options+["endmatchhere"]) + got_options=self.parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, \ + allowed_options=\ + (self.subst_limiting_options+["endmatchhere"] if is_substrules else []) \ + +(self.content_subst_options if is_substrules else ["substvar"]) # don't allow substesc in `[entry]` + ) for option in got_options: if option=="endmatchhere" and got_options['endmatchhere']==True: substrules_endmatchhere=True @@ -313,14 +318,24 @@ class GeneratorObject(_handlers.DataHandlers): 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 break else: self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(self.lineindex+1))) - if is_substrules: - for x in range(len(substrules_entries)): - entry=substrules_entries[x] + # For silence_warning in subst_variable_content + encountered_ids=set() + for x in range(len(entries)): + entry=entries[x] + match_pattern=entry[0] + if entry_name_substesc: match_pattern=self.handle_substesc(match_pattern) + 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], silence_warnings=entry[3] in encountered_ids) + encountered_ids.add(entry[3]) + if is_substrules: try: self.db_interface.add_subst_entry( - match_pattern=entry[0], \ + match_pattern=match_pattern, \ substitute_pattern=entry[1], \ effective_commands=substrules_options['effective_commands'], \ effective_locale=entry[2], \ @@ -330,4 +345,6 @@ class GeneratorObject(_handlers.DataHandlers): stdout_stderr_matchoption=substrules_stdout_stderr_option, \ 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])) \ No newline at end of file + 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 diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 8f01bc4..cb351fd 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -76,7 +76,6 @@ 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} match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) - match_pattern=obj.handle_singleline_content(match_pattern) # handle substesc and substvar 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) -- Gitee From 226de49f683819fdb1975efeab6be43a82ec5108 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 00:43:46 +0800 Subject: [PATCH 292/354] Add vim swap file to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 87faa43..e9df3af 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ venv/* src/*.egg-info .vscode .vscode/* +# Vim swap file +**/*.sw? # excluded files (ex) debian/*.ex debian/clitheme -- Gitee From 8dc473fb3db6c8a44960bc76bc597e5d97ea481c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 00:43:32 +0800 Subject: [PATCH 293/354] Update version (v2.0-dev20240618) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 67606e3..db5ffc7 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240617 +pkgver=2.0_dev20240618 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index f53881b..a6e7aee 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240617-1) unstable; urgency=low +clitheme (2.0-dev20240618-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Mon, 17 Jun 2024 22:44:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 19 Jun 2024 00:41:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index f3c3612..1996d44 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-dev20240617" +__version__="2.0-dev20240618" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240617" +version_main="2.0_dev20240618" version_buildnumber=1 \ No newline at end of file -- Gitee From 43f8e0e52f4c6daed78b4405ca88b6deac248452 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 10:15:09 +0800 Subject: [PATCH 294/354] Fix default argument value for unique_id in add_subst_entry --- src/clitheme/_generator/db_interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index fb8af34..e742259 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -71,7 +71,8 @@ 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[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, unique_id: uuid.UUID=uuid.uuid4(), line_number_debug: str="-1"): +def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, 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[str]=[] try: re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid except: raise bad_pattern(str(sys.exc_info()[1])) -- Gitee From 079058b9c75b8c72de159f1181c91a5fe8978a33 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 10:58:15 +0800 Subject: [PATCH 295/354] Multiple fixes to multiple entry names feature in header_section --- src/clitheme/_generator/_dataclass.py | 36 +++++++++++++++++----- src/clitheme/_generator/_entries_parser.py | 27 ++++++---------- src/clitheme/_generator/_handlers.py | 3 +- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 1429072..f5fcdd6 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -48,6 +48,9 @@ class GeneratorObject(_handlers.DataHandlers): self.really_really_global_options={} # options defined outside any sections self.global_variables={} self.really_really_global_variables={} # variables defined outside any sections + # For in_domainapp and in_subsection in {entries_section} + self.in_domainapp="" + self.in_subsection="" self.custom_infofile_name=custom_infofile_name self.filename=filename @@ -241,19 +244,23 @@ class GeneratorObject(_handlers.DataHandlers): 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[str], is_regex: bool, strictness: int - entryNames: list[tuple]=[(entry_name, uuid.uuid4(), self.lineindex+1)] # For supporting specifying multiple entries at once (name, uuid, debug_linenumber) + 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[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[tuple]=[] + substrules_endmatchhere=False substrules_stdout_stderr_option=0 - def check_valid_pattern(pattern: str): + + 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(self.lineindex+1), error_msg=sys.exc_info()[1])) - if is_substrules: check_valid_pattern(entry_name) + 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.lineindex Date: Wed, 19 Jun 2024 11:39:48 +0800 Subject: [PATCH 296/354] Change bold highlighting between Chinese and English README links --- README.en.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index 7bd86ef..44c3b08 100644 --- a/README.en.md +++ b/README.en.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 c56b217..dd6ddda 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # clitheme - 命令行自定义工具 -**中文** | [**English**](./README.en.md) +**中文** | [English](./README.en.md) --- -- Gitee From 9b1b851cd5c925e7ed6717a7b2a8df59e70f7bae Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 12:25:53 +0800 Subject: [PATCH 297/354] Fix substesc and substvar in handle_entry --- src/clitheme/_generator/_dataclass.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index f5fcdd6..d030a65 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -347,12 +347,13 @@ class GeneratorObject(_handlers.DataHandlers): for x in range(len(entries)): entry=entries[x] match_pattern=entry[0] - if entry_name_substesc: match_pattern=self.handle_substesc(match_pattern) + # 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) encountered_ids.add(entry[3]) if is_substrules: try: -- Gitee From f650cc12afe97c0a9ee1a9e2a0b387ce19575102 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 12:45:00 +0800 Subject: [PATCH 298/354] Use `except re.error` for pattern error handling Makes the error message less confusing and easier to debug --- src/clitheme/_generator/_dataclass.py | 2 +- src/clitheme/_generator/db_interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index d030a65..e9e15b3 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -260,7 +260,7 @@ class GeneratorObject(_handlers.DataHandlers): 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.lineindex Date: Wed, 19 Jun 2024 13:16:24 +0800 Subject: [PATCH 299/354] Add `CLITHEME_REGENERATE_DB` env var support in clitheme-exec to force db regeneration --- src/clitheme/exec/__init__.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index 0310e09..a81e8e3 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -28,10 +28,20 @@ frontend.global_domain="swiftycode" frontend.global_appname="clitheme" fd=frontend.FetchDescriptor(subsections="exec") +# Prevent recursion dead loops and accurately simulate that regeneration is only triggered once +db_already_regenerated=False + def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) -> bool: - try: db_interface.connect_db() + global db_already_regenerated + try: + # Support environment variable flag to force db regeneration (debug purposes) + if os.environ.get("CLITHEME_REGENERATE_DB")=="1" and not db_already_regenerated: + db_already_regenerated=True + 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...")) + orig_stdout=sys.stdout try: # gather files search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname @@ -54,13 +64,13 @@ def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) 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") - sys.stdout=sys.__stdout__ + 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")) except: - sys.stdout=sys.__stdout__ + 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]))) _globalvar.handle_exception() return False @@ -129,7 +139,8 @@ def main(arguments: list[str]): if subst: if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) - if not _check_regenerate_db(): return 1 + else: + if not _check_regenerate_db(): return 1 # determine platform if os.name=="posix": from . import output_handler_posix -- Gitee From 035dd05f71a789de5377ff2b2ad9a063f8b21ab5 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 13:55:33 +0800 Subject: [PATCH 300/354] Improve error handling in `cli.apply_theme` --- src/clitheme/cli.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 7c78a72..2057647 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -88,24 +88,30 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p print(line_prefix+f.feof("processing-file", "> Processing file {filename}...", filename=f"({i+1}/{len(file_contents)})"), end='') file_content=file_contents[i] # Generate data hierarchy, erase current data, copy it to data path + generator_msgs=io.StringIO() try: _generator.silence_warn=False # Output the warning messages correctly (make sure that they start on new line if any exists) - generator_msgs=io.StringIO() sys.stdout=generator_msgs final_path=_generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index), filename=filenames[i] if len(filenames)>0 else "") generate_path=False # Don't generate another temp folder after first one index+=1 - sys.stdout=orig_stdout # restore standard output - if generator_msgs.getvalue()!='': - print(("\n" if print_progress else "")+generator_msgs.getvalue(), end='') - except SyntaxError: + except: sys.stdout=orig_stdout - print(("\n" if print_progress else "")+f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ + print(("\n" if print_progress else ""), 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 + print(generator_msgs.getvalue(), end='') + print(f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ index=str(i+1), message=str(sys.exc_info()[1]))) _globalvar.handle_exception() return 1 - finally: sys.stdout=orig_stdout + else: + sys.stdout=orig_stdout # restore standard output + if generator_msgs.getvalue()!='': + print(("\n" if print_progress else "")+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("generate-data-success", "Successfully generated data")) -- Gitee From 2e2f4758e4b58e266266fcd7fc511b42674b8c2d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 13:56:09 +0800 Subject: [PATCH 301/354] Raise exception when `_check_regenerate_db` fails in _generator --- src/clitheme/_generator/_substrules_parser.py | 2 +- src/clitheme/strings/generator-strings.clithemedef.txt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index cb351fd..bacc4b9 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -25,7 +25,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): exit(1) + 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\"")) 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 diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 7acf269..fdf2b93 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -49,6 +49,10 @@ in_domainapp swiftycode clitheme # locale:default Missing or incomplete header or content sections locale:zh_CN 文件缺少或包含不完整的header或内容段落 [/entry] + [entry] db-regenerate-fail-err + # locale:default Failed to migrate existing substrules database; try performing the operation without using "--overlay" + locale:zh_CN 无法升级当前的substrules的数据库;请尝试不使用"--overlay"再次执行此操作 + [/entry] # 选项错误提示信息 # Options error messages [entry] option-not-allowed-err -- Gitee From 74c466421fa89f165b9764dbc4fceddfd96a0359 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 10:50:08 +0800 Subject: [PATCH 302/354] `_generator`: Show non-printable characters as plain text in message name content --- src/clitheme/_generator/__init__.py | 2 +- src/clitheme/_generator/_dataclass.py | 24 +++++++++---------- src/clitheme/_generator/_entries_parser.py | 2 +- src/clitheme/_generator/_handlers.py | 18 ++++++++------ src/clitheme/_generator/_header_parser.py | 2 +- src/clitheme/_generator/_manpage_parser.py | 2 +- src/clitheme/_generator/_substrules_parser.py | 2 +- src/clitheme/_globalvar.py | 11 +++++++++ 8 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 66155a5..66bfdc9 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -55,7 +55,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) - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=first_phrase, num=str(obj.lineindex+1))) + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(first_phrase), num=str(obj.lineindex+1))) def is_content_parsed() -> bool: content_sections=["entries", "substrules", "manpage"] diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index e9e15b3..bd7a009 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -62,13 +62,13 @@ class GeneratorObject(_handlers.DataHandlers): return self.lines_data[self.lineindex].strip()=="" or self.lines_data[self.lineindex].strip().startswith('#') def check_enough_args(self, phrases: list[str], count: int): if 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=phrases[0])) + 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 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={} @@ -82,10 +82,10 @@ class GeneratorObject(_handlers.DataHandlers): results=re.search(r"^(?P.+?):(?P.+)$", each_option) value: int if results==None: # no value specified - self.handle_error(self.fd.feof("option-without-value-err", "No value specified for option \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=option_name)) + self.handle_error(self.fd.feof("option-without-value-err", "No value specified for option \"{phrase}\" on line {num}", num=str(self.lineindex+1), phrase=self.fmt(option_name))) else: try: value=int(results.groupdict()['value']) - except ValueError: self.handle_error(self.fd.feof("option-value-not-int-err", "The value specified for option \"{phrase}\" is not an integer on line {num}", num=str(self.lineindex+1), phrase=option_name)) + except ValueError: self.handle_error(self.fd.feof("option-value-not-int-err", "The value specified for option \"{phrase}\" is not an integer on line {num}", num=str(self.lineindex+1), phrase=self.fmt(option_name))) # set option final_options[option_name]=value elif option_name in self.bool_options: @@ -96,16 +96,16 @@ class GeneratorObject(_handlers.DataHandlers): if option_name_preserve_no in option_group: for opt in options_data: if opt!=option_name_preserve_no and opt in option_group: - self.handle_error(self.fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(self.lineindex+1), option1=option_name_preserve_no, option2=opt)) + self.handle_error(self.fd.feof("option-conflict-err", "The option \"{option1}\" can't be set at the same time with \"{option2}\" on line {num}", num=str(self.lineindex+1), option1=self.fmt(option_name_preserve_no), option2=self.fmt(opt))) # set all other options to false for opt in option_group: final_options[opt]=False # set the option final_options[option_name_preserve_no]=True 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=option_name_preserve_no)) + 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: - self.handle_error(self.fd.feof("option-not-allowed-err", "Option \"{phrase}\" not allowed here at line {num}", num=str(self.lineindex+1), phrase=option_name)) + 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): # set options globally @@ -130,7 +130,7 @@ class GeneratorObject(_handlers.DataHandlers): var_content=self.global_variables[var_name] except KeyError: if not silence_warnings: self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", \ - num=line_number_debug if line_number_debug!=None else str(self.lineindex+1), name=var_name)) + num=line_number_debug if line_number_debug!=None else str(self.lineindex+1), name=self.fmt(var_name))) continue new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) return new_content @@ -144,7 +144,7 @@ class GeneratorObject(_handlers.DataHandlers): self.handle_error(self.fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase="setvar:", num=str(self.lineindex+1))) else: var_name=results.groupdict()['name'] # 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=var_name, num=str(self.lineindex+1))) + 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: @@ -224,7 +224,7 @@ class GeneratorObject(_handlers.DataHandlers): 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=option)) + 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 @@ -240,7 +240,7 @@ class GeneratorObject(_handlers.DataHandlers): 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)) elif disallow_cmdmatch_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=option)) + 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[str], is_regex: bool, strictness: int @@ -341,7 +341,7 @@ class GeneratorObject(_handlers.DataHandlers): elif option=="substvar" and got_options['substvar']==True: entry_name_substvar=True break - else: self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(self.lineindex+1))) + else: self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=self.fmt(phrases[0]), num=str(self.lineindex+1))) # For silence_warning in subst_variable_content encountered_ids=set() for x in range(len(entries)): diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index a68ff16..a9378ef 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -61,4 +61,4 @@ def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): if phrases[0]=="end_main": obj.handle_warning(obj.fd.feof("syntax-phrase-deprecation-warn", "Line {num}: phrase \"{old_phrase}\" is deprecated in this version; please use \"{new_phrase}\" instead", num=str(obj.lineindex+1), old_phrase="end_main", new_phrase=r"{/entries_section}")) break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(obj.lineindex+1))) + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index fae63ea..272cc98 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -9,6 +9,7 @@ Functions for data processing and others (internal module) """ import os import gzip +import re from typing import Optional from .. import _globalvar, frontend @@ -24,10 +25,13 @@ class DataHandlers: self.datapath=self.path+"/"+_globalvar.generator_data_pathname 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): - raise SyntaxError(self.fd.feof("error-str", "Syntax error: {msg}", msg=message)) + output=self.fd.feof("error-str", "Syntax error: {msg}", msg=message) + raise SyntaxError(output) def handle_warning(self, message: str): - if not self.silence_warn: print(self.fd.feof("warning-str", "Warning: {msg}", msg=message)) + output=self.fd.feof("warning-str", "Warning: {msg}", msg=message) + if not self.silence_warn: print(output) def recursive_mkdir(self, path: str, entry_name: str, line_number_debug: int): # recursively generate directories (excluding file itself) current_path=path current_entry="" # for error output @@ -36,7 +40,7 @@ class DataHandlers: current_path+="/"+x if os.path.isfile(current_path): # conflict with entry file self.handle_error(self.fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ - num=str(line_number_debug), name=current_entry)) + num=str(line_number_debug), name=self.fmt(current_entry))) elif os.path.isdir(str(current_path))==False: # directory does not exist os.mkdir(current_path) def add_entry(self, path: str, entry_name: str, entry_content: str, line_number_debug: int): # add entry to where it belongs @@ -46,10 +50,10 @@ class DataHandlers: target_path+="/"+x if os.path.isdir(target_path): self.handle_error(self.fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ - num=str(line_number_debug), name=entry_name)) + num=str(line_number_debug), name=self.fmt(entry_name))) elif os.path.isfile(target_path): self.handle_warning(self.fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ - num=str(line_number_debug), name=entry_name)) + num=str(line_number_debug), name=self.fmt(entry_name))) f=open(target_path,'w', encoding="utf-8") f.write(entry_content+"\n") def write_infofile(self, path: str, filename: str, content: str, line_number_debug: int, header_name_debug: str): @@ -58,7 +62,7 @@ class DataHandlers: target_path=path+"/"+filename if os.path.isfile(target_path): self.handle_warning(self.fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) + 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[str], line_number_debug: int, header_name_debug: str): @@ -67,7 +71,7 @@ class DataHandlers: target_path=path+"/"+filename if os.path.isfile(target_path): self.handle_warning(self.fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ - num=str(line_number_debug), name=header_name_debug)) + num=str(line_number_debug), name=self.fmt(header_name_debug))) f=open(target_path,'w', encoding="utf-8") for line in content_phrases: f.write(line+"\n") diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index e67b656..f1e266a 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -62,4 +62,4 @@ def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("header") break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(obj.lineindex+1))) \ No newline at end of file + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) \ No newline at end of file diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 49ae537..703de1a 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -43,7 +43,7 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): file_dir=parent_dir+("/" if parent_dir!="" else "")+_globalvar.splitarray_to_string(filepath).replace(" ","/") 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=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])) # write manpage files in theme-info for db migration feature to work successfully obj.write_manpage_file(filepath, filecontent, obj.lineindex+1, custom_parent_path=obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name+"/manpage_data") # expect "as" clause right on next line diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index bacc4b9..8f07d8b 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -87,4 +87,4 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("substrules") break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(obj.lineindex+1))) \ No newline at end of file + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) \ No newline at end of file diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 41845a7..f981618 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -12,6 +12,7 @@ import io import os import sys import re +import string from copy import copy from . import _version @@ -131,6 +132,16 @@ 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()) if results==None: raise ValueError("Match content failed (no matches)") else: return results.groupdict()['content'] +def make_printable(content: str) -> str: + final_str="" + for character in content: + if character.isprintable() or character in string.whitespace: final_str+=character + else: + exp=repr(character) + # Remove quotes in repr(character) + 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[str]: lang=[] def add_language(target_lang: str): -- Gitee From 906e39dc9cdd33de1a6970f257b03dabd5f5348c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 16:36:13 +0800 Subject: [PATCH 303/354] Add missing substvar and substesc handle in `locale` content line --- 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 bd7a009..9dbb20e 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -301,6 +301,7 @@ class GeneratorObject(_handlers.DataHandlers): 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])) -- Gitee From b34349d1e7ba581cb8329f4dd45a3092ae942449 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 22:47:10 +0800 Subject: [PATCH 304/354] Fix small grammar mistake at generator-strings --- src/clitheme/strings/generator-strings.clithemedef.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index fdf2b93..9e58914 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -39,7 +39,7 @@ in_domainapp swiftycode clitheme [/entry] [entry] invalid-phrase-err # locale:default Unexpected "{phrase}" on line {num} - locale:zh_CN 第{num}行:无效的"{phrase}" + locale:zh_CN 第{num}行:无效的"{phrase}"语句 [/entry] [entry] not-enough-args-err # locale:default Not enough arguments for "{phrase}" at line {num} -- Gitee From ebe827ad5228f2063c5e065d1737a9b9bf55c890 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 22:49:52 +0800 Subject: [PATCH 305/354] Add helpful notice about error-str and warning-str definitions --- src/clitheme/strings/generator-strings.clithemedef.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/clitheme/strings/generator-strings.clithemedef.txt b/src/clitheme/strings/generator-strings.clithemedef.txt index 9e58914..bea4f0a 100644 --- a/src/clitheme/strings/generator-strings.clithemedef.txt +++ b/src/clitheme/strings/generator-strings.clithemedef.txt @@ -21,6 +21,9 @@ in_domainapp swiftycode clitheme # locale:default Syntax error: {msg} locale:zh_CN 语法错误:{msg} [/entry] + + # 以下定义会是上方定义"{msg}"中的内容 + # The following definitions are the contents of "{msg}" in the above definition [entry] subsection-conflict-err # locale:default Line {num}: cannot create subsection "{name}" because an entry with the same name already exists locale:zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 @@ -111,6 +114,9 @@ in_domainapp swiftycode clitheme # locale:default Warning: {msg} locale:zh_CN 警告:{msg} [/entry] + + # 以下定义会是上方定义"{msg}"中的内容 + # The following definitions are the contents of "{msg}" in the above definition [entry] repeated-entry-warn # locale:default Line {num}: repeated entry "{name}", overwriting locale:zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 -- Gitee From 4bd1057a4cd1df000c7c4382e1509f10882baa71 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 23:37:06 +0800 Subject: [PATCH 306/354] Format non-printable characters in cli and clitheme-exec messages --- src/clitheme/cli.py | 29 ++++++++++++----------- src/clitheme/exec/output_handler_posix.py | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 2057647..b744692 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -17,6 +17,7 @@ import shutil import re import io from . import _globalvar, _generator, frontend +from ._globalvar import make_printable as fmt # A shorter alias of the function # spell-checker:ignore pathnames lsdir inpstr @@ -118,9 +119,9 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p global last_data_path; last_data_path=final_path if preserve_temp or generate_only: if os.name=="nt": - print(f.feof("view-temp-dir", "View at {path}", path=re.sub(r"/", r"\\", final_path))) # make the output look pretty + print(f.feof("view-temp-dir", "View at {path}", path=fmt(re.sub(r"/", r"\\", final_path)))) # make the output look pretty else: - print(f.feof("view-temp-dir", "View at {path}", path=final_path)) + print(f.feof("view-temp-dir", "View at {path}", path=fmt(final_path))) if generate_only: return 0 # ---Stop here if generate_only is set--- @@ -134,14 +135,14 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p else: raise except FileNotFoundError: pass except Exception: - print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) + print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=fmt(str(sys.exc_info()[1])))) _globalvar.handle_exception() return 1 try: shutil.copytree(final_path, _globalvar.clitheme_root_data_path, dirs_exist_ok=True) except Exception: - print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) + print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=fmt(str(sys.exc_info()[1])))) _globalvar.handle_exception() return 1 print(f.reof("apply-theme-success", "Theme applied successfully")) @@ -162,7 +163,7 @@ def unset_current_theme(): print(f.reof("no-data-found", "Error: No theme data present (no theme was set)")) return 1 except Exception: - print(f.feof("remove-data-error", "An error occurred while removing the data:\n{message}", message=str(sys.exc_info()[1]))) + print(f.feof("remove-data-error", "An error occurred while removing the data:\n{message}", message=fmt(str(sys.exc_info()[1])))) _globalvar.handle_exception() return 1 print(f.reof("remove-data-success", "Successfully removed the current theme data")) @@ -201,7 +202,7 @@ def get_current_theme_info(): version="(Unknown)" if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="version")): version=open(target_path+"/"+_globalvar.generator_info_filename.format(info="version"), 'r', encoding="utf-8").read().strip() - print(f.feof("version-str", "Version: {ver}", ver=version)) + print(f.feof("version-str", "Version: {ver}", ver=fmt(version))) # description description="(Unknown)" if os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="description")): @@ -216,12 +217,12 @@ def get_current_theme_info(): print(f.reof("locales-str", "Supported locales:")) for locale in locales.splitlines(): if locale.strip()!="": - print(f.feof("list-item", "• {content}", content=locale.strip())) + print(f.feof("list-item", "• {content}", content=fmt(locale.strip()))) elif os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="locales")): locales=open(target_path+"/"+_globalvar.generator_info_filename.format(info="locales"), 'r', encoding="utf-8").read().strip() print(f.reof("locales-str", "Supported locales:")) for locale in locales.split(): - print(f.feof("list-item", "• {content}", content=locale.strip())) + print(f.feof("list-item", "• {content}", content=fmt(locale.strip()))) # supported_apps supported_apps="(Unknown)" if os.path.isfile(target_path+"/"+_globalvar.generator_info_v2filename.format(info="supported_apps")): @@ -229,12 +230,12 @@ def get_current_theme_info(): print(f.reof("supported-apps-str", "Supported apps:")) for app in supported_apps.splitlines(): if app.strip()!="": - print(f.feof("list-item", "• {content}", content=app.strip())) + print(f.feof("list-item", "• {content}", content=fmt(app.strip()))) elif os.path.isfile(target_path+"/"+_globalvar.generator_info_filename.format(info="supported_apps")): supported_apps=open(target_path+"/"+_globalvar.generator_info_filename.format(info="supported_apps"), 'r', encoding="utf-8").read().strip() print(f.reof("supported-apps-str", "Supported apps:")) for app in supported_apps.split(): - print(f.feof("list-item", "• {content}", content=app.strip())) + print(f.feof("list-item", "• {content}", content=fmt(app.strip()))) return 0 def update_theme(): @@ -279,7 +280,7 @@ def update_theme(): _globalvar.handle_exception() return 1 except: - print(fi.feof("other-err", "An error occurred while processing file path information: {msg}\nPlease re-apply the current theme and try again")) + 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) @@ -324,7 +325,7 @@ def _get_file_contents(file_paths: list[str]) -> list[str]: content_list.append(open(path, 'r', encoding="utf-8").read()) except: print(fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ - index=str(i+1), message=path+": "+str(sys.exc_info()[1]))) + index=str(i+1), message=path+": "+fmt(str(sys.exc_info()[1])))) raise return content_list @@ -360,7 +361,7 @@ def main(cli_args: list[str]): 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=arg), arg_first) + 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") @@ -387,7 +388,7 @@ def main(cli_args: list[str]): check_extra_args(2) _handle_help_message(full_help=True) else: - return _handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=cli_args[1]), arg_first) + return _handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=fmt(cli_args[1])), arg_first) return 0 def _script_main(): # for script return main(sys.argv) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 0a33538..471eaf7 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -77,7 +77,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) # need to find a method to preserve exact order when using separated stdout and stderr pipes try: process=subprocess.Popen(command, stdin=stdout_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) except: - _labeled_print(fd.feof("command-fail-err", "Error: failed to run command: {msg}", msg=str(sys.exc_info()[1]))) + _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() return 1 output_lines=[] # (line_content, is_stderr, do_subst_operation) -- Gitee From 7028d2282d46d90e27b95a31aed300fe3bbdf066 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 23:39:28 +0800 Subject: [PATCH 307/354] Support substvar in `description` single-line definition of header_section --- src/clitheme/_generator/_header_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index f1e266a..393d200 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -24,7 +24,8 @@ def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): if phrases[0]=="name" or phrases[0]=="version" or phrases[0]=="description": obj.check_enough_args(phrases, 2) content=_globalvar.extract_content(obj.lines_data[obj.lineindex]) - content=obj.subst_variable_content(content) + if phrases[0]=="description": content=obj.handle_singleline_content(content) + else: content=obj.subst_variable_content(content) obj.write_infofile( \ obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ _globalvar.generator_info_filename.format(info=phrases[0]),\ -- Gitee From 11b07d3ec9c46935edfc3481e00f61042f6b0b26 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 19 Jun 2024 23:52:57 +0800 Subject: [PATCH 308/354] Update version (v2.0-dev20240619) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index db5ffc7..7e2924b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240618 +pkgver=2.0_dev20240619 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index a6e7aee..647960a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240618-1) unstable; urgency=low +clitheme (2.0-dev20240619-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 19 Jun 2024 00:41:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 19 Jun 2024 23:51:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 1996d44..f2d3884 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-dev20240618" +__version__="2.0-dev20240619" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240618" +version_main="2.0_dev20240619" version_buildnumber=1 \ No newline at end of file -- Gitee From 3508e6fc31ce792c7f56b508b20aa015d87af0f1 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 20 Jun 2024 15:26:23 +0800 Subject: [PATCH 309/354] db_interface: Raise error when database does not exist Prevents less errors and is a better error handling mechanism --- src/clitheme/_generator/db_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index e07fe96..1992834 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -113,6 +113,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F # 3. Match rules without command filter # 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") _connection=sqlite3.connect(db_path) final_cmdlist=[] final_cmdlist_exactmatch=[] -- Gitee From 95bc2ae65d4bb5f1e56f7e40291577aca2622577 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 20 Jun 2024 15:37:48 +0800 Subject: [PATCH 310/354] Fix TTY access issue in child process This change fixes error messages such as "No job control" and "Can't access TTY" in some programs. It is achieved with the following changes: - os.setsid is called to start new session - Temporarily open the pty in child fork before execution to make controlling terminal - Manually relay suspend and continue signals to child process --- src/clitheme/exec/output_handler_posix.py | 31 +++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 471eaf7..2e1572a 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -26,7 +26,7 @@ from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print -# spell-checker:ignore cbreak ICANON readsize splitarray +# spell-checker:ignore cbreak ICANON readsize splitarray ttyname RDWR preexec _globalvar.handle_set_themedef(frontend, "output_handler_posix") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="exec") @@ -72,14 +72,41 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) # Prevent apps from using "less" or "more" as pager, as it won't work here env['PAGER']="cat" prev_attrs=termios.tcgetattr(sys.stdin) + main_pid=os.getpid() process: subprocess.Popen # Redirect stderr to stdout for now (BETA) # need to find a method to preserve exact order when using separated stdout and stderr pipes - try: process=subprocess.Popen(command, stdin=stdout_slave, stdout=stdout_slave, stderr=stdout_slave, bufsize=0, close_fds=True, env=env) + + # Since a new session is started with os.setsid() in child process: + # - Suspend and continue signals must be manually relayed to the child process + # [Not implemented yet] - Suspend signal from child process must be manually relayed to the parent process + def signal_handler(sig, frame): + if sig==signal.SIGCONT: # continue signal + process.send_signal(sig) + signal.signal(signal.SIGTSTP, signal_handler) # Reset signal handler + if sig==signal.SIGTSTP: # suspend signal + 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 + try: + def child_init(): + # Must start new session or some programs might not work properly + os.setsid() + + # Make controlling terminal so programs can access TTY properly + # [Explicitly open the tty to make it become a controlling tty.] + # --This code and above description are from the source code of pty.fork()-- + 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) 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() return 1 + else: + signal.signal(signal.SIGTSTP, signal_handler) + signal.signal(signal.SIGCONT, 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)) last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder -- Gitee From 2d5dd5807db46e27d1f853984093c5dbcc9360e4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 20 Jun 2024 15:39:23 +0800 Subject: [PATCH 311/354] Add more words to cspell words list --- cspell.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index abe81f3..9193df0 100644 --- a/cspell.json +++ b/cspell.json @@ -18,8 +18,8 @@ "subststdoutonly", "subststderronly", "substall", "substvar", "substesc", "PKGBUILD", "MANPATH", - "tcgetattr", "tcsetattr", "getpid", "setitimer", - "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", "SIGALRM", "ITIMER", + "tcgetattr", "tcsetattr", "getpid", "setsid", "setitimer", + "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", "SIGALRM", "ITIMER", "SIGTSTP", "SIGSTOP", "SIGCONT", "showchars", "keepends", "appname", "domainapp", "sanitycheck", "debugmode", "splitarray", "disablelang" -- Gitee From dcc65a7fe9d8f996eaaff8aed71c05ce79dacdda Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 20 Jun 2024 17:40:00 +0800 Subject: [PATCH 312/354] Fix interrupt handle in output_handler_posix Send `^C` character to process pty instead of sending SIGINT signal, making it work when a shell is executing other commands --- src/clitheme/exec/output_handler_posix.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 2e1572a..3c3bcfe 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -208,8 +208,11 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) for thread in futures: os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), thread.result()) except KeyboardInterrupt: - try: process.send_signal(signal.SIGINT) - except KeyboardInterrupt: pass + os.write(stdout_fd, b'\x03') # '^C' character + # try: + # try: os.kill(os.tcgetpgrp(stdout_fd), signal.SIGINT) # Send signal to foreground process + # except OSError: process.send_signal(signal.SIGINT) + # except KeyboardInterrupt: pass except: 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 -- Gitee From 4555e015f88701e23e90217be607181bb5bb65de Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 00:16:36 +0800 Subject: [PATCH 313/354] Implement 'foregroundonly' option and process foreground detection - Use 'foregroundonly' option on command filter to only apply relevant substitution rules if the command is the current foreground process - Compare the value of os.tcgetpgrp with process PID to determine foreground state --- src/clitheme/_generator/_dataclass.py | 15 ++++---- src/clitheme/_generator/_substrules_parser.py | 13 +++++-- src/clitheme/_generator/db_interface.py | 36 +++++++++++-------- src/clitheme/_globalvar.py | 2 +- src/clitheme/exec/output_handler_posix.py | 20 +++++++++-- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 9dbb20e..ff78b44 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -23,8 +23,8 @@ class GeneratorObject(_handlers.DataHandlers): ## Defined option groups lead_indent_options=["leadtabindents", "leadspaces"] content_subst_options=["substesc","substvar"] - command_filter_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"] - subst_limiting_options=["subststdoutonly", "subststderronly", "substall"] + command_filter_options=["strictcmdmatch", "exactcmdmatch", "smartcmdmatch", "normalcmdmatch"]+["foregroundonly"] + subst_limiting_options=["subststdoutonly", "subststderronly", "substall"]+["endmatchhere"] # options used in handle_block_input block_input_options=lead_indent_options+content_subst_options @@ -32,11 +32,11 @@ class GeneratorObject(_handlers.DataHandlers): # value options: options requiring an integer value value_options=lead_indent_options # on/off options (use no<...> to disable) - bool_options=content_subst_options+["endmatchhere"] + bool_options=content_subst_options+["endmatchhere", "foregroundonly"] # only one of these options can be set to true at the same time (specific to groups) - switch_options=[command_filter_options] + switch_options=[command_filter_options[:4]] # Disable these options for now (BETA) - # switch_options+=[subst_limiting_options] + # switch_options+=[subst_limiting_options[:3]] def __init__(self, file_content: str, custom_infofile_name: str, filename: str, path: str, silence_warn: bool): # data to keep track of @@ -243,7 +243,7 @@ class GeneratorObject(_handlers.DataHandlers): 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[str], is_regex: bool, strictness: int + # substrules_options: {effective_commands: list[str], is_regex: bool, strictness: int, foreground_only: bool} entry_name_substesc=False; entry_name_substvar=False names_processed=False # Set to True when no more entry names are being specified @@ -327,7 +327,7 @@ 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+["endmatchhere"] 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]` ) for option in got_options: @@ -367,6 +367,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'], \ 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 8f07d8b..cbd446d 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -20,6 +20,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str end_phrase=r"{/substrules_section}" command_filters: Optional[list[str]]=None command_filter_strictness=0 + command_filter_foreground_only=False # initialize the database if os.path.exists(obj.path+"/"+_globalvar.db_filename): try: obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) @@ -39,7 +40,8 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str # read commands command_strings=content.splitlines() - strictness=0 #1: strictcmdmatch, 2: exactcmdmatch + 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,15 +53,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 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) 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 @@ -67,14 +73,17 @@ 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) command_filters=None elif phrases[0]=="[substitute_string]" or phrases[0]=="[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} + 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": diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 1992834..8e7482a 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -21,7 +21,7 @@ import queue from typing import Optional from .. import _globalvar, frontend -# spell-checker:ignore matchoption cmdlist exactmatch rowid +# spell-checker:ignore matchoption cmdlist exactmatch rowid pids tcpgrp connection=sqlite3.connect(":memory:") # placeholder db_path="" @@ -53,6 +53,7 @@ def init_db(file_path: str): effective_command TEXT, \ effective_locale TEXT, \ command_match_strictness INTEGER NOT NULL, \ + foreground_only INTEGER NOT NULL, \ end_match_here INTEGER NOT NULL, \ stdout_stderr_only INTEGER NOT NULL \ );") @@ -71,14 +72,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[str]], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, 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[str]], 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[str]=[] try: re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid 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"] + 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"] if effective_commands!=None and len(effective_commands)>0: for cmd in effective_commands: # remove extra spaces in the command @@ -91,7 +92,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))) + 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)) for cmd in cmdlist: # remove any existing values with the same match_pattern and effective_command strictness_condition="" @@ -102,10 +103,12 @@ 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))) + 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 match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False) -> 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: # 1. Match rules with exactcmdmatch option set # 2. Match rules with command filter having the same first phrase @@ -120,15 +123,16 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F if command!=None and len(command.split())>0: # command without paths (e.g. /usr/bin/bash -> bash) stripped_command=os.path.basename(command.split()[0])+" "+(_globalvar.splitarray_to_string(command.split()[1:]) if len(command.split())>1 else '') + cmdlist_items=["effective_command", "command_match_strictness"] # obtain a list of effective_command with the same first term - cmdlist=_connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ? or effective_command LIKE ?;", (command.split()[0].strip()+" %", stripped_command.split()[0].strip()+" %")).fetchall() + cmdlist=_connection.execute(f"SELECT DISTINCT {','.join(cmdlist_items)} FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ? or effective_command LIKE ?;", (command.split()[0].strip()+" %", stripped_command.split()[0].strip()+" %")).fetchall() # also include one-phrase commands - cmdlist+=_connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE effective_command=? or effective_command=?;", (command.split()[0].strip(),stripped_command.split()[0].strip())).fetchall() + cmdlist+=_connection.execute(f"SELECT DISTINCT {','.join(cmdlist_items)} FROM {_globalvar.db_data_tablename} WHERE effective_command=? or effective_command=?;", (command.split()[0].strip(),stripped_command.split()[0].strip())).fetchall() # sort by number of phrases (greatest to least) def split_len(obj: tuple) -> int: return len(obj[0].split()) cmdlist.sort(key=split_len, reverse=True) # prioritize effective_command with exact match requirement - cmdlist=_connection.execute(f"SELECT DISTINCT effective_command, command_match_strictness FROM {_globalvar.db_data_tablename} WHERE (effective_command=? OR effective_command=?) AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),re.sub(r" {2,}", " ", stripped_command).strip())).fetchall()+cmdlist + cmdlist=_connection.execute(f"SELECT DISTINCT {','.join(cmdlist_items)} FROM {_globalvar.db_data_tablename} WHERE (effective_command=? OR effective_command=?) AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),re.sub(r" {2,}", " ", stripped_command).strip())).fetchall()+cmdlist def process_smartcmdmatch_phrases(match_cmd: str) -> list[str]: match_cmd_phrases=[] for p in range(len(match_cmd.split())): @@ -167,7 +171,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F 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"] + fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only", "unique_id", "foreground_only"] # get locales locales=_globalvar.get_locale() nonlocal matches @@ -192,7 +196,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F if len(_running_processes_ids)==0: _init_process() result_id=uuid.uuid4() - global _input_values; _input_values.put((matches, content_str, is_stderr, result_id)) + global _input_values; _input_values.put((matches, content_str, is_stderr, result_id, pids)) counter=0 watchdog_timer=0 while counter=1.500: _init_process() - _input_values.put((matches, content_str, is_stderr, result_id)) + _input_values.put((matches, content_str, is_stderr, result_id, pids)) watchdog_timer=0 else: # executed when no "break" happens try: del _return_values[result_id] @@ -217,7 +221,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F _init_process() # restart the process raise TimeoutError("match operation timeout") else: - content_str=_handle_subst(matches, content_str, is_stderr) + content_str=_handle_subst(matches, content_str, is_stderr, pids) return content_str # --The following implementation (A) is for setting a timeout capacity on content match functions-- @@ -274,7 +278,7 @@ def __process_main_loop(input_vals: multiprocessing.Queue, return_vals: dict, pr try: content=input_vals.get_nowait() except queue.Empty: return return_vals[content[3]]=None # Processing - return_str=_handle_subst(content[0], content[1], content[2]) + return_str=_handle_subst(content[0], content[1], content[2], content[4]) return_vals[content[3]]=return_str while True: try: time.sleep(0.001) @@ -285,13 +289,15 @@ def __process_main_loop(input_vals: multiprocessing.Queue, return_vals: dict, pr except KeyboardInterrupt: pass # except: break -def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, ret: Optional[list[bytes]]=None): +def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, pids: tuple[int, int], ret: Optional[list[bytes]]=None): content_str=copy.copy(content) encountered_ids=set() 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]) + if match_data[6]==True: # Foreground only + if pids[0]!=pids[1]: continue matched=False if match_data[2]==True: # is regex try: diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index f981618..68356de 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -77,7 +77,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=2 +db_version=3 ## clitheme-exec timeout value for each output substitution operation output_subst_timeout=0.4 diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 3c3bcfe..4040d68 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -113,6 +113,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) # this mechanism prevents user input from being processed through substrules last_input_content=None executor=concurrent.futures.ThreadPoolExecutor() + last_tcgetpgrp=os.tcgetpgrp(stdout_fd) while True: try: # update terminal attributes from what the program sets @@ -133,12 +134,14 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) except: pass fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.01)[0] readsize=io.DEFAULT_BUFFER_SIZE + # 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 os.write(stdout_fd, data) + # Handle output from stdout and stderr def handle_output(is_stderr: bool): data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) do_subst_operation=True @@ -155,6 +158,17 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) if stderr_fd in fds: handle_output(is_stderr=True) if process.poll()!=None and len(output_lines)==0: break + + # Print message if foreground process changed + if "normal" in debug_mode: + foreground_pid=os.tcgetpgrp(stdout_fd) + if foreground_pid!=last_tcgetpgrp: + if (foreground_pid==process.pid)!=(last_tcgetpgrp==process.pid): + message=f"\x1b[1m! \x1b[{'32' if foreground_pid==process.pid else '31'}mForeground: \x1b[4m{'True' if foreground_pid==process.pid else 'False'}\x1b[0m\n" + os.write(sys.stdout.fileno(), bytes(message, 'utf-8')) + last_tcgetpgrp=foreground_pid + + # Process outputs def process_line(line: bytes, line_data): # subst operation subst_line=copy.copy(line) @@ -163,7 +177,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) def operation(): nonlocal subst_line, failed try: - subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1]) + subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1], pids=(process.pid, os.tcgetpgrp(stdout_fd))) except TimeoutError: failed=True # Happens when no theme is set/no subst-data.db except sqlite3.OperationalError: pass @@ -184,7 +198,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) 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 futures=[] - # Process outputs for x in range(len(output_lines)): line_data=output_lines[x] line: bytes=line_data[0] @@ -202,9 +215,10 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) else: last_input_content=None # subst operation if db_interface.enable_multiprocessing: futures.append(executor.submit(process_line, line, line_data)) + # print output else: os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) else: output_lines=[] # happens when no 'break' statement occurs - # Print outputs + # Print outputs (if enable_multiprocessing) for thread in futures: os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), thread.result()) except KeyboardInterrupt: -- Gitee From 282a44b5da14f4d3623a376803fe8dd829a1fd25 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 00:16:47 +0800 Subject: [PATCH 314/354] Add more words to cspell list --- cspell.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index 9193df0..f1a7d2a 100644 --- a/cspell.json +++ b/cspell.json @@ -15,10 +15,10 @@ "exactcmdmatch", "smartcmdmatch", "endmatchhere", "leadtabindents", "leadspaces", "strictcmdmatch", "normalcmdmatch", "exactcmdmatch", "smartcmdmatch", - "subststdoutonly", "subststderronly", "substall", + "subststdoutonly", "subststderronly", "substall", "foregroundonly", "substvar", "substesc", "PKGBUILD", "MANPATH", - "tcgetattr", "tcsetattr", "getpid", "setsid", "setitimer", + "tcgetattr", "tcsetattr", "getpid", "setsid", "setitimer", "tcgetpgrp", "tcsetpgrp", "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", "SIGALRM", "ITIMER", "SIGTSTP", "SIGSTOP", "SIGCONT", "showchars", "keepends", "appname", "domainapp", -- Gitee From 173fd2ee443e5eaf0073ce910b930703a73ca262 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 00:39:26 +0800 Subject: [PATCH 315/354] Rename fallback and demo files --- README-frontend.en.md | 6 +++--- README-frontend.md | 6 +++--- demo-clithemedef/README.zh-CN.md | 2 +- clitheme_demo.py => frontend_demo.py | 0 clitheme_fallback.py => frontend_fallback.py | 0 5 files changed, 7 insertions(+), 7 deletions(-) rename clitheme_demo.py => frontend_demo.py (100%) rename clitheme_fallback.py => frontend_fallback.py (100%) diff --git a/README-frontend.en.md b/README-frontend.en.md index cfca7e5..6905908 100644 --- a/README-frontend.en.md +++ b/README-frontend.en.md @@ -60,13 +60,13 @@ f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"" You can integrate the fallback frontend module provided by this project to better handle situations when `clitheme` does not exist on the system. This fallback module contains all the functions in the frontend module, and its functions will always return fallback values. -Import the `clitheme_fallback.py` file from the repository and insert the following code in your project to use it: +Import the `frontend_fallback.py` file from the repository and insert the following code in your project to use it: ```py try: from clitheme import frontend except (ModuleNotFoundError, ImportError): - import clitheme_fallback as frontend + import frontend_fallback as frontend ``` The fallback module provided by this project will update accordingly with new versions. Therefore, it is recommended to import the latest version of this module to adopt the latest features. @@ -103,7 +103,7 @@ begin_header name Example theme version 1.0 locales en_US - supported_apps clitheme_demo + supported_apps frontend_demo end_header begin_main diff --git a/README-frontend.md b/README-frontend.md index 079c52a..a5cda9e 100644 --- a/README-frontend.md +++ b/README-frontend.md @@ -60,13 +60,13 @@ f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"" 应用程序还可以在src中内置本项目提供的fallback模块,以便更好的处理`clitheme`模块不存在时的情况。该fallback模块包括了frontend模块中的所有定义和功能,并且会永远返回失败时的默认值(fallback)。 -如需使用,请在你的项目文件中导入`clitheme_fallback.py`文件,并且在你的程序中包括以下代码: +如需使用,请在你的项目文件中导入`frontend_fallback.py`文件,并且在你的程序中包括以下代码: ```py try: from clitheme import frontend except (ModuleNotFoundError, ImportError): - import clitheme_fallback as frontend + import frontend_fallback as frontend ``` 本项目提供的fallback文件会随版本更新而更改,所以请定期往你的项目里导入最新的fallback文件以适配最新的功能。 @@ -103,7 +103,7 @@ com.example example-app file-not-found name 样例主题 version 1.0 locales zh_CN - supported_apps clitheme_demo + supported_apps frontend_demo {/header_section} {entries_section} diff --git a/demo-clithemedef/README.zh-CN.md b/demo-clithemedef/README.zh-CN.md index bbf92b3..bfd0cfa 100644 --- a/demo-clithemedef/README.zh-CN.md +++ b/demo-clithemedef/README.zh-CN.md @@ -1,6 +1,6 @@ # 字符串定义说明 -本仓库中的`clitheme_demo.py`支持以下字符串定义。默认字符串文本会以blockquote样式显示在路径名称下方。部分定义会包含额外的说明。 +本仓库中的`frontend_demo.py`支持以下字符串定义。默认字符串文本会以blockquote样式显示在路径名称下方。部分定义会包含额外的说明。 --- diff --git a/clitheme_demo.py b/frontend_demo.py similarity index 100% rename from clitheme_demo.py rename to frontend_demo.py diff --git a/clitheme_fallback.py b/frontend_fallback.py similarity index 100% rename from clitheme_fallback.py rename to frontend_fallback.py -- Gitee From 0e1de0aa1ae8912526ee7ce5174158b285166fa2 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 10:31:26 +0800 Subject: [PATCH 316/354] Suspend handling: Write `^Z` character if not foreground --- src/clitheme/_generator/db_interface.py | 6 +++--- src/clitheme/exec/output_handler_posix.py | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 8e7482a..0f53028 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -237,13 +237,13 @@ enable_multiprocessing=False # timeout value for each match operation match_timeout=_globalvar.output_subst_timeout -_manager=multiprocessing.Manager() +_manager=multiprocessing.Manager() if enable_multiprocessing else None _process: Optional[multiprocessing.Process]=None _input_values=multiprocessing.Queue() # (matches, content, is_stderr, uuid) -_return_values=_manager.dict() # uuid : content_str (uuid:None means processing) +_return_values=_manager.dict() if _manager else {} # uuid : content_str (uuid:None means processing) _watchdog_process: Optional[multiprocessing.Process]=None -_running_processes_ids=_manager.list() +_running_processes_ids=_manager.list() if _manager else [] def _init_process(): global _process, _input_values, _return_values, _watchdog_process, _running_processes_ids diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 4040d68..770f7fe 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -85,9 +85,12 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) process.send_signal(sig) signal.signal(signal.SIGTSTP, signal_handler) # Reset signal handler if sig==signal.SIGTSTP: # suspend signal - 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 + 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 + 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 try: def child_init(): # Must start new session or some programs might not work properly -- Gitee From 8bb5fe94801d31ee736016751fdfd4fb14ee9094 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 11:06:53 +0800 Subject: [PATCH 317/354] Improve command match mode checking --- src/clitheme/_generator/db_interface.py | 76 +++++++++++++------------ 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 0f53028..f3d5d25 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -106,6 +106,33 @@ 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 + 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) @@ -122,7 +149,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F final_cmdlist_exactmatch=[] if command!=None and len(command.split())>0: # command without paths (e.g. /usr/bin/bash -> bash) - stripped_command=os.path.basename(command.split()[0])+" "+(_globalvar.splitarray_to_string(command.split()[1:]) if len(command.split())>1 else '') + stripped_command=os.path.basename(command.split()[0])+(" "+_globalvar.splitarray_to_string(command.split()[1:]) if len(command.split())>1 else '') cmdlist_items=["effective_command", "command_match_strictness"] # obtain a list of effective_command with the same first term cmdlist=_connection.execute(f"SELECT DISTINCT {','.join(cmdlist_items)} FROM {_globalvar.db_data_tablename} WHERE effective_command LIKE ? or effective_command LIKE ?;", (command.split()[0].strip()+" %", stripped_command.split()[0].strip()+" %")).fetchall() @@ -133,36 +160,12 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F cmdlist.sort(key=split_len, reverse=True) # prioritize effective_command with exact match requirement cmdlist=_connection.execute(f"SELECT DISTINCT {','.join(cmdlist_items)} FROM {_globalvar.db_data_tablename} WHERE (effective_command=? OR effective_command=?) AND command_match_strictness=2", (re.sub(r" {2,}", " ", command).strip(),re.sub(r" {2,}", " ", stripped_command).strip())).fetchall()+cmdlist - 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 # attempt to find matching command for target_command in [command, stripped_command]: for tp in cmdlist: - match_cmd: str=tp[0].strip() # extract value from tuple - strictness: int=tp[1] # strictness setting - 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 - if success: + match_cmd=tp[0].strip() + strictness=tp[1] + if _check_strictness(match_cmd, strictness, target_command)==True: # if found matching target_command if match_cmd not in final_cmdlist: final_cmdlist.append(match_cmd) @@ -171,7 +174,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F 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"] + fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only", "unique_id", "foreground_only", "effective_command", "command_match_strictness"] # get locales locales=_globalvar.get_locale() nonlocal matches @@ -196,7 +199,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F if len(_running_processes_ids)==0: _init_process() result_id=uuid.uuid4() - global _input_values; _input_values.put((matches, content_str, is_stderr, result_id, pids)) + global _input_values; _input_values.put((matches, content_str, is_stderr, result_id, pids, command)) counter=0 watchdog_timer=0 while counter=1.500: _init_process() - _input_values.put((matches, content_str, is_stderr, result_id, pids)) + _input_values.put((matches, content_str, is_stderr, result_id, pids, command)) watchdog_timer=0 else: # executed when no "break" happens try: del _return_values[result_id] @@ -221,7 +224,7 @@ def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=F _init_process() # restart the process raise TimeoutError("match operation timeout") else: - content_str=_handle_subst(matches, content_str, is_stderr, pids) + content_str=_handle_subst(matches, content_str, is_stderr, pids, command) return content_str # --The following implementation (A) is for setting a timeout capacity on content match functions-- @@ -278,7 +281,7 @@ def __process_main_loop(input_vals: multiprocessing.Queue, return_vals: dict, pr try: content=input_vals.get_nowait() except queue.Empty: return return_vals[content[3]]=None # Processing - return_str=_handle_subst(content[0], content[1], content[2], content[4]) + return_str=_handle_subst(content[0], content[1], content[2], content[4], content[5]) return_vals[content[3]]=return_str while True: try: time.sleep(0.001) @@ -289,13 +292,17 @@ def __process_main_loop(input_vals: multiprocessing.Queue, return_vals: dict, pr except KeyboardInterrupt: pass # except: break -def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, pids: tuple[int, int], ret: Optional[list[bytes]]=None): +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: 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 strictness + if target_command!=None and match_data[7]!=None and \ + _check_strictness(match_data[7], match_data[8], \ + os.path.basename(target_command.split()[0])+(" "+_globalvar.splitarray_to_string(target_command.split()[1:]) if len(target_command.split())>1 else ''))==False: continue if match_data[6]==True: # Foreground only if pids[0]!=pids[1]: continue matched=False @@ -317,5 +324,4 @@ def _handle_subst(matches: list[tuple], content: bytes, is_stderr: bool, pids: t 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 - if ret!=None: ret.append(content_str) return content_str -- Gitee From 81b2b76bf9d1f0cd1e17ed32939a9ced88b061c3 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 14:31:38 +0800 Subject: [PATCH 318/354] Improve foreground process detection By recording the pid of each line and reading continuous content altogether, it brings more accurate detection of the current foreground process for each line, since it eliminates differences between the read time and the process time. --- src/clitheme/exec/output_handler_posix.py | 88 +++++++++++++---------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 770f7fe..ef33fba 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -21,6 +21,7 @@ import struct import copy import re import sqlite3 +import time import concurrent.futures from .._generator import db_interface from .. import _globalvar, frontend @@ -117,6 +118,15 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) last_input_content=None executor=concurrent.futures.ThreadPoolExecutor() last_tcgetpgrp=os.tcgetpgrp(stdout_fd) + + def handle_debug_pgrp(foreground_pid: int): + nonlocal last_tcgetpgrp + if "normal" in debug_mode and foreground_pid!=last_tcgetpgrp: + if (foreground_pid==process.pid)!=(last_tcgetpgrp==process.pid): + message=f"\x1b[1m! \x1b[{'32' if foreground_pid==process.pid else '31'}mForeground: \x1b[4m{'True' if foreground_pid==process.pid else 'False'}\x1b[0m\n" + os.write(sys.stdout.fileno(), bytes(message, 'utf-8')) + last_tcgetpgrp=foreground_pid + while True: try: # update terminal attributes from what the program sets @@ -135,52 +145,56 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) fcntl.ioctl(stderr_fd, termios.TIOCSWINSZ, new_term_size) process.send_signal(signal.SIGWINCH) except: pass - fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.01)[0] readsize=io.DEFAULT_BUFFER_SIZE - # 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 - os.write(stdout_fd, data) - # Handle output from stdout and stderr - def handle_output(is_stderr: bool): - data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) - do_subst_operation=True - lines=data.splitlines(keepends=True) - for x in range(len(lines)): - line=lines[x] - # if last input did not end with newlines, append new content to it - if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines): - orig_line=output_lines[-1][0] - output_lines.pop() - output_lines.append((orig_line+line,is_stderr,do_subst_operation)) - else: output_lines.append((line,is_stderr,do_subst_operation)) - if stdout_fd in fds: handle_output(is_stderr=False) - if stderr_fd in fds: handle_output(is_stderr=True) + + start_time=time.perf_counter() + while time.perf_counter()-start_time<0.1: + fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.01)[0] + if fds==[]: + # Output the debug message if no outputs are written + handle_debug_pgrp(os.tcgetpgrp(stdout_fd)) + break + # 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 + os.write(stdout_fd, data) + # Handle output from stdout and stderr + def handle_output(is_stderr: bool): + 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 last output did not end with newlines, append new content to it + if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines): + orig_data=output_lines[-1] + orig_line=orig_data[0] + output_lines.pop() + output_lines.append((orig_line+line,is_stderr,do_subst_operation, orig_data[3])) + else: 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 process.poll()!=None and len(output_lines)==0: break - # Print message if foreground process changed - if "normal" in debug_mode: - foreground_pid=os.tcgetpgrp(stdout_fd) - if foreground_pid!=last_tcgetpgrp: - if (foreground_pid==process.pid)!=(last_tcgetpgrp==process.pid): - message=f"\x1b[1m! \x1b[{'32' if foreground_pid==process.pid else '31'}mForeground: \x1b[4m{'True' if foreground_pid==process.pid else 'False'}\x1b[0m\n" - os.write(sys.stdout.fileno(), bytes(message, 'utf-8')) - last_tcgetpgrp=foreground_pid - # Process outputs def process_line(line: bytes, line_data): + nonlocal last_tcgetpgrp # subst operation 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 + nonlocal subst_line, failed, foreground_pid try: - subst_line=db_interface.match_content(line, _globalvar.splitarray_to_string(command), is_stderr=line_data[1], pids=(process.pid, os.tcgetpgrp(stdout_fd))) + 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 @@ -206,14 +220,14 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) line: bytes=line_data[0] # if does not end with newlines, leave it for the next iteration if x==len(output_lines)-1 and not line.endswith(newlines): - if not len(line_data)>=4: # not from previous iteration + if not len(line_data)>=5: # not from previous iteration output_lines=[line_data+(True,)] # add another entry to signal it's from previous iteration break # 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); last_input_content=None + 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=(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 -- Gitee From 4d20c0ba9cbe19867ddce36435e3ff3b652397c0 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 15:11:00 +0800 Subject: [PATCH 319/354] Add `--debug-foreground` option to clitheme-exec --- docs/clitheme-exec.1 | 14 +++++++++++++- src/clitheme/exec/__init__.py | 5 ++++- src/clitheme/exec/output_handler_posix.py | 2 +- src/clitheme/strings/exec-strings.clithemedef.txt | 4 ++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/clitheme-exec.1 b/docs/clitheme-exec.1 index 058200b..d86e6bc 100644 --- a/docs/clitheme-exec.1 +++ b/docs/clitheme-exec.1 @@ -1,4 +1,4 @@ -.TH clitheme-exec 1 2024-05-07 +.TH clitheme-exec 1 2024-06-21 .SH NAME clitheme\-exec \- match and substitute output of a command .SH SYNOPSIS @@ -38,5 +38,17 @@ Display various control characters in plain text. The following characters will - Bell character (\\x07) .RE +.TP +.B --debug-foreground +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. +.P +.RS 14 +- "! Foreground: False ()": Process exits foreground state + +- "! Foreground: True ()": Process enters (re-enters) foreground state +.RE + .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 a81e8e3..c5c93ee 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -84,13 +84,14 @@ 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-nosubst] [command]") + print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--debug-showchars] [--debug-foreground] [--debug-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"+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")) def _handle_error(message: str): @@ -121,6 +122,8 @@ def main(arguments: list[str]): debug_mode.append("newlines") elif arg=="--debug-showchars": debug_mode.append("showchars") + elif arg=="--debug-foreground": + debug_mode.append("foreground") elif arg=="--debug-nosubst": subst=False elif arg=="--help": diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index ef33fba..240b2d9 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -121,7 +121,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) def handle_debug_pgrp(foreground_pid: int): nonlocal last_tcgetpgrp - if "normal" in debug_mode and foreground_pid!=last_tcgetpgrp: + if "foreground" in debug_mode and foreground_pid!=last_tcgetpgrp: if (foreground_pid==process.pid)!=(last_tcgetpgrp==process.pid): message=f"\x1b[1m! \x1b[{'32' if foreground_pid==process.pid else '31'}mForeground: \x1b[4m{'True' if foreground_pid==process.pid else 'False'}\x1b[0m\n" os.write(sys.stdout.fileno(), bytes(message, 'utf-8')) diff --git a/src/clitheme/strings/exec-strings.clithemedef.txt b/src/clitheme/strings/exec-strings.clithemedef.txt index b476073..cff2092 100644 --- a/src/clitheme/strings/exec-strings.clithemedef.txt +++ b/src/clitheme/strings/exec-strings.clithemedef.txt @@ -42,6 +42,10 @@ in_domainapp swiftycode clitheme # locale:default --debug-showchars: Display various control characters in plain text locale:zh_CN --debug-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] [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:不进行任何输出替换,即使已设定主题 -- Gitee From e334e55e82ccec17d64a0f28a5b19df8ca9c4bc7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 15:11:15 +0800 Subject: [PATCH 320/354] Add missing `--debug-nosubst` information in clitheme-exec man page --- docs/clitheme-exec.1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/clitheme-exec.1 b/docs/clitheme-exec.1 index d86e6bc..8016e8c 100644 --- a/docs/clitheme-exec.1 +++ b/docs/clitheme-exec.1 @@ -49,6 +49,11 @@ 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 +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. .SH SEE ALSO \fIclitheme(1)\fR \ No newline at end of file -- Gitee From 7bb0eb18e07b65180e80260862af5219a0895eed Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 15:15:43 +0800 Subject: [PATCH 321/354] Handle input/output error when writing stdin --- src/clitheme/exec/output_handler_posix.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 240b2d9..97f8efb 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -160,7 +160,8 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) # 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 - os.write(stdout_fd, 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 def handle_output(is_stderr: bool): data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) -- Gitee From ef1df494a2e8f5854bdeee27ba91cbcfb0294221 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 15:25:40 +0800 Subject: [PATCH 322/354] Show PID in output of `--debug-foreground`; - Show the debug message everytime when the foreground PID changes (instead of the state changes) --- src/clitheme/exec/output_handler_posix.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 97f8efb..b032169 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -122,9 +122,9 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) def handle_debug_pgrp(foreground_pid: int): nonlocal last_tcgetpgrp if "foreground" in debug_mode and foreground_pid!=last_tcgetpgrp: - if (foreground_pid==process.pid)!=(last_tcgetpgrp==process.pid): - message=f"\x1b[1m! \x1b[{'32' if foreground_pid==process.pid else '31'}mForeground: \x1b[4m{'True' if foreground_pid==process.pid else 'False'}\x1b[0m\n" - os.write(sys.stdout.fileno(), bytes(message, 'utf-8')) + # if (foreground_pid==process.pid)!=(last_tcgetpgrp==process.pid): + 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 while True: -- Gitee From eac0aa385f8758c5276215b2d9a21d984d116dca Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 20:24:23 +0800 Subject: [PATCH 323/354] Use the new foreground pid for appending content into previous line --- src/clitheme/exec/output_handler_posix.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index b032169..dd6281d 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -122,7 +122,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) def handle_debug_pgrp(foreground_pid: int): nonlocal last_tcgetpgrp if "foreground" in debug_mode and foreground_pid!=last_tcgetpgrp: - # if (foreground_pid==process.pid)!=(last_tcgetpgrp==process.pid): 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 @@ -175,7 +174,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) orig_data=output_lines[-1] orig_line=orig_data[0] output_lines.pop() - output_lines.append((orig_line+line,is_stderr,do_subst_operation, orig_data[3])) + output_lines.append((orig_line+line,is_stderr,do_subst_operation, foreground_pid)) else: 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) -- Gitee From 5f71e05b7ff8b3da03965bac21b01ea890528e78 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 21 Jun 2024 22:46:56 +0800 Subject: [PATCH 324/354] Update version (v2.0-dev20240621) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 7e2924b..0fc9cf0 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240619 +pkgver=2.0_dev20240621 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 647960a..991a2d6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240619-1) unstable; urgency=low +clitheme (2.0-dev20240621-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 19 Jun 2024 23:51:00 +0800 + -- swiftycode <3291929745@qq.com> Fri, 21 Jun 2024 22:46:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index f2d3884..08bb299 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-dev20240619" +__version__="2.0-dev20240621" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240619" +version_main="2.0_dev20240621" version_buildnumber=1 \ No newline at end of file -- Gitee From 896163964759a7feaa7b25925567cf095ef64a07 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 22 Jun 2024 13:28:13 +0800 Subject: [PATCH 325/354] Fix syntax usage in README-frontend --- README-frontend.en.md | 40 ++++++++++++++++++++-------------------- README-frontend.md | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README-frontend.en.md b/README-frontend.en.md index 6905908..3137ac2 100644 --- a/README-frontend.en.md +++ b/README-frontend.en.md @@ -99,32 +99,32 @@ You can also include this information in your project's official documentation. Consult the Wiki pages and documentation for detailed syntax of theme definition files. An example is provided below: ``` -begin_header +{header_section} name Example theme version 1.0 locales en_US supported_apps frontend_demo -end_header +{/header_section} -begin_main +{entries_section} in_domainapp com.example example-app - entry found-file - locale default o(≧v≦)o Great! Found {} files in current directory! - locale en_US o(≧v≦)o Great! Found {} files in current directory! - end_entry - entry installing-file - locale default (>^ω^<) Installing "{}"... - locale en_US (>^ω^<) Installing "{}"... - end_entry - entry install-success - locale default o(≧v≦)o Successfully installed {} files! - locale en_US o(≧v≦)o Successfully installed {} files! - end_entry - entry file-not-found - locale default ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found - locale en_US ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found - end_entry -end_main + [entry] found-file + locale:default o(≧v≦)o Great! Found {} files in current directory! + locale:en_US o(≧v≦)o Great! Found {} files in current directory! + [/entry] + [entry] installing-file + locale:default (>^ω^<) Installing "{}"... + locale:en_US (>^ω^<) Installing "{}"... + [/entry] + [entry] install-success + locale:default o(≧v≦)o Successfully installed {} files! + locale:en_US o(≧v≦)o Successfully installed {} files! + [/entry] + [entry] file-not-found + locale:default ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found + locale:en_US ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found + [/entry] +{/entries_section} ``` Use the command `clitheme apply-theme ` to apply the theme definition file onto the system. Supported applications will start using the string definitions listed in this file. diff --git a/README-frontend.md b/README-frontend.md index a5cda9e..8b7d014 100644 --- a/README-frontend.md +++ b/README-frontend.md @@ -124,7 +124,7 @@ com.example example-app file-not-found locale:default ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" locale:zh_CN ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" [/entry] -end_main +{/entries_section} ``` 编写好主题文件后,使用 `clitheme apply-theme `来应用主题。应用程序会直接采用主题中适配的字符串。 -- Gitee From 87bfe35c3825467ccc4ee779d849c76e65973b62 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 23 Jun 2024 18:00:09 +0800 Subject: [PATCH 326/354] Change exception handling statement in pattern checking --- src/clitheme/_generator/_dataclass.py | 2 +- src/clitheme/_generator/db_interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index ff78b44..764c770 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -260,7 +260,7 @@ class GeneratorObject(_handlers.DataHandlers): 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.lineindex Date: Sun, 23 Jun 2024 19:02:38 +0800 Subject: [PATCH 327/354] Fix disallowed option matching in `handle_block_input` Check whether if the option is specified in the block first --- 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 764c770..4a72992 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -240,7 +240,7 @@ class GeneratorObject(_handlers.DataHandlers): 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)) elif disallow_cmdmatch_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))) + 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[str], is_regex: bool, strictness: int, foreground_only: bool} -- Gitee From 09455d13f18ca1446e33231674ebddd75a67840c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 23 Jun 2024 23:46:27 +0800 Subject: [PATCH 328/354] Update version (v2.0-dev20240623) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 0fc9cf0..86237fe 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240621 +pkgver=2.0_dev20240623 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 991a2d6..28f9ec8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240621-1) unstable; urgency=low +clitheme (2.0-dev20240623-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Fri, 21 Jun 2024 22:46:00 +0800 + -- swiftycode <3291929745@qq.com> Sun, 23 Jun 2024 23:45:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 08bb299..ee8bfce 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-dev20240621" +__version__="2.0-dev20240623" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240621" +version_main="2.0_dev20240623" version_buildnumber=1 \ No newline at end of file -- Gitee From 22613c3ccc751fe09f095981cae51c68e538dbfa Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 25 Jun 2024 22:49:46 +0800 Subject: [PATCH 329/354] Forward termination signals by processes --- src/clitheme/exec/output_handler_posix.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index dd6281d..c6c35ba 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -250,4 +250,9 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) _labeled_print(fd.reof("internal-error-err", "Error: an internal error has occurred while executing the command (execution halted):")) raise termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes - return process.poll() + exit_code=process.poll() + try: + if exit_code!=None and exit_code<0: # Terminated by signal + os.kill(os.getpid(), abs(exit_code)) + except: pass + return exit_code -- Gitee From 7fc6e4638b71abb2db1e0a0eb340db3c59eb3431 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 26 Jun 2024 22:32:57 +0800 Subject: [PATCH 330/354] Change SIGINT handling in output_handler_posix Add SIGINT as a signal handler that sends the interrupt character instead of handling KeyboardInterrupt exception to prevent output processing from being interrupted, causing missing output content. --- src/clitheme/exec/output_handler_posix.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index c6c35ba..c373b0c 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -85,13 +85,15 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) if sig==signal.SIGCONT: # continue signal process.send_signal(sig) signal.signal(signal.SIGTSTP, signal_handler) # Reset signal handler - if sig==signal.SIGTSTP: # suspend signal + 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 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 try: def child_init(): # Must start new session or some programs might not work properly @@ -111,6 +113,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) else: 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) def get_terminal_size(): return fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH',0,0,0,0)) last_terminal_size=struct.pack('HHHH',0,0,0,0) # placeholder @@ -238,12 +241,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) # Print outputs (if enable_multiprocessing) for thread in futures: os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), thread.result()) - except KeyboardInterrupt: - os.write(stdout_fd, b'\x03') # '^C' character - # try: - # try: os.kill(os.tcgetpgrp(stdout_fd), signal.SIGINT) # Send signal to foreground process - # except OSError: process.send_signal(signal.SIGINT) - # except KeyboardInterrupt: pass except: 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 -- Gitee From b8d1e53201e24ea7f2e4c9368ff8662ecfb09528 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 26 Jun 2024 23:34:19 +0800 Subject: [PATCH 331/354] Merge line index change operation into `goto_next_line` Makes the process easier to manage across many similar uses --- src/clitheme/_generator/__init__.py | 4 +--- src/clitheme/_generator/_dataclass.py | 9 +++++++-- src/clitheme/_generator/_entries_parser.py | 3 +-- src/clitheme/_generator/_header_parser.py | 3 +-- src/clitheme/_generator/_manpage_parser.py | 8 +++----- src/clitheme/_generator/_substrules_parser.py | 3 +-- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 66bfdc9..b3fffdd 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -36,9 +36,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info ## Main code while obj.lineindex bool: return self.lines_data[self.lineindex].strip()=="" or self.lines_data[self.lineindex].strip().startswith('#') + def goto_next_line(self) -> bool: + while self.lineindex0 and obj.lines_data[obj.lineindex].split()[0]=="as": + # 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() 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)) diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index cbd446d..df4d2b7 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -31,8 +31,7 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str else: obj.db_interface.init_db(obj.path+"/"+_globalvar.db_filename) obj.db_interface.debug_mode=not obj.silence_warn while obj.lineindex Date: Thu, 27 Jun 2024 01:14:05 +0800 Subject: [PATCH 332/354] Fix pattern and sanity checking and entry name processing --- src/clitheme/_generator/_dataclass.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index e8dd2e1..4f775e5 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -272,17 +272,13 @@ class GeneratorObject(_handlers.DataHandlers): 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: + 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 is_substrules: check_valid_pattern(name, each_entry[2]) - else: - # Prevent leading . & prevent /,\ in entry name - if _globalvar.sanity_check(name)==False: - self.handle_error(self.fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(each_entry[2]), sanitycheck_msg=_globalvar.sanity_check_error_message)) + 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]) @@ -360,6 +356,12 @@ class GeneratorObject(_handlers.DataHandlers): # 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[5]), sanitycheck_msg=_globalvar.sanity_check_error_message)) encountered_ids.add(entry[3]) if is_substrules: try: -- Gitee From 133b84b33b933a2226ac52559fd8c11b3ca880d6 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 27 Jun 2024 01:14:56 +0800 Subject: [PATCH 333/354] Fix and optimize variable content substitution --- src/clitheme/_generator/_dataclass.py | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 4f775e5..819c29d 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -127,18 +127,22 @@ class GeneratorObject(_handlers.DataHandlers): if not override_check and (not "substvar" in self.global_options or self.global_options["substvar"]==False): return content # get all variables used in content new_content=copy.copy(content) - variables=re.findall(r"{{(.+?)}}", content) - if len(variables)>0: - for var_name in variables: - if var_name=="ESC": continue # skip {{ESC}}; leave it for substesc - var_content: str - try: - var_content=self.global_variables[var_name] - except KeyError: - if not silence_warnings: self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", \ - num=line_number_debug if line_number_debug!=None else str(self.lineindex+1), name=self.fmt(var_name))) - continue - new_content=new_content.replace(r"{{"+var_name+r"}}", var_content) + encountered_variables=set() + offset=0 + for match in re.finditer(r"{{(.+?)??}}", content): + var_name=match.group(1) + if var_name.strip()=='': continue + if var_name=="ESC": continue # skip {{ESC}}; leave it for substesc + var_content: str + try: + var_content=self.global_variables[var_name] + except KeyError: + if not silence_warnings and var_name not in encountered_variables: self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", \ + num=line_number_debug if line_number_debug!=None else str(self.lineindex+1), name=self.fmt(var_name))) + continue + new_content=new_content[:match.start()+offset]+var_content+new_content[match.end()+offset:] + offset+=len(var_content)-(match.end()-match.start()) + encountered_variables.add(var_name) return new_content def handle_set_variable(self, line_content: str, really_really_global: bool=False): if not line_content.split()[0].startswith("setvar:"): return -- Gitee From 792252740a39f8524e6254d4b4e73c588bb0d09b Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 27 Jun 2024 01:15:25 +0800 Subject: [PATCH 334/354] Fix bug in test program when file does not exist --- src/clithemedef-test_testprogram.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clithemedef-test_testprogram.py b/src/clithemedef-test_testprogram.py index 13fc0b3..4271d75 100644 --- a/src/clithemedef-test_testprogram.py +++ b/src/clithemedef-test_testprogram.py @@ -34,6 +34,7 @@ for line in expected_data.splitlines(): except FileNotFoundError: print("[File] file "+rootpath+"/"+current_path+" does not exist") errorcount+=1 + current_path="" if contents=="": continue if contents.strip()!=line.strip(): print("[Content] Content mismatch on file "+rootpath+"/"+current_path) -- Gitee From a68f6806593115e86e6935412fac2e8a6112feef Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 27 Jun 2024 22:56:09 +0800 Subject: [PATCH 335/354] Update version (v2.0-dev20240627) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 86237fe..cbccef5 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240623 +pkgver=2.0_dev20240627 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 28f9ec8..ba7743e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240623-1) unstable; urgency=low +clitheme (2.0-dev20240627-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Sun, 23 Jun 2024 23:45:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 27 Jun 2024 22:55:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index ee8bfce..aa19c4f 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-dev20240623" +__version__="2.0-dev20240627" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240623" +version_main="2.0_dev20240627" version_buildnumber=1 \ No newline at end of file -- Gitee From 88aff7f4307802fc1383f74e23a9063b158f83a4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 4 Jul 2024 22:51:36 +0800 Subject: [PATCH 336/354] Always show traceback of other exceptions in cli Makes it easier to debug issues and makes the error message clearer --- src/clitheme/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index b744692..b64d8fe 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -97,7 +97,7 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p final_path=_generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index), filename=filenames[i] if len(filenames)>0 else "") generate_path=False # Don't generate another temp folder after first one index+=1 - except: + except Exception as exc: sys.stdout=orig_stdout print(("\n" if print_progress else ""), end='') # Print any output messages if an error occurs @@ -106,7 +106,8 @@ def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, p print(generator_msgs.getvalue(), end='') print(f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ index=str(i+1), message=str(sys.exc_info()[1]))) - _globalvar.handle_exception() + if type(exc)==SyntaxError: _globalvar.handle_exception() + else: raise # Always raise exception if other error occurred in _generator return 1 else: sys.stdout=orig_stdout # restore standard output -- Gitee From 1a73601aeaa1c2d53a72e942f008a828706c4f48 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 10 Jul 2024 11:10:26 +0800 Subject: [PATCH 337/354] Improve repeated unknown variables handling Also improves the logic of the code at `subst_variable_content` --- src/clitheme/_generator/_dataclass.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 819c29d..873237d 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -139,10 +139,10 @@ class GeneratorObject(_handlers.DataHandlers): except KeyError: if not silence_warnings and var_name not in encountered_variables: self.handle_warning(self.fd.feof("unknown-variable-warn", "Line {num}: unknown variable \"{name}\", not performing substitution", \ num=line_number_debug if line_number_debug!=None else str(self.lineindex+1), name=self.fmt(var_name))) - continue - new_content=new_content[:match.start()+offset]+var_content+new_content[match.end()+offset:] - offset+=len(var_content)-(match.end()-match.start()) - encountered_variables.add(var_name) + else: + new_content=new_content[:match.start()+offset]+var_content+new_content[match.end()+offset:] + offset+=len(var_content)-(match.end()-match.start()) + encountered_variables.add(var_name) # Prevent repeated warnings return new_content def handle_set_variable(self, line_content: str, really_really_global: bool=False): if not line_content.split()[0].startswith("setvar:"): return -- Gitee From 42b1c3e94db5a04e35c40cb07ed8476f16702ec7 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 10 Jul 2024 11:31:03 +0800 Subject: [PATCH 338/354] Disallow spaces in variable name specification Something like `{{this }}` or `{{this and}}` won't be processed --- src/clitheme/_generator/_dataclass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 873237d..24eb7ce 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -129,9 +129,9 @@ class GeneratorObject(_handlers.DataHandlers): new_content=copy.copy(content) encountered_variables=set() offset=0 - for match in re.finditer(r"{{(.+?)??}}", content): + for match in re.finditer(r"{{([^\s]+?)??}}", content): var_name=match.group(1) - if var_name.strip()=='': continue + if var_name==None or var_name.strip()=='': continue if var_name=="ESC": continue # skip {{ESC}}; leave it for substesc var_content: str try: -- Gitee From b2ce84b8929a5311fad7afb627ae1e3dd2bc1b95 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 10 Jul 2024 11:55:04 +0800 Subject: [PATCH 339/354] Optimize logic of moving to next line Check the return value of the function directly in the `while` statement, making more logical sense than checking line index separately. --- src/clitheme/_generator/__init__.py | 3 +-- src/clitheme/_generator/_dataclass.py | 3 +-- src/clitheme/_generator/_entries_parser.py | 3 +-- src/clitheme/_generator/_header_parser.py | 3 +-- src/clitheme/_generator/_manpage_parser.py | 3 +-- src/clitheme/_generator/_substrules_parser.py | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index b3fffdd..37f175d 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -35,8 +35,7 @@ def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_info 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.lineindex Date: Wed, 10 Jul 2024 21:51:28 +0800 Subject: [PATCH 340/354] Add support for multiple `[file_content]` directive specification and `[include_file]` block; - Fixes an issue where `{manpage_section}` is not checked for invalid phrases --- src/clitheme/_generator/_manpage_parser.py | 68 +++++++++++++++++----- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 1827361..2701ea1 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -20,30 +20,49 @@ 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() - if phrases[0]=="[file_content]": - obj.check_enough_args(phrases, 2) - filepath=obj.subst_variable_content(_globalvar.splitarray_to_string(phrases[1:])).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)) - content=obj.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/file_content]") - 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() - 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)) - # get content - # if no filename provided, use current working directory as parent path; else, use the directory the file is in as the parent path + 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 if obj.filename.strip()!="": parent_dir+=os.path.dirname(obj.filename) file_dir=parent_dir+("/" if parent_dir!="" else "")+_globalvar.splitarray_to_string(filepath).replace(" ","/") + # 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])) # write manpage files in theme-info for db migration feature to work successfully obj.write_manpage_file(filepath, filecontent, obj.lineindex+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[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 + 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)) + return filepath + file_paths=[handle(phrases)] + # handle additional [file_content] phrases + prev_line_index=obj.lineindex + while obj.goto_next_line(): + p=obj.lines_data[obj.lineindex].split() + if p[0]=="[file_content]": + prev_line_index=obj.lineindex + file_paths.append(handle(p)) + else: + obj.lineindex=prev_line_index + break + content=obj.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/file_content]") + for filepath in file_paths: + 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() + 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() @@ -52,6 +71,24 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.write_manpage_file(target_file, filecontent, obj.lineindex+1) else: 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() + 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) + while obj.goto_next_line(): + 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() + 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) + elif p[0]=="[/include_file]": + obj.check_extra_args(p, 1, use_exact_count=True) + break + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) 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()) @@ -62,3 +99,4 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("manpage") break + else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) -- Gitee From f283850f902154d817fc9a97028e5e977893dfaa Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 10 Jul 2024 21:52:44 +0800 Subject: [PATCH 341/354] Don't show repeated file warnings when writing backup manpage data --- src/clitheme/_generator/_handlers.py | 2 +- src/clitheme/_generator/_manpage_parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index 272cc98..ae1ed18 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -84,7 +84,7 @@ class DataHandlers: 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))) full_path=parent_path+"/"+file_path[-1] if os.path.isfile(full_path): - self.handle_warning(self.fd.feof("repeated-manpage-warn","Line {num}: repeated manpage file, overwriting", num=str(line_number_debug))) + 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))) try: # write the compressed and original version of the file open(full_path, "w", encoding="utf-8").write(content) diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 2701ea1..809d3a2 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -32,7 +32,7 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: 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])) # write manpage files in theme-info for db migration feature to work successfully - obj.write_manpage_file(filepath, filecontent, obj.lineindex+1, custom_parent_path=obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name+"/manpage_data") + 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[str]) -> list[str]: -- Gitee From d8bc2d073d4f1565ab23f43896f10fcb30d87a33 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 10 Jul 2024 22:28:22 +0800 Subject: [PATCH 342/354] Merge invalid phrase handling into `handle_invalid_phrase` --- src/clitheme/_generator/__init__.py | 2 +- src/clitheme/_generator/_dataclass.py | 4 +++- src/clitheme/_generator/_entries_parser.py | 2 +- src/clitheme/_generator/_header_parser.py | 2 +- src/clitheme/_generator/_manpage_parser.py | 4 ++-- src/clitheme/_generator/_substrules_parser.py | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/clitheme/_generator/__init__.py b/src/clitheme/_generator/__init__.py index 37f175d..618b624 100644 --- a/src/clitheme/_generator/__init__.py +++ b/src/clitheme/_generator/__init__.py @@ -52,7 +52,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) - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(first_phrase), num=str(obj.lineindex+1))) + else: obj.handle_invalid_phrase(first_phrase) def is_content_parsed() -> bool: content_sections=["entries", "substrules", "manpage"] diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index 72a2733..e208e2b 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -75,6 +75,8 @@ 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 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]]: # merge_global_options: 0 - Don't merge; 1 - Merge self.global_options; 2 - Merge self.really_really_global_options final_options={} @@ -346,7 +348,7 @@ class GeneratorObject(_handlers.DataHandlers): elif option=="substvar" and got_options['substvar']==True: entry_name_substvar=True break - else: self.handle_error(self.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=self.fmt(phrases[0]), num=str(self.lineindex+1))) + else: self.handle_invalid_phrase(phrases[0]) # For silence_warning in subst_variable_content encountered_ids=set() for x in range(len(entries)): diff --git a/src/clitheme/_generator/_entries_parser.py b/src/clitheme/_generator/_entries_parser.py index fcef78a..1faae2a 100644 --- a/src/clitheme/_generator/_entries_parser.py +++ b/src/clitheme/_generator/_entries_parser.py @@ -59,4 +59,4 @@ def handle_entries_section(obj: _dataclass.GeneratorObject, first_phrase: str): if phrases[0]=="end_main": obj.handle_warning(obj.fd.feof("syntax-phrase-deprecation-warn", "Line {num}: phrase \"{old_phrase}\" is deprecated in this version; please use \"{new_phrase}\" instead", num=str(obj.lineindex+1), old_phrase="end_main", new_phrase=r"{/entries_section}")) break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) + else: obj.handle_invalid_phrase(phrases[0]) diff --git a/src/clitheme/_generator/_header_parser.py b/src/clitheme/_generator/_header_parser.py index eb27842..94dc8e2 100644 --- a/src/clitheme/_generator/_header_parser.py +++ b/src/clitheme/_generator/_header_parser.py @@ -61,4 +61,4 @@ def handle_header_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("header") break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) \ No newline at end of file + else: obj.handle_invalid_phrase(phrases[0]) \ No newline at end of file diff --git a/src/clitheme/_generator/_manpage_parser.py b/src/clitheme/_generator/_manpage_parser.py index 809d3a2..422da7f 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -88,7 +88,7 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): elif p[0]=="[/include_file]": obj.check_extra_args(p, 1, use_exact_count=True) break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) + 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()) @@ -99,4 +99,4 @@ def handle_manpage_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("manpage") break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) + else: obj.handle_invalid_phrase(phrases[0]) diff --git a/src/clitheme/_generator/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 606193d..94e3f91 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -94,4 +94,4 @@ def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str obj.check_extra_args(phrases, 1, use_exact_count=True) obj.handle_end_section("substrules") break - else: obj.handle_error(obj.fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=obj.fmt(phrases[0]), num=str(obj.lineindex+1))) \ No newline at end of file + else: obj.handle_invalid_phrase(phrases[0]) \ No newline at end of file -- Gitee From c16cdbba40cc996359a7a1b6d6ea3bb8dcfee92f Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 10 Jul 2024 22:30:56 +0800 Subject: [PATCH 343/354] Update version (v2.0-dev20240710) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index cbccef5..db43c85 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240627 +pkgver=2.0_dev20240710 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index ba7743e..2d5e07a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240627-1) unstable; urgency=low +clitheme (2.0-dev20240710-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Thu, 27 Jun 2024 22:55:00 +0800 + -- swiftycode <3291929745@qq.com> Wed, 10 Jul 2024 22:29:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index aa19c4f..96a896c 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-dev20240627" +__version__="2.0-dev20240710" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240627" +version_main="2.0_dev20240710" version_buildnumber=1 \ No newline at end of file -- Gitee From eefea2a5243ed28e9b257c8c0b91a1385e066310 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 14 Jul 2024 18:57:10 +0800 Subject: [PATCH 344/354] Don't append unfinished line content if foreground process is different --- 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 c373b0c..4473a89 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -173,7 +173,7 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) for x in range(len(lines)): line=lines[x] # if last output did not end with newlines, append new content to it - if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines): + if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines) and output_lines[-1][3]==foreground_pid: orig_data=output_lines[-1] orig_line=orig_data[0] output_lines.pop() -- Gitee From cf8d60fe69e38a713e19ffb950b973b1296d2c08 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sun, 14 Jul 2024 20:19:15 +0800 Subject: [PATCH 345/354] Handle outputs on separate thread - Increase accuracy and reliability of foreground process detection - Reduce polling delay previously needed to previously maintain accurate foreground process detection --- src/clitheme/exec/output_handler_posix.py | 101 +++++++++++++--------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 4473a89..fca372c 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 concurrent.futures +import threading from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print @@ -129,8 +130,10 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) os.write(sys.stdout.fileno(), bytes(message, 'utf-8')) last_tcgetpgrp=foreground_pid - while True: - try: + def output_read_loop(): + nonlocal last_terminal_size, last_input_content, output_lines + unfinished_output=None + while True: # update terminal attributes from what the program sets try: attrs=termios.tcgetattr(stdout_fd) @@ -147,41 +150,58 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) fcntl.ioctl(stderr_fd, termios.TIOCSWINSZ, new_term_size) process.send_signal(signal.SIGWINCH) except: pass + readsize=io.DEFAULT_BUFFER_SIZE - - start_time=time.perf_counter() - while time.perf_counter()-start_time<0.1: - fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.01)[0] - if fds==[]: - # Output the debug message if no outputs are written - handle_debug_pgrp(os.tcgetpgrp(stdout_fd)) - break - # 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 - def handle_output(is_stderr: bool): - 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 last output did not end with newlines, append new content to it - if x==0 and len(output_lines)>0 and not output_lines[-1][0].endswith(newlines) and output_lines[-1][3]==foreground_pid: - orig_data=output_lines[-1] - orig_line=orig_data[0] - output_lines.pop() + 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 + output_handled=True + + 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: output_lines.append((orig_line+line,is_stderr,do_subst_operation, foreground_pid)) - else: 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) + else: + output_lines.append(unfinished_output) + output_lines.append((line,is_stderr,do_subst_operation, foreground_pid)) + unfinished_output=None + # if last line of output did not end with newlines, leave for next iteration + elif x==len(lines)-1 and not line.endswith(newlines): + unfinished_output=(line,is_stderr,do_subst_operation, foreground_pid) + else: + 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 handle_output is called, 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 + + thread=threading.Thread(target=output_read_loop, daemon=True) + thread.start() + while True: + try: if process.poll()!=None and len(output_lines)==0: break # Process outputs @@ -217,15 +237,13 @@ 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 futures=[] - for x in range(len(output_lines)): - line_data=output_lines[x] + if len(output_lines)==0: + handle_debug_pgrp(os.tcgetpgrp(stdout_fd)) + while not len(output_lines)==0: + line_data=output_lines.pop(0) line: bytes=line_data[0] - # if does not end with newlines, leave it for the next iteration - if x==len(output_lines)-1 and not line.endswith(newlines): - if not len(line_data)>=5: # not from previous iteration - output_lines=[line_data+(True,)] # add another entry to signal it's from previous iteration - break # 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 @@ -237,7 +255,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) if db_interface.enable_multiprocessing: futures.append(executor.submit(process_line, line, line_data)) # print output else: os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) - else: output_lines=[] # happens when no 'break' statement occurs # Print outputs (if enable_multiprocessing) for thread in futures: os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), thread.result()) -- Gitee From f4f75dc335294c24fc7d288c447c98f652d63dac Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Mon, 15 Jul 2024 11:55:02 +0800 Subject: [PATCH 346/354] Remove all multiprocessing implementation code This implementation is not needed anymore and does not work well with future changes, so it is being removed for good. --- src/clitheme/_generator/db_interface.py | 101 +--------------------- src/clitheme/exec/output_handler_posix.py | 34 ++------ 2 files changed, 10 insertions(+), 125 deletions(-) diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 67f7e37..26fb2cd 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -14,10 +14,6 @@ import sqlite3 import re import copy import uuid -import time -import signal -import multiprocessing, concurrent.futures -import queue from typing import Optional from .. import _globalvar, frontend @@ -28,8 +24,6 @@ db_path="" debug_mode=False _globalvar.handle_set_themedef(frontend, "db_interface") fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") -try: multiprocessing.set_start_method('fork', force=True) -except: pass class need_db_regenerate(Exception): pass @@ -193,105 +187,12 @@ 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)") - global enable_multiprocessing, match_timeout - if enable_multiprocessing: - global _running_processes_ids - if len(_running_processes_ids)==0: - _init_process() - result_id=uuid.uuid4() - global _input_values; _input_values.put((matches, content_str, is_stderr, result_id, pids, command)) - counter=0 - watchdog_timer=0 - while counter=1.500: - _init_process() - _input_values.put((matches, content_str, is_stderr, result_id, pids, command)) - watchdog_timer=0 - else: # executed when no "break" happens - try: del _return_values[result_id] - except: pass - _init_process() # restart the process - raise TimeoutError("match operation timeout") - else: - content_str=_handle_subst(matches, content_str, is_stderr, pids, command) + content_str=_handle_subst(matches, content_str, is_stderr, pids, command) return content_str -# --The following implementation (A) is for setting a timeout capacity on content match functions-- - # - A main loop is started for handling substitution requests and returns the corresponding content based on UUID - # - If the main loop times out due to catastrophic backtracking or other issues, match_content terminates the loop - # - The main loop is checked and restored (if needed) every time match_content is called, while preserving input queue and return values (resumes seamlessly) - # ** May impact performance and is currently prone to random hangs, especially under Linux ** -# --An alternative implementation (B) is available in output_handler_posix, but multithreading can't be used and doesn't work under Windows-- - -# Flag to determine whether implementation A is used -# If False, implementation B is used in output_handler_posix under Unix/Linux -enable_multiprocessing=False # timeout value for each match operation match_timeout=_globalvar.output_subst_timeout -_manager=multiprocessing.Manager() if enable_multiprocessing else None -_process: Optional[multiprocessing.Process]=None -_input_values=multiprocessing.Queue() # (matches, content, is_stderr, uuid) -_return_values=_manager.dict() if _manager else {} # uuid : content_str (uuid:None means processing) - -_watchdog_process: Optional[multiprocessing.Process]=None -_running_processes_ids=_manager.list() if _manager else [] - -def _init_process(): - global _process, _input_values, _return_values, _watchdog_process, _running_processes_ids - # if _process!=None and _process.is_alive(): _process.terminate() - if _watchdog_process==None: - _watchdog_process=multiprocessing.Process(name="process_watchdog", target=__process_watchdog, args=(_running_processes_ids,), daemon=True) - _watchdog_process.start() - _process=multiprocessing.Process(name="subst_content_handler", target=__process_main_loop, args=(_input_values, _return_values, _running_processes_ids), daemon=True) - try: _process.start() - except AssertionError: _init_process();return # handle "cannot start a process twice" error by trying again - # _running_processes_ids.append(_process.pid) -# watchdog to terminate any processes other than the current one -def __process_watchdog(running_ids): - while True: - try: - time.sleep(0.01) - # kill all processes except the most recently started one (last in list) - l=running_ids[:-1] - for pid in l: - try: - os.kill(pid, signal.SIGTERM) - running_ids.remove(pid) - except: pass - except KeyboardInterrupt: pass -def __process_main_loop(input_vals: multiprocessing.Queue, return_vals: dict, process_ids: list): - process_ids.append(os.getpid()) - executor=concurrent.futures.ThreadPoolExecutor(max_workers=32) - def handler(): - nonlocal return_vals, input_vals - # the function might be called extra times if operation is queued, so a check is performed - content: tuple - try: content=input_vals.get_nowait() - except queue.Empty: return - return_vals[content[3]]=None # Processing - return_str=_handle_subst(content[0], content[1], content[2], content[4], content[5]) - return_vals[content[3]]=return_str - while True: - try: time.sleep(0.001) - except KeyboardInterrupt: pass - try: - executor.submit(handler) - # handler() - except KeyboardInterrupt: pass - # except: break - 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() diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index fca372c..379ad03 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -22,13 +22,12 @@ import copy import re import sqlite3 import time -import concurrent.futures import threading from .._generator import db_interface from .. import _globalvar, frontend from . import _labeled_print -# spell-checker:ignore cbreak ICANON readsize splitarray ttyname RDWR preexec +# 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") @@ -120,7 +119,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) 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 - executor=concurrent.futures.ThreadPoolExecutor() last_tcgetpgrp=os.tcgetpgrp(stdout_fd) def handle_debug_pgrp(foreground_pid: int): @@ -221,24 +219,15 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) except TimeoutError: failed=True # Happens when no theme is set/no subst-data.db except sqlite3.OperationalError: pass - if db_interface.enable_multiprocessing: - # First implementation (A): use the separate process in db_interface - # No additional actions required - operation() - else: - # Alternative implementation (B): use signal handlers to force exception in execution when catastrophic backtracking happens (timeout) - # --Multithreading cannot be used in implementation B-- - # This means that only one line is processed at the same time; not ideal if multiple output lines are experiencing catastrophic backtracking - 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) - operation() - # remove the interval timer to prevent exception when function finishes before timeout - signal.setitimer(signal.ITIMER_REAL, 0) + 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) + operation() + # remove the interval timer to prevent exception when function finishes before timeout + 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 - futures=[] if len(output_lines)==0: handle_debug_pgrp(os.tcgetpgrp(stdout_fd)) while not len(output_lines)==0: @@ -251,13 +240,8 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) 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 - if db_interface.enable_multiprocessing: futures.append(executor.submit(process_line, line, line_data)) - # print output - else: os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), process_line(line, line_data)) - # Print outputs (if enable_multiprocessing) - for thread in futures: - os.write(sys.stderr.fileno() if line_data[1]==True else sys.stdout.fileno(), thread.result()) + # 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: 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 -- Gitee From a41e1fecfdad3644f7c6ef6886512a54c74abc41 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Tue, 16 Jul 2024 11:19:02 +0800 Subject: [PATCH 347/354] Update version (v2.0-dev20240716) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index db43c85..6458b2b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240710 +pkgver=2.0_dev20240716 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 2d5e07a..483a57d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240710-1) unstable; urgency=low +clitheme (2.0-dev20240716-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Wed, 10 Jul 2024 22:29:00 +0800 + -- swiftycode <3291929745@qq.com> Tue, 16 Jul 2024 11:18:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 96a896c..493c99a 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-dev20240710" +__version__="2.0-dev20240716" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240710" +version_main="2.0_dev20240716" version_buildnumber=1 \ No newline at end of file -- Gitee From a8365285ddf479fe3e830af2083369d6a56df6e9 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Wed, 17 Jul 2024 13:18:42 +0800 Subject: [PATCH 348/354] Fix unfinished output handling when program exits Fix an issue where unfinished output might not get processed and added to `output_lines` array just before the program exits --- src/clitheme/exec/output_handler_posix.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 379ad03..8378a6b 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -163,7 +163,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) output_handled=False def handle_output(is_stderr: bool): nonlocal unfinished_output, output_lines, output_handled - output_handled=True data=os.read(stderr_fd if is_stderr else stdout_fd, readsize) foreground_pid=os.tcgetpgrp(stdout_fd) @@ -181,15 +180,17 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) output_lines.append(unfinished_output) output_lines.append((line,is_stderr,do_subst_operation, foreground_pid)) 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): 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)) if stdout_fd in fds: handle_output(is_stderr=False) if stderr_fd in fds: handle_output(is_stderr=True) - # if no handle_output is called, append the unfinished output if exists + # 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 -- Gitee From 3e4de5089d0bb279b5e83f6c1e3a4606a210ca0c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 18 Jul 2024 00:13:44 +0800 Subject: [PATCH 349/354] Update version (v2.0-dev20240717) --- PKGBUILD | 2 +- debian/changelog | 4 ++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 6458b2b..f583f32 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240716 +pkgver=2.0_dev20240717 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 483a57d..2fdd5ad 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -clitheme (2.0-dev20240716-1) unstable; urgency=low +clitheme (2.0-dev20240717-1) unstable; urgency=low * In development, please see commit logs for changes - -- swiftycode <3291929745@qq.com> Tue, 16 Jul 2024 11:18:00 +0800 + -- swiftycode <3291929745@qq.com> Thu, 18 Jul 2024 00:12:00 +0800 clitheme (2.0-beta1-2) unstable; urgency=low diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 493c99a..6c5bf57 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-dev20240716" +__version__="2.0-dev20240717" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240716" +version_main="2.0_dev20240717" version_buildnumber=1 \ No newline at end of file -- Gitee From 572e20342d5ffe65c52e1ee57680ecf54879635c Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Thu, 18 Jul 2024 21:35:30 +0800 Subject: [PATCH 350/354] Update terminal attributes on main loop instead Causes less problems and is more accurate with the delay caused by subst operation --- src/clitheme/exec/output_handler_posix.py | 34 +++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 8378a6b..90e5068 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -132,23 +132,6 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) nonlocal last_terminal_size, last_input_content, output_lines unfinished_output=None while True: - # 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.stdin, 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 - readsize=io.DEFAULT_BUFFER_SIZE fds=select.select([stdout_fd, sys.stdin, stderr_fd], [], [], 0.002)[0] # Handle user input from stdin @@ -202,6 +185,23 @@ def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True) while True: try: if process.poll()!=None and len(output_lines)==0: break + + # 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.stdin, 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): -- Gitee From 41022a1b8ac83d9fb14ac3ecf023a69714ad1834 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Fri, 19 Jul 2024 20:56:30 +0800 Subject: [PATCH 351/354] Remove type subscripts in type specification Make the code compatible with Python 3.8 --- src/clitheme/_generator/_dataclass.py | 14 +++++++------- src/clitheme/_generator/_handlers.py | 4 ++-- src/clitheme/_generator/_manpage_parser.py | 4 ++-- src/clitheme/_generator/_substrules_parser.py | 2 +- src/clitheme/_generator/db_interface.py | 10 +++++----- src/clitheme/_globalvar.py | 2 +- src/clitheme/cli.py | 14 +++++++------- src/clitheme/exec/__init__.py | 2 +- src/clitheme/exec/output_handler_posix.py | 4 ++-- src/clitheme/man.py | 2 +- 10 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/clitheme/_generator/_dataclass.py b/src/clitheme/_generator/_dataclass.py index e208e2b..7c939d1 100644 --- a/src/clitheme/_generator/_dataclass.py +++ b/src/clitheme/_generator/_dataclass.py @@ -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[str], count: int): + def check_enough_args(self, phrases: list, 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[str], merge_global_options: int, allowed_options: Optional[list]=None) -> dict[str, Union[int, bool]]: + def parse_options(self, options_data: list, merge_global_options: int, allowed_options: Optional[list]=None) -> dict: # 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) @@ -115,7 +115,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[str], really_really_global: bool=False): + def handle_set_global_options(self, options_data: list, 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) @@ -255,16 +255,16 @@ 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[str], is_regex: bool, strictness: int, foreground_only: bool} + # substrules_options: {effective_commands: list, is_regex: bool, strictness: int, foreground_only: bool} 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[tuple]=[(entry_name, uuid.uuid4(), self.lineindex+1)] + 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[tuple]=[] + entries: list=[] substrules_endmatchhere=False substrules_stdout_stderr_option=0 diff --git a/src/clitheme/_generator/_handlers.py b/src/clitheme/_generator/_handlers.py index ae1ed18..3412fd4 100644 --- a/src/clitheme/_generator/_handlers.py +++ b/src/clitheme/_generator/_handlers.py @@ -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[str], line_number_debug: int, header_name_debug: str): + def write_infofile_newlines(self, path: str, filename: str, content_phrases: list, 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[str], content: str, line_number_debug: int, custom_parent_path: Optional[str]=None): + def write_manpage_file(self, file_path: list, 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 422da7f..85e802c 100644 --- a/src/clitheme/_generator/_manpage_parser.py +++ b/src/clitheme/_generator/_manpage_parser.py @@ -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]) -> str: + def get_file_content(filepath: list) -> 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[str]) -> list[str]: + def handle(p: list) -> list: 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/_substrules_parser.py b/src/clitheme/_generator/_substrules_parser.py index 94e3f91..c5ce821 100644 --- a/src/clitheme/_generator/_substrules_parser.py +++ b/src/clitheme/_generator/_substrules_parser.py @@ -18,7 +18,7 @@ from . import _dataclass def handle_substrules_section(obj: _dataclass.GeneratorObject, first_phrase: str): obj.handle_begin_section("substrules") end_phrase=r"{/substrules_section}" - command_filters: Optional[list[str]]=None + command_filters: Optional[list]=None command_filter_strictness=0 command_filter_foreground_only=False # initialize the database diff --git a/src/clitheme/_generator/db_interface.py b/src/clitheme/_generator/db_interface.py index 26fb2cd..6306262 100644 --- a/src/clitheme/_generator/db_interface.py +++ b/src/clitheme/_generator/db_interface.py @@ -66,9 +66,9 @@ 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[str]], 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), line_number_debug: str="-1"): if unique_id==uuid.UUID(int=0): unique_id=uuid.uuid4() - cmdlist: list[str]=[] + cmdlist: list=[] 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") @@ -101,7 +101,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[str]: + def process_smartcmdmatch_phrases(match_cmd: str) -> list: match_cmd_phrases=[] for p in range(len(match_cmd.split())): ph=match_cmd.split()[p] @@ -127,7 +127,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[int, int]=(-1,-1)) -> bytes: +def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False, pids: tuple=(-1,-1)) -> bytes: # pids: (main_pid, current_tcpgrp) # Match order: @@ -193,7 +193,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[tuple], content: bytes, is_stderr: bool, pids: tuple[int, int], target_command: Optional[str]): +def _handle_subst(matches: list, content: bytes, is_stderr: bool, pids: tuple, 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 68356de..b99c5e5 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -142,7 +142,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[str]: +def get_locale(debug_mode: bool=False) -> list: lang=[] def add_language(target_lang: str): nonlocal lang diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index b64d8fe..3a7fe8b 100644 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -28,9 +28,9 @@ frontend.global_subsections="cli" _globalvar.handle_set_themedef(frontend, "cli") last_data_path="" -def apply_theme(file_contents: list[str], filenames: list[str], overlay: bool, preserve_temp=False, generate_only=False): +def apply_theme(file_contents: list, filenames: list, overlay: bool, preserve_temp=False, generate_only=False): """ - Apply the theme using the provided definition file contents and file pathnames in a list[str] object. + Apply the theme using the provided definition file contents and file pathnames in a list object. (Invokes 'clitheme apply-theme') @@ -246,8 +246,8 @@ def update_theme(): (Invokes 'clitheme update-theme') """ class invalid_theme(Exception): pass - file_contents: list[str] - file_paths: list[str] + file_contents: list + file_paths: list fi=frontend.FetchDescriptor(subsections="cli update-theme") try: search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname @@ -317,7 +317,7 @@ def _handle_help_message(full_help: bool=False): print("\t"+fd.reof("options-version", "--version: Outputs the current version of clitheme")) print("\t"+fd.reof("options-help", "--help: Display this help message")) -def _get_file_contents(file_paths: list[str]) -> list[str]: +def _get_file_contents(file_paths: list) -> list: fi=frontend.FetchDescriptor(subsections="cli apply-theme") content_list=[] for i in range(len(file_paths)): @@ -330,7 +330,7 @@ def _get_file_contents(file_paths: list[str]) -> list[str]: raise return content_list -def main(cli_args: list[str]): +def main(cli_args: list): """ Use this function invoke 'clitheme' with command line arguments @@ -366,7 +366,7 @@ def main(cli_args: list[str]): else: paths.append(arg) fi=frontend.FetchDescriptor(subsections="cli apply-theme") - content_list: list[str] + content_list: list try: content_list=_get_file_contents(paths) except: _globalvar.handle_exception() diff --git a/src/clitheme/exec/__init__.py b/src/clitheme/exec/__init__.py index c5c93ee..0da7ec3 100644 --- a/src/clitheme/exec/__init__.py +++ b/src/clitheme/exec/__init__.py @@ -99,7 +99,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[str]): +def main(arguments: list): """ 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 90e5068..3892f4a 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -34,7 +34,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[bytes], debug_mode: list[str], is_stderr: bool=False, matched: bool=False, failed: bool=False) -> list[bytes]: +def _process_debug(lines: list, debug_mode: list, is_stderr: bool=False, matched: bool=False, failed: bool=False) -> list: final_lines=[] for x in range(len(lines)): line=lines[x] @@ -61,7 +61,7 @@ def _process_debug(lines: list[bytes], debug_mode: list[str], is_stderr: bool=Fa final_lines.append(line) return final_lines -def handler_main(command: list[str], debug_mode: list[str]=[], subst: bool=True): +def handler_main(command: list, debug_mode: list=[], 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 b781075..2a19892 100644 --- a/src/clitheme/man.py +++ b/src/clitheme/man.py @@ -25,7 +25,7 @@ frontend.global_domain="swiftycode" frontend.global_appname="clitheme" fd=frontend.FetchDescriptor(subsections="man") -def main(args: list[str]): +def main(args: list): """ Invoke clitheme-man using the given command line arguments -- Gitee From 5c6c83e6e4325d881cf497a1b9287d36c43cde4d Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Jul 2024 11:58:23 +0800 Subject: [PATCH 352/354] Change Python version requirement to `3.8` --- PKGBUILD | 2 +- debian/control | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index f583f32..f69630b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,7 +6,7 @@ pkgdesc="A text theming library for command line applications" arch=('any') url="https://gitee.com/swiftycode/clitheme" license=('GPL3') -depends=('python>=3.7' 'sqlite>=3' 'man-db') +depends=('python>=3.8' 'sqlite>=3' 'man-db') makedepends=('git' 'python-setuptools' 'python-build' 'python-installer' 'python-wheel' 'gzip') checkdepends=() optdepends=() diff --git a/debian/control b/debian/control index 5ae09c6..040754b 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ Vcs-Git: https://gitee.com/swiftycode/clitheme.git Package: clitheme Architecture: all Multi-Arch: foreign -Depends: ${misc:Depends}, python3 (> 3.7), man-db, sqlite3 +Depends: ${misc:Depends}, python3 (> 3.8), man-db, sqlite3 Description: Application framework for text theming clitheme allows users to customize the output of supported programs, such as multi-language support or mimicking your favorite cartoon character. It has an diff --git a/pyproject.toml b/pyproject.toml index d8d03d0..d39afd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ authors = [ description = "A text theming library for command line applications" readme = "README.en.md" license = {text = "GNU General Public License v3 (GPLv3)"} -requires-python = ">=3.7" +requires-python = ">=3.8" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", -- Gitee From 936f9c2edbe63874a84c031e74d93859ef1249f4 Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Jul 2024 21:14:33 +0800 Subject: [PATCH 353/354] Handle `termios.error` properly when setting `prev_attrs` Don't cause the program to raise an exception --- src/clitheme/exec/output_handler_posix.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/clitheme/exec/output_handler_posix.py b/src/clitheme/exec/output_handler_posix.py index 3892f4a..08fe8cc 100644 --- a/src/clitheme/exec/output_handler_posix.py +++ b/src/clitheme/exec/output_handler_posix.py @@ -72,7 +72,9 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): env=copy.copy(os.environ) # Prevent apps from using "less" or "more" as pager, as it won't work here env['PAGER']="cat" - prev_attrs=termios.tcgetattr(sys.stdin) + prev_attrs=None + try: prev_attrs=termios.tcgetattr(sys.stdin) + except termios.error: pass main_pid=os.getpid() process: subprocess.Popen # Redirect stderr to stdout for now (BETA) @@ -129,7 +131,7 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): last_tcgetpgrp=foreground_pid def output_read_loop(): - nonlocal last_terminal_size, last_input_content, output_lines + nonlocal last_input_content, output_lines unfinished_output=None while True: readsize=io.DEFAULT_BUFFER_SIZE @@ -244,11 +246,11 @@ def handler_main(command: list, debug_mode: list=[], subst: bool=True): # 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: - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes + 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 - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, prev_attrs) # restore previous attributes + if prev_attrs!=None: termios.tcsetattr(sys.stdin, 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 c2ae1307d45c0cac6e510e9f3fe6ded6724380ec Mon Sep 17 00:00:00 2001 From: swiftycode <10752639+swiftycode@user.noreply.gitee.com> Date: Sat, 20 Jul 2024 22:02:10 +0800 Subject: [PATCH 354/354] Update version (v2.0-beta2) --- PKGBUILD | 2 +- debian/changelog | 32 ++++++++++++++++++++++++++++++-- src/clitheme/_version.py | 4 ++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index f69630b..570264f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: swiftycode <3291929745@qq.com> pkgname='clitheme' -pkgver=2.0_dev20240717 +pkgver=2.0_beta2 pkgrel=1 pkgdesc="A text theming library for command line applications" arch=('any') diff --git a/debian/changelog b/debian/changelog index 2fdd5ad..90b611f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,34 @@ -clitheme (2.0-dev20240717-1) unstable; urgency=low +clitheme (2.0-beta2-1) unstable; urgency=low - * In development, please see commit logs for changes + 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: + * 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`) + * Support specifying `substesc` and `substvar` options on `[/substitute_string]` and `[/substitute_regex]` in `{substrules_section}`; affects match expression + * Support specifying `substvar` on `[/entry]` in `{entries_section}`; affects path name + + Bug fixes and improvements + + * Non-printable characters in CLI output messages will be displayed in its plain-text representation + * Fix compatibility issues with Python 3.8 + * Fixes an issue where pressing CTRL-C in `clitheme-man` causes the program to unexpectedly terminate + * Fixes an issue where terminal control characters are unexpectedly displayed on screen in Windows Command Prompt + * Theme definition file `{substrules_section}`: Fixes and issue where substitution rules with `strictcmdmatch` option are not applied if executed command arguments are the same in command filter + * Fixes an issue where "specifying multiple `[entry]` phrases" feature in `{entries_section}` does not work properly + * Fix many compatibility issues with applications in `clitheme-exec` + * Theme definition file: Optimize processing of command filter definitions with same commands and different match options + * Theme definition file: Fix unexpected "Option not allowed here" error when using multi-line content blocks + * Theme definition file: Fix and optimize content variable processing + * Theme definition file: Fix missing "Unexpected phrase" error in `{manpage_section}` when encountering invalid file syntax + + Latest code changes and development version: https://gitee.com/swiftycode/clitheme/tree/v1.2_dev + + Documentation: https://gitee.com/swiftycode/clitheme-wiki-repo + + Full release notes for v2.0: [please see v2.0-beta1 release page] -- swiftycode <3291929745@qq.com> Thu, 18 Jul 2024 00:12:00 +0800 diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index 6c5bf57..80fc14d 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-dev20240717" +__version__="2.0-beta2" major=2 minor=0 release=-1 # -1 stands for "dev" # For PKGBUILD # version_main CANNOT contain hyphens (-); use underscores (_) instead -version_main="2.0_dev20240717" +version_main="2.0_beta2" version_buildnumber=1 \ No newline at end of file -- Gitee