diff --git a/gleam.toml b/gleam.toml index 13aa279..84d0e11 100644 --- a/gleam.toml +++ b/gleam.toml @@ -14,6 +14,7 @@ version = "1.0.0" [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" +ansi = ">= 0.1.0 and < 1.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..37f3e29 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,13 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "ansi", version = "0.1.0", build_tools = ["rebar3"], requirements = [], otp_app = "ansi", source = "hex", outer_checksum = "4BF92B41E29E0480350A3260DC3F7ED52073C56FE236EFCAB1680A5E4C3923A5" }, + { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, +] + +[requirements] +ansi = { version = ">= 0.1.0 and < 1.0.0"} +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/glitzer.gleam b/src/glitzer.gleam index 2e8e537..292fd37 100644 --- a/src/glitzer.gleam +++ b/src/glitzer.gleam @@ -1,5 +1,26 @@ -import gleam/io +import glitzer/progress + +@external(erlang, "glitzer_ffi", "sleep") +@external(javascript, "./glitzer_ffi.mjs", "sleep") +pub fn sleep(ms: Int) -> a pub fn main() { - io.println("Hello from glitzer!") + let bar = progress.default_bar() + + do_something(bar, 0) +} + +fn do_something(bar, count) { + case count < 100 { + True -> { + let bar = case count > 50 { + True -> progress.finish(bar) + False -> progress.tick(bar) + } + progress.print_bar(bar) + sleep(15) + do_something(bar, count + 1) + } + False -> Nil + } } diff --git a/src/glitzer/codes.gleam b/src/glitzer/codes.gleam new file mode 100644 index 0000000..c409ef1 --- /dev/null +++ b/src/glitzer/codes.gleam @@ -0,0 +1,11 @@ +/// Print this code to clear the whole screen. +pub const clear_screen_code = "\u{001b}[2J" + +/// Print this code to clear the current line. +pub const clear_line_code = "\u{001b}[2K" + +/// Print this code to return the cursor to the "home" position. +pub const return_home_code = "\u{001b}[H" + +/// Print this code to return to the start of the line +pub const return_line_start_code = "\r" diff --git a/src/glitzer/progress.gleam b/src/glitzer/progress.gleam new file mode 100644 index 0000000..be3e081 --- /dev/null +++ b/src/glitzer/progress.gleam @@ -0,0 +1,191 @@ +import gleam/io +import gleam/string +import gleam/string_builder.{type StringBuilder} + +import glitzer/codes + +/// A `String` with only one character. +pub opaque type Char { + Char(char: String) +} + +/// Create a `Char` from a `String`. Returns a `Char` with the given string if +/// the strings length is equal to one and a `Char` of "#" otherwise. +/// +///
+/// Example: +/// +/// ```gleam +/// import glitzer/progress +/// +/// fn example() { +/// let char = progress.char_from_string("A") +/// } +/// ``` +pub fn char_from_string(from in: String) -> Char { + let len = string.length(in) + + case len { + 1 -> Char(in) + _ -> Char("#") + } +} + +pub fn string_from_char(from in: Char) -> String { + in.char +} + +pub opaque type State { + State(progress: Int) +} + +/// The style of a progress bar. +pub opaque type ProgressStyle { + ProgressStyle( + left: String, + right: String, + empty: Char, + fill: Char, + length: Int, + state: State, + ) +} + +/// Create and return a default style for a progress bar. +pub fn default_bar() -> ProgressStyle { + ProgressStyle( + left: "[", + right: "]", + empty: Char(" "), + fill: Char("#"), + length: 100, + state: State(progress: 0), + ) +} + +/// Create a new (completely empty) progress bar. +/// +///
+/// Example: +/// +/// ```gleam +/// import glitzer/progress +/// +/// fn example() { +/// let bar = +/// progress.new_bar() +/// |> progress.with_length(50) +/// |> progress.with_left_text("Progress: ") +/// |> progress.with_fill(progress.char_from_string("+")) +/// } +/// ``` +pub fn new_bar() -> ProgressStyle { + ProgressStyle( + left: "", + right: "", + empty: Char(" "), + fill: Char(" "), + length: 0, + state: State(progress: 0), + ) +} + +/// Add left text to a progress bar. +pub fn with_left_text( + bar bar: ProgressStyle, + left text: String, +) -> ProgressStyle { + ProgressStyle(..bar, left: text) +} + +/// Add right text to a progress bar. +pub fn with_right_text( + bar bar: ProgressStyle, + right text: String, +) -> ProgressStyle { + ProgressStyle(..bar, right: text) +} + +/// Add a character to a progress bar that is used to represent the +/// "background" of the bar. +pub fn with_empty(bar bar: ProgressStyle, empty char: Char) -> ProgressStyle { + ProgressStyle(..bar, empty: char) +} + +/// Add a character to a progress bar that is used to fill the bar on each +/// tick. +pub fn with_fill(bar bar: ProgressStyle, fill char: Char) -> ProgressStyle { + ProgressStyle(..bar, fill: char) +} + +/// Add length to a progress bar. +pub fn with_length(bar bar: ProgressStyle, length len: Int) -> ProgressStyle { + ProgressStyle(..bar, length: len) +} + +/// Increase the progress of the bar by one. +pub fn tick(bar bar: ProgressStyle) -> ProgressStyle { + ProgressStyle(..bar, state: State(progress: bar.state.progress + 1)) +} + +/// Completely fill the progress bar. +pub fn finish(bar bar: ProgressStyle) -> ProgressStyle { + ProgressStyle(..bar, state: State(progress: bar.length + 1)) +} + +/// Print the progress bar to stderr. +/// +///
+/// Example: +/// +/// ```gleam +/// import glitzer/progress +/// +/// fn example() { +/// let bar = progress.default_bar() +/// +/// run_example(bar, 0) +/// } +/// +/// fn run_example(bar, count) { +/// case count < 100 { +/// True -> { +/// let bar = progress.tick(bar) +/// // do some awesome stuff :3 +/// progress.print_bar(bar) +/// run_example(bar, count + 1) +/// } +/// False -> Nil +/// } +/// } +/// ``` +pub fn print_bar(bar bar: ProgressStyle) { + let fill = + build_progress_fill(string_builder.new(), bar, bar.state.progress, 0) + |> string_builder.to_string + + io.print_error( + codes.clear_line_code + <> codes.return_line_start_code + <> bar.left + <> fill + <> bar.right, + ) +} + +fn build_progress_fill( + fill: StringBuilder, + bar: ProgressStyle, + left_nonempty: Int, + count: Int, +) -> StringBuilder { + let fill = case left_nonempty > 0 { + True -> string_builder.append(fill, bar.fill.char) + False -> string_builder.append(fill, bar.empty.char) + } + + case bar.length > count { + True -> build_progress_fill(fill, bar, left_nonempty - 1, count + 1) + False -> fill + } +} diff --git a/src/glitzer_ffi.erl b/src/glitzer_ffi.erl new file mode 100644 index 0000000..ff49e5c --- /dev/null +++ b/src/glitzer_ffi.erl @@ -0,0 +1,7 @@ +-module(glitzer_ffi). + +% Public API +-export([sleep/1]). + +sleep(Int) -> + timer:sleep(Int). diff --git a/src/glitzer_ffi.mjs b/src/glitzer_ffi.mjs new file mode 100644 index 0000000..69595b9 --- /dev/null +++ b/src/glitzer_ffi.mjs @@ -0,0 +1,3 @@ +export function sleep(ms = 0) { + return new Promise(resolve => setTimeout(resolve, ms)); +}