├── .gitignore ├── Images └── tiny-graphical-edtior.png ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md └── Sources └── Cedar ├── AppDelegate.swift ├── Cedar.swift ├── Editor.swift ├── File.swift └── Menu.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | *.swiftpm 7 | -------------------------------------------------------------------------------- /Images/tiny-graphical-edtior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielctull-apps/cedar/223391a630398ca8c585fb5e254c751831c6432f/Images/tiny-graphical-edtior.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Daniel Tull 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 7 | "state": { 8 | "branch": null, 9 | "revision": "82905286cc3f0fa8adc4674bf49437cab65a8373", 10 | "version": "1.1.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Cedar", 7 | platforms: [ 8 | .macOS(.v12), 9 | ], 10 | products: [ 11 | .executable(name: "cedar", targets: ["Cedar"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), 15 | ], 16 | targets: [ 17 | .executableTarget(name: "Cedar", dependencies: [ 18 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 19 | ]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cedar 2 | 3 | A tiny graphical text editor for use from the command line. 4 | 5 | ![tiny-graphical-edtior](Images/tiny-graphical-edtior.png) 6 | 7 | ## Installation 8 | 9 | I recommend using the awesome utility [Mint](https://github.com/yonaskolb/Mint) to install cedar. 10 | 11 | ```sh 12 | mint install danielctull-apps/cedar 13 | ``` 14 | 15 | ## Usage 16 | 17 | cedar can only be run from the command line and it takes just one argument, the file you wish to open. 18 | 19 | ```sh 20 | cedar 21 | ``` 22 | 23 | ### Using as git editor 24 | 25 | cedar is designed to open really fast so is perfect for using as a git editor. If you wish to use cedar as your editor of choice when running those beloved commands [`git commit`](https://www.git-scm.com/docs/git-commit) or [`git rebase -i`](https://www.git-scm.com/docs/git-rebase), perform the following command and it will be set in your global [git configuration](https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) file. 26 | 27 | ```sh 28 | git config --global core.editor cedar 29 | ``` 30 | 31 | ## Thanks 32 | 33 | There's code in this little app that takes heavy inspiration from the following. Thank you so much. 🧡 34 | 35 | * [Chris Eidhof](https://github.com/chriseidhof) for his [boilerplate.swift](https://gist.github.com/chriseidhof/26768f0b63fa3cdf8b46821e099df5ff) gist which shows all the configuration needed to get a macOS app running from the command line. 36 | * [Matt Gallagher](https://twitter.com/cocoawithlove) for his blog post [Minimalist Cocoa programming](https://www.cocoawithlove.com/2010/09/minimalist-cocoa-programming.html) which was useful background information to Chris' boilerplate gist. 37 | -------------------------------------------------------------------------------- /Sources/Cedar/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Inspired by Chris Eidhof 2 | // https://gist.github.com/chriseidhof/26768f0b63fa3cdf8b46821e099df5ff 3 | 4 | import AppKit 5 | import SwiftUI 6 | 7 | final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { 8 | 9 | let file: File 10 | var window: NSWindow? 11 | 12 | init(file: File) { 13 | self.file = file 14 | } 15 | 16 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 17 | true 18 | } 19 | 20 | func applicationDidFinishLaunching(_ notification: Notification) { 21 | 22 | let editor = Editor(text: file.binding) 23 | .frame(maxWidth: .infinity, maxHeight: .infinity) 24 | 25 | window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), 26 | styleMask: [.closable, 27 | .fullSizeContentView, 28 | .miniaturizable, 29 | .resizable, 30 | .titled], 31 | backing: .buffered, 32 | defer: false) 33 | window?.center() 34 | window?.setFrameAutosaveName(file.name) 35 | window?.contentView = NSHostingView(rootView: editor) 36 | window?.makeKeyAndOrderFront(nil) 37 | window?.delegate = self 38 | window?.titleVisibility = .hidden 39 | window?.titlebarAppearsTransparent = true 40 | window?.toolbar?.isVisible = true 41 | 42 | NSApp.activate(ignoringOtherApps: true) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Cedar/Cedar.swift: -------------------------------------------------------------------------------- 1 | 2 | import AppKit 3 | import ArgumentParser 4 | 5 | @main 6 | struct Cedar: ParsableCommand { 7 | 8 | @Argument(help: "The file to open.") 9 | var file: File 10 | 11 | func run() throws { 12 | let app = NSApplication.shared 13 | NSApp.setActivationPolicy(.accessory) 14 | let delegate = AppDelegate(file: file) 15 | app.delegate = delegate 16 | app.menu = .cedar 17 | app.run() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Cedar/Editor.swift: -------------------------------------------------------------------------------- 1 | 2 | import SwiftUI 3 | 4 | struct Editor: View { 5 | 6 | private let font = Font.system(size: 17, weight: .light, design: .monospaced) 7 | 8 | private let text: Binding 9 | init(text: Binding) { 10 | self.text = text 11 | } 12 | 13 | var body: some View { 14 | ZStack { 15 | Color.white 16 | .edgesIgnoringSafeArea(.all) 17 | TextEditor(text: text) 18 | .disableAutocorrection(false) 19 | .font(font) 20 | .edgesIgnoringSafeArea([.leading, .trailing, .bottom]) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Cedar/File.swift: -------------------------------------------------------------------------------- 1 | 2 | import ArgumentParser 3 | import Foundation 4 | import SwiftUI 5 | 6 | struct File: ExpressibleByArgument { 7 | 8 | private let url: URL 9 | let name: String 10 | 11 | init?(argument: String) { 12 | guard let directory = Process().currentDirectoryURL else { return nil } 13 | url = URL(fileURLWithPath: argument, relativeTo: directory) 14 | name = url.lastPathComponent 15 | } 16 | } 17 | 18 | extension File { 19 | 20 | var binding: Binding { 21 | Binding(get: content, set: setContent) 22 | } 23 | 24 | private func content() -> String { 25 | guard let data = try? Data(contentsOf: url) else { return "" } 26 | guard let string = String(data: data, encoding: .utf8) else { return "" } 27 | return string 28 | } 29 | 30 | private func setContent(_ content: String) { 31 | guard let data = content.data(using: .utf8) else { return } 32 | try? data.write(to: url) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Cedar/Menu.swift: -------------------------------------------------------------------------------- 1 | 2 | import AppKit 3 | 4 | extension NSMenu { 5 | 6 | static var cedar: NSMenu { 7 | 8 | let name = ProcessInfo.processInfo.processName 9 | 10 | let app = NSMenuItem() 11 | app.submenu = NSMenu() 12 | app.submenu?.items = [ 13 | NSMenuItem(title: "Quit \(name)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"), 14 | ] 15 | 16 | let edit = NSMenuItem() 17 | edit.submenu = NSMenu(title: "Edit") 18 | edit.submenu?.items = [ 19 | NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"), 20 | NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"), 21 | NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"), 22 | NSMenuItem.separator(), 23 | NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"), 24 | ] 25 | 26 | let window = NSMenuItem() 27 | window.submenu = NSMenu(title: "Window") 28 | window.submenu?.items = [ 29 | NSMenuItem(title: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w"), 30 | ] 31 | 32 | let menu = NSMenu(title: "Main Menu") 33 | menu.items = [app, edit, window] 34 | return menu 35 | } 36 | } 37 | --------------------------------------------------------------------------------