├── .gitignore
├── README.md
├── TextButler.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ └── contents.xcworkspacedata
├── TextButler
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── icon-1024.png
│ │ ├── icon-128.png
│ │ ├── icon-16.png
│ │ ├── icon-256.png
│ │ ├── icon-257.png
│ │ ├── icon-32.png
│ │ ├── icon-33.png
│ │ ├── icon-512.png
│ │ ├── icon-513.png
│ │ └── icon-64.png
│ ├── Contents.json
│ └── MenuIcon.imageset
│ │ ├── Contents.json
│ │ └── menu-icon.png
├── Info.plist
├── MainMenu.xib
├── ViewController.swift
└── default-snippets.json
└── artwork
├── icon-1024.png
├── icon-128.png
├── icon-16.png
├── icon-256.png
├── icon-32.png
├── icon-512.png
├── icon-64.png
├── icon.ai
├── menu-icon.png
└── menubar.png
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata/
2 | build/
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TextButler
2 |
3 | Auto-completing text utility for macOS.
4 |
5 | 
6 |
7 | TextButler sits in your menubar and listens for keyboard shortcuts that you have defined. It then quickly fills in the text of your choosing. So, instead of typing:
8 |
9 | > Dear Customer,
10 | >
11 | > Thank you so much for your question. I've forwarded this to our customer support and they will look at it as soon as possible.
12 | >
13 | > Kind regards,
14 | >
15 | > ThingCo Support Staff
16 |
17 | You can set it up to just write `;fwdsupp` and let TextButler type the full text automatically.
18 |
19 | Also, it has the cutest menu bar icon:
20 |
21 | 
22 |
23 | ## Download
24 |
25 | Download the ZIP file from the [releases page](https://github.com/fdb/textbutler/releases).
26 |
27 | **The releases are currently unsigned. Once downloaded, right-click the app and choose "Open".**
28 |
29 | ## Building on macOS Sierra
30 |
31 | git clone https://github.com/fdb/textbutler.git
32 | cd textbutler
33 | xcodebuild
34 | open build/Release/TextButler.app
35 |
36 | ## Example snippets file
37 |
38 | Snippets are stored under `/Users/username/Documents/TextButler/snippets.json`. Changes are automatically picked up.
39 |
40 | [
41 | {
42 | "shortcut": ";sig",
43 | "text": "Kind Greetings,\n\nJohn Doe"
44 | },
45 | {
46 | "shortcut": ",body",
47 | "text": "
\n\n\n"
48 | }
49 | ]
50 |
51 | ## TODO
52 | - Allow the user to edit the snippets in a custom GUI.
53 | - Allow snippets to control customer placement (e.g. between HTML tags).
54 | - Add an option to start the application at login.
55 | - Welcome screen explaining where to find snippet file, how to do expansions.
56 | - Create signed releases of the application.
57 | - Customizable location for snippets file.
58 | - Remove icon from dock.
59 |
--------------------------------------------------------------------------------
/TextButler.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 6179A6221F6C28C40059680A /* default-snippets.json in Resources */ = {isa = PBXBuildFile; fileRef = 6179A6211F6C28C40059680A /* default-snippets.json */; };
11 | 61DEED3F1F6BFDFF00958DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DEED3E1F6BFDFF00958DAF /* AppDelegate.swift */; };
12 | 61DEED411F6BFDFF00958DAF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DEED401F6BFDFF00958DAF /* ViewController.swift */; };
13 | 61DEED431F6BFDFF00958DAF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61DEED421F6BFDFF00958DAF /* Assets.xcassets */; };
14 | 61DEED4E1F6BFF5400958DAF /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 61DEED4D1F6BFF5400958DAF /* MainMenu.xib */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 6179A6211F6C28C40059680A /* default-snippets.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "default-snippets.json"; sourceTree = ""; };
19 | 61DEED3B1F6BFDFF00958DAF /* TextButler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TextButler.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | 61DEED3E1F6BFDFF00958DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
21 | 61DEED401F6BFDFF00958DAF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
22 | 61DEED421F6BFDFF00958DAF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | 61DEED471F6BFDFF00958DAF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
24 | 61DEED4D1F6BFF5400958DAF /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; };
25 | /* End PBXFileReference section */
26 |
27 | /* Begin PBXFrameworksBuildPhase section */
28 | 61DEED381F6BFDFF00958DAF /* Frameworks */ = {
29 | isa = PBXFrameworksBuildPhase;
30 | buildActionMask = 2147483647;
31 | files = (
32 | );
33 | runOnlyForDeploymentPostprocessing = 0;
34 | };
35 | /* End PBXFrameworksBuildPhase section */
36 |
37 | /* Begin PBXGroup section */
38 | 61DEED321F6BFDFF00958DAF = {
39 | isa = PBXGroup;
40 | children = (
41 | 61DEED3D1F6BFDFF00958DAF /* TextButler */,
42 | 61DEED3C1F6BFDFF00958DAF /* Products */,
43 | );
44 | sourceTree = "";
45 | };
46 | 61DEED3C1F6BFDFF00958DAF /* Products */ = {
47 | isa = PBXGroup;
48 | children = (
49 | 61DEED3B1F6BFDFF00958DAF /* TextButler.app */,
50 | );
51 | name = Products;
52 | sourceTree = "";
53 | };
54 | 61DEED3D1F6BFDFF00958DAF /* TextButler */ = {
55 | isa = PBXGroup;
56 | children = (
57 | 61DEED3E1F6BFDFF00958DAF /* AppDelegate.swift */,
58 | 61DEED401F6BFDFF00958DAF /* ViewController.swift */,
59 | 61DEED421F6BFDFF00958DAF /* Assets.xcassets */,
60 | 61DEED471F6BFDFF00958DAF /* Info.plist */,
61 | 61DEED4D1F6BFF5400958DAF /* MainMenu.xib */,
62 | 6179A6211F6C28C40059680A /* default-snippets.json */,
63 | );
64 | path = TextButler;
65 | sourceTree = "";
66 | };
67 | /* End PBXGroup section */
68 |
69 | /* Begin PBXNativeTarget section */
70 | 61DEED3A1F6BFDFF00958DAF /* TextButler */ = {
71 | isa = PBXNativeTarget;
72 | buildConfigurationList = 61DEED4A1F6BFDFF00958DAF /* Build configuration list for PBXNativeTarget "TextButler" */;
73 | buildPhases = (
74 | 61DEED371F6BFDFF00958DAF /* Sources */,
75 | 61DEED381F6BFDFF00958DAF /* Frameworks */,
76 | 61DEED391F6BFDFF00958DAF /* Resources */,
77 | );
78 | buildRules = (
79 | );
80 | dependencies = (
81 | );
82 | name = TextButler;
83 | productName = TextButler;
84 | productReference = 61DEED3B1F6BFDFF00958DAF /* TextButler.app */;
85 | productType = "com.apple.product-type.application";
86 | };
87 | /* End PBXNativeTarget section */
88 |
89 | /* Begin PBXProject section */
90 | 61DEED331F6BFDFF00958DAF /* Project object */ = {
91 | isa = PBXProject;
92 | attributes = {
93 | LastSwiftUpdateCheck = 0830;
94 | LastUpgradeCheck = 0830;
95 | ORGANIZATIONNAME = fdb;
96 | TargetAttributes = {
97 | 61DEED3A1F6BFDFF00958DAF = {
98 | CreatedOnToolsVersion = 8.3.3;
99 | DevelopmentTeam = 5X78EYG9RH;
100 | ProvisioningStyle = Automatic;
101 | };
102 | };
103 | };
104 | buildConfigurationList = 61DEED361F6BFDFF00958DAF /* Build configuration list for PBXProject "TextButler" */;
105 | compatibilityVersion = "Xcode 3.2";
106 | developmentRegion = English;
107 | hasScannedForEncodings = 0;
108 | knownRegions = (
109 | en,
110 | Base,
111 | );
112 | mainGroup = 61DEED321F6BFDFF00958DAF;
113 | productRefGroup = 61DEED3C1F6BFDFF00958DAF /* Products */;
114 | projectDirPath = "";
115 | projectRoot = "";
116 | targets = (
117 | 61DEED3A1F6BFDFF00958DAF /* TextButler */,
118 | );
119 | };
120 | /* End PBXProject section */
121 |
122 | /* Begin PBXResourcesBuildPhase section */
123 | 61DEED391F6BFDFF00958DAF /* Resources */ = {
124 | isa = PBXResourcesBuildPhase;
125 | buildActionMask = 2147483647;
126 | files = (
127 | 61DEED431F6BFDFF00958DAF /* Assets.xcassets in Resources */,
128 | 61DEED4E1F6BFF5400958DAF /* MainMenu.xib in Resources */,
129 | 6179A6221F6C28C40059680A /* default-snippets.json in Resources */,
130 | );
131 | runOnlyForDeploymentPostprocessing = 0;
132 | };
133 | /* End PBXResourcesBuildPhase section */
134 |
135 | /* Begin PBXSourcesBuildPhase section */
136 | 61DEED371F6BFDFF00958DAF /* Sources */ = {
137 | isa = PBXSourcesBuildPhase;
138 | buildActionMask = 2147483647;
139 | files = (
140 | 61DEED411F6BFDFF00958DAF /* ViewController.swift in Sources */,
141 | 61DEED3F1F6BFDFF00958DAF /* AppDelegate.swift in Sources */,
142 | );
143 | runOnlyForDeploymentPostprocessing = 0;
144 | };
145 | /* End PBXSourcesBuildPhase section */
146 |
147 | /* Begin XCBuildConfiguration section */
148 | 61DEED481F6BFDFF00958DAF /* Debug */ = {
149 | isa = XCBuildConfiguration;
150 | buildSettings = {
151 | ALWAYS_SEARCH_USER_PATHS = NO;
152 | CLANG_ANALYZER_NONNULL = YES;
153 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
154 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
155 | CLANG_CXX_LIBRARY = "libc++";
156 | CLANG_ENABLE_MODULES = YES;
157 | CLANG_ENABLE_OBJC_ARC = YES;
158 | CLANG_WARN_BOOL_CONVERSION = YES;
159 | CLANG_WARN_CONSTANT_CONVERSION = YES;
160 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
161 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
162 | CLANG_WARN_EMPTY_BODY = YES;
163 | CLANG_WARN_ENUM_CONVERSION = YES;
164 | CLANG_WARN_INFINITE_RECURSION = YES;
165 | CLANG_WARN_INT_CONVERSION = YES;
166 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
167 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
168 | CLANG_WARN_UNREACHABLE_CODE = YES;
169 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
170 | CODE_SIGN_IDENTITY = "-";
171 | COPY_PHASE_STRIP = NO;
172 | DEBUG_INFORMATION_FORMAT = dwarf;
173 | ENABLE_STRICT_OBJC_MSGSEND = YES;
174 | ENABLE_TESTABILITY = YES;
175 | GCC_C_LANGUAGE_STANDARD = gnu99;
176 | GCC_DYNAMIC_NO_PIC = NO;
177 | GCC_NO_COMMON_BLOCKS = YES;
178 | GCC_OPTIMIZATION_LEVEL = 0;
179 | GCC_PREPROCESSOR_DEFINITIONS = (
180 | "DEBUG=1",
181 | "$(inherited)",
182 | );
183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
185 | GCC_WARN_UNDECLARED_SELECTOR = YES;
186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
187 | GCC_WARN_UNUSED_FUNCTION = YES;
188 | GCC_WARN_UNUSED_VARIABLE = YES;
189 | MACOSX_DEPLOYMENT_TARGET = 10.12;
190 | MTL_ENABLE_DEBUG_INFO = YES;
191 | ONLY_ACTIVE_ARCH = YES;
192 | SDKROOT = macosx;
193 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
194 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
195 | };
196 | name = Debug;
197 | };
198 | 61DEED491F6BFDFF00958DAF /* Release */ = {
199 | isa = XCBuildConfiguration;
200 | buildSettings = {
201 | ALWAYS_SEARCH_USER_PATHS = NO;
202 | CLANG_ANALYZER_NONNULL = YES;
203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
205 | CLANG_CXX_LIBRARY = "libc++";
206 | CLANG_ENABLE_MODULES = YES;
207 | CLANG_ENABLE_OBJC_ARC = YES;
208 | CLANG_WARN_BOOL_CONVERSION = YES;
209 | CLANG_WARN_CONSTANT_CONVERSION = YES;
210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
211 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
212 | CLANG_WARN_EMPTY_BODY = YES;
213 | CLANG_WARN_ENUM_CONVERSION = YES;
214 | CLANG_WARN_INFINITE_RECURSION = YES;
215 | CLANG_WARN_INT_CONVERSION = YES;
216 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
217 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
218 | CLANG_WARN_UNREACHABLE_CODE = YES;
219 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
220 | CODE_SIGN_IDENTITY = "-";
221 | COPY_PHASE_STRIP = NO;
222 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
223 | ENABLE_NS_ASSERTIONS = NO;
224 | ENABLE_STRICT_OBJC_MSGSEND = YES;
225 | GCC_C_LANGUAGE_STANDARD = gnu99;
226 | GCC_NO_COMMON_BLOCKS = YES;
227 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
228 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
229 | GCC_WARN_UNDECLARED_SELECTOR = YES;
230 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
231 | GCC_WARN_UNUSED_FUNCTION = YES;
232 | GCC_WARN_UNUSED_VARIABLE = YES;
233 | MACOSX_DEPLOYMENT_TARGET = 10.12;
234 | MTL_ENABLE_DEBUG_INFO = NO;
235 | SDKROOT = macosx;
236 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
237 | };
238 | name = Release;
239 | };
240 | 61DEED4B1F6BFDFF00958DAF /* Debug */ = {
241 | isa = XCBuildConfiguration;
242 | buildSettings = {
243 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
244 | CODE_SIGN_IDENTITY = "Mac Developer";
245 | COMBINE_HIDPI_IMAGES = YES;
246 | DEVELOPMENT_TEAM = 5X78EYG9RH;
247 | INFOPLIST_FILE = TextButler/Info.plist;
248 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
249 | PRODUCT_BUNDLE_IDENTIFIER = com.enigmeta.TextButler;
250 | PRODUCT_NAME = "$(TARGET_NAME)";
251 | PROVISIONING_PROFILE_SPECIFIER = "";
252 | SWIFT_VERSION = 3.0;
253 | };
254 | name = Debug;
255 | };
256 | 61DEED4C1F6BFDFF00958DAF /* Release */ = {
257 | isa = XCBuildConfiguration;
258 | buildSettings = {
259 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
260 | CODE_SIGN_IDENTITY = "Mac Developer";
261 | COMBINE_HIDPI_IMAGES = YES;
262 | DEVELOPMENT_TEAM = 5X78EYG9RH;
263 | INFOPLIST_FILE = TextButler/Info.plist;
264 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
265 | PRODUCT_BUNDLE_IDENTIFIER = com.enigmeta.TextButler;
266 | PRODUCT_NAME = "$(TARGET_NAME)";
267 | PROVISIONING_PROFILE_SPECIFIER = "";
268 | SWIFT_VERSION = 3.0;
269 | };
270 | name = Release;
271 | };
272 | /* End XCBuildConfiguration section */
273 |
274 | /* Begin XCConfigurationList section */
275 | 61DEED361F6BFDFF00958DAF /* Build configuration list for PBXProject "TextButler" */ = {
276 | isa = XCConfigurationList;
277 | buildConfigurations = (
278 | 61DEED481F6BFDFF00958DAF /* Debug */,
279 | 61DEED491F6BFDFF00958DAF /* Release */,
280 | );
281 | defaultConfigurationIsVisible = 0;
282 | defaultConfigurationName = Release;
283 | };
284 | 61DEED4A1F6BFDFF00958DAF /* Build configuration list for PBXNativeTarget "TextButler" */ = {
285 | isa = XCConfigurationList;
286 | buildConfigurations = (
287 | 61DEED4B1F6BFDFF00958DAF /* Debug */,
288 | 61DEED4C1F6BFDFF00958DAF /* Release */,
289 | );
290 | defaultConfigurationIsVisible = 0;
291 | defaultConfigurationName = Release;
292 | };
293 | /* End XCConfigurationList section */
294 | };
295 | rootObject = 61DEED331F6BFDFF00958DAF /* Project object */;
296 | }
297 |
--------------------------------------------------------------------------------
/TextButler.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TextButler/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import Carbon.HIToolbox
3 |
4 | @NSApplicationMain
5 | class AppDelegate: NSObject, NSApplicationDelegate {
6 |
7 | @IBOutlet weak var menu: NSMenu!
8 |
9 | var currentKeyStream: String = ""
10 | var snippets = [String:String]()
11 | var longestShortcutLength = 0
12 | var statusItem: NSStatusItem!
13 | var monitoringEnabled = false
14 | var eventMonitor: Any!
15 |
16 | func _postKeyEvent(_ keyCode: Int, keyDown: Bool) {
17 | let eventSource = CGEventSource.init(stateID: CGEventSourceStateID.hidSystemState)
18 | let keyEvent = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(keyCode), keyDown: keyDown)
19 | let loc = CGEventTapLocation.cghidEventTap
20 | keyEvent!.post(tap: loc)
21 | }
22 |
23 | func typeKey(keyCode: Int) {
24 | _postKeyEvent(keyCode, keyDown: true)
25 | _postKeyEvent(keyCode, keyDown: false)
26 | }
27 |
28 | func typeLetter(c: UniChar) {
29 | let slice: [UniChar] = [c]
30 | let eventSource = CGEventSource.init(stateID: CGEventSourceStateID.hidSystemState)
31 | let keyEvent = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(0), keyDown: true)
32 | keyEvent?.keyboardSetUnicodeString(stringLength: 1, unicodeString: Array(slice))
33 | let loc = CGEventTapLocation.cghidEventTap
34 | keyEvent!.post(tap: loc)
35 | }
36 |
37 | func typeString(_ text: String) {
38 | for c in text.utf16 {
39 | if c == 10 {
40 | typeKey(keyCode: kVK_Return)
41 | } else {
42 | typeLetter(c: c)
43 | }
44 | }
45 | }
46 |
47 | func onGlobalKeyDown(_ event: NSEvent) {
48 | let text = event.characters!
49 | currentKeyStream += text
50 |
51 | // Keep only as much characters as needed.
52 | if currentKeyStream.characters.count > longestShortcutLength {
53 | let endIndex = currentKeyStream.index(currentKeyStream.endIndex, offsetBy: -longestShortcutLength)
54 | currentKeyStream.removeSubrange(currentKeyStream.startIndex.. URL {
99 | let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
100 | let appDir = documentsDir.appendingPathComponent("TextButler")
101 | return appDir
102 | }
103 |
104 | func snippetsFile() -> URL {
105 | let file = appDir().appendingPathComponent("snippets.json")
106 | return file
107 | }
108 |
109 | func ensureAppDirectory() {
110 | let dir = appDir()
111 | if !FileManager.default.fileExists(atPath: dir.path) {
112 | do {
113 | try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)
114 | } catch let error {
115 | print("Could not create TextButler directory (\(dir.path)): \(error.localizedDescription)")
116 | }
117 | }
118 | }
119 |
120 | func ensureSnippetsFile() {
121 | let file = snippetsFile()
122 | if !FileManager.default.fileExists(atPath: file.path) {
123 | let defaultFile = Bundle.main.url(forResource: "default-snippets", withExtension: "json")!
124 | do {
125 | try FileManager.default.copyItem(at: defaultFile, to: file)
126 | } catch let error {
127 | print("Could not copy file \(defaultFile) to \(file): \(error.localizedDescription)")
128 | }
129 | }
130 | }
131 |
132 | func reloadSnippetsFile(showNotification: Bool = false) {
133 | let file = snippetsFile()
134 | snippets.removeAll()
135 | let str: String
136 | do {
137 | str = try String(contentsOf: file, encoding: String.Encoding.utf8)
138 | } catch {
139 | print("Failed to load snippets.json: \(error.localizedDescription)")
140 | str = "[]"
141 | }
142 | let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!
143 | do {
144 | let json = try JSONSerialization.jsonObject(with: data, options: []) as! [AnyObject]
145 | longestShortcutLength = 0
146 | for item in json {
147 | let shortcut = item["shortcut"] as! String
148 | let text = item["text"] as! String
149 | snippets[shortcut] = text
150 | if shortcut.characters.count > longestShortcutLength {
151 | longestShortcutLength = shortcut.characters.count
152 | }
153 | }
154 | } catch let error {
155 | print("Failed to load: \(error.localizedDescription)")
156 | }
157 |
158 | if showNotification {
159 | let notification = NSUserNotification()
160 | notification.title = "TextButler"
161 | notification.informativeText = "Reloaded snippets file."
162 | NSUserNotificationCenter.default.deliver(notification)
163 | }
164 | }
165 |
166 | func startWatchingSnippetsFile() {
167 |
168 | func callback(
169 | _ stream: ConstFSEventStreamRef,
170 | clientCallbackInfo: UnsafeMutableRawPointer?,
171 | numEvents: Int,
172 | eventPaths: UnsafeMutableRawPointer,
173 | eventFlags: UnsafePointer?,
174 | eventIDs: UnsafePointer?) -> Void {
175 | //if let paths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] {
176 | // print("paths \(paths)")
177 | //}
178 | let appDelegate = unsafeBitCast(clientCallbackInfo, to: AppDelegate.self)
179 | appDelegate.reloadSnippetsFile(showNotification: true)
180 | }
181 |
182 | var context = FSEventStreamContext()
183 | context.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
184 | let paths = [snippetsFile().path]
185 |
186 | let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents)
187 |
188 | let stream = FSEventStreamCreate(kCFAllocatorDefault, callback, &context, paths as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), TimeInterval(0.2), flags)!
189 | FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
190 | FSEventStreamStart(stream)
191 | }
192 |
193 | func enableMonitor() {
194 | if !monitoringEnabled {
195 | eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: NSEventMask.keyDown, handler:onGlobalKeyDown)!
196 | monitoringEnabled = true
197 | }
198 | }
199 |
200 | func disableMonitor() {
201 | monitoringEnabled = false
202 | NSEvent.removeMonitor(eventMonitor)
203 |
204 | }
205 |
206 | @IBAction func toggleEnabled(_ sender: AnyObject) {
207 | let enabledMenuItem = self.menu.item(at: 0)!
208 | if monitoringEnabled {
209 | disableMonitor()
210 | enabledMenuItem.state = NSOffState
211 | } else {
212 | enableMonitor()
213 | enabledMenuItem.state = NSOnState
214 | }
215 | }
216 |
217 | @IBAction func openSnippetsFile(_ sender: AnyObject) {
218 | let p = Process()
219 | p.launchPath = "/usr/bin/open"
220 | p.arguments = ["-e", snippetsFile().path]
221 | p.launch()
222 | }
223 |
224 | @IBAction func quit(_ sender: AnyObject) {
225 | NSApp.terminate(self)
226 | }
227 |
228 | func applicationDidFinishLaunching(_ aNotification: Notification) {
229 | checkIfAccessibilityEnabled()
230 | ensureAppDirectory()
231 | ensureSnippetsFile()
232 | reloadSnippetsFile()
233 | startWatchingSnippetsFile()
234 | initializeStatusMenu()
235 | enableMonitor()
236 | }
237 |
238 | func applicationWillTerminate(_ aNotification: Notification) {
239 | // Insert code here to tear down your application
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon-16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon-32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "icon-33.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "icon-64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "icon-128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "icon-257.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "icon-256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "icon-512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "icon-513.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "icon-1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-1024.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-128.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-16.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-256.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-257.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-257.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-32.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-33.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-512.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-513.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-513.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/AppIcon.appiconset/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/AppIcon.appiconset/icon-64.png
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/MenuIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "menu-icon.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/TextButler/Assets.xcassets/MenuIcon.imageset/menu-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/TextButler/Assets.xcassets/MenuIcon.imageset/menu-icon.png
--------------------------------------------------------------------------------
/TextButler/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 0.2
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2017 Frederik De Bleser. All rights reserved.
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/TextButler/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
690 |
691 |
697 |
698 |
704 |
705 |
706 |
707 |
708 |
--------------------------------------------------------------------------------
/TextButler/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // TextButler
4 | //
5 | // Created by fdb on 15/09/2017.
6 | // Copyright © 2017 fdb. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class ViewController: NSViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | // Do any additional setup after loading the view.
17 | }
18 |
19 | override var representedObject: Any? {
20 | didSet {
21 | // Update the view, if already loaded.
22 | }
23 | }
24 |
25 |
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/TextButler/default-snippets.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "shortcut": ";sig",
4 | "text": "Best Regards,\n\nJohn Doe"
5 | },
6 | {
7 | "shortcut": ";em",
8 | "text": "johndoe@example.com"
9 | },
10 | {
11 | "shortcut": ";tel",
12 | "text": "+1 555 123 5678"
13 | },
14 | {
15 | "shortcut": ",dt",
16 | "text": "\n\n\n \n \n Document\n\n\n\n\n\n"
17 | }
18 | ]
19 |
--------------------------------------------------------------------------------
/artwork/icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon-1024.png
--------------------------------------------------------------------------------
/artwork/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon-128.png
--------------------------------------------------------------------------------
/artwork/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon-16.png
--------------------------------------------------------------------------------
/artwork/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon-256.png
--------------------------------------------------------------------------------
/artwork/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon-32.png
--------------------------------------------------------------------------------
/artwork/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon-512.png
--------------------------------------------------------------------------------
/artwork/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon-64.png
--------------------------------------------------------------------------------
/artwork/icon.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/icon.ai
--------------------------------------------------------------------------------
/artwork/menu-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/menu-icon.png
--------------------------------------------------------------------------------
/artwork/menubar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fdb/textbutler/f76bd5cd0c117aa230071bb6215501c4f01b1f50/artwork/menubar.png
--------------------------------------------------------------------------------