From 7e3f9f0c3f5a74adc48dcd22873980ee3f029b3f Mon Sep 17 00:00:00 2001 From: Christian Gaarden Gaardmark Date: Mon, 2 Mar 2026 06:16:01 -0800 Subject: [PATCH] New+: Fixed issue with files and folders containing only numbers (#45439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Supersedes https://github.com/microsoft/PowerToys/pull/41465 1) Fix for where template file or folder only contained numbers 2) Fix for where hidden files are shown in the list of templates ## PR Checklist - [x] Closes: #36216 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [n/a] **Tests:** Added/updated and all pass - [n/a] **Localization:** All end-user-facing strings can be localized - [n/a] **Dev docs:** Added/updated - [n/a] **New binaries:** Added on the required places - [n/a] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [n/a] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [n/a] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [n/a] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [n/a] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments 1) Fix for where template file or folder only contained numbers // Filename cases to support // type | filename | result // [file] | 01. First entry.txt | First entry.txt // [folder] | 02. Second entry | Second entry // [folder] | 03 Third entry | Third entry // [file] | 04 Fourth entry.txt | Fourth entry.txt // [file] | 05.Fifth entry.txt | Fifth entry.txt // [folder] | 001231 | 001231 // [file] | 001231.txt | 001231.txt // [file] | 13. 0123456789012345.txt | 0123456789012345.txt 2) Fix for where hidden files are shown in the list of templates Instead of excluding based on filename (desktop.ini) exclude based on hidden and system attribute being set ## Validation Steps Performed ### Before fix Notice 1) Folders with numbers only aren't displayed on the context menu 2) Files with extension with numbers only show extension on the context menu 3) Some hidden files are shown image ### After fixes #### Scenario 1 New+ Setting: Hide leading digits…: Yes New+ Setting: Hide file extension: Yes New+ Setting: Replace variables: No image #### Scenario 2 New+ Setting: Hide leading digits…: No New+ Setting: Hide file extension: No New+ Setting: Replace variables: No image #### Scenario 3 New+ Setting: Hide leading digits…: Yes New+ Setting: Hide file extension: Yes New+ Setting: Replace variables: Yes image --- .../helpers_filesystem.h | 16 ---- .../helpers_variables.h | 14 ++- .../template_folder.cpp | 2 +- .../template_item.cpp | 89 +++++++++++++++++-- 4 files changed, 98 insertions(+), 23 deletions(-) diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h index fac9561944..691b06bf91 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h @@ -4,22 +4,6 @@ namespace newplus::helpers::filesystem { - namespace constants::non_localizable - { - constexpr WCHAR desktop_ini_filename[] = L"desktop.ini"; - } - - inline bool is_hidden(const std::filesystem::path path) - { - const std::filesystem::path::string_type name = path.filename(); - if (name == constants::non_localizable::desktop_ini_filename) - { - return true; - } - - return false; - } - inline bool is_directory(const std::filesystem::path path) { const auto entry = std::filesystem::directory_entry(path); diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h index 1d511f2afe..2750fa59d1 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h @@ -129,6 +129,18 @@ namespace newplus::helpers::variables return result; } + static bool exclude_item(const std::filesystem::path& path) + { + DWORD attrs = GetFileAttributesW(path.c_str()); + if (attrs == INVALID_FILE_ATTRIBUTES) + { + return false; + } + + // Exclude if hidden or system + return (attrs & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) != 0; + } + inline void resolve_variables_in_filename_and_rename_files(const std::filesystem::path& path, const bool do_rename = true) { // Depth first recursion, so that we start renaming the leaves, and avoid having to rescan @@ -143,7 +155,7 @@ namespace newplus::helpers::variables // Perform the actual rename for (const auto& current : std::filesystem::directory_iterator(path)) { - if (!newplus::helpers::filesystem::is_hidden(current)) + if (!exclude_item(current)) { const std::filesystem::path resolved_path = resolve_variables_in_path(current.path()); diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp index 7f50dc0352..ac66d700f6 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp @@ -35,7 +35,7 @@ void template_folder::rescan_template_folder() } else { - if (!helpers::filesystem::is_hidden(entry.path())) + if (!newplus::helpers::variables::exclude_item(entry.path())) { files.push_back({ entry.path().wstring(), new template_item(entry) }); } diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp index a178e00195..e52bd7234d 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp @@ -1,5 +1,3 @@ - - #include "pch.h" #include "template_item.h" #include @@ -60,10 +58,91 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const { - filename.erase(0, std::min(filename.find_first_not_of(L"0123456789"), filename.size())); - filename.erase(0, std::min(filename.find_first_not_of(L" ."), filename.size())); + // Filename cases to support + // type | filename | result + // [file] | 01. First entry.txt | First entry.txt + // [folder] | 02. Second entry | Second entry + // [folder] | 03 Third entry | Third entry + // [file] | 04 Fourth entry.txt | Fourth entry.txt + // [file] | 05.Fifth entry.txt | Fifth entry.txt + // [folder] | 001231 | 001231 + // [file] | 001231.txt | 001231.txt + // [file] | 13. 0123456789012345.txt | 0123456789012345.txt - return filename; + std::filesystem::path filename_path(filename); + const std::wstring stem = filename_path.stem().wstring(); + + bool stem_is_only_digits = !stem.empty(); + for (const wchar_t c : stem) + { + if (c < L'0' || c > L'9') + { + stem_is_only_digits = false; + break; + } + } + + if (stem_is_only_digits) + { + // Edge cases where digits ARE the filename. + // If it's a file, we always keep it (e.g. 001231.txt or 001231). + // If it's a folder, we only strip if it looks like it has an extension (which is actually part of the name for folders). + // e.g. "0123.Name" -> Strip. "001231" -> Keep. + const bool is_folder = helpers::filesystem::is_directory(path); + const bool has_extension = filename_path.has_extension(); + + if (!is_folder || !has_extension) + { + return filename; + } + } + + // Find end of leading digits + size_t digits_end_index = 0; + while (digits_end_index < filename.length() && filename[digits_end_index] >= L'0' && filename[digits_end_index] <= L'9') + { + digits_end_index++; + } + + if (digits_end_index == 0) + { + // No leading digits + return filename; + } + + // Determine if we should also strip a separator (dot or space) + size_t strip_length = digits_end_index; + + // Check patterns to strip separators: + // 1. "01. Name" -> Strip "01. " + // 2. "01 .Name" -> Strip "01 ." + // 3. "01.Name" -> Strip "01." + // 4. "01 Name" -> Strip "01 " + // 5. "01Name" -> Strip "01" (No separator) + + if (strip_length < filename.length()) + { + if (filename[strip_length] == L'.') + { + strip_length++; + // If dot is followed by space, strip that too (e.g. "01. Name") + if (strip_length < filename.length() && filename[strip_length] == L' ') + { + strip_length++; + } + } + else if (filename[strip_length] == L' ') + { + strip_length++; + // If space is followed by dot, strip that too (e.g. "01 .Name") + if (strip_length < filename.length() && filename[strip_length] == L'.') + { + strip_length++; + } + } + } + + return filename.substr(strip_length); } std::wstring template_item::get_explorer_icon() const