├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Info.plist ├── LICENSE ├── Makefile ├── README.md ├── ROADMAP.md ├── SDAppDelegate.m ├── choose.el ├── choose.vim ├── choose.xcodeproj └── project.pbxproj ├── docs ├── Makefile ├── README.md └── choose.1.md └── fakedata.h /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | - '[0-9]+.[0-9]+.[0-9]+-**' 8 | 9 | jobs: 10 | macos: 11 | name: "Build release on MacOS" 12 | runs-on: macos-13 13 | if: startsWith(github.ref, 'refs/tags/') 14 | permissions: 15 | contents: write 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Build universal binary 19 | run: make release 20 | - name: Verify binary runs 21 | run: ./choose -v 22 | - name: Generate SHA256 checksum 23 | run: | 24 | shasum -a 256 choose > choose.sha256sum 25 | echo "SHA_CHECKSUM=$(cat choose.sha256sum)" >> $GITHUB_ENV 26 | - name: Determine git tag 27 | if: github.event_name == 'push' 28 | run: | 29 | TAG_NAME=${{ github.ref }} 30 | echo "TAG_VERSION=${TAG_NAME#refs/tags/}" >> $GITHUB_ENV 31 | - name: Get Changelog Entry 32 | id: changelog 33 | uses: mindsers/changelog-reader-action@v2 34 | with: 35 | version: ${{ env.TAG_VERSION }} 36 | path: "./CHANGELOG.md" 37 | - name: Publish 38 | uses: softprops/action-gh-release@v1 39 | with: 40 | name: choose ${{ env.TAG_VERSION }} 41 | fail_on_unmatched_files: true 42 | target_commitish: ${{ github.sha }} 43 | draft: false 44 | prerelease: ${{ steps.check-tag.outputs.match == 'true' }} 45 | files: | 46 | choose 47 | choose.sha256sum 48 | body: | 49 | ## Release Notes 50 | ${{ steps.changelog.outputs.changes }} 51 | ## SHA256 Checksum 52 | ``` 53 | ${{ env.SHA_CHECKSUM }} 54 | ``` 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | 20 | /choose 21 | /choose-x86_64 22 | /choose-arm64 23 | /choose*.zip 24 | /choose*.tgz 25 | 26 | docs/choose.1 27 | target/ 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.5.0] - 2025-02-08 11 | 12 | * Support running shell scripts when typing queries (#53) 13 | 14 | ## [1.4.1] - 2025-01-01 15 | 16 | * Fix published binary to reflect `1.4.x` instead of `1.3.x` 17 | 18 | ## [1.4.0] - 2024-11-23 19 | 20 | * Add usage example to README (#40) 21 | * Format examples for easier readability and copying 22 | * Support prepopulating query (#45) 23 | * Allow customizing input separator. This will allow for pasting multiline items (#48) 24 | * Support showing whitespace characters with placeholders (#48) 25 | * Allow empty input (#48) 26 | * Query field support for copy paste cut undo redo (#49) 27 | 28 | ## [1.3.1] - 2023-04-11 29 | 30 | * Bump `MACOSX_DEPLOYMENT_TARGET` from `10.10` to `10.13` as Xcode 14.3+ only 31 | supports `10.13` and higher 32 | 33 | ## [1.3.0] - 2023-04-10 34 | 35 | * Build color codes using snprintf for memory safety (#21) 36 | * Increment bufferSize to account for NUL terminating byte (#22) 37 | * Update README.md with related projects section (#24) 38 | * Allow wraparound on up/down arrow (#26) 39 | * Explicitly set xcodebuild configuration to Release (#29) 40 | * Installation command has no prompt sign (#33) 41 | * Adding prompt text to be displayed when query field is empty (#35) 42 | * Fuzzy search output also going to stdout (#36) 43 | 44 | ## [1.2.1] - 2020-09-24 45 | 46 | * Contains fix for illegal instruction 47 | 48 | ## [1.2.0] - 2020-08-14 49 | 50 | * [Internal] Update project configuration for upcoming version 1.2 and Catalina (fb6677f) 51 | * [Internal] Change default run configuration to debug (dc986e) 52 | * Add support for colors that work in light and dark modes (66e0f51) 53 | 54 | ## [1.1.0] - 2020-06-02 55 | 56 | * Added note about building and install docs 57 | 58 | ## [1.0.0] - 2015-03-18 59 | 60 | * This is the initial release 61 | 62 | [Unreleased]: https://github.com/chipsenkbeil/choose/compare/1.5.0...HEAD 63 | [1.5.0]: https://github.com/chipsenkbeil/choose/compare/1.4.1...1.5.0 64 | [1.4.1]: https://github.com/chipsenkbeil/choose/compare/1.4.0...1.4.1 65 | [1.4.0]: https://github.com/chipsenkbeil/choose/compare/1.3.1...1.4.0 66 | [1.3.1]: https://github.com/chipsenkbeil/choose/compare/1.3.0...1.3.1 67 | [1.3.0]: https://github.com/chipsenkbeil/choose/compare/1.2.1...1.3.0 68 | [1.2.1]: https://github.com/chipsenkbeil/choose/compare/1.2...1.2.1 69 | [1.2.0]: https://github.com/chipsenkbeil/choose/compare/1.1...1.2 70 | [1.1.0]: https://github.com/chipsenkbeil/choose/compare/1.0...1.1 71 | [1.0.0]: https://github.com/chipsenkbeil/choose/releases/tag/1.0 72 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleIdentifier 8 | org.senkbeil.choose 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleShortVersionString 12 | 1.5.0 13 | CFBundleVersion 14 | 1.5.0 15 | LSApplicationCategoryType 16 | public.app-category.productivity 17 | LSMinimumSystemVersion 18 | ${MACOSX_DEPLOYMENT_TARGET} 19 | NSHumanReadableCopyright 20 | Copyright © 2025 Chip Senkbeil. All rights reserved. 21 | NSPrincipalClass 22 | NSApplication 23 | LSUIElement 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original work Copyright (c) 2015 Steven Degutis 4 | Modified work Copyright (c) 2019 Chip Senkbeil 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs install-docs uninstall-docs help 2 | VERSION = $(shell defaults read `pwd`/Info CFBundleVersion) 3 | APPFILE = choose 4 | TGZFILE = choose-$(VERSION).tgz 5 | ZIPFILE = choose-$(VERSION).zip 6 | 7 | release: $(APPFILE) ## Build release files 8 | 9 | help: ## Display help information 10 | @printf 'usage: make [target] ...\n\ntargets:\n' 11 | @egrep '^(.+)\:\ .*##\ (.+)' ${MAKEFILE_LIST} | sed 's/:.*##/#/' | column -t -c 2 -s '#' 12 | 13 | package: $(TGZFILE) $(ZIPFILE) ## Build packages 14 | 15 | docs: ## Build documentation 16 | @$(MAKE) -C docs 17 | 18 | install-docs: ## Install documentation 19 | @$(MAKE) -C docs install 20 | 21 | uninstall-docs: ## Uninstall documentation 22 | @$(MAKE) -C docs uninstall 23 | 24 | clean: ## Remove generated files, packages, and documentation 25 | rm -rf $(APPFILE) $(APPFILE)-x86_64 $(APPFILE)-arm64 $(TGZFILE) $(ZIPFILE) 26 | @$(MAKE) -C docs clean 27 | 28 | ############################################################################### 29 | # INTERNAL 30 | ############################################################################### 31 | 32 | # Build a universal binary 33 | $(APPFILE): $(APPFILE)-x86_64 $(APPFILE)-arm64 34 | lipo -create -output $@ $^ 35 | 36 | # Explicitly build an x86_64 version of choose 37 | $(APPFILE)-x86_64: SDAppDelegate.m choose.xcodeproj 38 | rm -rf $@ 39 | xcodebuild \ 40 | -arch x86_64 \ 41 | -configuration Release \ 42 | clean build > /dev/null 43 | cp -R build/Release/choose $@ 44 | 45 | # Explicitly build an arm64 version of choose 46 | $(APPFILE)-arm64: SDAppDelegate.m choose.xcodeproj 47 | rm -rf $@ 48 | xcodebuild \ 49 | -arch arm64 \ 50 | -configuration Release \ 51 | clean build > /dev/null 52 | cp -R build/Release/choose $@ 53 | 54 | # Build a tar.gz containing the binary 55 | $(TGZFILE): $(APPFILE) 56 | tar -czf $@ $< 57 | 58 | # Build a zip containing the binary 59 | $(ZIPFILE): $(APPFILE) 60 | zip -qr $@ $< 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # choose 2 | 3 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/chipsenkbeil/choose) 4 | 5 | *Fuzzy matcher for OS X that uses both std{in,out} and a native GUI* 6 | 7 | --- 8 | 9 | - Gets list of items from stdin. 10 | - Fuzzy-searches as you type. 11 | - Sends result to stdout. 12 | - Run choose -h for more info. 13 | - [example vim integration](./choose.vim) 14 | - [example emacs integration](./choose.el) 15 | 16 | ![Animated Screenshot](/../Assets/screenshots/anim.gif?raw=true "Animated Screenshot") 17 | 18 | ## Install 19 | 20 | For the latest release, go to [the releases 21 | section](https://github.com/chipsenkbeil/choose/releases) and download the 22 | binary. 23 | 24 | ### Homebrew installation 25 | 26 | > Keep in mind that we do not maintain the homebrew formula here! So check the 27 | > version you have via `choose -v` and compare it to the latest version in [the 28 | > releases section](https://github.com/chipsenkbeil/choose/releases) . 29 | 30 | ```bash 31 | brew install choose-gui 32 | ``` 33 | 34 | ### Build and Install Documentation 35 | 36 | From root of repository, run: 37 | 38 | ```bash 39 | make docs 40 | make install-docs 41 | ``` 42 | 43 | You can then issue `man choose` to read the manual. 44 | 45 | Note that this requires `pandoc` to be installed on your system to build the 46 | manual page. 47 | 48 | ## Usage 49 | 50 | ### List the content from current directory 51 | 52 | ```bash 53 | ls | choose 54 | ``` 55 | 56 | ### Open apps from the applications directories 57 | 58 | ```bash 59 | ls /Applications/ /Applications/Utilities/ /System/Applications/ /System/Applications/Utilities/ | \ 60 | grep '\.app$' | \ 61 | sed 's/\.app$//g' | \ 62 | choose | \ 63 | xargs -I {} open -a "{}.app" 64 | ``` 65 | 66 | ### Query Passwords from password-store 67 | 68 | Assuming that your [passwordstore](https://www.passwordstore.org/) directory 69 | is `$HOME/.password-store`, the following will find a list of all the files 70 | containing passwords (using extension `.gpg`), strip off the pathing so you have 71 | something like `Personal/Coding/github.com`, present them as options using 72 | `choose`, and then if an option is selected this will run `pass show -c OPTION` 73 | to put the password into your clipboard. 74 | 75 | ```bash 76 | find $HOME/.password-store -type f -name '*.gpg' | \ 77 | sed "s|.*/\.password-store/||; s|\.gpg$||" | \ 78 | choose | \ 79 | xargs -r -I{} pass show -c {} 80 | ``` 81 | 82 | ### Use as a snippet manager 83 | 84 | Suppose you have some snippets in a text file and you want to quickly search and 85 | paste them with choose. Here is a command that you can bind to some shortcut 86 | with something like Karabiner: 87 | ```bash 88 | cat snippets_separated_with_two_newline_symbols.txt \ 89 | | choose -e -m -x \n\n - \ 90 | | pbcopy - \ 91 | && osascript -e 'tell application "System Events" to keystroke "v" using command down' 92 | ``` 93 | 94 | This will prompt choose, get its output, copy it to pasteboard, and trigger a 95 | paste shortcut `command+v`. 96 | 97 | For this to work in Karabiner, you need to give it access via 98 | 99 | ``` 100 | Privacy & Security -> Accessibility -> karabiner_console_user_server 101 | ``` 102 | 103 | Typically located at `/Library/ApplicationSupport/org.pqrs/Karabiner-Elements/bin/karabiner_console_user_server`, 104 | otherwise you will get `System Events got an error: osascript is not allowed to send keystrokes. (1002)` 105 | 106 | ## License 107 | 108 | See [MIT LICENSE](./LICENSE). 109 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | ## Version 2.0 2 | - [ ] Clean up code and/or port to something easier to work with (maybe something that can be platform-agnostic like ReasonML + [Revery](https://github.com/revery-ui/revery)/[Brisk](https://github.com/briskml/brisk)/[Cuite](https://github.com/let-def/cuite) 3 | 4 | ## Post Version 1.0 Cleanup 5 | - [x] Write more documentation and support manpage illustrating extra functionality 6 | - [ ] Move vim to proper plugin structure and/or check desire from community to support 7 | - [ ] Figure out the state of the emacs plugin (I don't use emacs) and/or check desire from community to support 8 | -------------------------------------------------------------------------------- /SDAppDelegate.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #define NSApp [NSApplication sharedApplication] 5 | 6 | /******************************************************************************/ 7 | /* User Options */ 8 | /******************************************************************************/ 9 | 10 | static NSColor* SDHighlightColor; 11 | static NSColor* SDHighlightBackgroundColor; 12 | static BOOL SDReturnsIndex; 13 | static NSFont* SDQueryFont; 14 | static NSString* PromptText; 15 | static NSString* InitialQuery; 16 | static NSString* Separator; 17 | static int SDNumRows; 18 | static int SDPercentWidth; 19 | static BOOL SDUnderlineDisabled; 20 | static BOOL SDReturnStringOnMismatch; 21 | static BOOL VisualizeWhitespaceCharacters; 22 | static BOOL AllowEmptyInput; 23 | static BOOL MatchFromBeginning; 24 | static BOOL ScoreFirstMatchedPosition; 25 | static BOOL Password; 26 | 27 | static NSString* LastQueryString; 28 | static int LastCursorPos; 29 | static NSString* ScriptAtInput; 30 | static NSString* ScriptAtList; 31 | 32 | /******************************************************************************/ 33 | /* Boilerplate Subclasses */ 34 | /******************************************************************************/ 35 | 36 | 37 | @interface NSApplication (ShutErrorsUp) 38 | @end 39 | @implementation NSApplication (ShutErrorsUp) 40 | - (void) setColorGridView:(id)view {} 41 | - (void) setView:(id)view {} 42 | @end 43 | 44 | 45 | @interface SDTableView : NSTableView 46 | @end 47 | @implementation SDTableView 48 | 49 | - (BOOL) acceptsFirstResponder { return NO; } 50 | - (BOOL) becomeFirstResponder { return NO; } 51 | - (BOOL) canBecomeKeyView { return NO; } 52 | 53 | @end 54 | 55 | 56 | @interface SDMainWindow : NSWindow 57 | @end 58 | @implementation SDMainWindow 59 | 60 | - (BOOL) canBecomeKeyWindow { return YES; } 61 | - (BOOL) canBecomeMainWindow { return YES; } 62 | 63 | @end 64 | 65 | /******************************************************************************/ 66 | /* Choice */ 67 | /******************************************************************************/ 68 | 69 | @interface SDChoice : NSObject 70 | 71 | @property NSString* normalized; 72 | @property NSString* raw; 73 | @property NSMutableIndexSet* indexSet; 74 | @property NSMutableAttributedString* displayString; 75 | 76 | @property BOOL hasAllCharacters; 77 | @property int score; 78 | 79 | @end 80 | 81 | @implementation SDChoice 82 | 83 | - (id) initWithString:(NSString*)str { 84 | if (self = [super init]) { 85 | self.raw = str; 86 | self.normalized = [self.raw lowercaseString]; 87 | self.indexSet = [NSMutableIndexSet indexSet]; 88 | 89 | NSString* displayStringRaw = self.raw; 90 | if (VisualizeWhitespaceCharacters) { 91 | displayStringRaw = [[self.raw stringByReplacingOccurrencesOfString:@"\n" withString:@"⏎"] stringByReplacingOccurrencesOfString:@"\t" withString:@"⇥"]; 92 | } 93 | self.displayString = [[NSMutableAttributedString alloc] initWithString:displayStringRaw attributes:nil]; 94 | } 95 | return self; 96 | } 97 | 98 | - (void) render { 99 | 100 | #ifdef DEBUG 101 | // for testing 102 | [self.displayString deleteCharactersInRange:NSMakeRange(0, [self.displayString length])]; 103 | [[self.displayString mutableString] appendString:self.raw]; 104 | [[self.displayString mutableString] appendFormat:@" [%d]", self.score]; 105 | #endif 106 | 107 | 108 | NSUInteger len = [self.normalized length]; 109 | NSRange fullRange = NSMakeRange(0, len); 110 | 111 | [self.displayString removeAttribute:NSForegroundColorAttributeName range:fullRange]; 112 | 113 | if (SDUnderlineDisabled) { 114 | [self.displayString removeAttribute:NSBackgroundColorAttributeName range:fullRange]; 115 | } 116 | else { 117 | [self.displayString removeAttribute:NSUnderlineColorAttributeName range:fullRange]; 118 | [self.displayString removeAttribute:NSUnderlineStyleAttributeName range:fullRange]; 119 | } 120 | 121 | [self.indexSet enumerateIndexesUsingBlock:^(NSUInteger i, BOOL *stop) { 122 | if (SDUnderlineDisabled) { 123 | [self.displayString addAttribute:NSBackgroundColorAttributeName value:[SDHighlightColor colorWithAlphaComponent:0.8] range:NSMakeRange(i, 1)]; 124 | } 125 | else { 126 | [self.displayString addAttribute:NSForegroundColorAttributeName value:SDHighlightColor range:NSMakeRange(i, 1)]; 127 | [self.displayString addAttribute:NSUnderlineColorAttributeName value:SDHighlightColor range:NSMakeRange(i, 1)]; 128 | [self.displayString addAttribute:NSUnderlineStyleAttributeName value:@1 range:NSMakeRange(i, 1)]; 129 | } 130 | }]; 131 | } 132 | 133 | - (void) analyze:(NSString*)query { 134 | 135 | // TODO: might not need this variable? 136 | self.hasAllCharacters = NO; 137 | 138 | [self.indexSet removeAllIndexes]; 139 | BOOL foundAll = YES; 140 | __block int firstOccurenceScore = 0; 141 | 142 | if (MatchFromBeginning) { 143 | NSUInteger firstPos = 0; 144 | for (NSInteger i = 0; i < [query length]; i++) { 145 | unichar qc = [query characterAtIndex: i]; 146 | BOOL found = NO; 147 | for (NSInteger i = firstPos; i <= [self.normalized length] - 1; i++) { 148 | unichar rc = [self.normalized characterAtIndex: i]; 149 | if (qc == rc) { 150 | if (firstPos == 0) { 151 | firstOccurenceScore = -i; 152 | } 153 | [self.indexSet addIndex: i]; 154 | firstPos = i+1; 155 | found = YES; 156 | break; 157 | } 158 | } 159 | if (!found) { 160 | foundAll = NO; 161 | break; 162 | } 163 | } 164 | } else { 165 | NSUInteger lastPos = [self.normalized length] - 1; 166 | 167 | for (NSInteger i = [query length] - 1; i >= 0; i--) { 168 | unichar qc = [query characterAtIndex: i]; 169 | BOOL found = NO; 170 | for (NSInteger i = lastPos; i >= 0; i--) { 171 | unichar rc = [self.normalized characterAtIndex: i]; 172 | if (qc == rc) { 173 | if (lastPos == [self.normalized length] - 1) { 174 | firstOccurenceScore = i - [self.normalized length] + 1; 175 | } 176 | [self.indexSet addIndex: i]; 177 | lastPos = i-1; 178 | found = YES; 179 | break; 180 | } 181 | } 182 | if (!found) { 183 | foundAll = NO; 184 | break; 185 | } 186 | } 187 | } 188 | 189 | if (!ScoreFirstMatchedPosition) { 190 | firstOccurenceScore = 0; 191 | } 192 | 193 | self.hasAllCharacters = foundAll; 194 | 195 | // skip the rest when it won't be used by the caller 196 | if (!self.hasAllCharacters) 197 | return; 198 | 199 | // update score 200 | 201 | self.score = 0; 202 | 203 | if ([self.indexSet count] == 0) 204 | return; 205 | 206 | __block int lengthScore = 0; 207 | __block int numRanges = 0; 208 | 209 | [self.indexSet enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) { 210 | numRanges++; 211 | lengthScore += (range.length * 100); 212 | }]; 213 | 214 | lengthScore /= numRanges; 215 | 216 | int percentScore = ((double)[self.indexSet count] / (double)[self.normalized length]) * 100.0; 217 | 218 | self.score = lengthScore + percentScore + firstOccurenceScore; 219 | } 220 | 221 | @end 222 | 223 | /******************************************************************************/ 224 | /* App Delegate */ 225 | /******************************************************************************/ 226 | 227 | @interface SDAppDelegate : NSObject 228 | 229 | // internal 230 | 231 | - (void)createMenu; 232 | @property NSWindow* window; 233 | @property NSArray* choices; 234 | @property NSMutableArray* filteredSortedChoices; 235 | @property SDTableView* listTableView; 236 | @property NSTextField* queryField; 237 | @property NSInteger choice; 238 | 239 | @property NSString* lastScriptOutputAtInput; 240 | 241 | @end 242 | 243 | @implementation SDAppDelegate 244 | 245 | /******************************************************************************/ 246 | /* Starting the app */ 247 | /******************************************************************************/ 248 | 249 | -(void)createMenu { 250 | /* create invisible menubar so that (copy paste cut undo redo) all work */ 251 | NSMenu *menubar = [[NSMenu alloc]init]; 252 | [NSApp setMainMenu:menubar]; 253 | 254 | NSMenuItem *menuBarItem = [[NSMenuItem alloc] init]; 255 | [menubar addItem:menuBarItem]; 256 | NSMenu *myMenu = [[NSMenu alloc]init]; 257 | 258 | // just FYI: some of those are prone to being renamed by the system 259 | // see https://github.com/tauri-apps/tauri/issues/7828#issuecomment-1723489849 260 | // and https://github.com/electron/electron/blob/706653d5e4d06922f75aa5621533a16fc34d3a77/shell/browser/ui/cocoa/electron_menu_controller.mm#L62 261 | NSMenuItem* copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; 262 | NSMenuItem* pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; 263 | NSMenuItem* cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]; 264 | NSMenuItem* undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; 265 | NSMenuItem* redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"z"]; 266 | [redoItem setKeyEquivalentModifierMask: NSShiftKeyMask | NSCommandKeyMask]; 267 | 268 | [myMenu addItem:copyItem]; 269 | [myMenu addItem:pasteItem]; 270 | [myMenu addItem:cutItem]; 271 | [myMenu addItem:undoItem]; 272 | [myMenu addItem:redoItem]; 273 | [menuBarItem setSubmenu:myMenu]; 274 | } 275 | 276 | - (void) applicationDidFinishLaunching:(NSNotification *)notification { 277 | [self createMenu]; 278 | NSArray* inputItems = [self getInputItems]; 279 | // NSLog(@"%ld", [inputItems count]); 280 | // NSLog(@"%@", inputItems); 281 | 282 | if ([inputItems count] < 1) 283 | [self cancel]; 284 | 285 | [NSApp activateIgnoringOtherApps: YES]; 286 | 287 | self.choices = [self choicesFromInputItems: inputItems]; 288 | 289 | NSRect winRect, textRect, dividerRect, listRect; 290 | [self getFrameForWindow: &winRect queryField: &textRect divider: ÷rRect tableView: &listRect]; 291 | 292 | [self setupWindow: winRect]; 293 | [self setupQueryField: textRect]; 294 | [self setupDivider: dividerRect]; 295 | [self setupResultsTable: listRect]; 296 | [self runQuery: self.queryField.stringValue]; 297 | [self resizeWindow]; 298 | [self.window center]; 299 | [self.window makeKeyAndOrderFront: nil]; 300 | 301 | // these even work inside NSAlert, so start them later 302 | [self setupKeyboardShortcuts]; 303 | } 304 | 305 | /******************************************************************************/ 306 | /* Setting up GUI elements */ 307 | /******************************************************************************/ 308 | 309 | - (void) setupWindow:(NSRect)winRect { 310 | BOOL usingYosemite = (NSClassFromString(@"NSVisualEffectView") != nil); 311 | 312 | NSUInteger styleMask = usingYosemite ? (NSFullSizeContentViewWindowMask | NSTitledWindowMask) : NSBorderlessWindowMask; 313 | self.window = [[SDMainWindow alloc] initWithContentRect: winRect 314 | styleMask: styleMask 315 | backing: NSBackingStoreBuffered 316 | defer: NO]; 317 | 318 | [self.window setDelegate: self]; 319 | 320 | if (usingYosemite) { 321 | self.window.titlebarAppearsTransparent = YES; 322 | NSVisualEffectView* blur = [[NSVisualEffectView alloc] initWithFrame: [[self.window contentView] bounds]]; 323 | [blur setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable ]; 324 | blur.material = NSVisualEffectMaterialMenu; 325 | blur.state = NSVisualEffectBlendingModeBehindWindow; 326 | [[self.window contentView] addSubview: blur]; 327 | } 328 | } 329 | 330 | - (void) setupQueryField:(NSRect)textRect { 331 | NSRect iconRect, space; 332 | NSDivideRect(textRect, &iconRect, &textRect, NSHeight(textRect) / 1.25, NSMinXEdge); 333 | NSDivideRect(textRect, &space, &textRect, 5.0, NSMinXEdge); 334 | 335 | CGFloat d = NSHeight(iconRect) * 0.10; 336 | iconRect = NSInsetRect(iconRect, d, d); 337 | 338 | NSImageView* icon = [[NSImageView alloc] initWithFrame: iconRect]; 339 | [icon setAutoresizingMask: NSViewMaxXMargin | NSViewMinYMargin ]; 340 | [icon setImage: [NSImage imageNamed: NSImageNameRightFacingTriangleTemplate]]; 341 | [icon setImageScaling: NSImageScaleProportionallyDown]; 342 | // [icon setImageFrameStyle: NSImageFrameButton]; 343 | [[self.window contentView] addSubview: icon]; 344 | 345 | self.queryField = Password ? [[NSSecureTextField alloc] initWithFrame: textRect] : [[NSTextField alloc] initWithFrame: textRect]; 346 | [self.queryField setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin ]; 347 | [self.queryField setDelegate: self]; 348 | [self.queryField setStringValue: InitialQuery]; 349 | [self.queryField setBezelStyle: NSTextFieldSquareBezel]; 350 | [self.queryField setBordered: NO]; 351 | [self.queryField setDrawsBackground: NO]; 352 | [self.queryField setFocusRingType: NSFocusRingTypeNone]; 353 | [self.queryField setFont: SDQueryFont]; 354 | [self.queryField setEditable: YES]; 355 | [self.queryField setPlaceholderString: PromptText]; 356 | [self.queryField setTarget: self]; 357 | [self.queryField setAction: @selector(choose:)]; 358 | [[self.queryField cell] setSendsActionOnEndEditing: NO]; 359 | [[self.window contentView] addSubview: self.queryField]; 360 | } 361 | 362 | - (void) getFrameForWindow:(NSRect*)winRect queryField:(NSRect*)textRect divider:(NSRect*)dividerRect tableView:(NSRect*)listRect { 363 | *winRect = NSMakeRect(0, 0, 100, 100); 364 | NSRect contentViewRect = NSInsetRect(*winRect, 10, 10); 365 | NSDivideRect(contentViewRect, textRect, listRect, NSHeight([SDQueryFont boundingRectForFont]), NSMaxYEdge); 366 | NSDivideRect(*listRect, dividerRect, listRect, 20.0, NSMaxYEdge); 367 | dividerRect->origin.y += NSHeight(*dividerRect) / 2.0; 368 | dividerRect->size.height = 1.0; 369 | } 370 | 371 | - (void) setupDivider:(NSRect)dividerRect { 372 | NSBox* border = [[NSBox alloc] initWithFrame: dividerRect]; 373 | [border setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin ]; 374 | [border setBoxType: NSBoxCustom]; 375 | [border setFillColor: [NSColor systemGrayColor]]; 376 | [border setBorderWidth: 0.0]; 377 | [[self.window contentView] addSubview: border]; 378 | } 379 | 380 | - (void) setupResultsTable:(NSRect)listRect { 381 | NSFont* rowFont = [NSFont fontWithName:[SDQueryFont fontName] size: [SDQueryFont pointSize] * 0.70]; 382 | 383 | NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:@"thing"]; 384 | [col setEditable: NO]; 385 | [col setWidth: 10000]; 386 | [[col dataCell] setFont: rowFont]; 387 | 388 | NSTextFieldCell* cell = [col dataCell]; 389 | [cell setLineBreakMode: NSLineBreakByCharWrapping]; 390 | 391 | self.listTableView = [[SDTableView alloc] init]; 392 | [self.listTableView setDataSource: self]; 393 | [self.listTableView setDelegate: self]; 394 | [self.listTableView setBackgroundColor: [NSColor clearColor]]; 395 | [self.listTableView setHeaderView: nil]; 396 | [self.listTableView setAllowsEmptySelection: NO]; 397 | [self.listTableView setAllowsMultipleSelection: NO]; 398 | [self.listTableView setAllowsTypeSelect: NO]; 399 | [self.listTableView setRowHeight: NSHeight([rowFont boundingRectForFont]) * 1.2]; 400 | [self.listTableView addTableColumn:col]; 401 | [self.listTableView setTarget: self]; 402 | [self.listTableView setDoubleAction: @selector(chooseByDoubleClicking:)]; 403 | [self.listTableView setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleNone]; 404 | 405 | NSScrollView* listScrollView = [[NSScrollView alloc] initWithFrame: listRect]; 406 | [listScrollView setVerticalScrollElasticity: NSScrollElasticityNone]; 407 | [listScrollView setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable ]; 408 | [listScrollView setDocumentView: self.listTableView]; 409 | [listScrollView setDrawsBackground: NO]; 410 | [[self.window contentView] addSubview: listScrollView]; 411 | } 412 | 413 | - (NSArray*) choicesFromInputItems:(NSArray*)inputItems { 414 | NSMutableArray* choices = [NSMutableArray array]; 415 | for (NSString* inputItem in inputItems) { 416 | if ([inputItem length] > 0) { 417 | [choices addObject: [[SDChoice alloc] initWithString: inputItem]]; 418 | } 419 | } 420 | return [choices copy]; 421 | } 422 | 423 | - (void) resizeWindow { 424 | NSRect screenFrame = [[NSScreen mainScreen] visibleFrame]; 425 | 426 | CGFloat rowHeight = [self.listTableView rowHeight]; 427 | CGFloat intercellHeight =[self.listTableView intercellSpacing].height; 428 | CGFloat allRowsHeight = (rowHeight + intercellHeight) * SDNumRows; 429 | 430 | CGFloat windowHeight = NSHeight([[self.window contentView] bounds]); 431 | CGFloat tableHeight = NSHeight([[self.listTableView superview] frame]); 432 | CGFloat finalHeight = (windowHeight - tableHeight) + allRowsHeight; 433 | 434 | CGFloat width; 435 | if (SDPercentWidth >= 0 && SDPercentWidth <= 100) { 436 | CGFloat percentWidth = (CGFloat)SDPercentWidth / 100.0; 437 | width = NSWidth(screenFrame) * percentWidth; 438 | } 439 | else { 440 | width = NSWidth(screenFrame) * 0.50; 441 | width = MIN(width, 800); 442 | width = MAX(width, 400); 443 | } 444 | 445 | NSRect winRect = NSMakeRect(0, 0, width, finalHeight); 446 | [self.window setFrame:winRect display:YES]; 447 | } 448 | 449 | - (void) setupKeyboardShortcuts { 450 | __weak id _self = self; 451 | [self addShortcut:@"1" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 0]; }]; 452 | [self addShortcut:@"2" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 1]; }]; 453 | [self addShortcut:@"3" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 2]; }]; 454 | [self addShortcut:@"4" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 3]; }]; 455 | [self addShortcut:@"5" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 4]; }]; 456 | [self addShortcut:@"6" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 5]; }]; 457 | [self addShortcut:@"7" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 6]; }]; 458 | [self addShortcut:@"8" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 7]; }]; 459 | [self addShortcut:@"9" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 8]; }]; 460 | [self addShortcut:@"q" mods:NSCommandKeyMask handler:^{ [_self cancel]; }]; 461 | [self addShortcut:@"a" mods:NSCommandKeyMask handler:^{ [_self selectAll: nil]; }]; 462 | [self addShortcut:@"c" mods:NSControlKeyMask handler:^{ [_self cancel]; }]; 463 | [self addShortcut:@"g" mods:NSControlKeyMask handler:^{ [_self cancel]; }]; 464 | } 465 | 466 | /******************************************************************************/ 467 | /* Table view */ 468 | /******************************************************************************/ 469 | 470 | - (void) reflectChoice { 471 | [self.listTableView selectRowIndexes:[NSIndexSet indexSetWithIndex: self.choice] byExtendingSelection:NO]; 472 | [self.listTableView scrollRowToVisible: self.choice]; 473 | } 474 | 475 | - (NSInteger) numberOfRowsInTableView:(NSTableView *)tableView { 476 | return [self.filteredSortedChoices count]; 477 | } 478 | 479 | - (id) tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { 480 | SDChoice* choice = [self.filteredSortedChoices objectAtIndex: row]; 481 | return choice.displayString; 482 | } 483 | 484 | - (void) tableViewSelectionDidChange:(NSNotification *)notification { 485 | self.choice = [self.listTableView selectedRow]; 486 | } 487 | 488 | - (void) tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { 489 | if ([[aTableView selectedRowIndexes] containsIndex:rowIndex]) 490 | [aCell setBackgroundColor: [SDHighlightBackgroundColor colorWithAlphaComponent: 0.5]]; 491 | else 492 | [aCell setBackgroundColor: [NSColor clearColor]]; 493 | 494 | [aCell setDrawsBackground:YES]; 495 | } 496 | 497 | - (void) runScriptAtList:(NSString*) query { 498 | if([ScriptAtList length] > 0){ 499 | NSArray *rows = [Script(ScriptAtList,query, @"list") componentsSeparatedByString:@"\n"]; 500 | int i; 501 | for (i=[rows count]-1; i>=0; i--){ 502 | if ([rows[i] length] > 0){ 503 | SDChoice* newChoice = [[SDChoice alloc] initWithString:rows[i]]; 504 | [self.filteredSortedChoices insertObject:newChoice atIndex:0]; 505 | } 506 | } 507 | } 508 | } 509 | 510 | - (void) runScriptAtInput:(NSString*) query { 511 | if([ScriptAtInput length] > 0){ 512 | self.lastScriptOutputAtInput = Script(ScriptAtInput,query, @"input"); 513 | 514 | if([[self.queryField stringValue] length] > [LastQueryString length]){ 515 | LastQueryString = [self.queryField stringValue]; 516 | NSString* queryWithOutput = [NSString stringWithFormat:@"%@%@", [self.queryField stringValue], self.lastScriptOutputAtInput]; 517 | [self.queryField setStringValue: queryWithOutput]; 518 | 519 | NSText* fieldEditor = [self.queryField currentEditor]; 520 | if([self.lastScriptOutputAtInput length] > 0){ 521 | [fieldEditor setSelectedRange: NSMakeRange([queryWithOutput length]-[self.lastScriptOutputAtInput length],[queryWithOutput length])]; 522 | } 523 | } else if ([[self.queryField stringValue] length] < [LastQueryString length]) { 524 | LastQueryString = [self.queryField stringValue]; 525 | } 526 | } 527 | } 528 | 529 | - (void) clearScriptOutputAtInput { 530 | NSRange range = [[[self.queryField window] fieldEditor:YES forObject:self.queryField] selectedRange]; 531 | if([self.lastScriptOutputAtInput length] > 0 && [[[self.queryField stringValue] substringWithRange:range] isEqualToString: self.lastScriptOutputAtInput]){ 532 | [[[self.queryField window] fieldEditor:YES forObject:self.queryField] setSelectedRange:NSMakeRange(LastCursorPos,0)]; 533 | [self.queryField setStringValue: [[self.queryField stringValue] substringWithRange:NSMakeRange(0,range.location)]]; 534 | self.lastScriptOutputAtInput = @""; 535 | } 536 | } 537 | 538 | /******************************************************************************/ 539 | /* Filtering! */ 540 | /******************************************************************************/ 541 | 542 | - (void) doQuery:(NSString*)query { 543 | query = [query lowercaseString]; 544 | 545 | self.filteredSortedChoices = [self.choices mutableCopy]; 546 | 547 | // analyze (cache) 548 | for (SDChoice* choice in self.filteredSortedChoices) 549 | [choice analyze: query]; 550 | 551 | if ([query length] >= 1) { 552 | 553 | // filter out non-matches 554 | for (SDChoice* choice in [self.filteredSortedChoices copy]) { 555 | if (!choice.hasAllCharacters) 556 | [self.filteredSortedChoices removeObject: choice]; 557 | } 558 | 559 | // sort remainder 560 | [self.filteredSortedChoices sortUsingComparator:^NSComparisonResult(SDChoice* a, SDChoice* b) { 561 | if (a.score > b.score) return NSOrderedAscending; 562 | if (a.score < b.score) return NSOrderedDescending; 563 | return NSOrderedSame; 564 | }]; 565 | 566 | } 567 | } 568 | 569 | 570 | - (void) runQuery:(NSString*)query { 571 | [self doQuery: query]; 572 | 573 | // render remainder 574 | for (SDChoice* choice in self.filteredSortedChoices) 575 | [choice render]; 576 | 577 | // running scripts on input, if available 578 | LastCursorPos = (int) [[[self.queryField window] fieldEditor:YES forObject:self.queryField] selectedRange].location; 579 | [self runScriptAtInput: query]; 580 | [self runScriptAtList: query]; 581 | 582 | // show! 583 | [self.listTableView reloadData]; 584 | 585 | // push choice back to start 586 | self.choice = 0; 587 | [self reflectChoice]; 588 | } 589 | 590 | /******************************************************************************/ 591 | /* Ending the app */ 592 | /******************************************************************************/ 593 | 594 | - (void) choose { 595 | if ([self.filteredSortedChoices count] == 0) { 596 | if (SDReturnStringOnMismatch) { 597 | [self writeOutput: [self.queryField stringValue]]; 598 | exit(0); 599 | } 600 | exit(1); 601 | } 602 | 603 | if (SDReturnsIndex) { 604 | SDChoice* choice = [self.filteredSortedChoices objectAtIndex: self.choice]; 605 | NSUInteger realIndex = [self.choices indexOfObject: choice]; 606 | [self writeOutput: [NSString stringWithFormat:@"%ld", realIndex]]; 607 | } 608 | else { 609 | SDChoice* choice = [self.filteredSortedChoices objectAtIndex: self.choice]; 610 | [self writeOutput: choice.raw]; 611 | } 612 | 613 | exit(0); 614 | } 615 | 616 | - (void) cancel { 617 | if (SDReturnsIndex) { 618 | [self writeOutput: [NSString stringWithFormat:@"%d", -1]]; 619 | } 620 | 621 | exit(1); 622 | } 623 | 624 | - (void) applicationDidResignActive:(NSNotification *)notification { 625 | [self cancel]; 626 | } 627 | 628 | - (void) pickIndex:(NSUInteger)idx { 629 | if (idx >= [self.filteredSortedChoices count]) 630 | return; 631 | 632 | self.choice = idx; 633 | [self choose]; 634 | } 635 | 636 | - (IBAction) choose:(id)sender { 637 | [self choose]; 638 | } 639 | 640 | - (IBAction) chooseByDoubleClicking:(id)sender { 641 | NSInteger row = [self.listTableView clickedRow]; 642 | if (row == -1) 643 | return; 644 | 645 | self.choice = row; 646 | [self choose]; 647 | } 648 | 649 | /******************************************************************************/ 650 | /* Search field callbacks */ 651 | /******************************************************************************/ 652 | 653 | - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { 654 | [self clearScriptOutputAtInput]; 655 | if (commandSelector == @selector(cancelOperation:)) { 656 | if ([[self.queryField stringValue] length] > 0) { 657 | [textView moveToBeginningOfDocument: nil]; 658 | [textView deleteToEndOfParagraph: nil]; 659 | } 660 | else { 661 | [self cancel]; 662 | } 663 | return YES; 664 | } 665 | else if (commandSelector == @selector(moveUp:)) { 666 | if (self.choice <= 0) { 667 | self.choice = [self.filteredSortedChoices count] - 1; 668 | } else { 669 | self.choice -= 1; 670 | } 671 | 672 | [self reflectChoice]; 673 | return YES; 674 | } 675 | else if (commandSelector == @selector(moveDown:)) { 676 | if (self.choice >= [self.filteredSortedChoices count] - 1) { 677 | self.choice = 0; 678 | } else { 679 | self.choice += 1; 680 | } 681 | 682 | [self reflectChoice]; 683 | return YES; 684 | } 685 | else if (commandSelector == @selector(insertTab:)) { 686 | [self.queryField setStringValue: [[self.filteredSortedChoices objectAtIndex: self.choice] raw]]; 687 | [[self.queryField currentEditor] setSelectedRange: NSMakeRange(self.queryField.stringValue.length, 0)]; 688 | return YES; 689 | } 690 | else if (commandSelector == @selector(deleteForward:)) { 691 | if ([[self.queryField stringValue] length] == 0) 692 | [self cancel]; 693 | } 694 | 695 | // NSLog(@"[%@]", NSStringFromSelector(commandSelector)); 696 | return NO; 697 | } 698 | 699 | - (void) controlTextDidChange:(NSNotification *)obj { 700 | [self clearScriptOutputAtInput]; 701 | [self runQuery: [self.queryField stringValue]]; 702 | } 703 | 704 | - (IBAction) selectAll:(id)sender { 705 | NSTextView* editor = (NSTextView*)[self.window fieldEditor:NO forObject:self.queryField]; 706 | [editor selectAll: sender]; 707 | } 708 | 709 | /******************************************************************************/ 710 | /* Helpers */ 711 | /******************************************************************************/ 712 | 713 | - (void) addShortcut:(NSString*)key mods:(NSEventModifierFlags)mods handler:(dispatch_block_t)action { 714 | static NSMutableArray* handlers; 715 | static dispatch_once_t onceToken; 716 | dispatch_once(&onceToken, ^{ 717 | handlers = [NSMutableArray array]; 718 | }); 719 | 720 | id x = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^ NSEvent*(NSEvent* event) { 721 | NSEventModifierFlags flags = ([event modifierFlags] & NSDeviceIndependentModifierFlagsMask); 722 | if (flags == mods && [[event charactersIgnoringModifiers] isEqualToString: key]) { 723 | action(); 724 | return nil; 725 | } 726 | return event; 727 | }]; 728 | [handlers addObject: x]; 729 | } 730 | 731 | - (void) writeOutput:(NSString*)str { 732 | NSFileHandle* stdoutHandle = [NSFileHandle fileHandleWithStandardOutput]; 733 | [stdoutHandle writeData: [str dataUsingEncoding:NSUTF8StringEncoding]]; 734 | } 735 | 736 | static NSColor* SDColorFromHex(NSString* hex) { 737 | NSScanner* scanner = [NSScanner scannerWithString: [hex uppercaseString]]; 738 | unsigned colorCode = 0; 739 | [scanner scanHexInt: &colorCode]; 740 | return [NSColor colorWithCalibratedRed:(CGFloat)(unsigned char)(colorCode >> 16) / 0xff 741 | green:(CGFloat)(unsigned char)(colorCode >> 8) / 0xff 742 | blue:(CGFloat)(unsigned char)(colorCode) / 0xff 743 | alpha: 1.0]; 744 | } 745 | 746 | static char* HexFromSDColor(NSColor* color) { 747 | size_t bufferSize = 7; 748 | char* buffer = (char*) malloc(bufferSize * sizeof(char)); 749 | NSColor* c = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; 750 | snprintf(buffer, bufferSize, "%2X%2X%2X", 751 | (unsigned int) ([c redComponent] * 255.99999), 752 | (unsigned int) ([c greenComponent] * 255.99999), 753 | (unsigned int) ([c blueComponent] * 255.99999)); 754 | return buffer; 755 | } 756 | 757 | static NSString* Script(NSString* pathToScript, NSString* queryInput, NSString* where) { 758 | int pid = [[NSProcessInfo processInfo] processIdentifier]; 759 | NSPipe *pipe = [NSPipe pipe]; 760 | NSPipe *pipeErr = [NSPipe pipe]; 761 | NSFileHandle *file = pipe.fileHandleForReading; 762 | 763 | NSTask *task = [[NSTask alloc] init]; 764 | task.launchPath = pathToScript; 765 | task.arguments = @[queryInput, where]; 766 | task.standardOutput = pipe; 767 | task.standardError = pipeErr; 768 | 769 | [task launch]; 770 | 771 | NSData *data = [file readDataToEndOfFile]; 772 | [file closeFile]; 773 | 774 | NSString *output = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; 775 | 776 | return output; 777 | } 778 | 779 | 780 | /******************************************************************************/ 781 | /* Getting input list */ 782 | /******************************************************************************/ 783 | 784 | - (NSArray*) getInputItems { 785 | 786 | #ifdef DEBUG 787 | 788 | #include "fakedata.h" 789 | 790 | #else 791 | 792 | NSFileHandle* stdinHandle = [NSFileHandle fileHandleWithStandardInput]; 793 | NSData* inputData = Password ? nil : [stdinHandle readDataToEndOfFile]; 794 | NSString* inputStrings = [[[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 795 | 796 | if ([inputStrings length] == 0 && !AllowEmptyInput) 797 | return nil; 798 | 799 | return [inputStrings componentsSeparatedByString: Separator]; 800 | 801 | #endif 802 | 803 | } 804 | 805 | @end 806 | 807 | /******************************************************************************/ 808 | /* Command line interface */ 809 | /******************************************************************************/ 810 | 811 | static NSString* SDAppVersionString(void) { 812 | return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; 813 | } 814 | 815 | static void SDShowVersion(const char* name) { 816 | printf("%s %s\n", name, [SDAppVersionString() UTF8String]); 817 | exit(0); 818 | } 819 | 820 | static void usage(const char* name) { 821 | printf("usage: %s\n", name); 822 | printf(" -i return index of selected element\n"); 823 | printf(" -v show choose version\n"); 824 | printf(" -n [10] set number of rows\n"); 825 | printf(" -w [50] set width of choose window\n"); 826 | printf(" -f [Menlo] set font used by choose\n"); 827 | printf(" -s [26] set font size used by choose\n"); 828 | printf(" -c [0000FF] highlight color for matched string\n"); 829 | printf(" -b [222222] background color of selected element\n"); 830 | printf(" -u disable underline and use background for matched string\n"); 831 | printf(" -m return the query string in case it doesn't match any item\n"); 832 | printf(" -p defines a prompt to be displayed when query field is empty\n"); 833 | printf(" -P conceals keyboard input / password mode (implies -m, -e and -n 0)\n"); 834 | printf(" -q defines initial query to start with (empty by default)\n"); 835 | printf(" -r path to a script to run when typing. Output appended to input field. Two args provided upon run:\n"); 836 | printf(" - the query text from input field\n"); 837 | printf(" - where output will be placed (\"input\" for -r or \"list\" for -t). \n"); 838 | printf(" -t same as -r, but outputs are in the form of extra list options (supports multiline outputs)\n"); 839 | printf(" -x defines separator string, a single newline (\\n) by default\n"); 840 | printf(" beware of escaping:\n"); 841 | printf(" passing -x \\n\\n will work\n"); 842 | printf(" passing -x '\\n\\n' will not work\n"); 843 | printf(" -y show newline and tab as symbols (⏎ ⇥)\n"); 844 | printf(" -e allow empty input (choose will show up even if there are no items to select)\n"); 845 | printf(" -o given a query, outputs results to standard output\n"); 846 | printf(" -z search matches symbols from beginning (instead of from end by weird default)\n"); 847 | printf(" -a rank early matches higher\n"); 848 | exit(0); 849 | } 850 | 851 | static void queryStdout(SDAppDelegate* delegate, const char* query) { 852 | delegate.choices = [delegate choicesFromInputItems: [delegate getInputItems]]; 853 | [delegate doQuery: [NSString stringWithUTF8String: query]]; 854 | 855 | for (SDChoice* choice in delegate.filteredSortedChoices) 856 | printf("%s\n", [choice.raw UTF8String]); 857 | 858 | exit(0); 859 | } 860 | 861 | int main(int argc, const char * argv[]) { 862 | @autoreleasepool { 863 | [NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory]; 864 | 865 | VisualizeWhitespaceCharacters = NO; 866 | AllowEmptyInput = NO; 867 | MatchFromBeginning = NO; 868 | ScoreFirstMatchedPosition = NO; 869 | SDReturnsIndex = NO; 870 | SDUnderlineDisabled = NO; 871 | const char* hexColor = HexFromSDColor(NSColor.systemBlueColor); 872 | const char* hexBackgroundColor = HexFromSDColor(NSColor.systemGrayColor); 873 | const char* queryFontName = "Menlo"; 874 | const char* queryPromptString = ""; 875 | InitialQuery = [NSString stringWithUTF8String: ""]; 876 | Separator = [NSString stringWithUTF8String: "\n"]; 877 | CGFloat queryFontSize = 26.0; 878 | SDNumRows = 10; 879 | SDReturnStringOnMismatch = NO; 880 | SDPercentWidth = -1; 881 | Password = NO; 882 | 883 | static SDAppDelegate* delegate; 884 | delegate = [[SDAppDelegate alloc] init]; 885 | [NSApp setDelegate: delegate]; 886 | 887 | int ch; 888 | while ((ch = getopt(argc, (char**)argv, "lvyezaf:s:r:c:b:n:w:p:q:r:t:x:o:Phium")) != -1) { 889 | switch (ch) { 890 | case 'i': SDReturnsIndex = YES; break; 891 | case 'f': queryFontName = optarg; break; 892 | case 'c': hexColor = optarg; break; 893 | case 'b': hexBackgroundColor = optarg; break; 894 | case 's': queryFontSize = atoi(optarg); break; 895 | case 'n': SDNumRows = atoi(optarg); break; 896 | case 'w': SDPercentWidth = atoi(optarg); break; 897 | case 'v': SDShowVersion(argv[0]); break; 898 | case 'u': SDUnderlineDisabled = YES; break; 899 | case 'm': SDReturnStringOnMismatch = YES; break; 900 | case 'p': queryPromptString = optarg; break; 901 | case 'P': Password = YES; AllowEmptyInput = YES; SDReturnStringOnMismatch = YES; SDNumRows = 0; break; 902 | case 'q': InitialQuery = [NSString stringWithUTF8String: optarg]; break; 903 | case 'r': ScriptAtInput = [NSString stringWithUTF8String: optarg]; break; 904 | case 't': ScriptAtList = [NSString stringWithUTF8String: optarg]; break; 905 | case 'x': Separator = [NSString stringWithUTF8String: optarg]; break; 906 | case 'y': VisualizeWhitespaceCharacters = YES; break; 907 | case 'e': AllowEmptyInput = YES; break; 908 | case 'z': MatchFromBeginning = YES; break; 909 | case 'a': ScoreFirstMatchedPosition = YES; break; 910 | case 'o': queryStdout(delegate, optarg); break; 911 | case '?': 912 | case 'h': 913 | default: 914 | usage(argv[0]); 915 | } 916 | } 917 | argc -= optind; 918 | argv += optind; 919 | 920 | SDQueryFont = [NSFont fontWithName:[NSString stringWithUTF8String: queryFontName] size:queryFontSize]; 921 | SDHighlightColor = SDColorFromHex([NSString stringWithUTF8String: hexColor]); 922 | SDHighlightBackgroundColor = SDColorFromHex([NSString stringWithUTF8String: hexBackgroundColor]); 923 | PromptText = [NSString stringWithUTF8String: queryPromptString]; 924 | 925 | if ([ScriptAtInput length] > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:ScriptAtInput]){ 926 | printf("No such file or directory for the script at input: %s\n", [ScriptAtInput UTF8String]); 927 | exit(1); 928 | } 929 | if ([ScriptAtList length] > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:ScriptAtList]){ 930 | printf("No such file or directory for the script at list: %s\n", [ScriptAtList UTF8String]); 931 | exit(1); 932 | } 933 | 934 | NSApplicationMain(argc, argv); 935 | } 936 | return 0; 937 | } 938 | -------------------------------------------------------------------------------- /choose.el: -------------------------------------------------------------------------------- 1 | ;; better find-file-in-repository 2 | ;; assumes you have magit and maybe other stuff 3 | (defun choose/find-file-in-git-repo () 4 | (interactive) 5 | (require 's) 6 | (let ((root-dir (magit-toplevel default-directory))) 7 | (if root-dir 8 | (let ((default-directory root-dir)) 9 | (let ((f (s-trim 10 | (shell-command-to-string 11 | "git ls-files -co --exclude-standard | choose")))) 12 | (unless (string= "" f) 13 | (find-file f)))) 14 | (call-interactively 'find-file)))) 15 | (global-set-key (kbd "C-x f") 'choose/find-file-in-git-repo) 16 | -------------------------------------------------------------------------------- /choose.vim: -------------------------------------------------------------------------------- 1 | " find file in git repo 2 | function! ChooseFile() 3 | let dir = expand("%:h") 4 | if empty(dir) | let dir = getcwd() | endif 5 | 6 | let root = system("cd " . dir . " && git rev-parse --show-toplevel") 7 | if v:shell_error != 0 | echo "Not in a git repo" | return | endif 8 | let root = root[0:-2] 9 | 10 | let selection = system("cd " . root . " && git ls-files -co --exclude-standard | choose") 11 | if empty(selection) | echo "Canceled" | return | end 12 | 13 | echo "Finding file..." 14 | exec ":e " . root . "/" . selection 15 | endfunction 16 | 17 | " shortcut 18 | nnoremap f :call ChooseFile() 19 | -------------------------------------------------------------------------------- /choose.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 94DDE04E19F2B78F001A3B2E /* SDAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 94DDE04D19F2B78F001A3B2E /* SDAppDelegate.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 94DDE03C19F2AC4C001A3B2E /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = /usr/share/man/man1/; 18 | dstSubfolderSpec = 0; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 1; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 9460AB981AB9ECF000DC9A3A /* fakedata.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fakedata.h; sourceTree = ""; }; 27 | 94DDE03E19F2AC4C001A3B2E /* choose */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = choose; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 94DDE04819F2AC61001A3B2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 94DDE04D19F2B78F001A3B2E /* SDAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDAppDelegate.m; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 94DDE03B19F2AC4C001A3B2E /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 9438161C19F4007B0094D4E6 /* choose */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | 94DDE04D19F2B78F001A3B2E /* SDAppDelegate.m */, 47 | 9460AB981AB9ECF000DC9A3A /* fakedata.h */, 48 | 94DDE04819F2AC61001A3B2E /* Info.plist */, 49 | ); 50 | name = choose; 51 | sourceTree = ""; 52 | }; 53 | 94DDE03519F2AC4C001A3B2E = { 54 | isa = PBXGroup; 55 | children = ( 56 | 9438161C19F4007B0094D4E6 /* choose */, 57 | 94DDE03F19F2AC4C001A3B2E /* Products */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | 94DDE03F19F2AC4C001A3B2E /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 94DDE03E19F2AC4C001A3B2E /* choose */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | /* End PBXGroup section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | 94DDE03D19F2AC4C001A3B2E /* choose */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = 94DDE04519F2AC4C001A3B2E /* Build configuration list for PBXNativeTarget "choose" */; 75 | buildPhases = ( 76 | 94DDE03A19F2AC4C001A3B2E /* Sources */, 77 | 94DDE03B19F2AC4C001A3B2E /* Frameworks */, 78 | 94DDE03C19F2AC4C001A3B2E /* CopyFiles */, 79 | ); 80 | buildRules = ( 81 | ); 82 | dependencies = ( 83 | ); 84 | name = choose; 85 | productName = choose; 86 | productReference = 94DDE03E19F2AC4C001A3B2E /* choose */; 87 | productType = "com.apple.product-type.tool"; 88 | }; 89 | /* End PBXNativeTarget section */ 90 | 91 | /* Begin PBXProject section */ 92 | 94DDE03619F2AC4C001A3B2E /* Project object */ = { 93 | isa = PBXProject; 94 | attributes = { 95 | LastUpgradeCheck = 1150; 96 | ORGANIZATIONNAME = "Tiny Robot Software"; 97 | TargetAttributes = { 98 | 94DDE03D19F2AC4C001A3B2E = { 99 | CreatedOnToolsVersion = 6.0.1; 100 | }; 101 | }; 102 | }; 103 | buildConfigurationList = 94DDE03919F2AC4C001A3B2E /* Build configuration list for PBXProject "choose" */; 104 | compatibilityVersion = "Xcode 3.2"; 105 | developmentRegion = en; 106 | hasScannedForEncodings = 0; 107 | knownRegions = ( 108 | en, 109 | Base, 110 | ); 111 | mainGroup = 94DDE03519F2AC4C001A3B2E; 112 | productRefGroup = 94DDE03F19F2AC4C001A3B2E /* Products */; 113 | projectDirPath = ""; 114 | projectRoot = ""; 115 | targets = ( 116 | 94DDE03D19F2AC4C001A3B2E /* choose */, 117 | ); 118 | }; 119 | /* End PBXProject section */ 120 | 121 | /* Begin PBXSourcesBuildPhase section */ 122 | 94DDE03A19F2AC4C001A3B2E /* Sources */ = { 123 | isa = PBXSourcesBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | 94DDE04E19F2B78F001A3B2E /* SDAppDelegate.m in Sources */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | /* End PBXSourcesBuildPhase section */ 131 | 132 | /* Begin XCBuildConfiguration section */ 133 | 94DDE04319F2AC4C001A3B2E /* Debug */ = { 134 | isa = XCBuildConfiguration; 135 | buildSettings = { 136 | ALWAYS_SEARCH_USER_PATHS = NO; 137 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 138 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 139 | CLANG_CXX_LIBRARY = "libc++"; 140 | CLANG_ENABLE_MODULES = YES; 141 | CLANG_ENABLE_OBJC_ARC = YES; 142 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 143 | CLANG_WARN_BOOL_CONVERSION = YES; 144 | CLANG_WARN_COMMA = YES; 145 | CLANG_WARN_CONSTANT_CONVERSION = YES; 146 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 147 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 148 | CLANG_WARN_EMPTY_BODY = YES; 149 | CLANG_WARN_ENUM_CONVERSION = YES; 150 | CLANG_WARN_INFINITE_RECURSION = YES; 151 | CLANG_WARN_INT_CONVERSION = YES; 152 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 153 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 154 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 155 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 156 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 157 | CLANG_WARN_STRICT_PROTOTYPES = YES; 158 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | COPY_PHASE_STRIP = NO; 162 | ENABLE_STRICT_OBJC_MSGSEND = YES; 163 | ENABLE_TESTABILITY = YES; 164 | GCC_C_LANGUAGE_STANDARD = gnu99; 165 | GCC_DYNAMIC_NO_PIC = NO; 166 | GCC_NO_COMMON_BLOCKS = YES; 167 | GCC_OPTIMIZATION_LEVEL = 0; 168 | GCC_PREPROCESSOR_DEFINITIONS = ( 169 | "DEBUG=1", 170 | "$(inherited)", 171 | ); 172 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 173 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 174 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 175 | GCC_WARN_UNDECLARED_SELECTOR = YES; 176 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 177 | GCC_WARN_UNUSED_FUNCTION = YES; 178 | GCC_WARN_UNUSED_VARIABLE = YES; 179 | MACOSX_DEPLOYMENT_TARGET = 10.13; 180 | MTL_ENABLE_DEBUG_INFO = YES; 181 | ONLY_ACTIVE_ARCH = YES; 182 | SDKROOT = macosx; 183 | }; 184 | name = Debug; 185 | }; 186 | 94DDE04419F2AC4C001A3B2E /* Release */ = { 187 | isa = XCBuildConfiguration; 188 | buildSettings = { 189 | ALWAYS_SEARCH_USER_PATHS = NO; 190 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 191 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 192 | CLANG_CXX_LIBRARY = "libc++"; 193 | CLANG_ENABLE_MODULES = YES; 194 | CLANG_ENABLE_OBJC_ARC = YES; 195 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 196 | CLANG_WARN_BOOL_CONVERSION = YES; 197 | CLANG_WARN_COMMA = YES; 198 | CLANG_WARN_CONSTANT_CONVERSION = YES; 199 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 200 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 201 | CLANG_WARN_EMPTY_BODY = YES; 202 | CLANG_WARN_ENUM_CONVERSION = YES; 203 | CLANG_WARN_INFINITE_RECURSION = YES; 204 | CLANG_WARN_INT_CONVERSION = YES; 205 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 206 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 207 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 208 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 209 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 210 | CLANG_WARN_STRICT_PROTOTYPES = YES; 211 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 212 | CLANG_WARN_UNREACHABLE_CODE = YES; 213 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 214 | COPY_PHASE_STRIP = YES; 215 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 216 | ENABLE_NS_ASSERTIONS = NO; 217 | ENABLE_STRICT_OBJC_MSGSEND = YES; 218 | GCC_C_LANGUAGE_STANDARD = gnu99; 219 | GCC_NO_COMMON_BLOCKS = YES; 220 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 221 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 222 | GCC_WARN_UNDECLARED_SELECTOR = YES; 223 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 224 | GCC_WARN_UNUSED_FUNCTION = YES; 225 | GCC_WARN_UNUSED_VARIABLE = YES; 226 | MACOSX_DEPLOYMENT_TARGET = 10.13; 227 | MTL_ENABLE_DEBUG_INFO = NO; 228 | SDKROOT = macosx; 229 | }; 230 | name = Release; 231 | }; 232 | 94DDE04619F2AC4C001A3B2E /* Debug */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | CODE_SIGN_IDENTITY = "-"; 236 | OTHER_LDFLAGS = ( 237 | "-sectcreate", 238 | __TEXT, 239 | __info_plist, 240 | "$(SRCROOT)/Info.plist", 241 | ); 242 | PRODUCT_NAME = choose; 243 | }; 244 | name = Debug; 245 | }; 246 | 94DDE04719F2AC4C001A3B2E /* Release */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | CODE_SIGN_IDENTITY = "-"; 250 | OTHER_LDFLAGS = ( 251 | "-sectcreate", 252 | __TEXT, 253 | __info_plist, 254 | "$(SRCROOT)/Info.plist", 255 | ); 256 | PRODUCT_NAME = choose; 257 | }; 258 | name = Release; 259 | }; 260 | /* End XCBuildConfiguration section */ 261 | 262 | /* Begin XCConfigurationList section */ 263 | 94DDE03919F2AC4C001A3B2E /* Build configuration list for PBXProject "choose" */ = { 264 | isa = XCConfigurationList; 265 | buildConfigurations = ( 266 | 94DDE04319F2AC4C001A3B2E /* Debug */, 267 | 94DDE04419F2AC4C001A3B2E /* Release */, 268 | ); 269 | defaultConfigurationIsVisible = 0; 270 | defaultConfigurationName = Debug; 271 | }; 272 | 94DDE04519F2AC4C001A3B2E /* Build configuration list for PBXNativeTarget "choose" */ = { 273 | isa = XCConfigurationList; 274 | buildConfigurations = ( 275 | 94DDE04619F2AC4C001A3B2E /* Debug */, 276 | 94DDE04719F2AC4C001A3B2E /* Release */, 277 | ); 278 | defaultConfigurationIsVisible = 0; 279 | defaultConfigurationName = Debug; 280 | }; 281 | /* End XCConfigurationList section */ 282 | }; 283 | rootObject = 94DDE03619F2AC4C001A3B2E /* Project object */; 284 | } 285 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Prefix to installing manual pages 2 | BASEDIR ?= /usr/local/share/man 3 | 4 | # Manual pages to build (and pseudo targets to install/uninstall) 5 | MANPAGES = choose.1 6 | INSTALL_TARGETS = $(foreach page,$(MANPAGES),$(page).install) 7 | UNINSTALL_TARGETS = $(foreach page,$(MANPAGES),$(page).uninstall) 8 | 9 | # Specify targets that aren't actually physical files to create 10 | .PHONY: clean install uninstall $(INSTALL_TARGETS) $(UNINSTALL_TARGETS) 11 | 12 | # Function to use to convert choose.1 into /man1 13 | F_OUTDIR = $(BASEDIR)/man$(subst .,,$(suffix $1)) 14 | 15 | # [DEFAULT] Build all manpages 16 | all: $(MANPAGES) 17 | 18 | # Copy all manpages to $BASEDIR 19 | install: $(INSTALL_TARGETS) 20 | 21 | # Remove all manpages from $BASEDIR 22 | uninstall: $(UNINSTALL_TARGETS) 23 | 24 | # Copy specific manpage denoted by .install to $BASEDIR 25 | $(INSTALL_TARGETS):%:$(MANPAGES) 26 | $(info Installing $(basename $@)) 27 | @mkdir -p $(call F_OUTDIR,$(basename $@)) 28 | @cp $(basename $@) $(call F_OUTDIR,$(basename $@))/$(basename $@) 29 | 30 | # Remove specific manpage denoted by .install from $BASEDIR 31 | $(UNINSTALL_TARGETS):%: 32 | $(info Uninstalling $(basename $@)) 33 | @rm -f $(call F_OUTDIR,$(basename $@))/$(basename $@) 34 | 35 | # Build specific manpage from .md to 36 | $(MANPAGES):%:%.md 37 | $(info Building $@ from $<) 38 | @pandoc -s -f markdown -t man $< | dos2unix > $@ 39 | 40 | # Remove all generated manpages 41 | clean: 42 | @rm -f $(MANPAGES) 43 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # choose Docs 2 | 3 | Contains documentation for the **choose** program including man pages. 4 | 5 | ## Building man pages 6 | 7 | In order to build man pages for **choose**, the following programs must be 8 | available and in your `PATH`: 9 | 10 | - [GNU Make](https://www.gnu.org/software/make/) (tested using version 3.81+) 11 | - [Pandoc](https://pandoc.org/) (tested using version 2.5+) 12 | 13 | Run `make` from the `docs/` directory to build the man pages and `make 14 | install` to copy the man pages to appropriate locations. 15 | 16 | Issuing `make uninstall` will remove man pages from appropriate locations and 17 | `make clean` will delete the locally-built man pages. 18 | 19 | Additionally, from the root of the project, you can build the man pages using 20 | `make docs`, install via `make install-docs`, and uninstall via `make 21 | uninstall-docs`. 22 | 23 | ## Installation configuration 24 | 25 | For package managers and personal customization, you can override the location 26 | in which man pages will be installed by explicitly setting the environment 27 | variable `BASEDIR`, which is set to `/usr/local/share/man` by default. 28 | -------------------------------------------------------------------------------- /docs/choose.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'choose' 3 | section: 1 4 | header: 'General Commands Manual' 5 | footer: 'MIT' 6 | date: 'April 27, 2019' 7 | --- 8 | 9 | # NAME 10 | 11 | **choose** - Fuzzy matcher for OS X that uses both std{in,out} and a native GUI 12 | 13 | # SYNOPSIS 14 | 15 | **choose** \[*options*\] 16 | 17 | # DESCRIPTION 18 | 19 | **choose** is a small, graphical program that gets a list of items from 20 | stdin, displays the list as a series of choices with the ability to fuzzily 21 | search, and prints out the selected item after being selected. 22 | 23 | ## OPTIONS 24 | 25 | -i 26 | 27 | : Return index of selected element 28 | 29 | -v 30 | 31 | : Print version of choose 32 | 33 | -n *rows* 34 | 35 | : Set number of rows (default: 10) 36 | 37 | -w *width* 38 | 39 | : Set width of window (default: 50) 40 | 41 | -f *font* 42 | 43 | : Set font used by window (default: Menlo) 44 | 45 | -s *size* 46 | 47 | : Set font size used by window (default: 26) 48 | 49 | -c *color* 50 | 51 | : Set highlight color for matched string (default: 0000FF) 52 | 53 | -b *color* 54 | 55 | : Set background color of selected elements (default: 222222) 56 | 57 | -u 58 | 59 | : Disable underline and use background for matched string 60 | 61 | -m 62 | 63 | : Return the query string in case it doesn't match any item 64 | 65 | # EXAMPLES 66 | 67 | The following displays **choose** for the current directory's contents 68 | 69 | ls | choose 70 | 71 | # BUGS 72 | 73 | None known so far. 74 | 75 | # AUTHOR 76 | 77 | | **choose** was started by Steven Degutis in 2015. 78 | | Ownership was transferred in 2019 to Chip Senkbeil. 79 | 80 | # COPYRIGHT 81 | 82 | choose is provided under the MIT license. 83 | 84 | | Original work Copyright (c) 2015 Steven Degutis 85 | | Modified work Copyright (c) 2019 Chip Senkbeil 86 | 87 | Permission is hereby granted, free of charge, to any person obtaining a copy 88 | of this software and associated documentation files (the "Software"), to deal 89 | in the Software without restriction, including without limitation the rights 90 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 91 | copies of the Software, and to permit persons to whom the Software is 92 | furnished to do so, subject to the following conditions: 93 | 94 | The above copyright notice and this permission notice shall be included in all 95 | copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 98 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 99 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 100 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 101 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 102 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 103 | SOFTWARE. 104 | -------------------------------------------------------------------------------- /fakedata.h: -------------------------------------------------------------------------------- 1 | return 2 | @[ 3 | @"foo", 4 | @"bar", 5 | @"todo: make this list a lot longer for better testing", 6 | ]; 7 | --------------------------------------------------------------------------------