Compare commits

...

5 Commits

Author SHA1 Message Date
Jiří Polášek
76058650a9 WIP 2026-02-27 14:48:35 +01:00
Jiří Polášek
795ecd79e9 Invert new checkboxes and flatten them with the rest 2026-02-27 10:29:27 +01:00
Jiří Polášek
78b67c3f58 Merge main 2026-02-27 10:05:27 +01:00
Jiří Polášek
d5f68760db Fix letter casing for location names 2026-02-27 01:05:55 +01:00
Jiří Polášek
26a4ab0d6a Add settings to hide non-apps from results
- Adds two checkboxes to All Apps settingsQ
- Allows users to hide generic files on Desktop and Start Menu from results
2026-02-23 03:00:14 +01:00
12 changed files with 338 additions and 60 deletions

View File

@@ -29,6 +29,11 @@ public class MockAppCache : IAppCache
/// </summary>
public IList<IUWPApplication> UWPs => _uwps.AsReadOnly();
/// <summary>
/// Gets a value indicating whether a background re-index is in progress.
/// </summary>
public bool IsIndexing => false;
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>

View File

@@ -13,6 +13,8 @@ public class Settings : ISettingsInterface
private readonly bool enableDesktopSource;
private readonly bool enableRegistrySource;
private readonly bool enablePathEnvironmentVariableSource;
private readonly bool includeNonAppsOnDesktop;
private readonly bool includeNonAppsInStartMenu;
private readonly List<string> programSuffixes;
private readonly List<string> runCommandSuffixes;
@@ -21,6 +23,8 @@ public class Settings : ISettingsInterface
bool enableDesktopSource = true,
bool enableRegistrySource = true,
bool enablePathEnvironmentVariableSource = true,
bool includeNonAppsOnDesktop = false,
bool includeNonAppsInStartMenu = true,
List<string> programSuffixes = null,
List<string> runCommandSuffixes = null)
{
@@ -28,6 +32,8 @@ public class Settings : ISettingsInterface
this.enableDesktopSource = enableDesktopSource;
this.enableRegistrySource = enableRegistrySource;
this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource;
this.includeNonAppsOnDesktop = includeNonAppsOnDesktop;
this.includeNonAppsInStartMenu = includeNonAppsInStartMenu;
this.programSuffixes = programSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url" };
this.runCommandSuffixes = runCommandSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url", "cpl", "msc" };
}
@@ -40,6 +46,10 @@ public class Settings : ISettingsInterface
public bool EnablePathEnvironmentVariableSource => enablePathEnvironmentVariableSource;
public bool IncludeNonAppsOnDesktop => includeNonAppsOnDesktop;
public bool IncludeNonAppsInStartMenu => includeNonAppsInStartMenu;
public List<string> ProgramSuffixes => programSuffixes;
public List<string> RunCommandSuffixes => runCommandSuffixes;

View File

