Compare commits

..

10 Commits

11 changed files with 158 additions and 116 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
.DS_Store .DS_Store
arm64 arm64
x86_64 x86_64
build
Grapp Grapp
Grapp.app Grapp.app
ids

5
CHANGELOG.txt Normal file
View File

@@ -0,0 +1,5 @@
1.94.1
- Fixed a bug where the new path was not selectable with Finder file picker
- Fixed a bug that caused crashing when removing a new path
- Search now includes the bundle extension
- Search now adds a background highlight to searched items

45
Makefile Normal file
View File

@@ -0,0 +1,45 @@
APPLE_ID := $(shell cat ./ids/APPLE_ID)
TEAM_ID := $(shell cat ./ids/TEAM_ID)
APP_SPECIFIC_PASSWORD := $(shell cat ./ids/APP_SPECIFIC_PASSWORD)
exe = Grapp
$(exe).app:
$(MAKE) -C src FLAGS=-O CFLAGS=-O3 UNIVERSAL=1 default
container:
ditto -c -k --keepParent ./src/$(exe).app ./build/$(exe).zip
notarize:
xcrun notarytool submit ./build/$(exe).zip \
--apple-id "$(APPLE_ID)" --team-id "$(TEAM_ID)" \
--password "$(APP_SPECIFIC_PASSWORD)" --wait
staple:
cd build && \
ditto -xk $(exe).zip . && \
rm $(exe).zip && \
xcrun stapler staple $(exe).app
zip:
cd build && \
ditto -c -k --keepParent $(exe).app $(exe).zip
dmg:
cp background.png build && cd build && \
create-dmg --volname "$(exe) Installer" --window-size 600 400 \
--background "background.png" --icon "$(exe).app" 200 170 \
--app-drop-link 400 170 --icon-size 100 "$(exe).dmg" "$(exe)"
rm build/background.png
all: $(exe).app container notarize staple zip dmg
status:
@echo 'xcrun notarytool log "" --apple-id "$(APPLE_ID)" --team-id "$(TEAM_ID)" --password "$(APP_SPECIFIC_PASSWORD)" developer_log.json'
clean:
rm -rf build
clean-all: clean
mkdir build
@$(MAKE) -C src clean-all

View File

