diff --git a/README.md b/README.md index 7c6f6c3..c53525d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Download from the store ## Supported programs - [x] visual studio code +- [x] visual studio - [x] pycharm - [x] clion - [x] goland @@ -24,17 +25,18 @@ You can access different programs using the following acronyms: ```json { - "vsc": "VSCODE", - "py": "PYCHARM", - "cl": "CLION", - "go": "GOLAND", - "idea": "INTELLIJ_IDEA", - "as": "ANDROID_STUDIO", - "cur":"CURSOR", - "pdf":"SUMATRA_PDF", - "trae":"TRAE", - "vscs":"VSCODE_SSH", - "ty": "TYPORA", + "VSCODE": "vsc", + "VISUAL_STUDIO": "vs", + "PYCHARM": "py", + "CLION": "cl", + "GOLAND": "go", + "INTELLIJ_IDEA": "idea", + "ANDROID_STUDIO": "as", + "CURSOR": "cur", + "SUMATRA": "pdf", + "TRAE": "trae", + "VSCODE_SSH": "vscs", + "TYPORA": "ty" } ``` @@ -46,7 +48,7 @@ All configurations can be done in the Settings Panel. Change the configurations - Configure program paths in the following format: - ```plaintext + ```ini APP_DOWNLOAD=C:/path/to/app.exe APP_STORAGE=C:/path/to/storage/file ``` @@ -54,19 +56,28 @@ All configurations can be done in the Settings Panel. Change the configurations - Each program needs both DOWNLOAD and STORAGE entries - Example configuration: - ```plaintext + ```ini VSCODE_DOWNLOAD=D:/VSCode/bin/code VSCODE_STORAGE=C:/Users/YourUsername/AppData/Roaming/Code/User/globalStorage/storage.json + + VISUAL_STUDIO_DOWNLOAD=D:/VisualStudio/Common7/IDE/devenv.exe + VISUAL_STUDIO_STORAGE=C:/Users/YourUsername/AppData/Local/Microsoft/VisualStudio/17.0_856acfe5/ApplicationPrivateSettings.xml + ANDROID_STUDIO_DOWNLOAD=D:/Android Studio/bin/studio64.exe ANDROID_STUDIO_STORAGE=C:/Users/YourUsername/AppData/Roaming/Google/AndroidStudio2024.1/options/recentProjects.xml + INTELLIJ_IDEA_DOWNLOAD=D:/IntelliJ IDEA 2024.3/bin/idea64.exe INTELLIJ_IDEA_STORAGE=C:/Users/YourUsername/AppData/Roaming/JetBrains/IntelliJIdea2024.3/options/recentProjects.xml + GOLAND_DOWNLOAD=D:/goland/GoLand 2023.2/bin/goland64.exe GOLAND_STORAGE=C:/Users/YourUsername/AppData/Roaming/JetBrains/GoLand2023.2/options/recentProjects.xml + CLION_DOWNLOAD=D:/Clion/CLion 2024.1.4/bin/clion64.exe CLION_STORAGE=C:/Users/YourUsername/AppData/Roaming/JetBrains/CLion2024.1/options/recentProjects.xml + CURSOR_DOWNLOAD=C:/Users/YourUsername/AppData/Local/Programs/cursor/Cursor.exe CURSOR_STORAGE=C:/Users/YourUsername/AppData/Roaming/Cursor/globalStorage/storage.json + TYPORA_DOWNLOAD=D:/Typora/Typora.exe TYPORA_STORAGE=C:/Users/YourUsername/AppData/Roaming/Typora/history.data ``` @@ -76,7 +87,7 @@ All configurations can be done in the Settings Panel. Change the configurations - You can customize the program suggestions list displayed when no input is provided. - Configure program suggestions list in the following format: - ```plaintext + ```ini VSCODE PYCHARM INTELLIJ_IDEA @@ -94,7 +105,7 @@ All configurations can be done in the Settings Panel. Change the configurations - Configure custom acronyms for your programs in the following format: - ```plaintext + ```ini VSCODE=vs PYCHARM=pc # ... other acronyms map diff --git a/icons/VISUAL_STUDIO.png b/icons/VISUAL_STUDIO.png new file mode 100644 index 0000000..fc67cd6 Binary files /dev/null and b/icons/VISUAL_STUDIO.png differ diff --git a/src/application/typora.py b/src/application/typora.py index f8cc7e1..cd11cec 100644 --- a/src/application/typora.py +++ b/src/application/typora.py @@ -54,6 +54,8 @@ def get_projects(self) -> List[Project]: all_items.sort(key=lambda x: x["timestamp"], reverse=True) for item in all_items: - projects.append(Project(self.name, item["path"])) + path = str(item["path"]).replace("\\", "/") + project = Project(self.name, path) + projects.append(project) return projects diff --git a/src/application/visual_studio.py b/src/application/visual_studio.py new file mode 100644 index 0000000..9057fcc --- /dev/null +++ b/src/application/visual_studio.py @@ -0,0 +1,67 @@ +import os +import json +import xml.etree.ElementTree as ET +from typing import List +from ..core.logger import get_logger +from ..core.project import Project +from ..core.registry import ApplicationRegistry +from .base_application import BaseApplication + +logger = get_logger() + + +@ApplicationRegistry.register("VISUAL_STUDIO") +class VisualStudio(BaseApplication): + def __init__(self, download_path: str, storage_file: str): + super().__init__(download_path, storage_file) + self.name = "VISUAL_STUDIO" + self.acronyms = "vs" + + def get_projects(self) -> List[Project]: + """获取Visual Studio最近打开项目路径""" + storage_file = self.storage_file + if storage_file is None or not os.path.exists(storage_file): + raise FileNotFoundError("storage_file not found") + try: + tree = ET.parse(storage_file) + root = tree.getroot() + collection = next( + ( + col + for col in root.findall(".//collection") + if col.attrib.get("name") == "CodeContainers.Offline" + ), + None, + ) + if collection is None: + return [] + value_node = next( + ( + val + for val in collection.findall("value") + if val.attrib.get("name") == "value" + ), + None, + ) + value_text = None + if value_node is not None: + value_text = value_node.text or ( + value_node.find("data").text + if (value_node.find("data") is not None) + else None + ) + if not value_text: + return [] + projects_data = json.loads(value_text) + except Exception: + # logger.error(f"解析 Visual Studio 项目 JSON 失败: {e}") + return [] + projects = [] + for proj in projects_data: + path = proj.get("Value", {}).get("LocalProperties", {}).get("FullPath", "") + if not path: + continue + path = path.replace("\\", "/") + project = Project(self.name, path) + projects.append(project) + return projects diff --git a/src/core/main.py b/src/core/main.py index dd89e81..936eb3e 100644 --- a/src/core/main.py +++ b/src/core/main.py @@ -1,3 +1,4 @@ +import re import subprocess import webbrowser from typing import Dict, List @@ -71,64 +72,62 @@ def query(self, param: str) -> List[Dict[str, str]]: def context_menu(self, data: str) -> list: """ - 使用任务管理器打开文件夹 - 复制文件夹路径 + 使用任务管理器打开文件夹或选中文件 + 复制文件夹或文件路径 """ - if data is None: - return MessageDTO.asDebugFlowMessage("data is None") - import re - - # 正则表达式 - pattern = r"^[A-Za-z]:/[\w\-/]+$" - - if len(data) > 1 and re.match(pattern, data[1]): - return [ - { - "title": "Copy path", - "subTitle": "Press enter to copy the path", - "icoPath": "icons/app.png", # related path to the image - "jsonRPCAction": { - "method": "copy_to_clipboard", - "parameters": [data[1]], - }, - "score": 0, - }, - { - "title": "Open in explorer", - "subTitle": "Press enter to open the explorer", - "icoPath": "icons/app.png", # related path to the image - "jsonRPCAction": { - "method": "cmd_command", - "parameters": ["start", data[1]], - }, - "score": 0, + + folder_pattern = r"^[A-Za-z]:/(?:[^/]+/)*[^/]+$" + file_pattern = r"^[A-Za-z]:/(?:[^/]+/)*[^/]+\.[^/]+$" + + def copy_path_item(path, type): + return { + "title": f"Copy {type} path", + "subTitle": f"Press enter to copy the {type} path", + "icoPath": "icons/app.png", + "jsonRPCAction": { + "method": "copy_to_clipboard", + "parameters": [path], }, - { - "title": "RecentProjectsOpen's Context menu", - "subTitle": "Press enter to open Flow the plugin's repo in GitHub", - "icoPath": "icons/app.png", - "jsonRPCAction": { - "method": "open_url", - "parameters": [ - "https://github.com/Attack825/RecentProjectsOpen" - ], + "score": 0, + } + + if data is not None and len(data) > 1: + # normalize the path to use forward slashes + path = data[1].replace("\\", "/") + # file path + if re.match(file_pattern, path): + path = path.replace("/", "\\") + logger.debug(f"/e,/select,{path}") + return [ + copy_path_item(path, "file"), + { + "title": "Show in explorer", + "subTitle": "Press enter to show and select the file in explorer", + "icoPath": "icons/app.png", + "jsonRPCAction": { + "method": "show_in_explorer", + "parameters": [path], + }, + "score": 0, }, - }, - ] - else: - return MessageDTO.asWarnFlowMessage( - { - "title": "RecentProjectsOpen's Context menu", - "subTitle": "Press enter to open Flow the plugin's repo in GitHub", - "icoPath": "icons/app.png", - "jsonRPCAction": { - "method": "open_url", - "parameters": [ - "https://github.com/Attack825/RecentProjectsOpen" - ], + ] + # folder path + elif re.match(folder_pattern, path): + return [ + copy_path_item(path, "folder"), + { + "title": "Open in explorer", + "subTitle": "Press enter to open the folder in explorer", + "icoPath": "icons/app.png", + "jsonRPCAction": { + "method": "cmd_command", + "parameters": ["start", path], + }, + "score": 0, }, - }, - ) + ] + + return None def open_url(self, url): webbrowser.open(url) @@ -144,6 +143,14 @@ def cmd_command(self, *args): shell=True, ) + def show_in_explorer(self, path: str): + _ = subprocess.Popen( + f"explorer /e,/select,{path}", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + ) + def copy_to_clipboard(self, text: str): _ = subprocess.Popen( ["echo", text, "|", "clip"],