// // MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui // // Authors: // Miguel de Icaza (miguel@gnome.org) // using System; using System.Collections.Generic; using System.Collections.ObjectModel; namespace Terminal.Gui { /// /// Public interface to create your own platform specific main loop driver. /// public interface IMainLoopDriver { /// /// Initializes the main loop driver, gets the calling main loop for the initialization. /// /// Main loop. void Setup (MainLoop mainLoop); /// /// Wakes up the mainloop that might be waiting on input, must be thread safe. /// void Wakeup (); /// /// Must report whether there are any events pending, or even block waiting for events. /// /// true, if there were pending events, false otherwise. /// If set to true wait until an event is available, otherwise return immediately. bool EventsPending (bool wait); /// /// The iteration function. /// void MainIteration (); } /// /// Simple main loop implementation that can be used to monitor /// file descriptor, run timers and idle handlers. /// /// /// Monitoring of file descriptors is only available on Unix, there /// does not seem to be a way of supporting this on Windows. /// public class MainLoop { /// /// Provides data for timers running manipulation. /// public sealed class Timeout { /// /// Time to wait before invoke the callback. /// public TimeSpan Span; /// /// The function that will be invoked. /// public Func Callback; } internal SortedList timeouts = new SortedList (); object timeoutsLockToken = new object (); /// /// The idle handlers and lock that must be held while manipulating them /// object idleHandlersLock = new object (); internal List> idleHandlers = new List> (); /// /// Gets the list of all timeouts sorted by the time ticks./>. /// A shorter limit time can be added at the end, but it will be called before an /// earlier addition that has a longer limit time. /// public SortedList Timeouts => timeouts; /// /// Gets a copy of the list of all idle handlers. /// public ReadOnlyCollection> IdleHandlers { get { lock (idleHandlersLock) { return new List> (idleHandlers).AsReadOnly (); } } } /// /// The current IMainLoopDriver in use. /// /// The driver. public IMainLoopDriver Driver { get; } /// /// Invoked when a new timeout is added to be used on the case /// if is true, /// public event Action TimeoutAdded; /// /// Creates a new Mainloop. /// /// Should match the (one of the implementations UnixMainLoop, NetMainLoop or WindowsMainLoop). public MainLoop (IMainLoopDriver driver) { Driver = driver; driver.Setup (this); } /// /// Runs action on the thread that is processing events /// /// the action to be invoked on the main processing thread. public void Invoke (Action action) { AddIdle (() => { action (); return false; }); } /// /// Adds specified idle handler function to mainloop processing. The handler function will be called once per iteration of the main loop after other events have been handled. /// /// /// /// Remove an idle hander by calling with the token this method returns. /// /// /// If the idleHandler returns false it will be removed and not called subsequently. /// /// /// Token that can be used to remove the idle handler with . public Func AddIdle (Func idleHandler) { lock (idleHandlersLock) { idleHandlers.Add (idleHandler); } Driver.Wakeup (); return idleHandler; } /// /// Removes an idle handler added with from processing. /// /// A token returned by /// Returns trueif the idle handler is successfully removed; otherwise, false. /// This method also returns false if the idle handler is not found. public bool RemoveIdle (Func token) { lock (idleHandlersLock) return idleHandlers.Remove (token); } void AddTimeout (TimeSpan time, Timeout timeout) { lock (timeoutsLockToken) { var k = (DateTime.UtcNow + time).Ticks; timeouts.Add (NudgeToUniqueKey (k), timeout); TimeoutAdded?.Invoke (k); } } /// /// Adds a timeout to the mainloop. /// /// /// When time specified passes, the callback will be invoked. /// If the callback returns true, the timeout will be reset, repeating /// the invocation. If it returns false, the timeout will stop and be removed. /// /// The returned value is a token that can be used to stop the timeout /// by calling . /// public object AddTimeout (TimeSpan time, Func callback) { if (callback == null) throw new ArgumentNullException (nameof (callback)); var timeout = new Timeout () { Span = time, Callback = callback }; AddTimeout (time, timeout); return timeout; } /// /// Removes a previously scheduled timeout /// /// /// The token parameter is the value returned by AddTimeout. /// /// Returns trueif the timeout is successfully removed; otherwise, false. /// This method also returns false if the timeout is not found. public bool RemoveTimeout (object token) { lock (timeoutsLockToken) { var idx = timeouts.IndexOfValue (token as Timeout); if (idx == -1) return false; timeouts.RemoveAt (idx); } return true; } void RunTimers () { long now = DateTime.UtcNow.Ticks; SortedList copy; // lock prevents new timeouts being added // after we have taken the copy but before // we have allocated a new list (which would // result in lost timeouts or errors during enumeration) lock (timeoutsLockToken) { copy = timeouts; timeouts = new SortedList (); } foreach (var t in copy) { var k = t.Key; var timeout = t.Value; if (k < now) { if (timeout.Callback (this)) AddTimeout (timeout.Span, timeout); } else { lock (timeoutsLockToken) { timeouts.Add (NudgeToUniqueKey (k), timeout); } } } } /// /// Finds the closest number to that is not /// present in (incrementally). /// /// /// private long NudgeToUniqueKey (long k) { lock (timeoutsLockToken) { while (timeouts.ContainsKey (k)) { k++; } } return k; } void RunIdle () { List> iterate; lock (idleHandlersLock) { iterate = idleHandlers; idleHandlers = new List> (); } foreach (var idle in iterate) { if (idle ()) lock (idleHandlersLock) idleHandlers.Add (idle); } } bool running; /// /// Stops the mainloop. /// public void Stop () { running = false; Driver.Wakeup (); } /// /// Determines whether there are pending events to be processed. /// /// /// You can use this method if you want to probe if events are pending. /// Typically used if you need to flush the input queue while still /// running some of your own code in your main thread. /// public bool EventsPending (bool wait = false) { return Driver.EventsPending (wait); } /// /// Runs one iteration of timers and file watches /// /// /// You use this to process all pending events (timers, idle handlers and file watches). /// /// You can use it like this: /// while (main.EvensPending ()) MainIteration (); /// public void MainIteration () { if (timeouts.Count > 0) RunTimers (); Driver.MainIteration (); bool runIdle = false; lock (idleHandlersLock) { runIdle = idleHandlers.Count > 0; } if (runIdle) { RunIdle (); } } /// /// Runs the mainloop. /// public void Run () { bool prev = running; running = true; while (running) { EventsPending (true); MainIteration (); } running = prev; } } }