├── .gitignore ├── .swiftformat.yml ├── .swiftlint.yml ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Core │ ├── Config │ │ ├── Language.swift │ │ └── Swift.swift │ ├── Direction.swift │ ├── Document.swift │ ├── Editor.swift │ ├── Helpers │ │ ├── CaseAccessable.swift │ │ └── Loger.swift │ ├── Highligher │ │ ├── Highlighter.swift │ │ ├── Token.swift │ │ └── Tokenizer.swift │ ├── Position.swift │ ├── Row.swift │ └── Terminal │ │ ├── Event.swift │ │ ├── EventParser.swift │ │ ├── EventReader.swift │ │ ├── SignalInterceptor.swift │ │ ├── Terminal.swift │ │ ├── TerminalStyle.swift │ │ └── Timeout.swift └── ax-editor │ ├── OpenCommand.swift │ └── main.swift ├── Tests ├── LinuxMain.swift └── ax-editorTests │ ├── XCTestManifests.swift │ └── ax_editorTests.swift └── assets └── demo.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | /Dependencies 7 | -------------------------------------------------------------------------------- /.swiftformat.yml: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --symlinks ignore 4 | --exclude Tests/XCTestManifests.swift,projects/tuist/fixtures/,projects/tuist/features/,Project.swift 5 | 6 | # format options 7 | 8 | --allman false 9 | --binarygrouping 4,8 10 | --commas always 11 | --comments indent 12 | --decimalgrouping 3,6 13 | --elseposition same-line 14 | --empty void 15 | --exponentcase lowercase 16 | --exponentgrouping disabled 17 | --fractiongrouping disabled 18 | --header ignore 19 | --hexgrouping 4,8 20 | --hexliteralcase uppercase 21 | --ifdef indent 22 | --indent 4 23 | --indentcase false 24 | --importgrouping testable-bottom 25 | --linebreaks lf 26 | --octalgrouping 4,8 27 | --operatorfunc spaced 28 | --patternlet hoist 29 | --ranges spaced 30 | --self remove 31 | --semicolons inline 32 | --stripunusedargs always 33 | --trimwhitespace always 34 | --wraparguments preserve 35 | --wrapcollections preserve 36 | --wraparguments before-first -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: # some rules are only opt-in 2 | - empty_count 3 | - redundant_nil_coalescing 4 | - force_unwrapping 5 | - closure_spacing 6 | - implicitly_unwrapped_optional 7 | - sorted_imports 8 | - valid_docs 9 | - nslocalizedstring_require_bundle 10 | 11 | included: 12 | - Sources 13 | 14 | excluded: # paths to ignore during linting. Takes precedence over `included`. 15 | - Tests 16 | 17 | line_length: 18 | ignores_interpolated_strings: true 19 | ignores_comments: true 20 | warning: 150 21 | error: 170 22 | identifier_name: 23 | min_length: 24 | error: 1 25 | warning: 1 26 | inclusive_language: 27 | override_allowed_terms: 28 | - masterKey 29 | type_name: 30 | min_length: 31 | error: 1 32 | warning: 1 33 | 34 | disabled_rules: # rule identifiers to exclude from running 35 | - trailing_whitespace 36 | - type_name 37 | - identifier_name 38 | - switch_case_on_newline 39 | - switch_case_alignment -------------------------------------------------------------------------------- /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": "986d191f94cec88f6350056da59c2e59e83d1229", 10 | "version": "0.4.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-log", 15 | "repositoryURL": "https://github.com/apple/swift-log.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", 19 | "version": "1.4.2" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ax-editor", 8 | products: [ 9 | .executable(name: "ax", targets: ["ax-editor"]) 10 | ], 11 | 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"), 14 | .package(url: "https://github.com/apple/swift-log.git", from: "1.2.0"), 15 | ], 16 | 17 | targets: [ 18 | .target( 19 | name: "Core", 20 | dependencies: [ 21 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 22 | .product(name: "Logging", package: "swift-log") 23 | ]), 24 | .target( 25 | name: "ax-editor", 26 | dependencies: [ 27 | "Core", 28 | ] 29 | ), 30 | .testTarget( 31 | name: "ax-editorTests", 32 | dependencies: ["ax-editor"]) 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # AX Editor 4 | 5 | 6 | 7 | A light weigt text editor with syntax highlighting. It is written completly in Swift using ANSI Escape Sequnces. **It is still not complete and buggy (work in progress) contributions are warmly welcomed 🙌** 8 | 9 | ![enter image description here](https://github.com/engali94/ax-editor/blob/master/assets/demo.png) 10 | 11 | 12 | # Installation 13 | 14 | - Clone and `cd` into the repository 15 | 16 | - Run `swift package generate-xcodeproj` 17 | 18 | - Run the following command to try it out: 19 | 20 | 21 | 22 | ```bash 23 | 24 | swift run ax --help 25 | 26 | ``` 27 | 28 | # Usage 29 | 30 | 31 | 32 | 33 | 34 | #### Opening files in Ax 35 | 36 | 37 | 38 | At the moment, you can open ax editor by using the command 39 | 40 | ```sh 41 | 42 | swift run ax 43 | 44 | ``` 45 | 46 | 47 | 48 | This will open up an empty document. 49 | 50 | 51 | 52 | If you wish to open a file straight from the command line, you can run 53 | 54 | ```sh 55 | 56 | swift run ax /path/to/file 57 | 58 | ``` 59 | 60 | To open and edit a file. 61 | 62 | 63 | 64 | 65 | #### Moving the cursor around 66 | 67 | 68 | 69 | You can use the arrow keys to move the cursor around 70 | 71 | 72 | 73 | You can also use: 74 | 75 | - PageUp - Go to the top of the document 76 | 77 | - PageDown - Go to the bottom of the document 78 | 79 | - Home - Go to the start of the current line 80 | 81 | - End - Go to the end of the current line 82 | 83 | 84 | 85 | #### Editing the file 86 | 87 | 88 | 89 | You can use the keys Backspace and Return / Enter as well as all the characters on your keyboard to edit files! 90 | 91 | 92 | 93 | 94 | OAx is controlled via your keyboard shortcuts. Here are the default shortcuts that you can use: 95 | 96 | 97 | 98 | | Keybinding | What it does | 99 | 100 | | Keybinding | What it does | 101 | | ------------ | ------------ | 102 | | `Ctrl + D` | Exits the the editor. | 103 | | `Ctrl + S` | Saves the open file to the disk **(To be Implemented)**. | 104 | | `Ctrl + F` | Searches the document for a search query. Allows pressing of and to move the cursor to the previous occurance fof the query and and to move to the next occurance of the query. Press Return to cancel the search at the current cursor position or Esc to cancel the search and return to the initial location of the cursor. Note: this allows you to use regular expressions. **(To be Implemented)**. | 105 | | `Ctrl + U` | Undoes your last action. The changes are committed to the undo stack every time you press the space bar, create / destroy a new line and when there is no activity after a certain period of time which can be used to capture points where you pause for thought or grab a coffee etc... | 106 | | `Ctrl + R` | Redoes your last action. The changes are committed to the undo stack every time you press the space bar, create / destroy a new line and when there is no activity after a certain period of time which can be used to capture points where you pause for thought or grab a coffee etc... | 107 | | `Ctrl + F` | Allows replacing of occurances in the document. Uses the same keybindings as the search feature: and to move the cursor to the previous occurance fof the query and and to move to the next occurance of the query. You can also press Return, y or Space to carry out the replace action. To exit replace mode once you're finished, you can press Esc to cancel and return back to your initial cursor position. Note: this allows you to use regular expressions.**(To be Implemented)**.| 108 | | `Ctrl + A` | Carries out a batch replace option. It will prompt you for a target to replace and what you want to replace it with and will then replace every occurance in the document. Note: this allows you to use regular expressions. **(To be Implemented)**.| 109 | 110 | 111 | 112 | # TODO 113 | 114 | - [X] Basic editing functions 115 | 116 | - [X] Line numbers 117 | 118 | - [X] Undo and Redo 119 | 120 | - [X] Syntax highlighting 121 | 122 | - [X] Loading files 123 | 124 | - [ ] Saving files 125 | 126 | - [ ] Searching and replacing 127 | 128 | - [ ] Command line bar 129 | 130 | - [ ] Status bar 131 | 132 | - [ ] Config files 133 | 134 | - [ ] Tabs for multitasking 135 | 136 | - [ ] Auto indentation 137 | 138 | - [ ] Prettifier / Automatic code formatter 139 | 140 | - [ ] Built In linters 141 | 142 | - [ ] Auto brackets 143 | 144 | - [ ] Auto complete 145 | 146 | - [ ] File tree 147 | 148 | - [ ] Start page 149 | 150 | # Contributing 151 | 152 | Contributions are warmly welcomed 🙌 153 | 154 | 155 | 156 | # Credits 157 | Thanks to all the authors and contributers of the following tools: 158 | [ColorizeSwift](https://github.com/mtynior/ColorizeSwift) 159 | [CrossTerm](https://github.com/crossterm-rs/crossterm) 160 | [Ox Editor](https://github.com/curlpipe/ox) 161 | 162 | # Licence 163 | 164 | It is released under the MIT license, see [Licence]() 165 | -------------------------------------------------------------------------------- /Sources/Core/Config/Language.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Language: Decodable { 10 | public typealias Keyword = String 11 | 12 | public let name: String 13 | public let icon: String 14 | public let extensions: [String] 15 | public let keywords: [Keyword] 16 | public let defentions: [Defintions] 17 | } 18 | 19 | public extension Language { 20 | typealias RegularExpression = String 21 | 22 | enum Defintions: Decodable { 23 | case comments(RegularExpression) 24 | case strings(RegularExpression) 25 | case numbers(RegularExpression) 26 | case types(RegularExpression) 27 | case functions(RegularExpression) 28 | case operators(RegularExpression) 29 | case attributes(RegularExpression) 30 | case dotAccess(RegularExpression) 31 | case properties(RegularExpression) 32 | case headers(RegularExpression) 33 | case macros(RegularExpression) 34 | case symbols(RegularExpression) 35 | 36 | public init(from decoder: Decoder) throws { 37 | fatalError("unimplemented") 38 | } 39 | } 40 | } 41 | 42 | extension Language.Defintions: CaseAccessible { 43 | public var regexp: String { 44 | associatedValue() ?? "" 45 | } 46 | } 47 | 48 | public struct Theme: Decodable { 49 | public typealias Highlights = TokenType 50 | 51 | public let backgroundColor: Color 52 | public let textColor: Color 53 | public let highlights: [Highlights: Color] 54 | } 55 | 56 | extension TokenType: Decodable { } 57 | 58 | extension Theme { 59 | public static let vsCode = Theme( 60 | backgroundColor: .init(r: 40, g: 44, b: 52), 61 | textColor: .init(r: 171, g: 178, b: 191), 62 | highlights: [ 63 | .keyword: .init(r: 16, g: 177, b: 254), 64 | .comment: .init(r: 99, g: 109, b: 131), 65 | .string: .init(r: 249, g: 200, b: 89), 66 | .type: .init(r: 255, g: 100, b: 128), 67 | .number: .init(r: 255, g: 120, b: 248), 68 | .property: .init(r: 206, g: 152, b: 254), 69 | .dotAccess: .init(r: 255, g: 147, b: 206), 70 | .preprocessing: .init(r: 255, g: 147, b: 106), 71 | .attribute: .init(r: 255, g: 147, b: 106), 72 | .operator: .init(r: 122, g: 130, b: 218), 73 | .methodCall: .init(r: 63, g: 197, b: 107) 74 | ]) 75 | } 76 | 77 | public struct Color: Decodable { 78 | public let r: UInt8 79 | public let g: UInt8 80 | public let b: UInt8 81 | } 82 | 83 | public struct Config: Decodable { 84 | public let tabWidth: Int 85 | public let theme: Theme 86 | public let languages: [Language] 87 | } 88 | 89 | extension Config { 90 | public static let `default` = Config(tabWidth: 4, theme: Theme.vsCode, languages: [swift]) 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Core/Config/Swift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Swift.swift 3 | // Core 4 | // 5 | // Created by Ali on 1.01.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public let swift = Language( 11 | name: "Swift", 12 | icon: " ", 13 | extensions: ["swift"], 14 | keywords: [ 15 | "as", 16 | "associatedtype", 17 | "break", 18 | "case", 19 | "catch", 20 | "class", 21 | "continue", 22 | "convenience", 23 | "default", 24 | "defer", 25 | "deinit", 26 | "else", 27 | "enum", 28 | "extension", 29 | "fallthrough", 30 | "false", 31 | "fileprivate", 32 | "final", 33 | "for", 34 | "func", 35 | "get", 36 | "guard", 37 | "if", 38 | "import", 39 | "in", 40 | "init", 41 | "inout", 42 | "internal", 43 | "is", 44 | "lazy", 45 | "let", 46 | "mutating", 47 | "nil", 48 | "nonmutating", 49 | "open", 50 | "operator", 51 | "override", 52 | "private", 53 | "protocol", 54 | "public", 55 | "repeat", 56 | "required", 57 | "rethrows", 58 | "return", 59 | "required", 60 | "self", 61 | "set", 62 | "static", 63 | "struct", 64 | "subscript", 65 | "super", 66 | "switch", 67 | "throw", 68 | "throws", 69 | "true", 70 | "try", 71 | "typealias", 72 | "unowned", 73 | "var", 74 | "weak", 75 | "where", 76 | "while", "Any", 77 | "Array", 78 | "AutoreleasingUnsafePointer", 79 | "BidirectionalReverseView", 80 | "Bit", 81 | "Bool", 82 | "CFunctionPointer", 83 | "COpaquePointer", 84 | "CVaListPointer", 85 | "Character", 86 | "CollectionOfOne", 87 | "ConstUnsafePointer", 88 | "ContiguousArray", 89 | "Data", 90 | "Dictionary", 91 | "DictionaryGenerator", 92 | "DictionaryIndex", 93 | "Double", 94 | "EmptyCollection", 95 | "EmptyGenerator", 96 | "EnumerateGenerator", 97 | "FilterCollectionView", 98 | "FilterCollectionViewIndex", 99 | "FilterGenerator", 100 | "FilterSequenceView", 101 | "Float", 102 | "Float80", 103 | "FloatingPointClassification", 104 | "GeneratorOf", 105 | "GeneratorOfOne", 106 | "GeneratorSequence", 107 | "HeapBuffer", 108 | "HeapBuffer", 109 | "HeapBufferStorage", 110 | "HeapBufferStorageBase", 111 | "ImplicitlyUnwrappedOptional", 112 | "IndexingGenerator", 113 | "Int", 114 | "Int16", 115 | "Int32", 116 | "Int64", 117 | "Int8", 118 | "IntEncoder", 119 | "LazyBidirectionalCollection", 120 | "LazyForwardCollection", 121 | "LazyRandomAccessCollection", 122 | "LazySequence", 123 | "Less", 124 | "MapCollectionView", 125 | "MapSequenceGenerator", 126 | "MapSequenceView", 127 | "MirrorDisposition", 128 | "ObjectIdentifier", 129 | "OnHeap", 130 | "Optional", 131 | "PermutationGenerator", 132 | "QuickLookObject", 133 | "RandomAccessReverseView", 134 | "Range", 135 | "RangeGenerator", 136 | "RawByte", 137 | "Repeat", 138 | "ReverseBidirectionalIndex", 139 | "Printable", 140 | "ReverseRandomAccessIndex", 141 | "SequenceOf", 142 | "SinkOf", 143 | "Slice", 144 | "StaticString", 145 | "StrideThrough", 146 | "StrideThroughGenerator", 147 | "StrideTo", 148 | "StrideToGenerator", 149 | "String", 150 | "Index", 151 | "UTF8View", 152 | "Index", 153 | "UnicodeScalarView", 154 | "IndexType", 155 | "GeneratorType", 156 | "UTF16View", 157 | "UInt", 158 | "UInt16", 159 | "UInt32", 160 | "UInt64", 161 | "UInt8", 162 | "UTF16", 163 | "UTF32", 164 | "UTF8", 165 | "UnicodeDecodingResult", 166 | "UnicodeScalar", 167 | "Unmanaged", 168 | "UnsafeArray", 169 | "UnsafeArrayGenerator", 170 | "UnsafeMutableArray", 171 | "UnsafePointer", 172 | "VaListBuilder", 173 | "Header", 174 | "Zip2", 175 | "ZipGenerator2" 176 | ], 177 | defentions: [ 178 | .comments("//(.*)"), // Single line 179 | .comments("/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/"), // multi-line comment 180 | .numbers("(?<=(\\s|\\[|,|:))(\\d)+"), 181 | .types("\\b\\s*[A-Z]+[a-z]*[A-Z]*\\s*"), 182 | .functions("(/func\\s+([a-z_]*\\w*)\\s*\\(/)"), 183 | .functions("\\w+\\(([a-z0-9_:]*\\w*[,]*)*\\)\\s*"), 184 | .operators("&&|<=|<|>|>=|!=|==|&|OR*|!|[||]{2}|\\|[^->]"), 185 | .attributes("(@).+"), 186 | .dotAccess("\\s*\\.\\w+\\s*?"), 187 | .properties("\\w\\S\\.\\w\\s+"), 188 | .macros("^(#)\\w\\s*") 189 | ]) 190 | -------------------------------------------------------------------------------- /Sources/Core/Direction.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | enum Direction { 10 | case up 11 | case down 12 | case left 13 | case right 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Core/Document.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | public final class Document { 10 | private var rows: [Row] 11 | private(set) var showsWelcome = false 12 | private(set) var lineOffset: Postion = .init(x: 0, y: 0) 13 | private(set) var cursorPosition: Postion = .init(x: 0, y: 0) 14 | private var isDirty = true 15 | private var undoStack = EventStack() 16 | private var redoStack = EventStack() 17 | private var language: Language? = swift 18 | private var tokenizer: Tokenizer = .init(language: swift) 19 | private var name: String = "" 20 | private lazy var highlighter: Highlighter? = Highlighter(tokenizer: tokenizer) 21 | 22 | public init( 23 | rows: [Row], 24 | name: String = "", 25 | language: Language? = nil 26 | ) { 27 | self.rows = rows 28 | self.name = name 29 | self.language = language 30 | showsWelcome = rows.isEmpty 31 | } 32 | 33 | func execute(_ event: Event, commitEvent: Bool = true) { 34 | isDirty = true 35 | func commit(_ event: Event, handler: () -> Void) { 36 | if event.isCommitable && commitEvent { 37 | redoStack.push(event) 38 | handler() 39 | } 40 | } 41 | switch event { 42 | case .insert(let char, let position): 43 | insert(character: char, at: position) 44 | commit(event) { 45 | undoStack.push(.delete(position: cursorPosition)) } 46 | case .delete(let position): 47 | let char = delete(at: position) 48 | commit(event) { 49 | undoStack.push(.insert(char: char, position: cursorPosition)) } 50 | case .insertLineAbove(let position): 51 | rows.insert(Row(text: ""), at: Int(position.y)) 52 | moveCursor(toDirection: .down) 53 | commit(event) { undoStack.push(.deleteLine(position: cursorPosition, direction: .up)) } 54 | case .insertLineBelow(let position): 55 | rows.insert(Row(text: ""), at: Int(position.y + 1)) 56 | moveCursor(toDirection: .down) 57 | moveCursor(toPosition: .init(x: 0, y: cursorPosition.y)) 58 | commit(event) { undoStack.push(.deleteLine(position: cursorPosition, direction: .up)) } 59 | case .splitLine: 60 | splitLine() 61 | commit(event) { undoStack.push(.spliceUp) } 62 | case .spliceUp: 63 | spliceUp() 64 | commit(event) { undoStack.push(.splitLine) } 65 | case .deleteLine(let pos, let dir): 66 | deleteLine(at: pos, direction: dir) 67 | case .moveTo(let direction): 68 | moveCursor(toDirection: direction) 69 | } 70 | } 71 | 72 | func undo() { 73 | if let event = undoStack.pop() { 74 | execute(event, commitEvent: false) 75 | } 76 | } 77 | 78 | func redo() { 79 | if let event = redoStack.pop() { 80 | execute(event, commitEvent: false) 81 | } 82 | } 83 | 84 | func row(atPosition pos: Postion) -> Row { 85 | rows[pos.y] 86 | } 87 | 88 | func row(atIndex index: Int) -> Row? { 89 | rows[safe: index] 90 | } 91 | 92 | func scrollIfNeeded(size: Size) { 93 | if cursorPosition.y >= lineOffset.y + Int(size.rows) - 2 { 94 | lineOffset.y += 1 //cursorPosition.y - Int(size.rows) + 1 95 | } else { 96 | lineOffset.y = max(0, lineOffset.y - 1) 97 | } 98 | } 99 | 100 | func highlight(_ row: Row) -> String { 101 | return highlighter?.highlight(code: row.text) ?? row.text 102 | } 103 | 104 | } 105 | 106 | // MARK: Event 107 | extension Document { 108 | enum Event { 109 | case insert(char: String, position: Postion) 110 | case delete(position: Postion) 111 | case insertLineAbove(position: Postion) 112 | case insertLineBelow(position: Postion) 113 | case splitLine 114 | case spliceUp 115 | case deleteLine(position: Postion, direction: Direction) 116 | case moveTo(direction: Direction) 117 | } 118 | } 119 | 120 | // MARK: - Private Helpers 121 | private extension Document { 122 | func insert(character: String, at position: Postion) { 123 | showsWelcome = false 124 | if rows.isEmpty { 125 | rows.append(Row(text: character)) 126 | moveCursor(toDirection: .right) 127 | return 128 | } 129 | rows[position.y].insert(char: character, at: position.x) 130 | moveCursor(toDirection: .right) 131 | //print(cursorPosition) 132 | } 133 | 134 | func delete(at position: Postion) -> String { 135 | let char = rows[position.y].delete(at: position.x - 1) 136 | moveCursor(toDirection: .left) 137 | return char 138 | } 139 | 140 | func moveCursor(toDirection dir: Direction) { 141 | switch dir { 142 | case .up: 143 | if cursorPosition.y <= 0 { return } 144 | cursorPosition.y -= 1 145 | case .down: 146 | if cursorPosition.y >= rows.count - 1 { return } 147 | cursorPosition.y += 1 148 | case .left: 149 | if cursorPosition.x <= 0 { return } 150 | cursorPosition.x -= 1 151 | case .right: 152 | if cursorPosition.x >= row(atPosition: cursorPosition).length() { return } 153 | cursorPosition.x += 1 154 | } 155 | } 156 | 157 | func moveCursor(toPosition pos: Postion) { 158 | cursorPosition = pos 159 | } 160 | 161 | func insert(row: Row, at rowIndex: Int) { 162 | guard rowIndex <= rows.count else { return } 163 | rows.insert(row, at: rowIndex) 164 | } 165 | 166 | func removeRow(at pos: Postion) { 167 | guard pos.y > 0 else { return } 168 | rows.remove(at: pos.y) 169 | } 170 | 171 | func splitLine() { 172 | let leftStr = row(atPosition: cursorPosition).textUpTo(index: cursorPosition.x) 173 | let rightStr = row(atPosition: cursorPosition).textFrom(index: cursorPosition.x) 174 | row(atPosition: cursorPosition).update(text: leftStr) 175 | insert(row: Row(text: rightStr), at: cursorPosition.y + 1) 176 | undoStack.push(.spliceUp) // Todo add cursor position meta data 177 | moveCursor(toDirection: .down) 178 | moveCursor(toPosition: .init(x: 0, y: cursorPosition.y)) 179 | } 180 | 181 | func spliceUp() { 182 | guard cursorPosition.y > 0 else { return } 183 | let currentLine = row(atPosition: cursorPosition) 184 | let aboveLine = row(atPosition: .init(x: cursorPosition.x, y: cursorPosition.y - 1)) 185 | let newRowText = aboveLine.text + currentLine.text 186 | aboveLine.update(text: newRowText) 187 | removeRow(at: cursorPosition) 188 | moveCursor(toPosition: .init(x: newRowText.count, y: cursorPosition.y - 1)) 189 | } 190 | 191 | func deleteLine(at position: Postion, direction: Direction) { 192 | removeRow(at: position) 193 | moveCursor(toDirection: direction) 194 | let row = self.row(atPosition: cursorPosition) // the previous line 195 | moveCursor(toPosition: .init(x: row.length(), y: cursorPosition.y)) 196 | } 197 | } 198 | 199 | extension Document.Event { 200 | 201 | var isCommitable: Bool { 202 | switch self { 203 | case .delete, .insert, .insertLineAbove, .insertLineBelow, .splitLine, .spliceUp: return true 204 | default: return false 205 | } 206 | } 207 | } 208 | 209 | struct EventStack { 210 | private var events = [Event]() 211 | 212 | var count: Int { 213 | events.count 214 | } 215 | 216 | mutating func push(_ event: Event) { 217 | events.append(event) 218 | } 219 | 220 | mutating func pop() -> Event? { 221 | events.popLast() 222 | } 223 | 224 | mutating func clear() { 225 | events.removeAll() 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Sources/Core/Editor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Darwin 8 | import Foundation 9 | 10 | public final class Editor { 11 | private let terminal: Terminal 12 | private var document: Document 13 | private var size: Size 14 | private var cursorPosition: Postion 15 | private var quit = false 16 | 17 | public init(terminal: Terminal, document: Document) { 18 | self.terminal = terminal 19 | self.document = document 20 | self.size = terminal.getWindowSize() 21 | self.cursorPosition = .init(x: 0, y: 0) 22 | } 23 | 24 | public func run() { 25 | terminal.enableRawMode() 26 | write(STDIN_FILENO, "\u{1b}[?1049h", "\u{1b}[?1049h".utf8.count) 27 | terminal.onWindowSizeChange = { [weak self] newSize in 28 | self?.size = newSize 29 | self?.update() 30 | } 31 | 32 | repeat { 33 | update() 34 | readKey() 35 | } while (quit == false) 36 | exitEditor() 37 | write(STDIN_FILENO, "\u{1b}[?1049h", "\u{1b}[?1049l".utf8.count) 38 | } 39 | 40 | private func update() { 41 | terminal.setBackgroundColor(Defaults.backgroundColor) 42 | terminal.hideCursor() 43 | terminal.restCursor() 44 | terminal.clean() 45 | render() 46 | terminal.showCursor() 47 | terminal.flush() 48 | document.scrollIfNeeded(size: size) 49 | terminal.goto(position: .init(x: document.cursorPosition.x + 4, y: document.cursorPosition.y)) 50 | } 51 | 52 | private func readKey() { 53 | while true { 54 | if terminal.poll(timeout: .milliseconds(16)) { 55 | if let event = terminal.reade() { 56 | if event == .key(.init(code: .undefined)) { continue } 57 | processInput(event) 58 | break 59 | } 60 | } 61 | } 62 | } 63 | 64 | private func processInput(_ event: Event) { 65 | switch event { 66 | case let .key(event): 67 | if event.code == .backspace { 68 | if cursorPosition.x == 0 && cursorPosition.y != 0 { 69 | document.execute(.spliceUp) 70 | } else if cursorPosition.x == 0 && cursorPosition.y == 0 { 71 | return 72 | } else { 73 | document.execute(.delete(position: cursorPosition)) 74 | } 75 | } 76 | 77 | if let dir = mapKeyEventToDirection(event.code) { 78 | document.execute(.moveTo(direction: dir)) 79 | } 80 | 81 | if event.code == .enter { 82 | if cursorPosition.x == 0 { 83 | document.execute(.insertLineAbove(position: cursorPosition)) 84 | } else if cursorPosition.x == document.row(atPosition: cursorPosition).length() { 85 | document.execute(.insertLineBelow(position: cursorPosition)) 86 | } else { 87 | document.execute(.splitLine) 88 | } 89 | } 90 | 91 | if event.code == .char("d") && event.modifiers == .some(.control) { 92 | exitEditor() 93 | return 94 | } 95 | 96 | if event.code == .char("u") && event.modifiers == .some(.control) { 97 | document.undo() 98 | return 99 | } 100 | 101 | if event.code == .char("r") && event.modifiers == .some(.control) { 102 | document.redo() 103 | return 104 | } 105 | 106 | if case .char(let value) = event.code { 107 | // terminal.writeOnScreen(String(value)) 108 | if event.modifiers == .some(.control) { return } 109 | document.execute(.insert(char: String(value), position: cursorPosition)) 110 | } 111 | } 112 | } 113 | 114 | private func mapKeyEventToDirection(_ code: KeyCode) -> Direction? { 115 | switch code { 116 | case .up: return .up 117 | case .down: return .down 118 | case .left: return .left 119 | case .right: return .right 120 | default: return nil 121 | } 122 | } 123 | 124 | private func render() { 125 | var frame = [""] 126 | let rows = size.rows 127 | cursorPosition = document.cursorPosition 128 | let offset = document.lineOffset 129 | for row in 0.. String { 163 | let paddingCount = Int(size.cols) / 2 - (message.count / 2) 164 | var str = "" 165 | let paddedMsg = message.padding(direction: .left, count: paddingCount) 166 | str = str 167 | .appending("~") // draw tilde 168 | .darkGray() 169 | .padding(direction: .left, count: 1) 170 | .appending(paddedMsg) 171 | .green() 172 | return str 173 | } 174 | 175 | private func exitEditor() { 176 | quit = true 177 | terminal.refreshScreen() 178 | terminal.disableRawMode() 179 | exit(0) 180 | } 181 | } 182 | 183 | extension String { 184 | enum PaddingDirection { 185 | case right 186 | case left 187 | } 188 | 189 | func padding(direction: PaddingDirection, count: Int) -> String { 190 | let padding = String(repeating: " ", count: count) 191 | switch direction { 192 | case .left: 193 | return padding + self 194 | case .right: 195 | return self + padding 196 | } 197 | } 198 | } 199 | 200 | struct Defaults { 201 | static let lineNoLeftPaddig = 1 // |<-1 202 | static let lineNoRightPadding = 2 // 1-->... 203 | static let backgroundColor: Color = .init(r: 41, g: 41, b: 50) 204 | static let foregroundColor: Color = .init(r: 255, g: 255, b: 255) 205 | // line number and tilde color 206 | // default background color 207 | // default text color 208 | // default status line color 209 | // Tab width 210 | } 211 | -------------------------------------------------------------------------------- /Sources/Core/Helpers/CaseAccessable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol CaseAccessible { 4 | var label: String { get } 5 | 6 | func associatedValue() -> AssociatedValue? 7 | func associatedValue(mathing pattern: (AssociatedValue) -> Self) -> AssociatedValue? 8 | } 9 | 10 | extension CaseAccessible { 11 | var label: String { 12 | return Mirror(reflecting: self).children.first?.label ?? String(describing: self) 13 | } 14 | 15 | func associatedValue() -> AssociatedValue? { 16 | return decompose()?.value 17 | } 18 | 19 | func associatedValue(mathing pattern: (AssociatedValue) -> Self) -> AssociatedValue? { 20 | guard let decomposed: (String, AssociatedValue) = decompose(), 21 | let patternLabel = Mirror(reflecting: pattern(decomposed.1)).children.first?.label, 22 | decomposed.0 == patternLabel else { return nil } 23 | 24 | return decomposed.1 25 | } 26 | 27 | private func decompose() -> (label: String, value: AssociatedValue)? { 28 | for case let (label?, value) in Mirror(reflecting: self).children { 29 | if let result = (value as? AssociatedValue) ?? (Mirror(reflecting: value).children.first?.value as? AssociatedValue) { 30 | return (label, result) 31 | } 32 | } 33 | return nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Core/Helpers/Loger.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2021 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import Logging 9 | 10 | public struct Logger { 11 | 12 | public typealias Level = Logging.Logger.Level 13 | 14 | private static let consoleLogger = Logging.Logger( 15 | label: "com.alihilal.ax-editor", 16 | factory: StreamLogHandler.standardError 17 | ) 18 | 19 | public static func log( 20 | event: Level, 21 | destination: Destination = .console, 22 | messages: Any..., 23 | file: StaticString = #filePath, 24 | line: UInt = #line 25 | ) { 26 | switch destination { 27 | case .console: 28 | logToConsole(event: event, messages: messages, file: file, line: line) 29 | case .disk: 30 | logToDisk(event: event, messages: messages, file: file, line: line) 31 | } 32 | } 33 | 34 | private static func logToConsole( 35 | event: Level, 36 | messages: Any..., 37 | file: StaticString = #filePath, 38 | line: UInt = #line 39 | ) { 40 | let string = event.icon + " " + messages.map { "\($0) " }.joined() 41 | consoleLogger.log(level: event, .init(stringLiteral: string)) 42 | } 43 | 44 | private static func logToDisk( 45 | event: Level, 46 | messages: Any..., 47 | file: StaticString = #filePath, 48 | line: UInt = #line 49 | ) { 50 | guard let desktopDir = NSSearchPathForDirectoriesInDomains(.desktopDirectory, .userDomainMask, true).last else { return } 51 | let string = event.icon + " \(Date()): " + messages.map { "\($0) " }.joined() 52 | guard let desktopUrl = URL(string: desktopDir) else { return } 53 | let fileContent = (try? String(contentsOf: desktopUrl.appendingPathComponent("log.txt"))) ?? "" 54 | let finalLog = fileContent.appending("\n " + string) 55 | 56 | try? finalLog.write(toFile: desktopDir + "//log.txt", atomically: true, encoding: .utf8) 57 | } 58 | 59 | 60 | } 61 | 62 | public extension Logger { 63 | enum Destination { 64 | case console 65 | case disk 66 | } 67 | } 68 | 69 | extension Logger { 70 | enum Event { 71 | case debug 72 | case error 73 | case success 74 | } 75 | } 76 | 77 | 78 | extension Logger.Level { 79 | var icon: String { 80 | switch self { 81 | case .debug: 82 | return "⚙️" 83 | case .error: 84 | return "🚨" 85 | case .info: 86 | return "✅" 87 | case .trace: 88 | return "🔔" 89 | case .notice: 90 | return "ℹ️" 91 | case .warning: 92 | return "⚠️" 93 | case .critical: 94 | return "⛔️" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/Core/Highligher/Highlighter.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Highlighter { 10 | public let config: Config = .default 11 | public let tokenizer: Tokenizer 12 | 13 | public func highlight(code: String) -> String { 14 | var code = code.uncolorized() 15 | let tokens = tokenizer.tokinze(code) 16 | let log = tokens 17 | .map { $0.kind.rawValue + " \($0.range.lowerBound) \($0.range.upperBound)" } 18 | .joined(separator: " ") 19 | Logger.log(event: .debug, destination: .disk, messages: log) 20 | tokens.forEach { token in 21 | let color = self.color(for: token.kind) 22 | code.highlight(token.text, with: color, at: token.range) 23 | } 24 | 25 | Logger.log(event: .debug, destination: .disk, messages: code) 26 | return code 27 | } 28 | } 29 | 30 | private extension Highlighter { 31 | func color(for token: TokenType) -> Color { 32 | config.theme.highlights[token] ?? Color(r: 250, g: 141, b: 87) 33 | } 34 | } 35 | 36 | extension String { 37 | mutating func highlight(_ word: String, with color: Color, at range: Range) { 38 | //let word = String(self[range]) 39 | //word = word.customForegroundColor(color)//"\u{001B}[38;2;\(color.r);\(color.g);\(color.b)m" + word + TerminalStyle.reset.open 40 | self = replacingOccurrences(of: word, with: word.customForegroundColor(color)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Core/Highligher/Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token.swift 3 | // Core 4 | // 5 | // Created by Ali on 1.01.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Token { 11 | /// The range of the token in the source string. 12 | public let range: Range 13 | public let kind: TokenType 14 | public let text: String 15 | } 16 | 17 | public enum TokenType: String { 18 | /// A language keyword 19 | case keyword 20 | /// A string literal 21 | case string 22 | /// A reference to a type 23 | case type 24 | /// A number, either interger of floating point 25 | case number 26 | /// A comment, either single or multi-line 27 | case comment 28 | /// A property being accessed, such as `object.property` 29 | case property 30 | /// A symbol being accessed through dot notation, such as `.myCase` 31 | case dotAccess 32 | /// A preprocessing symbol, such as `#if`, `#define` etc. 33 | case preprocessing 34 | /// An attribute symbol like `@objc` 35 | case attribute 36 | /// A special operator like `&&`, `||` etc. 37 | case `operator` 38 | /// like myMethod() 39 | case methodCall 40 | } 41 | 42 | extension TokenType { 43 | /// Return a string value representing the token type 44 | public var string: String { 45 | if case .`operator` = self { 46 | return "operator" 47 | } 48 | return "\(self)" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Core/Highligher/Tokenizer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | protocol TokenGenerator { 10 | func tokens(from input: String) -> [Token] 11 | } 12 | 13 | public struct Tokenizer { 14 | public let language: Language 15 | 16 | func tokinze(_ code: String) -> [Token] { 17 | let generators = self.generators(from: code) 18 | return generators.flatMap { $0.tokens(from: code) } 19 | } 20 | } 21 | 22 | private extension Tokenizer { 23 | 24 | func generators(from input: String) -> [TokenGenerator] { 25 | var generators = [TokenGenerator]() 26 | let keywords = keywordGenerator(language.keywords) 27 | let regs = language.defentions.compactMap { regexGenerator($0.regexp, tokenType: $0.tokenType) } 28 | generators.append(keywords) 29 | generators.append(contentsOf: regs) 30 | return generators 31 | } 32 | 33 | func regexGenerator(_ pattern: String, options: NSRegularExpression.Options = [], tokenType: TokenType) -> TokenGenerator? { 34 | guard let regex = try? NSRegularExpression(pattern: pattern, options: options) else { return nil } 35 | return RegexTokenGenerator(regularExpression: regex, tokenType: tokenType) 36 | } 37 | 38 | func keywordGenerator(_ words: [String]) -> TokenGenerator { 39 | return KeywordTokenGenerator(keywords: words) 40 | } 41 | 42 | } 43 | 44 | // MARK: Regex Token Generator 45 | private extension Tokenizer { 46 | struct RegexTokenGenerator: TokenGenerator { 47 | private let regularExpression: NSRegularExpression 48 | private let tokenType: TokenType 49 | 50 | init(regularExpression: NSRegularExpression, tokenType: TokenType) { 51 | self.regularExpression = regularExpression 52 | self.tokenType = tokenType 53 | } 54 | 55 | func tokens(from input: String) -> [Token] { 56 | generateRegexTokens(source: input) 57 | } 58 | 59 | private func generateRegexTokens(source: String) -> [Token] { 60 | var tokens = [Token]() 61 | let fullNSRange = NSRange(location: 0, length: source.utf16.count) 62 | for numberMatch in regularExpression.matches(in: source, options: [], range: fullNSRange) { 63 | guard let swiftRange = Range(numberMatch.range, in: source) else { 64 | continue 65 | } 66 | let text = String(source[swiftRange]) 67 | let token = Token(range: swiftRange, kind: tokenType, text: text) 68 | tokens.append(token) 69 | } 70 | return tokens 71 | } 72 | } 73 | } 74 | 75 | // MARK: Keyword Token Generator 76 | private extension Tokenizer { 77 | struct KeywordTokenGenerator: TokenGenerator { 78 | private let keywords: [String] 79 | 80 | init(keywords: [String]) { 81 | self.keywords = keywords 82 | } 83 | 84 | func tokens(from input: String) -> [Token] { 85 | generateKeywordTokens(source: input) 86 | } 87 | 88 | private func generateKeywordTokens(source: String) -> [Token] { 89 | var tokens = [Token]() 90 | source.enumerateSubstrings(in: source.startIndex.. Self { 28 | .init(x: 0, y: 0) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Core/Row.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | public final class Row { 10 | public var text: String 11 | public var isUpdated = true 12 | 13 | public init(text: String) { 14 | self.text = text 15 | } 16 | 17 | public func render(at index: Int) -> String { 18 | return text//.customBackgroundColor(Defaults.backgroundColor) 19 | } 20 | 21 | public func length() -> Int { 22 | return text.count 23 | } 24 | 25 | func insert(char: String, at index: Int) { 26 | isUpdated = true 27 | let lowerSubstring = text[0.. String { 34 | guard index >= 0 && index < text.count else { return "" } 35 | isUpdated = true 36 | var t = Array(text) 37 | let char = t.remove(at: index) 38 | text = String(t)//.remove(at: index) 39 | return String(char) 40 | } 41 | 42 | func update(text newText: String) { 43 | isUpdated = true 44 | text = newText 45 | } 46 | 47 | func textUpTo(index: Int) -> String { 48 | text.substring(toIndex: index) 49 | } 50 | 51 | func textFrom(index: Int) -> String { 52 | text.substring(fromIndex: index) 53 | } 54 | 55 | func renderLineNumber(_ number: Int) -> String { 56 | "\(number)" 57 | .darkGray() 58 | .padding(direction: .left, count: 1) 59 | .padding(direction: .right, count: 2) 60 | } 61 | 62 | func highlight(using highlighter: Highlighter) { 63 | //highlighter.highlight(code: text) 64 | isUpdated = false 65 | } 66 | } 67 | 68 | extension String { 69 | 70 | var length: Int { 71 | return count 72 | } 73 | 74 | subscript(i: Int) -> String { 75 | return self[i ..< i + 1] 76 | } 77 | 78 | func substring(fromIndex: Int) -> String { 79 | return self[min(fromIndex, length) ..< length] 80 | } 81 | 82 | func substring(toIndex: Int) -> String { 83 | return self[0 ..< max(0, toIndex)] 84 | } 85 | 86 | @discardableResult 87 | mutating func remove(at offset: Int) -> String { 88 | guard let index = index(startIndex, offsetBy: offset, limitedBy: endIndex) else { return self } 89 | remove(at: index) 90 | return self 91 | } 92 | 93 | subscript(r: Range) -> String { 94 | let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)), 95 | upper: min(length, max(0, r.upperBound)))) 96 | let start = index(startIndex, offsetBy: range.lowerBound) 97 | let end = index(start, offsetBy: range.upperBound - range.lowerBound) 98 | return String(self[start ..< end]) 99 | } 100 | } 101 | 102 | extension Collection { 103 | /// Returns the element at the specified index if it is within bounds, otherwise nil. 104 | subscript (safe index: Index) -> Element? { 105 | return indices.contains(index) ? self[index] : nil 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/Core/Terminal/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // Core 4 | // 5 | // Created by Ali on 24.12.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Event: Equatable { 11 | case key(KeyEvent) 12 | //case resize 13 | //case mouse 14 | } 15 | 16 | extension Event: CustomStringConvertible { 17 | public var description: String { 18 | switch self { 19 | case .key(let event): 20 | return String(describing: event) 21 | } 22 | } 23 | } 24 | 25 | public enum KeyModifier: UInt8, Equatable { 26 | case control = 1 27 | case shift = 2 28 | case alt = 3 29 | case none = 0 30 | } 31 | 32 | extension KeyModifier: CustomStringConvertible { 33 | public var description: String { 34 | switch self { 35 | case .alt: return "Alt" 36 | case .control: return "Control" 37 | case .shift: return "Shift" 38 | case .none: return "None" 39 | } 40 | } 41 | } 42 | 43 | public struct KeyEvent: Equatable { 44 | /// The key itself. 45 | public let code: KeyCode 46 | /// Additional key modifiers. 47 | public let modifiers: KeyModifier? 48 | 49 | public init(code: KeyCode, modifiers: KeyModifier? = nil) { 50 | self.code = code 51 | self.modifiers = modifiers 52 | } 53 | } 54 | 55 | extension KeyEvent: CustomStringConvertible { 56 | public var description: String { 57 | return "code: \(code), mods: \(modifiers ?? .none)" 58 | } 59 | } 60 | 61 | /// A type that represents a key. 62 | public enum KeyCode: Equatable { 63 | /// Undefined. 64 | case undefined 65 | /// Backspace key. 66 | case backspace 67 | /// Enter key. 68 | case enter 69 | /// Left arrow key. 70 | case left 71 | /// Right arrow key. 72 | case right 73 | /// Up arrow key. 74 | case up 75 | /// Down arrow key. 76 | case down 77 | /// Home key. 78 | case home 79 | /// End key. 80 | case end 81 | /// Page up key. 82 | case pageUp 83 | /// Page dow key. 84 | case pageDown 85 | /// Tab key. 86 | case tab 87 | /// Shift + Tab key. 88 | case backTab 89 | /// Delete key. 90 | case delete 91 | /// Insert key. 92 | case insert 93 | /// F(x) key. 94 | case f(UInt8) 95 | /// A character. 96 | case char(Character) 97 | /// Escape key. 98 | case esc 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Sources/Core/Terminal/EventParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Eventparser.swift 3 | // Core 4 | // 5 | 6 | import Foundation 7 | 8 | public struct EventParser { 9 | private let ESC = 27//"\u{1B}" // Escape character (27 or 1B) 10 | private let SS3 = 79//"O" // Single Shift Select of G3 charset 11 | private let CSI = 91//"[" // Control Sequence Introducer 12 | 13 | // The implementation of the parser looks ugly, 14 | // needs a better and swift implemenation. 15 | public func parse(buffer: inout [UInt8]) -> Event? { 16 | guard !buffer.isEmpty else { return nil } 17 | defer { buffer.removeAll() } 18 | // Possible buffer content: 19 | // ["ESC"] // ctrl+ Letter 20 | // [ASCII] // only letter 21 | // [ESC, [, ASCII] // Escape sequence UP, Down, Left, Right. 22 | // [ESC, [, NO, NO, ~] // Escpae Sequence F5...F12. 23 | // [ESC, [, NO, ; , NO, ASCII] // Shift + Letter 24 | // [ESC, O, ASCII] // Escape Sequence F1...F4. 25 | while buffer.first != nil { 26 | let byte = buffer.removeFirst() 27 | if byte == NonPrintableChar.escape.rawValue { 28 | return .key((parseCSI(from: &buffer))) 29 | } else if byte == NonPrintableChar.tab.rawValue { 30 | return .key(.init(code: .tab)) 31 | } else if byte == NonPrintableChar.enter.rawValue { 32 | return .key(.init(code: .enter)) 33 | } else if byte == NonPrintableChar.newLine.rawValue { 34 | return .key(.init(code: .enter)) 35 | } else if byte == NonPrintableChar.backspace.rawValue { 36 | return .key(.init(code: .backspace)) 37 | } else if iscntrl(Int32(byte)) != 0 { 38 | var ascii: UInt8 = 0 39 | if 1...26 ~= byte { 40 | ascii = (byte - 1) + 97 41 | } else { 42 | ascii = (byte - 28) + 52 43 | } 44 | let char = Character(UnicodeScalar(ascii)) 45 | return .key(.init(code: .char(char), modifiers: .control)) 46 | } else { 47 | return .key(parseUtf8Char(from: byte)) 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | public enum NonPrintableChar: UInt8 { 54 | case none = 0 //"\u{00}" // \0 NUL 55 | case tab = 9 //"\u{09}" // \t TAB (horizontal) 56 | case newLine = 10 //"\u{0A}" // \n LF 57 | case enter = 13 //"\u{0D}" // \r CR 58 | case endOfLine = 26 //"\u{1A}" // SUB or EOL 59 | case escape = 27 //"\u{1B}" // \e ESC 60 | case space = 32 //"\u{20}" // SPACE 61 | case backspace = 127 //"\u{7F}" // DEL 62 | } 63 | } 64 | 65 | private extension EventParser { 66 | 67 | func parseUtf8Char(from byte: UInt8 ) -> KeyEvent { 68 | let char = Character(UnicodeScalar(byte)) 69 | let mod: KeyModifier = char.isUppercase ? .shift : .none 70 | return .init(code: .char(char), modifiers: mod) 71 | } 72 | 73 | func parseCSI(from buffer: inout [UInt8]) -> KeyEvent { 74 | var currentByte: UInt8 = 0 75 | while buffer.first != nil { 76 | currentByte = buffer.removeFirst() 77 | let remainingBytes = buffer.count 78 | if currentByte == CSI && remainingBytes == 1 { // [ESC, [,] 79 | return KeyEvent(code: mapCSINumber(buffer[0]), modifiers: nil) 80 | } else if currentByte == CSI && remainingBytes == 3 { //[ESC, [, NO, NO, ~] 81 | let str = toString(ascii: buffer[0]) + toString(ascii: buffer[1]) 82 | guard let number = Int(str) else { return .init(code: .undefined) } 83 | return KeyEvent(code: mapCSINumber(UInt8(number))) 84 | } else if currentByte == CSI && isNumber(buffer[0]) && toString(ascii: buffer[1]) == ";" { 85 | // ["1", ";", "2", "B"] 86 | let code = mapCSINumber(buffer[3]) 87 | let mod = isModifer(buffer[2]) 88 | return KeyEvent(code: code, modifiers: mod) 89 | } else if currentByte == SS3 { // F1...4 90 | let number = buffer[0] 91 | return KeyEvent(code: mapCSINumber(number)) 92 | } else { 93 | break 94 | } 95 | } 96 | return .init(code: .undefined) 97 | } 98 | 99 | func toString(ascii: UInt8) -> String { 100 | String(UnicodeScalar(ascii)) 101 | } 102 | 103 | func isNumber(_ key: UInt8) -> Bool { 104 | return (48...57 ~= key) 105 | } 106 | 107 | func isModifer(_ key: UInt8) -> KeyModifier { 108 | switch key { 109 | case 2: return .shift // ESC [ x ; 2~ 110 | case 3: return .alt // ESC [ x ; 3~ 111 | case 5: return .control // ESC [ x ; 5~ 112 | default: return .none 113 | } 114 | } 115 | 116 | // swiftlint:disable cyclomatic_complexity 117 | /// Translates the ASCII code from escape sequence to its coreeosponding `KeyCode` 118 | /// - Parameter key: `UInt8` key to be mapped. 119 | /// - Returns: A key code instance. 120 | func mapCSINumber(_ key: UInt8) -> KeyCode { 121 | switch key { 122 | case 72: return .home // ESC [ H or ESC [ 1~ 123 | case 2: return .insert // ESC [ 2~ 124 | case 3: return .delete // ESC [ 3~ 125 | case 4: return .end // ESC [ F or ESC [ 4~ 126 | case 5: return .pageUp // ESC [ 5~ 127 | case 6: return .pageDown // ESC [ 6~ 128 | case 80: return .f(1) // ESC O P or ESC [ 11~ 129 | case 81: return .f(2) // ESC O Q or ESC [ 12~ 130 | case 82: return .f(3) // ESC O R or ESC [ 13~ 131 | case 83: return .f(4) // ESC O S or ESC [ 14~ 132 | case 15: return .f(5) // ESC [ 15~ 133 | case 17: return .f(6) // ESC [ 17~ 134 | case 18: return .f(7) // ESC [ 18~ 135 | case 19: return .f(8) // ESC [ 19~ 136 | case 20: return .f(9) // ESC [ 20~ 137 | case 21: return .f(10) // ESC [ 21~ 138 | case 23: return .f(11) // ESC [ 23~ 139 | case 24: return .f(12) // ESC [ 24~ 140 | case 65: return .up // ESC [ A 141 | case 66: return .down // ESC [ B 142 | case 67: return .right // ESC [ C 143 | case 68: return .left // ESC [ D 144 | case 90: return .backTab // ESC [ Z 145 | default: return .undefined 146 | } 147 | } 148 | } 149 | // swiftlint:enable cyclomatic_complexity 150 | -------------------------------------------------------------------------------- /Sources/Core/Terminal/EventReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventReader.swift 3 | // Core 4 | 5 | import Dispatch 6 | import Foundation 7 | 8 | public final class EventReader { 9 | private let parser: EventParser 10 | /// Buffer to hold the biggest possible response from the input stream. 11 | private var buffer: [UInt8] = [] 12 | /// A varaible to represent the reading buffer size. The numer 250 is 13 | /// chosen according to the biggest ANSI sequence response. 14 | private let bufferSize = 256 15 | 16 | init(parser: EventParser) { 17 | self.parser = parser 18 | } 19 | 20 | /// It allows you to check if there is or isn't an `Event` available within the given period 21 | /// of time. In other words - if subsequent call to the `read` function will block or not. 22 | /// - Parameter timeout: maximum waiting time for event availability. 23 | /// - Returns: `true` if an `Event` is available otherwise it returns `false`. 24 | public func poll(timeout: Timeout) -> Bool { 25 | var fds = [pollfd(fd: STDIN_FILENO, events: Int16(POLLIN), revents: 0)] 26 | return Darwin.poll(&fds, UInt32(fds.count), Int32(timeout.value)) > 0 27 | } 28 | 29 | /// Reads a single `Event` from the stdin. This function blocks until an `Event` 30 | /// is available. Combine it with the `poll` function to get non-blocking reads. 31 | /// - Returns: Returns `.success(Event)` if an `Event` is available otherwise 32 | /// it returns `.failure(Error)`` 33 | /// - seeAlso: `poll` 34 | public func readBuffer() -> Result { 35 | // sigaction(2, 1, 2) 36 | var chars: [UInt8] = Array(repeating: 0, count: bufferSize) 37 | let readCount = read(STDIN_FILENO, &chars, bufferSize) 38 | if readCount != -1 { 39 | buffer.append(contentsOf: chars) 40 | } else { 41 | let error = NSError(domain: POSIXError.errorDomain, code: Int(errno)) 42 | print(error) 43 | return .failure(error) 44 | } 45 | 46 | buffer = Array(buffer[0.. Void) { 27 | source.setEventHandler { 28 | action() 29 | } 30 | source.resume() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Core/Terminal/Terminal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Terminal.swift 3 | // ArgumentParser 4 | // 5 | // Created by Ali on 14.12.2020. 6 | // 7 | 8 | import Foundation 9 | #if os(Linux) 10 | import Glibc 11 | #else 12 | import Darwin 13 | #endif 14 | 15 | public final class Terminal { 16 | private var originalTerminal: termios 17 | private let stdout: FileHandle = .standardOutput 18 | private let stdin: FileHandle = .standardInput 19 | private let reader: EventReader 20 | private let interceptor = SignalInterceptor() 21 | 22 | public var onWindowSizeChange: ((Size) -> Void)? 23 | 24 | public init() { 25 | let termiosPointer = UnsafeMutablePointer.allocate(capacity: 1) 26 | let termiosRef = termiosPointer.pointee 27 | termiosPointer.deallocate() 28 | originalTerminal = termiosRef 29 | reader = EventReader(parser: EventParser()) 30 | listenToWindowSizeChange() 31 | } 32 | 33 | @discardableResult 34 | public func enableRawMode() -> termios { 35 | var raw: termios = originalTerminal 36 | tcgetattr(stdout.fileDescriptor, &raw) 37 | 38 | let original = raw 39 | 40 | cfmakeraw(&raw) 41 | // raw.c_lflag &= ~(UInt(ECHO | ICANON | IEXTEN | ISIG)) 42 | // raw.c_iflag &= ~(UInt(BRKINT | ICRNL | INPCK | ISTRIP | IXON)) 43 | // // IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON 44 | // raw.c_oflag &= ~(UInt(OPOST)) 45 | // raw.c_cflag |= UInt((CS8)) 46 | // 47 | // raw.c_cc.16 = 0 // VMIN 48 | // raw.c_cc.17 = 1 // VTIME 1/10 = 100 ms 49 | 50 | tcsetattr(stdout.fileDescriptor, TCSAFLUSH, &raw) 51 | return original 52 | } 53 | 54 | func disableRawMode() { 55 | var term = originalTerminal 56 | tcsetattr(stdout.fileDescriptor, TCSAFLUSH, &term) 57 | } 58 | 59 | func poll(timeout: Timeout) -> Bool { 60 | reader.poll(timeout: timeout) 61 | } 62 | 63 | func reade() -> Event? { 64 | switch reader.readBuffer() { 65 | case .success(let event): return event 66 | case .failure(let error): 67 | print(error.localizedDescription) 68 | return nil 69 | } 70 | } 71 | 72 | func writeOnScreen(_ text: String) { 73 | let bytesCount = text.utf8.count 74 | write(stdout.fileDescriptor, text, bytesCount) 75 | } 76 | 77 | func flush() { 78 | fflush(__stdoutp) 79 | } 80 | 81 | func refreshScreen() { 82 | hideCursor() 83 | clean() 84 | restCursor() 85 | showCursor() 86 | } 87 | 88 | func clean() { 89 | execute(command: .clean) 90 | } 91 | 92 | func cleanLine() { 93 | execute(command: .cleanLine) 94 | } 95 | 96 | func restCursor() { 97 | // execute(command: .repositionCursor) 98 | execute(command: .moveCursor(position: .init(x: 0, y: 0))) 99 | } 100 | 101 | func goto(position: Postion) { 102 | execute(command: .moveCursor(position: position)) 103 | } 104 | 105 | func cursorPosition() -> Postion { 106 | // https://vt100.net/docs/vt100-ug/chapter3.html#CPR 107 | guard execute(command: .cursorCurrentPosition) == 4 else { return .init(x: 0, y: 0) } 108 | var c: UInt8 = 0 109 | var response: [UInt8] = [] 110 | 111 | repeat { 112 | read(STDIN_FILENO, &c, 1) 113 | response.append(c) 114 | } while c != UInt8(ascii: "R") 115 | 116 | let result = response 117 | .map({ String(UnicodeScalar($0)) }) 118 | .compactMap(Int.init) 119 | 120 | return .init(result) 121 | } 122 | 123 | func getWindowSize() -> Size { 124 | var winSize = winsize() 125 | if ioctl(stdout.fileDescriptor, TIOCGWINSZ, &winSize) == -1 || winSize.ws_col == 0 { 126 | return .init(rows: 0, cols: 0) 127 | } else { 128 | return .init(rows: winSize.ws_row, cols: winSize.ws_col) 129 | } 130 | } 131 | 132 | func hideCursor() { 133 | execute(command: .hideCursor) 134 | } 135 | 136 | func showCursor() { 137 | execute(command: .showCursor) 138 | } 139 | 140 | func setBackgroundColor(_ color: Color) { 141 | execute(command: .custom("\u{001B}[48;2;\(41);\(41);\(50)m")) 142 | } 143 | 144 | private func listenToWindowSizeChange() { 145 | interceptor.intercept { 146 | let newSize = self.getWindowSize() 147 | self.onWindowSizeChange?(newSize) 148 | 149 | } 150 | } 151 | 152 | private typealias WriteResult = Int 153 | 154 | @discardableResult 155 | private func execute(command: ANSICommand) -> WriteResult { 156 | // STDOUT_FILENO 157 | write(stdout.fileDescriptor, command.rawValue, command.bytesCount) 158 | } 159 | 160 | } 161 | 162 | extension Terminal { 163 | enum ANSICommand { 164 | case clean 165 | case cleanLine 166 | case repositionCursor 167 | case cursorCurrentPosition 168 | case showCursor 169 | case hideCursor 170 | case moveCursor(position: Postion) 171 | case custom(String) 172 | 173 | var rawValue: String { 174 | switch self { 175 | case .clean: return "\u{1b}[2J" 176 | case .cleanLine: return "\u{1b}[K" 177 | case .repositionCursor: return "\u{1b}[H" 178 | case .cursorCurrentPosition: return "\u{1b}[6n" 179 | case .showCursor: return "\u{1b}[?25h" 180 | case .hideCursor: return "\u{1b}[?25l" 181 | case let .moveCursor(position): 182 | return "\u{1b}[\(position.y + 1);\(position.x + 1)H" 183 | case .custom(let str): return str 184 | } 185 | } 186 | 187 | var bytesCount: Int { 188 | rawValue.utf8.count 189 | } 190 | } 191 | 192 | enum KeyEvent { 193 | // case 194 | } 195 | } 196 | 197 | public struct Size { 198 | public let rows: UInt16 199 | public let cols: UInt16 200 | } 201 | 202 | extension Size: CustomStringConvertible { 203 | public var description: String { 204 | return "rows: \(rows), cols: \(cols)" 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Sources/Core/Terminal/TerminalStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TerminalStyle.swift 3 | // Core 4 | // 5 | // Created by Ali on 26.12.2020. 6 | // 7 | // Credit to: https://github.com/mtynior/ColorizeSwift by Michał Tynior 8 | import Foundation 9 | 10 | public typealias TerminalStyleCode = (open: String, close: String) 11 | 12 | public struct TerminalStyle { 13 | public static let bold: TerminalStyleCode = ("\u{001B}[1m", "\u{001B}[22m") 14 | public static let dim: TerminalStyleCode = ("\u{001B}[2m", "\u{001B}[22m") 15 | public static let italic: TerminalStyleCode = ("\u{001B}[3m", "\u{001B}[23m") 16 | public static let underline: TerminalStyleCode = ("\u{001B}[4m", "\u{001B}[24m") 17 | public static let blink: TerminalStyleCode = ("\u{001B}[5m", "\u{001B}[25m") 18 | public static let reverse: TerminalStyleCode = ("\u{001B}[7m", "\u{001B}[27m") 19 | public static let hidden: TerminalStyleCode = ("\u{001B}[8m", "\u{001B}[28m") 20 | public static let strikethrough: TerminalStyleCode = ("\u{001B}[9m", "\u{001B}[29m") 21 | public static let reset: TerminalStyleCode = ("\u{001B}[0m", "") 22 | 23 | public static let black: TerminalStyleCode = ("\u{001B}[30m", "\u{001B}[0m") 24 | public static let red: TerminalStyleCode = ("\u{001B}[31m", "\u{001B}[0m") 25 | public static let green: TerminalStyleCode = ("\u{001B}[32m", "\u{001B}[0m") 26 | public static let yellow: TerminalStyleCode = ("\u{001B}[33m", "\u{001B}[0m") 27 | public static let blue: TerminalStyleCode = ("\u{001B}[34m", "\u{001B}[0m") 28 | public static let magenta: TerminalStyleCode = ("\u{001B}[35m", "\u{001B}[0m") 29 | public static let cyan: TerminalStyleCode = ("\u{001B}[36m", "\u{001B}[0m") 30 | public static let lightGray: TerminalStyleCode = ("\u{001B}[37m", "\u{001B}[0m") 31 | public static let darkGray: TerminalStyleCode = ("\u{001B}[90m", "\u{001B}[0m") 32 | public static let lightRed: TerminalStyleCode = ("\u{001B}[91m", "\u{001B}[0m") 33 | public static let lightGreen: TerminalStyleCode = ("\u{001B}[92m", "\u{001B}[0m") 34 | public static let lightYellow: TerminalStyleCode = ("\u{001B}[93m", "\u{001B}[0m") 35 | public static let lightBlue: TerminalStyleCode = ("\u{001B}[94m", "\u{001B}[0m") 36 | public static let lightMagenta: TerminalStyleCode = ("\u{001B}[95m", "\u{001B}[0m") 37 | public static let lightCyan: TerminalStyleCode = ("\u{001B}[96m", "\u{001B}[0m") 38 | public static let white: TerminalStyleCode = ("\u{001B}[97m", "\u{001B}[0m") 39 | 40 | public static let onBlack: TerminalStyleCode = ("\u{001B}[40m", "\u{001B}[0m") 41 | public static let onRed: TerminalStyleCode = ("\u{001B}[41m", "\u{001B}[0m") 42 | public static let onGreen: TerminalStyleCode = ("\u{001B}[42m", "\u{001B}[0m") 43 | public static let onYellow: TerminalStyleCode = ("\u{001B}[43m", "\u{001B}[0m") 44 | public static let onBlue: TerminalStyleCode = ("\u{001B}[44m", "\u{001B}[0m") 45 | public static let onMagenta: TerminalStyleCode = ("\u{001B}[45m", "\u{001B}[0m") 46 | public static let onCyan: TerminalStyleCode = ("\u{001B}[46m", "\u{001B}[0m") 47 | public static let onLightGray: TerminalStyleCode = ("\u{001B}[47m", "\u{001B}[0m") 48 | public static let onDarkGray: TerminalStyleCode = ("\u{001B}[100m", "\u{001B}[0m") 49 | public static let onLightRed: TerminalStyleCode = ("\u{001B}[101m", "\u{001B}[0m") 50 | public static let onLightGreen: TerminalStyleCode = ("\u{001B}[102m", "\u{001B}[0m") 51 | public static let onLightYellow: TerminalStyleCode = ("\u{001B}[103m", "\u{001B}[0m") 52 | public static let onLightBlue: TerminalStyleCode = ("\u{001B}[104m", "\u{001B}[0m") 53 | public static let onLightMagenta: TerminalStyleCode = ("\u{001B}[105m", "\u{001B}[0m") 54 | public static let onLightCyan: TerminalStyleCode = ("\u{001B}[106m", "\u{001B}[0m") 55 | public static let onWhite: TerminalStyleCode = ("\u{001B}[107m", "\u{001B}[0m") 56 | } 57 | 58 | extension String { 59 | /// Enable/disable colorization 60 | public static var isColorizationEnabled = true 61 | 62 | public func bold() -> String { 63 | return applyStyle(TerminalStyle.bold) 64 | } 65 | 66 | public func dim() -> String { 67 | return applyStyle(TerminalStyle.dim) 68 | } 69 | 70 | public func italic() -> String { 71 | return applyStyle(TerminalStyle.italic) 72 | } 73 | 74 | public func underline() -> String { 75 | return applyStyle(TerminalStyle.underline) 76 | } 77 | 78 | public func blink() -> String { 79 | return applyStyle(TerminalStyle.blink) 80 | } 81 | 82 | public func reverse() -> String { 83 | return applyStyle(TerminalStyle.reverse) 84 | } 85 | 86 | public func hidden() -> String { 87 | return applyStyle(TerminalStyle.hidden) 88 | } 89 | 90 | public func strikethrough() -> String { 91 | return applyStyle(TerminalStyle.strikethrough) 92 | } 93 | 94 | public func reset() -> String { 95 | guard String.isColorizationEnabled else { return self } 96 | return "\u{001B}[0m" + self 97 | } 98 | 99 | public func foregroundColor(_ color: TerminalColor) -> String { 100 | return applyStyle(color.foregroundStyleCode()) 101 | } 102 | 103 | public func backgroundColor(_ color: TerminalColor) -> String { 104 | return applyStyle(color.backgroundStyleCode()) 105 | } 106 | 107 | public func colorize(_ foreground: TerminalColor, background: TerminalColor) -> String { 108 | return applyStyle(foreground.foregroundStyleCode()).applyStyle(background.backgroundStyleCode()) 109 | } 110 | 111 | public func uncolorized() -> String { 112 | guard let regex = try? NSRegularExpression(pattern: "\\\u{001B}\\[([0-9;]+)m") else { return self } 113 | 114 | return regex.stringByReplacingMatches(in: self, options: [], range: NSRange(0.. String { 118 | guard String.isColorizationEnabled else { return self } 119 | let str = self.replacingOccurrences(of: TerminalStyle.reset.open, with: TerminalStyle.reset.open + codeStyle.open) 120 | 121 | return codeStyle.open + str + TerminalStyle.reset.open 122 | } 123 | } 124 | 125 | extension String { 126 | public func black() -> String { 127 | return applyStyle(TerminalStyle.black) 128 | } 129 | 130 | public func red() -> String { 131 | return applyStyle(TerminalStyle.red) 132 | } 133 | 134 | public func green() -> String { 135 | return applyStyle(TerminalStyle.green) 136 | } 137 | 138 | public func yellow() -> String { 139 | return applyStyle(TerminalStyle.yellow) 140 | } 141 | 142 | public func blue() -> String { 143 | return applyStyle(TerminalStyle.blue) 144 | } 145 | 146 | public func magenta() -> String { 147 | return applyStyle(TerminalStyle.magenta) 148 | } 149 | 150 | public func cyan() -> String { 151 | return applyStyle(TerminalStyle.cyan) 152 | } 153 | 154 | public func lightGray() -> String { 155 | return applyStyle(TerminalStyle.lightGray) 156 | } 157 | 158 | public func darkGray() -> String { 159 | return applyStyle(TerminalStyle.darkGray) 160 | } 161 | 162 | public func lightRed() -> String { 163 | return applyStyle(TerminalStyle.lightRed) 164 | } 165 | 166 | public func lightGreen() -> String { 167 | return applyStyle(TerminalStyle.lightGreen) 168 | } 169 | 170 | public func lightYellow() -> String { 171 | return applyStyle(TerminalStyle.lightYellow) 172 | } 173 | 174 | public func lightBlue() -> String { 175 | return applyStyle(TerminalStyle.lightBlue) 176 | } 177 | 178 | public func lightMagenta() -> String { 179 | return applyStyle(TerminalStyle.lightMagenta) 180 | } 181 | 182 | public func lightCyan() -> String { 183 | return applyStyle(TerminalStyle.lightCyan) 184 | } 185 | 186 | public func white() -> String { 187 | return applyStyle(TerminalStyle.white) 188 | } 189 | 190 | public func onBlack() -> String { 191 | return applyStyle(TerminalStyle.onBlack) 192 | } 193 | 194 | public func onRed() -> String { 195 | return applyStyle(TerminalStyle.onRed) 196 | } 197 | 198 | public func onGreen() -> String { 199 | return applyStyle(TerminalStyle.onGreen) 200 | } 201 | 202 | public func onYellow() -> String { 203 | return applyStyle(TerminalStyle.onYellow) 204 | } 205 | 206 | public func onBlue() -> String { 207 | return applyStyle(TerminalStyle.onBlue) 208 | } 209 | 210 | public func onMagenta() -> String { 211 | return applyStyle(TerminalStyle.onMagenta) 212 | } 213 | 214 | public func onCyan() -> String { 215 | return applyStyle(TerminalStyle.onCyan) 216 | } 217 | 218 | public func onLightGray() -> String { 219 | return applyStyle(TerminalStyle.onLightGray) 220 | } 221 | 222 | public func onDarkGray() -> String { 223 | return applyStyle(TerminalStyle.onDarkGray) 224 | } 225 | 226 | public func onLightRed() -> String { 227 | return applyStyle(TerminalStyle.onLightRed) 228 | } 229 | 230 | public func onLightGreen() -> String { 231 | return applyStyle(TerminalStyle.onLightGreen) 232 | } 233 | 234 | public func onLightYellow() -> String { 235 | return applyStyle(TerminalStyle.onLightYellow) 236 | } 237 | 238 | public func onLightBlue() -> String { 239 | return applyStyle(TerminalStyle.onLightBlue) 240 | } 241 | 242 | public func onLightMagenta() -> String { 243 | return applyStyle(TerminalStyle.onLightMagenta) 244 | } 245 | 246 | public func onLightCyan() -> String { 247 | return applyStyle(TerminalStyle.onLightCyan) 248 | } 249 | 250 | public func onWhite() -> String { 251 | return applyStyle(TerminalStyle.onWhite) 252 | } 253 | 254 | public func customBackgroundColor(_ color: Color) -> String { 255 | applyStyle(TerminalColor.customBackground(color: color)) 256 | } 257 | 258 | public func customForegroundColor(_ color: Color) -> String { 259 | applyStyle(TerminalColor.customForeground(color: color)) 260 | } 261 | } 262 | 263 | // https://jonasjacek.github.io/colors/ 264 | public enum TerminalColor: UInt8 { 265 | case black = 0 266 | case maroon 267 | case green 268 | case olive 269 | case navy 270 | case purple 271 | case teal 272 | case silver 273 | case grey 274 | case red 275 | case lime 276 | case yellow 277 | case blue 278 | case fuchsia 279 | case aqua 280 | case white 281 | case grey0 282 | case navyBlue 283 | case darkBlue 284 | case blue3 285 | case blue3_2 286 | case blue1 287 | case darkGreen 288 | case deepSkyBlue4 289 | case deepSkyBlue4_2 290 | case deepSkyBlue4_3 291 | case dodgerBlue3 292 | case dodgerBlue2 293 | case green4 294 | case springGreen4 295 | case turquoise4 296 | case deepSkyBlue3 297 | case deepSkyBlue3_2 298 | case dodgerBlue1 299 | case green3 300 | case springGreen3 301 | case darkCyan 302 | case lightSeaGreen 303 | case deepSkyBlue2 304 | case deepSkyBlue1 305 | case green3_2 306 | case springGreen3_2 307 | case springGreen2 308 | case cyan3 309 | case darkTurquoise 310 | case turquoise2 311 | case green1 312 | case springGreen2_2 313 | case springGreen1 314 | case mediumSpringGreen 315 | case cyan2 316 | case cyan1 317 | case darkRed 318 | case deepPink4 319 | case purple4 320 | case purple4_2 321 | case purple3 322 | case blueViolet 323 | case orange4 324 | case grey37 325 | case mediumPurple4 326 | case slateBlue3 327 | case slateBlue3_2 328 | case royalBlue1 329 | case chartreuse4 330 | case darkSeaGreen4 331 | case paleTurquoise4 332 | case steelBlue 333 | case steelBlue3 334 | case cornflowerBlue 335 | case chartreuse3 336 | case darkSeaGreen4_2 337 | case cadetBlue 338 | case cadetBlue_2 339 | case skyBlue3 340 | case steelBlue1 341 | case chartreuse3_2 342 | case paleGreen3 343 | case seaGreen3 344 | case aquamarine3 345 | case mediumTurquoise 346 | case steelBlue1_2 347 | case chartreuse2 348 | case seaGreen2 349 | case seaGreen1 350 | case seaGreen1_2 351 | case aquamarine1 352 | case darkSlateGray2 353 | case darkRed_2 354 | case deepPink4_2 355 | case darkMagenta 356 | case darkMagenta_2 357 | case darkViolet 358 | case purple_2 359 | case orange4_2 360 | case lightPink4 361 | case plum4 362 | case mediumPurple3 363 | case mediumPurple3_2 364 | case slateBlue1 365 | case yellow4 366 | case wheat4 367 | case grey53 368 | case lightSlateGrey 369 | case mediumPurple 370 | case lightSlateBlue 371 | case yellow4_2 372 | case darkOliveGreen3 373 | case darkSeaGreen 374 | case lightSkyBlue3 375 | case lightSkyBlue3_2 376 | case skyBlue2 377 | case chartreuse2_2 378 | case darkOliveGreen3_2 379 | case paleGreen3_2 380 | case darkSeaGreen3 381 | case darkSlateGray3 382 | case skyBlue1 383 | case chartreuse1 384 | case lightGreen 385 | case lightGreen_2 386 | case paleGreen1 387 | case aquamarine1_2 388 | case darkSlateGray1 389 | case red3 390 | case deepPink4_3 391 | case mediumVioletRed 392 | case magenta3 393 | case darkViolet_2 394 | case purple_3 395 | case darkOrange3 396 | case indianRed 397 | case hotPink3 398 | case mediumOrchid3 399 | case mediumOrchid 400 | case mediumPurple2 401 | case darkGoldenrod 402 | case lightSalmon3 403 | case rosyBrown 404 | case grey63 405 | case mediumPurple2_2 406 | case mediumPurple1 407 | case gold3 408 | case darkKhaki 409 | case navajoWhite3 410 | case grey69 411 | case lightSteelBlue3 412 | case lightSteelBlue 413 | case yellow3 414 | case darkOliveGreen3_3 415 | case darkSeaGreen3_2 416 | case darkSeaGreen2 417 | case lightCyan3 418 | case lightSkyBlue1 419 | case greenYellow 420 | case darkOliveGreen2 421 | case paleGreen1_2 422 | case darkSeaGreen2_2 423 | case darkSeaGreen1 424 | case paleTurquoise1 425 | case red3_2 426 | case deepPink3 427 | case deepPink3_2 428 | case magenta3_2 429 | case magenta3_3 430 | case magenta2 431 | case darkOrange3_2 432 | case indianRed_2 433 | case hotPink3_2 434 | case hotPink2 435 | case orchid 436 | case mediumOrchid1 437 | case orange3 438 | case lightSalmon3_2 439 | case lightPink3 440 | case pink3 441 | case plum3 442 | case violet 443 | case gold3_2 444 | case lightGoldenrod3 445 | case tan 446 | case mistyRose3 447 | case thistle3 448 | case plum2 449 | case yellow3_2 450 | case khaki3 451 | case lightGoldenrod2 452 | case lightYellow3 453 | case grey84 454 | case lightSteelBlue1 455 | case yellow2 456 | case darkOliveGreen1 457 | case darkOliveGreen1_2 458 | case darkSeaGreen1_2 459 | case honeydew2 460 | case lightCyan1 461 | case red1 462 | case deepPink2 463 | case deepPink1 464 | case deepPink1_2 465 | case magenta2_2 466 | case magenta1 467 | case orangeRed1 468 | case indianRed1 469 | case indianRed1_2 470 | case hotPink 471 | case hotPink_2 472 | case mediumOrchid1_2 473 | case darkOrange 474 | case salmon1 475 | case lightCoral 476 | case paleVioletRed1 477 | case orchid2 478 | case orchid1 479 | case orange1 480 | case sandyBrown 481 | case lightSalmon1 482 | case lightPink1 483 | case pink1 484 | case plum1 485 | case gold1 486 | case lightGoldenrod2_2 487 | case lightGoldenrod2_3 488 | case navajoWhite1 489 | case mistyRose1 490 | case thistle1 491 | case yellow1 492 | case lightGoldenrod1 493 | case khaki1 494 | case wheat1 495 | case cornsilk1 496 | case grey100 497 | case grey3 498 | case grey7 499 | case grey11 500 | case grey15 501 | case grey19 502 | case grey23 503 | case grey27 504 | case grey30 505 | case grey35 506 | case grey39 507 | case grey42 508 | case grey46 509 | case grey50 510 | case grey54 511 | case grey58 512 | case grey62 513 | case grey66 514 | case grey70 515 | case grey74 516 | case grey78 517 | case grey82 518 | case grey85 519 | case grey89 520 | case grey93 521 | 522 | static func customBackground(color: Color) -> TerminalStyleCode { 523 | ("\u{001B}[48;2;\(color.r);\(color.g);\(color.b)m", TerminalStyle.reset.open) 524 | } 525 | 526 | static func customForeground(color: Color) -> TerminalStyleCode { 527 | ("\u{001B}[38;2;\(color.r);\(color.g);\(color.b)m", TerminalStyle.reset.open) 528 | } 529 | 530 | public func foregroundStyleCode() -> TerminalStyleCode { 531 | return ("\u{001B}[38;5;\(self.rawValue)m", TerminalStyle.reset.open) 532 | } 533 | 534 | public func backgroundStyleCode() -> TerminalStyleCode { 535 | return ("\u{001B}[48;5;\(self.rawValue)m", TerminalStyle.reset.open) 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /Sources/Core/Terminal/Timeout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timeout.swift 3 | // Core 4 | 5 | import Foundation 6 | 7 | /// Describes the timeout ammount for listening operation. 8 | public enum Timeout { 9 | /// Timeout ammount in milliseconds. 10 | case milliseconds(Int) 11 | 12 | /// Timeout ammount in seconds. 13 | case seconds(Int) 14 | 15 | /// The value of timeout measured by milliseconds. 16 | public var value: Int { 17 | switch self { 18 | case let .milliseconds(duration): 19 | return duration 20 | case let .seconds(duration): 21 | return (duration * 1000) 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ax-editor/OpenCommand.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ax Editor 3 | * Copyright (c) Ali Hilal 2020 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import ArgumentParser 8 | import Core 9 | import Foundation 10 | 11 | public struct OpenCommand: ParsableCommand { 12 | public init() {} 13 | @Argument(help: "The file to be edited") 14 | var file: String? 15 | 16 | public mutating func run() throws { 17 | let terminal = Terminal() 18 | if let file = file { 19 | let path = Path() 20 | let fullPath = try path.enumerateFullPath(from: file) 21 | let doc = try DocumentManager.open(from: fullPath) 22 | let editor = Editor(terminal: terminal, document: doc) 23 | editor.run() 24 | } else { 25 | let editor = Editor(terminal: terminal, document: Document(rows: [])) 26 | editor.run() 27 | } 28 | } 29 | } 30 | 31 | struct Path { 32 | func enumerateFullPath(from file: String) throws -> String { 33 | if isValidPath(file) { 34 | if FileManager.default.fileExists(atPath: file) { 35 | return file 36 | } else { 37 | throw CocoaError.error(.fileNoSuchFile) 38 | } 39 | } else { 40 | let process = Process() 41 | process.arguments = ["pwd"] 42 | let url = URL(fileURLWithPath: "/usr/bin/env") 43 | 44 | if #available(OSX 10.13, *) { 45 | process.executableURL = url 46 | } else { 47 | process.launchPath = url.path 48 | } 49 | let outputPipe = Pipe() 50 | 51 | process.standardOutput = outputPipe 52 | if #available(OSX 10.13, *) { 53 | try process.run() 54 | } else { 55 | process.launch() 56 | } 57 | let data = outputPipe.fileHandleForReading.readDataToEndOfFile() 58 | if let str = String(data: data, encoding: .utf8) { 59 | let path = str.appending("/\(file)") 60 | .removingAllWhitespaces 61 | return path 62 | } 63 | 64 | process.waitUntilExit() 65 | } 66 | return "" 67 | } 68 | 69 | private func isValidPath(_ path: String) -> Bool { 70 | path.starts(with: "~") || 71 | path.starts(with: "/") || 72 | path.starts(with: "./") || 73 | path.starts(with: "../") 74 | } 75 | } 76 | 77 | private extension StringProtocol where Self: RangeReplaceableCollection { 78 | var removingAllWhitespaces: Self { 79 | filter { !$0.isWhitespace } 80 | } 81 | } 82 | 83 | struct DocumentManager { 84 | static func open(from path: String) throws -> Document { 85 | let name = path.split(separator: "/").last ?? "" 86 | let langExt = path.split(separator: ".").last ?? "" 87 | let lang = Config.default.languages.first(where: { $0.extensions.contains(String(langExt)) }) 88 | 89 | guard let data = FileManager.default.contents(atPath: path) else { 90 | FileManager.default.createFile(atPath: path, contents: nil, attributes: nil) 91 | return Document(rows: [Row(text: "")], name: String(name), language: lang) 92 | //throw Error.documentCouldntBeOpened 93 | } 94 | 95 | if let str = String(data: data, encoding: .utf8) { 96 | let rows = str.split(separator: "\n").compactMap({ Row(text: String($0)) }) 97 | return Document(rows: rows, name: String(name), language: lang) 98 | } 99 | 100 | return Document(rows: [], name: String(name), language: lang) 101 | } 102 | 103 | enum Error: Swift.Error { 104 | case documentCouldntBeOpened 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/ax-editor/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Core 3 | import Foundation 4 | 5 | OpenCommand.main() 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ax_editorTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ax_editorTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/ax-editorTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ax_editorTests.allTests) 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/ax-editorTests/ax_editorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | final class ax_editorTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | // Some of the APIs that we use below are available in macOS 10.13 and above. 11 | guard #available(macOS 10.13, *) else { 12 | return 13 | } 14 | 15 | let fooBinary = productsDirectory.appendingPathComponent("ax-editor") 16 | 17 | let process = Process() 18 | process.executableURL = fooBinary 19 | 20 | let pipe = Pipe() 21 | process.standardOutput = pipe 22 | 23 | try process.run() 24 | process.waitUntilExit() 25 | 26 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 27 | let output = String(data: data, encoding: .utf8) 28 | 29 | XCTAssertEqual(output, "Hello, world!\n") 30 | } 31 | 32 | /// Returns path to the built products directory. 33 | var productsDirectory: URL { 34 | #if os(macOS) 35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 36 | return bundle.bundleURL.deletingLastPathComponent() 37 | } 38 | fatalError("couldn't find the products directory") 39 | #else 40 | return Bundle.main.bundleURL 41 | #endif 42 | } 43 | 44 | static var allTests = [ 45 | ("testExample", testExample) 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/ax-editor/2b6c1fcd92feba604f675e415661fb88e50dee4c/assets/demo.png --------------------------------------------------------------------------------