resize and ctrl+c signal handling

This commit is contained in:
Adam D. Ruppe 2013-10-03 16:16:20 -04:00
parent 1e94af20d8
commit 05172d7d91
1 changed files with 134 additions and 28 deletions

View File

@ -6,6 +6,13 @@
* this struct will perform console initialization; when the struct * this struct will perform console initialization; when the struct
* goes out of scope, any changes in console settings will be automatically * goes out of scope, any changes in console settings will be automatically
* reverted. * 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; module terminal;
@ -14,6 +21,19 @@ module terminal;
version(linux) version(linux)
enum SIGWINCH = 28; // FIXME: confirm this is correct on other posix 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 // parts of this were taken from Robik's ConsoleD
// https://github.com/robik/ConsoleD/blob/master/consoled.d // 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? /// 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. /// 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 { enum ConsoleInputFlags {
raw = 0, /// raw input returns keystrokes immediately, without line buffering raw = 0, /// raw input returns keystrokes immediately, without line buffering
echo = 1, /// do you want to automatically echo input back to the user? echo = 1, /// do you want to automatically echo input back to the user?
mouse = 2, /// capture mouse events mouse = 2, /// capture mouse events
paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes) paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes)
size = 8, /// window resize events size = 8, /// window resize events
allInputEvents = 8|4|2, /// subscribe to all input events.
} }
/// Defines how terminal output should be handled. /// Defines how terminal output should be handled.
@ -567,6 +591,9 @@ struct Terminal {
foreground ^= LowContrast; foreground ^= LowContrast;
background ^= LowContrast; background ^= LowContrast;
*/ */
// FIXME: is this if good?
//if(force == ForceOption.alwaysSend || foreground != _currentForeground || background != _currentBackground) {
SetConsoleTextAttribute( SetConsoleTextAttribute(
GetStdHandle(STD_OUTPUT_HANDLE), GetStdHandle(STD_OUTPUT_HANDLE),
cast(ushort)((background << 4) | foreground)); cast(ushort)((background << 4) | foreground));
@ -815,7 +842,8 @@ struct Terminal {
private string writeBuffer; 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 // FIXME: make sure all the data is sent, check for errors
version(Posix) { version(Posix) {
writeBuffer ~= s; // buffer it to do everything at once in flush() calls writeBuffer ~= s; // buffer it to do everything at once in flush() calls
@ -874,6 +902,8 @@ struct RealTimeConsoleInput {
version(Posix) { version(Posix) {
private int fd; private int fd;
private sigaction_t oldSigWinch;
private sigaction_t oldSigIntr;
private termios old; private termios old;
ubyte[128] hack; ubyte[128] hack;
// apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes).... // apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes)....
@ -928,34 +958,40 @@ struct RealTimeConsoleInput {
version(Posix) { version(Posix) {
this.fd = 0; // stdin this.fd = 0; // stdin
tcgetattr(fd, &old);
auto n = old;
auto f = ICANON; {
if(!(flags & ConsoleInputFlags.echo)) tcgetattr(fd, &old);
f |= ECHO; auto n = old;
n.c_lflag &= ~f; auto f = ICANON;
tcsetattr(fd, TCSANOW, &n); 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 // some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3
//destructor ~= { tcsetattr(fd, TCSANOW, &old); }; //destructor ~= { tcsetattr(fd, TCSANOW, &old); };
/+
if(flags & ConsoleInputFlags.size) { if(flags & ConsoleInputFlags.size) {
// FIXME: finish this
import core.sys.posix.signal; import core.sys.posix.signal;
sigaction n; sigaction_t n;
n.sa_handler = &handler; n.sa_handler = &sizeSignalHandler;
n.sa_mask = 0; n.sa_mask = cast(sigset_t) 0;
n.sa_flags = 0; n.sa_flags = 0;
sigaction o; sigaction(SIGWINCH, &n, &oldSigWinch);
sigaction(SIGWINCH, &n, &o);
// restoration
sigaction(SIGWINCH, &o, null);
} }
+/
{
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(flags & ConsoleInputFlags.mouse) {
if(terminal.terminalInFamily("xterm", "rxvt", "screen", "linux")) { 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 // the delegate thing doesn't actually work for this... for some reason
version(Posix) version(Posix)
tcsetattr(fd, TCSANOW, &old); 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 // we're just undoing everything the constructor did, in reverse order, same criteria
foreach_reverse(d; destructor) foreach_reverse(d; destructor)
d(); d();
@ -1048,13 +1093,23 @@ struct RealTimeConsoleInput {
//char[128] inputBuffer; //char[128] inputBuffer;
//int inputBufferPosition; //int inputBufferPosition;
version(Posix) version(Posix)
int nextRaw() { int nextRaw(bool interruptable = false) {
char[1] buf; char[1] buf;
try_again:
auto ret = read(fd, buf.ptr, buf.length); auto ret = read(fd, buf.ptr, buf.length);
if(ret == 0) if(ret == 0)
return 0; // input closed return 0; // input closed
if(ret == -1) if(ret == -1) {
throw new Exception("read failed"); 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]); //terminal.writef("RAW READ: %d\n", buf[0]);
@ -1108,9 +1163,23 @@ struct RealTimeConsoleInput {
return e; 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(); auto more = readNextEvents();
while(!more.length) if(!more.length)
more = readNextEvents(); 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); assert(more.length);
auto e = more[0]; auto e = more[0];
@ -1206,6 +1275,7 @@ struct RealTimeConsoleInput {
auto ev = record.WindowBufferSizeEvent; auto ev = record.WindowBufferSizeEvent;
// FIXME // FIXME
break; break;
// FIXME: can we catch ctrl+c here too?
default: default:
// ignore // ignore
} }
@ -1398,7 +1468,9 @@ struct RealTimeConsoleInput {
return null; 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(c == '\033') {
if(timedCheckForInput(50)) { if(timedCheckForInput(50)) {
// escape sequence // escape sequence
@ -1524,6 +1596,17 @@ struct MouseEvent {
uint modifierState; /// shift, ctrl, alt, meta, altgr 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 {} interface CustomEvent {}
/// GetNextEvent returns this. Check the type, then use get to get the more detailed input /// GetNextEvent returns this. Check the type, then use get to get the more detailed input
@ -1533,8 +1616,10 @@ struct InputEvent {
CharacterEvent, ///. CharacterEvent, ///.
NonCharacterKeyEvent, /// . NonCharacterKeyEvent, /// .
PasteEvent, /// . PasteEvent, /// .
MouseEvent, /// . MouseEvent, /// only sent if you subscribed to mouse events
CustomEvent SizeChangedEvent, /// only sent if you subscribed to size events
UserInterruptionEvent, /// the user hit ctrl+c
CustomEvent /// .
} }
/// . /// .
@ -1552,6 +1637,10 @@ struct InputEvent {
return pasteEvent; return pasteEvent;
else static if(T == Type.MouseEvent) else static if(T == Type.MouseEvent)
return 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) else static if(T == Type.CustomEvent)
return customEvent; return customEvent;
else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); else static assert(0, "Type " ~ T.stringof ~ " not added to the get function");
@ -1574,6 +1663,14 @@ struct InputEvent {
t = Type.MouseEvent; t = Type.MouseEvent;
mouseEvent = c; mouseEvent = c;
} }
this(SizeChangedEvent c) {
t = Type.SizeChangedEvent;
sizeChangedEvent = c;
}
this(UserInterruptionEvent c) {
t = Type.UserInterruptionEvent;
userInterruptionEvent = c;
}
this(CustomEvent c) { this(CustomEvent c) {
t = Type.CustomEvent; t = Type.CustomEvent;
customEvent = c; customEvent = c;
@ -1586,6 +1683,8 @@ struct InputEvent {
NonCharacterKeyEvent nonCharacterKeyEvent; NonCharacterKeyEvent nonCharacterKeyEvent;
PasteEvent pasteEvent; PasteEvent pasteEvent;
MouseEvent mouseEvent; MouseEvent mouseEvent;
SizeChangedEvent sizeChangedEvent;
UserInterruptionEvent userInterruptionEvent;
CustomEvent customEvent; CustomEvent customEvent;
} }
} }
@ -1596,7 +1695,7 @@ void main() {
auto terminal = Terminal(ConsoleOutputType.linear); auto terminal = Terminal(ConsoleOutputType.linear);
terminal.setTitle("Basic I/O"); 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); terminal.color(Color.green | Bright, Color.black);
@ -1611,6 +1710,13 @@ void main() {
void handleEvent(InputEvent event) { void handleEvent(InputEvent event) {
terminal.writef("%s\n", event.type); terminal.writef("%s\n", event.type);
final switch(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: case InputEvent.Type.CharacterEvent:
auto ev = event.get!(InputEvent.Type.CharacterEvent); auto ev = event.get!(InputEvent.Type.CharacterEvent);
terminal.writef("\t%s\n", ev); terminal.writef("\t%s\n", ev);