Skip to content

Commit 23a22a5

Browse files
committed
Use /dev/tty for input/output
Gives us slightly better compatibility with I/O redirection.
1 parent 84ffa0e commit 23a22a5

File tree

4 files changed

+95
-51
lines changed

4 files changed

+95
-51
lines changed

Sources/FuzzyTUI/FuzzySelector.swift

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ public typealias Selectable = CustomStringConvertible & Sendable & Equatable
88
@MainActor
99
final class FuzzySelectorView<T: Selectable> {
1010
private let appearance: Appearance
11+
private let outTTY: OutTTY
12+
private let tty: TTY
1113
private let viewState: ViewState<T>
1214

1315
init(
1416
appearance: Appearance,
17+
outTTY: OutTTY,
18+
tty: TTY,
1519
viewState: ViewState<T>
1620
) {
1721
self.appearance = appearance
22+
self.outTTY = outTTY
23+
self.tty = tty
1824
self.viewState = viewState
1925
}
2026
}
@@ -348,45 +354,38 @@ private extension FuzzySelectorView {
348354
case (true, true): return self.appearance.highlightedTextAttributes
349355
}
350356
}
351-
}
352357

353-
@MainActor
354-
func write(_ strings: [String]) {
355-
for string in strings {
356-
try! FileHandle.standardOutput.write(contentsOf: Data(string.utf8))
358+
func write(_ strings: [String]) {
359+
self.outTTY.write(strings)
357360
}
358-
try! FileHandle.standardOutput.synchronize()
359-
}
360361

361-
@MainActor
362-
func write(_ string: String) {
363-
write([string])
364-
}
362+
func write(_ string: String) {
363+
self.write([string])
364+
}
365365

366-
@MainActor
367-
func outputCode(_ code: ANSIControlCode) {
368-
write(code.ansiCommand.message)
369-
}
366+
func outputCode(_ code: ANSIControlCode) {
367+
self.write(code.ansiCommand.message)
368+
}
370369

371-
@MainActor
372-
func outputCodes(_ codes: [ANSIControlCode]) {
373-
write(codes.map(\.ansiCommand.message))
374-
}
370+
func outputCodes(_ codes: [ANSIControlCode]) {
371+
self.write(codes.map(\.ansiCommand.message))
372+
}
375373

376-
@MainActor
377-
func withSavedCursorPosition<T>(_ body: () throws -> T) rethrows -> T {
378-
outputCodes([
379-
.setCursorHidden(true),
380-
.saveCursorPosition,
381-
])
382-
defer {
383-
outputCodes([
384-
.restoreCursorPosition,
385-
.setCursorHidden(false),
374+
@discardableResult
375+
func withSavedCursorPosition<V>(_ body: () throws -> V) rethrows -> V {
376+
self.outputCodes([
377+
.setCursorHidden(true),
378+
.saveCursorPosition,
386379
])
387-
}
380+
defer {
381+
self.outputCodes([
382+
.restoreCursorPosition,
383+
.setCursorHidden(false),
384+
])
385+
}
388386

389-
return try body()
387+
return try body()
388+
}
390389
}
391390

392391
func setGraphicsModes(textAttributes: Set<Appearance.TextAttributes>) -> [SetGraphicsRendition] {
@@ -441,7 +440,9 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
441440
private let choices: Seq
442441
private let installSignalHandlers: Bool
443442
private let multipleSelection: Bool
443+
private let outTTY: OutTTY
444444
private let tty: TTY
445+
private let ttyHandle: FileHandle
445446
private let view: FuzzySelectorView<T>
446447
private let viewState: ViewState<T>
447448

@@ -455,8 +456,19 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
455456
reverse: Bool = true
456457
) {
457458
let appearance = appearance ?? .default
458-
let terminalSize = TerminalSize.current()
459-
guard let tty = TTY(fileHandle: STDIN_FILENO) else {
459+
guard let terminalSize = TerminalSize.current() else {
460+
// TODO: error
461+
return nil
462+
}
463+
let ttyHandle = FileHandle(forReadingAtPath: "/dev/tty")!
464+
guard let tty = TTY(fileHandle: ttyHandle) else {
465+
// TODO: error
466+
return nil
467+
}
468+
469+
guard let outTTYHandle = FileHandle(forWritingAtPath: "/dev/tty"),
470+
let outTTY = OutTTY(fileHandle: outTTYHandle)
471+
else {
460472
// TODO: error
461473
return nil
462474
}
@@ -472,8 +484,15 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
472484
self.choices = choices
473485
self.installSignalHandlers = installSignalHandlers
474486
self.multipleSelection = multipleSelection
487+
self.outTTY = outTTY
475488
self.tty = tty
476-
self.view = FuzzySelectorView(appearance: appearance, viewState: viewState)
489+
self.ttyHandle = ttyHandle
490+
self.view = FuzzySelectorView(
491+
appearance: appearance,
492+
outTTY: outTTY,
493+
tty: tty,
494+
viewState: viewState
495+
)
477496
self.viewState = viewState
478497
}
479498

@@ -482,7 +501,7 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
482501
/// The `run` method consumes the `choices` sequence given in init and asynchronously returns the selected items.
483502
public func run() async throws -> [T] {
484503
let keyReader = KeyReader(tty: tty)
485-
outputCodes([
504+
self.view.outputCodes([
486505
.setCursorHidden(true),
487506
.saveCursorPosition,
488507
.saveScreen,
@@ -539,7 +558,7 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
539558
self.view.showFilter()
540559
self.view.showStatus()
541560
case .key(.down):
542-
withSavedCursorPosition {
561+
self.view.withSavedCursorPosition {
543562
self.view.moveDown()
544563
}
545564
self.view.showFilter()
@@ -559,7 +578,7 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
559578
break eventLoop
560579
case .key(.suspend):
561580
try self.tty.unsetRaw()
562-
outputCodes([
581+
self.view.outputCodes([
563582
.disableAlternativeBuffer,
564583
.restoreScreen,
565584
])
@@ -570,7 +589,7 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
570589
case .key(.tab):
571590
if self.multipleSelection {
572591
self.viewState.toggleCurrentSelection()
573-
withSavedCursorPosition {
592+
self.view.withSavedCursorPosition {
574593
self.view.moveDown()
575594
self.view.redrawChoices()
576595
}
@@ -582,20 +601,20 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
582601
self.view.showFilter()
583602
self.view.showStatus()
584603
case .key(.up):
585-
withSavedCursorPosition {
604+
self.view.withSavedCursorPosition {
586605
self.view.moveUp()
587606
}
588607
self.view.showFilter()
589608
self.view.showStatus()
590609
case .key(nil): break
591610
case let .choice(choices):
592611
self.viewState.addChoices(choices)
593-
withSavedCursorPosition {
612+
self.view.withSavedCursorPosition {
594613
self.view.redrawChoices()
595614
}
596615
self.view.showStatus()
597616
case .viewStateChanged:
598-
withSavedCursorPosition {
617+
self.view.withSavedCursorPosition {
599618
self.view.redrawChoices()
600619
}
601620
self.view.showStatus()
@@ -608,7 +627,8 @@ public final class FuzzySelector<T: Selectable, E: Error, Seq> where Seq: AsyncS
608627
}
609628
}
610629
try self.tty.unsetRaw()
611-
outputCodes([
630+
631+
self.view.outputCodes([
612632
.disableAlternativeBuffer,
613633
.restoreScreen,
614634
.restoreCursorPosition,

Sources/FuzzyTUI/KeyReader.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ final class KeyReader {
1717
while !self.stopped.withLock({ $0 }) {
1818
let key = { () -> TerminalKey? in
1919
var buffer = [UInt8](repeating: 0, count: 4)
20-
let bytesRead = read(STDIN_FILENO, &buffer, 4)
20+
let bytesRead = read(self.tty.fileHandle.fileDescriptor, &buffer, 4)
2121
guard bytesRead > 0 else { return nil }
2222
if bytesRead == 1 {
2323
switch buffer[0] {

Sources/FuzzyTUI/TTY.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ import Foundation
22

33
@MainActor
44
final class TTY {
5-
private let fileHandle: Int32
5+
let fileHandle: FileHandle
66
private var originalTermios: termios?
77

8-
init?(fileHandle: Int32) {
9-
guard isatty(fileHandle) == 1 else { return nil }
8+
init?(fileHandle: FileHandle) {
9+
guard isatty(fileHandle.fileDescriptor) == 1 else { return nil }
1010
self.fileHandle = fileHandle
1111
}
1212

1313
func setRaw() throws {
1414
var originalTermios = termios()
1515

16-
if tcgetattr(fileHandle, &originalTermios) == -1 {
16+
if tcgetattr(fileHandle.fileDescriptor, &originalTermios) == -1 {
1717
throw Failure.getAttributes
1818
}
1919

@@ -29,14 +29,14 @@ final class TTY {
2929
$0.withMemoryRebound(to: cc_t.self, capacity: Int(NCCS)) { $0[Int(VMIN)] = 1 }
3030
}
3131

32-
if tcsetattr(fileHandle, TCSAFLUSH, &raw) < 0 {
32+
if tcsetattr(self.fileHandle.fileDescriptor, TCSAFLUSH, &raw) < 0 {
3333
throw Failure.setAttributes
3434
}
3535
}
3636

3737
func unsetRaw() throws {
3838
guard var originalTermios = self.originalTermios else { return }
39-
if tcsetattr(self.fileHandle, TCSAFLUSH, &originalTermios) < 0 {
39+
if tcsetattr(self.fileHandle.fileDescriptor, TCSAFLUSH, &originalTermios) < 0 {
4040
throw Failure.setAttributes
4141
}
4242
}
@@ -48,3 +48,26 @@ extension TTY {
4848
case setAttributes
4949
}
5050
}
51+
52+
@MainActor
53+
final class OutTTY {
54+
private let fileHandle: FileHandle
55+
56+
init?(fileHandle: FileHandle) {
57+
guard isatty(fileHandle.fileDescriptor) == 1 else { return nil }
58+
self.fileHandle = fileHandle
59+
}
60+
61+
func close() throws {
62+
try self.fileHandle.synchronize()
63+
tcflush(self.fileHandle.fileDescriptor, TCOFLUSH)
64+
try self.fileHandle.close()
65+
}
66+
67+
func write(_ strings: [String]) {
68+
for string in strings {
69+
try! self.fileHandle.write(contentsOf: Data(string.utf8))
70+
}
71+
try! self.fileHandle.synchronize()
72+
}
73+
}

Sources/FuzzyTUI/TerminalSize.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ public struct TerminalSize: Sendable, Equatable {
1010

1111
extension TerminalSize {
1212
/// Return the current terminal size.
13-
public static func current() -> Self {
13+
public static func current() -> Self? {
1414
var w = winsize()
15-
_ = ioctl(STDOUT_FILENO, UInt(TIOCGWINSZ), &w)
15+
guard let tty = FileHandle.init(forReadingAtPath: "/dev/tty") else { return nil }
16+
_ = ioctl(tty.fileDescriptor, UInt(TIOCGWINSZ), &w)
1617
return TerminalSize(height: Int(w.ws_row), width: Int(w.ws_col))
1718
}
1819
}

0 commit comments

Comments
 (0)