@@ -21,13 +21,19 @@ public sealed partial class AllAppsPage : ListPage
private readonly IAppCache _appCache;
private AppListItem[] allAppListItems = [];
private volatile bool _isRebuilding;
public AllAppsPage()
: this(AppCache.Instance.Value)
: this(AppCache.Instance.Value, AllAppsSettings.Instance.Settings)
{
}
public AllAppsPage(IAppCache appCache)
: this(appCache, new Settings())
{
}
public AllAppsPage(IAppCache appCache, Settings settings)
{
_appCache = appCache ?? throw new ArgumentNullException(nameof(appCache));
this.Name = Resources.all_apps;
@@ -43,12 +49,65 @@ public sealed partial class AllAppsPage : ListPage
BuildListItems();
}
});
settings.SettingsChanged += (s, a) =>
{
if (_appCache.IsIndexing && !_isRebuilding)
{
// A background re-index is in progress. Show a loading
// indicator and banner, then wait for it to finish.
_isRebuilding = true;
this.IsLoading = true;
RaiseItemsChanged();
_ = Task.Run(async () =>
{
while (_appCache.IsIndexing)
{
await Task.Delay(200).ConfigureAwait(false);
}
lock (_listLock)
{
allAppListItems = GetPrograms();
_appCache.ResetReloadFlag();
}
_isRebuilding = false;
this.IsLoading = false;
RaiseItemsChanged();
});
}
else
{
RaiseItemsChanged();
}
};
}
public override IListItem[] GetItems()
{
// Build or update the list if needed
BuildListItems();
if (!_isRebuilding)
{
// Build or update the list if needed
BuildListItems();
}
if (_isRebuilding && allAppListItems.Length > 0)
{
var banner = new ListItem(new NoOpCommand())
{
Title = Resources.refreshing_app_list,
Icon = Icons.Reloading,
};
var result = new IListItem[allAppListItems.Length + 3];
result[0] = new Separator(Resources.section_status);
result[1] = banner;
result[2] = new Separator(Resources.section_all_apps);
allAppListItems.CopyTo(result, 3);
return result;
}
return allAppListItems;
}
@@ -84,7 +143,7 @@ public sealed partial class AllAppsPage : ListPage
{
if (uwpApp.Enabled)
{
items.Add(new AppListItem(uwpApp.ToAppItem(), true));
items.Add(new AppListItem(uwpApp.ToAppItem(), useThumbnails: true));
}
}
@@ -92,7 +151,7 @@ public sealed partial class AllAppsPage : ListPage
{
if (win32App.Enabled && win32App.Valid)
{
items.Add(new AppListItem(win32App.ToAppItem(), true));
items.Add(new AppListItem(win32App.ToAppItem(), useThumbnails: true));
}
}

View File

@@ -62,6 +62,10 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
public bool EnablePathEnvironmentVariableSource => _enablePathEnvironmentVariableSource.Value;
public bool IncludeNonAppsOnDesktop => _includeNonAppsOnDesktop.Value;
public bool IncludeNonAppsInStartMenu => _includeNonAppsInStartMenu.Value;
private readonly ChoiceSetSetting _searchResultLimitSource = new(
Namespaced(nameof(SearchResultLimit)),
Resources.limit_fallback_results_source,
@@ -121,6 +125,18 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
string.Empty,
false); // this one is very VERY noisy
private readonly ToggleSetting _includeNonAppsOnDesktop = new(
Namespaced(nameof(IncludeNonAppsOnDesktop)),
Resources.include_non_apps_on_desktop,
string.Empty,
false);
private readonly ToggleSetting _includeNonAppsInStartMenu = new(
Namespaced(nameof(IncludeNonAppsInStartMenu)),
Resources.include_non_apps_in_start_menu,
string.Empty,
true);
public double MinScoreThreshold { get; set; } = 0.75;
internal const char SuffixSeparator = ';';
@@ -139,7 +155,9 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
FilePath = SettingsJsonPath();
Settings.Add(_enableStartMenuSource);
Settings.Add(_includeNonAppsInStartMenu);
Settings.Add(_enableDesktopSource);
Settings.Add(_includeNonAppsOnDesktop);
Settings.Add(_enableRegistrySource);
Settings.Add(_enablePathEnvironmentVariableSource);
Settings.Add(_searchResultLimitSource);

View File

@@ -83,6 +83,8 @@ public sealed partial class AppCache : IAppCache, IDisposable
}
}
public bool IsIndexing => _win32ProgramRepository.IsIndexing;
public bool ShouldReload() => _packageRepository.ShouldReload() || _win32ProgramRepository.ShouldReload();
public void ResetReloadFlag()

View File

@@ -19,4 +19,8 @@ public interface ISettingsInterface
public List<string> ProgramSuffixes { get; }
public List<string> RunCommandSuffixes { get; }
public bool IncludeNonAppsOnDesktop { get; }
public bool IncludeNonAppsInStartMenu { get; }
}

View File

