nitro_shared 0.30.0

Shared libraries and utilities for Nitrolaunch crates
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
use std::ops::{Deref, DerefMut};

use anyhow::bail;
use serde::{Deserialize, Serialize};

use crate::{
	lang::translate::TranslationKey,
	pkg::{PackageDiff, PkgRequest, ResolutionError},
};

/// Trait for a type that can output information about Nitrolaunch processes
#[async_trait::async_trait]
pub trait NitroOutput: Send {
	/// Base function for a simple message. Used as a fallback
	fn display_text(&mut self, text: String, level: MessageLevel);

	/// Function to display a message to the user
	fn display_message(&mut self, message: Message) {
		self.display_text(message.contents.default_format(), message.level);
	}

	/// Display a message at the important level
	fn display(&mut self, contents: MessageContents) {
		self.display_message(Message {
			contents,
			level: MessageLevel::Important,
		})
	}

	/// Display a message at the debug level
	fn debug(&mut self, contents: MessageContents) {
		self.display_message(Message {
			contents,
			level: MessageLevel::Debug,
		})
	}

	/// Display a message at the trace level
	fn trace(&mut self, contents: MessageContents) {
		self.display_message(Message {
			contents,
			level: MessageLevel::Trace,
		})
	}

	/// Start a process of multiple messages. Implementations can use this to replace a line
	/// multiple times
	fn start_process(&mut self) {}

	/// End an existing process
	fn end_process(&mut self) {}

	/// Start a new section / level of hierarchy. Implementations can use this to set the indent level
	fn start_section(&mut self) {}

	/// End the current section and go down a level of hierarchy
	fn end_section(&mut self) {}

