lots of line getting improvements and other bugs

This commit is contained in:
Adam D. Ruppe 2020-12-04 13:10:36 -05:00
parent 1f1163c8fd
commit e6ae6138d0
2 changed files with 552 additions and 128 deletions

View File

@ -952,7 +952,7 @@ struct Terminal {
} }
bool clipboardSupported() { bool clipboardSupported() {
version(Win32Console) return true; version(Win32Console) return true;
else return (tcaps & TerminalCapabilities.arsdImage) ? true : false; else return (tcaps & TerminalCapabilities.arsdClipboard) ? true : false;
} }
// only supported on my custom terminal emulator. guarded behind if(inlineImagesSupported) // only supported on my custom terminal emulator. guarded behind if(inlineImagesSupported)
@ -1059,6 +1059,16 @@ struct Terminal {
} }
} }
// it sets the internal selection, you are still responsible for showing to users if need be
// may not work though, check `clipboardSupported` or have some alternate way for the user to use the selection
void requestSetTerminalSelection(string text) {
if(clipboardSupported) {
import std.base64;
writeStringRaw("\033]52;s;"~Base64.encode(cast(ubyte[])text)~"\007");
}
}
bool hasDefaultDarkBackground() { bool hasDefaultDarkBackground() {
version(Win32Console) { version(Win32Console) {
return !(defaultBackgroundColor & 0xf); return !(defaultBackgroundColor & 0xf);
@ -3389,14 +3399,10 @@ struct RealTimeConsoleInput {
case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState); case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState);
// xterm extension for arbitrary keys with arbitrary modifiers // xterm extension for arbitrary keys with arbitrary modifiers
case "27": return keyPressAndRelease2(keyGot, modifierState); case "27": return keyPressAndRelease2(keyGot == '\x1b' ? KeyboardEvent.Key.escape : keyGot, modifierState);
// starting at 70 i do some magic for like shift+enter etc. // starting at 70 im free to do my own but i rolled all but ScrollLock into 27 as of Dec 3, 2020
// this only happens on my own terminal emulator.
case "70": return keyPressAndRelease(NonCharacterKeyEvent.Key.ScrollLock, modifierState); case "70": return keyPressAndRelease(NonCharacterKeyEvent.Key.ScrollLock, modifierState);
case "78": return keyPressAndRelease2('\b', modifierState);
case "79": return keyPressAndRelease2('\t', modifierState);
case "83": return keyPressAndRelease2('\n', modifierState);
default: default:
} }
break; break;
@ -3485,8 +3491,12 @@ struct RealTimeConsoleInput {
$(LIST $(LIST
* Ctrl+space bar sends char 0. * Ctrl+space bar sends char 0.
* Ctrl+ascii characters send char 1 - 26 as chars on all systems. * Ctrl+ascii characters send char 1 - 26 as chars on all systems. Ctrl+shift+ascii is generally not recognizable on Linux, but works on Windows and with my terminal emulator on all systems. Alt+ctrl+ascii, for example Alt+Ctrl+F, is sometimes sent as modifierState = alt|ctrl, key = 'f'. Sometimes modifierState = alt|ctrl, key = 'F'. Sometimes modifierState = ctrl|alt, key = 6. Which one you get depends on the system/terminal and the user's caps lock state. You're probably best off checking all three and being aware it might not work at all.
* Other modifier+key combinations may send random other things or not be detected as it is configuration-specific with no way to detect. It is reasonably reliable for the non-character keys (arrows, F1-F12, Home/End, etc.) but not perfectly so. Some systems just don't send them. * Some combinations like ctrl+i are indistinguishable from other keys like tab.
* Other modifier+key combinations may send random other things or not be detected as it is configuration-specific with no way to detect. It is reasonably reliable for the non-character keys (arrows, F1-F12, Home/End, etc.) but not perfectly so. Some systems just don't send them. If they do though, terminal will try to set `modifierState`.
* Alt+key combinations do not generally work on Windows since the operating system uses that combination for something else.
* Shift is sometimes applied to the character, sometimes set in modifierState, sometimes both, sometimes neither.
* On some systems, the return key sends \r and some sends \n.
) )
+/ +/
struct KeyboardEvent { struct KeyboardEvent {
@ -3496,15 +3506,73 @@ struct KeyboardEvent {
alias character = which; /// I often use this when porting old to new so i took it alias character = which; /// I often use this when porting old to new so i took it
uint modifierState; /// uint modifierState; ///
/// // filter irrelevant modifiers...
uint modifierStateFiltered() const {
uint ms = modifierState;
if(which < 32 && which != 9 && which != 8 && which != '\n')
ms &= ~ModifierState.control;
return ms;
}
/++
Returns true if the event was a normal typed character.
You may also want to check modifiers if you want to process things differently when alt, ctrl, or shift is pressed.
[modifierStateFiltered] returns only modifiers that are special in some way for the typed character. You can bitwise
and that against [ModifierState]'s members to test.
[isUnmodifiedCharacter] does such a check for you.
$(NOTE
Please note that enter, tab, and backspace count as characters.
)
+/
bool isCharacter() { bool isCharacter() {
return !(which >= Key.min && which <= Key.max); return !isNonCharacterKey() && !isProprietary();
}
/++
Returns true if this keyboard event represents a normal character keystroke, with no extraordinary modifier keys depressed.
Shift is considered an ordinary modifier except in the cases of tab, backspace, enter, and the space bar, since it is a normal
part of entering many other characters.
History:
Added December 4, 2020.
+/
bool isUnmodifiedCharacter() {
uint modsInclude = ModifierState.control | ModifierState.alt | ModifierState.meta;
if(which == '\b' || which == '\t' || which == '\n' || which == '\r' || which == ' ' || which == 0)
modsInclude |= ModifierState.shift;
return isCharacter() && (modifierStateFiltered() & modsInclude) == 0;
}
/++
Returns true if the key represents one of the range named entries in the [Key] enum.
This does not necessarily mean it IS one of the named entries, just that it is in the
range. Checking more precisely would require a loop in here and you are better off doing
that in your own `switch` statement, with a do-nothing `default`.
Remember that users can create synthetic input of any character value.
History:
While this function was present before, it was undocumented until December 4, 2020.
+/
bool isNonCharacterKey() {
return which >= Key.min && which <= Key.max;
}
///
bool isProprietary() {
return which >= ProprietaryPseudoKeys.min && which <= ProprietaryPseudoKeys.max;
} }
// these match Windows virtual key codes numerically for simplicity of translation there // these match Windows virtual key codes numerically for simplicity of translation there
// but are plus a unicode private use area offset so i can cram them in the dchar // but are plus a unicode private use area offset so i can cram them in the dchar
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx
/// . /++
Represents non-character keys.
+/
enum Key : dchar { enum Key : dchar {
escape = 0x1b + 0xF0000, /// . escape = 0x1b + 0xF0000, /// .
F1 = 0x70 + 0xF0000, /// . F1 = 0x70 + 0xF0000, /// .
@ -3530,9 +3598,33 @@ struct KeyboardEvent {
PageUp = 0x21 + 0xF0000, /// . PageUp = 0x21 + 0xF0000, /// .
PageDown = 0x22 + 0xF0000, /// . PageDown = 0x22 + 0xF0000, /// .
ScrollLock = 0x91 + 0xF0000, /// unlikely to work outside my custom terminal emulator ScrollLock = 0x91 + 0xF0000, /// unlikely to work outside my custom terminal emulator
/*
Enter = '\n',
Backspace = '\b',
Tab = '\t',
*/
} }
/++
These are extensions added for better interop with the embedded emulator.
As characters inside the unicode private-use area, you shouldn't encounter
them unless you opt in by using some other proprietary feature.
History:
Added December 4, 2020.
+/
enum ProprietaryPseudoKeys : dchar {
/++
If you use [Terminal.requestSetTerminalSelection], you should also process
this pseudo-key to clear the selection when the terminal tells you do to keep
you UI in sync.
History:
Added December 4, 2020.
+/
SelectNone = 0x0 + 0xF1000, // 987136
}
} }
/// Deprecated: use KeyboardEvent instead in new programs /// Deprecated: use KeyboardEvent instead in new programs
@ -3853,7 +3945,7 @@ void main() {
///* ///*
auto getter = new FileLineGetter(&terminal, "test"); auto getter = new FileLineGetter(&terminal, "test");
getter.prompt = "> "; getter.prompt = "> ";
getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; //getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
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());
@ -3908,6 +4000,7 @@ void main() {
break; break;
case InputEvent.Type.KeyboardEvent: case InputEvent.Type.KeyboardEvent:
auto ev = event.get!(InputEvent.Type.KeyboardEvent); auto ev = event.get!(InputEvent.Type.KeyboardEvent);
if(!ev.pressed) break;
terminal.writef("\t%s", ev); terminal.writef("\t%s", ev);
terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which); terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which);
terminal.writeln(); terminal.writeln();
@ -3924,10 +4017,10 @@ void main() {
break; break;
case InputEvent.Type.CharacterEvent: // obsolete case InputEvent.Type.CharacterEvent: // obsolete
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);
break; break;
case InputEvent.Type.NonCharacterKeyEvent: // obsolete case InputEvent.Type.NonCharacterKeyEvent: // obsolete
terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent)); //terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent));
break; break;
case InputEvent.Type.PasteEvent: case InputEvent.Type.PasteEvent:
terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent)); terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent));
@ -4112,7 +4205,7 @@ private uint /* TerminalCapabilities bitmask */ getTerminalCapabilities(int fdIn
private extern(C) int mkstemp(char *templ); private extern(C) int mkstemp(char *templ);
/** /*
FIXME: support lines that wrap FIXME: support lines that wrap
FIXME: better controls maybe FIXME: better controls maybe
@ -4121,11 +4214,19 @@ private extern(C) int mkstemp(char *templ);
hits "class foo { \n" and the app says "that line needs continuation" automatically. hits "class foo { \n" and the app says "that line needs continuation" automatically.
FIXME: fix lengths on prompt and suggestion FIXME: fix lengths on prompt and suggestion
*/
/**
A user-interactive line editor class, used by [Terminal.getline]. It is similar to
GNU readline, offering comparable features like tab completion, history, and graceful
degradation to adapt to the user's terminal.
A note on history: A note on history:
To save history, you must call LineGetter.dispose() when you're done with it. $(WARNING
History will not be automatically saved without that call! 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 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 open two programs that use the same one at the same time, the one that closes second
@ -4264,9 +4365,44 @@ class LineGetter {
mkdir(historyFileDirectory); mkdir(historyFileDirectory);
auto fn = historyPath(); auto fn = historyPath();
import std.stdio; import std.stdio;
auto file = File(fn, "wt"); auto file = File(fn, "wb");
file.write("// getline history file\r\n");
foreach(item; history) foreach(item; history)
file.writeln(item); file.writeln(item, "\r");
}
/// You may override this to do nothing
/* virtual */ void loadSettingsAndHistoryFromFile() {
import std.file;
history = null;
auto fn = historyPath();
if(exists(fn)) {
import std.stdio, std.algorithm, std.string;
string cur;
auto file = File(fn, "rb");
auto first = file.readln();
if(first.startsWith("// getline history file")) {
foreach(chunk; file.byChunk(1024)) {
auto idx = (cast(char[]) chunk).indexOf(cast(char) '\r');
while(idx != -1) {
cur ~= cast(char[]) chunk[0 .. idx];
history ~= cur;
cur = null;
chunk = chunk[idx + 2 .. $]; // skipping \r\n
idx = (cast(char[]) chunk).indexOf(cast(char) '\r');
}
cur ~= cast(char[]) chunk;
}
if(cur.length)
history ~= cur;
} else {
// old-style plain file
history ~= first;
foreach(line; file.byLine())
history ~= line.idup;
}
}
} }
/++ /++
@ -4277,25 +4413,13 @@ class LineGetter {
return ".history"; return ".history";
} }
private string historyPath() { /// semi-private, do not rely upon yet
final string historyPath() {
import std.path; import std.path;
auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension(); auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension();
return filename; return filename;
} }
/// You may override this to do nothing
/* virtual */ void loadSettingsAndHistoryFromFile() {
import std.file;
history = null;
auto fn = historyPath();
if(exists(fn)) {
import std.stdio;
foreach(line; File(fn, "rt").byLine)
history ~= line.idup;
}
}
/++ /++
Override this to provide tab completion. You may use the candidate Override this to provide tab completion. You may use the candidate
argument to filter the list, but you don't have to (LineGetter will argument to filter the list, but you don't have to (LineGetter will
@ -4539,11 +4663,26 @@ class LineGetter {
string editor = environment.get("EDITOR", "vi"); string editor = environment.get("EDITOR", "vi");
} }
// FIXME the spawned process changes terminal state! // FIXME the spawned process changes even more terminal state than set up here!
spawnProcess([editor, tmpName]).wait; try {
import std.string; version(none)
return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp; if(UseVtSequences) {
if(terminal.type == ConsoleOutputType.cellular) {
terminal.doTermcap("te");
}
}
spawnProcess([editor, tmpName]).wait;
if(UseVtSequences) {
if(terminal.type == ConsoleOutputType.cellular)
terminal.doTermcap("ti");
}
import std.string;
return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp;
} catch(Exception e) {
// edit failed, we should prolly tell them but idk how....
return null;
}
} }
//private RealTimeConsoleInput* rtci; //private RealTimeConsoleInput* rtci;
@ -4912,13 +5051,13 @@ class LineGetter {
lineLength--; lineLength--;
} }
void drawContent(T)(T towrite, int highlightBegin = 0, int highlightEnd = 0) { void drawContent(T)(T towrite, int highlightBegin = 0, int highlightEnd = 0, bool inverted = false) {
// FIXME: if there is a color at the end of the line it messes up as you scroll // FIXME: if there is a color at the end of the line it messes up as you scroll
// FIXME: need a way to go to multi-line editing // FIXME: need a way to go to multi-line editing
bool highlightOn = false; bool highlightOn = false;
void highlightOff() { void highlightOff() {
lg.terminal.color(lg.regularForeground, lg.background); lg.terminal.color(lg.regularForeground, lg.background, ForceOption.automatic, inverted);
highlightOn = false; highlightOn = false;
} }
@ -4935,7 +5074,7 @@ class LineGetter {
default: default:
if(highlightEnd) { if(highlightEnd) {
if(idx == highlightBegin) { if(idx == highlightBegin) {
lg.terminal.color(lg.regularForeground, Color.yellow); lg.terminal.color(lg.regularForeground, Color.yellow, ForceOption.automatic, inverted);
highlightOn = true; highlightOn = true;
} }
if(idx == highlightEnd) { if(idx == highlightEnd) {
@ -5015,7 +5154,21 @@ class LineGetter {
auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition; auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition;
auto cursorPositionToDrawY = 0; auto cursorPositionToDrawY = 0;
drawer.drawContent(towrite); if(selectionStart != selectionEnd) {
dchar[] beforeSelection, selection, afterSelection;
beforeSelection = line[0 .. selectionStart];
selection = line[selectionStart .. selectionEnd];
afterSelection = line[selectionEnd .. $];
drawer.drawContent(beforeSelection);
terminal.color(regularForeground, background, ForceOption.automatic, true);
drawer.drawContent(selection, 0, 0, true);
terminal.color(regularForeground, background);
drawer.drawContent(afterSelection);
} else {
drawer.drawContent(towrite);
}
string suggestion; string suggestion;
@ -5273,6 +5426,54 @@ class LineGetter {
private LineGetter supplementalGetter; private LineGetter supplementalGetter;
/* selection helpers */
protected {
// make sure you set the anchor first
void extendSelectionToCursor() {
if(cursorPosition < selectionStart)
selectionStart = cursorPosition;
else if(cursorPosition > selectionEnd)
selectionEnd = cursorPosition;
terminal.requestSetTerminalSelection(getSelection());
}
void setSelectionAnchorToCursor() {
if(selectionStart == -1)
selectionStart = selectionEnd = cursorPosition;
}
void sanitizeSelection() {
if(selectionStart == selectionEnd)
return;
if(selectionStart < 0 || selectionEnd < 0 || selectionStart > line.length || selectionEnd > line.length)
selectNone();
}
}
public {
// redraw after calling this
void selectAll() {
selectionStart = 0;
selectionEnd = cast(int) line.length;
}
// redraw after calling this
void selectNone() {
selectionStart = selectionEnd = -1;
}
string getSelection() {
sanitizeSelection();
if(selectionStart == selectionEnd)
return null;
import std.conv;
return to!string(line[selectionStart .. selectionEnd]);
}
}
private {
int selectionStart = -1;
int selectionEnd = -1;
}
/++ /++
for integrating into another event loop for integrating into another event loop
you can pass individual events to this and you can pass individual events to this and
@ -5316,6 +5517,10 @@ class LineGetter {
if(!(ev.modifierState & ModifierState.control)) if(!(ev.modifierState & ModifierState.control))
goto default; goto default;
goto case; goto case;
case KeyboardEvent.ProprietaryPseudoKeys.SelectNone:
selectNone();
redraw();
break;
case 'd', 4: // ctrl+d will also send a newline-equivalent case 'd', 4: // ctrl+d will also send a newline-equivalent
if(!(ev.modifierState & ModifierState.control)) if(!(ev.modifierState & ModifierState.control))
goto default; goto default;
@ -5325,10 +5530,22 @@ class LineGetter {
case '\r': case '\r':
case '\n': case '\n':
justHitTab = justKilled = false; justHitTab = justKilled = false;
if(ev.modifierState & ModifierState.shift) {
addChar('\n');
redraw();
break;
}
return false; return false;
case '\t': case '\t':
justKilled = false; justKilled = false;
if(ev.modifierState & ModifierState.shift) {
justHitTab = false;
addChar('\t');
redraw();
break;
}
auto relevantLineSection = line[0 .. cursorPosition]; auto relevantLineSection = line[0 .. cursorPosition];
auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]);
relevantLineSection = relevantLineSection[start .. $]; relevantLineSection = relevantLineSection[start .. $];
@ -5404,13 +5621,16 @@ class LineGetter {
break; break;
case KeyboardEvent.Key.F2: case KeyboardEvent.Key.F2:
justHitTab = justKilled = false; justHitTab = justKilled = false;
line = editLineInEditor(line, cursorPosition); auto got = editLineInEditor(line, cursorPosition);
if(cursorPosition > line.length) if(got !is null) {
cursorPosition = cast(int) line.length; line = got;
if(horizontalScrollPosition > line.length) if(cursorPosition > line.length)
horizontalScrollPosition = cast(int) line.length; cursorPosition = cast(int) line.length;
positionCursor(); if(horizontalScrollPosition > line.length)
redraw(); horizontalScrollPosition = cast(int) line.length;
positionCursor();
redraw();
}
break; break;
case 'r', 18: case 'r', 18:
if(!(ev.modifierState & ModifierState.control)) if(!(ev.modifierState & ModifierState.control))
@ -5446,11 +5666,39 @@ class LineGetter {
if(!(ev.modifierState & ModifierState.control)) if(!(ev.modifierState & ModifierState.control))
goto default; goto default;
justHitTab = justKilled = false; justHitTab = justKilled = false;
// FIXME: find matching delimiter // FIXME: would be cool if this worked with quotes and such too
if(cursorPosition >= 0 && cursorPosition < line.length) {
dchar at = line[cursorPosition];
int direction;
dchar lookFor;
switch(at) {
case '(': direction = 1; lookFor = ')'; break;
case '[': direction = 1; lookFor = ']'; break;
case '{': direction = 1; lookFor = '}'; break;
case ')': direction = -1; lookFor = '('; break;
case ']': direction = -1; lookFor = '['; break;
case '}': direction = -1; lookFor = '{'; break;
default:
}
if(direction) {
int pos = cursorPosition;
int count;
while(pos >= 0 && pos < line.length) {
if(line[pos] == at)
count++;
if(line[pos] == lookFor)
count--;
if(count == 0) {
cursorPosition = pos;
redraw();
break;
}
pos += direction;
}
}
}
break; break;
// FIXME: history should store original paste as blobs in the history file
// FIXME: on history let it filter by prefix if desired
// FIXME: should be able to update the selection with shift+arrows as well as mouse // FIXME: should be able to update the selection with shift+arrows as well as mouse
// if terminal emulator supports this, it can formally select it to the buffer for copy // if terminal emulator supports this, it can formally select it to the buffer for copy
// and sending to primary on X11 (do NOT do it on Windows though!!!) // and sending to primary on X11 (do NOT do it on Windows though!!!)
@ -5476,11 +5724,22 @@ class LineGetter {
break; break;
case KeyboardEvent.Key.LeftArrow: case KeyboardEvent.Key.LeftArrow:
justHitTab = justKilled = false; justHitTab = justKilled = false;
/*
if(ev.modifierState & ModifierState.shift)
setSelectionAnchorToCursor();
*/
if(ev.modifierState & ModifierState.control) if(ev.modifierState & ModifierState.control)
wordBack(); wordBack();
else if(cursorPosition) else if(cursorPosition)
charBack(); charBack();
/*
if(ev.modifierState & ModifierState.shift)
extendSelectionToCursor();
*/
redraw(); redraw();
break; break;
case KeyboardEvent.Key.RightArrow: case KeyboardEvent.Key.RightArrow:
@ -5522,6 +5781,13 @@ class LineGetter {
case 'a', 1: // this one conflicts with Windows-style select all... case 'a', 1: // this one conflicts with Windows-style select all...
if(!(ev.modifierState & ModifierState.control)) if(!(ev.modifierState & ModifierState.control))
goto default; goto default;
if(ev.modifierState & ModifierState.shift) {
// ctrl+shift+a will select all...
// for now I will have it just copy to clipboard but later once I get the time to implement full selection handling, I'll change it
import std.conv;
terminal.requestCopyToClipboard(to!string(line));
break;
}
goto case; goto case;
case KeyboardEvent.Key.Home: case KeyboardEvent.Key.Home:
justHitTab = justKilled = false; justHitTab = justKilled = false;
@ -5560,7 +5826,7 @@ class LineGetter {
rtti.requestPasteFromClipboard(); rtti.requestPasteFromClipboard();
} else if(ev.modifierState & ModifierState.control) { } else if(ev.modifierState & ModifierState.control) {
// copy // copy
// FIXME // FIXME we could try requesting it though this control unlikely to even come
} else { } else {
insertMode = !insertMode; insertMode = !insertMode;
@ -7230,17 +7496,14 @@ version(TerminalDirectToEmulator) {
} }
protected override void pasteFromClipboard(void delegate(in char[]) dg) { protected override void pasteFromClipboard(void delegate(in char[]) dg) {
static if(UsingSimpledisplayX11) getClipboardText(widget.parentWindow.win, (in char[] dataIn) {
getPrimarySelection(widget.parentWindow.win, dg); char[] data;
else // change Windows \r\n to plain \n
getClipboardText(widget.parentWindow.win, (in char[] dataIn) { foreach(char ch; dataIn)
char[] data; if(ch != 13)
// change Windows \r\n to plain \n data ~= ch;
foreach(char ch; dataIn) dg(data);
if(ch != 13) });
data ~= ch;
dg(data);
});
} }
protected override void copyToPrimary(string text) { protected override void copyToPrimary(string text) {
@ -7406,42 +7669,19 @@ version(TerminalDirectToEmulator) {
return; return;
} }
static string magic() { defaultKeyHandler!(typeof(ev.key))(
string code; ev.key
foreach(member; __traits(allMembers, TerminalKey)) , (ev.state & ModifierState.shift)?true:false
if(member != "Escape") , (ev.state & ModifierState.alt)?true:false
code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ " , (ev.state & ModifierState.ctrl)?true:false
, (ev.state & ModifierState.shift)?true:false , (ev.state & ModifierState.windows)?true:false
, (ev.state & ModifierState.alt)?true:false );
, (ev.state & ModifierState.ctrl)?true:false
, (ev.state & ModifierState.windows)?true:false
)) redraw(); break;";
return code;
}
switch(ev.key) {
mixin(magic());
default:
// keep going, not special
}
return; // the character event handler will do others return; // the character event handler will do others
}); });
widget.addEventListener("char", (Event ev) { widget.addEventListener("char", (Event ev) {
dchar c = ev.character; dchar c = ev.character;
if(skipNextChar) {
skipNextChar = false;
return;
}
endScrollback();
char[4] str;
import std.utf;
if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
auto data = str[0 .. encode(str, c)];
if(c == 0x1c) /* ctrl+\, force quit */ { if(c == 0x1c) /* ctrl+\, force quit */ {
version(Posix) { version(Posix) {
@ -7466,9 +7706,8 @@ version(TerminalDirectToEmulator) {
widget.term.interrupted = true; widget.term.interrupted = true;
outgoingSignal.notify(); outgoingSignal.notify();
} }
} else if(c != 127) { } else {
// on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler. defaultCharHandler(c);
sendToApplication(data);
} }
}); });
} }

View File

@ -242,7 +242,20 @@ class TerminalEmulator {
sendToApplication("\033[201~"); sendToApplication("\033[201~");
} }
private string overriddenSelection;
protected void cancelOverriddenSelection() {
if(overriddenSelection.length == 0)
return;
overriddenSelection = null;
sendToApplication("\033[27;0;987136~"); // fake "select none" key, see terminal.d's ProprietaryPseudoKeys for values.
// The reason that proprietary thing is ok is setting the selection is itself a proprietary extension
// so if it was ever set, it implies the user code is familiar with our magic.
}
public string getSelectedText() { public string getSelectedText() {
if(overriddenSelection.length)
return overriddenSelection;
return getPlainText(selectionStart, selectionEnd); return getPlainText(selectionStart, selectionEnd);
} }
@ -385,6 +398,7 @@ class TerminalEmulator {
cell.selected = false; cell.selected = false;
} }
cancelOverriddenSelection();
selectionEnd = idx; selectionEnd = idx;
// and the freshly selected portion needs to be invalidated // and the freshly selected portion needs to be invalidated
@ -464,6 +478,8 @@ class TerminalEmulator {
// we invalidate the old selection since it should no longer be highlighted... // we invalidate the old selection since it should no longer be highlighted...
makeSelectionOffsetsSane(selectionStart, selectionEnd); makeSelectionOffsetsSane(selectionStart, selectionEnd);
cancelOverriddenSelection();
auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen); auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen);
foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) { foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) {
cell.invalidated = true; cell.invalidated = true;
@ -505,6 +521,8 @@ class TerminalEmulator {
int changed1; int changed1;
int changed2; int changed2;
cancelOverriddenSelection();
auto click = termY * screenWidth + termX; auto click = termY * screenWidth + termX;
if(click < selectionStart) { if(click < selectionStart) {
auto oldSelectionStart = selectionStart; auto oldSelectionStart = selectionStart;
@ -553,6 +571,150 @@ class TerminalEmulator {
private int selectionStart; // an offset into the screen buffer private int selectionStart; // an offset into the screen buffer
private int selectionEnd; // ditto private int selectionEnd; // ditto
void requestRedraw() {}
private bool skipNextChar;
// assuming Key is an enum with members just like the one in simpledisplay.d
// returns true if it was handled here
protected bool defaultKeyHandler(Key)(Key key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
enum bool KeyHasNamedAscii = is(typeof(Key.A));
static string magic() {
string code;
foreach(member; __traits(allMembers, TerminalKey))
if(member != "Escape")
code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
, shift ?true:false
, alt ?true:false
, ctrl ?true:false
, windows ?true:false
)) requestRedraw(); return true;";
return code;
}
void specialAscii(dchar what) {
if(!alt)
skipNextChar = true;
if(sendKeyToApplication(
cast(TerminalKey) what
, shift ? true:false
, alt ? true:false
, ctrl ? true:false
, windows ? true:false
)) requestRedraw();
}
static if(KeyHasNamedAscii) {
enum Space = Key.Space;
enum Enter = Key.Enter;
enum Backspace = Key.Backspace;
enum Tab = Key.Tab;
enum Escape = Key.Escape;
} else {
enum Space = ' ';
enum Enter = '\n';
enum Backspace = '\b';
enum Tab = '\t';
enum Escape = '\033';
}
switch(key) {
//// I want the escape key to send twice to differentiate it from
//// other escape sequences easily.
//case Key.Escape: sendToApplication("\033"); break;
/*
case Key.V:
case Key.C:
if(shift && ctrl) {
skipNextChar = true;
if(key == Key.V)
pasteFromClipboard(&sendPasteData);
else if(key == Key.C)
copyToClipboard(getSelectedText());
}
break;
*/
// expansion of my own for like shift+enter to terminal.d users
case Enter, Backspace, Tab, Escape:
if(shift || alt || ctrl) {
static if(KeyHasNamedAscii) {
specialAscii(
cast(TerminalKey) (
key == Key.Enter ? '\n' :
key == Key.Tab ? '\t' :
key == Key.Backspace ? '\b' :
key == Key.Escape ? '\033' :
0 /* assert(0) */
)
);
} else {
specialAscii(key);
}
return true;
}
break;
case Space:
if(shift || alt) {
// ctrl+space sends 0 per normal translation char rules
specialAscii(' ');
return true;
}
break;
mixin(magic());
static if(is(typeof(Key.Shift))) {
// modifiers are not ascii, ignore them here
case Key.Shift, Key.Ctrl, Key.Alt, Key.Windows, Key.Alt_r, Key.Shift_r, Key.Ctrl_r, Key.CapsLock, Key.NumLock:
// nor are these special keys that don't return characters
case Key.Menu, Key.Pause, Key.PrintScreen:
return false;
}
default:
// alt basically always get special treatment, since it doesn't
// generate anything from the char handler. but shift and ctrl
// do, so we'll just use that unless both are pressed, in which
// case I want to go custom to differentiate like ctrl+c from ctrl+shift+c and such.
// FIXME: xterm offers some control on this, see: https://invisible-island.net/xterm/xterm.faq.html#xterm_modother
if(alt || (shift && ctrl)) {
if(key >= 'A' && key <= 'Z')
key += 32; // always use lowercase for as much consistency as we can since the shift modifier need not apply here. Windows' keysyms are uppercase while X's are lowercase too
specialAscii(key);
if(!alt)
skipNextChar = true;
return true;
}
}
return true;
}
protected bool defaultCharHandler(dchar c) {
if(skipNextChar) {
skipNextChar = false;
return true;
}
endScrollback();
char[4] str;
char[5] send;
import std.utf;
//if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
auto data = str[0 .. encode(str, c)];
// on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler.
if(c != 127)
sendToApplication(data);
return true;
}
/// Send a non-character key sequence /// Send a non-character key sequence
public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) { public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
bool redrawRequired = false; bool redrawRequired = false;
@ -596,7 +758,7 @@ class TerminalEmulator {
void sendToApplicationModified(string s) { void sendToApplicationModified(string s, int key = 0) {
bool anyModifier = shift || alt || ctrl || windows; bool anyModifier = shift || alt || ctrl || windows;
if(!anyModifier || applicationCursorKeys) if(!anyModifier || applicationCursorKeys)
sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh
@ -643,6 +805,11 @@ class TerminalEmulator {
if(otherModifier) if(otherModifier)
buffer ~= otherModifier; buffer ~= otherModifier;
buffer ~= modifierNumber[]; buffer ~= modifierNumber[];
if(key) {
buffer ~= ";";
import std.conv;
buffer ~= to!string(key);
}
buffer ~= terminator; buffer ~= terminator;
// the xterm style is last bit tell us what it is // the xterm style is last bit tell us what it is
sendToApplication(buffer[]); sendToApplication(buffer[]);
@ -652,7 +819,7 @@ class TerminalEmulator {
alias TerminalKey Key; alias TerminalKey Key;
import std.stdio; import std.stdio;
// writefln("Key: %x", cast(int) key); // writefln("Key: %x", cast(int) key);
final switch(key) { switch(key) {
case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break; case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break;
case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break; case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break;
case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break; case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break;
@ -683,13 +850,12 @@ class TerminalEmulator {
case Key.Escape: sendToApplicationModified("\033"); break; case Key.Escape: sendToApplicationModified("\033"); break;
// my extensions // my extensions, see terminator.d for the other side of it
case Key.ScrollLock: sendToApplicationModified("\033[70~"); break; case Key.ScrollLock: sendToApplicationModified("\033[70~"); break;
// see terminal.d for the other side of this // xterm extension for arbitrary modified unicode chars
case cast(TerminalKey) '\n': sendToApplicationModified("\033[83~"); break; default:
case cast(TerminalKey) '\b': sendToApplicationModified("\033[78~"); break; sendToApplicationModified("\033[27~", key);
case cast(TerminalKey) '\t': sendToApplicationModified("\033[79~"); break;
} }
return redrawRequired; return redrawRequired;
@ -2061,6 +2227,11 @@ class TerminalEmulator {
protected bool invalidateAll; protected bool invalidateAll;
void clearSelection() { void clearSelection() {
clearSelectionInternal();
cancelOverriddenSelection();
}
private void clearSelectionInternal() {
foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen) foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen)
if(tc.selected) { if(tc.selected) {
tc.selected = false; tc.selected = false;
@ -2252,6 +2423,10 @@ P s = 2 3 ; 2 → Restore xterm window title from stack.
// copy/paste control // copy/paste control
// echo -e "\033]52;p;?\007" // echo -e "\033]52;p;?\007"
// the p == primary // the p == primary
// c == clipboard
// q == secondary
// s == selection
// 0-7, cut buffers
// the data after it is either base64 stuff to copy or ? to request a paste // the data after it is either base64 stuff to copy or ? to request a paste
if(arg == "p;?") { if(arg == "p;?") {
@ -2280,6 +2455,16 @@ P s = 2 3 ; 2 → Restore xterm window title from stack.
} catch(Exception e) {} } catch(Exception e) {}
} }
// selection
if(arg.length > 2 && arg[0 .. 2] == "s;") {
auto info = arg[2 .. $];
try {
import std.base64;
auto data = Base64.decode(info);
clearSelectionInternal();
overriddenSelection = cast(string) data;
} catch(Exception e) {}
}
break; break;
case "4": case "4":
// palette change or query // palette change or query
@ -3066,30 +3251,30 @@ URXVT (1015)
// These match the numbers in terminal.d, so you can just cast it back and forth // These match the numbers in terminal.d, so you can just cast it back and forth
// and the names match simpledisplay.d so you can convert that automatically too // and the names match simpledisplay.d so you can convert that automatically too
enum TerminalKey : int { enum TerminalKey : int {
Escape = 0x1b,// + 0xF0000, /// . Escape = 0x1b + 0xF0000, /// .
F1 = 0x70,// + 0xF0000, /// . F1 = 0x70 + 0xF0000, /// .
F2 = 0x71,// + 0xF0000, /// . F2 = 0x71 + 0xF0000, /// .
F3 = 0x72,// + 0xF0000, /// . F3 = 0x72 + 0xF0000, /// .
F4 = 0x73,// + 0xF0000, /// . F4 = 0x73 + 0xF0000, /// .
F5 = 0x74,// + 0xF0000, /// . F5 = 0x74 + 0xF0000, /// .
F6 = 0x75,// + 0xF0000, /// . F6 = 0x75 + 0xF0000, /// .
F7 = 0x76,// + 0xF0000, /// . F7 = 0x76 + 0xF0000, /// .
F8 = 0x77,// + 0xF0000, /// . F8 = 0x77 + 0xF0000, /// .
F9 = 0x78,// + 0xF0000, /// . F9 = 0x78 + 0xF0000, /// .
F10 = 0x79,// + 0xF0000, /// . F10 = 0x79 + 0xF0000, /// .
F11 = 0x7A,// + 0xF0000, /// . F11 = 0x7A + 0xF0000, /// .
F12 = 0x7B,// + 0xF0000, /// . F12 = 0x7B + 0xF0000, /// .
Left = 0x25,// + 0xF0000, /// . Left = 0x25 + 0xF0000, /// .
Right = 0x27,// + 0xF0000, /// . Right = 0x27 + 0xF0000, /// .
Up = 0x26,// + 0xF0000, /// . Up = 0x26 + 0xF0000, /// .
Down = 0x28,// + 0xF0000, /// . Down = 0x28 + 0xF0000, /// .
Insert = 0x2d,// + 0xF0000, /// . Insert = 0x2d + 0xF0000, /// .
Delete = 0x2e,// + 0xF0000, /// . Delete = 0x2e + 0xF0000, /// .
Home = 0x24,// + 0xF0000, /// . Home = 0x24 + 0xF0000, /// .
End = 0x23,// + 0xF0000, /// . End = 0x23 + 0xF0000, /// .
PageUp = 0x21,// + 0xF0000, /// . PageUp = 0x21 + 0xF0000, /// .
PageDown = 0x22,// + 0xF0000, /// . PageDown = 0x22 + 0xF0000, /// .
ScrollLock = 0x91, ScrollLock = 0x91 + 0xF0000,
} }
/* These match simpledisplay.d which match terminal.d, so you can just cast them */ /* These match simpledisplay.d which match terminal.d, so you can just cast them */