@@ -33,4 +33,9 @@ public interface IAppCache : IDisposable
/// Resets the reload flag.
/// </summary>
void ResetReloadFlag();
/// <summary>
/// Gets a value indicating whether a background re-index is in progress.
/// </summary>
bool IsIndexing { get; }
}

View File

@@ -25,4 +25,6 @@ internal static class Icons
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
public static IconInfo GenericAppIcon { get; } = new("\uE737"); // Favicon
internal static IconInfo Reloading { get; } = new("\uF16A"); // ProgressRing
}

View File

@@ -67,6 +67,10 @@ public class Win32Program : IProgram
public bool Enabled { get; set; }
public bool IsInDesktop { get; set; }
public bool IsInStartMenu { get; set; }
public bool HasArguments => !string.IsNullOrEmpty(Arguments);
public string Arguments { get; set; } = string.Empty;
@@ -382,6 +386,8 @@ public class Win32Program : IProgram
}
program.LnkFilePath = program.FullPath;
program.IsInDesktop = StartsWithAnyFolder(program.LnkFilePath, Environment.SpecialFolder.Desktop, Environment.SpecialFolder.CommonDesktopDirectory);
program.IsInStartMenu = StartsWithAnyFolder(program.LnkFilePath, Environment.SpecialFolder.StartMenu, Environment.SpecialFolder.CommonStartMenu);
program.LnkResolvedExecutableName = Path.GetFileName(target);
program.LnkResolvedExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(target));
@@ -803,24 +809,6 @@ public class Win32Program : IProgram
public override bool Equals(object? obj)
=> obj is Win32Program win32Program && Win32ProgramEqualityComparer.Default.Equals(this, win32Program);
private sealed class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
{
public static readonly Win32ProgramEqualityComparer Default = new();
public bool Equals(Win32Program? app1, Win32Program? app2)
{
return app1 is null && app2 is null
? true
: app1 is not null
&& app2 is not null
&& (app1.Name?.ToUpperInvariant(), app1.ExecutableName?.ToUpperInvariant(), app1.FullPath?.ToUpperInvariant())
.Equals((app2.Name?.ToUpperInvariant(), app2.ExecutableName?.ToUpperInvariant(), app2.FullPath?.ToUpperInvariant()));
}
public int GetHashCode(Win32Program obj)
=> (obj.Name?.ToUpperInvariant(), obj.ExecutableName?.ToUpperInvariant(), obj.FullPath?.ToUpperInvariant()).GetHashCode();
}
public static List<Win32Program> DeduplicatePrograms(IEnumerable<Win32Program> programs)
{
// Create a HashSet with the custom equality comparer to automatically deduplicate programs
@@ -1065,4 +1053,44 @@ public class Win32Program : IProgram
FullExecutablePath = app.FullPath,
};
}
private static bool StartsWithAnyFolder(string path, params Environment.SpecialFolder[] folders)
{
foreach (var folder in folders)
{
var folderPath = Environment.GetFolderPath(folder, Environment.SpecialFolderOption.DoNotVerify);
if (!string.IsNullOrEmpty(folderPath) && path.StartsWith(folderPath, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private sealed class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
{
public static readonly Win32ProgramEqualityComparer Default = new();
public bool Equals(Win32Program? app1, Win32Program? app2)
{
if (app1 is null && app2 is null)
{
return true;
}
if (app1 is null || app2 is null)
{
return false;
}
return string.Equals(app1.Name, app2.Name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(app1.ExecutableName, app2.ExecutableName, StringComparison.OrdinalIgnoreCase)
&& string.Equals(app1.FullPath, app2.FullPath, StringComparison.OrdinalIgnoreCase)
&& string.Equals(app1.Arguments, app2.Arguments, StringComparison.Ordinal);
}
public int GetHashCode(Win32Program app)
=> (app.Name?.ToUpperInvariant(), app.ExecutableName?.ToUpperInvariant(), app.FullPath?.ToUpperInvariant(), app.Arguments).GetHashCode();
}
}

View File

@@ -106,7 +106,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
/// <summary>
/// Looks up a localized string similar to Include apps registered in the Registry.
/// Looks up a localized string similar to Include apps registered in the Windows Registry.
/// </summary>
internal static string enable_registry_source {
get {
@@ -115,7 +115,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
/// <summary>
/// Looks up a localized string similar to Include apps found in the Start Menu.
/// Looks up a localized string similar to Include apps found in the Start menu.
/// </summary>
internal static string enable_start_menu_source {
get {
@@ -141,6 +141,42 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Also include non-app shortcuts from the Start menu.
/// </summary>
internal static string include_non_apps_in_start_menu {
get {
return ResourceManager.GetString("include_non_apps_in_start_menu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Also include non-app shortcuts from the desktop.
/// </summary>
internal static string include_non_apps_on_desktop {
get {
return ResourceManager.GetString("include_non_apps_on_desktop", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Status.
/// </summary>
internal static string section_status {
get {
return ResourceManager.GetString("section_status", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All apps.
/// </summary>
internal static string section_all_apps {
get {
return ResourceManager.GetString("section_all_apps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installed apps.
/// </summary>
@@ -285,6 +321,15 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Refreshing app list....
/// </summary>
internal static string refreshing_app_list {
get {
return ResourceManager.GetString("refreshing_app_list", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Run as administrator.
/// </summary>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -170,13 +170,13 @@
<value>Run as different user</value>
</data>
<data name="enable_start_menu_source" xml:space="preserve">
<value>Include apps found in the Start Menu</value>
<value>Include apps found in the Start menu</value>
</data>
<data name="enable_desktop_source" xml:space="preserve">
<value>Include apps found on the desktop</value>
</data>
<data name="enable_registry_source" xml:space="preserve">
<value>Include apps registered in the Registry</value>
<value>Include apps registered in the Windows Registry</value>
</data>
<data name="enable_path_environment_variable_source" xml:space="preserve">
<value>Include apps anywhere on the %PATH%</value>
@@ -238,4 +238,19 @@
<value>Default ({0})</value>
<comment>default option; {0} = number of items</comment>
</data>
<data name="refreshing_app_list" xml:space="preserve">
<value>Refreshing app list...</value>
</data>
<data name="section_status" xml:space="preserve">
<value>Status</value>
</data>
<data name="section_all_apps" xml:space="preserve">
<value>All apps</value>
</data>
<data name="include_non_apps_on_desktop" xml:space="preserve">
<value>Also include non-app shortcuts from the desktop</value>
</data>
<data name="include_non_apps_in_start_menu" xml:space="preserve">
<value>Also include non-app shortcuts from the Start menu</value>
</data>
</root>

View File

@@ -8,27 +8,38 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Win32Program = Microsoft.CmdPal.Ext.Apps.Programs.Win32Program;
using Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps.Storage;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
internal sealed partial class Win32ProgramRepository : ListRepository<Programs.Win32Program>, IProgramRepository
internal sealed partial class Win32ProgramRepository : ListRepository<Programs.Win32Program>, IProgramRepository, IDisposable
{
private const string LnkExtension = ".lnk";
private const string UrlExtension = ".url";
private static readonly Collection<string> ExtensionsToWatch = ["*.exe", $"*{LnkExtension}", "*.appref-ms", $"*{UrlExtension}"];
private AllAppsSettings _settings;
private IList<IFileSystemWatcherWrapper> _fileSystemWatcherHelpers;
private string[] _pathsToWatch;
private int _numberOfPathsToWatch;
private Collection<string> extensionsToWatch = new Collection<string> { "*.exe", $"*{LnkExtension}", "*.appref-ms", $"*{UrlExtension}" };
// Snapshot of source-toggle values so we can detect when a re-index is needed.
private bool _lastEnableStartMenu;
private bool _lastEnableDesktop;
private bool _lastEnableRegistry;
private bool _lastEnablePath;
private bool _isDirty;
private volatile bool _isIndexing;
private CancellationTokenSource? _indexCts;
private static ConcurrentQueue<string> commonEventHandlingQueue = new ConcurrentQueue<string>();
public bool IsIndexing => _isIndexing;
private static ConcurrentQueue<string> commonEventHandlingQueue = [];
public Win32ProgramRepository(IList<IFileSystemWatcherWrapper> fileSystemWatcherHelpers, AllAppsSettings settings, string[] pathsToWatch)
{
@@ -36,8 +47,41 @@ internal sealed partial class Win32ProgramRepository : ListRepository<Programs.W
_settings = settings ?? throw new ArgumentNullException(nameof(settings), "Win32ProgramRepository requires an initialized settings object");
_pathsToWatch = pathsToWatch;
_numberOfPathsToWatch = pathsToWatch.Length;
SnapshotSourceSettings();
InitializeFileSystemWatchers();
_settings.Settings.SettingsChanged += (s, a) =>
{
if (HaveSourceSettingsChanged())
{
SnapshotSourceSettings();
_indexCts?.Cancel();
_indexCts?.Dispose();
_indexCts = new CancellationTokenSource();
var token = _indexCts.Token;
_isIndexing = true;
_isDirty = true;
_ = Task.Run(() =>
{
try
{
IndexPrograms(token);
_isIndexing = false;
_isDirty = true;
}
catch (OperationCanceledException)
{
// Superseded by a newer indexing request.
}
});
}
else
{
ApplyNonAppFilter();
_isDirty = true;
}
};
// This task would always run in the background trying to dequeue file paths from the queue at regular intervals.
_ = Task.Run(async () =>
{
@@ -83,7 +127,7 @@ internal sealed partial class Win32ProgramRepository : ListRepository<Programs.W
_fileSystemWatcherHelpers[index].NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
// filtering the app types that we want to monitor
_fileSystemWatcherHelpers[index].Filters = extensionsToWatch;
_fileSystemWatcherHelpers[index].Filters = ExtensionsToWatch;
// Registering the event handlers
_fileSystemWatcherHelpers[index].Created += OnAppCreated;
@@ -264,9 +308,50 @@ internal sealed partial class Win32ProgramRepository : ListRepository<Programs.W
}
}
public void IndexPrograms()
public void IndexPrograms() => IndexPrograms(CancellationToken.None);
public void IndexPrograms(CancellationToken cancellationToken)
{
var applications = Win32Program.All(_settings);
cancellationToken.ThrowIfCancellationRequested();
SetList(applications);
ApplyNonAppFilter();
}
private void ApplyNonAppFilter()
{
var includeNonAppsOnDesktop = _settings.IncludeNonAppsOnDesktop;
var includeNonAppsInStartMenu = _settings.IncludeNonAppsInStartMenu;
foreach (var app in this)
{
if (app.AppType is Win32Program.ApplicationType.GenericFile or Win32Program.ApplicationType.Folder
&& !string.IsNullOrEmpty(app.LnkFilePath))
{
app.Enabled = (includeNonAppsOnDesktop || !app.IsInDesktop)
&& (includeNonAppsInStartMenu || !app.IsInStartMenu);
}
}
}
private void SnapshotSourceSettings()
{
_lastEnableStartMenu = _settings.EnableStartMenuSource;
_lastEnableDesktop = _settings.EnableDesktopSource;
_lastEnableRegistry = _settings.EnableRegistrySource;
_lastEnablePath = _settings.EnablePathEnvironmentVariableSource;
}
private bool HaveSourceSettingsChanged()
{
return _settings.EnableStartMenuSource != _lastEnableStartMenu
|| _settings.EnableDesktopSource != _lastEnableDesktop
|| _settings.EnableRegistrySource != _lastEnableRegistry
|| _settings.EnablePathEnvironmentVariableSource != _lastEnablePath;
}
public void Dispose()
{
_indexCts?.Dispose();
}
}