dark bg, clipboard, and vt on windows beginning factor

This commit is contained in:
Adam D. Ruppe 2020-02-17 19:57:56 -05:00
parent 944abf3c72
commit c911434c8b
1 changed files with 540 additions and 424 deletions

View File

@ -1,4 +1,6 @@
// for optional dependency
// for VT on Windows P s = 1 8 → Report the size of the text area in characters as CSI 8 ; height ; width t
// could be used to have the TE volunteer the size
/++
Module for interacting with the user's terminal, including color output, cursor manipulation, and full-featured real-time mouse and keyboard input. Also includes high-level convenience methods, like [Terminal.getline], which gives the user a line editor with history, completion, etc. See the [#examples].
@ -17,8 +19,8 @@
As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\
On Mac Terminal btw, a lot of hacks are needed and mouse support doesn't work. Most functions basically
work now though.
On old Mac Terminal btw, a lot of hacks are needed and mouse support doesn't work. Most functions basically
work now with newer Mac OS versions though.
Future_Roadmap:
$(LIST
@ -185,8 +187,10 @@ version(Posix) {
// capabilities.
//version = Demo
version(Windows)
version(Windows) {
version(VtEscapeCodes) {} // cool
version=Win32Console;
}
version(Win32Console) {
import core.sys.windows.windows;
@ -198,27 +202,30 @@ version(Win32Console) {
}
version(Posix) {
version=VtEscapeCodes;
import core.sys.posix.termios;
import core.sys.posix.unistd;
import unix = core.sys.posix.unistd;
import core.sys.posix.sys.types;
import core.sys.posix.sys.time;
import core.stdc.stdio;
import core.sys.posix.sys.ioctl;
}
version(VtEscapeCodes) {
enum UseVtSequences = true;
version(Windows) {} else
private {
enum RED_BIT = 1;
enum GREEN_BIT = 2;
enum BLUE_BIT = 4;
}
version(linux) {
extern(C) int ioctl(int, int, ...);
enum int TIOCGWINSZ = 0x5413;
} else version(OSX) {
import core.stdc.config;
extern(C) int ioctl(int, c_ulong, ...);
enum TIOCGWINSZ = 1074295912;
} else static assert(0, "confirm the value of tiocgwinsz");
struct winsize {
ushort ws_row;
ushort ws_col;
@ -373,6 +380,8 @@ an|ansi|ansi-bbs|ANSI terminals (emulators):\
:tc=vt-generic:
`;
} else {
enum UseVtSequences = false;
}
/// A modifier for [Color]
@ -543,7 +552,6 @@ struct Terminal {
void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically
}
version(Posix) {
bool terminalInFamily(string[] terms...) {
import std.process;
import std.string;
@ -555,6 +563,7 @@ struct Terminal {
return false;
}
version(Posix) {
// This is a filthy hack because Terminal.app and OS X are garbage who don't
// work the way they're advertised. I just have to best-guess hack and hope it
// doesn't break anything else. (If you know a better way, let me know!)
@ -566,6 +575,7 @@ struct Terminal {
auto term = environment.get("TERM");
return term == "xterm-256color";
}
}
static string[string] termcapDatabase;
static void readTermcapFile(bool useBuiltinTermcap = false) {
@ -610,6 +620,7 @@ struct Terminal {
}
if(useBuiltinTermcap) {
version(VtEscapeCodes)
foreach(line; splitLines(builtinTermcap)) {
handleTermcapLine(line);
}
@ -839,7 +850,6 @@ struct Terminal {
writeStringRaw(buffer[0 .. bufferPos]);
return true;
}
}
uint tcaps;
@ -847,7 +857,8 @@ struct Terminal {
return (tcaps & TerminalCapabilities.arsdImage) ? true : false;
}
bool clipboardSupported() {
return (tcaps & TerminalCapabilities.arsdImage) ? true : false;
version(Win32Console) return true;
else return (tcaps & TerminalCapabilities.arsdImage) ? true : false;
}
// only supported on my custom terminal emulator. guarded behind if(inlineImagesSupported)
@ -918,7 +929,7 @@ struct Terminal {
}
}
// stubs that are dependent on tcaps
// dependent on tcaps...
void displayInlineImage()(ubyte[] imageData) {
if(inlineImagesSupported) {
import std.base64;
@ -934,7 +945,7 @@ struct Terminal {
}
void demandUserAttention() {
version(Posix) {
if(UseVtSequences) {
if(!terminalInFamily("linux"))
writeStringRaw("\033]5001;1\007");
}
@ -947,12 +958,6 @@ struct Terminal {
}
}
void requestPasteFromClipboard() {
if(clipboardSupported) {
writeStringRaw("\033]52;c;?\007");
}
}
void requestCopyToPrimary(string text) {
if(clipboardSupported) {
import std.base64;
@ -960,9 +965,16 @@ struct Terminal {
}
}
void requestPasteFromPrimary() {
if(clipboardSupported) {
writeStringRaw("\033]52;p;?\007");
bool hasDefaultDarkBackground() {
version(Win32Console) {
return !(defaultBackgroundColor & 0xf0);
} else {
// FIXME: there is probably a better way to do this
// but like idk how reliable it is.
if(terminalInFamily("linux"))
return true;
else
return false;
}
}
@ -982,8 +994,6 @@ struct Terminal {
this.getSizeOverride = getSizeOverride;
this.type = type;
readTermcap();
if(type == ConsoleOutputType.minimalProcessing) {
_suppressDestruction = true;
return;
@ -992,6 +1002,12 @@ struct Terminal {
tcaps = getTerminalCapabilities(fdIn, fdOut);
//writeln(tcaps);
initializeVt();
}
void initializeVt() {
readTermcap();
if(type == ConsoleOutputType.cellular) {
doTermcap("ti");
clear();
@ -1001,6 +1017,7 @@ struct Terminal {
if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) {
writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it)
}
}
// EXPERIMENTAL do not use yet
@ -1019,6 +1036,10 @@ struct Terminal {
version(Win32Console)
/// ditto
this(ConsoleOutputType type) {
if(UseVtSequences) {
hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
initializeVt();
} else {
if(type == ConsoleOutputType.cellular) {
hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, null, CONSOLE_TEXTMODE_BUFFER, null);
if(hConsole == INVALID_HANDLE_VALUE) {
@ -1068,6 +1089,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
SetConsoleCP(65001); // UTF-8
*/
}
}
version(Win32Console) {
private Color defaultBackgroundColor = Color.black;
@ -1079,12 +1101,13 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
// only use this if you are sure you know what you want, since the terminal is a shared resource you generally really want to reset it to normal when you leave...
bool _suppressDestruction;
version(Posix)
~this() {
if(_suppressDestruction) {
flush();
return;
}
if(UseVtSequences) {
if(type == ConsoleOutputType.cellular) {
doTermcap("te");
}
@ -1098,14 +1121,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
if(lineGetter !is null)
lineGetter.dispose();
}
version(Windows)
~this() {
if(_suppressDestruction) {
flush();
return;
}
} else version(Windows) {
flush(); // make sure user data is all flushed before resetting
reset();
showCursor();
@ -1121,6 +1137,8 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
SetConsoleActiveScreenBuffer(stdo);
if(hConsole !is stdo)
CloseHandle(hConsole);
}
}
// lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit)
@ -1285,7 +1303,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
void underline(bool set, ForceOption force = ForceOption.automatic) {
if(set == _underlined && force != ForceOption.alwaysSend)
return;
version(Posix) {
if(UseVtSequences) {
if(set)
writeStringRaw("\033[4m");
else
@ -1329,14 +1347,14 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
void moveTo(int x, int y, ForceOption force = ForceOption.automatic) {
if(force != ForceOption.neverSend && (force == ForceOption.alwaysSend || x != _cursorX || y != _cursorY)) {
executeAutoHideCursor();
version(Posix) {
if(UseVtSequences) {
doTermcap("cm", y, x);
} else version(Win32Console) {
flush(); // if we don't do this now, the buffering can screw up the position
COORD coord = {cast(short) x, cast(short) y};
SetConsoleCursorPosition(hConsole, coord);
} else static assert(0);
}
}
_cursorX = x;
@ -1345,9 +1363,9 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
/// shows the cursor
void showCursor() {
version(Posix)
if(UseVtSequences)
doTermcap("ve");
else {
else version(Win32Console) {
CONSOLE_CURSOR_INFO info;
GetConsoleCursorInfo(hConsole, &info);
info.bVisible = true;
@ -1357,9 +1375,9 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
/// hides the cursor
void hideCursor() {
version(Posix) {
if(UseVtSequences) {
doTermcap("vi");
} else {
} else version(Win32Console) {
CONSOLE_CURSOR_INFO info;
GetConsoleCursorInfo(hConsole, &info);
info.bVisible = false;
@ -1466,7 +1484,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
}
int[] getSize() {
version(Win32Console) {
version(Windows) {
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo( hConsole, &info );
@ -1615,17 +1633,12 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
// you really, really shouldn't use this unless you know what you are doing
/*private*/ void writeStringRaw(in char[] s) {
// FIXME: make sure all the data is sent, check for errors
version(Posix) {
writeBuffer ~= s; // buffer it to do everything at once in flush() calls
} else version(Win32Console) {
writeBuffer ~= s;
} else static assert(0);
}
/// Clears the screen.
void clear() {
version(Posix) {
if(UseVtSequences) {
doTermcap("cl");
} else version(Win32Console) {
// http://support.microsoft.com/kb/99261
@ -1705,6 +1718,64 @@ struct RealTimeConsoleInput {
@disable this();
@disable this(this);
/++
Requests the system to send paste data as a [PasteEvent] to this stream, if possible.
See_Also:
[Terminal.requestCopyToPrimary]
[Terminal.requestCopyToClipboard]
[Terminal.clipboardSupported]
History:
Added February 17, 2020.
It was in Terminal briefly during an undocumented period, but it had to be moved here to have the context needed to send the real time paste event.
+/
void requestPasteFromClipboard() {
version(Win32Console) {
HWND hwndOwner = null;
if(OpenClipboard(hwndOwner) == 0)
throw new Exception("OpenClipboard");
scope(exit)
CloseClipboard();
if(auto dataHandle = GetClipboardData(CF_UNICODETEXT)) {
if(auto data = cast(wchar*) GlobalLock(dataHandle)) {
scope(exit)
GlobalUnlock(dataHandle);
int len = 0;
auto d = data;
while(*d) {
d++;
len++;
}
string s;
s.reserve(len);
foreach(idx, dchar ch; data[0 .. len]) {
// CR/LF -> LF
if(ch == '\r' && idx + 1 < len && data[idx + 1] == '\n')
continue;
s ~= ch;
}
injectEvent(InputEvent(PasteEvent(s), terminal), InjectionPosition.tail);
}
}
} else
if(terminal.clipboardSupported) {
terminal.writeStringRaw("\033]52;c;?\007");
}
}
/// ditto
void requestPasteFromPrimary() {
if(terminal.clipboardSupported) {
terminal.writeStringRaw("\033]52;p;?\007");
}
}
version(Posix) {
private int fdOut;
private int fdIn;
@ -1739,7 +1810,7 @@ struct RealTimeConsoleInput {
GetConsoleMode(inputHandle, &oldInput);
DWORD mode = 0;
mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C which we probably want to be similar to linux
//mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C and automatic paste... which we probably want to be similar to linux
//if(flags & ConsoleInputFlags.size)
mode |= ENABLE_WINDOW_INPUT /* 0208 */; // gives size etc
if(flags & ConsoleInputFlags.echo)
@ -1980,7 +2051,7 @@ struct RealTimeConsoleInput {
bool timedCheckForInput_bypassingBuffer(int milliseconds) {
version(Win32Console) {
auto response = WaitForSingleObject(terminal.hConsole, milliseconds);
auto response = WaitForSingleObject(inputHandle, milliseconds);
if(response == 0)
return true; // the object is ready
return false;
@ -2039,8 +2110,8 @@ struct RealTimeConsoleInput {
//char[128] inputBuffer;
//int inputBufferPosition;
version(Posix)
int nextRaw(bool interruptable = false) {
version(Posix) {
if(fdIn == -1)
return 0;
@ -2067,12 +2138,20 @@ struct RealTimeConsoleInput {
return inputPrefilter ? inputPrefilter(buf[0]) : buf[0];
else
assert(0); // read too much, should be impossible
} else version(Windows) {
char[8] buf;
DWORD d;
import std.conv;
if(!ReadFile(inputHandle, buf.ptr, cast(int) buf.length, &d, null))
throw new Exception("ReadFile " ~ to!string(GetLastError()));
return buf[0];
}
}
version(Posix)
int delegate(char) inputPrefilter;
version(Posix)
// for VT
dchar nextChar(int starting) {
if(starting <= 127)
return cast(dchar) starting;
@ -2176,8 +2255,17 @@ struct RealTimeConsoleInput {
InputEvent[] inputQueue;
version(Win32Console)
InputEvent[] readNextEvents() {
if(UseVtSequences)
return readNextEventsVt();
else version(Windows)
return readNextEventsWin32();
else
assert(0);
}
version(Windows)
InputEvent[] readNextEventsWin32() {
terminal.flush(); // make sure all output is sent out before waiting for anything
INPUT_RECORD[32] buffer;
@ -2219,12 +2307,26 @@ struct RealTimeConsoleInput {
if(ev.UnicodeChar) {
// new style event goes first
if(ev.UnicodeChar == 3) {
// handling this internally for linux compat too
newEvents ~= InputEvent(UserInterruptionEvent(), terminal);
} else if(ev.UnicodeChar == '\r') {
// translating \r to \n for same result as linux...
ke.which = cast(dchar) cast(wchar) '\n';
newEvents ~= InputEvent(ke, terminal);
// old style event then follows as the fallback
e.character = cast(dchar) cast(wchar) '\n';
newEvents ~= InputEvent(e, terminal);
} else {
ke.which = cast(dchar) cast(wchar) ev.UnicodeChar;
newEvents ~= InputEvent(ke, terminal);
// old style event then follows as the fallback
e.character = cast(dchar) cast(wchar) ev.UnicodeChar;
newEvents ~= InputEvent(e, terminal);
}
} else {
// old style event
ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode;
@ -2302,8 +2404,8 @@ struct RealTimeConsoleInput {
return newEvents;
}
version(Posix)
InputEvent[] readNextEvents() {
// for UseVtSequences....
InputEvent[] readNextEventsVt() {
terminal.flush(); // make sure all output is sent out before we try to get input
// we want to starve the read, especially if we're called from an edge-triggered
@ -2327,7 +2429,7 @@ struct RealTimeConsoleInput {
}
// The helper reads just one actual event from the pipe...
version(Posix)
// for UseVtSequences....
InputEvent[] readNextEventsHelper() {
InputEvent[] charPressAndRelease(dchar character) {
if((flags & ConsoleInputFlags.releasedKeys))
@ -3711,11 +3813,11 @@ class LineGetter {
auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents | ConsoleInputFlags.noEolWrap);
//rtci = &i;
//scope(exit) rtci = null;
while(workOnLine(i.nextEvent())) {}
while(workOnLine(i.nextEvent(), &i)) {}
} else {
//rtci = input;
//scope(exit) rtci = null;
while(workOnLine(input.nextEvent())) {}
while(workOnLine(input.nextEvent(), input)) {}
}
return finishGettingLine();
}
@ -4118,12 +4220,19 @@ class LineGetter {
private bool maintainBuffer;
/// for integrating into another event loop
/// you can pass individual events to this and
/// the line getter will work on it
///
/// returns false when there's nothing more to do
bool workOnLine(InputEvent e) {
/++
for integrating into another event loop
you can pass individual events to this and
the line getter will work on it
returns false when there's nothing more to do
History:
On February 17, 2020, it was changed to take
a new argument which should be the input source
where the event came from.
+/
bool workOnLine(InputEvent e, RealTimeConsoleInput* rtti = null) {
switch(e.type) {
case InputEvent.Type.EndOfFileEvent:
justHitTab = false;
@ -4309,6 +4418,10 @@ class LineGetter {
scrollToEnd();
redraw();
break;
case ('v' - 'a' + 1):
if(rtti)
rtti.requestPasteFromClipboard();
break;
case KeyboardEvent.Key.Insert:
justHitTab = false;
if(ev.modifierState & ModifierState.shift) {
@ -4319,8 +4432,11 @@ class LineGetter {
// those work on Windows!!!! and many linux TEs too.
// but if it does make it here, we'll attempt it at this level
if(rtti)
rtti.requestPasteFromClipboard();
} else if(ev.modifierState & ModifierState.control) {
// copy
// FIXME
} else {
insertMode = !insertMode;