diff --git a/README.md b/README.md index a602da3..7c6f6c3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A plugin that enables quick access to recent projects across various IDEs like V Download from the store -## Supported applications +## Supported programs - [x] visual studio code - [x] pycharm @@ -20,9 +20,9 @@ Download from the store - [x] vscode ssh - [x] typora -You can access different IDEs using the following format: +You can access different programs using the following acronyms: -```python +```json { "vsc": "VSCODE", "py": "PYCHARM", @@ -44,14 +44,14 @@ All configurations can be done in the Settings Panel. Change the configurations ### Program Path -- Configure application paths in the following format: +- Configure program paths in the following format: ```plaintext APP_DOWNLOAD=C:/path/to/app.exe APP_STORAGE=C:/path/to/storage/file ``` -- Each application needs both DOWNLOAD and STORAGE entries +- Each program needs both DOWNLOAD and STORAGE entries - Example configuration: ```plaintext @@ -71,6 +71,37 @@ All configurations can be done in the Settings Panel. Change the configurations TYPORA_STORAGE=C:/Users/YourUsername/AppData/Roaming/Typora/history.data ``` +### Suggestions List + +- You can customize the program suggestions list displayed when no input is provided. +- Configure program suggestions list in the following format: + + ```plaintext + VSCODE + PYCHARM + INTELLIJ_IDEA + # ... other programs + ``` + +- Keep empty to display the default program suggestions list — the plugin will suggest the programs that are configured in the [Program Path](#program-path). + +### Plugin Trigger Keyword + +- Make sure to update this configuration when you change the trigger keyword of this plugin. +- It affects the usage of [Suggestions List](#suggestions-list). + +### Custom Acronyms Map + +- Configure custom acronyms for your programs in the following format: + + ```plaintext + VSCODE=vs + PYCHARM=pc + # ... other acronyms map + ``` + +- This allows you to use custom acronyms when the [default acronyms](#supported-programs) do not match your preferences. + ### Tips - **Verify Paths**: Ensure that the paths you enter are correct and that the files or executables exist at those locations @@ -81,23 +112,23 @@ All configurations can be done in the Settings Panel. Change the configurations To open a project named "MyProject" in Visual Studio Code, you would use: -r +`r` ![1752128883045](image/README/1752128883045.png) -r vsc +`r vsc` ![1733284352742](image/README/1733284352742.png) -r vsc My +`r vsc My` ![1733284374591](image/README/1733284374591.png) -r vsc 空 +`r vsc 空` ![1733284760505](image/README/1733284760505.png) -context menu +`context menu` ![1742873448581](image/README/1742873448581.png) diff --git a/SettingsTemplate.yaml b/SettingsTemplate.yaml index a2840d4..0535d0f 100644 --- a/SettingsTemplate.yaml +++ b/SettingsTemplate.yaml @@ -13,3 +13,44 @@ body: Each application needs both DOWNLOAD and STORAGE entries. defaultValue: "" + - type: textarea + attributes: + name: suggestions_list + label: Suggestions List + description: > + Configure list of program suggestions in the following format: + + VSCODE + + PYCHARM + + INTELLIJ_IDEA + + It will be displayed when the plugin is activated. + + Keep empty to display the default program suggestions. + - type: input + - type: input + attributes: + name: plugin_trigger_keyword + label: Plugin Trigger Keyword + description: > + The keyword to trigger the plugin. + + If you change it, please update here as well. + defaultValue: "r" + - type: textarea + attributes: + name: custom_acronyms_map + label: Custom Acronyms Map + description: > + Configure custom acronyms for your programs in the following format: + + VSCODE=vsc + + PYCHARM=py + + Each line defines a mapping from the program name to its acronyms. + defaultValue: "" + + diff --git a/icons/as_icon.png b/icons/ANDROID_STUDIO.png similarity index 100% rename from icons/as_icon.png rename to icons/ANDROID_STUDIO.png diff --git a/icons/cl_icon.png b/icons/CLION.png similarity index 100% rename from icons/cl_icon.png rename to icons/CLION.png diff --git a/icons/cur_icon.png b/icons/CURSOR.png similarity index 100% rename from icons/cur_icon.png rename to icons/CURSOR.png diff --git a/icons/go_icon.png b/icons/GOLAND.png similarity index 100% rename from icons/go_icon.png rename to icons/GOLAND.png diff --git a/icons/idea_icon.png b/icons/INTELLIJ_IDEA.png similarity index 100% rename from icons/idea_icon.png rename to icons/INTELLIJ_IDEA.png diff --git a/icons/py_icon.png b/icons/PYCHARM.png similarity index 100% rename from icons/py_icon.png rename to icons/PYCHARM.png diff --git a/icons/pdf_icon.png b/icons/SUMATRA_PDF.png similarity index 100% rename from icons/pdf_icon.png rename to icons/SUMATRA_PDF.png diff --git a/icons/TYPORA.png b/icons/TYPORA.png new file mode 100644 index 0000000..dec6916 Binary files /dev/null and b/icons/TYPORA.png differ diff --git a/icons/vsc_icon.png b/icons/VSCODE.png similarity index 100% rename from icons/vsc_icon.png rename to icons/VSCODE.png diff --git a/icons/vscs_icon.png b/icons/VSCODE_SSH.png similarity index 100% rename from icons/vscs_icon.png rename to icons/VSCODE_SSH.png diff --git a/icons/ty_icon.png b/icons/ty_icon.png deleted file mode 100644 index 99ee5ca..0000000 Binary files a/icons/ty_icon.png and /dev/null differ diff --git a/src/core/config.py b/src/core/config.py index dbb08bd..223bf63 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -1,6 +1,7 @@ -from click import UsageError - from .jsonrpc import settings +from .logger import get_logger + +logger = get_logger() class Config(dict): @@ -10,10 +11,16 @@ def __init__(self): def _parse_settings(self) -> None: """解析从 Flow Launcher 获取的配置""" - flow_settings = settings() # 直接从 jsonrpc 获取 + flow_settings = settings() if not flow_settings: return - program_path = flow_settings.get("program_path", "") + + # 解析 program_path 配置 + program_path = ( + flow_settings.get("program_path") + if "program_path" in flow_settings + else None + ) if program_path: # 解析多行配置,格式如: KEY=VALUE\nKEY2=VALUE2 for line in program_path.split("\n"): @@ -22,12 +29,45 @@ def _parse_settings(self) -> None: key, value = line.split("=", 1) self[key.strip()] = value.strip() - def get(self, key: str) -> str: - """获取配置值,如果不存在则抛出异常""" - value = super().get(key) - if not value: - raise UsageError(f"Missing config key: {key}") - return value + # 解析 suggestions_list 配置(每行一个程序名称) + suggestions_list = ( + flow_settings.get("suggestions_list") + if "suggestions_list" in flow_settings + else None + ) + if suggestions_list: + self["suggestions_list"] = [ + s.strip().upper() for s in suggestions_list.split("\n") if s.strip() + ] + + # 解析 acronyms_map 配置 + custom_acronyms_map = ( + flow_settings.get("custom_acronyms_map") + if "custom_acronyms_map" in flow_settings + else None + ) + if custom_acronyms_map: + self["custom_acronyms_map"] = {} + for line in custom_acronyms_map.split("\n"): + line = line.strip() + if line and "=" in line: + program, acronyms = line.split("=", 1) + program = program.strip().upper() + acronyms = acronyms.strip() + self["custom_acronyms_map"][program] = acronyms + + # 解析其他配置 + for key, value in flow_settings.items(): + if key not in self and key not in ( + "suggestions_list", + "program_path", + "custom_acronyms_map", + ): + self[key] = value + + def get(self, key: str, default=None) -> str: + """获取配置值,支持默认值参数""" + return super().get(key, default) config = Config() diff --git a/src/core/factory.py b/src/core/factory.py index 27b708b..fa6cdad 100644 --- a/src/core/factory.py +++ b/src/core/factory.py @@ -1,7 +1,10 @@ from abc import ABC, abstractmethod -from typing import Dict, List +from typing import Dict from .registry import ApplicationRegistry +from .logger import get_logger + +logger = get_logger() class AbstractFactory(ABC): @@ -32,50 +35,64 @@ def get_supported_applications(cls): return list(ApplicationRegistry.get_all_applications().keys()) @classmethod - def get_application_acronyms(cls) -> Dict[str, str]: + def get_application_acronyms(cls, plugin_config) -> Dict[str, str]: """ - 获取应用的缩写字典 + 获取应用的缩写字典,优先使用 plugin_config 中用户自定义的 custom_acronyms_map。 返回格式: { "vsc": "VSCODE", - "idea": "IDEA", - "pycharm": "PYCHARM" + "cs": "CURSOR", + ... } """ ApplicationRegistry.load_applications() - return ApplicationRegistry.get_acronyms_map() + default_acronyms_map = ApplicationRegistry.get_acronyms_map() + custom_acronyms_map = plugin_config.get("custom_acronyms_map", {}) + acronyms_map = {acr: prog.upper() for prog, acr in custom_acronyms_map.items()} + for acr, prog in default_acronyms_map.items(): + if prog not in acronyms_map.values(): + acronyms_map[acr] = prog + return acronyms_map @classmethod - def get_application_message(cls) -> List[Dict[str, str]]: - """获得所有applications的消息列表 - Returns: - [ - { - "title": "VSCODE", - "subTitle": "vsc", - "icoPath": "icons/app.png", - "jsonRPCAction": { - "method": "Flow.Launcher.ChangeQuery", - "parameters": ["r vsc", False], - "dontHideAfterAction": True, - }, - "score": 0, - }, - ] + def get_application_message(cls, plugin_config): + """ + 根据配置的 suggestions_list 生成 Flow Launcher 消息列表。 + 如果未配置,则只展示用户已配置路径的应用。 """ - application_dict = ConcreteFactory.get_application_acronyms() - keys = list(application_dict.keys()) - return [ - { - "title": application_dict[keys[i]], - "subTitle": keys[i], - "icoPath": f"icons/{keys[i]}_icon.png", - "jsonRPCAction": { - "method": "Flow.Launcher.ChangeQuery", - "parameters": [f"r {keys[i]}", False], - "dontHideAfterAction": True, - }, - "score": 0, - } - for i in range(len(keys)) - ] + suggestions_list = plugin_config.get("suggestions_list", None) + plugin_trigger_keyword = plugin_config.get("plugin_trigger_keyword", "r") + acronyms_dict = cls.get_application_acronyms(plugin_config) + reverse_acronyms_dict = {v: k for k, v in acronyms_dict.items()} + + # 只显示已配置的应用建议 + if suggestions_list is None: + suggestions_list = [] + for _, program_name in acronyms_dict.items(): + download_key = program_name + "_DOWNLOAD" + storage_key = program_name + "_STORAGE" + if download_key in plugin_config and storage_key in plugin_config: + suggestions_list.append(program_name) + + messages = [] + for idx, program_name in enumerate(suggestions_list): + if program_name in reverse_acronyms_dict: + acronyms = reverse_acronyms_dict[program_name] + messages.append( + { + "title": program_name, + "subTitle": acronyms, + "icoPath": f"icons/{program_name}.png", + "jsonRPCAction": { + "method": "Flow.Launcher.ChangeQuery", + "parameters": [ + f"{plugin_trigger_keyword} {acronyms} ", + False, + ], + "dontHideAfterAction": True, + }, + # 按原始顺序排序 + "score": (100 - idx) * 10000, + } + ) + return messages diff --git a/src/core/main.py b/src/core/main.py index 9097a75..dd89e81 100644 --- a/src/core/main.py +++ b/src/core/main.py @@ -21,12 +21,13 @@ def __init__(self): def query(self, param: str) -> List[Dict[str, str]]: args = param.strip() - acronyms_dict = ConcreteFactory.get_application_acronyms() - # 遍历显示 + # 如果没有输入参数,则根据 suggestions_list 展示建议列表 if len(args) == 0: - return ConcreteFactory.get_application_message() + return ConcreteFactory.get_application_message(config) + acronyms = args.split(" ")[0] + acronyms_dict = ConcreteFactory.get_application_acronyms(config) if acronyms not in acronyms_dict.keys(): return MessageDTO.asWarnFlowMessage( @@ -35,8 +36,7 @@ def query(self, param: str) -> List[Dict[str, str]]: ) else: app_name = acronyms_dict[acronyms] - logger.debug(f"app_name: {app_name}") - icon_path = "icons/{}_icon.png".format(acronyms) + icon_path = f"icons/{app_name}.png" query = "".join(args.split(" ")[1:]) # 读取配置 @@ -48,7 +48,7 @@ def query(self, param: str) -> List[Dict[str, str]]: "{0} app_download or app_storage is None".format(app_name) + str(e), "Please check your settings", ) - logger.debug(f"app_download: {app_download}, app_storage: {app_storage}") + # 读取recent_projects try: app = ConcreteFactory.create_app(app_name, app_download, app_storage) diff --git a/test/test_config.py b/test/test_config.py index 4162b53..ce05740 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -41,13 +41,23 @@ def test_settings_parsing(self, mock_settings): @patch("src.core.config.settings") def test_empty_settings(self, mock_settings): - """测试空配置的情况""" + """测试空配置""" mock_settings.return_value = {} config = Config() - with self.assertRaises(Exception): - config.get("NONEXISTENT_KEY") + self.assertIsNone(config.get("NONEXISTENT_KEY")) + + @patch("src.core.config.settings") + def test_settings_default_value(self, mock_settings): + """测试配置默认值""" + mock_settings.return_value = {} + + config = Config() + + self.assertEqual( + config.get("NONEXISTENT_KEY", "default_value"), "default_value" + ) if __name__ == "__main__":