├── .gitignore ├── .spi.yml ├── .zed └── settings.json ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── C_ncurses │ ├── c_ncurses.h │ └── module.modulemap ├── C_ncursesBinds │ ├── C_ncursesBinds.c │ └── include │ │ └── C_ncursesBinds.h ├── C_ncursesw │ ├── module.modulemap │ └── ncursesw.h ├── Examples │ ├── 01-helloWorld.swift │ ├── 02-init.swift │ ├── 03-print.swift │ ├── 04-input.swift │ ├── 05-attributes.swift │ ├── 06-chgat.swift │ ├── 07-window.swift │ ├── 08-more-border-functions.swift │ ├── 09-colors.swift │ ├── 10-keybboard.swift │ ├── 11-mouse.swift │ ├── 12-temporarilyLeavingCursesMode.swift │ ├── README.md │ ├── main.swift │ └── x01-color-definition.swift └── SwiftCurses │ ├── LinuxCompatibility.swift │ ├── Window+attributes.swift │ ├── Window+border.swift │ ├── Window+clear.swift │ ├── Window+controls.swift │ ├── Window+dump.swift │ ├── Window+input.swift │ ├── Window+mouse.swift │ ├── Window+output.swift │ ├── Window+util.swift │ ├── Window.swift │ ├── color.swift │ ├── error.swift │ ├── initScreen.swift │ ├── kernel.swift │ ├── keycode.swift │ ├── mouseEvent.swift │ └── settings.swift └── Tests └── ncursesTests └── ncursesTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | test.c 11 | a.out 12 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftCurses] 5 | 6 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | // Folder-specific settings 2 | // 3 | // For a full list of overridable settings, and general information on folder-specific settings, 4 | // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 5 | { 6 | "languages": { 7 | "Swift": { 8 | "tab_size": 4, 9 | "hard_tabs": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jonas Everaert and contributers 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 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | var targets: [Target] = [ 7 | .systemLibrary( 8 | name: "C_ncurses", 9 | pkgConfig: "ncurses", 10 | providers: [ 11 | .apt(["libncurses5-dev"]), 12 | ]), 13 | .target( 14 | name: "C_ncursesBinds", 15 | dependencies: ["C_ncurses"], 16 | cSettings: [ 17 | .headerSearchPath("../C_ncurses") 18 | ]), 19 | .target( 20 | name: "SwiftCurses", 21 | dependencies: [ 22 | "C_ncurses", 23 | "C_ncursesBinds", 24 | ], 25 | swiftSettings: []), 26 | .executableTarget( 27 | name: "Examples", 28 | dependencies: ["SwiftCurses"], 29 | exclude: ["README.md"], 30 | swiftSettings: [.unsafeFlags([ 31 | "-Xfrontend", 32 | "-warn-long-function-bodies=100", 33 | "-Xfrontend", 34 | "-warn-long-expression-type-checking=100" 35 | ])]), 36 | .testTarget( 37 | name: "ncursesTests", 38 | dependencies: ["SwiftCurses"]), 39 | ] 40 | 41 | #if os(macOS) 42 | targets 43 | .first(where: { $0.name == "SwiftCurses" })! 44 | .swiftSettings! 45 | .append(.define("SWIFTCURSES_OPAQUE")) 46 | #else 47 | targets.append( 48 | .systemLibrary( 49 | name: "C_ncursesw", 50 | pkgConfig: "ncursesw", 51 | providers: [ 52 | .brew(["ncurses"]), 53 | .apt(["libncursesw5-dev"]), 54 | .yum(["ncurses-devel"]) 55 | ]) 56 | ) 57 | targets 58 | .first(where: { $0.name == "SwiftCurses" })! 59 | .dependencies 60 | .append("C_ncursesw") 61 | #endif 62 | 63 | let package = Package( 64 | name: "SwiftCurses", 65 | products: [ 66 | .library( 67 | name: "SwiftCurses", 68 | targets: ["SwiftCurses"]), 69 | .executable( 70 | name: "Examples", 71 | targets: ["Examples"]) 72 | ], 73 | dependencies: [], 74 | targets: targets) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

SwiftCurses

3 | ❰ 4 | examples 5 | | 6 | documentation 7 | ❱ 8 |

9 |
10 | 11 | 12 |

