Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0f73170
Slight tweak to 'insertSpace' to avoid resolving objects when not needed
pjrobertson Jan 30, 2026
86c0965
Resolve indirects on a background thread - avoids blocking the main U…
pjrobertson Jan 30, 2026
bb3962c
Improve opening the prefs pane
pjrobertson Feb 1, 2026
96b2879
Remove spurious backtick
pjrobertson Feb 1, 2026
16bce56
Bring up main window syncronously
pjrobertson Feb 1, 2026
bcc7fa6
Change search documentation URL to the mkdocs instance + fix searching
pjrobertson Feb 1, 2026
d338388
Switch Bezel to use auto layout + fix up auto layout for the text mode
pjrobertson Feb 1, 2026
075b0d9
Initial plan
Copilot Feb 2, 2026
f5fc2f9
Update Quicksilver/Code-QuickStepInterface/QSInterfaceController.m
pjrobertson Feb 2, 2026
3896a96
Merge pull request #3092 from quicksilver/copilot/sub-pr-3091
pjrobertson Feb 2, 2026
37c0f95
Fix a crash when copy/pasting an NSColor
pjrobertson Feb 1, 2026
46dec5c
Perform more fuzzy searching for plugin names
pjrobertson Feb 2, 2026
2111812
Fix bezel interface auto layout issues
pjrobertson Feb 3, 2026
330e461
Set up ability to add additional search context to objects (that you …
pjrobertson Feb 3, 2026
363b53a
Extract text from image objects to allow searching for text in images
pjrobertson Feb 3, 2026
da4e0f1
Open plugin docs on the web (if they exist there)
pjrobertson Feb 3, 2026
3b29329
Filter out non-bundle appls
pjrobertson Feb 3, 2026
08cbaf5
Score 3rd pane objects when displaying
pjrobertson Feb 5, 2026
45648fe
Fix using backspace in the aSelector
pjrobertson Feb 5, 2026
6d58ce0
Fix issue when using ⌘⇧<letter> combination with an action that has k…
pjrobertson Feb 5, 2026
462458b
Update plugin changelog text color for dark mode
pjrobertson Feb 6, 2026
65ca176
Make ⌘F focus search bars in prefs
pjrobertson Feb 7, 2026
61ff927
Update donate URL to latest page
pjrobertson Feb 7, 2026
5bc4a5c
Make sure trigger hot keys are cleared when deleting with backspace i…
pjrobertson Feb 7, 2026
d1a0aca
Also make the search bar in QSAdvancedPrefPane focus with ⌘F
pjrobertson Feb 7, 2026
142d4c3
Also make the search bar in Actions Pref Pane focus with ⌘F
pjrobertson Feb 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions Quicksilver/Code-App/QSPlugInsPrefPane.m
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,32 @@ - (IBAction)downloadInBrowser:(id)sender {

- (IBAction)getInfo:(id)sender
{
[pluginInfoPanel makeKeyAndOrderFront:sender];
// Get the selected plugin(s)
NSArray *selectedPlugins = [self selectedPlugIns];
if (!selectedPlugins || [selectedPlugins count] == 0) {
[pluginInfoPanel makeKeyAndOrderFront:sender];
return;
}

// Get the first selected plugin
QSPlugIn *plugin = [selectedPlugins firstObject];
NSString *pluginIdentifier = [plugin identifier];

if (!pluginIdentifier) {
[pluginInfoPanel makeKeyAndOrderFront:sender];
return;
}

// Check if this plugin is hosted by Quicksilver
QSPlugInManager *manager = [QSPlugInManager sharedInstance];
if ([manager pluginIsHosted:pluginIdentifier]) {
// Plugin is hosted by Quicksilver - open the web page
NSString *urlString = [NSString stringWithFormat:@"https://qsapp.com/manual/plugins/s/?id=%@", pluginIdentifier];
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
} else {
// Plugin is not hosted by Quicksilver - show info panel as before
[pluginInfoPanel makeKeyAndOrderFront:sender];
}
}

- (IBAction)updatePlugIns:(id)sender { [[QSPlugInManager sharedInstance] checkForPlugInUpdates:nil]; }
Expand Down Expand Up @@ -341,6 +366,13 @@ - (NSMutableArray *)plugInSets {

- (BOOL)isItemExpanded:(id)item {return YES;}

- (NSString *)normalizedSearchString:(NSString *)string {
// Remove diacritics (é -> e, ñ -> n, etc.) and punctuation
NSString *normalized = [[string stringByApplyingTransform:NSStringTransformStripDiacritics reverse:NO] stringByReplacingCharacterRunsFromSet:[NSCharacterSet characterSetWithCharactersInString:@"?-.,$'\""] withString:@""];

return [normalized lowercaseString];
}

- (void)reloadFiltersIgnoringViewMode:(BOOL)ignoreView {
NSMutableArray *predicates = [NSMutableArray array];
if (!ignoreView) {
Expand All @@ -364,8 +396,14 @@ - (void)reloadFiltersIgnoringViewMode:(BOOL)ignoreView {
break;
}
}
if (search)
[predicates addObject:[NSPredicate predicateWithFormat:@"name contains[cd] %@", search]];
if (search) {
NSString *normalizedSearch = [self normalizedSearchString:search];
[predicates addObject:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
QSPlugIn *plugin = (QSPlugIn *)evaluatedObject;
NSString *normalizedName = [self normalizedSearchString:plugin.name];
return [normalizedName containsString:normalizedSearch];
}]];
}
if (category)
[predicates addObject:[NSPredicate predicateWithFormat:@"%@ IN SELF.categories", category]];
if (!mOptionKeyIsDown)
Expand Down
17 changes: 16 additions & 1 deletion Quicksilver/Code-App/QSPreferencesController.m
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
}

+ (void)showPrefs {
NSWindow *window = [[self sharedInstance] window];
[window orderFrontRegardless];
[NSApp activateIgnoringOtherApps:YES];
[[self sharedInstance] showWindow:nil];
[window makeKeyWindow];
[window makeMainWindow];
}


Expand Down Expand Up @@ -218,6 +221,18 @@ - (void)windowDidLoad {
[toolbar setSelectedItemIdentifier:@"QSMainMenuPrefPane"];
[self selectPaneWithIdentifier:@"QSMainMenuPrefPane"];
}

[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^NSEvent *(NSEvent *event) {
if (([event modifierFlags] & NSEventModifierFlagCommand) &&
[[event charactersIgnoringModifiers] isEqualToString:@"f"]) {
// Ask current pane if it can handle focusing a search field
if (self.currentPane.searchField) {
[self.currentPane.searchField becomeFirstResponder];
return nil; // Consume the event
}
}
return event;
}];
}

