├── .gitignore ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources ├── SwiftCursesTerm │ ├── Color.swift │ ├── SwiftCursesTerm.swift │ └── TextAttribute.swift └── curses │ ├── curses.h │ └── module.modulemap └── Tests ├── LinuxMain.swift └── SwiftCursesTermTests ├── SwiftCursesTermTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## Build generated 4 | build/ 5 | DerivedData/ 6 | 7 | ## Various settings 8 | ExportOptions.plist 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata/ 18 | 19 | ## Other 20 | *.moved-aside 21 | *.xccheckout 22 | *.xcscmblueprint 23 | 24 | ## We don't want any memgrap's to leak ;) 25 | **.memgraph 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | # 53 | # Add this line if you want to avoid checking in source code from the Xcode workspace 54 | # *.xcworkspace 55 | 56 | # Carthage 57 | # 58 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 59 | # Carthage/Checkouts 60 | 61 | Carthage/Build 62 | 63 | # Accio dependency management 64 | Dependencies/ 65 | .accio/ 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 70 | # screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots/**/*.png 77 | fastlane/test_output 78 | fastlane/*.env 79 | 80 | # Code Injection 81 | # 82 | # After new code Injection tools there's a generated folder /iOSInjectionProject 83 | # https://github.com/johnno1962/injectionforxcode 84 | 85 | iOSInjectionProject/ 86 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Derik Ramirez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 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: "SwiftCursesTerm", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SwiftCursesTerm", 12 | targets: ["SwiftCursesTerm"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .systemLibrary(name: "curses", providers: [ 22 | .apt(["ncurses"]), 23 | .brew(["ncurses"]) 24 | ]), 25 | .target( 26 | name: "SwiftCursesTerm", 27 | dependencies: ["curses"]), 28 | .testTarget( 29 | name: "SwiftCursesTermTests", 30 | dependencies: ["SwiftCursesTerm"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftCursesTerm 2 | 3 | You can use the SwiftcursesTerm library to create text based interfaces for your command-line tools. The library uses C `ncurses(3)` library. Currently the library is a work in progress, so the API might change to prived a more Swifty library. 4 | 5 | If you are interested in seeing a full implementation, the following post shows how to build a text-based clock using this library: 6 | 7 | [https://rderik.com/blog/building-a-text-based-application-using-swift-and-ncurses/](https://rderik.com/blog/building-a-text-based-application-using-swift-and-ncurses/) 8 | 9 | And the code for that application can be found in the following GitHub repository: 10 | 11 | [https://github.com/rderik/clock](https://github.com/rderik/clock) 12 | 13 | # Using it on your command-line tools 14 | 15 | 16 | You need to add it as a dependencty to your package manifesto `Package.swift`. For example: 17 | 18 | ```swift 19 | // swift-tools-version:5.2 20 | // The swift-tools-version declares the minimum version of Swift required to build this package. 21 | 22 | import PackageDescription 23 | 24 | let package = Package( 25 | name: "clock", 26 | dependencies: [ 27 | // Dependencies declare other packages that this package depends on. 28 | // .package(url: /* package url */, from: "1.0.0"), 29 | .package(name: "SwiftCursesTerm", url: "https://github.com/rderik/SwiftCursesTerm.git", from: "0.1.1"), 30 | ], 31 | targets: [ 32 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 33 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 34 | .target( 35 | name: "clock", 36 | dependencies: ["SwiftCursesTerm"]), 37 | .testTarget( 38 | name: "clockTests", 39 | dependencies: ["clock"]), 40 | ] 41 | ) 42 | ``` 43 | 44 | Check the latest release version at [https://github.com/rderik/SwiftCursesTerm/releases/latest](https://github.com/rderik/SwiftCursesTerm/releases/latest). 45 | 46 | 47 | Now you can import `SwiftCursesTerm` and use all of its functionality. 48 | 49 | #Example usage 50 | 51 | 52 | Text format: 53 | 54 | ```swift 55 | import Foundation 56 | import SwiftCursesTerm 57 | 58 | var term = SwiftCursesTerm() 59 | term.addStr(content: "Hello, world!", refresh: true) 60 | let green = term.defineColorPair(foreground: CursesColor.white, background: CursesColor.green) 61 | term.setAttributes([TextAttribute.bold, TextAttribute.underline], colorPair: green) 62 | term.addStr(content: "Hello, in Green!", refresh: true) 63 | getch() 64 | term.shutdown() 65 | exit(EXIT_SUCCESS) 66 | ``` 67 | 68 | The `SwiftCursesTerm` object frees memory when it is deinitilized, but if you want to make sure it frees the memory call the function `shutdown()` and it'll close the ncurses session and free any window you created. 69 | 70 | Windows 71 | 72 | ```swift 73 | import Foundation 74 | import SwiftCursesTerm 75 | 76 | var term = SwiftCursesTerm() 77 | term.refresh() 78 | var win1 = term.newWindow(height: 20, width: 20, line: 0, column: 0) 79 | var win2 = term.newWindow(height: 20, width: 20, line: 0, column: 21) 80 | term.addStr(window: win1, content: "Hello, world!", refresh: true) 81 | let green = term.defineColorPair(foreground: CursesColor.white, background: CursesColor.green) 82 | term.setWindowColorPair(window: win2, colorPair: green) 83 | term.setAttributes(window: win2, [TextAttribute.dim, TextAttribute.underline], colorPair: green) 84 | term.addStrTo(window: win2, content: "Hello, in Green!", line: 10, column: 0, refresh: true) 85 | getch() 86 | term.shutdown() 87 | exit(EXIT_SUCCESS) 88 | ``` 89 | 90 | Notice that you can call `addStr` to add a String on the current cursor position or call `addStrTo` to move to a position and then add the string. 91 | 92 | I encourage you to read the `SwiftCursesTerm.swift` file to view all the available options. 93 | 94 | # Contributing 95 | 96 | If you want to contribute: 97 | 98 | + Fork the project 99 | + Create a branch to hold your changes 100 | + Create a pull request 101 | 102 | # Author 103 | 104 | Derik Ramirez - [https://rderik.com](https://rderik.com) 105 | 106 | # License 107 | 108 | This library is under the [MIT Licence][./LICENSE] 109 | -------------------------------------------------------------------------------- /Sources/SwiftCursesTerm/Color.swift: -------------------------------------------------------------------------------- 1 | public enum CursesColor: Int { 2 | case clear = -1 3 | case black = 0 //curses.COLOR_BLACK 4 | case red = 1 //curses.COLOR_RED 5 | case green = 2 //curses.COLOR_GREEN 6 | case yellow = 3 //curses.COLOR_BROWN 7 | case blue = 4 //curses.COLOR_BLUE 8 | case magenta = 5 //curses.COLOR_MAGENTA 9 | case cyan = 6 //curses.COLOR_CYAN 10 | case white = 7 //curses.COLOR_WHITE 11 | } 12 | -------------------------------------------------------------------------------- /Sources/SwiftCursesTerm/SwiftCursesTerm.swift: -------------------------------------------------------------------------------- 1 | import curses 2 | import Foundation 3 | 4 | public typealias SCTWindowId = Int 5 | public typealias SCTColorPair = Int 6 | 7 | public class SwiftCursesTerm { 8 | 9 | var windows = [OpaquePointer]() 10 | var colours = 0 11 | 12 | 13 | public init() { 14 | curses.setlocale(LC_CTYPE, "en_US.UTF-8"); 15 | curses.newterm(nil, stderr, stdin) 16 | curses.use_default_colors() 17 | curses.start_color() 18 | } 19 | 20 | public func addStr(window: SCTWindowId? = nil, content: String, refresh: Bool = false) { 21 | if let window = window { 22 | curses.waddstr(windows[window - 1], content) 23 | } else { 24 | curses.addstr(content) 25 | } 26 | if refresh { 27 | self.refresh(window: window) 28 | } 29 | } 30 | 31 | public func newWindow(height: Int, width: Int, line: Int = 0, column: Int = 0) -> SCTWindowId { 32 | let win = newwin(Int32(height), Int32(width), Int32(line), Int32(column)) 33 | if win == nil { 34 | return -1 35 | } 36 | windows.append(win!) 37 | return windows.count 38 | } 39 | 40 | public func deleteWindow(window: Int) { 41 | guard window >= 1 && windows.count >= window else { return } 42 | delwin(windows[window - 1]) 43 | windows.remove(at: window - 1) 44 | } 45 | 46 | public func addStrTo(window: SCTWindowId? = nil, content: String, line: Int, column: Int, refresh: Bool = false) { 47 | self.move(window: window, line: line, column: column) 48 | self.addStr(window: window, content: content, refresh: refresh) 49 | } 50 | 51 | public func move(window: SCTWindowId?, line: Int, column: Int) { 52 | if let window = window { 53 | curses.wmove(windows[window - 1], Int32(line), Int32(column)) 54 | } else { 55 | curses.move(Int32(line), Int32(column)) 56 | } 57 | } 58 | 59 | public func refresh(window: SCTWindowId? = nil) { 60 | if let window = window { 61 | curses.wrefresh(windows[window - 1]) 62 | } else { 63 | curses.refresh() 64 | } 65 | } 66 | 67 | public func defineColorPair(foreground: CursesColor, background: CursesColor) -> SCTColorPair { 68 | colours += 1 69 | init_pair(Int16(colours), Int16(foreground.rawValue), Int16(background.rawValue)) 70 | return colours 71 | } 72 | 73 | public func setColor(window: SCTWindowId? = nil, colorPair color: SCTColorPair = 0) { 74 | var colorPair = TextAttribute.normal.cursesValue 75 | if color > 0 { 76 | colorPair = COLOR_PAIR(Int32(color)) 77 | } 78 | if let window = window { 79 | curses.wattrset(windows[window - 1], colorPair) 80 | } else { 81 | attrset(colorPair) 82 | } 83 | } 84 | 85 | public func setAttributes(window: SCTWindowId? = nil, _ attrs: [TextAttribute], colorPair: SCTColorPair? = nil) { 86 | let attrsValues = attrs.map { $0.cursesValue } 87 | if attrsValues.count > 0 { 88 | var consolidatedAttr = attrsValues.first! 89 | for attribute in attrsValues { 90 | consolidatedAttr |= attribute 91 | } 92 | if let colorPair = colorPair { 93 | consolidatedAttr |= COLOR_PAIR(Int32(colorPair)) 94 | } 95 | if let window = window { 96 | curses.wattrset(windows[window - 1],consolidatedAttr) 97 | } else { 98 | curses.attrset(consolidatedAttr) 99 | } 100 | } else { 101 | if let colorPair = colorPair { 102 | setColor(window: window, colorPair: colorPair) 103 | } 104 | } 105 | } 106 | 107 | public func setWindowColorPair(window: SCTWindowId? = nil, colorPair color: SCTColorPair = 0) { 108 | var colorPair = TextAttribute.normal.cursesValue 109 | if color > 0 { 110 | colorPair = COLOR_PAIR(Int32(color)) 111 | } 112 | if let window = window { 113 | curses.wbkgd(windows[window - 1], chtype(colorPair)) 114 | } else { 115 | bkgd(chtype(colorPair)) 116 | } 117 | } 118 | 119 | public func resetAttributes() { 120 | setAttributes([TextAttribute.normal]) 121 | } 122 | 123 | public func noDelay(window: SCTWindowId? = nil, _ active: Bool) { 124 | if let window = window { 125 | curses.nodelay(windows[window - 1], active) 126 | } else { 127 | curses.nodelay(stdscr, active) 128 | } 129 | } 130 | 131 | public func shutdown() { 132 | guard windows.count > 0 else { endwin(); return } 133 | for i in (1 ... windows.count).reversed() { 134 | deleteWindow(window: i) 135 | } 136 | endwin() 137 | } 138 | deinit { 139 | shutdown() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/SwiftCursesTerm/TextAttribute.swift: -------------------------------------------------------------------------------- 1 | public enum TextAttribute: UInt { 2 | case normal 3 | case attributes 4 | case charText 5 | case color 6 | case standOut 7 | case underline 8 | case reverse 9 | case blink 10 | case dim 11 | case bold 12 | case altCharSet 13 | case invisible 14 | case protected 15 | case horizontal 16 | case left 17 | case low 18 | case right 19 | case top 20 | case vertical 21 | 22 | static let NCURSES_ATTR_SHIFT = 8 23 | static func NCURSES_BITS(_ mask: UInt32, _ shift: UInt32) -> CInt { return CInt( mask << (shift + UInt32(NCURSES_ATTR_SHIFT))) } 24 | static let A_NORMAL = CInt(1) ^ CInt(1) 25 | static let A_ATTRIBUTES = NCURSES_BITS(~(UInt32(1) - UInt32(1)), 0) 26 | static let A_CHARTEXT = NCURSES_BITS(1, 0) - 1 27 | static let A_COLOR = NCURSES_BITS((1 << 8) - 1, 0) 28 | static let A_STANDOUT = NCURSES_BITS(1, 8) 29 | static let A_UNDERLINE = NCURSES_BITS(1, 9) 30 | static let A_REVERSE = NCURSES_BITS(1, 10) 31 | static let A_BLINK = NCURSES_BITS(1, 11) 32 | static let A_DIM = NCURSES_BITS(1, 12) 33 | static let A_BOLD = NCURSES_BITS(1, 13) 34 | static let A_ALTCHARSET = NCURSES_BITS(1, 14) 35 | static let A_INVIS = NCURSES_BITS(1, 15) 36 | static let A_PROTECT = NCURSES_BITS(1, 16) 37 | static let A_HORIZONTAL = NCURSES_BITS(1, 17) 38 | static let A_LEFT = NCURSES_BITS(1, 18) 39 | static let A_LOW = NCURSES_BITS(1, 19) 40 | static let A_RIGHT = NCURSES_BITS(1, 20) 41 | static let A_TOP = NCURSES_BITS(1, 21) 42 | static let A_VERTICAL = NCURSES_BITS(1, 22) 43 | 44 | var cursesValue: Int32 { 45 | switch self { 46 | case .normal: return TextAttribute.A_NORMAL 47 | case .attributes: return TextAttribute.A_ATTRIBUTES 48 | case .charText: return TextAttribute.A_CHARTEXT 49 | case .color: return TextAttribute.A_COLOR 50 | case .standOut: return TextAttribute.A_STANDOUT 51 | case .underline: return TextAttribute.A_UNDERLINE 52 | case .reverse: return TextAttribute.A_REVERSE 53 | case .blink: return TextAttribute.A_BLINK 54 | case .dim: return TextAttribute.A_DIM 55 | case .bold: return TextAttribute.A_BOLD 56 | case .altCharSet: return TextAttribute.A_ALTCHARSET 57 | case .invisible: return TextAttribute.A_INVIS 58 | case .protected: return TextAttribute.A_PROTECT 59 | case .horizontal: return TextAttribute.A_HORIZONTAL 60 | case .left: return TextAttribute.A_LEFT 61 | case .low: return TextAttribute.A_LOW 62 | case .right: return TextAttribute.A_RIGHT 63 | case .top: return TextAttribute.A_TOP 64 | case .vertical: return TextAttribute.A_VERTICAL 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/curses/curses.h: -------------------------------------------------------------------------------- 1 | #define _XOPEN_SOURCE_EXTENDED 1 2 | #include 3 | #include 4 | -------------------------------------------------------------------------------- /Sources/curses/module.modulemap: -------------------------------------------------------------------------------- 1 | module curses { 2 | header "curses.h" 3 | link "ncurses" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftCursesTermTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftCursesTermTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SwiftCursesTermTests/SwiftCursesTermTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftCursesTerm 3 | 4 | final class SwiftCursesTermTests: XCTestCase { 5 | func testExample() { 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 | XCTAssertEqual(SwiftCursesTerm().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/SwiftCursesTermTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftCursesTermTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------