Initial commit.
175
src/AppDelegate.swift
Normal file
@@ -0,0 +1,175 @@
|
||||
import Cocoa
|
||||
import Carbon
|
||||
import ServiceManagement
|
||||
import OSLog
|
||||
|
||||
struct Program {
|
||||
let path: String
|
||||
let name: String
|
||||
let ext: String
|
||||
}
|
||||
|
||||
func appActivatedHandler(nextHandler: EventHandlerCallRef?, theEvent: EventRef?, userData: UnsafeMutableRawPointer?) -> OSStatus {
|
||||
print("App was activated!")
|
||||
return noErr
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
fileprivate static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: String(describing: AppDelegate.self)
|
||||
)
|
||||
|
||||
var paths = ["/Applications", "/System/Applications",
|
||||
"/System/Applications/Utilities",
|
||||
"/Applications/Xcode.app/Contents/Applications",
|
||||
"/System/Library/CoreServices/Applications"]
|
||||
var programs: [Program] = []
|
||||
|
||||
let fileManager = FileManager.default
|
||||
|
||||
let window = PopoverPanel(viewController: SearchViewController())
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
Self.logger.debug("applicationDidFinishLaunching")
|
||||
|
||||
NSRunningApplication.current.hide()
|
||||
|
||||
window.delegate = self
|
||||
|
||||
//GlobalEventTap.shared.enable()
|
||||
|
||||
for path in paths {
|
||||
do {
|
||||
let items = try fileManager.contentsOfDirectory(atPath: path)
|
||||
for item in items {
|
||||
let name = String(item.dropLast(4))
|
||||
if item.hasSuffix(".app") {
|
||||
if !programs.contains(where: { name == $0.name }) {
|
||||
programs.append(Program(path: path, name: name, ext: ".app"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error reading directory: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
// TODO: Implement Unregister and Uninstall.
|
||||
// TODO: A user should be able to enter hot keys to trigger.
|
||||
// We either can use local event monitor or let user choose
|
||||
// from list.
|
||||
var hotKeyRef: EventHotKeyRef?
|
||||
let hotKeyID: EventHotKeyID = EventHotKeyID(signature: OSType("grap".fourCharCodeValue), id: 1)
|
||||
|
||||
// GetEventDispatcherTarget
|
||||
var err = RegisterEventHotKey(UInt32(kVK_Space), UInt32(optionKey), hotKeyID, GetApplicationEventTarget(), UInt32(kEventHotKeyNoOptions), &hotKeyRef)
|
||||
//let handler = NewEventHandlerUPP()
|
||||
|
||||
// Handler get executed on main thread.
|
||||
let handler: EventHandlerUPP = { (inHandlerCallRef, inEvent, inUserData) -> OSStatus in
|
||||
AppDelegate.logger.debug("Shortcut handler fired off.")
|
||||
if let delegate = NSApplication.shared.delegate as? AppDelegate {
|
||||
let window = delegate.window
|
||||
if window.isKeyWindow {
|
||||
window.resignKey()
|
||||
} else {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
}
|
||||
|
||||
return noErr
|
||||
}
|
||||
var eventHandlerRef: EventHandlerRef? = nil
|
||||
|
||||
if err == noErr {
|
||||
Self.logger.debug("Registered hot key.")
|
||||
|
||||
var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
|
||||
err = InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType, nil, &eventHandlerRef)
|
||||
|
||||
if err == noErr {
|
||||
Self.logger.debug("Event handler installed.")
|
||||
} else {
|
||||
Self.logger.debug("Failed to install event handler.")
|
||||
}
|
||||
} else {
|
||||
Self.logger.debug("Failed to register hot key.")
|
||||
}
|
||||
}
|
||||
|
||||
//func applicationWillTerminate(_ notification: Notification) {
|
||||
//}
|
||||
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
Self.logger.debug("Popover became key.")
|
||||
}
|
||||
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
Self.logger.debug("Popover resigned key.")
|
||||
|
||||
if window.isVisible {
|
||||
window.orderOut(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
|
||||
Self.logger.debug("Application reopened.")
|
||||
|
||||
if !window.isKeyWindow {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
subscript(index: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: index)]
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
public func levenshtein(_ other: String) -> Int {
|
||||
let sCount = self.count
|
||||
let oCount = other.count
|
||||
|
||||
guard sCount != 0 else {
|
||||
return oCount
|
||||
}
|
||||
|
||||
guard oCount != 0 else {
|
||||
return sCount
|
||||
}
|
||||
|
||||
let line : [Int] = Array(repeating: 0, count: oCount + 1)
|
||||
var mat : [[Int]] = Array(repeating: line, count: sCount + 1)
|
||||
|
||||
for i in 0...sCount {
|
||||
mat[i][0] = i
|
||||
}
|
||||
|
||||
for j in 0...oCount {
|
||||
mat[0][j] = j
|
||||
}
|
||||
|
||||
for j in 1...oCount {
|
||||
for i in 1...sCount {
|
||||
if self[i - 1] == other[j - 1] {
|
||||
mat[i][j] = mat[i - 1][j - 1] // no operation
|
||||
}
|
||||
else {
|
||||
let del = mat[i - 1][j] + 1 // deletion
|
||||
let ins = mat[i][j - 1] + 1 // insertion
|
||||
let sub = mat[i - 1][j - 1] + 1 // substitution
|
||||
mat[i][j] = min(min(del, ins), sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mat[sCount][oCount]
|
||||
}
|
||||
}
|
||||
33
src/EditableNSTextField.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import Cocoa
|
||||
import os
|
||||
|
||||
final class EditableNSTextField: NSTextField {
|
||||
private let commandKey = NSEvent.ModifierFlags.command.rawValue
|
||||
private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue
|
||||
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
if event.type == NSEvent.EventType.keyDown {
|
||||
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
|
||||
switch event.charactersIgnoringModifiers! {
|
||||
case "x":
|
||||
if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }
|
||||
case "c":
|
||||
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }
|
||||
case "v":
|
||||
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }
|
||||
case "z":
|
||||
if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true }
|
||||
case "a":
|
||||
if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true }
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
|
||||
if event.charactersIgnoringModifiers == "Z" {
|
||||
if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true }
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.performKeyEquivalent(with: event)
|
||||
}
|
||||
}
|
||||
55
src/EventMonitor.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Cocoa
|
||||
|
||||
class EventMonitor {
|
||||
fileprivate let mask: NSEvent.EventTypeMask
|
||||
fileprivate var monitor: Any?
|
||||
|
||||
fileprivate init(mask: NSEvent.EventTypeMask) {
|
||||
self.mask = mask
|
||||
}
|
||||
|
||||
deinit {
|
||||
stop()
|
||||
}
|
||||
|
||||
func start() {
|
||||
fatalError("start must be implemented by a subclass of EventMonitor")
|
||||
}
|
||||
|
||||
func stop() {
|
||||
if monitor != nil {
|
||||
NSEvent.removeMonitor(monitor!)
|
||||
monitor = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class LocalEventMonitor: EventMonitor {
|
||||
typealias Handler = (NSEvent) -> NSEvent?
|
||||
|
||||
private let handler: Handler
|
||||
|
||||
init(mask: NSEvent.EventTypeMask, handler: @escaping Handler) {
|
||||
self.handler = handler
|
||||
super.init(mask: mask)
|
||||
}
|
||||
|
||||
override func start() {
|
||||
monitor = NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
final class GlobalEventMonitor: EventMonitor {
|
||||
typealias Handler = (NSEvent) -> Void
|
||||
|
||||
private let handler: Handler
|
||||
|
||||
init(mask: NSEvent.EventTypeMask, handler: @escaping Handler) {
|
||||
self.handler = handler
|
||||
super.init(mask: mask)
|
||||
}
|
||||
|
||||
override func start() {
|
||||
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
|
||||
}
|
||||
}
|
||||
74
src/GlobalEventTap.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import AppKit
|
||||
import Carbon
|
||||
import OSLog
|
||||
|
||||
fileprivate func handleGlobalEvents(proxy: CGEventTapProxy,
|
||||
type: CGEventType, event: CGEvent,
|
||||
refcon: UnsafeMutableRawPointer?
|
||||
) -> Unmanaged<CGEvent>? {
|
||||
let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: String(describing: AppDelegate.self)
|
||||
)
|
||||
|
||||
switch type {
|
||||
case .keyDown:
|
||||
//logger.debug(".keyDown")
|
||||
|
||||
if (event.flags.rawValue & CGEventFlags.maskAlternate.rawValue) == CGEventFlags.maskAlternate.rawValue &&
|
||||
(event.flags.rawValue & (CGEventFlags.maskShift.rawValue | CGEventFlags.maskControl.rawValue | CGEventFlags.maskCommand.rawValue)) == 0 {
|
||||
logger.debug("maskAlternate")
|
||||
}
|
||||
|
||||
logger.debug("Option rawValue=\(CGEventFlags.maskAlternate.rawValue)")
|
||||
|
||||
// var keyCode = event.getIntegerValueField(.keyboardEventKeycode)
|
||||
//if keyCode == 49 {
|
||||
// logger.debug("EVENT TAP")
|
||||
// return nil
|
||||
//}
|
||||
case .keyUp:
|
||||
//logger.debug(".keyUp")
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
//event.setIntegerValueField(.keyboardEventKeycode, value: keyCode) // NOTE: ???
|
||||
|
||||
return Unmanaged.passUnretained(event)
|
||||
}
|
||||
|
||||
final class GlobalEventTap {
|
||||
fileprivate static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: String(describing: GlobalEventTap.self)
|
||||
)
|
||||
|
||||
static let shared = GlobalEventTap()
|
||||
|
||||
private init() {}
|
||||
|
||||
deinit {}
|
||||
|
||||
func enable() {
|
||||
let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
|
||||
guard let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap,
|
||||
place: .headInsertEventTap,
|
||||
options: .defaultTap,
|
||||
eventsOfInterest: CGEventMask(eventMask),
|
||||
callback: handleGlobalEvents,
|
||||
userInfo: nil) else {
|
||||
Self.logger.debug("Failed to create event.")
|
||||
return
|
||||
}
|
||||
|
||||
Self.logger.debug("Event was created.")
|
||||
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
|
||||
CGEvent.tapEnable(tap: eventTap, enable: true)
|
||||
CFRunLoopRun()
|
||||
}
|
||||
}
|
||||
33
src/Helpers.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import AppKit
|
||||
|
||||
func systemImage(_ name: String, _ size: NSFont.TextStyle, _ scale: NSImage.SymbolScale, _ configuration: NSImage.SymbolConfiguration) -> NSImage? {
|
||||
return NSImage(systemSymbolName: name, accessibilityDescription: nil)?
|
||||
.withSymbolConfiguration(
|
||||
NSImage.SymbolConfiguration(textStyle: size, scale: scale)
|
||||
.applying(configuration)
|
||||
)
|
||||
}
|
||||
|
||||
func isDirectory(atPath path: String) -> Bool {
|
||||
var isDir: ObjCBool = false
|
||||
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir) {
|
||||
return isDir.boolValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// This converts string to UInt as a fourCharCode
|
||||
public var fourCharCodeValue: Int {
|
||||
var result: Int = 0
|
||||
if let data = self.data(using: String.Encoding.macOSRoman) {
|
||||
data.withUnsafeBytes({ (rawBytes) in
|
||||
let bytes = rawBytes.bindMemory(to: UInt8.self)
|
||||
for i in 0 ..< data.count {
|
||||
result = result << 8 + Int(bytes[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
44
src/Info.plist
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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>Grapp</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.garikme.Grapp</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Grapp</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1</string>
|
||||
<key>DTPlatformName</key>
|
||||
<string>macosx</string>
|
||||
<key>DTPlatformVersion</key>
|
||||
<string>15.0</string>
|
||||
<key>DTSDKName</key>
|
||||
<string>macosx15.0</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
85
src/Makefile
Normal file
@@ -0,0 +1,85 @@
|
||||
FLAGS = -g
|
||||
CFLAGS = -g
|
||||
#-O
|
||||
MACOS_VERSION = 13.0
|
||||
SDK = $(shell xcrun --show-sdk-path)
|
||||
XCODE_PATH = $(shell xcode-select --print-path)
|
||||
|
||||
EXEC = Grapp
|
||||
|
||||
SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift \
|
||||
GlobalEventTap.swift PopoverPanel.swift SearchViewController.swift \
|
||||
AppDelegate.swift main.swift
|
||||
ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o))
|
||||
X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o))
|
||||
|
||||
LIBS =
|
||||
|
||||
FRAMEWORKS = -framework AppKit -framework ServiceManagement
|
||||
|
||||
./arm64/%.o: %.swift
|
||||
swift -frontend -c -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \
|
||||
-primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) $(FRAMEWORKS) -sdk $(SDK) \
|
||||
-module-name $(EXEC) -o $@ -emit-module
|
||||
|
||||
|
||||
ifdef UNIVERSAL
|
||||
./x86_64/%.o: %.swift
|
||||
@swift -frontend -c -target x86_64-apple-macos$(MACOS_VERSION) \
|
||||
$(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) $(FRAMEWORKS) \
|
||||
-sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module
|
||||
endif
|
||||
|
||||
./arm64/$(EXEC): $(ARMOBJMODULES)
|
||||
@ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch arm64 -macos_version_min \
|
||||
$(MACOS_VERSION).0 \
|
||||
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
|
||||
-sectcreate __TEXT __info_plist Info.plist \
|
||||
-L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \
|
||||
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift \
|
||||
-no_objc_category_merging -L $(XCODE_PATH) -rpath \
|
||||
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
|
||||
./arm64/main.o $(filter-out ./arm64/main.o, $(ARMOBJMODULES)) \
|
||||
-o $@
|
||||
|
||||
ifdef UNIVERSAL
|
||||
./x86_64/$(EXEC): $(X86OBJMODULES)
|
||||
@ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch x86_64 -macos_version_min \
|
||||
$(MACOS_VERSION).0 \
|
||||
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
|
||||
-sectcreate __TEXT __info_plist Info.plist \
|
||||
-L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \
|
||||
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift \
|
||||
-no_objc_category_merging -L $(XCODE_PATH) -rpath \
|
||||
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
|
||||
./x86_64/main.o $(filter-out ./x86_64/main.o, $(X86OBJMODULES)) \
|
||||
-o $@
|
||||
endif
|
||||
|
||||
ifdef UNIVERSAL
|
||||
$(EXEC): ./arm64/$(EXEC) ./x86_64/$(EXEC)
|
||||
@lipo -create -output $(EXEC) $^
|
||||
else
|
||||
$(EXEC): ./arm64/$(EXEC)
|
||||
@lipo -create -output $(EXEC) $^
|
||||
endif
|
||||
|
||||
$(EXEC).app: $(EXEC)
|
||||
@rm -rf $@
|
||||
@mkdir -p $@/Contents/MacOS/ && \
|
||||
mkdir -p $@/Contents/Resources/ && \
|
||||
cp Info.plist $@/Contents/ && \
|
||||
cp resources/AppIcon.icns $@/Contents/Resources/ && \
|
||||
cp $(EXEC) $@/Contents/MacOS/ && \
|
||||
codesign -s ${DEVELOPER_ID} -f --timestamp -o runtime $(EXEC).app
|
||||
|
||||
all: $(EXEC).app
|
||||
|
||||
run: all
|
||||
@open $(EXEC).app
|
||||
|
||||
clean:
|
||||
rm -rf $(EXEC) $(EXEC).app arm64 x86_64
|
||||
mkdir arm64 x86_64
|
||||
|
||||
clean-all: clean
|
||||
61
src/PopoverPanel.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Cocoa
|
||||
import OSLog
|
||||
|
||||
class PopoverPanel: NSPanel {
|
||||
fileprivate static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: String(describing: PopoverPanel.self)
|
||||
)
|
||||
|
||||
override var canBecomeKey: Bool { true }
|
||||
|
||||
init(viewController: NSViewController) {
|
||||
super.init(
|
||||
contentRect: CGRect(x: 0, y: 0, width: 100, height: 100),
|
||||
styleMask: [.titled, .nonactivatingPanel, .utilityWindow, .fullSizeContentView],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
super.contentViewController = viewController
|
||||
|
||||
title = ""
|
||||
//isMovable = false
|
||||
isMovableByWindowBackground = false
|
||||
isFloatingPanel = true
|
||||
isOpaque = false
|
||||
level = .statusBar
|
||||
titleVisibility = .hidden
|
||||
titlebarAppearsTransparent = true
|
||||
|
||||
animationBehavior = .none
|
||||
collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .transient]
|
||||
isReleasedWhenClosed = false
|
||||
hidesOnDeactivate = false
|
||||
|
||||
standardWindowButton(.closeButton)?.isHidden = true
|
||||
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
standardWindowButton(.zoomButton)?.isHidden = true
|
||||
}
|
||||
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
Self.logger.debug("performKeyEquivalent keyCode=\(event.keyCode)")
|
||||
let commandKey = NSEvent.ModifierFlags.command.rawValue
|
||||
|
||||
if event.type == NSEvent.EventType.keyDown {
|
||||
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey,
|
||||
event.charactersIgnoringModifiers! == "w" {
|
||||
resignKey()
|
||||
return true
|
||||
} else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey,
|
||||
event.charactersIgnoringModifiers! == "q" {
|
||||
NSApplication.shared.terminate(self)
|
||||
return true
|
||||
} else if event.keyCode == 53 {
|
||||
resignKey()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.performKeyEquivalent(with: event)
|
||||
}
|
||||
}
|
||||
175
src/SearchViewController.swift
Normal file
@@ -0,0 +1,175 @@
|
||||
import AppKit
|
||||
import OSLog
|
||||
|
||||
fileprivate enum ViewConstants {
|
||||
static let spacing2: CGFloat = 2
|
||||
static let spacing10: CGFloat = 10
|
||||
static let spacing20: CGFloat = 20
|
||||
static let spacing40: CGFloat = 40
|
||||
}
|
||||
|
||||
class SearchViewController: NSViewController, NSTextFieldDelegate {
|
||||
fileprivate static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: String(describing: SearchViewController.self)
|
||||
)
|
||||
|
||||
var foundProgram: Program? = nil
|
||||
|
||||
private var appIconImage: NSImageView = {
|
||||
//let image = NSImageView(image: NSApp.applicationIconImage)
|
||||
let image = NSImageView()
|
||||
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
|
||||
image.imageScaling = .scaleAxesIndependently
|
||||
image.translatesAutoresizingMaskIntoConstraints = false
|
||||
return image
|
||||
}()
|
||||
|
||||
private var searchInput: EditableNSTextField = {
|
||||
let textField = EditableNSTextField()
|
||||
textField.placeholderString = "Search programs . . ."
|
||||
textField.bezelStyle = .roundedBezel
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var programsLabel: NSTextField = {
|
||||
let textField = NSTextField()
|
||||
textField.stringValue = ""
|
||||
textField.isEditable = false
|
||||
textField.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
textField.alignment = .left
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var settingsButton: NSButton = {
|
||||
let button = NSButton()
|
||||
button.image = systemImage("gearshape.fill", .title2, .large, .init(paletteColors: [.white, .systemRed]))
|
||||
button.isBordered = false
|
||||
button.action = #selector(openSettings)
|
||||
button.sizeToFit()
|
||||
button.toolTip = "Quit"
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
|
||||
|
||||
private func addSubviews() {
|
||||
view.addSubview(appIconImage)
|
||||
view.addSubview(searchInput)
|
||||
view.addSubview(programsLabel)
|
||||
view.addSubview(settingsButton)
|
||||
}
|
||||
|
||||
private func setConstraints() {
|
||||
NSLayoutConstraint.activate([
|
||||
appIconImage.widthAnchor.constraint(equalToConstant: 70),
|
||||
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1),
|
||||
|
||||
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20),
|
||||
appIconImage.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing10),
|
||||
appIconImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10),
|
||||
|
||||
searchInput.widthAnchor.constraint(equalToConstant: 300),
|
||||
searchInput.topAnchor.constraint(equalTo: appIconImage.topAnchor),
|
||||
searchInput.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10),
|
||||
|
||||
settingsButton.firstBaselineAnchor.constraint(equalTo: searchInput.firstBaselineAnchor),
|
||||
settingsButton.leadingAnchor.constraint(equalTo: searchInput.trailingAnchor, constant: ViewConstants.spacing10),
|
||||
settingsButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10),
|
||||
|
||||
programsLabel.topAnchor.constraint(equalTo: searchInput.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
programsLabel.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10),
|
||||
programsLabel.trailingAnchor.constraint(equalTo: searchInput.trailingAnchor),
|
||||
|
||||
])
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
searchInput.delegate = self
|
||||
|
||||
addSubviews()
|
||||
setConstraints()
|
||||
}
|
||||
|
||||
override func viewDidAppear() {
|
||||
super.viewDidAppear()
|
||||
|
||||
self.view.window?.center()
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = NSView()
|
||||
}
|
||||
|
||||
//private func fetchIcon() {
|
||||
// for key in resultPaths.keys {
|
||||
// }
|
||||
//}
|
||||
|
||||
@objc
|
||||
private func openSettings() {
|
||||
}
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
var list = ""
|
||||
|
||||
let programs = delegate.programs
|
||||
for program in programs {
|
||||
if program.name.lowercased().contains(searchInput.stringValue.lowercased()) {
|
||||
if !list.isEmpty {
|
||||
list += ", "
|
||||
}
|
||||
list += program.name + program.ext
|
||||
foundProgram = program
|
||||
break
|
||||
} else {
|
||||
foundProgram = nil
|
||||
}
|
||||
}
|
||||
|
||||
if let program = foundProgram {
|
||||
programsLabel.stringValue = program.name + program.ext
|
||||
|
||||
let url = URL(fileURLWithPath: program.path).appendingPathComponent(program.name+program.ext)
|
||||
appIconImage.image = NSWorkspace.shared.icon(forFile: url.path)
|
||||
} else {
|
||||
programsLabel.stringValue = ""
|
||||
|
||||
appIconImage.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
||||
if let program = foundProgram {
|
||||
let url = URL(fileURLWithPath: program.path).appendingPathComponent(program.name+program.ext)
|
||||
let config = NSWorkspace.OpenConfiguration()
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: config, completionHandler: { [weak self] application, error in
|
||||
if let error = error {
|
||||
Self.logger.debug("Failed to open application: \(error.localizedDescription)")
|
||||
} else {
|
||||
Self.logger.debug("Application opened successfully")
|
||||
DispatchQueue.main.async {
|
||||
if let window = self?.view.window {
|
||||
window.resignKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// TODO: Send this whenever SearchViewController becomes visible.
|
||||
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self)
|
||||
return true
|
||||
} else if commandSelector == #selector(NSResponder.insertTab(_:)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
8
src/main.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
let app = NSApplication.shared
|
||||
let delegate = AppDelegate()
|
||||
app.delegate = delegate
|
||||
|
||||
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
||||
BIN
src/resources/AppIcon.icns
Normal file
BIN
src/resources/icon.iconset/icon_128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/resources/icon.iconset/icon_128x128@2x.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/resources/icon.iconset/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 746 B |
BIN
src/resources/icon.iconset/icon_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/resources/icon.iconset/icon_256x256.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/resources/icon.iconset/icon_256x256@2x.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
src/resources/icon.iconset/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/resources/icon.iconset/icon_32x32@2x.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/resources/icon.iconset/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
src/resources/icon.iconset/icon_512x512@2x.png
Normal file
|
After Width: | Height: | Size: 285 KiB |