mirror of
https://github.com/m-housh/dotfiles.git
synced 2026-02-14 22:22:40 +00:00
wip
This commit is contained in:
9
dots/.gitignore
vendored
Normal file
9
dots/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/config/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
||||||
18
dots/Makefile
Normal file
18
dots/Makefile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
PREFIX ?= $(HOME)/.local
|
||||||
|
BINDIR = $(PREFIX)/bin
|
||||||
|
COMPLETIONDIR = $(PREFIX)/completions
|
||||||
|
LIBDIR = $(PREFIX)/lib
|
||||||
|
|
||||||
|
build:
|
||||||
|
swiftc ./scripts/build.swift
|
||||||
|
./build
|
||||||
|
rm ./build
|
||||||
|
|
||||||
|
install: build
|
||||||
|
install -d "$(BINDIR)" "$(LIBDIR)"
|
||||||
|
install ./.build/release/dots "$(BINDIR)"
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm "$(BINDIR)/dots"
|
||||||
|
rm "$(COMPLETIONDIR)/_dots"
|
||||||
|
|
||||||
75
dots/Package.swift
Normal file
75
dots/Package.swift
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// swift-tools-version: 5.7
|
||||||
|
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "dots",
|
||||||
|
platforms: [
|
||||||
|
.macOS(.v12)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.executable(name: "dots", targets: ["dots"]),
|
||||||
|
.library(name: "CliMiddleware", targets: ["CliMiddleware"]),
|
||||||
|
.library(name: "CliMiddlewareLive", targets: ["CliMiddlewareLive"]),
|
||||||
|
.library(name: "FileClient", targets: ["FileClient"]),
|
||||||
|
.library(name: "LoggingDependency", targets: ["LoggingDependency"]),
|
||||||
|
.library(name: "ShellClient", targets: ["ShellClient"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
|
||||||
|
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "0.1.4"),
|
||||||
|
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
|
||||||
|
.package(url: "https://github.com/adorkable/swift-log-format-and-pipe.git", from: "0.1.0"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "CliMiddleware",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "CliMiddlewareLive",
|
||||||
|
dependencies: [
|
||||||
|
"CliMiddleware",
|
||||||
|
"FileClient",
|
||||||
|
"LoggingDependency",
|
||||||
|
"ShellClient"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "FileClient",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "LoggingDependency",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "Logging", package: "swift-log"),
|
||||||
|
.product(name: "LoggingFormatAndPipe", package: "swift-log-format-and-pipe"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.executableTarget(
|
||||||
|
name: "dots",
|
||||||
|
dependencies: [
|
||||||
|
"CliMiddlewareLive",
|
||||||
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "dotsTests",
|
||||||
|
dependencies: ["dots"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "ShellClient",
|
||||||
|
dependencies: [
|
||||||
|
"LoggingDependency",
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
3
dots/README.md
Normal file
3
dots/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# dots
|
||||||
|
|
||||||
|
A description of this package.
|
||||||
90
dots/Sources/CliMiddleware/Middleware.swift
Normal file
90
dots/Sources/CliMiddleware/Middleware.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import XCTestDynamicOverlay
|
||||||
|
|
||||||
|
/// Implements the logic for the `dots` command line tool.
|
||||||
|
///
|
||||||
|
/// Each command and it's sub-commands are implemented in the ``CliMiddlewareLive`` module. While this
|
||||||
|
/// represents the interface.
|
||||||
|
///
|
||||||
|
public struct CliMiddleware {
|
||||||
|
|
||||||
|
public var brew: (BrewContext) async throws -> Void
|
||||||
|
public var zsh: (ZshContext) async throws -> Void
|
||||||
|
|
||||||
|
public init(
|
||||||
|
brew: @escaping (BrewContext) async throws -> Void,
|
||||||
|
zsh: @escaping (ZshContext) async throws -> Void
|
||||||
|
) {
|
||||||
|
self.brew = brew
|
||||||
|
self.zsh = zsh
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GlobalContext {
|
||||||
|
public let dryRun: Bool
|
||||||
|
|
||||||
|
public init(dryRun: Bool) {
|
||||||
|
self.dryRun = dryRun
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct BrewContext {
|
||||||
|
public let routes: [Route]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
routes: [Route]
|
||||||
|
) {
|
||||||
|
self.routes = routes
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Route: String, CaseIterable {
|
||||||
|
case all
|
||||||
|
case appStore
|
||||||
|
case brews
|
||||||
|
case casks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ZshContext {
|
||||||
|
|
||||||
|
public let context: Context
|
||||||
|
|
||||||
|
public init(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Context {
|
||||||
|
case install
|
||||||
|
case uninstall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliMiddleware.GlobalContext: TestDependencyKey {
|
||||||
|
public static let testValue: CliMiddleware.GlobalContext = .init(dryRun: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliMiddleware: TestDependencyKey {
|
||||||
|
|
||||||
|
public static let noop = Self.init(
|
||||||
|
brew: unimplemented("\(Self.self).brew"),
|
||||||
|
zsh: unimplemented("\(Self.self).zsh")
|
||||||
|
)
|
||||||
|
|
||||||
|
public static let testValue = CliMiddleware.noop
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DependencyValues {
|
||||||
|
|
||||||
|
public var cliMiddleware: CliMiddleware {
|
||||||
|
get { self[CliMiddleware.self] }
|
||||||
|
set { self[CliMiddleware.self] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var globals: CliMiddleware.GlobalContext {
|
||||||
|
get { self[CliMiddleware.GlobalContext.self] }
|
||||||
|
set { self[CliMiddleware.GlobalContext.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
95
dots/Sources/CliMiddlewareLive/Internals/Brew.swift
Normal file
95
dots/Sources/CliMiddlewareLive/Internals/Brew.swift
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import Dependencies
|
||||||
|
import CliMiddleware
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import LoggingDependency
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
struct Brew {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.globals.dryRun) var dryRun
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
@Dependency(\.shellClient) var shellClient
|
||||||
|
|
||||||
|
let context: CliMiddleware.BrewContext
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
logger.info("Installing homebrew dependencies.")
|
||||||
|
for brewfile in try context.routes.brewfiles() {
|
||||||
|
logger.info("Installing dependencies from brewfile: \(brewfile.absoluteString)")
|
||||||
|
if !dryRun {
|
||||||
|
try shellClient.install(brewfile: brewfile)
|
||||||
|
logger.debug("Done installing dependencies from brewfile: \(brewfile.absoluteString)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("Done installing homebrew dependencies.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ShellClient {
|
||||||
|
|
||||||
|
func install(brewfile: URL) throws {
|
||||||
|
try foregroundShell(
|
||||||
|
"/opt/homebrew/bin/brew",
|
||||||
|
"bundle",
|
||||||
|
"--no-lock",
|
||||||
|
"--cleanup",
|
||||||
|
"--debug",
|
||||||
|
"--file",
|
||||||
|
brewfile.absoluteString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension FileClient {
|
||||||
|
var brewFileDirectory: URL {
|
||||||
|
dotfilesDirectory()
|
||||||
|
.appendingPathComponent("macOS")
|
||||||
|
.appendingPathComponent(".config")
|
||||||
|
.appendingPathComponent("macOS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension CliMiddleware.BrewContext.Route {
|
||||||
|
|
||||||
|
static func allBrews() throws -> [URL] {
|
||||||
|
let brews: [Self] = [.appStore, .brews, .casks]
|
||||||
|
return try brews.map { try $0.brewfile() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func brewfile() throws -> URL {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
switch self {
|
||||||
|
case .all:
|
||||||
|
// should never happen.
|
||||||
|
throw BrewfileError()
|
||||||
|
case .appStore:
|
||||||
|
return fileClient.brewFileDirectory.appendingPathComponent("AppStore.Brewfile")
|
||||||
|
case .brews:
|
||||||
|
return fileClient.brewFileDirectory.appendingPathComponent("Brewfile")
|
||||||
|
case .casks:
|
||||||
|
return fileClient.brewFileDirectory.appendingPathComponent("Casks.Brewfile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension Array where Element == CliMiddleware.BrewContext.Route {
|
||||||
|
|
||||||
|
func brewfiles() throws -> [URL] {
|
||||||
|
|
||||||
|
if self.count == 1 && self.first == .all {
|
||||||
|
return try CliMiddleware.BrewContext.Route.allBrews()
|
||||||
|
}
|
||||||
|
|
||||||
|
var urls = [URL]()
|
||||||
|
for route in self {
|
||||||
|
if route != .all {
|
||||||
|
let url = try route.brewfile()
|
||||||
|
urls.append(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BrewfileError: Error { }
|
||||||
87
dots/Sources/CliMiddlewareLive/Internals/Zsh.swift
Normal file
87
dots/Sources/CliMiddlewareLive/Internals/Zsh.swift
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import CliMiddleware
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import LoggingDependency
|
||||||
|
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct Zsh {
|
||||||
|
@Dependency(\.globals.dryRun) var dryRun
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
let context: CliMiddleware.ZshContext
|
||||||
|
|
||||||
|
func install() async throws {
|
||||||
|
let configString = fileClient.zshConfigDestination.absoluteString
|
||||||
|
.replacingOccurrences(of: "file://", with: "")
|
||||||
|
|
||||||
|
let destination = fileClient.zshEnvDestination
|
||||||
|
|
||||||
|
let destinationString = destination.absoluteString
|
||||||
|
.replacingOccurrences(of: "file://", with: "")
|
||||||
|
|
||||||
|
logger.info("Linking zsh configuration to: \(configString)")
|
||||||
|
logger.info("Linking .zshenv file to: \(destinationString)")
|
||||||
|
|
||||||
|
if !dryRun {
|
||||||
|
try await linkZshConfig()
|
||||||
|
try await fileClient.createSymlink(
|
||||||
|
source: fileClient.zshEnvSource,
|
||||||
|
destination: destination
|
||||||
|
)
|
||||||
|
}
|
||||||
|
logger.info("Done installing zsh configuration files.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func uninstall() async throws {
|
||||||
|
logger.info("Uninstalling zsh configuration from: \(fileClient.zshConfigDestination.absoluteString)")
|
||||||
|
if !dryRun {
|
||||||
|
logger.debug("Moving configuration to the trash.")
|
||||||
|
try await fileClient.moveToTrash(fileClient.zshConfigDestination)
|
||||||
|
logger.debug("Moving .zshenv to the trash.")
|
||||||
|
try await fileClient.moveToTrash(fileClient.zshEnvDestination)
|
||||||
|
}
|
||||||
|
logger.info("Done uninstalling zsh configuration, you will need to reload your shell.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
switch context.context {
|
||||||
|
case .install:
|
||||||
|
try await self.install()
|
||||||
|
case .uninstall:
|
||||||
|
try await self.uninstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkZshConfig() async throws {
|
||||||
|
try await fileClient.createDirectory(at: fileClient.configDirectory())
|
||||||
|
try await fileClient.createSymlink(
|
||||||
|
source: fileClient.zshDirectory,
|
||||||
|
destination: fileClient.zshConfigDestination
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension FileClient {
|
||||||
|
var zshDirectory: URL {
|
||||||
|
dotfilesDirectory()
|
||||||
|
.appendingPathComponent("zsh")
|
||||||
|
.appendingPathComponent("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
var zshConfigDestination: URL {
|
||||||
|
configDirectory().appendingPathComponent("zsh")
|
||||||
|
}
|
||||||
|
|
||||||
|
var zshEnvDestination: URL {
|
||||||
|
homeDirectory().appendingPathComponent(".zshenv")
|
||||||
|
}
|
||||||
|
|
||||||
|
var zshEnvSource: URL {
|
||||||
|
zshDirectory.appendingPathComponent(".zshenv")
|
||||||
|
}
|
||||||
|
}
|
||||||
19
dots/Sources/CliMiddlewareLive/LiveKey.swift
Normal file
19
dots/Sources/CliMiddlewareLive/LiveKey.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Dependencies
|
||||||
|
@_exported import CliMiddleware
|
||||||
|
@_exported import FileClient
|
||||||
|
@_exported import LoggingDependency
|
||||||
|
@_exported import ShellClient
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extension CliMiddleware: DependencyKey {
|
||||||
|
public static var liveValue: CliMiddleware {
|
||||||
|
.init(
|
||||||
|
brew: { try await Brew(context: $0).run() },
|
||||||
|
zsh: { try await Zsh(context: $0).run() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
dots/Sources/FileClient/Client.swift
Normal file
70
dots/Sources/FileClient/Client.swift
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
import XCTestDynamicOverlay
|
||||||
|
|
||||||
|
/// Represents interactions with the file system.
|
||||||
|
///
|
||||||
|
public struct FileClient {
|
||||||
|
|
||||||
|
public var configDirectory: () -> URL
|
||||||
|
public var createDirectory: (URL, Bool) async throws -> Void
|
||||||
|
public var createSymlink: (URL, URL) async throws -> Void
|
||||||
|
public var dotfilesDirectory: () -> URL
|
||||||
|
public var homeDirectory: () -> URL
|
||||||
|
public var exists: (URL) async throws -> Bool
|
||||||
|
public var readFile: (URL) async throws -> Data
|
||||||
|
public var moveToTrash: (URL) async throws -> Void
|
||||||
|
public var writeFile: (Data, URL) async throws -> Void
|
||||||
|
|
||||||
|
public func createDirectory(
|
||||||
|
at url: URL,
|
||||||
|
withIntermediates: Bool = true
|
||||||
|
) async throws {
|
||||||
|
let exists = try await self.exists(url)
|
||||||
|
if !exists {
|
||||||
|
try await createDirectory(url, withIntermediates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func createSymlink(
|
||||||
|
source: URL,
|
||||||
|
destination: URL
|
||||||
|
) async throws {
|
||||||
|
try await self.createSymlink(source, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func read(file: URL) async throws -> Data {
|
||||||
|
try await self.readFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func write(data: Data, to file: URL) async throws {
|
||||||
|
try await writeFile(data, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FileClient: TestDependencyKey {
|
||||||
|
public static let noop = Self.init(
|
||||||
|
configDirectory: unimplemented(placeholder: URL(string: "/")!),
|
||||||
|
createDirectory: unimplemented(),
|
||||||
|
createSymlink: unimplemented(),
|
||||||
|
dotfilesDirectory: unimplemented(placeholder: URL(string: "/")!),
|
||||||
|
homeDirectory: unimplemented(),
|
||||||
|
exists: unimplemented(placeholder: false),
|
||||||
|
readFile: unimplemented(placeholder: Data()),
|
||||||
|
moveToTrash: unimplemented(),
|
||||||
|
writeFile: unimplemented()
|
||||||
|
)
|
||||||
|
|
||||||
|
public static let testValue: FileClient = .noop
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DependencyValues {
|
||||||
|
public var fileClient: FileClient {
|
||||||
|
get { self[FileClient.self] }
|
||||||
|
set { self[FileClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
78
dots/Sources/FileClient/LiveKey.swift
Normal file
78
dots/Sources/FileClient/LiveKey.swift
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extension FileClient: DependencyKey {
|
||||||
|
|
||||||
|
public static var liveValue: FileClient {
|
||||||
|
.live(environment: ProcessInfo.processInfo.environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func live(environment: [String: String] = [:]) -> Self {
|
||||||
|
let environment = Environment(environment: environment)
|
||||||
|
return .init(
|
||||||
|
configDirectory: {
|
||||||
|
guard let xdgConfigHome = environment.xdgConfigHome,
|
||||||
|
let configUrl = URL(string: xdgConfigHome)
|
||||||
|
else {
|
||||||
|
return FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent(".config")
|
||||||
|
}
|
||||||
|
return configUrl
|
||||||
|
},
|
||||||
|
createDirectory: { url, withIntermediates in
|
||||||
|
try FileManager.default.createDirectory(
|
||||||
|
at: url,
|
||||||
|
withIntermediateDirectories: withIntermediates
|
||||||
|
)
|
||||||
|
},
|
||||||
|
createSymlink: { source, destination in
|
||||||
|
try FileManager.default.createSymbolicLink(
|
||||||
|
at: source,
|
||||||
|
withDestinationURL: destination
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dotfilesDirectory: {
|
||||||
|
guard let dotfiles = environment.dotfilesDirectory,
|
||||||
|
let dotfilesUrl = URL(string: dotfiles)
|
||||||
|
else {
|
||||||
|
return FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent(".dotfiles")
|
||||||
|
}
|
||||||
|
return dotfilesUrl
|
||||||
|
},
|
||||||
|
homeDirectory: {
|
||||||
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
},
|
||||||
|
exists: { path in
|
||||||
|
FileManager.default.fileExists(atPath: path.absoluteString)
|
||||||
|
},
|
||||||
|
readFile: { path in
|
||||||
|
try Data(contentsOf: path)
|
||||||
|
},
|
||||||
|
moveToTrash: { path in
|
||||||
|
try FileManager.default.trashItem(at: path, resultingItemURL: nil)
|
||||||
|
},
|
||||||
|
writeFile: { data, path in
|
||||||
|
try data.write(to: path)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct Environment {
|
||||||
|
let xdgConfigHome: String?
|
||||||
|
let dotfilesDirectory: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case xdgConfigHome = "XDG_CONFIG_HOME"
|
||||||
|
case dotfilesDirectory = "DOTFILES"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(environment: [String: String]) {
|
||||||
|
self.xdgConfigHome = environment[CodingKeys.xdgConfigHome.rawValue]
|
||||||
|
self.dotfilesDirectory = environment[CodingKeys.dotfilesDirectory.rawValue]
|
||||||
|
}
|
||||||
|
}
|
||||||
32
dots/Sources/LoggingDependency/Live.swift
Normal file
32
dots/Sources/LoggingDependency/Live.swift
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
@_exported import Logging
|
||||||
|
import LoggingFormatAndPipe
|
||||||
|
|
||||||
|
extension Logger: DependencyKey {
|
||||||
|
|
||||||
|
fileprivate static func factory(label: String) -> Self {
|
||||||
|
Logger(label: "dots") { _ in
|
||||||
|
LoggingFormatAndPipe.Handler(
|
||||||
|
formatter: BasicFormatter([.message]),
|
||||||
|
pipe: LoggerTextOutputStreamPipe.standardOutput
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var liveValue: Logger {
|
||||||
|
factory(label: "dots")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var testValue: Logger {
|
||||||
|
factory(label: "dots-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DependencyValues {
|
||||||
|
public var logger: Logger {
|
||||||
|
get { self[Logger.self] }
|
||||||
|
set { self[Logger.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
37
dots/Sources/ShellClient/Client.swift
Normal file
37
dots/Sources/ShellClient/Client.swift
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import XCTestDynamicOverlay
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public struct ShellClient {
|
||||||
|
public var foregroundShell: ([String]) throws -> Void
|
||||||
|
public var backgroundShell: ([String]) throws -> String
|
||||||
|
|
||||||
|
public func foregroundShell(_ arguments: String...) throws {
|
||||||
|
try self.foregroundShell(arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func backgroundShell(_ arguments: String...) throws -> String {
|
||||||
|
try self.backgroundShell(arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ShellClient: TestDependencyKey {
|
||||||
|
|
||||||
|
public static let noop = Self.init(
|
||||||
|
foregroundShell: unimplemented(),
|
||||||
|
backgroundShell: unimplemented(placeholder: "")
|
||||||
|
)
|
||||||
|
|
||||||
|
public static let testValue: ShellClient = .noop
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DependencyValues {
|
||||||
|
public var shellClient: ShellClient {
|
||||||
|
get { self[ShellClient.self] }
|
||||||
|
set { self[ShellClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
59
dots/Sources/ShellClient/LiveKey.swift
Normal file
59
dots/Sources/ShellClient/LiveKey.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import LoggingDependency
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extension ShellClient: DependencyKey {
|
||||||
|
|
||||||
|
public static var liveValue: ShellClient {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
return .init(
|
||||||
|
foregroundShell: { arguments in
|
||||||
|
logger.debug("Running in foreground shell.")
|
||||||
|
logger.debug("$ \(arguments.joined(separator: " "))")
|
||||||
|
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/usr/bin/env"
|
||||||
|
task.arguments = arguments
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
guard task.terminationStatus == 0 else {
|
||||||
|
throw ShellError(terminationStatus: task.terminationStatus)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundShell: { arguments in
|
||||||
|
logger.debug("Running background shell.")
|
||||||
|
logger.debug("$ \(arguments.joined(separator: " "))")
|
||||||
|
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/usr/bin/env"
|
||||||
|
task.arguments = arguments
|
||||||
|
// grab stdout
|
||||||
|
let output = Pipe()
|
||||||
|
task.standardOutput = output
|
||||||
|
// ignore stderr
|
||||||
|
let error = Pipe()
|
||||||
|
task.standardError = error
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
guard task.terminationStatus == 0 else {
|
||||||
|
throw ShellError(terminationStatus: task.terminationStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(decoding: output.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShellError: Swift.Error {
|
||||||
|
var terminationStatus: Int32
|
||||||
|
}
|
||||||
24
dots/Sources/dots/CliContext.swift
Normal file
24
dots/Sources/dots/CliContext.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CliContext {
|
||||||
|
let globals: GlobalOptions
|
||||||
|
let _run: () async throws -> Void
|
||||||
|
|
||||||
|
init(globals: GlobalOptions, run: @escaping () async throws -> Void) {
|
||||||
|
self.globals = globals
|
||||||
|
self._run = run
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await withDependencies {
|
||||||
|
if globals.verbose {
|
||||||
|
$0.logger.logLevel = .debug
|
||||||
|
}
|
||||||
|
$0.globals = .live(globals)
|
||||||
|
} operation: {
|
||||||
|
try await _run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
dots/Sources/dots/Commands/Brew.swift
Normal file
44
dots/Sources/dots/Commands/Brew.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliMiddleware
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import LoggingDependency
|
||||||
|
|
||||||
|
extension Dots {
|
||||||
|
|
||||||
|
struct Brew: AsyncParsableCommand {
|
||||||
|
static var configuration = CommandConfiguration(
|
||||||
|
abstract: "Manage homebrew dependency installation.",
|
||||||
|
subcommands: [
|
||||||
|
Install.self
|
||||||
|
],
|
||||||
|
defaultSubcommand: Install.self
|
||||||
|
)
|
||||||
|
|
||||||
|
struct Install: AsyncParsableCommand {
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
abstract: "Install brew dependencies from the brewfiles."
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
|
@Flag(help: "The homebrew dependencies to install from their brewfiles.")
|
||||||
|
var routes: [CliMiddleware.BrewContext.Route] = [.all]
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await CliContext(globals: globals) {
|
||||||
|
@Dependency(\.cliMiddleware.brew) var brew
|
||||||
|
@Dependency(\.logger) var logger: Logger
|
||||||
|
|
||||||
|
logger.debug("Routes: \(routes)")
|
||||||
|
try await brew(.init(routes: routes))
|
||||||
|
logger.info("Done.")
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliMiddleware.BrewContext.Route: EnumerableFlag { }
|
||||||
57
dots/Sources/dots/Commands/Zsh.swift
Normal file
57
dots/Sources/dots/Commands/Zsh.swift
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliMiddleware
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import LoggingDependency
|
||||||
|
|
||||||
|
extension Dots {
|
||||||
|
struct Zsh: AsyncParsableCommand {
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
abstract: "Manage zsh configuration.",
|
||||||
|
subcommands: [
|
||||||
|
Install.self,
|
||||||
|
Uninstall.self
|
||||||
|
],
|
||||||
|
defaultSubcommand: Install.self
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
struct Install: AsyncParsableCommand {
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
abstract: "Install zsh configuration files."
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await CliContext(globals: globals) {
|
||||||
|
@Dependency(\.cliMiddleware.zsh) var zsh
|
||||||
|
@Dependency(\.logger) var logger: Logger
|
||||||
|
|
||||||
|
try await zsh(.init(context: .install))
|
||||||
|
logger.info("Done.")
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Uninstall: AsyncParsableCommand {
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
abstract: "Uninstall zsh configuration files."
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await CliContext(globals: globals) {
|
||||||
|
@Dependency(\.cliMiddleware.zsh) var zsh
|
||||||
|
@Dependency(\.logger) var logger: Logger
|
||||||
|
|
||||||
|
try await zsh(.init(context: .uninstall))
|
||||||
|
logger.info("Done.")
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
dots/Sources/dots/GlobalOptions.swift
Normal file
24
dots/Sources/dots/GlobalOptions.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliMiddleware
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GlobalOptions: ParsableArguments {
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: "Perform an action as a dry-run, not removing or installing anything."
|
||||||
|
)
|
||||||
|
var dryRun: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: "Increase logging output level."
|
||||||
|
)
|
||||||
|
var verbose: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliMiddleware.GlobalContext {
|
||||||
|
static func live(_ globalOptions: GlobalOptions) -> Self {
|
||||||
|
.init(dryRun: globalOptions.dryRun)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
dots/Sources/dots/Version.swift
Normal file
2
dots/Sources/dots/Version.swift
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Do not change this value, it get's set by the build script
|
||||||
|
let VERSION: String? = nil
|
||||||
13
dots/Sources/dots/dots.swift
Normal file
13
dots/Sources/dots/dots.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct Dots: AsyncParsableCommand {
|
||||||
|
static var configuration = CommandConfiguration(
|
||||||
|
abstract: "Commands for installing / uninstalling dotfile configuration.",
|
||||||
|
version: VERSION ?? "0.0.0",
|
||||||
|
subcommands: [
|
||||||
|
Brew.self,
|
||||||
|
Zsh.self
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
8
dots/Tests/dotsTests/dotsTests.swift
Normal file
8
dots/Tests/dotsTests/dotsTests.swift
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import dots
|
||||||
|
|
||||||
|
final class dotsTests: XCTestCase {
|
||||||
|
func testExample() throws {
|
||||||
|
XCTAssert(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
81
dots/scripts/build.swift
Executable file
81
dots/scripts/build.swift
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env swift
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
try build()
|
||||||
|
|
||||||
|
func build() throws {
|
||||||
|
try withVersion(in: "Sources/dots/Version.swift", as: currentVersion()) {
|
||||||
|
try foregroundShell(
|
||||||
|
"swift", "build",
|
||||||
|
"--disable-sandbox",
|
||||||
|
"--configuration", "release",
|
||||||
|
"-Xswiftc", "-cross-module-optimization"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withVersion(in file: String, as version: String, _ closure: () throws -> ()) throws {
|
||||||
|
let fileURL = URL(fileURLWithPath: file)
|
||||||
|
let originalFileContents = try String(contentsOf: fileURL, encoding: .utf8)
|
||||||
|
// set version
|
||||||
|
try originalFileContents
|
||||||
|
.replacingOccurrences(of: "nil", with: "\"\(version)\"")
|
||||||
|
.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||||
|
defer {
|
||||||
|
// undo set version
|
||||||
|
try! originalFileContents
|
||||||
|
.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
// run closure
|
||||||
|
try closure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentVersion() throws -> String {
|
||||||
|
do {
|
||||||
|
let tag = try backgroundShell("git", "describe", "--tags", "--exact-match")
|
||||||
|
return tag
|
||||||
|
} catch {
|
||||||
|
let branch = try backgroundShell("git", "symbolic-ref", "-q", "--short", "HEAD")
|
||||||
|
let commit = try backgroundShell("git", "rev-parse", "--short", "HEAD")
|
||||||
|
return "\(branch) (\(commit))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func foregroundShell(_ args: String...) throws {
|
||||||
|
print("$", args.joined(separator: " "))
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/usr/bin/env"
|
||||||
|
task.arguments = args
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
guard task.terminationStatus == 0 else {
|
||||||
|
throw ShellError(terminationStatus: task.terminationStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func backgroundShell(_ args: String...) throws -> String {
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/usr/bin/env"
|
||||||
|
task.arguments = args
|
||||||
|
// grab stdout
|
||||||
|
let output = Pipe()
|
||||||
|
task.standardOutput = output
|
||||||
|
// ignore stderr
|
||||||
|
let error = Pipe()
|
||||||
|
task.standardError = error
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
guard task.terminationStatus == 0 else {
|
||||||
|
throw ShellError(terminationStatus: task.terminationStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(decoding: output.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShellError: Swift.Error {
|
||||||
|
var terminationStatus: Int32
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Open dotfiles.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
test -d "${DOTFILES}" || \
|
|
||||||
(echo "Dotfiles path not a directory or doesn't exist" && exit 1)
|
|
||||||
|
|
||||||
cd "${DOTFILES}" && vi .
|
|
||||||
@@ -96,7 +96,7 @@ main() {
|
|||||||
_parse_options "$@"
|
_parse_options "$@"
|
||||||
test "$remove" -eq 0 && _remove_scripts && exit "$?"
|
test "$remove" -eq 0 && _remove_scripts && exit "$?"
|
||||||
test "$uninstall" -eq 0 && _remove_scripts && exit "$?"
|
test "$uninstall" -eq 0 && _remove_scripts && exit "$?"
|
||||||
_install && exit "$?"
|
_install
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user