From 05172d7d9137bddf8b0cf265c103afd97d50a081 Mon Sep 17 00:00:00 2001
From: "Adam D. Ruppe" <destructionator@gmail.com>
Date: Thu, 3 Oct 2013 16:16:20 -0400
Subject: [PATCH] resize and ctrl+c signal handling

---
 terminal.d | 162 ++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 134 insertions(+), 28 deletions(-)

diff --git a/terminal.d b/terminal.d
index bd0f8a6..bb27477 100644
--- a/terminal.d
+++ b/terminal.d
@@ -6,6 +6,13 @@
  * this struct will perform console initialization; when the struct
  * goes out of scope, any changes in console settings will be automatically
  * reverted.
+ *
+ * Note: on Posix, it traps SIGINT and translates it into an input event. You should
+ * keep your event loop moving and keep an eye open for this to exit cleanly; simply break
+ * your event loop upon receiving a UserInterruptionEvent. (Without
+ * the signal handler, ctrl+c can leave your terminal in a bizarre state.)
+ *
+ * As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\
  */
 module terminal;
 
@@ -14,6 +21,19 @@ module terminal;
 version(linux)
 	enum SIGWINCH = 28; // FIXME: confirm this is correct on other posix
 
+version(Posix) {
+	__gshared bool windowSizeChanged = false;
+	__gshared bool interrupted = false;
+	extern(C)
+	void sizeSignalHandler(int sigNumber) {
+		windowSizeChanged = true;
+	}
+	extern(C)
+	void interruptSignalHandler(int sigNumber) {
+		interrupted = true;
+	}
+}
+
 // parts of this were taken from Robik's ConsoleD
 // https://github.com/robik/ConsoleD/blob/master/consoled.d
 
@@ -214,12 +234,16 @@ enum Color : ushort {
 /// When capturing input, what events are you interested in?
 ///
 /// Note: these flags can be OR'd together to select more than one option at a time.
+///
+/// Ctrl+C and other keyboard input is always captured, though it may be line buffered if you don't use raw.
 enum ConsoleInputFlags {
 	raw = 0, /// raw input returns keystrokes immediately, without line buffering
 	echo = 1, /// do you want to automatically echo input back to the user?
 	mouse = 2, /// capture mouse events
 	paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes)
 	size = 8, /// window resize events
+
+	allInputEvents = 8|4|2, /// subscribe to all input events.
 }
 
 /// Defines how terminal output should be handled.
