summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/3rd-party/DDHotKeyCenter.h93
-rw-r--r--src/3rd-party/DDHotKeyCenter.m284
-rw-r--r--src/3rd-party/DDHotKeyTextField.h20
-rw-r--r--src/3rd-party/DDHotKeyTextField.m138
-rw-r--r--src/3rd-party/DDHotKeyUtilities.h14
-rw-r--r--src/3rd-party/DDHotKeyUtilities.m145
-rw-r--r--src/AppDelegate.swift110
-rw-r--r--src/AppListProvider.swift100
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/Contents.json68
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_128@1x.pngbin0 -> 3914 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_128@2x.pngbin0 -> 8816 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_16@1x.pngbin0 -> 494 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_16@2x.pngbin0 -> 925 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_256@1x.pngbin0 -> 8816 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_256@2x.pngbin0 -> 9042 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_32@1x.pngbin0 -> 925 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_32@2x.pngbin0 -> 1731 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_512@1x.pngbin0 -> 9042 bytes
-rw-r--r--src/Assets.xcassets/AppIcon.appiconset/icon_512@2x.pngbin0 -> 47923 bytes
-rw-r--r--src/Assets.xcassets/Contents.json6
-rw-r--r--src/Base.lproj/GeneralSettingsViewController.xib54
-rw-r--r--src/Base.lproj/Main.storyboard105
-rw-r--r--src/BridgingHeader.h24
-rw-r--r--src/CommandArguments.swift22
-rw-r--r--src/GeneralSettingsViewController.swift38
-rw-r--r--src/Info.plist38
-rw-r--r--src/InputField.swift34
-rw-r--r--src/ListProvider.swift30
-rw-r--r--src/Notification+Name.swift21
-rw-r--r--src/PipeListProvider.swift38
-rw-r--r--src/ReadStdin.h26
-rw-r--r--src/ReadStdin.h.m40
-rw-r--r--src/ResultsView.swift106
-rw-r--r--src/SearchViewController.swift159
-rw-r--r--src/SearchWindow.swift49
-rw-r--r--src/SettingsViewController.swift53
-rw-r--r--src/VerticalAlignedTextFieldCell.swift63
37 files changed, 1878 insertions, 0 deletions
diff --git a/src/3rd-party/DDHotKeyCenter.h b/src/3rd-party/DDHotKeyCenter.h
new file mode 100644
index 0000000..6f79bbb
--- /dev/null
+++ b/src/3rd-party/DDHotKeyCenter.h
@@ -0,0 +1,93 @@
+/*
+ DDHotKey -- DDHotKeyCenter.h
+
+ Copyright (c) Dave DeLong <http://www.davedelong.com>
+
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+ The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+//a convenient typedef for the required signature of a hotkey block callback
+typedef void (^DDHotKeyTask)(NSEvent*);
+
+@interface DDHotKey : NSObject
+
+// creates a new hotkey but does not register it
++ (instancetype)hotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task;
+
+@property (nonatomic, assign, readonly) id target;
+@property (nonatomic, readonly) SEL action;
+@property (nonatomic, strong, readonly) id object;
+@property (nonatomic, copy, readonly) DDHotKeyTask task;
+
+@property (nonatomic, readonly) unsigned short keyCode;
+@property (nonatomic, readonly) NSUInteger modifierFlags;
+
+@end
+
+#pragma mark -
+
+@interface DDHotKeyCenter : NSObject
+
++ (instancetype)sharedHotKeyCenter;
+
+/**
+ Register a hotkey.
+ */
+- (DDHotKey *)registerHotKey:(DDHotKey *)hotKey;
+
+/**
+ Register a target/action hotkey.
+ The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask;
+ Returns the hotkey registered. If registration failed, returns nil.
+ */
+- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags target:(id)target action:(SEL)action object:(id)object;
+
+/**
+ Register a block callback hotkey.
+ The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask;
+ Returns the hotkey registered. If registration failed, returns nil.
+ */
+- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task;
+
+/**
+ See if a hotkey exists with the specified keycode and modifier flags.
+ NOTE: this will only check among hotkeys you have explicitly registered with DDHotKeyCenter. This does not check all globally registered hotkeys.
+ */
+- (BOOL)hasRegisteredHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags;
+
+/**
+ Unregister a specific hotkey
+ */
+- (void)unregisterHotKey:(DDHotKey *)hotKey;
+
+/**
+ Unregister all hotkeys
+ */
+- (void)unregisterAllHotKeys;
+
+/**
+ Unregister all hotkeys with a specific target
+ */
+- (void)unregisterHotKeysWithTarget:(id)target;
+
+/**
+ Unregister all hotkeys with a specific target and action
+ */
+- (void)unregisterHotKeysWithTarget:(id)target action:(SEL)action;
+
+/**
+ Unregister a hotkey with a specific keycode and modifier flags
+ */
+- (void)unregisterHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags;
+
+/**
+ Returns a set of currently registered hotkeys
+ **/
+- (NSSet *)registeredHotKeys;
+
+@end
+
diff --git a/src/3rd-party/DDHotKeyCenter.m b/src/3rd-party/DDHotKeyCenter.m
new file mode 100644
index 0000000..fd0b313
--- /dev/null
+++ b/src/3rd-party/DDHotKeyCenter.m
@@ -0,0 +1,284 @@
+/*
+ DDHotKey -- DDHotKeyCenter.m
+
+ Copyright (c) Dave DeLong <http://www.davedelong.com>
+
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+ The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
+ */
+
+#import <Carbon/Carbon.h>
+#import <objc/runtime.h>
+
+#import "DDHotKeyCenter.h"
+#import "DDHotKeyUtilities.h"
+
+#pragma mark Private Global Declarations
+
+OSStatus dd_hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData);
+
+#pragma mark DDHotKey
+
+@interface DDHotKey ()
+
+@property (nonatomic, retain) NSValue *hotKeyRef;
+@property (nonatomic) UInt32 hotKeyID;
+
+
+@property (nonatomic, assign, setter = _setTarget:) id target;
+@property (nonatomic, setter = _setAction:) SEL action;
+@property (nonatomic, strong, setter = _setObject:) id object;
+@property (nonatomic, copy, setter = _setTask:) DDHotKeyTask task;
+
+@property (nonatomic, setter = _setKeyCode:) unsigned short keyCode;
+@property (nonatomic, setter = _setModifierFlags:) NSUInteger modifierFlags;
+
+@end
+
+@implementation DDHotKey
+
++ (instancetype)hotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task {
+ DDHotKey *newHotKey = [[self alloc] init];
+ [newHotKey _setTask:task];
+ [newHotKey _setKeyCode:keyCode];
+ [newHotKey _setModifierFlags:flags];
+ return newHotKey;
+}
+
+- (void) dealloc {
+ [[DDHotKeyCenter sharedHotKeyCenter] unregisterHotKey:self];
+}
+
+- (NSUInteger)hash {
+ return [self keyCode] ^ [self modifierFlags];
+}
+
+- (BOOL)isEqual:(id)object {
+ BOOL equal = NO;
+ if ([object isKindOfClass:[DDHotKey class]]) {
+ equal = ([object keyCode] == [self keyCode]);
+ equal &= ([object modifierFlags] == [self modifierFlags]);
+ }
+ return equal;
+}
+
+- (NSString *)description {
+ NSMutableArray *bits = [NSMutableArray array];
+ if ((_modifierFlags & NSEventModifierFlagControl) > 0) { [bits addObject:@"NSControlKeyMask"]; }
+ if ((_modifierFlags & NSEventModifierFlagCommand) > 0) { [bits addObject:@"NSCommandKeyMask"]; }
+ if ((_modifierFlags & NSEventModifierFlagShift) > 0) { [bits addObject:@"NSShiftKeyMask"]; }
+ if ((_modifierFlags & NSEventModifierFlagOption) > 0) { [bits addObject:@"NSAlternateKeyMask"]; }
+
+ NSString *flags = [NSString stringWithFormat:@"(%@)", [bits componentsJoinedByString:@" | "]];
+ NSString *invokes = @"(block)";
+ if ([self target] != nil && [self action] != nil) {
+ invokes = [NSString stringWithFormat:@"[%@ %@]", [self target], NSStringFromSelector([self action])];
+ }
+ return [NSString stringWithFormat:@"%@\n\t(key: %hu\n\tflags: %@\n\tinvokes: %@)", [super description], [self keyCode], flags, invokes];
+}
+
+- (void)invokeWithEvent:(NSEvent *)event {
+ if (_target != nil && _action != nil && [_target respondsToSelector:_action]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [_target performSelector:_action withObject:event withObject:_object];
+#pragma clang diagnostic pop
+ } else if (_task != nil) {
+ _task(event);
+ }
+}
+
+@end
+
+#pragma mark DDHotKeyCenter
+
+static DDHotKeyCenter *sharedHotKeyCenter = nil;
+
+@implementation DDHotKeyCenter {
+ NSMutableSet *_registeredHotKeys;
+ UInt32 _nextHotKeyID;
+}
+
++ (instancetype)sharedHotKeyCenter {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedHotKeyCenter = [super allocWithZone:nil];
+ sharedHotKeyCenter = [sharedHotKeyCenter init];
+
+ EventTypeSpec eventSpec;
+ eventSpec.eventClass = kEventClassKeyboard;
+ eventSpec.eventKind = kEventHotKeyReleased;
+ InstallApplicationEventHandler(&dd_hotKeyHandler, 1, &eventSpec, NULL, NULL);
+ });
+ return sharedHotKeyCenter;
+}
+
++ (id)allocWithZone:(NSZone *)zone {
+ return sharedHotKeyCenter;
+}
+
+- (id)init {
+ if (self != sharedHotKeyCenter) { return sharedHotKeyCenter; }
+
+ self = [super init];
+ if (self) {
+ _registeredHotKeys = [[NSMutableSet alloc] init];
+ _nextHotKeyID = 1;
+ }
+ return self;
+}
+
+- (NSSet *)hotKeysMatching:(BOOL(^)(DDHotKey *hotkey))matcher {
+ NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+ return matcher(evaluatedObject);
+ }];
+ return [_registeredHotKeys filteredSetUsingPredicate:predicate];
+}
+
+- (BOOL)hasRegisteredHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags {
+ return [self hotKeysMatching:^BOOL(DDHotKey *hotkey) {
+ return hotkey.keyCode == keyCode && hotkey.modifierFlags == flags;
+ }].count > 0;
+}
+
+- (DDHotKey *)_registerHotKey:(DDHotKey *)hotKey {
+ if ([_registeredHotKeys containsObject:hotKey]) {
+ return hotKey;
+ }
+
+ EventHotKeyID keyID;
+ keyID.signature = 'htk1';
+ keyID.id = _nextHotKeyID;
+
+ EventHotKeyRef carbonHotKey;
+ UInt32 flags = DDCarbonModifierFlagsFromCocoaModifiers([hotKey modifierFlags]);
+ OSStatus err = RegisterEventHotKey([hotKey keyCode], flags, keyID, GetEventDispatcherTarget(), 0, &carbonHotKey);
+
+ //error registering hot key
+ if (err != 0) { return nil; }
+
+ NSValue *refValue = [NSValue valueWithPointer:carbonHotKey];
+ [hotKey setHotKeyRef:refValue];
+ [hotKey setHotKeyID:_nextHotKeyID];
+
+ _nextHotKeyID++;
+ [_registeredHotKeys addObject:hotKey];
+
+ return hotKey;
+}
+
+- (DDHotKey *)registerHotKey:(DDHotKey *)hotKey {
+ return [self _registerHotKey:hotKey];
+}
+
+- (void)unregisterHotKey:(DDHotKey *)hotKey {
+ NSValue *hotKeyRef = [hotKey hotKeyRef];
+ if (hotKeyRef) {
+ EventHotKeyRef carbonHotKey = (EventHotKeyRef)[hotKeyRef pointerValue];
+ UnregisterEventHotKey(carbonHotKey);
+ [hotKey setHotKeyRef:nil];
+ }
+
+ [_registeredHotKeys removeObject:hotKey];
+}
+
+- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task {
+ //we can't add a new hotkey if something already has this combo
+ if ([self hasRegisteredHotKeyWithKeyCode:keyCode modifierFlags:flags]) { return nil; }
+
+ DDHotKey *newHotKey = [[DDHotKey alloc] init];
+ [newHotKey _setTask:task];
+ [newHotKey _setKeyCode:keyCode];
+ [newHotKey _setModifierFlags:flags];
+
+ return [self _registerHotKey:newHotKey];
+}
+
+- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags target:(id)target action:(SEL)action object:(id)object {
+ //we can't add a new hotkey if something already has this combo
+ if ([self hasRegisteredHotKeyWithKeyCode:keyCode modifierFlags:flags]) { return nil; }
+
+ //build the hotkey object:
+ DDHotKey *newHotKey = [[DDHotKey alloc] init];
+ [newHotKey _setTarget:target];
+ [newHotKey _setAction:action];
+ [newHotKey _setObject:object];
+ [newHotKey _setKeyCode:keyCode];
+ [newHotKey _setModifierFlags:flags];
+ return [self _registerHotKey:newHotKey];
+}
+
+- (void)unregisterHotKeysMatching:(BOOL(^)(DDHotKey *hotkey))matcher {
+ //explicitly unregister the hotkey, since relying on the unregistration in -dealloc can be problematic
+ @autoreleasepool {
+ NSSet *matches = [self hotKeysMatching:matcher];
+ for (DDHotKey *hotKey in matches) {
+ [self unregisterHotKey:hotKey];
+ }
+ }
+}
+
+- (void)unregisterHotKeysWithTarget:(id)target {
+ [self unregisterHotKeysMatching:^BOOL(DDHotKey *hotkey) {
+ return hotkey.target == target;
+ }];
+}
+
+- (void)unregisterHotKeysWithTarget:(id)target action:(SEL)action {
+ [self unregisterHotKeysMatching:^BOOL(DDHotKey *hotkey) {
+ return hotkey.target == target && sel_isEqual(hotkey.action, action);
+ }];
+}
+
+- (void)unregisterHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags {
+ [self unregisterHotKeysMatching:^BOOL(DDHotKey *hotkey) {
+ return hotkey.keyCode == keyCode && hotkey.modifierFlags == flags;
+ }];
+}
+
+- (void)unregisterAllHotKeys {
+ NSSet *keys = [_registeredHotKeys copy];
+ for (DDHotKey *key in keys) {
+ [self unregisterHotKey:key];
+ }
+}
+
+- (NSSet *)registeredHotKeys {
+ return [self hotKeysMatching:^BOOL(DDHotKey *hotkey) {
+ return hotkey.hotKeyRef != NULL;
+ }];
+}
+
+@end
+
+OSStatus dd_hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
+ @autoreleasepool {
+ EventHotKeyID hotKeyID;
+ GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID);
+
+ UInt32 keyID = hotKeyID.id;
+
+ NSSet *matchingHotKeys = [[DDHotKeyCenter sharedHotKeyCenter] hotKeysMatching:^BOOL(DDHotKey *hotkey) {
+ return hotkey.hotKeyID == keyID;
+ }];
+ if ([matchingHotKeys count] > 1) { NSLog(@"ERROR!"); }
+ DDHotKey *matchingHotKey = [matchingHotKeys anyObject];
+
+ NSEvent *event = [NSEvent eventWithEventRef:theEvent];
+ NSEvent *keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyUp
+ location:[event locationInWindow]
+ modifierFlags:[event modifierFlags]
+ timestamp:[event timestamp]
+ windowNumber:-1
+ context:nil
+ characters:@""
+ charactersIgnoringModifiers:@""
+ isARepeat:NO
+ keyCode:[matchingHotKey keyCode]];
+
+ [matchingHotKey invokeWithEvent:keyEvent];
+ }
+
+ return noErr;
+}
diff --git a/src/3rd-party/DDHotKeyTextField.h b/src/3rd-party/DDHotKeyTextField.h
new file mode 100644
index 0000000..c399d62
--- /dev/null
+++ b/src/3rd-party/DDHotKeyTextField.h
@@ -0,0 +1,20 @@
+/*
+ DDHotKey -- DDHotKeyTextField.h
+
+ Copyright (c) Dave DeLong <http://www.davedelong.com>
+
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+ The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
+ */
+
+#import <Foundation/Foundation.h>
+#import "DDHotKeyCenter.h"
+
+@interface DDHotKeyTextField : NSTextField
+
+@property (nonatomic, strong) DDHotKey *hotKey;
+
+@end
+
+@interface DDHotKeyTextFieldCell : NSTextFieldCell @end \ No newline at end of file
diff --git a/src/3rd-party/DDHotKeyTextField.m b/src/3rd-party/DDHotKeyTextField.m
new file mode 100644
index 0000000..ccabd10
--- /dev/null
+++ b/src/3rd-party/DDHotKeyTextField.m
@@ -0,0 +1,138 @@
+/*
+ DDHotKey -- DDHotKeyTextField.m
+
+ Copyright (c) Dave DeLong <http://www.davedelong.com>
+
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+ The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
+ */
+
+#import <Carbon/Carbon.h>
+
+#import "DDHotKeyTextField.h"
+#import "DDHotKeyUtilities.h"
+
+@interface DDHotKeyTextFieldEditor : NSTextView
+
+@property (nonatomic, weak) DDHotKeyTextField *hotKeyField;
+
+@end
+
+static DDHotKeyTextFieldEditor *DDFieldEditor(void);
+static DDHotKeyTextFieldEditor *DDFieldEditor(void) {
+ static DDHotKeyTextFieldEditor *editor;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ editor = [[DDHotKeyTextFieldEditor alloc] initWithFrame:NSMakeRect(0, 0, 100, 32)];
+ [editor setFieldEditor:YES];
+ });
+ return editor;
+}
+
+@implementation DDHotKeyTextFieldCell
+
+- (NSTextView *)fieldEditorForView:(NSView *)view {
+ if ([view isKindOfClass:[DDHotKeyTextField class]]) {
+ DDHotKeyTextFieldEditor *editor = DDFieldEditor();
+ editor.insertionPointColor = editor.backgroundColor;
+ editor.hotKeyField = (DDHotKeyTextField *)view;
+ return editor;
+ }
+ return nil;
+}
+
+@end
+
+@implementation DDHotKeyTextField
+
++ (Class)cellClass {
+ return [DDHotKeyTextFieldCell class];
+}
+
+- (void)setHotKey:(DDHotKey *)hotKey {
+ if (_hotKey != hotKey) {
+ _hotKey = hotKey;
+ [super setStringValue:[DDStringFromKeyCode(hotKey.keyCode, hotKey.modifierFlags) uppercaseString]];
+ }
+}
+
+- (void)setStringValue:(NSString *)aString {
+ NSLog(@"-[DDHotKeyTextField setStringValue:] is not what you want. Use -[DDHotKeyTextField setHotKey:] instead.");
+ [super setStringValue:aString];
+}
+
+- (NSString *)stringValue {
+ NSLog(@"-[DDHotKeyTextField stringValue] is not what you want. Use -[DDHotKeyTextField hotKey] instead.");
+ return [super stringValue];
+}
+
+@end
+
+@implementation DDHotKeyTextFieldEditor {
+ BOOL _hasSeenKeyDown;
+ id _globalMonitor;
+ DDHotKey *_originalHotKey;
+}
+
+- (void)setHotKeyField:(DDHotKeyTextField *)hotKeyField {
+ _hotKeyField = hotKeyField;
+ _originalHotKey = _hotKeyField.hotKey;
+}
+
+- (void)processHotkeyEvent:(NSEvent *)event {
+ NSUInteger flags = event.modifierFlags;
+ BOOL hasModifier = (flags & (NSEventModifierFlagCommand | NSEventModifierFlagOption | NSEventModifierFlagControl | NSEventModifierFlagShift | NSEventModifierFlagFunction)) > 0;
+
+ if (event.type == NSEventTypeKeyDown) {
+ _hasSeenKeyDown = YES;
+ unichar character = [event.charactersIgnoringModifiers characterAtIndex:0];
+
+
+ if (hasModifier == NO && ([[NSCharacterSet newlineCharacterSet] characterIsMember:character] || event.keyCode == kVK_Escape)) {
+ if (event.keyCode == kVK_Escape) {
+ self.hotKeyField.hotKey = _originalHotKey;
+
+ NSString *str = DDStringFromKeyCode(_originalHotKey.keyCode, _originalHotKey.modifierFlags);
+ self.textStorage.mutableString.string = [str uppercaseString];
+ }
+ [self.hotKeyField sendAction:self.hotKeyField.action to:self.hotKeyField.target];
+ [self.window makeFirstResponder:nil];
+ return;
+ }
+ }
+
+ if ((event.type == NSEventTypeKeyDown || (event.type == NSEventTypeFlagsChanged && _hasSeenKeyDown == NO)) && hasModifier) {
+ self.hotKeyField.hotKey = [DDHotKey hotKeyWithKeyCode:event.keyCode modifierFlags:flags task:_originalHotKey.task];
+ NSString *str = DDStringFromKeyCode(event.keyCode, flags);
+ [self.textStorage.mutableString setString:[str uppercaseString]];
+ [self.hotKeyField sendAction:self.hotKeyField.action to:self.hotKeyField.target];
+ }
+}
+
+- (BOOL)becomeFirstResponder {
+ BOOL ok = [super becomeFirstResponder];
+ if (ok) {
+ _hasSeenKeyDown = NO;
+ _globalMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged) handler:^NSEvent*(NSEvent *event){
+ [self processHotkeyEvent:event];
+ return nil;
+ }];
+ }
+ return ok;
+}
+
+- (BOOL)resignFirstResponder {
+ BOOL ok = [super resignFirstResponder];
+ if (ok) {
+ self.hotKeyField = nil;
+ if (_globalMonitor) {
+ [NSEvent removeMonitor:_globalMonitor];
+ _globalMonitor = nil;
+ }
+ }
+
+ return ok;
+}
+
+@end
diff --git a/src/3rd-party/DDHotKeyUtilities.h b/src/3rd-party/DDHotKeyUtilities.h
new file mode 100644
index 0000000..54b25a4
--- /dev/null
+++ b/src/3rd-party/DDHotKeyUtilities.h
@@ -0,0 +1,14 @@
+/*
+ DDHotKey -- DDHotKeyUtilities.h
+
+ Copyright (c) Dave DeLong <http://www.davedelong.com>
+
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+ The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
+ */
+
+#import <Foundation/Foundation.h>
+
+extern NSString *DDStringFromKeyCode(unsigned short keyCode, NSUInteger modifiers);
+extern UInt32 DDCarbonModifierFlagsFromCocoaModifiers(NSUInteger flags);
diff --git a/src/3rd-party/DDHotKeyUtilities.m b/src/3rd-party/DDHotKeyUtilities.m
new file mode 100644
index 0000000..f1a51cd
--- /dev/null
+++ b/src/3rd-party/DDHotKeyUtilities.m
@@ -0,0 +1,145 @@
+/*
+ DDHotKey -- DDHotKeyUtilities.m
+
+ Copyright (c) Dave DeLong <http://www.davedelong.com>
+
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+ The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
+ */
+
+#import "DDHotKeyUtilities.h"
+#import <Carbon/Carbon.h>
+#import <AppKit/AppKit.h>
+
+static NSDictionary *_DDKeyCodeToCharacterMap(void);
+static NSDictionary *_DDKeyCodeToCharacterMap(void) {
+ static NSDictionary *keyCodeMap = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ keyCodeMap = @{
+ @(kVK_Return) : @"↩",
+ @(kVK_Tab) : @"⇥",
+ @(kVK_Space) : @"⎵",
+ @(kVK_Delete) : @"⌫",
+ @(kVK_Escape) : @"⎋",
+ @(kVK_Command) : @"⌘",
+ @(kVK_Shift) : @"⇧",
+ @(kVK_CapsLock) : @"⇪",
+ @(kVK_Option) : @"⌥",
+ @(kVK_Control) : @"⌃",
+ @(kVK_RightShift) : @"⇧",
+ @(kVK_RightOption) : @"⌥",
+ @(kVK_RightControl) : @"⌃",
+ @(kVK_VolumeUp) : @"🔊",
+ @(kVK_VolumeDown) : @"🔈",
+ @(kVK_Mute) : @"🔇",
+ @(kVK_Function) : @"\u2318",
+ @(kVK_F1) : @"F1",
+ @(kVK_F2) : @"F2",
+ @(kVK_F3) : @"F3",
+ @(kVK_F4) : @"F4",
+ @(kVK_F5) : @"F5",
+ @(kVK_F6) : @"F6",
+ @(kVK_F7) : @"F7",
+ @(kVK_F8) : @"F8",
+ @(kVK_F9) : @"F9",
+ @(kVK_F10) : @"F10",
+ @(kVK_F11) : @"F11",
+ @(kVK_F12) : @"F12",
+ @(kVK_F13) : @"F13",
+ @(kVK_F14) : @"F14",
+ @(kVK_F15) : @"F15",
+ @(kVK_F16) : @"F16",
+ @(kVK_F17) : @"F17",
+ @(kVK_F18) : @"F18",
+ @(kVK_F19) : @"F19",
+ @(kVK_F20) : @"F20",
+ // @(kVK_Help) : @"",
+ @(kVK_ForwardDelete) : @"⌦",
+ @(kVK_Home) : @"↖",
+ @(kVK_End) : @"↘",
+ @(kVK_PageUp) : @"⇞",
+ @(kVK_PageDown) : @"⇟",
+ @(kVK_LeftArrow) : @"←",
+ @(kVK_RightArrow) : @"→",
+ @(kVK_DownArrow) : @"↓",
+ @(kVK_UpArrow) : @"↑",
+ };
+ });
+ return keyCodeMap;
+}
+
+NSString *DDStringFromKeyCode(unsigned short keyCode, NSUInteger modifiers) {
+ NSMutableString *final = [NSMutableString stringWithString:@""];
+ NSDictionary *characterMap = _DDKeyCodeToCharacterMap();
+
+ if (modifiers & NSEventModifierFlagControl) {
+ [final appendString:[characterMap objectForKey:@(kVK_Control)]];
+ }
+ if (modifiers & NSEventModifierFlagOption) {
+ [final appendString:[characterMap objectForKey:@(kVK_Option)]];
+ }
+ if (modifiers & NSEventModifierFlagShift) {
+ [final appendString:[characterMap objectForKey:@(kVK_Shift)]];
+ }
+ if (modifiers & NSEventModifierFlagCommand) {
+ [final appendString:[characterMap objectForKey:@(kVK_Command)]];
+ }
+
+ if (keyCode == kVK_Control || keyCode == kVK_Option || keyCode == kVK_Shift || keyCode == kVK_Command) {
+ return final;
+ }
+
+ NSString *mapped = [characterMap objectForKey:@(keyCode)];
+ if (mapped != nil) {
+ [final appendString:mapped];
+ } else {
+
+ TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
+ CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
+
+ // Fix crash using non-unicode layouts, such as Chinese or Japanese.
+ if (!uchr) {
+ CFRelease(currentKeyboard);
+ currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
+ }
+
+ const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);
+
+ if (keyboardLayout) {
+ UInt32 deadKeyState = 0;
+ UniCharCount maxStringLength = 255;
+ UniCharCount actualStringLength = 0;
+ UniChar unicodeString[maxStringLength];
+
+ UInt32 keyModifiers = DDCarbonModifierFlagsFromCocoaModifiers(modifiers);
+
+ OSStatus status = UCKeyTranslate(keyboardLayout,
+ keyCode, kUCKeyActionDown, keyModifiers,
+ LMGetKbdType(), 0,
+ &deadKeyState,
+ maxStringLength,
+ &actualStringLength, unicodeString);
+
+ if (actualStringLength > 0 && status == noErr) {
+ NSString *characterString = [NSString stringWithCharacters:unicodeString length:(NSUInteger)actualStringLength];
+
+ [final appendString:characterString];
+ }
+ }
+ }
+
+ return final;
+}
+
+UInt32 DDCarbonModifierFlagsFromCocoaModifiers(NSUInteger flags) {
+ UInt32 newFlags = 0;
+ if ((flags & NSEventModifierFlagControl) > 0) { newFlags |= controlKey; }
+ if ((flags & NSEventModifierFlagCommand) > 0) { newFlags |= cmdKey; }
+ if ((flags & NSEventModifierFlagShift) > 0) { newFlags |= shiftKey; }
+ if ((flags & NSEventModifierFlagOption) > 0) { newFlags |= optionKey; }
+ if ((flags & NSEventModifierFlagCapsLock) > 0) { newFlags |= alphaLock; }
+ return newFlags;
+}
diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift
new file mode 100644
index 0000000..5c115b3
--- /dev/null
+++ b/src/AppDelegate.swift
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+import Carbon
+import LaunchAtLogin
+import Preferences
+import KeyboardShortcuts
+
+extension Settings.PaneIdentifier {
+ static let general = Self("general")
+}
+
+// import legacy settings if they existed
+let kLegacyKc = "kDefaultsGlobalShortcutKeycode"
+let kLegacyMf = "kDefaultsGlobalShortcutModifiedFlags"
+
+extension KeyboardShortcuts.Name {
+ static let activateSearch = Self("activateSearchGlobalShortcut", default: .init(
+ (UserDefaults.standard.object(forKey: kLegacyKc) != nil) ?
+ KeyboardShortcuts.Key(rawValue: UserDefaults.standard.integer(forKey: kLegacyKc)):
+ .space,
+ modifiers: (UserDefaults.standard.object(forKey: kLegacyKc) != nil) ?
+ NSEvent.ModifierFlags(rawValue: UInt(UserDefaults.standard.integer(forKey: kLegacyMf))) :
+ [.command]))
+}
+
+@NSApplicationMain
+class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
+ @IBOutlet var controllerWindow: NSWindowController?
+
+ private var statusItem: NSStatusItem!
+ private var startAtLaunch: NSMenuItem!
+
+ func applicationDidFinishLaunching(_ aNotification: Notification) {
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
+
+ if let button = statusItem.button {
+ button.title = "d"
+ }
+ setupMenus()
+ }
+
+ func applicationWillTerminate(_ aNotification: Notification) {
+ }
+
+ func setupMenus() {
+ let menu = NSMenu()
+
+ let open = NSMenuItem(title: "Open", action: #selector(resumeApp), keyEquivalent: "")
+ menu.addItem(open)
+
+ let settings = NSMenuItem(title: "Settings", action: #selector(openSettings), keyEquivalent: "")
+ menu.addItem(settings)
+
+ menu.addItem(NSMenuItem.separator())
+ startAtLaunch = NSMenuItem(title: "Launch at Login", action: #selector(toggleLaunchAtLogin), keyEquivalent: "")
+ startAtLaunch.state = LaunchAtLogin.isEnabled ? .on : .off
+ menu.addItem(startAtLaunch)
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: ""))
+
+ statusItem.menu = menu
+ }
+
+ @objc func resumeApp() {
+ let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: Bundle.main)
+ // swiftlint:disable force_cast
+ let mainPageController = storyboard.instantiateController(
+ withIdentifier: "SearchViewController") as! SearchViewController
+ // swiftlint:enable force_cast
+ mainPageController.resumeApp()
+ }
+
+ @objc func openSettings() {
+ settingsWindowController.show()
+ }
+
+ @objc func toggleLaunchAtLogin() {
+ let enabled = !LaunchAtLogin.isEnabled
+ LaunchAtLogin.isEnabled = enabled
+ startAtLaunch.state = enabled ? .on : .off
+ }
+
+ private lazy var settings: [SettingsPane] = [
+ GeneralSettingsViewController()
+ ]
+
+ private lazy var settingsWindowController = SettingsWindowController(
+ preferencePanes: settings,
+ style: .segmentedControl,
+ animated: true,
+ hidesToolbarForSingleItem: true
+ )
+}
diff --git a/src/AppListProvider.swift b/src/AppListProvider.swift
new file mode 100644
index 0000000..7aa3ad8
--- /dev/null
+++ b/src/AppListProvider.swift
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Foundation
+import FileWatcher
+import Fuse
+
+/**
+ * Provide a list of launcheable apps for the OS
+ */
+class AppListProvider: ListProvider {
+
+ var appDirDict = [String: Bool]()
+
+ var appList = [URL]()
+
+ init() {
+ let applicationDir = NSSearchPathForDirectoriesInDomains(
+ .applicationDirectory, .localDomainMask, true)[0]
+
+ // Catalina moved default applications under a different mask.
+ let systemApplicationDir = NSSearchPathForDirectoriesInDomains(
+ .applicationDirectory, .systemDomainMask, true)[0]
+
+ // appName to dir recursivity key/valye dict
+ appDirDict[applicationDir] = true
+ appDirDict[systemApplicationDir] = true
+ appDirDict["/System/Library/CoreServices/"] = false
+
+ initFileWatch(Array(appDirDict.keys))
+ updateAppList()
+ }
+
+ func initFileWatch(_ dirs: [String]) {
+ let filewatcher = FileWatcher(dirs)
+ filewatcher.callback = {_ in
+ self.updateAppList()
+ }
+ filewatcher.start()
+ }
+
+ func updateAppList() {
+ var newAppList = [URL]()
+ appDirDict.keys.forEach { path in
+ let urlPath = URL(fileURLWithPath: path, isDirectory: true)
+ let list = getAppList(urlPath, recursive: appDirDict[path]!)
+ newAppList.append(contentsOf: list)
+ }
+ appList = newAppList
+ }
+
+ func getAppList(_ appDir: URL, recursive: Bool = true) -> [URL] {
+ var list = [URL]()
+ let fileManager = FileManager.default
+
+ do {
+ let subs = try fileManager.contentsOfDirectory(atPath: appDir.path)
+
+ for sub in subs {
+ let dir = appDir.appendingPathComponent(sub)
+
+ if dir.pathExtension == "app" {
+ list.append(dir)
+ } else if dir.hasDirectoryPath && recursive {
+ list.append(contentsOf: self.getAppList(dir))
+ }
+ }
+ } catch {
+ NSLog("Error on getAppList: %@", error.localizedDescription)
+ }
+ return list
+ }
+
+ func get() -> [ListItem] {
+ return appList.map({ListItem(name: $0.deletingPathExtension().lastPathComponent, data: $0)})
+ }
+
+ func doAction(item: ListItem) {
+ guard let app: URL = item.data as? URL else {
+ NSLog("Cannot do action on item \(item.name)")
+ return
+ }
+ DispatchQueue.main.async {
+ NSWorkspace.shared.open(app)
+ }
+ }
+}
diff --git a/src/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..b32bc8b
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "1x",
+ "filename" : "icon_16@1x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "2x",
+ "filename" : "icon_16@2x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "1x",
+ "filename" : "icon_32@1x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "2x",
+ "filename" : "icon_32@2x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "1x",
+ "filename" : "icon_128@1x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "2x",
+ "filename" : "icon_128@2x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "1x",
+ "filename" : "icon_256@1x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "2x",
+ "filename" : "icon_256@2x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "1x",
+ "filename" : "icon_512@1x.png"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "2x",
+ "filename" : "icon_512@2x.png"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png
new file mode 100644
index 0000000..57a791f
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png
new file mode 100644
index 0000000..d508b78
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png
new file mode 100644
index 0000000..b765709
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png
new file mode 100644
index 0000000..b6c8724
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png
new file mode 100644
index 0000000..d508b78
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png
new file mode 100644
index 0000000..106eada
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png
new file mode 100644
index 0000000..b6c8724
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png
new file mode 100644
index 0000000..7e80008
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png
new file mode 100644
index 0000000..106eada
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png
Binary files differ
diff --git a/src/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png b/src/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png
new file mode 100644
index 0000000..a38e6bd
--- /dev/null
+++ b/src/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png
Binary files differ
diff --git a/src/Assets.xcassets/Contents.json b/src/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/src/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/Base.lproj/GeneralSettingsViewController.xib b/src/Base.lproj/GeneralSettingsViewController.xib
new file mode 100644
index 0000000..1b30b58
--- /dev/null
+++ b/src/Base.lproj/GeneralSettingsViewController.xib
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+ <dependencies>
+ <deployment identifier="macosx"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
+ <capability name="NSView safe area layout guides" minToolsVersion="12.0"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="GeneralSettingsViewController" customModule="dmenu_mac" customModuleProvider="target">
+ <connections>
+ <outlet property="customView" destination="4nh-zO-SVY" id="voW-MV-1d4"/>
+ <outlet property="view" destination="c22-O7-iKe" id="ZQe-UH-LCX"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+ <customView wantsLayer="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c22-O7-iKe">
+ <rect key="frame" x="0.0" y="0.0" width="510" height="65"/>
+ <subviews>
+ <customView autoresizesSubviews="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4nh-zO-SVY">
+ <rect key="frame" x="210" y="20" width="300" height="25"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="25" id="Bgf-ZC-F3a"/>
+ <constraint firstAttribute="width" constant="300" id="DUS-S2-npV"/>
+ </constraints>
+ <shadow key="shadow">
+ <color key="color" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ </shadow>
+ <viewLayoutGuide key="safeArea" id="yaK-ue-AZE"/>
+ <viewLayoutGuide key="layoutMargins" id="Le8-6r-xay"/>
+ </customView>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Vt6-jC-8Jo">
+ <rect key="frame" x="18" y="25" width="186" height="16"/>
+ <textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Global Shortcut" id="uKy-ok-dDi">
+ <font key="font" usesAppearanceFont="YES"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ </subviews>
+ <constraints>
+ <constraint firstAttribute="trailing" secondItem="4nh-zO-SVY" secondAttribute="trailing" id="ANe-tV-l64"/>
+ <constraint firstItem="4nh-zO-SVY" firstAttribute="leading" secondItem="Vt6-jC-8Jo" secondAttribute="trailing" constant="8" symbolic="YES" id="Czy-5O-KIG"/>
+ <constraint firstItem="4nh-zO-SVY" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="20" symbolic="YES" id="El7-4y-U6x"/>
+ <constraint firstItem="Vt6-jC-8Jo" firstAttribute="centerY" secondItem="4nh-zO-SVY" secondAttribute="centerY" id="Vbd-sJ-pbv"/>
+ <constraint firstItem="4nh-zO-SVY" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="bEk-HF-Wfw"/>
+ <constraint firstItem="Vt6-jC-8Jo" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="y8M-U2-sKU"/>
+ <constraint firstItem="Vt6-jC-8Jo" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="20" symbolic="YES" id="yXa-2Y-xkG"/>
+ </constraints>
+ <point key="canvasLocation" x="71" y="294.5"/>
+ </customView>
+ </objects>
+</document>
diff --git a/src/Base.lproj/Main.storyboard b/src/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..6fc3e41
--- /dev/null
+++ b/src/Base.lproj/Main.storyboard
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
+ <dependencies>
+ <deployment identifier="macosx"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--Application-->
+ <scene sceneID="zQX-TL-VWj">
+ <objects>
+ <application id="gxZ-1U-eOM" sceneMemberID="viewController">
+ <menu key="mainMenu" title="Main Menu" systemMenu="main" id="dDP-cD-PHO"/>
+ <connections>
+ <outlet property="delegate" destination="xgv-xq-kjR" id="r8A-cU-Ure"/>
+ </connections>
+ </application>
+ <customObject id="xgv-xq-kjR" customClass="AppDelegate" customModule="dmenu_mac" customModuleProvider="target"/>
+ <customObject id="RwB-ew-8eE" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="75" y="0.0"/>
+ </scene>
+ <!--Window Controller-->
+ <scene sceneID="R2V-B0-nI4">
+ <objects>
+ <windowController storyboardIdentifier="SearchWindowController" id="B8D-0N-5wS" sceneMemberID="viewController">
+ <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA" customClass="SearchWindow" customModule="dmenu_mac" customModuleProvider="target">
+ <rect key="contentRect" x="196" y="0.0" width="485" height="25"/>
+ <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
+ <connections>
+ <outlet property="delegate" destination="B8D-0N-5wS" id="WoP-cW-Qfs"/>
+ </connections>
+ </window>
+ <connections>
+ <segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
+ </connections>
+ </windowController>
+ <customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="74.5" y="249.5"/>
+ </scene>
+ <!--Search View Controller-->
+ <scene sceneID="hIz-AP-VOD">
+ <objects>
+ <viewController storyboardIdentifier="SearchViewController" id="XfG-lQ-9wD" customClass="SearchViewController" customModule="dmenu_mac" customModuleProvider="target" sceneMemberID="viewController">
+ <view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
+ <rect key="frame" x="0.0" y="0.0" width="629" height="31"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Fjb-7T-x1e" customClass="InputField" customModule="dmenu_mac" customModuleProvider="target">
+ <rect key="frame" x="10" y="0.0" width="150" height="31"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="150" id="gz9-SZ-TTB"/>
+ <constraint firstAttribute="height" constant="31" id="oAG-TV-bUz"/>
+ </constraints>
+ <textFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" focusRingType="none" alignment="left" placeholderString="Search" drawsBackground="YES" id="pMg-YR-zKP" customClass="VerticalAlignedTextFieldCell" customModule="dmenu_mac" customModuleProvider="target">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" white="1" alpha="0.0" colorSpace="deviceWhite"/>
+ </textFieldCell>
+ </textField>
+ <scrollView fixedFrame="YES" borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="QVh-If-BUC">
+ <rect key="frame" x="174" y="0.0" width="416" height="31"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <clipView key="contentView" autoresizesSubviews="NO" drawsBackground="NO" copiesOnScroll="NO" id="zRh-Jt-rH1">
+ <rect key="frame" x="0.0" y="0.0" width="416" height="31"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <view id="5lG-jR-0ih" customClass="ResultsView" customModule="dmenu_mac" customModuleProvider="target">
+ <rect key="frame" x="0.0" y="-3674" width="418" height="31"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <connections>
+ <outlet property="scrollView" destination="QVh-If-BUC" id="PXN-Pz-YRx"/>
+ </connections>
+ </view>
+ </subviews>
+ <color key="backgroundColor" red="0.11764705882352941" green="0.11764705882352941" blue="0.11764705882352941" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
+ </clipView>
+ <scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="uQ2-nO-u0R">
+ <rect key="frame" x="-100" y="-100" width="181" height="16"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </scroller>
+ <scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="Czm-aK-GgP">
+ <rect key="frame" x="-100" y="-100" width="16" height="94"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </scroller>
+ </scrollView>
+ </subviews>
+ <constraints>
+ <constraint firstAttribute="bottom" secondItem="Fjb-7T-x1e" secondAttribute="bottom" id="J9X-bX-kJ0"/>
+ <constraint firstItem="Fjb-7T-x1e" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="x2n-mW-u0G"/>
+ <constraint firstItem="Fjb-7T-x1e" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="10" id="yFY-Az-NlG"/>
+ </constraints>
+ </view>
+ <connections>
+ <outlet property="resultsText" destination="5lG-jR-0ih" id="bRp-pM-yOg"/>
+ <outlet property="searchText" destination="Fjb-7T-x1e" id="dUT-7x-0zh"/>
+ </connections>
+ </viewController>
+ <customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="203.5" y="481.5"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/src/BridgingHeader.h b/src/BridgingHeader.h
new file mode 100644
index 0000000..82e9a72
--- /dev/null
+++ b/src/BridgingHeader.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef BridgingHeader_h
+#define BridgingHeader_h
+
+#import "DDHotKeyCenter.h"
+#import "DDHotKeyTextField.h"
+#import "ReadStdin.h"
+
+#endif /* BridgingHeader_h */
diff --git a/src/CommandArguments.swift b/src/CommandArguments.swift
new file mode 100644
index 0000000..7031ce9
--- /dev/null
+++ b/src/CommandArguments.swift
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import ArgumentParser
+
+struct DmenuMac: ParsableArguments {
+ @Option(name: .shortAndLong, help: "Show a prompt instead of the search input.")
+ var prompt: String?
+}
diff --git a/src/GeneralSettingsViewController.swift b/src/GeneralSettingsViewController.swift
new file mode 100644
index 0000000..83d73e1
--- /dev/null
+++ b/src/GeneralSettingsViewController.swift
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+import Preferences
+import KeyboardShortcuts
+
+final class GeneralSettingsViewController: NSViewController, SettingsPane {
+ let preferencePaneIdentifier = Settings.PaneIdentifier.general
+ let preferencePaneTitle = "General"
+ let toolbarItemIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "General settings")!
+
+ @IBOutlet weak var customView: NSView?
+
+ override var nibName: NSNib.Name? { "GeneralSettingsViewController" }
+
+ override func loadView() {
+ super.loadView()
+
+ let recorder = KeyboardShortcuts.RecorderCocoa(for: .activateSearch)
+ recorder.frame = CGRect(x: 0, y: 0, width: 150, height: 25)
+ customView?.addSubview(recorder)
+
+ }
+}
diff --git a/src/Info.plist b/src/Info.plist
new file mode 100644
index 0000000..e55981b
--- /dev/null
+++ b/src/Info.plist
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>0.7.2</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>0.7.2</string>
+ <key>LSApplicationCategoryType</key>
+ <string>public.app-category.utilities</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+ <key>LSUIElement</key>
+ <true/>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2016 Jose Pereira. All rights reserved.</string>
+ <key>NSMainStoryboardFile</key>
+ <string>Main</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
diff --git a/src/InputField.swift b/src/InputField.swift
new file mode 100644
index 0000000..31fb8dd
--- /dev/null
+++ b/src/InputField.swift
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Foundation
+
+class InputField: NSTextField {
+ override func becomeFirstResponder() -> Bool {
+ let responderStatus = super.becomeFirstResponder()
+
+ if let fieldEditor = self.window?.fieldEditor(true, for: self) as? NSTextView {
+ fieldEditor.selectedTextAttributes = [
+ // Make selection transparent
+ NSAttributedString.Key.backgroundColor: NSColor.clear
+ ]
+ // Make blinking cursos transparent
+ fieldEditor.insertionPointColor = NSColor.clear
+ }
+
+ return responderStatus
+ }
+}
diff --git a/src/ListProvider.swift b/src/ListProvider.swift
new file mode 100644
index 0000000..e895414
--- /dev/null
+++ b/src/ListProvider.swift
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Foundation
+
+protocol ListProvider {
+ // Returns list of items
+ func get() -> [ListItem]
+
+ // Performs action on a selected item
+ func doAction(item: ListItem)
+}
+
+struct ListItem {
+ var name: String
+ var data: Any?
+}
diff --git a/src/Notification+Name.swift b/src/Notification+Name.swift
new file mode 100644
index 0000000..0abaffb
--- /dev/null
+++ b/src/Notification+Name.swift
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Foundation
+
+extension Notification.Name {
+ static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification")
+}
diff --git a/src/PipeListProvider.swift b/src/PipeListProvider.swift
new file mode 100644
index 0000000..32a678a
--- /dev/null
+++ b/src/PipeListProvider.swift
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Foundation
+
+/**
+ * Provide a list from a terminal pipe. When action is performed, quit app since we act like a prompt
+ */
+class PipeListProvider: ListProvider {
+ var choices = [String]()
+
+ init(str: String) {
+ choices = str.trimmingCharacters(in: .whitespacesAndNewlines)
+ .components(separatedBy: "\n")
+ }
+
+ func get() -> [ListItem] {
+ return choices.map({ListItem(name: $0, data: nil)})
+ }
+
+ func doAction(item: ListItem) {
+ print(item.name)
+ NSApplication.shared.terminate(self)
+ }
+}
diff --git a/src/ReadStdin.h b/src/ReadStdin.h
new file mode 100644
index 0000000..94b6744
--- /dev/null
+++ b/src/ReadStdin.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface ReadStdin : NSObject
+
++(NSString *)read;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/ReadStdin.h.m b/src/ReadStdin.h.m
new file mode 100644
index 0000000..08b4e19
--- /dev/null
+++ b/src/ReadStdin.h.m
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import "ReadStdin.h"
+
+#import <poll.h>
+
+@implementation ReadStdin
+
++(NSString *)read {
+ char buf[BUFSIZ];
+
+ // prevent fgets from being blocked
+ int flags;
+ flags = fcntl(STDIN_FILENO, F_GETFL, 0);
+ flags |= O_NONBLOCK;
+ fcntl(STDIN_FILENO, F_SETFL, flags);
+
+ NSMutableString *str = [NSMutableString string];
+ while (fgets(buf, sizeof(BUFSIZ), stdin) != 0) {
+ [str appendString:[NSString stringWithUTF8String:buf]];
+ }
+
+ return str;
+}
+
+@end
diff --git a/src/ResultsView.swift b/src/ResultsView.swift
new file mode 100644
index 0000000..075e87a
--- /dev/null
+++ b/src/ResultsView.swift
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class ResultsView: NSView {
+ @IBOutlet fileprivate var scrollView: NSScrollView!
+
+ let rectFillPadding: CGFloat = 5
+ var resultsList: [ListItem] = []
+
+ var dirtyWidth: Bool = false
+ var selectedRect = NSRect()
+
+ var selectedIndexValue: Int = 0
+ var selectedIndex: Int {
+ get {
+ return selectedIndexValue
+ }
+ set {
+ if newValue < 0 || newValue >= resultsList.count {
+ return
+ }
+
+ selectedIndexValue = newValue
+ needsDisplay = true
+ }
+ }
+
+ var list: [ListItem] {
+ get {
+ return resultsList
+ }
+ set {
+ selectedIndexValue = 0
+ resultsList = newValue
+ needsDisplay = true
+ }
+ }
+
+ func selectedItem() -> ListItem? {
+ if selectedIndexValue < 0 || selectedIndexValue >= resultsList.count {
+ return nil
+ } else {
+ return resultsList[selectedIndexValue]
+ }
+ }
+
+ func clear() {
+ resultsList.removeAll()
+ needsDisplay = true
+ }
+
+ override func draw(_ dirtyRect: NSRect) {
+ var textX = CGFloat(rectFillPadding)
+ let drawList = list.count > 0 ? list : [ListItem(name: "No results", data: nil)]
+
+ for i in 0 ..< drawList.count {
+ let item = (drawList[i].name) as NSString
+ let size = item.size(withAttributes: [NSAttributedString.Key: Any]())
+ let textY = (frame.height - size.height) / 2
+
+ if selectedIndexValue == i {
+ selectedRect = NSRect(
+ x: textX - rectFillPadding,
+ y: textY - rectFillPadding,
+ width: size.width + rectFillPadding * 2,
+ height: size.height + rectFillPadding * 2)
+ NSColor.selectedTextBackgroundColor.setFill()
+ __NSRectFill(selectedRect)
+ }
+
+ item.draw(in: NSRect(
+ x: textX,
+ y: textY,
+ width: size.width,
+ height: size.height), withAttributes: [
+ NSAttributedString.Key.foregroundColor: NSColor.textColor
+ ])
+
+ textX += 10 + size.width
+ }
+ if dirtyWidth {
+ frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: textX, height: frame.height)
+ dirtyWidth = false
+ scrollView.contentView.scrollToVisible(selectedRect)
+ }
+ }
+
+ func updateWidth() {
+ dirtyWidth = true
+ }
+}
diff --git a/src/SearchViewController.swift b/src/SearchViewController.swift
new file mode 100644
index 0000000..592937d
--- /dev/null
+++ b/src/SearchViewController.swift
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Carbon
+import Cocoa
+import Fuse
+import KeyboardShortcuts
+
+class SearchViewController: NSViewController, NSTextFieldDelegate, NSWindowDelegate {
+
+ @IBOutlet fileprivate var searchText: InputField!
+ @IBOutlet fileprivate var resultsText: ResultsView!
+ var hotkey: DDHotKey?
+ var listProvider: ListProvider?
+ var promptValue = ""
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ searchText.delegate = self
+
+ KeyboardShortcuts.onKeyUp(for: .activateSearch) { [self] in
+ resumeApp()
+ }
+
+ DistributedNotificationCenter.default.addObserver(
+ self,
+ selector: #selector(interfaceModeChanged),
+ name: .AppleInterfaceThemeChangedNotification,
+ object: nil
+ )
+
+ let stdinStr = ReadStdin.read()
+ if stdinStr.count > 0 {
+ listProvider = PipeListProvider(str: stdinStr)
+ } else {
+ listProvider = AppListProvider()
+ }
+
+ let options = DmenuMac.parseOrExit()
+ if options.prompt != nil {
+ promptValue = options.prompt!
+ }
+
+ clearFields()
+ resumeApp()
+ }
+
+ @objc func interfaceModeChanged(sender: NSNotification) {
+ updateColors()
+ }
+
+ func updateColors() {
+ guard let window = NSApp.windows.first else { return }
+
+ window.isOpaque = false
+ window.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.6)
+ searchText.textColor = NSColor.textColor
+ }
+
+ @objc func resumeApp() {
+ NSApplication.shared.activate(ignoringOtherApps: true)
+ view.window?.orderFrontRegardless()
+
+ if let controller = view.window as? SearchWindow {
+ controller.updatePosition()
+ }
+
+ updateColors()
+ }
+
+ func controlTextDidChange(_ obj: Notification) {
+ if searchText.stringValue == "" {
+ clearFields()
+ return
+ }
+
+ // Get provider list, filter using fuzzy search, apply
+ var scoreDict = [Int: Double]()
+
+ let fuse = Fuse(threshold: 0.4)
+ let pattern = fuse.createPattern(from: searchText.stringValue)
+
+ let list = listProvider?.get() ?? []
+
+ for (idx, item) in list.enumerated() {
+ guard let result = fuse.search(pattern, in: item.name) else {
+ continue
+ }
+ scoreDict[idx] = result.score
+ }
+
+ let sortedScoreDict = scoreDict.sorted(by: {$0.1 < $1.1}).map({list[$0.0]})
+ if !sortedScoreDict.isEmpty {
+ self.resultsText.list = sortedScoreDict
+ } else {
+ self.resultsText.clear()
+ }
+
+ self.resultsText.updateWidth()
+ }
+
+ func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+ let movingLeft: Bool =
+ commandSelector == #selector(moveLeft(_:)) ||
+ commandSelector == #selector(insertBacktab(_:))
+ let movingRight: Bool =
+ commandSelector == #selector(moveRight(_:)) ||
+ commandSelector == #selector(insertTab(_:))
+
+ if movingLeft {
+ resultsText.selectedIndex = resultsText.selectedIndex == 0 ?
+ resultsText.list.count - 1 : resultsText.selectedIndex - 1
+ resultsText.updateWidth()
+ return true
+ } else if movingRight {
+ resultsText.selectedIndex = (resultsText.selectedIndex + 1) % resultsText.list.count
+ resultsText.updateWidth()
+ return true
+ } else if commandSelector == #selector(insertNewline(_:)) {
+ // open current selected app
+ if let item = resultsText.selectedItem() {
+ listProvider?.doAction(item: item)
+ closeApp()
+ }
+
+ return true
+ } else if commandSelector == #selector(cancelOperation(_:)) {
+ closeApp()
+ return true
+ }
+
+ return false
+ }
+
+ func clearFields() {
+ self.searchText.stringValue = promptValue
+ self.resultsText.list = listProvider?.get().sorted(by: {$0.name < $1.name}) ?? []
+ }
+
+ func closeApp() {
+ clearFields()
+ if promptValue == "" {
+ NSApplication.shared.hide(nil)
+ }
+ }
+}
diff --git a/src/SearchWindow.swift b/src/SearchWindow.swift
new file mode 100644
index 0000000..ca0657e
--- /dev/null
+++ b/src/SearchWindow.swift
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class SearchWindow: NSWindow {
+
+ override func awakeFromNib() {
+ self.hasShadow = false
+ self.collectionBehavior = NSWindow.CollectionBehavior.canJoinAllSpaces
+ updatePosition()
+ }
+
+ /**
+ * Updates search window position.
+ */
+ func updatePosition() {
+ guard let screen = NSScreen.main else { return }
+
+ let frame = NSRect(
+ x: screen.frame.minX,
+ y: screen.frame.minY + screen.frame.height - self.frame.height,
+ width: screen.frame.width,
+ height: self.frame.height)
+
+ setFrame(frame, display: false)
+ }
+
+ override var canBecomeKey: Bool {
+ return true
+ }
+
+ override var canBecomeMain: Bool {
+ return true
+ }
+}
diff --git a/src/SettingsViewController.swift b/src/SettingsViewController.swift
new file mode 100644
index 0000000..12cd449
--- /dev/null
+++ b/src/SettingsViewController.swift
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+public protocol SettingsViewControllerDelegate: AnyObject {
+ func onSettingsCanceled()
+ func onSettingsApplied()
+}
+
+class SettingsViewController: NSViewController {
+
+ @IBOutlet var hotkeyTextField: DDHotKeyTextField!
+ weak var delegate: SettingsViewControllerDelegate?
+
+ override func viewDidLoad() {
+ let keycode = UserDefaults.standard
+ .integer(forKey: kDefaultsGlobalShortcutKeycode)
+ let modifierFlags = UserDefaults.standard
+ .integer(forKey: kDefaultsGlobalShortcutModifiedFlags)
+
+ hotkeyTextField.hotKey = DDHotKey(
+ keyCode: UInt16(keycode),
+ modifierFlags: UInt(modifierFlags),
+ task: nil)
+ }
+
+ @IBAction func applySettings(_ sender: AnyObject) {
+ UserDefaults.standard.set(
+ Int(hotkeyTextField.hotKey.keyCode), forKey: kDefaultsGlobalShortcutKeycode)
+ UserDefaults.standard.set(
+ Int(hotkeyTextField.hotKey.modifierFlags), forKey: kDefaultsGlobalShortcutModifiedFlags)
+
+ delegate?.onSettingsApplied()
+ }
+
+ @IBAction func cancelSettings(_ sender: AnyObject) {
+ delegate?.onSettingsCanceled()
+ }
+}
diff --git a/src/VerticalAlignedTextFieldCell.swift b/src/VerticalAlignedTextFieldCell.swift
new file mode 100644
index 0000000..a40a4ec
--- /dev/null
+++ b/src/VerticalAlignedTextFieldCell.swift
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class VerticalAlignedTextFieldCell: NSTextFieldCell {
+ var editingOrSelecting: Bool = false
+
+ override func drawingRect(forBounds theRect: NSRect) -> NSRect {
+ var newRect = super.drawingRect(forBounds: theRect)
+ if !editingOrSelecting {
+ let textSize = self.cellSize(forBounds: theRect)
+ let heightDelta = newRect.size.height - textSize.height
+ if heightDelta > 0 {
+ newRect.size.height -= heightDelta
+ newRect.origin.y += (heightDelta / 2)
+ }
+ }
+ return newRect
+ }
+
+ override func select(withFrame aRect: NSRect, in controlView: NSView,
+ editor textObj: NSText, delegate anObject: Any?,
+ start selStart: Int, length selLength: Int) {
+ let aRect = self.drawingRect(forBounds: aRect)
+
+ editingOrSelecting = true
+ super.select(withFrame: aRect,
+ in: controlView,
+ editor: textObj,
+ delegate: anObject,
+ start: selStart,
+ length: selLength)
+
+ editingOrSelecting = false
+ }
+
+ override func edit(withFrame aRect: NSRect, in controlView: NSView,
+ editor textObj: NSText, delegate anObject: Any?,
+ event theEvent: NSEvent?) {
+ let aRect = self.drawingRect(forBounds: aRect)
+ editingOrSelecting = true
+ self.edit(withFrame: aRect,
+ in: controlView,
+ editor: textObj,
+ delegate: anObject,
+ event: theEvent)
+ editingOrSelecting = false
+ }
+}