From 1db19fbe40ed43b0db07921efae954bbc4e6100e Mon Sep 17 00:00:00 2001 From: Ankit Aggarwal Date: Fri, 9 Feb 2018 22:35:52 -0800 Subject: [PATCH 1/2] [Basic] Consider terminal as non tty when TERM=dumb --- Sources/Basic/TerminalController.swift | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Sources/Basic/TerminalController.swift b/Sources/Basic/TerminalController.swift index 06a62bad835..5fdbcfc2453 100644 --- a/Sources/Basic/TerminalController.swift +++ b/Sources/Basic/TerminalController.swift @@ -15,6 +15,18 @@ import func POSIX.getenv /// Allows operations like cursor movement and colored text output on tty. public final class TerminalController { + /// The type of terminal. + public enum TerminalType { + /// The terminal is a TTY. + case tty + + /// TERM enviornment variable is set to "dumb". + case dumb + + /// The terminal is a file stream. + case file + } + /// Terminal color choices. public enum Color { case noColor @@ -70,7 +82,16 @@ public final class TerminalController { /// Checks if passed file stream is tty. public static func isTTY(_ stream: LocalFileOutputByteStream) -> Bool { - return isatty(fileno(stream.filePointer)) != 0 + return terminalType(stream) == .tty + } + + /// Computes the terminal type of the stream. + public static func terminalType(_ stream: LocalFileOutputByteStream) -> TerminalType { + if POSIX.getenv("TERM") == "dumb" { + return .dumb + } + let isTTY = isatty(fileno(stream.filePointer)) != 0 + return isTTY ? .tty : .file } /// Tries to get the terminal width first using COLUMNS env variable and From 7902eec51b630a5ba3e8ed81bb1c7e65ce344493 Mon Sep 17 00:00:00 2001 From: Ankit Aggarwal Date: Fri, 9 Feb 2018 23:23:27 -0800 Subject: [PATCH 2/2] [Utility] Add SingleLineProgressBar This progress bar is visually better if the terminal is "dumb". --- Sources/Commands/SwiftTestTool.swift | 4 +- Sources/Utility/ProgressBar.swift | 57 +++++++++++++++++++++-- Tests/UtilityTests/ProgressBarTests.swift | 2 +- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/Sources/Commands/SwiftTestTool.swift b/Sources/Commands/SwiftTestTool.swift index b52a10f6e4f..e3642f77a4f 100644 --- a/Sources/Commands/SwiftTestTool.swift +++ b/Sources/Commands/SwiftTestTool.swift @@ -463,7 +463,7 @@ final class ParallelTestRunner { init(testPath: AbsolutePath, processSet: ProcessSet) { self.testPath = testPath self.processSet = processSet - progressBar = createProgressBar(forStream: stdoutStream, header: "Tests") + progressBar = createProgressBar(forStream: stdoutStream, header: "Testing:") } /// Whether to display output from successful tests. @@ -539,7 +539,7 @@ final class ParallelTestRunner { // Wait till all threads finish execution. workers.forEach { $0.join() } - progressBar.complete() + progressBar.complete(success: outputs.failure.isEmpty) if shouldOutputSuccess { printOutput(outputs.success) diff --git a/Sources/Utility/ProgressBar.swift b/Sources/Utility/ProgressBar.swift index 1741bea499a..c94c877ab79 100644 --- a/Sources/Utility/ProgressBar.swift +++ b/Sources/Utility/ProgressBar.swift @@ -13,7 +13,44 @@ import Basic /// A protocol to operate on terminal based progress bars. public protocol ProgressBarProtocol { func update(percent: Int, text: String) - func complete() + func complete(success: Bool) +} + +/// A single line progress bar. +public final class SingleLineProgressBar: ProgressBarProtocol { + private let header: String + private var isClear: Bool + private var stream: OutputByteStream + private var displayed: Set = [] + + init(stream: OutputByteStream, header: String) { + self.stream = stream + self.header = header + self.isClear = true + } + + public func update(percent: Int, text: String) { + if isClear { + stream <<< header + stream <<< "\n" + stream.flush() + isClear = false + } + + let displayPercentage = Int(Double(percent / 10).rounded(.down)) * 10 + if percent != 100, !displayed.contains(displayPercentage) { + stream <<< String(displayPercentage) <<< ".. " + displayed.insert(displayPercentage) + } + stream.flush() + } + + public func complete(success: Bool) { + if success { + stream <<< "OK" + stream.flush() + } + } } /// Simple ProgressBar which shows the update text in new lines. @@ -41,7 +78,7 @@ public final class SimpleProgressBar: ProgressBarProtocol { stream.flush() } - public func complete() { + public func complete(success: Bool) { } } @@ -90,15 +127,27 @@ public final class ProgressBar: ProgressBarProtocol { term.moveCursor(up: 1) } - public func complete() { + public func complete(success: Bool) { term.endLine() } } /// Creates colored or simple progress bar based on the provided output stream. public func createProgressBar(forStream stream: OutputByteStream, header: String) -> ProgressBarProtocol { - if let stdStream = stream as? LocalFileOutputByteStream, let term = TerminalController(stream: stdStream) { + guard let stdStream = stream as? LocalFileOutputByteStream else { + return SimpleProgressBar(stream: stream, header: header) + } + + // If we have a terminal, use animated progress bar. + if let term = TerminalController(stream: stdStream) { return ProgressBar(term: term, header: header) } + + // If the terminal is dumb, use single line progress bar. + if TerminalController.terminalType(stdStream) == .dumb { + return SingleLineProgressBar(stream: stream, header: header) + } + + // Use simple progress bar by default. return SimpleProgressBar(stream: stream, header: header) } diff --git a/Tests/UtilityTests/ProgressBarTests.swift b/Tests/UtilityTests/ProgressBarTests.swift index 383cfb0d16f..42a907e8426 100644 --- a/Tests/UtilityTests/ProgressBarTests.swift +++ b/Tests/UtilityTests/ProgressBarTests.swift @@ -99,7 +99,7 @@ final class ProgressBarTests: XCTestCase { for i in 0...5 { bar.update(percent: i, text: String(i)) } - bar.complete() + bar.complete(success: true) } static var allTests = [