getline convenience function

This commit is contained in:
Adam D. Ruppe 2014-12-12 12:12:22 -05:00
parent 1933833c7b
commit 8f09236fc9
1 changed files with 140 additions and 16 deletions

View File

@ -17,7 +17,6 @@
module terminal; module terminal;
// FIXME: ctrl+d eof on stdin // 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 // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx
@ -26,7 +25,8 @@ version(linux)
version(Posix) { version(Posix) {
__gshared bool windowSizeChanged = false; __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) version(with_eventloop)
struct SignalFired {} struct SignalFired {}
@ -51,6 +51,17 @@ version(Posix) {
catch(Exception) {} 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 // 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. /// 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. /// 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 { 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?
@ -267,7 +279,10 @@ enum ConsoleInputFlags {
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. 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. /// 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 /// 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 { enum ForceOption {
automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right) 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 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(); showCursor();
reset(); reset();
flush(); flush();
if(lineGetter !is null)
lineGetter.dispose();
} }
version(Windows) version(Windows)
@ -665,8 +683,15 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
reset(); reset();
flush(); flush();
showCursor(); 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 _currentForeground = Color.DEFAULT;
int _currentBackground = Color.DEFAULT; int _currentBackground = Color.DEFAULT;
bool reverseVideo = false; bool reverseVideo = false;
@ -1083,6 +1108,31 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
_cursorX = 0; _cursorX = 0;
_cursorY = 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 int fdIn;
private sigaction_t oldSigWinch; private sigaction_t oldSigWinch;
private sigaction_t oldSigIntr; private sigaction_t oldSigIntr;
private sigaction_t oldHupIntr;
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)....
@ -1210,6 +1261,16 @@ struct RealTimeConsoleInput {
sigaction(SIGINT, &n, &oldSigIntr); 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) { if(flags & ConsoleInputFlags.mouse) {
// basic button press+release notification // basic button press+release notification
@ -1273,6 +1334,10 @@ struct RealTimeConsoleInput {
} }
if(windowSizeChanged) if(windowSizeChanged)
send(checkWindowSizeChanged()); send(checkWindowSizeChanged());
if(hangedUp) {
hangedUp = false;
send(InputEvent(HangupEvent()));
}
} }
import arsd.eventloop; import arsd.eventloop;
@ -1295,6 +1360,7 @@ struct RealTimeConsoleInput {
sigaction(SIGWINCH, &oldSigWinch, null); sigaction(SIGWINCH, &oldSigWinch, null);
} }
sigaction(SIGINT, &oldSigIntr, null); sigaction(SIGINT, &oldSigIntr, null);
sigaction(SIGHUP, &oldHupIntr, 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
@ -1333,12 +1399,16 @@ struct RealTimeConsoleInput {
} }
/// Get one character from the terminal, discarding other /// 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() { dchar getch() {
auto event = nextEvent(); auto event = nextEvent();
while(event.type != InputEvent.Type.CharacterEvent || event.characterEvent.eventType == CharacterEvent.Type.Released) { while(event.type != InputEvent.Type.CharacterEvent || event.characterEvent.eventType == CharacterEvent.Type.Released) {
if(event.type == InputEvent.Type.UserInterruptionEvent) if(event.type == InputEvent.Type.UserInterruptionEvent)
throw new Exception("Ctrl+c"); 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(); event = nextEvent();
} }
return event.characterEvent.character; return event.characterEvent.character;
@ -1441,6 +1511,11 @@ struct RealTimeConsoleInput {
return InputEvent(UserInterruptionEvent()); return InputEvent(UserInterruptionEvent());
} }
if(hangedUp) {
hangedUp = false;
return InputEvent(HangupEvent());
}
version(Posix) version(Posix)
if(windowSizeChanged) { if(windowSizeChanged) {
return checkWindowSizeChanged(); return checkWindowSizeChanged();
@ -1499,6 +1574,10 @@ struct RealTimeConsoleInput {
e.eventType = ev.bKeyDown ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released; e.eventType = ev.bKeyDown ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released;
ne.eventType = ev.bKeyDown ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.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; e.modifierState = ev.dwControlKeyState;
ne.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 terminal.flush(); // make sure all output is sent out before we try to get input
InputEvent[] charPressAndRelease(dchar character) { InputEvent[] charPressAndRelease(dchar character) {
if((flags & ConsoleInputFlags.releasedKeys))
return [ return [
InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0)), InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0)),
InputEvent(CharacterEvent(CharacterEvent.Type.Released, 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) { InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) {
if((flags & ConsoleInputFlags.releasedKeys))
return [ return [
InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)),
InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers)), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers)),
]; ];
else return [ InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)) ];
} }
char[30] sequenceBuffer; char[30] sequenceBuffer;
@ -1880,7 +1963,7 @@ struct RealTimeConsoleInput {
if(c == -1) if(c == -1)
return null; // interrupted; give back nothing so the other level can recheck signal flags return null; // interrupted; give back nothing so the other level can recheck signal flags
if(c == 0) 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(c == '\033') {
if(timedCheckForInput(50)) { if(timedCheckForInput(50)) {
// escape sequence // escape sequence
@ -2020,8 +2103,16 @@ struct SizeChangedEvent {
} }
/// the user hitting ctrl+c will send this /// the user hitting ctrl+c will send this
/// You should drop what you're doing and perhaps exit when this happens.
struct UserInterruptionEvent {} 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 {} interface CustomEvent {}
version(Windows) version(Windows)
@ -2055,10 +2146,12 @@ struct InputEvent {
enum Type { enum Type {
CharacterEvent, ///. CharacterEvent, ///.
NonCharacterKeyEvent, /// . 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 MouseEvent, /// only sent if you subscribed to mouse events
SizeChangedEvent, /// only sent if you subscribed to size events SizeChangedEvent, /// only sent if you subscribed to size events
UserInterruptionEvent, /// the user hit ctrl+c 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 /// . CustomEvent /// .
} }
@ -2081,6 +2174,10 @@ struct InputEvent {
return sizeChangedEvent; return sizeChangedEvent;
else static if(T == Type.UserInterruptionEvent) else static if(T == Type.UserInterruptionEvent)
return 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) 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");
@ -2111,6 +2208,14 @@ struct InputEvent {
t = Type.UserInterruptionEvent; t = Type.UserInterruptionEvent;
userInterruptionEvent = c; userInterruptionEvent = c;
} }
this(HangupEvent c) {
t = Type.HangupEvent;
hangupEvent = c;
}
this(EndOfFileEvent c) {
t = Type.EndOfFileEvent;
endOfFileEvent = c;
}
this(CustomEvent c) { this(CustomEvent c) {
t = Type.CustomEvent; t = Type.CustomEvent;
customEvent = c; customEvent = c;
@ -2125,6 +2230,8 @@ struct InputEvent {
MouseEvent mouseEvent; MouseEvent mouseEvent;
SizeChangedEvent sizeChangedEvent; SizeChangedEvent sizeChangedEvent;
UserInterruptionEvent userInterruptionEvent; UserInterruptionEvent userInterruptionEvent;
HangupEvent hangupEvent;
EndOfFileEvent endOfFileEvent;
CustomEvent customEvent; CustomEvent customEvent;
} }
} }
@ -2137,16 +2244,22 @@ void main() {
//terminal.color(Color.DEFAULT, Color.DEFAULT); //terminal.color(Color.DEFAULT, Color.DEFAULT);
// //
/*
auto getter = new LineGetter(&terminal, "test"); auto getter = new LineGetter(&terminal, "test");
getter.prompt = "> "; getter.prompt = "> ";
terminal.writeln("\n" ~ getter.getline()); terminal.writeln("\n" ~ getter.getline());
terminal.writeln("\n" ~ getter.getline()); terminal.writeln("\n" ~ getter.getline());
terminal.writeln("\n" ~ getter.getline()); terminal.writeln("\n" ~ getter.getline());
getter.dispose(); getter.dispose();
*/
terminal.writeln(terminal.getline());
terminal.writeln(terminal.getline());
terminal.writeln(terminal.getline());
//input.getch(); //input.getch();
return; // return;
// //
terminal.setTitle("Basic I/O"); terminal.setTitle("Basic I/O");
@ -2165,6 +2278,8 @@ void main() {
terminal.writef("%s\n", event.type); terminal.writef("%s\n", event.type);
final switch(event.type) { final switch(event.type) {
case InputEvent.Type.UserInterruptionEvent: case InputEvent.Type.UserInterruptionEvent:
case InputEvent.Type.HangupEvent:
case InputEvent.Type.EndOfFileEvent:
timeToBreak = true; timeToBreak = true;
version(with_eventloop) { version(with_eventloop) {
import arsd.eventloop; import arsd.eventloop;
@ -2638,12 +2753,17 @@ class LineGetter {
/// returns false when there's nothing more to do /// returns false when there's nothing more to do
bool workOnLine(InputEvent e) { bool workOnLine(InputEvent e) {
switch(e.type) { switch(e.type) {
case InputEvent.Type.EndOfFileEvent:
justHitTab = false;
return false;
break;
case InputEvent.Type.CharacterEvent: case InputEvent.Type.CharacterEvent:
if(e.characterEvent.eventType == CharacterEvent.Type.Released) if(e.characterEvent.eventType == CharacterEvent.Type.Released)
return true; return true;
/* Insert the character (unless it is backspace, tab, or some other control char) */ /* Insert the character (unless it is backspace, tab, or some other control char) */
auto ch = e.characterEvent.character; auto ch = e.characterEvent.character;
switch(ch) { switch(ch) {
case 4: // ctrl+d will also send a newline-equivalent
case '\r': case '\r':
case '\n': case '\n':
justHitTab = false; justHitTab = false;
@ -2779,6 +2899,10 @@ class LineGetter {
/* I'll take this as canceling the line. */ /* I'll take this as canceling the line. */
throw new Exception("user canceled"); // FIXME throw new Exception("user canceled"); // FIXME
break; break;
case InputEvent.Type.HangupEvent:
/* I'll take this as canceling the line. */
throw new Exception("user hanged up"); // FIXME
break;
default: default:
/* ignore. ideally it wouldn't be passed to us anyway! */ /* ignore. ideally it wouldn't be passed to us anyway! */
} }