13 | 14 | SwiftCurses is a Swifty wrapper for ncurses. 15 | 16 | > ncurses - CRT screen handling and optimization package 17 | 18 | ## Hello World 19 | 20 | ```swift 21 | import SwiftCurses 22 | 23 | try initScreen() { scr in 24 | try scr.print("Hello world !!!") 25 | scr.refresh() 26 | try scr.getChar() 27 | } 28 | ``` 29 | 30 | ## Installation 31 | 32 | [ncurses](https://invisible-island.net/ncurses#packages) must be installed on the system. 33 | 34 | In your swift package: 35 | 36 | ```swift 37 | dependencies: [ 38 | .package(url: "https://github.com/jomy10/SwiftCurses.git", branch: "master") 39 | ] 40 | ``` 41 | 42 | In a swift target: 43 | 44 | ```swift 45 | .target( 46 | name: "MyTarget", 47 | dependencies: ["SwiftCurses"] 48 | ) 49 | ``` 50 | 51 | ## Documentation / tutorials / links 52 | 53 | There is a great ncurses tutorial you can find [here](https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/), 54 | the [examples in this repository](/Sources/Examples) show the examples seen in the tutorial. 55 | 56 | NCurses documentation can be found [here](https://invisible-island.net/ncurses/man/ncurses.3x.html), 57 | though keep in mind some functions may be missing/have a different name. 58 | 59 | [ncurses info](https://invisible-island.net/ncurses/) 60 | 61 | ### Running the examples 62 | 63 | ```sh 64 | swift run Examples [name of the example (see main.swift)] 65 | ``` 66 | 67 | ## Questions 68 | 69 | Feel free to ask any questions. 70 | 71 | ## Contributing 72 | 73 | Always looking at improving this library, feel free to leave suggestions/pull requests. 74 | 75 | ## TODO 76 | 77 | - [x] border: https://invisible-island.net/ncurses/man/curs_border.3x.html 78 | - [x] scr_dump: https://invisible-island.net/ncurses/man/curs_scr_dump.3x.html 79 | - https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/otherlib.html 80 | - fill in the todos 81 | 82 | ## License 83 | 84 | Since the original is licensed under the MIT license, this library is also 85 | licensed under the [MIT license](LICENSE). 86 | -------------------------------------------------------------------------------- /Sources/C_ncurses/c_ncurses.h: -------------------------------------------------------------------------------- 1 | #define NCURSES_WIDECHAR 1 2 | #include 3 | #include 4 | -------------------------------------------------------------------------------- /Sources/C_ncurses/module.modulemap: -------------------------------------------------------------------------------- 1 | module ncurses [system] { 2 | header "c_ncurses.h" 3 | link "ncurses" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/C_ncursesBinds/C_ncursesBinds.c: -------------------------------------------------------------------------------- 1 | #define NCURSES_WIDECHAR 1 2 | #include 3 | #include 4 | #include // Required for wctomb 5 | #include "include/C_ncursesBinds.h" 6 | 7 | #ifndef wget_wch 8 | int wget_wch(WINDOW *win, wint_t *ch); 9 | #endif 10 | #ifndef waddwstr 11 | int waddwstr(WINDOW *win, const wint_t *ch); 12 | #endif 13 | 14 | // Swift does not pick up the `get_wch` method 15 | inline int swift_wget_wch(WINDOW* win, wchar_t* ch) { 16 | return wget_wch(win, ch); 17 | } 18 | 19 | inline int swift_waddwstr(WINDOW* win, const wchar_t* ch) { 20 | return waddwstr(win, ch); 21 | } 22 | 23 | inline struct swift_YX swift_getbegyx(WINDOW* win) { 24 | struct swift_YX yx; 25 | getbegyx(win, yx.y, yx.x); 26 | return yx; 27 | } 28 | 29 | inline struct swift_YX swift_getmaxyx(WINDOW* win) { 30 | struct swift_YX yx; 31 | getmaxyx(win, yx.y, yx.x); 32 | return yx; 33 | } 34 | 35 | inline struct swift_YX swift_getyx(WINDOW* win) { 36 | struct swift_YX yx; 37 | getyx(win, yx.y, yx.x); 38 | return yx; 39 | } 40 | 41 | inline struct swift_YX swift_getparyx(WINDOW* win) { 42 | struct swift_YX yx; 43 | getparyx(win, yx.y, yx.x); 44 | return yx; 45 | } 46 | 47 | //============ 48 | // Attributes 49 | //============ 50 | 51 | const int swift_A_NORMAL = A_NORMAL; 52 | const int swift_A_STANDOUT = A_STANDOUT; 53 | const int swift_A_UNDERLINE = A_UNDERLINE; 54 | const int swift_A_REVERSE = A_REVERSE; 55 | const int swift_A_BLINK = A_BLINK; 56 | const int swift_A_DIM = A_DIM; 57 | const int swift_A_BOLD = A_BOLD; 58 | const int swift_A_PROTECT = A_PROTECT; 59 | const int swift_A_INVIS = A_INVIS; 60 | const int swift_A_ALTCHARSET = A_ALTCHARSET; 61 | const int swift_A_CHARTEXT = A_CHARTEXT; 62 | inline int swift_COLOR_PAIR(int n) { 63 | return COLOR_PAIR(n); 64 | } 65 | #ifdef A_ITALIC 66 | const int swift_A_ITALIC = A_ITALIC; 67 | #else 68 | const int swift_A_ITALIC = NCURSES_BITS(1U,23); 69 | #endif 70 | 71 | //==================== 72 | // Wide char function 73 | //==================== 74 | 75 | struct swift_wcharBytesReturnType wcharToBytes(wchar_t wc) { 76 | static char charBuffer[10]; 77 | int len = wctomb(charBuffer, wc); 78 | return (struct swift_wcharBytesReturnType) { 79 | (int8_t*) charBuffer, 80 | len 81 | }; 82 | } 83 | 84 | //====== 85 | // Keys 86 | //====== 87 | 88 | // fn keys 89 | inline int swift_key_f(int n) { 90 | return KEY_F(n); 91 | } 92 | 93 | // Mouse events (macros not being picked uup by swift) 94 | const mmask_t swift_button1Pressed = BUTTON1_PRESSED; // mouse button 1 down 95 | const mmask_t swift_button1Released = BUTTON1_RELEASED; // mouse button 1 up 96 | const mmask_t swift_button1Clicked = BUTTON1_CLICKED; // mouse button 1 clicked 97 | const mmask_t swift_button1DoubleClicked = BUTTON1_DOUBLE_CLICKED; // mouse button 1 double clicked 98 | const mmask_t swift_button1TripleClicked = BUTTON1_TRIPLE_CLICKED; // mouse button 1 triple clicked 99 | const mmask_t swift_button2Pressed = BUTTON2_PRESSED; // mouse button 2 down 100 | const mmask_t swift_button2Released = BUTTON2_RELEASED; // mouse button 2 up 101 | const mmask_t swift_button2Clicked = BUTTON2_CLICKED; // mouse button 2 clicked 102 | const mmask_t swift_button2DoubleClicked = BUTTON2_DOUBLE_CLICKED; // mouse button 2 double clicked 103 | const mmask_t swift_button2TripleClicked = BUTTON2_TRIPLE_CLICKED; // mouse button 2 triple clicked 104 | const mmask_t swift_button3Pressed = BUTTON3_PRESSED; // mouse button 3 down 105 | const mmask_t swift_button3Released = BUTTON3_RELEASED; // mouse button 3 up 106 | const mmask_t swift_button3Clicked = BUTTON3_CLICKED; // mouse button 3 clicked 107 | const mmask_t swift_button3DoubleClicked = BUTTON3_DOUBLE_CLICKED; // mouse button 3 double clicked 108 | const mmask_t swift_button3TripleClicked = BUTTON3_TRIPLE_CLICKED; // mouse button 3 triple clicked 109 | const mmask_t swift_button4Pressed = BUTTON4_PRESSED; // mouse button 4 down 110 | const mmask_t swift_button4Released = BUTTON4_RELEASED; // mouse button 4 up 111 | const mmask_t swift_button4Clicked = BUTTON4_CLICKED; // mouse button 4 clicked 112 | const mmask_t swift_button4DoubleClicked = BUTTON4_DOUBLE_CLICKED; // mouse button 4 double clicked 113 | const mmask_t swift_button4TripleClicked = BUTTON4_TRIPLE_CLICKED; // mouse button 4 triple clicked 114 | const mmask_t swift_buttonShift = BUTTON_SHIFT; // shift was down during button state change 115 | const mmask_t swift_buttonCtrl = BUTTON_CTRL; // control was down during button state change 116 | const mmask_t swift_buttonAlt = BUTTON_ALT; // alt was down during button state change 117 | const mmask_t swift_allMouseEvents = ALL_MOUSE_EVENTS; // report all button state changes 118 | const mmask_t swift_reportMousePosition = REPORT_MOUSE_POSITION; // report mouse movement 119 | 120 | // TODO: ACS (https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/misc.html) 121 | -------------------------------------------------------------------------------- /Sources/C_ncursesBinds/include/C_ncursesBinds.h: -------------------------------------------------------------------------------- 1 | #ifndef _SWIFTCURSES_BINDS 2 | #define _SWIFTCURSES_BINDS 3 | 4 | #define NCURSES_WIDECHAR 1 5 | #include 6 | #include 7 | 8 | int swift_wget_wch(WINDOW* win, wchar_t* ch); 9 | int swift_waddwstr(WINDOW* win, const wchar_t* ch); 10 | 11 | struct swift_YX { 12 | int y, x; 13 | }; 14 | 15 | struct swift_YX swift_getbegyx(WINDOW* win); 16 | struct swift_YX swift_getmaxyx(WINDOW* win); 17 | struct swift_YX swift_getyx(WINDOW* win); 18 | struct swift_YX swift_getparyx(WINDOW* win); 19 | 20 | //============ 21 | // Attributes 22 | //============ 23 | 24 | extern const int swift_A_NORMAL; 25 | extern const int swift_A_STANDOUT; 26 | extern const int swift_A_UNDERLINE; 27 | extern const int swift_A_REVERSE; 28 | extern const int swift_A_BLINK; 29 | extern const int swift_A_DIM; 30 | extern const int swift_A_BOLD; 31 | extern const int swift_A_PROTECT; 32 | extern const int swift_A_INVIS; 33 | extern const int swift_A_ALTCHARSET; 34 | extern const int swift_A_CHARTEXT; 35 | int swift_COLOR_PAIR(int n); 36 | extern const int swift_A_ITALIC; 37 | 38 | //==================== 39 | // Wide char function 40 | //==================== 41 | 42 | struct swift_wcharBytesReturnType { 43 | const int8_t* bytes; 44 | int len; 45 | }; 46 | 47 | struct swift_wcharBytesReturnType wcharToBytes(wchar_t wc); 48 | 49 | //====== 50 | // Keys 51 | //====== 52 | 53 | int swift_key_f(int n); 54 | 55 | extern const mmask_t swift_button1Pressed; // mouse button 1 down 56 | extern const mmask_t swift_button1Released; // mouse button 1 up 57 | extern const mmask_t swift_button1Clicked; // mouse button 1 clicked 58 | extern const mmask_t swift_button1DoubleClicked; // mouse button 1 double clicked 59 | extern const mmask_t swift_button1TripleClicked; // mouse button 1 triple clicked 60 | extern const mmask_t swift_button2Pressed; // mouse button 2 down 61 | extern const mmask_t swift_button2Released; // mouse button 2 up 62 | extern const mmask_t swift_button2Clicked; // mouse button 2 clicked 63 | extern const mmask_t swift_button2DoubleClicked; // mouse button 2 double clicked 64 | extern const mmask_t swift_button2TripleClicked; // mouse button 2 triple clicked 65 | extern const mmask_t swift_button3Pressed; // mouse button 3 down 66 | extern const mmask_t swift_button3Released; // mouse button 3 up 67 | extern const mmask_t swift_button3Clicked; // mouse button 3 clicked 68 | extern const mmask_t swift_button3DoubleClicked; // mouse button 3 double clicked 69 | extern const mmask_t swift_button3TripleClicked; // mouse button 3 triple clicked 70 | extern const mmask_t swift_button4Pressed; // mouse button 4 down 71 | extern const mmask_t swift_button4Released; // mouse button 4 up 72 | extern const mmask_t swift_button4Clicked; // mouse button 4 clicked 73 | extern const mmask_t swift_button4DoubleClicked; // mouse button 4 double clicked 74 | extern const mmask_t swift_button4TripleClicked; // mouse button 4 triple clicked 75 | extern const mmask_t swift_buttonShift; // shift was down during button state change 76 | extern const mmask_t swift_buttonCtrl; // control was down during button state change 77 | extern const mmask_t swift_buttonAlt; // alt was down during button state change 78 | extern const mmask_t swift_allMouseEvents; // report all button state changes 79 | extern const mmask_t swift_reportMousePosition; // report mouse movement 80 | 81 | #endif 82 | -------------------------------------------------------------------------------- /Sources/C_ncursesw/module.modulemap: -------------------------------------------------------------------------------- 1 | module ncursesw [system] { 2 | header "ncursesw.h" 3 | link "ncursesw" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/C_ncursesw/ncursesw.h: -------------------------------------------------------------------------------- 1 | #ifndef CCC__NCURSES_H 2 | #define CCC__NCURSES_H 3 | 4 | #include "ncursesw.h" 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /Sources/Examples/01-helloWorld.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Hello World Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/helloworld.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 1: The Hello World !!! Program 11 | func helloWorld() throws { 12 | try initScreen() { scr in 13 | try scr.print("Hello world !!!") 14 | scr.refresh() 15 | try scr.getChar() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Examples/02-init.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Init Usage Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/init.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 2. Initialization Function Usage Example 11 | func initUsage() throws { 12 | try initScreen( 13 | settings: [.raw, .noEcho], 14 | windowSettings: [.keypad(true)] 15 | ) { scr in 16 | try scr.print("Type any character to see it in bold\n") 17 | let ch = try scr.getChar() 18 | 19 | switch ch { 20 | case .code(let code): 21 | if code == KeyCode.f(1) { 22 | try scr.print("F1 key pressed") 23 | } else { 24 | try scr.print("Function key \(code) pressed") 25 | } 26 | case .char(let char): 27 | try scr.print("The pressed key is ") 28 | try scr.withAttrs(.bold) { 29 | try scr.print(char) 30 | } 31 | } 32 | 33 | scr.refresh() 34 | try scr.getChar() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Examples/03-print.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Print Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/printw.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 3. A Simple printw example 11 | func printExample() throws { 12 | let mesg = "Just a string" 13 | 14 | try initScreen( 15 | settings: [], 16 | windowSettings: [] 17 | ) { scr in 18 | // `maxYX` returns a `Coordinate` struct, which can be deconstructed by turning 19 | // it into a tuple using `.tuple` 20 | let (row, col) = scr.maxYX.tuple // Get the number of rows and columns 21 | try scr.print(row: row/2, col: (col - Int32(mesg.count))/2, mesg) // print the message at the center of the screen 22 | try scr.print(row: row-2, col: 0, "This screen has \(row) rows and \(col) columns\n") 23 | try scr.print("Try resizing the window (if possible and then run this program again)") 24 | scr.refresh() 25 | try scr.getChar() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Examples/04-input.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Input Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/scanw.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | func inputExample() throws { 11 | let mesg = "Enter a string: " 12 | 13 | try initScreen( 14 | settings: [], 15 | windowSettings: [] 16 | ) { scr in 17 | let (row, col) = scr.maxYX.tuple 18 | try scr.print(row: row/2, col: (col - Int32(mesg.count))/2, mesg) 19 | 20 | let str = try scr.getStr() 21 | try scr.print(row: row - 2, col: 0, "You entered: \(str)") 22 | try scr.getChar() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Examples/05-attributes.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Attributes Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/attrib.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | import Foundation 10 | 11 | /// Example 5. A Simple Attributes example 12 | /// 13 | /// This example will show the contents of a file, and mark any multiline 14 | /// comment (/**/) in bold (passed in as a parameter; 15 | /// `swift run SwiftCursesExamples attributes [filename]`) 16 | func attributesExample() throws { 17 | let arg: String 18 | if CommandLine.argc == 3 { 19 | arg = CommandLine.arguments[2] 20 | } else if CommandLine.argc == 2 { 21 | arg = CommandLine.arguments[1] 22 | } else { 23 | print("Usage: \(CommandLine.arguments[0]) ") 24 | return 25 | } 26 | 27 | let url = URL(fileURLWithPath: arg) 28 | let contents = try String(contentsOf: url) 29 | try initScreen( 30 | settings: [], 31 | windowSettings: [] 32 | ) { scr in 33 | let (row, _) = scr.maxYX.tuple // Boundary of the screen 34 | var prev: Character = "\0" 35 | 36 | try contents.forEach { ch in 37 | var (y, x) = scr.yx.tuple // current curser position 38 | if (y == (row - 1)) { // are we at the end of the screen 39 | try scr.print("<-Press Any Key->") 40 | try scr.getChar() 41 | scr.clear() // clear the screen 42 | try scr.move(row: 0, col: 0)// start at the beginning of the screen 43 | } 44 | if prev == "/" && ch == "*" { // if it is / and * then only switch bold on 45 | scr.attrOn(.bold) // turn bold on 46 | (y, x) = scr.yx.tuple 47 | try scr.move(row: y, col: x - 1) // back up ine cursor position 48 | try scr.print("/\(ch)") // the actual printing is done here 49 | } else { 50 | try scr.print(ch) 51 | } 52 | 53 | scr.refresh() 54 | if prev == "*" && ch == "/" { 55 | scr.attrOff(.bold) // switch if off once we got * and then / 56 | } 57 | 58 | prev = ch 59 | } 60 | try scr.getChar() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Examples/06-chgat.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Chgat Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/attrib.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 6. Chgat() Usage Example 11 | func chgatExample() throws { 12 | try initScreen( 13 | settings: [.colors], 14 | windowSettings: [] 15 | ) { scr in 16 | try ColorPair.define(1, fg: Color.cyan, bg: Color.black) // init_pair in ncurses 17 | try scr.print("A Big string which i didn't care to type fully ") 18 | try scr.chgat(row: 0, col: 0, -1, .blink, color: 1) 19 | /* 20 | * First two parameters specify the position at which to start 21 | * Third parameter number of characters to update. -1 means till 22 | * end of line 23 | * Forth parameter is the normal attribute you wanted to give 24 | * to the charcter 25 | * Fifth is the color index. It is the index given during init_pair() 26 | * use 0 if you didn't want color 27 | * Sixth one is always NULL (has a default value in this implementation) 28 | */ 29 | scr.refresh() 30 | try scr.getChar() 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Sources/Examples/07-window.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Window Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/windows.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 7. Window Border example 11 | /// 12 | /// Use arrow keys to move the window, press f1 to exit 13 | /// (simple, unoptimized example) 14 | func windowExample() throws { 15 | try initScreen( 16 | settings: [.cbreak], 17 | windowSettings: [.keypad(true)] 18 | ) { scr in 19 | let height: Int32 = 3 20 | let width: Int32 = 10 21 | let (row, col) = scr.maxYX.tuple 22 | var starty = (row - height) / 2 23 | var startx = (col - width) / 2 24 | try scr.print("Press F1 to exit") 25 | scr.refresh() 26 | 27 | // Create our custom window defined below 28 | var myWin: BorderedWindow? = try BorderedWindow(rows: height, cols: width, begin: (starty, startx), settings: []) 29 | _ = myWin // supress Swift warning 30 | var ch = try scr.getChar() 31 | var keyCode: Int32? 32 | if case .code(let code) = ch { 33 | keyCode = code 34 | } 35 | while (keyCode ?? -1) != KeyCode.f(1) { 36 | switch keyCode ?? -1 { 37 | case KeyCode.left: 38 | startx -= 1 39 | myWin = nil // destroy the old window 40 | myWin = try BorderedWindow(rows: height, cols: width, begin: (starty, startx), settings: []) 41 | case KeyCode.right: 42 | startx += 1 43 | myWin = nil 44 | myWin = try BorderedWindow(rows: height, cols: width, begin: (starty, startx), settings: []) 45 | case KeyCode.up: 46 | starty -= 1 47 | myWin = nil 48 | myWin = try BorderedWindow(rows: height, cols: width, begin: (starty, startx), settings: []) 49 | case KeyCode.down: 50 | starty += 1 51 | myWin = nil 52 | myWin = try BorderedWindow(rows: height, cols: width, begin: (starty, startx), settings: []) 53 | default: 54 | break 55 | } 56 | 57 | ch = try scr.getChar() 58 | if case .code(let code) = ch { 59 | keyCode = code 60 | } else { 61 | keyCode = nil 62 | } 63 | } 64 | } 65 | } 66 | 67 | fileprivate class BorderedWindow: ManagedWindow { 68 | override func onInit() { 69 | self.box() // 0, 0 gives the default characters, or leaving the parameters empty, for the vertical and horizontal lines 70 | self.refresh() // show the box 71 | } 72 | 73 | override func onDeinit() { 74 | // the following code is not necessary as SwiftCurses cleans up windows 75 | // completely, including setting all of its cells to " " 76 | // set all border characters to be a space 77 | // self.border( 78 | // left: " ", 79 | // right: " ", 80 | // top: " ", 81 | // bottom: " ", 82 | // topLeft: " ", 83 | // topRight: " ", 84 | // bottomLeft: " ", 85 | // bottomRight: " " 86 | // ) 87 | // self.refresh() 88 | // delwin is done when this object is deallocated 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Examples/08-more-border-functions.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Border Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/windows.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 8. More border functions 11 | func borderExample() throws { 12 | try initScreen( 13 | settings: [.colors, .cbreak, .noEcho], 14 | windowSettings: [.keypad(true)] 15 | ) { scr in 16 | try ColorPair.define(1, fg: Color.cyan, bg: Color.black) 17 | 18 | let (row, col): (Int32, Int32) = scr.maxYX.tuple 19 | // Initialize the window parameters 20 | var win = Win(row, col) 21 | try win.printParams(scr) 22 | 23 | try scr.withAttrs(.colorPair(1)) { 24 | try scr.print("Press F1 to exit") 25 | scr.refresh() 26 | } 27 | 28 | try win.createBox(scr, flag: true) 29 | var ch = try scr.getChar() 30 | var keyCode: Int32? 31 | if case .code(let code) = ch { 32 | keyCode = code 33 | } 34 | while (keyCode ?? -1) != KeyCode.f(1) { 35 | switch keyCode ?? -1 { 36 | case KeyCode.left: 37 | try win.createBox(scr, flag: false) 38 | win.startx -= 1 39 | try win.createBox(scr, flag: true) 40 | case KeyCode.right: 41 | try win.createBox(scr, flag: false) 42 | win.startx += 1 43 | try win.createBox(scr, flag: true) 44 | case KeyCode.up: 45 | try win.createBox(scr, flag: false) 46 | win.starty -= 1 47 | try win.createBox(scr, flag: true) 48 | case KeyCode.down: 49 | try win.createBox(scr, flag: false) 50 | win.starty += 1 51 | try win.createBox(scr, flag: true) 52 | default: 53 | break 54 | } 55 | 56 | ch = try scr.getChar() 57 | if case .code(let code) = ch { 58 | keyCode = code 59 | } else { 60 | keyCode = nil 61 | } 62 | } 63 | } 64 | } 65 | 66 | fileprivate struct Win { 67 | var startx: Int32 68 | var starty: Int32 69 | var height: Int32 70 | var width: Int32 71 | let border: WinBorder 72 | 73 | init(_ maxLines: Int32, _ maxCols: Int32) { 74 | self.height = 3 75 | self.width = 10 76 | self.starty = (maxLines - self.height) / 2 77 | self.startx = (maxCols - self.width) / 2 78 | self.border = WinBorder() 79 | } 80 | 81 | func printParams(_ win: some WindowProtocol) throws { 82 | #if DEBUG 83 | try win.print(row: 25, col: 0, self.startx, self.starty, self.width, self.height) 84 | win.refresh() 85 | #endif 86 | } 87 | 88 | /// - `win`: the window to draw the box on 89 | func createBox(_ win: some WindowProtocol, flag: Bool) throws { 90 | let x = self.startx 91 | let y = self.starty 92 | let w = self.width 93 | let h = self.height 94 | 95 | if flag == true { 96 | try win.addChar(row: y, col: x, self.border.tl) 97 | try win.addChar(row: y, col: x + w, self.border.tr) 98 | try win.addChar(row: y + h, col: x, self.border.bl) 99 | try win.addChar(row: y + h, col: x + w, self.border.br) 100 | try win.horLine(row: y, col: x + 1, self.border.ts, w - 1) 101 | try win.horLine(row: y + h, col: x + 1, self.border.bs, w - 1) 102 | try win.vertLine(row: y + 1, col: x, self.border.ls, h - 1) 103 | try win.vertLine(row: y + 1, col: x + w, self.border.rs, h - 1) 104 | } else { 105 | for j in y...(y + h) { 106 | for i in x...(x + w) { 107 | try win.addChar(row: j, col: i, " ") 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | fileprivate struct WinBorder { 115 | let ls: Character = "|" 116 | let rs: Character = "|" 117 | let ts: Character = "-" 118 | let bs: Character = "-" 119 | let tl: Character = "+" 120 | let tr: Character = "+" 121 | let bl: Character = "+" 122 | let br: Character = "+" 123 | } 124 | -------------------------------------------------------------------------------- /Sources/Examples/09-colors.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Colors Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/color.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 9. A Simple Color example 11 | func colorsExample() throws { 12 | try initScreen( 13 | settings: [ 14 | .colors // enable colors 15 | ], 16 | windowSettings: [] 17 | ) { scr in 18 | let (row, _) = scr.maxYX.tuple 19 | try ColorPair.define(1, fg: Color.red, bg: Color.black) 20 | 21 | try scr.withAttrs(.colorPair(1)) { 22 | try printInMiddle(scr, row / 2, 0, 0, "Voila !!! In color ...") 23 | } 24 | try scr.getChar() 25 | } 26 | } 27 | 28 | fileprivate func printInMiddle(_ win: some WindowProtocol, _ starty: Int32, _ startx: Int32, _ _width: Int32, _ string: String) throws { 29 | var width = _width 30 | var (y, x) = win.yx.tuple 31 | if startx != 0 { 32 | x = startx 33 | } 34 | if starty != 0 { 35 | y = starty 36 | } 37 | if width == 0 { 38 | width = 80 39 | } 40 | 41 | let length = Int32(string.count) 42 | let temp = (width - length) / 2 43 | x = startx + temp 44 | try win.print(row: y, col: x, string) 45 | win.refresh() 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Examples/10-keybboard.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Keyboard Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/keys.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | fileprivate let WIDTH: Int32 = 30 11 | fileprivate let HEIGHT: Int32 = 10 12 | 13 | fileprivate nonisolated(unsafe) var startx: Int32 = 0 14 | fileprivate nonisolated(unsafe) var starty: Int32 = 0 15 | 16 | fileprivate let choices = [ 17 | "Choice1", 18 | "Choice2", 19 | "Choice3", 20 | "Choice 4", 21 | "Exit", 22 | ] 23 | fileprivate let nChoices = Int32(choices.count) 24 | 25 | /// Example 10. A Simple Key Usage example 26 | func keyboardExample() throws { 27 | let WIDTH: Int32 = 30 28 | let HEIGHT: Int32 = 10 29 | 30 | var startx: Int32 = 0 31 | var starty: Int32 = 0 32 | 33 | var highlight: Int32 = 1 34 | var choice: Int32 = 0 35 | 36 | try initScreen( 37 | settings: [.noEcho, .cbreak], 38 | windowSettings: [] 39 | ) { scr in 40 | scr.clear() 41 | 42 | startx = (80 - WIDTH) / 2 43 | starty = (24 - HEIGHT) / 2 44 | 45 | // WindowSettings.default returns [.keypad(true)], so it can be left out 46 | let menuWin = try ManagedWindow(rows: HEIGHT, cols: WIDTH, begin: (starty, startx)) 47 | try scr.print(row: 0, col: 0, "Use arrow key to go up and down. Press enter to select a choice.") 48 | scr.refresh() 49 | try printMenu(menuWin, highlight) 50 | loop: while true { 51 | let c = try menuWin.getChar() 52 | // try scr.print(row: 1, col: 0, "[\(c)]") 53 | switch c { 54 | case .code(let code): 55 | switch code { 56 | case KeyCode.up: 57 | if highlight == 1 { 58 | highlight = nChoices 59 | } else { 60 | highlight -= 1 61 | } 62 | case KeyCode.down: 63 | if highlight == nChoices { 64 | highlight = 1 65 | } else { 66 | highlight += 1 67 | } 68 | default: 69 | try scr.move(row: 24, col: 0) 70 | try scr.clear(until: .endOfLine) 71 | try scr.print(row: 24, col: 0, "Function key pressed is \(code)") 72 | scr.refresh() 73 | } // switch code 74 | try printMenu(menuWin, highlight) 75 | case .char(let char): 76 | if char == "\n" { // enter pressed = user choice an option 77 | choice = highlight 78 | break loop 79 | } 80 | try scr.move(row: 24, col: 0) 81 | try scr.clear(until: .endOfLine) 82 | try scr.print(row: 24, col: 0, "Character pressed is \(char)") 83 | scr.refresh() 84 | } 85 | } 86 | try scr.print(row: 23, col: 0, "You chose choice \(choice) with choice string \(choices[Int(choice) - 1])\n") 87 | try scr.clear(until: .endOfLine) 88 | try scr.getChar() 89 | scr.refresh() 90 | } 91 | } 92 | 93 | fileprivate func printMenu(_ menuWin: ManagedWindow, _ highlight: Int32) throws { 94 | let x: Int32 = 2 95 | var y: Int32 = 2 96 | menuWin.box() 97 | for i in 0..(_ win: W, _ highlight: Int) throws { 78 | let x: Int32 = 2 79 | var y: Int32 = 2 80 | win.box(0, 0) 81 | for i in 0..= i && mouseX <= i + Int32(choices[Int(choice)].count) { 102 | if choice == nChoices - 1 { 103 | pChoice = -1 104 | } else { 105 | pChoice = Int(choice + 1) 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Examples/12-temporarilyLeavingCursesMode.swift: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // Temporarily leaving curses Example 3 | // 4 | // Written by: Jonas Everaert 5 | // Based on: https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/misc.html 6 | //======================================================================= 7 | 8 | import SwiftCurses 9 | 10 | /// Example 12. Temporarily Leaving Curses Mode 11 | func temporarilyLeavingCurses() throws { 12 | try initScreen(settings: [], windowSettings: []) { scr in 13 | try scr.print("Hello world !!!\n") 14 | scr.refresh() 15 | shellMode { 16 | // do whatever you want in shell mode 17 | print("Hello from shell mode!") 18 | } 19 | try scr.print("Another String\n") 20 | scr.refresh() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | The numbers in the names of the examples represent the example number on 4 | [https://tldp.org/HOWTO/NCURSES-Programming-HOWTO](https://tldp.org/HOWTO/NCURSES-Programming-HOWTO). 5 | 6 | ## Running examples 7 | 8 | ```bash 9 | swift run Examples [name] 10 | ``` 11 | -------------------------------------------------------------------------------- /Sources/Examples/main.swift: -------------------------------------------------------------------------------- 1 | //======================= 2 | // Swift Curses Examples 3 | //======================= 4 | 5 | 6 | func main() throws { 7 | if CommandLine.arguments.count <= 1 { 8 | fatalError("Not enough arguments") 9 | // TODO: TUI for examples 10 | } 11 | 12 | let program = CommandLine.arguments[1] 13 | 14 | switch program { 15 | case "helloWorld": 16 | try helloWorld() 17 | case "init": 18 | try initUsage() 19 | case "print": 20 | try printExample() 21 | case "input": 22 | try inputExample() 23 | case "attributes": 24 | try attributesExample() 25 | case "chgat": 26 | try chgatExample() 27 | case "window": 28 | try windowExample() 29 | case "moreBorderFuncs": 30 | try borderExample() 31 | case "colors": 32 | try colorsExample() 33 | case "keyboard": 34 | try keyboardExample() 35 | case "mouse": 36 | try mouse() 37 | case "tempLeaveCurses": 38 | try temporarilyLeavingCurses() 39 | default: 40 | print("Invalid program \(program)") 41 | } 42 | } 43 | 44 | do { 45 | try main() 46 | } catch { 47 | print("Oops, that's an error:") 48 | print(error) 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Examples/x01-color-definition.swift: -------------------------------------------------------------------------------- 1 | // Change color definition 2 | // Color.forceXTerm256Color() // temporary fix for ncurses not detecting color mode on some terminals even though they support it 3 | // Color.define(Color.red, r: 700, g: 0, b: 0) 4 | 5 | // TODO: actual example 6 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/LinuxCompatibility.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | 3 | #if SWIFTCURSES_OPAQUE 4 | // We define NCURSES_OPAQUE 5 | public typealias WindowPointer = OpaquePointer 6 | #else 7 | public typealias WindowPointer = UnsafeMutablePointer 8 | #endif 9 | 10 | public typealias wchar_t = Int32 11 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+attributes.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | public enum Attribute: Sendable, Hashable { 5 | case normal 6 | case standout 7 | case underline 8 | case reverse 9 | case blink 10 | case dim 11 | case bold 12 | case protect 13 | case invisible 14 | case altCharset 15 | case chartext 16 | case colorPair(_ n: Int32) 17 | case italic 18 | } 19 | 20 | extension Attribute { 21 | @inlinable 22 | var value: Int32 { 23 | switch self { 24 | case .normal: 25 | return swift_A_NORMAL 26 | case .standout: 27 | return swift_A_STANDOUT 28 | case .underline: 29 | return swift_A_UNDERLINE 30 | case .reverse: 31 | return swift_A_REVERSE 32 | case .blink: 33 | return swift_A_BLINK 34 | case .dim: 35 | return swift_A_DIM 36 | case .bold: 37 | return swift_A_BOLD 38 | case .protect: 39 | return swift_A_PROTECT 40 | case .invisible: 41 | return swift_A_INVIS 42 | case .altCharset: 43 | return swift_A_ALTCHARSET 44 | case .chartext: 45 | return swift_A_CHARTEXT 46 | case .colorPair(let n): 47 | return swift_COLOR_PAIR(n) 48 | case .italic: 49 | return swift_A_ITALIC 50 | } 51 | } 52 | } 53 | 54 | extension [Attribute] { 55 | @usableFromInline 56 | func combine() -> Int32 { 57 | if self.count == 1 { 58 | return self[0].value 59 | } else { 60 | return self.reduce(0) { $0 | $1.value } 61 | } 62 | } 63 | } 64 | 65 | extension WindowProtocol { 66 | @inlinable 67 | public func attrOn(_ attrs: Attribute...) { 68 | self.attrOn(attrs) 69 | } 70 | 71 | @inlinable 72 | public func attrOn(_ attrs: [Attribute]) { 73 | ncurses.wattron(self.window, attrs.combine()) 74 | } 75 | 76 | @inlinable 77 | public func attrSet(_ attrs: Attribute...) { 78 | ncurses.wattrset(self.window, attrs.combine()) 79 | } 80 | 81 | @inlinable 82 | public func attrOff(_ attrs: Attribute...) { 83 | self.attrOff(attrs) 84 | } 85 | 86 | @inlinable 87 | public func attrOff(_ attrs: [Attribute]) { 88 | ncurses.wattroff(self.window, attrs.combine()) 89 | } 90 | 91 | public func withAttrs(_ attrs: Attribute..., body: () throws -> ()) rethrows { 92 | self.attrOn(attrs) 93 | 94 | try body() 95 | 96 | // TODO: resotre previous intead of turning off 97 | self.attrOff(attrs) 98 | } 99 | 100 | @inlinable 101 | public func standEnd() { 102 | ncurses.wstandend(self.window) 103 | } 104 | 105 | @available(*, unavailable) 106 | public func attrGet() -> Int32 { 107 | // TODO 108 | return 0 109 | } 110 | 111 | @inlinable 112 | func chgat(_ n: Int32, _ attrs: [Attribute], color pair: Int16, opts: UnsafeRawPointer? = nil) { 113 | ncurses.wchgat(self.window, n, attr_t(attrs.combine()), pair, opts) 114 | } 115 | 116 | @inlinable 117 | public func chgat(row: Int32, col: Int32, _ n: Int32, _ attrs: Attribute..., color pair: Int16, opts: UnsafeRawPointer? = nil) throws { 118 | try self.move(row: row, col: col) 119 | self.chgat(n, attrs, color: pair, opts: opts) 120 | } 121 | 122 | /// change the attributes of already printed text 123 | // TODO: better documentation/more expressive 124 | // TODO: consider renaming to changeAttr 125 | @inlinable 126 | public func chgat(_ n: Int32, _ attrs: Attribute..., color pair: Int16, opts: UnsafeRawPointer? = nil) { 127 | self.chgat(n, attrs, color: pair, opts: opts) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+border.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | // https://invisible-island.net/ncurses/man/curs_border.3x.html 5 | // X/Open does not define any error conditions. This implementation returns an error if the window pointer is null. 6 | extension WindowProtocol { 7 | //========= 8 | // Borders 9 | //========= 10 | 11 | // TODO: UTF-8 support 12 | @inlinable 13 | public func box(_ vertCh: Character = "\u{0}", _ horCh: Character = "\u{0}") { 14 | // X/Open does not define any error conditions. This implementation returns an error if the window pointer is null. 15 | ncurses.box(self.window, UInt32(vertCh.asciiValue!), UInt32(horCh.asciiValue!)) 16 | } 17 | 18 | @inlinable 19 | public func box(_ vertCh: UInt32, _ horCh: UInt32) { 20 | // X/Open does not define any error conditions. This implementation returns an error if the window pointer is null. 21 | ncurses.box(self.window, vertCh, horCh) 22 | } 23 | 24 | // TODO: UTF-8 support 25 | /// - fatal error: 26 | /// - If the character cannot be converted to an ASCII value 27 | @inlinable 28 | public func border( 29 | // sides 30 | left ls: Character = "\u{0}", 31 | right rs: Character = "\u{0}", 32 | top ts: Character = "\u{0}", 33 | bottom bs: Character = "\u{0}", 34 | // corners 35 | topLeft tl: Character = "\u{0}", 36 | topRight tr: Character = "\u{0}", 37 | bottomLeft bl: Character = "\u{0}", 38 | bottomRight br: Character = "\u{0}" 39 | ) { 40 | self.border( 41 | left: UInt32(ls.asciiValue!), 42 | right: UInt32(rs.asciiValue!), 43 | top: UInt32(ts.asciiValue!), 44 | bottom: UInt32(bs.asciiValue!), 45 | topLeft: UInt32(tl.asciiValue!), 46 | topRight: UInt32(tr.asciiValue!), 47 | bottomLeft: UInt32(bl.asciiValue!), 48 | bottomRight: UInt32(br.asciiValue!) 49 | ) 50 | } 51 | 52 | /// If any values are zero, then the default values are used 53 | public func border( 54 | // sides 55 | left ls: UInt32, 56 | right rs: UInt32, 57 | top ts: UInt32, 58 | bottom bs: UInt32, 59 | // corners 60 | topLeft tl: UInt32, 61 | topRight tr: UInt32, 62 | bottomLeft bl: UInt32, 63 | bottomRight br: UInt32 64 | ) { 65 | ncurses.wborder(self.window, ls, rs, ts, bs, tl, tr, bl, br) 66 | } 67 | 68 | @inlinable 69 | public func clearBorder() { 70 | self.border( 71 | left: " ", 72 | right: " ", 73 | top: " ", 74 | bottom: " ", 75 | topLeft: " ", 76 | topRight: " ", 77 | bottomLeft: " ", 78 | bottomRight: " " 79 | ) 80 | } 81 | 82 | //======= 83 | // Lines 84 | //======= 85 | 86 | @inlinable 87 | public func horLine(_ ch: Character = "\u{0}", _ n: Int32) { 88 | self.horLine(UInt32(ch.asciiValue!), n) 89 | } 90 | 91 | public func horLine(_ ch: UInt32, _ n: Int32) { 92 | ncurses.whline(self.window, ch, n) 93 | } 94 | 95 | @inlinable 96 | public func horLine(row y: Int32, col x: Int32, _ ch: Character = "\u{0}", _ n: Int32) throws { 97 | try self.horLine(row: y, col: x, UInt32(ch.asciiValue!), n) 98 | } 99 | 100 | @inlinable 101 | public func horLine(row y: Int32, col x: Int32, _ ch: UInt32, _ n: Int32) throws { 102 | try self.move(row: y, col: x) 103 | self.horLine(ch, n) 104 | } 105 | 106 | @inlinable 107 | public func vertLine(_ ch: Character = "\u{0}", _ n: Int32) { 108 | self.vertLine(UInt32(ch.asciiValue!), n) 109 | } 110 | 111 | public func vertLine(_ ch: UInt32, _ n: Int32) { 112 | ncurses.wvline(self.window, ch, n) 113 | } 114 | 115 | @inlinable 116 | public func vertLine(row y: Int32, col x: Int32, _ ch: Character = "\u{0}", _ n: Int32) throws { 117 | try self.vertLine(row: y, col: x, UInt32(ch.asciiValue!), n) 118 | } 119 | 120 | @inlinable 121 | public func vertLine(row y: Int32, col x: Int32, _ ch: UInt32, _ n: Int32) throws { 122 | try self.move(row: y, col: x) 123 | self.vertLine(ch, n) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+clear.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | /// Clear until option for `WindowProtocol.clear(until: ClearUntil)` method 5 | public enum ClearUntil: Sendable, Hashable { 6 | /// erase the current line to the right of the cursor, inclusive, to the end 7 | /// of the current line. 8 | case endOfLine 9 | /// erase from the cursor to the end of screen. That is, they erase all lines 10 | /// below the cursor in the window. Also, the current line to the right of the 11 | /// cursor, inclusive, is erased. 12 | case endOfScreen 13 | } 14 | 15 | extension WindowProtocol { 16 | /// copy blanks to every position in the window, clearing the screen 17 | @inlinable 18 | public func erase() { 19 | ncurses.werase(self.window) 20 | } 21 | 22 | /// like `erase`, but also calls `clearok`, so that the screen is cleared 23 | /// completely on the next call to `refresh` for the window and repainted 24 | /// from scratch 25 | @inlinable 26 | public func clear() { 27 | ncurses.wclear(self.window) 28 | } 29 | 30 | /// Erases the screen from the position of the current cursor, to `until` 31 | /// 32 | /// when `unitl` is `.endOfLine`, this function can throw if the cursor 33 | /// is about to wrap. 34 | @inlinable 35 | public func clear(until: ClearUntil) throws { 36 | switch until { 37 | case .endOfLine: 38 | if ncurses.wclrtoeol(self.window) == ERR { 39 | throw CursesError(.cursorAboutToWrap) 40 | } 41 | case .endOfScreen: 42 | ncurses.wclrtobot(self.window) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+controls.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | public enum Direction: Sendable, Hashable { 5 | case up 6 | case down 7 | case left 8 | case right 9 | } 10 | 11 | extension WindowProtocol { 12 | /// move the cursor to `row`th row and `col`th column 13 | @inlinable 14 | public func move(row: Int32, col: Int32) throws { 15 | if ncurses.wmove(self.window, row, col) == ERR { 16 | throw CursesError(.moveOutsideOfWindow) 17 | } 18 | } 19 | 20 | /// Move the cursor `x` step(s) into the specified direction 21 | @inlinable 22 | public func move(_ direction: Direction, times steps: Int32 = 1) throws { 23 | let (y, x) = self.yx.tuple 24 | switch direction { 25 | case .up: try self.move(row: y - steps, col: x) 26 | case .down: try self.move(row: y + steps, col: x) 27 | case .right: try self.move(row: y, col: x + steps) 28 | case .left: try self.move(row: y, col: x - steps) 29 | } 30 | } 31 | 32 | /// Move the cursor to the specified coordinate 33 | @inlinable 34 | public func move(to position: Coordinate) throws { 35 | try self.move(row: position.y, col: position.x) 36 | } 37 | 38 | // TODO: in some implementations, refresh might be a macro 39 | /// Refreshes the screen, drawing anything that hasn't been drawn yet 40 | @inlinable 41 | public func refresh() { 42 | // X/Open does not define any error conditions. 43 | ncurses.wrefresh(self.window) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+dump.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | #if canImport(Foundation) 5 | import Foundation 6 | #endif 7 | 8 | extension WindowProtocol { 9 | #if canImport(Foundation) 10 | 11 | @inlinable 12 | public func get(file: URL) throws -> WinType { 13 | guard let filePtr = file.withUnsafeFileSystemRepresentation( { fopen($0, "w") } ) else { 14 | throw CursesError(.couldNotOpenFile) 15 | } 16 | return WinType(ncurses.getwin(filePtr)) 17 | } 18 | 19 | @inlinable 20 | public func put(file: URL) throws { 21 | guard let filePtr = file.withUnsafeFileSystemRepresentation( { fopen($0, "w") } ) else { 22 | throw CursesError(.couldNotOpenFile) 23 | } 24 | if ncurses.putwin(self.window, filePtr) == ERR { 25 | throw CursesError(.couldNotOpenFile) 26 | } 27 | } 28 | 29 | #else 30 | 31 | @inlinable 32 | public func get(file: UnsafePointer) -> OpaquePointer { 33 | ncurses.getwin(file) 34 | } 35 | 36 | @inlinable 37 | public func put(file: UnsafePoiter) -> Int32 { 38 | ncurses.putwin(self.window, file) 39 | } 40 | 41 | #endif 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+input.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | #if canImport(Foundation) 5 | import Foundation 6 | #endif 7 | 8 | // Detect system encoding -> used for wide characters 9 | let string32Encoding: String.Encoding = (1.littleEndian == 1) ? .utf32LittleEndian : .utf32BigEndian 10 | 11 | public enum WideChar: Sendable, Hashable { 12 | /// A UTF-8 characer 13 | case char(Character) 14 | /// A KeyCode 15 | case code(Int32) 16 | } 17 | 18 | func wcharToCharacter(_ wc: wchar_t) throws -> Character { 19 | #if canImport(Foundation) 20 | return withUnsafePointer(to: wc) { ptr in 21 | let data = Data(bytes: ptr, count: MemoryLayout.stride) 22 | return String(data: data, encoding: string32Encoding)!.first.unsafelyUnwrapped // should always return a valid character (todo check) 23 | } 24 | #else 25 | guard let bytes = wcharToBytes(wc) else { 26 | throw CursesError(.error) 27 | } 28 | 29 | return String(cString: bytes).first.unsafelyUnwrapped 30 | #endif 31 | } 32 | 33 | extension WindowProtocol { 34 | //======= 35 | // getch 36 | //======= 37 | 38 | /// Get a UTF-8 character from the user. 39 | /// 40 | /// - Returns: 41 | /// - a `WideChar`: this is either a key code or a UTF-8 encoded character 42 | @discardableResult 43 | public func getChar() throws -> WideChar { 44 | var c: wchar_t = wchar_t.init() 45 | return try withUnsafeMutablePointer(to: &c) { (ptr: UnsafeMutablePointer) in 46 | let returnCode = swift_wget_wch(self.window, ptr) 47 | switch (returnCode) { 48 | case ERR: 49 | throw CursesError(.getCharError) 50 | case 256: // function keys 51 | return .code(ptr.pointee) 52 | default: // OK > character 53 | return .char(try wcharToCharacter(ptr.pointee)) 54 | } 55 | } 56 | } 57 | 58 | /// Get the ASCII value of the character or keypress (when using keypad mode) 59 | @discardableResult 60 | public func getCharCode() throws -> Int32 { 61 | let c = ncurses.wgetch(self.window) 62 | if c != OK { 63 | if c == ERR { 64 | throw CursesError(.timeoutWithoutData) 65 | } else if c == EINTR { 66 | throw CursesError(.interrupted) 67 | } 68 | } 69 | return c 70 | } 71 | 72 | //======= 73 | // scanw 74 | //======= 75 | 76 | // TODO: fix 77 | @available(*, unavailable) 78 | public func scan(_ format: String, _ args: UnsafeMutableRawPointer...) throws { 79 | if (withVaList(args.map { OpaquePointer($0) as CVarArg }) { valist in 80 | let cString = format.cString(using: .utf8)! 81 | return withUnsafePointer(to: cString[0]) { formatPtr in 82 | let mutFormatPtr = UnsafeMutablePointer(mutating: formatPtr) 83 | return ncurses.vwscanw(self.window, mutFormatPtr, valist) 84 | } 85 | }) == ERR { 86 | throw CursesError(.error) 87 | } 88 | } 89 | 90 | //======== 91 | // getstr 92 | //======== 93 | 94 | /// Read until a new line or carriage return is encountered. 95 | /// the terminating character is not included in the returned string. 96 | /// The pointer returned should be deallocated manually. 97 | @discardableResult 98 | public func getStrPtr() throws -> UnsafePointer { 99 | let c: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Int(self.maxYX.x) + 1) 100 | let errno = ncurses.wgetstr(self.window, c) 101 | if errno != OK { 102 | if errno == ERR { 103 | throw CursesError(.timeoutWithoutData) 104 | } else if errno == KEY_RESIZE { 105 | throw CursesError(.SIGWINCH) 106 | } 107 | } 108 | return UnsafePointer(c) 109 | } 110 | 111 | @discardableResult 112 | public func getStr() throws -> String { 113 | let ptr: UnsafePointer = try self.getStrPtr() 114 | let s = String(cString: ptr) 115 | ptr.deallocate() 116 | return s 117 | } 118 | 119 | @discardableResult 120 | public func getStr(terminator: Character) throws -> String { 121 | var c = try self.getChar() 122 | var s = String() 123 | loop: while (true) { 124 | switch c { 125 | case .code(_): continue loop 126 | case .char(let char): 127 | if char == terminator { 128 | break loop 129 | } 130 | s.append(char) 131 | } 132 | c = try self.getChar() 133 | } 134 | return s 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+mouse.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | public enum TrafoKind: Sendable, Hashable { 5 | /// x and y are window-relative coordinates and will be converted to 6 | /// stdscr-relative coordinates 7 | case toStdScrn 8 | case toWindow 9 | 10 | @usableFromInline 11 | var rawValue: Bool { 12 | switch self { 13 | case .toStdScrn: return false 14 | case .toWindow: return true 15 | } 16 | } 17 | } 18 | 19 | extension WindowProtocol { 20 | /// The trafo function transforms a given pir of coordinates from stdscr-relative 21 | /// coordinates to coordinates relative to the given window or vice versa. The 22 | /// The resulting stdscr-relative coordinates are not always identical to window-relative coordinates due to the mechanism to 23 | /// reserve lines on top or bottom of the screen for other purposes (see the ripoffline and slk_init(3x) calls, for example). 24 | /// 25 | /// - Returns: 26 | /// - false: 27 | /// - when kind `.toStdScrn`; if the location is not inside the window 28 | /// - when kind `.toWindow`; always returns true 29 | @inlinable 30 | public func mouseTrafo(y: inout Int32, x: inout Int32, kind: TrafoKind) -> Bool { 31 | return ncurses.wmouse_trafo(self.window, &y, &x, kind.rawValue) 32 | } 33 | 34 | /// The trafo function transforms a given pir of coordinates from stdscr-relative 35 | /// coordinates to coordinates relative to the given window or vice versa. The 36 | /// The resulting stdscr-relative coordinates are not always identical to window-relative coordinates due to the mechanism to 37 | /// reserve lines on top or bottom of the screen for other purposes (see the ripoffline and slk_init(3x) calls, for example). 38 | /// 39 | /// - Returns: 40 | /// - false: 41 | /// - when kind `.toStdScrn`; if the location is not inside the window 42 | /// - when kind `.toWindow`; always returns true 43 | public func mouseTrafo(y: Int32, x: Int32, kind: TrafoKind) -> (Bool, y: Int32, x: Int32) { 44 | var (lx, ly) = (x, y) 45 | let b = ncurses.wmouse_trafo(self.window, &ly, &lx, kind.rawValue) 46 | return (b, ly, lx) 47 | } 48 | 49 | /// `wenclose` 50 | public func encloses(y: Int32, x: Int32) -> Bool { 51 | ncurses.wenclose(self.window, y, x) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+output.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | extension WindowProtocol { 5 | //======== 6 | // printw 7 | //======== 8 | 9 | @inlinable 10 | internal func printArgs(_ items: [Any], _ separator: String) throws { 11 | try self.addStr(items.map { String(describing: $0) }.joined(separator: separator)) 12 | } 13 | 14 | public func print(_ items: Any..., separator: String = " ") throws { 15 | try self.printArgs(items, separator) 16 | } 17 | 18 | @inlinable 19 | public func print(row: Int32, col: Int32, _ items: Any..., separator: String = " ") throws { 20 | try self.move(row: row, col: col) 21 | try self.printArgs(items, separator) 22 | } 23 | 24 | @inlinable 25 | public func print(at pos: Coordinate, _ items: Any..., separator: String = " ") throws { 26 | try self.move(row: pos.y, col: pos.x) 27 | try self.printArgs(items, separator) 28 | } 29 | 30 | //======== 31 | // addstr 32 | //======== 33 | 34 | @inlinable 35 | public func addStr(_ str: String) throws { 36 | if ncurses.waddstr(self.window, str) == ERR { 37 | throw CursesError(.error, help: "https://invisible-island.net/ncurses/man/curs_addstr.3x.html#h2-RETURN-VALUE") 38 | } 39 | } 40 | 41 | @inlinable 42 | public func addStr(row: Int32, col: Int32, _ str: String) throws { 43 | try self.move(row: row, col: col) 44 | try self.addStr(str) 45 | } 46 | 47 | @inlinable 48 | public func addStr(at pos: Coordinate, _ str: String) throws { 49 | try self.addStr(row: pos.y, col: pos.x, str) 50 | } 51 | 52 | //====== 53 | // adch 54 | //====== 55 | 56 | public func addChar(_ ch: Character) throws { 57 | let u32 = ch.unicodeScalars.map { wchar_t(bitPattern: $0.value) } + [0] // null-terminated UTF-8 character 58 | if swift_waddwstr( 59 | self.window, 60 | u32.withUnsafeBufferPointer { $0.baseAddress! } 61 | ) == ERR { 62 | if !ncurses.is_scrollok(self.window) { 63 | throw CursesError(.unableToAddCompleteCharToScreen, help: "This may be due to adding a character to the bottom right corner: https://invisible-island.net/ncurses/man/curs_add_wch.3x.html#h2-RETURN-VALUE") 64 | } else { 65 | throw CursesError(.unableToAddCompleteCharToScreen, help: "https://invisible-island.net/ncurses/man/curs_add_wch.3x.html#h2-RETURN-VALUE") 66 | } 67 | } 68 | } 69 | 70 | /// - `mvaddch` 71 | @inlinable 72 | public func addChar(row: Int32, col: Int32, _ ch: Character) throws { 73 | try self.move(row: row, col: col) 74 | try self.addChar(ch) 75 | } 76 | 77 | @inlinable 78 | public func addChar(at pos: Coordinate, _ ch: Character) throws { 79 | try self.addChar(row: pos.y, col: pos.x, ch) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window+util.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | extension swift_YX { 5 | @inlinable public var row: Int32 { self.y } 6 | @inlinable public var col: Int32 { self.x } 7 | /// Convert the `Coordinate` struct to a tuple of the form `(y, x)` 8 | @inlinable public var tuple: (Int32, Int32) { (self.y, self.x) } 9 | } 10 | 11 | extension C_ncursesBinds.swift_YX: Swift.Equatable, Swift.Hashable { 12 | public static func ==(lhs: Self, rhs: Self) -> Bool { 13 | lhs.x == rhs.x && lhs.y == rhs.y 14 | } 15 | 16 | public func hash(into hasher: inout Hasher) { 17 | hasher.combine(self.x) 18 | hasher.combine(self.y) 19 | } 20 | } 21 | 22 | extension C_ncursesBinds.swift_YX: Swift.CustomStringConvertible { 23 | public var description: String { 24 | "(row: \(self.row), col: \(self.col))" 25 | } 26 | } 27 | 28 | public typealias Coordinate = swift_YX 29 | 30 | extension WindowProtocol { 31 | /// Beginning coordinates 32 | public var begYX: Coordinate { 33 | return swift_getbegyx(self.window) 34 | } 35 | 36 | /// Size of the window 37 | public var maxYX: Coordinate { 38 | return swift_getmaxyx(self.window) 39 | } 40 | 41 | /// Current cursor position 42 | public var yx: Coordinate { 43 | return swift_getyx(self.window) 44 | } 45 | 46 | /// Get the position of the cursor in the parent window 47 | /// If the window is not a subwindow, nil is returned 48 | public var parentYX: Coordinate? { 49 | let yx: swift_YX = swift_getparyx(self.window) 50 | return yx.y == -1 ? nil : yx 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/Window.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | 3 | public protocol WindowProtocol { 4 | var window: WindowPointer { get } 5 | } 6 | 7 | public struct Window: WindowProtocol { 8 | public let window: WindowPointer 9 | 10 | init(_ window: WindowPointer) { 11 | // screen is not a null pointer => we can negate some error checks in our functions 12 | self.window = window 13 | } 14 | } 15 | 16 | open class ManagedWindow: WindowProtocol { 17 | public let window: WindowPointer 18 | private var __rows: Int32? = nil 19 | private var __cols: Int32? = nil 20 | private var destroyed = false 21 | 22 | /// will not be known when loading a window from a file 23 | public var rows: Int32? { self.__rows } 24 | /// will not be known when loading a window from a file 25 | public var cols: Int32? { self.__cols } 26 | 27 | public required init(_ window: WindowPointer, rows: Int32? = nil, cols: Int32? = nil) { 28 | self.__rows = rows 29 | self.__cols = cols 30 | self.window = window 31 | self.onInit() 32 | } 33 | 34 | public convenience init(rows: Int32, cols: Int32, begin: (Int32, Int32), settings: [WindowSetting] = WindowSetting.defaultSettings) throws { 35 | let winPtr: WindowPointer = try newWindow(rows: rows, cols: cols, begin: begin, settings: settings) 36 | self.init(winPtr, rows: rows, cols: cols) 37 | } 38 | 39 | open func onInit() {} 40 | open func onDeinit() {} 41 | 42 | private func destroy() { 43 | onDeinit() 44 | if rows != nil { 45 | for r in 0.. WindowPointer { 84 | guard let win = ncurses.newwin(rows, cols, begin.0, begin.1) else { 85 | if begin.0 < 0 || begin.1 < 1 { 86 | throw CursesError(.negativeCoordinate, help: "The beginning coordinates of the window cannot be negative") 87 | } else if rows < 0 { 88 | throw CursesError(.negativeNumber, help: "The lines cannot be negative") 89 | } else if cols < 0 { 90 | throw CursesError(.negativeNumber, help: "The cols cannot be negative") 91 | } else { 92 | throw CursesError(.cannotCreateWindow) 93 | } 94 | } 95 | 96 | settings.forEach { $0.apply(win) } 97 | 98 | return win 99 | } 100 | 101 | /// Create a new window 102 | @inlinable 103 | public func newWindow(rows: Int32, cols: Int32, begin: Coordinate, settings: [WindowSetting] = WindowSetting.defaultSettings, _ body: (inout Window) -> ()) throws { 104 | try newWindow(rows: rows, cols: cols, begin: begin.tuple, settings: settings, body) 105 | } 106 | 107 | /// Create a new window 108 | public func newWindow(rows: Int32, cols: Int32, begin: (Int32, Int32), settings: [WindowSetting] = WindowSetting.defaultSettings, _ body: (inout Window) -> ()) throws { 109 | let win: WindowPointer = try newWindow(rows: rows, cols: cols, begin: begin, settings: settings) 110 | var window = Window(win) 111 | body(&window) 112 | delwin(win) 113 | } 114 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/color.swift: -------------------------------------------------------------------------------- 1 | @preconcurrency import ncurses // NOTE: preconcurrency --> we are importing constants from C, but they are imported as vars, they are concurrency safe, but the swift compiler has no way of knowing this 2 | 3 | public typealias ColorPairId = Int16 4 | public typealias ColorId = Int16 5 | public typealias ColorRGB = Int16 6 | 7 | // TODO: conform to OptionSet 8 | public struct Color: Sendable, Hashable { 9 | /// Whether the terminal can manipulate colors. 10 | /// 11 | /// This routine facilitates writing terminal-independent programs. 12 | /// For example, a programmer can use it to decide whether to use color or 13 | /// some other video attribute. 14 | public static let hasColors: Bool = has_colors() 15 | 16 | public static let black = ColorId(COLOR_BLACK) 17 | public static let red = ColorId(COLOR_RED) 18 | public static let green = ColorId(COLOR_GREEN) 19 | public static let yellow = ColorId(COLOR_YELLOW) 20 | public static let blue = ColorId(COLOR_BLUE) 21 | public static let magenta = ColorId(COLOR_MAGENTA) 22 | public static let cyan = ColorId(COLOR_CYAN) 23 | public static let white = ColorId(COLOR_WHITE) 24 | 25 | /// Whether the terminal supports color and can change their definition. 26 | /// 27 | /// This routine facilitates writing terminal-independent programs. 28 | public static let canChangeColor: Bool = can_change_color() 29 | 30 | public static let maxColors = COLORS 31 | 32 | public static func forceXTerm256Color() { 33 | setenv("TERM", "xterm-256color", 1) 34 | } 35 | 36 | public static func define(_ id: ColorId, r: ColorRGB, g: ColorRGB, b: ColorRGB) throws { 37 | if ncurses.init_color(id, r, g, b) == ERR { 38 | if !(0...1000).contains(r) { 39 | throw CursesError( 40 | .rgbValueOutOfRange, 41 | help: "red value out of range" 42 | ) 43 | } else if !(0...1000).contains(g) { 44 | throw CursesError( 45 | .rgbValueOutOfRange, 46 | help: "green value out of range" 47 | ) 48 | } else if !(0...1000).contains(b) { 49 | throw CursesError( 50 | .rgbValueOutOfRange, 51 | help: "blue value out of range" 52 | ) 53 | } else { 54 | throw CursesError( 55 | .colorUnsupported 56 | ) 57 | } 58 | } 59 | } 60 | 61 | public struct Content { 62 | public var r, g, b: ColorRGB 63 | init() { 64 | self.r = -1 65 | self.g = -1 66 | self.b = -1 67 | } 68 | } 69 | 70 | public static func content(for id: ColorId) throws -> Color.Content { 71 | var content = Content() 72 | ncurses.color_content(id, &content.r, &content.g, &content.b) 73 | if [content.r, content.g, content.b].contains(-1) { 74 | throw CursesError(.colorChangeUnsupported) 75 | } 76 | return content 77 | } 78 | } 79 | 80 | public struct ColorPair { 81 | public static let maxPairs = COLOR_PAIRS 82 | 83 | /// changes the definition of a color-pair 84 | /// 85 | /// equal to the `init_pair` subroutine 86 | public static func define(_ id: ColorPairId, fg: ColorId, bg: ColorId) throws { 87 | if ncurses.init_pair(id, fg, bg) == ERR { 88 | if !(0.. ColorPair.Content { 118 | var content = Content() 119 | ncurses.pair_content(id, &content.fg, &content.bg) 120 | if [content.fg, content.bg].contains(-1) { 121 | throw CursesError(.uninitializedColorPair) 122 | } 123 | return content 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/error.swift: -------------------------------------------------------------------------------- 1 | public enum ErrorKind: Sendable, Hashable { 2 | case halfdelayParameterOutsideOfRange 3 | /// Window pointer is NULL 4 | case cannotCreateWindow 5 | case moveOutsideOfWindow 6 | case getCharError 7 | case timeoutWithoutData 8 | case SIGWINCH 9 | case interrupted 10 | case negativeCoordinate 11 | case negativeNumber 12 | case colorTableCannotBeAllocated 13 | case colorPairOutsideOfRange 14 | case colorOutsideOfRange 15 | case rgbValueOutOfRange 16 | case colorUnsupported 17 | case colorChangeUnsupported 18 | case uninitializedColorPair 19 | case cursorAboutToWrap 20 | case mouseEventRegisterError 21 | case queueFull 22 | case couldNotOpenFile 23 | /// Terminal does not support moue as indicated by `MouseEvent.hasMouse` 24 | case noMouseSupport 25 | 26 | // add_wch 27 | case unableToAddCompleteCharToScreen 28 | 29 | /// Generic error 30 | case error 31 | 32 | public var help: String? { 33 | switch self { 34 | case .cannotCreateWindow: return "The window returned was a null pointer" 35 | case .getCharError: return "Timeout expired without having any data, or the eecution was interrupted by a signal (errno will be set to EINTR)" 36 | case .timeoutWithoutData: return "Timeout expired without having any data" 37 | case .SIGWINCH: return "SIGWINCH interrupt" 38 | case .interrupted: return "The execution was interrupted by a signal" 39 | case .colorUnsupported: return "Color.define: the terminal does not support this feature, e.g., the initialize_color capability is absent from the terminal description." 40 | case .colorChangeUnsupported: return "The terminal does not support changing color" 41 | case .uninitializedColorPair: return "The pair was not initialized using `init_pairs`" 42 | default: return nil 43 | } 44 | } 45 | } 46 | 47 | public struct CursesError: Error & CustomDebugStringConvertible, Sendable, Hashable { 48 | public let kind: ErrorKind 49 | public let help: String? 50 | 51 | @usableFromInline 52 | init(_ kind: ErrorKind, help: String? = nil) { 53 | self.kind = kind 54 | self.help = help ?? kind.help 55 | } 56 | 57 | public var debugDescription: String { 58 | "Error: \(self.kind)\n\(self.help == nil ? "" : "Help: \(self.help!)")" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/initScreen.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | #if os(Linux) 3 | import ncursesw 4 | #endif 5 | 6 | #if canImport(Darwin) 7 | import Darwin 8 | #elseif canImport(Glibc) 9 | import Glibc 10 | #elseif canImport(Musl) 11 | import Musl 12 | #endif 13 | 14 | /// Start a new curses mode terminal 15 | /// 16 | /// - Parameters: 17 | /// - `settings`: NCurses settings 18 | /// - `body`: The paramater passed to the `body` function can be used to call ncures functions on the screen that was created 19 | public func initScreen( 20 | settings: [TermSetting] = TermSetting.defaultSettings, 21 | windowSettings: [WindowSetting] = WindowSetting.defaultSettings, 22 | _ body: (inout Window) throws -> () 23 | ) throws { 24 | setlocale(LC_ALL, "") // support for wide chars 25 | 26 | guard let _scr = initscr() else { // start curses mode 27 | throw CursesError(.cannotCreateWindow) 28 | } 29 | // Assure cleanup on interrupt 30 | #if !os(Windows) 31 | signal(SIGINT, { _ in 32 | endwin() 33 | exit(0) 34 | }) 35 | #endif 36 | defer { 37 | endwin() // end curses mode 38 | } 39 | 40 | try settings.forEach { try $0.apply() } 41 | windowSettings.forEach { $0.apply(_scr) } 42 | 43 | var scr = Window(_scr) 44 | try body(&scr) 45 | } 46 | 47 | #if !os(Windows) 48 | fileprivate nonisolated(unsafe) var originalTermios = termios() 49 | #endif 50 | 51 | /// Start a new curses mode terminal 52 | /// 53 | /// This version of the `initScreen` function supports an asynchronous body function 54 | /// 55 | /// - Parameters: 56 | /// - `settings`: NCurses settings 57 | /// - `body`: The paramater passed to the `body` function can be used to call ncures functions on the screen that was created 58 | @available(macOS 10.15.0, *) 59 | public func initScreenAsync( 60 | settings: [TermSetting] = TermSetting.defaultSettings, 61 | windowSettings: [WindowSetting] = WindowSetting.defaultSettings, 62 | _ body: @Sendable (inout Window) async throws -> () 63 | ) async throws { 64 | setlocale(LC_ALL, "") // support fr wide chars 65 | 66 | var scr: Window? = nil 67 | #if !os(Windows) 68 | if (tcgetattr(STDIN_FILENO, &originalTermios) == -1) { 69 | // err 70 | } 71 | #endif 72 | guard let _scr = initscr() else { // start curses mode 73 | throw CursesError(.cannotCreateWindow) 74 | } 75 | // Assure cleanup on interrupt 76 | #if !os(Windows) 77 | signal(SIGINT, { _ in 78 | endwin() 79 | if (tcsetattr(STDIN_FILENO, 0, &originalTermios) == -1) { 80 | //print("error restoring termios") 81 | } 82 | exit(0) 83 | }) 84 | #endif 85 | defer { 86 | endwin() // end curses mode 87 | } 88 | 89 | try settings.forEach { try $0.apply() } 90 | windowSettings.forEach { $0.apply(_scr) } 91 | 92 | scr = Window(_scr) 93 | try await body(&scr!) 94 | } 95 | 96 | // https://invisible-island.net/ncurses/man/curs_scr_dump.3x.html // 97 | 98 | @inlinable 99 | public func scrDump(filename: String) { 100 | ncurses.scr_dump(filename) 101 | } 102 | 103 | @inlinable 104 | public func scrRestore(filename: String) { 105 | ncurses.scr_restore(filename) 106 | } 107 | 108 | @inlinable 109 | public func scrInit(filename: String) { 110 | ncurses.scr_init(filename) 111 | } 112 | 113 | @inlinable 114 | public func scrSet(filename: String) { 115 | ncurses.scr_set(filename) 116 | } 117 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/kernel.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | 3 | public enum CursorVisibility: Int32, Sendable, Hashable { 4 | case invisible = 0 5 | case normal = 1 6 | case veryVisible = 2 7 | } 8 | 9 | @inlinable 10 | public func cursorSet(_ visibility: CursorVisibility) { 11 | ncurses.curs_set(visibility.rawValue) // no error, because controlled visibility values 12 | } 13 | 14 | /// Save the program, exit it to return to shell mode. Afterwards, restore the program 15 | public func shellMode(_ body: () throws -> ()) rethrows { 16 | ncurses.def_prog_mode() 17 | ncurses.endwin() 18 | // cooked mode 19 | try body() 20 | ncurses.reset_prog_mode() 21 | ncurses.refresh() 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/keycode.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | public struct KeyCode: Sendable, Hashable { 5 | /// The type of the KeyCode values 6 | public typealias type = Int32 7 | 8 | public static let `break` = KEY_BREAK // Break key 9 | public static let down = KEY_DOWN // Arrow down 10 | public static let up = KEY_UP // Arrow up 11 | public static let left = KEY_LEFT // Arrow left 12 | public static let right = KEY_RIGHT // Arrow right 13 | public static let home = KEY_HOME // Home key 14 | public static let backspace = KEY_BACKSPACE // Backspace 15 | public static let f0 = KEY_F0 // Function key zero 16 | public static let dl = KEY_DL // Delete line 17 | public static let il = KEY_IL // Insert line 18 | public static let dc = KEY_DC // Delete character 19 | public static let ic = KEY_IC // Insert char or enter insert mode 20 | public static let eic = KEY_EIC // Exit insert char mode 21 | public static let clear = KEY_CLEAR // Clear screen 22 | public static let eos = KEY_EOS // Clear to end of screen 23 | public static let eol = KEY_EOL // Clear to end of line 24 | public static let sf = KEY_SF // Scroll 1 line forward 25 | public static let sr = KEY_SR // Scroll 1 line backward (reverse) 26 | public static let npage = KEY_NPAGE // Next page 27 | public static let ppage = KEY_PPAGE // Previous page 28 | public static let stab = KEY_STAB // Set tab 29 | public static let ctab = KEY_CTAB // Clear tab 30 | public static let catab = KEY_CATAB // Clear all tabs 31 | public static let enter = KEY_ENTER // Enter or send 32 | public static let sreset = KEY_SRESET // Soft (partial) reset 33 | public static let reset = KEY_RESET // Reset or hard reset 34 | public static let print = KEY_PRINT // Print or copy 35 | public static let ll = KEY_LL // Home down or bottom (lower left) 36 | public static let a1 = KEY_A1 // Upper left of keypad 37 | public static let a3 = KEY_A3 // Upper right of keypad 38 | public static let b2 = KEY_B2 // Center of keypad 39 | public static let c1 = KEY_C1 // Lower left of keypad 40 | public static let c3 = KEY_C3 // Lower right of keypad 41 | public static let btab = KEY_BTAB // Back tab key 42 | public static let beg = KEY_BEG // Beg(inning) key 43 | public static let cancel = KEY_CANCEL // Cancel key 44 | public static let close = KEY_CLOSE // Close key 45 | public static let command = KEY_COMMAND // Cmd (command) key 46 | public static let copy = KEY_COPY // Copy key 47 | public static let create = KEY_CREATE // Create key 48 | public static let end = KEY_END // End key 49 | public static let exit = KEY_EXIT // Exit key 50 | public static let find = KEY_FIND // Find key 51 | public static let help = KEY_HELP // Help key 52 | public static let mark = KEY_MARK // Mark key 53 | public static let message = KEY_MESSAGE // Message key 54 | 55 | // Table 5.3: the keypad constants, part 1 56 | public static let mouse = KEY_MOUSE // Mouse event read 57 | public static let move = KEY_MOVE // Move key 58 | public static let next = KEY_NEXT // Next object key 59 | public static let open = KEY_OPEN // Open key 60 | public static let options = KEY_OPTIONS // Options key 61 | public static let previous = KEY_PREVIOUS // Previous object key 62 | public static let redo = KEY_REDO // Redo key 63 | public static let reference = KEY_REFERENCE // Ref(erence) key 64 | public static let refresh = KEY_REFRESH // Refresh key 65 | public static let replace = KEY_REPLACE // Replace key 66 | public static let resize = KEY_RESIZE // Screen resized 67 | public static let restart = KEY_RESTART // Restart key 68 | public static let resume = KEY_RESUME // Resume key 69 | public static let save = KEY_SAVE // Save key 70 | public static let sbeg = KEY_SBEG // Shifted beginning key 71 | public static let scancel = KEY_SCANCEL // Shifted cancel key 72 | public static let scommand = KEY_SCOMMAND // Shifted command key 73 | public static let scopy = KEY_SCOPY // Shifted copy key 74 | public static let screate = KEY_SCREATE // Shifted create key 75 | public static let sdc = KEY_SDC // Shifted delete char key 76 | public static let sdl = KEY_SDL // Shifted delete line key 77 | public static let select = KEY_SELECT // Select key 78 | public static let send = KEY_SEND // Shifted end key 79 | public static let seol = KEY_SEOL // Shifted clear line key 80 | public static let sexit = KEY_SEXIT // Shifted exit key 81 | public static let sfind = KEY_SFIND // Shifted find key 82 | public static let shelp = KEY_SHELP // Shifted help key 83 | public static let shome = KEY_SHOME // Shifted home key 84 | public static let sic = KEY_SIC // Shifted input key 85 | public static let sleft = KEY_SLEFT // Shifted left arrow key 86 | public static let smessage = KEY_SMESSAGE // Shifted message key 87 | public static let smove = KEY_SMOVE // Shifted move key 88 | public static let snext = KEY_SNEXT // Shifted next key 89 | public static let soptions = KEY_SOPTIONS // Shifted options key 90 | public static let sprevious = KEY_SPREVIOUS // Shifted prev key 91 | public static let sprint = KEY_SPRINT // Shifted print key 92 | public static let sredo = KEY_SREDO // Shifted redo key 93 | public static let sreplace = KEY_SREPLACE // Shifted replace key 94 | public static let sright = KEY_SRIGHT // Shifted right arrow 95 | // public static let sresume = KEY_SRESUME // Shifted resume key 96 | public static let ssave = KEY_SSAVE // Shifted save key 97 | public static let ssuspend = KEY_SSUSPEND // Shifted suspend key 98 | public static let sundo = KEY_SUNDO // Shifted undo key 99 | public static let suspend = KEY_SUSPEND // Suspend key 100 | public static let undo = KEY_UNDO // Undo key 101 | 102 | /// Fn keys 103 | public static func f(_ n: Int32) -> Int32 { 104 | return swift_key_f(n) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/mouseEvent.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | import C_ncursesBinds 3 | 4 | public enum MouseEventMask: Sendable, Hashable { 5 | case button1Pressed // mouse button 1 down 6 | case button1Released // mouse button 1 up 7 | case button1Clicked // mouse button 1 clicked 8 | case button1DoubleClicked // mouse button 1 double clicked 9 | case button1TripleClicked // mouse button 1 triple clicked 10 | case button2Pressed // mouse button 2 down 11 | case button2Released // mouse button 2 up 12 | case button2Clicked // mouse button 2 clicked 13 | case button2DoubleClicked // mouse button 2 double clicked 14 | case button2TripleClicked // mouse button 2 triple clicked 15 | case button3Pressed // mouse button 3 down 16 | case button3Released // mouse button 3 up 17 | case button3Clicked // mouse button 3 clicked 18 | case button3DoubleClicked // mouse button 3 double clicked 19 | case button3TripleClicked // mouse button 3 triple clicked 20 | case button4Pressed // mouse button 4 down 21 | case button4Released // mouse button 4 up 22 | case button4Clicked // mouse button 4 clicked 23 | case button4DoubleClicked // mouse button 4 double clicked 24 | case button4TripleClicked // mouse button 4 triple clicked 25 | case buttonShift // shift was down during button state change 26 | case buttonCtrl // control was down during button state change 27 | case buttonAlt // alt was down during button state change 28 | case allMouseEvents // report all button state changes 29 | case reportMousePosition // report mouse movement 30 | 31 | var val: mmask_t { 32 | switch self { 33 | case .button1Pressed: return swift_button1Pressed 34 | case .button1Released: return swift_button1Released 35 | case .button1Clicked: return swift_button1Clicked 36 | case .button1DoubleClicked: return swift_button1DoubleClicked 37 | case .button1TripleClicked: return swift_button1TripleClicked 38 | case .button2Pressed: return swift_button2Pressed 39 | case .button2Released: return swift_button2Released 40 | case .button2Clicked: return swift_button2Clicked 41 | case .button2DoubleClicked: return swift_button2DoubleClicked 42 | case .button2TripleClicked: return swift_button2TripleClicked 43 | case .button3Pressed: return swift_button3Pressed 44 | case .button3Released: return swift_button3Released 45 | case .button3Clicked: return swift_button3Clicked 46 | case .button3DoubleClicked: return swift_button3DoubleClicked 47 | case .button3TripleClicked: return swift_button3TripleClicked 48 | case .button4Pressed: return swift_button4Pressed 49 | case .button4Released: return swift_button4Released 50 | case .button4Clicked: return swift_button4Clicked 51 | case .button4DoubleClicked: return swift_button4DoubleClicked 52 | case .button4TripleClicked: return swift_button4TripleClicked 53 | case .buttonShift: return swift_buttonShift 54 | case .buttonCtrl: return swift_buttonCtrl 55 | case .buttonAlt: return swift_buttonAlt 56 | case .allMouseEvents: return swift_allMouseEvents 57 | case .reportMousePosition: return swift_reportMousePosition 58 | } 59 | } 60 | } 61 | 62 | public typealias MouseEvent = ncurses.MEVENT 63 | 64 | public enum MouseButton: Sendable, Hashable { 65 | case button1 66 | case button2 67 | case button3 68 | case button4 69 | } 70 | 71 | extension MouseButton { 72 | @inlinable 73 | func pressed(_ mask: mmask_t) -> Bool { 74 | switch self { 75 | case .button1: return mask & swift_button1Pressed != 0 76 | case .button2: return mask & swift_button2Pressed != 0 77 | case .button3: return mask & swift_button3Pressed != 0 78 | case .button4: return mask & swift_button4Pressed != 0 79 | } 80 | } 81 | 82 | @inlinable 83 | func released(_ mask: mmask_t) -> Bool { 84 | switch self { 85 | case .button1: return mask & swift_button1Released != 0 86 | case .button2: return mask & swift_button2Released != 0 87 | case .button3: return mask & swift_button3Released != 0 88 | case .button4: return mask & swift_button4Released != 0 89 | } 90 | } 91 | 92 | @inlinable 93 | func clicked(_ mask: mmask_t) -> Bool { 94 | switch self { 95 | case .button1: return mask & swift_button1Clicked != 0 96 | case .button2: return mask & swift_button2Clicked != 0 97 | case .button3: return mask & swift_button3Clicked != 0 98 | case .button4: return mask & swift_button4Clicked != 0 99 | } 100 | } 101 | 102 | @inlinable 103 | func doubleClicked(_ mask: mmask_t) -> Bool { 104 | switch self { 105 | case .button1: return mask & swift_button1DoubleClicked != 0 106 | case .button2: return mask & swift_button2DoubleClicked != 0 107 | case .button3: return mask & swift_button3DoubleClicked != 0 108 | case .button4: return mask & swift_button4DoubleClicked != 0 109 | } 110 | } 111 | 112 | @inlinable 113 | func tripleClicked(_ mask: mmask_t) -> Bool { 114 | switch self { 115 | case .button1: return mask & swift_button1TripleClicked != 0 116 | case .button2: return mask & swift_button2TripleClicked != 0 117 | case .button3: return mask & swift_button3TripleClicked != 0 118 | case .button4: return mask & swift_button4TripleClicked != 0 119 | } 120 | } 121 | } 122 | 123 | public enum ModifierKey: Sendable, Hashable { 124 | case shift 125 | case ctrl 126 | case alt 127 | } 128 | 129 | extension ModifierKey { 130 | @inlinable 131 | func active(_ mask: mmask_t) -> Bool { 132 | switch self { 133 | case .shift: return mask & swift_buttonShift != 0 134 | case .ctrl: return mask & swift_buttonCtrl != 0 135 | case .alt: return mask & swift_buttonAlt != 0 136 | } 137 | } 138 | } 139 | 140 | extension MouseEvent { 141 | /// Register the given mouse events 142 | public static func register(_ events: MouseEventMask...) throws { 143 | if ncurses.mousemask(events.reduce(0) { $0 | $1.val }, nil) == 0 && events.count != 0 { 144 | throw CursesError(.mouseEventRegisterError) // TODO: help 145 | } 146 | } 147 | 148 | // @inlinable 149 | // public static var hasMouse: Bool { 150 | // ncurses.swift_has_mouse() 151 | // } 152 | 153 | @inlinable 154 | public static func interval(_ n: Int32) { 155 | ncurses.mouseinterval(n) 156 | } 157 | 158 | /// Once a class of mouse events has been made visible in a window, calling the 159 | /// GetCharCode function on that window may return `KeyEvent.mouse` as an indicator 160 | /// that a mouse event has been queued. To read the event data and op the event 161 | /// off the queue, call `MouseEvent.get()`. This function will a `MouseEvent` if a 162 | /// mouse event is actually visible in the given window, otherwise it might throw 163 | /// an error. 164 | /// 165 | /// When this function returns an event, the `position.x` and `position.y` in the event 166 | /// struct coordinates will be screen-relative character-cell cooridnates. The 167 | /// event will have exactly one `MouseButton` event, which can be got with the `KeyEvent.pressed` 168 | /// and similar functions. The corresponding data in the queue is marked invalid. A 169 | /// subsequent call to `MouseEvent.get()` will retrieve the next older item from 170 | /// the queue. 171 | public static func get() -> MouseEvent? { 172 | // TODO: figure out if err because no mouse-event, or other reason 173 | // if no mouse-event in queue -> return nil instead 174 | var event = MouseEvent() 175 | if getmouse(&event) != OK { 176 | // if !Self.hasMouse { 177 | // throw CursesError(.noMouseSupport) 178 | // } 179 | return nil 180 | } 181 | return event 182 | } 183 | 184 | public mutating func unget() throws { 185 | if ncurses.ungetmouse(&self) == ERR { 186 | throw CursesError(.queueFull) 187 | } 188 | } 189 | 190 | /// button is down 191 | public func isPressed(_ btn: MouseButton) -> Bool { 192 | btn.pressed(self.bstate) 193 | } 194 | 195 | /// button is up 196 | public func isReleased(_ btn: MouseButton) -> Bool { 197 | btn.released(self.bstate) 198 | } 199 | 200 | /// button is clicked 201 | public func isClicked(_ btn: MouseButton) -> Bool { 202 | btn.clicked(self.bstate) 203 | } 204 | 205 | public func isDoubleClicked(_ btn: MouseButton) -> Bool { 206 | btn.doubleClicked(self.bstate) 207 | } 208 | 209 | public func isTripleClicked(_ btn: MouseButton) -> Bool { 210 | btn.tripleClicked(self.bstate) 211 | } 212 | 213 | /// Whether the given modifier key was down during the button state change 214 | public func modifierActive(_ modifier: ModifierKey) -> Bool { 215 | modifier.active(self.bstate) 216 | } 217 | 218 | /// even coordinates 219 | public var position: (x: Int32, y: Int32, z: Int32) { 220 | (self.x, self.y, self.z) 221 | } 222 | 223 | /// device id 224 | public var device: Int16 { 225 | self.id 226 | } 227 | 228 | 229 | } 230 | -------------------------------------------------------------------------------- /Sources/SwiftCurses/settings.swift: -------------------------------------------------------------------------------- 1 | import ncurses 2 | 3 | // TODO: documentation > https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/init.html 4 | /// https://invisible-island.net/ncurses/man/curs_inopts.3x.html 5 | public enum TermSetting: Sendable, Hashable { 6 | case cbreak 7 | case noCbreak 8 | case echo 9 | case noEcho 10 | case nl 11 | case noNl 12 | /// Raw is similar to `cbreak`, the difference is that in raw mode, the interrupt, 13 | /// quit, suspend and flow control characters are passed through uninterreted instead 14 | /// of generating a signal 15 | case raw 16 | case noRaw 17 | case qiFlush 18 | case noQiFlush 19 | case halfdelay(_ tenths: Int32) 20 | case timeout(_ delay: Int32) 21 | case typeahead(_ fd: Int32) 22 | 23 | /// enable color support (equal to calling `start_color` in ncurses) 24 | case colors 25 | } 26 | 27 | /// https://invisible-island.net/ncurses/man/curs_inopts.3x.html 28 | public enum WindowSetting: Sendable, Hashable { 29 | /// If this option is enabled and an interrupt key is passed on the keyboard (interrupt, break, quit), 30 | /// all output in the tty driver queue will be flushed, giving the effect of faster response to the interrupt, 31 | /// but causing curses to have the wrong idea of what is on the screen. The default setting for this 32 | /// option is inherited from the tty driver settings 33 | case intrflush(Bool) 34 | case keypad(Bool) 35 | case meta(Bool) 36 | case nodelay(Bool) 37 | case notimeout(Bool) 38 | case timeout(_ delay: Int32) 39 | } 40 | 41 | extension TermSetting { 42 | public static let defaultSettings: [TermSetting] = [.cbreak, .noEcho, .colors] 43 | } 44 | 45 | extension WindowSetting { 46 | public static let defaultSettings: [WindowSetting] = [.keypad(true)] 47 | } 48 | 49 | extension TermSetting { 50 | // TODO: error handling of options 51 | @usableFromInline 52 | func apply() throws { 53 | switch self { 54 | case .cbreak: ncurses.cbreak() 55 | case .noCbreak: ncurses.nocbreak() 56 | case .echo: ncurses.echo() 57 | case .noEcho: ncurses.noecho() 58 | case .nl: ncurses.nl() 59 | case .noNl: ncurses.nonl() 60 | case .raw: ncurses.raw() 61 | case .noRaw: ncurses.noraw() 62 | case .qiFlush: ncurses.qiflush() 63 | case .noQiFlush: ncurses.noqiflush() 64 | case .halfdelay(let delay): 65 | if ncurses.halfdelay(delay) == ERR { 66 | throw CursesError(.halfdelayParameterOutsideOfRange) 67 | } 68 | case .timeout(let delay): ncurses.timeout(delay) 69 | case .typeahead(let fd): ncurses.typeahead(fd) 70 | case .colors: 71 | if ncurses.start_color() == ERR { 72 | throw CursesError(.colorTableCannotBeAllocated) 73 | } 74 | } 75 | } 76 | } 77 | 78 | extension WindowSetting { 79 | @usableFromInline 80 | func apply(_ win: WindowPointer) { 81 | switch self { 82 | case .intrflush(let bf): ncurses.intrflush(win, bf) 83 | case .keypad(let bf): ncurses.keypad(win, bf) 84 | case .meta(let bf): ncurses.meta(win, bf) 85 | case .nodelay(let bf): ncurses.nodelay(win, bf) 86 | case .notimeout(let bf): ncurses.notimeout(win, bf) 87 | case .timeout(let delay): ncurses.wtimeout(win, delay) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/ncursesTests/ncursesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ncurses 3 | 4 | final class ncursesTests: XCTestCase { 5 | func testExample() throws { 6 | } 7 | } 8 | --------------------------------------------------------------------------------