more getline improvements

This commit is contained in:
Adam D. Ruppe 2014-12-08 23:26:50 -05:00
parent 29d7ca6fe6
commit e483aff1f5
1 changed files with 149 additions and 34 deletions

View File

@ -2070,24 +2070,27 @@ struct InputEvent {
version(Demo) version(Demo)
void main() { void main() {
auto terminal = Terminal(ConsoleOutputType.linear); auto terminal = Terminal(ConsoleOutputType.cellular);
terminal.setTitle("Basic I/O");
auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents);
terminal.color(Color.green | Bright, Color.black);
//terminal.color(Color.DEFAULT, Color.DEFAULT); //terminal.color(Color.DEFAULT, Color.DEFAULT);
// //
auto getter = new LineGetter(&terminal); auto getter = new LineGetter(&terminal, "test");
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());
getter.dispose();
input.getch(); //input.getch();
return; return;
// //
terminal.setTitle("Basic I/O");
auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents);
terminal.color(Color.green | Bright, Color.black);
terminal.write("test some long string to see if it wraps or what because i dont really know what it is going to do so i just want to test i think it will wrap but gotta be sure lolololololololol"); terminal.write("test some long string to see if it wraps or what because i dont really know what it is going to do so i just want to test i think it will wrap but gotta be sure lolololololololol");
terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY);
@ -2166,14 +2169,32 @@ void main() {
} }
} }
/* /**
FIXME: support lines that wrap FIXME: support lines that wrap
FIXME: better controls maybe FIXME: better controls maybe
FIXME: tab completion
FIXME: handle when the thing scrolls cuz of tab complete or something FIXME: fix lengths on prompt and suggestion
FIXME: insert mode vs overstrike mode????
FIXME: read/save file A note on history:
FIXME: add a prompt
To save history, you must call LineGetter.dispose() when you're done with it.
History will not be automatically saved without that call!
The history saving and loading as a trivially encountered race condition: if you
open two programs that use the same one at the same time, the one that closes second
will overwrite any history changes the first closer saved.
GNU Getline does this too... and it actually kinda drives me nuts. But I don't know
what a good fix is except for doing a transactional commit straight to the file every
time and that seems like hitting the disk way too often.
We could also do like a history server like a database daemon that keeps the order
correct but I don't actually like that either because I kinda like different bashes
to have different history, I just don't like it all to get lost.
Regardless though, this isn't even used in bash anyway, so I don't think I care enough
to put that much effort into it. Just using separate files for separate tasks is good
enough I think.
*/ */
class LineGetter { class LineGetter {
/* A note on the assumeSafeAppends in here: since these buffers are private, we can be /* A note on the assumeSafeAppends in here: since these buffers are private, we can be
@ -2185,42 +2206,108 @@ class LineGetter {
// not saved // not saved
Terminal* terminal; Terminal* terminal;
this(Terminal* tty) { string historyFilename;
/// Make sure that the parent terminal struct remains in scope for the duration
/// of LineGetter's lifetime, as it does hold on to and use the passed pointer
/// throughout.
///
/// historyFilename will load and save an input history log to a particular folder.
/// Leaving it null will mean no file will be used and history will not be saved across sessions.
this(Terminal* tty, string historyFilename = null) {
this.terminal = tty; this.terminal = tty;
this.historyFilename = historyFilename;
line.reserve(128); line.reserve(128);
history = ["first", "second", "third"]; if(historyFilename.length)
loadSettingsAndHistoryFromFile();
regularForeground = cast(Color) terminal._currentForeground; regularForeground = cast(Color) terminal._currentForeground;
background = cast(Color) terminal._currentBackground; background = cast(Color) terminal._currentBackground;
suggestionForeground = Color.blue; suggestionForeground = Color.blue;
} }
/// Call this before letting LineGetter die so it can do any necessary
/// cleanup and save the updated history to a file.
void dispose() {
if(historyFilename.length)
saveSettingsAndHistoryToFile();
}
/// Override this to change the directory where history files are stored
///
/// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows.
string historyFileDirectory() {
version(Windows) {
char[1024] path;
// FIXME: this doesn't link because the crappy dmd lib doesn't have it
if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) {
import core.stdc.string;
return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline";
} else {
import std.process;
return environment["APPDATA"] ~ "\\arsd-getline";
}
} else version(Posix) {
import std.process;
return environment["HOME"] ~ "/.arsd-getline";
}
}
/// You can customize the colors here. You should set these after construction, but before
/// calling startGettingLine or getline.
Color suggestionForeground; Color suggestionForeground;
Color regularForeground; Color regularForeground; /// .
Color background; Color background; /// .
//bool reverseVideo; //bool reverseVideo;
/// Set this if you want a prompt to be drawn with the line. It does NOT support color in string.
string prompt;
/// Turn on auto suggest if you want a greyed thing of what tab
/// would be able to fill in as you type.
///
/// You might want to turn it off if generating a completion list is slow.
bool autoSuggest = true;
/// Override this if you don't want all lines added to the history. /// Override this if you don't want all lines added to the history.
/// You can return null to not add it at all, or you can transform it. /// You can return null to not add it at all, or you can transform it.
string historyFilter(string candidate) { string historyFilter(string candidate) {
return candidate; return candidate;
} }
/// You may override this to do nothing
void saveSettingsAndHistoryToFile() { void saveSettingsAndHistoryToFile() {
assert(0); // FIXME import std.file;
if(!exists(historyFileDirectory))
mkdir(historyFileDirectory);
auto fn = historyPath();
import std.stdio;
auto file = File(fn, "wt");
foreach(item; history)
file.writeln(item);
} }
private string historyPath() {
import std.path;
auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ ".history";
return filename;
}
/// You may override this to do nothing
void loadSettingsAndHistoryFromFile() { void loadSettingsAndHistoryFromFile() {
assert(0); // FIXME import std.file;
} history = null;
auto fn = historyPath();
if(exists(fn)) {
import std.stdio;
foreach(line; File(fn, "rt").byLine)
history ~= line.idup;
/// Turn on auto suggest if you want a greyed thing of what tab }
/// would be able to fill in as you type. }
///
/// You might want to turn it off if generating a completion list is slow.
bool autoSuggest = true;
/** /**
Override this to provide tab completion. You may use the candidate Override this to provide tab completion. You may use the candidate
@ -2252,6 +2339,7 @@ class LineGetter {
return f; return f;
} }
/// Override this to provide a custom display of the tab completion list
protected void showTabCompleteList(string[] list) { protected void showTabCompleteList(string[] list) {
if(list.length) { if(list.length) {
// FIXME: allow mouse clicking of an item, that would be cool // FIXME: allow mouse clicking of an item, that would be cool
@ -2361,14 +2449,17 @@ class LineGetter {
line ~= ch; line ~= ch;
else { else {
assert(line.length); assert(line.length);
line ~= ' '; if(insertMode) {
for(int i = line.length - 2; i >= cursorPosition; i --) line ~= ' ';
line[i + 1] = line[i]; for(int i = line.length - 2; i >= cursorPosition; i --)
line[i + 1] = line[i];
}
line[cursorPosition] = ch; line[cursorPosition] = ch;
} }
cursorPosition++; cursorPosition++;
} }
/// .
void addString(string s) { void addString(string s) {
// FIXME: this could be more efficient // FIXME: this could be more efficient
// but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still) // but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still)
@ -2390,6 +2481,9 @@ class LineGetter {
int lastDrawLength = 0; int lastDrawLength = 0;
void redraw() { void redraw() {
terminal.moveTo(startOfLineX, startOfLineY); terminal.moveTo(startOfLineX, startOfLineY);
terminal.write(prompt);
terminal.write(line); terminal.write(line);
auto suggestion = ((cursorPosition == line.length) && autoSuggest) ? this.suggestion() : null; auto suggestion = ((cursorPosition == line.length) && autoSuggest) ? this.suggestion() : null;
if(suggestion.length) { if(suggestion.length) {
@ -2398,16 +2492,18 @@ class LineGetter {
terminal.color(regularForeground, background); terminal.color(regularForeground, background);
} }
if(line.length < lastDrawLength) if(line.length < lastDrawLength)
foreach(i; line.length + suggestion.length .. lastDrawLength) foreach(i; line.length + suggestion.length + prompt.length .. lastDrawLength)
terminal.write(" "); terminal.write(" ");
lastDrawLength = line.length + suggestion.length; // FIXME: graphemes and utf-8 on suggestion lastDrawLength = line.length + suggestion.length + prompt.length; // FIXME: graphemes and utf-8 on suggestion/prompt
// FIXME: wrapping // FIXME: wrapping
terminal.moveTo(startOfLineX + cursorPosition, startOfLineY); terminal.moveTo(startOfLineX + cursorPosition + prompt.length, startOfLineY);
} }
// Make sure that you've flushed your input and output before calling this /// Starts getting a new line. Call workOnLine and finishGettingLine afterward.
// function or else you might lose events or get exceptions from this. ///
/// Make sure that you've flushed your input and output before calling this
/// function or else you might lose events or get exceptions from this.
void startGettingLine() { void startGettingLine() {
// reset from any previous call first // reset from any previous call first
cursorPosition = 0; cursorPosition = 0;
@ -2420,6 +2516,8 @@ class LineGetter {
} }
updateCursorPosition(); updateCursorPosition();
redraw();
} }
private void updateCursorPosition() { private void updateCursorPosition() {
@ -2433,6 +2531,11 @@ class LineGetter {
startOfLineY = info.dwCursorPosition.Y; startOfLineY = info.dwCursorPosition.Y;
} else { } else {
// request current cursor position // request current cursor position
// we have to turn off cooked mode to get this answer, otherwise it will all
// be messed up. (I hate unix terminals, the Windows way is so much easer.)
RealTimeConsoleInput input = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw);
terminal.writeStringRaw("\033[6n"); terminal.writeStringRaw("\033[6n");
terminal.flush(); terminal.flush();
@ -2568,6 +2671,11 @@ class LineGetter {
cursorPosition = line.length; cursorPosition = line.length;
redraw(); redraw();
break; break;
case NonCharacterKeyEvent.Key.Insert:
insertMode = !insertMode;
// FIXME: indicate this on the UI somehow
// like change the cursor or something
break;
case NonCharacterKeyEvent.Key.Delete: case NonCharacterKeyEvent.Key.Delete:
deleteChar(); deleteChar();
redraw(); redraw();
@ -2589,7 +2697,8 @@ class LineGetter {
if(me.eventType == MouseEvent.Type.Pressed) { if(me.eventType == MouseEvent.Type.Pressed) {
if(me.buttons & MouseEvent.Button.Left) { if(me.buttons & MouseEvent.Button.Left) {
if(me.y == startOfLineY) { if(me.y == startOfLineY) {
int p = me.x - startOfLineX; // FIXME: prompt.length should be graphemes or at least code poitns
int p = me.x - startOfLineX - prompt.length;
if(p >= 0 && p < line.length) { if(p >= 0 && p < line.length) {
justHitTab = false; justHitTab = false;
cursorPosition = p; cursorPosition = p;
@ -2625,6 +2734,12 @@ class LineGetter {
} }
} }
version(Windows) {
// to get the directory for saving history in the line things
enum CSIDL_APPDATA = 26;
extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR);
}
/* /*
// more efficient scrolling // more efficient scrolling