diff --git a/OpenWith/configs/OpenWithEng.lng b/OpenWith/configs/OpenWithEng.lng index 5bb80d1f7..97cf91e48 100644 --- a/OpenWith/configs/OpenWithEng.lng +++ b/OpenWith/configs/OpenWithEng.lng @@ -2,12 +2,14 @@ "OpenWith" "OpenWith: Choose application" +"OpenWith for " +" file(s)" "&OK" "&Cancel" "OpenWith Error" "Selected item must be a file accessible locally via a real path." -"Failed to save configuration." +"Failed to save modified settings; the changes will apply to the current session only." "No applications found. Detected MIME type:" "The application cannot be executed." "OpenWith plugin is not available on this platform." @@ -18,6 +20,7 @@ "Don't &wait for command completion" "C&lear selection if command executed" "Co&nfirm opening if file count exceeds:" +"Displa&y filename in the menu title" "Use `&xdg-mime` tool" "Use `&file` tool" @@ -38,9 +41,9 @@ "OpenWith: Details" -"&Filepath:" -"files selected: " -"&MIME profile:" +"F&iles selected:" +"&Filepaths:" +"&MIME profiles:" "L&aunch command:" "&Close" "&Launch" diff --git a/OpenWith/configs/OpenWithRus.lng b/OpenWith/configs/OpenWithRus.lng index edaab367e..c8d0144a3 100644 --- a/OpenWith/configs/OpenWithRus.lng +++ b/OpenWith/configs/OpenWithRus.lng @@ -2,12 +2,14 @@ "OpenWith" "OpenWith: Выберите приложение" +"OpenWith для " +" файл(ов)" "&ОК" "О&тмена" "Ошибка OpenWith" "Выбранный объект должен быть файлом, доступным локально по реальному пути." -"Ошибка записи конфигурации." +"Не удалось сохранить измененные настройки; они будут действовать только в текущем сеансе." "Приложения не найдены. Распознанный MIME-тип:" "Невозможно выполнить приложение." "Плагин OpenWith недоступен на данной платформе." @@ -18,6 +20,7 @@ "Не ждать &завершения команды" "&Снимать выделение, если выбрана команда" "По&дтверждать открытие, если файлов больше чем:" +"Отображать им&я открываемого файла в заголовке меню" "Использовать утилиту `&xdg-mime`" "Использовать утилиту `&file`" @@ -38,12 +41,12 @@ "OpenWith: Подробности" -"Адрес &файла:" -"выбрано файлов: " -"&MIME-профиль:" +"В&ыбрано файлов:" +"Адреса &файлов:" +"&MIME-профили:" "&Команда запуска:" "&Закрыть" -"З&апустить" +"За&пустить" "&Desktop файл:" "&Источник:" diff --git a/OpenWith/configs/help_en.hlf b/OpenWith/configs/help_en.hlf index 989149b33..28633a285 100644 --- a/OpenWith/configs/help_en.hlf +++ b/OpenWith/configs/help_en.hlf @@ -8,12 +8,14 @@ $^#Copyright (C) 2025-2026 Ivan # $^#Contents# The #OpenWith# plugin provides a context menu to open the selected file(s) with a suitable application. It intelligently detects the file type and queries the operating system for a list of registered applications that can handle it. If multiple files are selected, the plugin finds common applications that are registered to handle all of them by intersecting the candidate lists for each file. The plugin uses native OS mechanisms: on Linux/BSD, it adheres to XDG standards for MIME types and application discovery, while on macOS, it integrates with the Launch Services framework. - After invoking the plugin on a file, a menu is displayed with a list of applications. The user can then select one to launch, or press #F3# to view detailed information about the file and the selected application. The #F9# key opens the configuration dialog to fine-tune the plugin's behavior. Modifying options that affect application discovery, filtering, or ranking logic will cause the list of applications in the menu to update immediately. + After invoking the plugin on a file, a menu is displayed with a list of applications. The user can then select one to launch (#Enter# or #Shift+Enter#), or press #F3# to view detailed information about the file and the selected application. The #F9# key opens the configuration dialog to fine-tune the plugin's behavior. Modifying options that affect application discovery, filtering, or ranking logic will cause the list of applications in the menu to update immediately. ~Details Dialog (F3)~@DetailsDialog@ ~Configuration Dialog (F9)~@ConfigurationDialog@ + ~Forced launch mode (Shift+Enter)~@ShiftEnter@ + ~Tips & tricks~@Tips@ ~Troubleshooting~@Troubleshooting@ @@ -23,20 +25,23 @@ $^#OpenWith plugin# $^#Version 1.2# $^#Copyright (C) 2025-2026 Ivan # $^#Details Dialog (F3)# - The information dialog, accessible by pressing #F3# in the application selection menu, displays detailed technical information about the file, the highlighted application, and the exact command that will be used to launch it. + Displays comprehensive technical information about the selected file(s), the highlighted application, and the exact command that will be used to launch it. Accessible by pressing #F3# in the application selection menu. #1. COMMON FIELDS# - #-# Filepath - The full (absolute) path to the selected file. If multiple files are selected, this field will instead show their count. + #-# Files selected + The number of files the plugin is currently processing. Not shown when only one file is selected. + + #-# Filepaths + The full (absolute) paths to the selected files, formatted as a semicolon-separated list. - #-# MIME profile + #-# MIME profiles The file type(s) detected by the plugin. For multiple files, all unique detected profiles are shown. Format depends on the OS: - #Linux/BSD#: "Raw" results from all detection methods ("xdg-mime", "file", etc) enabled in the ~configuration dialog~@ConfigurationDialog@. If tools disagree, they are listed semicolon-separated: "(text/plain;application/x-shellscript)". + #Linux/BSD#: "Raw" results from all detection methods (xdg-mime, file, etc) enabled in the ~configuration dialog~@ConfigurationDialog@. If tools disagree on the same file, the results are combined into a semicolon‑separated list: "(text/plain;application/x-shellscript)". #macOS#: The MIME type corresponding to the file's system UTI. #-# Launch command - The final command line that will be executed if you press #Enter# or #Shift+Enter#. + The final command line that will be executed if you press #Enter# or ~Shift+Enter~@ShiftEnter@. #2. PLATFORM-SPECIFIC FIELDS# @@ -114,18 +119,18 @@ $^#OpenWith plugin# $^#Version 1.2# $^#Copyright (C) 2025-2026 Ivan # $^#Configuration Dialog (F9)# - The configuration dialog, accessible by pressing #F9# in the application selection menu or via ~Plugins Configuration~@:PluginsConfig@ in the ~Options Menu~@:OptMenu@, allows you to customize the plugin's behavior. It is divided into general and platform-specific settings. + Allows you to customize the plugin's behavior. Accessible via ~Options~@:OptMenu@ → ~Plugins Configuration~@:PluginsConfig@ (or #Alt+Shift+F9#), and by pressing #F9# in the application selection menu. #1. GENERAL SETTINGS# #-# Use external terminal for console apps If checked, applications that require a terminal (e.g., vim or mc) will be launched in a new ~external terminal~@:ExternalTerminal@ window. If unchecked, they will run inside far2l's ~built-in terminal~@:Terminal@. When using the built-in terminal with multiple files selected, the plugin applies a "smart" filter. It hides terminal applications that require a separate process for each file, while those that can handle all files in a single command remain in the list. - You can force a console application to launch in an external terminal regardless of this setting by pressing #Shift+Enter#. + You can force a console application to launch in an external terminal regardless of this setting by pressing ~Shift+Enter~@ShiftEnter@. #-# Don't wait for command completion If checked, GUI applications will be launched asynchronously, immediately returning control to far2l. If unchecked, far2l will wait for the launched application to close, allowing you to see its output (stdout/stderr) in the console. This setting does not affect terminal applications. It is also ignored if the application requires a separate launch for each selected file; in this case, asynchronous mode is forced to prevent interface blocking. - You can force a GUI application to launch asynchronously regardless of this setting by pressing #Shift+Enter#. + You can force a GUI application to launch asynchronously regardless of this setting by pressing ~Shift+Enter~@ShiftEnter@. #-# Clear selection if command executed If checked, launching an application from the menu resets the file selection on the panel. @@ -133,6 +138,9 @@ $^#Configuration Dialog (F9)# #-# Confirm opening if file count exceeds... To prevent resource-intensive launches, the plugin prompts a warning when the number of selected files exceeds the specified threshold. If you decline, you will be returned to the application selection menu or to the ~Details dialog~@DetailsDialog@. + #-# Display filename in the menu title + If checked, the plugin modifies the menu title to include the name of the processed file (or the total count, if multiple files are selected). If unchecked, a static "Choose application" title is used. + #2. PLATFORM-SPECIFIC SETTINGS# @@ -141,10 +149,10 @@ $^#Configuration Dialog (F9)# These settings fine-tune how the plugin identifies file types and discovers applications. The plugin gathers a list of potential MIME types for a file from all enabled sources. This list is then expanded and prioritized, and applications are ranked based on how well they match the most specific MIME types. This provides more flexible and accurate associations than relying on a single detection method. #-# Use `xdg-mime` tool - Determine the MIME type of the selected file by invoking the 'xdg-mime query filetype' command. This is the standard and most recommended method. The utility queries the system's shared-mime-info database, so its results are highly compatible with the "MimeType" entries in .desktop files. Although "xdg-mime" can additionally inspect file contents, it primarily relies on file names. This makes it susceptible to errors for files with incorrect extensions. + Determine the MIME type of the selected file by invoking the 'xdg-mime query filetype' command. This is the standard and most recommended method. The utility queries the system's shared-mime-info database, so its results are highly compatible with the "MimeType" entries in .desktop files. Although "xdg-mime" can additionally inspect file content, it primarily relies on file names. This makes it susceptible to errors for files with incorrect extensions. #-# Use `file` tool - Determine the MIME type of the selected file by invoking the 'file --mime-type' command. This utility performs deep analysis of the file’s binary content using its own "magic numbers" database. It is extremely reliable and cannot be fooled by incorrect file extensions. However, it can be slightly slower as it requires reading the file's contents. In addition, the MIME type names it returns sometimes differ from those in the shared-mime-info database, which can lead to fewer application matches. + Determine the MIME type of the selected file by invoking the 'file --mime-type' command. This utility performs a deep analysis of the binary data using its own "magic numbers" database. It is extremely reliable and cannot be fooled by incorrect file extensions. However, it can be slightly slower as it requires reading the file content. In addition, the MIME type names it returns sometimes differ from those in the shared-mime-info database, which can lead to fewer application matches. #-# Use `magika` tool Determine the MIME type of the selected file by invoking the 'magika --format %m' command. Instead of traditional magic numbers, it utilizes a deep learning model (ONNX) trained on a vast dataset to analyze file content. This enables accurate detection for over 200 formats, including modern programming languages, DevOps configuration files, and ML/AI data types. @@ -154,7 +162,7 @@ $^#Configuration Dialog (F9)# Determine the MIME type of the selected file by matching the filename against the patterns defined in the system's "globs2" database. This internal method is significantly faster than the "xdg-mime" tool as it avoids launching external processes. However, unlike "xdg-mime", it relies on the filename only and lacks "magic sniffing" (content analysis) to resolve conflicts between multiple applicable rules. Enabling this option simultaneously with "Use `xdg-mime` tool" option is redundant. It is best used as a high-performance alternative to "xdg-mime" or as a superior replacement for the "extension-based fallback", as it fully respects XDG pattern weights and case sensitivity. #-# Use extension-based fallback - As a last resort, if the above tools fail, are absent or disabled, the plugin attempts to guess the MIME type from a built-in static map of common file extensions. The matching is case-insensitive. This is the least reliable method and should only be used as a fallback on systems with a misconfigured or incomplete MIME database. + Try to guess the MIME type using a built-in static map of common file extensions. Matching is case-insensitive. This is the least reliable method and should only be used as a fallback when the above tools are missing or the MIME database is misconfigured. #-# Load MIME type aliases Enables loading MIME type associations from system "aliases" files. This helps find more applications by linking a file's MIME type with its alternative names (e.g., "application/x-deb" to "application/vnd.debian.binary-package", or "image/ico" to "image/vnd.microsoft.icon"). @@ -171,23 +179,22 @@ $^#Configuration Dialog (F9)# For text files ("text/*"), the "text/plain" type is additionally included to suggest standard text editors for source code and scripts. For example, for a file with the type "text/x-c++src", editors registered for "text/plain" will also be suggested. #-# Show universal handlers for all files - When enabled, this option adds the generic MIME type "application/octet-stream" as a fallback for any regular file. This ensures that even for files with unknown or unrecognized types, universal tools like hex editors will always be suggested. If disabled, the list may be empty for files without a clear association. + Add the generic MIME type "application/octet-stream" as a fallback for any regular file. This ensures that even for files with unknown or unrecognized types, universal tools like hex editors will always be suggested. If disabled, the list may be empty for files without a clear association. #-# Use mimeinfo.cache Speed up application lookups by using the "mimeinfo.cache" files. These are index files, automatically maintained by the system, that create a direct mapping from a MIME type (e.g., "image/png") to a list of .desktop files of applications that can handle it. Disabling this forces a slower, exhaustive scan of all .desktop files found in XDG data directories. #-# Filter by OnlyShowIn/NotShowIn - Respect the "OnlyShowIn" and "NotShowIn" keys in .desktop files. This will filter the application list to show only those relevant to the current desktop environment (e.g., GNOME, KDE) as specified in the $XDG_CURRENT_DESKTOP environment variable. + Show only applications relevant to the current desktop environment (e.g., GNOME, KDE). The plugin respects the "OnlyShowIn" and "NotShowIn" keys in .desktop files, matching them against the $XDG_CURRENT_DESKTOP environment variable. #-# Validate TryExec field Before displaying an application, verify that the executable specified by the "TryExec" key in its .desktop file exists. This check helps hide orphaned shortcuts from uninstalled software. Turning it off may make the menu appear slightly faster. #-# Disable ranking and sort alphabetically - When enabled, this option overrides the complex ranking logic and sorts the application list strictly in alphabetical order. This can be useful if you prefer a simple, predictable list rather than one prioritized by MIME type specificity and association source. - For a detailed explanation, see the "How does ranking work? Why is app X listed above app Y?" section in the ~Troubleshooting~@Troubleshooting@ guide. + Override the complex ranking logic and list the applications strictly by name. For a detailed explanation, see the "How does ranking work? Why is app X listed above app Y?" section in the ~Troubleshooting~@Troubleshooting@ guide. #-# Pass plain paths instead of file:// URIs - This option allows you to select the format for passing file paths to applications when constructing the launch command. The XDG specification for the "Exec" key explicitly permits local files to be passed either as file:// URIs or as plain file paths for the "%u" and "%U" field codes. By default, the plugin generates file:// URIs. However, some applications may not handle them correctly, despite claiming support. Enabling this option will switch the output to plain, escaped file paths, which can improve compatibility with such applications. + When constructing the launch command, replace the "%u" and "%U" field codes with plain escaped paths rather than file:// URIs. The XDG specification allows both formats for local files, but some applications mishandle file:// URIs despite claiming support. This option improves compatibility by using the alternative approach. #-# Show package qualifiers Tag applications in the menu installed as Flatpak or Snap packages. Helps avoid confusion when multiple variants of the same program are available. @@ -210,7 +217,7 @@ $^#Tips & tricks# #[KeyMacros/Shell/Apps]# #Description=OpenWith Plugin for selected file(s)# #DisableOutput=1# - #Sequence=F11 O# + #Sequence=F11 $if(menu.select("OpenWith",0)>0) Enter $else Esc $end# or variant 2: #[KeyMacros/Shell/Apps]# #Description=OpenWith Plugin for selected file(s)# @@ -229,7 +236,8 @@ $^#Tips & tricks# After restarting far2l/far2m the macro will be available for use. - #2.# To quickly open the current folder in an external file manager or another far2l instance, invoke the OpenWith plugin on the ".." entry in the file panel. The plugin will treat this as a request for the current directory and show a list of matching applications. Among them, you will find far2l-gui (if installed) and far2l-tty. Note that for console applications (far2l-tty, mc, etc.), the plugin supports two launch modes: in far2l's ~built-in terminal~@:Terminal@ or in a new ~external terminal~@:ExternalTerminal@ window. To ensure a new window opens, use #Shift+Enter# or enable the "Use external terminal for console apps" ~option~@ConfigurationDialog@. + #2.# To quickly open the current folder in an external file manager or another far2l instance, invoke the OpenWith plugin on the ".." entry in the file panel. The plugin will treat this as a request for the current directory and show a list of matching applications. Among them, you will find far2l-gui (if installed) and far2l-tty. + Note that for console applications (far2l-tty, mc, etc.), the plugin supports two launch modes: in far2l's ~built-in terminal~@:Terminal@ or in a new ~external terminal~@:ExternalTerminal@ window. To ensure a new window opens, use ~Shift+Enter~@ShiftEnter@ or enable the "Use external terminal for console apps" ~option~@ConfigurationDialog@. ~Contents~@Contents@ @@ -238,10 +246,10 @@ $^#OpenWith plugin# $^#Version 1.2# $^#Copyright (C) 2025-2026 Ivan # $^#Troubleshooting# - #Why is the application list empty when I select multiple files?# + #Why do I get the error "No applications found" when selecting multiple files?# The plugin uses an "intersection" logic: it seeks applications capable of opening all selected files simultaneously. - Example: If you select an archive (".zip") and an image (".jpg"), the list will likely be empty, as few applications support both formats. + Example: If you select an archive (".zip") and an image (".jpg"), the resulting list will likely be empty, as few applications support both formats. Solution: Process files of different categories in separate operations. This algorithm is most effective for batch-processing files of the same kind. @@ -254,14 +262,14 @@ $^#Troubleshooting# #Why doesn't my favorite app appear in the list? (Linux/BSD)# If an application doesn't appear even for a single file, check the following: - #-# Missing .desktop file: Ensure that a .desktop file for the application exists in one of the standard directories defined by $XDG_DATA_HOME and $XDG_DATA_DIRS environment variables (typically "~~/.local/share/applications" and "/usr/share/applications"). The plugin also automatically scans standard Flatpak and Snap application directories ("~~/.local/share/flatpak/exports/share/applications", "/var/lib/flatpak/exports/share/applications", "/var/lib/snapd/desktop/applications"). + #-# Missing .desktop file: Ensure that a .desktop file for the application exists in one of the standard directories defined by the $XDG_DATA_HOME and $XDG_DATA_DIRS environment variables (typically "~~/.local/share/applications" and "/usr/share/applications"). The plugin also automatically scans standard Flatpak and Snap application directories ("~~/.local/share/flatpak/exports/share/applications", "/var/lib/flatpak/exports/share/applications", "/var/lib/snapd/desktop/applications"). #-# Incorrect association: Check that the correct MIME type is listed in the "MimeType" key of the .desktop file, or that an association is defined in "~~/.config/mimeapps.list". - #-# Removed section: Ensure the application is not listed in the "[Removed Associations]" section of your "mimeapps.list" file. + #-# [Removed Associations] section: Ensure the desired application is not blacklisted in your "mimeapps.list" file. #-# Filtering options: Try disabling the "Filter by OnlyShowIn/NotShowIn" and "Validate TryExec" options in the ~configuration dialog~@ConfigurationDialog@, as they might be hiding the application. - #-# Outdated cache: Run 'update-desktop-database' and 'update-mime-database' in your terminal to refresh the system caches. + #-# Outdated caches: For diagnostics, temporarily disable the "Use mimeinfo.cache" option in the ~configuration dialog~@ConfigurationDialog@. If the application appears in the list after that, the problem is a stale association cache. To rebuild it, run 'update-desktop-database' in your terminal. It also makes sense to run 'update-mime-database', which refreshes the system-wide MIME type database, including glob patterns and magic numbers. - #File type is not detected ("none") and the application list is empty! (Linux/BSD)# + #Why is the file type not detected ("none") and the application list empty? (Linux/BSD)# This usually happens if analysis tools are disabled or unsuccessful. Solution: @@ -270,14 +278,10 @@ $^#Troubleshooting# #-# Enable "Show universal handlers for all files" in the ~configuration dialog~@ConfigurationDialog@. This ensures that at least basic utilities (such as hex editors) appear even if the exact file type is unknown. - #Why do I get the error "Selected item must be a file accessible locally via a real path."?# + #Why do I get the error "Selected item must be a file accessible locally via a real path"?# - This error means the selected item is either not a physical file or resides in a virtual environment that the OS cannot access directly, so it cannot be passed as a command-line argument to external applications. It may occur when trying to open via the plugin: - #-# non-file items (e.g., connection names in NetRocks); - #-# files inside archives, as they are virtual until extracted (e.g., in multiarc/arclite); - #-# internal file structure elements (e.g., ELF headers, sections, or symbols viewed via Inside); - #-# file objects on remote resources (e.g., via NetRocks). - Solution: Copy or extract the file to a real directory on your disk before opening it with this plugin. + This error means the selected item is either not a physical file or resides in a virtual environment that the OS cannot access directly, so it cannot be passed as a command-line argument to external applications. For example, this can happen if you invoke the plugin on files inside an archive (via the multiarc/arclite panel) or files on a remote resource (via the NetRocks panel). + Solution: Copy or extract the file to a real directory on your disk before opening it. #How does ranking work? Why is app X listed above app Y? (Linux/BSD)# @@ -286,21 +290,22 @@ $^#Troubleshooting# 1. MIME type specificity (primary factor): An association with a more specific type (e.g., "text/x-c++src") is always prioritized over a generic one (e.g., "text/plain"). 2. Association source (secondary factor): Sources are prioritized from highest to lowest: the global default ('xdg-mime query default'), the "[Default Applications]" section in "mimeapps.list", the "[Added Associations]" section, and finally, the "mimeinfo.cache" or the data from .desktop files themselves. Therefore, an application set as the system default for a very specific file type will always be at the top of the list. - If you prefer a simple, predictable list over this complex logic, turn it off using the "Disable ranking and sort alphabetically" option in the ~configuration dialog~@ConfigurationDialog@. + If you prefer a simple list sorted by name, use the "Disable ranking and sort alphabetically" option in the ~configuration dialog~@ConfigurationDialog@. - #An application is in the list, but it fails to launch. (Linux/BSD)# + #Why does a listed application fail to launch? (Linux/BSD)# This usually indicates an error in the "Exec" line of the .desktop file or a missing dependency. Press #F3# to ~view the command~@DetailsDialog@ the plugin is trying to execute. If the command looks correct, the application might be failing silently at startup. To diagnose this: 1. Open the ~configuration dialog~@ConfigurationDialog@. 2. Uncheck the "Don't wait for command completion" option. 3. Try launching the application again. - You will now see any error messages (stdout/stderr) printed by the application directly in the far2l console. - Also check the command for common syntax errors: - #-# Incorrect field codes: The plugin supports standard codes ("%f", "%F", "%u", "%U", "%c", "%k"). Some applications might use deprecated or non-standard codes that will not work. + You will now see any error messages (stdout/stderr) printed by the application directly in the far2l console. Note: when multiple files are selected, the plugin may launch the application several times if its .desktop file mandates a separate invocation per file (uses "%f" or "%u"). In that case, error output is not captured in the far2l console — test with a single file first. + + Also check the command for these common issues: + #-# Unsupported field codes: The plugin recognizes codes "%f", "%F", "%u", "%U", "%c", "%k". Applications that use deprecated or non-standard codes may not work as expected. #-# Missing quotes: Paths with spaces must be correctly quoted or escaped. Warning: Do NOT enclose field codes in quotes (e.g., use #%f#, not #"%f"#). According to the spec, field codes inside quotes are treated as literal text and are not expanded. #-# Program not in $PATH: If "Exec" contains just a program name, it must be located in one of the directories listed in the $PATH environment variable. - #-# Incorrect path format: The application might not correctly handle the file:// URIs that the plugin passes by default, even if its .desktop file uses the "%u" or "%U" field codes. Try enabling the "Pass plain paths instead of file:// URIs" option in the ~configuration dialog~@ConfigurationDialog@. + #-# Unsupported path format: The application might not correctly handle the file:// URIs that the plugin passes by default, even if its .desktop file uses the "%u" or "%U" field codes. Try enabling the "Pass plain paths instead of file:// URIs" option in the ~configuration dialog~@ConfigurationDialog@. #Why is a checkbox in the configuration dialog grayed out? (Linux/BSD)# @@ -309,11 +314,39 @@ $^#Troubleshooting# Solution: Install the missing tool via your package manager and ensure it is accessible in $PATH. - #Plugin works very slowly! Can I speed up the menu appearance? (Linux/BSD)# + #Why is the plugin so slow? Can I speed up the menu appearance? (Linux/BSD)# Delays are usually caused by launching "heavy" external tools for every file when processing a large number of them. Solution: #-# Prefer the "Use glob rules detection" ~option~@ConfigurationDialog@ over other MIME type discovery methods. Pattern matching is performed internally within the plugin and is instant. Launching external processes takes time; the "file" and "magika" tools always read file content, and "magika" is even slower as it loads a neural network model. #-# Avoid selecting too many files at once if external analysis tools are enabled. + + #Why do I get the error "Failed to save modified settings; the changes will apply to the current session only"?# + + This error occurs when the plugin cannot write to the configuration file. Common causes include missing permissions, a read-only file system, or insufficient disk space. The exact path to the target file is shown in the error message. + Note that this message does not appear every time you click "OK". To minimize disk operations, the plugin only attempts to overwrite the configuration file if you have actually modified the settings. If you simply open the ~configuration dialog~@ConfigurationDialog@ and click "OK" without making changes (or revert them to their original state), the plugin skips the save operation, so no error is triggered. + Any unsaved changes are kept in memory and remain active until you close the file manager. + + + #Why do I get the error "OpenWith plugin is not available on this platform"?# + + The plugin could not find a suitable application provider for your system. Most likely, your OS was not recognized as compatible at build time: the compiler did not define the required macros, or support for your specific OS variant has not yet been added to the plugin's source code. If you are running Linux, BSD, or macOS and are sure your system supports XDG (or Launch Services), check your build parameters or report the issue to the developers: ~https://github.com/elfmz/far2l/issues~@https://github.com/elfmz/far2l/issues@. + + ~Contents~@Contents@ + +@ShiftEnter +$^#OpenWith plugin# +$^#Version 1.2# +$^#Copyright (C) 2025-2026 Ivan # +$^#Forced launch mode (Shift+Enter)# + The effect of pressing #Shift+Enter# in the ~menu~@Contents@ depends on the currently selected application: + + #-# a console application will be launched in a new ~external terminal~@:ExternalTerminal@ window, + #-# a GUI application will be launched asynchronously, immediately returning control to far2l. + + This is a convenient way to apply the forced launch mode once, without modifying the permanent ~plugin settings~@ConfigurationDialog@ "Use external terminal for console apps" and "Don't wait for command completion". + + A regular #Enter# press (without Shift) launches the application according to the current values of these settings. + ~Contents~@Contents@ diff --git a/OpenWith/configs/help_ru.hlf b/OpenWith/configs/help_ru.hlf index cf6672b8d..0f8609506 100644 --- a/OpenWith/configs/help_ru.hlf +++ b/OpenWith/configs/help_ru.hlf @@ -8,12 +8,14 @@ $^#Copyright (C) 2025-2026 Ivan # $^#Содержание# Плагин #OpenWith# предоставляет контекстное меню для открытия выбранного файла с помощью подходящего приложения. Он интеллектуально определяет тип файла и запрашивает у операционной системы список зарегистрированных приложений, которые могут его обработать. Если файлов несколько, плагин находит общие приложения, зарегистрированные для обработки каждого из них, путём пересечения их индивидуальных списков кандидатов. Плагин использует нативные механизмы ОС: в Linux/BSD он следует стандартам XDG для определения MIME-типов и поиска приложений, а в macOS — интегрируется с фреймворком Launch Services. - После вызова плагина на файле отображается меню со списком приложений. Пользователь может выбрать одно из них для запуска или нажать #F3# для просмотра подробной информации о файле и выбранном приложении. По клавише #F9# вызывается диалог конфигурации для тонкой настройки поведения плагина. Изменение опций, влияющих на алгоритмы поиска, фильтрации или ранжирования кандидатов, приводит к немедленному обновлению списка приложений в меню. + После вызова плагина на файле отображается меню со списком приложений. Пользователь может выбрать одно из них для запуска (#Enter# или #Shift+Enter#) или нажать #F3# для просмотра подробной информации о файле и выбранном приложении. По клавише #F9# вызывается диалог конфигурации для тонкой настройки поведения плагина. Изменение опций, влияющих на алгоритмы поиска, фильтрации или ранжирования кандидатов, приводит к немедленному обновлению списка приложений в меню. - ~Информационный диалог (F3)~@DetailsDialog@ + ~Диалог подробностей (F3)~@DetailsDialog@ ~Диалог настроек (F9)~@ConfigurationDialog@ + ~Принудительный режим запуска (Shift+Enter)~@ShiftEnter@ + ~Советы по использованию и полезности~@Tips@ ~Решение проблем~@Troubleshooting@ @@ -22,28 +24,31 @@ $^#Содержание# $^#Плагин OpenWith# $^#Version 1.2# $^#Copyright (C) 2025-2026 Ivan # -$^#Информационный диалог (F3)# - Информационный диалог, доступный по нажатию #F3# в меню выбора приложений, отображает подробную техническую информацию о файле, выделенном приложении и точной команде, которая будет использована для его запуска. +$^#Диалог подробностей (F3)# + Отображает детальную техническую информацию о выбранных файлах, выделенном приложении и точной команде, которая будет использована для его запуска. Доступен по нажатию #F3# в меню выбора приложений. #1. ОБЩИЕ ПОЛЯ# - #-# Адрес файла - Полный (абсолютный) путь к выбранному файлу. Если выбрано несколько файлов, это поле будет отображать их количество. + #-# Выбрано файлов + Количество файлов, обрабатываемых плагином в данный момент. Не отображается, если выбран только один файл. + + #-# Адреса файлов + Полные (абсолютные) пути к выбранным файлам, перечисленные через точку с запятой. - #-# MIME-профиль - Тип(ы) файла, определённые плагином. Для группы файлов отображаются все уникальные найденные профили. Формат зависит от ОС: - #Linux/BSD#: "Сырые" результаты всех методов детекции ("xdg-mime", "file" и т.д.), включённых в ~настройках плагина~@ConfigurationDialog@. Если инструменты дали разные ответы, они перечисляются через точку с запятой: "(text/plain;application/x-shellscript)". + #-# MIME-профили + Тип(ы) файла или файлов, определённые плагином. Для группы файлов отображаются все уникальные найденные профили. Формат зависит от ОС: + #Linux/BSD#: "Сырые" результаты всех методов детекции (xdg-mime, file и т.д.), включённых в ~настройках плагина~@ConfigurationDialog@. Если инструменты расходятся в ответах для одного и того же файла, результаты объединяются через точку с запятой: "(text/plain;application/x-shellscript)". #macOS#: MIME-тип, соответствующий системному идентификатору (UTI) файла. #-# Команда запуска - Финальная командная строка, которая будет выполнена при нажатии #Enter# или #Shift+Enter#. + Финальная командная строка, которая будет выполнена при нажатии #Enter# или ~Shift+Enter~@ShiftEnter@. #2. ПЛАТФОРМОСПЕЦИФИЧНЫЕ ПОЛЯ# #2.1. Детали приложения в Linux/BSD# - Этот раздел отображает подробную информацию, извлеченную из .desktop-файла приложения. Эти файлы служат в качестве ярлыков и контейнеров с метаданными, описывая, как приложения следует именовать, отображать и запускать. + Этот раздел отображает подробную информацию, извлечённую из .desktop-файла приложения. Эти файлы служат в качестве ярлыков и контейнеров с метаданными, описывая, как приложения следует именовать, отображать и запускать. #-# Desktop файл Полный путь к .desktop-файлу, который описывает это приложение. @@ -63,7 +68,7 @@ $^#Информационный диалог (F3)# Описание приложения, обычно используемое в качестве всплывающей подсказки. #-# Categories - Список категорий, разделенных точкой с запятой, который используется окружением рабочего стола для организации приложений в меню. + Список категорий, разделённых точкой с запятой, который используется окружением рабочего стола для организации приложений в меню. #-# Exec Шаблон командной строки для запуска приложения. Он может содержать специальные заполнители (коды полей), такие как "%f" для пути к одному файлу, "%U" для списка URL-адресов или "%%" для символа процента. @@ -75,13 +80,13 @@ $^#Информационный диалог (F3)# Логическое значение (true или false), указывающее, должно ли приложение запускаться в окне терминала. #-# MimeType - Список MIME-типов, поддерживаемых данным приложением, разделенных точкой с запятой. По этому полю плагин определяет, будет ли программа показана в меню для выбранного файла. + Список MIME-типов, поддерживаемых данным приложением, разделённых точкой с запятой. По этому полю плагин определяет, будет ли программа показана в меню для выбранного файла. #-# OnlyShowIn - Список идентификаторов окружений рабочего стола, разделенных точкой с запятой (например, "GNOME;KDE;"). Если этот ключ присутствует, приложение должно отображаться только в указанных окружениях. Это позволяет разработчикам создавать инструменты, предназначенные для конкретных рабочих столов. + Список идентификаторов окружений рабочего стола, разделённых точкой с запятой (например, "GNOME;KDE;"). Если этот ключ присутствует, приложение должно отображаться только в указанных окружениях. Это позволяет разработчикам создавать инструменты, предназначенные для конкретных рабочих столов. #-# NotShowIn - Список идентификаторов окружений рабочего стола, разделенных точкой с запятой. Если этот ключ присутствует, приложение должно быть скрыто в указанных окружениях. Это обратное действие по отношению к OnlyShowIn, полезное для скрытия программ, которые плохо интегрируются с определенными рабочими столами. + Список идентификаторов окружений рабочего стола, разделённых точкой с запятой. Если этот ключ присутствует, приложение должно быть скрыто в указанных окружениях. Это обратное действие по отношению к OnlyShowIn, полезное для скрытия программ, которые плохо интегрируются с определёнными рабочими столами. #-# X-Flatpak Расширение Desktop Entry Specification. Глобально уникальный идентификатор приложения Flatpak (например, "org.gimp.GIMP"). Записывается системой Flatpak при установке. Гарантирует, что ярлык связан с правильным Flatpak-приложением, даже если имя файла отличается от этого ID. @@ -114,24 +119,27 @@ $^#Плагин OpenWith# $^#Version 1.2# $^#Copyright (C) 2025-2026 Ivan # $^#Диалог настроек (F9)# - Диалог настроек, доступный по нажатию #F9# в меню выбора приложений и через ~Параметры внешних модулей~@:PluginsConfig@ из ~Меню параметров~@:OptMenu@, позволяет вам кастомизировать поведение плагина. Он разделен на общие и платформоспецифичные настройки. + Позволяет кастомизировать поведение плагина. Доступен через ~Параметры~@:OptMenu@ → ~Параметры внешних модулей~@:PluginsConfig@ (или #Alt+Shift+F9#), а также по нажатию #F9# в меню выбора приложений. #1. ОБЩИЕ НАСТРОЙКИ# #-# Использовать внешний терминал для консольных приложений Если опция включена, приложения, требующие терминала (например, vim или mc), будут запускаться в новом окне ~внешнего терминала~@:ExternalTerminal@. Если отключена, они будут выполняться во ~встроенном терминале~@:Terminal@ far2l. Если используется встроенный терминал, то при выборе нескольких файлов плагин применит "умную" фильтрацию: из списка будут скрыты те терминальные приложения, которые требуют запуска отдельного процесса на каждый открываемый файл. Приложения, способные принять все выделенные файлы за один раз, останутся в списке. - Вы можете принудительно запустить консольное приложение во внешнем терминале независимо от этой настройки, нажав #Shift+Enter#. + Вы можете принудительно запустить консольное приложение во внешнем терминале независимо от этой настройки, нажав ~Shift+Enter~@ShiftEnter@. #-# Не ждать завершения команды Если опция включена, GUI-приложения будут запускаться асинхронно, немедленно возвращая управление в far2l. Если отключена, far2l будет ожидать закрытия запущенного приложения, позволяя увидеть его вывод (stdout/stderr) в консоль. Эта настройка не влияет на терминальные приложения. Она также игнорируется, если приложение требует отдельного запуска для каждого выбранного файла — в этом случае всегда используется асинхронный режим во избежание блокировки интерфейса. - Вы можете принудительно запустить GUI-приложение асинхронно независимо от этой настройки, нажав #Shift+Enter#. + Вы можете принудительно запустить GUI-приложение асинхронно независимо от этой настройки, нажав ~Shift+Enter~@ShiftEnter@. #-# Снимать выделение, если выбрана команда Когда опция включена, запуск приложения из меню сбросит выбор файлов на панели. #-# Подтверждать открытие, если файлов больше чем... - Чтобы предотвратить ресурсоемкие запуски, плагин покажет предупреждение, если количество выбранных файлов превысит указанный порог. При отказе вы вернётесь назад, в меню выбора приложений или в ~диалог "Подробности"~@DetailsDialog@. + Чтобы предотвратить ресурсоёмкие запуски, плагин покажет предупреждение, если количество выбранных файлов превысит указанный порог. При отказе вы вернётесь назад, в меню выбора приложений или в ~диалог подробностей~@DetailsDialog@. + + #-# Отображать имя открываемого файла в заголовке меню + Если опция включена, плагин изменяет заголовок меню, добавляя в него имя обрабатываемого файла (или их общее количество, если выбрано несколько файлов). Если выключена, используется статичный заголовок "Выберите приложение". #2. ПЛАТФОРМОСПЕЦИФИЧНЫЕ НАСТРОЙКИ# @@ -144,7 +152,7 @@ $^#Диалог настроек (F9)# Определять MIME‑тип выбранного файла вызовом команды 'xdg-mime query filetype'. Это стандартный и наиболее рекомендуемый метод. Утилита обращается к системной базе данных shared-mime-info, поэтому её результаты максимально совместимы с записями "MimeType" в .desktop-файлах. Хотя "xdg-mime" может дополнительно выполнять анализ содержимого, в первую очередь она опирается на имена файлов. Это делает её подверженной ошибкам при работе с файлами с неверным расширением. #-# Использовать утилиту `file` - Определять MIME‑тип выбранного файла вызовом команды 'file --mime-type'. Утилита выполняет глубокий анализ бинарного содержимого файла, используя собственную обширную базу "магических чисел". Она чрезвычайно надёжна, и её невозможно обмануть неверным расширением файла. Однако она может работать медленнее, так как требует чтения содержимого файла. Кроме того, возвращаемые ею имена MIME-типов иногда расходятся с именами в базе shared-mime-info, что может приводить к меньшему количеству найденных приложений. + Определять MIME‑тип выбранного файла вызовом команды 'file --mime-type'. Утилита выполняет глубокий анализ бинарных данных, используя собственную обширную базу "магических чисел". Она чрезвычайно надёжна, и её невозможно обмануть неверным расширением файла. Однако она может работать медленнее, так как требует чтения содержимого файла. Кроме того, возвращаемые ею имена MIME-типов иногда расходятся с именами в базе shared-mime-info, что может приводить к меньшему количеству найденных приложений. #-# Использовать утилиту `magika` Определять MIME-тип выбранного файла вызовом команды 'magika --format %m'. Вместо традиционных магических чисел утилита использует модель глубокого обучения (ONNX), созданную на обширном наборе данных для анализа содержимого. Это обеспечивает высокую точность распознавания более 200 форматов, включая современные языки программирования, файлы конфигурации DevOps и типы данных ML/AI. @@ -154,7 +162,7 @@ $^#Диалог настроек (F9)# Определять MIME-тип выбранного файла путем сопоставления имени с шаблонами из системной базы данных "globs2". Этот внутренний метод значительно быстрее утилиты "xdg-mime", так как не запускает сторонних процессов. Однако, в отличие от "xdg-mime", он полагается только на имя файла и не использует анализ содержимого ("magic sniffing") для разрешения конфликтов между несколькими подходящими правилами. Включение этой опции одновременно с опцией "Использовать утилиту `xdg-mime`" избыточно. Метод лучше всего подходит как быстрая альтернатива "xdg-mime" или как продвинутая замена "фолбэку по расширению", так как он учитывает веса шаблонов и регистр символов согласно спецификации XDG. #-# Использовать фолбэк по расширению - В крайнем случае, если вышеуказанные утилиты не справляются, отсутствуют или отключены, плагин пытается угадать MIME-тип по встроенной статической карте распространённых расширений. Сопоставление выполняется без учёта регистра символов. Это наименее надёжный метод, который следует использовать лишь как запасной вариант в системах с некорректно настроенной или неполной базой MIME-типов. + Попытаться угадать MIME-тип по встроенной статической карте распространённых расширений. Сопоставление выполняется без учёта регистра символов. Наименее надёжный метод, который следует использовать лишь как запасной вариант при отсутствии вышеуказанных утилит или некорректно настроенной базе MIME-типов. #-# Загружать псевдонимы MIME-типов Включает загрузку ассоциаций MIME-типов из системных файлов "aliases". Это помогает найти больше приложений, связывая MIME-тип файла с его альтернативными именами (например, "application/x-deb" с "application/vnd.debian.binary-package", или "image/ico" с "image/vnd.microsoft.icon"). @@ -171,23 +179,22 @@ $^#Диалог настроек (F9)# Для текстовых файлов ("text/*") будет дополнительно добавлен тип "text/plain", чтобы предложить стандартные текстовые редакторы для исходных кодов и скриптов. Например, для файла с типом "text/x-c++src" будут также предложены редакторы, зарегистрированные для "text/plain". #-# Показывать универсальные обработчики для всех файлов - Если опция включена, плагин будет добавлять универсальный MIME-тип "application/octet-stream" в качестве резервного варианта для любого обычного файла. Это гарантирует, что даже для файлов с неизвестным или неопознанным типом всегда будут предложены универсальные инструменты, такие как hex-редакторы. При отключении опции список может оказаться пустым для файлов без чёткой ассоциации. + Добавлять MIME-тип "application/octet-stream" в качестве резервного варианта для любого обычного файла. Это гарантирует, что даже для файлов с неизвестным или неопознанным типом всегда будут предложены универсальные инструменты, такие как hex-редакторы. При отключении опции список может оказаться пустым для файлов без чёткой ассоциации. #-# Использовать mimeinfo.cache Ускорить поиск приложений за счет использования файлов "mimeinfo.cache". Это индексные файлы, автоматически поддерживаемые системой, которые создают прямое сопоставление MIME-типа (например, "image/png") со списком .desktop-файлов приложений, способных его обработать. Отключение этой опции приведет к более медленному, полному сканированию всех .desktop-файлов, находящихся в каталогах данных XDG. #-# Фильтровать по OnlyShowIn/NotShowIn - Учитывать ключи "OnlyShowIn" и "NotShowIn" в .desktop-файлах. Это отфильтрует список приложений, чтобы показывать только те, которые релевантны для текущего окружения рабочего стола (например, GNOME, KDE), согласно переменной окружения $XDG_CURRENT_DESKTOP. + Показывать только те приложения, которые релевантны для текущего окружения рабочего стола (например, GNOME, KDE). Плагин учитывает ключи "OnlyShowIn" и "NotShowIn" из .desktop-файлов, сопоставляя их с переменной $XDG_CURRENT_DESKTOP. #-# Проверять TryExec Перед показом приложения проверять наличие исполняемого файла, указанного в ключе "TryExec" его .desktop-файла. Помогает скрыть "битые" ярлыки удалённых программ. Отключение этой опции может незначительно ускорить появление меню. #-# Отключить ранжирование и сортировать по имени - Если опция включена, она переопределяет сложную систему ранжирования и выводит список приложений строго в алфавитном порядке. Это может быть полезно, если вы предпочитаете простой и предсказуемый список вместо списка, приоритет в котором определяется специфичностью MIME-типа и авторитетностью источника ассоциации. - Подробное объяснение смотрите в разделе "Как работает ранжирование? Почему приложение X выше, чем Y?" руководства по ~решению проблем~@Troubleshooting@. + Переопределить сложную систему ранжирования и выводить список приложений строго в алфавитном порядке. Подробное объяснение смотрите в разделе "Как работает ранжирование? Почему приложение X выше, чем Y?" руководства по ~решению проблем~@Troubleshooting@. #-# Передавать простые пути вместо file:// URI - Эта опция позволяет выбрать формат передачи путей к файлам при конструировании команды запуска. Спецификация XDG для ключа "Exec" явно разрешает передавать локальные файлы для кодов "%u" и "%U" как в виде URI (file://), так и в виде обычных путей. По умолчанию плагин генерирует URI. Однако некоторые приложения некорректно обрабатывают ссылки вида file://, даже если заявляют их поддержку. Включение этой опции изменит формат на передачу простых экранированных путей, что улучшит совместимость с такими приложениями. + При конструировании команды запуска заменять коды полей "%u" и "%U" на простые экранированные пути, а не на file:// URI. Спецификация XDG для локальных файлов допускает оба варианта, но некоторые приложения некорректно обрабатывают file:// URI, даже заявляя о поддержке. Эта опция улучшает совместимость за счет использования альтернативного формата. #-# Показывать теги пакетов Помечать в меню приложения, установленные в виде пакетов Flatpak или Snap. Помогает не запутаться, если в системе присутствует несколько вариантов одной программы. @@ -210,7 +217,7 @@ $^#Советы по использованию и полезности# #[KeyMacros/Shell/Apps]# #Description=OpenWith Plugin for selected file(s)# #DisableOutput=1# - #Sequence=F11 O# + #Sequence=F11 $if(menu.select("OpenWith",0)>0) Enter $else Esc $end# или вариант 2: #[KeyMacros/Shell/Apps]# #Description=OpenWith Plugin for selected file(s)# @@ -229,7 +236,8 @@ $^#Советы по использованию и полезности# После повторного запуска far2l/far2m макрос станет доступен для использования. - #2.# Чтобы быстро открыть текущую папку во внешнем файловом менеджере или в другом экземпляре far2l, вызовите плагин OpenWith на элементе ".." в файловой панели. Плагин воспримет это как запрос для текущего каталога и покажет список подходящих приложений. Среди предложенных вариантов будут far2l-gui (если установлен) и far2l-tty. Обратите внимание, что для консольных приложений (far2l-tty, mc и т.п.) плагин поддерживает два режима запуска: во ~встроенном терминале~@:Terminal@ far2l или в новом окне ~внешнего терминала~@:ExternalTerminal@. Чтобы гарантированно открыть новое окно, воспользуйтесь #Shift+Enter# или включите ~опцию~@ConfigurationDialog@ "Использовать внешний терминал для консольных приложений". + #2.# Чтобы быстро открыть текущую папку во внешнем файловом менеджере или в другом экземпляре far2l, вызовите плагин OpenWith на элементе ".." в файловой панели. Плагин воспримет это как запрос для текущего каталога и покажет список подходящих приложений. Среди предложенных вариантов будут far2l-gui (если установлен) и far2l-tty. + Обратите внимание, что для консольных приложений (far2l-tty, mc и т.п.) плагин поддерживает два режима запуска: во ~встроенном терминале~@:Terminal@ far2l или в новом окне ~внешнего терминала~@:ExternalTerminal@. Чтобы гарантированно открыть новое окно, воспользуйтесь ~Shift+Enter~@ShiftEnter@ или включите ~опцию~@ConfigurationDialog@ "Использовать внешний терминал для консольных приложений". ~Содержание~@Contents@ @@ -238,7 +246,7 @@ $^#Плагин OpenWith# $^#Version 1.2# $^#Copyright (C) 2025-2026 Ivan # $^#Решение проблем# - #Почему список приложений пуст при выборе нескольких файлов?# + #Почему возникает ошибка "Приложения не найдены" при выборе нескольких файлов?# Плагин работает по принципу "пересечения": он ищет приложения, совместимые со всеми выбранными типами файлов. Пример: Если вы выбрали архив (".zip") и изображение (".jpg"), список, скорее всего, окажется пустым, поскольку программы, поддерживающие оба этих формата, встречаются редко. @@ -253,15 +261,15 @@ $^#Решение проблем# #Почему моё любимое приложение не отображается в списке? (Linux/BSD)# - Если приложение не появляется даже при выборе одного файла, проверьте следующие пункты: - #-# Отсутствие .desktop-файла: Убедитесь, что для приложения существует .desktop-файл в одном из стандартных каталогов, определённых переменными окружения $XDG_DATA_HOME и $XDG_DATA_DIRS (обычно "~~/.local/share/applications" и "/usr/share/applications"). Плагин также автоматически сканирует стандартные каталоги приложений Flatpak и Snap ("~~/.local/share/flatpak/exports/share/applications", "/var/lib/flatpak/exports/share/applications", "/var/lib/snapd/desktop/applications"). - #-# Некорректная ассоциация: Проверьте, что в "MimeType" .desktop-файла указан нужный MIME-тип, или что ассоциация прописана в "~~/.config/mimeapps.list". - #-# Секция Removed: Убедитесь, что приложение не перечислено в секции "[Removed Associations]" вашего файла "mimeapps.list". + Если приложение не появляется даже при выборе одного файла, проверьте: + #-# Наличие .desktop-файла: Убедитесь, что для приложения существует .desktop-файл в одном из стандартных каталогов, определённых переменными окружения $XDG_DATA_HOME и $XDG_DATA_DIRS (обычно "~~/.local/share/applications" и "/usr/share/applications"). Плагин также автоматически сканирует стандартные каталоги приложений Flatpak и Snap ("~~/.local/share/flatpak/exports/share/applications", "/var/lib/flatpak/exports/share/applications", "/var/lib/snapd/desktop/applications"). + #-# Корректность ассоциации: Убедитесь, что в "MimeType" .desktop-файла указан нужный MIME-тип, или что ассоциация прописана в "~~/.config/mimeapps.list". + #-# Секцию [Removed Associations]: Удостоверьтесь, что нужное приложение отсутствует в чёрном списке вашего файла "mimeapps.list". #-# Опции фильтрации: Попробуйте отключить в ~настройках плагина~@ConfigurationDialog@ опции "Фильтровать по OnlyShowIn/NotShowIn" и "Проверять TryExec", так как они могут скрывать приложение. - #-# Устаревший кэш: Выполните в терминале 'update-desktop-database' и 'update-mime-database', чтобы обновить системные кэши. + #-# Актуальность кэшей: Для диагностики можно временно выключить опцию "Использовать mimeinfo.cache" в ~настройках плагина~@ConfigurationDialog@. Если приложение появится в списке, значит, проблема в устаревшем кэше ассоциаций. Для его обновления выполните в терминале команду 'update-desktop-database'. Также имеет смысл запустить 'update-mime-database' — она обновляет системную базу MIME-типов, включая glob-шаблоны и магические числа. - #Тип файла не определяется ("none") и список приложений пуст! (Linux/BSD)# + #Почему тип файла не определяется ("none") и список приложений пуст? (Linux/BSD)# Обычно это происходит, если инструменты анализа отключены или не сработали. Решение: @@ -270,13 +278,9 @@ $^#Решение проблем# #-# Включите "Показывать универсальные обработчики для всех файлов" в ~настройках плагина~@ConfigurationDialog@. Это гарантирует появление хотя бы базовых утилит (таких как hex-редакторы), даже если точный тип файла неизвестен. - #Почему возникает ошибка "Выбранный объект должен быть файлом, доступным локально по реальному пути."?# + #Почему возникает ошибка "Выбранный объект должен быть файлом, доступным локально по реальному пути"?# - Эта ошибка означает, что выбранный элемент либо не является физическим файлом, либо находится в виртуальной среде, недоступной ОС напрямую, поэтому его нельзя передать как аргумент командной строки внешнему приложению. Она может произойти при попытке открыть с помощью плагина: - #-# объекты, не являющиеся файлами (например, имена соединений в NetRocks); - #-# файлы внутри архивов, так как они являются виртуальными до извлечения (например, в multiarc/arclite); - #-# элементы внутренних структур файлов (например, заголовки, секции или символы ELF, просматриваемые через Inside); - #-# файловые объекты на удалённых ресурсах, открытых через плагины (например, в NetRocks). + Эта ошибка означает, что выбранный элемент либо не является физическим файлом, либо находится в виртуальной среде, недоступной ОС напрямую, поэтому его нельзя передать как аргумент командной строки внешнему приложению. Например, она может произойти, если вы вызываете плагин для файлов внутри архива (на панели multiarc/arclite) или для файлов на удалённом ресурсе (на панели NetRocks). Решение: Скопируйте или извлеките файл в реальную папку на диске перед открытием. @@ -286,21 +290,22 @@ $^#Решение проблем# 1. Специфичность MIME-типа (главный фактор): Ассоциация с более точным типом ("text/x-c++src") всегда приоритетнее, чем с общим ("text/plain"). 2. Источник ассоциации (вторичный фактор): Приоритет источников от высшего к низшему: глобальные настройки по умолчанию ('xdg-mime query default'), секция "[Default Applications]" в "mimeapps.list", секция "[Added Associations]", и, наконец, кэш "mimeinfo.cache" или данные из самих .desktop-файлов. Таким образом, приложение, установленное по умолчанию в системе для самого специфичного типа файла, всегда будет на вершине списка. - Если вы предпочитаете простой и предсказуемый список вместо этой сложной логики, это можно сделать с помощью опции "Отключить ранжирование и сортировать по имени" в ~настройках плагина~@ConfigurationDialog@. + Если вы предпочитаете более простую логику алфавитной сортировки, это можно сделать с помощью опции "Отключить ранжирование и сортировать по имени" в ~настройках плагина~@ConfigurationDialog@. - #Приложение есть в списке, но не запускается. (Linux/BSD)# + #Что делать, если приложение из списка не запускается? (Linux/BSD)# Обычно это означает ошибку в строке "Exec" .desktop-файла или отсутствующую зависимость. Нажмите #F3# для ~просмотра команды~@DetailsDialog@, которую пытается выполнить плагин. Если команда выглядит верной, возможно, приложение "молча" падает при старте. Чтобы продиагностировать проблему: 1. Откройте ~настройки плагина~@ConfigurationDialog@. 2. Выключите опцию "Не ждать завершения команды". 3. Попробуйте запустить приложение снова. - Теперь вы увидите сообщения об ошибках (stdout/stderr) прямо в консоли far2l, что поможет понять причину сбоя. - Также проверьте команду на типичные синтаксические ошибки: - #-# Неверные коды полей: Плагин поддерживает стандартные коды ("%f", "%F", "%u", "%U", "%c", "%k"). Некоторые приложения могут использовать устаревшие или нестандартные коды, которые не будут работать. + Теперь вы увидите сообщения об ошибках (stdout/stderr) прямо в консоли far2l, что поможет понять причину сбоя. Обратите внимание: при выборе нескольких файлов плагин может запустить приложение несколько раз, если его .desktop-файл предписывает отдельный вызов для каждого файла (использует коды "%f" или "%u"). В этом случае вывод ошибок в консоль far2l не попадает — лучше протестировать запуск на одном файле. + + Также проверьте команду на такие типичные проблемы: + #-# Неподдерживаемые коды полей: Плагин распознаёт коды "%f", "%F", "%u", "%U", "%c", "%k". Приложения, использующие устаревшие или нестандартные коды, могут работать непредсказуемо. #-# Отсутствие кавычек: Пути с пробелами должны быть корректно экранированы. Внимание: НЕ заключайте коды полей в кавычки (например, используйте #%f#, а не #"%f"#). Согласно спецификации, коды внутри кавычек воспринимаются как обычный текст и не раскрываются. #-# Программа не в $PATH: Если "Exec" содержит просто имя программы, она должна находиться в одном из каталогов, перечисленных в переменной окружения $PATH. - #-# Неверный формат пути: Приложение может некорректно обрабатывать file:// URI, которые плагин передает по умолчанию, даже если его .desktop-файл использует коды "%u" или "%U". Попробуйте включить опцию "Передавать простые пути вместо file:// URI" в ~настройках плагина~@ConfigurationDialog@. + #-# Неподдерживаемый формат пути: Приложение может некорректно обрабатывать file:// URI, которые плагин передает по умолчанию, даже если его .desktop-файл использует коды "%u" или "%U". Попробуйте включить опцию "Передавать простые пути вместо file:// URI" в ~настройках плагина~@ConfigurationDialog@. #Почему опция в диалоге настроек неактивна (серая)? (Linux/BSD)# @@ -309,11 +314,39 @@ $^#Решение проблем# Решение: Установите недостающую утилиту через менеджер пакетов и убедитесь, что она доступна в $PATH. - #Плагин работает очень медленно! Можно ли ускорить появление меню? (Linux/BSD)# + #Почему плагин работает так медленно? Можно ли ускорить появление меню? (Linux/BSD)# Задержки обычно вызваны запуском "тяжелых" внешних утилит для каждого файла при обработке их большого количества. Решение: #-# Отдавайте предпочтение ~опции~@ConfigurationDialog@ "Использовать правила glob-шаблонов" перед другими методами определения MIME-типов. Сопоставление по шаблонам выполняется внутри плагина и происходит мгновенно. Запуск внешних процессов требует времени; утилиты "file" и "magika" всегда производят чтение содержимого файла, а "magika" работает ещё медленнее, так как загружает модель нейросети. #-# Избегайте выбора слишком большого количества файлов одновременно, если включены внешние инструменты анализа. + + #Почему возникает ошибка "Не удалось сохранить изменённые настройки; они будут действовать только в текущем сеансе"?# + + Эта ошибка возникает, когда плагину не удаётся записать данные в конфигурационный файл. Причиной может быть отсутствие необходимых прав доступа, файловая система в режиме "только чтение" или нехватка свободного места на диске. Точный путь к целевому файлу отображается в самом сообщении об ошибке. + Обратите внимание: данное сообщение появляется не при каждом нажатии кнопки "ОК". Чтобы минимизировать количество дисковых операций, плагин пытается перезаписать файл конфигурации только в том случае, если вы действительно изменили какие-либо параметры. Если вы просто открыли ~настройки~@ConfigurationDialog@ и нажали "OK" (или вернули значения к исходным), операция сохранения пропускается, и ошибка не возникает. + Любые несохранённые изменения остаются в памяти и действуют только до закрытия файлового менеджера. + + + #Почему возникает ошибка "Плагин OpenWith недоступен на данной платформе"?# + + Плагин не смог найти подходящий провайдер приложений для вашей системы. Скорее всего, на этапе сборки ваша ОС не была распознана как совместимая: компилятор не определил нужные макросы либо поддержка вашей разновидности ОС ещё не добавлена в исходный код плагина. Если вы работаете в Linux, BSD или macOS и уверены, что система поддерживает XDG (или Launch Services), проверьте параметры сборки или сообщите о проблеме разработчикам: ~https://github.com/elfmz/far2l/issues~@https://github.com/elfmz/far2l/issues@. + + ~Содержание~@Contents@ + +@ShiftEnter +$^#Плагин OpenWith# +$^#Version 1.2# +$^#Copyright (C) 2025-2026 Ivan # +$^#Принудительный режим запуска (Shift+Enter)# + Эффект от нажатия #Shift+Enter# в ~меню~@Contents@ зависит от выбранного в данный момент приложения: + + #-# консольное приложение будет запущено в новом окне ~внешнего терминала~@:ExternalTerminal@, + #-# GUI-приложение будет запущено асинхронно, с немедленным возвратом управления в far2l. + + Это удобный способ однократно применить принудительный режим запуска, не изменяя постоянные ~настройки плагина~@ConfigurationDialog@ "Использовать внешний терминал для консольных приложений" и "Не ждать завершения команды". + + Обычное нажатие #Enter# (без Shift) запускает приложение в соответствии с текущими значениями этих настроек. + ~Содержание~@Contents@ diff --git a/OpenWith/src/AppProvider.cpp b/OpenWith/src/AppProvider.cpp index bc7baf1ae..f82ecd230 100644 --- a/OpenWith/src/AppProvider.cpp +++ b/OpenWith/src/AppProvider.cpp @@ -5,18 +5,18 @@ std::unique_ptr AppProvider::CreateAppProvider(TMsgGetter msg_getter) { - std::unique_ptr provider = nullptr; -#ifdef __linux__ - provider = std::make_unique(msg_getter); +#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) + return std::make_unique(msg_getter); #elif defined(__APPLE__) - provider = std::make_unique(msg_getter); -#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) - provider = std::make_unique(msg_getter); + return std::make_unique(msg_getter); +#else + return nullptr; #endif +} - if (provider) { - provider->LoadPlatformSettings(); - } - return provider; -} +AppProvider* AppProvider::GetInstance(TMsgGetter msg_getter) +{ + static auto s_provider = CreateAppProvider(msg_getter); + return s_provider.get(); +} \ No newline at end of file diff --git a/OpenWith/src/AppProvider.hpp b/OpenWith/src/AppProvider.hpp index 64354a2c0..9a7ec7882 100644 --- a/OpenWith/src/AppProvider.hpp +++ b/OpenWith/src/AppProvider.hpp @@ -10,13 +10,15 @@ struct ProviderSetting { - std::wstring internal_key; - std::wstring display_name; + std::wstring internal_key; // persistent INI key and internal identifier + std::wstring display_name; // localized UI label bool value; - bool disabled = false; // true if the setting should be grayed out in the UI - bool affects_candidates = true; // true if changing this setting affects the contents or order of the candidate list + bool disabled; // true if the setting should be grayed out in the UI + bool affects_candidates; // true if changing this setting affects the contents or order of the candidate list }; +class KeyFileReadHelper; +class KeyFileHelper; class AppProvider { @@ -26,7 +28,7 @@ class AppProvider explicit AppProvider(TMsgGetter msg_getter) : m_GetMsg(std::move(msg_getter)) {} virtual ~AppProvider() = default; - static std::unique_ptr CreateAppProvider(TMsgGetter msg_getter); + static AppProvider* GetInstance(TMsgGetter msg_getter = nullptr); virtual std::vector GetAppCandidates(const std::vector& filepaths) = 0; virtual std::vector GetMimeTypes() = 0; @@ -35,9 +37,13 @@ class AppProvider virtual std::vector GetPlatformSettings() { return {}; } virtual void SetPlatformSettings(const std::vector& settings) { } - virtual void LoadPlatformSettings() { } - virtual void SavePlatformSettings() { } + virtual void LoadPlatformSettings(const KeyFileReadHelper& key_reader) { } + virtual void SavePlatformSettings(KeyFileHelper& key_writer) { } + protected: TMsgGetter m_GetMsg; + +private: + static std::unique_ptr CreateAppProvider(TMsgGetter msg_getter); }; diff --git a/OpenWith/src/ExtensionMimeMap.cpp b/OpenWith/src/ExtensionMimeMap.cpp index fc3749f76..8d2d17b78 100644 --- a/OpenWith/src/ExtensionMimeMap.cpp +++ b/OpenWith/src/ExtensionMimeMap.cpp @@ -1,176 +1,176 @@ #if defined (__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) #include "XDGBasedAppProvider.hpp" -#include +#include #include -const std::unordered_map& XDGBasedAppProvider::GetExtMimeMap() +const std::unordered_map& XDGBasedAppProvider::GetExtMimeMap() { - static const std::unordered_map map = { + static const std::unordered_map map = { // Shell / scripts / source code - {".sh", "application/x-shellscript"}, - {".bash", "application/x-shellscript"}, - {".csh", "application/x-csh"}, - {".zsh", "application/x-shellscript"}, - {".ps1", "application/x-powershell"}, - {".py", "text/x-python"}, - {".pyw", "text/x-python"}, - {".pl", "text/x-perl"}, - {".pm", "text/x-perl"}, - {".rb", "text/x-ruby"}, - {".php", "application/x-php"}, - {".phps", "application/x-php"}, - {".js", "application/javascript"}, - {".mjs", "application/javascript"}, - {".java", "text/x-java-source"}, - {".c", "text/x-csrc"}, - {".h", "text/x-chdr"}, - {".cpp", "text/x-c++src"}, - {".cc", "text/x-c++src"}, - {".cxx", "text/x-c++src"}, - {".hpp", "text/x-c++hdr"}, - {".go", "text/x-go"}, - {".rs", "text/rust"}, - {".swift", "text/x-swift"}, + {"sh", "application/x-shellscript"}, + {"bash", "application/x-shellscript"}, + {"csh", "application/x-csh"}, + {"zsh", "application/x-shellscript"}, + {"ps1", "application/x-powershell"}, + {"py", "text/x-python"}, + {"pyw", "text/x-python"}, + {"pl", "text/x-perl"}, + {"pm", "text/x-perl"}, + {"rb", "text/x-ruby"}, + {"php", "application/x-php"}, + {"phps", "application/x-php"}, + {"js", "application/javascript"}, + {"mjs", "application/javascript"}, + {"java", "text/x-java-source"}, + {"c", "text/x-csrc"}, + {"h", "text/x-chdr"}, + {"cpp", "text/x-c++src"}, + {"cc", "text/x-c++src"}, + {"cxx", "text/x-c++src"}, + {"hpp", "text/x-c++hdr"}, + {"go", "text/x-go"}, + {"rs", "text/rust"}, + {"swift", "text/x-swift"}, // Plain text / markup / data - {".txt", "text/plain"}, - {".md", "text/markdown"}, - {".markdown","text/markdown"}, - {".tex", "application/x-tex"}, - {".csv", "text/csv"}, - {".tsv", "text/tab-separated-values"}, - {".log", "text/plain"}, - {".json", "application/json"}, - {".yaml", "text/yaml"}, - {".yml", "text/yaml"}, - {".xml", "application/xml"}, - {".html", "text/html"}, - {".htm", "text/html"}, - {".xhtml", "application/xhtml+xml"}, - {".ics", "text/calendar"}, + {"txt", "text/plain"}, + {"md", "text/markdown"}, + {"markdown","text/markdown"}, + {"tex", "application/x-tex"}, + {"csv", "text/csv"}, + {"tsv", "text/tab-separated-values"}, + {"log", "text/plain"}, + {"json", "application/json"}, + {"yaml", "text/yaml"}, + {"yml", "text/yaml"}, + {"xml", "application/xml"}, + {"html", "text/html"}, + {"htm", "text/html"}, + {"xhtml", "application/xhtml+xml"}, + {"ics", "text/calendar"}, // Office / documents - {".pdf", "application/pdf"}, - {".doc", "application/msword"}, - {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - {".xls", "application/vnd.ms-excel"}, - {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, - {".ppt", "application/vnd.ms-powerpoint"}, - {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, - {".odt", "application/vnd.oasis.opendocument.text"}, - {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, - {".odp", "application/vnd.oasis.opendocument.presentation"}, - {".epub", "application/epub+zip"}, - {".rtf", "application/rtf"}, + {"pdf", "application/pdf"}, + {"doc", "application/msword"}, + {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {"xls", "application/vnd.ms-excel"}, + {"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {"ppt", "application/vnd.ms-powerpoint"}, + {"pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {"odt", "application/vnd.oasis.opendocument.text"}, + {"ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {"odp", "application/vnd.oasis.opendocument.presentation"}, + {"epub", "application/epub+zip"}, + {"rtf", "application/rtf"}, // Images - {".jpg", "image/jpeg"}, - {".jpeg", "image/jpeg"}, - {".jpe", "image/jpeg"}, - {".png", "image/png"}, - {".gif", "image/gif"}, - {".webp", "image/webp"}, - {".svg", "image/svg+xml"}, - {".ico", "image/vnd.microsoft.icon"}, - {".bmp", "image/bmp"}, - {".tif", "image/tiff"}, - {".tiff", "image/tiff"}, - {".heic", "image/heic"}, - {".avif", "image/avif"}, - {".apng", "image/apng"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"jpe", "image/jpeg"}, + {"png", "image/png"}, + {"gif", "image/gif"}, + {"webp", "image/webp"}, + {"svg", "image/svg+xml"}, + {"ico", "image/vnd.microsoft.icon"}, + {"bmp", "image/bmp"}, + {"tif", "image/tiff"}, + {"tiff", "image/tiff"}, + {"heic", "image/heic"}, + {"avif", "image/avif"}, + {"apng", "image/apng"}, // Audio - {".mp3", "audio/mpeg"}, - {".m4a", "audio/mp4"}, - {".aac", "audio/aac"}, - {".ogg", "audio/ogg"}, - {".oga", "audio/ogg"}, - {".opus", "audio/opus"}, - {".wav", "audio/x-wav"}, - {".flac", "audio/flac"}, - {".mid", "audio/midi"}, - {".midi", "audio/midi"}, - {".weba", "audio/webm"}, + {"mp3", "audio/mpeg"}, + {"m4a", "audio/mp4"}, + {"aac", "audio/aac"}, + {"ogg", "audio/ogg"}, + {"oga", "audio/ogg"}, + {"opus", "audio/opus"}, + {"wav", "audio/x-wav"}, + {"flac", "audio/flac"}, + {"mid", "audio/midi"}, + {"midi", "audio/midi"}, + {"weba", "audio/webm"}, // Video - {".mp4", "video/mp4"}, - {".m4v", "video/mp4"}, - {".mov", "video/quicktime"}, - {".mkv", "video/x-matroska"}, - {".webm", "video/webm"}, - {".ogv", "video/ogg"}, - {".avi", "video/x-msvideo"}, - {".flv", "video/x-flv"}, - {".wmv", "video/x-ms-wmv"}, - {".3gp", "video/3gpp"}, - {".3g2", "video/3gpp2"}, - {".ts", "video/mp2t"}, + {"mp4", "video/mp4"}, + {"m4v", "video/mp4"}, + {"mov", "video/quicktime"}, + {"mkv", "video/x-matroska"}, + {"webm", "video/webm"}, + {"ogv", "video/ogg"}, + {"avi", "video/x-msvideo"}, + {"flv", "video/x-flv"}, + {"wmv", "video/x-ms-wmv"}, + {"3gp", "video/3gpp"}, + {"3g2", "video/3gpp2"}, + {"ts", "video/mp2t"}, // Archives / compressed - {".zip", "application/zip"}, - {".tar", "application/x-tar"}, - {".gz", "application/gzip"}, - {".tgz", "application/gzip"}, - {".bz", "application/x-bzip"}, - {".bz2", "application/x-bzip2"}, - {".xz", "application/x-xz"}, - {".7z", "application/x-7z-compressed"}, - {".rar", "application/vnd.rar"}, - {".jar", "application/java-archive"}, + {"zip", "application/zip"}, + {"tar", "application/x-tar"}, + {"gz", "application/gzip"}, + {"tgz", "application/gzip"}, + {"bz", "application/x-bzip"}, + {"bz2", "application/x-bzip2"}, + {"xz", "application/x-xz"}, + {"7z", "application/x-7z-compressed"}, + {"rar", "application/vnd.rar"}, + {"jar", "application/java-archive"}, // Executables / binaries - {".exe", "application/x-ms-dos-executable"}, - {".dll", "application/x-msdownload"}, - {".so", "application/x-sharedlib"}, - {".elf", "application/x-executable"}, - {".bin", "application/octet-stream"}, - {".class", "application/java-vm"}, + {"exe", "application/x-ms-dos-executable"}, + {"dll", "application/x-msdownload"}, + {"so", "application/x-sharedlib"}, + {"elf", "application/x-executable"}, + {"bin", "application/octet-stream"}, + {"class", "application/java-vm"}, // Fonts - {".ttf", "font/ttf"}, - {".otf", "font/otf"}, - {".woff", "font/woff"}, - {".woff2", "font/woff2"}, - {".eot", "application/vnd.ms-fontobject"}, + {"ttf", "font/ttf"}, + {"otf", "font/otf"}, + {"woff", "font/woff"}, + {"woff2", "font/woff2"}, + {"eot", "application/vnd.ms-fontobject"}, // PostScript / vector - {".ps", "application/postscript"}, - {".eps", "application/postscript"}, - {".ai", "application/postscript"}, + {"ps", "application/postscript"}, + {"eps", "application/postscript"}, + {"ai", "application/postscript"}, // Disk images / containers - {".iso", "application/x-iso9660-image"}, - {".img", "application/octet-stream"}, - {".dmg", "application/x-apple-diskimage"}, + {"iso", "application/x-iso9660-image"}, + {"img", "application/octet-stream"}, + {"dmg", "application/x-apple-diskimage"}, // Web / misc - {".css", "text/css"}, - {".map", "application/json"}, - {".wasm", "application/wasm"}, - {".jsonld","application/ld+json"}, - {".webmanifest","application/manifest+json"}, + {"css", "text/css"}, + {"map", "application/json"}, + {"wasm", "application/wasm"}, + {"jsonld","application/ld+json"}, + {"webmanifest","application/manifest+json"}, // CAD / specialized - {".dxf", "image/vnd.dxf"}, - {".dwg", "application/acad"}, + {"dxf", "image/vnd.dxf"}, + {"dwg", "application/acad"}, // Mail / office miscellany - {".msg", "application/vnd.ms-outlook"} + {"msg", "application/vnd.ms-outlook"} }; return map; } diff --git a/OpenWith/src/MacOSAppProvider.hpp b/OpenWith/src/MacOSAppProvider.hpp index fc3a4474a..a550b13da 100644 --- a/OpenWith/src/MacOSAppProvider.hpp +++ b/OpenWith/src/MacOSAppProvider.hpp @@ -9,6 +9,9 @@ #include #include +class KeyFileReadHelper; +class KeyFileHelper; + class MacOSAppProvider : public AppProvider { public: @@ -19,8 +22,8 @@ class MacOSAppProvider : public AppProvider std::vector GetCandidateDetails(const CandidateInfo& candidate) override; std::vector GetPlatformSettings() override { return {}; } void SetPlatformSettings(const std::vector& settings) override {} - void LoadPlatformSettings() override {} - void SavePlatformSettings() override {} + void LoadPlatformSettings(const KeyFileReadHelper &key_reader) override {} + void SavePlatformSettings(KeyFileHelper& key_writer) override {} private: // A struct to cache the results of a file type query. diff --git a/OpenWith/src/MacOSAppProvider.mm b/OpenWith/src/MacOSAppProvider.mm index b0d96e811..5ea4b1c71 100644 --- a/OpenWith/src/MacOSAppProvider.mm +++ b/OpenWith/src/MacOSAppProvider.mm @@ -88,14 +88,15 @@ static MacCandidateTempInfo AppBundleToTempInfo(NSURL *appURL) { // To optimize performance when handling many files, this function caches the application lists // based on the file's Uniform Type Identifier (UTI). std::vector MacOSAppProvider::GetAppCandidates(const std::vector& filepaths) { + // Clear the class-level profile cache for the new operation + // MUST be done before any early returns to ensure Singleton safety. + _last_uti_profiles.clear(); + // Return immediately if the input vector is empty. if (filepaths.empty()) { return {}; } - // Clear the class-level profile cache for the new operation - _last_uti_profiles.clear(); - // --- Part 1: Candidate Discovery and Scoring with Caching --- // A map to store definitive metadata for every unique app encountered. diff --git a/OpenWith/src/OpenWith.cpp b/OpenWith/src/OpenWith.cpp index a0427cf3b..8a79b3f9f 100644 --- a/OpenWith/src/OpenWith.cpp +++ b/OpenWith/src/OpenWith.cpp @@ -1,11 +1,12 @@ #include "OpenWith.hpp" #include "AppProvider.hpp" -#include "farplug-wide.h" -#include "KeyFileHelper.h" -#include "WinCompat.h" #include "lng.hpp" #include "common.hpp" +#include "WideMB.h" #include "utils.h" +#include "farplug-wide.h" +#include "KeyFileHelper.h" +#include "WinCompat.h" #include #include #include @@ -13,11 +14,11 @@ #include #include -#define INI_LOCATION InMyConfig("plugins/openwith/config.ini") -#define INI_SECTION "Settings" - namespace OpenWith { +constexpr const char* INI_FILEPATH = "plugins/openwith/config.ini"; +constexpr const char* INI_SECTION_GENERAL = "Settings"; + // ****************************** Public API ****************************** @@ -25,7 +26,7 @@ namespace OpenWith { // displays the selection menu, and handles user actions: F3 (details), F9 (settings), (Shift+)Enter (launch). void OpenWithPlugin::ProcessFiles(const std::vector& filepaths, const std::wstring& base_path) { - auto provider = AppProvider::CreateAppProvider(&GetMsg); + auto* provider = AppProvider::GetInstance(); if (!provider) { ShowError({ GetMsg(MUnsupportedPlatform) }); return; @@ -54,53 +55,59 @@ void OpenWithPlugin::ProcessFiles(const std::vector& filepaths, co active_menu_idx = 0; } - constexpr int BREAK_KEYS[] = {VK_F3, VK_F9, MAKELONG(VK_RETURN, PKF_SHIFT), 0}; - constexpr int F3_DETAILS = 0, F9_SETTINGS = 1, SHIFT_ENTER_ALT_LAUNCH = 2; + constexpr int BREAK_KEYS[] = { VK_F3, VK_F9, MAKELONG(VK_RETURN, PKF_SHIFT), 0 }; + enum class MenuAction : int { DETAILS, SETTINGS, FORCED_LAUNCH, LAUNCH = -1 }; int menu_break_code = -1; // Display the menu and get the user's selection. menu_items[active_menu_idx].Selected = true; - int selected_menu_idx = g_info.Menu(g_info.ModuleNumber, -1, -1, 0, FMENU_WRAPMODE | FMENU_SHOWAMPERSAND | FMENU_CHANGECONSOLETITLE, - GetMsg(MChooseApplication), L"F3 F9 Ctrl+Alt+F", L"Contents", BREAK_KEYS, &menu_break_code, + const int selected_menu_idx = g_info.Menu(g_info.ModuleNumber, -1, -1, 0, FMENU_WRAPMODE | FMENU_SHOWAMPERSAND | FMENU_CHANGECONSOLETITLE, + FormatMenuTitle(filepaths).c_str(), L" Enter Shift+Enter F3 F9 Ctrl+Alt+F ", L"Contents", BREAK_KEYS, &menu_break_code, menu_items.data(), static_cast(menu_items.size())); if (selected_menu_idx == -1) { - return; // User cancelled the menu; exit the plugin. + return; // User cancelled the menu (Esc/F10); exit the plugin. } menu_items[active_menu_idx].Selected = false; active_menu_idx = selected_menu_idx; const auto& selected_app = (*app_candidates)[selected_menu_idx]; - if (menu_break_code == F3_DETAILS) { - const auto mime_profiles = provider->GetMimeTypes(); - const auto app_info = provider->GetCandidateDetails(selected_app); - const auto cmds = provider->ConstructLaunchCommands(selected_app, filepaths); - // Repeat until user either launches the application or closes the dialog to go back. - while (true) { - if (ShowDetailsDlg(filepaths, mime_profiles, app_info, cmds) == DetailsDlgResult::Launch) { - if (AskForLaunchConfirmation(selected_app, filepaths.size())) { - LaunchApplication(selected_app, cmds); - return; // Application launched; exit the plugin. + switch (static_cast(menu_break_code)) { + + case MenuAction::DETAILS: { + const auto mime_profiles = provider->GetMimeTypes(); + const auto app_info = provider->GetCandidateDetails(selected_app); + const auto cmds = provider->ConstructLaunchCommands(selected_app, filepaths); + // Repeat until user either launches the application or closes the dialog to go back. + while (true) { + if (ShowDetailsDlg(filepaths, mime_profiles, app_info, cmds) == DetailsDlgResult::Launch) { + if (AskForLaunchConfirmation(selected_app, filepaths.size())) { + LaunchApplication(selected_app, cmds); + return; // Application launched; exit the plugin. + } + } else { + break; // User clicked "Close"; return to the main menu. } - } else { - break; // User clicked "Close"; return to the main menu. } + break; } - } else if (menu_break_code == F9_SETTINGS) { - auto config_result = ShowConfigDlg(); - if (config_result.is_platform_settings_changed) { - provider->LoadPlatformSettings(); - } - if (config_result.should_refresh_candidates) { - app_candidates.reset(); + case MenuAction::SETTINGS: { + auto config_result = ShowConfigDlg(); + if (config_result.should_refresh_candidates) { + app_candidates.reset(); + } + break; } - } else { // Enter or Shift+Enter to launch. - if (AskForLaunchConfirmation(selected_app, filepaths.size())) { - const auto cmds = provider->ConstructLaunchCommands(selected_app, filepaths); - LaunchApplication(selected_app, cmds, - (menu_break_code == SHIFT_ENTER_ALT_LAUNCH) ? LaunchMode::Alternative : LaunchMode::Standard); - return; // Application launched; exit the plugin. + case MenuAction::LAUNCH: + case MenuAction::FORCED_LAUNCH: { + if (AskForLaunchConfirmation(selected_app, filepaths.size())) { + const auto cmds = provider->ConstructLaunchCommands(selected_app, filepaths); + LaunchApplication(selected_app, cmds, + (static_cast(menu_break_code) == MenuAction::FORCED_LAUNCH) ? LaunchMode::Forced : LaunchMode::Standard); + return; // Application launched; exit the plugin. + } + break; } } } @@ -112,15 +119,12 @@ void OpenWithPlugin::ProcessFiles(const std::vector& filepaths, co // and platform-specific settings fetched dynamically from the current AppProvider. OpenWithPlugin::ConfigDlgResult OpenWithPlugin::ShowConfigDlg() { - // Create a temporary provider to access platform-specific settings (they are loaded automatically). - auto provider = AppProvider::CreateAppProvider(&GetMsg); + auto* provider = AppProvider::GetInstance(); if (!provider) { ShowError({ GetMsg(MUnsupportedPlatform) }); return {}; } - LoadGeneralSettings(); - constexpr int CONFIG_DIALOG_WIDTH = 70; const bool old_use_external_terminal = s_use_external_terminal; @@ -162,10 +166,12 @@ OpenWithPlugin::ConfigDlgResult OpenWithPlugin::ShowConfigDlg() FarDialogItem confirm_launch_chkbx = { DI_CHECKBOX, 5, current_y, 0, current_y, FALSE, {}, DIF_NONE, FALSE, confirm_launch_label, 0 }; confirm_launch_chkbx.Param.Selected = s_confirm_launch; - auto confirm_launch_chkbx_idx = add_item(confirm_launch_chkbx); - auto confirm_launch_edit_idx = add_item({ DI_FIXEDIT, confirm_launch_label_width + 10, current_y, confirm_launch_label_width + 13, current_y, FALSE, {(DWORD_PTR)L"9999"}, DIF_MASKEDIT, FALSE, threshold_str.c_str(), 0 }); + auto confirm_launch_chkbx_idx = add_item(confirm_launch_chkbx); + auto confirm_launch_edit_idx = add_item({ DI_FIXEDIT, confirm_launch_label_width + 10, current_y, confirm_launch_label_width + 13, current_y, FALSE, {(DWORD_PTR)L"9999"}, DIF_MASKEDIT, FALSE, threshold_str.c_str(), 0 }); current_y++; + auto display_filename_idx = add_checkbox(GetMsg(MDisplayFilename), s_display_filename); + // ----- Add platform-specific settings. ----- std::vector> dynamic_settings; dynamic_settings.reserve(old_platform_settings.size()); @@ -184,21 +190,21 @@ OpenWithPlugin::ConfigDlgResult OpenWithPlugin::ShowConfigDlg() int config_dialog_height = current_y + 3; config_dialog_items[0].Y2 = config_dialog_height - 2; - HANDLE dlg = g_info.DialogInit(g_info.ModuleNumber, -1, -1, CONFIG_DIALOG_WIDTH, config_dialog_height, L"ConfigurationDialog", - config_dialog_items.data(), static_cast(config_dialog_items.size()), 0, 0, nullptr, 0); - bool is_platform_settings_changed = false; bool is_platform_settings_requiring_candidate_list_refresh_changed = false; - if (dlg != INVALID_HANDLE_VALUE) { + HANDLE config_dlg = g_info.DialogInit(g_info.ModuleNumber, -1, -1, CONFIG_DIALOG_WIDTH, config_dialog_height, L"ConfigurationDialog", + config_dialog_items.data(), static_cast(config_dialog_items.size()), 0, 0, nullptr, 0); + + if (config_dlg != INVALID_HANDLE_VALUE) { - int exit_code = g_info.DialogRun(dlg); + int exit_code = g_info.DialogRun(config_dlg); // ----- Process results if "OK" was pressed. ----- if (exit_code == static_cast(ok_btn_idx)) { - auto is_checked = [&dlg](size_t i) -> bool { - return g_info.SendDlgMessage(dlg, DM_GETCHECK, i, 0) == BSTATE_CHECKED; + auto is_checked = [&config_dlg](size_t i) -> bool { + return g_info.SendDlgMessage(config_dlg, DM_GETCHECK, i, 0) == BSTATE_CHECKED; }; s_use_external_terminal = is_checked(use_external_terminal_idx); @@ -206,17 +212,21 @@ OpenWithPlugin::ConfigDlgResult OpenWithPlugin::ShowConfigDlg() s_clear_selection = is_checked(clear_selection_idx); s_confirm_launch = is_checked(confirm_launch_chkbx_idx); - auto threshold_str = (const wchar_t*)g_info.SendDlgMessage(dlg, DM_GETCONSTTEXTPTR, confirm_launch_edit_idx, 0); + auto threshold_str = (const wchar_t*)g_info.SendDlgMessage(config_dlg, DM_GETCONSTTEXTPTR, confirm_launch_edit_idx, 0); s_confirm_launch_threshold = wcstol(threshold_str, nullptr, 10); + s_confirm_launch_threshold = std::clamp(s_confirm_launch_threshold, 0, 9999); + + s_display_filename = is_checked(display_filename_idx); - SaveGeneralSettings(); + KeyFileHelper key_writer(InMyConfig(INI_FILEPATH)); + SaveGeneralSettings(key_writer); // Propagate changes to dynamic platform-specific settings back to the provider. if (!dynamic_settings.empty()) { std::vector new_platform_settings; new_platform_settings.reserve(dynamic_settings.size()); - for (const auto& [idx, setting] : dynamic_settings) { + for (auto& [idx, setting] : dynamic_settings) { bool new_value = is_checked(idx); if (setting.value != new_value) { if (setting.affects_candidates) { @@ -224,20 +234,24 @@ OpenWithPlugin::ConfigDlgResult OpenWithPlugin::ShowConfigDlg() } is_platform_settings_changed = true; } - new_platform_settings.push_back({ setting.internal_key, setting.display_name, new_value }); + new_platform_settings.push_back(std::move(setting)); + new_platform_settings.back().value = new_value; } if (is_platform_settings_changed) { provider->SetPlatformSettings(new_platform_settings); - provider->SavePlatformSettings(); } + provider->SavePlatformSettings(key_writer); + } + + if (!key_writer.Save(true)) { + ShowError({GetMsg(MSaveConfigError), StrMB2Wide(InMyConfig(INI_FILEPATH))}); } } - g_info.DialogFree(dlg); + g_info.DialogFree(config_dlg); } - return { is_platform_settings_changed, - (is_platform_settings_requiring_candidate_list_refresh_changed || (old_use_external_terminal != s_use_external_terminal)) }; + return { is_platform_settings_requiring_candidate_list_refresh_changed || (old_use_external_terminal != s_use_external_terminal) }; } @@ -262,14 +276,14 @@ void OpenWithPlugin::ShowError(const std::vector& error_lines) // and the built‑in far2l terminal is used, since multiple concurrent instances cannot be managed. void OpenWithPlugin::FilterOutTerminalCandidates(std::vector &candidates, size_t file_count) { - if (file_count > 1 && !s_use_external_terminal) { - candidates.erase( - std::remove_if(candidates.begin(), candidates.end(), - [](const CandidateInfo& c) { - return c.terminal && !c.multi_file_aware; - }), - candidates.end()); - } + if (file_count <= 1 || s_use_external_terminal) return; + + auto should_remove_candidate = [](const CandidateInfo& c) { + return c.terminal && !c.multi_file_aware; + }; + + candidates.erase(std::remove_if(candidates.begin(), candidates.end(), should_remove_candidate), + candidates.end()); } @@ -298,13 +312,12 @@ void OpenWithPlugin::LaunchApplication(const CandidateInfo& app, const std::vect unsigned int execute_flags = 0; if (app.terminal) { - if (s_use_external_terminal || (launch_mode == LaunchMode::Alternative)) { + if (s_use_external_terminal || (launch_mode == LaunchMode::Forced)) { execute_flags |= EF_EXTERNALTERM; } } else { // If we have multiple commands to run, force asynchronous execution to avoid UI blocking. - bool force_no_wait = cmds.size() > 1; - if (s_no_wait_for_command_completion || force_no_wait || (launch_mode == LaunchMode::Alternative)) { + if (s_no_wait_for_command_completion || (launch_mode == LaunchMode::Forced) || (cmds.size() > 1)) { execute_flags |= (EF_NOWAIT | EF_HIDEOUT); } } @@ -331,14 +344,13 @@ OpenWithPlugin::DetailsDlgResult OpenWithPlugin::ShowDetailsDlg(const std::vecto const std::vector& cmds) { std::vector file_info; - // For a single file, show its full path. For multiple files, show a summary count. - if (filepaths.size() == 1) { - file_info.push_back({ GetMsg(MPathname), filepaths[0] }); - } else { - auto count_msg = std::wstring(GetMsg(MFilesSelected)) + std::to_wstring(filepaths.size()); - file_info.push_back({ GetMsg(MPathname), count_msg }); + auto file_count = filepaths.size(); + if (file_count != 1) { + file_info.push_back({ GetMsg(MFilesSelected), std::to_wstring(file_count)}); } + file_info.push_back({ GetMsg(MFilepaths), JoinStrings(filepaths, L"; ") }); file_info.push_back({ GetMsg(MMimeProfile), JoinStrings(unique_mime_profiles, L"; ") }); + Field launch_command { GetMsg(MLaunchCommand), JoinStrings(cmds, L"; ") }; constexpr int DETAILS_DIALOG_MIN_WIDTH = 40; @@ -394,12 +406,12 @@ OpenWithPlugin::DetailsDlgResult OpenWithPlugin::ShowDetailsDlg(const std::vecto details_dialog_items.push_back({ DI_BUTTON, 0, current_y, 0, current_y, FALSE, {}, DIF_CENTERGROUP, 0, GetMsg(MLaunch), 0 }); const int launch_btn_idx = static_cast(details_dialog_items.size()) - 1; - HANDLE dlg = g_info.DialogInit(g_info.ModuleNumber, -1, -1, details_dialog_width, details_dialog_height, L"DetailsDialog", + HANDLE details_dlg = g_info.DialogInit(g_info.ModuleNumber, -1, -1, details_dialog_width, details_dialog_height, L"DetailsDialog", details_dialog_items.data(), static_cast(details_dialog_items.size()), 0, 0, nullptr, 0); - if (dlg != INVALID_HANDLE_VALUE) { - int exit_code = g_info.DialogRun(dlg); - g_info.DialogFree(dlg); + if (details_dlg != INVALID_HANDLE_VALUE) { + int exit_code = g_info.DialogRun(details_dlg); + g_info.DialogFree(details_dlg); if (exit_code == launch_btn_idx) { return DetailsDlgResult::Launch; } @@ -410,32 +422,28 @@ OpenWithPlugin::DetailsDlgResult OpenWithPlugin::ShowDetailsDlg(const std::vecto // Loads platform-independent configuration from the INI file. -void OpenWithPlugin::LoadGeneralSettings() +void OpenWithPlugin::LoadGeneralSettings(const KeyFileReadHelper& key_reader) { - KeyFileReadSection kfh(INI_LOCATION, INI_SECTION); - s_use_external_terminal = kfh.GetInt("UseExternalTerminal", 0) != 0; - s_no_wait_for_command_completion = kfh.GetInt("NoWaitForCommandCompletion", 1) != 0; - s_clear_selection = kfh.GetInt("ClearSelection", 0) != 0; - s_confirm_launch = kfh.GetInt("ConfirmLaunch", 1) != 0; - s_confirm_launch_threshold = kfh.GetInt("ConfirmLaunchThreshold", 10); + s_use_external_terminal = key_reader.GetInt(INI_SECTION_GENERAL, "UseExternalTerminal", 0) != 0; + s_no_wait_for_command_completion = key_reader.GetInt(INI_SECTION_GENERAL, "NoWaitForCommandCompletion", 1) != 0; + s_clear_selection = key_reader.GetInt(INI_SECTION_GENERAL, "ClearSelection", 0) != 0; + s_confirm_launch = key_reader.GetInt(INI_SECTION_GENERAL, "ConfirmLaunch", 1) != 0; + s_confirm_launch_threshold = key_reader.GetInt(INI_SECTION_GENERAL, "ConfirmLaunchThreshold", 10); s_confirm_launch_threshold = std::clamp(s_confirm_launch_threshold, 0, 9999); + s_display_filename = key_reader.GetInt(INI_SECTION_GENERAL, "DisplayFilename", 0) != 0; } // Saves current platform-independent configuration to the INI file. -void OpenWithPlugin::SaveGeneralSettings() +void OpenWithPlugin::SaveGeneralSettings(KeyFileHelper& key_writer) { - KeyFileHelper kfh(INI_LOCATION); - kfh.SetInt(INI_SECTION, "UseExternalTerminal", s_use_external_terminal); - kfh.SetInt(INI_SECTION, "NoWaitForCommandCompletion", s_no_wait_for_command_completion); - kfh.SetInt(INI_SECTION, "ClearSelection", s_clear_selection); - kfh.SetInt(INI_SECTION, "ConfirmLaunch", s_confirm_launch); - s_confirm_launch_threshold = std::clamp(s_confirm_launch_threshold, 0, 9999); - kfh.SetInt(INI_SECTION, "ConfirmLaunchThreshold", s_confirm_launch_threshold); - if (!kfh.Save()) { - ShowError({ GetMsg(MSaveConfigError) }); - } + key_writer.SetInt(INI_SECTION_GENERAL, "UseExternalTerminal", s_use_external_terminal); + key_writer.SetInt(INI_SECTION_GENERAL, "NoWaitForCommandCompletion", s_no_wait_for_command_completion); + key_writer.SetInt(INI_SECTION_GENERAL, "ClearSelection", s_clear_selection); + key_writer.SetInt(INI_SECTION_GENERAL, "ConfirmLaunch", s_confirm_launch); + key_writer.SetInt(INI_SECTION_GENERAL, "ConfirmLaunchThreshold", s_confirm_launch_threshold); + key_writer.SetInt(INI_SECTION_GENERAL, "DisplayFilename", s_display_filename); } @@ -490,6 +498,39 @@ int OpenWithPlugin::GetScreenWidth() +// Returns either a dynamic title (screen-fitted filename or file count) or a static default one depending on the configuration. +std::wstring OpenWithPlugin::FormatMenuTitle(const std::vector& filepaths) +{ + if (!s_display_filename) { + return GetMsg(MChooseApplication); + } + + std::wstring title = GetMsg(MOpenWithFor); + + if (filepaths.size() != 1) { + title += std::to_wstring(filepaths.size()) + GetMsg(MFile_s); + return title; + } + + auto filename = ExtractFileName(filepaths.front()); + + constexpr int menu_ui_overhead_cells = 1 + 4 + 1 + 1 + 4 + 1; + + const int title_cells = static_cast(g_fsf.StrCellsCount(title.c_str(), title.size())); + const int max_filename_cells = std::max(1, GetScreenWidth() - title_cells - menu_ui_overhead_cells); + + g_fsf.TruncStr(filename.data(), max_filename_cells); + filename.resize(wcslen(filename.c_str())); + + title += L'"'; + title += filename; + title += L'"'; + + return title; +} + + + // Retrieves a localized message string from the language file by its ID. const wchar_t* GetMsg(int msg_id) { @@ -497,13 +538,6 @@ const wchar_t* GetMsg(int msg_id) } -// ****************************** Static member initialization. ****************************** - -bool OpenWithPlugin::s_use_external_terminal = false; -bool OpenWithPlugin::s_no_wait_for_command_completion = true; -bool OpenWithPlugin::s_clear_selection = false; -bool OpenWithPlugin::s_confirm_launch = true; -int OpenWithPlugin::s_confirm_launch_threshold = 10; // ****************************** Plugin entry points ****************************** @@ -515,7 +549,12 @@ SHAREDSYMBOL void WINAPI SetStartupInfoW(const struct PluginStartupInfo *Info) g_info = *Info; g_fsf = *Info->FSF; g_info.FSF = &g_fsf; - OpenWithPlugin::LoadGeneralSettings(); + + KeyFileReadHelper key_reader(InMyConfig(INI_FILEPATH)); + OpenWithPlugin::LoadGeneralSettings(key_reader); + if (auto provider = AppProvider::GetInstance(&GetMsg)) { + provider->LoadPlatformSettings(key_reader); + } } diff --git a/OpenWith/src/OpenWith.hpp b/OpenWith/src/OpenWith.hpp index 5536d6a94..8b2a66e6a 100644 --- a/OpenWith/src/OpenWith.hpp +++ b/OpenWith/src/OpenWith.hpp @@ -1,5 +1,6 @@ #pragma once +#include "KeyFileHelper.h" #include "farplug-wide.h" #include "common.hpp" #include @@ -18,41 +19,42 @@ class OpenWithPlugin struct ConfigDlgResult { - bool is_platform_settings_changed = false; bool should_refresh_candidates = false; }; static void ProcessFiles(const std::vector& filepaths, const std::wstring& base_path); static ConfigDlgResult ShowConfigDlg(); static void ShowError(const std::vector& error_lines); - static void LoadGeneralSettings(); + static void LoadGeneralSettings(const KeyFileReadHelper& key_reader); private: enum class DetailsDlgResult { Close, Launch }; - static bool s_use_external_terminal; - static bool s_no_wait_for_command_completion; - static bool s_clear_selection; - static bool s_confirm_launch; - static int s_confirm_launch_threshold; + inline static bool s_use_external_terminal; + inline static bool s_no_wait_for_command_completion; + inline static bool s_clear_selection; + inline static bool s_confirm_launch; + inline static int s_confirm_launch_threshold; + inline static bool s_display_filename; static void FilterOutTerminalCandidates(std::vector &candidates, size_t file_count); static bool AskForLaunchConfirmation(const CandidateInfo& app, size_t file_count); enum class LaunchMode { - Standard, // Enter - Alternative // Shift+Enter + Standard, // Enter + Forced // Shift+Enter }; static void LaunchApplication(const CandidateInfo& app, const std::vector& cmds, LaunchMode launch_mode = LaunchMode::Standard); static DetailsDlgResult ShowDetailsDlg(const std::vector& filepaths, const std::vector& unique_mime_profiles, const std::vector &application_info, const std::vector& cmds); - static void SaveGeneralSettings(); + static void SaveGeneralSettings(KeyFileHelper& key_writer); static std::wstring JoinStrings(const std::vector& vec, const std::wstring& delimiter); static size_t GetLabelCellWidth(const Field& field); static size_t GetMaxLabelCellWidth(const std::vector& fields); static int GetScreenWidth(); + static std::wstring FormatMenuTitle(const std::vector& filepaths); }; diff --git a/OpenWith/src/XDGBasedAppProvider.cpp b/OpenWith/src/XDGBasedAppProvider.cpp index 9dd4748de..e397c143d 100644 --- a/OpenWith/src/XDGBasedAppProvider.cpp +++ b/OpenWith/src/XDGBasedAppProvider.cpp @@ -3,9 +3,9 @@ #include "AppProvider.hpp" #include "XDGBasedAppProvider.hpp" #include "lng.hpp" +#include "common.hpp" #include "KeyFileHelper.h" #include "WideMB.h" -#include "common.hpp" #include "utils.h" #include #include @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -23,11 +25,7 @@ #include #include - -#define INI_LOCATION_XDG InMyConfig("plugins/openwith/config.ini") -#define INI_SECTION_XDG "Settings.XDG" - - +constexpr const char* INI_SECTION_XDG_PROVIDER = "Settings.XDG"; // ****************************** Public API ****************************** @@ -46,35 +44,32 @@ XDGBasedAppProvider::XDGBasedAppProvider(TMsgGetter msg_getter) : AppProvider(st { "UseGenericMimeFallbacks", MUseGenericMimeFallbacks, &XDGBasedAppProvider::_use_generic_mime_fallbacks, true, true }, { "ShowUniversalHandlers", MShowUniversalHandlers, &XDGBasedAppProvider::_show_universal_handlers, true, true }, { "UseMimeinfoCache", MUseMimeinfoCache, &XDGBasedAppProvider::_use_mimeinfo_cache, true, true }, - { "FilterByShowIn", MFilterByShowIn, &XDGBasedAppProvider::_filter_by_show_in, false, true }, - { "ValidateTryExec", MValidateTryExec, &XDGBasedAppProvider::_validate_try_exec, false, true }, + { "FilterByShowIn", MFilterByShowIn, &XDGBasedAppProvider::_filter_by_show_in, true, true }, + { "ValidateTryExec", MValidateTryExec, &XDGBasedAppProvider::_validate_try_exec, true, true }, { "SortAlphabetically", MSortAlphabetically, &XDGBasedAppProvider::_sort_alphabetically, false, true }, { "TreatUrlsAsPaths", MTreatUrlsAsPaths, &XDGBasedAppProvider::_treat_urls_as_paths, false, false }, { "ShowPackageTags", MShowPackageTags, &XDGBasedAppProvider::_show_package_tags, true, true } }; for (const auto& def : _platform_settings_definitions) { - _key_wide_to_member_map[StrMB2Wide(def.key)] = def.member_variable; + _key_wide_to_member_map[StrMB2Wide(def.internal_key)] = def.member_variable; } } -void XDGBasedAppProvider::LoadPlatformSettings() +void XDGBasedAppProvider::LoadPlatformSettings(const KeyFileReadHelper &key_reader) { - KeyFileReadSection key_file_reader(INI_LOCATION_XDG, INI_SECTION_XDG); for (const auto& def : _platform_settings_definitions) { - this->*(def.member_variable) = key_file_reader.GetInt(def.key.c_str(), def.default_value) != 0; + this->*(def.member_variable) = key_reader.GetInt(INI_SECTION_XDG_PROVIDER, def.internal_key.c_str(), def.default_value) != 0; } } -void XDGBasedAppProvider::SavePlatformSettings() +void XDGBasedAppProvider::SavePlatformSettings(KeyFileHelper& key_writer) { - KeyFileHelper key_file_helper(INI_LOCATION_XDG); for (const auto& def : _platform_settings_definitions) { - key_file_helper.SetInt(INI_SECTION_XDG, def.key.c_str(), this->*(def.member_variable)); + key_writer.SetInt(INI_SECTION_XDG_PROVIDER, def.internal_key.c_str(), this->*(def.member_variable)); } - key_file_helper.Save(); } @@ -86,14 +81,14 @@ std::vector XDGBasedAppProvider::GetPlatformSettings() // Check if this setting is linked to a command-line tool via its internal string key. bool is_disabled = false; - if (auto it = s_tool_key_map.find(def.key); it != s_tool_key_map.end()) { + if (auto it = s_tool_key_map.find(def.internal_key); it != s_tool_key_map.end()) { // If a corresponding tool name is found, check for the executable's existence. // The option is disabled if the tool is not found on the system. const std::string& tool_name = it->second; is_disabled = !IsExecutableAvailable(tool_name); } - settings.push_back({StrMB2Wide(def.key), m_GetMsg(def.display_name_id), this->*(def.member_variable), + settings.push_back({StrMB2Wide(def.internal_key), m_GetMsg(def.display_name_id), this->*(def.member_variable), is_disabled, def.affects_candidates}); } return settings; @@ -229,52 +224,45 @@ std::vector XDGBasedAppProvider::ConstructLaunchCommands(const Can } const std::string desktop_id = StrWide2MB(candidate.id); + auto it = _desktop_id_to_desktop_entry_cache.find(desktop_id); - if (auto it = _desktop_id_to_desktop_entry_cache.find(desktop_id); - it == _desktop_id_to_desktop_entry_cache.end() || !it->second.has_value()) { + if (it == _desktop_id_to_desktop_entry_cache.end() || !it->second.has_value()) { return {}; - } else { - const DesktopEntry& desktop_entry = it->second.value(); - - if (desktop_entry.exec.empty()) { - return {}; - } - - AnalyzeExecLine(desktop_entry); - - if (desktop_entry.arg_templates.empty()) { - return {}; - } + } - std::vector filepaths; - filepaths.reserve(filepaths_wide.size()); - for (const auto& filepath_wide : filepaths_wide) { - filepaths.push_back(StrWide2MB(filepath_wide)); + const DesktopEntry& desktop_entry = it->second.value(); + if (desktop_entry.exec.empty()) { + return {}; + } + AnalyzeExecLine(desktop_entry); + if (desktop_entry.arg_templates.empty()) { + return {}; + } + std::vector filepaths; + filepaths.reserve(filepaths_wide.size()); + for (const auto& filepath_wide : filepaths_wide) { + filepaths.push_back(StrWide2MB(filepath_wide)); + } + std::vector launch_commands_wide; + auto add_command_from_batch = [&](const std::vector& batch) { + auto command = AssembleLaunchCommand(desktop_entry, batch); + if (!command.empty()) { + launch_commands_wide.push_back(StrMB2Wide(command)); } - - std::vector launch_commands_wide; - - auto add_command_from_batch = [&](const std::vector& batch) { - auto command = AssembleLaunchCommand(desktop_entry, batch); - if (!command.empty()) { - launch_commands_wide.push_back(StrMB2Wide(command)); - } - }; - - if (desktop_entry.execution_model == ExecutionModel::PerFile) { - // The application uses %f or %u field codes and accepts only one file per invocation. - // Generate a separate command line for each selected file. - launch_commands_wide.reserve(filepaths.size()); - for (const auto& filepath : filepaths) { - add_command_from_batch({ filepath }); - } - } else { - // The application uses %F, %U, or has no field codes (Legacy). - // It accepts the entire list of files in a single invocation. - add_command_from_batch(filepaths); + }; + if (desktop_entry.execution_model == ExecutionModel::PerFile) { + // The application uses %f or %u field codes and accepts only one file per invocation. + // Generate a separate command line for each selected file. + launch_commands_wide.reserve(filepaths.size()); + for (const auto& filepath : filepaths) { + add_command_from_batch({ filepath }); } - return launch_commands_wide; + } else { + // The application uses %F, %U, or has no field codes (Legacy). + // It accepts the entire list of files in a single invocation. + add_command_from_batch(filepaths); } + return launch_commands_wide; } @@ -632,9 +620,9 @@ bool XDGBasedAppProvider::IsAssociationRemoved(const std::string& mime, const st // 2. Check for a wildcard match (e.g., "image/*") // Some implementations or users might ban an app from handling an entire category of types. - const size_t slash_pos = mime.find('/'); - if (slash_pos != std::string::npos) { - const std::string wildcard_mime = mime.substr(0, slash_pos) + "/*"; + const auto major = GetMajorMimeType(mime); + if (!major.empty()) { + const std::string wildcard_mime = std::string(major) + "/*"; auto it_wildcard = _op_mimeapps_lists_cache.removed.find(wildcard_mime); if (it_wildcard != _op_mimeapps_lists_cache.removed.end() && it_wildcard->second.count(desktop_id)) { return true; @@ -968,12 +956,12 @@ bool XDGBasedAppProvider::GlobMatch(const std::string &text, const std::string & } -std::string XDGBasedAppProvider::GuessMimeTypeByExtension(const std::string& filepath) +std::string_view XDGBasedAppProvider::GuessMimeTypeByExtension(const std::string& filepath) { auto filename = GetBaseName(filepath); auto dot_pos = filename.rfind('.'); - if (dot_pos != std::string::npos) { - std::string ext = ToLowerASCII(filename.substr(dot_pos)); + if (dot_pos != std::string::npos && dot_pos > 0 && dot_pos + 1 < filename.size()) { + std::string ext = ToLowerASCII(filename.substr(dot_pos + 1)); const auto& map = GetExtMimeMap(); auto it = map.find(ext); if (it != map.end()) { @@ -1241,7 +1229,7 @@ void XDGBasedAppProvider::ParseMimeappsList(const std::string& filepath, Mimeapp } } else if (current_section == "[Added Associations]") { for (const auto& desktop_id : desktop_ids) { - if (blacklist.find(desktop_id) == blacklist.end()) { + if (!blacklist.count(desktop_id)) { mimeapps_lists.added[mime].push_back(DesktopAssociation(desktop_id, filepath)); } } @@ -1408,7 +1396,7 @@ std::vector XDGBasedAppProvider::GenerateLocaleSuffixes() if (size_t dot = locale.find('.'); dot != std::string::npos) { size_t at = locale.find('@'); if (at != std::string::npos && at > dot) { - locale.replace(dot, at - dot, ""); // Remove encoding between '.' and '@' + locale.erase(dot, at - dot); // Remove encoding between '.' and '@' } else { locale.resize(dot); // Remove trailing encoding } @@ -1448,7 +1436,7 @@ std::unordered_map XDGBasedAppProvider::LoadMimeAliase std::string line; while (std::getline(file, line)) { - line = XDGBasedAppProvider::Trim(line); + line = Trim(line); if (line.empty() || line[0] == '#') continue; std::stringstream ss(line); @@ -1566,7 +1554,7 @@ void XDGBasedAppProvider::ParseGlobs2File(const std::string& filepath, std::vect int weight = 50; try { weight = std::stoi(line.substr(0, first_colon)); - } catch (...) { + } catch (const std::exception&) { continue; } @@ -1602,12 +1590,8 @@ void XDGBasedAppProvider::ParseGlobs2File(const std::string& filepath, std::vect bool case_sensitive = false; if (!flags_str.empty()) { - std::vector flags = SplitString(flags_str, ','); - for (const auto& f : flags) { - if (f == "cs") { - case_sensitive = true; - } - } + const auto flags = SplitString(flags_str, ','); + case_sensitive = (std::find(flags.begin(), flags.end(), "cs") != flags.end()); } if (!mime.empty() && !pattern.empty()) { @@ -1761,9 +1745,9 @@ std::vector XDGBasedAppProvider::GetMimeDatabaseSearchDirpaths() } }; - std::string home = XDGBasedAppProvider::GetEnv("HOME"); - std::string xdg_data_home = XDGBasedAppProvider::GetEnv("XDG_DATA_HOME"); - std::string xdg_data_dirs = XDGBasedAppProvider::GetEnv("XDG_DATA_DIRS", "/usr/local/share:/usr/share"); + std::string home = GetEnv("HOME"); + std::string xdg_data_home = GetEnv("XDG_DATA_HOME"); + std::string xdg_data_dirs = GetEnv("XDG_DATA_DIRS", "/usr/local/share:/usr/share"); if (!xdg_data_home.empty() && xdg_data_home[0] == '/') { add_path(xdg_data_home + "/mime"); @@ -1771,7 +1755,7 @@ std::vector XDGBasedAppProvider::GetMimeDatabaseSearchDirpaths() add_path(home + "/.local/share/mime"); } - for (const auto& dir : XDGBasedAppProvider::SplitString(xdg_data_dirs, ':')) { + for (const auto& dir : SplitString(xdg_data_dirs, ':')) { if (dir.empty() || dir[0] != '/') continue; add_path(dir + "/mime"); } diff --git a/OpenWith/src/XDGBasedAppProvider.hpp b/OpenWith/src/XDGBasedAppProvider.hpp index 2f78d4f10..c7888bb92 100644 --- a/OpenWith/src/XDGBasedAppProvider.hpp +++ b/OpenWith/src/XDGBasedAppProvider.hpp @@ -7,6 +7,7 @@ #include "lng.hpp" #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include +class KeyFileReadHelper; class XDGBasedAppProvider : public AppProvider { @@ -31,8 +33,8 @@ class XDGBasedAppProvider : public AppProvider // Platform-specific settings API std::vector GetPlatformSettings() override; void SetPlatformSettings(const std::vector& settings) override; - void LoadPlatformSettings() override; - void SavePlatformSettings() override; + void LoadPlatformSettings(const KeyFileReadHelper& key_reader) override; + void SavePlatformSettings(KeyFileHelper& key_writer) override; private: @@ -146,20 +148,17 @@ class XDGBasedAppProvider : public AppProvider bool operator<(const GlobRule& other) const { // Higher weights are checked first. - if (weight != other.weight) - { + if (weight != other.weight) { return weight > other.weight; } // Exact filenames (e.g., 'Makefile') take precedence over globs (*.txt). - if (is_literal != other.is_literal) - { + if (is_literal != other.is_literal) { return is_literal; } // Longer patterns are more specific (e.g., '*.tar.gz' > '*.gz'). - if (pattern.length() != other.pattern.length()) - { + if (pattern.length() != other.pattern.length()) { return pattern.length() > other.pattern.length(); } @@ -277,16 +276,13 @@ class XDGBasedAppProvider : public AppProvider // Group 5: Plugin Internal Configuration // ****************************************************************************** - // A helper struct to define a platform setting, linking its INI key, - // localized UI display name, its corresponding class member variable, and default value. - struct PlatformSettingDefinition { - std::string key; - LanguageID display_name_id; - bool XDGBasedAppProvider::* member_variable; - bool default_value; - bool affects_candidates; + std::string internal_key; // persistent INI key and internal identifier + LanguageID display_name_id; // ID to fetch the localized UI label + bool XDGBasedAppProvider::* member_variable; // pointer to the linked boolean class member + bool default_value; // fallback value if missing in the INI file + bool affects_candidates; // true if changing this setting affects the contents or order of the candidate list }; @@ -345,8 +341,8 @@ class XDGBasedAppProvider : public AppProvider std::string DetectMimeTypeWithMagikaTool(const std::string& filepath_escaped); std::string DetectMimeTypeViaGlobRules(const std::string& filepath); static bool GlobMatch(const std::string &text, const std::string &pattern, bool case_sensitive); - std::string GuessMimeTypeByExtension(const std::string& filepath); - static const std::unordered_map& GetExtMimeMap(); + std::string_view GuessMimeTypeByExtension(const std::string& filepath); + static const std::unordered_map& GetExtMimeMap(); // --- XDG database parsing & caching --- std::unordered_map IndexAllDesktopFiles(); diff --git a/OpenWith/src/common.hpp b/OpenWith/src/common.hpp index 48461a254..ea39dbf40 100644 --- a/OpenWith/src/common.hpp +++ b/OpenWith/src/common.hpp @@ -11,9 +11,8 @@ struct Field struct CandidateInfo { - std::wstring name; // display name for the application selection menu - std::wstring id; // unique identifier: Desktop file ID or path to the .app - bool terminal; // whether a terminal is required or not - bool multi_file_aware = false; + std::wstring name; // display name for the application selection menu + std::wstring id; // unique identifier: Desktop file ID or path to the .app + bool terminal; // whether a terminal is required or not + bool multi_file_aware = false; // whether the application can open multiple files in a single invocation }; - diff --git a/OpenWith/src/lng.hpp b/OpenWith/src/lng.hpp index 17428a937..2b29f0eac 100644 --- a/OpenWith/src/lng.hpp +++ b/OpenWith/src/lng.hpp @@ -4,6 +4,8 @@ enum LanguageID { MPluginTitle, MChooseApplication, + MOpenWithFor, + MFile_s, MOk, MCancel, @@ -20,6 +22,7 @@ enum LanguageID MNoWaitForCommandCompletion, MClearSelection, MConfirmLaunchOption, + MDisplayFilename, MUseXdgMimeTool, MUseFileTool, @@ -40,8 +43,8 @@ enum LanguageID MDetails, - MPathname, MFilesSelected, + MFilepaths, MMimeProfile, MLaunchCommand, MClose, diff --git a/changelog.md b/changelog.md index bdcd3005a..64a16383e 100644 --- a/changelog.md +++ b/changelog.md @@ -18,7 +18,7 @@ or via `git log --no-merges --pretty=format:"%as: %B"`). * SysID for all Plugins (may be used from macros to call plugins via macrofunction `callplugin`; a pluguin's SysID may see via `far:about` or in plugin's source code) * _ADB plugin_: New panel plugin for accessing Android devices in developer mode, both shell commands and file system; see [adb/README.md](https://github.com/elfmz/far2l/blob/master/adb/README.md)) * _edsort plugin_: Support unique row sorting and preserve dialog values -* _OpenWith plugin_: Update to v1.2. Allow plugin invocation on .. (treated as current directory). New option to show Snap/Flatpak markers in app menu. +* _OpenWith plugin_: Update to v1.2. Allow plugin invocation on .. (treated as current directory). New option to show Snap/Flatpak markers in app menu. New option to display filename in the menu title. * _python plugin_: fixes and new subplugins uimgimage.py, uimgpdf.py, udockerrunlike.py [#3346](https://github.com/elfmz/far2l/issues/3346) * Several bugfixes and improvements