diff --git a/mods/open-in-admin-terminal.wh.cpp b/mods/open-in-admin-terminal.wh.cpp index 83d63e923e..4766ef3189 100644 --- a/mods/open-in-admin-terminal.wh.cpp +++ b/mods/open-in-admin-terminal.wh.cpp @@ -1,8 +1,8 @@ // ==WindhawkMod== // @id open-in-admin-terminal // @name Open in Admin Terminal -// @description Adds an Explorer classic context menu entry to open an elevated terminal in the current or selected folder. -// @version 1.16 +// @description Adds Explorer classic context menu entries to open a terminal (elevated or not) in the current or selected folder, and to run script files. +// @version 1.17.1 // @author aimagist // @github https://github.com/aimagist // @include explorer.exe @@ -21,10 +21,13 @@ - Right-click a folder background and open an admin terminal in that location - Right-click a folder item and open an admin terminal inside it - Right-click a drive and open an admin terminal at its root +- Optionally add a second terminal entry that opens without administrator privileges +- Optionally run `.ps1`, `.bat`, `.cmd`, `.vbs`, and `.js` script files normally, as administrator, or both - Optionally show the entry when right-clicking filesystem folders and drives in the navigation pane and Quick access - Choose your preferred terminal: Auto, Windows Terminal, PowerShell 7, Windows PowerShell, Command Prompt, WSL, Git Bash, WezTerm, Alacritty, ConEmu, or a custom command - Customize the context menu label, or let the mod use a smart default based on your terminal choice - Optionally append the terminal name to a custom label (e.g. "Open elevated (Windows Terminal)") +- Choose whether script entries include the terminal name or use shorter labels ## Preview @@ -46,9 +49,12 @@ Screenshots may show earlier builds, but current releases use runtime classic-me - The entry is injected only while Explorer's classic menu is open; disabling the mod leaves no registry cleanup behind. - The mod intentionally targets filesystem folders and drive roots only, including optional navigation pane and Quick access support. - Auto chooses Windows Terminal, PowerShell 7, Windows PowerShell, then Command Prompt. If another built-in preset is unavailable, the mod falls back to Auto instead of hiding the entry. +- Script actions use the selected terminal only when it can safely host the required interpreter. Batch scripts and keep-open Windows Script Host scripts launch through Command Prompt; menu labels show Command Prompt or Windows Script Host. WSL, Git Bash, and custom commands fall back to a compatible Windows interpreter. - Diagnostics use Windhawk's built-in logging controls. ## Version log +- 1.17.1: Fix navigation pane menu not appearing: the entry was missing when right-clicking folders or drives in File Explorer’s left sidebar and Quick access. Now it works :) +- 1.17: Added optional non-elevated terminal entries, configurable normal/elevated script actions (.ps1/.bat/.cmd/.vbs/.js), and shorter script labels. The two extra entry features are opt-in and disabled by default; terminal names remain shown in script entries by default for compatibility. - 1.16: Added native UAC shield overlay composition on the terminal menu icon using `SHGetStockIconInfo(SIID_SHIELD)`. - 1.15: Added optional navigation pane and Quick access support for filesystem folders and drives. - 1.14: Added support for Desktop context menu targets. @@ -106,6 +112,34 @@ Screenshots may show earlier builds, but current releases use runtime classic-me - appendTerminalName: false $name: Append terminal name $description: When Menu text is set, append the selected terminal name in parentheses. +- showNonElevatedEntry: false + $name: Show non-elevated entry + $description: Add a second context menu entry to open a terminal without admin privileges. +- nonElevatedMenuText: "" + $name: Non-elevated menu text + $description: Leave empty for auto label based on terminal name. +- showOnScriptFiles: false + $name: Show on script files + $description: Add context menu entries when right-clicking supported script files. +- scriptMenuEntries: admin + $name: Script menu entries + $description: Choose which actions to show for script files. + $options: + - admin: Run as administrator + - normal: Run normally + - both: Show both +- appendScriptTerminalName: true + $name: Append terminal name to script entries + $description: Add the selected terminal name to script actions. Disable for shorter labels. +- scriptExtensions: ".ps1;.bat;.cmd;.vbs;.js" + $name: Script extensions + $description: "Semicolon-separated filter for supported script extensions: .ps1, .bat, .cmd, .vbs, and .js." +- keepOpenAfterScript: true + $name: Keep terminal open after script + $description: Terminal stays open after script finishes. +- scriptExecutionPolicyBypass: false + $name: Bypass execution policy for .ps1 + $description: Adds -ExecutionPolicy Bypass for PowerShell scripts. Disabled by default. */ // ==/WindhawkModSettings== @@ -134,6 +168,14 @@ struct Settings { bool showOnDriveItem; bool showOnNavigationPane; std::wstring position; + bool showNonElevatedEntry; + std::wstring nonElevatedMenuText; + bool showOnScriptFiles; + std::wstring scriptMenuEntries; + bool appendScriptTerminalName; + std::wstring scriptExtensions; + bool keepOpenAfterScript; + bool scriptExecutionPolicyBypass; }; enum class TargetKind { @@ -141,6 +183,7 @@ enum class TargetKind { FolderBackground, FolderItem, DriveItem, + ScriptItem, }; struct MenuTarget { @@ -154,10 +197,18 @@ struct LaunchSpec { std::wstring workingDirectory; }; +static Settings GetSettingsSnapshot(); +static void LaunchTerminalNonElevated(const MenuTarget&); +static LaunchSpec BuildScriptLaunchSpec(const Settings&, const std::wstring&); +static bool IsScriptExtension(const std::wstring&, const Settings&); +static bool IsShellViewWindow(HWND hwnd); +static bool ResolveSystemExecutablePath(PCWSTR exe, std::wstring& pathOut); + static Settings g_settings; static SRWLOCK g_settingsLock = SRWLOCK_INIT; static const UINT kMenuCommandId = 0xBF31; +static const UINT kMenuCommandIdNonElevated = 0xBF32; #ifndef IO_REPARSE_TAG_APPEXECLINK #define IO_REPARSE_TAG_APPEXECLINK (0x8000001BL) @@ -428,7 +479,7 @@ static void ResolveSettingsTerminal(Settings& s) { } s.terminalEffectiveChoice = L"cmd"; - s.terminalDisplayCommand = L"cmd.exe"; + ResolveSystemExecutablePath(L"cmd.exe", s.terminalDisplayCommand); } static std::wstring GetTerminalDisplayNameForChoice(const std::wstring& choice) { @@ -486,6 +537,14 @@ static Settings LoadSettings() { s.showOnDriveItem = GetSettingBool(L"showOnDriveItem"); s.showOnNavigationPane = GetSettingBool(L"showOnNavigationPane"); s.position = GetSettingString(L"position", L"Top"); + s.showNonElevatedEntry = GetSettingBool(L"showNonElevatedEntry"); + s.nonElevatedMenuText = GetSettingString(L"nonElevatedMenuText", L""); + s.showOnScriptFiles = GetSettingBool(L"showOnScriptFiles"); + s.scriptMenuEntries = GetSettingString(L"scriptMenuEntries", L"admin"); + s.appendScriptTerminalName = GetSettingBool(L"appendScriptTerminalName"); + s.scriptExtensions = GetSettingString(L"scriptExtensions", L".ps1;.bat;.cmd;.vbs;.js"); + s.keepOpenAfterScript = GetSettingBool(L"keepOpenAfterScript"); + s.scriptExecutionPolicyBypass = GetSettingBool(L"scriptExecutionPolicyBypass"); ResolveSettingsTerminal(s); @@ -508,9 +567,32 @@ static bool IsTargetEnabled(const Settings& s, TargetKind kind) { if (kind == TargetKind::DriveItem) { return s.showOnDriveItem; } + if (kind == TargetKind::ScriptItem) { + return s.showOnScriptFiles; + } return false; } +static bool ResolveSystemExecutablePath(PCWSTR exe, std::wstring& pathOut) { + pathOut.clear(); + + WCHAR systemDirectory[MAX_PATH] = {}; + UINT length = GetSystemDirectoryW(systemDirectory, ARRAYSIZE(systemDirectory)); + if (!length || length >= ARRAYSIZE(systemDirectory)) { + return false; + } + + pathOut.assign(systemDirectory, length); + pathOut += L'\\'; + pathOut += exe; + if (!IsFilePath(pathOut)) { + pathOut.clear(); + return false; + } + + return true; +} + static PCWSTR TargetKindName(TargetKind kind) { if (kind == TargetKind::FolderBackground) { return L"folder-background"; @@ -521,6 +603,9 @@ static PCWSTR TargetKindName(TargetKind kind) { if (kind == TargetKind::DriveItem) { return L"drive-item"; } + if (kind == TargetKind::ScriptItem) { + return L"script-item"; + } return L"none"; } @@ -529,6 +614,33 @@ static bool IsDirectoryPath(const std::wstring& path) { return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY); } +static bool IsScriptExtension(const std::wstring& path, const Settings& s) { + PCWSTR ext = PathFindExtensionW(path.c_str()); + if (!ext || !*ext) { + return false; + } + + if (_wcsicmp(ext, L".ps1") != 0 && _wcsicmp(ext, L".bat") != 0 && + _wcsicmp(ext, L".cmd") != 0 && _wcsicmp(ext, L".vbs") != 0 && + _wcsicmp(ext, L".js") != 0) { + return false; + } + + size_t start = 0; + while (start <= s.scriptExtensions.size()) { + size_t end = s.scriptExtensions.find(L';', start); + std::wstring token = TrimString(s.scriptExtensions.substr(start, end - start)); + if (_wcsicmp(token.c_str(), ext) == 0) { + return true; + } + if (end == std::wstring::npos) { + break; + } + start = end + 1; + } + return false; +} + static bool IsDriveRootPath(const std::wstring& path) { return PathIsRootW(path.c_str()) != FALSE; } @@ -574,6 +686,17 @@ static std::wstring QuoteCommandLineArgument(const std::wstring& arg) { return result; } +static std::wstring QuoteCmdPath(const std::wstring& path) { + return L'"' + path + L'"'; +} + +static std::wstring BuildCmdCommandLine(bool keepOpen, + const std::wstring& command) { + // /s makes cmd strip the outer pair, preserving quotes inside the command. + return std::wstring(keepOpen ? L"/s /k \"" : L"/s /c \"") + command + + L'"'; +} + static std::wstring JoinCommandLineArguments( const std::vector& args) { std::wstring result; @@ -637,11 +760,17 @@ static bool SplitCustomCommand(const std::wstring& command, return !executable.empty(); } -static void ReplaceAll(std::wstring& value, - const std::wstring& placeholder, - const std::wstring& replacement) { +static void ExpandCustomPlaceholder(std::wstring& value, + const std::wstring& placeholder, + const std::wstring& target) { size_t pos = 0; while ((pos = value.find(placeholder, pos)) != std::wstring::npos) { + bool alreadyQuoted = pos > 0 && + pos + placeholder.size() < value.size() && + value[pos - 1] == L'"' && + value[pos + placeholder.size()] == L'"'; + std::wstring replacement = + alreadyQuoted ? target : QuoteCommandLineArgument(target); value.replace(pos, placeholder.size(), replacement); pos += replacement.size(); } @@ -650,8 +779,8 @@ static void ReplaceAll(std::wstring& value, static std::wstring ExpandCustomParameters(const std::wstring& parameters, const std::wstring& target) { std::wstring result = parameters; - ReplaceAll(result, L"%V", target); - ReplaceAll(result, L"%1", target); + ExpandCustomPlaceholder(result, L"%V", target); + ExpandCustomPlaceholder(result, L"%1", target); return result; } @@ -660,15 +789,25 @@ static LaunchSpec BuildLaunchSpec(const Settings& s, const std::wstring& target) ? s.terminalChoice : s.terminalEffectiveChoice; LaunchSpec spec; - spec.executable = - s.terminalDisplayCommand.empty() ? L"cmd.exe" : s.terminalDisplayCommand; + if (s.terminalDisplayCommand.empty()) { + ResolveSystemExecutablePath(L"cmd.exe", spec.executable); + } else { + spec.executable = s.terminalDisplayCommand; + } spec.workingDirectory = target; if (choice == L"custom") { std::wstring executable; std::wstring parameters; if (SplitCustomCommand(s.customTerminalCommand, executable, parameters)) { - spec.executable = executable; + if (PathIsRelativeW(executable.c_str())) { + std::wstring resolved; + if (!SearchExecutablePath(executable.c_str(), resolved)) { + return {}; + } + executable = std::move(resolved); + } + spec.executable = std::move(executable); spec.parameters = ExpandCustomParameters(parameters, target); } return spec; @@ -689,7 +828,8 @@ static LaunchSpec BuildLaunchSpec(const Settings& s, const std::wstring& target) return spec; } if (choice == L"cmd") { - spec.parameters = L"/k cd /d " + QuoteCommandLineArgument(target); + spec.parameters = + BuildCmdCommandLine(true, L"cd /d " + QuoteCmdPath(target)); return spec; } if (choice == L"wsl") { @@ -716,6 +856,112 @@ static LaunchSpec BuildLaunchSpec(const Settings& s, const std::wstring& target) return spec; } +static LaunchSpec BuildScriptInterpreterSpec(const Settings& s, + const std::wstring& scriptPath) { + LaunchSpec spec; + PCWSTR ext = PathFindExtensionW(scriptPath.c_str()); + size_t separator = scriptPath.find_last_of(L"\\/"); + spec.workingDirectory = scriptPath.substr( + 0, separator == 2 && scriptPath.size() > 2 && scriptPath[1] == L':' + ? separator + 1 + : separator); + + if (_wcsicmp(ext, L".ps1") == 0) { + if (s.terminalEffectiveChoice == L"pwsh" || + s.terminalEffectiveChoice == L"powershell") { + spec.executable = s.terminalDisplayCommand; + } else if (!ResolveTerminalChoiceExecutable(L"pwsh", spec.executable)) { + ResolveTerminalChoiceExecutable(L"powershell", spec.executable); + } + std::vector args; + if (s.keepOpenAfterScript) { + args.push_back(L"-NoExit"); + } + if (s.scriptExecutionPolicyBypass) { + args.push_back(L"-ExecutionPolicy"); + args.push_back(L"Bypass"); + } + args.push_back(L"-File"); + args.push_back(scriptPath); + spec.parameters = JoinCommandLineArguments(args); + } else if (_wcsicmp(ext, L".bat") == 0 || + _wcsicmp(ext, L".cmd") == 0) { + ResolveSystemExecutablePath(L"cmd.exe", spec.executable); + spec.parameters = BuildCmdCommandLine(s.keepOpenAfterScript, + QuoteCmdPath(scriptPath)); + } else if (_wcsicmp(ext, L".vbs") == 0 || + _wcsicmp(ext, L".js") == 0) { + std::wstring cscriptPath; + if (!ResolveSystemExecutablePath(L"cscript.exe", cscriptPath)) { + return spec; + } + if (s.keepOpenAfterScript) { + if (!ResolveSystemExecutablePath(L"cmd.exe", spec.executable)) { + return {}; + } + spec.parameters = BuildCmdCommandLine( + true, QuoteCmdPath(cscriptPath) + L" //nologo " + + QuoteCmdPath(scriptPath)); + } else { + spec.executable = std::move(cscriptPath); + spec.parameters = L"//nologo " + QuoteCommandLineArgument(scriptPath); + } + } + return spec; +} + +static bool IsScriptHostChoice(const std::wstring& choice) { + return choice == L"wt" || choice == L"wezterm" || + choice == L"alacritty" || choice == L"conemu"; +} + +static bool UsesCmdWrapper(const Settings& s, PCWSTR extension) { + return _wcsicmp(extension, L".bat") == 0 || + _wcsicmp(extension, L".cmd") == 0 || + (s.keepOpenAfterScript && + (_wcsicmp(extension, L".vbs") == 0 || + _wcsicmp(extension, L".js") == 0)); +} + +static bool UsesSelectedTerminalHost(const Settings& s, + const std::wstring& scriptPath) { + return IsScriptHostChoice(s.terminalEffectiveChoice) && + !UsesCmdWrapper(s, PathFindExtensionW(scriptPath.c_str())); +} + +static LaunchSpec BuildScriptLaunchSpec(const Settings& s, + const std::wstring& scriptPath) { + LaunchSpec interpreter = BuildScriptInterpreterSpec(s, scriptPath); + if (!UsesSelectedTerminalHost(s, scriptPath)) { + return interpreter; + } + + LaunchSpec spec; + spec.executable = s.terminalDisplayCommand; + spec.workingDirectory = interpreter.workingDirectory; + std::vector args; + + if (s.terminalEffectiveChoice == L"wt") { + args = {L"new-tab", L"-d", interpreter.workingDirectory, + interpreter.executable}; + } else if (s.terminalEffectiveChoice == L"wezterm") { + args = {L"start", L"--cwd", interpreter.workingDirectory, L"--", + interpreter.executable}; + } else if (s.terminalEffectiveChoice == L"alacritty") { + args = {L"--working-directory", interpreter.workingDirectory, L"-e", + interpreter.executable}; + } else { + args = {L"-Dir", interpreter.workingDirectory, L"-run", + interpreter.executable}; + } + + spec.parameters = JoinCommandLineArguments(args); + if (!interpreter.parameters.empty()) { + spec.parameters += L" " + interpreter.parameters; + } + return spec; +} + static IServiceProvider* GetExplorerServiceProviderForHwnd(HWND topLevel) { IShellWindows* shellWindows = nullptr; HRESULT hr = CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL, @@ -972,34 +1218,22 @@ static UINT GetSelectedPaths(IShellView* shellView, return selectedCount; } -static bool GetSingleFilesystemPathFromShellItemArray(IShellItemArray* items, - std::wstring& pathOut) { +static bool GetFilesystemPathFromShellItem(IShellItem* item, + std::wstring& pathOut) { pathOut.clear(); - if (!items) { - return false; - } - - DWORD count = 0; - if (FAILED(items->GetCount(&count)) || count != 1) { - return false; - } - - IShellItem* item = nullptr; - if (FAILED(items->GetItemAt(0, &item)) || !item) { + if (!item) { return false; } PWSTR path = nullptr; - bool ok = SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path && - path[0]; + bool ok = SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && + path && path[0]; if (ok) { pathOut = path; } if (path) { CoTaskMemFree(path); } - - item->Release(); return ok; } @@ -1028,7 +1262,53 @@ static bool IsNavigationPaneWindow(HWND hwnd) { return false; } -static bool ResolveNavigationPaneMenuTarget(HWND hwnd, MenuTarget& targetOut) { +static bool IsKeyboardContextMenuPoint(const POINT& invocationPoint) { + return invocationPoint.x == -1 && invocationPoint.y == -1; +} + +static bool ShouldUseFocusedNavigationPaneFallback( + bool invocationPointIsNavigationPane, + bool focusedWindowIsNavigationPane, + bool ownerIsShellView, + bool invocationPointIsSameExplorerFrame) { + return !invocationPointIsNavigationPane && + !invocationPointIsSameExplorerFrame && + focusedWindowIsNavigationPane && !ownerIsShellView; +} + +static bool IsNavigationPaneContextWindow(HWND hwnd, + const POINT& invocationPoint, + bool& useSelectionFallback) { + useSelectionFallback = false; + bool invocationPointIsNavigationPane = false; + bool invocationPointIsSameExplorerFrame = false; + if (!IsKeyboardContextMenuPoint(invocationPoint)) { + HWND invocationWindow = WindowFromPoint(invocationPoint); + invocationPointIsNavigationPane = + invocationWindow && IsNavigationPaneWindow(invocationWindow); + if (invocationPointIsNavigationPane) { + return true; + } + + HWND invocationRoot = + invocationWindow ? GetAncestor(invocationWindow, GA_ROOT) : nullptr; + HWND ownerRoot = GetAncestor(hwnd, GA_ROOT); + invocationPointIsSameExplorerFrame = + invocationRoot && ownerRoot && invocationRoot == ownerRoot; + } + + HWND focusedWindow = GetFocus(); + useSelectionFallback = ShouldUseFocusedNavigationPaneFallback( + invocationPointIsNavigationPane, + focusedWindow && IsNavigationPaneWindow(focusedWindow), + IsShellViewWindow(hwnd), invocationPointIsSameExplorerFrame); + return useSelectionFallback; +} + +static bool ResolveNavigationPaneMenuTarget(HWND hwnd, + const POINT& invocationPoint, + bool useSelectionFallback, + MenuTarget& targetOut) { targetOut = {}; HWND root = GetAncestor(hwnd, GA_ROOT); @@ -1047,20 +1327,58 @@ static bool ResolveNavigationPaneMenuTarget(HWND hwnd, MenuTarget& targetOut) { SID_SNavigationPane, IID_INameSpaceTreeControl, reinterpret_cast(&navigationPane))) && navigationPane) { - IShellItemArray* selectedItems = nullptr; - if (SUCCEEDED(navigationPane->GetSelectedItems(&selectedItems)) && - selectedItems) { - std::wstring path; - if (GetSingleFilesystemPathFromShellItemArray(selectedItems, path) && - IsDirectoryPath(path)) { - targetOut.path = std::move(path); - targetOut.kind = IsDriveRootPath(targetOut.path) - ? TargetKind::DriveItem - : TargetKind::FolderItem; - ok = true; + IShellBrowser* shellBrowser = nullptr; + HWND treeWindow = nullptr; + if (SUCCEEDED(serviceProvider->QueryService( + SID_STopLevelBrowser, IID_IShellBrowser, + reinterpret_cast(&shellBrowser))) && + shellBrowser) { + shellBrowser->GetControlWindow(FCW_TREE, &treeWindow); + shellBrowser->Release(); + } + + POINT clientPoint = invocationPoint; + if (!IsKeyboardContextMenuPoint(invocationPoint) && treeWindow && + ScreenToClient(treeWindow, &clientPoint)) { + IShellItem* hitItem = nullptr; + if (SUCCEEDED(navigationPane->HitTest(&clientPoint, &hitItem)) && + hitItem) { + std::wstring path; + if (GetFilesystemPathFromShellItem(hitItem, path) && + IsDirectoryPath(path)) { + targetOut.path = std::move(path); + targetOut.kind = IsDriveRootPath(targetOut.path) + ? TargetKind::DriveItem + : TargetKind::FolderItem; + ok = true; + } + hitItem->Release(); + } + } + + if (!ok && useSelectionFallback) { + IShellItemArray* selectedItems = nullptr; + if (SUCCEEDED(navigationPane->GetSelectedItems(&selectedItems)) && + selectedItems) { + DWORD count = 0; + IShellItem* item = nullptr; + if (SUCCEEDED(selectedItems->GetCount(&count)) && count == 1 && + SUCCEEDED(selectedItems->GetItemAt(0, &item)) && item) { + std::wstring path; + if (GetFilesystemPathFromShellItem(item, path) && + IsDirectoryPath(path)) { + targetOut.path = std::move(path); + targetOut.kind = IsDriveRootPath(targetOut.path) + ? TargetKind::DriveItem + : TargetKind::FolderItem; + ok = true; + } + item->Release(); + } + selectedItems->Release(); } - selectedItems->Release(); } + navigationPane->Release(); } @@ -1068,17 +1386,31 @@ static bool ResolveNavigationPaneMenuTarget(HWND hwnd, MenuTarget& targetOut) { return ok; } +static bool ShouldUseDesktopFolderFallback(bool targetResolved, + bool sawSelection, + bool isDesktopShellView) { + return !targetResolved && !sawSelection && isDesktopShellView; +} + static bool ResolveMenuTarget(HWND hwnd, - bool allowNavigationPane, + const Settings& settings, + const POINT& invocationPoint, MenuTarget& targetOut) { targetOut = {}; - bool isNavigationPane = IsNavigationPaneWindow(hwnd); + bool useNavigationPaneSelectionFallback = false; + bool isNavigationPane = IsNavigationPaneContextWindow( + hwnd, invocationPoint, useNavigationPaneSelectionFallback); if (isNavigationPane) { - if (!allowNavigationPane) { + if (!settings.showOnNavigationPane) { return false; } - return ResolveNavigationPaneMenuTarget(hwnd, targetOut); + return ResolveNavigationPaneMenuTarget( + hwnd, invocationPoint, useNavigationPaneSelectionFallback, targetOut); + } + + if (!IsShellViewWindow(hwnd)) { + return false; } HWND root = GetAncestor(hwnd, GA_ROOT); @@ -1096,6 +1428,7 @@ static bool ResolveMenuTarget(HWND hwnd, } bool ok = false; + bool sawSelection = false; IFolderView* folderView = nullptr; if (SUCCEEDED(shellView->QueryInterface(IID_IFolderView, reinterpret_cast(&folderView))) && @@ -1103,6 +1436,9 @@ static bool ResolveMenuTarget(HWND hwnd, int selectedCount = 0; bool hasSelectedCount = SUCCEEDED(folderView->ItemCount(SVGIO_SELECTION, &selectedCount)); + if (hasSelectedCount && selectedCount > 0) { + sawSelection = true; + } if (hasSelectedCount && selectedCount > 1) { ok = false; @@ -1117,6 +1453,12 @@ static bool ResolveMenuTarget(HWND hwnd, } else { std::vector selectedPaths; UINT shellSelectedCount = GetSelectedPaths(shellView, selectedPaths, 2); + if (shellSelectedCount > 0) { + sawSelection = true; + } + if (!selectedPaths.empty()) { + sawSelection = true; + } if (selectedPaths.empty()) { if (!hasSelectedCount && shellSelectedCount == 0) { @@ -1136,13 +1478,22 @@ static bool ResolveMenuTarget(HWND hwnd, targetOut.kind = IsDriveRootPath(targetOut.path) ? TargetKind::DriveItem : TargetKind::FolderItem; ok = true; + } else if (selectedPaths.size() == 1 && + IsFilePath(selectedPaths[0])) { + if (settings.showOnScriptFiles && + IsScriptExtension(selectedPaths[0], settings)) { + targetOut.path = selectedPaths[0]; + targetOut.kind = TargetKind::ScriptItem; + ok = true; + } } } folderView->Release(); } - if (!ok && isDesktopShellView) { + if (ShouldUseDesktopFolderFallback(ok, sawSelection, + isDesktopShellView)) { std::wstring folderPath; if (GetDesktopFolderPath(folderPath) && IsDirectoryPath(folderPath)) { targetOut.kind = TargetKind::FolderBackground; @@ -1408,6 +1759,51 @@ static HBITMAP GetCachedMenuBitmapForTerminal(const Settings& settings) { return bitmap; } +static HBITMAP GetCachedMenuBitmapNoShield(const Settings& settings) { + std::wstring key = GetMenuBitmapCacheKey(settings) + L"\nno-shield"; + + AcquireSRWLockShared(&g_menuBitmapLock); + for (const auto& entry : g_menuBitmapCache) { + if (entry.key == key) { + HBITMAP bitmap = entry.bitmap; + ReleaseSRWLockShared(&g_menuBitmapLock); + return bitmap; + } + } + ReleaseSRWLockShared(&g_menuBitmapLock); + + std::wstring exePath; + HBITMAP bitmap = nullptr; + if (ResolveExecutablePathForIcon(settings, exePath)) { + SHFILEINFOW shfi = {}; + if (SHGetFileInfoW(exePath.c_str(), FILE_ATTRIBUTE_NORMAL, &shfi, + sizeof(shfi), + SHGFI_ICON | SHGFI_SMALLICON | + SHGFI_USEFILEATTRIBUTES)) { + bitmap = CreateMenuBitmapFromIcon(shfi.hIcon, nullptr); + if (shfi.hIcon) { + DestroyIcon(shfi.hIcon); + } + } + } + + AcquireSRWLockExclusive(&g_menuBitmapLock); + for (const auto& entry : g_menuBitmapCache) { + if (entry.key == key) { + if (bitmap) { + DeleteObject(bitmap); + } + bitmap = entry.bitmap; + ReleaseSRWLockExclusive(&g_menuBitmapLock); + return bitmap; + } + } + g_menuBitmapCache.push_back({std::move(key), bitmap}); + ReleaseSRWLockExclusive(&g_menuBitmapLock); + + return bitmap; +} + static void ClearMenuBitmapCache() { AcquireSRWLockExclusive(&g_menuBitmapLock); for (const auto& entry : g_menuBitmapCache) { @@ -1425,7 +1821,47 @@ static void ClearCurrentMenuState() { g_currentMenuHwnd = nullptr; } -static void InsertAdminTerminalMenuItem(HMENU menu, const Settings& settings) { +static bool ShouldClearMenuStateAfterTracking(UINT flags, BOOL result) { + return (flags & TPM_RETURNCMD) || !result; +} + +static std::wstring GetScriptTerminalDisplayName(const Settings& settings, + const std::wstring& scriptPath) { + if (UsesSelectedTerminalHost(settings, scriptPath)) { + return GetTerminalDisplayName(settings); + } + + PCWSTR extension = PathFindExtensionW(scriptPath.c_str()); + if (_wcsicmp(extension, L".bat") == 0 || + _wcsicmp(extension, L".cmd") == 0) { + return L"Command Prompt"; + } + if (_wcsicmp(extension, L".vbs") == 0 || + _wcsicmp(extension, L".js") == 0) { + return L"Windows Script Host"; + } + + LaunchSpec interpreter = BuildScriptInterpreterSpec(settings, scriptPath); + PCWSTR executableName = PathFindFileNameW(interpreter.executable.c_str()); + return _wcsicmp(executableName, L"pwsh.exe") == 0 ? L"PowerShell 7" + : L"Windows PowerShell"; +} + +static Settings GetScriptIconSettings(const Settings& settings, + const std::wstring& scriptPath) { + if (UsesSelectedTerminalHost(settings, scriptPath)) { + return settings; + } + + Settings iconSettings = settings; + LaunchSpec interpreter = BuildScriptInterpreterSpec(settings, scriptPath); + iconSettings.terminalEffectiveChoice.clear(); + iconSettings.terminalDisplayCommand = interpreter.executable; + return iconSettings; +} + +static void InsertAdminTerminalMenuItem(HMENU menu, const Settings& settings, + const MenuTarget& target) { int itemCount = GetMenuItemCount(menu); int insertPos = 0; bool separatorAbove = false; @@ -1457,35 +1893,105 @@ static void InsertAdminTerminalMenuItem(HMENU menu, const Settings& settings) { insertPos = 0; } + bool isScript = target.kind == TargetKind::ScriptItem; + bool showAdmin = !isScript || settings.scriptMenuEntries != L"normal"; + bool showNormal = isScript + ? settings.scriptMenuEntries == L"normal" || + settings.scriptMenuEntries == L"both" + : settings.showNonElevatedEntry; + Settings iconSettings = isScript + ? GetScriptIconSettings(settings, target.path) + : settings; + std::wstring displayName = isScript + ? GetScriptTerminalDisplayName(settings, target.path) + : GetTerminalDisplayName(settings); + std::wstring adminLabel; + if (isScript) { + adminLabel = L"Run script"; + if (settings.appendScriptTerminalName) { + adminLabel += L" in " + displayName; + } + adminLabel += L" as administrator"; + } else { + adminLabel = settings.menuText; + } + std::wstring normalLabel; + if (isScript) { + normalLabel = L"Run script"; + if (settings.appendScriptTerminalName) { + normalLabel += L" in " + displayName; + } + } else { + normalLabel = settings.nonElevatedMenuText; + if (normalLabel.empty()) { + normalLabel = L"Open " + displayName + L" here"; + } + } + + int actionPos = insertPos; if (separatorAbove) { InsertMenuW(menu, insertPos, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr); - InsertMenuW(menu, insertPos + 1, MF_BYPOSITION | MF_STRING, kMenuCommandId, - settings.menuText.c_str()); - } else { - InsertMenuW(menu, insertPos, MF_BYPOSITION | MF_STRING, kMenuCommandId, - settings.menuText.c_str()); - InsertMenuW(menu, insertPos + 1, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr); - } - - HBITMAP menuBitmap = GetCachedMenuBitmapForTerminal(settings); - if (menuBitmap) { - MENUITEMINFOW itemInfo = {}; - itemInfo.cbSize = sizeof(itemInfo); - itemInfo.fMask = MIIM_BITMAP; - itemInfo.hbmpItem = menuBitmap; - if (!SetMenuItemInfoW(menu, kMenuCommandId, FALSE, &itemInfo)) { - Wh_Log(L"Menu icon assignment failed"); + actionPos++; + } + + if (showAdmin) { + InsertMenuW(menu, actionPos++, MF_BYPOSITION | MF_STRING, kMenuCommandId, + adminLabel.c_str()); + } + if (showNormal) { + InsertMenuW(menu, actionPos++, MF_BYPOSITION | MF_STRING, + kMenuCommandIdNonElevated, normalLabel.c_str()); + } + if (!separatorAbove) { + InsertMenuW(menu, actionPos, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr); + } + + if (showAdmin) { + HBITMAP menuBitmap = GetCachedMenuBitmapForTerminal(iconSettings); + if (menuBitmap) { + MENUITEMINFOW itemInfo = {}; + itemInfo.cbSize = sizeof(itemInfo); + itemInfo.fMask = MIIM_BITMAP; + itemInfo.hbmpItem = menuBitmap; + if (!SetMenuItemInfoW(menu, kMenuCommandId, FALSE, &itemInfo)) { + Wh_Log(L"Menu icon assignment failed"); + } else { + Wh_Log(L"Menu icon assigned"); + } } else { - Wh_Log(L"Menu icon assigned"); + Wh_Log(L"Menu icon unavailable"); + } + } + + if (showNormal) { + HBITMAP menuBitmapNoShield = GetCachedMenuBitmapNoShield(iconSettings); + if (menuBitmapNoShield) { + MENUITEMINFOW itemInfo = {}; + itemInfo.cbSize = sizeof(itemInfo); + itemInfo.fMask = MIIM_BITMAP; + itemInfo.hbmpItem = menuBitmapNoShield; + if (!SetMenuItemInfoW(menu, kMenuCommandIdNonElevated, FALSE, + &itemInfo)) { + Wh_Log(L"Non-elevated menu icon assignment failed"); + } else { + Wh_Log(L"Non-elevated menu icon assigned"); + } + } else { + Wh_Log(L"Non-elevated menu icon unavailable"); } - } else { - Wh_Log(L"Menu icon unavailable"); } } static void LaunchAdminTerminal(const MenuTarget& target) { Settings settings = GetSettingsSnapshot(); - LaunchSpec launch = BuildLaunchSpec(settings, target.path); + LaunchSpec launch = (target.kind == TargetKind::ScriptItem) + ? BuildScriptLaunchSpec(settings, target.path) + : BuildLaunchSpec(settings, target.path); + if (launch.executable.empty()) { + Wh_Log(L"Launch failed: no executable resolved target=%ls", + target.path.c_str()); + return; + } SHELLEXECUTEINFOW executeInfo = {}; executeInfo.cbSize = sizeof(executeInfo); @@ -1518,6 +2024,48 @@ static void LaunchAdminTerminal(const MenuTarget& target) { } } +static void LaunchTerminalNonElevated(const MenuTarget& target) { + Settings settings = GetSettingsSnapshot(); + LaunchSpec launch = (target.kind == TargetKind::ScriptItem) + ? BuildScriptLaunchSpec(settings, target.path) + : BuildLaunchSpec(settings, target.path); + if (launch.executable.empty()) { + Wh_Log(L"Launch failed: no executable resolved target=%ls", + target.path.c_str()); + return; + } + + SHELLEXECUTEINFOW executeInfo = {}; + executeInfo.cbSize = sizeof(executeInfo); + executeInfo.fMask = SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI; + executeInfo.lpVerb = L"open"; + executeInfo.lpFile = launch.executable.c_str(); + executeInfo.lpParameters = + launch.parameters.empty() ? nullptr : launch.parameters.c_str(); + executeInfo.lpDirectory = + launch.workingDirectory.empty() ? nullptr : launch.workingDirectory.c_str(); + executeInfo.nShow = SW_SHOWNORMAL; + + POINT cursorPos; + if (GetCursorPos(&cursorPos)) { + executeInfo.fMask |= SEE_MASK_HMONITOR; + executeInfo.hMonitor = MonitorFromPoint(cursorPos, MONITOR_DEFAULTTONEAREST); + } + + if (ShellExecuteExW(&executeInfo)) { + Wh_Log(L"Launch succeeded: target=%ls executable=%ls parameters=%ls", + target.path.c_str(), launch.executable.c_str(), + launch.parameters.c_str()); + } else { + DWORD error = GetLastError(); + if (error != ERROR_CANCELLED) { + Wh_Log(L"Launch failed: error=%lu target=%ls executable=%ls parameters=%ls", + error, target.path.c_str(), launch.executable.c_str(), + launch.parameters.c_str()); + } + } +} + BOOL WINAPI TrackPopupMenuEx_Hook(HMENU menu, UINT flags, int x, @@ -1527,11 +2075,21 @@ BOOL WINAPI TrackPopupMenuEx_Hook(HMENU menu, bool injected = false; ClearCurrentMenuState(); - if (menu && hwnd && - (IsShellViewWindow(hwnd) || IsNavigationPaneWindow(hwnd))) { + HWND root = hwnd ? GetAncestor(hwnd, GA_ROOT) : nullptr; + WCHAR rootClass[64] = {}; + if (root) { + GetClassNameW(root, rootClass, ARRAYSIZE(rootClass)); + } + bool eligibleWindow = hwnd && + (IsShellViewWindow(hwnd) || + _wcsicmp(rootClass, L"CabinetWClass") == 0 || + _wcsicmp(rootClass, L"ExploreWClass") == 0); + + if (menu && eligibleWindow) { MenuTarget target; Settings settings = GetSettingsSnapshot(); - if (!ResolveMenuTarget(hwnd, settings.showOnNavigationPane, target)) { + POINT invocationPoint = {x, y}; + if (!ResolveMenuTarget(hwnd, settings, invocationPoint, target)) { Wh_Log(L"Injection skipped: no eligible filesystem directory target"); } else if (!IsTargetEnabled(settings, target.kind)) { Wh_Log(L"Injection skipped: target kind disabled kind=%ls path=%ls", @@ -1545,9 +2103,9 @@ BOOL WINAPI TrackPopupMenuEx_Hook(HMENU menu, } Wh_Log(L"Injection target: kind=%ls path=%ls", TargetKindName(target.kind), target.path.c_str()); - InsertAdminTerminalMenuItem(menu, settings); - Wh_Log(L"Injection inserted: position=%ls text=%ls", - settings.position.c_str(), settings.menuText.c_str()); + InsertAdminTerminalMenuItem(menu, settings, target); + Wh_Log(L"Injection inserted: position=%ls kind=%ls", + settings.position.c_str(), TargetKindName(target.kind)); injected = true; g_currentMenuHwnd = hwnd; g_currentMenuEligible = true; @@ -1565,6 +2123,17 @@ BOOL WINAPI TrackPopupMenuEx_Hook(HMENU menu, return 0; } + if (injected && (flags & TPM_RETURNCMD) && + static_cast(result) == kMenuCommandIdNonElevated) { + MenuTarget target = g_currentTarget; + ClearCurrentMenuState(); + LaunchTerminalNonElevated(target); + return 0; + } + + if (ShouldClearMenuStateAfterTracking(flags, result)) { + ClearCurrentMenuState(); + } return result; } @@ -1579,11 +2148,21 @@ BOOL WINAPI PostMessageW_Hook(HWND hwnd, UINT message, WPARAM wParam, LPARAM lPa return TRUE; } + if (message == WM_COMMAND && + LOWORD(wParam) == kMenuCommandIdNonElevated && + g_currentMenuEligible && + hwnd == g_currentMenuHwnd) { + MenuTarget target = g_currentTarget; + ClearCurrentMenuState(); + LaunchTerminalNonElevated(target); + return TRUE; + } + return PostMessageW_Orig(hwnd, message, wParam, lParam); } BOOL Wh_ModInit() { - Wh_Log(L"Init v1.16-classic"); + Wh_Log(L"Init"); g_shellIdListClipboardFormat = RegisterClipboardFormatW(L"Shell IDList Array");