├── fakedata.h ├── .gitignore ├── ROADMAP.md ├── choose.vim ├── choose.el ├── Info.plist ├── docs ├── README.md ├── Makefile └── choose.1.md ├── LICENSE ├── .github └── workflows │ └── release.yml ├── Makefile ├── CHANGELOG.md ├── README.md ├── choose.xcodeproj └── project.pbxproj └── SDAppDelegate.m /fakedata.h: -------------------------------------------------------------------------------- 1 | return 2 | @[ 3 | @"foo", 4 | @"bar", 5 | @"todo: make this list a lot longer for better testing", 6 | ]; 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 AutoSelectSingleChoice; 26 | static BOOL MatchWords; 27 | static BOOL SortMatches; 28 | static BOOL Password; 29 | 30 | static NSString* LastQueryString; 31 | static int LastCursorPos; 32 | static NSString* ScriptAtInput; 33 | static NSString* ScriptAtList; 34 | 35 | typedef NS_ENUM(NSInteger, CaseSpecification) { 36 | SENSITIVE, 37 | INSENSITIVE, 38 | SMART 39 | }; 40 | 41 | static CaseSpecification SearchCase; 42 | 43 | /******************************************************************************/ 44 | /* Boilerplate Subclasses */ 45 | /******************************************************************************/ 46 | 47 | 48 | @interface NSApplication (ShutErrorsUp) 49 | @end 50 | @implementation NSApplication (ShutErrorsUp) 51 | - (void) setColorGridView:(id)view {} 52 | - (void) setView:(id)view {} 53 | @end 54 | 55 | 56 | @interface SDTableView : NSTableView 57 | @end 58 | @implementation SDTableView 59 | 60 | - (BOOL) acceptsFirstResponder { return NO; } 61 | - (BOOL) becomeFirstResponder { return NO; } 62 | - (BOOL) canBecomeKeyView { return NO; } 63 | 64 | @end 65 | 66 | 67 | @interface SDMainWindow : NSWindow 68 | @end 69 | @implementation SDMainWindow 70 | 71 | - (BOOL) canBecomeKeyWindow { return YES; } 72 | - (BOOL) canBecomeMainWindow { return YES; } 73 | 74 | @end 75 | 76 | /******************************************************************************/ 77 | /* Choice */ 78 | /******************************************************************************/ 79 | 80 | @interface SDChoice : NSObject 81 | 82 | @property NSString* raw; 83 | @property NSMutableIndexSet* indexSet; 84 | @property NSMutableAttributedString* displayString; 85 | 86 | @property BOOL isMatchForQuery; 87 | @property int score; 88 | 89 | @end 90 | 91 | @implementation SDChoice 92 | 93 | - (id) initWithString:(NSString*)str { 94 | if (self = [super init]) { 95 | self.raw = str; 96 | self.indexSet = [NSMutableIndexSet indexSet]; 97 | 98 | NSString* displayStringRaw = self.raw; 99 | if (VisualizeWhitespaceCharacters) { 100 | displayStringRaw = [[self.raw stringByReplacingOccurrencesOfString:@"\n" withString:@"⏎"] stringByReplacingOccurrencesOfString:@"\t" withString:@"⇥"]; 101 | } 102 | self.displayString = [[NSMutableAttributedString alloc] initWithString:displayStringRaw attributes:nil]; 103 | } 104 | return self; 105 | } 106 | 107 | - (void) render { 108 | 109 | #ifdef DEBUG 110 | // for testing 111 | [self.displayString deleteCharactersInRange:NSMakeRange(0, [self.displayString length])]; 112 | [[self.displayString mutableString] appendString:self.raw]; 113 | [[self.displayString mutableString] appendFormat:@" [%d]", self.score]; 114 | #endif 115 | 116 | 117 | NSUInteger len = [self.raw length]; 118 | NSRange fullRange = NSMakeRange(0, len); 119 | 120 | [self.displayString removeAttribute:NSForegroundColorAttributeName range:fullRange]; 121 | 122 | if (SDUnderlineDisabled) { 123 | [self.displayString removeAttribute:NSBackgroundColorAttributeName range:fullRange]; 124 | } 125 | else { 126 | [self.displayString removeAttribute:NSUnderlineColorAttributeName range:fullRange]; 127 | [self.displayString removeAttribute:NSUnderlineStyleAttributeName range:fullRange]; 128 | } 129 | 130 | [self.indexSet enumerateIndexesUsingBlock:^(NSUInteger i, BOOL *stop) { 131 | if (SDUnderlineDisabled) { 132 | [self.displayString addAttribute:NSBackgroundColorAttributeName value:[SDHighlightColor colorWithAlphaComponent:0.8] range:NSMakeRange(i, 1)]; 133 | } 134 | else { 135 | [self.displayString addAttribute:NSForegroundColorAttributeName value:SDHighlightColor range:NSMakeRange(i, 1)]; 136 | [self.displayString addAttribute:NSUnderlineColorAttributeName value:SDHighlightColor range:NSMakeRange(i, 1)]; 137 | [self.displayString addAttribute:NSUnderlineStyleAttributeName value:@1 range:NSMakeRange(i, 1)]; 138 | } 139 | }]; 140 | } 141 | 142 | - (void) matchAgainstQuery: (NSArray*) queryTokens 143 | isCaseSensitive: (BOOL) isCaseSensitive 144 | { 145 | // given a query split into queryTokens (either characters or words 146 | // depending on user options), find if this choice matches the query and 147 | // update self.isMatchForQuery and self.indexSet accordingly. 148 | 149 | NSString* text = self.raw; 150 | int len = [text length]; 151 | 152 | NSStringCompareOptions options = 0; 153 | if ( ! MatchFromBeginning ) { 154 | options |= NSBackwardsSearch; 155 | } 156 | if ( isCaseSensitive ) { 157 | options |= NSLiteralSearch; 158 | } else { 159 | options |= NSCaseInsensitiveSearch; 160 | } 161 | 162 | int nextIdx = MatchFromBeginning ? 0 : (len - 1); 163 | self.isMatchForQuery = YES; 164 | [self.indexSet removeAllIndexes]; 165 | for (NSString* token in queryTokens) { 166 | if ( (nextIdx < 0) || (nextIdx >= len) ) { 167 | self.isMatchForQuery = NO; 168 | break; 169 | } 170 | NSRange searchRange; 171 | if ( MatchFromBeginning ) { 172 | searchRange = NSMakeRange(nextIdx, len - nextIdx); 173 | } else { 174 | searchRange = NSMakeRange(0, nextIdx + 1); 175 | } 176 | NSRange foundRange = [ text rangeOfString:token options:options 177 | range:searchRange ]; 178 | if (foundRange.location == NSNotFound) { 179 | self.isMatchForQuery = NO; 180 | break; 181 | } 182 | [self.indexSet addIndexesInRange:foundRange]; 183 | if ( MatchFromBeginning ) { 184 | nextIdx = NSMaxRange(foundRange); 185 | } else { 186 | nextIdx = foundRange.location - 1; 187 | } 188 | } 189 | } 190 | 191 | - (void) scoreMatch { 192 | 193 | if ( ( ! self.isMatchForQuery ) 194 | || ( ! SortMatches ) 195 | || ( [self.indexSet count] == 0 ) ) { 196 | self.score = 0; 197 | return; 198 | } 199 | 200 | int firstOccurenceScore = 0; 201 | if ( ScoreFirstMatchedPosition ) { 202 | if (MatchFromBeginning) 203 | firstOccurenceScore = -self.indexSet.firstIndex; 204 | else 205 | firstOccurenceScore = self.indexSet.lastIndex 206 | - [self.raw length] + 1; 207 | } 208 | 209 | __block int lengthScore = 0; 210 | __block int numRanges = 0; 211 | 212 | [self.indexSet enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) { 213 | numRanges++; 214 | lengthScore += (range.length * 100); 215 | }]; 216 | 217 | lengthScore /= numRanges; 218 | 219 | int percentScore = ( (double)[self.indexSet count] / 220 | (double)[self.raw length] ) * 100.0; 221 | 222 | self.score = lengthScore + percentScore + firstOccurenceScore; 223 | } 224 | 225 | - (void) analyze: (NSArray*) queryTokens 226 | isCaseSensitive: (BOOL) isCaseSensitive { 227 | [ self matchAgainstQuery: queryTokens isCaseSensitive: isCaseSensitive ]; 228 | [ self scoreMatch ]; 229 | } 230 | 231 | @end 232 | 233 | /******************************************************************************/ 234 | /* App Delegate */ 235 | /******************************************************************************/ 236 | 237 | @interface SDAppDelegate : NSObject 238 | 239 | // internal 240 | 241 | - (void)createMenu; 242 | @property NSWindow* window; 243 | @property NSArray* choices; 244 | @property NSMutableArray* filteredSortedChoices; 245 | @property SDTableView* listTableView; 246 | @property NSTextField* queryField; 247 | @property NSInteger choice; 248 | 249 | @property NSString* lastScriptOutputAtInput; 250 | 251 | @end 252 | 253 | @implementation SDAppDelegate 254 | 255 | /******************************************************************************/ 256 | /* Starting the app */ 257 | /******************************************************************************/ 258 | 259 | -(void)createMenu { 260 | /* create invisible menubar so that (copy paste cut undo redo) all work */ 261 | NSMenu *menubar = [[NSMenu alloc]init]; 262 | [NSApp setMainMenu:menubar]; 263 | 264 | NSMenuItem *menuBarItem = [[NSMenuItem alloc] init]; 265 | [menubar addItem:menuBarItem]; 266 | NSMenu *myMenu = [[NSMenu alloc]init]; 267 | 268 | // just FYI: some of those are prone to being renamed by the system 269 | // see https://github.com/tauri-apps/tauri/issues/7828#issuecomment-1723489849 270 | // and https://github.com/electron/electron/blob/706653d5e4d06922f75aa5621533a16fc34d3a77/shell/browser/ui/cocoa/electron_menu_controller.mm#L62 271 | NSMenuItem* copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; 272 | NSMenuItem* pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; 273 | NSMenuItem* cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]; 274 | NSMenuItem* undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; 275 | NSMenuItem* redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"z"]; 276 | [redoItem setKeyEquivalentModifierMask: NSShiftKeyMask | NSCommandKeyMask]; 277 | 278 | [myMenu addItem:copyItem]; 279 | [myMenu addItem:pasteItem]; 280 | [myMenu addItem:cutItem]; 281 | [myMenu addItem:undoItem]; 282 | [myMenu addItem:redoItem]; 283 | [menuBarItem setSubmenu:myMenu]; 284 | } 285 | 286 | - (void) applicationDidFinishLaunching:(NSNotification *)notification { 287 | [self createMenu]; 288 | NSArray* inputItems = [self getInputItems]; 289 | // NSLog(@"%ld", [inputItems count]); 290 | // NSLog(@"%@", inputItems); 291 | 292 | if ([inputItems count] < 1) 293 | [self cancel]; 294 | 295 | [NSApp activateIgnoringOtherApps: YES]; 296 | 297 | self.choices = [self choicesFromInputItems: inputItems]; 298 | 299 | NSRect winRect, textRect, dividerRect, listRect; 300 | [self getFrameForWindow: &winRect queryField: &textRect divider: ÷rRect tableView: &listRect]; 301 | 302 | [self setupWindow: winRect]; 303 | [self setupQueryField: textRect]; 304 | [self setupDivider: dividerRect]; 305 | [self setupResultsTable: listRect]; 306 | [self runQuery: self.queryField.stringValue]; 307 | [self resizeWindow]; 308 | [self.window center]; 309 | [self.window makeKeyAndOrderFront: nil]; 310 | 311 | // these even work inside NSAlert, so start them later 312 | [self setupKeyboardShortcuts]; 313 | } 314 | 315 | /******************************************************************************/ 316 | /* Setting up GUI elements */ 317 | /******************************************************************************/ 318 | 319 | - (void) setupWindow:(NSRect)winRect { 320 | BOOL usingYosemite = (NSClassFromString(@"NSVisualEffectView") != nil); 321 | 322 | NSUInteger styleMask = usingYosemite ? (NSFullSizeContentViewWindowMask | NSTitledWindowMask) : NSBorderlessWindowMask; 323 | self.window = [[SDMainWindow alloc] initWithContentRect: winRect 324 | styleMask: styleMask 325 | backing: NSBackingStoreBuffered 326 | defer: NO]; 327 | 328 | [self.window setDelegate: self]; 329 | 330 | if (usingYosemite) { 331 | self.window.titlebarAppearsTransparent = YES; 332 | NSVisualEffectView* blur = [[NSVisualEffectView alloc] initWithFrame: [[self.window contentView] bounds]]; 333 | [blur setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable ]; 334 | blur.material = NSVisualEffectMaterialMenu; 335 | blur.state = NSVisualEffectBlendingModeBehindWindow; 336 | [[self.window contentView] addSubview: blur]; 337 | } 338 | } 339 | 340 | 341 | - (void) setCursorAtEndOfQueryField { 342 | [[self.queryField currentEditor] setSelectedRange: NSMakeRange(self.queryField.stringValue.length, 0)]; 343 | } 344 | 345 | - (void) setupQueryField:(NSRect)textRect { 346 | NSRect iconRect, space; 347 | NSDivideRect(textRect, &iconRect, &textRect, NSHeight(textRect) / 1.25, NSMinXEdge); 348 | NSDivideRect(textRect, &space, &textRect, 5.0, NSMinXEdge); 349 | 350 | CGFloat d = NSHeight(iconRect) * 0.10; 351 | iconRect = NSInsetRect(iconRect, d, d); 352 | 353 | NSImageView* icon = [[NSImageView alloc] initWithFrame: iconRect]; 354 | [icon setAutoresizingMask: NSViewMaxXMargin | NSViewMinYMargin ]; 355 | [icon setImage: [NSImage imageNamed: NSImageNameRightFacingTriangleTemplate]]; 356 | [icon setImageScaling: NSImageScaleProportionallyDown]; 357 | // [icon setImageFrameStyle: NSImageFrameButton]; 358 | [[self.window contentView] addSubview: icon]; 359 | 360 | self.queryField = Password ? [[NSSecureTextField alloc] initWithFrame: textRect] : [[NSTextField alloc] initWithFrame: textRect]; 361 | [self.queryField setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin ]; 362 | [self.queryField setDelegate: self]; 363 | [self.queryField setStringValue: InitialQuery]; 364 | [self.queryField setBezelStyle: NSTextFieldSquareBezel]; 365 | [self.queryField setBordered: NO]; 366 | [self.queryField setDrawsBackground: NO]; 367 | [self.queryField setFocusRingType: NSFocusRingTypeNone]; 368 | [self.queryField setFont: SDQueryFont]; 369 | [self.queryField setEditable: YES]; 370 | [self.queryField setPlaceholderString: PromptText]; 371 | [self.queryField setTarget: self]; 372 | [self.queryField setAction: @selector(choose:)]; 373 | [[self.queryField cell] setSendsActionOnEndEditing: NO]; 374 | [[self.window contentView] addSubview: self.queryField]; 375 | 376 | // schedule to set cursor position after a delay, after the main run loop 377 | // has completed its initial cycle 378 | [self performSelector:@selector(setCursorAtEndOfQueryField) withObject:nil afterDelay:0.0]; 379 | } 380 | 381 | - (void) getFrameForWindow:(NSRect*)winRect queryField:(NSRect*)textRect divider:(NSRect*)dividerRect tableView:(NSRect*)listRect { 382 | *winRect = NSMakeRect(0, 0, 100, 100); 383 | NSRect contentViewRect = NSInsetRect(*winRect, 10, 10); 384 | NSDivideRect(contentViewRect, textRect, listRect, NSHeight([SDQueryFont boundingRectForFont]), NSMaxYEdge); 385 | NSDivideRect(*listRect, dividerRect, listRect, 20.0, NSMaxYEdge); 386 | dividerRect->origin.y += NSHeight(*dividerRect) / 2.0; 387 | dividerRect->size.height = 1.0; 388 | } 389 | 390 | - (void) setupDivider:(NSRect)dividerRect { 391 | NSBox* border = [[NSBox alloc] initWithFrame: dividerRect]; 392 | [border setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin ]; 393 | [border setBoxType: NSBoxCustom]; 394 | [border setFillColor: [NSColor systemGrayColor]]; 395 | [border setBorderWidth: 0.0]; 396 | [[self.window contentView] addSubview: border]; 397 | } 398 | 399 | - (void) setupResultsTable:(NSRect)listRect { 400 | NSFont* rowFont = [NSFont fontWithName:[SDQueryFont fontName] size: [SDQueryFont pointSize] * 0.70]; 401 | 402 | NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:@"thing"]; 403 | [col setEditable: NO]; 404 | [col setWidth: 10000]; 405 | [[col dataCell] setFont: rowFont]; 406 | 407 | NSTextFieldCell* cell = [col dataCell]; 408 | [cell setLineBreakMode: NSLineBreakByCharWrapping]; 409 | 410 | self.listTableView = [[SDTableView alloc] init]; 411 | [self.listTableView setDataSource: self]; 412 | [self.listTableView setDelegate: self]; 413 | [self.listTableView setBackgroundColor: [NSColor clearColor]]; 414 | [self.listTableView setHeaderView: nil]; 415 | [self.listTableView setAllowsEmptySelection: NO]; 416 | [self.listTableView setAllowsMultipleSelection: NO]; 417 | [self.listTableView setAllowsTypeSelect: NO]; 418 | [self.listTableView setRowHeight: NSHeight([rowFont boundingRectForFont]) * 1.2]; 419 | [self.listTableView addTableColumn:col]; 420 | [self.listTableView setTarget: self]; 421 | [self.listTableView setDoubleAction: @selector(chooseByDoubleClicking:)]; 422 | [self.listTableView setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleNone]; 423 | 424 | NSScrollView* listScrollView = [[NSScrollView alloc] initWithFrame: listRect]; 425 | [listScrollView setVerticalScrollElasticity: NSScrollElasticityNone]; 426 | [listScrollView setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable ]; 427 | [listScrollView setDocumentView: self.listTableView]; 428 | [listScrollView setDrawsBackground: NO]; 429 | [[self.window contentView] addSubview: listScrollView]; 430 | } 431 | 432 | - (NSArray*) choicesFromInputItems:(NSArray*)inputItems { 433 | NSMutableArray* choices = [NSMutableArray array]; 434 | for (NSString* inputItem in inputItems) { 435 | if ([inputItem length] > 0) { 436 | [choices addObject: [[SDChoice alloc] initWithString: inputItem]]; 437 | } 438 | } 439 | return [choices copy]; 440 | } 441 | 442 | - (void) resizeWindow { 443 | NSRect screenFrame = [[NSScreen mainScreen] visibleFrame]; 444 | 445 | CGFloat rowHeight = [self.listTableView rowHeight]; 446 | CGFloat intercellHeight =[self.listTableView intercellSpacing].height; 447 | CGFloat allRowsHeight = (rowHeight + intercellHeight) * SDNumRows; 448 | 449 | CGFloat windowHeight = NSHeight([[self.window contentView] bounds]); 450 | CGFloat tableHeight = NSHeight([[self.listTableView superview] frame]); 451 | CGFloat finalHeight = (windowHeight - tableHeight) + allRowsHeight; 452 | 453 | CGFloat width; 454 | if (SDPercentWidth >= 0 && SDPercentWidth <= 100) { 455 | CGFloat percentWidth = (CGFloat)SDPercentWidth / 100.0; 456 | width = NSWidth(screenFrame) * percentWidth; 457 | } 458 | else { 459 | width = NSWidth(screenFrame) * 0.50; 460 | width = MIN(width, 800); 461 | width = MAX(width, 400); 462 | } 463 | 464 | NSRect winRect = NSMakeRect(0, 0, width, finalHeight); 465 | [self.window setFrame:winRect display:YES]; 466 | } 467 | 468 | - (void) setupKeyboardShortcuts { 469 | __weak id _self = self; 470 | [self addShortcut:@"1" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 0]; }]; 471 | [self addShortcut:@"2" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 1]; }]; 472 | [self addShortcut:@"3" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 2]; }]; 473 | [self addShortcut:@"4" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 3]; }]; 474 | [self addShortcut:@"5" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 4]; }]; 475 | [self addShortcut:@"6" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 5]; }]; 476 | [self addShortcut:@"7" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 6]; }]; 477 | [self addShortcut:@"8" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 7]; }]; 478 | [self addShortcut:@"9" mods:NSCommandKeyMask handler:^{ [_self pickIndex: 8]; }]; 479 | [self addShortcut:@"q" mods:NSCommandKeyMask handler:^{ [_self cancel]; }]; 480 | [self addShortcut:@"a" mods:NSCommandKeyMask handler:^{ [_self selectAll: nil]; }]; 481 | [self addShortcut:@"c" mods:NSControlKeyMask handler:^{ [_self cancel]; }]; 482 | [self addShortcut:@"g" mods:NSControlKeyMask handler:^{ [_self cancel]; }]; 483 | } 484 | 485 | /******************************************************************************/ 486 | /* Table view */ 487 | /******************************************************************************/ 488 | 489 | - (void) reflectChoice { 490 | [self.listTableView selectRowIndexes:[NSIndexSet indexSetWithIndex: self.choice] byExtendingSelection:NO]; 491 | [self.listTableView scrollRowToVisible: self.choice]; 492 | } 493 | 494 | - (NSInteger) numberOfRowsInTableView:(NSTableView *)tableView { 495 | return [self.filteredSortedChoices count]; 496 | } 497 | 498 | - (id) tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { 499 | SDChoice* choice = [self.filteredSortedChoices objectAtIndex: row]; 500 | return choice.displayString; 501 | } 502 | 503 | - (void) tableViewSelectionDidChange:(NSNotification *)notification { 504 | self.choice = [self.listTableView selectedRow]; 505 | } 506 | 507 | - (void) tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { 508 | if ([[aTableView selectedRowIndexes] containsIndex:rowIndex]) 509 | [aCell setBackgroundColor: [SDHighlightBackgroundColor colorWithAlphaComponent: 0.5]]; 510 | else 511 | [aCell setBackgroundColor: [NSColor clearColor]]; 512 | 513 | [aCell setDrawsBackground:YES]; 514 | } 515 | 516 | - (void) runScriptAtList:(NSString*) query { 517 | if([ScriptAtList length] > 0){ 518 | NSArray *rows = [Script(ScriptAtList,query, @"list") componentsSeparatedByString:@"\n"]; 519 | int i; 520 | for (i=[rows count]-1; i>=0; i--){ 521 | if ([rows[i] length] > 0){ 522 | SDChoice* newChoice = [[SDChoice alloc] initWithString:rows[i]]; 523 | [self.filteredSortedChoices insertObject:newChoice atIndex:0]; 524 | } 525 | } 526 | } 527 | } 528 | 529 | - (void) runScriptAtInput:(NSString*) query { 530 | if([ScriptAtInput length] > 0){ 531 | self.lastScriptOutputAtInput = Script(ScriptAtInput,query, @"input"); 532 | 533 | if([[self.queryField stringValue] length] > [LastQueryString length]){ 534 | LastQueryString = [self.queryField stringValue]; 535 | NSString* queryWithOutput = [NSString stringWithFormat:@"%@%@", [self.queryField stringValue], self.lastScriptOutputAtInput]; 536 | [self.queryField setStringValue: queryWithOutput]; 537 | 538 | NSText* fieldEditor = [self.queryField currentEditor]; 539 | if([self.lastScriptOutputAtInput length] > 0){ 540 | [fieldEditor setSelectedRange: NSMakeRange([queryWithOutput length]-[self.lastScriptOutputAtInput length],[queryWithOutput length])]; 541 | } 542 | } else if ([[self.queryField stringValue] length] < [LastQueryString length]) { 543 | LastQueryString = [self.queryField stringValue]; 544 | } 545 | } 546 | } 547 | 548 | - (void) clearScriptOutputAtInput { 549 | NSRange range = [[[self.queryField window] fieldEditor:YES forObject:self.queryField] selectedRange]; 550 | if([self.lastScriptOutputAtInput length] > 0 && [[[self.queryField stringValue] substringWithRange:range] isEqualToString: self.lastScriptOutputAtInput]){ 551 | [[[self.queryField window] fieldEditor:YES forObject:self.queryField] setSelectedRange:NSMakeRange(LastCursorPos,0)]; 552 | [self.queryField setStringValue: [[self.queryField stringValue] substringWithRange:NSMakeRange(0,range.location)]]; 553 | self.lastScriptOutputAtInput = @""; 554 | } 555 | } 556 | 557 | /******************************************************************************/ 558 | /* Filtering! */ 559 | /******************************************************************************/ 560 | 561 | - (NSArray*) tokenizeQuery: (NSString*) query { 562 | 563 | NSMutableArray* mtokens = [NSMutableArray array]; 564 | 565 | int len = [query length]; 566 | int nextIdx = MatchFromBeginning ? 0 : (len - 1); 567 | while ( (nextIdx >= 0) && (nextIdx < len) ) { 568 | 569 | // get the next token if one exists 570 | // if it exists, append it to mtokens 571 | // update nextIdx 572 | 573 | NSString* nextToken; 574 | 575 | if ( ! MatchWords ) { 576 | // next token = just the current character 577 | nextToken = [query substringWithRange: NSMakeRange(nextIdx, 1)]; 578 | [mtokens addObject:nextToken]; 579 | nextIdx += MatchFromBeginning ? 1 : -1; 580 | continue; 581 | } 582 | 583 | // next token = next word that starts at the current character 584 | // if there is one! 585 | 586 | NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; 587 | NSCharacterSet *nonWhitespace = [whitespace invertedSet]; 588 | 589 | // starting from nextIdx and going in the right direction, look for a 590 | // non-whitespace character to begin our word 591 | 592 | NSStringCompareOptions options; 593 | NSRange nonWSSearchRange; 594 | if ( MatchFromBeginning ) { 595 | options = 0; 596 | nonWSSearchRange = NSMakeRange(nextIdx, len - nextIdx); 597 | } else { 598 | options = NSBackwardsSearch; 599 | nonWSSearchRange = NSMakeRange(0, nextIdx + 1); 600 | } 601 | 602 | NSRange nonWSFoundRange = [ 603 | query rangeOfCharacterFromSet:nonWhitespace options:options 604 | range:nonWSSearchRange ]; 605 | 606 | if (nonWSFoundRange.location == NSNotFound) { 607 | // there's no next token; we're done! 608 | break; 609 | } 610 | 611 | // there is a next token; it starts at the non-whitespace character we 612 | // just found 613 | 614 | int nonWSIdx = nonWSFoundRange.location; 615 | 616 | // now start at the character that immediately follows this 617 | // non-whitespace character. Look for a subsequent whitespace 618 | // character; if found, this will mark the end of our word. If not, our 619 | // word continues until the end of the query. 620 | 621 | NSRange wsSearchRange; 622 | if ( MatchFromBeginning ) { 623 | wsSearchRange = NSMakeRange(nonWSIdx + 1, len - nonWSIdx - 1); 624 | } else { 625 | wsSearchRange = NSMakeRange(0, nonWSIdx); 626 | } 627 | 628 | NSRange wsFoundRange = [ 629 | query rangeOfCharacterFromSet:whitespace options:options 630 | range:wsSearchRange ]; 631 | 632 | int wsIdx; 633 | if (wsFoundRange.location == NSNotFound) { 634 | wsIdx = MatchFromBeginning ? len : -1; 635 | } else { 636 | wsIdx = wsFoundRange.location; 637 | } 638 | 639 | int nextTokenStartIdx = nonWSIdx; 640 | int nextTokenEndIdx = wsIdx - (MatchFromBeginning ? 1 : -1); 641 | if ( ! MatchFromBeginning ) { 642 | int temp = nextTokenStartIdx; 643 | nextTokenStartIdx = nextTokenEndIdx; 644 | nextTokenEndIdx = temp; 645 | } 646 | 647 | int nextTokenLength = nextTokenEndIdx - nextTokenStartIdx + 1; 648 | nextToken = [ query substringWithRange: NSMakeRange( 649 | nextTokenStartIdx, nextTokenLength) ]; 650 | 651 | [mtokens addObject:nextToken]; 652 | if (MatchFromBeginning) { 653 | nextIdx = nextTokenEndIdx + 1; 654 | } else { 655 | nextIdx = nextTokenStartIdx - 1; 656 | } 657 | 658 | } 659 | 660 | return [mtokens copy]; 661 | } 662 | 663 | - (BOOL) getIsCaseSensitive: (NSString*) query { 664 | if ( SearchCase == INSENSITIVE ) { 665 | return NO; 666 | } else if (SearchCase == SENSITIVE) { 667 | return YES; 668 | } else { 669 | // Smart case 670 | NSRange uppercaseRange = [ query rangeOfCharacterFromSet: 671 | [NSCharacterSet uppercaseLetterCharacterSet] ]; 672 | if (uppercaseRange.location == NSNotFound) { 673 | // query is all lowercase, so we perform case-insensitive search 674 | return NO; 675 | } else { 676 | // query contains uppercase letters, so we perform case-sensitive 677 | // search. 678 | return YES; 679 | } 680 | } 681 | } 682 | 683 | - (void) doQuery:(NSString*)query { 684 | 685 | NSArray* queryTokens = [ self tokenizeQuery: query ]; 686 | BOOL isCaseSensitive = [ self getIsCaseSensitive: query ]; 687 | 688 | self.filteredSortedChoices = [self.choices mutableCopy]; 689 | 690 | // analyze (cache) 691 | for (SDChoice* choice in self.filteredSortedChoices) 692 | [ choice analyze:queryTokens isCaseSensitive:isCaseSensitive ]; 693 | 694 | if ([query length] >= 1) { 695 | 696 | // filter out non-matches 697 | for (SDChoice* choice in [self.filteredSortedChoices copy]) { 698 | if (!choice.isMatchForQuery) 699 | [self.filteredSortedChoices removeObject: choice]; 700 | } 701 | 702 | // sort remainder 703 | if (SortMatches) { 704 | [self.filteredSortedChoices sortUsingComparator:^NSComparisonResult(SDChoice* a, SDChoice* b) { 705 | if (a.score > b.score) return NSOrderedAscending; 706 | if (a.score < b.score) return NSOrderedDescending; 707 | return NSOrderedSame; 708 | }]; 709 | } 710 | 711 | } 712 | } 713 | 714 | 715 | - (void) runQuery:(NSString*)query { 716 | [self doQuery: query]; 717 | 718 | // render remainder 719 | for (SDChoice* choice in self.filteredSortedChoices) 720 | [choice render]; 721 | 722 | // running scripts on input, if available 723 | LastCursorPos = (int) [[[self.queryField window] fieldEditor:YES forObject:self.queryField] selectedRange].location; 724 | [self runScriptAtInput: query]; 725 | [self runScriptAtList: query]; 726 | 727 | // show! 728 | [self.listTableView reloadData]; 729 | 730 | // push choice back to start 731 | self.choice = 0; 732 | [self reflectChoice]; 733 | 734 | // if there's only one choice, and AutoSelectSingleChoice is enabled, pick 735 | // this choice and exit the app 736 | if (AutoSelectSingleChoice && [self.filteredSortedChoices count] == 1) { 737 | self.choice = 0; 738 | [self choose]; 739 | } 740 | } 741 | 742 | /******************************************************************************/ 743 | /* Ending the app */ 744 | /******************************************************************************/ 745 | 746 | - (void) choose { 747 | if ([self.filteredSortedChoices count] == 0) { 748 | if (SDReturnStringOnMismatch) { 749 | [self writeOutput: [self.queryField stringValue]]; 750 | exit(0); 751 | } 752 | exit(1); 753 | } 754 | 755 | if (SDReturnsIndex) { 756 | SDChoice* choice = [self.filteredSortedChoices objectAtIndex: self.choice]; 757 | NSUInteger realIndex = [self.choices indexOfObject: choice]; 758 | [self writeOutput: [NSString stringWithFormat:@"%ld", realIndex]]; 759 | } 760 | else { 761 | SDChoice* choice = [self.filteredSortedChoices objectAtIndex: self.choice]; 762 | [self writeOutput: choice.raw]; 763 | } 764 | 765 | exit(0); 766 | } 767 | 768 | - (void) cancel { 769 | if (SDReturnsIndex) { 770 | [self writeOutput: [NSString stringWithFormat:@"%d", -1]]; 771 | } 772 | 773 | exit(1); 774 | } 775 | 776 | - (void) applicationDidResignActive:(NSNotification *)notification { 777 | [self cancel]; 778 | } 779 | 780 | - (void) pickIndex:(NSUInteger)idx { 781 | if (idx >= [self.filteredSortedChoices count]) 782 | return; 783 | 784 | self.choice = idx; 785 | [self choose]; 786 | } 787 | 788 | - (IBAction) choose:(id)sender { 789 | [self choose]; 790 | } 791 | 792 | - (IBAction) chooseByDoubleClicking:(id)sender { 793 | NSInteger row = [self.listTableView clickedRow]; 794 | if (row == -1) 795 | return; 796 | 797 | self.choice = row; 798 | [self choose]; 799 | } 800 | 801 | /******************************************************************************/ 802 | /* Search field callbacks */ 803 | /******************************************************************************/ 804 | 805 | - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { 806 | [self clearScriptOutputAtInput]; 807 | if (commandSelector == @selector(cancelOperation:)) { 808 | if ([[self.queryField stringValue] length] > 0) { 809 | [textView moveToBeginningOfDocument: nil]; 810 | [textView deleteToEndOfParagraph: nil]; 811 | } 812 | else { 813 | [self cancel]; 814 | } 815 | return YES; 816 | } 817 | else if (commandSelector == @selector(moveUp:)) { 818 | if (self.choice <= 0) { 819 | self.choice = [self.filteredSortedChoices count] - 1; 820 | } else { 821 | self.choice -= 1; 822 | } 823 | 824 | [self reflectChoice]; 825 | return YES; 826 | } 827 | else if (commandSelector == @selector(moveDown:)) { 828 | if (self.choice >= [self.filteredSortedChoices count] - 1) { 829 | self.choice = 0; 830 | } else { 831 | self.choice += 1; 832 | } 833 | 834 | [self reflectChoice]; 835 | return YES; 836 | } 837 | else if (commandSelector == @selector(insertTab:)) { 838 | [self.queryField setStringValue: [[self.filteredSortedChoices objectAtIndex: self.choice] raw]]; 839 | [[self.queryField currentEditor] setSelectedRange: NSMakeRange(self.queryField.stringValue.length, 0)]; 840 | return YES; 841 | } 842 | else if (commandSelector == @selector(deleteForward:)) { 843 | if ([[self.queryField stringValue] length] == 0) 844 | [self cancel]; 845 | } 846 | 847 | // NSLog(@"[%@]", NSStringFromSelector(commandSelector)); 848 | return NO; 849 | } 850 | 851 | - (void) controlTextDidChange:(NSNotification *)obj { 852 | [self clearScriptOutputAtInput]; 853 | [self runQuery: [self.queryField stringValue]]; 854 | } 855 | 856 | - (IBAction) selectAll:(id)sender { 857 | NSTextView* editor = (NSTextView*)[self.window fieldEditor:NO forObject:self.queryField]; 858 | [editor selectAll: sender]; 859 | } 860 | 861 | /******************************************************************************/ 862 | /* Helpers */ 863 | /******************************************************************************/ 864 | 865 | - (void) addShortcut:(NSString*)key mods:(NSEventModifierFlags)mods handler:(dispatch_block_t)action { 866 | static NSMutableArray* handlers; 867 | static dispatch_once_t onceToken; 868 | dispatch_once(&onceToken, ^{ 869 | handlers = [NSMutableArray array]; 870 | }); 871 | 872 | id x = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^ NSEvent*(NSEvent* event) { 873 | NSEventModifierFlags flags = ([event modifierFlags] & NSDeviceIndependentModifierFlagsMask); 874 | if (flags == mods && [[event charactersIgnoringModifiers] isEqualToString: key]) { 875 | action(); 876 | return nil; 877 | } 878 | return event; 879 | }]; 880 | [handlers addObject: x]; 881 | } 882 | 883 | - (void) writeOutput:(NSString*)str { 884 | NSFileHandle* stdoutHandle = [NSFileHandle fileHandleWithStandardOutput]; 885 | [stdoutHandle writeData: [str dataUsingEncoding:NSUTF8StringEncoding]]; 886 | } 887 | 888 | static NSColor* SDColorFromHex(NSString* hex) { 889 | NSScanner* scanner = [NSScanner scannerWithString: [hex uppercaseString]]; 890 | unsigned colorCode = 0; 891 | [scanner scanHexInt: &colorCode]; 892 | return [NSColor colorWithCalibratedRed:(CGFloat)(unsigned char)(colorCode >> 16) / 0xff 893 | green:(CGFloat)(unsigned char)(colorCode >> 8) / 0xff 894 | blue:(CGFloat)(unsigned char)(colorCode) / 0xff 895 | alpha: 1.0]; 896 | } 897 | 898 | static char* HexFromSDColor(NSColor* color) { 899 | size_t bufferSize = 7; 900 | char* buffer = (char*) malloc(bufferSize * sizeof(char)); 901 | NSColor* c = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; 902 | snprintf(buffer, bufferSize, "%2X%2X%2X", 903 | (unsigned int) ([c redComponent] * 255.99999), 904 | (unsigned int) ([c greenComponent] * 255.99999), 905 | (unsigned int) ([c blueComponent] * 255.99999)); 906 | return buffer; 907 | } 908 | 909 | static NSString* Script(NSString* pathToScript, NSString* queryInput, NSString* where) { 910 | int pid = [[NSProcessInfo processInfo] processIdentifier]; 911 | NSPipe *pipe = [NSPipe pipe]; 912 | NSPipe *pipeErr = [NSPipe pipe]; 913 | NSFileHandle *file = pipe.fileHandleForReading; 914 | 915 | NSTask *task = [[NSTask alloc] init]; 916 | task.launchPath = pathToScript; 917 | task.arguments = @[queryInput, where]; 918 | task.standardOutput = pipe; 919 | task.standardError = pipeErr; 920 | 921 | [task launch]; 922 | 923 | NSData *data = [file readDataToEndOfFile]; 924 | [file closeFile]; 925 | 926 | NSString *output = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; 927 | 928 | return output; 929 | } 930 | 931 | 932 | /******************************************************************************/ 933 | /* Getting input list */ 934 | /******************************************************************************/ 935 | 936 | - (NSArray*) getInputItems { 937 | 938 | #ifdef DEBUG 939 | 940 | #include "fakedata.h" 941 | 942 | #else 943 | 944 | NSFileHandle* stdinHandle = [NSFileHandle fileHandleWithStandardInput]; 945 | NSData* inputData = Password ? nil : [stdinHandle readDataToEndOfFile]; 946 | NSString* inputStrings = [[[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet: [NSCharacterSet newlineCharacterSet]]; 947 | 948 | if ([inputStrings length] == 0 && !AllowEmptyInput) 949 | return nil; 950 | 951 | return [inputStrings componentsSeparatedByString: Separator]; 952 | 953 | #endif 954 | 955 | } 956 | 957 | @end 958 | 959 | /******************************************************************************/ 960 | /* Command line interface */ 961 | /******************************************************************************/ 962 | 963 | static NSString* SDAppVersionString(void) { 964 | return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; 965 | } 966 | 967 | static void SDShowVersion(const char* name) { 968 | printf("%s %s\n", name, [SDAppVersionString() UTF8String]); 969 | exit(0); 970 | } 971 | 972 | static void usage(const char* name) { 973 | printf("usage: %s\n", name); 974 | printf(" -i return index of selected element\n"); 975 | printf(" -v show choose version\n"); 976 | printf(" -n [10] set number of rows\n"); 977 | printf(" -w [50] set width of choose window\n"); 978 | printf(" -f [Menlo] set font used by choose\n"); 979 | printf(" -s [26] set font size used by choose\n"); 980 | printf(" -c [0000FF] highlight color for matched string\n"); 981 | printf(" -b [222222] background color of selected element\n"); 982 | printf(" -u disable underline and use background for matched string\n"); 983 | printf(" -m return the query string in case it doesn't match any item\n"); 984 | printf(" -p defines a prompt to be displayed when query field is empty\n"); 985 | printf(" -P conceals keyboard input / password mode (implies -m, -e and -n 0)\n"); 986 | printf(" -q defines initial query to start with (empty by default)\n"); 987 | printf(" -r path to a script to run when typing. Output appended to input field. Two args provided upon run:\n"); 988 | printf(" - the query text from input field\n"); 989 | printf(" - where output will be placed (\"input\" for -r or \"list\" for -t). \n"); 990 | printf(" -t same as -r, but outputs are in the form of extra list options (supports multiline outputs)\n"); 991 | printf(" -x defines separator string, a single newline (\\n) by default\n"); 992 | printf(" beware of escaping:\n"); 993 | printf(" passing -x \\n\\n will work\n"); 994 | printf(" passing -x '\\n\\n' will not work\n"); 995 | printf(" -y show newline and tab as symbols (⏎ ⇥)\n"); 996 | printf(" -e allow empty input (choose will show up even if there are no items to select)\n"); 997 | printf(" -o given a query, outputs results to standard output\n"); 998 | printf(" -z search matches symbols from beginning (instead of from end by weird default)\n"); 999 | printf(" -a rank early matches higher\n"); 1000 | printf(" -1 if there's only one element, select it automatically\n"); 1001 | printf(" -W match words (rather than characters) from the query field\n"); 1002 | printf(" -S do not sort matches (ie, present them in the same order they appeared in the input)\n"); 1003 | printf(" -C [i] i = case-insensitive, I = case-sensitive, s = smart (case-sensitive if query contains uppercase characters, case-insensitive otherwise)\n"); 1004 | exit(0); 1005 | } 1006 | 1007 | static void queryStdout(SDAppDelegate* delegate, const char* query) { 1008 | delegate.choices = [delegate choicesFromInputItems: [delegate getInputItems]]; 1009 | [delegate doQuery: [NSString stringWithUTF8String: query]]; 1010 | 1011 | for (SDChoice* choice in delegate.filteredSortedChoices) 1012 | printf("%s\n", [choice.raw UTF8String]); 1013 | 1014 | exit(0); 1015 | } 1016 | 1017 | static CaseSpecification getSearchCase(const char *optarg, const char *name) { 1018 | 1019 | if (strlen(optarg) != 1) { 1020 | usage(name); 1021 | } 1022 | 1023 | switch (*optarg) { 1024 | case 'i': 1025 | return INSENSITIVE; 1026 | case 'I': 1027 | return SENSITIVE; 1028 | case 's': 1029 | return SMART; 1030 | default: 1031 | usage(name); 1032 | } 1033 | 1034 | return INSENSITIVE; 1035 | } 1036 | 1037 | int main(int argc, const char * argv[]) { 1038 | @autoreleasepool { 1039 | [NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory]; 1040 | 1041 | VisualizeWhitespaceCharacters = NO; 1042 | AllowEmptyInput = NO; 1043 | MatchFromBeginning = NO; 1044 | ScoreFirstMatchedPosition = NO; 1045 | SDReturnsIndex = NO; 1046 | SDUnderlineDisabled = NO; 1047 | const char* hexColor = HexFromSDColor(NSColor.systemBlueColor); 1048 | const char* hexBackgroundColor = HexFromSDColor(NSColor.systemGrayColor); 1049 | const char* queryFontName = "Menlo"; 1050 | const char* queryPromptString = ""; 1051 | InitialQuery = [NSString stringWithUTF8String: ""]; 1052 | Separator = [NSString stringWithUTF8String: "\n"]; 1053 | CGFloat queryFontSize = 26.0; 1054 | SDNumRows = 10; 1055 | SDReturnStringOnMismatch = NO; 1056 | SDPercentWidth = -1; 1057 | AutoSelectSingleChoice = NO; 1058 | MatchWords = NO; 1059 | SortMatches = YES; 1060 | SearchCase = INSENSITIVE; 1061 | Password = NO; 1062 | 1063 | static SDAppDelegate* delegate; 1064 | delegate = [[SDAppDelegate alloc] init]; 1065 | [NSApp setDelegate: delegate]; 1066 | 1067 | int ch; 1068 | while ((ch = getopt(argc, (char**)argv, "lvyezaf:s:r:c:b:n:w:p:q:r:t:x:o:Phium1WSC:")) != -1) { 1069 | switch (ch) { 1070 | case 'i': SDReturnsIndex = YES; break; 1071 | case 'f': queryFontName = optarg; break; 1072 | case 'c': hexColor = optarg; break; 1073 | case 'b': hexBackgroundColor = optarg; break; 1074 | case 's': queryFontSize = atoi(optarg); break; 1075 | case 'n': SDNumRows = atoi(optarg); break; 1076 | case 'w': SDPercentWidth = atoi(optarg); break; 1077 | case 'v': SDShowVersion(argv[0]); break; 1078 | case 'u': SDUnderlineDisabled = YES; break; 1079 | case 'm': SDReturnStringOnMismatch = YES; break; 1080 | case 'p': queryPromptString = optarg; break; 1081 | case 'P': Password = YES; AllowEmptyInput = YES; SDReturnStringOnMismatch = YES; SDNumRows = 0; break; 1082 | case 'q': InitialQuery = [NSString stringWithUTF8String: optarg]; break; 1083 | case 'r': ScriptAtInput = [NSString stringWithUTF8String: optarg]; break; 1084 | case 't': ScriptAtList = [NSString stringWithUTF8String: optarg]; break; 1085 | case 'x': Separator = [NSString stringWithUTF8String: optarg]; break; 1086 | case 'y': VisualizeWhitespaceCharacters = YES; break; 1087 | case 'e': AllowEmptyInput = YES; break; 1088 | case 'z': MatchFromBeginning = YES; break; 1089 | case 'a': ScoreFirstMatchedPosition = YES; break; 1090 | case 'o': queryStdout(delegate, optarg); break; 1091 | case '1': AutoSelectSingleChoice = YES; break; 1092 | case 'W': MatchWords = YES; break; 1093 | case 'S': SortMatches = NO; break; 1094 | case 'C': SearchCase = getSearchCase(optarg, argv[0]); break; 1095 | case '?': 1096 | case 'h': 1097 | default: 1098 | usage(argv[0]); 1099 | } 1100 | } 1101 | argc -= optind; 1102 | argv += optind; 1103 | 1104 | SDQueryFont = [NSFont fontWithName:[NSString stringWithUTF8String: queryFontName] size:queryFontSize]; 1105 | SDHighlightColor = SDColorFromHex([NSString stringWithUTF8String: hexColor]); 1106 | SDHighlightBackgroundColor = SDColorFromHex([NSString stringWithUTF8String: hexBackgroundColor]); 1107 | PromptText = [NSString stringWithUTF8String: queryPromptString]; 1108 | 1109 | if ([ScriptAtInput length] > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:ScriptAtInput]){ 1110 | printf("No such file or directory for the script at input: %s\n", [ScriptAtInput UTF8String]); 1111 | exit(1); 1112 | } 1113 | if ([ScriptAtList length] > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:ScriptAtList]){ 1114 | printf("No such file or directory for the script at list: %s\n", [ScriptAtList UTF8String]); 1115 | exit(1); 1116 | } 1117 | 1118 | NSApplicationMain(argc, argv); 1119 | } 1120 | return 0; 1121 | } 1122 | 1123 | --------------------------------------------------------------------------------