@@ -2,21 +2,14 @@ import AppKit
// TODO: Change to appropriate links. // TODO: Change to appropriate links.
fileprivate enum AboutLinks { fileprivate enum AboutLinks {
// static let website = "https://cmdbar.app" static let website = "https://cmdbar.rednera.com"
// static let documentation = "https://cmdbar.app/documentation" // static let documentation = "https://cmdbar.app/documentation"
// static let privacy = "https://cmdbar.app/#privacy-policy" static let privacy = "https://cmdbar.rednera.com/#privacy-policy"
static let author = "https://kolokolnikov.org" static let author = "https://kolokolnikov.dev"
} }
enum Strings { enum Strings {
static let copyright = "Copyright © 2024\nGarikMI. All rights reserved." static let copyright = "Copyright © 2026 Rednera.\nAll rights reserved."
static let evaluationTitle = "License - Evaluation"
static let evaluationMessage = "You are currently using evaluation license. CmdBar will quit after 20 minutes. If you already own a license, enter it below or purchase a license."
static let activate = "Activate"
static let proTitle = "License - Activated"
static let proMessage = "Thank you for purchasing CmdBar! Enjoy!"
static let deactivate = "Deactivate"
static let activating = "Activating..."
} }
class AboutViewController: NSViewController, NSTextFieldDelegate { class AboutViewController: NSViewController, NSTextFieldDelegate {
@@ -83,26 +76,26 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
return textField return textField
}() }()
private var authorButton: NSButton = { // private var authorButton: NSButton = {
let button = NSButton()
button.title = "Author"
button.sizeToFit()
button.bezelStyle = .rounded
button.action = #selector(author)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
// private var privacyButton: NSButton = {
// let button = NSButton() // let button = NSButton()
// button.title = "Privacy Policy" // button.title = "Author"
// button.sizeToFit() // button.sizeToFit()
// button.bezelStyle = .rounded // button.bezelStyle = .rounded
// button.action = #selector(privacy) // button.action = #selector(author)
// button.translatesAutoresizingMaskIntoConstraints = false // button.translatesAutoresizingMaskIntoConstraints = false
// return button // return button
// }() // }()
private var privacyButton: NSButton = {
let button = NSButton()
button.title = "Privacy Policy"
button.sizeToFit()
button.bezelStyle = .rounded
button.action = #selector(privacy)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
// private var documentationButton: NSButton = { // private var documentationButton: NSButton = {
// let button = NSButton() // let button = NSButton()
// button.title = "Docs" // button.title = "Docs"
@@ -112,16 +105,16 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
// button.translatesAutoresizingMaskIntoConstraints = false // button.translatesAutoresizingMaskIntoConstraints = false
// return button // return button
// }() // }()
//
// private var websiteButton: NSButton = { private var websiteButton: NSButton = {
// let button = NSButton() let button = NSButton()
// button.title = "CmdBar.app" button.title = "Grapp"
// button.sizeToFit() button.sizeToFit()
// button.bezelStyle = .rounded button.bezelStyle = .rounded
// button.action = #selector(website) button.action = #selector(website)
// button.translatesAutoresizingMaskIntoConstraints = false button.translatesAutoresizingMaskIntoConstraints = false
// return button return button
// }() }()
private var buttonsContainer: NSLayoutGuide = { private var buttonsContainer: NSLayoutGuide = {
let container = NSLayoutGuide() let container = NSLayoutGuide()
@@ -139,10 +132,10 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
// Buttons // Buttons
view.addLayoutGuide(buttonsContainer) view.addLayoutGuide(buttonsContainer)
// view.addSubview(privacyButton) view.addSubview(privacyButton)
// view.addSubview(documentationButton) // view.addSubview(documentationButton)
// view.addSubview(websiteButton) view.addSubview(websiteButton)
view.addSubview(authorButton) // view.addSubview(authorButton)
setupConstraints() setupConstraints()
} }
@@ -208,51 +201,51 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
buttonsContainer.centerXAnchor buttonsContainer.centerXAnchor
.constraint(equalTo: view.centerXAnchor), .constraint(equalTo: view.centerXAnchor),
authorButton.topAnchor // authorButton.topAnchor
.constraint(equalTo: buttonsContainer.topAnchor),
authorButton.bottomAnchor
.constraint(equalTo: buttonsContainer.bottomAnchor),
authorButton.leadingAnchor
.constraint(equalTo: buttonsContainer.leadingAnchor),
authorButton.trailingAnchor
.constraint(equalTo: buttonsContainer.trailingAnchor),
// privacyButton.topAnchor
// .constraint(equalTo: buttonsContainer.topAnchor), // .constraint(equalTo: buttonsContainer.topAnchor),
// privacyButton.bottomAnchor // authorButton.bottomAnchor
// .constraint(equalTo: buttonsContainer.bottomAnchor), // .constraint(equalTo: buttonsContainer.bottomAnchor),
// privacyButton.leadingAnchor // authorButton.leadingAnchor
// .constraint(equalTo: buttonsContainer.leadingAnchor), // .constraint(equalTo: buttonsContainer.leadingAnchor),
// // authorButton.trailingAnchor
// .constraint(equalTo: buttonsContainer.trailingAnchor),
privacyButton.topAnchor
.constraint(equalTo: buttonsContainer.topAnchor),
privacyButton.bottomAnchor
.constraint(equalTo: buttonsContainer.bottomAnchor),
privacyButton.leadingAnchor
.constraint(equalTo: buttonsContainer.leadingAnchor),
// documentationButton.firstBaselineAnchor // documentationButton.firstBaselineAnchor
// .constraint(equalTo: privacyButton.firstBaselineAnchor), // .constraint(equalTo: privacyButton.firstBaselineAnchor),
// documentationButton.leadingAnchor // documentationButton.leadingAnchor
// .constraint(equalTo: privacyButton.trailingAnchor, // .constraint(equalTo: privacyButton.trailingAnchor,
// constant: ViewConstants.spacing10), // constant: ViewConstants.spacing10),
//
// websiteButton.firstBaselineAnchor websiteButton.firstBaselineAnchor
// .constraint(equalTo: privacyButton.firstBaselineAnchor), .constraint(equalTo: privacyButton.firstBaselineAnchor),
// websiteButton.leadingAnchor websiteButton.leadingAnchor
// .constraint(equalTo: documentationButton.trailingAnchor, .constraint(equalTo: privacyButton.trailingAnchor,
// constant: ViewConstants.spacing10), constant: ViewConstants.spacing10),
// websiteButton.trailingAnchor websiteButton.trailingAnchor
// .constraint(equalTo: buttonsContainer.trailingAnchor), .constraint(equalTo: buttonsContainer.trailingAnchor),
]) ])
} }
@objc private func author() { // @objc private func author() {
NSWorkspace.shared.open(URL(string: AboutLinks.author)!) // NSWorkspace.shared.open(URL(string: AboutLinks.author)!)
// }
@objc private func privacy() {
NSWorkspace.shared.open(URL(string: AboutLinks.privacy)!)
} }
// @objc private func privacy() {
// NSWorkspace.shared.open(URL(string: AboutLinks.privacy)!)
// }
//
// @objc private func documentation() { // @objc private func documentation() {
// NSWorkspace.shared.open(URL(string: AboutLinks.documentation)!) // NSWorkspace.shared.open(URL(string: AboutLinks.documentation)!)
// } // }
//
// @objc private func website() { @objc private func website() {
// NSWorkspace.shared.open(URL(string: AboutLinks.website)!) NSWorkspace.shared.open(URL(string: AboutLinks.website)!)
// } }
} }

