From 143c9ba4f7ddc59286b6b1a70f3e134861790223 Mon Sep 17 00:00:00 2001 From: Federico Claudi Date: Fri, 16 Dec 2022 09:45:23 -0500 Subject: [PATCH] `Prompt` (#175) * prompt init * small fixes to repr and error * fixed small repr bugs * removed deps * wip errors * bump * bump * bump * bump * prompt wip * bump * prompt almost finished * added tests and docs for prompt * bump * coverage and CI --- Project.toml | 5 +- docs/make.jl | 1 + docs/src/adv/errors_tracebacks.md | 4 +- docs/src/api/api_prompt.md | 14 ++ docs/src/basics/prompt.md | 55 +++++ src/Term.jl | 9 +- src/_errors.jl | 27 ++- src/_repr.jl | 6 +- src/errors.jl | 18 +- src/prompt.jl | 302 +++++++++++++++++++++++++-- src/theme.jl | 5 + src/tprint.jl | 12 +- test/24_prompts.jl | 51 +++++ test/runtests.jl | 1 + test/txtfiles/prompt_print_1.txt | 2 + test/txtfiles/prompt_print_1_ln.txt | 2 + test/txtfiles/prompt_print_1_t.txt | 2 + test/txtfiles/prompt_print_1_tln.txt | 2 + test/txtfiles/prompt_print_2.txt | 2 + test/txtfiles/prompt_print_2_ln.txt | 2 + test/txtfiles/prompt_print_2_t.txt | 2 + test/txtfiles/prompt_print_2_tln.txt | 2 + test/txtfiles/prompt_print_3.txt | 2 + test/txtfiles/prompt_print_3_ln.txt | 2 + test/txtfiles/prompt_print_3_t.txt | 2 + test/txtfiles/prompt_print_3_tln.txt | 2 + test/txtfiles/prompt_print_4.txt | 2 + test/txtfiles/prompt_print_4_ln.txt | 2 + test/txtfiles/prompt_print_4_t.txt | 2 + test/txtfiles/prompt_print_4_tln.txt | 2 + test/txtfiles/prompt_print_5.txt | 2 + test/txtfiles/prompt_print_5_ln.txt | 2 + test/txtfiles/prompt_print_5_t.txt | 2 + test/txtfiles/prompt_print_5_tln.txt | 2 + test/txtfiles/prompt_print_6.txt | 2 + test/txtfiles/prompt_print_6_ln.txt | 2 + test/txtfiles/prompt_print_6_t.txt | 2 + test/txtfiles/prompt_print_6_tln.txt | 2 + test/txtfiles/prompt_print_7.txt | 2 + test/txtfiles/prompt_print_7_ln.txt | 2 + test/txtfiles/prompt_print_7_t.txt | 2 + test/txtfiles/prompt_print_7_tln.txt | 2 + 42 files changed, 521 insertions(+), 45 deletions(-) create mode 100644 docs/src/api/api_prompt.md create mode 100644 docs/src/basics/prompt.md create mode 100644 test/24_prompts.jl create mode 100644 test/txtfiles/prompt_print_1.txt create mode 100644 test/txtfiles/prompt_print_1_ln.txt create mode 100644 test/txtfiles/prompt_print_1_t.txt create mode 100644 test/txtfiles/prompt_print_1_tln.txt create mode 100644 test/txtfiles/prompt_print_2.txt create mode 100644 test/txtfiles/prompt_print_2_ln.txt create mode 100644 test/txtfiles/prompt_print_2_t.txt create mode 100644 test/txtfiles/prompt_print_2_tln.txt create mode 100644 test/txtfiles/prompt_print_3.txt create mode 100644 test/txtfiles/prompt_print_3_ln.txt create mode 100644 test/txtfiles/prompt_print_3_t.txt create mode 100644 test/txtfiles/prompt_print_3_tln.txt create mode 100644 test/txtfiles/prompt_print_4.txt create mode 100644 test/txtfiles/prompt_print_4_ln.txt create mode 100644 test/txtfiles/prompt_print_4_t.txt create mode 100644 test/txtfiles/prompt_print_4_tln.txt create mode 100644 test/txtfiles/prompt_print_5.txt create mode 100644 test/txtfiles/prompt_print_5_ln.txt create mode 100644 test/txtfiles/prompt_print_5_t.txt create mode 100644 test/txtfiles/prompt_print_5_tln.txt create mode 100644 test/txtfiles/prompt_print_6.txt create mode 100644 test/txtfiles/prompt_print_6_ln.txt create mode 100644 test/txtfiles/prompt_print_6_t.txt create mode 100644 test/txtfiles/prompt_print_6_tln.txt create mode 100644 test/txtfiles/prompt_print_7.txt create mode 100644 test/txtfiles/prompt_print_7_ln.txt create mode 100644 test/txtfiles/prompt_print_7_t.txt create mode 100644 test/txtfiles/prompt_print_7_tln.txt diff --git a/Project.toml b/Project.toml index 3f8b30b35..0e602a346 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Term" uuid = "22787eb5-b846-44ae-b979-8e399b8463ab" authors = ["FedeClaudi and contributors"] -version = "1.1.0" +version = "1.2.0" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" @@ -13,14 +13,12 @@ Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" MyterialColors = "1c23619d-4212-4747-83aa-717207fae70f" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" ProgressLogging = "33c8b6b6-d38a-422a-b730-caa89a2f386c" SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" UnicodeFun = "1cfade01-22cf-5700-b092-accc4b62d6e1" -WordTokenizers = "796a5d58-b03d-544a-977e-18100b691f6e" [compat] CodeTracking = "1" @@ -32,7 +30,6 @@ ProgressLogging = "0.1" SnoopPrecompile = "1" Tables = "1" UnicodeFun = "0.4" -WordTokenizers = "0.5" julia = "1.6" [extras] diff --git a/docs/make.jl b/docs/make.jl index 347fdfc52..acd4d4451 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -23,6 +23,7 @@ makedocs(; "basics/renderables.md", "basics/tprint.md", "basics/console.md", + "basics/prompt.md", ], "Renderables" => Any[ "ren/intro.md", diff --git a/docs/src/adv/errors_tracebacks.md b/docs/src/adv/errors_tracebacks.md index 0c3346db9..273b0786c 100644 --- a/docs/src/adv/errors_tracebacks.md +++ b/docs/src/adv/errors_tracebacks.md @@ -44,8 +44,8 @@ install_term_stacktrace( but you can also do more, if you just want to quickly change some options (e.g. to deal with a particularly though bug). You can set flags to change the behavior on the fly: ```@example -import Term: STACKTRACE_HIDE_MODULES, STACKTRACE_HIDE_FRAME +import Term: STACKTRACE_HIDE_MODULES, STACKTRACE_HIDE_FRAMES STACKTRACE_HIDE_MODULES[] = ["REPL", "OhMyREPL"] # list names of modules you want ignored in the stacktrace -STACKTRACE_HIDE_FRAME[] = false # set to true to hide frame, false to show all of them +STACKTRACE_HIDE_FRAMES[] = false # set to true to hide frame, false to show all of them ``` \ No newline at end of file diff --git a/docs/src/api/api_prompt.md b/docs/src/api/api_prompt.md new file mode 100644 index 000000000..dd2461877 --- /dev/null +++ b/docs/src/api/api_prompt.md @@ -0,0 +1,14 @@ +# Prompts +```@meta +CurrentModule = Term.Prompts +``` + + +```@index +Pages = ["api_prompt.md"] +``` + + +```@autodocs +Modules = [Prompts] +``` \ No newline at end of file diff --git a/docs/src/basics/prompt.md b/docs/src/basics/prompt.md new file mode 100644 index 000000000..77f70b31f --- /dev/null +++ b/docs/src/basics/prompt.md @@ -0,0 +1,55 @@ +# Prompt + +Time for a little example of a simple thing you can use `Term.jl` for: asking for some input. Use a `Prompt`, ask a question, get an answer. Simple, but a little extra style. That's what `AbstractPrompt` types are for. There's a few different flavors, which we'll look at in due time, but essentially a prompt is made of a bit of text, the `prompt` that is displayed to the user, and some machinery to capture the user's input and parse it/ validate it. + +For example: + +```@example prompt +using Term.Prompts + +Prompt("Simple, right?") |> ask +``` + +here we construct a basic `Prompt` and "ask" it: print the message and capture the reply. `ask` returns the answer, which you can do with as you please. A small warning before we carry on: + +!!! warning "Using VSCode" + As you can see [here](https://discourse.julialang.org/t/vscode-errors-with-user-input-readline/75097/4?u=fedeclaudi), `readlines`, which `AbstractPrompts` use to get the user's input, is a bit troublesome in VSCode. In VSCode, after the prompt gets printed you need to enter "space" (hit the space bar) and then enter and **after** that you can enter your actual replies. + +## Prompt flavours +There's a couple more ways you can use prompts like. One is to ensure you get an answer of the correct `Type`, using the immaginatively names `TypePrompt`: + +```@example prompt + +# this only accepts `Int`s +TypePrompt(Int, "give me a number") |> ask +``` + +If your answer can't be converted to the correct type you'll get a `AnswerValidationError`, not good. + + +So, what if you want to get user inputs, but you don't want to handle any crazy input they can provide? Fear not, use `OptionsPrompt` so that only acceptable options will be ok. This will keep "asking" your prompt until the user's answer matches one of the given options + +```@example prompt +OptionsPrompt(["a lot", "so much", "the most"], "How likely would you be to recomend Term.jl to a friend or colleague?") |> ask +``` + +Okay, so much typing though. Let's be realistic, most likely you just want to ask a yes/no question and the answer is likely just yes. So just use a `DefaultPrompt`: + +```@example prompt + +# one says the first option is the default +DefaultPropt(["yes", "no"], 1, "Confirm?") |> ask +``` + +still too much typing? Ask the user to `confirm`: + +```@example prompt +confirm() +``` + +## Style +The style of prompt elements (e.g. the color of the prompt's text or of the options) is defined in `Theme`. You can also pass style information during prompt creation: + +```@example prompt +Promt("Do you like this color?", "red") |> println +DefaultPropt(["yes", "no"], 1, "Confirm?", "green", "blue", "red") \ No newline at end of file diff --git a/src/Term.jl b/src/Term.jl index 8a807d737..192916b3e 100644 --- a/src/Term.jl +++ b/src/Term.jl @@ -33,7 +33,7 @@ module Term using Unicode const STACKTRACE_HIDDEN_MODULES = Ref(String[]) -const STACKTRACE_HIDE_FRAME = Ref(true) +const STACKTRACE_HIDE_FRAMES = Ref(true) const DEBUG_ON = Ref(false) @@ -86,6 +86,7 @@ include("repr.jl") include("compositors.jl") include("grid.jl") include("introspection.jl") +include("prompt.jl") export RenderableText, Panel, TextBox, @nested_panels export TERM_THEME, highlight @@ -176,6 +177,12 @@ using .Repr: @with_repr, termshow, install_term_repr, @showme using .Grid +using .Prompts + +# ---------------------------------------------------------------------------- # +# PRE COMPILATION # +# ---------------------------------------------------------------------------- # + using SnoopPrecompile @precompile_setup begin diff --git a/src/_errors.jl b/src/_errors.jl index 286e1cc80..ee0585418 100644 --- a/src/_errors.jl +++ b/src/_errors.jl @@ -9,7 +9,8 @@ import Term: read_file_lines function get_frame_file(frame::StackFrame) file = string(frame.file) file = Base.fixup_stdlib_path(file) - Base.stacktrace_expand_basepaths() && (file = something(Base.find_source_file(file), file)) + Base.stacktrace_expand_basepaths() && + (file = something(Base.find_source_file(file), file)) Base.stacktrace_contract_userdir() && (file = Base.contractuser(file)) return if isnothing(file) @@ -127,7 +128,6 @@ function get_frame_function_name(frame::StackFrame, ctx::StacktraceContext) (func = parse_kw_func_name(frame)) catch end - # format function name func = replace( @@ -137,11 +137,15 @@ function get_frame_function_name(frame::StackFrame, ctx::StacktraceContext) ), ) - func = highlight(func) |> apply_style - func = replace(func, RECURSIVE_OPEN_TAG_REGEX => "") + try + func = replace(func, RECURSIVE_OPEN_TAG_REGEX => "") + catch + end # reshape but taking care of potential curly bracktes + func = highlight(func) |> apply_style func = reshape_text(func, ctx.func_name_w; ignore_markup = true) + return RenderableText(func) end @@ -385,7 +389,7 @@ function render_backtrace( to_skip = should_skip(frame, hide_frames, curr_module) && num ∉ [1, length(bt)] && - STACKTRACE_HIDE_FRAME[] + STACKTRACE_HIDE_FRAMES[] # keep track of frames being skipped if num ∉ [1, length(bt)] @@ -403,7 +407,7 @@ function render_backtrace( else # show number of frames skipped - if (to_skip == false || num == length(bt) - 1) && n_skipped > 0 + if to_skip == false && n_skipped > 0 add_number_frames_skipped!( content, ctx, @@ -426,6 +430,17 @@ function render_backtrace( end end else + if num == length(bt) && n_skipped > 0 + add_number_frames_skipped!( + content, + ctx, + to_skip, + num, + bt, + n_skipped, + skipped_frames_modules, + ) + end tot_frames_added += 1 end diff --git a/src/_repr.jl b/src/_repr.jl index e54ae9e50..eb59eee41 100644 --- a/src/_repr.jl +++ b/src/_repr.jl @@ -182,7 +182,11 @@ function style_function_methods(fun, methods::String; max_n = 11, width = defaul ) end methods_contents = if N > 1 - methods_texts = RenderableText.(escape_brackets.(apply_style.(highlight.(_methods))); width = width - 20) + methods_texts = + RenderableText.( + escape_brackets.(apply_style.(highlight.(_methods))); + width = width - 20, + ) join(string.(map(i -> counts[i] * methods_texts[i], 1:length(counts))), '\n') else fun |> methods |> string |> split_lines |> first diff --git a/src/errors.jl b/src/errors.jl index 0d4de3569..4310715e1 100644 --- a/src/errors.jl +++ b/src/errors.jl @@ -18,7 +18,7 @@ import Term: do_by_line, RECURSIVE_OPEN_TAG_REGEX, STACKTRACE_HIDDEN_MODULES, - STACKTRACE_HIDE_FRAME + STACKTRACE_HIDE_FRAMES import ..Links: Link import ..Style: apply_style @@ -97,8 +97,9 @@ function install_term_stacktrace(; hide_frames = true, ) @eval begin - function Base.showerror(io::IO, er, bt::Vector; backtrace = true) + function Base.showerror(io::IO, er, bt; backtrace = true) print("\n") + # @info "Showing" er bt # shorten very long backtraces isa(er, StackOverflowError) && (bt = [bt[1:25]..., bt[(end - 25):end]...]) @@ -139,10 +140,14 @@ function install_term_stacktrace(; end # print message panel if VSCode is not handling that through a second call to this fn - isa(io.io, Base.TTY) && + isa(io.io, Base.TTY) && begin + msg = highlight(error_message(er)) |> apply_style + msg = replace(msg, RECURSIVE_OPEN_TAG_REGEX => "") + msg = reshape_text(msg, ctx.module_line_w; ignore_markup = true) + Panel( RenderableText( - highlight(error_message(er)); + escape_brackets(apply_style(highlight(error_message(er)))); width = ctx.module_line_w, ); width = ctx.out_w, @@ -152,17 +157,18 @@ function install_term_stacktrace(; title_justify = :center, fit = false, ) |> print + end catch cought_err # catch when something goes wrong during error handling in Term @error "Term.jl: error while rendering error message: " cought_err - + for (i, (exc, _bt)) in enumerate(current_exceptions()) i == 1 && println("Error during term's stacktrace generation:") Base.show_backtrace(io, _bt) print(io, '\n'^3) Base.showerror(io, exc) end - + print(io, '\n'^5) println(io, "Original error:") Base.show_backtrace(io, bt) diff --git a/src/prompt.jl b/src/prompt.jl index 768f395de..3fcc04469 100644 --- a/src/prompt.jl +++ b/src/prompt.jl @@ -1,32 +1,296 @@ -using Term +""" + module Prompts +Defines functionality relative to prompts in the terminal. +Typically a prompt is composed of a piece of text that gets displayed prompting +the user to provide an input and some machinery to parse/validate the user's inputs. +For example, some prompts may only accept as replies objects of a given type (e.g. an `Int`). +Additionally, some prompts will have "options" the user can choose between and the answer +has to be one of these options. +""" +module Prompts + +import Term +import Term: highlight, TERM_THEME +import ..Style: apply_style +import ..Tprint: tprint, tprintln +import ..Repr: @with_repr, termshow + +export Prompt, TypePrompt, OptionsPrompt, DefaultPropt, confirm, ask + +""" +Prompts in VSCODE require a bit of a hack: +https://discourse.julialang.org/t/vscode-errors-with-user-input-readline/75097/4?u=fedeclaudi + +When the text is displayed, the user should input "space" and a new line before inputting the +actual reponse. This is not a Term.jl problem. +""" + +# ------------------------------ abstract prompt ----------------------------- # + +""" Prompt types """ abstract type AbstractPrompt end -function validate_default(default::String, options::Vector{String}) - default ∉ options && error("Default is not a valid option: $default, $(options).") +_print_prompt_text(io::IO, prompt::AbstractPrompt) = + tprintln(io, "{$(prompt.style)}{dim}❯❯❯ {/dim}" * prompt.prompt * "{/$(prompt.style)}") + +""" + Base.print(io::IO, prompt::AbstractPrompt) + +Default prompt printing, just prints the message `prompt` +with a bit of style. +""" +Base.print(io::IO, prompt::AbstractPrompt) = _print_prompt_text(io, prompt) + +Base.println(io::IO, prompt::AbstractPrompt) = print(io, prompt, "\n") +tprint(io::IO, prompt::AbstractPrompt) = print(io, prompt) +tprintln(io::IO, prompt::AbstractPrompt) = println(io, prompt) + +""" + ask + +Ask does three things: + 1. displays a prompt + 2. accepts user input and validates it + 3. if the answer was accepted, returns the desired value. +""" +function ask end + +""" + ask(io::IO, prompt::AbstractPrompt) + +Default `ask` method for generic prompt objects. +""" +function ask(io::IO, prompt::AbstractPrompt) + print(io, prompt) + ans = readline() + return validate_answer(ans, prompt) end -function ask(p::AbstractPrompt) - _options = join( - map(o -> o == p.default ? "{bold underline}$o{/bold underline}" : o, p.options), - ", ", - ) +ask(prompt::AbstractPrompt) = ask(stdout, prompt) + +""" + validate_answer + +Validate user's answer for a prompt type. +The validation mechanism depends on the type of prompt. +Validate answer will return the answer if it passed validation +or raise and error otherwise. +""" +function validate_answer end + +# -------------------------- answer validation error ------------------------- # + +""" + AnswerValidationError <: Exception + +Exception to handle cases in which the user's answer to a +prompt failed to pass validation. +""" +struct AnswerValidationError <: Exception + answer_type + expected_type + err +end + +Term.Errors.error_message(e::AnswerValidationError) = + highlight( + "TypePrompt expected an answer of type: `$(e.expected_type)`, got `$(e.answer_type)` instead\nConversion to `$(e.expected_type)` failed because of: $(e.err)", + ) |> apply_style + +# ---------------------------------------------------------------------------- # +# PROMPT # +# ---------------------------------------------------------------------------- # + +""" + struct Prompt{T} <: AbstractPrompt + prompt::String + style::String = TERM_THEME[].prompt_text + end + +Generic prompt, accepts any answer +""" +@with_repr struct Prompt <: AbstractPrompt + prompt::String + style::String +end +Prompt(prompt::String) = Prompt(prompt, TERM_THEME[].prompt_text) + +validate_answer(ans, ::Prompt) = ans + +# ---------------------------------------------------------------------------- # +# TYPE PROMPT # +# ---------------------------------------------------------------------------- # + +""" + struct TypePrompt{T} + answer_type::Union{Union, DataType} = T + prompt::String + end + +Asks for input given `prompt` and checks/converts the answer to type `T` +""" +struct TypePrompt{T} <: AbstractPrompt + answer_type::T + prompt::String + style::String +end + +TypePrompt(answer_type, prompt::String) = + TypePrompt(answer_type, prompt, TERM_THEME[].prompt_text) + +""" + validate_answer(answer, prompt::TypePrompt) - tprint("$(p.text)? $(_options)\n\n") - reply = readline() +For a TypePrompt an anwer is valid if it is of the correct type +or if a string containg the answer can be parsed as the correct type. +For example, `answer="1.0"` can be accepted for a TypePrompt +asking for a `Number`. +If validation fails, an error is raised. +""" +function validate_answer(answer, prompt::TypePrompt) + answer isa prompt.answer_type && return answer + + err = nothing + try + return parse(prompt.answer_type, answer) + catch err + end + throw( + AnswerValidationError(typeof(answer), prompt.answer_type, apply_style(string(err))), + ) end -struct YNPrompt <: AbstractPrompt - text::String +# ---------------------------------------------------------------------------- # +# OPTIONS PROMPTS # +# ---------------------------------------------------------------------------- # +""" Prompt types where user can only choose among options """ +abstract type AbstractOptionsPrompt <: AbstractPrompt end + +""" + struct OptionsPrompt <: AbstractOptionsPrompt + options::Vector{String} + prompt::String + style::String + answers_style::String + end + +Just a simple prompt, giving some pre-defined options. +""" +@with_repr struct OptionsPrompt <: AbstractOptionsPrompt options::Vector{String} - default::String + prompt::String + style::String + answers_style::String +end + +OptionsPrompt(options, prompt::String) = + OptionsPrompt(options, prompt, TERM_THEME[].prompt_text, TERM_THEME[].prompt_options) + +""" + Base.print(io::IO, prompt::AbstractOptionsPrompt) - function YNPrompt(text::String, options::Vector{String}, default::String) - validate_default(default, options) - @assert length(options) >= 2 "Need at least two options for a prompt" - return new(text, options, default) +Options prompts additionally print the available options. +""" +function Base.print(io::IO, prompt::AbstractOptionsPrompt) + _print_prompt_text(io, prompt) + tprint( + io, + " {$(prompt.answers_style)}" * + join(prompt.options, " {$(prompt.style)}/{/$(prompt.style)} ") * + "{/$(prompt.answers_style)}"; + highlight = false, + ) +end + +""" + validate_answer(answer, prompt::AbstractOptionsPrompt) + +For an AbstractOptionsPrompt an answer is accepted if its one of the options. +Additionally, for an `AbstractDefaultPrompt`, if no answer is given that's +also accepted and the default option is returned. +""" +function validate_answer(answer, prompt::AbstractOptionsPrompt) + (prompt isa AbstractDefaultPrompt && strip(answer) == "") && + return prompt.options[prompt.default] + strip(answer) ∉ prompt.options && begin + tprintln("{dim}Answer `$(answer)` is not valid.{/dim}") + return nothing end + return answer end -confirm = YNPrompt("Confirm", ["Yes", "No"], "Yes") -ask(confirm) +""" + ask(io::IO, prompt::AbstractOptionsPrompt) + +In asking an `AbstractOptionsPrompt`, keep asking for input +until an accepted answer is provided. +""" +function ask(io::IO, prompt::AbstractOptionsPrompt) + ans = nothing + while isnothing(ans) + println(io, prompt) + ans = validate_answer(readline(), prompt) + end + return ans +end + +# ---------------------------------------------------------------------------- # +# DEFAULT PROMPT # +# ---------------------------------------------------------------------------- # + +""" Options prompt types with a default answer """ +abstract type AbstractDefaultPrompt <: AbstractOptionsPrompt end + +""" + +""" +@with_repr struct DefaultPropt <: AbstractDefaultPrompt + options::Vector{String} + default::Int + prompt::String + style::String + answers_style::String + default_answer_style::String +end + +function DefaultPropt(options::Vector, default::Int, prompt::String, args...) + @assert default > 0 && default < length(options) "Default answer number: $default not valid" + DefaultPropt(options, default, prompt, args...) +end + +function DefaultPropt(options::Vector, default::Int, prompt::String) + DefaultPropt( + options, + default, + prompt, + TERM_THEME[].prompt_text, + TERM_THEME[].prompt_options, + TERM_THEME[].prompt_default_option, + ) +end + +""" + Base.print(io::IO, prompt::AbstractDefaultPrompt) + +Print a prompt with style applied to the default option. +""" +function Base.print(io::IO, prompt::AbstractDefaultPrompt) + n_options = length(prompt.options) + _print_prompt_text(io, prompt) + answer_styles = map( + i -> i == prompt.default ? prompt.default_answer_style : prompt.answers_style, + 1:n_options, + ) + options = join( + (map( + i -> "{$(answer_styles[i])}$(prompt.options[i]){/$(answer_styles[i])}", + 1:n_options, + )), + ", ", + ) + tprint(io, " " * options) +end + +confirm() = ask(DefaultPropt(["yes", "no"], 1, "Confirm?")) +end diff --git a/src/theme.jl b/src/theme.jl index 1bfe14f40..1998686b7 100644 --- a/src/theme.jl +++ b/src/theme.jl @@ -108,6 +108,11 @@ style outputs to terminal. tb_columns::String = "defualt" tb_footer::String = "default" tb_box::Symbol = :MINIMAL_HEAVY_HEAD + + # prompt + prompt_text::String = blue + prompt_default_option::String = "underline bold $green" + prompt_options::String = "default" end DarkTheme = Theme(name = "dark") diff --git a/src/tprint.jl b/src/tprint.jl index 2a904b32b..6698678fe 100644 --- a/src/tprint.jl +++ b/src/tprint.jl @@ -54,16 +54,14 @@ Equivalent to `print(x)` function tprint(io::IO, x::AbstractRenderable; highlight = true) w = console_width() x = x.measure.w > console_width() ? trim_renderable(x, w) : x - print(io, x; highlight = highlight) + print(io, x) end -function tprint(io::IO, args...) +function tprint(io::IO, args...; highlight = true) for (n, arg) in enumerate(args) - tprint(io, arg) + tprint(io, arg; highlight = highlight) - if n < length(args) - args[n + 1] isa AbstractRenderable || print(io, " ") - end + (n < length(args) && args[n + 1] isa AbstractRenderable) || print(io, " ") end return nothing end @@ -88,5 +86,7 @@ styling functionality. """ tprintln(args...; highlight = true) = tprint(args..., "\n"; highlight = highlight) +tprintln(io::IO, args...; highlight = true) = + tprint(io, args..., "\n"; highlight = highlight) end diff --git a/test/24_prompts.jl b/test/24_prompts.jl new file mode 100644 index 000000000..bbfdbe118 --- /dev/null +++ b/test/24_prompts.jl @@ -0,0 +1,51 @@ +using Term.Prompts +import Term.Prompts: validate_answer, AnswerValidationError + +@testset "Prompt, Creation" begin + basic = Prompt("basic prompt?") + basic_with_color = Prompt("basic prompt?", "red") + + type_prompt = TypePrompt(Int, "Gimme a number") + type_prompt_style = TypePrompt(Int, "Gimme a number", "bold red") + + opts = OptionsPrompt(["one", "two"], "What option?") + opts_style = OptionsPrompt(["one", "two"], "What option?", "red", "green") + + default = DefaultPropt(["yes", "no"], 1, "asking", "red", "green", "blue") + default = DefaultPropt(["yes", "no"], 1, "asking") +end + +basic = Prompt("basic prompt?") +basic_with_color = Prompt("basic prompt?", "red") + +type_prompt = TypePrompt(Int, "Gimme a number") +type_prompt_style = TypePrompt(Int, "Gimme a number", "bold red") + +opts = OptionsPrompt(["one", "two"], "What option?") +opts_style = OptionsPrompt(["one", "two"], "What option?", "red", "green") + +default = DefaultPropt(["yes", "no"], 1, "asking", "red", "green", "blue") + +all_prompts = + (basic, basic_with_color, type_prompt, type_prompt_style, opts, opts_style, default) + +IS_WIN || @testset "Prompt, printing" begin + for (i, p) in enumerate(all_prompts) + @compare_to_string(sprint(print, p), "prompt_print_$i") + @compare_to_string(sprint(println, p), "prompt_print_$(i)_ln") + @compare_to_string(sprint(tprintln, p), "prompt_print_$(i)_t") + @compare_to_string(sprint(tprintln, p), "prompt_print_$(i)_tln") + end +end + +@testset "Prompt, validation" begin + @test validate_answer("test", basic) == "test" + + @test_throws AnswerValidationError validate_answer("sdadas", type_prompt) + @test validate_answer("1", type_prompt) == 1 + + @test isnothing(validate_answer("asdada", opts)) + @test validate_answer("one", opts) == "one" + + @test validate_answer("", default) == "yes" +end diff --git a/test/runtests.jl b/test/runtests.jl index 3432514dd..339886875 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -56,6 +56,7 @@ end @runner "21_test_markdown.jl" @runner "22_test_grid.jl" @runner "23_test_link.jl" +@runner "24_prompts.jl" @runner "98_test_examples.jl" @runner "99_test_errors.jl" diff --git a/test/txtfiles/prompt_print_1.txt b/test/txtfiles/prompt_print_1.txt new file mode 100644 index 000000000..ae7a1fdbd --- /dev/null +++ b/test/txtfiles/prompt_print_1.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + \ No newline at end of file diff --git a/test/txtfiles/prompt_print_1_ln.txt b/test/txtfiles/prompt_print_1_ln.txt new file mode 100644 index 000000000..3099ba19c --- /dev/null +++ b/test/txtfiles/prompt_print_1_ln.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + diff --git a/test/txtfiles/prompt_print_1_t.txt b/test/txtfiles/prompt_print_1_t.txt new file mode 100644 index 000000000..3099ba19c --- /dev/null +++ b/test/txtfiles/prompt_print_1_t.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + diff --git a/test/txtfiles/prompt_print_1_tln.txt b/test/txtfiles/prompt_print_1_tln.txt new file mode 100644 index 000000000..3099ba19c --- /dev/null +++ b/test/txtfiles/prompt_print_1_tln.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + diff --git a/test/txtfiles/prompt_print_2.txt b/test/txtfiles/prompt_print_2.txt new file mode 100644 index 000000000..9b1088759 --- /dev/null +++ b/test/txtfiles/prompt_print_2.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + \ No newline at end of file diff --git a/test/txtfiles/prompt_print_2_ln.txt b/test/txtfiles/prompt_print_2_ln.txt new file mode 100644 index 000000000..b30cc5922 --- /dev/null +++ b/test/txtfiles/prompt_print_2_ln.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + diff --git a/test/txtfiles/prompt_print_2_t.txt b/test/txtfiles/prompt_print_2_t.txt new file mode 100644 index 000000000..b30cc5922 --- /dev/null +++ b/test/txtfiles/prompt_print_2_t.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + diff --git a/test/txtfiles/prompt_print_2_tln.txt b/test/txtfiles/prompt_print_2_tln.txt new file mode 100644 index 000000000..b30cc5922 --- /dev/null +++ b/test/txtfiles/prompt_print_2_tln.txt @@ -0,0 +1,2 @@ +❯❯❯ basic prompt? + diff --git a/test/txtfiles/prompt_print_3.txt b/test/txtfiles/prompt_print_3.txt new file mode 100644 index 000000000..4d88a0004 --- /dev/null +++ b/test/txtfiles/prompt_print_3.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + \ No newline at end of file diff --git a/test/txtfiles/prompt_print_3_ln.txt b/test/txtfiles/prompt_print_3_ln.txt new file mode 100644 index 000000000..544a44577 --- /dev/null +++ b/test/txtfiles/prompt_print_3_ln.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + diff --git a/test/txtfiles/prompt_print_3_t.txt b/test/txtfiles/prompt_print_3_t.txt new file mode 100644 index 000000000..544a44577 --- /dev/null +++ b/test/txtfiles/prompt_print_3_t.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + diff --git a/test/txtfiles/prompt_print_3_tln.txt b/test/txtfiles/prompt_print_3_tln.txt new file mode 100644 index 000000000..544a44577 --- /dev/null +++ b/test/txtfiles/prompt_print_3_tln.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + diff --git a/test/txtfiles/prompt_print_4.txt b/test/txtfiles/prompt_print_4.txt new file mode 100644 index 000000000..4cefa93d6 --- /dev/null +++ b/test/txtfiles/prompt_print_4.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + \ No newline at end of file diff --git a/test/txtfiles/prompt_print_4_ln.txt b/test/txtfiles/prompt_print_4_ln.txt new file mode 100644 index 000000000..47385c4af --- /dev/null +++ b/test/txtfiles/prompt_print_4_ln.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + diff --git a/test/txtfiles/prompt_print_4_t.txt b/test/txtfiles/prompt_print_4_t.txt new file mode 100644 index 000000000..47385c4af --- /dev/null +++ b/test/txtfiles/prompt_print_4_t.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + diff --git a/test/txtfiles/prompt_print_4_tln.txt b/test/txtfiles/prompt_print_4_tln.txt new file mode 100644 index 000000000..47385c4af --- /dev/null +++ b/test/txtfiles/prompt_print_4_tln.txt @@ -0,0 +1,2 @@ +❯❯❯ Gimme a number + diff --git a/test/txtfiles/prompt_print_5.txt b/test/txtfiles/prompt_print_5.txt new file mode 100644 index 000000000..52f3a9647 --- /dev/null +++ b/test/txtfiles/prompt_print_5.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two \ No newline at end of file diff --git a/test/txtfiles/prompt_print_5_ln.txt b/test/txtfiles/prompt_print_5_ln.txt new file mode 100644 index 000000000..dd48eb985 --- /dev/null +++ b/test/txtfiles/prompt_print_5_ln.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two diff --git a/test/txtfiles/prompt_print_5_t.txt b/test/txtfiles/prompt_print_5_t.txt new file mode 100644 index 000000000..dd48eb985 --- /dev/null +++ b/test/txtfiles/prompt_print_5_t.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two diff --git a/test/txtfiles/prompt_print_5_tln.txt b/test/txtfiles/prompt_print_5_tln.txt new file mode 100644 index 000000000..dd48eb985 --- /dev/null +++ b/test/txtfiles/prompt_print_5_tln.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two diff --git a/test/txtfiles/prompt_print_6.txt b/test/txtfiles/prompt_print_6.txt new file mode 100644 index 000000000..7356b5db8 --- /dev/null +++ b/test/txtfiles/prompt_print_6.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two \ No newline at end of file diff --git a/test/txtfiles/prompt_print_6_ln.txt b/test/txtfiles/prompt_print_6_ln.txt new file mode 100644 index 000000000..01fe7526c --- /dev/null +++ b/test/txtfiles/prompt_print_6_ln.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two diff --git a/test/txtfiles/prompt_print_6_t.txt b/test/txtfiles/prompt_print_6_t.txt new file mode 100644 index 000000000..01fe7526c --- /dev/null +++ b/test/txtfiles/prompt_print_6_t.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two diff --git a/test/txtfiles/prompt_print_6_tln.txt b/test/txtfiles/prompt_print_6_tln.txt new file mode 100644 index 000000000..01fe7526c --- /dev/null +++ b/test/txtfiles/prompt_print_6_tln.txt @@ -0,0 +1,2 @@ +❯❯❯ What option? + one / two diff --git a/test/txtfiles/prompt_print_7.txt b/test/txtfiles/prompt_print_7.txt new file mode 100644 index 000000000..39d5a05d9 --- /dev/null +++ b/test/txtfiles/prompt_print_7.txt @@ -0,0 +1,2 @@ +❯❯❯ asking + yes, no \ No newline at end of file diff --git a/test/txtfiles/prompt_print_7_ln.txt b/test/txtfiles/prompt_print_7_ln.txt new file mode 100644 index 000000000..c34fab75e --- /dev/null +++ b/test/txtfiles/prompt_print_7_ln.txt @@ -0,0 +1,2 @@ +❯❯❯ asking + yes, no diff --git a/test/txtfiles/prompt_print_7_t.txt b/test/txtfiles/prompt_print_7_t.txt new file mode 100644 index 000000000..c34fab75e --- /dev/null +++ b/test/txtfiles/prompt_print_7_t.txt @@ -0,0 +1,2 @@ +❯❯❯ asking + yes, no diff --git a/test/txtfiles/prompt_print_7_tln.txt b/test/txtfiles/prompt_print_7_tln.txt new file mode 100644 index 000000000..c34fab75e --- /dev/null +++ b/test/txtfiles/prompt_print_7_tln.txt @@ -0,0 +1,2 @@ +❯❯❯ asking + yes, no