@@ -567,6 +591,9 @@ struct Terminal {
 				foreground ^= LowContrast;
 				background ^= LowContrast;
 				*/
+
+				// FIXME: is this if good?
+				//if(force == ForceOption.alwaysSend || foreground != _currentForeground || background != _currentBackground) {
 				SetConsoleTextAttribute(
 					GetStdHandle(STD_OUTPUT_HANDLE),
 					cast(ushort)((background << 4) | foreground));
@@ -815,7 +842,8 @@ struct Terminal {
 
 	private string writeBuffer;
 
-	private void writeStringRaw(in char[] s) {
+	// you really, really shouldn't use this unless you know what you are doing
+	/*private*/ void writeStringRaw(in char[] s) {
 		// FIXME: make sure all the data is sent, check for errors
 		version(Posix) {
 			writeBuffer ~= s; // buffer it to do everything at once in flush() calls
@@ -874,6 +902,8 @@ struct RealTimeConsoleInput {
 
 	version(Posix) {
 		private int fd;
+		private sigaction_t oldSigWinch;
+		private sigaction_t oldSigIntr;
 		private termios old;
 		ubyte[128] hack;
 		// apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes)....
@@ -928,34 +958,40 @@ struct RealTimeConsoleInput {
 
 		version(Posix) {
 			this.fd = 0; // stdin
-			tcgetattr(fd, &old);
-			auto n = old;
 
-			auto f = ICANON;
-			if(!(flags & ConsoleInputFlags.echo))
-				f |= ECHO;
+			{
+				tcgetattr(fd, &old);
+				auto n = old;
 
-			n.c_lflag &= ~f;
-			tcsetattr(fd, TCSANOW, &n);
+				auto f = ICANON;
+				if(!(flags & ConsoleInputFlags.echo))
+					f |= ECHO;
+
+				n.c_lflag &= ~f;
+				tcsetattr(fd, TCSANOW, &n);
+			}
 
 			// some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3
 			//destructor ~= { tcsetattr(fd, TCSANOW, &old); };
 
-			/+
 			if(flags & ConsoleInputFlags.size) {
-				// FIXME: finish this
 				import core.sys.posix.signal;
-				sigaction n;
-				n.sa_handler = &handler;
-				n.sa_mask = 0;
+				sigaction_t n;
+				n.sa_handler = &sizeSignalHandler;
+				n.sa_mask = cast(sigset_t) 0;
 				n.sa_flags = 0;
-				sigaction o;
-				sigaction(SIGWINCH, &n, &o);
-
-				// restoration
-				sigaction(SIGWINCH, &o, null);
+				sigaction(SIGWINCH, &n, &oldSigWinch);
 			}
-			+/
+
+			{
+				import core.sys.posix.signal;
+				sigaction_t n;
+				n.sa_handler = &interruptSignalHandler;
+				n.sa_mask = cast(sigset_t) 0;
+				n.sa_flags = 0;
+				sigaction(SIGINT, &n, &oldSigIntr);
+			}
+
 
 			if(flags & ConsoleInputFlags.mouse) {
 				if(terminal.terminalInFamily("xterm", "rxvt", "screen", "linux")) {
@@ -1007,6 +1043,15 @@ struct RealTimeConsoleInput {
 		// the delegate thing doesn't actually work for this... for some reason
 		version(Posix)
 			tcsetattr(fd, TCSANOW, &old);
+
+		version(Posix) {
+			if(flags & ConsoleInputFlags.size) {
+				// restoration
+				sigaction(SIGWINCH, &oldSigWinch, null);
+			}
+			sigaction(SIGINT, &oldSigIntr, null);
+		}
+
 		// we're just undoing everything the constructor did, in reverse order, same criteria
 		foreach_reverse(d; destructor)
 			d();
@@ -1048,13 +1093,23 @@ struct RealTimeConsoleInput {
 	//char[128] inputBuffer;
 	//int inputBufferPosition;
 	version(Posix)
-	int nextRaw() {
+	int nextRaw(bool interruptable = false) {
 		char[1] buf;
+		try_again:
 		auto ret = read(fd, buf.ptr, buf.length);
 		if(ret == 0)
 			return 0; // input closed
-		if(ret == -1)
-			throw new Exception("read failed");
+		if(ret == -1) {
+			import core.stdc.errno;
+			if(errno == EINTR)
+				// interrupted by signal call, quite possibly resize or ctrl+c which we want to check for in the event loop
+				if(interruptable)
+					return -1;
+				else
+					goto try_again;
+			else
+				throw new Exception("read failed");
+		}
 
 		//terminal.writef("RAW READ: %d\n", buf[0]);
 
@@ -1108,9 +1163,23 @@ struct RealTimeConsoleInput {
 			return e;
 		}
 
+		wait_for_more:
+		if(interrupted) {
+			return InputEvent(UserInterruptionEvent());
+		}
+
+		if(windowSizeChanged) {
+			auto oldWidth = terminal.width;
+			auto oldHeight = terminal.height;
+			terminal.updateSize();
+			windowSizeChanged = false;
+			return InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height));
+		}
+
 		auto more = readNextEvents();
-		while(!more.length)
-			more = readNextEvents();
+		if(!more.length)
+			goto wait_for_more; // i used to do a loop (readNextEvents can read something, but it might be discarded by the input filter) but now it goto's above because readNextEvents might be interrupted by a SIGWINCH aka size event so we want to check that at least
+
 		assert(more.length);
 
 		auto e = more[0];
@@ -1206,6 +1275,7 @@ struct RealTimeConsoleInput {
 					auto ev = record.WindowBufferSizeEvent;
 					// FIXME
 				break;
+				// FIXME: can we catch ctrl+c here too?
 				default:
 					// ignore
 			}
@@ -1398,7 +1468,9 @@ struct RealTimeConsoleInput {
 			return null;
 		}
 
-		auto c = nextRaw();
+		auto c = nextRaw(true);
+		if(c == -1)
+			return null; // interrupted; give back nothing so the other level can recheck signal flags
 		if(c == '\033') {
 			if(timedCheckForInput(50)) {
 				// escape sequence
@@ -1524,6 +1596,17 @@ struct MouseEvent {
 	uint modifierState; /// shift, ctrl, alt, meta, altgr
 }
 
+/// .
+struct SizeChangedEvent {
+	int oldWidth;
+	int oldHeight;
+	int newWidth;
+	int newHeight;
+}
+
+/// the user hitting ctrl+c will send this
+struct UserInterruptionEvent {}
+
 interface CustomEvent {}
 
 /// GetNextEvent returns this. Check the type, then use get to get the more detailed input
@@ -1533,8 +1616,10 @@ struct InputEvent {
 		CharacterEvent, ///.
 		NonCharacterKeyEvent, /// .
 		PasteEvent, /// .
-		MouseEvent, /// .
-		CustomEvent
+		MouseEvent, /// only sent if you subscribed to mouse events
+		SizeChangedEvent, /// only sent if you subscribed to size events
+		UserInterruptionEvent, /// the user hit ctrl+c
+		CustomEvent /// .
 	}
 
 	/// .
@@ -1552,6 +1637,10 @@ struct InputEvent {
 			return pasteEvent;
 		else static if(T == Type.MouseEvent)
 			return mouseEvent;
+		else static if(T == Type.SizeChangedEvent)
+			return sizeChangedEvent;
+		else static if(T == Type.UserInterruptionEvent)
+			return userInterruptionEvent;
 		else static if(T == Type.CustomEvent)
 			return customEvent;
 		else static assert(0, "Type " ~ T.stringof ~ " not added to the get function");
@@ -1574,6 +1663,14 @@ struct InputEvent {
 			t = Type.MouseEvent;
 			mouseEvent = c;
 		}
+		this(SizeChangedEvent c) {
+			t = Type.SizeChangedEvent;
+			sizeChangedEvent = c;
+		}
+		this(UserInterruptionEvent c) {
+			t = Type.UserInterruptionEvent;
+			userInterruptionEvent = c;
+		}
 		this(CustomEvent c) {
 			t = Type.CustomEvent;
 			customEvent = c;
@@ -1586,6 +1683,8 @@ struct InputEvent {
 			NonCharacterKeyEvent nonCharacterKeyEvent;
 			PasteEvent pasteEvent;
 			MouseEvent mouseEvent;
+			SizeChangedEvent sizeChangedEvent;
+			UserInterruptionEvent userInterruptionEvent;
 			CustomEvent customEvent;
 		}
 	}
@@ -1596,7 +1695,7 @@ void main() {
 	auto terminal = Terminal(ConsoleOutputType.linear);
 
 	terminal.setTitle("Basic I/O");
-	auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.mouse | ConsoleInputFlags.paste);
+	auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.mouse | ConsoleInputFlags.paste | ConsoleInputFlags.size);
 
 	terminal.color(Color.green | Bright, Color.black);
 
@@ -1611,6 +1710,13 @@ void main() {
 	void handleEvent(InputEvent event) {
 		terminal.writef("%s\n", event.type);
 		final switch(event.type) {
+			case InputEvent.Type.UserInterruptionEvent:
+				timeToBreak = true;
+			break;
+			case InputEvent.Type.SizeChangedEvent:
+				auto ev = event.get!(InputEvent.Type.SizeChangedEvent);
+				terminal.writeln(ev);
+			break;
 			case InputEvent.Type.CharacterEvent:
 				auto ev = event.get!(InputEvent.Type.CharacterEvent);
 				terminal.writef("\t%s\n", ev);