diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000000000000000000000000000000000000..7639254c7be78f4556172fcc9da237e4a5c0ed2b --- /dev/null +++ b/README.en.md @@ -0,0 +1,267 @@ +# clitheme - A CLI application framework for output customization + +[中文](README.md) | **English** + +`clitheme` allows you to customize the output of command line applications, giving them the style and personality you want. + +Example: +``` +$ example-app install-files +Found 2 files in current directory +-> Installing "example-file"... +-> Installing "example-file-2"... +Successfully installed 2 files +$ example-app install-file foo-nonexist +Error: File "foo-nonexist" not found +``` +``` +$ clitheme apply-theme example-app-theme_clithemedef.txt +==> Generating data... +Successfully generated data +==> Applying theme...Success +Theme applied successfully +``` +``` +$ example-app install-files +o(≧v≦)o Great! Found 2 files in current directory! +(>^ω^<) Installing "example-file"... +(>^ω^<) Installing "example-file-2:"... +o(≧v≦)o Successfully installed 2 files! +$ example-app install-file foo-nonexist +ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found +``` + +## Features + +- Multi-language (Internationalization) support +- Supports applying multiple themes simultaneously +- Clear and easy-to-understand theme definition file (`clithemedef`) syntax +- The theme data can be easily accessed without the use of frontend module + +Not only `clitheme` can customize the output of command-line applications, it can also: +- Add support for another language for an application +- Support GUI applications + +# Basic usage + +## Data hierarchy and path naming + +Applications use **path names** to specify the string definitions they want. Subsections in the path name is separated using spaces. The first two subsections are usually reserved for the developer and application name. Theme definition files will use this path name to adopt corresponding string definitions, achieving the effect of output customization. + +For example, the path name `com.example example-app example-text` refers to the `example-text` string definition for the `example-app` application developed by `com.example`. + +It is not required to always follow this path naming convention and specifying global definitions (not related to any specific application) is allowed. For example, `global-entry` and `global-example global-text` are also valid path names. + +### Directly accessing the theme data hierarchy + +One of the key design principles of `clitheme` is that the use of frontend module is not needed to access the theme data hierarchy, and its method is easy to understand and implement. This is important especially in applications written in languages other than Python because Python is the only language supported by the frontend module. + +The data hierarchy is organized in a **subfolder structure**, meaning that every subsection in the path name represent a file or folder in the data hierarchy. + +For example, the contents of string definition `com.example example-app example-text` is stored in the directory `/com.example/example-app`. `` is `$XDG_DATA_HOME/clitheme/theme-data` or `~/.local/share/clitheme/theme-data` under Linux and macOS systems. + +Under Windows systems, `` is `%USERPROFILE%\.local\share\clitheme\theme-data` or `C:\Users\\.local\share\clitheme\theme-data`. + +To access a specific language of a string definition, add `__` plus the locale name to the end of the directory path. For example: `/com.example/example-app/example-text__en_US` + +In conclusion, to directly access a specific string definition, convert the path name to a directory path and access the file located there. + +## Frontend implementation and writing theme definition files + +### Using the built-in frontend module + +Using the frontend module provided by `clitheme` is very easy and straightforward. To access a string definition in the current theme setting, create a new `frontend.FetchDescriptor` object and use the provided `retrieve_entry_or_fallback` function. + +You need to pass the path name and a fallback string to this function. If the current theme setting does not provide the specified path name and string definition, the function will return the fallback string. + +You can pass the `domain_name`, `app_name`, and `subsections` arguments when creating a new `frontend.FetchDescriptor` object. When specified, these arguments will be automatically appended in front of the path name provided when calling the `retrieve_entry_or_fallback` function. + +Let's demonstrate it using the previous examples: + +```py +from clitheme import frontend + +# Create a new FetchDescriptor object +f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") + +# Corresponds to "Found 2 files in current directory" +fcount="[...]" +f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) + +# Corresponds to "-> Installing "example-file"..." +filename="[...]" +f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) + +# Corresponds to "Successfully installed 2 files" +f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) + +# Corresponds to "Error: File "foo-nonexist" not found" +filename_err="[...]" +f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) +``` + +### Using the fallback frontend module + +You can integrate the fallback frontend module provided by this project to better handle situations when `clitheme` does not exist on the system. This fallback module contains all the functions in the frontend module, and its functions will always return fallback values. + +Import the `clitheme_fallback.py` file from the repository and insert the following code in your project to use it: + +```py +try: + from clitheme import frontend +except (ModuleNotFoundError, ImportError): + import clitheme_fallback as frontend +``` + +The fallback module provided by this project will update accordingly with new versions. Therefore, it is recommended to import the latest version of this module to adopt the latest features. + +### Information your application should provide + +To allow users to write theme definition files of your application, your application should provide information about supported string definitions with its path name and default string. + +For example, your app can implement a feature to output all supported string definitions: + +``` +$ example-app --clitheme-output-defs +com.example example-app found-file +Found {} files in current directory + +com.example example-app installing-file +-> Installing "{}"... + +com.example example-app install-success +Successfully installed {} files + +com.example example-app file-not-found +Error: file "{}" not found +``` + +You can also include this information in your project's official documentation. The demo application in this repository provides an example of it and the corresponding README file is located in the folder `example-clithemedef`. + +### Writing theme definition files + +Consult the Wiki pages and documentation for detailed syntax of theme definition files. An example is provided below: + +``` +begin_header + name Example theme + version 1.0 + locales en_US + supported_apps clitheme_demo +end_header + +begin_main + in_domainapp com.example example-app + entry found-file + locale default o(≧v≦)o Great! Found {} files in current directory! + locale en_US o(≧v≦)o Great! Found {} files in current directory! + end_entry + entry installing-file + locale default (>^ω^<) Installing "{}"... + locale en_US (>^ω^<) Installing "{}"... + end_entry + entry install-success + locale default o(≧v≦)o Successfully installed {} files! + locale en_US o(≧v≦)o Successfully installed {} files! + end_entry + entry file-not-found + locale default ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found + locale en_US ಥ_ಥ Oh no, something went wrong! File "foo-nonexist" not found + end_entry +end_main +``` + +Use the command `clitheme apply-theme ` to apply the theme definition file onto the system. Supported applications will start using the string definitions listed in this file. + +# Installation + +Installing `clitheme` is very easy. You can use the provided Arch Linux, Debian, or pip package to install it. + +### Install using pip + +Download the whl file from the latest release and install it using `pip`: + + $ pip install clitheme--py3-none-any.whl + +### Install using Arch Linux package + +Because `pip` cannot be used to install Python packages onto an Arch Linux system, this project provides an Arch Linux package. + +Because the built package only supports a specific Python version and will stop working when Python is upgraded, this project only provides files needed to build the package. Please see **Build Arch Linux package** for more information. + +### Install using Debian package + +Because `pip` cannot be used to install Python packages onto certain Debian Linux distributions, this project provides a Debian package. + +Download the `.deb` file from the latest release and install it using `apt`: + + $ sudo apt install ./clitheme__all.deb + +## Building the installation package + +You can also build the installation package from source code, which allows you to include the latest or custom changes. This is the only method to install the latest development version of `clitheme`. + +### Build pip package + +`clitheme` uses the `hatchling` build backend, so installing it is required for building the package. + +First, install the `hatch` package. You can use the software package provided by your Linux distribution, or install it using `pip`: + + $ pip install hatch + +Next, making sure that you are under the project directory, use `hatch build` to build the package: + + $ hatch build + +If this command does not work, try using `hatchling build` instead. + +The corresponding pip package (whl file) can be found in the `dist` folder under the working directory. + +### Build Arch Linux package + +Make sure that the `base-devel` package is installed before building the package. You can install it using the following command: + + $ sudo pacman -S base-devel + +To build the package, run `makepkg` under the project directory. You can use the following commands: + +```sh +# Delete the temporary directories if makepkg has been run before. Issues will occur if you do not do so. +rm -rf buildtmp srctmp + +makepkg -si +# -s: install dependencies required for building the package +# -i: automatically install the built package + +# You can remove the temporary directories after you are finished +rm -rf buildtmp srctmp +``` + +**Warning:** You must rebuild the package every time Python is upgraded, because the package only works under the Python version when the package is built. + +### Build Debian package + +Because `pip` cannot be used to install Python packages onto certain Debian Linux distributions, this project provides a Debian package. + +The following packages are required prior to building the package: + +- `debhelper` +- `dh-python` +- `python3-hatchling` +- `dpkg-dev` + +They can be installed using this command: + + sudo apt install debhelper dh-python python3-hatchling dpkg-dev + +Run `dpkg-buildpackage -b` to build the package. A `.deb` file will be generated in the upper folder after the build process finishes. + +## More information + +- For more information, please reference the project's Wiki pages: https://gitee.com/swiftycode/clitheme/wikis/pages + - You can also access the pages in these repositories: + - https://gitee.com/swiftycode/clitheme-wiki-repo + - https://github.com/swiftycode256/clitheme-wiki-repo +- This repository is also synced onto GitHub (using Gitee automatic sync feature): https://github.com/swiftycode256/clitheme +- You are welcome to propose suggestions and changes using Issues and Pull Requests + - Use the Wiki repositories listed above for Wiki-related suggestions \ No newline at end of file diff --git a/README.md b/README.md index 0106cd1621515badb66feeb400ab507e7fcae04e..bea038a6138bed582f9ab8458defbfb1bbf5f09f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # clitheme - 命令行应用文本主题框架 +**中文** | [English](README.en.md) + `clitheme` 允许你定制命令行应用程序的输出,给它们一个你想要的风格和个性。 样例: @@ -34,14 +36,12 @@ $ example-app install-file foo-nonexist - 多语言支持 - 支持同时应用多个主题 - 简洁易懂的主题信息文件(`clithemedef`)语法 -- 无需前端API也可访问当前主题数据(易懂的数据结构) +- 无需frontend模块也可访问当前主题数据(易懂的数据结构) `clitheme` 不仅可以定制命令行应用的输出,它还可以: -- 为应用添加多语言支持 +- 为应用程序添加多语言支持 - 支持图形化应用 -**注意:**`clitheme`当前仅支持拥有Python 3的Linux和macOS系统,暂不支持Windows系统。 - # 基本用法 ## 数据结构和路径名称 @@ -54,21 +54,23 @@ $ example-app install-file foo-nonexist ### 直接访问主题数据结构 -`clitheme`的核心设计理念之一包括无需使用前端API就可以访问主题数据,并且访问方法直观易懂。这一点在使用其他语言编写的程序中尤其重要,因为前端API目前只提供Python程序的支持。 +`clitheme`的核心设计理念之一包括无需使用frontend模块就可以访问主题数据,并且访问方法直观易懂。这一点在使用其他语言编写的程序中尤其重要,因为frontend模块目前只提供Python程序的支持。 `clitheme`的数据结构采用了**子文件夹**的结构,意味着路径中的每一段代表着数据结构中的一个文件夹/文件。 -比如说,`com.example example-app example-text` 的字符串会被存储在`$datapath/com.example/example-app/example-text`。一般情况下,`$datapath`(数据根目录)是 `~/.local/share/clitheme/theme-data`。 +比如说,`com.example example-app example-text` 的字符串会被存储在`/com.example/example-app/example-text`。在Linux和macOS系统下,``是 `$XDG_DATA_HOME/clitheme/theme-data`或`~/.local/share/clitheme/theme-data`。 + +在Windows系统下,``是`%USERPROFILE%\.local\share\clitheme\theme-data`。(`C:\Users\<用户名称>\.local\share\clitheme\theme-data`) -如果需要访问该字符串的其他语言,直接在路径的最后添加`__`加上locale名称就可以了。比如:`$datapath/com.example/example-app/example-text__zh_CN` +如果需要访问该字符串的其他语言,直接在路径的最后添加`__`加上locale名称就可以了。比如:`/com.example/example-app/example-text__zh_CN` 所以说,如果需要直接访问字符串信息,只需要访问对应的文件路径就可以了。 ## 前端实施和编写主题文件 -### 使用内置前端API +### 使用内置frontend模块 -使用`clitheme`的python前端API非常简单。只需要新建一个`frontend.FetchDescriptor`实例然后调用该实例中的`retrieve_entry_or_fallback`即可。 +使用`clitheme`的frontend模块非常简单。只需要新建一个`frontend.FetchDescriptor`实例然后调用该实例中的`retrieve_entry_or_fallback`即可。 该函数需要提供路径名称和默认字符串。如果当前主题设定没有适配该字符串,则函数会返回提供的默认字符串。 @@ -83,24 +85,24 @@ from clitheme import frontend f=frontend.FetchDescriptor(domain_name="com.example", app_name="example-app") # 对应 “在当前目录找到了2个文件” -fcount=[...] +fcount="[...]" f.retrieve_entry_or_fallback("found-file", "在当前目录找到了{}个文件".format(str(fcount))) # 对应 “-> 正在安装 "example-file"...” -filename=[...] +filename="[...]" f.retrieve_entry_or_fallback("installing-file", "-> 正在安装\"{}\"...".format(filename)) # 对应 “已成功安装2个文件” f.retrieve_entry_or_fallback("install-success", "已成功安装{}个文件".format(str(fcount))) # 对应 “错误:找不到文件 "foo-nonexist"” -filename_err=[...] +filename_err="[...]" f.retrieve_entry_or_fallback("file-not-found", "错误:找不到文件 \"{}\"".format(filename_err)) ``` -### 使用前端fallback模块 +### 使用fallback模块 -应用程序还可以在src中内置本项目提供的fallback模块,以便更好的处理`clitheme`模块不存在时的情况。该fallback模块包括了前端API中的所有定义和功能,并且会永远返回失败时的默认值(fallback)。 +应用程序还可以在src中内置本项目提供的fallback模块,以便更好的处理`clitheme`模块不存在时的情况。该fallback模块包括了frontend模块中的所有定义和功能,并且会永远返回失败时的默认值(fallback)。 如需使用,请在你的项目文件中导入`clitheme_fallback.py`文件,并且在你的程序中包括以下代码: @@ -111,7 +113,7 @@ except (ModuleNotFoundError, ImportError): import clitheme_fallback as frontend ``` -本项目提供的fallback文件会随版本更新而更改,所以请定期往你的项目里导入最新的fallback文件以获得最新的功能。 +本项目提供的fallback文件会随版本更新而更改,所以请定期往你的项目里导入最新的fallback文件以适配最新的功能。 ### 应用程序应该提供的信息 @@ -134,7 +136,7 @@ com.example example-app file-not-found 错误:找不到文件 "{}" ``` -应用程序还可以在对应的官方文档中包括此信息。如需样例,请参考本仓库中`example-clithemedef`文件夹的README文件。 +应用程序还可以在对应的官方文档中包括此信息。如需样例,请参考本仓库中`example-clithemedef`文件夹的[README文件](example-clithemedef/README.zh-CN.md)。 ### 编写主题文件 @@ -145,6 +147,7 @@ begin_header name 样例主题 version 1.0 locales zh_CN + supported_apps clitheme_demo end_header begin_main @@ -172,7 +175,7 @@ end_main # 安装 -安装`clitheme`非常简单,您可以通过Arch Linux软件包或者pip软件包安装。 +安装`clitheme`非常简单,您可以通过Arch Linux软件包,Debian软件包,或者pip软件包安装。 ### 通过pip软件包安装 @@ -186,7 +189,7 @@ end_main 因为构建的Arch Linux软件包只兼容特定的Python版本,并且升级Python版本后会导致原软件包失效,本项目仅提供构建软件包的方式,不提供构建好的软件包。详细请见下方的**构建Arch Linux软件包**。 -### 通过deb软件包安装 +### 通过Debian软件包安装 因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 @@ -210,6 +213,8 @@ end_main $ hatch build +如果这个指令无法正常运行,请尝试运行`hatchling build`。 + 构建完成后,相应的安装包文件可以在当前目录中的`dist`文件夹中找到。 ### 构建Arch Linux软件包 @@ -221,6 +226,9 @@ end_main 构建软件包只需要在仓库目录中执行`makepkg`指令就可以了。你可以通过以下一系列命令来完成这些操作: ```sh +# 如果之前执行过makepkg,请删除之前生成的临时文件夹,否则构建时会出现问题 +rm -rf buildtmp srctmp + makepkg -si # -s:自动安装构建时需要的软件包 # -i:构建完后自动安装生成的软件包 @@ -231,7 +239,7 @@ rm -rf buildtmp srctmp **注意:** 每次升级Python版本时,你需要重新构建并安装软件包,因为软件包只兼容构建时使用的Python版本。 -### 构建deb软件包 +### 构建Debian软件包 因为部分Debian系统(如Ubuntu)上无法使用`pip`往系统里直接安装pip软件包,所以本项目提供Debian软件包。 @@ -240,17 +248,20 @@ rm -rf buildtmp srctmp - `debhelper` - `dh-python` - `python3-hatchling` +- `dpkg-dev` 你可以使用以下命令安装: - sudo apt install debhelper dh-python python3-hatchling + sudo apt install debhelper dh-python python3-hatchling dpkg-dev 安装完后,请在仓库目录中执行`dpkg-buildpackage -b`以构建软件包。完成后,你会在上层目录中获得一个`.deb`的文件。 ## 更多信息 - 更多的详细信息和文档请参考本项目Wiki页面:https://gitee.com/swiftycode/clitheme/wikis/pages + - 你也可以通过以下仓库访问这些Wiki页面: + - https://gitee.com/swiftycode/clitheme-wiki-repo + - https://github.com/swiftycode256/clitheme-wiki-repo +- 本仓库中的代码也同步在GitHub上(使用Gitee仓库镜像功能自动同步):https://github.com/swiftycode256/clitheme - 欢迎通过Issues和Pull Requests提交建议和改进。 -- 本仓库中的代码也同步在GitHub上:https://github.com/swiftycode256/clitheme - - GitHub仓库上只包含仓库代码,不包含新版本公告和发行版信息。 - - 不建议在GitHub仓库上提交Issues和Pull Requests,因为我可能不会及时回复。 + - Wiki页面也可以;你可以在上方列出的仓库中提交Issues和Pull Requests \ No newline at end of file diff --git a/clitheme-testblock_testprogram.py b/clitheme-testblock_testprogram.py new file mode 100644 index 0000000000000000000000000000000000000000..b18aa132c70f2a4368daf0a57bc1ddd3d8d6f81b --- /dev/null +++ b/clitheme-testblock_testprogram.py @@ -0,0 +1,72 @@ +#!/usr/bin/python3 + +# Program for testing multi-line (block) processing of _generator +from src.clitheme import _generator, frontend + +file_data=""" +begin_header + name untitled +end_header + +begin_main + entry test_entry + locale_block default en_US en C + + + this + and + that + + is just good + #enough + should have leading 2 lines and trailing 3 lines + + + + end_block + locale_block zh_CN + + + + 这是一个 + 很好的东西 + + #非常好 + ... + should have leading 3 lines and trailing 2 lines + + + end_block + end_entry +end_main +""" + +frontend.global_debugmode=True +if frontend.set_local_themedef(file_data)==False: + print("Error: set_local_themedef failed") + exit(1) +f=frontend.FetchDescriptor() +print("Default locale:") +f.disable_lang=True +# Not printing because debug mode already prints +(f.reof("test_entry", "Nonexistent")) +print("zh_CN locale:") +f.disable_lang=False +f.lang="zh_CN" +(f.reof("test_entry", "Nonexistent")) +f.debug_mode=False +for lang in ["C", "en", "en_US", "zh_CN"]: + f.disable_lang=True + name=f"test_entry__{lang}" + if f.entry_exists(name): + print(f"{name} OK") + else: + print(f"{name} not found") + +import sys +if sys.argv.__contains__("--preserve-temp"): + print(f"View generated data at {_generator.path}") + exit() + +import shutil +shutil.rmtree(_generator.path) \ No newline at end of file diff --git a/clitheme_example.py b/clitheme_demo.py similarity index 98% rename from clitheme_example.py rename to clitheme_demo.py index 4b9398acb86970cf8c9118ba56ece45a869f413f..533d0f318dead8cdc4b0b6324db8058ecba12f43 100755 --- a/clitheme_example.py +++ b/clitheme_demo.py @@ -1,5 +1,7 @@ #!/usr/bin/python3 +# This file is originally named clitheme_example.py + import os import sys from src.clitheme import frontend diff --git a/clitheme_fallback.py b/clitheme_fallback.py index 7a4754d11d69652d938b58b1688575c3e77e1168..8b94b6b266c1ea426ee1e7b8d4ca4c4107934820 100644 --- a/clitheme_fallback.py +++ b/clitheme_fallback.py @@ -1,29 +1,40 @@ """ -clitheme fallback frontend for 1.0 (returns fallback values for all functions) +clitheme fallback frontend for 1.1 (returns fallback values for all functions) """ -import os,sys -import random -import string from typing import Optional +data_path="" + global_domain="" global_appname="" global_subsections="" global_debugmode=False -global_lang="" # Override locale +global_lang="" global_disablelang=False +alt_path=None + +def set_local_themedef(file_content: str) -> bool: + """Fallback set_local_themedef function (always returns False)""" + return False +def unset_local_themedef(): + """Fallback unset_local_themedef function""" + return + class FetchDescriptor(): """ Object containing domain and app information used for fetching entries """ 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): - """fallback init function""" + """Fallback init function""" return - def retrieve_entry_or_fallback(self, entry_path: str, fallback_string: str) -> str: - """fallback retrieve_entry_or_fallback function (always return fallback string)""" + def retrieve_entry_or_Fallback(self, entry_path: str, fallback_string: str) -> str: + """Fallback retrieve_entry_or_Fallback function (always return Fallback string)""" return fallback_string - reof=retrieve_entry_or_fallback # a shorter alias of the function + reof=retrieve_entry_or_Fallback # a shorter alias of the function + def format_entry_or_fallback(self, entry_path: str, fallback_string: str, *args, **kwargs) -> str: + return fallback_string.format(*args, **kwargs) + feof=format_entry_or_fallback def entry_exists(self, entry_path: str) -> bool: - """fallback entry_exists function (always return false)""" + """Fallback entry_exists function (always return false)""" return False \ No newline at end of file diff --git a/clithemedef-test_testprogram.py b/clithemedef-test_testprogram.py index 9fb3f1931b1fb2d4a26738ce9de5264fc6cc356e..fb03f2aa17d4b7fd123dff514902dbd9f7944001 100644 --- a/clithemedef-test_testprogram.py +++ b/clithemedef-test_testprogram.py @@ -5,8 +5,8 @@ import random import string print("Testing generator function...") -mainfile_data=open("tests/clithemedef-test_mainfile.clithemedef.txt",'r').read() -expected_data=open("tests/clithemedef-test_expected.txt",'r').read() +mainfile_data=open("tests/clithemedef-test_mainfile.clithemedef.txt",'r', encoding="utf-8").read() +expected_data=open("tests/clithemedef-test_expected.txt",'r', encoding="utf-8").read() funcresult=_generator.generate_data_hierarchy(mainfile_data) errorcount=0 @@ -21,7 +21,7 @@ for line in expected_data.splitlines(): # read the file contents="" try: - contents=open(rootpath+"/"+current_path).read() + contents=open(rootpath+"/"+current_path, 'r', encoding="utf-8").read() print("File "+rootpath+"/"+current_path+" OK") except FileNotFoundError: print("[File] file "+rootpath+"/"+current_path+" does not exist") @@ -38,7 +38,7 @@ from src.clitheme import frontend frontend.global_lang="en_US.UTF-8" frontend.global_debugmode=True frontend.data_path=_generator.path+"/"+_globalvar.generator_data_pathname -expected_data_frontend=open("tests/clithemedef-test_expected-frontend.txt", 'r').read() +expected_data_frontend=open("tests/clithemedef-test_expected-frontend.txt", 'r', encoding="utf-8").read() current_path_frontend="" errorcount_frontend=0 for line in expected_data_frontend.splitlines(): diff --git a/debian/changelog b/debian/changelog index 46a1dc3eca231d578508ce4c82ebaae77f3226d3..37f8e4ce9abc7d6169c2c810397336005ea60fd7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +clitheme (1.1-r1-1) unstable; urgency=medium + + * Version 1.1-r1 in Debian package format + For more information please see: + https://gitee.com/swiftycode/clitheme/releases/tag/v1.1-r1 + + -- swiftycode <3291929745@qq.com> Wed, 17 Jan 2024 22:14:00 +0800 + clitheme (1.0-r2-1) unstable; urgency=medium * Version 1.0-r2 in Debian package format diff --git a/debian/control b/debian/control index 9e293d43e350515fc2d4f4dcfe9167c1dc640063..8227c380c52aa8eb5dd9363fab2e9d4422cf32c7 100644 --- a/debian/control +++ b/debian/control @@ -14,7 +14,7 @@ Multi-Arch: foreign Depends: ${misc:Depends}, python3 (> 3.7) Description: Application framework for text theming clitheme allows users to customize the output of supported programs, such as - multi-language support or mimicing your favorite cartoon character. It has an + multi-language support or mimicking your favorite cartoon character. It has an easy-to-understand syntax and implementation module for programs. . For more information, visit the homepage and the pages in the Wiki section. diff --git a/docs/clitheme.1 b/docs/clitheme.1 index 3d0c5b4af96ead8c557ef55a4273347f75d9d203..d9614b2915b24e750d17bbb555aead001101814b 100644 --- a/docs/clitheme.1 +++ b/docs/clitheme.1 @@ -1,4 +1,4 @@ -.TH clitheme 1 2023-12-16 +.TH clitheme 1 2024-01-20 .SH NAME clitheme \- frontend to customize output of applications .SH SYNOPSIS @@ -7,11 +7,11 @@ clitheme \- frontend to customize output of applications clitheme is a framework for applications that allows users to customize its output and messages through theme definition files. This CLI interface allows the user to control their current settings and theme definition on the system. .SH OPTIONS .P -.B apply-theme [themedef-file] [--overlay] [--preserve-temp] +.B apply-theme [themedef-file(s)] [--overlay] [--preserve-temp] .RS 7 -Applies a given theme definition file into the current system. Supported applications will immediately start using the defined values after performing this operation. +Applies the given theme definition file(s) into the current system. Supported applications will immediately start using the defined values after performing this operation. -Specify \fB--overlay\fR to append value definitions in the file onto the current data. +Specify \fB--overlay\fR to append value definitions in the file(s) onto the current data. Specify \fB--preserve-temp\fR to prevent the temporary directory from removed after the operation. .RE @@ -26,9 +26,9 @@ Outputs detailed information about the currently applied theme. If multiple them Removes the current theme data from the system. Supported applications will immediately stop using the defined values after this operation. .RE .P -.B generate-data-hierarchy [themedef-file] [--overlay] +.B generate-data [themedef-file(s)] [--overlay] .RS 7 -Generates the data hierarchy for a given theme definition file. This operation generates the same data as \fBapply-theme\fR, but does not apply it onto the system. +Generates the data hierarchy for the given theme definition file(s). This operation generates the same data as \fBapply-theme\fR, but does not apply it onto the system. This command is for debug purposes only. .RE diff --git a/example-clithemedef/README.zh-CN.md b/example-clithemedef/README.zh-CN.md index 722fe5bc50eec0cd76fbe955dafd5ec3d8922369..d05e345356fc602ba327bc99f6e26b3b95714ac4 100644 --- a/example-clithemedef/README.zh-CN.md +++ b/example-clithemedef/README.zh-CN.md @@ -4,37 +4,37 @@ --- -以下字符串会在`install-files`和`install-file`指令输出中用到: +**以下字符串会在`install-files`和`install-file`指令输出中用到:** -`com.example example-app found-file` +### `com.example example-app found-file` > 在当前目录找到了{}个文件 -`com.example example-app installing-file` +### `com.example example-app installing-file` > -> 正在安装 "{}"... -`com.example example-app install-success` +### `com.example example-app install-success` > 已成功安装{}个文件 该文本会被`install-files`指令输出调用。 -`com.example example-app install-success-file` +### `com.example example-app install-success-file` > 已成功安装"{}" 该文本会被`install-file`指令输出调用。 -`com.example example-app file-not-found` +### `com.example example-app file-not-found` > 错误:找不到文件"{}" -`com.example example-app format-error` +### `com.example example-app format-error` > 错误:命令语法不正确 -`com.example example-app directory-empty` +### `com.example example-app directory-empty` > 错误:当前目录里没有任何文件 @@ -42,20 +42,20 @@ --- -以下字符串会在帮助信息中用到: +**以下字符串会在帮助信息中用到:** -`com.example example-app helpmessage description-general` +### `com.example example-app helpmessage description-general` > 文件安装程序样例(不会修改系统中的文件) 该文本提供应用程序的名称,会在第一行显示。 -`com.example example-app helpmessage description-usageprompt` +### `com.example example-app helpmessage description-usageprompt` > 使用方法: 你可以通过此字符串定义”使用方法“的输出样式,会在命令列表前一行显示。 -`com.example example-app helpmessage unknown-command` +### `com.example example-app helpmessage unknown-command` > 错误:未知命令"{}" \ No newline at end of file diff --git a/example-clithemedef/example-theme-textemojis.clithemedef.txt b/example-clithemedef/example-theme-textemojis.clithemedef.txt index 256175323ee88eda1e236948e09e221e603862b6..afd87ce2b7c15a1d6c52bfdf78492a224842f339 100644 --- a/example-clithemedef/example-theme-textemojis.clithemedef.txt +++ b/example-clithemedef/example-theme-textemojis.clithemedef.txt @@ -1,8 +1,24 @@ begin_header name 颜文字样例主题 version 1.0 - locales zh_CN - supported_apps clitheme_example + # testing block input + locales_block + Simplified Chinese + 简体中文 + zh_CN + end_block + supported_apps_block + clitheme example + clitheme 样例应用 + clitheme_example + end_block + description_block + 适配项目中提供的example程序的一个颜文字主题,把它的输出变得可爱。 + 应用这个主题,沉浸在颜文字的世界中吧! + + 不要小看我的年龄,人家可是非常萌的~! + end_block + end_header begin_main @@ -49,4 +65,4 @@ begin_main locale default ಥ_ಥ 找不到指令"{}"!呜呜呜~ locale zh_CN ಥ_ಥ 找不到指令"{}"!呜呜呜~ end_entry -end_main \ No newline at end of file +end_main diff --git a/src/clitheme/_generator.py b/src/clitheme/_generator.py index 57c31ebce38056bfbef2701ca999210057755103..e927ffacf6e23ff3c0987bdb7e84f538c31844c1 100644 --- a/src/clitheme/_generator.py +++ b/src/clitheme/_generator.py @@ -1,27 +1,25 @@ """ Generator function used in applying themes (should not be invoked directly) """ -import os,sys +import os import string import random -import warnings +import re try: from . import _globalvar + from . import frontend except ImportError: # for test program import _globalvar - -header_begin=\ - """# clitheme theme info header - # This file is automatically generated by clitheme and should not be edited - """ + import frontend path="" # to be generated by function +fd=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") + def handle_error(message): - raise SyntaxError(message) + raise SyntaxError(fd.feof("error-str", "Syntax error: {msg}", msg=message)) def handle_warning(message): - # warnings.warn(message,SyntaxWarning) - print("Warning: {}".format(message)) + print(fd.feof("warning-str", "Warning: {msg}", msg=message)) def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate directories (excluding file itself) current_path=path current_entry="" # for error output @@ -29,8 +27,8 @@ def recursive_mkdir(path, entry_name, line_number_debug): # recursively generate current_entry+=x+" " current_path+="/"+x if os.path.isfile(current_path): # conflict with entry file - handle_error("Line {}: cannot create subsection \"{}\" because an entry with the same name already exists"\ - .format(str(line_number_debug), current_entry)) + handle_error(fd.feof("subsection-conflict-err", "Line {num}: cannot create subsection \"{name}\" because an entry with the same name already exists", \ + num=str(line_number_debug), name=current_entry)) elif os.path.isdir(str(current_path))==False: # directory does not exist os.mkdir(current_path) def add_entry(path, entry_name, entry_content, line_number_debug): # add entry to where it belongs (assuming recursive_mkdir already completed) @@ -38,12 +36,12 @@ def add_entry(path, entry_name, entry_content, line_number_debug): # add entry t for x in entry_name.split(): target_path+="/"+x if os.path.isdir(target_path): - handle_error("Line {}: cannot create entry \"{}\" because a subsection with the same name already exists"\ - .format(str(line_number_debug),entry_name)) + handle_error(fd.feof("entry-conflict-err", "Line {num}: cannot create entry \"{name}\" because a subsection with the same name already exists", \ + num=str(line_number_debug), name=entry_name)) elif os.path.isfile(target_path): - handle_warning("Line {}: repeated entry \"{}\", overwriting"\ - .format(str(line_number_debug),entry_name)) - f=open(target_path,'w') + handle_warning(fd.feof("repeated-entry-warn", "Line {num}: repeated entry \"{name}\", overwriting", \ + num=str(line_number_debug), name=entry_name)) + f=open(target_path,'w', encoding="utf-8") f.write(entry_content+"\n") def splitarray_to_string(split_content): final="" @@ -55,11 +53,22 @@ def write_infofile(path,filename,content,line_number_debug, header_name_debug): os.makedirs(path) target_path=path+"/"+filename if os.path.isfile(target_path): - handle_warning("Line {}: repeated header info \"{}\", overwriting"\ - .format(str(line_number_debug), header_name_debug)) - f=open(target_path,'w') + handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ + num=str(line_number_debug), name=header_name_debug)) + f=open(target_path,'w', encoding="utf-8") f.write(content+'\n') +def write_infofile_v2(path: str, filename: str, content_phrases: list[str], line_number_debug: int, header_name_debug: str): + if not os.path.isdir(path): + os.makedirs(path) + target_path=path+"/"+filename + if os.path.isfile(target_path): + handle_warning(fd.feof("repeated-header-warn", "Line {num}: repeated header info \"{name}\", overwriting", \ + num=str(line_number_debug), name=header_name_debug)) + f=open(target_path,'w', encoding="utf-8") + for line in content_phrases: + f.write(line+"\n") + def generate_custom_path(): # Generate a temporary path global path @@ -79,60 +88,142 @@ def generate_data_hierarchy(file_content, custom_path_gen=True, custom_infofile_ if not os.path.exists(path): os.mkdir(path) datapath=path+"/"+_globalvar.generator_data_pathname if not os.path.exists(datapath): os.mkdir(datapath) - # headerinfo_file=open(path+"/current-theme.clithemeheader",'x') - # headerinfo_file.write(header_begin) current_status="" # header, main, entry linenumber=0 # To detect repeated blocks headerparsed=False mainparsed=False + current_domainapp="" # for in_domainapp and unset_domainapp in main block current_entry_name="" # for entry current_subsection="" # for in_subsection + + current_entry_locale="" # for handling locale_block + current_entry_linenumber=-1 + + current_header_entry="" # for block input in header + current_header_linenumber=-1 + + blockinput=False # for multi-line (block) input + blockinput_data="" # data of current block input + blockinput_minspaces=-1 # min number of whitespaces for line in file_content.splitlines(): linenumber+=1 phrases=line.split() - if line.strip()=="" or line.strip()[0]=="#": # if empty line or comment + if blockinput==False and (line.strip()=="" or line.strip()[0]=="#"): # if empty line or comment (except in block input mode) continue - if current_status=="": # expect begin_header or begin_main + + if blockinput==True: + if len(phrases)>0 and phrases[0]=="end_block": + if blockinput_minspaces!=-1: + # process whitespaces + # trim amount of leading whitespaces on each line + pattern=r"(?P\n|^)[ ]{"+str(blockinput_minspaces)+"}" + blockinput_data=re.sub(pattern,r"\g", blockinput_data) + if current_status=="entry": + for this_locale in current_entry_locale.split(): + target_entry=current_entry_name + if this_locale!="default": + target_entry+="__"+this_locale + add_entry(datapath,target_entry, blockinput_data, current_entry_linenumber) + # clear data + current_entry_locale="" + current_entry_linenumber=-1 + elif current_status=="header": + if current_header_entry!="description": + # trim all leading whitespaces + blockinput_data=re.sub(r"(?P\n|^)[ ]+",r"\g", blockinput_data) + # trim all trailing whitespaces + blockinput_data=re.sub(r"[ ]+(?P\n|$)",r"\g", blockinput_data) + # trim all leading/trailing newlines + blockinput_data=re.sub(r"(\A\n+|\n+\Z)", "", blockinput_data) + filename="clithemeinfo_"+current_header_entry+"_v2" + if current_header_entry=="description": + filename="clithemeinfo_"+current_header_entry + write_infofile( \ + path+"/"+_globalvar.generator_info_pathname+"/"+custom_infofile_name, \ + filename,\ + blockinput_data,current_header_linenumber,current_header_entry) # e.g. [...]/theme-info/1/clithemeinfo_description_v2 + # clear data + current_header_entry="" + current_header_linenumber=-1 + else: # the unlikely case + handle_error(fd.feof("internal-error-blockinput", "Line {num}: internal error while handling block input; please file a bug report", num=str(linenumber))) + # clear data + blockinput=False + blockinput_data="" + else: + if blockinput_data!="": blockinput_data+="\n" + line_content=line.strip() + if line_content=="": # empty line + if blockinput_data=="": blockinput_data+=" " + continue + # Calculate whitespaces + spaces=-1 + ws_match=re.search(r"^\s+", line) # match leading whitespaces + if ws_match==None: # no leading spaces + spaces=0 + else: + leading_whitespace=ws_match.group() + # substitute \t with 8 spaces + leading_whitespace=re.sub(r"\t"," "*8, leading_whitespace) + # append it to line_content + line_content=leading_whitespace+line_content + # write line_content to data + blockinput_data+=line_content + spaces=len(leading_whitespace) + # update min count + if spaces!=-1 and (spaces3: + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) # sanity check if _globalvar.sanity_check(phrases[1]+" "+phrases[2])==False: - handle_error("Line {}: domain and app names {}".format(str(linenumber), _globalvar.sanity_check_error_message)) + handle_error(fd.feof("sanity-check-domainapp-err", "Line {num}: domain and app names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) current_domainapp=phrases[1]+" "+phrases[2] current_subsection="" elif phrases[0]=="in_subsection": if len(phrases)<2: - handle_error("Format error in {} at line {}".format(phrases[0],linenumber)) + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) # check if in_domainapp is set if current_domainapp=="": - handle_error("Line {}: in_subsection used before in_domainapp".format(linenumber)) + handle_error(fd.feof("subsection-before-domainapp-err", "Line {num}: in_subsection used before in_domainapp", num=str(linenumber))) # sanity check if _globalvar.sanity_check(splitarray_to_string(phrases[1:]))==False: - handle_error("Line {}: subsection names {}".format(str(linenumber), _globalvar.sanity_check_error_message)) + handle_error(fd.feof("sanity-check-subsection-err", "Line {num}: subsection names {sanitycheck_msg}", num=str(linenumber), sanitycheck_msg=_globalvar.sanity_check_error_message)) current_subsection=splitarray_to_string(phrases[1:]) elif phrases[0]=="unset_domainapp": if len(phrases)!=1: - handle_error("Extra arguments after \"{}\" on line {}".format(phrases[0],str(linenumber))) + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) current_domainapp="" current_subsection="" elif phrases[0]=="unset_subsection": if len(phrases)!=1: - handle_error("Extra arguments after \"{}\" on line {}".format(phrases[0],str(linenumber))) + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) current_subsection="" elif phrases[0]=="end_main": if len(phrases)!=1: - handle_error("Extra arguments after \"{}\" on line {}".format(phrases[0],str(linenumber))) + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) current_status="" mainparsed=True - else: handle_error("Unexpected \"{}\" on line {}".format(phrases[0],str(linenumber))) + else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(linenumber))) elif current_status=="entry": # expect locale, end_entry if phrases[0]=="locale": if len(phrases)<3: - handle_error("Not enough arguments for {} line at line {}"\ - .format(phrases[0],str(linenumber))) + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) content=splitarray_to_string(phrases[2:]) target_entry=current_entry_name if phrases[1]!="default": target_entry+="__"+phrases[1] add_entry(datapath,target_entry,content,linenumber) + elif phrases[0]=="locale_block": + if len(phrases)<2: + handle_error(fd.feof("not-enough-args-err", "Not enough arguments for \"{phrase}\" at line {num}", phrase=phrases[0], num=str(linenumber))) + current_entry_locale=splitarray_to_string(phrases[1:]) + current_entry_linenumber=linenumber + blockinput=True # start block input elif phrases[0]=="end_entry": if len(phrases)!=1: - handle_error("Extra arguments after \"{}\" on line {}".format(phrases[0],str(linenumber))) + handle_error(fd.feof("extra-arguments-err", "Extra arguments after \"{phrase}\" on line {num}", num=str(linenumber), phrase=phrases[0])) current_status="main" - else: handle_error("Unexpected \"{}\" on line {}".format(phrases[0],str(linenumber))) + current_entry_name="" + else: handle_error(fd.feof("invalid-phrase-err", "Unexpected \"{phrase}\" on line {num}", phrase=phrases[0], num=str(linenumber))) if not headerparsed or not mainparsed: - handle_error("Missing or incomplete header or main block") + handle_error(fd.reof("incomplete-block-err", "Missing or incomplete header or main block")) # Update current theme index - theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w') + theme_index=open(path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename, 'w', encoding="utf-8") theme_index.write(custom_infofile_name+"\n") - return True # Everything is successul! :) \ No newline at end of file + return True # Everything is successful! :) diff --git a/src/clitheme/_globalvar.py b/src/clitheme/_globalvar.py index 2c6bb7ecd276463968091884966d5e8dea905c50..8bcc48e63226f38ba991bedc317dc59427303e6a 100644 --- a/src/clitheme/_globalvar.py +++ b/src/clitheme/_globalvar.py @@ -7,38 +7,68 @@ try: from . import _version except ImportError: import _version clitheme_root_data_path="" -try: - clitheme_root_data_path=os.environ["XDG_DATA_HOME"]+"/clitheme" -except KeyError: None +if os.name=="posix": # Linux/macOS only + try: + clitheme_root_data_path=os.environ["XDG_DATA_HOME"]+"/clitheme" + except KeyError: pass + +error_msg_str= \ +"""[clitheme] Error: unable to get your home directory or invalid home directory information. +Please make sure that the {var} environment variable is set correctly. +Try restarting your terminal session to fix this issue.""" if clitheme_root_data_path=="": # prev did not succeed try: - if not os.environ['HOME'].startswith("/"): # sanity check - raise KeyError - clitheme_root_data_path=os.environ["HOME"]+"/.local/share/clitheme" + if os.name=="nt": # Windows + clitheme_root_data_path=os.environ["USERPROFILE"]+"\\.local\\share\\clitheme" + else: + if not os.environ['HOME'].startswith('/'): # sanity check + raise KeyError + clitheme_root_data_path=os.environ["HOME"]+"/.local/share/clitheme" except KeyError: - print("[clitheme] Error: unable to get your home directory or invaild home directory information") - print("Please make sure that the $HOME environment variable is set correctly.") - print("Try restarting your terminal session to fix this issue.") + var="$HOME" + if os.name=="nt": + var=r"%USERPROFILE%" + print(error_msg_str.format(var=var)) exit(1) clitheme_temp_root="/tmp" +if os.name=="nt": + clitheme_temp_root=os.environ['TEMP'] clitheme_version=_version.__version__ generator_info_pathname="theme-info" # e.g. ~/.local/share/clitheme/theme-info generator_data_pathname="theme-data" # e.g. ~/.local/share/clitheme/theme-data generator_index_filename="current_theme_index" entry_banphrases=['/','\\'] -# function to check whether the pathname contains invaild phrases +startswith_banphrases=['.'] +banphrase_error_message="cannot contain '{char}'" +startswith_error_message="cannot start with '{char}'" +# function to check whether the pathname contains invalid phrases # - cannot start with . # - cannot contain banphrases sanity_check_error_message="" + +# retrieve the entry only once to avoid dead loop in frontend.FetchDescriptor callbacks +msg_retrieved=False def sanity_check(path): + # retrieve the entry (only for the first time) + try: from . import frontend + except ImportError: import frontend + global msg_retrieved + if not msg_retrieved: + msg_retrieved=True + f=frontend.FetchDescriptor(domain_name="swiftycode", app_name="clitheme", subsections="generator") + global banphrase_error_message + banphrase_error_message=f.feof("sanity-check-msg-banphrase-err", banphrase_error_message, char="{char}") + global startswith_error_message + startswith_error_message=f.feof("sanity-check-msg-startswith-err", startswith_error_message, char="{char}") global sanity_check_error_message for p in path.split(): - if p.startswith('.'): - sanity_check_error_message="cannot start with '.'" - return False + for b in startswith_banphrases: + if p.startswith(b): + sanity_check_error_message=startswith_error_message.format(char=b) + return False for b in entry_banphrases: if p.find(b)!=-1: - sanity_check_error_message="cannot contain '{}'".format(b) + sanity_check_error_message=banphrase_error_message.format(char=b) return False - return True \ No newline at end of file + return True diff --git a/src/clitheme/_version.py b/src/clitheme/_version.py index a9896aba629bc2c53118ebf8081dfd19aaa31262..2c71cedefb192b6c5c77a3eda7baa8cedff8d35a 100644 --- a/src/clitheme/_version.py +++ b/src/clitheme/_version.py @@ -1,10 +1,10 @@ # Version definition file; define the package version here -# Version CANNOT contain hyphens (-); use underscores (_) instead # The __version__ variable must be a literal string; DO NOT use variables -__version__="1.0-r2" +__version__="1.1-r1" major=1 -minor=0 -release=2 # 0 stands for "dev" +minor=1 +release=1 # 0 stands for "dev" # For PKGBUILD -version_main="1.0_r2" +# version_main CANNOT contain hyphens (-); use underscores (_) instead +version_main="1.1_r1" version_buildnumber=1 \ No newline at end of file diff --git a/src/clitheme/cli.py b/src/clitheme/cli.py index 725ccd2b8f633fbd61ed9f14aad90d9b42525849..b9abdc5714d651b33d0461c6c4280d812d2513e3 100755 --- a/src/clitheme/cli.py +++ b/src/clitheme/cli.py @@ -7,128 +7,123 @@ clitheme command line utility interface import os import sys import shutil +import re try: from . import _globalvar from . import _generator + from . import frontend except ImportError: import _globalvar import _generator + import frontend usage_description=\ """Usage: {0} apply-theme [themedef-file] [--overlay] [--preserve-temp] {0} get-current-theme-info {0} unset-current-theme - {0} generate-data-hierarchy [themedef-file] [--overlay] + {0} generate-data [themedef-file] [--overlay] {0} --help {0} --version""" -def apply_theme(file_content: str, overlay: bool, preserve_temp=False): +frontend.global_domain="swiftycode" +frontend.global_appname="clitheme" +frontend.global_subsections="cli" + +def apply_theme(file_contents: list[str], overlay: bool, preserve_temp=False, generate_only=False): """ - Apply the theme using the provided definition file content. + Apply the theme using the provided definition file contents in a list[str] object. - Set overlay=True to overlay the theme on top of existing theme[s] - Set preserve_temp=True to preserve the temp directory (debugging purposes) + - Set generate_only=True to generate the data hierarchy only (and not apply the theme) """ - if overlay: print("Overlay specified") - print("==> Generating data...") + f=frontend.FetchDescriptor(subsections="cli apply-theme") + if overlay: print(f.reof("overlay-msg", "Overlay specified")) + print(f.reof("generating-data", "==> Generating data...")) index=1 generate_path=True if overlay: # Check if current data exists if not os.path.isfile(_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename): - print("Error: no theme set or the current data is corrupt") - print("Try setting a theme first") + print(f.reof("overlay-no-data", \ + "Error: no theme set or the current data is corrupt\nTry setting a theme first")) return 1 # update index - try: index=int(open(_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename,'r').read().strip())+1 + try: index=int(open(_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename,'r', encoding="utf-8").read().strip())+1 except ValueError: - print("Error: the current data is corrupt") - print("Remove the current theme, set the theme, and try again") + print(f.reof("overlay-data-error", \ + "Error: the current data is corrupt\nRemove the current theme, set the theme, and try again")) return 1 # copy the current data into the temp directory _generator.generate_custom_path() shutil.copytree(_globalvar.clitheme_root_data_path, _generator.path) generate_path=False - # Generate data hierarchy, erase current data, copy it to data path - try: - _generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index)) - except SyntaxError: - print("Error\nAn error occurred while generating the data:\n{}".format(str(sys.exc_info()[1]))) - return 1 - print("Successfully generated data") - if preserve_temp: - print("View at {}".format(_generator.path)) - print("==> Applying theme...",end='') + for i in range(len(file_contents)): + if len(file_contents)>1: + print(" "+f.feof("processing-file", "> Processing file {filename}...", filename=str(i+1))) + file_content=file_contents[i] + # Generate data hierarchy, erase current data, copy it to data path + try: + _generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index)) + generate_path=False # Don't generate another temp folder after first one + index+=1 + except SyntaxError: + print(f.feof("generate-data-error", "[File {index}] An error occurred while generating the data:\n{message}", \ + index=str(i+1), message=str(sys.exc_info()[1]) )) + return 1 + if len(file_contents)>1: + print(" "+f.reof("all-finished", "> All finished")) + print(f.reof("generate-data-success", "Successfully generated data")) + if preserve_temp or generate_only: + if os.name=="nt": + print(f.feof("view-temp-dir", "View at {path}", path=re.sub(r"/", r"\\", _generator.path))) # make the output look pretty + else: + print(f.feof("view-temp-dir", "View at {path}", path=_generator.path)) + if generate_only: return 0 + # ---Stop here if generate_only is set--- + + print(f.reof("applying-theme", "==> Applying theme...")) # remove the current data, ignoring directory not found error try: shutil.rmtree(_globalvar.clitheme_root_data_path) - except FileNotFoundError: None + except FileNotFoundError: pass + except Exception: + print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) + return 1 + try: shutil.copytree(_generator.path, _globalvar.clitheme_root_data_path) except Exception: - print("Error\nAn error occurred while applying the theme:\n{}".format(str(sys.exc_info()[1]))) + print(f.feof("apply-theme-error", "An error occurred while applying the theme:\n{message}", message=str(sys.exc_info()[1]))) return 1 - print("Success\nTheme applied successfully") + print(f.reof("apply-theme-success", "Theme applied successfully")) if not preserve_temp: try: shutil.rmtree(_generator.path) - except Exception: None - return 0 - -def generate_data_hierarchy(file_content: str, overlay: bool): - """ - Generate the data hierarchy at the temporary directory (debugging purposes only) - """ - if overlay: print("Overlay specified") - print("==> Generating data...") - index=1 - generate_path=True - if overlay: - # Check if current data exists - if not os.path.isfile(_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename): - print("Error: no theme set or the current data is corrupt") - print("Try setting a theme first") - return 1 - # update index - try: index=int(open(_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname+"/"+_globalvar.generator_index_filename,'r').read().strip())+1 - except ValueError: - print("Error: the current data is corrupt") - print("Remove the current theme, set the theme, and try again") - return 1 - # copy the current data into the temp directory - _generator.generate_custom_path() - shutil.copytree(_globalvar.clitheme_root_data_path, _generator.path) - generate_path=False - # Generate data hierarchy, erase current data, copy it to data path - try: - _generator.generate_data_hierarchy(file_content, custom_path_gen=generate_path,custom_infofile_name=str(index)) - except SyntaxError: - print("Error\nAn error occurred while generating the data:\n{}".format(str(sys.exc_info()[1]))) - return 1 - print("Successfully generated data") - print("View at {}".format(_generator.path)) + except Exception: pass return 0 def unset_current_theme(): """ Delete the current theme data hierarchy from the data path """ - print("==> Removing data...", end='') + f=frontend.FetchDescriptor(subsections="cli unset-current-theme") try: shutil.rmtree(_globalvar.clitheme_root_data_path) except FileNotFoundError: - print("Error\nNo theme data present (no theme was set)") + print(f.reof("no-data-found", "Error: No theme data present (no theme was set)")) return 1 except Exception: - print("Error\nAn error occurred while removing the data:\n{}".format(str(sys.exc_info()[1]))) + print(f.feof("remove-data-error", "An error occurred while removing the data:\n{message}", message=str(sys.exc_info()[1]))) return 1 - print("Success\nSuccessfully removed the current theme data") + print(f.reof("remove-data-success", "Successfully removed the current theme data")) return 0 def get_current_theme_info(): """ Get the current theme info """ + f=frontend.FetchDescriptor(subsections="cli get-current-theme-info") search_path=_globalvar.clitheme_root_data_path+"/"+_globalvar.generator_info_pathname if not os.path.isdir(search_path): - print("No theme currently set") + print(f.reof("no-theme", "No theme currently set")) return 1 lsdir_result=os.listdir(search_path) lsdir_result.sort(reverse=True) # sort by latest installed @@ -136,43 +131,64 @@ def get_current_theme_info(): for x in lsdir_result: if os.path.isdir(search_path+"/"+x): lsdir_num+=1 - if lsdir_num<=1: print("Currently installed theme: ") - else: print("Overlay history (sorted by latest installed):") + if lsdir_num<=1: + print(f.reof("current-theme-msg", "Currently installed theme:")) + else: + print(f.reof("overlay-history-msg", "Overlay history (sorted by latest installed):")) for theme_pathname in lsdir_result: target_path=search_path+"/"+theme_pathname if not os.path.isdir(target_path): continue # skip current_theme_index file # name name="(Unknown)" if os.path.isfile(target_path+"/"+"clithemeinfo_name"): - name=open(target_path+"/"+"clithemeinfo_name", 'r').read().strip() + name=open(target_path+"/"+"clithemeinfo_name", 'r', encoding="utf-8").read().strip() print("[{}]: {}".format(theme_pathname, name)) # version version="(Unknown)" if os.path.isfile(target_path+"/"+"clithemeinfo_version"): - version=open(target_path+"/"+"clithemeinfo_version", 'r').read().strip() - print("Version: {}".format(version)) + version=open(target_path+"/"+"clithemeinfo_version", 'r', encoding="utf-8").read().strip() + print(f.feof("version-str", "Version: {ver}", ver=version)) + # description + description="(Unknown)" + if os.path.isfile(target_path+"/"+"clithemeinfo_description"): + description=open(target_path+"/"+"clithemeinfo_description", 'r', encoding="utf-8").read() + print(f.reof("description-str", "Description:")) + print(description) # locales locales="(Unknown)" - if os.path.isfile(target_path+"/"+"clithemeinfo_locales"): - locales=open(target_path+"/"+"clithemeinfo_locales", 'r').read().strip() - print("Supported locales: ") + # version 2: items are separated by newlines instead of spaces + if os.path.isfile(target_path+"/"+"clithemeinfo_locales_v2"): + locales=open(target_path+"/"+"clithemeinfo_locales_v2", 'r', encoding="utf-8").read().strip() + print(f.reof("locales-str", "Supported locales:")) + for locale in locales.splitlines(): + if locale.strip()!="": + print(f.feof("list-item", "• {content}", content=locale.strip())) + elif os.path.isfile(target_path+"/"+"clithemeinfo_locales"): + locales=open(target_path+"/"+"clithemeinfo_locales", 'r', encoding="utf-8").read().strip() + print(f.reof("locales-str", "Supported locales:")) for locale in locales.split(): - print("• {}".format(locale)) + print(f.feof("list-item", "• {content}", content=locale.strip())) # supported_apps supported_apps="(Unknown)" - if os.path.isfile(target_path+"/"+"clithemeinfo_supported_apps"): - supported_apps=open(target_path+"/"+"clithemeinfo_supported_apps", 'r').read().strip() - print("Supported apps: ") + if os.path.isfile(target_path+"/"+"clithemeinfo_supported_apps_v2"): + supported_apps=open(target_path+"/"+"clithemeinfo_supported_apps_v2", 'r', encoding="utf-8").read().strip() + print(f.reof("supported-apps-str", "Supported apps:")) + for app in supported_apps.splitlines(): + if app.strip()!="": + print(f.feof("list-item", "• {content}", content=app.strip())) + elif os.path.isfile(target_path+"/"+"clithemeinfo_supported_apps"): + supported_apps=open(target_path+"/"+"clithemeinfo_supported_apps", 'r', encoding="utf-8").read().strip() + print(f.reof("supported-apps-str", "Supported apps:")) for app in supported_apps.split(): - print("• {}".format(app)) - print() # newline + print(f.feof("list-item", "• {content}", content=app.strip())) return 0 def is_option(arg): return arg.strip()[0:1]=="-" def handle_usage_error(message, cli_args_first): + f=frontend.FetchDescriptor() print(message) - print("Run {0} --help for usage information".format(cli_args_first)) + print(f.feof("help-usage-prompt", "Run {clitheme} --help for usage information", clitheme=cli_args_first)) return 1 def main(cli_args): """ @@ -180,70 +196,72 @@ def main(cli_args): Provide a list of command line arguments to this function through cli_args. """ + f=frontend.FetchDescriptor() + arg_first="clitheme" # controls what appears as the command name in messages if len(cli_args)<=1: # no arguments passed - print(usage_description.format(cli_args[0])) - print("Error: no command or option specified") + print(usage_description.format(arg_first)) + print(f.reof("no-command", "Error: no command or option specified")) return 1 - if cli_args[1]=="apply-theme": + if cli_args[1]=="apply-theme" or cli_args[1]=="generate-data" or cli_args[1]=="generate-data-hierarchy": if len(cli_args)<3: - return handle_usage_error("Error: not enough arguments", cli_args[0]) - path="" + return handle_usage_error(f.reof("not-enough-arguments", "Error: not enough arguments"), arg_first) + generate_only=(cli_args[1]=="generate-data" or cli_args[1]=="generate-data-hierarchy") + paths=[] overlay=False preserve_temp=False for arg in cli_args[2:]: if is_option(arg): if arg.strip()=="--overlay": overlay=True - elif arg.strip()=="--preserve-temp": preserve_temp=True - else: return handle_usage_error("Unknown option \"{}\"".format(arg), cli_args[0]) + elif arg.strip()=="--preserve-temp" and not generate_only: preserve_temp=True + else: return handle_usage_error(f.feof("unknown-option", "Error: unknown option \"{option}\"", option=arg), arg_first) else: - if path!="": # already specified path - return handle_usage_error("Error: too many arguments", cli_args[0]) - path=arg - contents="" - try: - contents=open(path, 'r').read() - except Exception: - print("An error occured while reading the file: \n{}".format(str(sys.exc_info()[1]))) - return 1 - return apply_theme(contents, overlay=overlay, preserve_temp=preserve_temp) + paths.append(arg) + fi=frontend.FetchDescriptor(subsections="cli apply-theme") + if len(paths)>1 or True: # currently set to True for now + if generate_only: + print(fi.reof("generate-data-msg", "The theme data will be generated from the following definition files in the following order:")) + else: + print(fi.reof("apply-theme-msg", "The following definition files will be applied in the following order: ")) + for i in range(len(paths)): + path=paths[i] + print("\t{}: {}".format(str(i+1), path)) + if not generate_only: + if os.path.isdir(_globalvar.clitheme_root_data_path) and overlay==False: + print(fi.reof("overwrite-notice", "The existing theme data will be overwritten if you continue.")) + if overlay==True: + print(fi.reof("overlay-notice", "The definition files will be appended on top of the existing theme data.")) + inpstr=fi.reof("confirm-prompt", "Do you want to continue? [y/n]") + inp=input(inpstr+" ").strip().lower() + if not (inp=="y" or inp=="yes"): + return 1 + content_list=[] + for i in range(len(paths)): + path=paths[i] + try: + content_list.append(open(path, 'r', encoding="utf-8").read()) + except Exception: + print(fi.feof("read-file-error", "[File {index}] An error occurred while reading the file: \n{message}", \ + index=str(i+1), message=str(sys.exc_info()[1]))) + return 1 + return apply_theme(content_list, overlay=overlay, preserve_temp=preserve_temp, generate_only=generate_only) elif cli_args[1]=="get-current-theme-info": if len(cli_args)>2: # disabled additional options - return handle_usage_error("Error: too many arguments", cli_args[0]) + return handle_usage_error(f.reof("too-many-arguments", "Error: too many arguments"), arg_first) return get_current_theme_info() elif cli_args[1]=="unset-current-theme": if len(cli_args)>2: - return handle_usage_error("Error: too many arguments", cli_args[0]) + return handle_usage_error(f.reof("too-many-arguments", "Error: too many arguments"), arg_first) return unset_current_theme() - elif cli_args[1]=="generate-data-hierarchy": - if len(cli_args)<3: - return handle_usage_error("Error: not enough arguments", cli_args[0]) - path="" - overlay=False - for arg in cli_args[2:]: - if is_option(arg): - if arg.strip()=="--overlay": overlay=True - else: return handle_usage_error("Unknown option \"{}\"".format(arg), cli_args[0]) - else: - if path!="": # already specified path - return handle_usage_error("Error: too many arguments", cli_args[0]) - path=arg - contents="" - try: - contents=open(path, 'r').read() - except Exception: - print("An error occured while reading the file: \n{}".format(str(sys.exc_info()[1]))) - return 1 - return generate_data_hierarchy(contents, overlay=overlay) elif cli_args[1]=="--version": - print("clitheme version {0}".format(_globalvar.clitheme_version)) + print(f.feof("version-str", "clitheme version {ver}", ver=_globalvar.clitheme_version)) else: if cli_args[1]=="--help": - print(usage_description.format(cli_args[0])) + print(usage_description.format(arg_first)) else: - return handle_usage_error("Error: unknown command \"{0}\"".format(cli_args[1]), cli_args[0]) + return handle_usage_error(f.feof("unknown-command", "Error: unknown command \"{cmd}\"", cmd=cli_args[1]), arg_first) return 0 def script_main(): # for script exit(main(sys.argv)) if __name__=="__main__": - exit(main(sys.argv)) \ No newline at end of file + exit(main(sys.argv)) diff --git a/src/clitheme/frontend.py b/src/clitheme/frontend.py index 8c3f372296660511c3a31d798688f1390483760f..d71d1764fa316301619365cfaaf41d120cfe8756 100644 --- a/src/clitheme/frontend.py +++ b/src/clitheme/frontend.py @@ -5,6 +5,8 @@ clitheme front-end interface for accessing entries import os,sys import random import string +import re +import hashlib from typing import Optional try: from . import _globalvar @@ -19,6 +21,53 @@ global_debugmode=False global_lang="" # Override locale global_disablelang=False +alt_path=None +alt_path_dirname=None +# Support for setting a local definition file +# - Generate the data in a temporary directory named after content hash +# - First try alt_path then data_path + +def set_local_themedef(file_content: str, overlay: bool=False) -> bool: + """ + Sets a local theme definition file for the current frontend instance. + When set, the FetchDescriptor functions will try the local definition before falling back to global theme data. + + - Set overlay=True to overlay on top of existing local definition data (if exists) + + WARNING: Pass the file content in str to this function; DO NOT pass the path to the file. + + This function returns True if successful, otherwise returns False. + """ + try: from . import _generator + except ImportError: import _generator + # Determine directory name + h=hashlib.shake_256(bytes(file_content, "utf-8")) + global alt_path_dirname + dir_name=f"clitheme-data-{h.hexdigest(6)}" # length of 12 (6*2) + if alt_path_dirname!=None and overlay==True: # overlay + dir_name=alt_path_dirname + path_name=_globalvar.clitheme_temp_root+"/"+dir_name + if global_debugmode: print("[Debug] "+path_name) + # Generate data hierarchy as needed + if not os.path.exists(path_name): + _generator.path=path_name + try: + _generator.generate_data_hierarchy(file_content, custom_path_gen=False) + except SyntaxError: + if global_debugmode: print("[Debug] Generator error: "+str(sys.exc_info()[1])) + return False + global alt_path + alt_path=path_name+"/"+_globalvar.generator_data_pathname + alt_path_dirname=dir_name + return True +def unset_local_themedef(): + """ + Unsets the local theme definition file for the current frontend instance. + After this operation, FetchDescriptor functions will no longer use local definitions. + """ + global alt_path; alt_path=None + global alt_path_dirname; alt_path_dirname=None + class FetchDescriptor(): """ Object containing domain and app information used for fetching entries @@ -36,24 +85,24 @@ class FetchDescriptor(): # Leave domain and app names blank for global reference if domain_name==None: - self.domain_name=global_domain + self.domain_name=global_domain.strip() else: self.domain_name=domain_name.strip() if app_name==None: - self.app_name=global_appname + self.app_name=global_appname.strip() else: self.app_name=app_name.strip() if subsections==None: - self.subsections=global_subsections + self.subsections=global_subsections.strip() else: self.subsections=subsections.strip() if lang==None: - self.lang=global_lang + self.lang=global_lang.strip() else: - self.lang=lang + self.lang=lang.strip() if debug_mode==None: self.debug_mode=global_debugmode @@ -77,43 +126,105 @@ class FetchDescriptor(): # Sanity check the path if _globalvar.sanity_check(entry_path)==False: - if self.debug_mode: print("Error: entry names/subsections {}".format(_globalvar.sanity_check_error_message)) + if self.debug_mode: print("[Debug] Error: entry names/subsections {}".format(_globalvar.sanity_check_error_message)) return fallback_string lang="" - lang_without_encoding="" + # Language handling: see https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Environment-Variables for more information if not self.disable_lang: if self.lang!="": - lang=self.lang - elif os.environ.__contains__("LANG"): - lang=os.environ["LANG"] - if lang.strip()!="": # not empty - for char in lang: - if char=='.': # if reaches e.g. ".UTF-8" section - break - lang_without_encoding+=char - if self.debug_mode: print("[Debug]", lang, lang_without_encoding, entry_path) + if self.debug_mode: print("[Debug] Locale: Using defined self.lang") + if not _globalvar.sanity_check(self.lang)==False: + lang=self.lang + else: + if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) + else: + if self.debug_mode: print("[Debug] Locale: Using environment variables") + # $LANGUAGE (list of languages separated by colons) + if os.environ.__contains__("LANGUAGE"): + target_str=os.environ['LANGUAGE'] + for each_language in target_str.strip().split(":"): + # avoid exploit of accessing top-level folders + if _globalvar.sanity_check(each_language)==False: continue + # Ignore en and en_US (See https://wiki.archlinux.org/title/Locale#LANGUAGE:_fallback_locales) + if each_language!="en" and each_language!="en_US": + # Treat C as en_US also + if re.sub(r"(?P.+)[\.].+", r"\g", each_language)=="C": + lang+=re.sub(r".+[\.]", "en_US.", each_language)+" " + lang+="en_US"+" " + lang+=each_language+" " + # no encoding + lang+=re.sub(r"(?P.+)[\.].+", r"\g", each_language)+" " + lang=lang.strip() + # $LC_ALL + elif os.environ.__contains__("LC_ALL"): + target_str=os.environ["LC_ALL"].strip() + if not _globalvar.sanity_check(target_str)==False: + lang=target_str+" " + lang+=re.sub(r"(?P.+)[\.].+", r"\g", target_str) + else: + if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) + # $LANG + elif os.environ.__contains__("LANG"): + target_str=os.environ["LANG"].strip() + if not _globalvar.sanity_check(target_str)==False: + lang=target_str+" " + lang+=re.sub(r"(?P.+)[\.].+", r"\g", target_str) + else: + if self.debug_mode: print("[Debug] Locale: sanity check failed ({})".format(_globalvar.sanity_check_error_message)) + + if self.debug_mode: print(f"[Debug] lang: {lang}\n[Debug] entry_path: {entry_path}") + # just being lazy here I don't want to check the variables before using ಥ_ಥ (because it doesn't matter) path=data_path+"/"+self.domain_name+"/"+self.app_name+"/"+self.subsections + path2=None + if alt_path!=None: path2=alt_path+"/"+self.domain_name+"/"+self.app_name+"/"+self.subsections for section in entry_path.split(): path+="/"+section - # path with lang, path with lang but without e.g. .UTF-8, path wth no lang - possible_paths=[path+"__"+lang, path+"__"+lang_without_encoding, path] + if path2!=None: path2+="/"+section + # path with lang, path with lang but without e.g. .UTF-8, path with no lang + possible_paths=[] + for l in lang.split(): + possible_paths.append(path+"__"+l) + possible_paths.append(path) + if path2!=None: + for l in lang.split(): + possible_paths.append(path2+"__"+l) + possible_paths.append(path2) for p in possible_paths: if self.debug_mode: print("Trying "+p, end="...") try: - f=open(p,'r') - dat=f.read().strip() + f=open(p,'r', encoding="utf-8") + dat=f.read() if self.debug_mode: print("Success:\n> "+dat) - return dat + # since the generator adds an extra newline in the entry data, we need to remove it + return re.sub(r"\n\Z", "", dat) except (FileNotFoundError, IsADirectoryError): if self.debug_mode: print("Failed") return fallback_string reof=retrieve_entry_or_fallback # a shorter alias of the function + def format_entry_or_fallback(self, entry_path: str, fallback_string: str, *args, **kwargs) -> str: + """ + Attempt to retrieve and format the entry based on given entry path and arguments. + If the entry does not exist or an error occurs while formatting the entry string, use the provided fallback string instead. + """ + # retrieve the entry + if not self.entry_exists(entry_path): + if self.debug_mode: print("[Debug] Entry not found") + return fallback_string.format(*args, **kwargs) + entry=self.retrieve_entry_or_fallback(entry_path, "") + # format the string + try: + return entry.format(*args, **kwargs) + except Exception: + if self.debug_mode: print("[Debug] Format error: {err}".format(err=str(sys.exc_info()[1]))) + return fallback_string.format(*args, **kwargs) + feof=format_entry_or_fallback # a shorter alias of the function + def entry_exists(self, entry_path: str) -> bool: """ Check if the entry at the given entry path exists. - Returns true if exists and false if does not exist + Returns true if exists and false if does not exist. """ # just being lazy here I don't want to rewrite this all over again ಥ_ಥ fallback_string="" @@ -121,4 +232,4 @@ class FetchDescriptor(): fallback_string+=random.choice(string.ascii_letters) recieved_content=self.retrieve_entry_or_fallback(entry_path, fallback_string) if recieved_content.strip()==fallback_string: return False - else: return True \ No newline at end of file + else: return True diff --git a/tests/clithemedef-test_expected.txt b/tests/clithemedef-test_expected.txt index 1b0d7ab962df24070da1b3e022654e132bbb5743..a23da4a3877d5da84aa0a0a29807d711567ccfd8 100644 --- a/tests/clithemedef-test_expected.txt +++ b/tests/clithemedef-test_expected.txt @@ -1,12 +1,3 @@ -../theme-info/1/clithemeinfo_locales -en_US zh_CN -../theme-info/1/clithemeinfo_version -1.0 -../theme-info/1/clithemeinfo_name -Example theme -../theme-info/1/clithemeinfo_supported_apps -example-app example-app-two another-example shound_unset-app - com.example/example-app/text-one Some example text one com.example/example-app/text-one__en_US