- (BOOL)relaunchRequested {
Expand Down
1 change: 1 addition & 0 deletions Quicksilver/Code-QuickStepCore/QSBasicObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
@protocol QSObject
- (NSString *)identifier;
- (NSString *)label;
- (NSString *)additionalSearchContext;
- (NSString *)name;
- (BOOL)enabled;
- (void)setEnabled:(BOOL)flag;
Expand Down
1 change: 1 addition & 0 deletions Quicksilver/Code-QuickStepCore/QSBasicObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ - (id)init {
#pragma mark QSObject Protocol
- (NSString *)identifier {return nil;}
- (NSString *)label {return nil;}
- (NSString *)additionalSearchContext { return @""; }
- (NSString *)name {return @"Object";}
- (BOOL)enabled {return ![[QSLibrarian sharedInstance] itemIsOmitted:self];}
- (void)setEnabled:(BOOL)flag {[[QSLibrarian sharedInstance] setItem:self isOmitted:!flag];}
Expand Down
2 changes: 1 addition & 1 deletion Quicksilver/Code-QuickStepCore/QSLibrarian.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#import <Foundation/Foundation.h>
#import "QSCatalogEntry.h"
#import <QSFoundation/QSThreadSafeMutableDictionary.h>`
#import <QSFoundation/QSThreadSafeMutableDictionary.h>

#define kCustomCatalogID @"QSCatalogCustom"

Expand Down
5 changes: 5 additions & 0 deletions Quicksilver/Code-QuickStepCore/QSLibrarian.m
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,11 @@ - (NSMutableArray *)scoredArrayForString:(NSString *)searchString inSet:(id)set
- (NSMutableArray *)scoredArrayForString:(NSString *)searchString inSet:(NSArray *)set mnemonicsOnly:(BOOL)mnemonicsOnly {
BOOL includeOmitted = NO;

// for the case where 'set' is a '[DefaultAction, [All Actions]]' then we need to take the 2nd element
if ([set count] == 2 && [set[1] isKindOfClass:[NSArray class]]) {
set = set[1];
}

// if we are searching within 'set' (i.e. we have drilled down or we have search results),
// then include any items that have been 'omitted' from the global catalog.
if (set) {
Expand Down
6 changes: 5 additions & 1 deletion Quicksilver/Code-QuickStepCore/QSObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ extern NSSize QSMaxIconSize;
#define kQSObjectChildrenLoadDate @"QSObjectChildrenLoadDate"
#define kQSContents @"QSObjectContents"
#define kQSObjectComponents @"QSObjectComponents"
#define kQSIsCombinedObject @"QSIsCombinedObject"
#define kQSIsCombinedObject @"QSIsCombinedObject"
#define kAdditionalSearchContext @"QSAdditionalSearchContext"



typedef struct _QSObjectFlags {
unsigned int multiTyped:1;
Expand Down Expand Up @@ -143,6 +146,7 @@ typedef struct _QSObjectFlags {
- (void)setIdentifier:(NSString *)newIdentifier;
- (NSString *)name;
- (void)setName:(NSString *)newName;
- (NSString *)additionalSearchContext;
- (NSArray *)children;
- (void)setChildren:(NSArray *)newChildren;
- (NSArray *)altChildren;
Expand Down
4 changes: 4 additions & 0 deletions Quicksilver/Code-QuickStepCore/QSObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,10 @@ - (void)setName:(NSString *)newName {
}
}

- (NSString *)additionalSearchContext {
return [self objectForCache:kAdditionalSearchContext];
}

- (NSArray *)children {
if (!flags.childrenLoaded || ![self childrenValid])
[self loadChildren];
Expand Down
1 change: 1 addition & 0 deletions Quicksilver/Code-QuickStepCore/QSObjectRanker.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extern NSString *QSRankingIncludeOmitted; // BOOL. Specifies whether the ranke
NSDictionary *usageMnemonics;
NSObject <QSStringRanker> *nameRanker;
NSObject <QSStringRanker> *labelRanker;
NSObject <QSStringRanker> *cacheRanker;
}
+ (NSMutableArray *)rankedObjectsForAbbreviation:(NSString *)anAbbreviation options:(NSDictionary *)options;
@end
32 changes: 26 additions & 6 deletions Quicksilver/Code-QuickStepCore/QSObjectRanker.m
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,18 @@ + (NSMutableArray *)rankedObjectsForAbbreviation:(NSString *)anAbbreviation opti
NSMutableArray *rankObjects = [NSMutableArray arrayWithCapacity:[objectsInSet count]];

BOOL includeOmitted = [[options objectForKey:QSRankingIncludeOmitted] boolValue];
QSBasicObject *thisObject;
QSScoreForObjectIMP scoreForObjectIMP =
(QSScoreForObjectIMP) [self instanceMethodForSelector:@selector(rankedObject:forAbbreviation:options:)];

NSObject *lock = [[NSObject alloc] init];

@autoreleasepool {
for (thisObject in objectsInSet) {
if (!includeOmitted && [[QSLibrarian sharedInstance] itemIsOmitted:thisObject]) continue;
[objectsInSet enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id thisObject, NSUInteger idx, BOOL *stop) {
if (!includeOmitted && [[QSLibrarian sharedInstance] itemIsOmitted:thisObject]) return;

id ranker = [thisObject ranker];

QSRankedObject *rankedObject;
QSRankedObject *rankedObject = nil;
if ([ranker isKindOfClass:[QSDefaultObjectRanker class]]) {
rankedObject = (*scoreForObjectIMP) (ranker, @selector(rankedObject:forAbbreviation:options:),
thisObject, anAbbreviation, options);
Expand All @@ -100,9 +103,11 @@ + (NSMutableArray *)rankedObjectsForAbbreviation:(NSString *)anAbbreviation opti
}

if (rankedObject) {
[rankObjects addObject:rankedObject];
@synchronized(lock) {
[rankObjects addObject:rankedObject];
}
}
}
}];
}
return rankObjects;
}
Expand All @@ -122,10 +127,15 @@ - (id)initWithObject:(QSBasicObject *)object {
if (self = [super init]) {
nameRanker = nil;
labelRanker = nil;
cacheRanker = nil;
if ([object name])
nameRanker = [[QSCurrentStringRanker alloc] initWithString:[object name]];
if ([object label] && ![[object label] isEqualToString:[object name]])
labelRanker = [[QSCurrentStringRanker alloc] initWithString:[object label]];
// Initialize cache ranker with additional search context (e.g., OCR text from images)
id cacheContent = [object additionalSearchContext];
if (cacheContent && [cacheContent isKindOfClass:[NSString class]])
cacheRanker = [[QSCurrentStringRanker alloc] initWithString:(NSString *)cacheContent];
usageMnemonics = [[QSMnemonics sharedInstance] objectMnemonicsForID:[object identifier]];
}
return self;
Expand All @@ -135,6 +145,7 @@ - (void)dealloc {
usageMnemonics = nil;
nameRanker = nil;
labelRanker = nil;
cacheRanker = nil;
}

- (NSString*)description {
Expand Down Expand Up @@ -196,6 +207,15 @@ - (QSRankedObject *)rankedObject:(QSBasicObject *)object forAbbreviation:(NSStri
matchedString = [labelRanker rankedString];
}
}

// Also check cache for matches (e.g., OCR text from images)
if (cacheRanker) {
CGFloat cacheScore = (*scoreForAbbrevIMP) (cacheRanker, @selector(scoreForAbbreviation:), anAbbreviation);
if (cacheScore > newScore) {
newScore = cacheScore;
matchedString = [cacheRanker rankedString];
}
}
}

// NSLog(@"newscore %f %@", newScore, rankedObject);
Expand Down
59 changes: 59 additions & 0 deletions Quicksilver/Code-QuickStepCore/QSObject_FileHandling.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#import "QSScreenshots.h"
#import "QSPaths.h"
#import <sys/mount.h>
#import <Vision/Vision.h>

#import "NSApplication_BLTRExtensions.h"

Expand Down Expand Up @@ -688,10 +689,68 @@ - (id)initWithArray:(NSArray *)paths {
[self setIdentifier:thisIdentifier];
[self setPrimaryType:QSFilePathType];
[self getNameFromFiles];
[self loadAdditionalSearchContext];
}
return self;
}

- (void)loadAdditionalSearchContext {
if (@available(macOS 10.15, *)) {

// For images, use text recognition to extract any text and store as searchable context
NSString *path = [self validSingleFilePath];
if (!path) {
return;
}

NSString *uti = [self fileUTI];
// Check if this is an image file
if (!QSTypeConformsTo(uti, (NSString *)kUTTypeImage)) {
return;
}

NSImage *image = [[NSImage alloc] initWithContentsOfFile:path];
if (!image) {
return;
}

// Get the CGImage from NSImage
CGImageRef cgImage = [image CGImageForProposedRect:NULL context:NULL hints:nil];
if (!cgImage) {
return;
}

// Create a text recognition request
VNRecognizeTextRequest *request = [[VNRecognizeTextRequest alloc] initWithCompletionHandler:^(VNRequest *request, NSError *error) {
if (error) {
NSLog(@"Text recognition error: %@", error);
return;
}

NSMutableString *recognizedText = [NSMutableString string];
for (VNRecognizedTextObservation *observation in request.results) {
VNRecognizedText *text = [[observation topCandidates:1] firstObject];
if (text) {
[recognizedText appendFormat:@" %@", text.string];
}
}

if ([recognizedText length] > 0) {
[self setObject:recognizedText forCache:kAdditionalSearchContext];
}
}];

// Create an image request handler
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCGImage:cgImage options:@{}];

NSError *error = nil;
[handler performRequests:@[request] error:&error];
if (error) {
NSLog(@"Error performing text recognition: %@", error);
}
}
}

- (NSDictionary *)infoRecord {

@synchronized (self) {
Expand Down
4 changes: 2 additions & 2 deletions Quicksilver/Code-QuickStepCore/QSPaths.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
#define kForumsURL @"http://groups.google.com/group/blacktree-quicksilver"
#define kBugsURL @"https://github.com/quicksilver/Quicksilver/issues"
#define kWebSiteURL @"https://qsapp.com/"
#define kDonatePageURL @"https://qsapp.com/donate.php"
#define kDonatePageURL @"https://qsapp.com/donate"
#define kChangelogURL @"https://github.com/quicksilver/Quicksilver/releases/"
#define kHelpURL @"https://qsapp.com/manual/"
#define kHelpSearchURL @"https://qsapp.com/w/index.php?title=Special:Search&search=%@&go=Go"
#define kHelpSearchURL @"https://qsapp.com/manual/search?q=%@"
// URL to crash reporter server/script
#define kCrashReporterURL @"https://qs0.qsapp.com/crashreports/reporter.php"
// Wiki page detailing why we collect crash reports
Expand Down
1 change: 1 addition & 0 deletions Quicksilver/Code-QuickStepCore/QSPlugInManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ typedef void(^QSPluginUpdatePromptBlock)(QSPluginUpdateStatus status);
- (void)suggestOldPlugInRemoval;
- (BOOL)liveLoadPlugIn:(QSPlugIn *)plugin;
- (NSArray *)knownPlugInsWithWebInfo ;
- (BOOL)pluginIsHosted:(NSString *)pluginID;
//- (BOOL)shouldLoadPlugIn:(QSPlugIn *)plugIn inGroup:(NSDictionary *)loadingBundles;
- (QSPlugIn *)plugInBundleWasInstalled:(NSBundle *)bundle;
- (void)deletePlugIns:(NSArray *)deletePlugIns fromWindow:(NSWindow *)window;
Expand Down
9 changes: 9 additions & 0 deletions Quicksilver/Code-QuickStepCore/QSPlugInManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ - (id)init {
localPlugIns = [[NSMutableDictionary alloc] init];
knownPlugIns = [[NSMutableDictionary alloc] init];
loadedPlugIns = [[NSMutableDictionary alloc] init];
plugInWebData = [[NSMutableDictionary alloc] init];

receivedData = nil;
oldPlugIns = [[NSMutableArray alloc] init];
Expand Down Expand Up @@ -320,6 +321,14 @@ - (NSArray *)knownPlugInsWithWebInfo {
return [knownPlugIns allValues];
}

- (BOOL)pluginIsHosted:(NSString *)pluginID {
if (!pluginID || !plugInWebData) {
return NO;
}
return [plugInWebData objectForKey:pluginID] != nil;
}


- (void)deletePlugIns:(NSArray *)deletePlugIns fromWindow:(NSWindow *)window {
NSArray *loaded = [[self loadedPlugIns] allValues];
BOOL needsRelaunch = nil != [deletePlugIns firstObjectCommonWithArray:loaded];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ - (void)setOptions:(NSDictionary *)options {
static NSString *css = nil;
if (css == nil) {
// CSS for making the web view blend in. !!-Not valid HTML (no <head>,<body>)
css = @"<style>body {margin:0px;padding:0px;font-size:11px;font-family:\"lucida grande\";}ul {-webkit-padding-start:16px;list-style-type:square;margin:0px}</style>";
css = @"<style>body {margin:0px;padding:0px;font-size:11px;font-family:\"lucida grande\";color:text;}ul {-webkit-padding-start:16px;list-style-type:square;margin:0px}@media (prefers-color-scheme:dark){body{color:#ffffff;}}@media (prefers-color-scheme:light){body{color:#000000;}}</style>";
}
WebFrame *wf = self.webView.mainFrame;
[wf loadHTMLString:[NSString stringWithFormat:@"%@%@",css,[thisPlugin releaseNotes]] baseURL:nil];
Expand Down
4 changes: 4 additions & 0 deletions Quicksilver/Code-QuickStepCore/QSProcessMonitor.m
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ - (BOOL)includeApp:(NSRunningApplication *)app {
if ([[[app.bundleURL path] pathExtension] isEqualToString:@"xpc"]) {
return NO;
}
// Only include processes with valid bundle URLs
if (!app.bundleURL) {
return NO;
}
return YES;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
- (void)showIndirectSelector:(id)sender;
- (void)hideIndirectSelector:(id)sender;

- (NSArray *)rankedActions;
- (void)updateActions;
- (void)updateActionsNow;

Expand Down
Loading