diff --git a/terminal.d b/terminal.d index f1d11a9..eca6b01 100644 --- a/terminal.d +++ b/terminal.d @@ -952,7 +952,7 @@ struct Terminal { } bool clipboardSupported() { 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) @@ -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() { version(Win32Console) { return !(defaultBackgroundColor & 0xf); @@ -3389,14 +3399,10 @@ struct RealTimeConsoleInput { case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState); // 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. - // this only happens on my own terminal emulator. + // starting at 70 im free to do my own but i rolled all but ScrollLock into 27 as of Dec 3, 2020 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: } break; @@ -3485,8 +3491,12 @@ struct RealTimeConsoleInput { $(LIST * Ctrl+space bar sends char 0. - * Ctrl+ascii characters send char 1 - 26 as chars on all systems. - * 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. + * 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. + * 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 { @@ -3496,15 +3506,73 @@ struct KeyboardEvent { alias character = which; /// I often use this when porting old to new so i took it 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() { - 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 // 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 - /// . + /++ + Represents non-character keys. + +/ enum Key : dchar { escape = 0x1b + 0xF0000, /// . F1 = 0x70 + 0xF0000, /// . @@ -3530,9 +3598,33 @@ struct KeyboardEvent { PageUp = 0x21 + 0xF0000, /// . PageDown = 0x22 + 0xF0000, /// . 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 @@ -3853,7 +3945,7 @@ void main() { ///* auto getter = new FileLineGetter(&terminal, "test"); getter.prompt = "> "; - getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; + //getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; terminal.writeln("\n" ~ getter.getline()); terminal.writeln("\n" ~ getter.getline()); terminal.writeln("\n" ~ getter.getline()); @@ -3908,6 +4000,7 @@ void main() { break; case InputEvent.Type.KeyboardEvent: auto ev = event.get!(InputEvent.Type.KeyboardEvent); + if(!ev.pressed) break; terminal.writef("\t%s", ev); terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which); terminal.writeln(); @@ -3924,10 +4017,10 @@ void main() { break; case InputEvent.Type.CharacterEvent: // obsolete auto ev = event.get!(InputEvent.Type.CharacterEvent); - terminal.writef("\t%s\n", ev); + //terminal.writef("\t%s\n", ev); break; 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; case 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); -/** +/* FIXME: support lines that wrap 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. 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: - To save history, you must call LineGetter.dispose() when you're done with it. - History will not be automatically saved without that call! + $(WARNING + 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 @@ -4264,9 +4365,44 @@ class LineGetter { mkdir(historyFileDirectory); auto fn = historyPath(); import std.stdio; - auto file = File(fn, "wt"); + auto file = File(fn, "wb"); + file.write("// getline history file\r\n"); 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"; } - private string historyPath() { + /// semi-private, do not rely upon yet + final string historyPath() { import std.path; auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension(); 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 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"); } - // FIXME the spawned process changes terminal state! + // FIXME the spawned process changes even more terminal state than set up here! - spawnProcess([editor, tmpName]).wait; - import std.string; - return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp; + try { + version(none) + 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; @@ -4912,13 +5051,13 @@ class LineGetter { 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: need a way to go to multi-line editing bool highlightOn = false; void highlightOff() { - lg.terminal.color(lg.regularForeground, lg.background); + lg.terminal.color(lg.regularForeground, lg.background, ForceOption.automatic, inverted); highlightOn = false; } @@ -4935,7 +5074,7 @@ class LineGetter { default: if(highlightEnd) { if(idx == highlightBegin) { - lg.terminal.color(lg.regularForeground, Color.yellow); + lg.terminal.color(lg.regularForeground, Color.yellow, ForceOption.automatic, inverted); highlightOn = true; } if(idx == highlightEnd) { @@ -5015,7 +5154,21 @@ class LineGetter { auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition; 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; @@ -5273,6 +5426,54 @@ class LineGetter { 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 you can pass individual events to this and @@ -5316,6 +5517,10 @@ class LineGetter { if(!(ev.modifierState & ModifierState.control)) goto default; goto case; + case KeyboardEvent.ProprietaryPseudoKeys.SelectNone: + selectNone(); + redraw(); + break; case 'd', 4: // ctrl+d will also send a newline-equivalent if(!(ev.modifierState & ModifierState.control)) goto default; @@ -5325,10 +5530,22 @@ class LineGetter { case '\r': case '\n': justHitTab = justKilled = false; + if(ev.modifierState & ModifierState.shift) { + addChar('\n'); + redraw(); + break; + } return false; case '\t': justKilled = false; + if(ev.modifierState & ModifierState.shift) { + justHitTab = false; + addChar('\t'); + redraw(); + break; + } + auto relevantLineSection = line[0 .. cursorPosition]; auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); relevantLineSection = relevantLineSection[start .. $]; @@ -5404,13 +5621,16 @@ class LineGetter { break; case KeyboardEvent.Key.F2: justHitTab = justKilled = false; - line = editLineInEditor(line, cursorPosition); - if(cursorPosition > line.length) - cursorPosition = cast(int) line.length; - if(horizontalScrollPosition > line.length) - horizontalScrollPosition = cast(int) line.length; - positionCursor(); - redraw(); + auto got = editLineInEditor(line, cursorPosition); + if(got !is null) { + line = got; + if(cursorPosition > line.length) + cursorPosition = cast(int) line.length; + if(horizontalScrollPosition > line.length) + horizontalScrollPosition = cast(int) line.length; + positionCursor(); + redraw(); + } break; case 'r', 18: if(!(ev.modifierState & ModifierState.control)) @@ -5446,11 +5666,39 @@ class LineGetter { if(!(ev.modifierState & ModifierState.control)) goto default; 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; - // 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 // 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!!!) @@ -5476,11 +5724,22 @@ class LineGetter { break; case KeyboardEvent.Key.LeftArrow: justHitTab = justKilled = false; + + /* + if(ev.modifierState & ModifierState.shift) + setSelectionAnchorToCursor(); + */ + if(ev.modifierState & ModifierState.control) wordBack(); else if(cursorPosition) charBack(); + /* + if(ev.modifierState & ModifierState.shift) + extendSelectionToCursor(); + */ + redraw(); break; case KeyboardEvent.Key.RightArrow: @@ -5522,6 +5781,13 @@ class LineGetter { case 'a', 1: // this one conflicts with Windows-style select all... if(!(ev.modifierState & ModifierState.control)) 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; case KeyboardEvent.Key.Home: justHitTab = justKilled = false; @@ -5560,7 +5826,7 @@ class LineGetter { rtti.requestPasteFromClipboard(); } else if(ev.modifierState & ModifierState.control) { // copy - // FIXME + // FIXME we could try requesting it though this control unlikely to even come } else { insertMode = !insertMode; @@ -7230,17 +7496,14 @@ version(TerminalDirectToEmulator) { } protected override void pasteFromClipboard(void delegate(in char[]) dg) { - static if(UsingSimpledisplayX11) - getPrimarySelection(widget.parentWindow.win, dg); - else - getClipboardText(widget.parentWindow.win, (in char[] dataIn) { - char[] data; - // change Windows \r\n to plain \n - foreach(char ch; dataIn) - if(ch != 13) - data ~= ch; - dg(data); - }); + getClipboardText(widget.parentWindow.win, (in char[] dataIn) { + char[] data; + // change Windows \r\n to plain \n + foreach(char ch; dataIn) + if(ch != 13) + data ~= ch; + dg(data); + }); } protected override void copyToPrimary(string text) { @@ -7406,42 +7669,19 @@ version(TerminalDirectToEmulator) { return; } - static string magic() { - string code; - foreach(member; __traits(allMembers, TerminalKey)) - if(member != "Escape") - code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ " - , (ev.state & ModifierState.shift)?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 - } + defaultKeyHandler!(typeof(ev.key))( + ev.key + , (ev.state & ModifierState.shift)?true:false + , (ev.state & ModifierState.alt)?true:false + , (ev.state & ModifierState.ctrl)?true:false + , (ev.state & ModifierState.windows)?true:false + ); return; // the character event handler will do others }); widget.addEventListener("char", (Event ev) { 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 */ { version(Posix) { @@ -7466,9 +7706,8 @@ version(TerminalDirectToEmulator) { widget.term.interrupted = true; outgoingSignal.notify(); } - } else if(c != 127) { - // 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. - sendToApplication(data); + } else { + defaultCharHandler(c); } }); } diff --git a/terminalemulator.d b/terminalemulator.d index b1902d9..5da6f31 100644 --- a/terminalemulator.d +++ b/terminalemulator.d @@ -242,7 +242,20 @@ class TerminalEmulator { 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() { + if(overriddenSelection.length) + return overriddenSelection; return getPlainText(selectionStart, selectionEnd); } @@ -385,6 +398,7 @@ class TerminalEmulator { cell.selected = false; } + cancelOverriddenSelection(); selectionEnd = idx; // 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... makeSelectionOffsetsSane(selectionStart, selectionEnd); + cancelOverriddenSelection(); + auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen); foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) { cell.invalidated = true; @@ -505,6 +521,8 @@ class TerminalEmulator { int changed1; int changed2; + cancelOverriddenSelection(); + auto click = termY * screenWidth + termX; if(click < selectionStart) { auto oldSelectionStart = selectionStart; @@ -553,6 +571,150 @@ class TerminalEmulator { private int selectionStart; // an offset into the screen buffer 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 public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = 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; if(!anyModifier || applicationCursorKeys) sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh @@ -643,6 +805,11 @@ class TerminalEmulator { if(otherModifier) buffer ~= otherModifier; buffer ~= modifierNumber[]; + if(key) { + buffer ~= ";"; + import std.conv; + buffer ~= to!string(key); + } buffer ~= terminator; // the xterm style is last bit tell us what it is sendToApplication(buffer[]); @@ -652,7 +819,7 @@ class TerminalEmulator { alias TerminalKey Key; import std.stdio; // writefln("Key: %x", cast(int) key); - final switch(key) { + switch(key) { case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break; case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break; case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break; @@ -683,13 +850,12 @@ class TerminalEmulator { 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; - // see terminal.d for the other side of this - case cast(TerminalKey) '\n': sendToApplicationModified("\033[83~"); break; - case cast(TerminalKey) '\b': sendToApplicationModified("\033[78~"); break; - case cast(TerminalKey) '\t': sendToApplicationModified("\033[79~"); break; + // xterm extension for arbitrary modified unicode chars + default: + sendToApplicationModified("\033[27~", key); } return redrawRequired; @@ -2061,6 +2227,11 @@ class TerminalEmulator { protected bool invalidateAll; void clearSelection() { + clearSelectionInternal(); + cancelOverriddenSelection(); + } + + private void clearSelectionInternal() { foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen) if(tc.selected) { tc.selected = false; @@ -2252,6 +2423,10 @@ P s = 2 3 ; 2 → Restore xterm window title from stack. // copy/paste control // echo -e "\033]52;p;?\007" // 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 if(arg == "p;?") { @@ -2280,6 +2455,16 @@ P s = 2 3 ; 2 → Restore xterm window title from stack. } 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; case "4": // 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 // and the names match simpledisplay.d so you can convert that automatically too enum TerminalKey : int { - Escape = 0x1b,// + 0xF0000, /// . - F1 = 0x70,// + 0xF0000, /// . - F2 = 0x71,// + 0xF0000, /// . - F3 = 0x72,// + 0xF0000, /// . - F4 = 0x73,// + 0xF0000, /// . - F5 = 0x74,// + 0xF0000, /// . - F6 = 0x75,// + 0xF0000, /// . - F7 = 0x76,// + 0xF0000, /// . - F8 = 0x77,// + 0xF0000, /// . - F9 = 0x78,// + 0xF0000, /// . - F10 = 0x79,// + 0xF0000, /// . - F11 = 0x7A,// + 0xF0000, /// . - F12 = 0x7B,// + 0xF0000, /// . - Left = 0x25,// + 0xF0000, /// . - Right = 0x27,// + 0xF0000, /// . - Up = 0x26,// + 0xF0000, /// . - Down = 0x28,// + 0xF0000, /// . - Insert = 0x2d,// + 0xF0000, /// . - Delete = 0x2e,// + 0xF0000, /// . - Home = 0x24,// + 0xF0000, /// . - End = 0x23,// + 0xF0000, /// . - PageUp = 0x21,// + 0xF0000, /// . - PageDown = 0x22,// + 0xF0000, /// . - ScrollLock = 0x91, + Escape = 0x1b + 0xF0000, /// . + F1 = 0x70 + 0xF0000, /// . + F2 = 0x71 + 0xF0000, /// . + F3 = 0x72 + 0xF0000, /// . + F4 = 0x73 + 0xF0000, /// . + F5 = 0x74 + 0xF0000, /// . + F6 = 0x75 + 0xF0000, /// . + F7 = 0x76 + 0xF0000, /// . + F8 = 0x77 + 0xF0000, /// . + F9 = 0x78 + 0xF0000, /// . + F10 = 0x79 + 0xF0000, /// . + F11 = 0x7A + 0xF0000, /// . + F12 = 0x7B + 0xF0000, /// . + Left = 0x25 + 0xF0000, /// . + Right = 0x27 + 0xF0000, /// . + Up = 0x26 + 0xF0000, /// . + Down = 0x28 + 0xF0000, /// . + Insert = 0x2d + 0xF0000, /// . + Delete = 0x2e + 0xF0000, /// . + Home = 0x24 + 0xF0000, /// . + End = 0x23 + 0xF0000, /// . + PageUp = 0x21 + 0xF0000, /// . + PageDown = 0x22 + 0xF0000, /// . + ScrollLock = 0x91 + 0xF0000, } /* These match simpledisplay.d which match terminal.d, so you can just cast them */