1//// The `cli` module is how we create "scripts" that are intended to be run from
2//// the command line.
3
4// IMPORTS ---------------------------------------------------------------------
5
6import gleam/dict.{type Dict}
7import gleam/erlang
8import gleam/io
9import gleam/list
10import gleam/result
11import gleam_community/ansi
12import glint
13import lustre_dev_tools/error.{type Error, TemplateMissing}
14import lustre_dev_tools/project.{type Config}
15import simplifile
16import spinner.{type Spinner}
17import tom
18
19// TYPES -----------------------------------------------------------------------
20
21///
22///
23pub opaque type Cli(a) {
24 Cli(run: fn(Env) -> #(Env, Result(a, Error)))
25}
26
27type Env {
28 Env(muted: Bool, spinner: SpinnerStatus, flags: glint.Flags, config: Config)
29}
30
31type SpinnerStatus {
32 Running(spinner: Spinner, message: String)
33 Paused
34}
35
36// RUNNING CLI SCRIPTS ---------------------------------------------------------
37
38///
39///
40pub fn run(step: Cli(a), flags: glint.Flags) -> Result(a, Error) {
41 use config <- result.try(project.config())
42 let env = Env(muted: False, spinner: Paused, flags: flags, config: config)
43 let #(env, result) = step.run(env)
44
45 case env.spinner {
46 Running(spinner, _) -> spinner.stop(spinner)
47 Paused -> Nil
48 }
49
50 case result, env.spinner {
51 // In case the spinner was still running when we got an error we print
52 // the message where the spinner got stuck.
53 Error(_), Running(_, message) -> io.println("❌ " <> ansi.red(message))
54 Error(_), _ | Ok(_), _ -> Nil
55 }
56
57 result
58}
59
60// CREATING CLI SCRIPTS FROM SIMPLE VALUES -------------------------------------
61
62///
63///
64pub fn return(value: a) -> Cli(a) {
65 use env <- Cli
66
67 #(env, Ok(value))
68}
69
70///
71///
72pub fn throw(error: Error) -> Cli(a) {
73 use env <- Cli
74
75 #(env, Error(error))
76}
77
78pub fn from_result(result: Result(a, Error)) -> Cli(a) {
79 use env <- Cli
80
81 #(env, result)
82}
83
84// COMBINATORS -----------------------------------------------------------------
85
86///
87///
88pub fn do(step: Cli(a), then next: fn(a) -> Cli(b)) -> Cli(b) {
89 use env <- Cli
90 let #(env, result) = step.run(env)
91
92 case result {
93 Ok(value) -> next(value).run(env)
94 Error(error) -> {
95 case env.spinner {
96 Running(spinner, _message) -> spinner.stop(spinner)
97 Paused -> Nil
98 }
99
100 #(env, Error(error))
101 }
102 }
103}
104
105pub fn in(value: fn() -> a) -> Cli(a) {
106 use env <- Cli
107
108 #(env, Ok(value()))
109}
110
111pub fn map(step: Cli(a), then next: fn(a) -> b) -> Cli(b) {
112 use env <- Cli
113 let #(env, result) = step.run(env)
114 let result = result.map(result, next)
115
116 #(env, result)
117}
118
119///
120///
121pub fn try(result: Result(a, Error), then next: fn(a) -> Cli(b)) -> Cli(b) {
122 use env <- Cli
123
124 case result {
125 Ok(a) -> next(a).run(env)
126 Error(error) -> {
127 case env.spinner {
128 Running(spinner, _message) -> spinner.stop(spinner)
129 Paused -> Nil
130 }
131
132 #(env, Error(error))
133 }
134 }
135}
136
137// LOGGING ---------------------------------------------------------------------
138
139///
140///
141pub fn log(message: String, then next: fn() -> Cli(a)) -> Cli(a) {
142 use env <- Cli
143 let env = case env.muted {
144 True -> env
145 False ->
146 Env(
147 ..env,
148 spinner: case env.spinner {
149 Paused ->
150 Running(
151 spinner.new(message)
152 |> spinner.with_colour(ansi.magenta)
153 |> spinner.with_frames(spinner.snake_frames)
154 |> spinner.start,
155 message,
156 )
157
158 Running(spinner, _) -> {
159 spinner.set_text(spinner, message)
160 Running(spinner, message)
161 }
162 },
163 )
164 }
165
166 next().run(env)
167}
168
169pub fn success(message: String, then next: fn() -> Cli(a)) -> Cli(a) {
170 use env <- Cli
171 let env =
172 Env(
173 ..env,
174 spinner: case env.spinner {
175 Paused -> Paused
176 Running(spinner, _) -> {
177 spinner.stop(spinner)
178 Paused
179 }
180 },
181 )
182
183 case env.muted {
184 True -> Nil
185 False -> io.println("✅ " <> ansi.green(message))
186 }
187
188 next().run(env)
189}
190
191pub fn notify(message: String, then next: fn() -> Cli(a)) -> Cli(a) {
192 use env <- Cli
193 let env =
194 Env(
195 ..env,
196 spinner: case env.spinner {
197 Paused -> Paused
198 Running(spinner, _) -> {
199 spinner.stop(spinner)
200 Paused
201 }
202 },
203 )
204
205 case env.muted {
206 True -> Nil
207 False -> io.println(ansi.bright_cyan(message))
208 }
209
210 next().run(env)
211}
212
213pub fn mute() -> Cli(Nil) {
214 use env <- Cli
215
216 #(Env(..env, muted: True), Ok(Nil))
217}
218
219pub fn unmute() -> Cli(Nil) {
220 use env <- Cli
221
222 #(Env(..env, muted: False), Ok(Nil))
223}
224
225// UTILS -----------------------------------------------------------------------
226
227pub fn template(name: String, then next: fn(String) -> Cli(a)) -> Cli(a) {
228 use env <- Cli
229 let assert Ok(priv) = erlang.priv_directory("lustre_dev_tools")
230
231 case simplifile.read(priv <> "/template/" <> name) {
232 Ok(template) -> next(template).run(env)
233 Error(error) -> #(env, Error(TemplateMissing(name, error)))
234 }
235}
236
237// ENV -------------------------------------------------------------------------
238
239pub fn get_config() -> Cli(Config) {
240 use env <- Cli
241
242 #(env, Ok(env.config))
243}
244
245pub fn get_name() -> Cli(String) {
246 use env <- Cli
247
248 #(env, Ok(env.config.name))
249}
250
251// FLAGS -----------------------------------------------------------------------
252
253pub fn get_flags() -> Cli(glint.Flags) {
254 use env <- Cli
255
256 #(env, Ok(env.flags))
257}
258
259pub fn get_config_value(
260 name: String,
261 fallback: a,
262 namespace: List(String),
263 toml: fn(Dict(String, tom.Toml), List(String)) -> Result(a, _),
264 flag: fn(glint.Flags) -> Result(a, _),
265) -> Cli(a) {
266 use env <- Cli
267 let toml_path = list.concat([["lustre-dev"], namespace, [name]])
268 let value =
269 result.or(
270 result.nil_error(flag(env.flags)),
271 result.nil_error(toml(env.config.toml, toml_path)),
272 )
273 |> result.unwrap(fallback)
274
275 #(env, Ok(value))
276}
277
278pub fn get_int(
279 name: String,
280 fallback: Int,
281 namespace: List(String),
282 flag: fn(glint.Flags) -> Result(Int, _),
283) -> Cli(Int) {
284 get_config_value(name, fallback, namespace, tom.get_int, flag)
285}
286
287pub fn get_string(
288 name: String,
289 fallback: String,
290 namespace: List(String),
291 flag: fn(glint.Flags) -> Result(String, _),
292) -> Cli(String) {
293 get_config_value(name, fallback, namespace, tom.get_string, flag)
294}
295
296pub fn get_bool(
297 name: String,
298 fallback: Bool,
299 namespace: List(String),
300 flag: fn(glint.Flags) -> Result(Bool, _),
301) -> Cli(Bool) {
302 get_config_value(name, fallback, namespace, tom.get_bool, flag)
303}
304// CONFIG FETCHING -----------------------------------------------------------------------