Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[SwiftLeeds '23] Delightful Swift CLI applications

[SwiftLeeds '23] Delightful Swift CLI applications

Pol Piella Abadia

October 09, 2023
Tweet

More Decks by Pol Piella Abadia

Other Decks in Programming

Transcript

  1. // swift-tools-version: 5.9 import PackageDescription let package = Package( name:

    "MusiCLI", dependencies: [], targets: [ .executableTarget( name: "MusiCLI", dependencies: [ ] ) ] )
  2. // swift-tools-version: 5.9 import PackageDescription let package = Package( name:

    "Musicli", platforms: [.macOS(.v12)], dependencies: [ ], targets: [ .executableTarget( name: "Musicli", dependencies: [] ), ] )
  3. // swift-tools-version: 5.9 import PackageDescription let package = Package( name:

    "Musicli", platforms: [.macOS(.v12)], dependencies: [ .package(url: "https: // github.com/apple/swift-argument-parser.git", exact: "1.2.3") ], targets: [ .executableTarget( name: "Musicli", dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")] ), ] )
  4. import MusicKit import ArgumentParser @main struct Musicli: AsyncParsableCommand { func

    run() async throws { guard await isAppAuthorised() else { return } } private func isAppAuthorised() async - > Bool { guard MusicAuthorization.currentStatus != .authorized else { return true } let response = await MusicAuthorization.request() return response == .authorized } }
  5. import MusicKit import ArgumentParser @main struct Musicli: AsyncParsableCommand { func

    run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } } private func isAppAuthorised() async - > Bool { guard MusicAuthorization.currentStatus != .authorized else { return true } let response = await MusicAuthorization.request() return response == .authorized } }
  6. import MusicKit import ArgumentParser @main struct Musicli: AsyncParsableCommand { func

    run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() } private func isAppAuthorised() async - > Bool { guard MusicAuthorization.currentStatus != .authorized else { return true } let response = await MusicAuthorization.request() return response == .authorized } }
  7. import MusicKit import ArgumentParser @main struct Musicli: AsyncParsableCommand { func

    run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() print("Search results for: '\(query)'") for song in searchResponse.songs { print("Title: " + song.title) print("Artist: " + song.artistName) print("Album: " + song.albumTitle!) print("Genres: " + song.genreNames.joined(separator: ", ")) print("") } } private func isAppAuthorised() async - > Bool { guard MusicAuthorization.currentStatus != .authorized else { return true } let response = await MusicAuthorization.request() return response == .authorized } }
  8. // swift-tools-version: 5.9 import PackageDescription let package = Package( name:

    "Musicli", platforms: [.macOS(.v12)], products: [ .executable(name: "Musicli", targets: ["Musicli"]) ], dependencies: [ .package(url: "https: // github.com/apple/swift-argument-parser.git", exact: "1.2.3") ], targets: [ .executableTarget( name: "Musicli", dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")], linkerSettings: [ .unsafeFlags([ "-Xlinker", "-sectcreate", "-Xlinker", " __ TEXT", "-Xlinker", " __ info_plist", "-Xlinker", "Sources/Resources/Info.plist" ]) ] ), ] ) https://forums.swift.org/t/swift-package-manager-use-of-info-plist-use-for-apps/6532/13
  9. // swift-tools-version: 5.9 import PackageDescription let package = Package( name:

    "Musicli", platforms: [.macOS(.v12)], products: [ .executable(name: "Musicli", targets: ["Musicli"]) ], dependencies: [ .package(url: "https: // github.com/apple/swift-argument-parser.git", exact: "1.2.3") ], targets: [ .executableTarget( name: "Musicli", dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")], linkerSettings: [ .unsafeFlags([ "-Xlinker", "-sectcreate", "-Xlinker", " __ TEXT", "-Xlinker", " __ info_plist", "-Xlinker", "Sources/Resources/Info.plist" ]) ] ), ] ) https://forums.swift.org/t/swift-package-manager-use-of-info-plist-use-for-apps/6532/13
  10. import MusicKit import ArgumentParser @main struct Musicli: AsyncParsableCommand { func

    run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() print("Search results for: '\(query)'") for song in searchResponse.songs { print("Title: " + song.title) print("Artist: " + song.artistName) print("Album: " + song.albumTitle!) print("Genres: " + song.genreNames.joined(separator: ", ")) print("") } } }
  11. import MusicKit import ArgumentParser import Spinner @main struct Musicli: AsyncParsableCommand

    { func run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() print("Search results for: '\(query)'") for song in searchResponse.songs { print("Title: " + song.title) print("Artist: " + song.artistName) print("Album: " + song.albumTitle!) print("Genres: " + song.genreNames.joined(separator: ", ")) print("") } } }
  12. import MusicKit import ArgumentParser import Spinner @main struct Musicli: AsyncParsableCommand

    { func run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } let spinner = Spinner(.dots, "Loading music results", color: .green) spinner.start() let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() spinner.stop() print("Search results for: '\(query)'") for song in searchResponse.songs { print("Title: " + song.title) print("Artist: " + song.artistName) print("Album: " + song.albumTitle!) print("Genres: " + song.genreNames.joined(separator: ", ")) print("") } } }
  13. import MusicKit import ArgumentParser import Spinner import ANSITerminal @main struct

    Musicli: AsyncParsableCommand { func run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } let spinner = Spinner(.dots, "Loading music results", color: .green) spinner.start() let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() spinner.stop() print("Search results for: '\(query)'") for song in searchResponse.songs { print("Title: " + song.title) print("Artist: " + song.artistName) print("Album: " + song.albumTitle!) print("Genres: " + song.genreNames.joined(separator: ", ")) print("") } } }
  14. import MusicKit import ArgumentParser import Spinner import ANSITerminal @main struct

    Musicli: AsyncParsableCommand { func run() async throws { guard await isAppAuthorised() else { return } print("Enter the name of the song you'd like to search for . .. ") guard let query = readLine() else { return } let spinner = Spinner(.dots, "Loading music results", color: .green) spinner.start() let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() spinner.stop() print("Search results for: '\(query)'") for song in searchResponse.songs { print("Title: ".bold + song.title) print("Artist: ".bold + song.artistName) print("Album: ".bold + song.albumTitle!) print("Genres: ".bold + song.genreNames.joined(separator: ", ")) print("") } } }
  15. import ArgumentParser import Spinner import MusicKit import ANSITerminal struct Search:

    AsyncParsableCommand { @Argument(help: "The search parameter used to find a song") var searchQuery: String? func run() async throws { guard await isAppAuthorised(), let query = askUserIfNeeded() else { return } let spinner = Spinner(.dots, "Loading music results", color: .green) spinner.start() let request = MusicCatalogSearchRequest(term: query, types: [Song.self]) let searchResponse = try await request.response() spinner.stop() print("Search results for: '\(query)'") for song in searchResponse.songs { print("Title: ".bold + song.title) print("Artist: ".bold + song.artistName) print("Album: ".bold + song.albumTitle!) print("Genres: ".bold + song.genreNames.joined(separator: ", ")) print("") } } }
  16. import ArgumentParser @main struct Musicli: AsyncParsableCommand { static var configuration

    = CommandConfiguration( subcommands: [Search.self], defaultSubcommand: Search.self ) }
  17. import MusicKit import ArgumentParser struct Display: AsyncParsableCommand { @Argument(help: "The

    song you'd like to display information for") var song: String func run() async throws { } }
  18. import MusicKit import ArgumentParser struct Display: AsyncParsableCommand { @Argument(help: "The

    song you'd like to display information for") var song: String func run() async throws { let request = MusicCatalogSearchRequest(term: song, types: [Song.self]) guard let song = try await request.response().songs.first else { print("Could not find a song to play ... ") return } } }
  19. import MusicKit import ArgumentParser struct Display: AsyncParsableCommand { @Argument(help: "The

    song you'd like to display information for") var song: String func run() async throws { let request = MusicCatalogSearchRequest(term: song, types: [Song.self]) guard let song = try await request.response().songs.first else { print("Could not find a song to play ... ") return } await App(song: song).run() } }
  20. import AppKit import MusicKit @MainActor class App { let song:

    Song let delegate: AppDelegate init(song: Song) { self.song = song self.delegate = AppDelegate(song: song) } func run() { let app = NSApplication.shared app.delegate = delegate app.setActivationPolicy(.regular) app.activate(ignoringOtherApps: true) app.run() } }
  21. import AppKit import SwiftUI import MusicKit final class AppDelegate: NSObject,

    NSApplicationDelegate { var window: NSWindow! let song: Song init(song: Song) { self.song = song } func applicationDidFinishLaunching(_ notification: Notification) { let window = NSWindow( contentRect: .zero, styleMask: [.closable, .resizable, .titled], backing: .buffered, defer: false ) window.contentViewController = NSHostingController( rootView: SongView(song: song) ) window.makeKey() window.center() window.orderFrontRegardless() window.title = "🎶 \(song.title) - \(song.artistName)" self.window = window } }
  22. import SwiftUI import MusicKit struct SongView: View { let song:

    Song var body: some View { ZStack { Color(song.artwork ?. backgroundColor ?? .white) .ignoresSafeArea() VStack(spacing: 8) { AsyncImage(url: song.artwork ?. url(width: 400, height: 400)) { image in image .resizable() .frame(width: 400, height: 400) .clipShape(RoundedRectangle(cornerRadius: 20)) } placeholder: { Text("Loading . .. ") } Text(song.title) .font(.largeTitle) .fontWeight(.heavy) .fontDesign(.rounded) Text("\(song.artistName) − \(song.albumTitle ?? "")") .font(.body) .foregroundStyle(Color(cgColor: song.artwork ?. tertiaryTextColor ?? .black)) } .foregroundStyle(Color(song.artwork ? . primaryTextColor ?? .black)) } .frame(width: 600, height: 600) } }
  23. import ArgumentParser @main struct Musicli: AsyncParsableCommand { static var configuration

    = CommandConfiguration( subcommands: [Search.self], defaultSubcommand: Search.self ) }
  24. import ArgumentParser @main struct Musicli: AsyncParsableCommand { static var configuration

    = CommandConfiguration( subcommands: [Search.self, Display.self], defaultSubcommand: Search.self ) }