CommandLineToolkit

Форк
0
/
ANSITerminal.swift 
166 строк · 4.8 Кб
1
import Darwin
2
import SignalHandling
3

4
extension String {
5
    static let ESC = "\u{1B}"  // Escape character (27 or 1B)
6
    static let SS2 = ESC + "N"   // Single Shift Select of G2 charset
7
    static let SS3 = ESC + "O"   // Single Shift Select of G3 charset
8
    static let DCS = ESC + "P"   // Device Control String
9
    static let CSI = ESC + "["   // Control Sequence Introducer
10
    static let OSC = ESC + "]"   // Operating System Command
11
    static let BACK = ESC + "b"  // Word Back
12
    static let FORV = ESC + "f"  // Word Forward
13
}
14

15
public final class ANSITerminal {
16
    public static let shared: ANSITerminal = .init()
17

18
    private var terminal: termios = termios()
19
    private(set) var isNonBlockingMode = false
20
    private(set) var isNonBlockingExitSetUp = false
21
    var isCursorVisible = true
22

23
    private let stdoutStream: StdioOutputStream = .stdout
24
    private let stderrStream: StdioOutputStream = .stderr
25

26
    private let lock: ReadWriteLock = .init()
27
    private(set) var size: Size = .init(rows: 0, cols: 0)
28

29
    private init() {
30
        size = self.getSize()
31

32
        SignalHandling.addSignalHandler(signal: .user(SIGWINCH)) { _ in
33
            self.lock.withWriterLockVoid {
34
                self.size = self.getSize()
35
            }
36
        }
37
    }
38

39
    /// Check key from input poll
40
    func keyPressed() -> Bool {
41
        if !isNonBlockingMode { enableNonBlockingTerminal() }
42
        var fds = [pollfd(fd: STDIN_FILENO, events: Int16(POLLIN), revents: 0)]
43
        return poll(&fds, 1, 0) > 0
44
    }
45

46
    /// Read key as character
47
    func readChar() -> Character {
48
        var bytes: [UInt8] = []
49

50
        while true {
51
            bytes.append(readByte())
52

53
            guard let str = String(bytes: bytes, encoding: .utf8) else {
54
                continue
55
            }
56

57
            guard let char = str.first else {
58
                continue
59
            }
60

61
            return char
62
        }
63
    }
64

65
    /// Read key as ascii code
66
    func readByte() -> UInt8 {
67
        var key: UInt8 = 0
68
        let res = read(STDIN_FILENO, &key, 1)
69
        return res < 0 ? 0 : key
70
    }
71

72
    /// Request terminal info using ansi esc command and return the response value
73
    func ansiRequest(_ command: String, endChar: Character) -> String {
74
        // store current input mode
75
        let nonBlock = isNonBlockingMode
76
        if !nonBlock { enableNonBlockingTerminal() }
77

78
        // send request
79
        write(command)
80

81
        // read response
82
        var res: String = ""
83
        var key: UInt8  = 0
84
        repeat {
85
            read(STDIN_FILENO, &key, 1)
86
            res.append(Character(UnicodeScalar(key)))
87
        } while key != endChar.asciiValue
88

89
        // restore input mode and return response value
90
        if !nonBlock { disableNonBlockingTerminal() }
91
        return res
92
    }
93

94
    /// Direct write to standard output
95
    func write(_ text: String) {
96
        stdoutStream.write(text)
97
    }
98

99
    /// Direct write to standard output
100
    func write(_ text: [String]) {
101
        write(text.joined())
102
    }
103

104
    /// Direct write to standard output
105
    func write(_ text: String...) {
106
        write(text)
107
    }
108

109
    /// Direct write to standard output with new line
110
    func writeln(_ text: String...) {
111
        write(text + ["\n"])
112
    }
113

114
    /// Direct write to standard output only new line
115
    func writeln() {
116
        write("\n")
117
    }
118

119
    private func getSize() -> Size {
120
        var winsz = winsize()
121
        _ = ioctl(0, TIOCGWINSZ, &winsz)
122
        return Size(rows: Int(winsz.ws_row), cols: Int(winsz.ws_col))
123
    }
124
}
125

126
// MARK: - Non Blocking
127

128
extension ANSITerminal {
129
    func disableNonBlockingTerminal() {
130
        // restore default terminal mode
131
        var blockTerm = terminal
132
        
133
        // enable CANONical mode and ECHO-ing input
134
        blockTerm.c_lflag |= tcflag_t(ICANON | ECHO | ECHOCTL)
135
        // acknowledge CRNL line ending and UTF8 input
136
        blockTerm.c_iflag |= tcflag_t(ICRNL | IUTF8)
137
        
138
        tcsetattr(STDIN_FILENO, TCSANOW, &blockTerm)
139
        isNonBlockingMode = false
140
    }
141

142
    func enableNonBlockingTerminal() {
143
        // store current terminal mode
144
        tcgetattr(STDIN_FILENO, &terminal)
145
        if !isNonBlockingExitSetUp {
146
            atexit(exitDisableNonBlockingTerminal)
147
            isNonBlockingExitSetUp = true
148
        }
149
        isNonBlockingMode = true
150

151
        // configure non-blocking and non-echoing terminal mode
152
        var nonBlockTerm = terminal
153
        
154
        // disable CANONical mode and ECHO-ing input
155
        nonBlockTerm.c_lflag &= ~tcflag_t(ICANON | ECHO | ECHOCTL)
156
        // acknowledge CRNL line ending and UTF8 input
157
        nonBlockTerm.c_iflag &= ~tcflag_t(ICRNL | IUTF8)
158

159
        // enable new terminal mode
160
        tcsetattr(STDIN_FILENO, TCSANOW, &nonBlockTerm)
161
    }
162
}
163

164
private func exitDisableNonBlockingTerminal() {
165
    ANSITerminal.shared.disableNonBlockingTerminal()
166
}
167

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.