View File

@@ -18,6 +18,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
window.delegate = self window.delegate = self
// TODO: Move down.
// NOTE: Here we check wether the program was launched by the // NOTE: Here we check wether the program was launched by the
// system (e.g. launch-at-login). If it was not, then display the // system (e.g. launch-at-login). If it was not, then display the
// window. // window.

View File

@@ -11,7 +11,7 @@
<key>CFBundleIconName</key> <key>CFBundleIconName</key>
<string>AppIcon</string> <string>AppIcon</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.garikme.Grapp</string> <string>com.rednera.Grapp</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
@@ -19,13 +19,13 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.2</string> <string>0.2.0</string>
<key>CFBundleSupportedPlatforms</key> <key>CFBundleSupportedPlatforms</key>
<array> <array>
<string>MacOSX</string> <string>MacOSX</string>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.2</string> <string>0.2.0</string>
<key>DTPlatformName</key> <key>DTPlatformName</key>
<string>macosx</string> <string>macosx</string>
<key>DTPlatformVersion</key> <key>DTPlatformVersion</key>
@@ -40,5 +40,7 @@
<true/> <true/>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Rednera. All rights reserved.</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,3 +1,6 @@
APPLE_DEVELOPMENT := $(shell cat ../ids/APPLE_DEVELOPMENT)
APPLE_DEVELOPER_ID_APPLICATION := $(shell cat ../ids/APPLE_DEVELOPER_ID_APPLICATION)
FLAGS = -g FLAGS = -g
CFLAGS = -g CFLAGS = -g
#-O #-O
@@ -23,12 +26,6 @@ FRAMEWORKS = -framework AppKit -framework ServiceManagement
default: $(EXEC).app default: $(EXEC).app
# HACK: Target is getting touched because timestamps of the generated
# object file don't change unless there's an actual change in the
# outputted object code. This results in this target running every
# single time. I'm not sure whether that's the exact reason, but
# I can't imagine why timestamps wouldn't change. When clang
# generates same exact executable, timestamps do change.
./arm64/%.o: %.swift ./arm64/%.o: %.swift
swift -frontend -c \ swift -frontend -c \
-target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \ -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \

View File

