diff --git a/src/interactive.jl b/src/interactive.jl index 991ead4..dfe2273 100644 --- a/src/interactive.jl +++ b/src/interactive.jl @@ -16,15 +16,6 @@ else show_field(x::AbstractString) = repr(contractuser(x)) end -""" - interactive(::Type{T<:Plugin}) -> T - -Create a [`Plugin`](@ref) of type `T` interactively from user input. -""" -function interactive(::Type{T}) where T <: Plugin - return T() # TODO -end - function Base.show(io::IO, m::MIME"text/plain", t::Template) println(io, "Template:") foreach(fieldnames(Template)) do n @@ -41,6 +32,29 @@ function Base.show(io::IO, m::MIME"text/plain", t::Template) end end +""" + interactive(::Type{T<:Plugin}) -> T + +Create a [`Plugin`](@ref) of type `T` interactively from user input. +""" +function interactive(::Type{T}) where T <: Plugin + kwargs = Dict{Symbol, Any}() + + foreach(fieldnames(T)) do name + F = fieldtype(T, name) + v = Val(name) + required = !applicable(defaultkw, T, v) + default = required ? defaultkw(F) : defaultkw(T, v) + kwargs[name] = if applicable(prompt, T, v) + prompt(F, "$T: $(prompt(T, v))", default, required=required) + else + prompt(F, "$T: Value for field '$name' ($F)", default; required=required) + end + end + + return T(; kwargs...) +end + leaves(T::Type) = isconcretetype(T) ? [T] : vcat(map(leaves, subtypes(T))...) function plugin_types() @@ -56,28 +70,34 @@ function Template(::Val{true}; kwargs...) opts = Dict{Symbol, Any}(kwargs) if !haskey(opts, :user) - opts[:user] = prompt(String, "Git hosting service username", defaultkw(:user)) + default = defaultkw(Template, :user) + opts[:user] = prompt(String, "Git hosting service username", default) end if !haskey(opts, :host) - opts[:host] = prompt(String, "Git hosting service URL", defaultkw(:host)) + default = defaultkw(Template, :host) + opts[:host] = prompt(String, "Git hosting service URL", default) end if !haskey(opts, :authors) - opts[:authors] = prompt(String, "Package author(s)", defaultkw(:authors)) + default = defaultkw(Template, :authors) + opts[:authors] = prompt(String, "Package author(s)", default) end if !haskey(opts, :dir) - opts[:dir] = prompt(String, "Path to package parent directory", defaultkw(:dir)) + default = defaultkw(Template, :dir) + opts[:dir] = prompt(String, "Path to package parent directory", default) end if !haskey(opts, :julia) - opts[:julia] = prompt(VersionNumber, "Supported Julia version", defaultkw(:julia)) + default = defaultkw(Template, :julia) + opts[:julia] = prompt(VersionNumber, "Supported Julia version", default) end if !haskey(opts, :disable_defaults) available = map(typeof, default_plugins()) - opts[:disable_defaults] = select("Select defaults to disable:", available, []) + initial = defaultkw(Template, :disable_defaults) + opts[:disable_defaults] = select("Select defaults to disable:", available, initial) end if !haskey(opts, :plugins) @@ -91,20 +111,35 @@ function Template(::Val{true}; kwargs...) return Template(Val(false); opts...) end -function prompt(::Type{String}, s::AbstractString, default; required::Bool=false) +defaultkw(::Type{String}) = "" +defaultkw(::Type{Union{T, Nothing}}) where T = nothing +defaultkw(::Type{T}) where T <: Number = zero(T) +defaultkw(::Type{Vector{T}}) where T = T[] + +function prompt( + ::Type{<:Union{String, Nothing}}, s::AbstractString, default; + required::Bool=false, +) default isa AbstractString && (default = contractuser(default)) - default_display = required ? "REQUIRED" : repr(default) + default_display = if required + "REQUIRED" + elseif default === nothing + "None" + else + repr(default) + end + print("$s [$default_display]: ") input = strip(readline()) + return if isempty(input) && required - println("This option is required") + println("This option is required") prompt(String, s, default; required=required) elseif isempty(input) default else input end - end function prompt( @@ -123,11 +158,19 @@ end function prompt(::Type{Bool}, s::AbstractString, default::Bool; required::Bool=false) b = prompt(String, s, default; required=required) - return uppercase(b) in ("Y", "YES", "T", "TRUE") + return b === default ? default : uppercase(b) in ("Y", "YES", "T", "TRUE") end -function prompt(::Type{Vector{String}}, s::AbstractString, default::Vector{<:AbstractString}) - # TODO +function prompt(::Type{Vector}, s::AbstractString, default::Vector; required::Bool=false) + return prompt(Vector{String}, s, default; required=required) +end + +function prompt( + ::Type{Vector{String}}, s::AbstractString, default::Vector{<:AbstractString}; + required::Bool=false, +) + s = prompt(String, "$s (comma-delimited)", join(default, ", "); required=required) + return convert(Vector{String}, map(strip, split(s, ","; keepempty=false))) end # TODO: These can be made simpler when this is merged: @@ -144,8 +187,8 @@ function select(f::Function, s::AbstractString, xs::Vector, initial::Vector) end # Select one item frm oa collection. -function select(f::Function, s::AbstractString, xs::Vector, _initial) - # Can't use the initial value yet. - selection = request(s, RadioMenu(map(f, xs))) +function select(f::Function, s::AbstractString, xs::Vector, initial) + print(stdin.buffer, repeat("\e[B", findfirst(==(initial), xs) - 1)) + selection = request("$s:", RadioMenu(map(f, xs); pagesize=length(xs))) return xs[selection] end diff --git a/src/plugin.jl b/src/plugin.jl index 52fd04c..23cf306 100644 --- a/src/plugin.jl +++ b/src/plugin.jl @@ -235,6 +235,11 @@ If you are implementing a plugin that uses the `user` field of a [`Template`](@r """ needs_username(::Plugin) = false +function prompt end + +iscall(x, ::Symbol) = false +iscall(ex::Expr, s::Symbol) = ex.head === :call && ex.args[1] === s + """ @with_defaults struct T #= ... =# end @@ -253,13 +258,28 @@ end ``` """ macro with_defaults(ex::Expr) - T = esc(ex.args[2].args[1]) + T = esc(ex.args[2].args[1]) # This assumes T <: U. - # TODO: Parse out `<- "prompt"` stuff. - funcs = map(filter(arg -> arg isa Expr && arg.head === :(=), ex.args[3].args)) do arg - name = QuoteNode(arg.args[1].args[1]) - val = arg.args[2] - :(PkgTemplates.defaultkw(::Type{$T}, ::Val{$name}) = $(esc(arg.args[2]))) + # This is a bit nasty. + funcs = Expr[] + foreach(filter(arg -> arg isa Expr, ex.args[3].args)) do arg + if iscall(arg, :<) && iscall(arg.args[3], :-) # x::T <- "prompt" + name = QuoteNode(arg.args[2].args[1]) + prompt = arg.args[2] + push!(funcs, :(PkgTemplates.prompt(::Type{$T}, ::Val{$name}) = $(esc(prompt)))) + elseif arg.head === :(=) + rhs = arg.args[2] + if iscall(rhs, :<) && iscall(rhs.args[3], :-) # x::T = "foo" <- "prompt" + name = QuoteNode(arg.args[1].args[1]) + prompt = rhs.args[3].args[2] + default = arg.args[2] = rhs.args[2] + push!( + funcs, + :(PkgTemplates.prompt(::Type{$T}, ::Val{$name}) = $(esc(prompt))), + :(PkgTemplates.defaultkw(::Type{$T}, ::Val{$name}) = $(esc(default))), + ) + end + end end return Expr(:block, esc(with_kw(ex, __module__, false)), funcs...) diff --git a/src/plugins/ci.jl b/src/plugins/ci.jl index 0b2b41b..a9ce0fc 100644 --- a/src/plugins/ci.jl +++ b/src/plugins/ci.jl @@ -45,13 +45,13 @@ Integrates your packages with [Travis CI](https://travis-ci.com). $EXTRA_VERSIONS_DOC """ @with_defaults struct TravisCI <: BasicPlugin - file::String = default_file("travis.yml") - linux::Bool = true - osx::Bool = true - windows::Bool = true - x86::Bool = false - coverage::Bool = true - extra_versions::Vector = DEFAULT_CI_VERSIONS + file::String = default_file("travis.yml") <- "Path to .travis.yml template" + linux::Bool = true <- "Enable Linux bulds" + osx::Bool = true <- "Enable OSX builds" + windows::Bool = true <- "Enable Windows builds" + x86::Bool = false <- "Enable 32-bit builds" + coverage::Bool = true <- "Enable code coverage submission" + extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test" end source(p::TravisCI) = p.file @@ -114,10 +114,10 @@ Integrates your packages with [AppVeyor](https://appveyor.com) via [AppVeyor.jl] $EXTRA_VERSIONS_DOC """ @with_defaults struct AppVeyor <: BasicPlugin - file::String = default_file("appveyor.yml") - x86::Bool = false - coverage::Bool = true - extra_versions::Vector = DEFAULT_CI_VERSIONS + file::String = default_file("appveyor.yml") <- "Path to .appveyor.yml template" + x86::Bool = false <- "Enable 32-bit builds" + coverage::Bool = true <- "Enable code coverage submission" + extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test" end source(p::AppVeyor) = p.file @@ -167,10 +167,10 @@ $EXTRA_VERSIONS_DOC Code coverage submission from Cirrus CI is not yet supported by [Coverage.jl](https://github.com/JuliaCI/Coverage.jl). """ @with_defaults struct CirrusCI <: BasicPlugin - file::String = default_file("cirrus.yml") - image::String = "freebsd-12-0-release-amd64" - coverage::Bool = true - extra_versions::Vector = DEFAULT_CI_VERSIONS + file::String = default_file("cirrus.yml") <- "Path to .cirrus.yml template" + image::String = "freebsd-12-0-release-amd64" <- "FreeBSD image" + coverage::Bool = true <- "Enable code coverage submission" + extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test" end source(p::CirrusCI) = p.file @@ -216,10 +216,10 @@ See [`Documenter`](@ref) for more information. Nightly Julia is not supported. """ @with_defaults struct GitLabCI <: BasicPlugin - file::String = default_file("gitlab-ci.yml") - coverage::Bool = true + file::String = default_file("gitlab-ci.yml") <- "Path to .gitlab-ci.yml template" + coverage::Bool = true <- "Enable code coverage submission" # Nightly has no Docker image. - extra_versions::Vector = map(format_version, [default_version(), VERSION]) + extra_versions::Vector = map(format_version, [default_version(), VERSION]) <- "Extra Julia versions to test" end gitignore(p::GitLabCI) = p.coverage ? COVERAGE_GITIGNORE : String[] diff --git a/src/plugins/citation.jl b/src/plugins/citation.jl index fcb8e6f..68b6f25 100644 --- a/src/plugins/citation.jl +++ b/src/plugins/citation.jl @@ -8,8 +8,8 @@ Creates a `CITATION.bib` file for citing package repositories. - `readme::Bool`: Whether or not to include a section about citing in the README. """ @with_defaults struct Citation <: BasicPlugin - file::String = default_file("CITATION.bib") - readme::Bool = false + file::String = default_file("CITATION.bib") <- "Path to CITATION.bib template" + readme::Bool = false <- """Enable "Citing" README section""" end tags(::Citation) = "<<", ">>" diff --git a/src/plugins/coverage.jl b/src/plugins/coverage.jl index 58b4cae..26d11df 100644 --- a/src/plugins/coverage.jl +++ b/src/plugins/coverage.jl @@ -9,7 +9,7 @@ Sets up code coverage submission from CI to [Codecov](https://codecov.io). - `file::Union{AbstractString, Nothing}`: Template file for `.codecov.yml`, or `nothing` to create no file. """ @with_defaults struct Codecov <: BasicPlugin - file::Union{String, Nothing} = nothing + file::Union{String, Nothing} = nothing <- "Path to .codecov.yml template" end source(p::Codecov) = p.file @@ -30,7 +30,7 @@ Sets up code coverage submission from CI to [Coveralls](https://coveralls.io). - `file::Union{AbstractString, Nothing}`: Template file for `.coveralls.yml`, or `nothing` to create no file. """ @with_defaults struct Coveralls <: BasicPlugin - file::Union{String, Nothing} = nothing + file::Union{String, Nothing} = nothing <- "Path to .coveralls.yml template" end source(p::Coveralls) = p.file diff --git a/src/plugins/git.jl b/src/plugins/git.jl index c1ec679..8ba5718 100644 --- a/src/plugins/git.jl +++ b/src/plugins/git.jl @@ -13,10 +13,10 @@ Creates a Git repository and a `.gitignore` file. This option requires that the Git CLI is installed, and for you to have a GPG key associated with your committer identity. """ @with_defaults struct Git <: Plugin - ignore::Vector{String} = [] - ssh::Bool = false - manifest::Bool = false - gpgsign::Bool = false + ignore::Vector{String} = String[] <- "Gitignore entries" + ssh::Bool = false <- "Enable SSH Git remote" + manifest::Bool = false <- "Commit Manifest.toml" + gpgsign::Bool = false <- "GPG-sign commits" end # Try to make sure that no files are created after we commit. diff --git a/src/plugins/license.jl b/src/plugins/license.jl index f6bf72c..5a2fc20 100644 --- a/src/plugins/license.jl +++ b/src/plugins/license.jl @@ -16,7 +16,7 @@ struct License <: BasicPlugin destination::String end -function License( +function License(; name::AbstractString="MIT", path::Union{AbstractString, Nothing}=nothing, destination::AbstractString="LICENSE", @@ -28,10 +28,21 @@ function License( return License(path, destination) end - source(p::License) = p.path destination(p::License) = p.destination view(::License, t::Template, ::AbstractString) = Dict( "AUTHORS" => join(t.authors, ", "), "YEAR" => year(today()), ) + +function interactive(::Type{License}) + destination = prompt(String, "License: License file destination", "LICENSE") + return if prompt(Bool, "License: Use custom license file", false) + path = prompt(String, "License: Path to custom license file", ""; required=true) + License(; path=path, destination=destination) + else + available = sort(readdir(joinpath(TEMPLATES_DIR, "licenses"))) + name = select("License: Select a license", available, "MIT") + License(; name=name, destination=destination) + end +end diff --git a/src/plugins/readme.jl b/src/plugins/readme.jl index c277f62..f982fab 100644 --- a/src/plugins/readme.jl +++ b/src/plugins/readme.jl @@ -15,9 +15,9 @@ By default, it includes badges for other included plugins - `inline_badges::Bool`: Whether or not to put the badges on the same line as the package name. """ @with_defaults struct Readme <: BasicPlugin - file::String = default_file("README.md") - destination::String = "README.md" - inline_badges::Bool = false + file::String = default_file("README.md") <- "Path to README.md template" + destination::String = "README.md" <- "README file destination" + inline_badges::Bool = false <- "Enable inline badges" end source(p::Readme) = p.file diff --git a/src/plugins/src_dir.jl b/src/plugins/src_dir.jl index 38b9acc..9f0c58a 100644 --- a/src/plugins/src_dir.jl +++ b/src/plugins/src_dir.jl @@ -7,7 +7,7 @@ Creates a module entrypoint. - `file::AbstractString`: Template file for `src/.jl`. """ @with_defaults mutable struct SrcDir <: BasicPlugin - file::String = default_file("src", "module.jl") + file::String = default_file("src", "module.jl") <- "Path to src/.jl template" destination::String = joinpath("src", ".jl") end diff --git a/src/plugins/tests.jl b/src/plugins/tests.jl index 1dc4684..e125ee1 100644 --- a/src/plugins/tests.jl +++ b/src/plugins/tests.jl @@ -15,8 +15,8 @@ Sets up testing for packages. Managing test dependencies with `test/Project.toml` is only supported in Julia 1.2 and later. """ @with_defaults struct Tests <: BasicPlugin - file::String = default_file("test", "runtests.jl") - project::Bool = false + file::String = default_file("test", "runtests.jl") <- "Path to runtests.jl template" + project::Bool = false <- "Enable test/Project.toml" end source(p::Tests) = p.file diff --git a/src/template.jl b/src/template.jl index 9a08ae0..fa19018 100644 --- a/src/template.jl +++ b/src/template.jl @@ -127,14 +127,14 @@ function getplugin(t::Template, ::Type{T}) where T <: Plugin end # Get a keyword, or compute some default value. -getkw(kwargs, k) = get(() -> defaultkw(k), kwargs, k) +getkw(kwargs, k) = get(() -> defaultkw(Template, k), kwargs, k) # Default Template keyword values. -defaultkw(s::Symbol) = defaultkw(Val(s)) -defaultkw(::Val{:authors}) = default_authors() -defaultkw(::Val{:dir}) = Pkg.devdir() -defaultkw(::Val{:disable_defaults}) = DataType[] -defaultkw(::Val{:host}) = "github.com" -defaultkw(::Val{:julia}) = default_version() -defaultkw(::Val{:plugins}) = Plugin[] -defaultkw(::Val{:user}) = default_user() +defaultkw(::Type{T}, s::Symbol) where T = defaultkw(T, Val(s)) +defaultkw(::Type{Template}, ::Val{:authors}) = default_authors() +defaultkw(::Type{Template}, ::Val{:dir}) = Pkg.devdir() +defaultkw(::Type{Template}, ::Val{:disable_defaults}) = DataType[] +defaultkw(::Type{Template}, ::Val{:host}) = "github.com" +defaultkw(::Type{Template}, ::Val{:julia}) = default_version() +defaultkw(::Type{Template}, ::Val{:plugins}) = Plugin[] +defaultkw(::Type{Template}, ::Val{:user}) = default_user()