├── .github ├── README-frontend.en.md └── README.md ├── .gitignore ├── LICENSE ├── PKGBUILD ├── README-frontend.md ├── README.md ├── cspell.json ├── debian ├── changelog ├── clitheme.manpages ├── clitheme.substvars ├── control ├── copyright ├── debhelper-build-stamp ├── rules ├── source │ └── format └── upstream │ └── metadata ├── demo-clithemedef ├── README.zh-CN.md └── demo-theme-textemojis.clithemedef.txt ├── docs ├── clitheme-exec.1 ├── clitheme-man.1 ├── clitheme.1 └── docs.clithemedef.txt ├── frontend_demo.py ├── frontend_fallback.py ├── pyproject.toml └── src ├── clitheme-testblock_testprogram.py ├── clitheme ├── __init__.py ├── __main__.py ├── _generator │ ├── __init__.py │ ├── _data_handlers.py │ ├── _entries_parser.py │ ├── _entry_block_handler.py │ ├── _header_parser.py │ ├── _manpage_parser.py │ ├── _parser_handlers.py │ ├── _substrules_parser.py │ └── db_interface.py ├── _get_resource.py ├── _globalvar.py ├── _version.py ├── cli.py ├── exec │ ├── __init__.py │ ├── __main__.py │ └── output_handler_posix.py ├── frontend.py ├── man.py └── strings │ ├── cli-strings.clithemedef.txt │ ├── exec-strings.clithemedef.txt │ ├── generator-strings.clithemedef.txt │ └── man-strings.clithemedef.txt ├── clithemedef-test_testprogram.py ├── db_interface_tests.py └── testprogram-data ├── clithemedef-test_expected-frontend.txt ├── clithemedef-test_expected.txt └── clithemedef-test_mainfile.clithemedef.txt /.github/README-frontend.en.md: -------------------------------------------------------------------------------- 1 | # Frontend API and string entries demo 2 | 3 | ## Data hierarchy and path naming 4 | 5 | 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. 6 | 7 | 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`. 8 | 9 | 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. 10 | 11 | ### Directly accessing the theme data hierarchy 12 | 13 | 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. 14 | 15 | 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. 16 | 17 | 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. 18 | 19 | Under Windows systems, `` is `%USERPROFILE%\.local\share\clitheme\theme-data` or `C:\Users\\.local\share\clitheme\theme-data`. 20 | 21 | 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` 22 | 23 | In conclusion, to directly access a specific string definition, convert the path name to a directory path and access the file located there. 24 | 25 | ## Frontend implementation and writing theme definition files 26 | 27 | ### Using the built-in frontend module 28 | 29 | 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. 30 | 31 | 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. 32 | 33 | 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. 34 | 35 | Let's demonstrate it using the previous examples: 36 | 37 | ```py 38 | from clitheme import frontend 39 | 40 | # Create a new FetchDescriptor object 41 | f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") 42 | 43 | # Corresponds to "Found 2 files in current directory" 44 | fcount="[...]" 45 | f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) 46 | 47 | # Corresponds to "-> Installing "example-file"..." 48 | filename="[...]" 49 | f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) 50 | 51 | # Corresponds to "Successfully installed 2 files" 52 | f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) 53 | 54 | # Corresponds to "Error: File "foo-nonexist" not found" 55 | filename_err="[...]" 56 | f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) 57 | ``` 58 | 59 | ### Using the fallback frontend module 60 | 61 | 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. 62 | 63 | Import the `frontend_fallback.py` file from the repository and insert the following code in your project to use it: 64 | 65 | ```py 66 | try: 67 | from clitheme import frontend 68 | except (ModuleNotFoundError, ImportError): 69 | import frontend_fallback as frontend 70 | ``` 71 | 72 | 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. 73 | 74 | ### Information your application should provide 75 | 76 | 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. 77 | 78 | For example, your app can implement a feature to output all supported string definitions: 79 | 80 | ``` 81 | $ example-app --clitheme-output-defs 82 | com.example example-app found-file 83 | Found {} files in current directory 84 | 85 | com.example example-app installing-file 86 | -> Installing "{}"... 87 | 88 | com.example example-app install-success 89 | Successfully installed {} files 90 | 91 | com.example example-app file-not-found 92 | Error: file "{}" not found 93 | ``` 94 | 95 | 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`. 96 | 97 | ### Writing theme definition files 98 | 99 | Consult the Wiki pages and documentation for detailed syntax of theme definition files. An example is provided below: 100 | 101 | ``` 102 | {header_section} 103 | name Example theme 104 | version 1.0 105 | locales en_US 106 | supported_apps frontend_demo 107 | {/header_section} 108 | 109 | {entries_section} 110 | in_domainapp com.example example-app 111 | [entry] found-file 112 | locale:default o(≧v≦)o Great! Found {} files in current directory! 113 | locale:en_US o(≧v≦)o Great! Found {} files in current directory! 114 | [/entry] 115 | [entry] installing-file 116 | locale:default (>^ω^<) Installing "{}"... 117 | locale:en_US (>^ω^<) Installing "{}"... 118 | [/entry] 119 | [entry] install-success 120 | locale:default o(≧v≦)o Successfully installed {} files! 121 | locale:en_US o(≧v≦)o Successfully installed {} files! 122 | [/entry] 123 | [entry] file-not-found 124 | locale:default ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found 125 | locale:en_US ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found 126 | [/entry] 127 | {/entries_section} 128 | ``` 129 | 130 | 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. 131 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # CLItheme - Command line customization utility 2 | 3 | [中文](../README.md) | **English** 4 | 5 | **Disclaimer:** Please do not use this tool to create harmful or illegal content. The author of this software does not claim any responsibility for content and definition files created by others. 6 | 7 | --- 8 | 9 | CLItheme allows you to customize the output of command line applications, giving them the style and personality you want. 10 | 11 | Example: 12 | ```plaintext 13 | $ clang test.c 14 | test.c:1:1: error: unknown type name 'bool' 15 | bool *func(int *a) { 16 | ^ 17 | test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] 18 | b=a; 19 | ^~ 20 | 2 errors generated 21 | ``` 22 | ```plaintext 23 | $ clitheme apply-theme clang-theme.clithemedef.txt 24 | ==> Processing files... 25 | Successfully processed files 26 | ==> Applying theme... 27 | Theme applied successfully 28 | ``` 29 | ```plaintext 30 | $ clitheme-exec clang test.c 31 | test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ 32 | bool *func(int *a) { 33 | ^ 34 | test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] 35 | b=a; 36 | ^~ 37 | 2 errors generated. 38 | ``` 39 | 40 | ## Features 41 | 42 | CLItheme has these main features: 43 | 44 | - Customize and modify the output of any command line application through defining substitution rules 45 | - Customize Unix/Linux manual pages (man pages) 46 | - A frontend API for applications similar to localization toolkits (like GNU gettext), which can help users better customize output messages 47 | 48 | Other characteristics: 49 | 50 | - Multi-language/internalization support 51 | - This means that you can also use CLItheme to add internalization support for command line applications 52 | - Easy-to-understand **theme definition file** syntax 53 | - The string entries in the current theme setting can be accessed without using the frontend API (easy-to-understand data structure) 54 | 55 | For more information, please see the project's Wiki documentation page. It can be accessed through the following links: 56 | 57 | - https://gitee.com/swiftycode/clitheme/wikis 58 | - https://gitee.com/swiftycode/clitheme-wiki-repo 59 | - https://github.com/swiftycode256/clitheme-wiki-repo 60 | 61 | # Feature examples and demos 62 | 63 | ## Command line output substitution 64 | 65 | Get the command line output, including any terminal control characters: 66 | 67 | ```plaintext 68 | # --debug: Add a marker at the beginning of each line; contains information on whether the output is stdout/stderr ("o>" or "e>") 69 | # --showchars: Show terminal control characters in the output 70 | # --nosubst: Even if a theme is set, do not apply substitution rules (get original output content) 71 | 72 | $ clitheme-exec --debug --showchars --nosubst clang test.c 73 | e> {{ESC}}[1mtest.c:1:1: {{ESC}}[0m{{ESC}}[0;1;31merror: {{ESC}}[0m{{ESC}}[1munknown type name 'bool'{{ESC}}[0m\r\n 74 | e> bool *func(int *a) {\r\n 75 | e> {{ESC}}[0;1;32m^\r\n 76 | 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 77 | e> b=a;\r\n 78 | e> {{ESC}}[0;1;32m ^~\r\n 79 | e> {{ESC}}[0m2 errors generated.\r\n 80 | ``` 81 | 82 | Write theme definition file and substitution rules based on the output: 83 | 84 | ```plaintext 85 | # Define basic information for this theme in header_section; required 86 | {header_section} 87 | # `name` is a required entry in header_section 88 | name clang example theme 89 | [description] 90 | An example theme for clang (for demonstration purposes) 91 | [/description] 92 | {/header_section} 93 | 94 | {substrules_section} 95 | # Set "substesc" option: "{{ESC}}" in content will be replaced with the ASCII Escape terminal control character 96 | set_options substesc 97 | # Command filter: following substitution rules will be applied only if these commands are invoked. It is recommended as it can prevent unwanted output substitutions. 98 | [filter_commands] 99 | clang 100 | clang++ 101 | gcc 102 | g++ 103 | [/filter_commands] 104 | [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)warning: (?P({{ESC}}.*?m)*)incompatible pointer types assigning to '(?P.+)' from '(?P.+)' 105 | # Use "locale:en_US" if you only want the substitution rule to applied when the system locale setting is English (en_US) 106 | # Use "locale:default" to not apply any locale filters 107 | locale:default \gnote: \gincompatible pointer types '\g' and '\g', they're so……so incompatible!~ 108 | [/subst_regex] 109 | [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)error: (?P({{ESC}}.*?m)*)unknown type name '(?P.+)' 110 | locale:default \gError! : \gunknown type name '\g', you forgot to d……define it!~ಥ_ಥ 111 | [/subst_regex] 112 | {/substrules_section} 113 | ``` 114 | 115 | After applying the theme with `clitheme apply-theme `, execute the command with `clitheme-exec` to apply the substitution rules onto the output: 116 | 117 | ```plaintext 118 | $ clitheme apply-theme clang-theme.clithemedef.txt 119 | $ clitheme-exec clang test.c 120 | test.c:1:1: Error! : unknown type name 'bool', you forgot to d……define it!~ಥ_ಥ 121 | bool *func(int *a) { 122 | ^ 123 | test.c:4:3: note: incompatible pointer types 'char *' and 'int *', they're so……so incompatible!~ [-Wincompatible-pointer-types] 124 | b=a; 125 | ^~ 126 | 2 errors generated. 127 | ``` 128 | 129 | ## Custom man pages 130 | 131 | Write/edit the source code of the man page and save it into a location: 132 | 133 | ```plaintext 134 | $ nano man-pages/1/ls-custom.txt 135 | # 136 | $ nano man-pages/1/cat-custom.txt 137 | # 138 | ``` 139 | 140 | Write a theme definition file: 141 | 142 | ```plaintext 143 | {header_section} 144 | name Example manual page theme 145 | description An example man page theme 146 | {/header_section} 147 | 148 | {manpage_section} 149 | # Add the file path *separated by spaces* after "include_file" (with the directory the theme definition file is placed as the parent directory) 150 | # Add the target file path (e.g. where the file is placed under `/usr/share/man`) *separated by spaces* after "as" 151 | include_file man-pages 1 ls-custom.txt 152 | as man1 ls.1 153 | include_file man-pages 1 cat-custom.txt 154 | as man1 cat.1 155 | {/manpage_section} 156 | ``` 157 | 158 | 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`): 159 | 160 | ```plaintext 161 | $ clitheme apply-theme manpage-theme.clithemedef.txt 162 | $ clitheme-man cat 163 | $ clitheme-man ls 164 | ``` 165 | 166 | ## Application frontend API and string entries 167 | 168 | Please see [this article](./README-frontend.en.md) 169 | 170 | # Installing and building 171 | 172 | CLItheme can be installed through pip package, Debian package, and Arch Linux package. 173 | 174 | ### Install using Python/pip package 175 | 176 | First, ensure that Python 3 is installed on the system. CLItheme requires Python 3.8 or higher. 177 | 178 | - On Linux distributions, you can use relevant package manager to install 179 | - On macOS, you can install Python through Xcode command line developer tools (use `xcode-select --install` command), or through Python website ( https://www.python.org/downloads ) 180 | - On Windows, you can install Python through Microsoft Store ([Python 3.13 link](https://apps.microsoft.com/detail/9pnrbtzxmb4z)), or through Python website ( https://www.python.org/downloads ) 181 | 182 | Then, ensure that `pip` is installed within Python. The following command will perform an offline install of `pip` if it's not detected. 183 | 184 | $ python3 -m ensurepip 185 | 186 | Download the `.whl` file from latest distribution page and install it using `pip`: 187 | 188 | $ python3 -m pip install ./clitheme--py3-none-any.whl 189 | 190 | ### Install using Arch Linux package 191 | 192 | 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. 193 | 194 | ### Install using Debian package 195 | 196 | Download the `.deb` file from the latest distribution page and install using `apt`: 197 | 198 | $ sudo apt install ./clitheme__all.deb 199 | 200 | ## Building packages 201 | 202 | 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. 203 | 204 | ### Build pip package 205 | 206 | CLItheme uses the `setuptools` build system, so it needs to be installed beforehand. 207 | 208 | First, install `setuptools`, `build`, and `wheel` packages. You can use the packages provided by your Linux distribution, or install using `pip`: 209 | 210 | $ python3 -m pip install --upgrade setuptools build wheel 211 | 212 | Then, switch to project directory and use the following command to build the package: 213 | 214 | $ python3 -m build --wheel --no-isolation 215 | 216 | The package file can be found in the `dist` folder after build finishes. 217 | 218 | ### Build Arch Linux package 219 | 220 | Ensure that the `base-devel` package is installed before building. Use the following command to install: 221 | 222 | $ sudo pacman -S base-devel 223 | 224 | Before build the package, make sure that any changes in the repository are committed (git commit): 225 | 226 | $ git add . 227 | $ git commit 228 | 229 | Execute `makepkg` to build the package. Use the following commands to perform these operations: 230 | 231 | ```sh 232 | # If makepkg is executed before, delete the temporary directories to prevent issues 233 | rm -rf buildtmp srctmp 234 | 235 | makepkg -si 236 | # -s: Automatically install required build dependencies (e.g. python-setuptools, python-build) 237 | # -i:Automatically install the built package 238 | 239 | # You can delete the temporary directories after it completes 240 | rm -rf buildtmp srctmp 241 | ``` 242 | 243 | **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 244 | 245 | ### Build Debian package 246 | 247 | Install the following packages before building: 248 | 249 | - `debhelper` 250 | - `dh-python` 251 | - `python3-setuptools` 252 | - `dpkg-dev` 253 | - `pybuild-plugin-pyproject` 254 | 255 | You can use the following command to install: 256 | 257 | sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject 258 | 259 | 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. 260 | 261 | # More information 262 | 263 | - This repository is also synced onto GitHub (using Gitee automatic sync feature): https://github.com/swiftycode256/clitheme 264 | - 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 265 | - Feel free to propose suggestions and changes using Issues and Pull Requests 266 | - Use the Wiki repositories listed above for Wiki-related suggestions 267 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Sample gitignore file to make your life easier (>^ω^<) 2 | __pycache__ 3 | __pycache__/* 4 | dist 5 | dist/* 6 | build 7 | build/* 8 | venv 9 | venv/* 10 | src/*.egg-info 11 | .vscode 12 | .vscode/* 13 | # Vim swap file 14 | **/*.sw? 15 | # excluded files (ex) 16 | debian/*.ex 17 | debian/clitheme 18 | debian/.debhelper 19 | debian/files 20 | debian/*.debhelper.log 21 | .pybuild 22 | .pybuild/* 23 | # for building Arch Linux package 24 | buildtmp 25 | buildtmp/* 26 | srctmp 27 | srctmp/* 28 | *.pkg.tar.zst 29 | .DS_Store -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: swiftycode <3291929745@qq.com> 2 | pkgname='clitheme' 3 | pkgver=2.0_beta3 4 | pkgrel=1 5 | pkgdesc="A text theming library for command line applications" 6 | arch=('any') 7 | url="https://gitee.com/swiftycode/clitheme" 8 | license=('GPL3') 9 | depends=('python>=3.8' 'sqlite>=3' 'man-db') 10 | makedepends=('git' 'python-setuptools' 'python-build' 'python-installer' 'python-wheel' 'gzip') 11 | checkdepends=() 12 | optdepends=() 13 | provides=() 14 | conflicts=($pkgname) 15 | replaces=() 16 | backup=() 17 | options=() 18 | install= 19 | changelog='debian/changelog' 20 | source=("srctmp::git+file://$PWD") # Commit any active changes before building the package! 21 | noextract=() 22 | md5sums=('SKIP') 23 | validpgpkeys=() 24 | # Make sure that it doesn't conflict with "src" directory 25 | BUILDDIR="$PWD/buildtmp" 26 | pkgver(){ 27 | cd srctmp 28 | cd src/clitheme 29 | pkgrel=$(python3 -c "from _version import version_buildnumber; print(version_buildnumber)") 30 | python3 -c "from _version import version_main; print(version_main)" 31 | } 32 | 33 | build() { 34 | cd srctmp 35 | python3 -m build --wheel --no-isolation 36 | } 37 | 38 | check() { 39 | cd srctmp 40 | echo -n "Ensuring generated wheel files exist..." 41 | test ! -f dist/*.whl && echo "Error" && return 1 42 | echo "OK" 43 | # manpage 44 | echo "Ensuring manpage files (in docs directory) exist:" 45 | echo -n "docs/clitheme.1 ..." 46 | test ! -f docs/clitheme.1 && echo "Error" && return 1 47 | echo "OK" 48 | echo -n "docs/clitheme-exec.1 ..." 49 | test ! -f docs/clitheme-exec.1 && echo "Error" && return 1 50 | echo "OK" 51 | echo -n "docs/clitheme-man.1 ..." 52 | test ! -f docs/clitheme-man.1 && echo "Error" && return 1 53 | echo "OK" 54 | } 55 | 56 | package() { 57 | cd srctmp 58 | python3 -m installer --destdir="$pkgdir" dist/*.whl 59 | # install manpage 60 | mkdir -p $pkgdir/usr/share/man/man1 61 | gzip -c docs/clitheme.1 > $pkgdir/usr/share/man/man1/clitheme.1.gz 62 | gzip -c docs/clitheme-exec.1 > $pkgdir/usr/share/man/man1/clitheme-exec.1.gz 63 | gzip -c docs/clitheme-man.1 > $pkgdir/usr/share/man/man1/clitheme-man.1.gz 64 | } 65 | -------------------------------------------------------------------------------- /README-frontend.md: -------------------------------------------------------------------------------- 1 | # 应用程序API和字符串定义示范 2 | 3 | ## 数据结构和路径名称 4 | 5 | 应用程序是主要通过**路径名称**来指定所需的字符串。这个路径由空格来区别子路径(`subsections`)。大部分时候路径的前两个名称是用来指定开发者和应用名称的。主题文件会通过该路径名称来适配对应的字符串,从而达到自定义输出的效果。 6 | 7 | 比如`com.example example-app example-text`指的是`com.example`开发的`example-app`中的`example-text`字符串。 8 | 9 | 当然,路径名称也可以是全局的(不和任何应用信息关联),如`global-entry`或`global-example global-text`。 10 | 11 | ### 直接访问主题数据结构 12 | 13 | CLItheme的核心设计理念之一包括无需使用frontend模块就可以访问主题数据,并且访问方法直观易懂。这一点在使用其他语言编写的程序中尤其重要,因为frontend模块目前只提供Python程序的支持。 14 | 15 | CLItheme的数据结构采用了**子文件夹**的结构,意味着路径中的每一段代表着数据结构中的一个文件夹/文件。 16 | 17 | 比如说,`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`。 18 | 19 | 在Windows系统下,``是`%USERPROFILE%\.local\share\clitheme\theme-data`。(`C:\Users\<用户名称>\.local\share\clitheme\theme-data`) 20 | 21 | 如果需要访问该字符串的其他语言,直接在路径的最后添加`__`加上locale名称就可以了。比如:`/com.example/example-app/example-text__zh_CN` 22 | 23 | 所以说,如果需要直接访问字符串信息,只需要访问对应的文件路径就可以了。 24 | 25 | ## 前端实施和编写主题文件 26 | 27 | ### 使用内置frontend模块 28 | 29 | 使用CLItheme的frontend模块非常简单。只需要新建一个`frontend.FetchDescriptor`实例然后调用该实例中的`retrieve_entry_or_fallback`即可。 30 | 31 | 该函数需要提供路径名称和默认字符串。如果当前主题设定没有适配该字符串,则函数会返回提供的默认字符串。 32 | 33 | 如果新建`FetchDescriptor`时提供了`domain_name`,`app-name`,或`subsections`,则调用函数时会自动把它添加到路径名称前。 34 | 35 | 我们拿上面的样例来示范: 36 | 37 | ```py 38 | from clitheme import frontend 39 | 40 | # 新建FetchDescriptor实例 41 | f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") 42 | 43 | # 对应 “在当前目录找到了2个文件” 44 | fcount="[...]" 45 | f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) 46 | 47 | # 对应 “-> 正在安装 "example-file"...” 48 | filename="[...]" 49 | f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) 50 | 51 | # 对应 “已成功安装2个文件” 52 | f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) 53 | 54 | # 对应 “错误:找不到文件 "foo-nonexist"” 55 | filename_err="[...]" 56 | f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) 57 | ``` 58 | 59 | ### 使用fallback模块 60 | 61 | 应用程序还可以在src中内置本项目提供的fallback模块,以便更好的处理CLItheme模块不存在时的情况。该fallback模块包括了frontend模块中的所有定义和功能,并且会永远返回失败时的默认值(fallback)。 62 | 63 | 如需使用,请在你的项目文件中导入`frontend_fallback.py`文件,并且在你的程序中包括以下代码: 64 | 65 | ```py 66 | try: 67 | from clitheme import frontend 68 | except (ModuleNotFoundError, ImportError): 69 | import frontend_fallback as frontend 70 | ``` 71 | 72 | 本项目提供的fallback文件会随版本更新而更改,所以请定期往你的项目里导入最新的fallback文件以适配最新的功能。 73 | 74 | ### 应用程序应该提供的信息 75 | 76 | 为了让用户更容易编写主题文件,应用程序应该加入输出字符串定义的功能。该输出信息应该包含路径名称和默认字符串。 77 | 78 | 比如说,应用程序可以通过`--clitheme-output-defs`来输出所有的字符串定义: 79 | 80 | ``` 81 | $ example-app --clitheme-output-defs 82 | com.example example-app found-file 83 | 在当前目录找到了{}个文件 84 | 85 | com.example example-app installing-file 86 | -> 正在安装"{}"... 87 | 88 | com.example example-app install-success 89 | 已成功安装{}个文件 90 | 91 | com.example example-app file-not-found 92 | 错误:找不到文件 "{}" 93 | ``` 94 | 95 | 应用程序还可以在对应的官方文档中包括此信息。如需样例,请参考本仓库中`example-clithemedef`文件夹的[README文件](example-clithemedef/README.zh-CN.md)。 96 | 97 | ### 编写主题文件 98 | 99 | 关于主题文件的详细语法请见Wiki文档,下面将展示一个样例: 100 | 101 | ``` 102 | {header_section} 103 | name 样例主题 104 | version 1.0 105 | locales zh_CN 106 | supported_apps frontend_demo 107 | {/header_section} 108 | 109 | {entries_section} 110 | in_domainapp com.example example-app 111 | [entry] found-file 112 | locale:default o(≧v≦)o 太好了,在当前目录找到了{}个文件! 113 | locale:zh_CN o(≧v≦)o 太好了,在当前目录找到了{}个文件! 114 | [/entry] 115 | [entry] installing-file 116 | locale:default (>^ω^<) 正在安装 "{}"... 117 | locale:zh_CN (>^ω^<) 正在安装 "{}"... 118 | [/entry] 119 | [entry] install-success 120 | locale:default o(≧v≦)o 已成功安装{}个文件! 121 | locale:zh_CN o(≧v≦)o 已成功安装{}个文件! 122 | [/entry] 123 | [entry] file-not-found 124 | locale:default ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" 125 | locale:zh_CN ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" 126 | [/entry] 127 | {/entries_section} 128 | ``` 129 | 130 | 编写好主题文件后,使用 `clitheme apply-theme `来应用主题。应用程序会直接采用主题中适配的字符串。 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLItheme - 命令行自定义工具 2 | 3 | **中文** | [English](.github/README.md) 4 | 5 | **免责声明:** 请不要利用该软件传播有害或违法内容。本软件作者对其他人制作的内容和定义文件不承担任何责任。 6 | 7 | --- 8 | 9 | CLItheme允许你对命令行输出进行个性化定制,给它们一个你想要的风格和个性。 10 | 11 | 样例: 12 | ```plaintext 13 | $ clang test.c 14 | test.c:1:1: error: unknown type name 'bool' 15 | bool *func(int *a) { 16 | ^ 17 | test.c:4:3: warning: incompatible pointer types assigning to 'char *' from 'int *' [-Wincompatible-pointer-types] 18 | b=a; 19 | ^~ 20 | 2 errors generated 21 | ``` 22 | ```plaintext 23 | $ clitheme apply-theme clang-theme.clithemedef.txt 24 | ==> Processing files... 25 | Successfully processed files 26 | ==> Applying theme... 27 | Theme applied successfully 28 | ``` 29 | ```plaintext 30 | $ clitheme-exec clang test.c 31 | test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ 32 | bool *func(int *a) { 33 | ^ 34 | test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] 35 | b=a; 36 | ^~ 37 | 2 errors generated. 38 | ``` 39 | 40 | ## 功能 41 | 42 | CLItheme包含以下主要功能: 43 | 44 | - 对任何命令行应用程序的输出通过定义替换规则进行修改和自定义 45 | - 自定义Unix/Linux文档手册(manpage) 46 | - 包含类似于本地化套件(如GNU gettext)的应用程序API,帮助用户更好的自定义提示信息的内容 47 | 48 | 其他特性: 49 | 50 | - 多语言支持 51 | - 这意味着你也可以用CLItheme来为应用程序添加多语言支持 52 | - 简洁易懂的**主题定义文件**语法 53 | - 无需应用程序API也可以访问当前主题中的字符串定义(易懂的数据结构) 54 | 55 | 更多信息请见本项目的Wiki文档页面。你可以通过以下位置访问这些文档: 56 | - https://gitee.com/swiftycode/clitheme/wikis 57 | - https://gitee.com/swiftycode/clitheme-wiki-repo 58 | - https://github.com/swiftycode256/clitheme-wiki-repo 59 | 60 | # 功能样例和示范 61 | 62 | ## 命令行输出自定义 63 | 64 | 获取包含终端控制符号的原始输出内容: 65 | 66 | ```plaintext 67 | # --debug:在每一行的输出前添加标记;包含输出是否为stdout或stderr的信息("o>"或"e>") 68 | # --showchars:显示输出中的终端控制符号 69 | # --nosubst:即使设定了主题,不对输出应用替换规则(获取原始输出) 70 | 71 | $ clitheme-exec --debug --showchars --nosubst clang test.c 72 | e> {{ESC}}[1mtest.c:1:1: {{ESC}}[0m{{ESC}}[0;1;31merror: {{ESC}}[0m{{ESC}}[1munknown type name 'bool'{{ESC}}[0m\r\n 73 | e> bool *func(int *a) {\r\n 74 | e> {{ESC}}[0;1;32m^\r\n 75 | 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 76 | e> b=a;\r\n 77 | e> {{ESC}}[0;1;32m ^~\r\n 78 | e> {{ESC}}[0m2 errors generated.\r\n 79 | ``` 80 | 81 | 根据输出内容编写主题定义文件和替换规则: 82 | 83 | ```plaintext 84 | # 在header_section中定义一些关于该主题定义的基本信息;必须包括 85 | {header_section} 86 | # 在header_section中必须定义`name`条目 87 | name clang样例主题 88 | [description] 89 | 一个为clang打造的的样例主题,为了演示作用 90 | [/description] 91 | {/header_section} 92 | 93 | {substrules_section} 94 | # 设定"substesc"选项:内容中的"{{ESC}}"字样会被替换成ASCII Escape终端控制符号 95 | set_options substesc 96 | # 命令限制条件:以下的替换规则仅会在以下命令被调用时被应用。建议设定这个条件,因为可以尽量防止不应该的输出替换。 97 | [filter_commands] 98 | clang 99 | clang++ 100 | gcc 101 | g++ 102 | [/filter_commands] 103 | [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)warning: (?P({{ESC}}.*?m)*)incompatible pointer types assigning to '(?P.+)' from '(?P.+)' 104 | # 如果你想仅在系统语言设定为中文(zh_CN)时应用这个替换规则,你可以使用"locale:zh_CN" 105 | # 使用"locale:default"时不会添加系统语言限制 106 | locale:default \g提示: \g'\g'从不兼容的指针类型赋值为'\g',两者怎么都……都说不过去!^^; 107 | [/subst_regex] 108 | [subst_regex] (?P^({{ESC}}.*?m)*(.+:\d+:\d+:) ({{ESC}}.*?m)*)error: (?P({{ESC}}.*?m)*)unknown type name '(?P.+)' 109 | locale:default \g错误!: \g未知的类型名'\g',忘记定义了~ಥ_ಥ 110 | [/subst_regex] 111 | {/substrules_section} 112 | ``` 113 | 114 | 使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-exec`执行命令以对输出应用这些替换规则: 115 | 116 | ```plaintext 117 | $ clitheme apply-theme clang-theme.clithemedef.txt 118 | $ clitheme-exec clang test.c 119 | test.c:1:1: 错误!: 未知的类型名'bool',忘记定义了~ಥ_ಥ 120 | bool *func(int *a) { 121 | ^ 122 | test.c:4:3: 提示: 'char *'从不兼容的指针类型赋值为'int *',两者怎么都……都说不过去!^^; [-Wincompatible-pointer-types] 123 | b=a; 124 | ^~ 125 | 2 errors generated. 126 | ``` 127 | 128 | ## 自定义manpage文档 129 | 130 | 编写/编辑manpage文档的源代码,并且保存在一个位置中: 131 | 132 | ```plaintext 133 | $ nano man-pages/1/ls-custom.txt 134 | # <编辑文件> 135 | $ nano man-pages/1/cat-custom.txt 136 | # <编辑文件> 137 | ``` 138 | 139 | 编写主题定义文件: 140 | 141 | ```plaintext 142 | {header_section} 143 | name 样例文档手册主题 144 | description 一个manpage文档手册样例主题 145 | {/header_section} 146 | 147 | {manpage_section} 148 | # 在"include_file"后添加*由空格分开*的文件路径(以主题定义文件所在的路径为父路径) 149 | # 在"as"后添加*由空格分开*的目标文件路径(放在如`/usr/share/man`文件夹下的文件路径) 150 | include_file man-pages 1 ls-custom.txt 151 | as man1 ls.1 152 | include_file man-pages 1 cat-custom.txt 153 | as man1 cat.1 154 | {/manpage_section} 155 | ``` 156 | 157 | 使用`clitheme apply-theme <文件>`应用主题后,使用`clitheme-man`查看这些自定义文档(使用方法和选项和`man`一样): 158 | 159 | ```plaintext 160 | $ clitheme apply-theme manpage-theme.clithemedef.txt 161 | $ clitheme-man cat 162 | $ clitheme-man ls 163 | ``` 164 | 165 | ## 应用程序API和字符串定义 166 | 167 | 请见[此文档](./README-frontend.md) 168 | 169 | # 安装与构建 170 | 171 | 安装CLItheme非常简单,您可以通过pip软件包,Arch Linux软件包,或者Debian软件包安装。 172 | 173 | ### 通过Python/pip软件包安装 174 | 175 | 首先,确保Python 3已安装在系统中。CLItheme需要Python 3.8或更高版本。 176 | 177 | - 在Linux发行版上,你可以通过对应的软件包管理器安装Python 178 | - 在macOS上,你可以通过Xcode命令行开发者工具安装Python(使用`xcode-select --install`命令),或者通过Python官网( https://www.python.org/downloads )下载 179 | - 在Windows上,你可以通过Microsoft Store安装Python([Python 3.13链接](https://apps.microsoft.com/detail/9pnrbtzxmb4z)),或者通过Python官网( https://www.python.org/downloads )下载 180 | 181 | 然后,确保`pip`软件包管理器已安装在Python中。以下命令将会通过本地安装`pip`,如果检测到没有安装。 182 | 183 | $ python3 -m ensurepip 184 | 185 | 从最新发行版页面下载`.whl`文件,使用`pip`直接安装即可: 186 | 187 | $ python3 -m pip install ./clitheme--py3-none-any.whl 188 | 189 | ### 通过Arch Linux软件包安装 190 | 191 | 因为构建的Arch Linux软件包只兼容特定的Python版本,并且升级Python版本后会导致原软件包失效,本项目仅提供构建软件包的方式,不提供构建好的软件包。详细请见下方的**构建Arch Linux软件包**。 192 | 193 | ### 通过Debian软件包安装 194 | 195 | 如需在Debian系统上安装,请从最新发行版页面下载`.deb`文件,使用`apt`安装即可: 196 | 197 | $ sudo apt install ./clitheme__all.deb 198 | 199 | ## 构建安装包 200 | 201 | 你也可以从仓库源代码构建安装包,以包括最新或自定义更改。如果需要安装最新的开发版本,则需要通过此方法安装。 202 | 203 | ### 构建pip软件包 204 | 205 | CLItheme使用的是`setuptools`构建器,所以构建软件包前需要安装它。 206 | 207 | 首先,安装`setuptools`、`build`、和`wheel`软件包。你可以通过你使用的Linux发行版提供的软件包,或者使用以下命令通过`pip`安装: 208 | 209 | $ python3 -m pip install --upgrade setuptools build wheel 210 | 211 | 然后,切换到项目目录,使用以下命令构建软件包: 212 | 213 | $ python3 -m build --wheel --no-isolation 214 | 215 | 构建完成后,相应的安装包文件可以在当前目录中的`dist`文件夹中找到。 216 | 217 | ### 构建Arch Linux软件包 218 | 219 | 构建Arch Linux软件包前,请确保`base-devel`软件包已安装。如需安装,请使用以下命令: 220 | 221 | $ sudo pacman -S base-devel 222 | 223 | 构建软件包前,请先确保任何对仓库文件的更改以被提交(git commit): 224 | 225 | $ git add . 226 | $ git commit 227 | 228 | 构建软件包只需要在仓库目录中执行`makepkg`指令就可以了。你可以通过以下一系列命令来完成这些操作: 229 | 230 | ```sh 231 | # 如果之前执行过makepkg,请删除之前生成的临时文件夹,否则构建时会出现问题 232 | rm -rf buildtmp srctmp 233 | 234 | makepkg -si 235 | # -s:自动安装构建时需要的软件包 236 | # -i:构建完后自动安装生成的软件包 237 | 238 | # 完成后,你可以删除临时文件夹 239 | rm -rf buildtmp srctmp 240 | ``` 241 | 242 | **注意:** 每次升级Python时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 243 | 244 | ### 构建Debian软件包 245 | 246 | 构建Debian软件包前,你需要安装以下用于构建的系统组件: 247 | 248 | - `debhelper` 249 | - `dh-python` 250 | - `python3-setuptools` 251 | - `dpkg-dev` 252 | - `pybuild-plugin-pyproject` 253 | 254 | 你可以使用以下命令安装: 255 | 256 | sudo apt install debhelper dh-python python3-setuptools dpkg-dev pybuild-plugin-pyproject 257 | 258 | 安装完后,请在仓库目录中执行`dpkg-buildpackage -b`以构建软件包。完成后,你会在上层目录中获得一个`.deb`的文件。 259 | 260 | # 更多信息 261 | 262 | - 本仓库中的代码也同步在GitHub上(使用Gitee仓库镜像功能自动同步):https://github.com/swiftycode256/clitheme 263 | - 该项目的最新进展、未来计划、和开发中的新功能会在这里Gitee仓库中的Issues里列出:https://gitee.com/swiftycode/clitheme/issues 264 | - 欢迎通过Issues和Pull Requests提交建议和改进。 265 | - Wiki页面也可以;你可以在上方列出的仓库中提交Issues和Pull Requests -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.2 4 | "version": "0.2", 5 | // language - current active spelling language 6 | "language": "en", 7 | // words - list of words to be always considered correct 8 | "words": [ 9 | "globalvar", 10 | "swiftycode", 11 | "clitheme", 12 | "feof", "reof", 13 | "themedef", "clithemeinfo", "clithemedef", "infofile", 14 | "substrules", "manpages", 15 | "exactcmdmatch", "smartcmdmatch", "endmatchhere", 16 | "leadtabindents", "leadspaces", 17 | "strictcmdmatch", "normalcmdmatch", "exactcmdmatch", "smartcmdmatch", 18 | "subststdoutonly", "subststderronly", "substallstreams", "foregroundonly", 19 | "substvar", "substesc", 20 | "PKGBUILD", "MANPATH", 21 | "tcgetattr", "tcsetattr", "getpid", "setsid", "setitimer", "tcgetpgrp", "tcsetpgrp", 22 | "TIOCGWINSZ", "TIOCSWINSZ", "TCSADRAIN", "SIGWINCH", "SIGALRM", "ITIMER", "SIGTSTP", "SIGSTOP", "SIGCONT", "SIGUSR1", "SIGUSR2", 23 | "showchars", "keepends", 24 | "appname", "domainapp", 25 | "sanitycheck", "debugmode", "splitarray", "disablelang" 26 | ], 27 | // flagWords - list of words to be always considered incorrect 28 | // This is useful for offensive words and common spelling errors. 29 | "flagWords": [ 30 | ] 31 | } -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | clitheme (2.0-beta3-1) unstable; urgency=medium 2 | 3 | New features 4 | 5 | * Support using content variables in option definitions 6 | * Show prompt when reading files and message when reading from standard input in CLI interface 7 | * CLI interface: `get-current-theme-info` supports only displaying theme name and source file path for a more concise output 8 | * Rename certain options in `clitheme-exec` 9 | * A warning is now displayed when trying to reference a content variable without enabling the `substvar` option 10 | * Add new functions for modifying global default settings only for current file in `frontend` module, keeping separate settings for different files 11 | * Add `set_local_themedefs` function in `frontend` module, supporting specifying multiple files at once 12 | * Add `!require_version` definition in theme definition file, requiring minimum `clitheme` version 13 | * Support specifying content variables in `locale:` syntax 14 | * Support using more concise `[subst_regex]` and `[subst_string]` syntax 15 | * Support `--name` and `--file-path` options in `clitheme get-current-theme-info` command 16 | * Support using `--yes` option to skip confirmation prompt in `clitheme apply-theme` and `clitheme update-theme` commands 17 | 18 | Bug fixes and improvements 19 | 20 | * clitheme-exec: Specifying `--debug-newlines` option requires `--debug` option to be specified at the same time 21 | * Add more restricted characters to string entry pathnames 22 | * Change how the `foregroundonly` option work: it now applies to individual substitution rules 23 | - If specified in a command filter rule (specified after `[/filter_commands]`), it will reset to previous global setting after exiting current rule 24 | * Fix an issue where read segments repeatedly occur at non-newline positions 25 | * `clitheme-exec`: Ignore backspace characters with identical user input from output substitution processing 26 | * Rename "Generating data" prompt to "Processing files" 27 | * Fix an issue where outputs of `clitheme get-current-theme-info` and `clitheme update-theme` command have incorrect file ordering 28 | * `clitheme-exec`: Properly handle piped standard input 29 | * `{header_section}` now requires defining `name` entry 30 | * `clitheme-exec`: Fix high idle CPU usage 31 | * `clitheme-exec`: Database fetches are now cached in memory, significantly improving performance 32 | * Fix an issue when in multi-line block content and `substesc` option is enabled, `{{ESC}}` in content variables might not be processed 33 | * Important change: `endmatchhere` option only applies to substitution rules in the current file; other files are not affected 34 | 35 | -- swiftycode <3291929745@qq.com> Wed, 01 Jan 2025 11:54:00 +0800 36 | 37 | clitheme (2.0-beta2-1) unstable; urgency=medium 38 | 39 | New features 40 | 41 | * Support specifying multiple `[file_content]` phrases and filenames in `{manpage_section}` 42 | * New `[include_file]` syntax in `{manpage_section}`, supporting specifying multiple target file paths at once 43 | * New `foregroundonly` option for `filter_command` and `[filter_commands]` in `{substrules_section}`; only apply substitution rules if process is in foreground state 44 | * Applicable for shell and other command interpreter applications that changes foreground state when executing processes 45 | * `clitheme-exec`: new `--debug-foreground` option; outputs message when foreground state changes (shown as `! Foreground: False` and `! Foreground: True`) 46 | * Support specifying `substesc` and `substvar` options on `[/substitute_string]` and `[/substitute_regex]` in `{substrules_section}`; affects match expression 47 | * Support specifying `substvar` on `[/entry]` in `{entries_section}`; affects path name 48 | 49 | Bug fixes and improvements 50 | 51 | * Non-printable characters in CLI output messages will be displayed in its plain-text representation 52 | * Fix compatibility issues with Python 3.8 53 | * Fixes an issue where pressing CTRL-C in `clitheme-man` causes the program to unexpectedly terminate 54 | * Fixes an issue where terminal control characters are unexpectedly displayed on screen in Windows Command Prompt 55 | * 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 56 | * Fixes an issue where "specifying multiple `[entry]` phrases" feature in `{entries_section}` does not work properly 57 | * Fix many compatibility issues with applications in `clitheme-exec` 58 | * Theme definition file: Optimize processing of command filter definitions with same commands and different match options 59 | * Theme definition file: Fix unexpected "Option not allowed here" error when using multi-line content blocks 60 | * Theme definition file: Fix and optimize content variable processing 61 | * Theme definition file: Fix missing "Unexpected phrase" error in `{manpage_section}` when encountering invalid file syntax 62 | 63 | Latest code changes and development version: https://gitee.com/swiftycode/clitheme/tree/v1.2_dev 64 | 65 | Documentation: https://gitee.com/swiftycode/clitheme-wiki-repo 66 | 67 | Full release notes for v2.0: [please see v2.0-beta1 release page] 68 | 69 | -- swiftycode <3291929745@qq.com> Thu, 18 Jul 2024 00:12:00 +0800 70 | 71 | clitheme (2.0-beta1-2) unstable; urgency=low 72 | 73 | * Fixed an issue where clitheme-man might use system man pages instead of custom defined man pages 74 | 75 | -- swiftycode <3291929745@qq.com> Sat, 15 Jun 2024 23:55:00 +0800 76 | 77 | clitheme (2.0-beta1-1) unstable; urgency=low 78 | 79 | Known issues (Beta version) 80 | 81 | * Because redirecting stdout and stderr to separate streams cannot guarantee original output order as of now: 82 | * The `subststdoutonly` and `subststderronly` options in theme definition file is currently unavailable 83 | * The output marker will always show that output is stdout (`o>`) when using `clitheme-exec --debug` 84 | * When executing certain commands using `clitheme-exec`, error messages such as `No access to TTY` or `No job control` might appear 85 | * When executing `fish`, the error message `Inappropriate ioctl for device` will appear and will not run properly 86 | * When executing `vim` and suspending the program (using `:suspend` or `^Z`), terminal settings/attributes will not be restored to normal 87 | * Command line output substitution feature does not currently support Windows systems 88 | * This feature might be delayed for version `v2.1` 89 | 90 | Code repository and latest code changes: https://gitee.com/swiftycode/clitheme/tree/v1.2_dev 91 | 92 | Documentation: https://gitee.com/swiftycode/clitheme-wiki-repo/tree/v1.2_dev 93 | 94 | Full release notes for v2.0: [please see v2.0-beta1 release page] 95 | 96 | -- swiftycode <3291929745@qq.com> Thu, 13 Jun 2024 08:44:00 +0800 97 | 98 | clitheme (1.1-r2-1) unstable; urgency=medium 99 | 100 | Version 1.1 release 2 101 | 102 | Bug fixes: 103 | * Fixes an issue where setting overlay=True in frontend.set_local_themedef causes the function to not work properly 104 | * Fixes an issue where the subsections in frontend functions are incorrectly parsed 105 | 106 | For more information please see: 107 | https://gitee.com/swiftycode/clitheme/releases/tag/v1.1-r2 108 | 109 | -- swiftycode <3291929745@qq.com> Sat, 02 Mar 2024 23:49:00 +0800 110 | 111 | clitheme (1.1-r1-1) unstable; urgency=medium 112 | 113 | Version 1.1 release 1 114 | 115 | New features: 116 | * New description header definition for theme definition files 117 | * Add support for multi-line/block input in theme definition files 118 | * Support local deployment of theme definition files 119 | * The CLI interface can be modified using theme definition files 120 | * frontend module: Add format_entry_or_fallback function 121 | * Add support for Windows 122 | * CLI interface: Support specifying multiple files on apply-theme and generate-data-hierarchy commands 123 | 124 | Improvements and bug fixes: 125 | * frontend module: Optimize language detection process 126 | * CLI interface: Rename generate-data-hierarchy command to generate-data (the original command can still be used) 127 | * Fix some grammar and spelling errors in output messages 128 | 129 | For more information please see: 130 | https://gitee.com/swiftycode/clitheme/releases/tag/v1.1-r1 131 | 132 | -- swiftycode <3291929745@qq.com> Wed, 17 Jan 2024 22:14:00 +0800 133 | 134 | clitheme (1.0-r2-1) unstable; urgency=medium 135 | 136 | Version 1.0 release 2 137 | 138 | For more information please see: 139 | https://gitee.com/swiftycode/clitheme/releases/tag/v1.0-r2 140 | 141 | -- swiftycode <3291929745@qq.com> Fri, 15 Dec 2023 12:42:28 +0800 142 | 143 | clitheme (1.0-r1-1) unstable; urgency=medium 144 | 145 | Version 1.0 release 1 146 | 147 | -- swiftycode <3291929745@qq.com> Wed, 13 Dec 2023 17:16:33 +0800 148 | -------------------------------------------------------------------------------- /debian/clitheme.manpages: -------------------------------------------------------------------------------- 1 | docs/clitheme.1 2 | docs/clitheme-exec.1 3 | docs/clitheme-man.1 -------------------------------------------------------------------------------- /debian/clitheme.substvars: -------------------------------------------------------------------------------- 1 | misc:Depends= 2 | misc:Pre-Depends= 3 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: clitheme 2 | Section: libs 3 | Priority: optional 4 | Maintainer: swiftycode <3291929745@qq.com> 5 | Rules-Requires-Root: no 6 | Build-Depends: debhelper-compat (= 13), python3, dh-python, python3-setuptools, pybuild-plugin-pyproject 7 | Standards-Version: 4.6.2 8 | Homepage: https://gitee.com/swiftycode/clitheme 9 | Vcs-Git: https://gitee.com/swiftycode/clitheme.git 10 | 11 | Package: clitheme 12 | Architecture: all 13 | Multi-Arch: foreign 14 | Depends: ${misc:Depends}, python3 (> 3.8), man-db, sqlite3 15 | Description: Application framework for text theming 16 | clitheme allows users to customize the output of supported programs, such as 17 | multi-language support or mimicking your favorite cartoon character. It has an 18 | easy-to-understand syntax and implementation module for programs. 19 | . 20 | For more information, visit the homepage and the pages in the Wiki section. 21 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://gitee.com/swiftycode/clitheme 3 | Upstream-Name: clitheme 4 | Upstream-Contact: swiftycode <3291929745@qq.com> 5 | 6 | Files: 7 | * 8 | Copyright: 2023 swiftycode <3291929745@qq.com> 9 | License: GPL-3.0+ 10 | 11 | License: GPL-3.0+ 12 | This program is free software: you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation, either version 3 of the License, or 15 | (at your option) any later version. 16 | . 17 | This package is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | . 22 | You should have received a copy of the GNU General Public License 23 | along with this program. If not, see . 24 | Comment: 25 | On Debian systems, the complete text of the GNU General 26 | Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". 27 | 28 | -------------------------------------------------------------------------------- /debian/debhelper-build-stamp: -------------------------------------------------------------------------------- 1 | clitheme 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export PYBUILD_NAME=clitheme 4 | export PYBUILD_INSTALL_DIR=/usr/lib/python3/dist-packages 5 | 6 | %: 7 | dh $@ --with python3 --buildsystem=pybuild 8 | 9 | override_dh_auto_install: 10 | dh_auto_install 11 | # remove __pycache__ 12 | -rm -r debian/$(PYBUILD_NAME)/usr/lib/python3/dist-packages/$(PYBUILD_NAME)/__pycache__ 13 | -rm -r debian/$(PYBUILD_NAME)/usr/lib/python3/dist-packages/$(PYBUILD_NAME)/*/__pycache__ 14 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/upstream/metadata: -------------------------------------------------------------------------------- 1 | # Example file for upstream/metadata. 2 | # See https://wiki.debian.org/UpstreamMetadata for more info/fields. 3 | # Below an example based on a github project. 4 | 5 | Bug-Database: https://gitee.com/swiftycode/clitheme/issues 6 | Bug-Submit: https://gitee.com/swiftycode/clitheme/issues/new 7 | # Changelog: https://gitee.com/swiftycode/clitheme/blob/master/CHANGES 8 | Documentation: https://gitee.com/swiftycode/clitheme/wiki 9 | Repository-Browse: https://gitee.com/swiftycode/clitheme 10 | Repository: https://gitee.com/swiftycode/clitheme.git 11 | -------------------------------------------------------------------------------- /demo-clithemedef/README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # 字符串定义说明 2 | 3 | 本仓库中的`frontend_demo.py`支持以下字符串定义。默认字符串文本会以blockquote样式显示在路径名称下方。部分定义会包含额外的说明。 4 | 5 | --- 6 | 7 | **以下字符串会在`install-files`和`install-file`指令输出中用到:** 8 | 9 | ### `com.example example-app found-file` 10 | 11 | > 在当前目录找到了{}个文件 12 | 13 | ### `com.example example-app installing-file` 14 | 15 | > -> 正在安装 "{}"... 16 | 17 | ### `com.example example-app install-success` 18 | 19 | > 已成功安装{}个文件 20 | 21 | 该文本会被`install-files`指令输出调用。 22 | 23 | ### `com.example example-app install-success-file` 24 | 25 | > 已成功安装"{}" 26 | 27 | 该文本会被`install-file`指令输出调用。 28 | 29 | ### `com.example example-app file-not-found` 30 | 31 | > 错误:找不到文件"{}" 32 | 33 | ### `com.example example-app format-error` 34 | 35 | > 错误:命令语法不正确 36 | 37 | ### `com.example example-app directory-empty` 38 | 39 | > 错误:当前目录里没有任何文件 40 | 41 | 该文本只会被`install-files`指令输出调用。 42 | 43 | --- 44 | 45 | **以下字符串会在帮助信息中用到:** 46 | 47 | ### `com.example example-app helpmessage description-general` 48 | 49 | > 文件安装程序样例(不会修改系统中的文件) 50 | 51 | 该文本提供应用程序的名称,会在第一行显示。 52 | 53 | ### `com.example example-app helpmessage description-usageprompt` 54 | 55 | > 使用方法: 56 | 57 | 你可以通过此字符串定义”使用方法“的输出样式,会在命令列表前一行显示。 58 | 59 | ### `com.example example-app helpmessage unknown-command` 60 | 61 | > 错误:未知命令"{}" -------------------------------------------------------------------------------- /demo-clithemedef/demo-theme-textemojis.clithemedef.txt: -------------------------------------------------------------------------------- 1 | {header_section} 2 | name 颜文字样例主题 3 | version 1.0 4 | # testing block input 5 | [locales] 6 | Simplified Chinese 7 | 简体中文 8 | zh_CN 9 | [/locales] 10 | [supported_apps] 11 | clitheme example 12 | clitheme 样例应用 13 | clitheme_example 14 | [/supported_apps] 15 | [description] 16 | 适配项目中提供的example程序的一个颜文字主题,把它的输出变得可爱。 17 | 应用这个主题,沉浸在颜文字的世界中吧! 18 | 19 | 不要小看我的年龄,人家可是非常萌的~! 20 | [/description] 21 | {/header_section} 22 | 23 | {entries_section} 24 | in_domainapp com.example example-app 25 | [entry] found-file 26 | locale:default o(≧v≦)o 太好了,在当前目录找到了{}个文件! 27 | locale:zh_CN o(≧v≦)o 太好了,在当前目录找到了{}个文件! 28 | [/entry] 29 | [entry] installing-file 30 | locale:default (>^ω^<) 正在安装 "{}"... (>^ω^<) 31 | locale:zh_CN (>^ω^<) 正在安装 "{}"... (>^ω^<) 32 | [/entry] 33 | [entry] install-success 34 | locale:default o(≧v≦)o 已成功安装{}个文件! 35 | locale:zh_CN o(≧v≦)o 已成功安装{}个文件! 36 | [/entry] 37 | [entry] install-success-file 38 | locale:default o(≧v≦)o 已成功安装"{}"! 39 | locale:zh_CN o(≧v≦)o 已成功安装"{}"! 40 | [/entry] 41 | [entry] file-not-found 42 | locale:default ಥ_ಥ 糟糕,出错啦!找不到文件 "{}"!呜呜呜~ 43 | locale:zh_CN ಥ_ಥ 糟糕,出错啦!找不到文件 "{}" !呜呜呜~ 44 | [/entry] 45 | [entry] format-error 46 | locale:default ಥ_ಥ 糟糕,命令语法不正确!(ToT)/~~~ 47 | locale:zh_CN ಥ_ಥ 糟糕,命令语法不正确!(ToT)/~~~ 48 | [/entry] 49 | [entry] directory-empty 50 | locale:default ಥ_ಥ 糟糕,当前目录里没有任何文件!呜呜呜~ 51 | locale:zh_CN ಥ_ಥ 糟糕,当前目录里没有任何文件!呜呜呜~ 52 | [/entry] 53 | 54 | in_subsection helpmessage 55 | [entry] description-general 56 | locale:default (⊙ω⊙) 文件安装程序样例(不会修改系统中的文件哦~) 57 | locale:zh_CN (⊙ω⊙) 文件安装程序样例(不会修改系统中的文件哦~) 58 | [/entry] 59 | [entry] description-usageprompt 60 | locale:default (>﹏<) 使用方法:(◐‿◑) 61 | locale:zh_CN (>﹏<) 使用方法:(◐‿◑) 62 | [/entry] 63 | [entry] unknown-command 64 | locale:default ಥ_ಥ 找不到指令"{}"!呜呜呜~ 65 | locale:zh_CN ಥ_ಥ 找不到指令"{}"!呜呜呜~ 66 | [/entry] 67 | {/entries_section} 68 | -------------------------------------------------------------------------------- /docs/clitheme-exec.1: -------------------------------------------------------------------------------- 1 | .TH clitheme-exec 1 2024-06-21 2 | .SH NAME 3 | clitheme\-exec \- match and substitute output of a command 4 | .SH SYNOPSIS 5 | .B clitheme-exec [--debug] [--debug-color] [--debug-newlines] [--showchars] [--foreground-stat] [--nosubst] \fIcommand\fR 6 | .SH DESCRIPTION 7 | \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. 8 | .SH OPTIONS 9 | .TP 10 | .B --debug 11 | Display an indicator at the beginning of each line of output. The indicator contains information about stdout/stderr and whether if substitution happened. 12 | .P 13 | .RS 14 14 | - \fIo>\fR: stdout output 15 | 16 | - \fIe>\fR: stderr output 17 | .RE 18 | .TP 19 | .B --debug-color 20 | Applies color on the output contents; used to determine whether output is stdout or stderr. 21 | 22 | For stdout, yellow color is applied. For stderr, red color is applied. 23 | .TP 24 | .B --debug-newlines 25 | For output that does not end on a newline, display the output ending with newlines. 26 | .TP 27 | .B --showchars 28 | Display various control characters in plain text. The following characters will be displayed as its code name: 29 | .P 30 | .RS 14 31 | - ASCII escape character (ESC) 32 | 33 | - Carriage return (\\r) 34 | 35 | - Newline character (\\n) 36 | 37 | - Backspace character (\\x08) 38 | 39 | - Bell character (\\x07) 40 | .RE 41 | .TP 42 | .B --foreground-stat 43 | When the foreground status of the main process changes (determined using value of \fItcgetpgrp(3)\fR system call), output a message showing this change. 44 | 45 | Such change happens when running a shell in \fIclitheme-exec\fR and running another command in that shell. 46 | .P 47 | .RS 14 48 | - "! Foreground: False ()": Process exits foreground state 49 | 50 | - "! Foreground: True ()": Process enters (re-enters) foreground state 51 | .RE 52 | .TP 53 | .B --nosubst 54 | Even if a theme is set, do not perform any output substitution operations. 55 | 56 | This is useful if you are trying to get the original output of the command with control characters displayed on-screen using \fI--showchars\fR. 57 | 58 | .SH SEE ALSO 59 | \fIclitheme(1)\fR -------------------------------------------------------------------------------- /docs/clitheme-man.1: -------------------------------------------------------------------------------- 1 | .TH clitheme-man 1 2024-05-07 2 | .SH NAME 3 | clitheme\-man \- access manual pages in the current theme 4 | .SH SYNOPSIS 5 | .B clitheme-man [OPTIONS] 6 | .SH DESCRIPTION 7 | \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. 8 | .P 9 | \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. 10 | .SH OPTIONS 11 | For a list of options you can use, please see \fIman(1)\fR. 12 | .SH SEE ALSO 13 | \fIclitheme(1)\fR, \fIman(1)\fR -------------------------------------------------------------------------------- /docs/clitheme.1: -------------------------------------------------------------------------------- 1 | .TH clitheme 1 2024-12-19 2 | .SH NAME 3 | clitheme \- frontend to customize output of applications 4 | .SH SYNOPSIS 5 | .B clitheme [COMMAND] [OPTIONS] 6 | .SH DESCRIPTION 7 | \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. 8 | .SH OPTIONS 9 | .TP 10 | .B apply-theme [themedef-file] [--overlay] [--preserve-temp] [--yes] 11 | Applies the given theme definition file(s) into the current system. Supported applications will immediately start using the defined values after performing this operation. 12 | 13 | Specify \fB--overlay\fR to to add file(s) onto the current data. 14 | 15 | Specify \fB--preserve-temp\fR to preserve the temporary directory after the operation. (Debug purposes only) 16 | 17 | Specify \fB--yes\fR to skip the confirmation prompt. 18 | 19 | .TP 20 | .B get-current-theme-info [--name] [--file-path] 21 | 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. 22 | 23 | Specify \fB--name\fR to only display the name of each theme. 24 | 25 | Specify \fB--file-path\fR to only display the source file path of each theme. 26 | 27 | (Both will be displayed when both specified) 28 | 29 | .TP 30 | .B unset-current-theme 31 | Removes the current theme data from the system. Supported applications will immediately stop using the defined values after this operation. 32 | .TP 33 | .B update-theme [--yes] 34 | Re-applies the theme definition files specified in the previous "apply-theme" command (previous commands if \fB--overlay\fR is used) 35 | 36 | Calling this command is equivalent to calling "clitheme apply-theme" with previously-specified files. 37 | 38 | Specify \fB--yes\fR to skip the confirmation prompt. 39 | 40 | .TP 41 | .B generate-data [themedef-file] [--overlay] 42 | 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. 43 | 44 | This command is for debug purposes only. 45 | .TP 46 | .B --help 47 | Outputs a short help message consisting of available commands. 48 | .TP 49 | .B --version 50 | Outputs the current version of the program. 51 | .SH SEE ALSO 52 | \fIclitheme-exec(1)\fR, \fIclitheme-man(1)\fR 53 | 54 | For more information, please see the project homepage and the project wiki: 55 | .P 56 | .I https://gitee.com/swiftycode/clitheme 57 | .P 58 | .I https://gitee.com/swiftycode/clitheme/wikis/pages 59 | or 60 | .I https://gitee.com/swiftycode/clitheme-wiki-repo -------------------------------------------------------------------------------- /docs/docs.clithemedef.txt: -------------------------------------------------------------------------------- 1 | {header_section} 2 | name clitheme manual pages 3 | version 2.0 4 | [description] 5 | This file contains manual pages for clitheme. 6 | Apply this theme to install the manual pages in the "docs" directory. 7 | After that, use "clitheme-man" to access them. 8 | [/description] 9 | {/header_section} 10 | 11 | {manpage_section} 12 | set_options substvar 13 | setvar:name clitheme 14 | include_file {{name}}.1 15 | as man1 {{name}}.1 16 | include_file {{name}}-exec.1 17 | as man1 {{name}}-exec.1 18 | include_file {{name}}-man.1 19 | as man1 {{name}}-man.1 20 | {/manpage_section} -------------------------------------------------------------------------------- /frontend_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # 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. 4 | # 这个程序展示了clitheme的应用程序frontend API。请应用一个在"demo-clithemedef"文件夹中的任意一个主题定义文件以观察它的效果。 5 | 6 | import os 7 | import sys 8 | from src.clitheme import frontend 9 | 10 | demo_message="""正在展示{}演示(不会修改系统上的文件):""" 11 | 12 | help_usage=\ 13 | """ 14 | {0} install-files 15 | {0} install-file [FILE] 16 | {0} --help 17 | {0} --clitheme-output-defs 18 | """ 19 | 20 | outputdefs_string=\ 21 | """ 22 | com.example example-app found-file 23 | 在当前目录找到了{}个文件 24 | 25 | com.example example-app installing-file 26 | -> 正在安装 "{}"... 27 | 28 | com.example example-app install-success 29 | 已成功安装{}个文件 30 | 31 | com.example example-app install-success-file 32 | 已成功安装"{}" 33 | 34 | com.example example-app file-not-found 35 | 错误:找不到文件"{}" 36 | 37 | com.example example-app format-error 38 | 错误:命令语法不正确 39 | 40 | com.example example-app directory-empty 41 | 错误:当前目录里没有任何文件 42 | 43 | com.example example-app helpmessage description-general 44 | 文件安装程序样例(不会修改系统中的文件) 45 | 46 | com.example example-app helpmessage description-usageprompt 47 | 使用方法: 48 | 49 | com.example example-app helpmessage unknown-command 50 | 错误:未知命令"{}" 51 | """ 52 | 53 | frontend.set_domain("com.example") 54 | frontend.set_appname("example-app") 55 | f=frontend.FetchDescriptor() 56 | 57 | if len(sys.argv)>1 and sys.argv[1]=="install-files": 58 | if len(sys.argv)!=2: 59 | print(f.retrieve_entry_or_fallback("format-error", "错误:命令语法不正确")) 60 | exit(1) 61 | print(demo_message.format(sys.argv[1])) 62 | dirfiles=os.listdir() 63 | if len(dirfiles)==0: 64 | print(f.retrieve_entry_or_fallback("directory-empty","错误:当前目录里没有任何文件")) 65 | print(f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件").format(str(len(dirfiles)))) 66 | for item in dirfiles: 67 | print(f.retrieve_entry_or_fallback("installing-file", "-> 正在安装 \"{}\"...").format(item)) 68 | print(f.retrieve_entry_or_fallback("install-success","已成功安装{}个文件").format(str(len(dirfiles)))) 69 | elif len(sys.argv)>1 and sys.argv[1]=="install-file": 70 | if len(sys.argv)!=3: 71 | print(f.retrieve_entry_or_fallback("format-error", "错误:命令语法不正确")) 72 | exit(1) 73 | print(demo_message.format(sys.argv[1])) 74 | item=sys.argv[2].strip() 75 | if os.path.exists(item): 76 | print(f.retrieve_entry_or_fallback("installing-file", "-> 正在安装 \"{}\"...").format(item)) 77 | print(f.retrieve_entry_or_fallback("install-success-file","已成功安装\"{}\"").format(item)) 78 | else: 79 | print(f.retrieve_entry_or_fallback("file-not-found","错误:找不到文件\"{}\"").format(item)) 80 | exit(1) 81 | elif len(sys.argv)>1 and sys.argv[1]=="--clitheme-output-defs": 82 | print(outputdefs_string) 83 | else: 84 | f2=frontend.FetchDescriptor(subsections="helpmessage") 85 | print(f2.retrieve_entry_or_fallback("description-general","文件安装程序样例(不会修改系统中的文件)")) 86 | print(f2.retrieve_entry_or_fallback("description-usageprompt","使用方法:")) 87 | print(help_usage.format(sys.argv[0])) 88 | if len(sys.argv)>1 and sys.argv[1]=="--help": 89 | exit(0) 90 | elif len(sys.argv)>1: 91 | print(f2.retrieve_entry_or_fallback("unknown-command","错误:未知命令\"{}\"").format(sys.argv[1])) 92 | exit(1) -------------------------------------------------------------------------------- /frontend_fallback.py: -------------------------------------------------------------------------------- 1 | """ 2 | clitheme fallback frontend for version 2.0 (returns fallback values for all functions) 3 | """ 4 | from typing import Optional, List 5 | 6 | data_path="" 7 | 8 | global_domain="" 9 | global_appname="" 10 | global_subsections="" 11 | global_debugmode=False 12 | global_lang="" 13 | global_disablelang=False 14 | 15 | def set_domain(value: Optional[str]): pass 16 | def set_appname(value: Optional[str]): pass 17 | def set_subsections(value: Optional[str]): pass 18 | def set_debugmode(value: Optional[bool]): pass 19 | def set_lang(value: Optional[str]): pass 20 | def set_disablelang(value: Optional[bool]): pass 21 | 22 | alt_path=None 23 | alt_path_dirname=None 24 | alt_path_hash=None 25 | 26 | def set_local_themedef(file_content: str, overlay: bool=False) -> bool: 27 | """Fallback set_local_themedef function (always returns False)""" 28 | return False 29 | def set_local_themedefs(file_contents: List[str], overlay: bool=False): 30 | """Fallback set_local_themedefs function (always returns False)""" 31 | return False 32 | def unset_local_themedef(): 33 | """Fallback unset_local_themedef function""" 34 | return 35 | 36 | class FetchDescriptor(): 37 | """ 38 | Object containing domain and app information used for fetching entries 39 | """ 40 | def __init__(self, domain_name: Optional[str] = None, app_name: Optional[str] = None, subsections: Optional[str] = None, lang: Optional[str] = None, debug_mode: Optional[bool] = None, disable_lang: Optional[bool] = None): 41 | """Fallback init function""" 42 | return 43 | def retrieve_entry_or_Fallback(self, entry_path: str, fallback_string: str) -> str: 44 | """Fallback retrieve_entry_or_Fallback function (always return Fallback string)""" 45 | return fallback_string 46 | reof=retrieve_entry_or_Fallback # a shorter alias of the function 47 | def format_entry_or_fallback(self, entry_path: str, fallback_string: str, *args, **kwargs) -> str: 48 | return fallback_string.format(*args, **kwargs) 49 | feof=format_entry_or_fallback 50 | def entry_exists(self, entry_path: str) -> bool: 51 | """Fallback entry_exists function (always return false)""" 52 | return False -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools.dynamic] 6 | version = {attr = "clitheme._version.__version__"} 7 | 8 | [tool.setuptools.packages.find] 9 | where = ["src"] 10 | exclude = ["*test*"] 11 | 12 | [tool.setuptools.package-data] 13 | "*" = ["*"] 14 | 15 | [project] 16 | name = "clitheme" 17 | dynamic = ["version"] 18 | authors = [ 19 | { name="swiftycode", email="3291929745@qq.com" }, 20 | ] 21 | description = "A text theming library for command line applications" 22 | readme = ".github/README.md" 23 | license = {text = "GNU General Public License v3 (GPLv3)"} 24 | requires-python = ">=3.8" 25 | classifiers = [ 26 | "Programming Language :: Python :: 3", 27 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 28 | "Operating System :: OS Independent", 29 | "Private :: Do Not Upload" 30 | ] 31 | 32 | [project.scripts] 33 | clitheme = "clitheme:cli._script_main" 34 | clitheme-exec = "clitheme:exec._script_main" 35 | clitheme-man = "clitheme:man._script_main" 36 | 37 | [project.urls] 38 | Repository = "https://gitee.com/swiftycode/clitheme" 39 | Documentation = "https://gitee.com/swiftycode/clitheme/wikis/pages" 40 | Issues = "https://gitee.com/swiftycode/clitheme/issues" -------------------------------------------------------------------------------- /src/clitheme-testblock_testprogram.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | # Program for testing multi-line (block) processing of _generator 8 | from clitheme import _generator, frontend 9 | 10 | file_data=""" 11 | begin_header 12 | name untitled 13 | end_header 14 | 15 | begin_main 16 | set_options leadtabindents:1 17 | entry test_entry 18 | locale_block default en_US en C 19 | 20 | 21 | this 22 | and 23 | that 24 | 25 | is just good 26 | #enough 27 | should have leading 2 lines and trailing 3 lines 28 | 29 | 30 | 31 | end_block 32 | end_entry 33 | end_main 34 | """ 35 | 36 | file_data_2=""" 37 | begin_header 38 | name untitled 39 | end_header 40 | 41 | begin_main 42 | entry test_entry 43 | locale_block zh_CN 44 | 45 | 46 | 47 | 这是一个 48 | 很好的东西 49 | 50 | #非常好 51 | ... 52 | should have leading 3 lines and trailing 2 lines 53 | 54 | 55 | end_block leadspaces:4 56 | end_entry 57 | end_main 58 | """ 59 | 60 | frontend.set_debugmode(True) 61 | if frontend.set_local_themedef(file_data)==False: 62 | print("Error: set_local_themedef failed") 63 | exit(1) 64 | if frontend.set_local_themedef(file_data_2, overlay=True)==False: # test overlay function 65 | print("Error: set_local_themedef failed") 66 | exit(1) 67 | f=frontend.FetchDescriptor() 68 | print("Default locale:") 69 | f.disable_lang=True 70 | # Not printing because debug mode already prints 71 | (f.reof("test_entry", "Nonexistent")) 72 | print("zh_CN locale:") 73 | f.disable_lang=False 74 | f.lang="zh_CN" 75 | (f.reof("test_entry", "Nonexistent")) 76 | f.debug_mode=False 77 | for lang in ["C", "en", "en_US", "zh_CN"]: 78 | f.disable_lang=True 79 | name=f"test_entry__{lang}" 80 | if f.entry_exists(name): 81 | print(f"{name} found") 82 | else: 83 | print(f"{name} not found") -------------------------------------------------------------------------------- /src/clitheme/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command line customization toolkit 3 | """ 4 | # Copyright © 2023-2024 swiftycode 5 | 6 | # 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. 7 | # 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. 8 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 9 | 10 | __all__=["frontend"] 11 | 12 | # Prevent RuntimeWarning from displaying when running a submodule (e.g. "python3 -m clitheme.exec") 13 | import warnings 14 | warnings.simplefilter("ignore", category=RuntimeWarning) 15 | del warnings 16 | 17 | # Enable processing of escape characters in Windows Command Prompt 18 | import os 19 | if os.name=="nt": 20 | import ctypes 21 | 22 | try: 23 | handle=ctypes.windll.kernel32.GetStdHandle(-11) # standard output handle 24 | console_mode=ctypes.c_long() 25 | if ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(console_mode))==0: 26 | raise Exception("GetConsoleMode failed: "+str(ctypes.windll.kernel32.GetLastError())) 27 | console_mode.value|=0x0004 # ENABLE_VIRTUAL_TERMINAL_PROCESSING 28 | if ctypes.windll.kernel32.SetConsoleMode(handle, console_mode.value)==0: 29 | raise Exception("SetConsoleMode failed: "+str(ctypes.windll.kernel32.GetLastError())) 30 | except: 31 | pass 32 | del os 33 | 34 | # Expose these modules when "clitheme" is imported 35 | from . import _globalvar, frontend 36 | _globalvar.handle_set_themedef(frontend, "global") # type: ignore 37 | del _globalvar # Don't expose this module -------------------------------------------------------------------------------- /src/clitheme/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from . import cli 3 | 4 | exit(cli.main(sys.argv)) -------------------------------------------------------------------------------- /src/clitheme/_generator/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Generator function used in applying themes (should not be invoked directly) 9 | """ 10 | import os 11 | import string 12 | import random 13 | from typing import Optional 14 | from .. import _globalvar 15 | from . import _parser_handlers 16 | from . import _header_parser, _entries_parser, _substrules_parser, _manpage_parser 17 | 18 | # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent 19 | 20 | path="" 21 | silence_warn=False 22 | __all__=["generate_data_hierarchy"] 23 | 24 | def generate_custom_path() -> str: 25 | # Generate a temporary path 26 | global path 27 | path=_globalvar.clitheme_temp_root+"/clitheme-temp-" 28 | for x in range(8): 29 | path+=random.choice(string.ascii_letters) 30 | return path 31 | 32 | 33 | def generate_data_hierarchy(file_content: str, custom_path_gen=True, custom_infofile_name="1", filename: str="") -> str: 34 | # make directories 35 | if custom_path_gen: 36 | generate_custom_path() 37 | global path 38 | obj=_parser_handlers.GeneratorObject(file_content=file_content, custom_infofile_name=custom_infofile_name, filename=filename, path=path, silence_warn=silence_warn) 39 | 40 | before_content_lines=True 41 | while obj.goto_next_line(): 42 | phrases=obj.lines_data[obj.lineindex].split() 43 | first_phrase=phrases[0] 44 | is_content=True 45 | if first_phrase in ("begin_header", r"{header_section}"): 46 | _header_parser.handle_header_section(obj, first_phrase) 47 | elif first_phrase in ("begin_main", r"{entries_section}"): 48 | _entries_parser.handle_entries_section(obj, first_phrase) 49 | elif first_phrase==r"{substrules_section}": 50 | _substrules_parser.handle_substrules_section(obj, first_phrase) 51 | elif first_phrase==r"{manpage_section}": 52 | _manpage_parser.handle_manpage_section(obj, first_phrase) 53 | elif obj.handle_setters(really_really_global=True): pass 54 | elif first_phrase=="!require_version": 55 | is_content=False 56 | obj.check_enough_args(phrases, 2) 57 | obj.check_extra_args(phrases, 2, use_exact_count=True) 58 | if not before_content_lines: 59 | obj.handle_error(obj.fd.feof("phrase-precedence-err", "Line {num}: header macro \"{phrase}\" must be specified before other lines", num=str(obj.lineindex+1), phrase=first_phrase)) 60 | obj.check_version(phrases[1]) 61 | else: obj.handle_invalid_phrase(first_phrase) 62 | 63 | if is_content: before_content_lines=False 64 | 65 | def is_content_parsed() -> bool: 66 | content_sections=["entries", "substrules", "manpage"] 67 | for section in content_sections: 68 | if section in obj.parsed_sections: return True 69 | return False 70 | if obj.section_parsing or not "header" in obj.parsed_sections or not is_content_parsed(): 71 | obj.handle_error(obj.fd.reof("incomplete-section-err", "Missing or incomplete header or content sections")) 72 | # record file content for database migration/upgrade feature 73 | obj.write_infofile(obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, "file_content", obj.file_content, obj.lineindex+1, "") 74 | # record *full* file path for update-themes feature 75 | 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, "") 76 | # Update current theme index 77 | theme_index=open(obj.path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") 78 | theme_index.write(obj.custom_infofile_name+"\n") 79 | path=obj.path 80 | return obj.path 81 | -------------------------------------------------------------------------------- /src/clitheme/_generator/_data_handlers.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Functions for data processing and error handling (internal module) 9 | """ 10 | import os 11 | import gzip 12 | import re 13 | from typing import Optional, List 14 | from .. import _globalvar, frontend 15 | 16 | # spell-checker:ignore datapath 17 | 18 | class DataHandlers: 19 | frontend=frontend 20 | 21 | def __init__(self, path: str, silence_warn: bool): 22 | self.path=path 23 | self.silence_warn=silence_warn 24 | if not os.path.exists(self.path): os.mkdir(self.path) 25 | self.datapath=self.path+"/"+_globalvar.generator_data_pathname 26 | if not os.path.exists(self.datapath): os.mkdir(self.datapath) 27 | self.fd=self.frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") 28 | self.fmt=_globalvar.make_printable # alias for the make_printable function 29 | def handle_error(self, message: str, not_syntax_error: bool=False): 30 | output=message if not_syntax_error else self.fd.feof("error-str", "Syntax error: {msg}", msg=message) 31 | raise SyntaxError(output) 32 | def handle_warning(self, message: str): 33 | output=self.fd.feof("warning-str", "Warning: {msg}", msg=message) 34 | if not self.silence_warn: print(output) 35 | def recursive_mkdir(self, path: str, entry_name: str, line_number_debug: int): # recursively generate directories (excluding file itself) 36 | current_path=path 37 | current_entry="" # for error output 38 | for x in entry_name.split()[:-1]: 39 | current_entry+=x+" " 40 | current_path+="/"+x 41 | if os.path.isfile(current_path): # conflict with entry file 42 | self.handle_error(self.fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ 43 | num=str(line_number_debug), name=self.fmt(current_entry))) 44 | elif os.path.isdir(str(current_path))==False: # directory does not exist 45 | os.mkdir(current_path) 46 | def add_entry(self, path: str, entry_name: str, entry_content: str, line_number_debug: int): # add entry to where it belongs 47 | self.recursive_mkdir(path, entry_name, line_number_debug) 48 | target_path=path 49 | for x in entry_name.split(): 50 | target_path+="/"+x 51 | if os.path.isdir(target_path): 52 | self.handle_error(self.fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ 53 | num=str(line_number_debug), name=self.fmt(entry_name))) 54 | elif os.path.isfile(target_path): 55 | self.handle_warning(self.fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ 56 | num=str(line_number_debug), name=self.fmt(entry_name))) 57 | f=open(target_path,'w', encoding="utf-8") 58 | f.write(entry_content+"\n") 59 | def write_infofile(self, path: str, filename: str, content: str, line_number_debug: int, header_name_debug: str): 60 | if not os.path.isdir(path): 61 | os.makedirs(path) 62 | target_path=path+"/"+filename 63 | if os.path.isfile(target_path): 64 | self.handle_warning(self.fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ 65 | num=str(line_number_debug), name=self.fmt(header_name_debug))) 66 | f=open(target_path,'w', encoding="utf-8") 67 | f.write(content+'\n') 68 | def write_infofile_newlines(self, path: str, filename: str, content_phrases: List[str], line_number_debug: int, header_name_debug: str): 69 | if not os.path.isdir(path): 70 | os.makedirs(path) 71 | target_path=path+"/"+filename 72 | if os.path.isfile(target_path): 73 | self.handle_warning(self.fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ 74 | num=str(line_number_debug), name=self.fmt(header_name_debug))) 75 | f=open(target_path,'w', encoding="utf-8") 76 | for line in content_phrases: 77 | f.write(line+"\n") 78 | def write_manpage_file(self, file_path: List[str], content: str, line_number_debug: int, custom_parent_path: Optional[str]=None): 79 | parent_path=custom_parent_path if custom_parent_path!=None else self.path+"/"+_globalvar.generator_manpage_pathname 80 | parent_path+='/'+os.path.dirname(_globalvar.splitarray_to_string(file_path).replace(" ","/")) 81 | # create the parent directory 82 | try: os.makedirs(parent_path, exist_ok=True) 83 | except (FileExistsError, NotADirectoryError): 84 | self.handle_error(self.fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug)), not_syntax_error=True) 85 | full_path=parent_path+"/"+file_path[-1] 86 | if os.path.isfile(full_path): 87 | 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))) 88 | try: 89 | # write the compressed and original version of the file 90 | open(full_path, "w", encoding="utf-8").write(content) 91 | open(full_path+".gz", "wb").write(gzip.compress(bytes(content, "utf-8"))) 92 | except IsADirectoryError: 93 | self.handle_error(self.fd.feof("manpage-subdir-file-conflict-err", "Line {num}: conflicting files and subdirectories; please check previous definitions", num=str(line_number_debug)), not_syntax_error=True) -------------------------------------------------------------------------------- /src/clitheme/_generator/_entries_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | entries_section parser function (internal module) 9 | """ 10 | from typing import Optional 11 | from .. import _globalvar 12 | from . import _parser_handlers 13 | 14 | # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent 15 | 16 | def handle_entries_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): 17 | obj.handle_begin_section("entries") 18 | end_phrase="end_main" if first_phrase=="begin_main" else r"{/entries_section}" 19 | if first_phrase=="begin_main": 20 | 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}")) 21 | obj.in_domainapp="" 22 | obj.in_subsection="" 23 | while obj.goto_next_line(): 24 | phrases=obj.lines_data[obj.lineindex].split() 25 | if phrases[0]=="in_domainapp": 26 | this_phrases=obj.parse_content(obj.lines_data[obj.lineindex].strip(), pure_name=True).split() 27 | obj.check_enough_args(this_phrases, 3) 28 | obj.check_extra_args(this_phrases, 3, use_exact_count=False) 29 | obj.in_domainapp=this_phrases[1]+" "+this_phrases[2] 30 | if _globalvar.sanity_check(obj.in_domainapp)==False: 31 | obj.handle_error(obj.fd.feof("sanity-check-domainapp-err", "Line {num}: domain and app names {sanitycheck_msg}", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) 32 | obj.in_subsection="" # clear subsection 33 | elif phrases[0]=="in_subsection": 34 | obj.check_enough_args(phrases, 2) 35 | obj.in_subsection=_globalvar.splitarray_to_string(phrases[1:]) 36 | obj.in_subsection=obj.parse_content(obj.in_subsection, pure_name=True) 37 | if _globalvar.sanity_check(obj.in_subsection)==False: 38 | obj.handle_error(obj.fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(obj.lineindex+1), sanitycheck_msg=_globalvar.sanity_check_error_message)) 39 | elif phrases[0]=="unset_domainapp": 40 | obj.check_extra_args(phrases, 1, use_exact_count=True) 41 | obj.in_domainapp=""; obj.in_subsection="" 42 | elif phrases[0]=="unset_subsection": 43 | obj.check_extra_args(phrases, 1, use_exact_count=True) 44 | obj.in_subsection="" 45 | elif phrases[0] in ("entry", "[entry]"): 46 | obj.check_enough_args(phrases, 2) 47 | entry_name=_globalvar.extract_content(obj.lines_data[obj.lineindex]) 48 | obj.handle_entry(entry_name, start_phrase=phrases[0], end_phrase="[/entry]" if phrases[0]=="[entry]" else "end_entry") 49 | elif obj.handle_setters(): pass 50 | elif phrases[0]==end_phrase: 51 | obj.check_extra_args(phrases, 1, use_exact_count=True) 52 | obj.handle_end_section("entries") 53 | # deprecation warning 54 | if phrases[0]=="end_main": 55 | 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}")) 56 | break 57 | else: obj.handle_invalid_phrase(phrases[0]) 58 | -------------------------------------------------------------------------------- /src/clitheme/_generator/_entry_block_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | import sys 8 | import re 9 | import copy 10 | import uuid 11 | from typing import Optional, Union, List, Dict, Any 12 | from .. import _globalvar 13 | 14 | 15 | def handle_entry(obj, entry_name: str, start_phrase: str, end_phrase: str, is_substrules: bool=False, substrules_options: Dict[str, Any]={}): 16 | # Workaround to circular import issue 17 | from . import _parser_handlers 18 | self: _parser_handlers.GeneratorObject=obj 19 | # substrules_options: {effective_commands: list, is_regex: bool, strictness: int} 20 | 21 | entry_name_substesc=False; entry_name_substvar=False 22 | names_processed=False # Set to True when no more entry names are being specified 23 | 24 | # For supporting specifying multiple entries at once (0: name, 1: uuid, 2: debug_linenumber) 25 | entryNames: List[tuple]=[(entry_name, uuid.uuid4(), self.lineindex+1)] 26 | # For substrules_section: (0: match_content, 1: substitute_content, 2: locale, 3: entry_name_uuid, 4: content_linenumber_str, 5: match_content_linenumber) 27 | # For entries_section: (0: target_entry, 1: content, 2: debug_linenumber, 3: entry_name_uuid, 4: entry_name_linenumber) 28 | entries: List[tuple]=[] 29 | 30 | substrules_endmatchhere=False 31 | substrules_stdout_stderr_option=0 32 | substrules_foregroundonly=False 33 | 34 | def check_valid_pattern(pattern: str, debug_linenumber: Union[str, int]=self.lineindex+1): 35 | # check if patterns are valid 36 | try: re.compile(pattern) 37 | 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])) 38 | while self.goto_next_line(): 39 | phrases=self.lines_data[self.lineindex].split() 40 | line_content=self.lines_data[self.lineindex] 41 | # Support specifying multiple match pattern/entry names in one definition block 42 | if phrases[0]!=start_phrase and not names_processed: 43 | names_processed=True # Prevent specifying it after other definition syntax 44 | # --Process entry names-- 45 | for x in range(len(entryNames)): 46 | each_entry=entryNames[x] 47 | name=each_entry[0] 48 | if not is_substrules: 49 | if self.in_subsection!="": name=self.in_subsection+" "+name 50 | if self.in_domainapp!="": name=self.in_domainapp+" "+name 51 | entryNames[x]=(name, each_entry[1], each_entry[2]) 52 | 53 | if phrases[0]==start_phrase and not names_processed: 54 | self.check_enough_args(phrases, 2) 55 | pattern=_globalvar.extract_content(line_content) 56 | entryNames.append((pattern, uuid.uuid4(), self.lineindex+1)) 57 | elif phrases[0]=="locale" or phrases[0].startswith("locale:"): 58 | content: str 59 | locale: str 60 | if phrases[0].startswith("locale:"): 61 | self.check_enough_args(phrases, 2) 62 | results=re.search(r"locale:(?P.+)", phrases[0]) 63 | if results==None: 64 | 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))) 65 | else: 66 | locale=results.groupdict()['locale'] 67 | content=_globalvar.extract_content(line_content) 68 | else: 69 | self.check_enough_args(phrases, 3) 70 | content=_globalvar.extract_content(line_content, begin_phrase_count=2) 71 | locale=phrases[1] 72 | locales=self.parse_content(locale, pure_name=True).split() 73 | content=self.parse_content(content) 74 | for this_locale in locales: 75 | for each_name in entryNames: 76 | if is_substrules: 77 | entries.append((each_name[0], content, None if this_locale=="default" else this_locale, each_name[1], str(self.lineindex+1), each_name[2])) 78 | else: 79 | target_entry=copy.copy(each_name[0]) 80 | if this_locale!="default": 81 | target_entry+="__"+this_locale 82 | entries.append((target_entry, content, self.lineindex+1, each_name[1], each_name[2])) 83 | elif phrases[0] in ("locale_block", "[locale]"): 84 | self.check_enough_args(phrases, 2) 85 | locales=self.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() 86 | begin_line_number=self.lineindex+1+1 87 | content=self.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/locale]" if phrases[0]=="[locale]" else "end_block") 88 | for this_locale in locales: 89 | for each_name in entryNames: 90 | if is_substrules: 91 | 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])) 92 | else: 93 | target_entry=copy.copy(each_name[0]) 94 | if this_locale!="default": 95 | target_entry+="__"+this_locale 96 | entries.append((target_entry, content, begin_line_number, each_name[1], each_name[2])) 97 | elif phrases[0]==end_phrase: 98 | got_options=self.parse_options(phrases[1:] if len(phrases)>1 else [], merge_global_options=True, \ 99 | allowed_options=\ 100 | (self.subst_limiting_options if is_substrules else []) 101 | +(self.content_subst_options if is_substrules else ["substvar"]) # don't allow substesc in `[entry]` 102 | +(['foregroundonly'] if is_substrules else []) 103 | ) 104 | for option in got_options: 105 | if option=="endmatchhere" and got_options['endmatchhere']==True: 106 | substrules_endmatchhere=True 107 | elif option=="subststdoutonly" and got_options['subststdoutonly']==True: 108 | substrules_stdout_stderr_option=1 109 | elif option=="subststderronly" and got_options['subststderronly']==True: 110 | substrules_stdout_stderr_option=2 111 | elif option=="substesc" and got_options['substesc']==True: 112 | entry_name_substesc=True 113 | elif option=="substvar" and got_options['substvar']==True: 114 | entry_name_substvar=True 115 | elif option=="foregroundonly" and got_options['foregroundonly']==True: 116 | substrules_foregroundonly=True 117 | break 118 | else: self.handle_invalid_phrase(phrases[0]) 119 | # For silence_warning in subst_variable_content 120 | encountered_ids=set() 121 | for x in range(len(entries)): 122 | entry=entries[x] 123 | match_pattern=entry[0] 124 | # substvar MUST come before substesc or "{{ESC}}" in variable content will not be processed 125 | debug_linenumber=entry[5] if is_substrules else entry[4] 126 | match_pattern=self.subst_variable_content(match_pattern, custom_condition=entry_name_substvar, \ 127 | line_number_debug=debug_linenumber, \ 128 | # Don't show warnings for the same match_pattern 129 | silence_warnings=entry[3] in encountered_ids) 130 | match_pattern=self.handle_substesc(match_pattern, condition=entry_name_substesc==True, line_number_debug=debug_linenumber) 131 | 132 | if is_substrules: check_valid_pattern(match_pattern, entry[5]) 133 | else: 134 | # Prevent leading . & prevent /,\ in entry name 135 | if _globalvar.sanity_check(match_pattern)==False: 136 | self.handle_error(self.fd.feof("sanity-check-entry-err", "Line {num}: entry subsections/names {sanitycheck_msg}", num=str(entry[4]), sanitycheck_msg=_globalvar.sanity_check_error_message)) 137 | encountered_ids.add(entry[3]) 138 | if is_substrules: 139 | try: 140 | self.db_interface.add_subst_entry( 141 | match_pattern=match_pattern, \ 142 | substitute_pattern=entry[1], \ 143 | effective_commands=substrules_options['effective_commands'], \ 144 | effective_locale=entry[2], \ 145 | is_regex=substrules_options['is_regex'], \ 146 | command_match_strictness=substrules_options['strictness'], \ 147 | end_match_here=substrules_endmatchhere, \ 148 | stdout_stderr_matchoption=substrules_stdout_stderr_option, \ 149 | foreground_only=substrules_foregroundonly, \ 150 | line_number_debug=entry[4], \ 151 | file_id=self.file_id, \ 152 | unique_id=entry[3]) 153 | 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])) 154 | else: 155 | self.add_entry(self.datapath, match_pattern, entry[1], entry[2]) -------------------------------------------------------------------------------- /src/clitheme/_generator/_header_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | header_section parser function (internal module) 9 | """ 10 | import re 11 | from typing import Optional 12 | from .. import _globalvar 13 | from . import _parser_handlers 14 | 15 | # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent 16 | 17 | def handle_header_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): 18 | obj.handle_begin_section("header") 19 | end_phrase="end_header" if first_phrase=="begin_header" else r"{/header_section}" 20 | specified_info=[] 21 | while obj.goto_next_line(): 22 | phrases=obj.lines_data[obj.lineindex].split() 23 | if phrases[0] in ("name", "version", "description"): 24 | obj.check_enough_args(phrases, 2) 25 | content=_globalvar.extract_content(obj.lines_data[obj.lineindex]) 26 | content=obj.parse_content(content, pure_name=phrases[0]!="description") 27 | obj.write_infofile( \ 28 | obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ 29 | _globalvar.generator_info_filename.format(info=phrases[0]),\ 30 | content,obj.lineindex+1,phrases[0]) # e.g. [...]/theme-info/1/clithemeinfo_name 31 | elif phrases[0] in ("locales", "supported_apps"): 32 | obj.check_enough_args(phrases, 2) 33 | content=obj.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() 34 | obj.write_infofile_newlines( \ 35 | obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ 36 | _globalvar.generator_info_v2filename.format(info=phrases[0]),\ 37 | content,obj.lineindex+1,phrases[0]) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 38 | elif phrases[0] in ("locales_block", "supported_apps_block", "description_block", "[locales]", "[supported_apps]", "[description]"): 39 | obj.check_extra_args(phrases, 1, use_exact_count=True) 40 | # handle block input 41 | content=""; file_name="" 42 | endphrase="end_block" 43 | if not phrases[0].endswith("_block"): endphrase=phrases[0].replace("[", "[/") 44 | if phrases[0] in ("description_block", "[description]"): 45 | content=obj.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase=endphrase) 46 | file_name=_globalvar.generator_info_filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) 47 | else: 48 | content=obj.handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=endphrase, disable_substesc=True) 49 | file_name=_globalvar.generator_info_v2filename.format(info=re.sub(r'_block$', '', phrases[0]).replace('[','').replace(']','')) 50 | obj.write_infofile( \ 51 | obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name, \ 52 | file_name,\ 53 | content,obj.lineindex+1,re.sub(r'_block$','',phrases[0])) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 54 | elif obj.handle_setters(): pass 55 | elif phrases[0]==end_phrase: 56 | obj.check_extra_args(phrases, 1, use_exact_count=True) 57 | if not "name" in specified_info: 58 | obj.handle_error(obj.fd.feof("missing-info-err", "{sect_name} section missing required entries: {entries}", sect_name="header", entries="name")) 59 | obj.handle_end_section("header") 60 | break 61 | else: obj.handle_invalid_phrase(phrases[0]) 62 | specified_info.append(phrases[0]) -------------------------------------------------------------------------------- /src/clitheme/_generator/_manpage_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | substrules_section parser function (internal module) 9 | """ 10 | import os 11 | import sys 12 | from typing import Optional, List 13 | from .. import _globalvar 14 | from . import _parser_handlers 15 | 16 | # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent 17 | 18 | def handle_manpage_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): 19 | obj.handle_begin_section("manpage") 20 | end_phrase="{/manpage_section}" 21 | while obj.goto_next_line(): 22 | phrases=obj.lines_data[obj.lineindex].split() 23 | def get_file_content(filepath: List[str]) -> str: 24 | # determine file path 25 | parent_dir="" 26 | # if no filename provided, use current working directory as parent path; else, use the directory the file is in as the parent path 27 | if obj.filename.strip()!="": 28 | parent_dir+=os.path.dirname(obj.filename) 29 | file_dir=parent_dir+("/" if parent_dir!="" else "")+_globalvar.splitarray_to_string(filepath).replace(" ","/") 30 | # get file content 31 | orig_stdout=sys.stdout 32 | sys.stdout=sys.__stdout__ 33 | is_stdin=_globalvar.handle_stdin_prompt(file_dir) 34 | filecontent: str 35 | try: filecontent=open(file_dir, 'r', encoding="utf-8").read() 36 | except: obj.handle_error(obj.fd.feof("include-file-read-err", "Line {num}: unable to read file \"{filepath}\":\n{error_msg}", num=str(obj.lineindex+1), filepath=obj.fmt(file_dir), error_msg=sys.exc_info()[1]), not_syntax_error=True) 37 | if is_stdin: print() 38 | sys.stdout=orig_stdout 39 | # write manpage files in theme-info for db migration feature to work successfully 40 | obj.write_manpage_file(filepath, filecontent, -1, custom_parent_path=obj.path+"/"+_globalvar.generator_info_pathname+"/"+obj.custom_infofile_name+"/manpage_data") 41 | return filecontent 42 | if phrases[0]=="[file_content]": 43 | def handle(p: List[str]) -> List[str]: 44 | obj.check_enough_args(p, 2) 45 | filepath=obj.parse_content(_globalvar.splitarray_to_string(p[1:]), pure_name=True).split() 46 | # sanity check the file path 47 | if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: 48 | 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)) 49 | return filepath 50 | file_paths=[handle(phrases)] 51 | # handle additional [file_content] phrases 52 | prev_line_index=obj.lineindex 53 | while obj.goto_next_line(): 54 | p=obj.lines_data[obj.lineindex].split() 55 | if p[0]=="[file_content]": 56 | prev_line_index=obj.lineindex 57 | file_paths.append(handle(p)) 58 | else: 59 | obj.lineindex=prev_line_index 60 | break 61 | content=obj.handle_block_input(preserve_indents=True, preserve_empty_lines=True, end_phrase="[/file_content]") 62 | for filepath in file_paths: 63 | obj.write_manpage_file(filepath, content, obj.lineindex+1) 64 | elif phrases[0]=="include_file": 65 | obj.check_enough_args(phrases, 2) 66 | filepath=obj.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() 67 | if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: 68 | 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)) 69 | 70 | filecontent=get_file_content(filepath) 71 | # expect "as" clause on next line 72 | if obj.goto_next_line() and len(obj.lines_data[obj.lineindex].split())>0 and obj.lines_data[obj.lineindex].split()[0]=="as": 73 | target_file=obj.parse_content(_globalvar.splitarray_to_string(obj.lines_data[obj.lineindex].split()[1:]), pure_name=True).split() 74 | if _globalvar.sanity_check(_globalvar.splitarray_to_string(target_file))==False: 75 | 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)) 76 | obj.write_manpage_file(target_file, filecontent, obj.lineindex+1) 77 | else: 78 | 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))) 79 | elif phrases[0]=="[include_file]": 80 | obj.check_enough_args(phrases, 2) 81 | filepath=obj.parse_content(_globalvar.splitarray_to_string(phrases[1:]), pure_name=True).split() 82 | if _globalvar.sanity_check(_globalvar.splitarray_to_string(filepath))==False: 83 | 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)) 84 | filecontent=get_file_content(filepath) 85 | while obj.goto_next_line(): 86 | p=obj.lines_data[obj.lineindex].split() 87 | if p[0]=="as": 88 | obj.check_enough_args(p, 2) 89 | target_file=obj.parse_content(_globalvar.splitarray_to_string(obj.lines_data[obj.lineindex].split()[1:]), pure_name=True).split() 90 | if _globalvar.sanity_check(_globalvar.splitarray_to_string(target_file))==False: 91 | 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)) 92 | obj.write_manpage_file(target_file, filecontent, obj.lineindex+1) 93 | elif p[0]=="[/include_file]": 94 | obj.check_extra_args(p, 1, use_exact_count=True) 95 | break 96 | else: obj.handle_invalid_phrase(phrases[0]) 97 | elif obj.handle_setters(): pass 98 | elif phrases[0]==end_phrase: 99 | obj.check_extra_args(phrases, 1, use_exact_count=True) 100 | obj.handle_end_section("manpage") 101 | break 102 | else: obj.handle_invalid_phrase(phrases[0]) 103 | -------------------------------------------------------------------------------- /src/clitheme/_generator/_substrules_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | substrules_section parser function (internal module) 9 | """ 10 | import os 11 | import copy 12 | from typing import Optional 13 | from .. import _globalvar 14 | from . import _parser_handlers 15 | 16 | # spell-checker:ignore infofile splitarray datapath lineindex banphrases cmdmatch minspaces blockinput optline matchoption endphrase filecontent 17 | 18 | def handle_substrules_section(obj: _parser_handlers.GeneratorObject, first_phrase: str): 19 | obj.handle_begin_section("substrules") 20 | end_phrase=r"{/substrules_section}" 21 | command_filters: Optional[list]=None 22 | command_filter_strictness=0 23 | # If True, reset foregroundonly option to beforehand during next command filter 24 | outline_foregroundonly=None 25 | def reset_outline_foregroundonly(): 26 | """ 27 | Set foregroundonly option to false if foregroundonly option is "inline" and not enabled previously 28 | """ 29 | nonlocal outline_foregroundonly 30 | if outline_foregroundonly!=None: 31 | obj.global_options['foregroundonly']=outline_foregroundonly 32 | outline_foregroundonly=None 33 | # initialize the database 34 | if os.path.exists(obj.path+"/"+_globalvar.db_filename): 35 | try: obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) 36 | except obj.db_interface.need_db_regenerate: 37 | from ..exec import _check_regenerate_db 38 | if not _check_regenerate_db(obj.path): obj.handle_error(obj.fd.reof("db-regenerate-fail-err", "Failed to migrate existing substrules database; try performing the operation without using \"--overlay\""), not_syntax_error=True) 39 | obj.db_interface.connect_db(path=obj.path+"/"+_globalvar.db_filename) 40 | else: obj.db_interface.init_db(obj.path+"/"+_globalvar.db_filename) 41 | obj.db_interface.debug_mode=not obj.silence_warn 42 | while obj.goto_next_line(): 43 | phrases=obj.lines_data[obj.lineindex].split() 44 | if phrases[0]=="[filter_commands]": 45 | obj.check_extra_args(phrases, 1, use_exact_count=True) 46 | reset_outline_foregroundonly() 47 | content=obj.handle_block_input(preserve_indents=False, preserve_empty_lines=False, end_phrase=r"[/filter_commands]", disallow_other_options=False, disable_substesc=True) 48 | # read commands 49 | command_strings=content.splitlines() 50 | 51 | strictness=0 52 | # parse strictcmdmatch, exactcmdmatch, and other cmdmatch options here 53 | got_options=copy.copy(obj.global_options) 54 | inline_options={} 55 | if len(obj.lines_data[obj.lineindex].split())>1: 56 | 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) 57 | inline_options=obj.parse_options(obj.lines_data[obj.lineindex].split()[1:], merge_global_options=False, allowed_options=obj.block_input_options+obj.command_filter_options) 58 | for this_option in got_options: 59 | if this_option=="strictcmdmatch" and got_options['strictcmdmatch']==True: 60 | strictness=1 61 | elif this_option=="exactcmdmatch" and got_options['exactcmdmatch']==True: 62 | strictness=2 63 | elif this_option=="smartcmdmatch" and got_options['smartcmdmatch']==True: 64 | strictness=-1 65 | elif this_option=="foregroundonly" and "foregroundonly" in inline_options.keys(): 66 | outline_foregroundonly=obj.global_options.get('foregroundonly')==True 67 | obj.global_options['foregroundonly']=inline_options['foregroundonly'] 68 | command_filters=[] 69 | for cmd in command_strings: 70 | command_filters.append(cmd.strip()) 71 | command_filter_strictness=strictness 72 | elif phrases[0]=="filter_command": 73 | obj.check_enough_args(phrases, 2) 74 | reset_outline_foregroundonly() 75 | content=_globalvar.splitarray_to_string(phrases[1:]) 76 | content=obj.parse_content(content, pure_name=True) 77 | strictness=0 78 | for this_option in obj.global_options: 79 | if this_option=="strictcmdmatch" and obj.global_options['strictcmdmatch']==True: 80 | strictness=1 81 | elif this_option=="exactcmdmatch" and obj.global_options['exactcmdmatch']==True: 82 | strictness=2 83 | elif this_option=="smartcmdmatch" and obj.global_options['smartcmdmatch']==True: 84 | strictness=-1 85 | command_filters=[content] 86 | command_filter_strictness=strictness 87 | elif phrases[0]=="unset_filter_command": 88 | obj.check_extra_args(phrases, 1, use_exact_count=True) 89 | reset_outline_foregroundonly() 90 | command_filters=None 91 | elif phrases[0] in ("[subst_string]", "[substitute_string]", "[subst_regex]", "[substitute_regex]"): 92 | obj.check_enough_args(phrases, 2) 93 | options={"effective_commands": copy.copy(command_filters), 94 | "is_regex": phrases[0] in ("[subst_regex]", "[substitute_regex]"), 95 | "strictness": command_filter_strictness} 96 | match_pattern=_globalvar.extract_content(obj.lines_data[obj.lineindex]) 97 | obj.handle_entry(match_pattern, start_phrase=phrases[0], end_phrase=phrases[0].replace('[', '[/'), is_substrules=True, substrules_options=options) 98 | elif obj.handle_setters(): pass 99 | elif phrases[0]==end_phrase: 100 | obj.check_extra_args(phrases, 1, use_exact_count=True) 101 | obj.handle_end_section("substrules") 102 | break 103 | else: obj.handle_invalid_phrase(phrases[0]) -------------------------------------------------------------------------------- /src/clitheme/_generator/db_interface.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Interface for adding and matching substitution entries in database (internal module) 9 | """ 10 | 11 | import sys 12 | import os 13 | import sqlite3 14 | import re 15 | import copy 16 | import uuid 17 | import gc 18 | from typing import Optional, List, Tuple, Dict 19 | from .. import _globalvar, frontend 20 | 21 | # spell-checker:ignore matchoption cmdlist exactmatch rowid pids tcpgrp 22 | 23 | connection=sqlite3.connect(":memory:") # placeholder 24 | db_path="" 25 | debug_mode=False 26 | fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") 27 | 28 | class need_db_regenerate(Exception): pass 29 | class bad_pattern(Exception): pass 30 | class db_not_found(Exception): pass 31 | 32 | def _handle_warning(message: str): 33 | if debug_mode: print(fd.feof("warning-str", "Warning: {msg}", msg=message)) 34 | def init_db(file_path: str): 35 | global connection, db_path 36 | db_path=file_path 37 | connection=sqlite3.connect(file_path) 38 | # create the table 39 | # command_match_strictness: 0: default match options, 1: must start with pattern, 2: must exactly equal pattern 40 | # stdout_stderr_only: 0: no limiter, 1: match stdout only, 2: match stderr only 41 | connection.execute(f"CREATE TABLE {_globalvar.db_data_tablename} ( \ 42 | match_pattern TEXT NOT NULL, \ 43 | substitute_pattern TEXT NOT NULL, \ 44 | is_regex INTEGER NOT NULL, \ 45 | unique_id TEXT NOT NULL, \ 46 | file_id TEXT NOT NULL, \ 47 | effective_command TEXT, \ 48 | effective_locale TEXT, \ 49 | command_match_strictness INTEGER NOT NULL, \ 50 | foreground_only INTEGER NOT NULL, \ 51 | end_match_here INTEGER NOT NULL, \ 52 | stdout_stderr_only INTEGER NOT NULL \ 53 | );") 54 | connection.execute(f"CREATE TABLE {_globalvar.db_data_tablename}_version (value INTEGER NOT NULL);") 55 | connection.execute(f"INSERT INTO {_globalvar.db_data_tablename}_version (value) VALUES (?)", (_globalvar.db_version,)) 56 | connection.commit() 57 | def connect_db(path: str=f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): 58 | global db_path 59 | db_path=path 60 | if not os.path.exists(path): 61 | raise FileNotFoundError("No theme set or theme does not contain substrules") 62 | global connection 63 | connection=sqlite3.connect(db_path) 64 | # check db version 65 | version=int(connection.execute(f"SELECT value FROM {_globalvar.db_data_tablename}_version").fetchone()[0]) 66 | if version!=_globalvar.db_version: 67 | raise need_db_regenerate 68 | 69 | def add_subst_entry(match_pattern: str, substitute_pattern: str, effective_commands: Optional[list], effective_locale: Optional[str]=None, is_regex: bool=True, command_match_strictness: int=0, end_match_here: bool=False, stdout_stderr_matchoption: int=0, foreground_only: bool=False, unique_id: uuid.UUID=uuid.UUID(int=0), file_id: uuid.UUID=uuid.UUID(int=0), line_number_debug: str="-1"): 70 | if unique_id==uuid.UUID(int=0): unique_id=uuid.uuid4() 71 | cmdlist: List[str]=[] 72 | try: re.sub(match_pattern, substitute_pattern, "") # test if patterns are valid 73 | except: raise bad_pattern(str(sys.exc_info()[1])) 74 | # handle condition where no effective_locale is specified ("default") 75 | locale_condition="AND effective_locale=?" if effective_locale!=None else "AND typeof(effective_locale)=typeof(?)" 76 | insert_values=["match_pattern", "substitute_pattern", "effective_command", "is_regex", "command_match_strictness", "end_match_here", "effective_locale", "stdout_stderr_only", "unique_id", "foreground_only", "file_id"] 77 | if effective_commands!=None and len(effective_commands)>0: 78 | for cmd in effective_commands: 79 | # remove extra spaces in the command 80 | cmdlist.append(re.sub(r" {2,}", " ", cmd).strip()) 81 | else: 82 | # remove any existing values with the same match_pattern 83 | match_condition=f"match_pattern=? AND typeof(effective_command)=typeof(null) {locale_condition} AND stdout_stderr_only=? AND is_regex=?" 84 | match_params=(match_pattern, effective_locale, stdout_stderr_matchoption, is_regex) 85 | if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: 86 | _handle_warning(fd.feof("repeated-substrules-warn", "Repeated substrules entry at line {num}, overwriting", num=line_number_debug)) 87 | connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params) 88 | # insert the entry into the main table 89 | connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern, substitute_pattern, None, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption, str(unique_id), foreground_only, str(file_id))) 90 | for cmd in cmdlist: 91 | # remove any existing values with the same match_pattern and effective_command 92 | strictness_condition="" 93 | # if command_match_strictness==2: strictness_condition="AND command_match_strictness=2" 94 | match_condition=f"match_pattern=? AND effective_command=? {strictness_condition} {locale_condition} AND stdout_stderr_only=? AND is_regex=?" 95 | match_params=(match_pattern, cmd, effective_locale, stdout_stderr_matchoption, is_regex) 96 | if len(connection.execute(f"SELECT * FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params).fetchall())>0: 97 | _handle_warning(fd.feof("repeated-substrules-warn", "Repeated substrules entry at line {num}, overwriting", num=line_number_debug)) 98 | connection.execute(f"DELETE FROM {_globalvar.db_data_tablename} WHERE {match_condition};", match_params) 99 | # insert the entry into the main table 100 | connection.execute(f"INSERT INTO {_globalvar.db_data_tablename} ({','.join(insert_values)}) VALUES ({','.join('?'*len(insert_values))});", (match_pattern, substitute_pattern, cmd, is_regex, command_match_strictness, end_match_here, effective_locale, stdout_stderr_matchoption, str(unique_id), foreground_only, str(file_id))) 101 | connection.commit() 102 | 103 | ## Database fetch caching 104 | 105 | _db_last_state: Optional[float]=None 106 | _matches_cache: Dict[Optional[str],List[tuple]]={} 107 | 108 | def _is_db_updated() -> bool: 109 | global _db_last_state 110 | cur_state: Optional[float] 111 | try: 112 | # Check modification time 113 | cur_state=os.stat(db_path).st_mtime 114 | except FileNotFoundError: cur_state=None 115 | updated=False 116 | if cur_state!=_db_last_state: 117 | updated=True 118 | _db_last_state=cur_state 119 | return updated 120 | 121 | def _fetch_matches(command: Optional[str]) -> List[tuple]: 122 | global _matches_cache 123 | updated=_is_db_updated() 124 | if _db_last_state==None: raise db_not_found("file at db_path does not exist") 125 | if updated or _matches_cache.get(command)==None: 126 | if updated: 127 | _matches_cache.clear() 128 | gc.collect() # Reduce memory leak 129 | _matches_cache[command]=_get_matches(command) 130 | return _matches_cache[command] 131 | 132 | ## Output processing and matching 133 | 134 | def _get_matches(command: Optional[str]) -> List[tuple]: 135 | _connection=sqlite3.connect(db_path) 136 | final_cmdlist=[] 137 | final_cmdlist_exactmatch=[] 138 | if command!=None and len(command.split())>0: 139 | # command without paths (e.g. /usr/bin/bash -> bash) 140 | stripped_command=os.path.basename(command.split()[0])+(" "+_globalvar.splitarray_to_string(command.split()[1:]) if len(command.split())>1 else '') 141 | cmdlist_items=["effective_command", "command_match_strictness"] 142 | # obtain a list of effective_command with the same first term 143 | 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() 144 | # also include one-phrase commands 145 | 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() 146 | # sort by number of phrases (greatest to least) 147 | def split_len(obj: tuple) -> int: return len(obj[0].split()) 148 | cmdlist.sort(key=split_len, reverse=True) 149 | # prioritize effective_command with exact match requirement 150 | 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 151 | # attempt to find matching command 152 | for target_command in [command, stripped_command]: 153 | for tp in cmdlist: 154 | match_cmd=tp[0].strip() 155 | strictness=tp[1] 156 | if _check_strictness(match_cmd, strictness, target_command)==True: 157 | # if found matching target_command 158 | if match_cmd not in final_cmdlist: 159 | final_cmdlist.append(match_cmd) 160 | final_cmdlist_exactmatch.append(strictness==2) 161 | matches=[] 162 | def fetch_matches_by_locale(filter_condition: str, filter_data: tuple=tuple()): 163 | fetch_items=["match_pattern", "substitute_pattern", "is_regex", "end_match_here", "stdout_stderr_only", "unique_id", "foreground_only", "effective_command", "command_match_strictness", "file_id"] 164 | # get locales 165 | locales=_globalvar.get_locale() 166 | nonlocal matches 167 | # try the ones with locale defined 168 | for this_locale in locales: 169 | 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() 170 | if len(fetch_data)>0: 171 | matches+=fetch_data 172 | # else, fetches the ones without locale defined 173 | 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() 174 | if len(final_cmdlist)>0: 175 | for x in range(len(final_cmdlist)): 176 | cmd=final_cmdlist[x] 177 | # prioritize exact match 178 | if final_cmdlist_exactmatch[x]==True: fetch_matches_by_locale("effective_command=? AND command_match_strictness=2", (cmd,)) 179 | # also append matches with other strictness 180 | fetch_matches_by_locale("effective_command=? AND command_match_strictness!=2", (cmd,)) 181 | fetch_matches_by_locale("typeof(effective_command)=typeof(null)") 182 | return matches 183 | 184 | def _check_strictness(match_cmd: str, strictness: int, target_command: str): 185 | def process_smartcmdmatch_phrases(match_cmd: str) -> List[str]: 186 | match_cmd_phrases=[] 187 | for p in range(len(match_cmd.split())): 188 | ph=match_cmd.split()[p] 189 | results=re.search(r"^-([^-]+)$",ph) 190 | if p>0 and results!=None: 191 | for character in results.groups()[0]: match_cmd_phrases.append("-"+character) 192 | else: match_cmd_phrases.append(ph) 193 | return match_cmd_phrases 194 | success=True 195 | if strictness==1: # must start with pattern in terms of space-separated phrases 196 | condition=len(match_cmd.split())<=len(target_command.split()) and target_command.split()[:len(match_cmd.split())]==match_cmd.split() 197 | if not condition==True: success=False 198 | elif strictness==2: # must equal to pattern 199 | if not re.sub(r" {2,}", " ", target_command).strip()==match_cmd: success=False 200 | elif strictness==-1: # smartcmdmatch: split phrases starting with one '-' and split them. Then, perform strictness==0 operation 201 | # process both phrases 202 | match_cmd_phrases=process_smartcmdmatch_phrases(match_cmd) 203 | command_phrases=process_smartcmdmatch_phrases(target_command) 204 | for phrase in match_cmd_phrases: 205 | if phrase not in command_phrases: success=False 206 | else: # implying strictness==0; must contain all phrases in pattern 207 | for phrase in match_cmd.split(): 208 | if phrase not in target_command.split(): success=False 209 | return success 210 | 211 | def match_content(content: bytes, command: Optional[str]=None, is_stderr: bool=False, pids: Tuple[int,int]=(-1,-1)) -> bytes: 212 | # pids: (main_pid, current_tcpgrp) 213 | 214 | # Match order: 215 | # 1. Match rules with exactcmdmatch option set 216 | # 2. Match rules with command filter having the same first phrase 217 | # - Command filters with greater number of phrases are prioritized over others 218 | # 3. Match rules without command filter 219 | 220 | # retrieve a list of effective commands matching first argument 221 | matches=_fetch_matches(command) 222 | content_str=copy.copy(content) 223 | content_str=_handle_subst(matches, content_str, is_stderr, pids, command) 224 | return content_str 225 | 226 | # timeout value for each match operation 227 | match_timeout=_globalvar.output_subst_timeout 228 | 229 | def _handle_subst(matches: List[tuple], content: bytes, is_stderr: bool, pids: Tuple[int,int], target_command: Optional[str]) -> bytes: 230 | content_str=copy.copy(content) 231 | encountered_ids=set() 232 | skipped_files=set() # File ids skipped with endmatchhere option 233 | for match_data in matches: 234 | if match_data[4]!=0 and is_stderr+1!=match_data[4]: continue # check stdout/stderr constraint 235 | if match_data[5] in encountered_ids: continue # check uuid 236 | else: encountered_ids.add(match_data[5]) 237 | # check if corresponding file is skipped due to endmatchhere 238 | if match_data[9] in skipped_files: continue 239 | # Check strictness 240 | if target_command!=None and match_data[7]!=None and \ 241 | _check_strictness(match_data[7], match_data[8], \ 242 | os.path.basename(target_command.split()[0])+(" "+_globalvar.splitarray_to_string(target_command.split()[1:]) if len(target_command.split())>1 else ''))==False: continue 243 | if match_data[6]==True: # Foreground only 244 | if pids[0]!=pids[1]: continue 245 | matched=False 246 | if match_data[2]==True: # is regex 247 | try: 248 | ret_val: tuple=re.subn(match_data[0], match_data[1], content_str.decode('utf-8')) 249 | matched=ret_val[1]>0 250 | content_str=bytes(ret_val[0], 'utf-8') 251 | except UnicodeDecodeError: 252 | ret_val: tuple=re.subn(bytes(match_data[0],'utf-8'), bytes(match_data[1], 'utf-8'), content_str) 253 | matched=ret_val[1]>0 254 | content_str=ret_val[0] 255 | else: # is string 256 | try: 257 | matched=match_data[0] in content_str.decode('utf-8') 258 | content_str=bytes(content_str.decode('utf-8').replace(match_data[0], match_data[1]), 'utf-8') 259 | except UnicodeDecodeError: 260 | matched=bytes(match_data[0], 'utf-8') in content_str 261 | content_str=content_str.replace(bytes(match_data[0],'utf-8'), bytes(match_data[1],'utf-8')) 262 | if match_data[3]==True and matched: # endmatchhere is set 263 | skipped_files.add(match_data[9]) 264 | return content_str 265 | -------------------------------------------------------------------------------- /src/clitheme/_get_resource.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Script to get contents of file inside the module 9 | """ 10 | import os 11 | l=__file__.split(os.sep) 12 | l.pop() 13 | final_str="" # directory where the script files are in 14 | for part in l: 15 | final_str+=part+os.sep 16 | def read_file(path: str) -> str: 17 | return open(final_str+os.sep+path, encoding="utf-8").read() -------------------------------------------------------------------------------- /src/clitheme/_globalvar.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Global variable definitions for clitheme 9 | """ 10 | 11 | import io 12 | import os 13 | import sys 14 | import re 15 | import string 16 | import stat 17 | from copy import copy 18 | from . import _version 19 | from typing import List 20 | 21 | # spell-checker:ignoreRegExp banphrase[s]{0,1} 22 | 23 | error_msg_str= \ 24 | """[clitheme] Error: unable to get your home directory or invalid home directory information. 25 | Please make sure that the {var} environment variable is set correctly. 26 | Try restarting your terminal session to fix this issue.""" 27 | 28 | clitheme_version=_version.__version__ 29 | 30 | ## Core data paths 31 | clitheme_root_data_path="" 32 | if os.name=="posix": # Linux/macOS only: Try to get XDG_DATA_HOME if possible 33 | try: 34 | clitheme_root_data_path=os.environ["XDG_DATA_HOME"]+"/clitheme" 35 | except KeyError: pass 36 | 37 | if clitheme_root_data_path=="": # prev did not succeed 38 | try: 39 | if os.name=="nt": # Windows 40 | clitheme_root_data_path=os.environ["USERPROFILE"]+"\\.local\\share\\clitheme" 41 | else: 42 | if not os.environ['HOME'].startswith('/'): # sanity check 43 | raise KeyError 44 | clitheme_root_data_path=os.environ["HOME"]+"/.local/share/clitheme" 45 | except KeyError: 46 | var="$HOME" 47 | if os.name=="nt": 48 | var=r"%USERPROFILE%" 49 | print(error_msg_str.format(var=var)) 50 | exit(1) 51 | clitheme_temp_root="/tmp" if os.name!="nt" else os.environ['TEMP'] 52 | 53 | ## _generator file and folder names 54 | generator_info_pathname="theme-info" # e.g. ~/.local/share/clitheme/theme-info 55 | generator_data_pathname="theme-data" # e.g. ~/.local/share/clitheme/theme-data 56 | generator_manpage_pathname="manpages" # e.g. ~/.local/share/clitheme/manpages 57 | generator_index_filename="current_theme_index" # e.g. [...]/theme-info/current_theme_index 58 | generator_info_filename="clithemeinfo_{info}" # e.g. [...]/theme-info/1/clithemeinfo_name 59 | generator_info_v2filename=generator_info_filename+"_v2" # e.g. [...]/theme-info/1/clithemeinfo_description_v2 60 | 61 | ## _generator.db_interface file and table names 62 | db_data_tablename="clitheme_subst_data" 63 | db_filename="subst-data.db" # e.g. ~/.local/share/clitheme/subst-data.db 64 | db_version=4 65 | 66 | ## clitheme-exec timeout value for each output substitution operation 67 | output_subst_timeout=0.4 68 | 69 | ## Sanity check function 70 | entry_banphrases=['<', '>', ':', '"', '/', '\\', '|', '?', '*'] 71 | startswith_banphrases=['.'] 72 | banphrase_error_message="cannot contain '{char}'" 73 | banphrase_error_message_orig=copy(banphrase_error_message) 74 | startswith_error_message="cannot start with '{char}'" 75 | startswith_error_message_orig=copy(startswith_error_message) 76 | # function to check whether the pathname contains invalid phrases 77 | # - cannot start with . 78 | # - cannot contain banphrases 79 | sanity_check_error_message="" 80 | # retrieve the entry only once to avoid dead loop in frontend.FetchDescriptor callbacks 81 | msg_retrieved=False 82 | from . import frontend, _get_resource 83 | def sanity_check(path: str, use_orig: bool=False) -> bool: 84 | def retrieve_entry(): 85 | # retrieve the entry (only for the first time) 86 | global msg_retrieved 87 | global sanity_check_error_message, banphrase_error_message, startswith_error_message 88 | if not msg_retrieved: 89 | msg_retrieved=True 90 | f=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") 91 | banphrase_error_message=f.feof("sanity-check-msg-banphrase-err", banphrase_error_message, char="{char}") 92 | startswith_error_message=f.feof("sanity-check-msg-startswith-err", startswith_error_message, char="{char}") 93 | global sanity_check_error_message 94 | for p in path.split(): 95 | for b in startswith_banphrases: 96 | if p.startswith(b): 97 | if not use_orig: retrieve_entry() 98 | sanity_check_error_message=startswith_error_message.format(char=b) if not use_orig else startswith_error_message_orig.format(char=b) 99 | return False 100 | for b in entry_banphrases: 101 | if p.find(b)!=-1: 102 | if not use_orig: retrieve_entry() 103 | sanity_check_error_message=banphrase_error_message.format(char=b) if not use_orig else banphrase_error_message_orig.format(char=b) 104 | return False 105 | return True 106 | 107 | ## Convenience functions 108 | 109 | class _direct_exit(Exception): 110 | def __init__(self, code): 111 | """ 112 | Custom exception for handling return code inside another function callback 113 | """ 114 | self.code=code 115 | def splitarray_to_string(split_content: List[str]) -> str: 116 | final="" 117 | for phrase in split_content: 118 | final+=phrase+" " 119 | return final.strip() 120 | def extract_content(line_content: str, begin_phrase_count: int=1) -> str: 121 | results=re.search(r"(?:\s*.+?\s+){"+str(begin_phrase_count)+r"}(?P.+)", line_content.strip()) 122 | if results==None: raise ValueError("Match content failed (no matches)") 123 | else: return results.groupdict()['content'] 124 | def list_directory(dirname: str): 125 | lsdir_result=os.listdir(dirname) 126 | final_result=[] 127 | for name in lsdir_result: 128 | if not name.startswith('.'): 129 | final_result.append(name) 130 | return final_result 131 | def make_printable(content: str) -> str: 132 | final_str="" 133 | for character in content: 134 | if character.isprintable() or character in string.whitespace: final_str+=character 135 | else: 136 | exp=repr(character) 137 | # Remove quotes in repr(character) 138 | exp=re.sub(r"""^(?P['"]?)(?P.+)(?P=quote)$""", r"<\g>", exp) 139 | final_str+=exp 140 | return final_str 141 | def get_locale(debug_mode: bool=False) -> List[str]: 142 | lang=[] 143 | def add_language(target_lang: str): 144 | nonlocal lang 145 | if not sanity_check(target_lang, use_orig=True)==False: 146 | no_encoding=re.sub(r"^(?P.+)[\.].+$", r"\g", target_lang) 147 | lang.append(target_lang) 148 | if no_encoding!=target_lang: lang.append(no_encoding) 149 | else: 150 | if debug_mode: print("[Debug] Locale \"{0}\": sanity check failed ({1})".format(target_lang, sanity_check_error_message)) 151 | 152 | # Skip $LANGUAGE if both $LANG and $LC_ALL is set to C (treat empty as C also) 153 | LANG_value=os.environ["LANG"] if "LANG" in os.environ and os.environ["LANG"].strip()!='' else "C" 154 | LC_ALL_value=os.environ["LC_ALL"] if "LC_ALL" in os.environ and os.environ["LC_ALL"].strip()!='' else "C" 155 | skip_LANGUAGE=(LANG_value=="C" or LANG_value.startswith("C.")) and (LC_ALL_value=="C" or LC_ALL_value.startswith("C.")) 156 | # $LANGUAGE (list of languages separated by colons) 157 | if "LANGUAGE" in os.environ and not skip_LANGUAGE: 158 | if debug_mode: print("[Debug] Using LANGUAGE variable") 159 | target_str=os.environ['LANGUAGE'] 160 | for language in target_str.split(":"): 161 | each_language=language.strip() 162 | if each_language=="": continue 163 | # Ignore en and en_US (See https://wiki.archlinux.org/title/Locale#LANGUAGE:_fallback_locales) 164 | if each_language!="en" and each_language!="en_US": 165 | # Treat C as en_US also 166 | if re.sub(r"^(?P.+)[\.].+$", r"\g", each_language)=="C": 167 | for item in ["en_US", "en"]: 168 | with_encoding=re.subn(r"^.+[\.]", f"{item}.", each_language) # (content, num_of_substitutions) 169 | if with_encoding[1]>0: add_language(with_encoding[0]) 170 | else: add_language(item) 171 | add_language(each_language) 172 | # $LC_ALL 173 | elif "LC_ALL" in os.environ and os.environ["LC_ALL"].strip()!="": 174 | if debug_mode: print("[Debug] Using LC_ALL variable") 175 | target_str=os.environ["LC_ALL"].strip() 176 | add_language(target_str) 177 | # $LANG 178 | elif "LANG" in os.environ and os.environ["LANG"].strip()!="": 179 | if debug_mode: print("[Debug] Using LANG variable") 180 | target_str=os.environ["LANG"].strip() 181 | add_language(target_str) 182 | return lang 183 | 184 | def handle_exception(): 185 | env_var="CLITHEME_SHOW_TRACEBACK" 186 | if env_var in os.environ and os.environ[env_var]=="1": 187 | raise 188 | 189 | def handle_stdin_prompt(path: str) -> bool: 190 | fi=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="cli apply-theme") 191 | is_stdin=False 192 | try: 193 | if os.stat(path).st_ino==os.stat(sys.stdin.fileno()).st_ino: 194 | is_stdin=True 195 | print("\n"+fi.reof("reading-stdin-note", "Reading from standard input")) 196 | if not stat.S_ISFIFO(os.stat(path).st_mode): 197 | print(fi.feof("stdin-interactive-finish-prompt", "Input file content here and press {shortcut} to finish", shortcut="CTRL-D" if os.name=="posix" else "CTRL-Z+")) 198 | except: pass 199 | return is_stdin 200 | 201 | def handle_set_themedef(fr: frontend, debug_name: str): # type: ignore 202 | prev_mode=False 203 | # Prevent interference with other code piping stdout 204 | orig_stdout=sys.stdout 205 | try: 206 | files=["strings/generator-strings.clithemedef.txt", "strings/cli-strings.clithemedef.txt", "strings/exec-strings.clithemedef.txt", "strings/man-strings.clithemedef.txt"] 207 | file_contents=list(map(lambda name: _get_resource.read_file(name), files)) 208 | msg=io.StringIO() 209 | sys.stdout=msg 210 | fr.set_debugmode(True) 211 | if not fr.set_local_themedefs(file_contents): raise RuntimeError("Full log below: \n"+msg.getvalue()) 212 | fr.set_debugmode(prev_mode) 213 | sys.stdout=orig_stdout 214 | except: 215 | sys.stdout=orig_stdout 216 | fr.set_debugmode(prev_mode) 217 | # If pre-release build or manual environment variable flag set, display error 218 | if _version.release<0 or os.environ.get("CLITHEME_SHOW_TRACEBACK")=='1': 219 | print(f"{debug_name} set_local_themedef failed: "+str(sys.exc_info()[1]), file=sys.__stdout__) 220 | handle_exception() 221 | finally: sys.stdout=orig_stdout 222 | def result_sort_cmp(obj1,obj2) -> int: 223 | cmp1='';cmp2='' 224 | try: 225 | cmp1=int(obj1); cmp2=int(obj2) 226 | except ValueError: 227 | cmp1=obj1; cmp2=obj2 228 | if cmp1>cmp2: return 1 229 | elif cmp1==cmp2: return 0 230 | else: return -1 -------------------------------------------------------------------------------- /src/clitheme/_version.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Version information definition file 9 | """ 10 | # spell-checker:ignore buildnumber 11 | 12 | # Version definition file; define the package version here 13 | # The __version__ variable must be a literal string; DO NOT use variables 14 | __version__="2.0-beta3" 15 | major=2 16 | minor=0 17 | release=-1 # -1 stands for "dev" 18 | beta_release=3 # None if not beta 19 | # For PKGBUILD 20 | # version_main CANNOT contain hyphens (-); use underscores (_) instead 21 | version_main="2.0_beta3" 22 | version_buildnumber=1 -------------------------------------------------------------------------------- /src/clitheme/exec/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Module used for clitheme-exec 9 | 10 | - You can access clitheme-exec by invoking this module directly: 'python3 -m clitheme.exec' 11 | - You can also invoke clitheme-exec in scripts using the 'main' function 12 | """ 13 | import sys 14 | import os 15 | import re 16 | import io 17 | import shutil 18 | import functools 19 | def _labeled_print(msg: str): 20 | for line in msg.splitlines(): 21 | print("[clitheme-exec] "+line) 22 | 23 | from .. import _globalvar, cli, frontend 24 | from .._generator import db_interface 25 | from typing import List 26 | 27 | # spell-checker:ignore lsdir showhelp argcount nosubst 28 | 29 | frontend.set_domain("swiftycode") 30 | frontend.set_appname("clitheme") 31 | fd=frontend.FetchDescriptor(subsections="exec") 32 | 33 | # Prevent recursion dead loops and accurately simulate that regeneration is only triggered once 34 | db_already_regenerated=False 35 | 36 | def _check_regenerate_db(dest_root_path: str=_globalvar.clitheme_root_data_path) -> bool: 37 | global db_already_regenerated 38 | try: 39 | # Support environment variable flag to force db regeneration (debug purposes) 40 | if os.environ.get("CLITHEME_REGENERATE_DB")=="1" and not db_already_regenerated: 41 | db_already_regenerated=True 42 | raise db_interface.need_db_regenerate("Forced database regeneration with $CLITHEME_REGENERATE_DB=1") 43 | else: db_interface.connect_db() 44 | except db_interface.need_db_regenerate: 45 | _labeled_print(fd.reof("substrules-update-msg", "Updating database...")) 46 | orig_stdout=sys.stdout 47 | try: 48 | # gather files 49 | search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname 50 | if not os.path.isdir(search_path): raise Exception(search_path+" not directory") 51 | lsdir_result=_globalvar.list_directory(search_path); lsdir_result.sort(key=functools.cmp_to_key(_globalvar.result_sort_cmp)) 52 | lsdir_num=0 53 | for x in lsdir_result: 54 | if os.path.isdir(search_path+"/"+x): lsdir_num+=1 55 | if lsdir_num<1: raise Exception("empty directory") 56 | 57 | file_contents=[] 58 | paths=[] 59 | for pathname in lsdir_result: 60 | target_path=search_path+"/"+pathname 61 | if (not os.path.isdir(target_path)) or re.search(r"^\d+$", pathname.strip())==None: continue # skip current_theme_index file 62 | content=open(target_path+"/file_content", encoding="utf-8").read() 63 | file_contents.append(content) 64 | paths.append(target_path+"/manpage_data/file_content") # small hack/workaround 65 | cli_msg=io.StringIO() 66 | sys.stdout=cli_msg 67 | if not cli.apply_theme(file_contents, filenames=paths, overlay=False, generate_only=True, preserve_temp=True)==0: 68 | raise Exception(fd.reof("db-update-generator-err", "Failed to generate data (full log below):")+"\n"+cli_msg.getvalue()+"\n") 69 | sys.stdout=orig_stdout 70 | try: os.remove(dest_root_path+"/"+_globalvar.db_filename) 71 | except FileNotFoundError: raise 72 | shutil.copy(cli.last_data_path+"/"+_globalvar.db_filename, dest_root_path+"/"+_globalvar.db_filename) 73 | _labeled_print(fd.reof("db-update-success-msg", "Successfully updated database, proceeding execution")) 74 | except: 75 | sys.stdout=orig_stdout 76 | _labeled_print(fd.feof("db-update-err", "An error occurred while updating the database: {msg}\nPlease re-apply the theme and try again", msg=str(sys.exc_info()[1]))) 77 | _globalvar.handle_exception() 78 | return False 79 | except FileNotFoundError: pass 80 | except Exception as exc: 81 | msg=fd.reof("db-invalid-version", "Invalid database version information")\ 82 | if type(exc) in (ValueError, TypeError) else str(sys.exc_info()[1]) 83 | # ValueError: value is not an integer; TypeError: fetched value is None 84 | _labeled_print(fd.feof("db-read-err", "An error occurred while reading the database: {msg}\nPlease re-apply the theme and try again", msg=msg)) 85 | _globalvar.handle_exception() 86 | return False 87 | return True 88 | 89 | def _handle_help_message(full_help: bool=False): 90 | fd2=frontend.FetchDescriptor(subsections="exec help-message") 91 | print(fd2.reof("usage-str", "Usage:")) 92 | print("\tclitheme-exec [--debug] [--debug-color] [--debug-newlines] [--showchars] [--foreground-stat] [--nosubst] [command]") 93 | if not full_help: return 94 | print(fd2.reof("options-str", "Options:")) 95 | print("\t"+fd2.reof("options-debug", "--debug: Display indicator at the beginning of each read output by line")) 96 | print("\t\t"+fd2.reof("options-debug-newlines", "--debug-newlines: Use newlines to display output that does not end on a newline")) 97 | print("\t"+fd2.reof("options-debug-color", "--debug-color: Apply color on output; used to determine stdout or stderr (BETA: stdout/stderr not implemented)")) 98 | print("\t"+fd2.reof("options-showchars", "--showchars: Display various control characters in plain text")) 99 | print("\t"+fd2.reof("options-foreground-stat", "--foreground-stat: Display message when the foreground status of the process changes (value of tcgetpgrp)")) 100 | print("\t"+fd2.reof("options-nosubst", "--nosubst: Do not perform any output substitutions even if a theme is set")) 101 | 102 | def _handle_error(message: str): 103 | print(message) 104 | print(fd.reof("help-usage-prompt", "Run \"clitheme-exec --help\" for usage information")) 105 | return 1 106 | 107 | def main(arguments: List[str]): 108 | """ 109 | Invoke clitheme-exec using the given command line arguments 110 | 111 | Note: the first item in the argument list must be the program name 112 | (e.g. ['clitheme-exec', ] or ['example-app', ]) 113 | """ 114 | # process debug mode arguments 115 | debug_mode=[] 116 | argcount=0 117 | showhelp=False 118 | subst=True 119 | for arg in arguments[1:]: 120 | if not arg.startswith('-'): break 121 | argcount+=1 122 | if arg=="--debug": 123 | debug_mode.append("normal") 124 | elif arg=="--debug-color": 125 | debug_mode.append("color") 126 | elif arg=="--debug-newlines": 127 | debug_mode.append("newlines") 128 | elif arg in ("--showchars", "--debug-showchars"): 129 | debug_mode.append("showchars") 130 | elif arg in ("--foreground-stat", "--debug-foreground"): 131 | debug_mode.append("foreground") 132 | elif arg in ("--nosubst", "--debug-nosubst"): 133 | subst=False 134 | elif arg=="--help": 135 | showhelp=True 136 | else: 137 | return _handle_error(fd.feof("unknown-option-err", "Error: unknown option \"{phrase}\"", phrase=arg)) 138 | if "newlines" in debug_mode and not "normal" in debug_mode: 139 | return _handle_error(fd.reof("debug-newlines-not-with-debug", "Error: \"--debug-newlines\" must be used with \"--debug\" option")) 140 | if len(arguments)<=1+argcount: 141 | if showhelp: 142 | _handle_help_message(full_help=True) 143 | return 0 144 | else: 145 | _handle_help_message() 146 | return _handle_error(fd.reof("no-command-err", "Error: no command specified")) 147 | # check database 148 | if subst: 149 | if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.db_filename}"): 150 | _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not have substrules")) 151 | else: 152 | if not _check_regenerate_db(): return 1 153 | # determine platform 154 | if os.name=="posix": 155 | from . import output_handler_posix 156 | return output_handler_posix.handler_main(arguments[1+argcount:], debug_mode, subst) 157 | elif os.name=="nt": 158 | _labeled_print("Error: Windows platform is not currently supported") 159 | return 1 160 | else: 161 | _labeled_print("Error: Unsupported platform") 162 | return 1 163 | return 0 164 | def _script_main(): # for script 165 | return main(sys.argv) -------------------------------------------------------------------------------- /src/clitheme/exec/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | import sys 3 | exit(main(sys.argv)) -------------------------------------------------------------------------------- /src/clitheme/frontend.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | clitheme frontend interface for accessing entries 9 | 10 | - Create a FetchDescriptor instance and optionally pass information such as domain&app name and subsections 11 | - Use the 'retrieve_entry_or_fallback' or 'reof' function in the instance to retrieve content of an entry definition 12 | - Use the 'format_entry_or_fallback' or 'feof' function in the instance to retrieve and format content of entry definition using str.format 13 | """ 14 | 15 | import os,sys 16 | import random 17 | import string 18 | import re 19 | import hashlib 20 | import shutil 21 | import inspect 22 | from typing import Optional, List, Union, Dict 23 | from . import _globalvar 24 | 25 | # spell-checker:ignore newhash numorig numcur 26 | 27 | data_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_data_pathname 28 | 29 | _setting_defs: Dict[str, type]={ 30 | "domain": str, 31 | "appname": str, 32 | "subsections": str, 33 | "debugmode": bool, 34 | "lang": str, 35 | "disablelang": bool, 36 | } 37 | 38 | _local_settings: Dict[str, Dict[str, Union[None,str,bool]]]={} 39 | 40 | for name in _setting_defs.keys(): 41 | _local_settings[name]={} 42 | 43 | def _get_caller() -> str: 44 | assert len(inspect.stack())>=4, "Cannot determine filename from call stack" 45 | # inspect.stack(): [0: this function, 1: update/get settings, 2: function in frontend module, 3: target calling function] 46 | filename=inspect.stack()[3].filename 47 | # Find the first function in the stack OUTSIDE of frontend module 48 | # (The stack[3] may also be some function in frontend) 49 | for s in inspect.stack()[3:]: 50 | filename=s.filename 51 | if filename!=__file__: break 52 | return filename 53 | 54 | def _update_local_settings(key: str, value: Union[None,str,bool]): 55 | _local_settings[key][_get_caller()]=value 56 | if _get_setting("debugmode", _get_caller())==True: 57 | print(f"[Debug] Set {key}={value} for file \"{_get_caller()}\"") 58 | 59 | 60 | _desc=\ 61 | """ 62 | Set default value for `{}` option in future FetchDescriptor instances. 63 | This setting is valid for the module/code file that invokes this function. 64 | 65 | - Set value=None to unset the default value and use values defined in global variables 66 | - Change global variables (e.g. global_domain, global_debugmode) to set the default value for all files in an invoking module 67 | """ 68 | 69 | def set_domain(value: Optional[str]): _desc.format("domain_name");_update_local_settings("domain", value) 70 | def set_appname(value: Optional[str]): _desc.format("app_name");_update_local_settings("appname", value) 71 | def set_subsections(value: Optional[str]): _desc.format("subsections");_update_local_settings("subsections", value) 72 | def set_debugmode(value: Optional[bool]): _desc.format("debug_mode");_update_local_settings("debugmode", value) 73 | def set_lang(value: Optional[str]): _desc.format("lang");_update_local_settings("lang", value) 74 | def set_disablelang(value: Optional[bool]): _desc.format("disable_lang");_update_local_settings("disablelang", value) 75 | 76 | global_domain="" 77 | global_appname="" 78 | global_subsections="" 79 | global_debugmode=False 80 | global_lang="" # Override locale 81 | global_disablelang=False 82 | 83 | def _get_setting(key: str, caller: Optional[str]=None) -> Union[str,bool]: 84 | # Get local settings 85 | value=_local_settings[key].get(caller if caller!=None else _get_caller()) 86 | if value!=None: return value 87 | # Get global settings if not found 88 | else: 89 | return eval(f"global_{key}") 90 | 91 | _alt_path=None 92 | _alt_path_dirname=None 93 | _alt_path_hash=None 94 | _alt_info_index: int=1 95 | # Support for setting a local definition file 96 | # - Generate the data in a temporary directory named after content hash 97 | # - First try alt_path then data_path 98 | 99 | def set_local_themedef(file_content: str, overlay: bool=False) -> bool: 100 | """ 101 | Sets a local theme definition file for the current frontend instance. 102 | When set, the FetchDescriptor functions will try the local definition before falling back to global theme data. 103 | 104 | - Set overlay=True to overlay on top of existing local definition data (if exists) 105 | 106 | WARNING: Pass the file content in str to this function; DO NOT pass the path to the file. 107 | 108 | This function returns True if successful, otherwise returns False. 109 | """ 110 | from . import _generator 111 | # Determine directory name 112 | h=hashlib.shake_256(bytes(file_content, "utf-8")) 113 | d=h.hexdigest(6) # length of 12 (6*2) 114 | global _alt_path_hash 115 | local_path_hash=_alt_path_hash 116 | # if overlay, update hash with new contents of file 117 | if _alt_path_hash!=None and overlay==True: 118 | newhash="" 119 | for x in range(len(_alt_path_hash)): 120 | chart=string.ascii_uppercase+string.ascii_lowercase+string.digits 121 | numorig=0 122 | numcur=0 123 | if d[x]>='A' and d[x]<='Z': #uppercase letters 124 | numorig=ord(d[x])-ord('A') 125 | elif d[x]>='a' and d[x]<='z': #lowercase letters 126 | numorig=(ord(d[x])-ord('a'))+len(string.ascii_uppercase) 127 | elif d[x]>='0' and d[x]<='9': #digit 128 | numorig=ord(d[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) 129 | if _alt_path_hash[x]>='A' and _alt_path_hash[x]<='Z': #uppercase letters 130 | numcur=ord(_alt_path_hash[x])-ord('A') 131 | elif _alt_path_hash[x]>='a' and _alt_path_hash[x]<='z': #lowercase letters 132 | numcur=(ord(_alt_path_hash[x])-ord('a'))+len(string.ascii_uppercase) 133 | elif _alt_path_hash[x]>='0' and _alt_path_hash[x]<='9': #digit 134 | numcur=ord(_alt_path_hash[x])-ord('0')+len(string.ascii_uppercase+string.ascii_lowercase) 135 | newhash+=chart[(numorig+numcur)%len(chart)] 136 | local_path_hash=newhash 137 | else: local_path_hash=d # else, use generated hash 138 | dir_name=f"clitheme-data-{local_path_hash}" 139 | _generator.generate_custom_path() # prepare _generator.path 140 | global _alt_path_dirname, _alt_info_index 141 | global global_debugmode 142 | path_name=_globalvar.clitheme_temp_root+"/"+dir_name 143 | if _alt_path_dirname!=None and overlay==True: # overlay 144 | if not os.path.exists(path_name): shutil.copytree(_globalvar.clitheme_temp_root+"/"+_alt_path_dirname, _generator.path) 145 | if _get_setting("debugmode"): print("[Debug] set_local_themedef data path: "+path_name) 146 | # Generate data hierarchy as needed 147 | if not os.path.exists(path_name): 148 | _generator.silence_warn=True 149 | return_val: str 150 | d_copy=(global_debugmode, _generator.silence_warn) 151 | try: 152 | # Set this to prevent extra messages from being displayed 153 | global_debugmode=False 154 | return_val=_generator.generate_data_hierarchy(file_content, custom_path_gen=False, custom_infofile_name=str(_alt_info_index)) 155 | _alt_info_index+=1 156 | except SyntaxError: 157 | if _get_setting("debugmode"): print("[Debug] Generator error: "+str(sys.exc_info()[1])) 158 | return False 159 | finally: global_debugmode, _generator.silence_warn=d_copy 160 | if not os.path.exists(path_name): 161 | shutil.copytree(return_val, path_name) 162 | try: shutil.rmtree(return_val) 163 | except: pass 164 | global _alt_path 165 | _alt_path_hash=local_path_hash 166 | _alt_path=path_name+"/"+_globalvar.generator_data_pathname 167 | _alt_path_dirname=dir_name 168 | return True 169 | 170 | def set_local_themedefs(file_contents: List[str], overlay: bool=False): 171 | """ 172 | Sets multiple local theme definition files for the current frontend instance. 173 | When set, the FetchDescriptor functions will try the local definition before falling back to global theme data. 174 | 175 | - Set overlay=True to overlay on top of existing local definition data (if exists) 176 | 177 | WARNING: Pass the file content in str to this function; DO NOT pass the path to the file. 178 | 179 | This function returns True if successful, otherwise returns False. 180 | """ 181 | global _alt_path, _alt_path_hash, _alt_path_dirname 182 | orig=(_alt_path, _alt_path_hash, _alt_path_dirname) 183 | for x in range(len(file_contents)): 184 | content=file_contents[x] 185 | if not set_local_themedef(content, overlay=(x>0 or overlay)): 186 | _alt_path, _alt_path_hash, _alt_path_dirname=orig 187 | return False 188 | return True 189 | 190 | def unset_local_themedef(): 191 | """ 192 | Unset the local theme definition file for the current frontend instance. 193 | After this operation, FetchDescriptor functions will no longer use local definitions. 194 | """ 195 | global _alt_path; _alt_path=None 196 | global _alt_path_dirname; _alt_path_dirname=None 197 | global _alt_path_hash; _alt_path_hash=None 198 | global _alt_info_index; _alt_info_index=1 199 | 200 | class FetchDescriptor(): 201 | """ 202 | Object containing domain and app information used for fetching entries 203 | """ 204 | def __init__(self, domain_name: Optional[str] = None, app_name: Optional[str] = None, subsections: Optional[str] = None, lang: Optional[str] = None, debug_mode: Optional[bool] = None, disable_lang: Optional[bool] = None): 205 | """ 206 | Create a new instance of the object. 207 | 208 | - Provide domain_name and app_name to automatically append them for retrieval functions 209 | - Provide subsections to automatically append them after domain_name+app_name 210 | - Provide lang to override the automatically detected system locale information 211 | - Set debug_mode=True to output underlying operations when retrieving entries (debug purposes only) 212 | - Set disable_lang=True to disable localization detection and use "default" entry for all retrieval operations 213 | """ 214 | # Leave domain and app names blank for global reference 215 | 216 | if domain_name==None: 217 | self.domain_name: str=_get_setting("domain").strip() #type:ignore 218 | else: 219 | self.domain_name=domain_name.strip() 220 | if len(self.domain_name.split())>1: 221 | raise SyntaxError("Only one phrase is allowed for domain_name") 222 | 223 | if app_name==None: 224 | self.app_name: str=_get_setting("appname").strip() #type:ignore 225 | else: 226 | self.app_name=app_name.strip() 227 | if len(self.app_name.split())>1: 228 | raise SyntaxError("Only one phrase is allowed for app_name") 229 | 230 | if subsections==None: 231 | self.subsections: str=_get_setting("subsections").strip() #type:ignore 232 | else: 233 | self.subsections=subsections.strip() 234 | self.subsections=re.sub(" {2,}", " ", self.subsections) 235 | 236 | if lang==None: 237 | self.lang=_get_setting("lang").strip() #type:ignore 238 | else: 239 | self.lang=lang.strip() 240 | 241 | if debug_mode==None: 242 | self.debug_mode: bool=_get_setting("debugmode") #type:ignore 243 | else: 244 | self.debug_mode=debug_mode 245 | 246 | if disable_lang==None: 247 | self.disable_lang: bool=_get_setting("disablelang") #type:ignore 248 | else: 249 | self.disable_lang=disable_lang 250 | 251 | # sanity check the domain, app, and subsections 252 | if _globalvar.sanity_check(self.domain_name+" "+self.app_name+" "+self.subsections, use_orig=True)==False: 253 | raise SyntaxError("Domain, app, or subsection names {}".format(_globalvar.sanity_check_error_message)) 254 | def retrieve_entry_or_fallback(self, entry_path: str, fallback_string: str) -> str: 255 | """ 256 | Attempt to retrieve the entry based on given entry path. 257 | If the entry does not exist, use the provided fallback string instead. 258 | """ 259 | # entry_path e.g. "class-a sample_text" 260 | 261 | # Sanity check the path 262 | if entry_path.strip()=="": 263 | raise SyntaxError("Empty entry name") 264 | if _globalvar.sanity_check(entry_path, use_orig=True)==False: 265 | raise SyntaxError("Entry names and subsections {}".format(_globalvar.sanity_check_error_message)) 266 | lang=[] 267 | # Language handling: see https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Environment-Variables for more information 268 | if not self.disable_lang: 269 | if self.lang!="": 270 | if self.debug_mode: print("[Debug] Locale: Using defined self.lang") 271 | if not _globalvar.sanity_check(self.lang, use_orig=True)==False: 272 | lang=[self.lang] 273 | else: 274 | if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) 275 | else: 276 | if self.debug_mode: print("[Debug] Locale: Using environment variables") 277 | lang=_globalvar.get_locale(debug_mode=self.debug_mode) 278 | 279 | if self.debug_mode: print(f"[Debug] lang: {lang}\n[Debug] entry_path: {entry_path}") 280 | # just being lazy here I don't want to check the variables before using ಥ_ಥ (because it doesn't matter) 281 | path=data_path+"/"+self.domain_name+"/"+self.app_name+"/"+re.sub(" ",r"/", self.subsections) 282 | path2=None 283 | if _alt_path!=None: path2=_alt_path+"/"+self.domain_name+"/"+self.app_name+"/"+re.sub(" ",r"/", self.subsections) 284 | for section in entry_path.split(): 285 | path+="/"+section 286 | if path2!=None: path2+="/"+section 287 | # path with lang, path with lang but without e.g. .UTF-8, path with no lang 288 | possible_paths=[] 289 | for l in lang: 290 | possible_paths.append(path+"__"+l) 291 | possible_paths.append(path) 292 | if path2!=None: 293 | for l in lang: 294 | possible_paths.append(path2+"__"+l) 295 | possible_paths.append(path2) 296 | for p in possible_paths: 297 | if self.debug_mode: print("Trying "+p, end=" ...") 298 | try: 299 | f=open(p,'r', encoding="utf-8") 300 | # since the generator adds an extra newline in the entry data, we need to remove it 301 | dat=re.sub(r"\n\Z", "", f.read()) 302 | if self.debug_mode: print("Success:\n> "+dat) 303 | return dat 304 | except (FileNotFoundError, IsADirectoryError): 305 | if self.debug_mode: print("Failed") 306 | return fallback_string 307 | 308 | reof=retrieve_entry_or_fallback # a shorter alias of the function 309 | 310 | def format_entry_or_fallback(self, entry_path: str, fallback_string: str, *args, **kwargs) -> str: 311 | """ 312 | Attempt to retrieve and format the entry using str.format based on given entry path and arguments. 313 | If the entry does not exist or an error occurs while formatting the entry string, use the provided fallback string instead. 314 | """ 315 | # retrieve the entry 316 | if not self.entry_exists(entry_path): 317 | if self.debug_mode: print("[Debug] Entry not found") 318 | return fallback_string.format(*args, **kwargs) 319 | entry=self.retrieve_entry_or_fallback(entry_path, "") 320 | # format the string 321 | try: 322 | return entry.format(*args, **kwargs) 323 | except Exception: 324 | if self.debug_mode: print("[Debug] Format error: {err}".format(err=str(sys.exc_info()[1]))) 325 | return fallback_string.format(*args, **kwargs) 326 | feof=format_entry_or_fallback # a shorter alias of the function 327 | 328 | def entry_exists(self, entry_path: str) -> bool: 329 | """ 330 | Check if the entry at the given entry path exists. 331 | Returns true if exists and false if does not exist. 332 | """ 333 | # just being lazy here I don't want to rewrite this all over again ಥ_ಥ 334 | fallback_string="" 335 | for x in range(30): 336 | fallback_string+=random.choice(string.ascii_letters) 337 | received_content=self.retrieve_entry_or_fallback(entry_path, fallback_string) 338 | if received_content.strip()==fallback_string: return False 339 | else: return True 340 | -------------------------------------------------------------------------------- /src/clitheme/man.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | """ 8 | Module used for clitheme-man 9 | 10 | - You can access clitheme-man by invoking this module directly: 'python3 -m clitheme.man' 11 | - You can also invoke clitheme-man in scripts using the 'main' function 12 | """ 13 | import sys 14 | import os 15 | import subprocess 16 | import shutil 17 | import signal 18 | import time 19 | from . import _globalvar, frontend 20 | from typing import List 21 | def _labeled_print(msg: str): 22 | print("[clitheme-man] "+msg) 23 | 24 | frontend.set_domain("swiftycode") 25 | frontend.set_appname("clitheme") 26 | fd=frontend.FetchDescriptor(subsections="man") 27 | 28 | def main(args: List[str]): 29 | """ 30 | Invoke clitheme-man using the given command line arguments 31 | 32 | Note: the first item in the argument list must be the program name 33 | (e.g. ['clitheme-man', ] or ['example-app', ]) 34 | """ 35 | if os.name=="nt": 36 | _labeled_print(fd.reof("win32-not-supported", "Error: Windows platform not supported")) 37 | return 1 38 | # check if "man" exists on system 39 | man_executable: str=shutil.which("man") # type: ignore 40 | if man_executable==None: 41 | _labeled_print(fd.reof("man-not-installed", "Error: \"man\" is not installed on this system")) 42 | return 1 43 | env=os.environ 44 | prev_manpath=env.get('MANPATH') 45 | # check if theme is set 46 | theme_set=True 47 | if not os.path.exists(f"{_globalvar.clitheme_root_data_path}/{_globalvar.generator_manpage_pathname}"): 48 | _labeled_print(fd.reof("no-theme-warn", "Warning: no theme set or theme does not contain manpages")) 49 | theme_set=False 50 | # set MANPATH 51 | if theme_set: env['MANPATH']=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_manpage_pathname 52 | # Only try "man" with fallback settings if content arguments are specified 53 | for x in range(1,len(args)): 54 | arg=args[x] 55 | # Specified '--' and contains following content arguments 56 | if arg=='--' and len(args)>x+1: break 57 | if not arg.startswith('-'): break 58 | else: theme_set=False 59 | # invoke man 60 | def run_process(env) -> int: 61 | process=subprocess.Popen([man_executable]+args[1:], env=env) 62 | while process.poll()==None: 63 | try: time.sleep(0.001) 64 | except KeyboardInterrupt: process.send_signal(signal.SIGINT) 65 | return process.poll() # type: ignore 66 | returncode=run_process(env) 67 | # Return code is negative when exited due to signal 68 | if returncode>0 and theme_set: 69 | _labeled_print(fd.reof("prev-command-fail", "Executing \"man\" with custom path failed, trying execution with normal settings")) 70 | env["MANPATH"]=prev_manpath if prev_manpath!=None else '' 71 | returncode=run_process(os.environ) 72 | # If return code is a signal, handle the exit code properly 73 | return 128+abs(returncode) if returncode<0 else returncode 74 | 75 | def _script_main(): # for script 76 | return main(sys.argv) 77 | if __name__=="__main__": 78 | exit(main(sys.argv)) -------------------------------------------------------------------------------- /src/clitheme/strings/cli-strings.clithemedef.txt: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | {header_section} 8 | name clitheme message text translations (cli) 9 | version 2.0 10 | locales zh_CN 11 | supported_apps clitheme 12 | {/header_section} 13 | 14 | {entries_section} 15 | in_domainapp swiftycode clitheme 16 | in_subsection cli 17 | [entry] no-command 18 | # locale:default Error: no command or option specified 19 | locale:zh_CN 错误:没有提供指令或选项 20 | [/entry] 21 | [entry] not-enough-arguments 22 | # locale:default Error: not enough arguments 23 | locale:zh_CN 错误:参数不够 24 | [/entry] 25 | [entry] unknown-option 26 | # locale:default Error: unknown option "{option}" 27 | locale:zh_CN 错误:未知选项"{option}" 28 | [/entry] 29 | [entry] unknown-command 30 | # locale:default Error: unknown command "{cmd}" 31 | locale:zh_CN 错误:未知指令"{cmd}" 32 | [/entry] 33 | [entry] too-many-arguments 34 | # locale:default Error: too many arguments 35 | locale:zh_CN 错误:参数太多 36 | [/entry] 37 | [entry] help-usage-prompt 38 | # locale:default Run "{clitheme} --help" for usage information 39 | locale:zh_CN 使用"{clitheme} --help"以获取使用方法 40 | [/entry] 41 | [entry] version-str 42 | # locale:default clitheme version {ver} 43 | locale:zh_CN clitheme 版本:{ver} 44 | [/entry] 45 | # apply-theme 和 generate-data 指令 46 | # apply-theme and generate-data commands 47 | in_subsection cli apply-theme 48 | [entry] reading-file 49 | # locale:default ==> Reading file {filename}... 50 | locale:zh_CN ==> 正在读取文件{filename}... 51 | [/entry] 52 | [entry] reading-stdin-note 53 | # locale:default Reading from standard input 54 | locale:zh_CN 正在从stdin读取文件 55 | [/entry] 56 | [entry] stdin-interactive-finish-prompt 57 | # locale:default Input file content here and press {shortcut} to finish 58 | locale:zh_CN 在此输入文件内容,并按下{shortcut}以结束 59 | [/entry] 60 | [entry] generate-data-msg 61 | # locale:default The theme data will be generated from the following definition files in the following order: 62 | locale:zh_CN 主题定义数据将会从以下顺序的主题定义文件生成: 63 | [/entry] 64 | [entry] apply-theme-msg 65 | # locale:default The following definition files will be applied in the following order: 66 | locale:zh_CN 这些主题定义文件将会通过以下顺序被应用: 67 | [/entry] 68 | [entry] overwrite-notice 69 | # locale:default The existing theme data will be overwritten if you continue. 70 | locale:zh_CN 如果继续,当前的主题数据将会被覆盖。 71 | [/entry] 72 | [entry] overlay-notice 73 | # locale:default The definition files will be appended on top of the existing theme data. 74 | locale:zh_CN 这些主题定义文件会被叠加在当前的数据上。 75 | [/entry] 76 | [entry] confirm-prompt 77 | # locale:default Do you want to continue? [y/n] 78 | locale:zh_CN 是否继续操作?[y/n] 79 | [/entry] 80 | [entry] overlay-msg 81 | # locale:default Overlay specified 82 | locale:zh_CN 已使用数据叠加模式 83 | [/entry] 84 | [entry] processing-files 85 | # locale:default ==> Processing files... 86 | locale:zh_CN ==> 正在处理文件... 87 | [/entry] 88 | [entry] processing-file 89 | # locale:default > Processing file {filename}... 90 | locale:zh_CN > 正在处理文件{filename}... 91 | [/entry] 92 | [entry] process-files-success 93 | # locale:default Successfully processed files 94 | locale:zh_CN 已成功处理文件 95 | [/entry] 96 | [entry] view-temp-dir 97 | # locale:default View at {path} 98 | locale:zh_CN 生成的数据可以在"{path}"查看 99 | [/entry] 100 | [entry] applying-theme 101 | # locale:default ==> Applying theme... 102 | locale:zh_CN ==> 正在应用主题... 103 | [/entry] 104 | [entry] apply-theme-success 105 | # locale:default Theme applied successfully 106 | locale:zh_CN 已成功应用主题 107 | [/entry] 108 | [entry] read-file-error 109 | # [locale] default 110 | # [File {index}] An error occurred while reading the file: 111 | # {message} 112 | # [/locale] 113 | [locale] zh_CN 114 | [文件{index}] 读取文件时出现了错误: 115 | {message} 116 | [/locale] 117 | [/entry] 118 | [entry] overlay-no-data 119 | # [locale] default 120 | # Error: no theme set or the current data is corrupt 121 | # Try setting a theme first 122 | # [/locale] 123 | [locale] zh_CN 124 | 错误:当前没有设定主题或当前数据损坏 125 | 请尝试设定主题 126 | [/locale] 127 | [/entry] 128 | [entry] overlay-data-error 129 | # [locale] default 130 | # Error: the current data is corrupt 131 | # Remove the current theme, set the theme, and try again 132 | # [/locale] 133 | [locale] zh_CN 134 | 错误:当前主题数据损坏 135 | 请移除当前数据和重新设定主题后重试 136 | [/locale] 137 | [/entry] 138 | [entry] process-files-error 139 | # [locale] default 140 | # [File {index}] An error occurred while processing the file: 141 | # {message} 142 | # [/locale] 143 | [locale] zh_CN 144 | [文件{index}] 处理文件时发生了错误: 145 | {message} 146 | [/locale] 147 | [/entry] 148 | # unset-current-theme 指令 149 | # unset-current-theme command 150 | in_subsection cli unset-current-theme 151 | [entry] no-data-found 152 | # locale:default Error: No theme data present (no theme was set) 153 | locale:zh_CN 错误:当前没有设定主题 154 | [/entry] 155 | [entry] remove-data-error 156 | # [locale] default 157 | # An error occurred while removing the data: 158 | # {message} 159 | # [/locale] 160 | [locale] zh_CN 161 | 移除当前数据时发生了错误: 162 | {message} 163 | [/locale] 164 | [/entry] 165 | [entry] remove-data-success 166 | # locale:default Successfully removed the current theme data 167 | locale:zh_CN 已成功移除当前主题数据 168 | [/entry] 169 | # get-current-theme-info 指令 170 | # get-current-theme-info command 171 | in_subsection cli get-current-theme-info 172 | [entry] no-theme 173 | # locale:default No theme currently set 174 | locale:zh_CN 当前没有设定任何主题 175 | [/entry] 176 | [entry] current-theme-msg 177 | # locale:default Currently installed theme(s): 178 | locale:zh_CN 当前设定的主题: 179 | [/entry] 180 | [entry] version-str 181 | # locale:default Version: {ver} 182 | locale:zh_CN 版本:{ver} 183 | [/entry] 184 | [entry] description-str 185 | # locale:default Description: 186 | locale:zh_CN 详细说明: 187 | [/entry] 188 | [entry] locales-str 189 | # locale:default Supported locales: 190 | locale:zh_CN 支持的语言: 191 | [/entry] 192 | [entry] supported-apps-str 193 | # locale:default Supported apps: 194 | locale:zh_CN 支持的应用程序: 195 | [/entry] 196 | # update-theme 指令 197 | # update-theme command 198 | in_subsection cli update-theme 199 | [entry] no-theme-err 200 | # locale:default Error: no theme currently set 201 | locale:zh_CN 错误:当前没有设定主题 202 | [/entry] 203 | [entry] not-available-err 204 | # [locale] default 205 | # update-theme cannot be used with the current theme setting 206 | # Please re-apply the current theme and try again 207 | # [/locale] 208 | [locale] zh_CN 209 | update-theme在当前主题设定上无法使用 210 | 请重新应用当前主题后重试 211 | [/locale] 212 | [/entry] 213 | [entry] other-err 214 | # [locale] default 215 | # An error occurred while processing file path information: {msg} 216 | # Please re-apply the current theme and try again 217 | # [/locale] 218 | [locale] zh_CN 219 | 处理文件路径信息时发生错误:{msg} 220 | 请重新应用当前主题后重试 221 | [/locale] 222 | [/entry] 223 | # 用于clitheme --help的字符串定义 224 | # String entries used for "clitheme --help" 225 | in_subsection cli help-message 226 | [entry] usage-str 227 | # locale:default Usage: 228 | locale:zh_CN 使用方式: 229 | [/entry] 230 | [entry] options-str 231 | # locale:default Options: 232 | locale:zh_CN 选项: 233 | [/entry] 234 | [entry] options-apply-theme 235 | # [locale] default 236 | # apply-theme: Apply the given theme definition file(s). 237 | # Specify --overlay to add file(s) onto the current data. 238 | # Specify --preserve-temp to preserve the temporary directory after the operation. (Debug purposes only) 239 | # [/locale] 240 | [locale] zh_CN 241 | apply-theme:应用指定的主题定义文件 242 | 指定"--overlay"选项以将文件添加到当前数据中 243 | 指定"--preserve-temp"以保留该操作生成的临时目录(调试用途) 244 | [/locale] 245 | [/entry] 246 | [entry] options-get-current-theme-info 247 | # [locale] default 248 | # get-current-theme-info: Show information about the currently applied theme(s) 249 | # Specify --name to only display the name of each theme 250 | # Specify --file-path to only display the source file path of each theme 251 | # (Both will be displayed when both specified) 252 | # [/locale] 253 | [locale] zh_CN 254 | get-current-theme-info:显示当前主题设定的详细信息 255 | 指定"--name"以仅显示每个主题的名称 256 | 指定"--file-path"以仅显示每个主题的源文件路径 257 | (同时指定时,两者都会显示) 258 | [/locale] 259 | [/entry] 260 | [entry] options-unset-current-theme 261 | # locale:default unset-current-theme: Remove the current theme data from the system 262 | locale:zh_CN unset-current-theme:取消设定当前主题定义和数据 263 | [/entry] 264 | [entry] options-update-theme 265 | # locale:default update-theme: Re-apply the theme definition files specified in the previous \"apply-theme\" command (previous commands if --overlay is used) 266 | locale:zh_CN update-theme:重新应用上一个apply-theme操作中指定的主题定义文件(前几次操作,如果使用了"--overlay") 267 | [/entry] 268 | [entry] options-generate-data 269 | # locale:default generate-data: [Debug purposes only] Generate a data hierarchy from specified theme definition files in a temporary directory 270 | locale:zh_CN generate-data:【仅供调试用途】对于指定的主题定义文件在临时目录中生成一个数据结构 271 | [/entry] 272 | [entry] options-yes 273 | # locale:default [For supported commands, specify --yes to skip the confirmation prompt] 274 | locale:zh_CN 【在支持的指令中,指定"--yes"以跳过确认提示】 275 | [/entry] 276 | [entry] options-version 277 | # locale:default --version: Show the current version of clitheme 278 | locale:zh_CN --version:显示clitheme的当前版本信息 279 | [/entry] 280 | [entry] options-help 281 | # locale:default --help: Show this help message 282 | locale:zh_CN --help:显示这个帮助提示 283 | [/entry] 284 | {/entries_section} 285 | -------------------------------------------------------------------------------- /src/clitheme/strings/exec-strings.clithemedef.txt: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | # spell-checker:ignore nosubst 8 | 9 | {header_section} 10 | name clitheme message text translations (clitheme-exec) 11 | version 2.0 12 | locales zh_CN 13 | supported_apps clitheme 14 | {/header_section} 15 | 16 | {entries_section} 17 | in_domainapp swiftycode clitheme 18 | # 用于clitheme-exec --help的字符串定义 19 | # String entries used for "clitheme-exec --help" 20 | in_subsection exec help-message 21 | [entry] usage-str 22 | # locale:default Usage: 23 | locale:zh_CN 使用方式: 24 | [/entry] 25 | [entry] options-str 26 | # locale:default Options: 27 | locale:zh_CN 选项: 28 | [/entry] 29 | [entry] options-debug 30 | # locale:default --debug: Display indicator at the beginning of each read output by line 31 | locale:zh_CN --debug:在每一行被读取的输出前显示标记 32 | [/entry] 33 | [entry] options-debug-color 34 | # locale:default --debug-color: Apply color on output; used to determine stdout or stderr (BETA: stdout/stderr not implemented) 35 | locale:zh_CN --debug-color:为输出设定颜色;用于区分stdout和stderr(BETA:stdout/stderr未实现) 36 | [/entry] 37 | [entry] options-debug-newlines 38 | # locale:default --debug-newlines: Use newlines to display output that does not end on a newline 39 | locale:zh_CN --debug-newlines:使用新的一行来显示没有新行的输出 40 | [/entry] 41 | [entry] options-showchars 42 | # locale:default --showchars: Display various control characters in plain text 43 | locale:zh_CN --showchars:使用明文显示终端控制符号 44 | [/entry] 45 | [entry] options-foreground-stat 46 | # locale:default --foreground-stat: Display message when the foreground status of the process changes (value of tcgetpgrp) 47 | locale:zh_CN --foreground-stat: 当进程的前台状态(tcgetpgrp的返回值)变动时,显示提示信息 48 | [/entry] 49 | [entry] options-nosubst 50 | # locale:default --nosubst: Do not perform any output substitutions even if a theme is set 51 | locale:zh_CN --nosubst:不进行任何输出替换,即使已设定主题 52 | [/entry] 53 | in_subsection exec 54 | [entry] help-usage-prompt 55 | # locale:default Run "clitheme-exec --help" for usage information 56 | locale:zh_CN 使用"clitheme-exec --help"以获取使用方式 57 | [/entry] 58 | [entry] unknown-option-err 59 | # locale:default Error: unknown option "{phrase}" 60 | locale:zh_CN 错误:未知选项"{phrase}" 61 | [/entry] 62 | [entry] debug-newlines-not-with-debug 63 | # locale:default Error: "--debug-newlines" must be used with "--debug" option 64 | locale:zh_CN 错误:"--debug-newlines"选项必须与"--debug"选项同时指定 65 | [/entry] 66 | [entry] no-command-err 67 | # locale:default Error: no command specified 68 | locale:zh_CN 错误:未指定命令 69 | [/entry] 70 | [entry] no-theme-warn 71 | # locale:default Warning: no theme set or theme does not have substrules 72 | locale:zh_CN 警告:没有设定主题或当前主题没有substrules定义 73 | [/entry] 74 | [entry] substrules-update-msg 75 | # locale:default Updating database... 76 | locale:zh_CN 正在更新数据库... 77 | [/entry] 78 | [entry] db-update-generator-err 79 | # locale:default Failed to generate data (full log below): 80 | locale:zh_CN 无法生成数据(完整日志在此): 81 | [/entry] 82 | [entry] db-update-err 83 | # [locale] default 84 | # An error occurred while updating the database: {msg} 85 | # Please re-apply the theme and try again 86 | # [/locale] 87 | [locale] zh_CN 88 | 更新数据库时发生了错误:{msg} 89 | 请重新应用当前主题,然后重试 90 | [/locale] 91 | [/entry] 92 | [entry] db-read-err 93 | # [locale] default 94 | # An error occurred while reading the database: {msg} 95 | # Please re-apply the theme and try again 96 | # [/locale] 97 | [locale] zh_CN 98 | 读取数据库时发生了错误:{msg} 99 | 请重新应用当前主题,然后重试 100 | [/locale] 101 | [/entry] 102 | [entry] db-invalid-version 103 | # locale:default Invalid database version information 104 | locale:zh_CN 无效数据库版本信息 105 | [/entry] 106 | [entry] db-update-success-msg 107 | # locale:default Successfully updated database, proceeding execution 108 | locale:zh_CN 数据库更新完成,继续执行命令 109 | [/entry] 110 | [entry] command-fail-err 111 | # locale:default Error: failed to run command: {msg} 112 | locale:zh_CN 错误:无法执行命令:{msg} 113 | [/entry] 114 | [entry] internal-error-err 115 | # locale:default Error: an internal error has occurred while executing the command (execution halted): 116 | locale:zh_CN 错误:执行命令时发生内部错误(执行已终止): 117 | [/entry] 118 | [entry] output-interrupted-exit 119 | # locale:default Output interrupted after command exit 120 | locale:zh_CN 输出在命令退出后被中断 121 | [/entry] 122 | {/entries_section} -------------------------------------------------------------------------------- /src/clitheme/strings/generator-strings.clithemedef.txt: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | # spell-checker:ignore subdir banphrase startswith 8 | {header_section} 9 | name clitheme message text translations (generator) 10 | version 2.0 11 | locales zh_CN 12 | supported_apps clitheme 13 | {/header_section} 14 | 15 | {entries_section} 16 | in_domainapp swiftycode clitheme 17 | in_subsection generator 18 | # 错误提示 19 | # error messages 20 | [entry] error-str 21 | # locale:default Syntax error: {msg} 22 | locale:zh_CN 语法错误:{msg} 23 | [/entry] 24 | 25 | # 以下定义会是上方定义"{msg}"中的内容 26 | # The following definitions are the contents of "{msg}" in the above definition 27 | [entry] subsection-conflict-err 28 | # locale:default Line {num}: cannot create subsection "{name}" because an entry with the same name already exists 29 | locale:zh_CN 第{num}行:无法创建子路径"{name}",因为拥有相同名称的定义已存在 30 | [/entry] 31 | [entry] entry-conflict-err 32 | # locale:default Line {num}: cannot create entry "{name}" because a subsection with the same name already exists 33 | locale:zh_CN 第{num}行:无法创建定义"{name}",因为拥有相同名称的子路径已存在 34 | [/entry] 35 | [entry] extra-arguments-err 36 | # locale:default Extra arguments after "{phrase}" on line {num} 37 | locale:zh_CN 第{num}行:"{phrase}"后的参数太多 38 | [/entry] 39 | [entry] repeated-section-err 40 | # locale:default Repeated {section} section at line {num} 41 | locale:zh_CN 第{num}行:重复的{phrase}段落 42 | [/entry] 43 | [entry] invalid-phrase-err 44 | # locale:default Unexpected "{phrase}" on line {num} 45 | locale:zh_CN 第{num}行:无效的"{phrase}"语句 46 | [/entry] 47 | [entry] not-enough-args-err 48 | # locale:default Not enough arguments for "{phrase}" at line {num} 49 | locale:zh_CN 第{num}行:"{phrase}"后参数不够 50 | [/entry] 51 | [entry] incomplete-section-err 52 | # locale:default Missing or incomplete header or content sections 53 | locale:zh_CN 文件缺少或包含不完整的header或内容段落 54 | [/entry] 55 | [entry] db-regenerate-fail-err 56 | # locale:default Failed to migrate existing substrules database; try performing the operation without using "--overlay" 57 | locale:zh_CN 无法升级当前的substrules的数据库;请尝试不使用"--overlay"再次执行此操作 58 | [/entry] 59 | [entry] invalid-version-err 60 | # locale:default Invalid version information "{ver}" on line {num} 61 | locale:zh_CN 第{num}行:无效版本信息"{ver}" 62 | [/entry] 63 | [entry] unsupported-version-err 64 | # locale:default Current version of clitheme ({cur_ver}) does not support this file (requires {req_ver} or higher) 65 | locale:zh_CN 当前版本的clitheme({cur_ver})不支持此文件(需要 {req_ver} 或更高版本) 66 | [/entry] 67 | [entry] phrase-precedence-err 68 | # locale:default Line {num}: header macro "{phrase}" must be specified before other lines 69 | locale:zh_CN 第{num}行:头定义"{phrase}"必须在其他行之前声明 70 | [/entry] 71 | # 选项错误提示信息 72 | # Options error messages 73 | [entry] option-not-allowed-err 74 | # locale:default Option "{phrase}" not allowed here at line {num} 75 | locale:zh_CN 第{num}行:选项"{phrase}"不允许在这里指定 76 | [/entry] 77 | [entry] unknown-option-err 78 | # locale:default Unknown option "{phrase}" on line {num} 79 | locale:zh_CN 第{num}行:未知选项"{phrase}" 80 | [/entry] 81 | [entry] option-without-value-err 82 | # locale:default No value specified for option "{phrase}" on line {num} 83 | locale:zh_CN 第{num}行:选项"{phrase}"未指定数值 84 | [/entry] 85 | [entry] optional-value-not-int-err 86 | # locale:default The value specified for option "{phrase}" is not an integer on line {num} 87 | locale:zh_CN 第{num}行:选项"{phrase}"指定的数值不是整数 88 | [/entry] 89 | [entry] option-conflict-err 90 | # locale:default The option "{option1}" can't be set at the same time with "{option2}" on line {num} 91 | locale:zh_CN 第{num}行:选项"{option1}"和"{option2}"不能同时指定 92 | [/entry] 93 | [entry] bad-match-pattern-err 94 | # locale:default Bad match pattern at line {num} ({error_msg}) 95 | locale:zh_CN 第{num}行:无效的匹配正则表达式({error_msg}) 96 | [/entry] 97 | [entry] bad-subst-pattern-err 98 | # locale:default Bad substitute pattern at line {num} ({error_msg}) 99 | locale:zh_CN 第{num}行:无效的替换正则表达式({error_msg}) 100 | [/entry] 101 | [entry] bad-var-name-err 102 | # locale:default Line {num}: "{name}" is not a valid variable name 103 | locale:zh_CN 第{num}行:"{name}"不是一个有效的变量名称 104 | [/entry] 105 | [entry] manpage-subdir-file-conflict-err 106 | # locale:default Line {num}: conflicting files and subdirectories; please check previous definitions 107 | locale:zh_CN 第{num}行:子路径和文件有冲突;请检查之前的定义 108 | [/entry] 109 | [entry] include-file-read-err 110 | # [locale] default 111 | # Line {num}: unable to read file "{filepath}": 112 | # {error_msg} 113 | # [/locale] 114 | [locale] zh_CN 115 | 第{num}行:无法读取文件"{filepath}": 116 | {error_msg} 117 | [/locale] 118 | [/entry] 119 | [entry] include-file-missing-phrase-err 120 | # locale:default Missing "as " phrase on next line of line {num} 121 | locale:zh_CN 第{num}行:在下一行缺少"as <文件名>"语句 122 | [/entry] 123 | [entry] missing-info-err 124 | # locale:default {sect_name} section missing required entries: {entries} 125 | locale:zh_CN {sect_name}段落缺少必要条目:{entries} 126 | [/entry] 127 | # 警告提示 128 | # Warning messages 129 | [entry] warning-str 130 | # locale:default Warning: {msg} 131 | locale:zh_CN 警告:{msg} 132 | [/entry] 133 | 134 | # 以下定义会是上方定义"{msg}"中的内容 135 | # The following definitions are the contents of "{msg}" in the above definition 136 | [entry] repeated-entry-warn 137 | # locale:default Line {num}: repeated entry "{name}", overwriting 138 | locale:zh_CN 第{num}行:重复的定义"{name}";之前的定义内容将会被覆盖 139 | [/entry] 140 | [entry] repeated-substrules-warn 141 | # locale:default Repeated substrules entry at line {num}, overwriting 142 | locale:zh_CN 第{num}行:重复的substrules定义;之前的定义内容将会被覆盖 143 | [/entry] 144 | [entry] repeated-header-warn 145 | # locale:default Line {num}: repeated header info "{name}", overwriting 146 | locale:zh_CN 第{num}行:重复的header信息"{name}";之前的定义内容将会被覆盖 147 | [/entry] 148 | [entry] syntax-phrase-deprecation-warn 149 | # locale:default Line {num}: phrase "{old_phrase}" is deprecated in this version; please use "{new_phrase}" instead 150 | locale:zh_CN 第{num}行:"{old_phrase}"在当前版本中已被弃用;请使用"{new_phrase}" 151 | [/entry] 152 | [entry] unknown-variable-warn 153 | # locale:default Line {num}: unknown variable "{name}", not performing substitution 154 | locale:zh_CN 第{num}行:未知变量名称"{name}",不会进行替换 155 | [/entry] 156 | [entry] repeated-manpage-warn 157 | # locale:default Line {num}: repeated manpage file, overwriting 158 | locale:zh_CN 第{num}行:重复的manpage文件;之前的文件内容将会被覆盖 159 | [/entry] 160 | # 路径检查功能提示 161 | # Sanity check feature messages 162 | [entry] sanity-check-entry-err 163 | # locale:default Line {num}: entry subsections/names {sanitycheck_msg} 164 | locale:zh_CN 第{num}行:定义路径名称{sanitycheck_msg} 165 | [/entry] 166 | [entry] sanity-check-domainapp-err 167 | # locale:default Line {num}: domain and app names {sanitycheck_msg} 168 | locale:zh_CN 第{num}行:开发者和应用程序名称{sanitycheck_msg} 169 | [/entry] 170 | [entry] sanity-check-subsection-err 171 | # locale:default Line {num}: subsection names {sanitycheck_msg} 172 | locale:zh_CN 第{num}行:子路径名称{sanitycheck_msg} 173 | [/entry] 174 | [entry] sanity-check-manpage-err 175 | # locale:default Line {num}: manpage paths {sanitycheck_msg}; use spaces to denote subdirectories 176 | locale:zh_CN 第{num}行:manpage路径{sanitycheck_msg};使用空格以指定子路径 177 | [/entry] 178 | [entry] set-substvar-warn 179 | # locale:default Line {num}: attempted to reference a defined variable, but "substvar" option is not enabled 180 | locale:zh_CN 第{num}行:尝试引用定义的变量,但"substvar"选项未被启用 181 | [/entry] 182 | [entry] set-substesc-warn 183 | # Must use "{{{{ESC}}}}" to represent "{{ESC}}" in the message 184 | # locale:default Line {num}: attempted to use "{{{{ESC}}}}", but "substesc" option is not enabled 185 | locale:zh_CN Line {num}: 尝试引用"{{{{ESC}}}}",但"substesc"选项未被启用 186 | [/entry] 187 | # 以下提示会是上方定义中"{sanitycheck_msg}"的内容 188 | # The following messages are the contents of "{sanitycheck_msg}" in the above entries 189 | [entry] sanity-check-msg-banphrase-err 190 | # locale:default cannot contain '{char}' 191 | locale:zh_CN 不能包含'{char}' 192 | [/entry] 193 | [entry] sanity-check-msg-startswith-err 194 | # locale:default cannot start with '{char}' 195 | locale:zh_CN 不能以'{char}'开头 196 | [/entry] 197 | {/entries_section} 198 | -------------------------------------------------------------------------------- /src/clitheme/strings/man-strings.clithemedef.txt: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | {header_section} 8 | name clitheme message text translations (man) 9 | version 2.0 10 | locales zh_CN 11 | supported_apps clitheme 12 | {/header_section} 13 | 14 | {entries_section} 15 | in_domainapp swiftycode clitheme 16 | in_subsection man 17 | [entry] man-not-installed 18 | # locale:default Error: "man" is not installed on this system 19 | locale:zh_CN 错误:"man"未安装在此系统中 20 | [/entry] 21 | [entry] no-theme-warn 22 | # locale:default Warning: no theme set or theme does not contain manpages 23 | locale:zh_CN 警告:没有设定主题或当前主题没有manpages定义 24 | [/entry] 25 | [entry] win32-not-supported 26 | # locale:default Error: Windows platform not supported 27 | locale:zh_CN 错误:不支持Windows平台 28 | [/entry] 29 | [entry] prev-command-fail 30 | locale:zh_CN 设定自定义路径执行"man"时发生错误,正在尝试以正常设定执行 31 | [/entry] 32 | {/entries_section} 33 | 34 | -------------------------------------------------------------------------------- /src/clithemedef-test_testprogram.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | import shutil 8 | from clitheme import _generator 9 | from clitheme import _globalvar 10 | import random 11 | import string 12 | import os 13 | 14 | # spell-checker:ignore rootpath errorcount mainfile 15 | 16 | l=__file__.split(os.sep) 17 | l.pop() 18 | root_directory="" # directory where the script files are in 19 | for part in l: 20 | root_directory+=part+os.sep 21 | print("Testing generator function...") 22 | mainfile_data=open(root_directory+"/testprogram-data/clithemedef-test_mainfile.clithemedef.txt",'r', encoding="utf-8").read() 23 | expected_data=open(root_directory+"/testprogram-data/clithemedef-test_expected.txt",'r', encoding="utf-8").read() 24 | generator_path=_generator.generate_data_hierarchy(mainfile_data) 25 | 26 | errorcount=0 27 | rootpath=generator_path+"/"+_globalvar.generator_data_pathname 28 | current_path="" 29 | for line in expected_data.splitlines(): 30 | if line.strip()=='' or line.strip()[0]=='#': 31 | continue 32 | if current_path=="": # on path line 33 | current_path=line.strip() 34 | else: # on content line 35 | # read the file 36 | contents="" 37 | try: 38 | contents=open(rootpath+"/"+current_path, 'r', encoding="utf-8").read() 39 | print("File "+rootpath+"/"+current_path+" OK") 40 | except FileNotFoundError: 41 | print("[File] file "+rootpath+"/"+current_path+" does not exist") 42 | errorcount+=1 43 | current_path="" 44 | if contents=="": continue 45 | if contents.strip()!=line.strip(): 46 | print("[Content] Content mismatch on file "+rootpath+"/"+current_path) 47 | errorcount+=1 48 | current_path="" 49 | 50 | # Test frontend 51 | print("Testing frontend...") 52 | from clitheme import frontend 53 | frontend.set_debugmode(True) 54 | frontend.set_lang("en_US.UTF-8") 55 | frontend.data_path=generator_path+"/"+_globalvar.generator_data_pathname 56 | expected_data_frontend=open(root_directory+"/testprogram-data/clithemedef-test_expected-frontend.txt", 'r', encoding="utf-8").read() 57 | current_path_frontend="" 58 | errorcount_frontend=0 59 | for line in expected_data_frontend.splitlines(): 60 | if line.strip()=='' or line.strip()[0]=='#': 61 | continue 62 | if current_path_frontend=="": # on path line 63 | current_path_frontend=line.strip() 64 | else: # on content line 65 | phrases=current_path_frontend.split() 66 | descriptor=None 67 | entry_path=None 68 | if len(phrases)>2: 69 | descriptor=frontend.FetchDescriptor(domain_name=phrases[0],app_name=phrases[1]) 70 | entry_path=_globalvar.splitarray_to_string(phrases[2:]) # just being lazy here 71 | else: 72 | descriptor=frontend.FetchDescriptor() 73 | entry_path=current_path_frontend 74 | expected_content=line.strip() 75 | fallback_string="" 76 | for x in range(30): # reduce inaccuracies 77 | fallback_string+=random.choice(string.ascii_letters) 78 | received_content=descriptor.retrieve_entry_or_fallback(entry_path, fallback_string) 79 | if expected_content.strip()!=received_content.strip(): 80 | if received_content.strip()==fallback_string: 81 | print("[Error] Failed to retrieve entry for \""+current_path_frontend+"\"") 82 | else: 83 | print("[Content] Content mismatch on path \""+current_path_frontend+"\"") 84 | errorcount_frontend+=1 85 | current_path_frontend="" 86 | print("\n\nTest results:") 87 | print("==> ",end='') 88 | if errorcount>0: 89 | print("Generator test error: "+str(errorcount)+" errors found") 90 | print("See "+generator_path+" for more details") 91 | exit(1) 92 | else: 93 | print("Generator test OK") 94 | print("==> ",end='') 95 | if errorcount_frontend>0: 96 | print("Frontend test error: "+str(errorcount_frontend)+" errors found") 97 | print("See "+generator_path+" for more details") 98 | exit(1) 99 | else: 100 | print("Frontend test OK") 101 | if errorcount>0 and errorcount_frontend>0: 102 | shutil.rmtree(generator_path) # remove the temp directory -------------------------------------------------------------------------------- /src/db_interface_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2023-2024 swiftycode 2 | 3 | # 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. 4 | # 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. 5 | # You should have received a copy of the GNU General Public License along with this program. If not, see . 6 | 7 | from clitheme._generator import db_interface 8 | from clitheme import _generator, _globalvar 9 | import shutil 10 | 11 | # sample input for testing 12 | sample_inputs=[("rm: missing operand", "rm"), 13 | ("type rm --help for more information", "rm"), 14 | ("rm: /etc/folder: Permission denied", "rm /etc/folder -rf"), 15 | ("rm: /etc/file: Permission denied", "rm /etc/folder"), # test multiple phrase detection (substitution should not happen) 16 | ("cat: /dev/mem: Permission denied","cat /dev/mem"), 17 | ("bash: /etc/secret: Permission denied","cd /etc/secret"), 18 | ("ls: /etc/secret: Permission denied","ls /etc/secret"), 19 | ("ls: /etc/secret: Permission denied","wef ls /etc/secret"), # test first phrase detection (substitution should not happen) 20 | ("ls: unrecognized option '--help'", "ls --help"), 21 | ("Warning: invalid input", "input anything"), 22 | ("Error: invalid input ","input anything"), # test extra spaces 23 | ("Error: sample message", "example_app --this install-stuff"), # test strictcmdmatch (substitution should not happen) 24 | ("Error: sample message", "example_app install-stuff --this"), # test strictcmdmatch and endmatchhere options 25 | ("Error: sample message", "example_app install-stuff"), # test strictcmdmatch with SAME command as defined in filter 26 | ("rm: : Operation not permitted", "rm file.ban"), # test exactcmdmatch 27 | ("example_app: using recursive directories", "example_app -rlc"), # test smartcmdmatch 28 | ("example_app: using list options", "/usr/bin/example_app -rlc"), # test smartcmdmatch and command basename handling 29 | ] 30 | expected_outputs=[ 31 | ("rm says: missing arguments and options (>﹏<)", "rm 说:缺少参数和选项 (>﹏<)"), 32 | ("For more information, use rm --help (。ì _ í。)", "关于更多信息,请使用rm --help (。ì _ í。)"), 33 | ("rm says: Access denied to /etc/folder! ಥ_ಥ", "rm 说:文件\"/etc/folder\"拒绝访问!ಥ_ಥ"), 34 | ("rm: /etc/file: Permission denied",), 35 | ("cat says: Access denied to /dev/mem! ಥ_ಥ", "cat 说:文件\"/dev/mem\"拒绝访问!ಥ_ಥ"), 36 | ("bash says: Access denied to /etc/secret! ಥ_ಥ", "bash 说:文件\"/etc/secret\"拒绝访问!ಥ_ಥ"), 37 | ("ls says: Access denied to /etc/secret! ಥ_ಥ", "ls 说:文件\"/etc/secret\"拒绝访问!ಥ_ಥ"), 38 | ("ls: /etc/secret: Permission denied",), 39 | ("ls says: option \"--help\" not known! (ToT)/~~~", "ls 说:未知选项\"--help\"!(ToT)/~~~"), 40 | ("o(≧v≦)o Note: input is invalid! ಥ_ಥ", "o(≧v≦)o 提示: 无效输入!ಥ_ಥ"), 41 | ("(ToT)/~~~ Error: input is invalid! ಥ_ಥ", "(ToT)/~~~ 错误:无效输入!ಥ_ಥ"), 42 | ("(ToT)/~~~ Error: sample message", "(ToT)/~~~ 错误:sample message"), 43 | ("Error: sample message! (>﹏<)", "错误:样例提示!(>﹏<)"), 44 | ("Error: sample message! (>﹏<)", "错误:样例提示!(>﹏<)"), 45 | ("rm says: Operation not permitted! ಥ_ಥ", "rm 说:不允许的操作!ಥ_ಥ"), 46 | ("o(≧v≦)o example_app says: using recursive directories! (。ì _ í。)", "o(≧v≦)o example_app 说: 正在使用子路径!(。ì _ í。)"), 47 | ("o(≧v≦)o example_app says: using list options! (⊙ω⊙)", "o(≧v≦)o example_app 说: 正在使用列表选项!(⊙ω⊙)"), 48 | ] 49 | # substitute patterns 50 | substrules_file=r""" 51 | {header_section} 52 | name test 53 | {/header_section} 54 | {substrules_section} 55 | filter_command rm 56 | [substitute_string] rm: missing operand 57 | locale:default rm says: missing arguments and options (>﹏<) 58 | locale:zh_CN rm 说:缺少参数和选项 (>﹏<) 59 | [/substitute_string] 60 | [substitute_string] type rm --help for more information 61 | locale:default For more information, use rm --help (。ì _ í。) 62 | locale:zh_CN 关于更多信息,请使用rm --help (。ì _ í。) 63 | [/substitute_string] 64 | 65 | [filter_commands] 66 | rm -rf 67 | cat 68 | cd 69 | ls 70 | [/filter_commands] 71 | [substitute_regex] (?P.+): (?P.+): Permission denied 72 | locale:default \g says: Access denied to \g! ಥ_ಥ 73 | locale:zh_CN \g 说:文件"\g"拒绝访问!ಥ_ಥ 74 | [/substitute_regex] 75 | 76 | filter_command ls 77 | # testing repeated entry detection 78 | [substitute_regex] (?P.+): unrecognized option '(?P.+)' 79 | locale:default wef 80 | [/substitute_regex] 81 | [substitute_regex] (?P.+): unrecognized option '(?P.+)' 82 | locale:default \g says: option "\g" not known! (ToT)/~~~ 83 | locale:zh_CN \g 说:未知选项"\g"!(ToT)/~~~ 84 | [/substitute_regex] 85 | unset_filter_command 86 | 87 | # global substitutions 88 | [substitute_regex] ^Warning:( ) 89 | locale:default o(≧v≦)o Note:\g<1> 90 | locale:zh_CN o(≧v≦)o 提示:\g<1> 91 | [/substitute_regex] 92 | [substitute_regex] ^Error:( ) 93 | locale:default (ToT)/~~~ Error:\g<1> 94 | locale:zh_CN (ToT)/~~~ 错误: 95 | [/substitute_regex] 96 | [substitute_regex] invalid input( )*$ 97 | locale:default input is invalid! ಥ_ಥ 98 | locale:zh_CN 无效输入!ಥ_ಥ 99 | [/substitute_regex] 100 | 101 | set_options strictcmdmatch 102 | filter_command example_app install-stuff 103 | [substitute_string] Error: sample message 104 | locale:default Error: sample message! (>﹏<) 105 | locale:zh_CN 错误:样例提示!(>﹏<) 106 | [/substitute_string] endmatchhere 107 | 108 | set_options exactcmdmatch 109 | filter_command rm file.ban 110 | [substitute_regex] (?P.+): (?P.+): Operation not permitted 111 | locale:default \g says: Operation not permitted! ಥ_ಥ 112 | locale:zh_CN \g 说:不允许的操作!ಥ_ಥ 113 | [/substitute_regex] 114 | 115 | set_options normalcmdmatch 116 | filter_command example_app 117 | [substitute_string] example_app: 118 | locale:default o(≧v≦)o example_app says: 119 | locale:zh_CN o(≧v≦)o example_app 说: 120 | [/substitute_string] 121 | set_options smartcmdmatch 122 | filter_command example_app -r 123 | [substitute_string] using recursive directories 124 | locale:default using recursive directories! (。ì _ í。) 125 | locale:zh_CN 正在使用子路径!(。ì _ í。) 126 | [/substitute_string] 127 | filter_command example_app -l 128 | [substitute_string] using list options 129 | locale:default using list options! (⊙ω⊙) 130 | locale:zh_CN 正在使用列表选项!(⊙ω⊙) 131 | [/substitute_string] 132 | set_options normalcmdmatch 133 | {/substrules_section} 134 | """ 135 | 136 | db_interface.debug_mode=True 137 | generator_path=_generator.generate_data_hierarchy(substrules_file) 138 | db_interface.connect_db(generator_path+"/"+_globalvar.db_filename) 139 | 140 | print("Successfully recorded data\nTesting sample outputs: ") 141 | for x in range(len(sample_inputs)): 142 | inp=sample_inputs[x] 143 | expected=expected_outputs[x] 144 | content=db_interface.match_content(bytes(inp[0],'utf-8'),command=inp[1]).decode('utf-8') 145 | if content in expected: 146 | print("\x1b[1;32mOK\x1b[0;1m:\x1b[0m "+content) 147 | else: 148 | print("\x1b[1;31mMismatch\x1b[0;1m:\x1b[0m "+content) 149 | 150 | try: shutil.rmtree(generator_path) 151 | except: pass -------------------------------------------------------------------------------- /src/testprogram-data/clithemedef-test_expected-frontend.txt: -------------------------------------------------------------------------------- 1 | com.example example-app text-one 2 | Some example text one 3 | com.example example-app text-two 4 | Some example text two 5 | 6 | com.example example-app-two text_one 7 | Some text 8 | com.example example-app-two text_two 9 | Some text two 10 | 11 | com.example example-app-two subsection-one text_one 12 | Some text 13 | com.example example-app-two subsection-one text_two 14 | Some text two 15 | 16 | com.example example-app-two subsection-two text_one 17 | Some text 18 | com.example example-app-two subsection-two text_two 19 | Some text two 20 | 21 | com.example-two another-example repeat_test_text_one 22 | Some other text 23 | com.example-two another-example repeat_test_text_two 24 | Some other text two 25 | 26 | should_unset.example should_unset-app text 27 | Should have reset 28 | 29 | sample_global_entry 30 | Some text 31 | 32 | global.example global_entry 33 | Global entry in app 34 | -------------------------------------------------------------------------------- /src/testprogram-data/clithemedef-test_expected.txt: -------------------------------------------------------------------------------- 1 | com.example/example-app/text-one 2 | Some example text one 3 | com.example/example-app/text-one__en_US 4 | Some example text one 5 | com.example/example-app/text-one__zh_CN 6 | 一些样例文字(一) 7 | com.example/example-app/text-two 8 | Some example text two 9 | com.example/example-app/text-two__en_US 10 | Some example text two 11 | com.example/example-app/text-two__zh_CN 12 | 一些样例文字(二) 13 | 14 | com.example/example-app-two/text_one 15 | Some text 16 | com.example/example-app-two/text_one__en_US 17 | Some text 18 | com.example/example-app-two/text_one__zh_CN 19 | 一些文本 20 | com.example/example-app-two/text_two 21 | Some text two 22 | com.example/example-app-two/text_two__en_US 23 | Some text two 24 | com.example/example-app-two/text_two__zh_CN 25 | 一些文本(二) 26 | 27 | com.example/example-app-two/subsection-one/text_one 28 | Some text 29 | com.example/example-app-two/subsection-one/text_one__en_US 30 | Some text 31 | com.example/example-app-two/subsection-one/text_one__zh_CN 32 | 一些文本 33 | com.example/example-app-two/subsection-one/text_two 34 | Some text two 35 | com.example/example-app-two/subsection-one/text_two__en_US 36 | Some text two 37 | com.example/example-app-two/subsection-one/text_two__zh_CN 38 | 一些文本(二) 39 | 40 | com.example/example-app-two/subsection-two/text_one 41 | Some text 42 | com.example/example-app-two/subsection-two/text_one__en_US 43 | Some text 44 | com.example/example-app-two/subsection-two/text_one__zh_CN 45 | 一些文本 46 | com.example/example-app-two/subsection-two/text_two 47 | Some text two 48 | com.example/example-app-two/subsection-two/text_two__en_US 49 | Some text two 50 | com.example/example-app-two/subsection-two/text_two__zh_CN 51 | 一些文本(二) 52 | 53 | com.example-two/another-example/repeat_test_text_one 54 | Some other text 55 | com.example-two/another-example/repeat_test_text_one__en_US 56 | Some other text 57 | com.example-two/another-example/repeat_test_text_one__zh_CN 58 | 一些其他文本 59 | com.example-two/another-example/repeat_test_text_two 60 | Some other text two 61 | com.example-two/another-example/repeat_test_text_two__en_US 62 | Some other text two 63 | com.example-two/another-example/repeat_test_text_two__zh_CN 64 | 一些其他文本(二) 65 | 66 | should_unset.example/should_unset-app/text 67 | Should have reset 68 | should_unset.example/should_unset-app/text__en_US 69 | Should have reset 70 | should_unset.example/should_unset-app/text__zh_CN 71 | 应该已经重置 72 | 73 | sample_global_entry 74 | Some text 75 | sample_global_entry__en_US 76 | Some text 77 | sample_global_entry__zh_CN 78 | 一些文本 79 | 80 | global.example/global_entry 81 | Global entry in app 82 | global.example/global_entry__en_US 83 | Global entry in app 84 | global.example/global_entry__zh_CN 85 | app内的通用实例 86 | -------------------------------------------------------------------------------- /src/testprogram-data/clithemedef-test_mainfile.clithemedef.txt: -------------------------------------------------------------------------------- 1 | # Sample theme definition file for parser/generator testing 2 | 3 | # Header information 4 | {header_section} 5 | name Example theem 6 | version 0.1 7 | locales en_US 8 | 9 | # Testing repeated handling of entries 10 | locales en_US zh_CN 11 | version 1.0 12 | name Example theme 13 | supported_apps example-app example-app-two another-example shound_unset-app 14 | {/header_section} 15 | 16 | # Main block 17 | {entries_section} 18 | [entry] com.example example-app text-one 19 | locale:default Some example text one 20 | locale:en_US Some example text one 21 | locale:zh_CN 一些样例文字(一) 22 | [/entry] 23 | [entry] com.example example-app text-two 24 | locale:default Some example text two 25 | locale:en_US Some example text two 26 | locale:zh_CN 一些样例文字(二) 27 | [/entry] 28 | 29 | # Testing in_domainapp 30 | in_domainapp com.example example-app-two 31 | [entry] text_one 32 | locale:default Some text 33 | locale:en_US Some text 34 | locale:zh_CN 一些文本 35 | [/entry] 36 | 37 | # Testing subsections 38 | [entry] subsection-one text_one 39 | locale:default Some text 40 | locale:en_US Some text 41 | locale:zh_CN 一些文本 42 | [/entry] 43 | [entry] subsection-one text_two 44 | locale:default Some text two 45 | locale:en_US Some text two 46 | locale:zh_CN 一些文本(二) 47 | [/entry] 48 | 49 | # Testing in_subsection 50 | in_subsection subsection-two 51 | [entry] text_one 52 | locale:default Some text 53 | locale:en_US Some text 54 | locale:zh_CN 一些文本 55 | [/entry] 56 | [entry] text_two 57 | locale:default Some text two 58 | locale:en_US Some text two 59 | locale:zh_CN 一些文本(二) 60 | [/entry] 61 | # Testing unset_subsection 62 | unset_subsection 63 | [entry] text_two 64 | locale:default Some text two 65 | locale:en_US Some text two 66 | locale:zh_CN 一些文本(二) 67 | [/entry] 68 | 69 | in_domainapp com.example-two another-example 70 | # Ignore entries in here 71 | [entry] repeat_test_text_one 72 | locale:default Some text 73 | locale:en_US Some text 74 | locale:zh_CN 一些文本 75 | [/entry] 76 | [entry] repeat_test_text_two 77 | locale:default Some text two 78 | locale:en_US Some text two 79 | locale:zh_CN 一些文本(二) 80 | [/entry] 81 | 82 | # Testing handling of repeated entries 83 | [entry] repeat_test_text_one 84 | locale:default Some other text 85 | locale:en_US Some other text 86 | locale:zh_CN 一些其他文本 87 | [/entry] 88 | [entry] repeat_test_text_two 89 | locale:default Some other text two 90 | locale:en_US Some other text two 91 | locale:zh_CN 一些其他文本(二) 92 | [/entry] 93 | 94 | # Testing unset_domainapp 95 | unset_domainapp 96 | [entry] should_unset.example should_unset-app text 97 | locale:default Should have reset 98 | locale:en_US Should have reset 99 | locale:zh_CN 应该已经重置 100 | [/entry] 101 | 102 | # Testing global entries (without domain or/and app) 103 | [entry] sample_global_entry 104 | locale:default Some text 105 | locale:en_US Some text 106 | locale:zh_CN 一些文本 107 | [/entry] 108 | in_subsection global.example 109 | [entry] global_entry 110 | locale:default Global entry in app 111 | locale:en_US Global entry in app 112 | locale:zh_CN app内的通用实例 113 | [/entry] 114 | {/entries_section} --------------------------------------------------------------------------------