aboutsummaryrefslogtreecommitdiff
path: root/aoc2023/build/packages/glint/src/glint.gleam
diff options
context:
space:
mode:
Diffstat (limited to 'aoc2023/build/packages/glint/src/glint.gleam')
-rw-r--r--aoc2023/build/packages/glint/src/glint.gleam588
1 files changed, 588 insertions, 0 deletions
diff --git a/aoc2023/build/packages/glint/src/glint.gleam b/aoc2023/build/packages/glint/src/glint.gleam
new file mode 100644
index 0000000..b159016
--- /dev/null
+++ b/aoc2023/build/packages/glint/src/glint.gleam
@@ -0,0 +1,588 @@
+import gleam/map.{type Map}
+import gleam/option.{type Option, None, Some}
+import gleam/list
+import gleam/io
+import gleam/string
+import snag.{type Result}
+import glint/flag.{type Flag, type Map as FlagMap}
+import gleam/string_builder as sb
+import gleam_community/ansi
+import gleam_community/colour.{type Colour}
+import gleam/result
+import gleam/function
+
+// --- CONFIGURATION ---
+
+// -- CONFIGURATION: TYPES --
+
+/// Config for glint
+///
+pub type Config {
+ Config(pretty_help: Option(PrettyHelp), name: Option(String))
+}
+
+/// PrettyHelp defines the header colours to be used when styling help text
+///
+pub type PrettyHelp {
+ PrettyHelp(usage: Colour, flags: Colour, subcommands: Colour)
+}
+
+// -- CONFIGURATION: CONSTANTS --
+
+/// Default config
+///
+pub const default_config = Config(pretty_help: None, name: None)
+
+// -- CONFIGURATION: FUNCTIONS --
+
+/// Add the provided config to the existing command tree
+///
+pub fn with_config(glint: Glint(a), config: Config) -> Glint(a) {
+ Glint(..glint, config: config)
+}
+
+/// Enable custom colours for help text headers
+/// For a pre-made colouring use `default_pretty_help()`
+///
+pub fn with_pretty_help(glint: Glint(a), pretty: PrettyHelp) -> Glint(a) {
+ Config(..glint.config, pretty_help: Some(pretty))
+ |> with_config(glint, _)
+}
+
+/// Disable custom colours for help text headers
+///
+pub fn without_pretty_help(glint: Glint(a)) -> Glint(a) {
+ Config(..glint.config, pretty_help: None)
+ |> with_config(glint, _)
+}
+
+pub fn with_name(glint: Glint(a), name: String) -> Glint(a) {
+ Config(..glint.config, name: Some(name))
+ |> with_config(glint, _)
+}
+
+// --- CORE ---
+
+// -- CORE: TYPES --
+
+/// Glint container type for config and commands
+///
+pub opaque type Glint(a) {
+ Glint(config: Config, cmd: CommandNode(a), global_flags: FlagMap)
+}
+
+/// CommandNode contents
+///
+pub opaque type Command(a) {
+ Command(do: Runner(a), flags: FlagMap, description: String)
+}
+
+/// Input type for `Runner`.
+///
+pub type CommandInput {
+ CommandInput(args: List(String), flags: FlagMap)
+}
+
+/// Function type to be run by `glint`.
+///
+pub type Runner(a) =
+ fn(CommandInput) -> a
+
+/// CommandNode tree representation.
+///
+type CommandNode(a) {
+ CommandNode(
+ contents: Option(Command(a)),
+ subcommands: Map(String, CommandNode(a)),
+ )
+}
+
+/// Ok type for command execution
+///
+pub type Out(a) {
+ /// Container for the command return value
+ Out(a)
+ /// Container for the generated help string
+ Help(String)
+}
+
+/// Result type for command execution
+///
+pub type CmdResult(a) =
+ Result(Out(a))
+
+// -- CORE: BUILDER FUNCTIONS --
+
+/// Creates a new command tree.
+///
+pub fn new() -> Glint(a) {
+ Glint(config: default_config, cmd: empty_command(), global_flags: map.new())
+}
+
+/// Adds a new command to be run at the specified path.
+///
+/// If the path is `[]`, the root command is set with the provided function and
+/// flags.
+///
+/// Note: all command paths are sanitized by stripping whitespace and removing any empty string elements.
+///
+pub fn add(
+ to glint: Glint(a),
+ at path: List(String),
+ do contents: Command(a),
+) -> Glint(a) {
+ Glint(
+ ..glint,
+ cmd: path
+ |> sanitize_path
+ |> do_add(to: glint.cmd, put: contents),
+ )
+}
+
+/// Recursive traversal of the command tree to find where to puth the provided command
+///
+fn do_add(
+ to root: CommandNode(a),
+ at path: List(String),
+ put contents: Command(a),
+) -> CommandNode(a) {
+ case path {
+ // update current command with provided contents
+ [] -> CommandNode(..root, contents: Some(contents))
+ // continue down the path, creating empty command nodes along the way
+ [x, ..xs] ->
+ CommandNode(
+ ..root,
+ subcommands: {
+ use node <- map.update(root.subcommands, x)
+ node
+ |> option.lazy_unwrap(empty_command)
+ |> do_add(xs, contents)
+ },
+ )
+ }
+}
+
+/// Helper for initializing empty commands
+///
+fn empty_command() -> CommandNode(a) {
+ CommandNode(contents: None, subcommands: map.new())
+}
+
+/// Trim each path element and remove any resulting empty strings.
+///
+fn sanitize_path(path: List(String)) -> List(String) {
+ path
+ |> list.map(string.trim)
+ |> list.filter(is_not_empty)
+}
+
+/// Create a Command(a) from a Runner(a)
+///
+pub fn command(do runner: Runner(a)) -> Command(a) {
+ Command(do: runner, flags: map.new(), description: "")
+}
+
+/// Attach a description to a Command(a)
+///
+pub fn description(cmd: Command(a), description: String) -> Command(a) {
+ Command(..cmd, description: description)
+}
+
+/// add a `flag.Flag` to a `Command`
+///
+pub fn flag(
+ cmd: Command(a),
+ at key: String,
+ of flag: flag.FlagBuilder(_),
+) -> Command(a) {
+ Command(..cmd, flags: map.insert(cmd.flags, key, flag.build(flag)))
+}
+
+/// Add a `flag.Flag to a `Command` when the flag name and builder are bundled as a #(String, flag.FlagBuilder(a)).
+///
+/// This is merely a convenience function and calls `glint.flag` under the hood.
+///
+pub fn flag_tuple(
+ cmd: Command(a),
+ with tup: #(String, flag.FlagBuilder(_)),
+) -> Command(a) {
+ flag(cmd, tup.0, tup.1)
+}
+
+/// Add multiple `Flag`s to a `Command`, note that this function uses `Flag` and not `FlagBuilder(_)`, so the user will need to call `flag.build` before providing the flags here.
+///
+/// It is recommended to call `glint.flag` instead.
+///
+pub fn flags(cmd: Command(a), with flags: List(#(String, Flag))) -> Command(a) {
+ use cmd, #(key, flag) <- list.fold(flags, cmd)
+ Command(..cmd, flags: map.insert(cmd.flags, key, flag))
+}
+
+/// Add global flags to the existing command tree
+///
+pub fn global_flag(
+ glint: Glint(a),
+ at key: String,
+ of flag: flag.FlagBuilder(_),
+) -> Glint(a) {
+ Glint(
+ ..glint,
+ global_flags: map.insert(glint.global_flags, key, flag.build(flag)),
+ )
+}
+
+/// Add global flags to the existing command tree.
+///
+pub fn global_flag_tuple(
+ glint: Glint(a),
+ with tup: #(String, flag.FlagBuilder(_)),
+) -> Glint(a) {
+ global_flag(glint, tup.0, tup.1)
+}
+
+/// Add global flags to the existing command tree.
+///
+/// Like `glint.flags`, this function requires `Flag`s insead of `FlagBuilder(_)`.
+///
+/// It is recommended to use `glint.global_flag` instead.
+///
+pub fn global_flags(glint: Glint(a), flags: List(#(String, Flag))) -> Glint(a) {
+ Glint(
+ ..glint,
+ global_flags: {
+ list.fold(
+ flags,
+ glint.global_flags,
+ fn(acc, tup) { map.insert(acc, tup.0, tup.1) },
+ )
+ },
+ )
+}
+
+// -- CORE: EXECUTION FUNCTIONS --
+
+/// Determines which command to run and executes it.
+///
+/// Sets any provided flags if necessary.
+///
+/// Each value prefixed with `--` is parsed as a flag.
+///
+/// This function does not print its output and is mainly intended for use within `glint` itself.
+/// If you would like to print or handle the output of a command please see the `run_and_handle` function.
+///
+pub fn execute(glint: Glint(a), args: List(String)) -> CmdResult(a) {
+ // create help flag to check for
+ let help_flag = help_flag()
+
+ // check if help flag is present
+ let #(help, args) = case list.pop(args, fn(s) { s == help_flag }) {
+ Ok(#(_, args)) -> #(True, args)
+ _ -> #(False, args)
+ }
+
+ // split flags out from the args list
+ let #(flags, args) = list.partition(args, string.starts_with(_, flag.prefix))
+
+ // search for command and execute
+ do_execute(glint.cmd, glint.config, glint.global_flags, args, flags, help, [])
+}
+
+/// Find which command to execute and run it with computed flags and args
+///
+fn do_execute(
+ cmd: CommandNode(a),
+ config: Config,
+ global_flags: FlagMap,
+ args: List(String),
+ flags: List(String),
+ help: Bool,
+ command_path: List(String),
+) -> CmdResult(a) {
+ case args {
+ // when there are no more available arguments
+ // and help flag has been passed, generate help message
+ [] if help ->
+ command_path
+ |> cmd_help(cmd, config, global_flags)
+ |> Help
+ |> Ok
+
+ // when there are no more available arguments
+ // run the current command
+ [] -> execute_root(cmd, global_flags, [], flags)
+
+ // when there are arguments remaining
+ // check if the next one is a subcommand of the current command
+ [arg, ..rest] ->
+ case map.get(cmd.subcommands, arg) {
+ // subcommand found, continue
+ Ok(cmd) ->
+ do_execute(
+ cmd,
+ config,
+ global_flags,
+ rest,
+ flags,
+ help,
+ [arg, ..command_path],
+ )
+ // subcommand not found, but help flag has been passed
+ // generate and return help message
+ _ if help ->
+ command_path
+ |> cmd_help(cmd, config, global_flags)
+ |> Help
+ |> Ok
+ // subcommand not found, but help flag has not been passed
+ // execute the current command
+ _ -> execute_root(cmd, global_flags, args, flags)
+ }
+ }
+}
+
+/// Executes the current root command.
+///
+fn execute_root(
+ cmd: CommandNode(a),
+ global_flags: FlagMap,
+ args: List(String),
+ flag_inputs: List(String),
+) -> CmdResult(a) {
+ case cmd.contents {
+ Some(contents) -> {
+ use new_flags <- result.try(list.try_fold(
+ over: flag_inputs,
+ from: map.merge(global_flags, contents.flags),
+ with: flag.update_flags,
+ ))
+ CommandInput(args, new_flags)
+ |> contents.do
+ |> Out
+ |> Ok
+ }
+ None -> snag.error("command not found")
+ }
+ |> snag.context("failed to run command")
+}
+
+/// A wrapper for `execute` that prints any errors enountered or the help text if requested.
+/// This function ignores any value returned by the command that was run.
+/// If you would like to do something with the command output please see the run_and_handle function.
+///
+pub fn run(from glint: Glint(a), for args: List(String)) -> Nil {
+ run_and_handle(from: glint, for: args, with: function.constant(Nil))
+}
+
+/// A wrapper for `execute` that prints any errors enountered or the help text if requested.
+/// This function calls the provided handler with the value returned by the command that was run.
+///
+pub fn run_and_handle(
+ from glint: Glint(a),
+ for args: List(String),
+ with handle: fn(a) -> _,
+) -> Nil {
+ case execute(glint, args) {
+ Error(err) ->
+ err
+ |> snag.pretty_print
+ |> io.println
+ Ok(Help(help)) -> io.println(help)
+ Ok(Out(out)) -> {
+ handle(out)
+ Nil
+ }
+ }
+}
+
+/// Default pretty help heading colouring
+/// mint (r: 182, g: 255, b: 234) colour for usage
+/// pink (r: 255, g: 175, b: 243) colour for flags
+/// buttercup (r: 252, g: 226, b: 174) colour for subcommands
+///
+pub fn default_pretty_help() -> PrettyHelp {
+ let assert Ok(usage_colour) = colour.from_rgb255(182, 255, 234)
+ let assert Ok(flags_colour) = colour.from_rgb255(255, 175, 243)
+ let assert Ok(subcommands_colour) = colour.from_rgb255(252, 226, 174)
+
+ PrettyHelp(
+ usage: usage_colour,
+ flags: flags_colour,
+ subcommands: subcommands_colour,
+ )
+}
+
+// constants for setting up sections of the help message
+const flags_heading = "FLAGS:"
+
+const subcommands_heading = "SUBCOMMANDS:"
+
+const usage_heading = "USAGE:"
+
+/// Helper for filtering out empty strings
+///
+fn is_not_empty(s: String) -> Bool {
+ s != ""
+}
+
+const help_flag_name = "help"
+
+const help_flag_message = "--help\t\t\tPrint help information"
+
+/// Function to create the help flag string
+/// Exported for testing purposes only
+///
+pub fn help_flag() -> String {
+ flag.prefix <> help_flag_name
+}
+
+// -- HELP: FUNCTIONS --
+
+fn wrap_with_space(s: String) -> String {
+ case s {
+ "" -> " "
+ _ -> " " <> s <> " "
+ }
+}
+
+/// generate the usage help string for a command
+fn usage_help(cmd_name: String, flags: FlagMap, config: Config) -> String {
+ let app_name = option.unwrap(config.name, "gleam run")
+ let flags =
+ flags
+ |> map.to_list
+ |> list.map(flag.flag_type_help)
+ |> list.sort(string.compare)
+
+ let flag_sb = case flags {
+ [] -> sb.new()
+ _ ->
+ flags
+ |> list.intersperse(" ")
+ |> sb.from_strings()
+ |> sb.prepend(prefix: " [ ")
+ |> sb.append(suffix: " ]")
+ }
+
+ [app_name, wrap_with_space(cmd_name), "[ ARGS ]"]
+ |> sb.from_strings
+ |> sb.append_builder(flag_sb)
+ |> sb.prepend(
+ config.pretty_help
+ |> option.map(fn(styling) { heading_style(usage_heading, styling.usage) })
+ |> option.unwrap(usage_heading) <> "\n\t",
+ )
+ |> sb.to_string
+}
+
+/// generate the help text for a command
+fn cmd_help(
+ path: List(String),
+ cmd: CommandNode(a),
+ config: Config,
+ global_flags: FlagMap,
+) -> String {
+ // recreate the path of the current command
+ // reverse the path because it is created by prepending each section as do_execute walks down the tree
+ let name =
+ path
+ |> list.reverse
+ |> string.join(" ")
+
+ let flags =
+ option.map(cmd.contents, fn(contents) { contents.flags })
+ |> option.lazy_unwrap(map.new)
+ |> map.merge(global_flags, _)
+
+ let flags_help_body =
+ config.pretty_help
+ |> option.map(fn(p) { heading_style(flags_heading, p.flags) })
+ |> option.unwrap(flags_heading) <> "\n\t" <> string.join(
+ list.sort([help_flag_message, ..flag.flags_help(flags)], string.compare),
+ "\n\t",
+ )
+
+ let usage = usage_help(name, flags, config)
+
+ let description =
+ cmd.contents
+ |> option.map(fn(contents) { contents.description })
+ |> option.unwrap("")
+
+ // create the header block from the name and description
+ let header_items =
+ [name, description]
+ |> list.filter(is_not_empty)
+ |> string.join("\n")
+
+ // create the subcommands help block
+ let subcommands = case subcommands_help(cmd.subcommands) {
+ "" -> ""
+ subcommands_help_body ->
+ config.pretty_help
+ |> option.map(fn(p) { heading_style(subcommands_heading, p.subcommands) })
+ |> option.unwrap(subcommands_heading) <> "\n\t" <> subcommands_help_body
+ }
+
+ // join the resulting help blocks into the final help message
+ [header_items, usage, flags_help_body, subcommands]
+ |> list.filter(is_not_empty)
+ |> string.join("\n\n")
+}
+
+// create the help text for subcommands
+fn subcommands_help(cmds: Map(String, CommandNode(a))) -> String {
+ cmds
+ |> map.map_values(subcommand_help)
+ |> map.values
+ |> list.sort(string.compare)
+ |> string.join("\n\t")
+}
+
+// generate the help text for a subcommand
+fn subcommand_help(name: String, cmd: CommandNode(_)) -> String {
+ case cmd.contents {
+ None -> name
+ Some(contents) -> name <> "\t\t" <> contents.description
+ }
+}
+
+/// Style heading text with the provided rgb colouring
+/// this is only intended for use within glint itself.
+///
+fn heading_style(heading: String, colour: Colour) -> String {
+ heading
+ |> ansi.bold
+ |> ansi.underline
+ |> ansi.italic
+ |> ansi.hex(colour.to_rgb_hex(colour))
+ |> ansi.reset
+}
+
+// -- DEPRECATED: STUBS --
+
+/// DEPRECATED: use `glint.cmd` and related new functions instead to create a Command
+///
+/// Create command stubs to be used in `add_command_from_stub`
+///
+pub type Stub(a) {
+ Stub(
+ path: List(String),
+ run: Runner(a),
+ flags: List(#(String, Flag)),
+ description: String,
+ )
+}
+
+/// Add a command to the root given a stub
+///
+@deprecated("use `glint.cmd` and related new functions instead to create a Command")
+pub fn add_command_from_stub(to glint: Glint(a), with stub: Stub(a)) -> Glint(a) {
+ add(
+ to: glint,
+ at: stub.path,
+ do: command(stub.run)
+ |> flags(stub.flags)
+ |> description(stub.description),
+ )
+}