sync-async-runner 0.1.0

Run async functions synchronously (like coroutines)
Documentation
//! A good example where a synchronous but separate thread of execution is useful.
//! here, we have two separate "threads": one for the display logic, and one for the
//! dialog.
//!
//! We could theoretically do this with other schedulers (ex. tokio), but doing so would
//! cause lots of dependency and runtime resource bloat for such a simple task.

use futures_channel::{
	mpsc,
	oneshot,
};
use futures_util::{
	future::FutureExt,
	pin_mut,
	sink::SinkExt,
};
use std::{
	io::{
		self,
		BufRead,
		Write,
	},
	thread::sleep,
	time::Duration,
};
use sync_async_runner;

/// Our dialog.
///
/// Dialog changes over time, and can interact with input, taking different branches, etc.
/// As such, it makes sense to model it as a thread of execution, akin to a coroutine.
async fn hello_dialog(mut d: DialogSender) {
	d.text("Hello there!").await;
	d.text("I'm going to ask you a question.").await;
	d.text("What's your favorite color?").await;
	let choice = d.choices(vec!["Red", "Green", "Blue"]).await;
	match choice {
		0 => d.text("Just like a rose! How pretty!").await,
		1 => d.text("Green is not a creative color...").await,
		2 => d.text("That reminds me, I am thirsty").await,
		_ => unreachable!(),
	};
	d.text("Thank you for talking with me.").await;
}

/// A convenience struct, wrapping a channel, to send dialog actions to the main routine.
struct DialogSender(mpsc::Sender<DialogAction>);
impl DialogSender {
	/// Sends some text to display
	pub async fn text(&mut self, text: &'static str) {
		self.0.send(DialogAction::Text(text)).await.unwrap();
	}

	/// Ask the user to pick one option from the supplied set of choices.
	///
	/// Completes with the index of the choice.
	pub async fn choices(&mut self, choices: Vec<&'static str>) -> usize {
		// Make a oneshot channel to get the response.
		let (sender, receiver) = oneshot::channel();
		self.0
			.send(DialogAction::Choice {
				choices,
				response: sender,
			})
			.await
			.unwrap();
		receiver.await.unwrap()
	}
}

/// Object sent over the channel from the dialog routine to the main routine.
#[derive(Debug)]
enum DialogAction {
	Text(&'static str),
	Choice {
		choices: Vec<&'static str>,
		response: oneshot::Sender<usize>,
	},
}

fn main() {
	// Set up needed objects
	let stdin = io::stdin();
	let stdout = io::stdout();
	let (dialog_sender, mut dialog_receiver) = mpsc::channel(5);
	let dialog_sender = DialogSender(dialog_sender);

	// Create the dialog routine.
	//
	// Add fuse to prevent a panic when calling poll on the future after it exits
	let dialog_coroutine = sync_async_runner::runner(hello_dialog(dialog_sender).fuse());
	pin_mut!(dialog_coroutine);

	// Run the dialog routine and receive the actions it sends us
	loop {
		// Continue the coroutine.
		//
		// Don't actually care if its finished or not; the stream closing will tell us when
		// (in fact, the future finishes as soon as it has pushed its last bit of dialog!)
		let _ = dialog_coroutine.as_mut().poll();

		// Pop a command off the queue and process it
		let command = dialog_receiver.try_next().ok().flatten();
		match command {
			Some(DialogAction::Text(t)) => {
				// Just print the text
				println!("{}", t);
				sleep(Duration::from_millis(1000));
			}
			Some(DialogAction::Choice { choices, response }) => {
				// Present the choices, parse user input, then send the
				// response over the oneshot channel
				for (i, choice) in choices.iter().enumerate() {
					println!("{}. {}", i, choice);
				}
				loop {
					print!("Choose by number: ");
					stdout.lock().flush().unwrap();

					let mut input = String::new();
					stdin.lock().read_line(&mut input).unwrap();
					if let Ok(v) = input.trim().parse() {
						response.send(v).unwrap();
						break;
					}
				}
			}
			None => {
				// All done, exit main loop
				break;
			}
		}
	}
}