	/// Gets an OutputProcess object for this output
	fn get_process(&'_ mut self) -> OutputProcess<'_, Self>
	where
		Self: Sized,
	{
		OutputProcess::new(self)
	}

	/// Gets an OutputSection object for this output
	fn get_section(&'_ mut self) -> OutputSection<'_, Self>
	where
		Self: Sized,
	{
		OutputSection::new(self)
	}

	/// Offer a confirmation / yes no prompt to the user.
	/// The default is the default value of the prompt.
	async fn prompt_yes_no(
		&mut self,
		default: bool,
		message: MessageContents,
	) -> anyhow::Result<bool> {
		let _message = message;
		Ok(default)
	}

	/// Offer a password / secret prompt
	async fn prompt_password(&mut self, message: MessageContents) -> anyhow::Result<String> {
		let _ = message;
		bail!("No password prompt available")
	}

	/// Offer a new password / secret prompt
	async fn prompt_new_password(&mut self, message: MessageContents) -> anyhow::Result<String> {
		self.prompt_password(message).await
	}

	/// Get the translation for the specified key
	fn translate(&self, key: TranslationKey) -> &str {
		key.get_default()
	}

	// Specialized implementations for certain combinations of messages.
	// These all have default impls which you can override for more specific behavior

	/// Specialized implementation for details given to the user for Microsoft authentication
	fn display_special_ms_auth(&mut self, url: &str, code: &str) {
		default_special_ms_auth(self, url, code);
	}

	/// Specialized implementation for displaying a package resolution error to the user
	fn display_special_resolution_error(&mut self, error: ResolutionError, instance_id: &str) {
		self.display(MessageContents::Error(format!(
			"Failed to resolve packages for instance {instance_id}: {error}"
		)))
	}

	/// Specialized implementation for prompting an account passkey
	async fn prompt_special_account_passkey(
		&mut self,
		message: MessageContents,
		account_id: &str,
	) -> anyhow::Result<String> {
		let _ = account_id;
		self.prompt_password(message).await
	}

	/// Specialized implementation for showing changes to installed packages and asking if the user
	/// wants to proceed
	async fn prompt_special_package_diffs(
		&mut self,
		diffs: Vec<PackageDiff>,
	) -> anyhow::Result<bool> {
		self.display(MessageContents::Header("Package changes:".into()));

		for diff in diffs {
			match &diff {
				PackageDiff::Added(pkg)
				| PackageDiff::Removed(pkg)
				| PackageDiff::VersionChanged(pkg, ..) => {
					let pkg = PkgRequest::clone(pkg);

					let message = match &diff {
						PackageDiff::Added(..) => "Added".to_string(),
						PackageDiff::Removed(..) => "Removed".to_string(),
						PackageDiff::VersionChanged(_, old_version, new_version) => {
							format!("{old_version} -> {new_version}")
						}
						_ => unreachable!(),
					};
					self.display(MessageContents::Package(
						pkg,
						Box::new(MessageContents::Simple(message)),
					));
				}
				PackageDiff::ManyAdded(count) => {
					self.display(MessageContents::Simple(format!("Added {count} packages")))
				}
				PackageDiff::ManyRemoved(count) => {
					self.display(MessageContents::Simple(format!("Removed {count} packages")))
				}
			}
		}

		self.prompt_yes_no(
			false,
			MessageContents::Simple("Would you like to proceed with these changes?".into()),
		)
		.await
	}

	/// Gets a copy of this output that may technically be used in asynchronous tasks,
	/// but will most likely be used for something synchronous like the output of a plugin command
	fn get_greater_copy(&self) -> Box<dyn NitroOutput + Sync> {
		self.get_lesser_copy()
	}

	/// Gets a copy of this output that can be used for asynchronous tasks.
	/// Note that this does not have to be an exact copy.
	/// If multiple sources writing to your output at the same time would mess up the formatting (like for a terminal),
	/// this copy can be a lesser version that just saves to logging, for example.
	fn get_lesser_copy(&self) -> Box<dyn NitroOutput + Sync> {
		Box::new(NoOp)
	}
}

#[async_trait::async_trait]
impl<T: NitroOutput + Sync + ?Sized> NitroOutput for Box<T> {
	fn display_text(&mut self, text: String, level: MessageLevel) {
		self.deref_mut().display_text(text, level)
	}

	fn display_message(&mut self, message: Message) {
		self.deref_mut().display_message(message)
	}

	fn start_process(&mut self) {
		self.deref_mut().start_process()
	}

	fn end_process(&mut self) {
		self.deref_mut().end_process()
	}

	fn start_section(&mut self) {
		self.deref_mut().start_section()
	}

	fn end_section(&mut self) {
		self.deref_mut().end_section()
	}

	async fn prompt_yes_no(
		&mut self,
		default: bool,
		message: MessageContents,
	) -> anyhow::Result<bool> {
		self.deref_mut().prompt_yes_no(default, message).await
	}

	async fn prompt_password(&mut self, message: MessageContents) -> anyhow::Result<String> {
		self.deref_mut().prompt_password(message).await
	}

	async fn prompt_new_password(&mut self, message: MessageContents) -> anyhow::Result<String> {
		self.deref_mut().prompt_new_password(message).await
	}

	fn translate(&self, key: TranslationKey) -> &str {
		self.deref().translate(key)
	}

	fn display_special_ms_auth(&mut self, url: &str, code: &str) {
		self.deref_mut().display_special_ms_auth(url, code)
	}

	fn display_special_resolution_error(&mut self, error: ResolutionError, instance_id: &str) {
		self.deref_mut()
			.display_special_resolution_error(error, instance_id)
	}

	async fn prompt_special_account_passkey(
		&mut self,
		message: MessageContents,
		account_id: &str,
	) -> anyhow::Result<String> {
		self.deref_mut()
			.prompt_special_account_passkey(message, account_id)
			.await
	}

	async fn prompt_special_package_diffs(
		&mut self,
		diffs: Vec<PackageDiff>,
	) -> anyhow::Result<bool> {
		self.deref_mut().prompt_special_package_diffs(diffs).await
	}

	fn get_greater_copy(&self) -> Box<dyn NitroOutput + Sync> {
		self.deref().get_lesser_copy()
	}

	fn get_lesser_copy(&self) -> Box<dyn NitroOutput + Sync> {
		self.deref().get_lesser_copy()
	}
}

/// Displays the default Microsoft authentication messages
pub fn default_special_ms_auth(o: &mut (impl NitroOutput + ?Sized), url: &str, code: &str) {
	o.display(MessageContents::Property(
		"Open this link in your web browser if it has not opened already".into(),
		Box::new(MessageContents::Hyperlink(url.into())),
	));
	o.display(MessageContents::Property(
		"and enter the code".into(),
		Box::new(MessageContents::Copyable(code.into())),
	));
}

/// A message supplied to the output
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Message {
	/// The contents of the message
	pub contents: MessageContents,
	/// The printing level of the message
	pub level: MessageLevel,
}

/// Contents of a message. Different types represent different formatting
#[non_exhaustive]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum MessageContents {
	/// Simple message with no formatting
	Simple(String),
	/// An important notice to the user
	Notice(String),
	/// A warning to the user
	Warning(String),
	/// An error
	Error(String),
	/// A success / finish message
	Success(String),
	/// A key-value property
	Property(String, Box<MessageContents>),
	/// A header / big message
	Header(String),
	/// An start of some long running process. Usually ends with ...
	StartProcess(String),
	/// A message with an associated value displayed along with it.
	Associated(Box<MessageContents>, Box<MessageContents>),
	/// Message with an associated package
	Package(PkgRequest, Box<MessageContents>),
	/// A hyperlink
	Hyperlink(String),
	/// An item in an unordered list
	ListItem(Box<MessageContents>),
	/// Text that can be copied, such as a verification code
	Copyable(String),
	/// A progress indicator
	Progress {
		/// The current amount completed
		current: u32,
		/// The total amount that needs to be completed
		total: u32,
	},
}

impl MessageContents {
	/// Message formatting for the default implementation
	pub fn default_format(self) -> String {
		match self {
			MessageContents::Simple(text)
			| MessageContents::Success(text)
			| MessageContents::Hyperlink(text)
			| MessageContents::Copyable(text) => text,
			MessageContents::Notice(text) => format!("Notice: {text}"),
			MessageContents::Warning(text) => format!("Warning: {text}"),
			MessageContents::Error(text) => format!("Error: {text}"),
			MessageContents::Property(key, value) => {
				format!("{key}: {}", value.default_format())
			}
			MessageContents::Header(text) => text.to_uppercase(),
			MessageContents::StartProcess(text) => format!("{text}..."),
			MessageContents::Associated(item, message) => {
				format!("[{}] {}", item.default_format(), message.default_format())
			}
			MessageContents::Package(pkg, message) => {
				format!("[{pkg}] {}", message.default_format())
			}
			MessageContents::ListItem(item) => format!(" - {}", item.default_format()),
			MessageContents::Progress { current, total } => format!("{current}/{total}"),
		}
	}
}

/// The level of logging that a message has
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum MessageLevel {
	/// Very Debug-level messages. Should only be used for logging
	Trace,
	/// Debug-level messages. Good for logging but should not be displayed to
	/// the user unless they ask
	Debug,
	/// Messages that should always be displayed
	Important,
}

/// Dummy NitroOutput that doesn't print anything
#[derive(Clone, Copy)]
pub struct NoOp;

impl NitroOutput for NoOp {
	fn display_text(&mut self, _text: String, _level: MessageLevel) {}
}

/// NitroOutput with simple terminal printing
#[derive(Clone, Copy)]
pub struct Simple(pub MessageLevel);

impl NitroOutput for Simple {
	fn display_text(&mut self, text: String, level: MessageLevel) {
		if level < self.0 {
			return;
		}

		println!("{text}");
	}

