diff --git a/terminal.d b/terminal.d index 6b49233..b2233f3 100644 --- a/terminal.d +++ b/terminal.d @@ -17,7 +17,6 @@ module terminal; // FIXME: ctrl+d eof on stdin -// FIXME: sig hup // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx @@ -26,7 +25,8 @@ version(linux) version(Posix) { __gshared bool windowSizeChanged = false; - __gshared bool interrupted = false; + __gshared bool interrupted = false; /// you might periodically check this in a long operation and abort if it is set. Remember it is volatile. It is also sent through the input event loop via RealTimeConsoleInput + __gshared bool hangedUp = false; /// similar to interrupted. version(with_eventloop) struct SignalFired {} @@ -51,6 +51,17 @@ version(Posix) { catch(Exception) {} } } + extern(C) + void hangupSignalHandler(int sigNumber) nothrow { + hangedUp = true; + version(with_eventloop) { + import arsd.eventloop; + try + send(SignalFired()); + catch(Exception) {} + } + } + } // parts of this were taken from Robik's ConsoleD @@ -260,6 +271,7 @@ enum Color : ushort { /// 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. +/// The rationale for that is to ensure the Terminal destructor has a chance to run, since the terminal is a shared resource and should be put back before the program terminates. 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? @@ -267,7 +279,10 @@ enum ConsoleInputFlags { 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. + releasedKeys = 64, /// key release events. Not reliable on Posix. + + allInputEvents = 8|4|2, /// subscribe to all input events. Note: in previous versions, this also returned release events. It no longer does, use allInputEventsWithRelease if you want them. + allInputEventsWithRelease = allInputEvents|releasedKeys, /// subscribe to all input events, including (unreliable on Posix) key release events. } /// Defines how terminal output should be handled. @@ -282,7 +297,7 @@ enum ConsoleOutputType { /// Some methods will try not to send unnecessary commands to the screen. You can override their judgement using a ForceOption parameter, if present enum ForceOption { automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right) - neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution because this can + neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution. alwaysSend = 1, /// always send the data, even if it doesn't seem necessary } @@ -658,6 +673,9 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as showCursor(); reset(); flush(); + + if(lineGetter !is null) + lineGetter.dispose(); } version(Windows) @@ -665,8 +683,15 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as reset(); flush(); showCursor(); + + if(lineGetter !is null) + lineGetter.dispose(); } + // lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit) + // and some history storage. + LineGetter lineGetter; + int _currentForeground = Color.DEFAULT; int _currentBackground = Color.DEFAULT; bool reverseVideo = false; @@ -1083,6 +1108,31 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as _cursorX = 0; _cursorY = 0; } + + /// gets a line, including user editing. Convenience method around the LineGetter class and RealTimeConsoleInput facilities - use them if you need more control. + /// You really shouldn't call this if stdin isn't actually a user-interactive terminal! So if you expect people to pipe data to your app, check for that or use something else. + // FIXME: add a method to make it easy to check if stdin is actually a tty and use other methods there. + string getline(string prompt = null) { + if(lineGetter is null) + lineGetter = new LineGetter(&this); + // since the struct might move (it shouldn't, this should be unmovable!) but since + // it technically might, I'm updating the pointer before using it just in case. + lineGetter.terminal = &this; + + lineGetter.prompt = prompt; + + auto line = lineGetter.getline(); + + // lineGetter leaves us exactly where it was when the user hit enter, giving best + // flexibility to real-time input and cellular programs. The convenience function, + // however, wants to do what is right in most the simple cases, which is to actually + // print the line (echo would be enabled without RealTimeConsoleInput anyway and they + // did hit enter), so we'll do that here too. + writePrintableString("\n"); + + return line; + } + } /+ @@ -1121,6 +1171,7 @@ struct RealTimeConsoleInput { private int fdIn; private sigaction_t oldSigWinch; private sigaction_t oldSigIntr; + private sigaction_t oldHupIntr; private termios old; ubyte[128] hack; // apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes).... @@ -1210,6 +1261,16 @@ struct RealTimeConsoleInput { sigaction(SIGINT, &n, &oldSigIntr); } + { + import core.sys.posix.signal; + sigaction_t n; + n.sa_handler = &hangupSignalHandler; + n.sa_mask = cast(sigset_t) 0; + n.sa_flags = 0; + sigaction(SIGHUP, &n, &oldHupIntr); + } + + if(flags & ConsoleInputFlags.mouse) { // basic button press+release notification @@ -1273,6 +1334,10 @@ struct RealTimeConsoleInput { } if(windowSizeChanged) send(checkWindowSizeChanged()); + if(hangedUp) { + hangedUp = false; + send(InputEvent(HangupEvent())); + } } import arsd.eventloop; @@ -1295,6 +1360,7 @@ struct RealTimeConsoleInput { sigaction(SIGWINCH, &oldSigWinch, null); } sigaction(SIGINT, &oldSigIntr, null); + sigaction(SIGHUP, &oldHupIntr, null); } // we're just undoing everything the constructor did, in reverse order, same criteria @@ -1333,12 +1399,16 @@ struct RealTimeConsoleInput { } /// Get one character from the terminal, discarding other - /// events in the process. + /// events in the process. Returns dchar.init upon receiving end-of-file. dchar getch() { auto event = nextEvent(); while(event.type != InputEvent.Type.CharacterEvent || event.characterEvent.eventType == CharacterEvent.Type.Released) { if(event.type == InputEvent.Type.UserInterruptionEvent) throw new Exception("Ctrl+c"); + if(event.type == InputEvent.Type.HangupEvent) + throw new Exception("Hangup"); + if(event.type == InputEvent.Type.EndOfFileEvent) + return dchar.init; event = nextEvent(); } return event.characterEvent.character; @@ -1441,6 +1511,11 @@ struct RealTimeConsoleInput { return InputEvent(UserInterruptionEvent()); } + if(hangedUp) { + hangedUp = false; + return InputEvent(HangupEvent()); + } + version(Posix) if(windowSizeChanged) { return checkWindowSizeChanged(); @@ -1499,6 +1574,10 @@ struct RealTimeConsoleInput { e.eventType = ev.bKeyDown ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released; ne.eventType = ev.bKeyDown ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.Type.Released; + // only send released events when specifically requested + if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) + break; + e.modifierState = ev.dwControlKeyState; ne.modifierState = ev.dwControlKeyState; @@ -1582,16 +1661,20 @@ struct RealTimeConsoleInput { terminal.flush(); // make sure all output is sent out before we try to get input InputEvent[] charPressAndRelease(dchar character) { - return [ - InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0)), - InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0)), - ]; + if((flags & ConsoleInputFlags.releasedKeys)) + return [ + InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0)), + InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0)), + ]; + else return [ InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0)) ]; } InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { - return [ - InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)), - InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers)), - ]; + if((flags & ConsoleInputFlags.releasedKeys)) + return [ + InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)), + InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers)), + ]; + else return [ InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)) ]; } char[30] sequenceBuffer; @@ -1880,7 +1963,7 @@ struct RealTimeConsoleInput { if(c == -1) return null; // interrupted; give back nothing so the other level can recheck signal flags if(c == 0) - throw new Exception("stdin has reached end of file"); // FIXME: return this as an event instead + return [InputEvent(EndOfFileEvent())]; if(c == '\033') { if(timedCheckForInput(50)) { // escape sequence @@ -2020,8 +2103,16 @@ struct SizeChangedEvent { } /// the user hitting ctrl+c will send this +/// You should drop what you're doing and perhaps exit when this happens. struct UserInterruptionEvent {} +/// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent. +/// If you receive it, you should generally cleanly exit. +struct HangupEvent {} + +/// Sent upon receiving end-of-file from stdin. +struct EndOfFileEvent {} + interface CustomEvent {} version(Windows) @@ -2055,10 +2146,12 @@ struct InputEvent { enum Type { CharacterEvent, ///. NonCharacterKeyEvent, /// . - PasteEvent, /// . + PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead. MouseEvent, /// only sent if you subscribed to mouse events SizeChangedEvent, /// only sent if you subscribed to size events UserInterruptionEvent, /// the user hit ctrl+c + EndOfFileEvent, /// stdin has received an end of file + HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator CustomEvent /// . } @@ -2081,6 +2174,10 @@ struct InputEvent { return sizeChangedEvent; else static if(T == Type.UserInterruptionEvent) return userInterruptionEvent; + else static if(T == Type.EndOfFileEvent) + return endOfFileEvent; + else static if(T == Type.HangupEvent) + return hangupEvent; else static if(T == Type.CustomEvent) return customEvent; else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); @@ -2111,6 +2208,14 @@ struct InputEvent { t = Type.UserInterruptionEvent; userInterruptionEvent = c; } + this(HangupEvent c) { + t = Type.HangupEvent; + hangupEvent = c; + } + this(EndOfFileEvent c) { + t = Type.EndOfFileEvent; + endOfFileEvent = c; + } this(CustomEvent c) { t = Type.CustomEvent; customEvent = c; @@ -2125,6 +2230,8 @@ struct InputEvent { MouseEvent mouseEvent; SizeChangedEvent sizeChangedEvent; UserInterruptionEvent userInterruptionEvent; + HangupEvent hangupEvent; + EndOfFileEvent endOfFileEvent; CustomEvent customEvent; } } @@ -2137,16 +2244,22 @@ void main() { //terminal.color(Color.DEFAULT, Color.DEFAULT); // + /* auto getter = new LineGetter(&terminal, "test"); getter.prompt = "> "; terminal.writeln("\n" ~ getter.getline()); terminal.writeln("\n" ~ getter.getline()); terminal.writeln("\n" ~ getter.getline()); getter.dispose(); + */ + + terminal.writeln(terminal.getline()); + terminal.writeln(terminal.getline()); + terminal.writeln(terminal.getline()); //input.getch(); - return; + // return; // terminal.setTitle("Basic I/O"); @@ -2165,6 +2278,8 @@ void main() { terminal.writef("%s\n", event.type); final switch(event.type) { case InputEvent.Type.UserInterruptionEvent: + case InputEvent.Type.HangupEvent: + case InputEvent.Type.EndOfFileEvent: timeToBreak = true; version(with_eventloop) { import arsd.eventloop; @@ -2638,12 +2753,17 @@ class LineGetter { /// returns false when there's nothing more to do bool workOnLine(InputEvent e) { switch(e.type) { + case InputEvent.Type.EndOfFileEvent: + justHitTab = false; + return false; + break; case InputEvent.Type.CharacterEvent: if(e.characterEvent.eventType == CharacterEvent.Type.Released) return true; /* Insert the character (unless it is backspace, tab, or some other control char) */ auto ch = e.characterEvent.character; switch(ch) { + case 4: // ctrl+d will also send a newline-equivalent case '\r': case '\n': justHitTab = false; @@ -2779,6 +2899,10 @@ class LineGetter { /* I'll take this as canceling the line. */ throw new Exception("user canceled"); // FIXME break; + case InputEvent.Type.HangupEvent: + /* I'll take this as canceling the line. */ + throw new Exception("user hanged up"); // FIXME + break; default: /* ignore. ideally it wouldn't be passed to us anyway! */ }