@@ -1,5 +1,7 @@
import AppKit import AppKit
fileprivate let INDEX_DEEPNESS = 2
struct Program { struct Program {
var path: String = "" var path: String = ""
var name: String = "" var name: String = ""
@@ -19,9 +21,9 @@ final class PathManager {
"/Applications", "/Applications",
"/System/Applications", "/System/Applications",
"/System/Applications/Utilities", "/System/Applications/Utilities",
"/System/Library/CoreServices", "/System/Library/CoreServices", // TODO: NOTE: Remove this one? This one contains Finder and Siri.
"/Applications/Xcode.app/Contents/Applications", "/System/Library/CoreServices/Applications",
"/System/Library/CoreServices/Applications" "/Applications/Xcode.app/Contents/Applications"
] ]
private(set) var paths: [String: [Program]] = [:] private(set) var paths: [String: [Program]] = [:]
@@ -106,7 +108,7 @@ final class PathManager {
// allocations. // allocations.
public func rebuildIndex(at path: String) { public func rebuildIndex(at path: String) {
paths[path] = [] paths[path] = []
paths[path] = indexDirs(at: path, deepness: 2) paths[path] = indexDirs(at: path, deepness: INDEX_DEEPNESS)
} }
public func indexDirs(at path: String, deepness: Int) -> [Program] { public func indexDirs(at path: String, deepness: Int) -> [Program] {
@@ -134,7 +136,6 @@ final class PathManager {
} }
public func updateIndex() { public func updateIndex() {
print("updateIndex()")
for path in paths { for path in paths {
rebuildIndex(at: path.key) rebuildIndex(at: path.key)
} }

View File

@@ -22,6 +22,7 @@ class ProgramsTableViewCell: NSTableCellView {
public var indexLabel: NSTextField = { public var indexLabel: NSTextField = {
let field = NSTextField(labelWithString: "-") let field = NSTextField(labelWithString: "-")
field.isEditable = false
field.alignment = .center field.alignment = .center
// field.drawsBackground = true // field.drawsBackground = true
@@ -49,16 +50,18 @@ class ProgramsTableViewCell: NSTableCellView {
public var titleField: NSTextField = { public var titleField: NSTextField = {
let field = NSTextField() let field = NSTextField()
field.isEditable = false
field.isBordered = false field.isBordered = false
field.drawsBackground = false field.drawsBackground = false
field.lineBreakMode = .byTruncatingTail field.lineBreakMode = .byTruncatingTail
field.textColor = NSColor.secondaryLabelColor field.textColor = NSColor.textColor
field.translatesAutoresizingMaskIntoConstraints = false field.translatesAutoresizingMaskIntoConstraints = false
return field return field
}() }()
public var progPathLabel: NSTextField = { public var progPathLabel: NSTextField = {
let field = NSTextField() let field = NSTextField()
field.isEditable = false
field.isBordered = false field.isBordered = false
field.drawsBackground = false field.drawsBackground = false
field.lineBreakMode = .byTruncatingTail field.lineBreakMode = .byTruncatingTail

View File

@@ -61,6 +61,7 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
textField.lineBreakMode = .byTruncatingHead textField.lineBreakMode = .byTruncatingHead
textField.focusRingType = .none textField.focusRingType = .none
textField.placeholderString = "Program Search" textField.placeholderString = "Program Search"
textField.isAutomaticTextCompletionEnabled = false
textField.bezelStyle = .roundedBezel textField.bezelStyle = .roundedBezel
textField.font = NSFont textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor .systemFont(ofSize: NSFontDescriptor
@@ -346,7 +347,9 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
private func reloadProgramsTableViewData() { private func reloadProgramsTableViewData() {
if listIndex > 0 { if listIndex > 0 {
tableViewHeightAnchor?.constant = 210 // TODO: Why is this located here, randomly? Make it a global
// config variable.
tableViewHeightAnchor?.constant = 250
} else { } else {
tableViewHeightAnchor?.constant = 0 tableViewHeightAnchor?.constant = 0
} }
@@ -388,7 +391,7 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
if listIndex >= maxItems { break outerloop } if listIndex >= maxItems { break outerloop }
let prog = path.value[i] let prog = path.value[i]
if prog.name.lowercased() if (prog.name.lowercased() + prog.ext)
.contains(searchInput.stringValue.lowercased()) .contains(searchInput.stringValue.lowercased())
{ {
programsList[listIndex].path = prog.path programsList[listIndex].path = prog.path
@@ -477,9 +480,9 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
) )
let attributedString = NSMutableAttributedString(string: app) let attributedString = NSMutableAttributedString(string: app)
attributedString attributedString
.addAttributes([.foregroundColor: NSColor.labelColor], .addAttributes([.backgroundColor:
NSColor.systemYellow.withAlphaComponent(0.5)],
range: rangeToHighlight) range: rangeToHighlight)
cell.titleField.attributedStringValue = attributedString cell.titleField.attributedStringValue = attributedString
cell.progPathLabel.stringValue = program.path cell.progPathLabel.stringValue = program.path
cell.appIconImage.image = program.img cell.appIconImage.image = program.img

View File

@@ -5,9 +5,8 @@ import ServiceManagement
// TODO: Rework the paths table and selection. Right now, it is very // TODO: Rework the paths table and selection. Right now, it is very
// disfunctional and error-prone. // disfunctional and error-prone.
class SettingsViewController: NSViewController, class SettingsViewController: NSViewController,
NSTextFieldDelegate, KeyDetectorButtonDelegate, NSTextFieldDelegate, KeyDetectorButtonDelegate, NSTableViewDataSource,
NSTableViewDataSource, NSTableViewDelegate, NSTableViewDelegate, PathsTableCellViewDelegate
PathsTableCellViewDelegate
{ {
private var keyboardEvents: EventMonitor? private var keyboardEvents: EventMonitor?
@@ -15,6 +14,8 @@ class SettingsViewController: NSViewController,
// NOTE: This is the default shortcut. If you were to change it, don't // NOTE: This is the default shortcut. If you were to change it, don't
// forget to change other places in this file and delegate, too. // forget to change other places in this file and delegate, too.
// TODO: Come up with a way where we don't have to specify these in
// multiple locations.
private var keyCode = Int(kVK_Space) private var keyCode = Int(kVK_Space)
private var modifiers = Int(optionKey) private var modifiers = Int(optionKey)
@@ -356,7 +357,7 @@ class SettingsViewController: NSViewController,
let modifiers = event.modifierFlags.rawValue let modifiers = event.modifierFlags.rawValue
if modsContains(keys: OSCmd, in: modifiers) && if modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_ANSI_Q || modsContainsNone(in: modifiers) key == kVK_ANSI_Q
{ {
NSApplication.shared.terminate(self) NSApplication.shared.terminate(self)
} }
@@ -467,7 +468,7 @@ class SettingsViewController: NSViewController,
@objc @objc
private func reset() { private func reset() {
keyCode = Int(kVK_Space) keyCode = Int(kVK_Space) // TODO: Put into something like Defaults.swift file.
modifiers = Int(optionKey) modifiers = Int(optionKey)
HotKeyManager.shared.registerHotKey(key: keyCode, HotKeyManager.shared.registerHotKey(key: keyCode,
modifiers: modifiers) modifiers: modifiers)
@@ -553,6 +554,10 @@ class SettingsViewController: NSViewController,
) as? PathsTableCellView)?.startEditing() ) as? PathsTableCellView)?.startEditing()
break break
case 1: case 1:
(pathsTableView
.view(atColumn: 0, row: pathsTableView.selectedRow,
makeIfNecessary: false
) as? PathsTableCellView)?.stopEditing()
if pathsTableView.selectedRow > -1 { if pathsTableView.selectedRow > -1 {
paths.remove(at: pathsTableView.selectedRow) paths.remove(at: pathsTableView.selectedRow)
pathsTableView.reloadData() pathsTableView.reloadData()
@@ -623,37 +628,22 @@ class SettingsViewController: NSViewController,
func selectionButtonClicked(tag: Int) { func selectionButtonClicked(tag: Int) {
if dirPicker == nil { if dirPicker == nil {
dirPicker = NSOpenPanel() dirPicker = NSOpenPanel()
dirPicker!.message = "Select a directory to search applications in..." dirPicker!.message = "Select a directory with applications..."
dirPicker!.canChooseDirectories = true dirPicker!.canChooseDirectories = true
dirPicker!.canChooseFiles = false dirPicker!.canChooseFiles = false
dirPicker!.allowsMultipleSelection = false dirPicker!.allowsMultipleSelection = false
} }
// WARN:
// FIX: There is a bug where the program crashes when adding a new
// path. This happens because the settings popup is closed before
// displaying the selection modal, as a result the new path item
// is cleared (well, b/c it's empty) and the path gets set into
// non-existent memory which results in segmentation fault.
NSRunningApplication.current.activate(options: .activateAllWindows)
delegate.window.level = .normal
delegate.aboutWindow.performClose(nil)
if dirPicker!.runModal() == .OK { if dirPicker!.runModal() == .OK {
if let url = dirPicker!.url { if let url = dirPicker!.url {
print("tag=\(tag) url.path=\(url.path)")
(pathsTableView
.view(atColumn: 0, row: tag, makeIfNecessary: false
) as? PathsTableCellView)?.titleField.stringValue = url.path
paths[tag] = url.path paths[tag] = url.path
pathsTableView.reloadData() pathsTableView.reloadData()
} }
} }
delegate.window.level = .statusBar
delegate.window.makeKeyAndOrderFront(nil)
if let controller =
delegate.window.contentViewController as? SearchViewController
{
controller.openSettings()
}
} }
func numberOfRows(in tableView: NSTableView) -> Int { func numberOfRows(in tableView: NSTableView) -> Int {
@@ -663,9 +653,9 @@ class SettingsViewController: NSViewController,
func tableView(_ tableView: NSTableView, func tableView(_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
{ {
let rect = NSRect(x: 0, y: 0, width: tableColumn!.width, let rect = NSRect(x: 0, y: 0, width: tableColumn!.width, height: 20)
height: 20)
let cell = PathsTableCellView(frame: rect) let cell = PathsTableCellView(frame: rect)
cell.titleField.textColor = isDirectory(paths[row]) ? NSColor.labelColor : NSColor.systemRed
cell.titleField.stringValue = paths[row] cell.titleField.stringValue = paths[row]
cell.delegate = self cell.delegate = self
cell.id = row cell.id = row