From 0fa2cdc9b3679642b0aa16019c1b2511d7fc5063 Mon Sep 17 00:00:00 2001 From: Xavier Heimgartner Date: Mon, 8 Sep 2025 16:12:06 +0200 Subject: [PATCH 1/4] Windows Explorer Integration: Added support for other languages & exporer tabs in Windows 11 + code refactoring --- apps/windows_explorer/windows_explorer.py | 348 ++++++++++++++++++---- 1 file changed, 287 insertions(+), 61 deletions(-) diff --git a/apps/windows_explorer/windows_explorer.py b/apps/windows_explorer/windows_explorer.py index 1ab0d2846e..0943f5c083 100644 --- a/apps/windows_explorer/windows_explorer.py +++ b/apps/windows_explorer/windows_explorer.py @@ -1,7 +1,51 @@ +""" +Windows Explorer Integration for Talon Voice Control + +This module provides voice control integration for Windows File Explorer by: + +1. LANGUAGE DETECTION & LOCALIZATION: + - Detects system UI language (English, German, etc.) + - Maps localized folder names to actual filesystem paths + - Handles different Explorer window title formats per language + +2. WINDOW TITLE PROCESSING: + - Strips Explorer-specific suffixes (e.g., "- File Explorer", multi-tab indicators) + - Uses regex patterns to clean window titles for path extraction + - Supports both single and multi-tab Explorer windows + +3. ONEDRIVE INTEGRATION: + - Detects OneDrive installation and folder structure + - Maps standard folders (Desktop, Documents, Pictures) to OneDrive paths when applicable + - Handles hybrid OneDrive/local folder scenarios + +4. PATH MAPPING & EXCLUSIONS: + - Maps localized display names to actual filesystem paths + - Excludes system dialogs and non-navigable windows + - Handles user display name mapping for personalized folders + +5. VOICE COMMAND ACTIONS: + - Navigation (open parent, go to directory) + - File operations (new folder, open file, properties) + - Address bar control (focus, copy, navigate) + - Terminal integration (open command prompt in current location) + +This has been tested with: + - Windows 11 24H2, Version 10.0.26100 Build 26100, English and German system language +""" + import os +import re +import logging from talon import Context, Module, actions, app, ui +# Configuration +DEBUG_LOGGING = False + +# Configure logging +if DEBUG_LOGGING: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s') + mod = Module() apps = mod.apps @@ -29,125 +73,307 @@ app: windows_file_browser """ +# Global variables user_path = os.path.expanduser("~") directories_to_remap = {} -directories_to_exclude = {} +directories_to_exclude = [] +title_suffix_patterns = [] + +def get_system_language(): + """Detect system UI language and return language code.""" + try: + ui_language_id = ctypes.windll.kernel32.GetUserDefaultUILanguage() + system_language = locale.windows_locale[ui_language_id] + language_code = system_language.split('_')[0] + if DEBUG_LOGGING: + logging.debug(f"Detected system language: {system_language}, code: {language_code}") + return language_code + except KeyError: + if DEBUG_LOGGING: + logging.debug("Language detection failed, defaulting to English") + return 'en' + +def get_user_display_name(): + """Get Windows user display name for directory mapping.""" + try: + GetUserNameEx = ctypes.windll.secur32.GetUserNameExW + NameDisplay = 3 + size = ctypes.pointer(ctypes.c_ulong(0)) + GetUserNameEx(NameDisplay, None, size) + nameBuffer = ctypes.create_unicode_buffer(size.contents.value) + GetUserNameEx(NameDisplay, nameBuffer, size) + return nameBuffer.value + except Exception: + return None if app.platform == "windows": - is_windows = True import ctypes + import locale + + language_code = get_system_language() + user_display_name = get_user_display_name() + # Standard Windows folder names (always in English) + STANDARD_FOLDERS = [ + 'Desktop', 'Documents', 'Downloads', 'Music', 'OneDrive', + 'Pictures', 'Videos', 'Links', 'Favorites', 'Contacts' + ] - GetUserNameEx = ctypes.windll.secur32.GetUserNameExW - NameDisplay = 3 - - size = ctypes.pointer(ctypes.c_ulong(0)) - GetUserNameEx(NameDisplay, None, size) - - nameBuffer = ctypes.create_unicode_buffer(size.contents.value) - GetUserNameEx(NameDisplay, nameBuffer, size) - one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) - - # this is probably not the correct way to check for onedrive, quick and dirty - if os.path.isdir(os.path.expanduser(os.path.join("~", r"OneDrive\Desktop"))): - directories_to_remap = { - "Desktop": os.path.join(one_drive_path, "Desktop"), - "Documents": os.path.join(one_drive_path, "Documents"), - "Downloads": os.path.join(user_path, "Downloads"), - "Music": os.path.join(user_path, "Music"), - "OneDrive": one_drive_path, - "Pictures": os.path.join(one_drive_path, "Pictures"), - "Videos": os.path.join(user_path, "Videos"), - } - else: - # todo use expanduser for cross platform support - directories_to_remap = { - "Desktop": os.path.join(user_path, "Desktop"), - "Documents": os.path.join(user_path, "Documents"), - "Downloads": os.path.join(user_path, "Downloads"), - "Music": os.path.join(user_path, "Music"), - "OneDrive": one_drive_path, - "Pictures": os.path.join(user_path, "Pictures"), - "Videos": os.path.join(user_path, "Videos"), + # Language-specific mappings with standardized structure + # TODO: Contributors - Add your language mappings here! + # Each language needs: suffix_patterns, folder_names, excludes + # See existing 'en' and 'de' entries as templates + # -> Set DEBUG_LOGGING = True + language_mappings = { + 'en': { + 'suffix_patterns': [ + r' and [0-9]+ more tabs - File Explorer$', + r' and 1 more tab - File Explorer$', + r' - File Explorer$' + ], + 'folder_names': { + 'Desktop': 'Desktop', + 'Documents': 'Documents', + 'Downloads': 'Downloads', + 'Music': 'Music', + 'OneDrive': 'OneDrive', + 'Pictures': 'Pictures', + 'Videos': 'Videos', + 'Links': 'Links', + 'Favorites': 'Favorites', + 'Contacts': 'Contacts', + }, + 'excludes': [ + 'Task Switching', + 'Task View', + 'This PC', + 'File Explorer', + 'Program Manager', + 'Run', + 'Gallery', + 'Home' + ] + }, + 'de': { + 'suffix_patterns': [ + r' und [0-9]+ weitere Registerkarten – Explorer$', + r' und 1 weitere Registerkarte – Explorer$', + r' – Datei-Explorer$' + ], + 'folder_names': { # Right side is the localized name + 'Desktop': 'Desktop', + 'Documents': 'Dokumente', + 'Downloads': 'Downloads', + 'Music': 'Musik', + 'OneDrive': 'OneDrive', + 'Pictures': 'Bilder', + 'Videos': 'Videos', + 'Links': 'Links', + 'Favorites': 'Favoriten', + 'Contacts': 'Kontakte', + }, + 'excludes': [ + 'Dieser PC', + 'Datei-Explorer', + 'Ausführen', + 'Katalog', + 'Start' + ] } + } - if nameBuffer.value: - directories_to_remap[nameBuffer.value] = user_path - - directories_to_exclude = [ - "", - "Run", - "Task Switching", - "Task View", - "This PC", - "File Explorer", - "Program Manager", - ] + def merge_language_mappings(base_mapping, language_code, language_mappings): + """Merge language-specific mappings with base English mapping.""" + if language_code == 'en' or language_code not in language_mappings: + return base_mapping + + lang_specific = language_mappings[language_code] + if DEBUG_LOGGING: + logging.debug(f"Loading patterns for '{language_code}': {lang_specific.get('suffix_patterns', [])}") + + # Merge each component safely + for key in ['suffix_patterns', 'excludes']: + if key in lang_specific and isinstance(lang_specific[key], list): + base_mapping[key].extend(lang_specific[key]) + + if 'folder_names' in lang_specific and isinstance(lang_specific['folder_names'], dict): + base_mapping['folder_names'].update(lang_specific['folder_names']) + + return base_mapping + + # Build current language mapping + current_language_mapping = merge_language_mappings( + language_mappings['en'].copy(), + language_code, + language_mappings + ) + + title_suffix_patterns = current_language_mapping['suffix_patterns'] + + def setup_onedrive_detection(): + """Detect OneDrive and return path resolver function.""" + one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) + has_onedrive_desktop = os.path.isdir(os.path.join(one_drive_path, "Desktop")) + + if DEBUG_LOGGING: + logging.debug(f"OneDrive: path={one_drive_path}, has_desktop={has_onedrive_desktop}") + + def get_folder_path(folder_key): + """Get actual filesystem path for folder (OneDrive-aware).""" + if has_onedrive_desktop and folder_key in ['Desktop', 'Documents', 'Pictures']: + return os.path.join(one_drive_path, folder_key) + elif folder_key == 'OneDrive': + return one_drive_path + else: + return os.path.join(user_path, folder_key) + + return get_folder_path + + get_folder_path = setup_onedrive_detection() + + def build_directory_mappings(folder_names, get_folder_path, user_display_name): + """Build directory remapping dictionary.""" + mappings = {} + + # Map standard folders + for folder_key in STANDARD_FOLDERS: + localized_name = folder_names.get(folder_key, folder_key) + actual_path = get_folder_path(folder_key) + mappings[localized_name] = actual_path + + if DEBUG_LOGGING: + logging.debug(f"Directory mapping: {localized_name} -> {actual_path}") + + # Add user display name mapping if available + if user_display_name: + mappings[user_display_name] = user_path + if DEBUG_LOGGING: + logging.debug(f"User mapping: {user_display_name} -> {user_path}") + + return mappings + + # Initialize directory mappings and exclusions + directories_to_remap = build_directory_mappings( + current_language_mapping['folder_names'], + get_folder_path, + user_display_name + ) + + directories_to_exclude = [""] + current_language_mapping['excludes'] + if DEBUG_LOGGING: + logging.debug(f"Directories to exclude: {directories_to_exclude}") + + +def _strip_explorer_suffixes(path): + """Remove Explorer-specific suffixes from window title.""" + for pattern in title_suffix_patterns: + if DEBUG_LOGGING: + logging.debug(f"Testing pattern '{pattern}' against '{path}'") + match = re.search(pattern, path) + if match: + path = path[:match.start()] + if DEBUG_LOGGING: + logging.debug(f"Stripped pattern '{pattern}': {path}") + break + elif DEBUG_LOGGING: + logging.debug(f"Pattern '{pattern}' did not match") + return path + + +def _apply_directory_mappings(path): + """Apply localized to actual path mappings.""" + if path in directories_to_remap: + mapped_path = directories_to_remap[path] + if DEBUG_LOGGING: + logging.debug(f"Directory remapped: {path} -> {mapped_path}") + return mapped_path + return path + + +def _handle_path_exclusions(path): + """Handle excluded paths by hiding pickers and returning empty string.""" + if path in directories_to_exclude: + if DEBUG_LOGGING: + logging.debug(f"Directory excluded: {path}") + actions.user.file_manager_hide_pickers() + return "" + return path @ctx.action_class("user") class UserActions: def file_manager_open_parent(): + """Navigate to parent directory.""" actions.key("alt-up") def file_manager_current_path(): + """Extract and process current file manager path from window title.""" path = ui.active_window().title + if DEBUG_LOGGING: + logging.debug(f"Original window title: {path}") - if path in directories_to_remap: - path = directories_to_remap[path] - - if path in directories_to_exclude: - actions.user.file_manager_hide_pickers() - path = "" + # Process path through pipeline + path = _strip_explorer_suffixes(path) + path = _apply_directory_mappings(path) + path = _handle_path_exclusions(path) + if DEBUG_LOGGING: + logging.debug(f"Final path: {path}") return path def file_manager_terminal_here(): + """Open command prompt in current directory.""" actions.key("ctrl-l") actions.insert("cmd.exe") actions.key("enter") def file_manager_show_properties(): - """Shows the properties for the file""" + """Show properties dialog for selected item.""" actions.key("alt-enter") + # Navigation actions def file_manager_open_directory(path: str): - """opens the directory that's already visible in the view""" + """Navigate to specified directory.""" actions.key("ctrl-l") actions.insert(path) actions.key("enter") def file_manager_select_directory(path: str): - """selects the directory""" + """Select directory by typing its path.""" actions.insert(path) + def file_manager_open_volume(volume: str): + """Open specified volume/drive.""" + actions.user.file_manager_open_directory(volume) + + # File operations def file_manager_new_folder(name: str): - """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + """Create new folder with specified name.""" actions.key("home") actions.key("ctrl-shift-n") actions.insert(name) def file_manager_open_file(path: str): - """opens the file""" + """Open file at specified path.""" actions.key("home") actions.insert(path) actions.key("enter") def file_manager_select_file(path: str): - """selects the file""" + """Select file by typing its path.""" actions.key("home") actions.insert(path) - def file_manager_open_volume(volume: str): - """file_manager_open_volume""" - actions.user.file_manager_open_directory(volume) - + # Address bar operations def address_focus(): + """Focus the address bar.""" actions.key("ctrl-l") def address_copy_address(): + """Copy current address to clipboard.""" actions.key("ctrl-l") actions.edit.copy() def address_navigate(address: str): + """Navigate to specified address.""" actions.user.file_manager_open_directory(address) From 91b5de031ea455bfa87cef70e2b13142fca3ea1c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:47:53 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- apps/windows_explorer/windows_explorer.py | 203 ++++++++++++---------- 1 file changed, 111 insertions(+), 92 deletions(-) diff --git a/apps/windows_explorer/windows_explorer.py b/apps/windows_explorer/windows_explorer.py index 0943f5c083..1857843a64 100644 --- a/apps/windows_explorer/windows_explorer.py +++ b/apps/windows_explorer/windows_explorer.py @@ -33,9 +33,9 @@ - Windows 11 24H2, Version 10.0.26100 Build 26100, English and German system language """ +import logging import os import re -import logging from talon import Context, Module, actions, app, ui @@ -44,7 +44,7 @@ # Configure logging if DEBUG_LOGGING: - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s') + logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") mod = Module() apps = mod.apps @@ -79,19 +79,23 @@ directories_to_exclude = [] title_suffix_patterns = [] + def get_system_language(): """Detect system UI language and return language code.""" try: ui_language_id = ctypes.windll.kernel32.GetUserDefaultUILanguage() system_language = locale.windows_locale[ui_language_id] - language_code = system_language.split('_')[0] + language_code = system_language.split("_")[0] if DEBUG_LOGGING: - logging.debug(f"Detected system language: {system_language}, code: {language_code}") + logging.debug( + f"Detected system language: {system_language}, code: {language_code}" + ) return language_code except KeyError: if DEBUG_LOGGING: logging.debug("Language detection failed, defaulting to English") - return 'en' + return "en" + def get_user_display_name(): """Get Windows user display name for directory mapping.""" @@ -106,16 +110,25 @@ def get_user_display_name(): except Exception: return None + if app.platform == "windows": import ctypes import locale - + language_code = get_system_language() user_display_name = get_user_display_name() # Standard Windows folder names (always in English) STANDARD_FOLDERS = [ - 'Desktop', 'Documents', 'Downloads', 'Music', 'OneDrive', - 'Pictures', 'Videos', 'Links', 'Favorites', 'Contacts' + "Desktop", + "Documents", + "Downloads", + "Music", + "OneDrive", + "Pictures", + "Videos", + "Links", + "Favorites", + "Contacts", ] # Language-specific mappings with standardized structure @@ -124,141 +137,147 @@ def get_user_display_name(): # See existing 'en' and 'de' entries as templates # -> Set DEBUG_LOGGING = True language_mappings = { - 'en': { - 'suffix_patterns': [ - r' and [0-9]+ more tabs - File Explorer$', - r' and 1 more tab - File Explorer$', - r' - File Explorer$' + "en": { + "suffix_patterns": [ + r" and [0-9]+ more tabs - File Explorer$", + r" and 1 more tab - File Explorer$", + r" - File Explorer$", ], - 'folder_names': { - 'Desktop': 'Desktop', - 'Documents': 'Documents', - 'Downloads': 'Downloads', - 'Music': 'Music', - 'OneDrive': 'OneDrive', - 'Pictures': 'Pictures', - 'Videos': 'Videos', - 'Links': 'Links', - 'Favorites': 'Favorites', - 'Contacts': 'Contacts', + "folder_names": { + "Desktop": "Desktop", + "Documents": "Documents", + "Downloads": "Downloads", + "Music": "Music", + "OneDrive": "OneDrive", + "Pictures": "Pictures", + "Videos": "Videos", + "Links": "Links", + "Favorites": "Favorites", + "Contacts": "Contacts", }, - 'excludes': [ - 'Task Switching', - 'Task View', - 'This PC', - 'File Explorer', - 'Program Manager', - 'Run', - 'Gallery', - 'Home' - ] + "excludes": [ + "Task Switching", + "Task View", + "This PC", + "File Explorer", + "Program Manager", + "Run", + "Gallery", + "Home", + ], }, - 'de': { - 'suffix_patterns': [ - r' und [0-9]+ weitere Registerkarten – Explorer$', - r' und 1 weitere Registerkarte – Explorer$', - r' – Datei-Explorer$' + "de": { + "suffix_patterns": [ + r" und [0-9]+ weitere Registerkarten – Explorer$", + r" und 1 weitere Registerkarte – Explorer$", + r" – Datei-Explorer$", ], - 'folder_names': { # Right side is the localized name - 'Desktop': 'Desktop', - 'Documents': 'Dokumente', - 'Downloads': 'Downloads', - 'Music': 'Musik', - 'OneDrive': 'OneDrive', - 'Pictures': 'Bilder', - 'Videos': 'Videos', - 'Links': 'Links', - 'Favorites': 'Favoriten', - 'Contacts': 'Kontakte', + "folder_names": { # Right side is the localized name + "Desktop": "Desktop", + "Documents": "Dokumente", + "Downloads": "Downloads", + "Music": "Musik", + "OneDrive": "OneDrive", + "Pictures": "Bilder", + "Videos": "Videos", + "Links": "Links", + "Favorites": "Favoriten", + "Contacts": "Kontakte", }, - 'excludes': [ - 'Dieser PC', - 'Datei-Explorer', - 'Ausführen', - 'Katalog', - 'Start' - ] - } + "excludes": [ + "Dieser PC", + "Datei-Explorer", + "Ausführen", + "Katalog", + "Start", + ], + }, } def merge_language_mappings(base_mapping, language_code, language_mappings): """Merge language-specific mappings with base English mapping.""" - if language_code == 'en' or language_code not in language_mappings: + if language_code == "en" or language_code not in language_mappings: return base_mapping - + lang_specific = language_mappings[language_code] if DEBUG_LOGGING: - logging.debug(f"Loading patterns for '{language_code}': {lang_specific.get('suffix_patterns', [])}") - + logging.debug( + f"Loading patterns for '{language_code}': {lang_specific.get('suffix_patterns', [])}" + ) + # Merge each component safely - for key in ['suffix_patterns', 'excludes']: + for key in ["suffix_patterns", "excludes"]: if key in lang_specific and isinstance(lang_specific[key], list): base_mapping[key].extend(lang_specific[key]) - - if 'folder_names' in lang_specific and isinstance(lang_specific['folder_names'], dict): - base_mapping['folder_names'].update(lang_specific['folder_names']) - + + if "folder_names" in lang_specific and isinstance( + lang_specific["folder_names"], dict + ): + base_mapping["folder_names"].update(lang_specific["folder_names"]) + return base_mapping - + # Build current language mapping current_language_mapping = merge_language_mappings( - language_mappings['en'].copy(), - language_code, - language_mappings + language_mappings["en"].copy(), language_code, language_mappings ) - title_suffix_patterns = current_language_mapping['suffix_patterns'] - + title_suffix_patterns = current_language_mapping["suffix_patterns"] + def setup_onedrive_detection(): """Detect OneDrive and return path resolver function.""" one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) has_onedrive_desktop = os.path.isdir(os.path.join(one_drive_path, "Desktop")) - + if DEBUG_LOGGING: - logging.debug(f"OneDrive: path={one_drive_path}, has_desktop={has_onedrive_desktop}") - + logging.debug( + f"OneDrive: path={one_drive_path}, has_desktop={has_onedrive_desktop}" + ) + def get_folder_path(folder_key): """Get actual filesystem path for folder (OneDrive-aware).""" - if has_onedrive_desktop and folder_key in ['Desktop', 'Documents', 'Pictures']: + if has_onedrive_desktop and folder_key in [ + "Desktop", + "Documents", + "Pictures", + ]: return os.path.join(one_drive_path, folder_key) - elif folder_key == 'OneDrive': + elif folder_key == "OneDrive": return one_drive_path else: return os.path.join(user_path, folder_key) - + return get_folder_path - + get_folder_path = setup_onedrive_detection() - + def build_directory_mappings(folder_names, get_folder_path, user_display_name): """Build directory remapping dictionary.""" mappings = {} - + # Map standard folders for folder_key in STANDARD_FOLDERS: localized_name = folder_names.get(folder_key, folder_key) actual_path = get_folder_path(folder_key) mappings[localized_name] = actual_path - + if DEBUG_LOGGING: logging.debug(f"Directory mapping: {localized_name} -> {actual_path}") - + # Add user display name mapping if available if user_display_name: mappings[user_display_name] = user_path if DEBUG_LOGGING: logging.debug(f"User mapping: {user_display_name} -> {user_path}") - + return mappings - + # Initialize directory mappings and exclusions directories_to_remap = build_directory_mappings( - current_language_mapping['folder_names'], - get_folder_path, - user_display_name + current_language_mapping["folder_names"], get_folder_path, user_display_name ) - - directories_to_exclude = [""] + current_language_mapping['excludes'] + + directories_to_exclude = [""] + current_language_mapping["excludes"] if DEBUG_LOGGING: logging.debug(f"Directories to exclude: {directories_to_exclude}") @@ -270,7 +289,7 @@ def _strip_explorer_suffixes(path): logging.debug(f"Testing pattern '{pattern}' against '{path}'") match = re.search(pattern, path) if match: - path = path[:match.start()] + path = path[: match.start()] if DEBUG_LOGGING: logging.debug(f"Stripped pattern '{pattern}': {path}") break From f1ccc7189726d479068dd2d322fb0001a3af77d6 Mon Sep 17 00:00:00 2001 From: Xavier Heimgartner Date: Mon, 15 Sep 2025 17:39:56 +0200 Subject: [PATCH 3/4] Windows Explorer Integration: Major code restructure with and Windows API integration Refactoring of previous Windows Explorer integration: **Improvements:** - Use Windows API for user folder display names instead of hardcoded values - Use Windows API for user folder paths. This removes the need for the previous OneDrive user folder detection - Simplified language mapping structure and removed merging logic - Reorganized code into logical sections **Code review adjustments:** - Use "logger = logging.getLogger(__name__)" for logging - Removed docstrings from UserActions methods --- apps/windows_explorer/windows_explorer.py | 543 ++++++++++++---------- 1 file changed, 310 insertions(+), 233 deletions(-) diff --git a/apps/windows_explorer/windows_explorer.py b/apps/windows_explorer/windows_explorer.py index 1857843a64..3e888c639f 100644 --- a/apps/windows_explorer/windows_explorer.py +++ b/apps/windows_explorer/windows_explorer.py @@ -6,17 +6,15 @@ 1. LANGUAGE DETECTION & LOCALIZATION: - Detects system UI language (English, German, etc.) - Maps localized folder names to actual filesystem paths - - Handles different Explorer window title formats per language + - Handles different Explorer window title formats per language (EN, DE) 2. WINDOW TITLE PROCESSING: - Strips Explorer-specific suffixes (e.g., "- File Explorer", multi-tab indicators) - - Uses regex patterns to clean window titles for path extraction - Supports both single and multi-tab Explorer windows -3. ONEDRIVE INTEGRATION: - - Detects OneDrive installation and folder structure - - Maps standard folders (Desktop, Documents, Pictures) to OneDrive paths when applicable - - Handles hybrid OneDrive/local folder scenarios +3. FOLDER PATH RESOLUTION: + - Maps standard folders (Desktop, Documents, Pictures, etc.) to actual paths + - Handles OneDrive redirection automatically 4. PATH MAPPING & EXCLUSIONS: - Maps localized display names to actual filesystem paths @@ -29,8 +27,13 @@ - Address bar control (focus, copy, navigate) - Terminal integration (open command prompt in current location) -This has been tested with: - - Windows 11 24H2, Version 10.0.26100 Build 26100, English and German system language +TESTED WITH: + - Windows 11 24H2, Version 10.0.26100 Build 26100 + - English and German system languages + +KNOWN LIMITATIONS: + - Nested localized standard folders (e.g., "Pictures/Screenshots") + - WSL root folder navigation (e.g., "Linux/Ubuntu") """ import logging @@ -39,13 +42,25 @@ from talon import Context, Module, actions, app, ui -# Configuration -DEBUG_LOGGING = False +# Windows-specific imports (conditional) +if app.platform == "windows": + import ctypes + import locale + try: + from win32com.shell import shell, shellcon + except ImportError: + shell = None + shellcon = None +else: + shell = None + shellcon = None # Configure logging -if DEBUG_LOGGING: - logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") +logger = logging.getLogger(__name__) +if logger.level == logging.NOTSET: + logger.setLevel(logging.ERROR) # Set to DEBUG if you want to implement a new language mapping +# App definition mod = Module() apps = mod.apps @@ -73,32 +88,95 @@ app: windows_file_browser """ -# Global variables -user_path = os.path.expanduser("~") -directories_to_remap = {} -directories_to_exclude = [] -title_suffix_patterns = [] +USER_PATH = os.path.expanduser("~") +STANDARD_FOLDERS = [ + "Desktop", "Documents", "Downloads", "Music", "OneDrive", + "Pictures", "Videos", "Links", "Favorites", "Contacts" +] + +KNOWN_FOLDER_GUIDS = { + 'Desktop': '{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}', + 'Documents': '{FDD39AD0-238F-46AF-ADB4-6C85480369C7}', + 'Downloads': '{374DE290-123F-4565-9164-39C4925E467B}', + 'Music': '{4BD8D571-6D19-48D3-BE97-422220080E43}', + 'Pictures': '{33E28130-4E1E-4676-835A-98395C3BC3BB}', + 'Videos': '{18989B1D-99B5-455B-841C-AB7C74E4DDFC}', + 'Links': '{bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968}', + 'Favorites': '{1777F761-68AD-4D8A-87BD-30B759FA33DD}', + 'Contacts': '{56784854-C6CB-462b-8169-88E350ACB882}' +} + +LANGUAGE_MAPPINGS = { + "en": { + "suffix_patterns": [ + r" and [0-9]+ more tabs - File Explorer$", + r" and 1 more tab - File Explorer$", + r" - File Explorer$", + ], + "excludes": ["", "This PC", "File Explorer", "Gallery", "Home", "Network"], + }, + "de": { + "suffix_patterns": [ + r" und [0-9]+ weitere Registerkarten – Explorer$", + r" und 1 weitere Registerkarte – Explorer$", + r" – Datei-Explorer$", + ], + "excludes": ["", "Dieser PC", "Datei-Explorer", "Katalog", "Start", "Netzwerk"], + }, +} +directories_to_remap = {} +# Utility functions +def _string_to_guid(guid_string): + """Convert GUID string to ctypes GUID structure. + + Args: + guid_string (str): GUID string in format '{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}' + + Returns: + GUID: ctypes GUID structure + """ + class GUID(ctypes.Structure): + _fields_ = [ + ('Data1', ctypes.c_ulong), + ('Data2', ctypes.c_ushort), + ('Data3', ctypes.c_ushort), + ('Data4', ctypes.c_ubyte * 8) + ] + + guid_string = guid_string.strip('{}').replace('-', '') + guid = GUID() + guid.Data1 = int(guid_string[0:8], 16) + guid.Data2 = int(guid_string[8:12], 16) + guid.Data3 = int(guid_string[12:16], 16) + for i in range(8): + guid.Data4[i] = int(guid_string[16 + i*2:18 + i*2], 16) + return guid + +# Windows-specific functions def get_system_language(): - """Detect system UI language and return language code.""" + """Detect system UI language and return language code. + + Returns: + str: Two-letter language code (e.g., 'en', 'de') or 'en' as fallback. + """ try: ui_language_id = ctypes.windll.kernel32.GetUserDefaultUILanguage() system_language = locale.windows_locale[ui_language_id] language_code = system_language.split("_")[0] - if DEBUG_LOGGING: - logging.debug( - f"Detected system language: {system_language}, code: {language_code}" - ) + logger.debug(f"windows_explorer.get_system_language: {system_language}, code: {language_code}") return language_code except KeyError: - if DEBUG_LOGGING: - logging.debug("Language detection failed, defaulting to English") + logger.error("windows_explorer.get_system_language: Language detection failed, defaulting to English") return "en" - def get_user_display_name(): - """Get Windows user display name for directory mapping.""" + """Get Windows user display name for directory mapping. + + Returns: + str or None: User's display name or None if retrieval fails. + """ try: GetUserNameEx = ctypes.windll.secur32.GetUserNameExW NameDisplay = 3 @@ -110,209 +188,225 @@ def get_user_display_name(): except Exception: return None - -if app.platform == "windows": - import ctypes - import locale - - language_code = get_system_language() - user_display_name = get_user_display_name() - # Standard Windows folder names (always in English) - STANDARD_FOLDERS = [ - "Desktop", - "Documents", - "Downloads", - "Music", - "OneDrive", - "Pictures", - "Videos", - "Links", - "Favorites", - "Contacts", - ] - - # Language-specific mappings with standardized structure - # TODO: Contributors - Add your language mappings here! - # Each language needs: suffix_patterns, folder_names, excludes - # See existing 'en' and 'de' entries as templates - # -> Set DEBUG_LOGGING = True - language_mappings = { - "en": { - "suffix_patterns": [ - r" and [0-9]+ more tabs - File Explorer$", - r" and 1 more tab - File Explorer$", - r" - File Explorer$", - ], - "folder_names": { - "Desktop": "Desktop", - "Documents": "Documents", - "Downloads": "Downloads", - "Music": "Music", - "OneDrive": "OneDrive", - "Pictures": "Pictures", - "Videos": "Videos", - "Links": "Links", - "Favorites": "Favorites", - "Contacts": "Contacts", - }, - "excludes": [ - "Task Switching", - "Task View", - "This PC", - "File Explorer", - "Program Manager", - "Run", - "Gallery", - "Home", - ], - }, - "de": { - "suffix_patterns": [ - r" und [0-9]+ weitere Registerkarten – Explorer$", - r" und 1 weitere Registerkarte – Explorer$", - r" – Datei-Explorer$", - ], - "folder_names": { # Right side is the localized name - "Desktop": "Desktop", - "Documents": "Dokumente", - "Downloads": "Downloads", - "Music": "Musik", - "OneDrive": "OneDrive", - "Pictures": "Bilder", - "Videos": "Videos", - "Links": "Links", - "Favorites": "Favoriten", - "Contacts": "Kontakte", - }, - "excludes": [ - "Dieser PC", - "Datei-Explorer", - "Ausführen", - "Katalog", - "Start", - ], - }, - } - - def merge_language_mappings(base_mapping, language_code, language_mappings): - """Merge language-specific mappings with base English mapping.""" - if language_code == "en" or language_code not in language_mappings: - return base_mapping - - lang_specific = language_mappings[language_code] - if DEBUG_LOGGING: - logging.debug( - f"Loading patterns for '{language_code}': {lang_specific.get('suffix_patterns', [])}" +def get_localized_folder_name(folder_id): + """Get localized display name for a known folder using Windows Shell API. + + Args: + folder_id (str): Folder identifier (e.g., 'Documents', 'Desktop') + + Returns: + str or None: Localized folder name or None if retrieval fails + """ + if folder_id not in KNOWN_FOLDER_GUIDS: + return None + + try: + guid_str = KNOWN_FOLDER_GUIDS[folder_id] + folder_guid = _string_to_guid(guid_str) + IID_IShellItem = _string_to_guid('{43826d1e-e718-42ee-bc55-a1e261c37bfe}') + + shell32 = ctypes.windll.shell32 + ole32 = ctypes.windll.ole32 + ole32.CoInitialize(None) + + try: + ppsi = ctypes.POINTER(ctypes.c_void_p)() + hr = shell32.SHGetKnownFolderItem( + ctypes.byref(folder_guid), 0, None, + ctypes.byref(IID_IShellItem), ctypes.byref(ppsi) ) - - # Merge each component safely - for key in ["suffix_patterns", "excludes"]: - if key in lang_specific and isinstance(lang_specific[key], list): - base_mapping[key].extend(lang_specific[key]) - - if "folder_names" in lang_specific and isinstance( - lang_specific["folder_names"], dict - ): - base_mapping["folder_names"].update(lang_specific["folder_names"]) - - return base_mapping - - # Build current language mapping - current_language_mapping = merge_language_mappings( - language_mappings["en"].copy(), language_code, language_mappings - ) - - title_suffix_patterns = current_language_mapping["suffix_patterns"] - - def setup_onedrive_detection(): - """Detect OneDrive and return path resolver function.""" - one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) - has_onedrive_desktop = os.path.isdir(os.path.join(one_drive_path, "Desktop")) - - if DEBUG_LOGGING: - logging.debug( - f"OneDrive: path={one_drive_path}, has_desktop={has_onedrive_desktop}" + + if hr != 0 or not ppsi: + return None + + display_name_ptr = ctypes.c_wchar_p() + vtable = ctypes.cast(ppsi.contents, ctypes.POINTER(ctypes.c_void_p)) + get_display_name_func = ctypes.cast( + vtable[5], + ctypes.WINFUNCTYPE( + ctypes.c_long, ctypes.c_void_p, ctypes.c_ulong, + ctypes.POINTER(ctypes.c_wchar_p) + ) ) - - def get_folder_path(folder_key): - """Get actual filesystem path for folder (OneDrive-aware).""" - if has_onedrive_desktop and folder_key in [ - "Desktop", - "Documents", - "Pictures", - ]: - return os.path.join(one_drive_path, folder_key) - elif folder_key == "OneDrive": - return one_drive_path - else: - return os.path.join(user_path, folder_key) - - return get_folder_path - - get_folder_path = setup_onedrive_detection() - - def build_directory_mappings(folder_names, get_folder_path, user_display_name): - """Build directory remapping dictionary.""" - mappings = {} - - # Map standard folders + + hr = get_display_name_func(ppsi, 0, ctypes.byref(display_name_ptr)) + + if hr == 0 and display_name_ptr: + display_name = display_name_ptr.value + ole32.CoTaskMemFree(display_name_ptr) + + release_func = ctypes.cast( + vtable[2], + ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p) + ) + release_func(ppsi) + return display_name + + if ppsi: + release_func = ctypes.cast( + vtable[2], + ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p) + ) + release_func(ppsi) + + finally: + ole32.CoUninitialize() + + except Exception as e: + logger.exception(f"windows_explorer.get_localized_folder_name: Exception getting localized name for {folder_id}: {e}") + + return None + +def get_folder_names(): + """Get localized folder names using Win32 Shell APIs with caching and fallback. + + Returns: + dict: Mapping of folder keys to localized names (or English fallback) + """ + global _localized_folder_cache + if _localized_folder_cache: + return _localized_folder_cache + + try: + folder_names = {} for folder_key in STANDARD_FOLDERS: - localized_name = folder_names.get(folder_key, folder_key) - actual_path = get_folder_path(folder_key) - mappings[localized_name] = actual_path - - if DEBUG_LOGGING: - logging.debug(f"Directory mapping: {localized_name} -> {actual_path}") - - # Add user display name mapping if available - if user_display_name: - mappings[user_display_name] = user_path - if DEBUG_LOGGING: - logging.debug(f"User mapping: {user_display_name} -> {user_path}") - - return mappings - - # Initialize directory mappings and exclusions - directories_to_remap = build_directory_mappings( - current_language_mapping["folder_names"], get_folder_path, user_display_name - ) - - directories_to_exclude = [""] + current_language_mapping["excludes"] - if DEBUG_LOGGING: - logging.debug(f"Directories to exclude: {directories_to_exclude}") + if folder_key == "OneDrive": + folder_names[folder_key] = "OneDrive" + continue + + try: + localized_name = get_localized_folder_name(folder_key) + folder_names[folder_key] = localized_name or folder_key + # logger.debug(f"windows_explorer.get_folder_names: {folder_key} -> {folder_names[folder_key]}") + except Exception as e: + folder_names[folder_key] = folder_key + logger.exception(f"windows_explorer.get_folder_names: API error for {folder_key}: {e}") + + _localized_folder_cache = folder_names + return folder_names + except Exception as e: + logger.exception(f"windows_explorer.get_folder_names: API approach failed, using English fallback: {e}") + return {folder: folder for folder in STANDARD_FOLDERS} + +def get_folder_path(folder_key): + """Get actual filesystem path for folder using Win32 API. + + Automatically handles OneDrive redirection and other folder redirections. + + Args: + folder_key (str): Folder identifier (e.g., 'Documents', 'Desktop') + + Returns: + str: Full filesystem path to the folder + """ + if not shell: + logger.error("windows_explorer.get_folder_path: pywin32 is not installed.") + return os.path.join(USER_PATH, folder_key) + + if not hasattr(get_folder_path, '_folder_id_map'): + get_folder_path._folder_id_map = { + "Desktop": shellcon.FOLDERID_Desktop, + "Documents": shellcon.FOLDERID_Documents, + "Downloads": shellcon.FOLDERID_Downloads, + "Music": shellcon.FOLDERID_Music, + "OneDrive": shellcon.FOLDERID_OneDrive, + "Pictures": shellcon.FOLDERID_Pictures, + "Videos": shellcon.FOLDERID_Videos, + "Links": shellcon.FOLDERID_Links, + "Favorites": shellcon.FOLDERID_Favorites, + "Contacts": shellcon.FOLDERID_Contacts, + } + + folder_id = get_folder_path._folder_id_map.get(folder_key) + if folder_id: + try: + return shell.SHGetKnownFolderPath(folder_id, 0) + except Exception as e: + logger.exception(f"windows_explorer.get_folder_path: SHGetKnownFolderPath failed for {folder_key}: {e}") + + return os.path.join(USER_PATH, folder_key) + +# Directory mapping functions +def initialize_directory_mappings(): + """Ensure directory mappings are loaded (lazy initialization).""" + global directories_to_remap + if not directories_to_remap: + try: + folder_names = get_folder_names() + mappings = {} + + for folder_key in STANDARD_FOLDERS: + localized_name = folder_names.get(folder_key, folder_key) + actual_path = get_folder_path(folder_key) + mappings[localized_name] = actual_path + logger.debug(f"windows_explorer.initialize_directory_mappings: {localized_name} -> {actual_path}") + + if user_display_name: + mappings[user_display_name] = USER_PATH + logger.debug(f"windows_explorer.initialize_directory_mappings: User mapping {user_display_name} -> {USER_PATH}") + + directories_to_remap = mappings + except Exception as e: + logger.exception(f"windows_explorer.initialize_directory_mappings: Failed to load directory mappings: {e}") + directories_to_remap = {} + + +# Windows platform initialization +if app.platform == "windows": + language_code = get_system_language() + user_display_name = get_user_display_name() + _localized_folder_cache = {} + directories_to_remap = {} + current_language_mapping = LANGUAGE_MAPPINGS.get(language_code, LANGUAGE_MAPPINGS["en"]) + logger.debug(f"windows_explorer: Windows platform init using language mapping for '{language_code}'") +# Path processing functions def _strip_explorer_suffixes(path): - """Remove Explorer-specific suffixes from window title.""" - for pattern in title_suffix_patterns: - if DEBUG_LOGGING: - logging.debug(f"Testing pattern '{pattern}' against '{path}'") - match = re.search(pattern, path) - if match: - path = path[: match.start()] - if DEBUG_LOGGING: - logging.debug(f"Stripped pattern '{pattern}': {path}") - break - elif DEBUG_LOGGING: - logging.debug(f"Pattern '{pattern}' did not match") + """Remove Explorer-specific suffixes from window title. + + Args: + path (str): Window title path + + Returns: + str: Path with Explorer suffixes removed + """ + if 'current_language_mapping' in globals(): + for pattern in current_language_mapping["suffix_patterns"]: + match = re.search(pattern, path) + if match: + path = path[:match.start()] + logger.debug(f"windows_explorer._strip_explorer_suffixes: Stripped suffix from '{path}'") + break return path - def _apply_directory_mappings(path): - """Apply localized to actual path mappings.""" + """Apply localized to actual path mappings. + + Args: + path (str): Localized path from window title + + Returns: + str: Actual filesystem path or original path if no mapping exists + """ + initialize_directory_mappings() if path in directories_to_remap: mapped_path = directories_to_remap[path] - if DEBUG_LOGGING: - logging.debug(f"Directory remapped: {path} -> {mapped_path}") + logger.debug(f"windows_explorer._apply_directory_mappings: {path} -> {mapped_path}") return mapped_path return path - def _handle_path_exclusions(path): - """Handle excluded paths by hiding pickers and returning empty string.""" - if path in directories_to_exclude: - if DEBUG_LOGGING: - logging.debug(f"Directory excluded: {path}") + """Handle excluded paths by hiding pickers and returning empty string. + + Args: + path (str): Path to check for exclusion + + Returns: + str: Empty string if excluded, original path otherwise + """ + if 'current_language_mapping' in globals() and path in current_language_mapping["excludes"]: + logger.debug(f"windows_explorer._handle_path_exclusions: Directory excluded '{path}'") actions.user.file_manager_hide_pickers() return "" return path @@ -322,77 +416,60 @@ def _handle_path_exclusions(path): class UserActions: def file_manager_open_parent(): - """Navigate to parent directory.""" actions.key("alt-up") def file_manager_current_path(): - """Extract and process current file manager path from window title.""" path = ui.active_window().title - if DEBUG_LOGGING: - logging.debug(f"Original window title: {path}") + logger.debug(f"windows_explorer.UserActions.file_manager_current_path: Window title '{path}'") - # Process path through pipeline path = _strip_explorer_suffixes(path) path = _apply_directory_mappings(path) path = _handle_path_exclusions(path) - - if DEBUG_LOGGING: - logging.debug(f"Final path: {path}") + logger.debug(f"windows_explorer.UserActions.file_manager_current_path: Final path '{path}'") return path def file_manager_terminal_here(): - """Open command prompt in current directory.""" actions.key("ctrl-l") actions.insert("cmd.exe") actions.key("enter") def file_manager_show_properties(): - """Show properties dialog for selected item.""" actions.key("alt-enter") # Navigation actions def file_manager_open_directory(path: str): - """Navigate to specified directory.""" actions.key("ctrl-l") actions.insert(path) actions.key("enter") def file_manager_select_directory(path: str): - """Select directory by typing its path.""" actions.insert(path) def file_manager_open_volume(volume: str): - """Open specified volume/drive.""" actions.user.file_manager_open_directory(volume) # File operations def file_manager_new_folder(name: str): - """Create new folder with specified name.""" actions.key("home") actions.key("ctrl-shift-n") actions.insert(name) def file_manager_open_file(path: str): - """Open file at specified path.""" actions.key("home") actions.insert(path) actions.key("enter") def file_manager_select_file(path: str): - """Select file by typing its path.""" actions.key("home") actions.insert(path) # Address bar operations def address_focus(): - """Focus the address bar.""" actions.key("ctrl-l") def address_copy_address(): - """Copy current address to clipboard.""" actions.key("ctrl-l") actions.edit.copy() def address_navigate(address: str): - """Navigate to specified address.""" actions.user.file_manager_open_directory(address) From ad833028135d181cc4233191d384ecedda509d4d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:39:31 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- apps/windows_explorer/windows_explorer.py | 229 ++++++++++++++-------- 1 file changed, 144 insertions(+), 85 deletions(-) diff --git a/apps/windows_explorer/windows_explorer.py b/apps/windows_explorer/windows_explorer.py index 3e888c639f..311ab36fa8 100644 --- a/apps/windows_explorer/windows_explorer.py +++ b/apps/windows_explorer/windows_explorer.py @@ -46,6 +46,7 @@ if app.platform == "windows": import ctypes import locale + try: from win32com.shell import shell, shellcon except ImportError: @@ -58,7 +59,9 @@ # Configure logging logger = logging.getLogger(__name__) if logger.level == logging.NOTSET: - logger.setLevel(logging.ERROR) # Set to DEBUG if you want to implement a new language mapping + logger.setLevel( + logging.ERROR + ) # Set to DEBUG if you want to implement a new language mapping # App definition mod = Module() @@ -90,20 +93,28 @@ USER_PATH = os.path.expanduser("~") STANDARD_FOLDERS = [ - "Desktop", "Documents", "Downloads", "Music", "OneDrive", - "Pictures", "Videos", "Links", "Favorites", "Contacts" + "Desktop", + "Documents", + "Downloads", + "Music", + "OneDrive", + "Pictures", + "Videos", + "Links", + "Favorites", + "Contacts", ] KNOWN_FOLDER_GUIDS = { - 'Desktop': '{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}', - 'Documents': '{FDD39AD0-238F-46AF-ADB4-6C85480369C7}', - 'Downloads': '{374DE290-123F-4565-9164-39C4925E467B}', - 'Music': '{4BD8D571-6D19-48D3-BE97-422220080E43}', - 'Pictures': '{33E28130-4E1E-4676-835A-98395C3BC3BB}', - 'Videos': '{18989B1D-99B5-455B-841C-AB7C74E4DDFC}', - 'Links': '{bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968}', - 'Favorites': '{1777F761-68AD-4D8A-87BD-30B759FA33DD}', - 'Contacts': '{56784854-C6CB-462b-8169-88E350ACB882}' + "Desktop": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}", + "Documents": "{FDD39AD0-238F-46AF-ADB4-6C85480369C7}", + "Downloads": "{374DE290-123F-4565-9164-39C4925E467B}", + "Music": "{4BD8D571-6D19-48D3-BE97-422220080E43}", + "Pictures": "{33E28130-4E1E-4676-835A-98395C3BC3BB}", + "Videos": "{18989B1D-99B5-455B-841C-AB7C74E4DDFC}", + "Links": "{bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968}", + "Favorites": "{1777F761-68AD-4D8A-87BD-30B759FA33DD}", + "Contacts": "{56784854-C6CB-462b-8169-88E350ACB882}", } LANGUAGE_MAPPINGS = { @@ -127,37 +138,40 @@ directories_to_remap = {} + # Utility functions def _string_to_guid(guid_string): """Convert GUID string to ctypes GUID structure. - + Args: guid_string (str): GUID string in format '{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}' - + Returns: GUID: ctypes GUID structure """ + class GUID(ctypes.Structure): _fields_ = [ - ('Data1', ctypes.c_ulong), - ('Data2', ctypes.c_ushort), - ('Data3', ctypes.c_ushort), - ('Data4', ctypes.c_ubyte * 8) + ("Data1", ctypes.c_ulong), + ("Data2", ctypes.c_ushort), + ("Data3", ctypes.c_ushort), + ("Data4", ctypes.c_ubyte * 8), ] - - guid_string = guid_string.strip('{}').replace('-', '') + + guid_string = guid_string.strip("{}").replace("-", "") guid = GUID() guid.Data1 = int(guid_string[0:8], 16) guid.Data2 = int(guid_string[8:12], 16) guid.Data3 = int(guid_string[12:16], 16) for i in range(8): - guid.Data4[i] = int(guid_string[16 + i*2:18 + i*2], 16) + guid.Data4[i] = int(guid_string[16 + i * 2 : 18 + i * 2], 16) return guid + # Windows-specific functions def get_system_language(): """Detect system UI language and return language code. - + Returns: str: Two-letter language code (e.g., 'en', 'de') or 'en' as fallback. """ @@ -165,15 +179,20 @@ def get_system_language(): ui_language_id = ctypes.windll.kernel32.GetUserDefaultUILanguage() system_language = locale.windows_locale[ui_language_id] language_code = system_language.split("_")[0] - logger.debug(f"windows_explorer.get_system_language: {system_language}, code: {language_code}") + logger.debug( + f"windows_explorer.get_system_language: {system_language}, code: {language_code}" + ) return language_code except KeyError: - logger.error("windows_explorer.get_system_language: Language detection failed, defaulting to English") + logger.error( + "windows_explorer.get_system_language: Language detection failed, defaulting to English" + ) return "en" + def get_user_display_name(): """Get Windows user display name for directory mapping. - + Returns: str or None: User's display name or None if retrieval fails. """ @@ -188,122 +207,134 @@ def get_user_display_name(): except Exception: return None + def get_localized_folder_name(folder_id): """Get localized display name for a known folder using Windows Shell API. - + Args: folder_id (str): Folder identifier (e.g., 'Documents', 'Desktop') - + Returns: str or None: Localized folder name or None if retrieval fails """ if folder_id not in KNOWN_FOLDER_GUIDS: return None - + try: guid_str = KNOWN_FOLDER_GUIDS[folder_id] folder_guid = _string_to_guid(guid_str) - IID_IShellItem = _string_to_guid('{43826d1e-e718-42ee-bc55-a1e261c37bfe}') - + IID_IShellItem = _string_to_guid("{43826d1e-e718-42ee-bc55-a1e261c37bfe}") + shell32 = ctypes.windll.shell32 ole32 = ctypes.windll.ole32 ole32.CoInitialize(None) - + try: ppsi = ctypes.POINTER(ctypes.c_void_p)() hr = shell32.SHGetKnownFolderItem( - ctypes.byref(folder_guid), 0, None, - ctypes.byref(IID_IShellItem), ctypes.byref(ppsi) + ctypes.byref(folder_guid), + 0, + None, + ctypes.byref(IID_IShellItem), + ctypes.byref(ppsi), ) - + if hr != 0 or not ppsi: return None - + display_name_ptr = ctypes.c_wchar_p() vtable = ctypes.cast(ppsi.contents, ctypes.POINTER(ctypes.c_void_p)) get_display_name_func = ctypes.cast( - vtable[5], + vtable[5], ctypes.WINFUNCTYPE( - ctypes.c_long, ctypes.c_void_p, ctypes.c_ulong, - ctypes.POINTER(ctypes.c_wchar_p) - ) + ctypes.c_long, + ctypes.c_void_p, + ctypes.c_ulong, + ctypes.POINTER(ctypes.c_wchar_p), + ), ) - + hr = get_display_name_func(ppsi, 0, ctypes.byref(display_name_ptr)) - + if hr == 0 and display_name_ptr: display_name = display_name_ptr.value ole32.CoTaskMemFree(display_name_ptr) - + release_func = ctypes.cast( - vtable[2], - ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p) + vtable[2], ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p) ) release_func(ppsi) return display_name - + if ppsi: release_func = ctypes.cast( - vtable[2], - ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p) + vtable[2], ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p) ) release_func(ppsi) - + finally: ole32.CoUninitialize() - + except Exception as e: - logger.exception(f"windows_explorer.get_localized_folder_name: Exception getting localized name for {folder_id}: {e}") - + logger.exception( + f"windows_explorer.get_localized_folder_name: Exception getting localized name for {folder_id}: {e}" + ) + return None + def get_folder_names(): """Get localized folder names using Win32 Shell APIs with caching and fallback. - + Returns: dict: Mapping of folder keys to localized names (or English fallback) """ global _localized_folder_cache if _localized_folder_cache: return _localized_folder_cache - + try: folder_names = {} for folder_key in STANDARD_FOLDERS: if folder_key == "OneDrive": folder_names[folder_key] = "OneDrive" continue - + try: localized_name = get_localized_folder_name(folder_key) folder_names[folder_key] = localized_name or folder_key # logger.debug(f"windows_explorer.get_folder_names: {folder_key} -> {folder_names[folder_key]}") except Exception as e: folder_names[folder_key] = folder_key - logger.exception(f"windows_explorer.get_folder_names: API error for {folder_key}: {e}") - + logger.exception( + f"windows_explorer.get_folder_names: API error for {folder_key}: {e}" + ) + _localized_folder_cache = folder_names return folder_names except Exception as e: - logger.exception(f"windows_explorer.get_folder_names: API approach failed, using English fallback: {e}") + logger.exception( + f"windows_explorer.get_folder_names: API approach failed, using English fallback: {e}" + ) return {folder: folder for folder in STANDARD_FOLDERS} + def get_folder_path(folder_key): """Get actual filesystem path for folder using Win32 API. - + Automatically handles OneDrive redirection and other folder redirections. - + Args: folder_key (str): Folder identifier (e.g., 'Documents', 'Desktop') - + Returns: str: Full filesystem path to the folder """ if not shell: logger.error("windows_explorer.get_folder_path: pywin32 is not installed.") return os.path.join(USER_PATH, folder_key) - - if not hasattr(get_folder_path, '_folder_id_map'): + + if not hasattr(get_folder_path, "_folder_id_map"): get_folder_path._folder_id_map = { "Desktop": shellcon.FOLDERID_Desktop, "Documents": shellcon.FOLDERID_Documents, @@ -316,16 +347,19 @@ def get_folder_path(folder_key): "Favorites": shellcon.FOLDERID_Favorites, "Contacts": shellcon.FOLDERID_Contacts, } - + folder_id = get_folder_path._folder_id_map.get(folder_key) if folder_id: try: return shell.SHGetKnownFolderPath(folder_id, 0) except Exception as e: - logger.exception(f"windows_explorer.get_folder_path: SHGetKnownFolderPath failed for {folder_key}: {e}") - + logger.exception( + f"windows_explorer.get_folder_path: SHGetKnownFolderPath failed for {folder_key}: {e}" + ) + return os.path.join(USER_PATH, folder_key) + # Directory mapping functions def initialize_directory_mappings(): """Ensure directory mappings are loaded (lazy initialization).""" @@ -334,20 +368,26 @@ def initialize_directory_mappings(): try: folder_names = get_folder_names() mappings = {} - + for folder_key in STANDARD_FOLDERS: localized_name = folder_names.get(folder_key, folder_key) actual_path = get_folder_path(folder_key) mappings[localized_name] = actual_path - logger.debug(f"windows_explorer.initialize_directory_mappings: {localized_name} -> {actual_path}") + logger.debug( + f"windows_explorer.initialize_directory_mappings: {localized_name} -> {actual_path}" + ) if user_display_name: mappings[user_display_name] = USER_PATH - logger.debug(f"windows_explorer.initialize_directory_mappings: User mapping {user_display_name} -> {USER_PATH}") + logger.debug( + f"windows_explorer.initialize_directory_mappings: User mapping {user_display_name} -> {USER_PATH}" + ) directories_to_remap = mappings except Exception as e: - logger.exception(f"windows_explorer.initialize_directory_mappings: Failed to load directory mappings: {e}") + logger.exception( + f"windows_explorer.initialize_directory_mappings: Failed to load directory mappings: {e}" + ) directories_to_remap = {} @@ -357,56 +397,71 @@ def initialize_directory_mappings(): user_display_name = get_user_display_name() _localized_folder_cache = {} directories_to_remap = {} - current_language_mapping = LANGUAGE_MAPPINGS.get(language_code, LANGUAGE_MAPPINGS["en"]) - logger.debug(f"windows_explorer: Windows platform init using language mapping for '{language_code}'") + current_language_mapping = LANGUAGE_MAPPINGS.get( + language_code, LANGUAGE_MAPPINGS["en"] + ) + logger.debug( + f"windows_explorer: Windows platform init using language mapping for '{language_code}'" + ) # Path processing functions def _strip_explorer_suffixes(path): """Remove Explorer-specific suffixes from window title. - + Args: path (str): Window title path - + Returns: str: Path with Explorer suffixes removed """ - if 'current_language_mapping' in globals(): + if "current_language_mapping" in globals(): for pattern in current_language_mapping["suffix_patterns"]: match = re.search(pattern, path) if match: - path = path[:match.start()] - logger.debug(f"windows_explorer._strip_explorer_suffixes: Stripped suffix from '{path}'") + path = path[: match.start()] + logger.debug( + f"windows_explorer._strip_explorer_suffixes: Stripped suffix from '{path}'" + ) break return path + def _apply_directory_mappings(path): """Apply localized to actual path mappings. - + Args: path (str): Localized path from window title - + Returns: str: Actual filesystem path or original path if no mapping exists """ initialize_directory_mappings() if path in directories_to_remap: mapped_path = directories_to_remap[path] - logger.debug(f"windows_explorer._apply_directory_mappings: {path} -> {mapped_path}") + logger.debug( + f"windows_explorer._apply_directory_mappings: {path} -> {mapped_path}" + ) return mapped_path return path + def _handle_path_exclusions(path): """Handle excluded paths by hiding pickers and returning empty string. - + Args: path (str): Path to check for exclusion - + Returns: str: Empty string if excluded, original path otherwise """ - if 'current_language_mapping' in globals() and path in current_language_mapping["excludes"]: - logger.debug(f"windows_explorer._handle_path_exclusions: Directory excluded '{path}'") + if ( + "current_language_mapping" in globals() + and path in current_language_mapping["excludes"] + ): + logger.debug( + f"windows_explorer._handle_path_exclusions: Directory excluded '{path}'" + ) actions.user.file_manager_hide_pickers() return "" return path @@ -420,12 +475,16 @@ def file_manager_open_parent(): def file_manager_current_path(): path = ui.active_window().title - logger.debug(f"windows_explorer.UserActions.file_manager_current_path: Window title '{path}'") + logger.debug( + f"windows_explorer.UserActions.file_manager_current_path: Window title '{path}'" + ) path = _strip_explorer_suffixes(path) path = _apply_directory_mappings(path) path = _handle_path_exclusions(path) - logger.debug(f"windows_explorer.UserActions.file_manager_current_path: Final path '{path}'") + logger.debug( + f"windows_explorer.UserActions.file_manager_current_path: Final path '{path}'" + ) return path def file_manager_terminal_here():