	fn get_lesser_copy(&self) -> Box<dyn NitroOutput + Sync> {
		Box::new(Self(self.0))
	}
}

/// Dummy NitroOutput that records messages for testing
#[derive(Clone)]
pub struct TestOutput(pub Vec<Message>);

impl NitroOutput for TestOutput {
	fn display_text(&mut self, _text: String, _level: MessageLevel) {}

	fn display_message(&mut self, message: Message) {
		self.0.push(message);
	}
}

/// RAII struct that opens and closes an output process
pub struct OutputProcess<'a, O: NitroOutput>(&'a mut O);

impl<'a, O> OutputProcess<'a, O>
where
	O: NitroOutput,
{
	/// Create a new OutputProcess from an NitroOutput
	pub fn new(o: &'a mut O) -> Self {
		o.start_process();
		Self(o)
	}

	/// Finish the proces early
	pub fn finish(self) {}
}

impl<O> Drop for OutputProcess<'_, O>
where
	O: NitroOutput,
{
	fn drop(&mut self) {
		self.0.end_process();
	}
}

impl<O> Deref for OutputProcess<'_, O>
where
	O: NitroOutput,
{
	type Target = O;

	fn deref(&self) -> &Self::Target {
		self.0
	}
}

impl<O> DerefMut for OutputProcess<'_, O>
where
	O: NitroOutput,
{
	fn deref_mut(&mut self) -> &mut Self::Target {
		self.0
	}
}

/// RAII struct that opens and closes an output section
pub struct OutputSection<'a, O: NitroOutput>(&'a mut O);

impl<'a, O> OutputSection<'a, O>
where
	O: NitroOutput,
{
	/// Create a new OutputProcess from an NitroOutput
	pub fn new(o: &'a mut O) -> Self {
		o.start_section();
		Self(o)
	}

	/// Finish the section early
	pub fn finish(self) {}
}

impl<O> Drop for OutputSection<'_, O>
where
	O: NitroOutput,
{
	fn drop(&mut self) {
		self.0.end_section();
	}
}

impl<O> Deref for OutputSection<'_, O>
where
	O: NitroOutput,
{
	type Target = O;

	fn deref(&self) -> &Self::Target {
		self.0
	}
}

impl<O> DerefMut for OutputSection<'_, O>
where
	O: NitroOutput,
{
	fn deref_mut(&mut self) -> &mut Self::Target {
		self.0
	}
}