New+: Fixed issue with files and folders containing only numbers (#45439)

## 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
<img width="1893" height="786" alt="image"
src="https://github.com/user-attachments/assets/3845a541-499f-47a7-ae99-a92886f74214"
/>



### After fixes
#### Scenario 1
New+ Setting: Hide leading digits…: Yes
New+ Setting: Hide file extension: Yes
New+ Setting: Replace variables: No
<img width="1816" height="1185" alt="image"
src="https://github.com/user-attachments/assets/5ed2c205-d5ce-4366-90d9-c08ef4d2881f"
/>


#### Scenario 2
New+ Setting: Hide leading digits…: No
New+ Setting: Hide file extension: No
New+ Setting: Replace variables: No
<img width="1819" height="1197" alt="image"
src="https://github.com/user-attachments/assets/710265d5-94e9-4fee-9a47-a7bbb78b45bd"
/>


#### Scenario 3
New+ Setting: Hide leading digits…: Yes
New+ Setting: Hide file extension: Yes
New+ Setting: Replace variables: Yes


<img width="1816" height="1197" alt="image"
src="https://github.com/user-attachments/assets/45a90cdd-ec21-4425-9de0-c323ec90f149"
/>
This commit is contained in:
Christian Gaarden Gaardmark
2026-03-02 06:16:01 -08:00
committed by GitHub
parent 9e4bf1e3e0
commit 7e3f9f0c3f
4 changed files with 98 additions and 23 deletions

View File

@@ -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);

View File

@@ -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());

View File

@@ -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) });
}

View File

@@ -1,5 +1,3 @@
#include "pch.h"
#include "template_item.h"
#include <shellapi.h>
@@ -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