Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,19 @@ opt_in_rules:
analyzer_rules:
- unused_import
custom_rules:
prefer_dependency_print:
name: "Dependency Output"
included: '/Sources/'
excluded: '/(Tests|Examples)/'
regex: '(?<![\w.])(?:Swift\s*\.\s*)?print\s*\('
match_kinds:
- identifier
message: "Use d.out.print instead of print or Swift.print."
severity: error
spaces_not_tabs:
name: "Spaces not Tabs"
regex: ^\t
message: "Use four spaces, not tabs"
point_zero:
name: "Point Zero"
regex: '(?<!iOS\s)(?<!macOS\s)(?<!\.)\b[\d_]+\.0\b'
match_kinds:
- number
- attribute.builtin
message: "Don't add a .0 to the end of floating point literals"
force_try: warning
force_cast: warning
function_body_length: 100
Expand Down
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ let package = Package(
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.6.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio", from: "2.77.0"),
.package(url: "https://github.com/apple/swift-collections", from: "1.7.1"),
.package(url: "https://github.com/apple/xcode-project-format.git", .upToNextMinor(from: "0.1.0")),

Expand All @@ -73,6 +72,7 @@ let package = Package(

.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.2"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"),
// we don't use this but need the explicit dependency to keep the lockfile stable.
// TODO: remove once https://github.com/pointfreeco/combine-schedulers/issues/120 is fixed
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0"),
Expand Down Expand Up @@ -108,6 +108,8 @@ let package = Package(
.target(
name: "XUtils",
dependencies: [
"CXKit",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "SystemPackage", package: "swift-system"),
.product(
name: "Subprocess",
Expand Down Expand Up @@ -161,9 +163,20 @@ let package = Package(
dependencies: [
"XKit",
"XToolSupport",
"XCLIUtils",
"PackLib",
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "X509", package: "swift-certificates"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings,
),
.target(
name: "XCLIUtils",
dependencies: [
"XUtils",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
],
swiftSettings: swiftSettings,
),
Expand All @@ -172,10 +185,11 @@ let package = Package(
dependencies: [
"SwiftyMobileDevice",
"XKit",
"XCLIUtils",
"PackLib",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "libunxip", package: "unxip"),
],
cSettings: cSettings,
Expand Down
65 changes: 65 additions & 0 deletions Sources/XCLIUtils/CommandInput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Dependencies
import Foundation
import XUtils

package struct CommandInput: DependencyKey, Sendable {
// nil = EOF
private let readImpl: @Sendable (DataChannel.Limit) async throws -> Data?

package init(
read: @escaping @Sendable (DataChannel.Limit) async throws -> Data?,
) {
self.readImpl = read
}

package init(channel: DataChannel) {
self.init(read: { try await channel.read(upTo: $0) })
}

package func read(upTo limit: DataChannel.Limit = .unlimited) async throws -> Data? {
try Task.checkCancellation()
return try await readImpl(limit)
}

/// Cancellation discards bytes already consumed for this line.
package func readLine() async throws -> String? {
var line = Data()
while let chunk = try await read(upTo: .newline) {
line.append(chunk)
if line.last == UInt8(ascii: "\n") {
line.removeLast()
return String(decoding: line, as: UTF8.self).trimmingTrailingCarriageReturn()
}
}
guard !line.isEmpty else { return nil }
return String(decoding: line, as: UTF8.self).trimmingTrailingCarriageReturn()
}

package static var liveValue: CommandInput {
@Dependency(\.shutdownRegistry) var shutdownRegistry
do {
let input = try DispatchInput(fileDesciptor: .standardInput)
shutdownRegistry.register { await input.close() }
return Self(read: { try await input.read(upTo: $0) })
} catch {
return Self(read: { _ in throw error })
}
}

package static var testValue: CommandInput {
Self(read: unimplemented("CommandInput.read", placeholder: nil))
}
}

private extension String {
func trimmingTrailingCarriageReturn() -> String {
hasSuffix("\r") ? String(dropLast()) : self
}
}

extension DependencyValues {
package var input: CommandInput {
get { self[CommandInput.self] }
set { self[CommandInput.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,15 @@
import Foundation
import XKit
import XUtils
import NIOPosix
import NIOCore
import Dependencies

enum Console {
private static func withStdio<T>(
_ body: (
_ stdin: NIOAsyncChannelInboundStream<ByteBuffer>,
_ stdout: NIOAsyncChannelOutboundWriter<ByteBuffer>
) async throws -> T
) async throws -> T {
try await NIOPipeBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.takingOwnershipOfDescriptors(
input: FileDescriptor.standardInput.duplicate().rawValue,
output: FileDescriptor.standardOutput.duplicate().rawValue
)
.flatMapThrowing { try NIOAsyncChannel(wrappingChannelSynchronously: $0) }
.get()
.executeThenClose { try await body($0, $1) }
}

static func prompt(_ message: String) async throws -> String {
try await withStdio { stdin, stdout in
try await stdout.write(ByteBuffer(bytes: message.utf8))

fflush(stdoutSafe)

var data = Data()
for try await chunk in stdin {
let view = chunk.readableBytesView
if let endIndex = view.firstIndex(of: UInt8(ascii: "\n")) {
data += view[..<endIndex]
break
} else {
data += view
}
}
let str = String(decoding: data, as: UTF8.self)
return str.hasSuffix("\r") ? String(str.dropLast()) : str
}
package enum Console {
package static func prompt(_ message: String) async throws -> String {
d.out.print(message, terminator: "", flush: true)
guard let line = try await d.input.readLine() else { throw Console.Error("Input ended before a response was received.") }
return line
}

static func promptRequired(_ message: String, existing: String?) async throws -> String {
package static func promptRequired(_ message: String, existing: String?) async throws -> String {
let value: String
if let existing {
value = existing
Expand All @@ -55,12 +22,12 @@ enum Console {
return value
}

static func getPassword(_ message: String) async throws -> String {
package static func getPassword(_ message: String) async throws -> String {
if !message.isEmpty {
print(message, terminator: "")
d.out.print(message, terminator: "")
}
let password = try await withoutEcho { try await prompt("") }
print()
d.out.print()
return password
}

Expand Down Expand Up @@ -91,7 +58,7 @@ enum Console {
#endif
}

static func chooseNumber(in range: Range<Int>) async throws -> Int {
package static func chooseNumber(in range: Range<Int>) async throws -> Int {
let message = "Choice (\(range.lowerBound)-\(range.upperBound - 1)): "
while true {
if let choice = try await Int(prompt(message)), range.contains(choice) {
Expand All @@ -100,7 +67,7 @@ enum Console {
}
}

static func choose<T>(
package static func choose<T>(
from elements: [T],
onNoElement: () throws -> T,
multiPrompt: @autoclosure () -> String,
Expand All @@ -112,9 +79,9 @@ enum Console {
case 1:
return elements[0]
default:
print(multiPrompt())
d.out.print(multiPrompt())
try elements.enumerated().forEach { index, element in
try print("\(index): \(formatter(element))")
try d.out.print("\(index): \(formatter(element))")
}
let choice = try await chooseNumber(in: elements.indices)
return elements[choice]
Expand All @@ -124,7 +91,7 @@ enum Console {
private static let yesSet: Set<String> = ["yes", "y"]
private static let noSet: Set<String> = ["no", "n"]

static func confirm(_ message: String) async throws -> Bool {
package static func confirm(_ message: String) async throws -> Bool {
while true {
let resp = try await prompt("\(message) (yes/no): ").lowercased()
if yesSet.contains(resp) {
Expand All @@ -135,9 +102,9 @@ enum Console {
}
}

struct Error: Swift.Error, CustomStringConvertible {
let description: String
init(_ description: String) {
package struct Error: Swift.Error, CustomStringConvertible {
package let description: String
package init(_ description: String) {
self.description = description
}
}
Expand Down
Loading
Loading