jeflog/
lib.rs

1use std::{
2	io::{self, Write},
3	sync::{atomic::{AtomicBool, Ordering}, Mutex}, thread, time::Duration,
4};
5
6#[derive(Clone, Copy, Debug)]
7struct Task {
8	pub row_offset: i32
9}
10
11static TASKS: Mutex<Vec<Task>> = Mutex::new(Vec::new());
12static SPINNING: AtomicBool = AtomicBool::new(false);
13
14/// Begins a task or subtask with a spinner.
15#[macro_export]
16macro_rules! task {
17	($($tokens:tt)*) => {
18		$crate::__start_task__(format!($($tokens)*));
19	}
20}
21
22/// Indicates that the most recently created task has passed by
23/// replacing the spinner with a green check mark.
24#[macro_export]
25macro_rules! pass {
26	($($tokens:tt)*) => {
27		$crate::__end_task__("\x1b[32;1m✔\x1b[0m", format!($($tokens)*));
28	}
29}
30
31/// Indicates that the most recently created task has passed with a
32/// warning by replacing the spinner with a yellow triangle.
33#[macro_export]
34macro_rules! warn {
35	($($tokens:tt)*) => {
36		$crate::__end_task__("\x1b[33;1m▲\x1b[0m", format!($($tokens)*));
37	}
38}
39
40/// Indicates that the most recently created task has failed by
41/// replacing the spinner with a red x.
42#[macro_export]
43macro_rules! fail {
44	($($tokens:tt)*) => {
45		$crate::__end_task__("\x1b[31;1m✘\x1b[0m", format!($($tokens)*));
46	}
47}
48
49#[doc(hidden)]
50pub fn __start_task__(message: String) {
51	// this can never panic because mutex locks can only
52	// fail if the thread holding the lock panics.
53	// this is guaranteed as long as:
54	//   1. TASKS is never locked outside of jeflog
55	//   2. jeflog code never panics
56	// as long as these two invariants are satisfied
57	// (and they are by design) then locks of TASKS
58	// cannot panic.
59	let mut tasks = TASKS.lock().unwrap();
60
61	if tasks.len() > 0 {
62		// adjust the offset (from bottom row) of each task
63		for task in tasks.iter_mut() {
64			task.row_offset += 1;
65		}
66
67		println!();
68	}
69
70	if let Some(last_row) = tasks.last().map(|task| task.row_offset) {
71		print!("\x1b[s");
72
73		if last_row > 1 {
74			print!("\x1b[{}A\x1b[{}G┣", last_row - 1, (tasks.len() - 1) * 5 + 3);
75		}
76
77		for _ in 1..last_row {
78			print!("\x1b[1D\x1b[1B┃");
79		}
80
81		print!("\x1b[u");
82	}
83
84	tasks.push(Task { row_offset: 0 });
85
86	if tasks.len() > 1 {
87		print!("{}", " ".repeat((tasks.len() - 2) * 5 + 2) + "┗━ ");
88	}
89
90	// attempt to print message, ignore if flush fails
91	print!("\x1b[33;1m-\x1b[0m {message}");
92	_ = io::stdout().flush();
93
94	// atomically check if the spinner is running
95	// if not, then start the spinner
96	if SPINNING.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) == Ok(false) {
97		thread::spawn(spin);
98	}
99}
100
101#[doc(hidden)]
102pub fn __end_task__(symbol: &str, message: String) {
103	let mut tasks = TASKS.lock().unwrap();
104
105	if let Some(Task { row_offset: row }) = tasks.pop() {
106		let column = tasks.len() * 5 + 1;
107		// replace spinner with symbol:
108		// \x1b[s         : save cursor's current position
109		// \x1b[{row}A    : move the cursor up to correct row
110		// \x1b[{column}G : move the cursor to correct column
111		// {symbol}       : print the symbol replacing the spinner
112		// \x1b[K         : clear the current line
113		// {message}      : print the ending message overwriting the old message
114
115		print!("\x1b[s");
116
117		if row > 0 {
118			print!("\x1b[{row}A");
119		}
120
121		print!("\x1b[{column}G{symbol} \x1b[K{message}");
122
123		// restore the cursor's position if not the last task
124		if row != 0 {
125			print!("\x1b[u");
126		}
127
128		if tasks.len() == 0 {
129			println!();
130		}
131
132		_ = io::stdout().flush();
133	} else {
134		// if no task is running, just print the symbol and message
135		println!("{symbol} {message}");
136	}
137}
138
139fn spin() {
140	let mut spinner = '-';
141
142	loop {
143		let tasks = TASKS.lock().unwrap();
144
145		// kill the thread if there are no more tasks
146		if tasks.len() == 0 {
147			break;
148		}
149
150		let mut column = 1;
151
152		for Task { row_offset: row } in tasks.iter() {
153			// replace spinner with new spinner:
154			// \x1b[s         : save the cursor's current position
155			// \x1b[{row}A    : move the cursor up to correct row
156			// \x1b[{column}G : move the cursor to correct column
157			// \x1b[33;1m     : set the foreground color to yellow and font to bold
158			// {spinner}      : print the updated spinner character
159			// \x1b[0m        : reset all formatting
160			// \x1b[u         : restore saved cursor position
161
162			print!("\x1b[s");
163
164			if *row > 0 {
165				print!("\x1b[{row}A");
166			}
167
168			print!("\x1b[{column}G\x1b[33;1m{spinner}\x1b[0m\x1b[u");
169			
170			column += 5;
171		}
172
173		// most systems flush stdout by newlines
174		// since no newlines were printed, we need
175		// to flush stdout explicitly
176		_ = io::stdout().flush();
177
178		// update spinner to next spinner character (clockwise)
179		spinner = match spinner {
180			'-' => '\\',
181			'\\' => '|',
182			'|' => '/',
183			'/' => '-',
184			_ => '-', // this is not possible, but Rust demands it
185		};
186
187		// drop tasks before the wait so other threads may use it
188		drop(tasks);
189
190		// wait for 100ms; this can be changed to make the spinner go faster
191		thread::sleep(Duration::from_millis(100));
192	}
193
194	// if the loop has ended, then the spinner has stopped and
195	// will need to be restarted if another task starts
196	SPINNING.store(false, Ordering::Relaxed);
197}