swift package init
provides a fast way to initialize projects, users can select from several hard-coded templates to start with.
There are many different needs for such templates, so there should be a way to customize these templates and share them with others without having to make changes to the SwiftPM codebase itself. It could also be useful to make templates interactive, bring user-friendliness to SwiftPM's CLI.
Define a fixed location to store local templates and using git repositories to store remote templates. Once local template is stored, use swift package init --type <projectName>
to use. Use swift package init --url <YourTemplatesURL.git>
to fetch remote templates. Once copied the template, swiftPM will decode template.json
file(where interactive information is stored) and make init process interactive. We'll add interactivity to hard-coded projects as well.
Bringing interactivity to swiftPM's cli, such interactivity could be:
- Include tests
- Include plugins
- Include a set of dependencies
- Name of the project
- Type of template to start with
Use template.json to define the interactivity of the template,the format could be something like this:
- template name
"name" : "MyTemplate"
- dependencies
"dependencies" : [
{
"url": "https://github.com/wadetregaskis/FluidMenuBarExtra.git",
"from":"1.0.1"
},
{
"url": "https://github.com/SwiftBeta/SwiftOpenAI.git",
"branch":"main"
}
]
- include tests
"test" : [
{
"path" : "path/to/test/directory",
"default_include" : "false",
"interact" : "true"
},
{
"path" : "path/to/another/test/directory",
"default_include" : "true"
}
]
- include plugins
"plugins" : [
{
"path" : "path/to/local/plugin",
"interact" : "true"
},
{
"url" : "https://github.com/apple/swift-docc-plugin",
"default_include" : "false",
"interact" : "true"
}
]
Selecting the type of template to include
Users can use arrow keys and enter to choose the type of template to start with. To enable that in terminal, we have to use Raw Mode Input
to process keyboard input. Normally, one has to press enter in order to let program reads each line of input, such input mode is called canonical input
. If wanting to read a key just after it is pressed, we have to change terminal's input mode to Raw Mode
. Using c code to enable such is easier and more straightforward here.
- Solution 1
Write ICANON
flag in termios.c_lflag, use c_lflag &= ~(ICANON);
to turn off canonical input
.
#if os(Linux) || os(FreeBSD)
raw.c_iflag &= ~UInt32(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
raw.c_oflag &= ~UInt32(OPOST)
raw.c_cflag |= UInt32(CS8)
raw.c_lflag &= ~UInt32(ECHO | ICANON | IEXTEN | ISIG)
#else
raw.c_iflag &= ~UInt(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
raw.c_oflag &= ~UInt(OPOST)
raw.c_cflag |= UInt(CS8)
raw.c_lflag &= ~UInt(ECHO | ICANON | IEXTEN | ISIG)
#endif
Code referenced from here.
- Solution 2
Using stty system command to change terminal characteristics. In this case, we'll use stty raw
. To use the stty command, we need to use C. Integrate into swiftPM's source code using c interop.
system("stty raw");
This is the solution I used.
In new target folder RawModeInputLibc
, I created rawModeInput.c
, adding code below:
#include "include/rawModeInput.h"
#include <stdlib.h>
void setRawModeInput () {
system("stty raw");
}
void setCookedModeInput() {
system("stty cooked");
}
In Package.swift
, add target and use it for dependency of workspace.
.target(
name: "RawModeInputLibc"
),
.target(
/** High level functionality */
name: "Workspace",
dependencies: [
"Basics",
"PackageFingerprint",
"PackageGraph",
"PackageModel",
"PackageRegistry",
"PackageSigning",
"SourceControl",
"SPMBuildCore",
"RawModeInputLibc" // add dependency
],
exclude: ["CMakeLists.txt"]
),
Add static public method interactiveInit()
and private method processInput()
in InitPackage.swift
public static func interactiveInit() throws -> PackageType{
return try processInput()
}
// for interactive init
static let types = 7
static let initMode: [PackageType] = [.library, .executable, .empty, .tool, .buildToolPlugin, .commandPlugin, .macro]
// ANSI escape code
static let plain = "\u{001B}[0m"
static let yellow = "\u{001B}[38;5;202m"
static var firstWrite = true
static var initModeBool : [Bool] = []
private static func processInput() throws -> PackageType {
// set default mode library
var currentMode = 0
for _ in 0...types {
initModeBool.append(false)
}
initModeBool[currentMode] = true
while true {
// set canonical input, otherwise print will be weird.
setCookedModeInput()
if firstWrite == false {
print("\u{001B}[\(types+1)A",terminator: "")
}
firstWrite = false
print("Choose package type:")
for packageType in 0...types - 1{
ANSIPrint(type: packageType)
}
setRawModeInput()
initModeBool[currentMode] = false
let cha = getchar()
// use q to quit
if (cha == 113) {
setCookedModeInput()
exit(0)
} else if (cha == 13) { // enter
setCookedModeInput()
return initMode[currentMode]
} else if (cha == 27) { // arrow key up and down
let cha2 = getchar()
if (cha2 == 91) {
let cha3 = getchar()
if (cha3 == 65) { // up
currentMode = currentMode - 1 < 0 ? currentMode - 1 + types : currentMode - 1
} else if (cha3 == 66) { // down
currentMode = (currentMode + 1) % types
}
}
}
initModeBool[currentMode] = true
}
}
private static func ANSIPrint (type: Int) throws {
if (initModeBool[type] == true) {
print(yellow, terminator: "")
}
print(initMode[type])
print(plain,terminator: "")
}
If I broke the programming code, I'm very sorry. I'm also very sorry if I use too many static.
This is for demonstration purposes only, if you feel like there are problems please let me know.
In order to use interactive init, cross-platform is a big problem, I will focus on this issue in the future.