├── Custom Columns ├── BlankSpacerColumn.js ├── FullShortcutTargetColumn.vbs └── VerifySignature.js ├── LICENSE ├── Other └── ExplorerDialogPathSelector.ahk ├── README.md ├── Script Add-Ins ├── AutoFolderCommand.vbs └── SplitViewArrow.js └── User Command Scripts ├── Combine_To_TXT_File.js ├── Copy_File_Names_Auto_Quoted.js ├── Copy_File_Paths_Auto_Quoted.js ├── Copy_Relative_Paths.js ├── Copy_Shortcut_Target_With_Arguments.vbs ├── Copy_Tree_Structure.js ├── Create_Bak_Button.js ├── Edit_Alternate_Data_Streams.js └── Unique_Tab_Names.js /Custom Columns/BlankSpacerColumn.js: -------------------------------------------------------------------------------- 1 | function OnInit(initData) { 2 | initData.name = 'BlankSpacerScript'; 3 | initData.version = '1.0.1'; 4 | initData.copyright = ''; 5 | initData.desc = 'Adds blank metadata coumn'; 6 | initData.default_enable = true; 7 | initData.min_version = '12.0'; 8 | } 9 | 10 | function OnAddColumns(addColData) { 11 | var col = addColData.AddColumn(); 12 | col.method = 'OnColumn'; 13 | col.name = 'BlankSpacerColumn'; 14 | col.label = '[Blank Spacer]'; 15 | col.header = ' '; 16 | col.justify = 'center'; 17 | col.defwidth = 20; 18 | col.autorefresh = 0; 19 | col.autogroup = true; 20 | col.nosort=true; 21 | col.nogroup=true; 22 | } 23 | -------------------------------------------------------------------------------- /Custom Columns/FullShortcutTargetColumn.vbs: -------------------------------------------------------------------------------- 1 | ' Version: 2.3.0 - 9/7/24 2 | ' This add-in script is basically a column version of Copy_Shortcut_Target_With_Arguments.vbs 3 | ' It will display the full path of a shortcut (either .lnk or .url file), including arguments 4 | Option Explicit 5 | 6 | ' The script column definition 7 | Function OnInit(initData) 8 | initData.name = "Full Shortcut Target" 9 | initData.version = "2.0" 10 | initData.copyright = "ThioJoe" 11 | initData.desc = "Displays the target path and arguments for .lnk and .url files" 12 | initData.default_enable = true 13 | initData.min_version = "12.0" 14 | 15 | ' Add a custom column to display the target path and arguments 16 | Dim col 17 | Set col = initData.AddColumn() 18 | col.name = "ShortcutTargetArgs" 19 | col.method = "OnShortcutTargetArgs" 20 | col.label = "Full Target" 21 | col.justify = "left" 22 | col.autogroup = true 23 | End Function 24 | 25 | Function OnShortcutTargetArgs(scriptColData) 26 | Dim item, fs, path, fExt, shellLink, shellLinkAlt 27 | Set item = scriptColData.item 28 | Set fs = CreateObject("Scripting.FileSystemObject") 29 | path = item.realpath 30 | fExt = LCase(fs.GetExtensionName(path)) 31 | 32 | If fExt = "lnk" Then 33 | ' Handle .lnk files 34 | Set shellLink = CreateObject("Shell.Application") 35 | Set shellLinkAlt = CreateObject("WScript.Shell") 36 | scriptColData.value = GetLnkFullPath(shellLink, shellLinkAlt, fs, path) 37 | Set shellLink = Nothing 38 | Set shellLinkAlt = Nothing 39 | ElseIf fExt = "url" Then 40 | scriptColData.value = GetUrlFullPath(fs, path) 41 | Else 42 | ' For other files, return empty 43 | scriptColData.value = "" 44 | End If 45 | End Function 46 | 47 | ' Function to return the full path and arguments of a .lnk shortcut file 48 | ' Where ShellLinkObj is created via: CreateObject("Shell.Application") 49 | ' ShellLinkObjAlt is created via: CreateObject("WScript.Shell") 50 | ' and 'fs' is created via: CreateObject("Scripting.FileSystemObject") 51 | Function GetLnkFullPath(shellLinkObj, shellLinkObjAlt, fs, path) 52 | Dim linkData, targetPath, arguments 53 | 54 | On Error Resume Next 55 | 56 | ' Try using Shell.Application first 57 | Set linkData = shellLinkObj.Namespace(fs.GetParentFolderName(path)).ParseName(fs.GetFileName(path)).GetLink 58 | 59 | If Err Then 60 | ' Fall back to WScript.Shell if any error occurs 61 | Err.Clear 62 | Set linkData = shellLinkObjAlt.CreateShortcut(path) 63 | targetPath = linkData.TargetPath 64 | arguments = linkData.Arguments 65 | Else 66 | targetPath = linkData.Target.Path 67 | arguments = linkData.Arguments 68 | End If 69 | 70 | ' Return to normal error handling behavior 71 | On Error Goto 0 72 | 73 | If Err.Number <> 0 Then 74 | ' If both methods fail, return the original path 75 | GetLnkFullPath = Trim(path) 76 | ElseIf targetPath <> "" Then 77 | GetLnkFullPath = Trim(targetPath & " " & arguments) 78 | Else 79 | GetLnkFullPath = "[Error]" ' Fallback to original path 80 | End If 81 | End Function 82 | 83 | ' Function to read text from .url file to get URL target. 84 | ' Where 'path' is the .url file path and 'fs' is created via: CreateObject("Scripting.FileSystemObject") 85 | Function GetUrlFullPath(fs, path) 86 | Dim urlFile, url 87 | Set urlFile = fs.OpenTextFile(path, 1) ' 1 = ForReading 88 | Do Until urlFile.AtEndOfStream 89 | url = urlFile.ReadLine 90 | If Left(LCase(url), 4) = "url=" Then 91 | GetUrlFullPath = Mid(url, 5) 92 | Exit Do 93 | End If 94 | Loop 95 | urlFile.Close 96 | End Function 97 | -------------------------------------------------------------------------------- /Custom Columns/VerifySignature.js: -------------------------------------------------------------------------------- 1 | //1 2 | function OnInit(initData) 3 | { 4 | initData.name = "Verify File Signatures"; 5 | initData.version = "1.2.1"; 6 | initData.copyright = "(c) 2024"; 7 | initData.desc = "Column to check if file signature is valid using FastSigCheck or SignTool"; 8 | initData.default_enable = true; 9 | initData.min_version = "12.23"; 10 | 11 | // Configuration settings 12 | initData.config_desc = DOpus.Create().Map(); 13 | initData.config_groups = DOpus.Create().Map(); 14 | 15 | // Configuration for exeToolPath 16 | initData.config_desc("Exe_Tool_Path") = "Full path to the executable (FastSigCheck.exe or SignTool.exe). If the exe is already in the PATH environment variable, you can just put the exe name without the full path."; 17 | initData.config.Exe_Tool_Path = "C:\\Path\\To\\Tool.exe"; 18 | 19 | // Configuration for tool choice 20 | initData.config_desc("Tool_Choice") = "Choose the tool for signature check"; 21 | initData.config.Tool_Choice = DOpus.Create.Vector(0,"SignTool", "FastSigCheck"); 22 | 23 | // Enable Debug Logging 24 | initData.config_desc("Enable_Debug") = "Enable debug output for this script. Note: Doesn't output the debug from exe tools."; 25 | initData.config.Enable_Debug = DOpus.Create.Vector(0,"Disabled", "Enabled"); 26 | 27 | // Limit File Types to Check 28 | initData.config_desc("Use_File_Types_List") = "Whether to only check filetypes in the Supported_File_Extensions list. If false, all files will be checked always."; 29 | initData.config.Use_File_Types_List = DOpus.Create.Vector(1,"Disabled", "Enabled"); 30 | 31 | // Custom Column Messages 32 | initData.config_desc("Signature_Valid_Message") = "String to display for files that are signed with valid certificate."; 33 | initData.config.Signature_Valid_Message = "✔ Signed"; 34 | 35 | initData.config_desc("Signature_Invalid_Message") = "(FastSigCheck Only) String to display for files that are signed with invalid/untrusted certificate."; 36 | initData.config.Signature_Invalid_Message = "⚠ Invalid"; 37 | 38 | initData.config_desc("Signature_NoSignature_Message") = "String to display for files that have no signature. Note: When using signtool, this message will apply for ALL results that are not a valid signature - even any errors."; 39 | initData.config.Signature_NoSignature_Message = ""; 40 | 41 | initData.config_desc("Signature_UnsupportedType_Message") = "(FastSigCheck Only) String to display for files that have unsupported types for signature check."; 42 | initData.config.Signature_UnsupportedType_Message = ""; 43 | 44 | initData.config_desc("Signature_Error_Message") = "(FastSigCheck Only) String to display for files that encounter an error during signature check."; 45 | initData.config.Signature_Error_Message = "Error"; 46 | 47 | initData.config_desc("Text_Align") = "Align text in column to left, center, or right. Note: You might need to disable & re-enable the script for this to take effect."; 48 | initData.config.Text_Align = DOpus.Create.Vector(0,"Left", "Center", "Right"); 49 | 50 | initData.config_desc("Signtool_Arguments") = "Arguments to use with the 'signtool verify' command. Run 'signtool verify' to see the options. /pa is recommended because otherwise it will default to a stricter policy that really only applies to drivers and would think everything is not properly signed. /q minimizes output, which is not necessary because this script can't receive the output anyway, just the exit codes. To also check catalog signatures you can add /a , but it will take a lot longer. If checking catalog signatures you might want to edit the supported file types list to include file types expected to be signed that way, because any type of file can be signed via catalog."; 51 | initData.config.Signtool_Arguments = "/pa /q"; 52 | 53 | // Supported File Extensions 54 | initData.config_desc("Supported_File_Extensions") = "List of supported file extensions"; 55 | initData.config.Supported_File_Extensions = DOpus.Create.Vector( 56 | ".appx", ".appxbundle", ".arx", ".cab", ".cat", ".cbx", ".cpl", ".crx", 57 | ".dbx", ".deploy", ".dll", ".doc", ".docm", ".dot", ".dotm", ".drx", 58 | ".efi", ".exe", ".js", ".mpp", ".mpt", ".msi", ".msix", ".msixbundle", 59 | ".msm", ".msp", ".ocx", ".pot", ".potm", ".ppa", ".ppam", ".pps", 60 | ".ppsm", ".ppt", ".pptm", ".ps1", ".psi", ".psm1", ".pub", ".stl", 61 | ".sys", ".vbs", ".vdw", ".vdx", ".vsd", ".vsdm", ".vsix", ".vss", 62 | ".vssm", ".vst", ".vstm", ".vsx", ".vtx", ".vxd", ".wiz", ".wsf", 63 | ".xap", ".xla", ".xlam", ".xls", ".xlsb", ".xlsm", ".xlt", ".xltm", 64 | ".xsn" 65 | ); 66 | 67 | // Group the configuration settings 68 | initData.config_groups("Exe_Tool_Path") = "1 - Tool Settings"; 69 | initData.config_groups("Tool_Choice") = "1 - Tool Settings"; 70 | initData.config_groups("Signtool_Arguments") = "1 - Tool Settings"; 71 | 72 | initData.config_groups("Supported_File_Extensions") = "2 - File Options"; 73 | initData.config_groups("Use_File_Types_List") = "2 - File Options"; 74 | 75 | initData.config_groups("Signature_Valid_Message") = "3 - Custom Column Messages"; 76 | initData.config_groups("Signature_Invalid_Message") = "3 - Custom Column Messages"; 77 | initData.config_groups("Signature_NoSignature_Message") = "3 - Custom Column Messages"; 78 | initData.config_groups("Signature_UnsupportedType_Message") = "3 - Custom Column Messages"; 79 | initData.config_groups("Signature_Error_Message") = "3 - Custom Column Messages"; 80 | 81 | initData.config_groups("Enable_Debug") = "4 - Other Options"; 82 | initData.config_groups("Text_Align") = "4 - Other Options"; 83 | 84 | } 85 | 86 | // 2 87 | function OnAddColumns(addColData) 88 | { 89 | AddColumn(addColData, "VerifySignature", "Verify Signature", "verifysignature"); 90 | } 91 | 92 | // 3 93 | function AddColumn(addColData, colName, colLabel, checkType) 94 | { 95 | var col = addColData.AddColumn(); 96 | col.name = colName; 97 | col.label = colLabel; 98 | col.header = 'Signature'; 99 | col.method = "OnColumns"; 100 | col.multicol = false; 101 | col.autogroup = true; 102 | col.autorefresh = true; 103 | col.userdata = checkType; 104 | col.namerefresh = true; // Refresh the name after each change 105 | 106 | switch (Script.config.Text_Align) 107 | { 108 | case 0: 109 | col.justify = "left"; 110 | break; 111 | case 1: 112 | col.justify = "center"; 113 | break; 114 | case 2: 115 | col.justify = "right"; 116 | break; 117 | default: 118 | col.justify = "left"; // Default to left if configuration is not set correctly 119 | break; 120 | } 121 | } 122 | 123 | // 4 124 | function OnColumns(scriptColData) 125 | { 126 | try 127 | { 128 | var item = scriptColData.item; 129 | if (item.is_dir) return; 130 | 131 | var enable_debug = Script.config.Enable_Debug; 132 | 133 | if (enable_debug === 1) DOpus.Output("Processing item: " + item.realpath); 134 | 135 | var fileExtension = item.ext.toLowerCase(); 136 | var supportedExtensions = DOpus.Create().StringSet(); 137 | supportedExtensions.assign(Script.config.Supported_File_Extensions); 138 | 139 | if (Script.config.Use_File_Types_List === 1 && !supportedExtensions.exists(fileExtension)) 140 | { 141 | scriptColData.value = Script.config.Signature_UnsupportedType_Message; 142 | if (enable_debug === 1) DOpus.Output("File extension not in supported list, skipping: " + fileExtension); 143 | return; 144 | } 145 | 146 | var toolChoiceIndex = Script.config.Tool_Choice; 147 | var exeToolPath = Script.config.Exe_Tool_Path; 148 | var signtool_args = Script.config.Signtool_Arguments; 149 | var cmd; 150 | 151 | //If using FastSigCheck.exe 152 | if (toolChoiceIndex === 1) { 153 | cmd = '"' + exeToolPath + '" "' + item.realpath + '"'; 154 | //If using signtool.exe 155 | } else if (toolChoiceIndex === 0) { 156 | cmd = '"' + exeToolPath + '" verify ' + signtool_args + ' "' + item.realpath + '"'; 157 | } else { 158 | scriptColData.value = "Error: Invalid tool choice: " + toolChoiceIndex; 159 | return; 160 | } 161 | 162 | if (enable_debug === 1) DOpus.Output("Command: " + cmd); 163 | 164 | var shell = new ActiveXObject("WScript.Shell"); 165 | 166 | var exitCode = shell.Run(cmd, 0, true); 167 | if (enable_debug === 1) DOpus.Output("Executed command, exit code: " + exitCode); 168 | 169 | switch (exitCode) 170 | { 171 | // Valid Signature 172 | case 0: 173 | scriptColData.value = Script.config.Signature_Valid_Message; 174 | break; 175 | // No Signature. If using SignTool, this could mean any invalid signature. 176 | case 1: 177 | scriptColData.value = Script.config.Signature_NoSignature_Message; 178 | break; 179 | // Signed but invalid 180 | case 2: 181 | scriptColData.value = Script.config.Signature_Invalid_Message; 182 | break; 183 | // Unsupported file type for checking signature 184 | case 3: 185 | scriptColData.value = Script.config.Signature_UnsupportedType_Message; 186 | break; 187 | // Other error 188 | default: 189 | scriptColData.value = Script.config.Signature_Error_Message; 190 | break; 191 | } 192 | } 193 | catch (e) 194 | { 195 | scriptColData.value = "Error"; 196 | DOpus.Output("Exception: " + e.message); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ThioJoe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Other/ExplorerDialogPathSelector.ahk: -------------------------------------------------------------------------------- 1 | ; This script lets you press a key (default middle mouse) within an Explorer Save/Open dialog window, and it will show a list of paths from any currently open Directory Opus and/or Windows Explorer windows. 2 | ; 3 | ; Source Repo: https://github.com/ThioJoe/D-Opus-Scripts 4 | ; Parts of the logic from this script: https://gist.github.com/akaleeroy/f23bd4dd2ddae63ece2582ede842b028#file-currently-opened-folders-md 5 | 6 | #Requires AutoHotkey v2.0 7 | #SingleInstance force 8 | SetWorkingDir(A_ScriptDir) 9 | 10 | ; Configuration 11 | activeTabSuffix := "" ; Appears to the right of the active path for each window group 12 | activeTabPrefix := "► " ; Appears to the left of the active path for each window group 13 | inactiveTabPrefix := " " ; Indentation for inactive tabs, so they line up 14 | dopusRTPath := "C:\Program Files\GPSoftware\Directory Opus\dopusrt.exe" ; Path to dopusrt.exe - can be empty to disable Directory Opus integration 15 | 16 | ; Hotkey to show the menu. Default is Middle Mouse Button. "~" in front of the key name ensures it doesn't hijack all use of the key for other programs. 17 | f_Hotkey := "~MButton" 18 | 19 | ; Auto-execute section 20 | Hotkey(f_Hotkey, f_DisplayMenu) 21 | 22 | ; Navigate to the chosen path 23 | f_Navigate(A_ThisMenuItem := "", A_ThisMenuItemPos := "", MyMenu := "", *) 24 | { 25 | global 26 | ; Strip any prefix markers from the path 27 | f_path := RegExReplace(A_ThisMenuItem, "^[►▶→•\s]+\s*", "") 28 | ; Strip any custom suffix if present 29 | if (activeTabSuffix) 30 | f_path := RegExReplace(f_path, "\Q" activeTabSuffix "\E$", "") 31 | 32 | if (f_path = "") 33 | return 34 | 35 | if (f_class = "#32770") ; It's a dialog 36 | { 37 | WinActivate("ahk_id " f_window_id) 38 | Send("!{d}") 39 | Sleep(50) 40 | addressbar := ControlGetFocus("a") 41 | ControlSetText(f_path, addressbar, "a") 42 | ControlSend("{Enter}", addressbar, "a") 43 | ControlFocus("Edit1", "a") 44 | return 45 | } 46 | else if (f_class = "ConsoleWindowClass") 47 | { 48 | WinActivate("ahk_id " f_window_id) 49 | SetKeyDelay(0) 50 | Send("{Esc}pushd " f_path "{Enter}") 51 | return 52 | } 53 | } 54 | 55 | RemoveToolTip() 56 | { 57 | SetTimer(RemoveToolTip, 0) 58 | ToolTip() 59 | } 60 | 61 | ; Get Explorer paths 62 | getAllExplorerPaths() { 63 | paths := [] 64 | 65 | ; Get all CabinetWClass windows 66 | explorerHwnds := WinGetList("ahk_class CabinetWClass") 67 | 68 | ; Get Shell.Application once 69 | shell := ComObject("Shell.Application") 70 | 71 | ; IShellBrowser interface ID 72 | static IID_IShellBrowser := "{000214E2-0000-0000-C000-000000000046}" 73 | 74 | ; For each Explorer window 75 | for explorerHwnd in explorerHwnds { 76 | try { 77 | ; First get the main window path 78 | for window in shell.Windows { 79 | try { 80 | if window && window.hwnd && window.hwnd = explorerHwnd { 81 | path := window.Document.Folder.Self.Path 82 | if path && !HasValue(paths, path) 83 | paths.Push(path) 84 | 85 | ; Now try to get tabs 86 | tabCtrl := ControlGetHwnd("ShellTabWindowClass1", explorerHwnd) 87 | if tabCtrl { 88 | ; Get the shell browser interface 89 | shellBrowser := ComObjQuery(window, IID_IShellBrowser, IID_IShellBrowser) 90 | if shellBrowser { 91 | try { 92 | ; Get the tab window object 93 | tabWindow := window.Document.Application.Windows 94 | if tabWindow { 95 | ; Loop through shell windows again to find ones matching this window's tabs 96 | for tabShell in shell.Windows { 97 | try { 98 | if tabShell && tabShell.hwnd = explorerHwnd { 99 | tabPath := tabShell.Document.Folder.Self.Path 100 | if tabPath && !HasValue(paths, tabPath) 101 | paths.Push(tabPath) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | break ; Found the main window, move to next explorer 110 | } 111 | } 112 | } 113 | } 114 | } 115 | return paths 116 | } 117 | 118 | ; Helper function to check if value exists in array 119 | HasValue(haystack, needle) { 120 | if !(IsObject(haystack)) 121 | return false 122 | for index, value in haystack { 123 | if (value = needle) 124 | return true 125 | } 126 | return false 127 | } 128 | 129 | ; Parse the XML and return an array of path objects 130 | GetDOpusPaths() 131 | { 132 | if (dopusRTPath = "") { 133 | return [] 134 | } 135 | 136 | if !FileExist(dopusRTPath) { 137 | MsgBox("Directory Opus Runtime (dopusrt.exe) not found at:`n" dopusRTPath "`n`nDirectory Opus integration won't work. To enable it, set the correct path in the script configuration. Or set it to an empty string to avoid this error.", "DOpus Integration Error", "Icon!") 138 | return [] 139 | } 140 | 141 | tempFile := A_Temp "\dopus_paths.xml" 142 | try FileDelete(tempFile) 143 | 144 | try { 145 | cmd := '"' dopusRTPath '" /info "' tempFile '",paths' 146 | RunWait(cmd,, "Hide") 147 | 148 | if !FileExist(tempFile) 149 | return [] 150 | 151 | xmlContent := FileRead(tempFile) 152 | FileDelete(tempFile) 153 | 154 | ; Parse paths from XML 155 | paths := [] 156 | 157 | ; Start after the XML declaration 158 | xmlContent := RegExReplace(xmlContent, "^.*?", "") 159 | 160 | ; Extract each path element with its attributes 161 | while RegExMatch(xmlContent, "s)]*)>(.*?)", &match) { 162 | ; Get attributes 163 | attrs := Map() 164 | RegExMatch(match[1], "lister=`"(0x[^`"]*)`"", &listerMatch) 165 | RegExMatch(match[1], "active_tab=`"([^`"]*)`"", &activeTabMatch) 166 | RegExMatch(match[1], "active_lister=`"([^`"]*)`"", &activeListerMatch) 167 | 168 | ; Create path object 169 | pathObj := { 170 | path: match[2], 171 | lister: listerMatch ? listerMatch[1] : "unknown", 172 | isActiveTab: activeTabMatch ? (activeTabMatch[1] = "1") : false, 173 | isActiveLister: activeListerMatch ? (activeListerMatch[1] = "1") : false 174 | } 175 | paths.Push(pathObj) 176 | 177 | ; Remove the processed path element and continue searching 178 | xmlContent := SubStr(xmlContent, match.Pos + match.Len) 179 | } 180 | 181 | return paths 182 | } 183 | catch as err { 184 | MsgBox("Error reading Directory Opus paths: " err.Message "`n`nDirectory Opus integration will be disabled.", "DOpus Integration Error", "Icon!") 185 | return [] 186 | } 187 | } 188 | 189 | ; Display the menu 190 | f_DisplayMenu(ThisHotkey) 191 | { 192 | global 193 | ; Detect windows with error handling 194 | try { 195 | f_window_id := WinGetID("a") 196 | f_class := WinGetClass("a") 197 | } catch as err { 198 | ; If we can't get window info, wait briefly and try once more 199 | Sleep(25) 200 | try { 201 | f_window_id := WinGetID("a") 202 | f_class := WinGetClass("a") 203 | } catch as err { 204 | ToolTip("Unable to detect active window") 205 | SetTimer(RemoveToolTip, 1000) 206 | return 207 | } 208 | } 209 | 210 | ; Verify we got valid window info 211 | if (!f_window_id || !f_class) { 212 | ToolTip("No valid window detected") 213 | SetTimer(RemoveToolTip, 1000) 214 | return 215 | } 216 | 217 | ; Don't display menu unless it's a dialog or console window 218 | if !(f_class ~= "^(?i:#32770|ConsoleWindowClass)$") 219 | return 220 | 221 | CurrentLocations := Menu() 222 | hasItems := false 223 | 224 | ; Only get Directory Opus paths if dopusRTPath is set 225 | if (dopusRTPath != "") { 226 | ; Get paths from Directory Opus using DOpusRT 227 | paths := GetDOpusPaths() 228 | 229 | ; Group paths by lister 230 | listers := Map() 231 | 232 | ; First, group all paths by their lister 233 | for pathObj in paths { 234 | if !listers.Has(pathObj.lister) 235 | listers[pathObj.lister] := [] 236 | listers[pathObj.lister].Push(pathObj) 237 | } 238 | 239 | ; First add paths from active lister 240 | for pathObj in paths { 241 | if (pathObj.isActiveLister) { 242 | CurrentLocations.Add("Opus Window " A_Index " (Active)", f_Navigate) 243 | CurrentLocations.Disable("Opus Window " A_Index " (Active)") 244 | 245 | ; Add all paths for this lister 246 | listerPaths := listers[pathObj.lister] 247 | for tabObj in listerPaths { 248 | menuText := tabObj.path 249 | ; Add prefix and suffix for active tab based on global settings 250 | if (tabObj.isActiveTab) 251 | menuText := activeTabPrefix menuText activeTabSuffix 252 | else 253 | menuText := inactiveTabPrefix menuText 254 | 255 | CurrentLocations.Add(menuText, f_Navigate) 256 | CurrentLocations.SetIcon(menuText, A_WinDir . "\system32\imageres.dll", "4") 257 | hasItems := true 258 | } 259 | 260 | ; Remove this lister from the map so we don't show it again 261 | listers.Delete(pathObj.lister) 262 | break 263 | } 264 | } 265 | 266 | ; Then add remaining Directory Opus listers 267 | windowNum := 2 268 | for lister, listerPaths in listers { 269 | CurrentLocations.Add("Opus Window " windowNum, f_Navigate) 270 | CurrentLocations.Disable("Opus Window " windowNum) 271 | 272 | ; Add all paths for this lister 273 | for pathObj in listerPaths { 274 | menuText := pathObj.path 275 | ; Add prefix and suffix for active tab based on global settings 276 | if (pathObj.isActiveTab) 277 | menuText := activeTabPrefix menuText activeTabSuffix 278 | else 279 | menuText := inactiveTabPrefix menuText 280 | 281 | CurrentLocations.Add(menuText, f_Navigate) 282 | CurrentLocations.SetIcon(menuText, A_WinDir . "\system32\imageres.dll", "4") 283 | hasItems := true 284 | } 285 | 286 | windowNum++ 287 | } 288 | } 289 | 290 | ; Get Explorer paths 291 | explorerPaths := getAllExplorerPaths() 292 | 293 | ; Add Explorer paths if any exist 294 | if explorerPaths.Length > 0 { 295 | ; Add separator if we had Directory Opus paths 296 | if (hasItems) 297 | CurrentLocations.Add() 298 | 299 | ; Add Explorer header 300 | CurrentLocations.Add("Windows Explorer", f_Navigate) 301 | CurrentLocations.Disable("Windows Explorer") 302 | 303 | ; Add Explorer paths 304 | for path in explorerPaths { 305 | menuText := inactiveTabPrefix path 306 | CurrentLocations.Add(menuText, f_Navigate) 307 | CurrentLocations.SetIcon(menuText, A_WinDir . "\system32\imageres.dll", "4") 308 | hasItems := true 309 | } 310 | } 311 | 312 | ; Show menu if we have items, otherwise show tooltip 313 | if (hasItems) { 314 | CurrentLocations.Show() 315 | } else { 316 | ToolTip("No folders open") 317 | SetTimer(RemoveToolTip, 1000) 318 | } 319 | 320 | ; Clean up 321 | CurrentLocations := "" 322 | } 323 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Various Scripts for Directory Opus 2 | 3 | ### Script Types: 4 | **User Command Scripts:** Add these by going to the Customize menu > User Commands > New, and then set the "Type" to "Script Function", and "Script Type" to either VBScript or JScript depending. Don't forget to copy the template from the script comments into the template box. 5 | 6 | **Custom Columns:** Scripts that add a new type of column you can add to any lister. These can be added by just dragging the script file into the "Script Management" menu. 7 | 8 | **Script Add-Ins:** These can be added by just dragging the script file into the "Script Management" menu 9 | 10 | ----- 11 | 12 | ## User Command Scripts 13 | 14 | - **Create_Bak_Button:** Creates '.bak' backups for any number of selected files. If a .bak already exists for a file, it will create .bak2, .bak3 and so on. 15 | 16 | - **Copy_File_Names_Auto_Quoted:** Copies selected files or folders _names_ to clipboard with quotes around it only if it has spaces. Various optional behavior for multiple selected items 17 | 18 | - **Copy_File_Paths_Auto_Quoted:** Copies selected files or folders _full paths_ to clipboard with quotes around it only if it has spaces. Various optional behavior for multiple selected items. 19 | 20 | - **Copy_Relative_Paths:** Copy the list of paths to selected files/folders, relative to current path (such as when using flat view) 21 | 22 | - **Copy_Tree_Structure:** This script copies a tree view of selected files and folders to the clipboard, offering customization options for branching characters, folder prefixes, sorting, and depth expansion. 23 | 24 | - **Edit_Alternate_Data_Streams:** Use as a button to edit the alternate data streams of a selected file using the text editor of your choice. 25 | 26 | ## Script Add-Ins 27 | 28 | - **AutoFolderCommand.vbs:** Allows running specific commands when entering or leaving specific folder paths, with several options. 29 | 30 | - **SplitViewArrow:** (Work in Progress) Dynamically sets the lister background with an arrow image pointing from the source to the destination in dual lister view, to easily identify which is the destination/source side for operations. 31 | 32 | ## Custom Columns 33 | 34 | - **BlankSpacerColumn:** This script adds a blank spacer column to the Directory Opus lister, improving visual organization. 35 | 36 | - **FullShortcutTargetColumn.vbs:** This script adds a column that displays the full target path for shortcuts (.lnk files), including arguments. 37 | 38 | - **VerifySignature:** Adds a column that shows the digital signature status of files using either FastSigCheck or SignTool. 39 | 40 | ## Other Tools/Scripts 41 | 42 | - **ExplorerDialogPathSelector.ahk:** Autohotkey V2 Script that lets you press a key (default middle mouse) within an Explorer Save/Open dialog window, and it will show a list of paths from any currently open Directory Opus and/or Windows Explorer windows. 43 | -------------------------------------------------------------------------------- /Script Add-Ins/AutoFolderCommand.vbs: -------------------------------------------------------------------------------- 1 | ' Script to automatically run Directory Opus commands when entering specific paths with several options. 2 | ' Version: 1.1.0 - 11/1/24 3 | ' Author: ThioJoe (https://github.com/ThioJoe) 4 | ' 5 | ' Available at GitHub repo: https://github.com/ThioJoe/D-Opus-Scripts 6 | ' Forum Thread: https://resource.dopus.com/t/script-for-running-various-commands-when-entering-specific-paths/51839 7 | 8 | Option Explicit 9 | 10 | Function OnInit(initData) 11 | initData.name = "Auto Commands For Specific Folders" 12 | initData.version = "1.1" 13 | initData.desc = "Automatically run specified commands when entering or leaving configured folders." 14 | initData.default_enable = true 15 | initData.min_version = "13.0" 16 | 17 | ' Configuration settings 18 | Dim config: Set config = initData.config 19 | Dim config_desc: Set config_desc = DOpus.Create.Map 20 | Dim config_groups: Set config_groups = DOpus.Create.Map 21 | 22 | ' FolderCommandPairs configuration 23 | config.FolderCommandPairs = "Path1 = C:\SomeExample\*\Path" & vbNewLine & _ 24 | "EntryCommand1 = Toolbar Example_Toolbar_Name" & vbNewLine & _ 25 | "LeaveCommand1 = Toolbar Example_Toolbar_Name CLOSE" & vbNewLine & _ 26 | "Switches1 = AlwaysRunLeave, AlwaysRunEntry" 27 | 28 | config_desc("FolderCommandPairs") = _ 29 | "Enter path-command pairs, one per line. Use the following format:" & vbNewLine & _ 30 | " PathX = " & vbNewLine & _ 31 | " EntryCommandX = command_to_run_on_entry>" & vbNewLine & _ 32 | " LeaveCommandX = " & vbNewLine & _ 33 | " SwitchesX = " & vbNewLine & _ 34 | vbNewLine & _ 35 | "Where:" & vbNewLine & _ 36 | "• X is a number (1, 2, 3, etc.) to group related commands" & vbNewLine & _ 37 | "• Available switches: AlwaysRunEntry, AlwaysRunLeave, DontResolvePath" & vbNewLine & _ 38 | " -- AlwaysRunEntry, AlwaysRunLeave: Lets you run the entry/leave commands even when the next folder also matches the rule." & vbNewLine & _ 39 | " -- DontResolvePath: The given path will not be resolved before being checked against the lister path. May be necessary for paths like lib:// which would be re-written as C:\Users\... and therefore might not match when expected" & vbNewLine & _ 40 | vbNewLine & _ 41 | "Notes:" & vbNewLine & _ 42 | "• can include wildcards (*), folder aliases, and Windows environment variables" & vbNewLine & _ 43 | "• Commands can even include built-in Opus function arguments like {sourcepath}" & vbNewLine & _ 44 | "• Lines starting with // are treated as comments. Empty lines are also ignored." 45 | 46 | config_groups("FolderCommandPairs") = "1 - Folder Commands" 47 | 48 | ' Debug logging option 49 | config_desc("DebugLevel") = "Set the level of debug output" 50 | config.DebugLevel = DOpus.Create.Vector(0, "0 - Off (Default)", "1 - Info", "2 - Verbose", "3 - Debug", "4 - Debug Extra") 51 | config_groups("DebugLevel") = "2 - Options" 52 | 53 | ' Disable variable cache for debugging 54 | config.DisableCache = False 55 | config_desc("DisableCache") = "Disables using Script.Vars cache and re-parses config on every instance. You might need to enable this temporarily after changing the config." 56 | config_groups("DisableCache") = "2 - Options" 57 | 58 | initData.config_desc = config_desc 59 | initData.config_groups = config_groups 60 | End Function 61 | 62 | Sub DebugOutput(level, message) 63 | If Script.config.DebugLevel >= level Then 64 | Dim levelString 65 | Select Case level 66 | Case 1 67 | levelString = "[Info] | " 68 | Case 2 69 | levelString = "[Verbose] | " 70 | Case 3 71 | levelString = "[Debug] | " 72 | Case 4 73 | levelString = "[DebugExtra] | " 74 | Case Else 75 | levelString = "[Log] | " 76 | End Select 77 | 78 | DOpus.Output levelString & message 79 | End If 80 | End Sub 81 | 82 | Function ParseFolderCommandPairs() 83 | ' Check if we have cached folder command pairs and if caching is not disabled 84 | If Not Script.config.DisableCache And Script.vars.Exists("CachedFolderCommandPairs") Then 85 | DebugOutput 4, "------------------------------------------" 86 | DebugOutput 4, "ParseFolderCommandPairs(): Using cached folder command pairs" 87 | Set ParseFolderCommandPairs = Script.vars.Get("CachedFolderCommandPairs") 88 | Exit Function 89 | End If 90 | 91 | DebugOutput 2, "----------- BEGIN PARSE CONFIGS -----------" 92 | 93 | Dim pairs, line, path, entryCommand, leaveCommand, switches 94 | Dim result: Set result = CreateObject("Scripting.Dictionary") 95 | 96 | pairs = Split(Script.Config.FolderCommandPairs, vbNewLine) 97 | path = "" 98 | entryCommand = "" 99 | leaveCommand = "" 100 | switches = Array(False, False, False) ' Array of Bools Per Switch: [AlwaysRunEntry, AlwaysRunLeave, DontResolvePath] 101 | 102 | For Each line In pairs 103 | line = Trim(line) 104 | 105 | ' Skip blank lines and comments 106 | If line = "" Or Left(line, 2) = "//" Then 107 | If line = "" Then 108 | DebugOutput 4, "Skipping blank line" 109 | Else 110 | DebugOutput 4, "Skipping comment: " & line 111 | End If 112 | Else 113 | Dim parts: parts = Split(line, "=", 2) 114 | If UBound(parts) = 1 Then 115 | Dim key: key = LCase(Trim(parts(0))) 116 | Dim value: value = Trim(parts(1)) 117 | 118 | ' Check for the keywords we're looking for. 119 | ' "Left" function takes parameters of the string and the number of characters to check (from the left) 120 | ' "Key" is the left side of the line split on the equals (=) sign which has been trimmed and converted to lowercase, and should contain the config keyword 121 | ' "Value" is the right side of the line split on the equals (=) sign which has been trimmed and should contain the value for that keyword 122 | 123 | If Left(key, 4) = "path" Then 124 | ' We found a new 'path=' line, so save the accumulated commands for the current path before moving on and starting a new group 125 | If path <> "" Then 126 | result.Add path, Array(entryCommand, leaveCommand, switches) 127 | DebugOutput 2, "Added pair - Path: " & path & ", EntryCommand: " & entryCommand & ", LeaveCommand: " & leaveCommand & ", Switches: AlwaysRunEntry=" & switches(0) & ", AlwaysRunLeave=" & switches(1) & ", DontResolvePath=" & switches(2) 128 | ' Reset the variables for the next group 129 | entryCommand = "" 130 | leaveCommand = "" 131 | switches = Array(False, False, False) 132 | End If 133 | 134 | path = value ' Store raw path, will resolve in the other loop later depending on switches 135 | 136 | ElseIf Left(key, 12) = "entrycommand" Then 137 | DebugOutput 3, "Parsing entry command: " & line 138 | entryCommand = value 139 | ElseIf Left(key, 12) = "leavecommand" Then 140 | DebugOutput 3, "Parsing leave command: " & line 141 | leaveCommand = value 142 | ElseIf Left(key, 8) = "switches" Then 143 | DebugOutput 3, "Parsing switches: " & line 144 | Dim switchList: switchList = Split(value, ",") 145 | Dim switch 146 | For Each switch In switchList 147 | switch = Trim(LCase(switch)) 148 | If switch = "alwaysrunentry" Then 149 | switches(0) = True 150 | ElseIf switch = "alwaysrunleave" Then 151 | switches(1) = True 152 | ElseIf switch = "dontresolvepath" Then 153 | switches(2) = True 154 | Else 155 | DebugOutput 3, "Ignoring unrecognized switch: " & switch 156 | End If 157 | Next 158 | Else 159 | DebugOutput 3, "Ignoring unrecognized line: " & line 160 | End If 161 | Else 162 | DebugOutput 3, "Ignoring malformed line: " & line 163 | End If 164 | End If 165 | Next 166 | 167 | ' Add the last path if there is one 168 | If path <> "" Then 169 | result.Add path, Array(entryCommand, leaveCommand, switches) 170 | DebugOutput 2, "Added pair - Path: " & path & ", EntryCommand: " & entryCommand & ", LeaveCommand: " & leaveCommand & ", Switches: AlwaysRunEntry=" & switches(0) & ", AlwaysRunLeave=" & switches(1) & ", DontResolvePath=" & switches(2) 171 | End If 172 | 173 | ' Create a new dictionary to store resolved paths 174 | Dim resolvedResult: Set resolvedResult = CreateObject("Scripting.Dictionary") 175 | 176 | ' Parse and resolve paths here after we know the switches so we can do so based on DontResolvePath switch 177 | ' If set to not resolve, then paths like lib:// will not be changed to the absolute drive path 178 | Dim pathKey 179 | For Each pathKey in result.Keys 180 | Dim pathData: pathData = result(pathKey) 181 | Dim resolvedPath 182 | 183 | ' Check if this path should be resolved (based on DontResolvePath switch) 184 | If pathData(2)(2) Then ' switches(2) is DontResolvePath 185 | DebugOutput 3, "Not resolving path: " & pathKey 186 | resolvedPath = pathKey 187 | Else 188 | DebugOutput 3, "Resolving path: " & pathKey 189 | resolvedPath = DOpus.FSUtil.Resolve(pathKey, "j") 190 | DebugOutput 3, " > Resolved to: " & resolvedPath 191 | End If 192 | 193 | ' Add to new dictionary with resolved path 194 | resolvedResult.Add resolvedPath, pathData 195 | Next 196 | 197 | ' Cache the result 198 | Script.vars.Set "CachedFolderCommandPairs", resolvedResult 199 | 200 | Set ParseFolderCommandPairs = resolvedResult 201 | 202 | DebugOutput 2, "----------- END PARSE CONFIGS -----------" 203 | End Function 204 | 205 | Function TerminatePath(p) 206 | TerminatePath = p 207 | DebugOutput 3, " Running TerminatePath function for path: " & p 208 | 209 | If (Len(TerminatePath) > 0) Then 210 | Dim c, pathType, slashToUse 211 | c = Right(TerminatePath, 1) 212 | pathType = DOpus.FSUtil.PathType(TerminatePath) 213 | 214 | If pathType = "ftp" Then 215 | slashToUse = "/" 216 | DebugOutput 4, " > FTP path detected, using forward slash (/)" 217 | Else 218 | slashToUse = "\" 219 | DebugOutput 4, " > Local path detected, using backslash (\)" 220 | End If 221 | 222 | If (c <> "\" And c <> "/" And c <> "*" And c <> "?") Then 223 | TerminatePath = TerminatePath & slashToUse 224 | DebugOutput 4, " > Appending slash - Path is now: " & TerminatePath 225 | ElseIf (c = "\" Or c = "/") And c <> slashToUse Then 226 | ' Replace the existing slash if it's the wrong type 227 | TerminatePath = Left(TerminatePath, Len(TerminatePath) - 1) & slashToUse 228 | DebugOutput 4, " > Replacing slash - Path is now: " & TerminatePath 229 | End If 230 | End If 231 | 232 | DebugOutput 3, " > TerminatePath: Before = " & p & ", After = " & TerminatePath 233 | End Function 234 | 235 | Sub CheckAndExecuteLeaveCommands(oldPath, newPath, sourceTab) 236 | Dim fsu, folderPattern, commandArray 237 | Set fsu = DOpus.FSUtil 238 | 239 | Dim folderCommandPairs: Set folderCommandPairs = ParseFolderCommandPairs() 240 | Dim leaveCommands: Set leaveCommands = CreateObject("Scripting.Dictionary") 241 | 242 | DebugOutput 3, "*****************************************************" 243 | 'DebugOutput 3, "------------------------------------------" 244 | DebugOutput 3, "Testing For Leave Commands:" 245 | 246 | For Each folderPattern In folderCommandPairs.Keys 247 | Dim wildPath: Set wildPath = fsu.NewWild(TerminatePath(folderPattern), "d") 248 | DebugOutput 3, "- Checking With Pattern: " & folderPattern 249 | 250 | commandArray = folderCommandPairs(folderPattern) 251 | Dim alwaysRunLeave: alwaysRunLeave = commandArray(2)(1) ' AlwaysRunLeave switch is the second element in the switches array 252 | DebugOutput 3, " alwaysRunLeave: " & alwaysRunLeave 253 | 254 | ' Check for leaving a matched folder 255 | If wildPath.Match(oldPath) Then 256 | DebugOutput 3, " > Match Found For Old Path -- " & oldPath 257 | 258 | Dim shouldRunLeaveCommand: shouldRunLeaveCommand = False 259 | 260 | If newPath = "" Then 261 | DebugOutput 3, " > New path is empty, will queue leave command" 262 | shouldRunLeaveCommand = True 263 | ElseIf Not wildPath.Match(newPath) Then 264 | DebugOutput 3, " > No Match For New Path, queuing leave command -- " & newPath 265 | shouldRunLeaveCommand = True 266 | ElseIf alwaysRunLeave Then 267 | DebugOutput 3, " > New Path matched so leave command wouldn't have been queued, but queuing anyway because AlwaysRunLeave is True" 268 | shouldRunLeaveCommand = True 269 | Else 270 | DebugOutput 3, " > Match found for new path and AlwaysRunLeave is False, not queuing leave command: " & newPath 271 | End If 272 | 273 | If shouldRunLeaveCommand Then 274 | If commandArray(1) <> "" Then 275 | DebugOutput 3, "Queuing leave command for path: " & folderPattern 276 | leaveCommands.Add folderPattern, commandArray(1) 277 | Else 278 | DebugOutput 3, "Tried to run leave command, but no leave command set for pattern: " & folderPattern 279 | End If 280 | End If 281 | Else 282 | DebugOutput 3, " > No match for oldPath, not queuing leave command" 283 | End If 284 | Next 285 | 286 | ' Execute leave commands 287 | For Each folderPattern In leaveCommands.Keys 288 | DebugOutput 3, "------------------------------------------" 289 | DebugOutput 2, "Running leave command for path: " & folderPattern 290 | DebugOutput 2, " Leave command: " & leaveCommands(folderPattern) 291 | 292 | Dim leaveCmd 293 | Set leaveCmd = DOpus.Create.Command 294 | leaveCmd.SetSourceTab sourceTab 295 | leaveCmd.RunCommand leaveCommands(folderPattern) 296 | Next 297 | End Sub 298 | 299 | Sub QueueEntryCommands(oldPath, newPath) 300 | Dim fsu, folderPattern, commandArray 301 | Set fsu = DOpus.FSUtil 302 | 303 | Dim folderCommandPairs: Set folderCommandPairs = ParseFolderCommandPairs() 304 | Dim enterCommands: Set enterCommands = CreateObject("Scripting.Dictionary") 305 | 306 | DebugOutput 3, "*****************************************************" 307 | DebugOutput 3, "Testing For Entry Commands: " 308 | 309 | For Each folderPattern In folderCommandPairs.Keys 310 | Dim wildPath: Set wildPath = fsu.NewWild(TerminatePath(folderPattern), "d") 311 | DebugOutput 3, "- Checking With Pattern: " & folderPattern 312 | 313 | commandArray = folderCommandPairs(folderPattern) 314 | Dim alwaysRunEntry: alwaysRunEntry = commandArray(2)(0) ' AlwaysRunEntry switch is the first element in the switches array 315 | DebugOutput 3, " alwaysRunEntry: " & alwaysRunEntry 316 | 317 | Dim shouldQueueCommand 318 | shouldQueueCommand = False 319 | 320 | ' Check for entering a matched folder 321 | If wildPath.Match(newPath) Then 322 | DebugOutput 3, " > Match Found For New Path -- " & newPath 323 | 324 | If oldPath = "" Then 325 | DebugOutput 3, "oldPath is empty - No need to check if still inside rule match, will queue entry command" 326 | shouldQueueCommand = True 327 | ElseIf Not wildPath.Match(oldPath) Then 328 | DebugOutput 3, " > No Match For Old Path, queuing command -- " & oldPath 329 | shouldQueueCommand = True 330 | ElseIf alwaysRunEntry Then 331 | DebugOutput 3, " > Old path matched so entry command would not have been queued, but queuing anyway because AlwaysRunEntry is True" 332 | shouldQueueCommand = True 333 | Else 334 | DebugOutput 3, " > Match Found For Old Path and AlwaysRunEntry is False, not queuing entry command -- " & oldPath 335 | End If 336 | Else 337 | DebugOutput 3, " > No Match For New Path, not queuing entry command -- " & newPath 338 | End If 339 | 340 | ' Queue entry command if applicable 341 | If shouldQueueCommand Then 342 | If commandArray(0) <> "" Then 343 | DebugOutput 3, "Queuing entry command for path: " & folderPattern 344 | enterCommands.Add folderPattern, commandArray(0) 345 | Else 346 | DebugOutput 3, "Tried to run entry command, but no entry command set for pattern: " & folderPattern 347 | End If 348 | End If 349 | Next 350 | 351 | ' Store enter commands in Script.vars for later execution 352 | Script.vars.Set "PendingEntryCommands", enterCommands 353 | End Sub 354 | 355 | Sub ExecuteQueuedEntryCommands(sourceTab) 356 | If Script.vars.Exists("PendingEntryCommands") Then 357 | Dim enterCommands: Set enterCommands = Script.vars.Get("PendingEntryCommands") 358 | 359 | ' Check if enterCommands is not empty 360 | If enterCommands.Count > 0 Then 361 | DebugOutput 3, "------------------------------------------" 362 | ' Execute entry commands 363 | Dim folderPattern 364 | For Each folderPattern In enterCommands.Keys 365 | DebugOutput 2, "Running entry command for path: " & folderPattern 366 | DebugOutput 2, " Entry command: " & enterCommands(folderPattern) 367 | 368 | Dim enterCmd 369 | Set enterCmd = DOpus.Create.Command 370 | enterCmd.SetSourceTab sourceTab 371 | enterCmd.RunCommand enterCommands(folderPattern) 372 | Next 373 | 374 | ' Clear pending entry commands 375 | Script.vars.Delete "PendingEntryCommands" 376 | Else 377 | DebugOutput 3, "OnAfterFolderChange - No entry commands were run. (PendingEntryCommands was empty)" 378 | End If 379 | Else 380 | DebugOutput 3, "OnAfterFolderChange - No entry commands were run. (PendingEntryCommands was not set)" 381 | End If 382 | End Sub 383 | 384 | Function OnBeforeFolderChange(beforeFolderChangeData) 385 | If Script.config.DebugLevel >= 2 Then 386 | DOpus.Output "===================================== Folder Change =====================================" 387 | End If 388 | 389 | Dim currentPath, newPath 390 | currentPath = TerminatePath(beforeFolderChangeData.tab.path) 391 | newPath = TerminatePath(beforeFolderChangeData.path) 392 | 393 | DebugOutput 2, "OnBeforeFolderChange - Current path : " & currentPath 394 | DebugOutput 2, "OnBeforeFolderChange - New path : " & newPath 395 | 396 | ' Execute leave commands 397 | DebugOutput 2, "------------------------------------------" 398 | CheckAndExecuteLeaveCommands currentPath, newPath, beforeFolderChangeData.tab 399 | 400 | ' Queue entry commands for execution in OnAfterFolderChange 401 | QueueEntryCommands currentPath, newPath 402 | 403 | ' Allow the folder change to proceed 404 | OnBeforeFolderChange = False 405 | End Function 406 | 407 | Function OnAfterFolderChange(afterFolderChangeData) 408 | If Not afterFolderChangeData.result Then 409 | DebugOutput 2, "Folder change failed, not executing entry commands" 410 | Exit Function 411 | End If 412 | 413 | ExecuteQueuedEntryCommands afterFolderChangeData.tab 414 | End Function 415 | 416 | Function OnActivateTab(activateTabData) 417 | If Script.config.DebugLevel >= 2 Then 418 | DOpus.Output "===================================== Tab Activation =====================================" 419 | End If 420 | 421 | Dim oldPath, newPath 422 | 423 | If Not activateTabData.oldtab Is Nothing Then 424 | 'DOpus.Output("OldTab: " + activateTabData.oldtab) 425 | 'DOpus.Output("OldTab Type: " + DOpus.TypeOf(activateTabData.oldtab)) 426 | 'DOpus.Output("OldTab Path" + activateTabData.oldtab.Path) 427 | oldPath = TerminatePath(activateTabData.oldtab.Path) 428 | Else 429 | oldPath = "" 430 | End If 431 | 432 | If Not activateTabData.newtab Is Nothing Then 433 | newPath = TerminatePath(activateTabData.newtab.Path) 434 | Else 435 | newPath = "" 436 | End If 437 | 438 | DebugOutput 2, "OnActivateTab - Old path: " & oldPath 439 | DebugOutput 2, "OnActivateTab - New path: " & newPath 440 | 441 | ' Execute leave commands 442 | CheckAndExecuteLeaveCommands oldPath, newPath, activateTabData.oldtab 443 | 444 | ' Queue and execute entry commands immediately for tab activation 445 | QueueEntryCommands oldPath, newPath 446 | ExecuteQueuedEntryCommands activateTabData.newtab 447 | End Function 448 | -------------------------------------------------------------------------------- /Script Add-Ins/SplitViewArrow.js: -------------------------------------------------------------------------------- 1 | // Automatically sets lister background to point from the source to the destination side of a dual lister view 2 | // Allows setting different images for dark/light mode. 3 | 4 | // Still To Do: 5 | // - Add proper config setting of variables 6 | // - See if there are any possible universal default images to use 7 | // - Update the arrows if the swap button is used 8 | 9 | // ------------ 10 | 11 | //upImage = "Up-Vector-Arrow.svg"; 12 | //downImage = "Down-Vector-Arrow.svg"; 13 | //leftImage = "Left-Vector-Arrow.svg"; 14 | //rightImage = "Right-Vector-Arrow.svg"; 15 | 16 | // Set arrow file names, or whatever you want to use to point in a certain direction 17 | var upImage = "Up-Gray-Arrow.png"; 18 | var downImage = "Down-Gray-Arrow.png"; 19 | var leftImage = "Left-Gray-Arrow.png"; 20 | var rightImage = "Right-Gray-Arrow.png"; 21 | 22 | // Options 23 | var opacityPercent = "20"; 24 | var useFill = false; 25 | var lightModeFill = ""; 26 | var darkModeFill = ""; 27 | 28 | // Set directory with the images. Defaults to folder called "BG_Images" within the "User Data" directory opus directory 29 | var imagesDir = "/dopusdata\\User Data\\BG_Images\\" 30 | 31 | // ------------------------- Don't change anything below this line ------------------------- 32 | 33 | var wasInDualMode = false; 34 | 35 | function OnInit(initData) { 36 | initData.name = "Split View Arrow Indicator"; 37 | initData.desc = "Displays an arrow (or any custom image) pointing to the destination (non-active) lister in split view."; 38 | initData.copyright = "ThioJoe" 39 | initData.version = "0.1" // Work in progress 40 | initData.min_version = "12.0" 41 | initData.default_enable = true; 42 | return false; 43 | } 44 | 45 | function OnActivateLister(data) { 46 | //DOpus.Output("OnActivateLister called"); 47 | // If it's not the active lister we don't care 48 | if (data.active == false) { 49 | return; 50 | } 51 | //DOpus.Output("Lister is in dual mode: " + data.lister.dual); 52 | if (data.lister.dual) { 53 | updateArrow(data.lister); 54 | } else if (wasInDualMode == true) { 55 | removeArrow(data.lister); 56 | } 57 | } 58 | 59 | function OnActivateTab(data) { 60 | //DOpus.Output("OnActivateTab called"); 61 | // Only update if the Lister containing the new active tab is in dual mode 62 | if (data.newtab && data.newtab.lister && data.newtab.lister.dual) { 63 | updateArrow(data.newtab.lister); 64 | } 65 | } 66 | 67 | function OnSourceDestChange(data) { 68 | //DOpus.Output("OnSourceDestChange called"); 69 | if (data.tab.lister.dual) { 70 | //DOpus.Output("Dual Type: " + data.tab.lister.dual); 71 | updateArrow(data.tab.lister); 72 | } 73 | } 74 | 75 | function updateArrow(lister) { 76 | //DOpus.Output("updateArrow called"); 77 | wasInDualMode = true; 78 | 79 | // Check if Opus is in dark mode 80 | var isDarkMode = DOpus.Create.SysInfo().DarkMode; 81 | 82 | var fillColorString="" 83 | if (isDarkMode && useFill == true) { 84 | fillColorString = ",fillcolor:" + darkModeFill; 85 | } else if (useFill == true) { 86 | fillColorString = ",fillcolor:" + lightModeFill; 87 | } 88 | DOpus.output("Fill Color String: " + fillColorString); 89 | 90 | var imageName; 91 | if (lister.dual === 2) { // Horizontal split 92 | imageName = (lister.activetab.right) ? upImage : downImage; 93 | } else if (lister.dual === 1) { // Vertical split 94 | imageName = (lister.activetab.right) ? leftImage : rightImage; 95 | 96 | } else { 97 | // Handle the case where the Lister is not in dual mode, if needed 98 | imageName = ""; // Or set a default image if appropriate 99 | } 100 | 101 | var imagePath = DOpus.FSUtil.Resolve(imagesDir + imageName); 102 | //DOpus.Output("Image path: " + imagePath); 103 | var cmd = DOpus.create.Command(); 104 | var commandString = 'Set BACKGROUNDIMAGE="filedisplay:' + imagePath + '" BACKGROUNDIMAGEOPTS=shared,center,nofade,local,opacity:' + opacityPercent + fillColorString; 105 | //DOpus.Output("Command String: " + commandString); 106 | 107 | cmd.RunCommand(commandString); 108 | } 109 | 110 | function removeArrow(lister) { 111 | //DOpus.Output("removeArrow called"); 112 | var cmd = DOpus.Create.Command(); 113 | //cmd.SetSourceTab(lister.activetab); //Not sure if this is necessary 114 | var commandString = 'Set BACKGROUNDIMAGE="all:" BACKGROUNDIMAGEOPTS=nofade,local,reset' 115 | //DOpus.Output("Removal Command String: " + commandString); 116 | cmd.RunCommand(commandString); 117 | wasInDualMode = false; 118 | } 119 | 120 | function OnListerUIChange(data) { 121 | //DOpus.Output("OnListerUIChange called"); 122 | //DOpus.Output("Change type: " + data.change); 123 | if (data.change == "dual" || data.change == "duallayout") { 124 | if (data.lister.dual) { 125 | updateArrow(data.lister); 126 | } else { 127 | removeArrow(data.lister); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /User Command Scripts/Combine_To_TXT_File.js: -------------------------------------------------------------------------------- 1 | // User command to combine selected files into a single text file with several options 2 | // 3 | // Argument Template: 4 | // EXCLUDE_HEADERS/S, USE_RELATIVE_PATH/S, HIDE_EXTENSIONS/S, DEBUG_MODE/S, SORT_TYPE/K[name,date,name-reverse,date-reverse,directory], OUTPUT_UP_ONE/S, OUTPUT/K, INPUT_DIR/O, NO_FINISHED_DIALOG/S 5 | // 6 | // Arguments: 7 | // EXCLUDE_HEADERS (Switch): Do not add name of files before the contents of each file in the final combined text file. 8 | // USE_RELATIVE_PATH (Switch): Use relative paths for headers instead of just file names. 9 | // HIDE_EXTENSIONS (Switch): Hide file extensions in the headers. 10 | // DEBUG_MODE (Switch): Only print headers without file contents, useful for debugging sorting and file selection. 11 | // SORT_TYPE (Keyword): Specify the sorting type for the files. Options are: 12 | // name: Sort by file name in ascending order. 13 | // date: Sort by modification date in ascending order. 14 | // name-reverse: Sort by file name in descending order. 15 | // date-reverse: Sort by modification date in descending order. 16 | // directory: Sort by directory structure with files first. 17 | // OUTPUT_UP_ONE (Switch): Output the resulting file one directory higher than the current directory (parent directory). 18 | // OUTPUT (String): Specify the output path. Can be a full path, relative path, or just the file name. If set, it overrides OUTPUT_UP_ONE. 19 | // INPUT_DIR (Optional - Include with or without assigned value): Input a path to a folder. If no value is set, use the selected folder(s) in Directory Opus. Can be used to specify a folder for combining all files within it. 20 | // NO_FINISHED_DIALOG (Switch): Hide the completion dialog that shows the output path when the process is finished. 21 | 22 | 23 | function OnClick(clickData) { 24 | var sortType = ""; // Default to empty to determine if argument was passed 25 | var excludeHeaders = false; 26 | var useRelativePath = false; 27 | var hideExtensions = false; 28 | var debugMode = false; 29 | var outputUpOne = false; 30 | var noFinishedDialog = false; // Whether to hide the completion dialog that shows output path 31 | var outputPath = clickData.func.sourcetab.path; // Default output path 32 | var outputFileName = ""; // Default to empty to check if argument was passed 33 | var inputDir = ""; // Default to empty to check if argument was passed 34 | 35 | // Parse optional arguments if they're there 36 | if (clickData.func.args.got_arg.SORT_TYPE) { 37 | sortType = clickData.func.args.SORT_TYPE; 38 | } 39 | if (clickData.func.args.got_arg.EXCLUDE_HEADERS) { 40 | excludeHeaders = clickData.func.args.EXCLUDE_HEADERS; 41 | } 42 | if (clickData.func.args.got_arg.NO_FINISHED_DIALOG) { 43 | noFinishedDialog = clickData.func.args.NO_FINISHED_DIALOG; 44 | } 45 | if (clickData.func.args.got_arg.USE_RELATIVE_PATH) { 46 | useRelativePath = clickData.func.args.USE_RELATIVE_PATH; 47 | } 48 | if (clickData.func.args.got_arg.HIDE_EXTENSIONS) { 49 | hideExtensions = clickData.func.args.HIDE_EXTENSIONS; 50 | } 51 | if (clickData.func.args.got_arg.DEBUG_MODE) { 52 | debugMode = clickData.func.args.DEBUG_MODE; 53 | } 54 | if (clickData.func.args.got_arg.OUTPUT_UP_ONE) { 55 | outputUpOne = clickData.func.args.OUTPUT_UP_ONE; 56 | } 57 | if (clickData.func.args.got_arg.OUTPUT) { 58 | outputFileName = clickData.func.args.OUTPUT; 59 | } 60 | if (clickData.func.args.got_arg.INPUT_DIR) { 61 | inputDir = clickData.func.args.INPUT_DIR; 62 | DOpus.Output("Got inputDir Value: " + inputDir); 63 | DOpus.Output("inputDir Type: " + typeof(inputDir)); 64 | } 65 | 66 | var sortOption = 0; // Initialize to zero to check if it was set later 67 | 68 | if (!sortType) { 69 | var dlg = clickData.func.Dlg; 70 | dlg.window = clickData.func.sourcetab; 71 | dlg.title = "Concatenate Text Files"; 72 | dlg.message = "Choose sorting option mode and options."; 73 | dlg.buttons = "Name|Date|Name Reverse|Date Reverse|Directory Structure|Cancel"; 74 | dlg.icon = "question"; 75 | 76 | // Add checkbox options 77 | dlg.options[0].label = "Exclude file divider headers in combined file."; 78 | dlg.options[0].state = excludeHeaders; 79 | dlg.options[1].label = "Use relative path for headers instead of just file name."; 80 | dlg.options[1].state = useRelativePath; 81 | dlg.options[2].label = "Hide file extensions in headers"; 82 | dlg.options[2].state = hideExtensions; 83 | dlg.options[3].label = "[Debug] Sorting debug mode (only print headers, no file contents)"; 84 | dlg.options[3].state = debugMode; 85 | 86 | var result = dlg.Show(); 87 | 88 | if (result == 0) { 89 | return; // Cancel button clicked 90 | } 91 | sortOption = result; // 1: Name, 2: Date, 3: Name Reverse, 4: Date Reverse, 5: Directory Structure 92 | excludeHeaders = dlg.options[0].state; 93 | useRelativePath = dlg.options[1].state; 94 | hideExtensions = dlg.options[2].state; 95 | debugMode = dlg.options[3].state; 96 | } else { 97 | // Convert sortType string to sortOption number 98 | switch (sortType.toLowerCase()) { 99 | case "name": 100 | sortOption = 1; 101 | break; 102 | case "date": 103 | sortOption = 2; 104 | break; 105 | case "name-reverse": 106 | sortOption = 3; 107 | break; 108 | case "date-reverse": 109 | sortOption = 4; 110 | break; 111 | case "directory": 112 | sortOption = 5; 113 | break; 114 | default: 115 | DOpus.Output("Invalid sort type provided."); 116 | return; 117 | } 118 | } 119 | 120 | var filesArray = []; 121 | 122 | // Determine the source of the files: selected files or input directory 123 | if (inputDir) { 124 | if (typeof inputDir === "string") { 125 | // Use the provided directory 126 | var folderEnum = DOpus.FSUtil.ReadDir(inputDir, "r"); 127 | while (!folderEnum.complete) { 128 | var item = folderEnum.next(); 129 | if (item.is_dir) { 130 | continue; 131 | } 132 | var inputDirPathObj = DOpus.FSUtil.NewPath(inputDir); 133 | filesArray.push({ 134 | name: item.name, 135 | path: item.realpath, 136 | date: item.modify, 137 | relativePath: getRelativePath(item, inputDirPathObj) 138 | }); 139 | } 140 | } else { 141 | // Use the selected folders 142 | var selectedDirs = clickData.func.sourcetab.selected_dirs; 143 | var e = new Enumerator(selectedDirs); 144 | for (; !e.atEnd(); e.moveNext()) { 145 | var folder = e.item(); 146 | var folderEnum = DOpus.FSUtil.ReadDir(folder.realpath, "r"); 147 | while (!folderEnum.complete) { 148 | var item = folderEnum.next(); 149 | if (item.is_dir) { 150 | continue; 151 | } 152 | filesArray.push({ 153 | name: item.name, 154 | path: item.realpath, 155 | date: item.modify, 156 | relativePath: getRelativePath(item, folder.realpath) 157 | }); 158 | } 159 | } 160 | } 161 | } else { 162 | var selectedFiles = clickData.func.sourcetab.selected_files; 163 | if (selectedFiles.count == 0) { 164 | DOpus.Output("No files selected"); 165 | return; 166 | } 167 | var e = new Enumerator(selectedFiles); 168 | for (; !e.atEnd(); e.moveNext()) { 169 | var item = e.item(); 170 | filesArray.push({ 171 | name: item.name, 172 | path: item.realpath, 173 | date: item.modify, 174 | relativePath: getRelativePath(item, clickData.func.sourcetab.path) 175 | }); 176 | } 177 | } 178 | 179 | // Sort the files based on the selected option 180 | sortFiles(filesArray, sortOption, clickData.func.sourcetab.path); 181 | 182 | // Determine the output path and file name 183 | var fso = new ActiveXObject("Scripting.FileSystemObject"); 184 | if (outputFileName) { 185 | // Check if outputFileName is a full path 186 | if (fso.FolderExists(fso.GetParentFolderName(outputFileName)) || fso.FileExists(outputFileName)) { 187 | outputPath = fso.GetParentFolderName(outputFileName); 188 | outputFileName = fso.GetFileName(outputFileName); 189 | } else if (outputFileName.indexOf("\\") != -1 || outputFileName.indexOf("/") != -1) { 190 | outputPath = clickData.func.sourcetab.path + "\\" + fso.GetParentFolderName(outputFileName); 191 | outputFileName = fso.GetFileName(outputFileName); 192 | } else { 193 | outputPath = clickData.func.sourcetab.path; 194 | } 195 | } else { 196 | if (outputUpOne) { 197 | outputPath = DOpus.FSUtil.Resolve(clickData.func.sourcetab.path + "\\.."); 198 | } 199 | outputFileName = (debugMode ? "DebugSortedList" : "CombinedTextFile") + ".txt"; 200 | } 201 | 202 | var outputFilePath = outputPath + "\\" + outputFileName; 203 | 204 | // Initialize the Progress object 205 | var progress = clickData.func.command.progress; 206 | progress.abort = true; 207 | progress.owned = true; 208 | progress.delay = false; 209 | progress.bytes = false; 210 | progress.Init(clickData.func.sourcetab, debugMode ? "Generating Debug List" : "Concatenating Text Files"); 211 | 212 | // Calculate total byte size of selected files (only if not in debug mode) 213 | var totalBytes = 0; 214 | if (!debugMode) { 215 | for (var i = 0; i < filesArray.length; i++) { 216 | var file = filesArray[i]; 217 | var fileSize = fso.GetFile(file.path).Size; 218 | totalBytes += fileSize; 219 | } 220 | } 221 | 222 | progress.SetFiles(filesArray.length); 223 | progress.Show(); 224 | 225 | var outputFile = fso.CreateTextFile(outputFilePath, true); 226 | var separator = "----------------------------------------"; 227 | var bytesProcessed = 0; 228 | 229 | // Remove the SetPercent call and update the progress with StepFiles 230 | for (var i = 0; i < filesArray.length; i++) { 231 | var file = filesArray[i]; 232 | progress.SetName(file.name); 233 | progress.SetType("file"); 234 | var header; 235 | if (useRelativePath) { 236 | header = "\\" + file.relativePath; 237 | if (hideExtensions) { 238 | header = header.replace(/\.[^\\]*$/, ''); 239 | } 240 | } else { 241 | header = hideExtensions ? file.name.replace(/\.[^\.]*$/, '') : file.name; 242 | } 243 | if (debugMode) { 244 | outputFile.WriteLine(header); 245 | } else { 246 | if (!excludeHeaders) { 247 | outputFile.WriteLine(separator + " " + header + " " + separator); 248 | } 249 | var inputFile = fso.OpenTextFile(file.path, 1); 250 | var content = inputFile.ReadAll(); 251 | var fileSize = fso.GetFile(file.path).Size; 252 | inputFile.Close(); 253 | outputFile.WriteLine(content); 254 | if (!excludeHeaders) { 255 | outputFile.WriteLine(); // Add a blank line between files 256 | } 257 | bytesProcessed += fileSize; 258 | } 259 | progress.StepFiles(1); 260 | 261 | if (progress.GetAbortState() == "a") { 262 | outputFile.Close(); 263 | fso.DeleteFile(outputFilePath); 264 | progress.Hide(); // Close the progress window 265 | return; 266 | } 267 | } 268 | 269 | outputFile.Close(); 270 | 271 | progress.Hide(); // Close the progress window 272 | 273 | if (!noFinishedDialog) { 274 | var finalDlg = clickData.func.Dlg; 275 | finalDlg.window = clickData.func.sourcetab; 276 | finalDlg.message = debugMode ? 277 | "Debug list generated successfully.\nOutput file: " + outputFilePath : 278 | "Text files concatenated successfully.\nOutput file: " + outputFilePath; 279 | finalDlg.buttons = "OK"; 280 | finalDlg.icon = "info"; 281 | finalDlg.Show(); 282 | } 283 | } 284 | 285 | function getRelativePath(item, sourcePath) { 286 | var sourcePathDepth = sourcePath.Split.count; 287 | var relativePath = DOpus.FSUtil.NewPath(); 288 | relativePath.set(item.realpath.Split(sourcePathDepth)); 289 | return String(relativePath); 290 | } 291 | 292 | function sortFiles(filesArray, sortOption, sourcePath) { 293 | switch (sortOption) { 294 | case 1: // Name 295 | filesArray.sort(function(a, b) { 296 | return a.name.localeCompare(b.name); 297 | }); 298 | break; 299 | case 2: // Date 300 | filesArray.sort(function(a, b) { 301 | return b.date.compare(a.date); 302 | }); 303 | break; 304 | case 3: // Name Reverse 305 | filesArray.sort(function(a, b) { 306 | return b.name.localeCompare(a.name); 307 | }); 308 | break; 309 | case 4: // Date Reverse 310 | filesArray.sort(function(a, b) { 311 | return a.date.compare(b.date); 312 | }); 313 | break; 314 | case 5: // Directory Structure with Files First 315 | filesArray.sort(function(a, b) { 316 | var aParts = a.relativePath.split("\\"); 317 | var bParts = b.relativePath.split("\\"); 318 | var minDepth = Math.min(aParts.length, bParts.length); 319 | 320 | for (var i = 0; i < minDepth; i++) { 321 | if (i === aParts.length - 1 && i === bParts.length - 1) { 322 | // Both are files in the same directory 323 | return aParts[i].localeCompare(bParts[i]); 324 | } 325 | if (i === aParts.length - 1) { 326 | // a is a file, b is a directory or a file in a subdirectory 327 | return -1; 328 | } 329 | if (i === bParts.length - 1) { 330 | // b is a file, a is a directory or a file in a subdirectory 331 | return 1; 332 | } 333 | // Compare directory names 334 | var comp = aParts[i].localeCompare(bParts[i]); 335 | if (comp !== 0) return comp; 336 | } 337 | 338 | // If we get here, one path is a subset of the other 339 | return aParts.length - bParts.length; 340 | }); 341 | break; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /User Command Scripts/Copy_File_Names_Auto_Quoted.js: -------------------------------------------------------------------------------- 1 | // Copies files or folders names to clipboard with quotes around it if it has spaces. Various optional behavior for multiple selected items 2 | // By ThioJoe 3 | // Updated: 3/15/2025 4 | 5 | // Arguments Template (Must put this in the 'Template' box in the command editor to use arguments): 6 | // MULTILINE_QUOTE_MODE/K[auto,always,never],SINGLE_ITEM_QUOTE_MODE/K[auto,always,never],PREPEND_INSIDE_QUOTES/K,PREPEND_OUTSIDE_QUOTES/K,APPEND_INSIDE_QUOTES/K,APPEND_OUTSIDE_QUOTES/K 7 | 8 | // Example usage of arguments: 9 | // Copy_File_Names_Auto_Quoted MULTILINE_QUOTE_MODE="never" SINGLE_ITEM_QUOTE_MODE="auto" PREPEND_OUTSIDE_QUOTES="prefix_" APPEND_INSIDE_QUOTES="_suffix" 10 | 11 | function OnClick(clickData) { 12 | //------------ Options (Note: If arguments are used when calling the script, these values will be overrided by the arguments) ------------ 13 | // multiLineQuoteMode: Affects how quotes are added to each file name when multiple are selected 14 | // Set to 'never' to never add quotes when multiple selected. 'always' to add to all lines. 'auto' to add for lines with spaces in file name 15 | // > Optional Argument Name: MULTILINE_QUOTE_MODE (string value) 16 | var multiLineQuoteMode = "never"; 17 | // singleItemQuoteMode: Affects how quotes are added to each file name when a single file/folder is selected 18 | // > Optional Argument Name: SINGLE_ITEM_QUOTE_MODE (string value) 19 | var singleItemQuoteMode = "auto"; 20 | // prependInsideQuotes: String to prepend to each file name inside quotes 21 | // > Optional Argument Name: PREPEND_INSIDE_QUOTES (string value) 22 | var prependInsideQuotes = ""; 23 | // prependOutsideQuotes: String to prepend to each file name outside quotes 24 | // > Optional Argument Name: PREPEND_OUTSIDE_QUOTES (string value) 25 | var prependOutsideQuotes = ""; 26 | // appendInsideQuotes: String to append to each file name inside quotes 27 | // > Optional Argument Name: APPEND_INSIDE_QUOTES (string value) 28 | var appendInsideQuotes = ""; 29 | // appendOutsideQuotes: String to append to each file name outside quotes 30 | // > Optional Argument Name: APPEND_OUTSIDE_QUOTES (string value) 31 | var appendOutsideQuotes = ""; 32 | //--------------------------------- 33 | 34 | var tab = clickData.func.sourcetab; 35 | var selectedItems = tab.selected; 36 | clickData.func.command.deselect = false 37 | 38 | if (selectedItems.count == 0) { 39 | return; // No files selected, nothing to do. 40 | } 41 | 42 | if (clickData.func.args.got_arg.SINGLE_ITEM_QUOTE_MODE) { 43 | //Validate argument value 44 | argString = clickData.func.args.SINGLE_ITEM_QUOTE_MODE.toLowerCase(); 45 | if (argString == "never" || argString == "always" || argString == "auto") { 46 | singleItemQuoteMode = argString; 47 | } else { 48 | singleItemQuoteMode = "auto"; 49 | DOpus.Output("ERROR: Invalid SINGLE_ITEM_QUOTE_MODE argument. Must be either 'never', 'always', or 'auto'. Got: " + argString); 50 | } 51 | //DOpus.Output("Received SINGLE_ITEM_QUOTE_MODE argument: " + String(clickData.func.args.SINGLE_ITEM_QUOTE_MODE)); 52 | } 53 | 54 | if (clickData.func.args.got_arg.MULTILINE_QUOTE_MODE) { 55 | //Validate argument value 56 | argString = clickData.func.args.MULTILINE_QUOTE_MODE.toLowerCase(); 57 | if (argString == "never" || argString == "always" || argString == "auto") { 58 | multiLineQuoteMode = argString; 59 | } else { 60 | multiLineQuoteMode = "never"; 61 | DOpus.Output("ERROR: Invalid MULTILINE_QUOTE_MODE argument. Must be either 'never', 'always', or 'auto'. Got: " + argString); 62 | } 63 | //DOpus.Output("Received MULTILINE_QUOTE_MODE argument: " + String(clickData.func.args.MULTILINE_QUOTE_MODE)); 64 | } 65 | 66 | if (clickData.func.args.got_arg.PREPEND_INSIDE_QUOTES) { 67 | prependInsideQuotes = clickData.func.args.PREPEND_INSIDE_QUOTES; 68 | //DOpus.Output("Received PREPEND_INSIDE_QUOTES argument: " + String(prependInsideQuotes)); 69 | } 70 | 71 | if (clickData.func.args.got_arg.PREPEND_OUTSIDE_QUOTES) { 72 | prependOutsideQuotes = clickData.func.args.PREPEND_OUTSIDE_QUOTES; 73 | //DOpus.Output("Received PREPEND_OUTSIDE_QUOTES argument: " + String(prependOutsideQuotes)); 74 | } 75 | 76 | if (clickData.func.args.got_arg.APPEND_INSIDE_QUOTES) { 77 | appendInsideQuotes = clickData.func.args.APPEND_INSIDE_QUOTES; 78 | //DOpus.Output("Received APPEND_INSIDE_QUOTES argument: " + String(appendInsideQuotes)); 79 | } 80 | 81 | if (clickData.func.args.got_arg.APPEND_OUTSIDE_QUOTES) { 82 | appendOutsideQuotes = clickData.func.args.APPEND_OUTSIDE_QUOTES; 83 | //DOpus.Output("Received APPEND_OUTSIDE_QUOTES argument: " + String(appendOutsideQuotes)); 84 | } 85 | 86 | multiLineQuoteMode = multiLineQuoteMode.toLowerCase(); 87 | singleItemQuoteMode = singleItemQuoteMode.toLowerCase(); 88 | 89 | var clipboardText = ""; 90 | // If single item is selected 91 | if (selectedItems.count == 1) { 92 | var singleItem = selectedItems(0); 93 | // If no spaces in the filename or argument given to not use quotes 94 | if (singleItemQuoteMode != "always" && (singleItem.name.indexOf(" ") == -1 || singleItemQuoteMode == "never")) { 95 | clipboardText = prependOutsideQuotes + prependInsideQuotes + singleItem.name + appendInsideQuotes + appendOutsideQuotes; 96 | } else { 97 | // Filename contains spaces 98 | clipboardText = prependOutsideQuotes + '"' + prependInsideQuotes + singleItem.name + appendInsideQuotes + '"' + appendOutsideQuotes; 99 | } 100 | // Multiple files are selected 101 | } else { 102 | for (var i = 0; i < selectedItems.count; i++) { 103 | var fileName = selectedItems(i).name; 104 | //Add a newline character to the beginning starting after the first line 105 | if (i > 0) { 106 | clipboardText += "\n"; 107 | } 108 | if (multiLineQuoteMode === "always") { 109 | clipboardText += prependOutsideQuotes + '"' + prependInsideQuotes + fileName + appendInsideQuotes + '"' + appendOutsideQuotes; 110 | } else if (multiLineQuoteMode === "auto" && fileName.indexOf(" ") !== -1) { 111 | clipboardText += prependOutsideQuotes + '"' + prependInsideQuotes + fileName + appendInsideQuotes + '"' + appendOutsideQuotes; 112 | } else { 113 | clipboardText += prependOutsideQuotes + prependInsideQuotes + fileName + appendInsideQuotes + appendOutsideQuotes; 114 | } 115 | } 116 | } 117 | 118 | DOpus.SetClip(clipboardText); 119 | // For debugging: 120 | //DOpus.Output("--- Copied to clipboard: ---\n" + clipboardText); 121 | } 122 | -------------------------------------------------------------------------------- /User Command Scripts/Copy_File_Paths_Auto_Quoted.js: -------------------------------------------------------------------------------- 1 | // Copies files or folders full paths to clipboard with quotes around it if it has spaces. Various optional behavior for multiple selected items 2 | // By ThioJoe 3 | // Updated: 3/15/25 4 | 5 | // Discussion Thread / Latest Version: https://resource.dopus.com/t/scripts-to-copy-file-folder-name-s-or-path-s-with-automatic-surrounding-quotes-based-on-spaces/51122 6 | // Updates also available on my GitHub repo: https://github.com/ThioJoe/D-Opus-Scripts 7 | 8 | // Arguments Template (Must put this in the 'Template' box in the command editor to use arguments): 9 | // MULTILINE_QUOTE_MODE/K[auto,always,never],SINGLE_ITEM_QUOTE_MODE/K[auto,always,never],RESOLVE_SYMLINKS/K[none,symlinkedFiles,symlinkedFolders,both],FOLDER_TERMINATOR/O,PREPEND_INSIDE_QUOTES/K,PREPEND_OUTSIDE_QUOTES/K,APPEND_INSIDE_QUOTES/K,APPEND_OUTSIDE_QUOTES/K 10 | 11 | // Example usage of arguments: 12 | // Copy_File_Paths_Auto_Quoted MULTILINE_QUOTE_MODE="never" SINGLE_ITEM_QUOTE_MODE="auto" FOLDER_TERMINATOR="\" RESOLVE_SYMLINKS="both" PREPEND_OUTSIDE_QUOTES="prefix_" APPEND_INSIDE_QUOTES="_suffix" 13 | 14 | function OnClick(clickData) { 15 | //------------ Options (Note: If arguments are used when calling the script, these values will be overrided by the arguments) ------------ 16 | // multiLineQuoteMode: Affects how quotes are added around each file path when multiple are selected 17 | // Set to 'never' to never add quotes when multiple selected. 'always' to add to all lines. 'auto' to add for lines with spaces in file path 18 | // > Optional Argument Name: MULTILINE_QUOTE_MODE (string value) 19 | var multiLineQuoteMode = "never"; 20 | // Which trailing path terminator to add (if any) to the end of full paths of folders (basically like the 'noterm' modifier) 21 | // Just set to empty string if none wanted. Don't forget to escape as necessary (to add a backslash would be like: "\\") 22 | // > Optional Argument Name: FOLDER_TERMINATOR (string value) 23 | var includeTrailingTerminator = ""; 24 | // singleItemQuoteMode: Affects how quotes are added to each file name when a single file/folder is selected 25 | // > Optional Argument Name: SINGLE_ITEM_QUOTE_MODE (string value) 26 | var singleItemQuoteMode = "auto"; 27 | // resolveSymlinksAndJunctions: Whether to copy the resulting linked target path instead of the 'virtual' file/folder for symbolic links (can be either files/folders) and junctions (folders). 28 | // Note: Doesn't apply to shortcuts (.lnk files) 29 | // Possible values: none, symlinkedFiles, symlinkedFolders, both 30 | // > Optional Argument Name: RESOLVE_SYMLINKS (string value) 31 | var resolveSymlinks = "none"; 32 | // prependInsideQuotes: String to prepend to each file path inside quotes 33 | // > Optional Argument Name: PREPEND_INSIDE_QUOTES (string value) 34 | var prependInsideQuotes = ""; 35 | // prependOutsideQuotes: String to prepend to each file path outside quotes 36 | // > Optional Argument Name: PREPEND_OUTSIDE_QUOTES (string value) 37 | var prependOutsideQuotes = ""; 38 | // appendInsideQuotes: String to append to each file path inside quotes 39 | // > Optional Argument Name: APPEND_INSIDE_QUOTES (string value) 40 | var appendInsideQuotes = ""; 41 | // appendOutsideQuotes: String to append to each file path outside quotes 42 | // > Optional Argument Name: APPEND_OUTSIDE_QUOTES (string value) 43 | var appendOutsideQuotes = ""; 44 | //--------------------------------- 45 | 46 | var tab = clickData.func.sourcetab; 47 | var selectedItems = tab.selected; 48 | clickData.func.command.deselect = false 49 | 50 | if (selectedItems.count == 0) { 51 | return; // No files selected, nothing to do. 52 | } 53 | 54 | if (clickData.func.args.got_arg.SINGLE_ITEM_QUOTE_MODE) { 55 | //Validate argument value 56 | argString = clickData.func.args.SINGLE_ITEM_QUOTE_MODE.toLowerCase(); 57 | if (argString == "never" || argString == "always" || argString == "auto") { 58 | singleItemQuoteMode = argString; 59 | } else { 60 | singleItemQuoteMode = "auto"; 61 | DOpus.Output("ERROR: Invalid SINGLE_ITEM_QUOTE_MODE argument. Must be either 'never', 'always', or 'auto'. Got: " + argString); 62 | } 63 | //DOpus.Output("Received SINGLE_ITEM_QUOTE_MODE argument: " + String(clickData.func.args.SINGLE_ITEM_QUOTE_MODE)); 64 | } 65 | 66 | if (clickData.func.args.got_arg.MULTILINE_QUOTE_MODE) { 67 | //Validate argument value 68 | argString = clickData.func.args.MULTILINE_QUOTE_MODE.toLowerCase(); 69 | if (argString == "never" || argString == "always" || argString == "auto") { 70 | multiLineQuoteMode = argString; 71 | } else { 72 | multiLineQuoteMode = "never"; 73 | DOpus.Output("ERROR: Invalid MULTILINE_QUOTE_MODE argument. Must be either 'never', 'always', or 'auto'. Got: " + argString); 74 | } 75 | //DOpus.Output("Received MULTILINE_QUOTE_MODE argument: " + String(clickData.func.args.MULTILINE_QUOTE_MODE)); 76 | } 77 | 78 | if (clickData.func.args.got_arg.RESOLVE_SYMLINKS) { 79 | //Validate argument value 80 | argString = clickData.func.args.RESOLVE_SYMLINKS.toLowerCase(); 81 | if (argString == "none" || argString == "symlinkedfiles" || argString == "symlinkedfolders" || argString == "both") { 82 | resolveSymlinks = argString; 83 | } else { 84 | resolveSymlinks = "none"; 85 | DOpus.Output("ERROR: Invalid RESOLVE_SYMLINKS argument. Must be either 'none', 'symlinkedFiles', 'symlinkedFolders', or 'both'. Got: " + argString); 86 | } 87 | //DOpus.Output("Received RESOLVE_SYMLINKS argument: " + String(clickData.func.args.RESOLVE_SYMLINKS)); 88 | } 89 | 90 | if (clickData.func.args.got_arg.FOLDER_TERMINATOR) { 91 | includeTrailingTerminator = clickData.func.args.FOLDER_TERMINATOR; 92 | //DOpus.Output("Received FOLDER_TERMINATOR argument"); 93 | } 94 | 95 | if (clickData.func.args.got_arg.PREPEND_INSIDE_QUOTES) { 96 | prependInsideQuotes = clickData.func.args.PREPEND_INSIDE_QUOTES; 97 | //DOpus.Output("Received PREPEND_INSIDE_QUOTES argument: " + String(prependInsideQuotes)); 98 | } 99 | 100 | if (clickData.func.args.got_arg.PREPEND_OUTSIDE_QUOTES) { 101 | prependOutsideQuotes = clickData.func.args.PREPEND_OUTSIDE_QUOTES; 102 | //DOpus.Output("Received PREPEND_OUTSIDE_QUOTES argument: " + String(prependOutsideQuotes)); 103 | } 104 | 105 | if (clickData.func.args.got_arg.APPEND_INSIDE_QUOTES) { 106 | appendInsideQuotes = clickData.func.args.APPEND_INSIDE_QUOTES; 107 | //DOpus.Output("Received APPEND_INSIDE_QUOTES argument: " + String(appendInsideQuotes)); 108 | } 109 | 110 | if (clickData.func.args.got_arg.APPEND_OUTSIDE_QUOTES) { 111 | appendOutsideQuotes = clickData.func.args.APPEND_OUTSIDE_QUOTES; 112 | //DOpus.Output("Received APPEND_OUTSIDE_QUOTES argument: " + String(appendOutsideQuotes)); 113 | } 114 | 115 | // Normalize variables to lower case 116 | multiLineQuoteMode = multiLineQuoteMode.toLowerCase(); 117 | singleItemQuoteMode = singleItemQuoteMode.toLowerCase(); 118 | resolveSymlinks = resolveSymlinks.toLowerCase(); 119 | 120 | var clipboardText = ""; 121 | // If single item is selected 122 | if (selectedItems.count == 1) { 123 | var singleItem = selectedItems(0); 124 | var filePath = String(singleItem.realpath); 125 | 126 | // Resolve symlink/junction if needed 127 | if (resolveSymlinks == "both" || (resolveSymlinks == "symlinkedfiles" && !singleItem.is_dir) || (resolveSymlinks == "symlinkedfolders" && singleItem.is_dir)) { 128 | var resolvedPath = DOpus.FSUtil.Resolve(String(singleItem.realpath), "j"); 129 | filePath = String(resolvedPath); 130 | } 131 | 132 | if (singleItem.is_dir) { 133 | filePath += includeTrailingTerminator; 134 | } 135 | // If no spaces in the file path or option set to not use quotes 136 | if (singleItemQuoteMode != "always" && (filePath.indexOf(" ") == -1 || singleItemQuoteMode == "never")) { 137 | clipboardText = prependOutsideQuotes + prependInsideQuotes + filePath + appendInsideQuotes + appendOutsideQuotes; 138 | } else { 139 | // File path contains spaces or option set to always use quotes 140 | clipboardText = prependOutsideQuotes + '"' + prependInsideQuotes + filePath + appendInsideQuotes + '"' + appendOutsideQuotes; 141 | } 142 | } else { 143 | // Multiple items selected 144 | for (var i = 0; i < selectedItems.count; i++) { 145 | var filePath = String(selectedItems(i).realpath); 146 | 147 | // Resolve symlink/junction if needed 148 | if (resolveSymlinks == "both" || (resolveSymlinks == "symlinkedfiles" && !selectedItems(i).is_dir) || (resolveSymlinks == "symlinkedfolders" && selectedItems(i).is_dir)) { 149 | var resolvedPath = DOpus.FSUtil.Resolve(String(selectedItems(i).realpath), "j"); 150 | filePath = String(resolvedPath); 151 | } 152 | 153 | if (selectedItems(i).is_dir) { 154 | filePath += includeTrailingTerminator; 155 | } 156 | //Add a newline character to the beginning starting after the first line 157 | if (i > 0) { 158 | clipboardText += "\n"; 159 | } 160 | if (multiLineQuoteMode === "always") { 161 | clipboardText += prependOutsideQuotes + '"' + prependInsideQuotes + filePath + appendInsideQuotes + '"' + appendOutsideQuotes; 162 | } else if (multiLineQuoteMode === "auto" && filePath.indexOf(" ") !== -1) { 163 | clipboardText += prependOutsideQuotes + '"' + prependInsideQuotes + filePath + appendInsideQuotes + '"' + appendOutsideQuotes; 164 | } else { 165 | clipboardText += prependOutsideQuotes + prependInsideQuotes + filePath + appendInsideQuotes + appendOutsideQuotes; 166 | } 167 | } 168 | } 169 | 170 | DOpus.SetClip(clipboardText); 171 | // For debugging: 172 | //DOpus.Output("--- Copied to clipboard: ---\n" + clipboardText); 173 | } 174 | -------------------------------------------------------------------------------- /User Command Scripts/Copy_Relative_Paths.js: -------------------------------------------------------------------------------- 1 | // Copy the list of paths to selected files/folders, relative to current path (such as when using flat view) 2 | // By ThioJoe 3 | // Updated: 6/5/24 (First Version) 4 | 5 | // Argument Template: 6 | // INCLUDE_CURRENT_DIR/O/S,PREFIX/O,FOLDER_TERMINATOR/O 7 | 8 | function OnClick(clickData) 9 | { 10 | // ------- Options (These values will be used if no corresponding argument is specified when calling script -------- 11 | // True/False - Whether or not to include the name of the current-level folder name as the first part of the relative path 12 | // > Optional Argument Name: INCLUDE_CURRENT_DIR (Switch, no value needed) 13 | var includeCurrentFolderName = false; 14 | // This lets you prefix the relative paths with a string or character (such as a backslash) if you want. Or you can set it to a blank string. 15 | // Remember to escape if necessary (for backslash do two of them like "\\") 16 | // > Optional Argument Name: PREFIX (string value) 17 | var prefix = "\\"; 18 | // Sets which trailing path terminator to add (if any) to the end of full paths of folders (like the 'noterm' modifier). Doesn't apply to files. 19 | // Just set to empty string if none wanted. Don't forget to escape as necessary (to add a backslash would be like: "\\") 20 | // > Optional Argument Name: FOLDER_TERMINATOR (string value) 21 | var trailingFolderTerminator = "\\"; 22 | // ----------------------------------------------------------------------------------------------------------------- 23 | // Example usage of arguments: 24 | // Copy_Relative_Paths INCLUDE_CURRENT_DIR PREFIX="\" FOLDER_TERM="\" 25 | // ----------------------------------------------------------------------------------------------------------------- 26 | 27 | // Parse optional arguments if they're there 28 | if (clickData.func.args.got_arg.INCLUDE_CURRENT_DIR) { 29 | includeCurrentFolderName = true; 30 | //DOpus.Output("Received INCLUDE_CURRENT_DIR argument"); 31 | } 32 | if (clickData.func.args.got_arg.PREFIX) { 33 | prefix = clickData.func.args.PREFIX; 34 | //DOpus.Output("Received PREFIX argument"); 35 | } 36 | if (clickData.func.args.got_arg.FOLDER_TERMINATOR) { 37 | trailingFolderTerminator = clickData.func.args.FOLDER_TERMINATOR; 38 | //DOpus.Output("Received FOLDER_TERMINATOR argument"); 39 | } 40 | 41 | // For debugging 42 | //DOpus.Output("\n"); 43 | //DOpus.Output("Include Current Dir: " + String(clickData.func.args.include_current_dir)); 44 | //DOpus.Output("Prefix: " + String(clickData.func.args.prefix)); 45 | //DOpus.Output("Folder Terminator: " + String(clickData.func.args.folder_terminator)); 46 | //DOpus.Output("\n"); 47 | 48 | var tab = clickData.func.sourcetab; 49 | var selectedItems = tab.selected; 50 | var sourcePathDepth = tab.path.Split.count; 51 | 52 | var initialDepth = 0; 53 | if (includeCurrentFolderName == true) { 54 | initialDepth = sourcePathDepth-1; 55 | } else { 56 | initialDepth = sourcePathDepth; 57 | } 58 | 59 | // For Debugging 60 | //DOpus.Output("Source Path: " + tab.path); 61 | //DOpus.Output("Source Path Depth: " + sourcePathDepth); 62 | 63 | var clipboardText = ""; 64 | for (var i = 0; i < selectedItems.count; i++) { 65 | var relativePath = DOpus.FSUtil.NewPath(); 66 | relativePath.set(selectedItems(i).realpath.Split(initialDepth)); 67 | finalRelativePathString = prefix + String(relativePath); 68 | 69 | if (selectedItems(i).is_dir) { 70 | finalRelativePathString += trailingFolderTerminator; 71 | } 72 | 73 | // For Debugging 74 | //DOpus.Output("Original Path: " + String(selectedItems(i).realpath)); 75 | //DOpus.Output("Relative Path: " + finalRelativePathString); 76 | 77 | //Add a newline character to the beginning starting after the first line 78 | if (i > 0) { 79 | clipboardText += "\n"; 80 | } 81 | clipboardText += finalRelativePathString; 82 | } 83 | DOpus.SetClip(clipboardText); 84 | } 85 | -------------------------------------------------------------------------------- /User Command Scripts/Copy_Shortcut_Target_With_Arguments.vbs: -------------------------------------------------------------------------------- 1 | ' Version: 2.3.0 - 9/7/24 2 | ' IMPORTANT PERFORMANCE TIP: Use the "@runonce:" command modifier if using this with a context menu button 3 | ' Just put it before whatever name you set for the user command. So for example: @runonce:Copy_Shortcut_Target_With_Arguments 4 | 5 | ' Arguments Template (Must put this in the 'Template' box in the command editor to use arguments): 6 | ' PARSE_SHELL_ID/S,DONT_CROP_SHELL/S 7 | Option Explicit 8 | 9 | Function OnClick(ByRef clickData) 10 | ' ------------------------ Options ------------------------ 11 | ' Argument: PARSE_SHELL_ID (switch, no value given) 12 | ' Instead of copying the entire string, if the path contains "shell:", "shell:::", or starts with "::", it will only copy what comes after those points 13 | ' Argument: DONT_CROP_SHELL (switch, no value given) 14 | ' When using PARSE_SHELL_ID arg, this will keep the "shell:::", "shell:", and "::" part of the parsed shell ID instead of cutting it off 15 | '---------------------------------------------------------- 16 | 17 | Dim GetCLSID, DontCropShell 18 | GetCLSID = False 'Default to false unless switch argument is given. Tries to parse CLSID. 19 | DontCropShell = False 20 | 21 | ' Check for arguments 22 | If clickData.func.args.got_arg.PARSE_SHELL_ID Then 23 | GetCLSID = True 24 | End If 25 | 26 | If clickData.func.args.got_arg.DONT_CROP_SHELL Then 27 | DontCropShell = True 28 | End If 29 | 30 | ' Create necessary objects and variables 31 | Dim fs, selectedItems, item, path, fExt, resolvedPaths, itemIndex 32 | Set fs = CreateObject("Scripting.FileSystemObject") 33 | Dim shellLink, shellLinkAlt 34 | Set shellLink = CreateObject("Shell.Application") 35 | Set shellLinkAlt = CreateObject("WScript.Shell") 36 | resolvedPaths = "" 37 | Set selectedItems = clickData.func.sourcetab.selected 38 | clickData.func.command.deselect = False 39 | 40 | If selectedItems.count = 0 Then 41 | 'DOpus.Output "No files selected." 42 | Exit Function 43 | End If 44 | 45 | ' Begin main logic 46 | itemIndex = 0 47 | For Each item In selectedItems 48 | If itemIndex > 0 Then 49 | resolvedPaths = resolvedPaths & vbCrLf 50 | End If 51 | path = item.realpath 52 | fExt = LCase(fs.GetExtensionName(path)) 53 | Dim resolvedPath 54 | Select Case fExt 55 | Case "lnk" 56 | resolvedPath = GetLnkFullPath(shellLink, shellLinkAlt, fs, path) 57 | Case "url" 58 | resolvedPath = GetUrlFullPath(fs, path) 59 | Case Else 60 | resolvedPath = path 61 | End Select 62 | 63 | If GetCLSID Then 64 | resolvedPath = parseCLSID(resolvedPath, DontCropShell) 65 | End If 66 | 67 | resolvedPaths = resolvedPaths & resolvedPath 68 | itemIndex = itemIndex + 1 69 | Next 70 | Set shellLink = Nothing 71 | Set shellLinkAlt = Nothing 72 | Set fs = Nothing 73 | 74 | DOpus.SetClip Trim(resolvedPaths) 75 | 'DOpus.Output "Resolved paths: " & resolvedPaths 76 | End Function 77 | 78 | Function parseCLSID(path, DontCropShell) 79 | If Left(path, 2) = "::" Then 80 | ' Case 1 and 2: Starts with "::" 81 | If DontCropShell Then 82 | parseCLSID = path 83 | Else 84 | parseCLSID = Mid(path, 3) 85 | End If 86 | ElseIf InStr(path, "shell:::") > 0 Then 87 | ' Case 3: Contains "shell:::" 88 | If DontCropShell Then 89 | parseCLSID = Mid(path, InStr(path, "shell:::")) 90 | Else 91 | parseCLSID = Mid(path, InStr(path, "shell:::") + 8) 92 | End If 93 | ElseIf InStr(path, "shell:") > 0 Then 94 | ' Case 4: Contains "shell:" 95 | If DontCropShell Then 96 | parseCLSID = Mid(path, InStr(path, "shell:")) 97 | Else 98 | parseCLSID = Mid(path, InStr(path, "shell:") + 6) 99 | End If 100 | Else 101 | ' No CLSID found, keep the original path 102 | parseCLSID = path 103 | End If 104 | End Function 105 | 106 | ' Function to return the full path and arguments of a .lnk shortcut file 107 | ' Where ShellLinkObj is created via: CreateObject("Shell.Application") 108 | ' ShellLinkObjAlt is created via: CreateObject("WScript.Shell") 109 | ' and 'fs' is created via: CreateObject("Scripting.FileSystemObject") 110 | Function GetLnkFullPath(shellLinkObj, shellLinkObjAlt, fs, path) 111 | Dim linkData, targetPath, arguments 112 | 113 | On Error Resume Next 114 | 115 | ' Try using Shell.Application first 116 | Set linkData = shellLinkObj.Namespace(fs.GetParentFolderName(path)).ParseName(fs.GetFileName(path)).GetLink 117 | 118 | If Err Then 119 | ' Fall back to WScript.Shell if any error occurs 120 | Err.Clear 121 | Set linkData = shellLinkObjAlt.CreateShortcut(path) 122 | targetPath = linkData.TargetPath 123 | arguments = linkData.Arguments 124 | Else 125 | targetPath = linkData.Target.Path 126 | arguments = linkData.Arguments 127 | End If 128 | 129 | ' Return to normal error handling behavior 130 | On Error Goto 0 131 | 132 | If Err.Number <> 0 Then 133 | ' If both methods fail, return the original path 134 | GetLnkFullPath = Trim(path) 135 | ElseIf targetPath <> "" Then 136 | GetLnkFullPath = Trim(targetPath & " " & arguments) 137 | Else 138 | GetLnkFullPath = "[Error]" ' Fallback to original path 139 | End If 140 | End Function 141 | 142 | ' Function to read text from .url file to get URL target. 143 | ' Where 'path' is the .url file path and 'fs' is created via: CreateObject("Scripting.FileSystemObject") 144 | Function GetUrlFullPath(fs, path) 145 | Dim urlFile, url 146 | Set urlFile = fs.OpenTextFile(path, 1) ' 1 = ForReading 147 | Do Until urlFile.AtEndOfStream 148 | url = urlFile.ReadLine 149 | If Left(LCase(url), 4) = "url=" Then 150 | GetUrlFullPath = Mid(url, 5) 151 | Exit Do 152 | End If 153 | Loop 154 | urlFile.Close 155 | End Function 156 | -------------------------------------------------------------------------------- /User Command Scripts/Copy_Tree_Structure.js: -------------------------------------------------------------------------------- 1 | // Copy a tree view of selected files/folders 2 | // By ThioJoe 3 | // Updated: 7/3/24 (1.0.2) 4 | 5 | // Argument Template: 6 | // UP_ONE_CONTEXT/O/S,FILES_FIRST/O/S,EXPAND_DEPTH/O/N,CONTEXT_LINE/O 7 | 8 | function OnClick(clickData) 9 | { 10 | // ------- Options (These values will be used if no corresponding argument is specified when calling script -------- 11 | // True/False - Whether to list files before folders 12 | // > Optional Argument Name: FILES_FIRST (Switch, no value needed) 13 | var filesFirst = false; 14 | 15 | // Integer - Depth to expand folders. 0 for no expansion, -1 for unlimited depth 16 | // > Optional Argument Name: EXPAND_DEPTH (Integer value) 17 | var expandDepth = -1; 18 | 19 | // String - What to show as the top line. Nothing, name of the source folder, or path of the source folder 20 | // Possible Values: "none", "folder", "path" 21 | // > Optional Argument Name: CONTEXT_LINE (String value) 22 | var contextLine = "folder"; 23 | 24 | // True/False - If true (or argument included), the context line will be moved up by one folder. 25 | // > Optional Argument Name: UP_ONE_CONTEXT (Switch, no value needed) 26 | var showSourceOneUpContext = false; 27 | 28 | // ----------------------------------------------------------------------------------------------------------------- 29 | // Example usage of arguments: 30 | // Copy_Tree_View UP_ONE_CONTEXT FILES_FIRST EXPAND_DEPTH=1 CONTEXT_LINE="path" 31 | // ----------------------------------------------------------------------------------------------------------------- 32 | 33 | // ------------------------ APPEARANCE SETTINGS ------------------------ 34 | // The character(s) to use for files at a middle branch of the tree 35 | var middleFileBranch = "├───"; 36 | // The character(s) to use for files at an end branch of the tree 37 | var endFileBranch = "└───"; 38 | // The character(s) to use for folders at a middle branch of the tree 39 | var middleFolderBranch = "├───"; 40 | // The character(s) to use for folders at an end branch of the tree 41 | var endFolderBranch = "└───"; 42 | // The character(s) to use as a spacer and directory layers not connected to a file/folder 43 | var verticalBranch = "│"; 44 | 45 | // Folder name settings, such as to add brackets surrounding folder names like this or something: [Folder Name] 46 | // The string to prefix folder names with (default is empty string) 47 | var folderPrefix = ""; 48 | // The string to suffix folder names with (default is empty string) 49 | var folderSuffix = ""; 50 | 51 | // In files first mode, this sets whether to show the endFileBranch string above for the last file in the folder, even if there are folders below it 52 | var discontinuousBranchForLastFile = true; 53 | 54 | // --------------------------------------------------------------------- 55 | 56 | // Parse optional arguments if they're there 57 | if (clickData.func.args.got_arg.UP_ONE_CONTEXT) { 58 | showSourceOneUpContext = true; 59 | //DOpus.Output("Received UP_ONE_CONTEXT argument"); 60 | } 61 | 62 | if (clickData.func.args.got_arg.FILES_FIRST) { 63 | filesFirst = true; 64 | //DOpus.Output("Received FILES_FIRST argument"); 65 | } 66 | 67 | if (clickData.func.args.got_arg.EXPAND_DEPTH) { 68 | // Validate argument value 69 | var argExpandDepth = parseInt(clickData.func.args.EXPAND_DEPTH, 10); 70 | if (!isNaN(argExpandDepth) && (argExpandDepth >= -1)) { 71 | expandDepth = argExpandDepth; 72 | } else { 73 | expandDepth = -1; 74 | DOpus.Output("ERROR: Invalid EXPAND_DEPTH argument. Must be an integer >= -1. Got: " + clickData.func.args.EXPAND_DEPTH); 75 | } 76 | //DOpus.Output("Received EXPAND_DEPTH argument: " + expandDepth); 77 | } 78 | 79 | if (clickData.func.args.got_arg.CONTEXT_LINE) { 80 | // Validate argument value 81 | var argContextLine = clickData.func.args.CONTEXT_LINE.toLowerCase(); 82 | if (argContextLine === "none" || argContextLine === "folder" || argContextLine === "path") { 83 | contextLine = argContextLine; 84 | } else { 85 | contextLine = "folder"; 86 | DOpus.Output("ERROR: Invalid CONTEXT_LINE argument. Must be either 'none', 'folder', or 'path'. Got: " + clickData.func.args.CONTEXT_LINE); 87 | } 88 | //DOpus.Output("Received CONTEXT_LINE argument: " + contextLine); 89 | } 90 | 91 | // Further variable setup 92 | contextLine = contextLine.toLowerCase() 93 | if (discontinuousBranchForLastFile === false) { 94 | var lastFileBranch = middleFileBranch; 95 | } else { 96 | var lastFileBranch = endFileBranch; 97 | } 98 | 99 | var tab = clickData.func.sourcetab; 100 | var selectedItems = tab.selected; 101 | var sourcePathDepth = tab.path.Split.count; 102 | 103 | // Adjust initial depth based on the context 104 | var initialDepth = showSourceOneUpContext ? sourcePathDepth - 1 : sourcePathDepth; 105 | 106 | var expandedItems = expandSelectedItems(selectedItems, expandDepth); 107 | 108 | // Decide top level line to print. Whether current folder name, current full path name, or up-one context 109 | var topLine = ""; 110 | if (contextLine !== "none") { 111 | 112 | if (showSourceOneUpContext === true) { 113 | 114 | var parentPath = DOpus.FSUtil.NewPath(tab.path); 115 | parentPath.Parent(); 116 | 117 | if (contextLine === "folder") { 118 | topLine = parentPath.filepart + "\n"; 119 | } else if (contextLine === "path") { 120 | topLine = parentPath + "\n"; 121 | } 122 | 123 | } else if (showSourceOneUpContext === false) { 124 | 125 | if (contextLine === "folder") { 126 | topLine = tab.path.filepart + "\n"; 127 | } else if (contextLine === "path") { 128 | topLine = tab.path + "\n"; 129 | } 130 | } 131 | } 132 | 133 | // Create the tree output 134 | var treeOutput = topLine; 135 | treeOutput += generateTree(expandedItems, initialDepth, filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch); 136 | 137 | DOpus.SetClip(treeOutput); 138 | } 139 | 140 | function expandSelectedItems(items, expandDepth) { 141 | var expandedItems = DOpus.Create().Vector(); 142 | 143 | function expand(item, depth) { 144 | expandedItems.push_back(item); 145 | if (item.is_dir && (expandDepth === -1 || depth < expandDepth)) { 146 | var folderEnum = DOpus.FSUtil.ReadDir(item, false); 147 | while (!folderEnum.complete) { 148 | var subItem = folderEnum.Next(); 149 | if (subItem) { 150 | expand(subItem, depth + 1); 151 | } 152 | } 153 | } 154 | } 155 | 156 | for (var i = 0; i < items.count; i++) { 157 | expand(items(i), 0); 158 | } 159 | 160 | return expandedItems; 161 | } 162 | 163 | function generateTree(items, baseDepth, filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch) { 164 | var treeText = ""; 165 | var pathTree = {}; 166 | 167 | // Build the tree structure 168 | for (var i = 0; i < items.count; i++) { 169 | var item = items(i); 170 | var relativePathParts = item.realpath.Split(baseDepth); 171 | 172 | // Navigate through the tree structure 173 | var currentLevel = pathTree; 174 | for (var j = 0; j < relativePathParts.count; j++) { 175 | var part = relativePathParts(j); 176 | if (!currentLevel[part]) { 177 | currentLevel[part] = { "_isDir": (j < relativePathParts.count - 1 || item.is_dir) }; 178 | } 179 | currentLevel = currentLevel[part]; 180 | } 181 | } 182 | 183 | // Convert the tree structure to text 184 | treeText = convertTreeToText(pathTree, "", "", filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, true); 185 | 186 | return treeText; 187 | } 188 | 189 | function convertTreeToText(tree, folderTerminator, indent, filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, isRoot) { 190 | var treeText = ""; 191 | var entries = []; 192 | var dirs = []; 193 | var files = []; 194 | 195 | for (var key in tree) { 196 | if (tree.hasOwnProperty(key) && key !== "_isDir") { 197 | if (tree[key]["_isDir"]) { 198 | dirs.push(key); 199 | } else { 200 | files.push(key); 201 | } 202 | } 203 | } 204 | 205 | // Sort files and directories as needed 206 | if (filesFirst) { 207 | entries = files.concat(dirs); 208 | } else { 209 | entries = dirs.concat(files); 210 | } 211 | 212 | for (var i = 0; i < entries.length; i++) { 213 | var key = entries[i]; 214 | var isDir = tree[key]["_isDir"]; 215 | var isLastEntry = (i === entries.length - 1); 216 | var isLastFile = (i === files.length - 1 && filesFirst && i < files.length); 217 | 218 | var line = indent + (isDir ? (isLastEntry ? endFolderBranch : middleFolderBranch) : (isLastEntry ? endFileBranch : (isLastFile ? lastFileBranch : middleFileBranch))) + (isDir ? folderPrefix + key + folderSuffix : key) + "\n"; 219 | treeText += line; 220 | 221 | if (isDir) { 222 | var subTreeText = convertTreeToText(tree[key], folderTerminator, indent + (isLastEntry ? " " : verticalBranch + " "), filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, false); 223 | treeText += subTreeText; 224 | 225 | // Check if current directory is empty 226 | var isCurrentDirEmpty = subTreeText.replace(/^\s+|\s+$/g, '').length === 0; 227 | 228 | // Only add spacing if it's not the last entry 229 | if (!isLastEntry) { 230 | // Get the next entry 231 | var nextEntry = entries[i + 1]; 232 | 233 | // Check if the next entry is a directory 234 | if (tree[nextEntry] && tree[nextEntry]["_isDir"]) { 235 | var nextSubTreeText = convertTreeToText(tree[nextEntry], folderTerminator, "", filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, false); 236 | var isNextDirEmpty = nextSubTreeText.replace(/^\s+|\s+$/g, '').length === 0; 237 | 238 | // Add spacer line if: 239 | // 1. Current directory is not empty, OR 240 | // 2. Current directory is empty but the next one is not 241 | if (!isCurrentDirEmpty || (isCurrentDirEmpty && !isNextDirEmpty)) { 242 | treeText += indent + verticalBranch + "\n"; 243 | } 244 | } else { 245 | // If next entry is not a directory, always add a spacer 246 | treeText += indent + verticalBranch + "\n"; 247 | } 248 | } 249 | } 250 | 251 | // Add a vertical line after the last file and before the first folder 252 | if (isLastFile && filesFirst && dirs.length > 0) { 253 | treeText += indent + verticalBranch + "\n"; 254 | } 255 | } 256 | 257 | return treeText; 258 | } 259 | -------------------------------------------------------------------------------- /User Command Scripts/Create_Bak_Button.js: -------------------------------------------------------------------------------- 1 | // Button / User Command script that creates '.bak' backups for any number of selected files or folders. If a .bak already exists for an item, it will create .bak2, .bak3 and so on. Also has an argument option to restore a file. 2 | // By ThioJoe 3 | // Updated: 9/3/24 4 | 5 | // Argument Template: 6 | // RESTORE/S,RENAME/S,BACKUP_EXTENSION/K 7 | 8 | // Example usages of arguments: 9 | // Make_bak BACKUP_EXTENSION=".bak" 10 | // Make_bak RESTORE 11 | // Make_bak BACKUP_EXTENSION=".backup" RESTORE 12 | 13 | function OnClick(clickData) { 14 | // You can change the backup extension base string to be whatever you want here. Must include period. 15 | // Default = '.bak' 16 | // > Optional Argument Name: BACKUP_EXTENSION (string value) 17 | var backupExtension = '.bak'; 18 | //////////////////////////////////////////////////////////////////////// 19 | 20 | // With this set to true (or if argument is used), the highest numbered .bak for the selected file will replace the main file 21 | // Note: Selected backup file to restore from must match the base backupExtension variable. (It's ok if it's numbered, for example if backupExtension is '.bak' you can still restore a '.bak4' file. 22 | // > Optional Argument Name: RESTORE (Switch, no value needed) 23 | var doRestore = false; 24 | 25 | // With this set to true (or if argument is used), the original file/folder will be renamed with the backup extension, instead of a copy being made 26 | // > Optional Argument Name: RENAME (Switch, no value needed) 27 | var doRename = false; 28 | 29 | // ----------------------------------------------------------------------- 30 | 31 | // Parse optional arguments if they're there 32 | if (clickData.func.args.got_arg.RESTORE) { 33 | doRestore = true; 34 | //DOpus.Output("Received RESTORE switch argument"); 35 | } 36 | 37 | if (clickData.func.args.got_arg.RENAME) { 38 | doRename = true; 39 | //DOpus.Output("Received RENAME switch argument"); 40 | } 41 | 42 | if (clickData.func.args.got_arg.BACKUP_EXTENSION) { 43 | //Validate argument value 44 | argString = String(clickData.func.args.BACKUP_EXTENSION); 45 | if (argString.charAt(0) == ".") { 46 | backupExtension = argString; 47 | } else { 48 | backupExtension = "." + argString; 49 | DOpus.Output("WARNING: BACKUP_EXTENSION argument did not include a period so one was added. Got argument: " + argString); 50 | } 51 | //DOpus.Output("Received BACKUP_EXTENSION argument: " + String(clickData.func.args.BACKUP_EXTENSION)); 52 | } 53 | 54 | 55 | function createBak(item) { 56 | // Create item object of selected file or folder 57 | var selectedItem = item; 58 | // Get name of selected file or folder 59 | //var selectedItemExt = String(selectedItem.ext); 60 | //var selectedItemNameStem = String(selectedItem.name_stem); 61 | 62 | //DOpus.Output("Processing: " + selectedItem.name); 63 | var lastBakNum = getLastBakNum(item); 64 | //DOpus.Output("LastBakNum: " + lastBakNum); 65 | 66 | // Determine string to append with number if necessary 67 | var bakNumString = "" 68 | if (lastBakNum == 0) { 69 | bakNumString = "" 70 | } else { 71 | bakNumString = String(lastBakNum + 1) 72 | } 73 | 74 | // Construct command string depending on arguments 75 | var commandString; 76 | if (doRename == true) { 77 | // Renames to the new name. Added autonumber just in case 78 | commandString = 'Rename FROM "' + selectedItem + '" TO *' + backupExtension + bakNumString + " AUTONUMBER"; 79 | } else { 80 | commandString = 'Copy DUPLICATE "' + selectedItem + '" AS *' + backupExtension + bakNumString; 81 | } 82 | //DOpus.Output("Running command: " + commandString); 83 | clickData.func.command.RunCommand(commandString); 84 | 85 | } 86 | 87 | function restoreBak(item) { 88 | // Create a FileSystemObject 89 | var fso = new ActiveXObject("Scripting.FileSystemObject"); 90 | 91 | // Get the full name of the selected .bak file 92 | var selectedBakFullName = String(item.name); 93 | 94 | // Determine the base name of the original file by removing the .bak or .bak# extension 95 | var originalFileName; 96 | var baseNameRegex = new RegExp('^(.+)' + backupExtension.replace('.', '\\.') + '(\\d*)$'); 97 | //DOpus.Output("baseNameRegex: " + baseNameRegex); 98 | var match = selectedBakFullName.match(baseNameRegex); 99 | if (match) { 100 | originalFileName = match[1]; 101 | } else { 102 | // Show error dialogue if the selected file is not a valid .bak file 103 | DOpus.Dlg.Request("Error: The selected file (" + selectedBakFullName + ") doesn't appear to match the selected backup extension: " + backupExtension, "OK"); 104 | return; 105 | } 106 | 107 | // Determine the paths for the selected .bak file and the original file 108 | var bakFilePath = String(clickData.func.sourcetab.path) + '\\' + selectedBakFullName; 109 | var originalFilePath = String(clickData.func.sourcetab.path) + '\\' + originalFileName; 110 | // Ensure expected selected file path works 111 | if (fso.FileExists(bakFilePath) || fso.FolderExists(bakFilePath)) { 112 | // If original file already exists then delete it first 113 | if (fso.FileExists(originalFilePath) || fso.FolderExists(bakFilePath)) { 114 | var commandString = 'Delete QUIET "' + originalFilePath + '"'; 115 | //DOpus.Output("Running command: " + commandString); 116 | clickData.func.command.RunCommand(commandString); 117 | } 118 | 119 | // Rename the .bak file to the original file name 120 | commandString = 'Copy DUPLICATE "' + bakFilePath + '" AS "' + originalFileName + '"'; 121 | //DOpus.Output("Running command: " + commandString); 122 | clickData.func.command.RunCommand(commandString); 123 | } else { 124 | DOpus.Output("Backup file does not exist: " + bakFilePath); 125 | } 126 | } 127 | 128 | function getLastBakNum (item) { 129 | // Create a FileSystemObject 130 | var lastBakNum = 0; 131 | var selectedItemFullName = String(item.name); 132 | 133 | var fso = new ActiveXObject("Scripting.FileSystemObject"); 134 | // Get the parent folder of the selected item 135 | var parentFolder = fso.GetFolder(clickData.func.sourcetab.path); 136 | var files = new Enumerator(parentFolder.Files); 137 | var subfolders = new Enumerator(parentFolder.SubFolders); 138 | 139 | // Combine files and folders into a single array 140 | var items = []; 141 | while (!files.atEnd()) { 142 | items.push(files.item()); 143 | files.moveNext(); 144 | } 145 | while (!subfolders.atEnd()) { 146 | items.push(subfolders.item()); 147 | subfolders.moveNext(); 148 | } 149 | 150 | // Go through filenames in folder, if any contains itemFullName.bak, check if # at end is larger than current, if so record into lastBakNum 151 | for (var i = 0; i < items.length; i++) { 152 | var currentItem = items[i]; 153 | var currentItemName = String(currentItem.Name); 154 | 155 | //DOpus.Output("Checking existing item: " + currentItemName); 156 | 157 | // Checks if stem of currently scanned item is same as selected item with .bak added 158 | var theoreticalBakName = selectedItemFullName + backupExtension; 159 | var theoreticalBakNameLength = theoreticalBakName.length; 160 | 161 | // Checking if the currently scanned item is already a .bak item of the selected item or folder 162 | // By checking if scanned item contains selected item name + bak, from beginning 163 | if (currentItemName.substr(0, theoreticalBakNameLength) == theoreticalBakName) { 164 | //DOpus.Output("Found backup match: " + currentItemName); 165 | 166 | // Checks if extension is .bak or .bak* 167 | if (currentItemName.length == theoreticalBakNameLength) { 168 | if (lastBakNum == 0) { 169 | lastBakNum = 1; 170 | } 171 | //DOpus.Output("Setting lastBakNum to 1"); 172 | } else { 173 | // Gets text or number after ".bak", which should be a number 174 | var extEnding = currentItemName.substr(theoreticalBakNameLength); 175 | // Checks if anything after .bak is not a number 176 | if (!isNaN(extEnding)) { 177 | // Parse the ending number into an integer in base 10 178 | var extEndingNum = parseInt(extEnding, 10); 179 | // Only update lastBakNum if it is the largest .bak # found so far 180 | if (extEndingNum > lastBakNum) { 181 | lastBakNum = extEndingNum; 182 | //DOpus.Output("Updating lastBakNum to: " + lastBakNum); 183 | } 184 | } 185 | } 186 | } 187 | } 188 | return lastBakNum; 189 | } 190 | 191 | // Get data about selected items 192 | var allSelectedItems = clickData.func.sourcetab.selected; 193 | var enumSelectedItems = new Enumerator(allSelectedItems); 194 | 195 | // Runs the main function for each selected item 196 | enumSelectedItems.moveFirst(); 197 | while (!enumSelectedItems.atEnd()) { 198 | var currentItem = enumSelectedItems.item(); 199 | 200 | // Whether to restore files or create backup based on argument switch 201 | if (doRestore === true) { 202 | restoreBak(currentItem); 203 | } else { 204 | createBak(currentItem); 205 | } 206 | enumSelectedItems.moveNext(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /User Command Scripts/Edit_Alternate_Data_Streams.js: -------------------------------------------------------------------------------- 1 | // Argument Template: 2 | // OPENALL/S 3 | 4 | // Optional Arguments: 5 | // OPENALL (Switch) 6 | // Opens all the streams at once 7 | 8 | function OnClick(clickData) { 9 | // Choose a default editor if desired, otherwise leave as empty string 10 | var customEditorPath = "" 11 | 12 | // Check whether to use custom editor. Checks if the variable exists and not commented out, and also not empty 13 | var editorPath; 14 | if ((typeof customEditorPath != "undefined") && (customEditorPath)) { 15 | if (DOpus.FSUtil.Exists(customEditorPath)) { 16 | editorPath = customEditorPath; 17 | } else { 18 | DOpus.Output("customEditorPath value (" + customEditorPath + ") doesn't seem to exist, using notepad.exe as fallback"); 19 | editorPath = "notepad.exe"; 20 | } 21 | } else { 22 | editorPath = "notepad.exe"; 23 | } 24 | 25 | // Set openAll to true or false, or use argument, depending on whether you want to open all streams without prompting 26 | var openAll = false; 27 | 28 | // Parse optional arguments if they're there 29 | if (clickData.func.args.got_arg.OPENALL) { 30 | openAll = true; 31 | //DOpus.Output("Received OPENALL argument"); 32 | } 33 | 34 | // Don't deselect after using command 35 | clickData.func.command.deselect = false; 36 | 37 | var item = clickData.func.sourcetab.selected_files(0); // Get the first selected file 38 | if (!item) { 39 | //DOpus.Output("No file selected"); 40 | return; 41 | } 42 | 43 | var fsUtil = DOpus.FSUtil; 44 | var adsNames = fsUtil.GetADSNames(item.RealPath); 45 | 46 | if (adsNames.count == 0) { 47 | //DOpus.Output("No alternate data streams found for the selected file."); 48 | return; 49 | } 50 | 51 | // Function to open all streams 52 | function openAllStreams(adsNames, item) { 53 | for (var i = 0; i < adsNames.count; i++) { 54 | var fullPathToADS = item.RealPath + adsNames(i); // Full path to ADS 55 | //var cmd = 'notepad.exe "' + fullPathToADS + '"'; 56 | var cmd = editorPath + ' "' + fullPathToADS + '"'; 57 | //DOpus.Output("Opening: " + fullPathToADS); 58 | clickData.func.command.RunCommand(cmd); 59 | } 60 | } 61 | 62 | // If openAll is true, automatically open all alternate data streams 63 | if (openAll) { 64 | openAllStreams(adsNames, item); 65 | return; 66 | } 67 | 68 | // Create a dialog to show the alternate data streams with an additional "Open All" button 69 | var dlg = clickData.func.Dlg; 70 | dlg.title = "Select Alternate Data Stream"; 71 | dlg.message = "Select the ADS you want to edit or choose 'Open All':"; 72 | dlg.buttons = "OK||Open All|Cancel"; // Added Open All button 73 | dlg.choices = adsNames; // Set the choices to the ADS names 74 | dlg.selection = 0; // Sets default selection to the first item in the dropdown 75 | dlg.icon = "info"; 76 | 77 | var result = dlg.Show(); 78 | //DOpus.Output("Result: " + result); 79 | 80 | // Handle the dialog result 81 | if (result == 2) { // Open All button pressed 82 | openAllStreams(adsNames, item); 83 | return; 84 | } 85 | 86 | if (result != 1) { // Cancel or close dialog 87 | //DOpus.Output("Operation cancelled."); 88 | return; 89 | } 90 | 91 | // Get the selected ADS by the index from the dropdown (dlg.selection is zero-based) 92 | var selectedADS = adsNames(dlg.selection); 93 | var fullPathToADS = item.RealPath + selectedADS; // No need to add another colon 94 | 95 | // Open the selected ADS in Notepad 96 | //var cmd = 'notepad.exe "' + fullPathToADS + '"'; 97 | var cmd = editorPath + ' "' + fullPathToADS + '"'; 98 | //DOpus.Output("Executing: " + cmd); 99 | clickData.func.command.RunCommand(cmd); 100 | } 101 | -------------------------------------------------------------------------------- /User Command Scripts/Unique_Tab_Names.js: -------------------------------------------------------------------------------- 1 | // Renames tab labels in the current lister to be unique, when there are tabs for identically-named folders located in different paths 2 | // - It will prepend the tab names with the deepest-level part of the path necessary to uniquely identify the tab 3 | // - Optionally, instead of only prepending a single path part, you can prepend all relative path parts 4 | // 5 | // By: ThioJoe 6 | // Updated 3/17/25 7 | 8 | // Set the divider as desired 9 | var divider = " … "; 10 | 11 | // If true, only show one differentiating ancestor folder in front of the final folder. 12 | // If false, show from the first differing folder down to the folder immediately above the final folder. 13 | var ShowOnlySingleAncestorDiff = true; 14 | 15 | // Polyfill to add support for trim() in older JScript versions 16 | if (typeof String.prototype.trim !== 'function') { 17 | String.prototype.lTrim = function(chr){chr=chr||"\\s";return new String(this.replace(new RegExp("^"+chr+"*"),''));}; 18 | String.prototype.rTrim = function(chr){chr=chr||"\\s";return new String(this.replace(new RegExp(chr+"*$"),''));}; 19 | String.prototype.trim = function(chr){return this.lTrim(chr).rTrim(chr);}; 20 | } 21 | 22 | // Called when the user clicks the button 23 | function OnClick(clickData) { 24 | var lister = clickData.func.sourcetab.lister; 25 | var tabs = lister.tabs; 26 | 27 | // Group tabs by their final folder name 28 | var folderGroups = DOpus.Create.Map(); 29 | for (var e = new Enumerator(tabs); !e.atEnd(); e.moveNext()) { 30 | var tab = e.item(); 31 | var folderName = tab.path.filepart; 32 | 33 | if (!folderGroups.exists(folderName)) { 34 | folderGroups(folderName) = DOpus.Create.Vector(); 35 | } 36 | folderGroups(folderName).push_back(tab); 37 | } 38 | 39 | // We'll store the final label for each unique path in this map: 40 | // Key = path (string) 41 | // Value = label (string) 42 | var tabNames = DOpus.Create.Map(); 43 | 44 | // Process each group 45 | for (var e = new Enumerator(folderGroups); !e.atEnd(); e.moveNext()) { 46 | var folderName = e.item(); 47 | var group = folderGroups(folderName); 48 | 49 | if (group.count === 1) { 50 | // Only one tab with this folder name => label is just the folder name 51 | tabNames(group(0).path) = folderName; 52 | continue; 53 | } 54 | 55 | // We have multiple tabs sharing the same final folder name. 56 | // 1) Remove duplicates so we only create one label per unique path. 57 | var uniqueTabsMap = DOpus.Create.Map(); 58 | for (var i = 0; i < group.count; i++) { 59 | var t = group(i); 60 | if (!uniqueTabsMap.exists(t.path)) { 61 | uniqueTabsMap(t.path) = t; 62 | } 63 | } 64 | 65 | // 2) Put them in a vector so we can iterate 66 | var uniqueTabsVector = DOpus.Create.Vector(); 67 | for (var ee = new Enumerator(uniqueTabsMap); !ee.atEnd(); ee.moveNext()) { 68 | var uniquePath = ee.item(); 69 | uniqueTabsVector.push_back(uniqueTabsMap(uniquePath)); 70 | } 71 | 72 | // 3) Assign labels based on Single vs Multi logic 73 | if (ShowOnlySingleAncestorDiff) { 74 | AssignLabelsSingleAncestor(uniqueTabsVector, tabNames, folderName); 75 | } else { 76 | AssignLabelsMultiAncestor(uniqueTabsVector, tabNames, folderName); 77 | } 78 | } 79 | 80 | // Finally apply these labels to all tabs 81 | for (var tEnum = new Enumerator(tabs); !tEnum.atEnd(); tEnum.moveNext()) { 82 | var tab = tEnum.item(); 83 | var path = tab.path; 84 | if (tabNames.exists(path)) { 85 | var finalName = tabNames(path); 86 | if (tab.displayed_label != finalName) { 87 | //DOpus.Output("Renaming tab: " + path); 88 | //DOpus.Output(" Original: " + tab.displayed_label); 89 | //DOpus.Output(" New: " + finalName); 90 | 91 | var cmd = clickData.func.command; 92 | cmd.SetSourceTab(tab); 93 | cmd.RunCommand("Go TABNAME=\"" + EscapeOpusArg(finalName) + "\""); 94 | } 95 | } 96 | } 97 | } 98 | 99 | //--------------------------------------------------------------------------------- 100 | // ShowOnlySingleAncestorDiff = true 101 | // We pick exactly one ancestor folder that’s unique if possible 102 | function AssignLabelsSingleAncestor(tabsVector, tabNames, folderName) { 103 | var count = tabsVector.count; 104 | if (count < 1) return; 105 | 106 | // chosenAncestors[i] holds the single differentiating ancestor for tabsVector[i] 107 | var chosenAncestors = []; 108 | var doneFlags = []; 109 | for (var i = 0; i < count; i++) { 110 | chosenAncestors.push(""); 111 | doneFlags.push(false); 112 | } 113 | 114 | var level = 1; // how many levels above the final folder (1=immediate parent, 2=grandparent, etc.) 115 | while (true) { 116 | var stillNotDone = false; 117 | 118 | // For each tab not done, pick the folder name at the current 'level' 119 | for (var i = 0; i < count; i++) { 120 | if (doneFlags[i]) continue; 121 | var pathComps = tabsVector(i).path.Split(); 122 | var idx = pathComps.count - 1 - level; // the 'ancestor' we're examining 123 | if (idx >= 0) { 124 | chosenAncestors[i] = pathComps(idx); 125 | } else { 126 | chosenAncestors[i] = ""; // we've gone above root 127 | } 128 | } 129 | 130 | // Determine how many times each chosen ancestor appears 131 | var ancestorCount = DOpus.Create.Map(); 132 | for (var i = 0; i < count; i++) { 133 | var aKey = chosenAncestors[i].toLowerCase(); 134 | if (!ancestorCount.exists(aKey)) { 135 | ancestorCount(aKey) = 0; 136 | } 137 | ancestorCount(aKey) = ancestorCount(aKey) + 1; 138 | } 139 | 140 | // Mark tabs with a unique ancestor as done 141 | for (var i = 0; i < count; i++) { 142 | if (!doneFlags[i]) { 143 | var aKey = chosenAncestors[i].toLowerCase(); 144 | if (ancestorCount(aKey) === 1) { 145 | doneFlags[i] = true; 146 | } 147 | } 148 | } 149 | 150 | // Check if still not done 151 | for (var i = 0; i < count; i++) { 152 | if (!doneFlags[i]) { 153 | stillNotDone = true; 154 | break; 155 | } 156 | } 157 | if (!stillNotDone) { 158 | // All done or can't get more unique 159 | break; 160 | } 161 | level++; 162 | if (level > 100) { 163 | // Safety stop 164 | break; 165 | } 166 | } 167 | 168 | // Assign final names 169 | for (var i = 0; i < count; i++) { 170 | var tab = tabsVector(i); 171 | var anc = chosenAncestors[i]; 172 | if (anc === "") { 173 | // No unique difference found; label is just the final folder 174 | tabNames(tab.path) = folderName; 175 | } else { 176 | tabNames(tab.path) = anc + divider + folderName; 177 | } 178 | } 179 | } 180 | 181 | //--------------------------------------------------------------------------------- 182 | // ShowOnlySingleAncestorDiff = false 183 | // We show from the first folder that differs among all tabs, down to the folder 184 | // immediately above the final folder. This is done by removing the "longest common 185 | // prefix" from all paths (except the final folder), and using whatever remains. 186 | // e.g. "Test2\Example … MyFolder" or "Test3\Example … MyFolder" 187 | function AssignLabelsMultiAncestor(tabsVector, tabNames, folderName) { 188 | var count = tabsVector.count; 189 | if (count < 1) return; 190 | 191 | // 1) Gather splitted paths 192 | var splittedArr = []; 193 | for (var i = 0; i < count; i++) { 194 | splittedArr.push(tabsVector(i).path.Split()); 195 | } 196 | 197 | // 2) Find the longest common prefix among these paths, 198 | // ignoring the final folder component in each path. 199 | var commonPrefixLen = findLongestCommonPrefixLength(splittedArr); 200 | 201 | // 3) Build label for each path by skipping that common prefix, 202 | // and also skipping the final component (which is folderName). 203 | for (var i = 0; i < count; i++) { 204 | var tab = tabsVector(i); 205 | var comps = splittedArr[i]; 206 | // The final folder is comps[comps.length-1]. 207 | // So the relevant portion is comps[commonPrefixLen .. comps.length-2]. 208 | var lastParentIndex = comps.length - 2; // the parent folder of the final 209 | var prefixComps = []; 210 | 211 | for (var c = commonPrefixLen; c <= lastParentIndex; c++) { 212 | if (c >= 0 && c < comps.length - 1) { 213 | prefixComps.push(comps[c]); 214 | } 215 | } 216 | 217 | var prefixStr = prefixComps.join("\\"); // use backslash as the user prefers 218 | 219 | if (prefixStr === "") { 220 | // Means there's no difference in parent path 221 | tabNames(tab.path) = folderName; 222 | } else { 223 | tabNames(tab.path) = prefixStr + divider + folderName; 224 | } 225 | } 226 | } 227 | 228 | //--------------------------------------------------------------------------------- 229 | // Find the number of leading components that are common among all paths, ignoring 230 | // their final folder. We'll compare each path up to path.length - 1 (exclude final). 231 | // Returns an integer prefix length (0-based). 232 | function findLongestCommonPrefixLength(pathsArr) { 233 | // If there's only 1 path, the "common prefix" is all but the final folder 234 | // but we can just return pathsArr[0].length-1. Either approach is fine. 235 | if (pathsArr.length === 1) { 236 | return 0; // trivially no need for a prefix 237 | } 238 | 239 | // We'll compare component by component 240 | var minLength = 999999; // This will be updated later 241 | // first find the shortest path's relevant length (excluding final folder) 242 | for (var i = 0; i < pathsArr.length; i++) { 243 | var lenExcludingFinal = pathsArr[i].length - 1; 244 | if (lenExcludingFinal < minLength) { 245 | minLength = lenExcludingFinal; 246 | } 247 | } 248 | // minLength is the maximum possible prefix length we can compare 249 | 250 | var prefixLen = 0; 251 | while (prefixLen < minLength) { 252 | // Compare the prefixLen'th component of all 253 | var firstComp = pathsArr[0][prefixLen].toLowerCase(); 254 | var allSame = true; 255 | for (var j = 1; j < pathsArr.length; j++) { 256 | var comp = pathsArr[j][prefixLen].toLowerCase(); 257 | if (comp !== firstComp) { 258 | allSame = false; 259 | break; 260 | } 261 | } 262 | if (!allSame) { 263 | break; 264 | } 265 | prefixLen++; 266 | } 267 | return prefixLen; 268 | } 269 | 270 | //--------------------------------------------------------------------------------- 271 | function EscapeOpusArg(arg) { 272 | // Escape double-quotes for Opus commands 273 | return arg.replace(/"/g, '""'); 274 | } 275 | --------------------------------------------------------------------------------