├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------