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