arsd/terminalemulator.d

5099 lines
144 KiB
D

/++
This is an extendible unix terminal emulator and some helper functions to help actually implement one.
You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
See minigui_addons/terminal_emulator_widget in arsd repo or nestedterminalemulator.d or main.d in my terminal-emulator repo for how I did it.
History:
Written September/October 2013ish. Moved to arsd 2020-03-26.
+/
module arsd.terminalemulator;
/+
FIXME
terminal optimization:
first invalidated + last invalidated to slice the array
when looking for things that need redrawing.
FIXME: writing a line in color then a line in ordinary does something
wrong.
huh if i do underline then change color it undoes the underline
FIXME: make shift+enter send something special to the application
and shift+space, etc.
identify itself somehow too for client extensions
ctrl+space is supposed to send char 0.
ctrl+click on url pattern could open in browser perhaps
FIXME: scroll stuff should be higher level in the implementation.
so like scroll Rect, DirectionAndAmount
There should be a redraw thing that is given batches of instructions
in here that the other thing just implements.
FIXME: the save stack stuff should do cursor style too
+/
import arsd.color;
import std.algorithm : max;
enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:";
/+
The ;90 ones are my extensions.
90 - clipboard extensions
91 - image extensions
92 - hyperlink extensions
+/
enum terminalIdCode = "\033[?64;1;2;6;9;15;16;17;18;21;22;28;90;91;92c";
interface NonCharacterData {
//const(ubyte)[] serialize();
}
struct BinaryDataTerminalRepresentation {
int width;
int height;
TerminalEmulator.TerminalCell[] representation;
}
// old name, don't use in new programs anymore.
deprecated alias BrokenUpImage = BinaryDataTerminalRepresentation;
struct CustomGlyph {
TrueColorImage image;
dchar substitute;
}
void unknownEscapeSequence(in char[] esc) {
import std.file;
version(Posix) {
debug append("/tmp/arsd-te-bad-esc-sequences.txt", esc ~ "\n");
} else {
debug append("arsd-te-bad-esc-sequences.txt", esc ~ "\n");
}
}
// This is used for the double-click word selection
bool isWordSeparator(dchar ch) {
return ch == ' ' || ch == '"' || ch == '<' || ch == '>' || ch == '(' || ch == ')' || ch == ',';
}
TerminalEmulator.TerminalCell[] sliceTrailingWhitespace(TerminalEmulator.TerminalCell[] t) {
size_t end = t.length;
while(end >= 1) {
if(t[end-1].hasNonCharacterData || t[end-1].ch != ' ')
break;
end--;
}
t = t[0 .. end];
/*
import std.stdio;
foreach(ch; t)
write(ch.ch);
writeln("*");
*/
return t;
}
struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) {
T[maxSize] bufferInternal;
T[] buffer;
size_t length;
bool isNull = true;
T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
void opOpAssign(string op : "~")(in T rhs) {
if(buffer is null) buffer = bufferInternal[];
isNull = false;
static if(allowGrowth) {
if(this.length == buffer.length)
buffer.length = buffer.length * 2;
buffer[this.length++] = rhs;
} else {
if(this.length < buffer.length) // i am silently discarding more crap
buffer[this.length++] = rhs;
}
}
void opOpAssign(string op : "~")(in T[] rhs) {
if(buffer is null) buffer = bufferInternal[];
isNull = false;
buffer[this.length .. this.length + rhs.length] = rhs[];
this.length += rhs.length;
}
void opAssign(in T[] rhs) {
isNull = rhs is null;
if(buffer is null) buffer = bufferInternal[];
buffer[0 .. rhs.length] = rhs[];
this.length = rhs.length;
}
void opAssign(typeof(null)) {
isNull = true;
length = 0;
}
T opIndex(size_t idx) {
assert(!isNull);
assert(idx < length);
return buffer[idx];
}
void clear() {
isNull = true;
length = 0;
}
}
/**
An abstract class that does terminal emulation. You'll have to subclass it to make it work.
The terminal implements a subset of what xterm does and then, optionally, some special features.
Its linear mode (normal) screen buffer is infinitely long and infinitely wide. It is the responsibility
of your subclass to do line wrapping, etc., for display. This i think is actually incompatible with xterm but meh.
actually maybe it *should* automatically wrap them. idk. I think GNU screen does both. FIXME decide.
Its cellular mode (alternate) screen buffer can be any size you want.
*/
class TerminalEmulator {
/* override these to do stuff on the interface.
You might be able to stub them out if there's no state maintained on the target, since TerminalEmulator maintains its own internal state */
protected abstract void changeWindowTitle(string); /// the title of the window
protected abstract void changeIconTitle(string); /// the shorter window/iconified window
protected abstract void changeWindowIcon(IndexedImage); /// change the window icon. note this may be null
protected abstract void changeCursorStyle(CursorStyle); /// cursor style
protected abstract void changeTextAttributes(TextAttributes); /// current text output attributes
protected abstract void soundBell(); /// sounds the bell
protected abstract void sendToApplication(scope const(void)[]); /// send some data to the program running in the terminal, so keypresses etc.
protected abstract void copyToClipboard(string); /// copy the given data to the clipboard (or you can do nothing if you can't)
protected abstract void pasteFromClipboard(void delegate(in char[])); /// requests a paste. we pass it a delegate that should accept the data
protected abstract void copyToPrimary(string); /// copy the given data to the PRIMARY X selection (or you can do nothing if you can't)
protected abstract void pasteFromPrimary(void delegate(in char[])); /// requests a paste from PRIMARY. we pass it a delegate that should accept the data
abstract protected void requestExit(); /// the program is finished and the terminal emulator is requesting you to exit
/// Signal the UI that some attention should be given, e.g. blink the taskbar or sound the bell.
/// The default is to ignore the demand by instantly acknowledging it - if you override this, do NOT call super().
protected void demandAttention() {
attentionReceived();
}
/// After it demands attention, call this when the attention has been received
/// you may call it immediately to ignore the demand (the default)
public void attentionReceived() {
attentionDemanded = false;
}
protected final {
version(invalidator_2) {
int invalidatedMin;
int invalidatedMax;
}
void clearInvalidatedRange() {
version(invalidator_2) {
invalidatedMin = int.max;
invalidatedMax = 0;
}
}
void extendInvalidatedRange() {
version(invalidator_2) {
invalidatedMin = 0;
invalidatedMax = int.max;
}
}
void extendInvalidatedRange(int x, int y, int x2, int y2) {
version(invalidator_2) {
extendInvalidatedRange(y * screenWidth + x, y2 * screenWidth + x2);
}
}
void extendInvalidatedRange(int o1, int o2) {
version(invalidator_2) {
if(o1 < invalidatedMin)
invalidatedMin = o1;
if(o2 > invalidatedMax)
invalidatedMax = o2;
if(invalidatedMax < invalidatedMin)
invalidatedMin = invalidatedMax;
}
}
}
// I believe \033[50buffer[] and up are available for extensions everywhere.
// when keys are shifted, xterm sends them as \033[1;2F for example with end. but is this even sane? how would we do it with say, F5?
// apparently shifted F5 is ^[[15;2~
// alt + f5 is ^[[15;3~
// alt+shift+f5 is ^[[15;4~
private string pasteDataPending = null;
protected void justRead() {
if(pasteDataPending.length) {
sendPasteData(pasteDataPending);
import core.thread; Thread.sleep(50.msecs); // hack to keep it from closing, broken pipe i think
}
}
// my custom extension.... the data is the text content of the link, the identifier is some bits attached to the unit
public void sendHyperlinkData(scope const(dchar)[] data, uint identifier) {
if(bracketedHyperlinkMode) {
sendToApplication("\033[220~");
import std.conv;
// FIXME: that second 0 is a "command", like which menu option, which mouse button, etc.
sendToApplication(to!string(identifier) ~ ";0;" ~ to!string(data));
sendToApplication("\033[221~");
} else {
// without bracketed hyperlink, it simulates a paste
import std.conv;
sendPasteData(to!string(data));
}
}
public void sendPasteData(scope const(char)[] data) {
//if(pasteDataPending.length)
//throw new Exception("paste data being discarded, wtf, shouldnt happen");
// FIXME: i should put it all together so the brackets don't get separated by threads
if(bracketedPasteMode)
sendToApplication("\033[200~");
version(use_libssh2)
enum MAX_PASTE_CHUNK = 1024 * 40;
else
enum MAX_PASTE_CHUNK = 1024 * 1024 * 10;
if(data.length > MAX_PASTE_CHUNK) {
// need to chunk it in order to receive echos, etc,
// to avoid deadlocks
pasteDataPending = data[MAX_PASTE_CHUNK .. $].idup;
data = data[0 .. MAX_PASTE_CHUNK];
} else {
pasteDataPending = null;
}
if(data.length)
sendToApplication(data);
if(bracketedPasteMode)
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);
}
bool dragging;
int lastDragX, lastDragY;
public bool sendMouseInputToApplication(int termX, int termY, MouseEventType type, MouseButton button, bool shift, bool ctrl, bool alt) {
if(termX < 0)
termX = 0;
if(termX >= screenWidth)
termX = screenWidth - 1;
if(termY < 0)
termY = 0;
if(termY >= screenHeight)
termY = screenHeight - 1;
version(Windows) {
// I'm swapping these because my laptop doesn't have a middle button,
// and putty swaps them too by default so whatevs.
if(button == MouseButton.right)
button = MouseButton.middle;
else if(button == MouseButton.middle)
button = MouseButton.right;
}
int baseEventCode() {
int b;
// lol the xterm mouse thing sucks like javascript! unbelievable
// it doesn't support two buttons at once...
if(button == MouseButton.left)
b = 0;
else if(button == MouseButton.right)
b = 2;
else if(button == MouseButton.middle)
b = 1;
else if(button == MouseButton.wheelUp)
b = 64 | 0;
else if(button == MouseButton.wheelDown)
b = 64 | 1;
else
b = 3; // none pressed or button released
if(shift)
b |= 4;
if(ctrl)
b |= 16;
if(alt) // sending alt as meta
b |= 8;
return b;
}
if(type == MouseEventType.buttonReleased) {
// X sends press and release on wheel events, but we certainly don't care about those
if(button == MouseButton.wheelUp || button == MouseButton.wheelDown)
return false;
if(dragging) {
auto text = getSelectedText();
if(text.length) {
copyToPrimary(text);
} else if(!mouseButtonReleaseTracking || shift || (selectiveMouseTracking && ((!alternateScreenActive || scrollingBack) || termY != 0) && termY != cursorY)) {
// hyperlink check
int idx = termY * screenWidth + termX;
auto screen = (alternateScreenActive ? alternateScreen : normalScreen);
if(screen[idx].hyperlinkStatus & 0x01) {
// it is a link! need to find the beginning and the end
auto start = idx;
auto end = idx;
auto value = screen[idx].hyperlinkStatus;
while(start > 0 && screen[start].hyperlinkStatus == value)
start--;
if(screen[start].hyperlinkStatus != value)
start++;
while(end < screen.length && screen[end].hyperlinkStatus == value)
end++;
uint number;
dchar[64] buffer;
foreach(i, ch; screen[start .. end]) {
if(i >= buffer.length)
break;
if(!ch.hasNonCharacterData)
buffer[i] = ch.ch;
if(i < 16) {
number |= (ch.hyperlinkBit ? 1 : 0) << i;
}
}
if((cast(size_t) (end - start)) <= buffer.length)
sendHyperlinkData(buffer[0 .. end - start], number);
}
}
}
dragging = false;
if(mouseButtonReleaseTracking) {
int b = baseEventCode;
b |= 3; // always send none / button released
ScopeBuffer!(char, 16) buffer;
buffer ~= "\033[M";
buffer ~= cast(char) (b | 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (termX+1 + 32);
//buffer ~= cast(char) (termY+1 + 32);
sendToApplication(buffer[]);
}
}
if(type == MouseEventType.motion) {
if(termX != lastDragX || termY != lastDragY) {
lastDragY = termY;
lastDragX = termX;
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
int b = baseEventCode;
ScopeBuffer!(char, 16) buffer;
buffer ~= "\033[M";
buffer ~= cast(char) ((b | 32) + 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (termX+1 + 32);
//buffer ~= cast(char) (termY+1 + 32);
sendToApplication(buffer[]);
}
if(dragging) {
auto idx = termY * screenWidth + termX;
// the no-longer-selected portion needs to be invalidated
int start, end;
if(idx > selectionEnd) {
start = selectionEnd;
end = idx;
} else {
start = idx;
end = selectionEnd;
}
if(start < 0 || end >= ((alternateScreenActive ? alternateScreen.length : normalScreen.length)))
return false;
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
cell.invalidated = true;
cell.selected = false;
}
extendInvalidatedRange(start, end);
cancelOverriddenSelection();
selectionEnd = idx;
// and the freshly selected portion needs to be invalidated
if(selectionStart > selectionEnd) {
start = selectionEnd;
end = selectionStart;
} else {
start = selectionStart;
end = selectionEnd;
}
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
cell.invalidated = true;
cell.selected = true;
}
extendInvalidatedRange(start, end);
return true;
}
}
}
if(type == MouseEventType.buttonPressed) {
// double click detection
import std.datetime;
static SysTime lastClickTime;
static int consecutiveClicks = 1;
if(button != MouseButton.wheelUp && button != MouseButton.wheelDown) {
if(Clock.currTime() - lastClickTime < dur!"msecs"(350))
consecutiveClicks++;
else
consecutiveClicks = 1;
lastClickTime = Clock.currTime();
}
// end dbl click
if(!(shift) && mouseButtonTracking) {
if(selectiveMouseTracking && termY != 0 && termY != cursorY) {
if(button == MouseButton.left || button == MouseButton.right)
goto do_default_behavior;
if((!alternateScreenActive || scrollingBack) && (button == MouseButton.wheelUp || button.MouseButton.wheelDown))
goto do_default_behavior;
}
// top line only gets special cased on full screen apps
if(selectiveMouseTracking && (!alternateScreenActive || scrollingBack) && termY == 0 && cursorY != 0)
goto do_default_behavior;
int b = baseEventCode;
int x = termX;
int y = termY;
x++; y++; // applications expect it to be one-based
ScopeBuffer!(char, 16) buffer;
buffer ~= "\033[M";
buffer ~= cast(char) (b | 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (x + 32);
//buffer ~= cast(char) (y + 32);
sendToApplication(buffer[]);
} else {
do_default_behavior:
if(button == MouseButton.middle) {
pasteFromPrimary(&sendPasteData);
}
if(button == MouseButton.wheelUp) {
scrollback(alt ? 0 : (ctrl ? 10 : 1), alt ? -(ctrl ? 10 : 1) : 0);
return true;
}
if(button == MouseButton.wheelDown) {
scrollback(alt ? 0 : -(ctrl ? 10 : 1), alt ? (ctrl ? 10 : 1) : 0);
return true;
}
if(button == MouseButton.left) {
// 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;
cell.selected = false;
}
extendInvalidatedRange(selectionStart, selectionEnd);
if(consecutiveClicks == 1) {
selectionStart = termY * screenWidth + termX;
selectionEnd = selectionStart;
} else if(consecutiveClicks == 2) {
selectionStart = termY * screenWidth + termX;
selectionEnd = selectionStart;
while(selectionStart > 0 && !isWordSeparator((*activeScreen)[selectionStart-1].ch)) {
selectionStart--;
}
while(selectionEnd < (*activeScreen).length && !isWordSeparator((*activeScreen)[selectionEnd].ch)) {
selectionEnd++;
}
} else if(consecutiveClicks == 3) {
selectionStart = termY * screenWidth;
selectionEnd = selectionStart + screenWidth;
}
dragging = true;
lastDragX = termX;
lastDragY = termY;
// then invalidate the new selection as well since it should be highlighted
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[selectionStart .. selectionEnd]) {
cell.invalidated = true;
cell.selected = true;
}
extendInvalidatedRange(selectionStart, selectionEnd);
return true;
}
if(button == MouseButton.right) {
int changed1;
int changed2;
cancelOverriddenSelection();
auto click = termY * screenWidth + termX;
if(click < selectionStart) {
auto oldSelectionStart = selectionStart;
selectionStart = click;
changed1 = selectionStart;
changed2 = oldSelectionStart;
} else if(click > selectionEnd) {
auto oldSelectionEnd = selectionEnd;
selectionEnd = click;
changed1 = oldSelectionEnd;
changed2 = selectionEnd;
}
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[changed1 .. changed2]) {
cell.invalidated = true;
cell.selected = true;
}
extendInvalidatedRange(changed1, changed2);
auto text = getPlainText(selectionStart, selectionEnd);
if(text.length) {
copyToPrimary(text);
}
return true;
}
}
}
return false;
}
private void addMouseCoordinates(ref ScopeBuffer!(char, 16) buffer, int x, int y) {
// 1-based stuff and 32 is the base value
x += 1 + 32;
y += 1 + 32;
if(utf8MouseMode) {
import std.utf;
char[4] str;
foreach(char ch; str[0 .. encode(str, x)])
buffer ~= ch;
foreach(char ch; str[0 .. encode(str, y)])
buffer ~= ch;
} else {
buffer ~= cast(char) x;
buffer ~= cast(char) y;
}
}
protected void returnToNormalScreen() {
alternateScreenActive = false;
if(cueScrollback) {
showScrollbackOnScreen(normalScreen, 0, true, 0);
newLine(false);
cueScrollback = false;
}
notifyScrollbarRelevant(true, true);
extendInvalidatedRange();
}
protected void outputOccurred() { }
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(alt) { // it used to be shift || alt here, but like shift+space is more trouble than it is worth in actual usage experience. too easily to accidentally type it in the middle of something else to be unambiguously useful. I wouldn't even set a hotkey on it so gonna just send it as plain space always.
// 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;
if((!alternateScreenActive || scrollingBack) && key == TerminalKey.ScrollLock) {
toggleScrollLock();
return true;
}
/*
So ctrl + A-Z, [, \, ], ^, and _ are all chars 1-31
ctrl+5 send ^]
FIXME: for alt+keys and the other ctrl+them, send the xterm ascii magc thing terminal.d knows how to use
*/
// scrollback controls. Unlike xterm, I only want to do this on the normal screen, since alt screen
// doesn't have scrollback anyway. Thus the key will be forwarded to the application.
if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageUp && (shift || scrollLock)) {
scrollback(10);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageDown && (shift || scrollLock)) {
scrollback(-10);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Left && (shift || scrollLock)) {
scrollback(0, ctrl ? -10 : -1);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Right && (shift || scrollLock)) {
scrollback(0, ctrl ? 10 : 1);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Up && (shift || scrollLock)) {
scrollback(ctrl ? 10 : 1);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Down && (shift || scrollLock)) {
scrollback(ctrl ? -10 : -1);
return true;
} else if((!alternateScreenActive || scrollingBack)) { // && ev.key != Key.Shift && ev.key != Key.Shift_r) {
if(endScrollback())
redrawRequired = true;
}
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
else {
ScopeBuffer!(char, 16) modifierNumber;
char otherModifier = 0;
if(shift && alt && ctrl) modifierNumber = "8";
if(alt && ctrl && !shift) modifierNumber = "7";
if(shift && ctrl && !alt) modifierNumber = "6";
if(ctrl && !shift && !alt) modifierNumber = "5";
if(shift && alt && !ctrl) modifierNumber = "4";
if(alt && !shift && !ctrl) modifierNumber = "3";
if(shift && !alt && !ctrl) modifierNumber = "2";
// FIXME: meta and windows
// windows is an extension
if(windows) {
if(modifierNumber.length)
otherModifier = '2';
else
modifierNumber = "20";
/* // the below is what we're really doing
int mn = 0;
if(modifierNumber.length)
mn = modifierNumber[0] + '0';
mn += 20;
*/
}
string keyNumber;
char terminator;
if(s[$-1] == '~') {
keyNumber = s[2 .. $-1];
terminator = '~';
} else {
keyNumber = "1";
terminator = s[$ - 1];
}
ScopeBuffer!(char, 32) buffer;
buffer ~= "\033[";
buffer ~= keyNumber;
buffer ~= ";";
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[]);
}
}
alias TerminalKey Key;
import std.stdio;
// writefln("Key: %x", cast(int) 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;
case Key.Right: sendToApplicationModified(applicationCursorKeys ? "\033OC" : "\033[C"); break;
case Key.Home: sendToApplicationModified(applicationCursorKeys ? "\033OH" : (1 ? "\033[H" : "\033[1~")); break;
case Key.Insert: sendToApplicationModified("\033[2~"); break;
case Key.Delete: sendToApplicationModified("\033[3~"); break;
// the 1? is xterm vs gnu screen. but i really want xterm compatibility.
case Key.End: sendToApplicationModified(applicationCursorKeys ? "\033OF" : (1 ? "\033[F" : "\033[4~")); break;
case Key.PageUp: sendToApplicationModified("\033[5~"); break;
case Key.PageDown: sendToApplicationModified("\033[6~"); break;
// the first one here is preferred, the second option is what xterm does if you turn on the "old function keys" option, which most apps don't actually expect
case Key.F1: sendToApplicationModified(1 ? "\033OP" : "\033[11~"); break;
case Key.F2: sendToApplicationModified(1 ? "\033OQ" : "\033[12~"); break;
case Key.F3: sendToApplicationModified(1 ? "\033OR" : "\033[13~"); break;
case Key.F4: sendToApplicationModified(1 ? "\033OS" : "\033[14~"); break;
case Key.F5: sendToApplicationModified("\033[15~"); break;
case Key.F6: sendToApplicationModified("\033[17~"); break;
case Key.F7: sendToApplicationModified("\033[18~"); break;
case Key.F8: sendToApplicationModified("\033[19~"); break;
case Key.F9: sendToApplicationModified("\033[20~"); break;
case Key.F10: sendToApplicationModified("\033[21~"); break;
case Key.F11: sendToApplicationModified("\033[23~"); break;
case Key.F12: sendToApplicationModified("\033[24~"); break;
case Key.Escape: sendToApplicationModified("\033"); break;
// my extensions, see terminator.d for the other side of it
case Key.ScrollLock: sendToApplicationModified("\033[70~"); break;
// xterm extension for arbitrary modified unicode chars
default:
sendToApplicationModified("\033[27~", key);
}
return redrawRequired;
}
/// if a binary extension is triggered, the implementing class is responsible for figuring out how it should be made to fit into the screen buffer
protected /*abstract*/ BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[]) {
return BinaryDataTerminalRepresentation();
}
/// If you subclass this and return true, you can scroll on command without needing to redraw the entire screen;
/// returning true here suppresses the automatic invalidation of scrolled lines (except the new one).
protected bool scrollLines(int howMany, bool scrollUp) {
return false;
}
// might be worth doing the redraw magic in here too.
// FIXME: not implemented
@disable protected void drawTextSection(int x, int y, TextAttributes attributes, in dchar[] text, bool isAllSpaces) {
// if you implement this it will always give you a continuous block on a single line. note that text may be a bunch of spaces, in that case you can just draw the bg color to clear the area
// or you can redraw based on the invalidated flag on the buffer
}
// FIXME: what about image sections? maybe it is still necessary to loop through them
/// Style of the cursor
enum CursorStyle {
block, /// a solid block over the position (like default xterm or many gui replace modes)
underline, /// underlining the position (like the vga text mode default)
bar, /// a bar on the left side of the cursor position (like gui insert modes)
}
// these can be overridden, but don't have to be
TextAttributes defaultTextAttributes() {
TextAttributes ta;
ta.foregroundIndex = 256; // terminal.d uses this as Color.DEFAULT
ta.backgroundIndex = 256;
import std.process;
// I'm using the environment for this because my programs and scripts
// already know this variable and then it gets nicely inherited. It is
// also easy to set without buggering with other arguments. So works for me.
version(with_24_bit_color) {
if(environment.get("ELVISBG") == "dark") {
ta.foreground = Color.white;
ta.background = Color.black;
} else {
ta.foreground = Color.black;
ta.background = Color.white;
}
}
return ta;
}
Color defaultForeground;
Color defaultBackground;
Color[256] palette;
/// .
static struct TextAttributes {
align(1):
bool bold() { return (attrStore & 1) ? true : false; } ///
void bold(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } ///
bool blink() { return (attrStore & 2) ? true : false; } ///
void blink(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } ///
bool invisible() { return (attrStore & 4) ? true : false; } ///
void invisible(bool t) { attrStore &= ~4; if(t) attrStore |= 4; } ///
bool inverse() { return (attrStore & 8) ? true : false; } ///
void inverse(bool t) { attrStore &= ~8; if(t) attrStore |= 8; } ///
bool underlined() { return (attrStore & 16) ? true : false; } ///
void underlined(bool t) { attrStore &= ~16; if(t) attrStore |= 16; } ///
bool italic() { return (attrStore & 32) ? true : false; } ///
void italic(bool t) { attrStore &= ~32; if(t) attrStore |= 32; } ///
bool strikeout() { return (attrStore & 64) ? true : false; } ///
void strikeout(bool t) { attrStore &= ~64; if(t) attrStore |= 64; } ///
bool faint() { return (attrStore & 128) ? true : false; } ///
void faint(bool t) { attrStore &= ~128; if(t) attrStore |= 128; } ///
// if the high bit here is set, you should use the full Color values if possible, and the value here sans the high bit if not
bool foregroundIsDefault() { return (attrStore & 256) ? true : false; } ///
void foregroundIsDefault(bool t) { attrStore &= ~256; if(t) attrStore |= 256; } ///
bool backgroundIsDefault() { return (attrStore & 512) ? true : false; } ///
void backgroundIsDefault(bool t) { attrStore &= ~512; if(t) attrStore |= 512; } ///
// I am doing all this to get the store a bit smaller but
// I could go back to just plain `ushort foregroundIndex` etc.
///
@property ushort foregroundIndex() {
if(foregroundIsDefault)
return 256;
else
return foregroundIndexStore;
}
///
@property ushort backgroundIndex() {
if(backgroundIsDefault)
return 256;
else
return backgroundIndexStore;
}
///
@property void foregroundIndex(ushort v) {
if(v == 256)
foregroundIsDefault = true;
else
foregroundIsDefault = false;
foregroundIndexStore = cast(ubyte) v;
}
///
@property void backgroundIndex(ushort v) {
if(v == 256)
backgroundIsDefault = true;
else
backgroundIsDefault = false;
backgroundIndexStore = cast(ubyte) v;
}
ubyte foregroundIndexStore; /// the internal storage
ubyte backgroundIndexStore; /// ditto
ushort attrStore = 0; /// ditto
version(with_24_bit_color) {
Color foreground; /// ditto
Color background; /// ditto
}
}
//pragma(msg, TerminalCell.sizeof);
/// represents one terminal cell
align((void*).sizeof)
static struct TerminalCell {
align(1):
private union {
// OMG the top 11 bits of a dchar are always 0
// and i can reuse them!!!
struct {
dchar chStore = ' '; /// the character
TextAttributes attributesStore; /// color, etc.
}
// 64 bit pointer also has unused 16 bits but meh.
NonCharacterData nonCharacterDataStore; /// iff hasNonCharacterData
}
dchar ch() {
assert(!hasNonCharacterData);
return chStore;
}
void ch(dchar c) {
hasNonCharacterData = false;
chStore = c;
}
ref TextAttributes attributes() return {
assert(!hasNonCharacterData);
return attributesStore;
}
NonCharacterData nonCharacterData() {
assert(hasNonCharacterData);
return nonCharacterDataStore;
}
void nonCharacterData(NonCharacterData c) {
hasNonCharacterData = true;
nonCharacterDataStore = c;
}
// bits: RRHLLNSI
// R = reserved, H = hyperlink ID bit, L = link, N = non-character data, S = selected, I = invalidated
ubyte attrStore = 1; // just invalidated to start
bool invalidated() { return (attrStore & 1) ? true : false; } /// if it needs to be redrawn
void invalidated(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } /// ditto
bool selected() { return (attrStore & 2) ? true : false; } /// if it is currently selected by the user (for being copied to the clipboard)
void selected(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } /// ditto
bool hasNonCharacterData() { return (attrStore & 4) ? true : false; } ///
void hasNonCharacterData(bool t) { attrStore &= ~4; if(t) attrStore |= 4; }
// 0 means it is not a hyperlink. Otherwise, it just alternates between 1 and 3 to tell adjacent links apart.
// value of 2 is reserved for future use.
ubyte hyperlinkStatus() { return (attrStore & 0b11000) >> 3; }
void hyperlinkStatus(ubyte t) { assert(t < 4); attrStore &= ~0b11000; attrStore |= t << 3; }
bool hyperlinkBit() { return (attrStore & 0b100000) >> 5; }
void hyperlinkBit(bool t) { (attrStore &= ~0b100000); if(t) attrStore |= 0b100000; }
}
bool hyperlinkFlipper;
bool hyperlinkActive;
int hyperlinkNumber;
/// Cursor position, zero based. (0,0) == upper left. (0, 1) == second row, first column.
static struct CursorPosition {
int x; /// .
int y; /// .
alias y row;
alias x column;
}
// these public functions can be used to manipulate the terminal
/// clear the screen
void cls() {
TerminalCell plain;
plain.ch = ' ';
plain.attributes = currentAttributes;
plain.invalidated = true;
foreach(i, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
cell = plain;
}
extendInvalidatedRange(0, 0, screenWidth, screenHeight);
}
void makeSelectionOffsetsSane(ref int offsetStart, ref int offsetEnd) {
auto buffer = &alternateScreen;
if(offsetStart < 0)
offsetStart = 0;
if(offsetEnd < 0)
offsetEnd = 0;
if(offsetStart > (*buffer).length)
offsetStart = cast(int) (*buffer).length;
if(offsetEnd > (*buffer).length)
offsetEnd = cast(int) (*buffer).length;
// if it is backwards, we can flip it
if(offsetEnd < offsetStart) {
auto tmp = offsetStart;
offsetStart = offsetEnd;
offsetEnd = tmp;
}
}
public string getPlainText(int offsetStart, int offsetEnd) {
auto buffer = alternateScreenActive ? &alternateScreen : &normalScreen;
makeSelectionOffsetsSane(offsetStart, offsetEnd);
if(offsetStart == offsetEnd)
return null;
int x = offsetStart % screenWidth;
int firstSpace = -1;
string ret;
foreach(cell; (*buffer)[offsetStart .. offsetEnd]) {
if(cell.hasNonCharacterData)
break;
ret ~= cell.ch;
x++;
if(x == screenWidth) {
x = 0;
if(firstSpace != -1) {
// we ended with a bunch of spaces, let's replace them with a single newline so the next is more natural
ret = ret[0 .. firstSpace];
ret ~= "\n";
firstSpace = -1;
}
} else {
if(cell.ch == ' ' && firstSpace == -1)
firstSpace = cast(int) ret.length - 1;
else if(cell.ch != ' ')
firstSpace = -1;
}
}
if(firstSpace != -1) {
bool allSpaces = true;
foreach(item; ret[firstSpace .. $]) {
if(item != ' ') {
allSpaces = false;
break;
}
}
if(allSpaces)
ret = ret[0 .. firstSpace];
}
return ret;
}
void scrollDown(int count = 1) {
if(cursorY + 1 < screenHeight) {
TerminalCell plain;
plain.ch = ' ';
plain.attributes = defaultTextAttributes();
plain.invalidated = true;
foreach(i; 0 .. count) {
// FIXME: should that be cursorY or scrollZoneTop?
for(int y = scrollZoneBottom; y > cursorY; y--)
foreach(x; 0 .. screenWidth) {
ASS[y][x] = ASS[y - 1][x];
ASS[y][x].invalidated = true;
}
foreach(x; 0 .. screenWidth)
ASS[cursorY][x] = plain;
}
extendInvalidatedRange(0, cursorY, screenWidth, scrollZoneBottom);
}
}
void scrollUp(int count = 1) {
if(cursorY + 1 < screenHeight) {
TerminalCell plain;
plain.ch = ' ';
plain.attributes = defaultTextAttributes();
plain.invalidated = true;
foreach(i; 0 .. count) {
// FIXME: should that be cursorY or scrollZoneBottom?
for(int y = scrollZoneTop; y < cursorY; y++)
foreach(x; 0 .. screenWidth) {
ASS[y][x] = ASS[y + 1][x];
ASS[y][x].invalidated = true;
}
foreach(x; 0 .. screenWidth)
ASS[cursorY][x] = plain;
}
extendInvalidatedRange(0, scrollZoneTop, screenWidth, cursorY);
}
}
int readingExtensionData = -1;
string extensionData;
immutable(dchar[dchar])* characterSet = null; // null means use regular UTF-8
bool readingEsc = false;
ScopeBuffer!(ubyte, 1024, true) esc;
/// sends raw input data to the terminal as if the application printf()'d it or it echoed or whatever
void sendRawInput(in ubyte[] datain) {
const(ubyte)[] data = datain;
//import std.array;
//assert(!readingEsc, replace(cast(string) esc, "\033", "\\"));
again:
foreach(didx, b; data) {
if(readingExtensionData >= 0) {
if(readingExtensionData == extensionMagicIdentifier.length) {
if(b) {
switch(b) {
case 13, 10:
// ignore
break;
case 'A': .. case 'Z':
case 'a': .. case 'z':
case '0': .. case '9':
case '=':
case '+', '/':
case '_', '-':
// base64 ok
extensionData ~= b;
break;
default:
// others should abort the read
readingExtensionData = -1;
}
} else {
readingExtensionData = -1;
import std.base64;
auto got = handleBinaryExtensionData(Base64.decode(extensionData));
auto rep = got.representation;
foreach(y; 0 .. got.height) {
foreach(x; 0 .. got.width) {
addOutput(rep[0]);
rep = rep[1 .. $];
}
newLine(true);
}
}
} else {
if(b == extensionMagicIdentifier[readingExtensionData])
readingExtensionData++;
else {
// put the data back into the buffer, if possible
// (if the data was split across two packets, this may
// not be possible. but in that case, meh.)
if(cast(int) didx - cast(int) readingExtensionData >= 0)
data = data[didx - readingExtensionData .. $];
readingExtensionData = -1;
goto again;
}
}
continue;
}
if(b == 0) {
readingExtensionData = 0;
extensionData = null;
continue;
}
if(readingEsc) {
if(b == 27) {
// an esc in the middle of a sequence will
// cancel the first one
esc = null;
continue;
}
if(b == 10) {
readingEsc = false;
}
esc ~= b;
if(esc.length == 1 && esc[0] == '7') {
pushSavedCursor(cursorPosition);
esc = null;
readingEsc = false;
} else if(esc.length == 1 && esc[0] == 'M') {
// reverse index
esc = null;
readingEsc = false;
if(cursorY <= scrollZoneTop)
scrollDown();
else
cursorY = cursorY - 1;
} else if(esc.length == 1 && esc[0] == '=') {
// application keypad
esc = null;
readingEsc = false;
} else if(esc.length == 2 && esc[0] == '%' && esc[1] == 'G') {
// UTF-8 mode
esc = null;
readingEsc = false;
} else if(esc.length == 1 && esc[0] == '8') {
cursorPosition = popSavedCursor;
esc = null;
readingEsc = false;
} else if(esc.length == 1 && esc[0] == 'c') {
// reset
// FIXME
esc = null;
readingEsc = false;
} else if(esc.length == 1 && esc[0] == '>') {
// normal keypad
esc = null;
readingEsc = false;
} else if(esc.length > 1 && (
(esc[0] == '[' && (b >= 64 && b <= 126)) ||
(esc[0] == ']' && b == '\007')))
{
try {
tryEsc(esc[]);
} catch(Exception e) {
unknownEscapeSequence(e.msg ~ " :: " ~ cast(char[]) esc[]);
}
esc = null;
readingEsc = false;
} else if(esc.length == 3 && esc[0] == '%' && esc[1] == 'G') {
// UTF-8 mode. ignored because we're always in utf-8 mode (though should we be?)
esc = null;
readingEsc = false;
} else if(esc.length == 2 && esc[0] == ')') {
// more character set selection. idk exactly how this works
esc = null;
readingEsc = false;
} else if(esc.length == 2 && esc[0] == '(') {
// xterm command for character set
// FIXME: handling esc[1] == '0' would be pretty boss
// and esc[1] == 'B' == united states
if(esc[1] == '0')
characterSet = &lineDrawingCharacterSet;
else
characterSet = null; // our default is UTF-8 and i don't care much about others anyway.
esc = null;
readingEsc = false;
} else if(esc.length == 1 && esc[0] == 'Z') {
// identify terminal
sendToApplication(terminalIdCode);
}
continue;
}
if(b == 27) {
readingEsc = true;
debug if(esc.isNull && esc.length) {
import std.stdio; writeln("discarding esc ", cast(string) esc[]);
}
esc = null;
continue;
}
if(b == 13) {
cursorX = 0;
setTentativeScrollback(0);
continue;
}
if(b == 7) {
soundBell();
continue;
}
if(b == 8) {
cursorX = cursorX - 1;
extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY);
setTentativeScrollback(cursorX);
continue;
}
if(b == 9) {
int howMany = 8 - (cursorX % 8);
// so apparently it is just supposed to move the cursor.
// it breaks mutt to output spaces
cursorX = cursorX + howMany;
if(!alternateScreenActive)
foreach(i; 0 .. howMany)
addScrollbackOutput(' '); // FIXME: it would be nice to actually put a tab character there for copy/paste accuracy (ditto with newlines actually)
continue;
}
// std.stdio.writeln("READ ", data[w]);
addOutput(b);
}
}
/// construct
this(int width, int height) {
// initialization
import std.process;
if(environment.get("ELVISBG") == "dark") {
defaultForeground = Color.white;
defaultBackground = Color.black;
} else {
defaultForeground = Color.black;
defaultBackground = Color.white;
}
currentAttributes = defaultTextAttributes();
cursorColor = Color.white;
palette[] = xtermPalette[];
resizeTerminal(width, height);
// update the other thing
if(windowTitle.length == 0)
windowTitle = "Terminal Emulator";
changeWindowTitle(windowTitle);
changeIconTitle(iconTitle);
changeTextAttributes(currentAttributes);
}
private {
TerminalCell[] scrollbackMainScreen;
bool scrollbackCursorShowing;
int scrollbackCursorX;
int scrollbackCursorY;
}
protected {
bool scrollingBack;
int currentScrollback;
int currentScrollbackX;
}
// FIXME: if it is resized while scrolling back, stuff can get messed up
private int scrollbackLength_;
private void scrollbackLength(int i) {
scrollbackLength_ = i;
}
int scrollbackLength() {
return scrollbackLength_;
}
private int scrollbackWidth_;
int scrollbackWidth() {
return scrollbackWidth_ > screenWidth ? scrollbackWidth_ : screenWidth;
}
/* virtual */ void notifyScrollbackAdded() {}
/* virtual */ void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) {}
/* virtual */ void notifyScrollbarPosition(int x, int y) {}
// coordinates are for a scroll bar, where 0,0 is the beginning of history
void scrollbackTo(int x, int y) {
if(alternateScreenActive && !scrollingBack)
return;
if(!scrollingBack) {
startScrollback();
}
if(y < 0)
y = 0;
if(x < 0)
x = 0;
currentScrollbackX = x;
currentScrollback = scrollbackLength - y;
if(currentScrollback < 0)
currentScrollback = 0;
if(currentScrollbackX < 0)
currentScrollbackX = 0;
if(!scrollLock && currentScrollback == 0 && currentScrollbackX == 0) {
endScrollback();
} else {
cls();
showScrollbackOnScreen(alternateScreen, currentScrollback, false, currentScrollbackX);
}
}
void scrollback(int delta, int deltaX = 0) {
if(alternateScreenActive && !scrollingBack)
return;
if(!scrollingBack) {
if(delta <= 0 && deltaX == 0)
return; // it does nothing to scroll down when not scrolling back
startScrollback();
}
currentScrollback += delta;
if(!scrollbackReflow && deltaX) {
currentScrollbackX += deltaX;
int max = scrollbackWidth - screenWidth;
if(max < 0)
max = 0;
if(currentScrollbackX > max)
currentScrollbackX = max;
if(currentScrollbackX < 0)
currentScrollbackX = 0;
}
int max = cast(int) scrollbackBuffer.length - screenHeight;
if(scrollbackReflow && max < 0) {
foreach(line; scrollbackBuffer[]) {
if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
max += 0;
else
max += cast(int) line.length / screenWidth;
}
}
if(max < 0)
max = 0;
if(scrollbackReflow && currentScrollback > max) {
foreach(line; scrollbackBuffer[]) {
if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
max += 0;
else
max += cast(int) line.length / screenWidth;
}
}
if(currentScrollback > max)
currentScrollback = max;
if(currentScrollback < 0)
currentScrollback = 0;
if(!scrollLock && currentScrollback <= 0 && currentScrollbackX <= 0)
endScrollback();
else {
cls();
showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
}
}
private void startScrollback() {
if(scrollingBack)
return;
currentScrollback = 0;
currentScrollbackX = 0;
scrollingBack = true;
scrollbackCursorX = cursorX;
scrollbackCursorY = cursorY;
scrollbackCursorShowing = cursorShowing;
scrollbackMainScreen = alternateScreen.dup;
alternateScreenActive = true;
cursorShowing = false;
}
bool endScrollback() {
//if(scrollLock)
// return false;
if(!scrollingBack)
return false;
scrollingBack = false;
cursorX = scrollbackCursorX;
cursorY = scrollbackCursorY;
cursorShowing = scrollbackCursorShowing;
alternateScreen = scrollbackMainScreen;
alternateScreenActive = false;
currentScrollback = 0;
currentScrollbackX = 0;
if(!scrollLock) {
scrollbackReflow = true;
recalculateScrollbackLength();
}
notifyScrollbarPosition(0, int.max);
return true;
}
private bool scrollbackReflow = true;
/* deprecated? */
public void toggleScrollbackWrap() {
scrollbackReflow = !scrollbackReflow;
recalculateScrollbackLength();
}
private bool scrollLockLockEnabled = false;
package void scrollLockLock() {
scrollLockLockEnabled = true;
if(!scrollLock)
toggleScrollLock();
}
private bool scrollLock = false;
public void toggleScrollLock() {
if(scrollLockLockEnabled && scrollLock)
goto nochange;
scrollLock = !scrollLock;
scrollbackReflow = !scrollLock;
nochange:
recalculateScrollbackLength();
if(scrollLock) {
startScrollback();
cls();
currentScrollback = 0;
currentScrollbackX = 0;
showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
} else {
endScrollback();
}
//cls();
//drawScrollback();
}
private void recalculateScrollbackLength() {
int count = cast(int) scrollbackBuffer.length;
int max;
if(scrollbackReflow) {
foreach(line; scrollbackBuffer[]) {
if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
{} // intentionally blank, the count is fine since this line isn't reflowed anyway
else
count += cast(int) line.length / screenWidth;
}
} else {
foreach(line; scrollbackBuffer[]) {
if(line.length > max)
max = cast(int) line.length;
}
}
scrollbackWidth_ = max;
scrollbackLength = count;
notifyScrollbackAdded();
notifyScrollbarPosition(currentScrollbackX, currentScrollback ? scrollbackLength - currentScrollback : int.max);
}
/++
Writes the text in the scrollback buffer to the given file.
Discards formatting information and embedded images.
See_Also:
[writeScrollbackToDelegate]
+/
public void writeScrollbackToFile(string filename) {
import std.stdio;
auto file = File(filename, "wt");
foreach(line; scrollbackBuffer[]) {
foreach(c; line)
if(!c.hasNonCharacterData)
file.write(c.ch); // I hope this is buffered
file.writeln();
}
}
/++
Writes the text in the scrollback buffer to the given delegate, one character at a time.
Discards formatting information and embedded images.
See_Also:
[writeScrollbackToFile]
History:
Added March 14, 2021 (dub version 9.4)
+/
public void writeScrollbackToDelegate(scope void delegate(dchar c) dg) {
foreach(line; scrollbackBuffer[]) {
foreach(c; line)
if(!c.hasNonCharacterData)
dg(c.ch);
dg('\n');
}
}
public void drawScrollback(bool useAltScreen = false) {
showScrollbackOnScreen(useAltScreen ? alternateScreen : normalScreen, 0, true, 0);
}
private void showScrollbackOnScreen(ref TerminalCell[] screen, int howFar, bool reflow, int howFarX) {
int start;
cursorX = 0;
cursorY = 0;
int excess = 0;
if(scrollbackReflow) {
int numLines;
int idx = cast(int) scrollbackBuffer.length - 1;
foreach_reverse(line; scrollbackBuffer[]) {
auto lineCount = 1 + line.length / screenWidth;
// if the line has an image in it, it cannot be reflowed. this hack to check just the first and last thing is the cheapest way rn
if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
lineCount = 1;
numLines += lineCount;
if(numLines >= (screenHeight + howFar)) {
start = cast(int) idx;
excess = numLines - (screenHeight + howFar);
break;
}
idx--;
}
} else {
auto termination = cast(int) scrollbackBuffer.length - howFar;
if(termination < 0)
termination = cast(int) scrollbackBuffer.length;
start = termination - screenHeight;
if(start < 0)
start = 0;
}
TerminalCell overflowCell;
overflowCell.ch = '\&raquo;';
overflowCell.attributes.backgroundIndex = 3;
overflowCell.attributes.foregroundIndex = 0;
version(with_24_bit_color) {
overflowCell.attributes.foreground = Color(40, 40, 40);
overflowCell.attributes.background = Color.yellow;
}
outer: foreach(line; scrollbackBuffer[start .. $]) {
if(excess) {
line = line[excess * screenWidth .. $];
excess = 0;
}
if(howFarX) {
if(howFarX <= line.length)
line = line[howFarX .. $];
else
line = null;
}
bool overflowed;
foreach(cell; line) {
cell.invalidated = true;
if(overflowed) {
screen[cursorY * screenWidth + cursorX] = overflowCell;
break;
} else {
screen[cursorY * screenWidth + cursorX] = cell;
}
if(cursorX == screenWidth-1) {
if(scrollbackReflow) {
// don't attempt to reflow images
if(cell.hasNonCharacterData)
break;
cursorX = 0;
if(cursorY + 1 == screenHeight)
break outer;
cursorY = cursorY + 1;
} else {
overflowed = true;
}
} else
cursorX = cursorX + 1;
}
if(cursorY + 1 == screenHeight)
break;
cursorY = cursorY + 1;
cursorX = 0;
}
extendInvalidatedRange();
cursorX = 0;
}
protected bool cueScrollback;
public void resizeTerminal(int w, int h) {
if(w == screenWidth && h == screenHeight)
return; // we're already good, do nothing to avoid wasting time and possibly losing a line (bash doesn't seem to like being told it "resized" to the same size)
// do i like this?
if(scrollLock)
toggleScrollLock();
// FIXME: hack
endScrollback();
screenWidth = w;
screenHeight = h;
normalScreen.length = screenWidth * screenHeight;
alternateScreen.length = screenWidth * screenHeight;
scrollZoneBottom = screenHeight - 1;
if(scrollZoneTop < 0 || scrollZoneTop >= scrollZoneBottom)
scrollZoneTop = 0;
// we need to make sure the state is sane all across the board, so first we'll clear everything...
TerminalCell plain;
plain.ch = ' ';
plain.attributes = defaultTextAttributes;
plain.invalidated = true;
normalScreen[] = plain;
alternateScreen[] = plain;
extendInvalidatedRange();
// then, in normal mode, we'll redraw using the scrollback buffer
//
// if we're in the alternate screen though, keep it blank because
// while redrawing makes sense in theory, odds are the program in
// charge of the normal screen didn't get the resize signal.
if(!alternateScreenActive)
showScrollbackOnScreen(normalScreen, 0, true, 0);
else
cueScrollback = true;
// but in alternate mode, it is the application's responsibility
// the property ensures these are within bounds so this set just forces that
cursorY = cursorY;
cursorX = cursorX;
recalculateScrollbackLength();
}
private CursorPosition popSavedCursor() {
CursorPosition pos;
//import std.stdio; writeln("popped");
if(savedCursors.length) {
pos = savedCursors[$-1];
savedCursors = savedCursors[0 .. $-1];
savedCursors.assumeSafeAppend(); // we never keep references elsewhere so might as well reuse the memory as much as we can
}
// If the screen resized after this was saved, it might be restored to a bad amount, so we need to sanity test.
if(pos.x < 0)
pos.x = 0;
if(pos.y < 0)
pos.y = 0;
if(pos.x > screenWidth)
pos.x = screenWidth - 1;
if(pos.y > screenHeight)
pos.y = screenHeight - 1;
return pos;
}
private void pushSavedCursor(CursorPosition pos) {
//import std.stdio; writeln("pushed");
savedCursors ~= pos;
}
public void clearScrollbackHistory() {
if(scrollingBack)
endScrollback();
scrollbackBuffer.clear();
scrollbackLength_ = 0;
scrollbackWidth_ = 0;
notifyScrollbackAdded();
}
public void moveCursor(int x, int y) {
cursorX = x;
cursorY = y;
}
/* FIXME: i want these to be private */
protected {
TextAttributes currentAttributes;
CursorPosition cursorPosition;
CursorPosition[] savedCursors; // a stack
CursorStyle cursorStyle;
Color cursorColor;
string windowTitle;
string iconTitle;
bool attentionDemanded;
IndexedImage windowIcon;
IndexedImage[] iconStack;
string[] titleStack;
bool bracketedPasteMode;
bool bracketedHyperlinkMode;
bool mouseButtonTracking;
private bool _mouseMotionTracking;
bool utf8MouseMode;
bool mouseButtonReleaseTracking;
bool mouseButtonMotionTracking;
bool selectiveMouseTracking;
/+
When set, it causes xterm to send CSI I when the terminal gains focus, and CSI O when it loses focus.
this is turned on by mode 1004 with mouse events.
FIXME: not implemented.
+/
bool sendFocusEvents;
bool mouseMotionTracking() {
return _mouseMotionTracking;
}
void mouseMotionTracking(bool b) {
_mouseMotionTracking = b;
}
void allMouseTrackingOff() {
selectiveMouseTracking = false;
mouseMotionTracking = false;
mouseButtonTracking = false;
mouseButtonReleaseTracking = false;
mouseButtonMotionTracking = false;
sendFocusEvents = false;
}
bool wraparoundMode = true;
bool alternateScreenActive;
bool cursorShowing = true;
bool reverseVideo;
bool applicationCursorKeys;
bool scrollingEnabled = true;
int scrollZoneTop;
int scrollZoneBottom;
int screenWidth;
int screenHeight;
// assert(alternateScreen.length = screenWidth * screenHeight);
TerminalCell[] alternateScreen;
TerminalCell[] normalScreen;
// the lengths can be whatever
ScrollbackBuffer scrollbackBuffer;
static struct ScrollbackBuffer {
TerminalCell[][] backing;
enum maxScrollback = 8192 / 2; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask...
int start;
int length_;
size_t length() {
return length_;
}
void clear() {
start = 0;
length_ = 0;
backing = null;
}
// FIXME: if scrollback hits limits the scroll bar needs
// to understand the circular buffer
void opOpAssign(string op : "~")(TerminalCell[] line) {
if(length_ < maxScrollback) {
backing.assumeSafeAppend();
backing ~= line;
length_++;
} else {
backing[start] = line;
start++;
if(start == maxScrollback)
start = 0;
}
}
/*
int opApply(scope int delegate(ref TerminalCell[]) dg) {
foreach(ref l; backing)
if(auto res = dg(l))
return res;
return 0;
}
int opApplyReverse(scope int delegate(size_t, ref TerminalCell[]) dg) {
foreach_reverse(idx, ref l; backing)
if(auto res = dg(idx, l))
return res;
return 0;
}
*/
TerminalCell[] opIndex(int idx) {
return backing[(start + idx) % maxScrollback];
}
ScrollbackBufferRange opSlice(int startOfIteration, Dollar end) {
return ScrollbackBufferRange(&this, startOfIteration);
}
ScrollbackBufferRange opSlice() {
return ScrollbackBufferRange(&this, 0);
}
static struct ScrollbackBufferRange {
ScrollbackBuffer* item;
int position;
int remaining;
this(ScrollbackBuffer* item, int startOfIteration) {
this.item = item;
position = startOfIteration;
remaining = cast(int) item.length - startOfIteration;
}
TerminalCell[] front() { return (*item)[position]; }
bool empty() { return remaining <= 0; }
void popFront() {
position++;
remaining--;
}
TerminalCell[] back() { return (*item)[remaining - 1 - position]; }
void popBack() {
remaining--;
}
}
static struct Dollar {};
Dollar opDollar() { return Dollar(); }
}
struct Helper2 {
size_t row;
TerminalEmulator t;
this(TerminalEmulator t, size_t row) {
this.t = t;
this.row = row;
}
ref TerminalCell opIndex(size_t cell) {
auto thing = t.alternateScreenActive ? &(t.alternateScreen) : &(t.normalScreen);
return (*thing)[row * t.screenWidth + cell];
}
}
struct Helper {
TerminalEmulator t;
this(TerminalEmulator t) {
this.t = t;
}
Helper2 opIndex(size_t row) {
return Helper2(t, row);
}
}
@property Helper ASS() {
return Helper(this);
}
@property int cursorX() { return cursorPosition.x; }
@property int cursorY() { return cursorPosition.y; }
@property void cursorX(int x) {
if(x < 0)
x = 0;
if(x >= screenWidth)
x = screenWidth - 1;
cursorPosition.x = x;
}
@property void cursorY(int y) {
if(y < 0)
y = 0;
if(y >= screenHeight)
y = screenHeight - 1;
cursorPosition.y = y;
}
void addOutput(string b) {
foreach(c; b)
addOutput(c);
}
TerminalCell[] currentScrollbackLine;
ubyte[6] utf8SequenceBuffer;
int utf8SequenceBufferPosition;
// int scrollbackWrappingAt = 0;
dchar utf8Sequence;
int utf8BytesRemaining;
int currentUtf8Shift;
bool newLineOnNext;
void addOutput(ubyte b) {
void addChar(dchar c) {
if(newLineOnNext) {
newLineOnNext = false;
// only if we're still on the right side...
if(cursorX == screenWidth - 1)
newLine(false);
}
TerminalCell tc;
if(characterSet !is null) {
if(auto replacement = utf8Sequence in *characterSet)
utf8Sequence = *replacement;
}
tc.ch = utf8Sequence;
tc.attributes = currentAttributes;
tc.invalidated = true;
if(hyperlinkActive) {
tc.hyperlinkStatus = hyperlinkFlipper ? 3 : 1;
tc.hyperlinkBit = hyperlinkNumber & 0x01;
hyperlinkNumber >>= 1;
}
addOutput(tc);
}
// this takes in bytes at a time, but since the input encoding is assumed to be UTF-8, we need to gather the bytes
if(utf8BytesRemaining == 0) {
// we're at the beginning of a sequence
utf8Sequence = 0;
if(b < 128) {
utf8Sequence = cast(dchar) b;
// one byte thing, do nothing more...
} else {
// the number of bytes in the sequence is the number of set bits in the first byte...
ubyte checkingBit = 7;
while(b & (1 << checkingBit)) {
utf8BytesRemaining++;
checkingBit--;
}
uint shifted = b & ((1 << checkingBit) - 1);
utf8BytesRemaining--; // since this current byte counts too
currentUtf8Shift = utf8BytesRemaining * 6;
shifted <<= currentUtf8Shift;
utf8Sequence = cast(dchar) shifted;
utf8SequenceBufferPosition = 0;
utf8SequenceBuffer[utf8SequenceBufferPosition++] = b;
}
} else {
// add this to the byte we're doing right now...
utf8BytesRemaining--;
currentUtf8Shift -= 6;
if((b & 0b11000000) != 0b10000000) {
// invalid utf-8 sequence,
// discard it and try to continue
utf8BytesRemaining = 0;
utf8Sequence = 0xfffd;
foreach(i; 0 .. utf8SequenceBufferPosition)
addChar(utf8Sequence); // put out replacement char for everything in there so far
utf8SequenceBufferPosition = 0;
addOutput(b); // retry sending this byte as a new sequence after abandoning the old crap
return;
}
uint shifted = b;
shifted &= 0b00111111;
shifted <<= currentUtf8Shift;
utf8Sequence |= shifted;
if(utf8SequenceBufferPosition < utf8SequenceBuffer.length)
utf8SequenceBuffer[utf8SequenceBufferPosition++] = b;
}
if(utf8BytesRemaining)
return; // not enough data yet, wait for more before displaying anything
if(utf8Sequence == 10) {
newLineOnNext = false;
auto cx = cursorX; // FIXME: this cx thing is a hack, newLine should prolly just do the right thing
/*
TerminalCell tc;
tc.ch = utf8Sequence;
tc.attributes = currentAttributes;
tc.invalidated = true;
addOutput(tc);
*/
newLine(true);
cursorX = cx;
} else {
addChar(utf8Sequence);
}
}
private int recalculationThreshold = 0;
public void addScrollbackLine(TerminalCell[] line) {
scrollbackBuffer ~= line;
if(scrollbackBuffer.length_ == ScrollbackBuffer.maxScrollback) {
recalculationThreshold++;
if(recalculationThreshold > 100) {
recalculateScrollbackLength();
notifyScrollbackAdded();
recalculationThreshold = 0;
}
} else {
if(!scrollbackReflow && line.length > scrollbackWidth_)
scrollbackWidth_ = cast(int) line.length;
if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
scrollbackLength = scrollbackLength + 1;
else
scrollbackLength = cast(int) (scrollbackLength + 1 + (scrollbackBuffer[cast(int) scrollbackBuffer.length - 1].length) / screenWidth);
notifyScrollbackAdded();
}
if(!alternateScreenActive)
notifyScrollbarPosition(0, int.max);
}
protected int maxScrollbackLength() pure const @nogc nothrow {
return 1024;
}
bool insertMode = false;
void newLine(bool commitScrollback) {
extendInvalidatedRange(); // FIXME
if(!alternateScreenActive && commitScrollback) {
// I am limiting this because obscenely long lines are kinda useless anyway and
// i don't want it to eat excessive memory when i spam some thing accidentally
if(currentScrollbackLine.length < maxScrollbackLength())
addScrollbackLine(currentScrollbackLine.sliceTrailingWhitespace);
else
addScrollbackLine(currentScrollbackLine[0 .. maxScrollbackLength()].sliceTrailingWhitespace);
currentScrollbackLine = null;
currentScrollbackLine.reserve(64);
// scrollbackWrappingAt = 0;
}
cursorX = 0;
if(scrollingEnabled && cursorY >= scrollZoneBottom) {
size_t idx = scrollZoneTop * screenWidth;
// When we scroll up, we need to update the selection position too
if(selectionStart != selectionEnd) {
selectionStart -= screenWidth;
selectionEnd -= screenWidth;
}
foreach(l; scrollZoneTop .. scrollZoneBottom) {
if(alternateScreenActive) {
if(idx + screenWidth * 2 > alternateScreen.length)
break;
alternateScreen[idx .. idx + screenWidth] = alternateScreen[idx + screenWidth .. idx + screenWidth * 2];
} else {
if(screenWidth <= 0)
break;
if(idx + screenWidth * 2 > normalScreen.length)
break;
normalScreen[idx .. idx + screenWidth] = normalScreen[idx + screenWidth .. idx + screenWidth * 2];
}
idx += screenWidth;
}
/*
foreach(i; 0 .. screenWidth) {
if(alternateScreenActive) {
alternateScreen[idx] = alternateScreen[idx + screenWidth];
alternateScreen[idx].invalidated = true;
} else {
normalScreen[idx] = normalScreen[idx + screenWidth];
normalScreen[idx].invalidated = true;
}
idx++;
}
*/
/*
foreach(i; 0 .. screenWidth) {
if(alternateScreenActive) {
alternateScreen[idx].ch = ' ';
alternateScreen[idx].attributes = currentAttributes;
alternateScreen[idx].invalidated = true;
} else {
normalScreen[idx].ch = ' ';
normalScreen[idx].attributes = currentAttributes;
normalScreen[idx].invalidated = true;
}
idx++;
}
*/
TerminalCell plain;
plain.ch = ' ';
plain.attributes = currentAttributes;
if(alternateScreenActive) {
alternateScreen[idx .. idx + screenWidth] = plain;
} else {
normalScreen[idx .. idx + screenWidth] = plain;
}
} else {
if(insertMode) {
scrollDown();
} else
cursorY = cursorY + 1;
}
invalidateAll = true;
}
protected bool invalidateAll;
void clearSelection() {
clearSelectionInternal();
cancelOverriddenSelection();
}
private void clearSelectionInternal() {
foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen)
if(tc.selected) {
tc.selected = false;
tc.invalidated = true;
}
selectionStart = 0;
selectionEnd = 0;
extendInvalidatedRange();
}
private int tentativeScrollback = int.max;
private void setTentativeScrollback(int a) {
tentativeScrollback = a;
}
void addScrollbackOutput(dchar ch) {
TerminalCell plain;
plain.ch = ch;
plain.attributes = currentAttributes;
addScrollbackOutput(plain);
}
void addScrollbackOutput(TerminalCell tc) {
if(tentativeScrollback != int.max) {
if(tentativeScrollback >= 0 && tentativeScrollback < currentScrollbackLine.length) {
currentScrollbackLine = currentScrollbackLine[0 .. tentativeScrollback];
currentScrollbackLine.assumeSafeAppend();
}
tentativeScrollback = int.max;
}
/*
TerminalCell plain;
plain.ch = ' ';
plain.attributes = currentAttributes;
int lol = cursorX + scrollbackWrappingAt;
while(lol >= currentScrollbackLine.length)
currentScrollbackLine ~= plain;
currentScrollbackLine[lol] = tc;
*/
currentScrollbackLine ~= tc;
}
void addOutput(TerminalCell tc) {
if(alternateScreenActive) {
if(alternateScreen[cursorY * screenWidth + cursorX].selected) {
clearSelection();
}
alternateScreen[cursorY * screenWidth + cursorX] = tc;
} else {
if(normalScreen[cursorY * screenWidth + cursorX].selected) {
clearSelection();
}
// FIXME: make this more efficient if it is writing the same thing,
// then it need not be invalidated. Same with above for the alt screen
normalScreen[cursorY * screenWidth + cursorX] = tc;
addScrollbackOutput(tc);
}
extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY);
// FIXME: the wraparoundMode seems to help gnu screen but then it doesn't go away properly and that messes up bash...
//if(wraparoundMode && cursorX == screenWidth - 1) {
if(cursorX == screenWidth - 1) {
// FIXME: should this check the scrolling zone instead?
newLineOnNext = true;
//if(!alternateScreenActive || cursorY < screenHeight - 1)
//newLine(false);
// scrollbackWrappingAt = cast(int) currentScrollbackLine.length;
} else
cursorX = cursorX + 1;
}
void tryEsc(ubyte[] esc) {
bool[2] sidxProcessed;
int[][2] argsAtSidx;
int[12][2] argsAtSidxBuffer;
int[12][4] argsBuffer;
int argsBufferLocation;
int[] getArgsBase(int sidx, int[] defaults) {
assert(sidx == 1 || sidx == 2);
if(sidxProcessed[sidx - 1]) {
int[] bfr = argsBuffer[argsBufferLocation++][];
if(argsBufferLocation == argsBuffer.length)
argsBufferLocation = 0;
bfr[0 .. defaults.length] = defaults[];
foreach(idx, v; argsAtSidx[sidx - 1])
if(v != int.min)
bfr[idx] = v;
return bfr[0 .. max(argsAtSidx[sidx - 1].length, defaults.length)];
}
auto end = esc.length - 1;
foreach(iii, b; esc[sidx .. end]) {
if(b >= 0x20 && b < 0x30)
end = iii + sidx;
}
auto argsSection = cast(char[]) esc[sidx .. end];
int[] args = argsAtSidxBuffer[sidx - 1][];
import std.string : split;
import std.conv : to;
int lastIdx = 0;
foreach(i, arg; split(argsSection, ";")) {
int value;
if(arg.length) {
//import std.stdio; writeln(esc);
value = to!int(arg);
} else
value = int.min; // defaults[i];
if(args.length > i)
args[i] = value;
else
assert(0);
lastIdx++;
}
argsAtSidx[sidx - 1] = args[0 .. lastIdx];
sidxProcessed[sidx - 1] = true;
return getArgsBase(sidx, defaults);
}
int[] getArgs(int[] defaults...) {
return getArgsBase(1, defaults);
}
// FIXME
// from http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
// check out this section: "Window manipulation (from dtterm, as well as extensions)"
// especially the title stack, that should rock
/*
P s = 2 2 ; 0 Save xterm icon and window title on stack.
P s = 2 2 ; 1 Save xterm icon title on stack.
P s = 2 2 ; 2 Save xterm window title on stack.
P s = 2 3 ; 0 Restore xterm icon and window title from stack.
P s = 2 3 ; 1 Restore xterm icon title from stack.
P s = 2 3 ; 2 Restore xterm window title from stack.
*/
if(esc[0] == ']' && esc.length > 1) {
int idx = -1;
foreach(i, e; esc)
if(e == ';') {
idx = cast(int) i;
break;
}
if(idx != -1) {
auto arg = cast(char[]) esc[idx + 1 .. $-1];
switch(cast(char[]) esc[1..idx]) {
case "0":
// icon name and window title
windowTitle = iconTitle = arg.idup;
changeWindowTitle(windowTitle);
changeIconTitle(iconTitle);
break;
case "1":
// icon name
iconTitle = arg.idup;
changeIconTitle(iconTitle);
break;
case "2":
// window title
windowTitle = arg.idup;
changeWindowTitle(windowTitle);
break;
case "10":
// change default text foreground color
break;
case "11":
// change gui background color
break;
case "12":
if(arg.length)
arg = arg[1 ..$]; // skip past the thing
if(arg.length) {
cursorColor = Color.fromString(arg);
foreach(ref p; cursorColor.components[0 .. 3])
p ^= 0xff;
} else
cursorColor = Color.white;
break;
case "50":
// change font
break;
case "52":
// 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;?") {
// i'm using this to request a paste. not quite compatible with xterm, but kinda
// because xterm tends not to answer anyway.
pasteFromPrimary(&sendPasteData);
} else if(arg.length > 2 && arg[0 .. 2] == "p;") {
auto info = arg[2 .. $];
try {
import std.base64;
auto data = Base64.decode(info);
copyToPrimary(cast(string) data);
} catch(Exception e) {}
}
if(arg == "c;?") {
// i'm using this to request a paste. not quite compatible with xterm, but kinda
// because xterm tends not to answer anyway.
pasteFromClipboard(&sendPasteData);
} else if(arg.length > 2 && arg[0 .. 2] == "c;") {
auto info = arg[2 .. $];
try {
import std.base64;
auto data = Base64.decode(info);
copyToClipboard(cast(string) data);
} 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
// set color #0 == black
// echo -e '\033]4;0;black\007'
/*
echo -e '\033]4;9;?\007' ; cat
^[]4;9;rgb:ffff/0000/0000^G
*/
// FIXME: if the palette changes, we should redraw so the change is immediately visible (as if we were using a real palette)
break;
case "104":
// palette reset
// reset color #0
// echo -e '\033[104;0\007'
break;
/* Extensions */
case "5000":
// change window icon (send a base64 encoded image or something)
/*
The format here is width and height as a single char each
'0'-'9' == 0-9
'a'-'z' == 10 - 36
anything else is invalid
then a palette in hex rgba format (8 chars each), up to 26 entries
then a capital Z
if a palette entry == 'P', it means pull from the current palette (FIXME not implemented)
then 256 characters between a-z (must be lowercase!) which are the palette entries for
the pixels, top to bottom, left to right, so the image is 16x16. if it ends early, the
rest of the data is assumed to be zero
you can also do e.g. 22a, which means repeat a 22 times for some RLE.
anything out of range aborts the operation
*/
auto img = readSmallTextImage(arg);
windowIcon = img;
changeWindowIcon(img);
break;
case "5001":
// demand attention
attentionDemanded = true;
demandAttention();
break;
/+
// this might reduce flickering but would it really? idk.
case "5002":
// disable redraw
break;
case "5003":
// re-enable redraw, force it now.
break;
+/
default:
unknownEscapeSequence("" ~ cast(char) esc[1]);
}
}
} else if(esc[0] == '[' && esc.length > 1) {
switch(esc[$-1]) {
case 'Z':
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
// FIXME?
break;
case 'n':
switch(esc[$-2]) {
import std.string;
// request status report, reply OK
case '5': sendToApplication("\033[0n"); break;
// request cursor position
case '6': sendToApplication(format("\033[%d;%dR", cursorY + 1, cursorX + 1)); break;
default: unknownEscapeSequence(cast(string) esc);
}
break;
case 'A': if(cursorY) cursorY = cursorY - getArgs(1)[0]; break;
case 'B': if(cursorY != this.screenHeight - 1) cursorY = cursorY + getArgs(1)[0]; break;
case 'D': if(cursorX) cursorX = cursorX - getArgs(1)[0]; setTentativeScrollback(cursorX); break;
case 'C': if(cursorX != this.screenWidth - 1) cursorX = cursorX + getArgs(1)[0]; break;
case 'd': cursorY = getArgs(1)[0]-1; break;
case 'E': cursorY = cursorY + getArgs(1)[0]; cursorX = 0; break;
case 'F': cursorY = cursorY - getArgs(1)[0]; cursorX = 0; break;
case 'G': cursorX = getArgs(1)[0] - 1; break;
case 'f': // wikipedia says it is the same except it is a format func instead of editor func. idk what the diff is
case 'H':
auto got = getArgs(1, 1);
cursorX = got[1] - 1;
if(got[0] - 1 == cursorY)
setTentativeScrollback(cursorX);
else
setTentativeScrollback(0);
cursorY = got[0] - 1;
newLineOnNext = false;
break;
case 'L':
// insert lines
scrollDown(getArgs(1)[0]);
break;
case 'M':
// delete lines
if(cursorY + 1 < screenHeight) {
TerminalCell plain;
plain.ch = ' ';
plain.attributes = defaultTextAttributes();
foreach(i; 0 .. getArgs(1)[0]) {
foreach(y; cursorY .. scrollZoneBottom)
foreach(x; 0 .. screenWidth) {
ASS[y][x] = ASS[y + 1][x];
ASS[y][x].invalidated = true;
}
foreach(x; 0 .. screenWidth) {
ASS[scrollZoneBottom][x] = plain;
}
}
extendInvalidatedRange();
}
break;
case 'K':
auto arg = getArgs(0)[0];
int start, end;
if(arg == 0) {
// clear from cursor to end of line
start = cursorX;
end = this.screenWidth;
} else if(arg == 1) {
// clear from cursor to beginning of line
start = 0;
end = cursorX + 1;
} else if(arg == 2) {
// clear entire line
start = 0;
end = this.screenWidth;
}
TerminalCell plain;
plain.ch = ' ';
plain.attributes = currentAttributes;
for(int i = start; i < end; i++) {
if(ASS[cursorY][i].selected)
clearSelection();
ASS[cursorY]
[i] = plain;
}
break;
case 's':
pushSavedCursor(cursorPosition);
break;
case 'u':
cursorPosition = popSavedCursor();
break;
case 'g':
auto arg = getArgs(0)[0];
TerminalCell plain;
plain.ch = ' ';
plain.attributes = currentAttributes;
if(arg == 0) {
// clear current column
for(int i = 0; i < this.screenHeight; i++)
ASS[i]
[cursorY] = plain;
} else if(arg == 3) {
// clear all
cls();
}
break;
case 'q':
// xterm also does blinks on the odd numbers (x-1)
if(esc == "[0 q")
cursorStyle = CursorStyle.block; // FIXME: restore default
if(esc == "[2 q")
cursorStyle = CursorStyle.block;
else if(esc == "[4 q")
cursorStyle = CursorStyle.underline;
else if(esc == "[6 q")
cursorStyle = CursorStyle.bar;
changeCursorStyle(cursorStyle);
break;
case 't':
// window commands
// i might support more of these but for now i just want the stack stuff.
auto args = getArgs(0, 0);
if(args[0] == 22) {
// save window title to stack
// xterm says args[1] should tell if it is the window title, the icon title, or both, but meh
titleStack ~= windowTitle;
iconStack ~= windowIcon;
} else if(args[0] == 23) {
// restore from stack
if(titleStack.length) {
windowTitle = titleStack[$ - 1];
changeWindowTitle(titleStack[$ - 1]);
titleStack = titleStack[0 .. $ - 1];
}
if(iconStack.length) {
windowIcon = iconStack[$ - 1];
changeWindowIcon(iconStack[$ - 1]);
iconStack = iconStack[0 .. $ - 1];
}
}
break;
case 'm':
// FIXME used by xterm to decide whether to construct
// CSI > Pp ; Pv m CSI > Pp m Set/reset key modifier options, xterm.
if(esc[1] == '>')
goto default;
// done
argsLoop: foreach(argIdx, arg; getArgs(0))
switch(arg) {
case 0:
// normal
currentAttributes = defaultTextAttributes;
break;
case 1:
currentAttributes.bold = true;
break;
case 2:
currentAttributes.faint = true;
break;
case 3:
currentAttributes.italic = true;
break;
case 4:
currentAttributes.underlined = true;
break;
case 5:
currentAttributes.blink = true;
break;
case 6:
// rapid blink, treating the same as regular blink
currentAttributes.blink = true;
break;
case 7:
currentAttributes.inverse = true;
break;
case 8:
currentAttributes.invisible = true;
break;
case 9:
currentAttributes.strikeout = true;
break;
case 10:
// primary font
break;
case 11: .. case 19:
// alternate fonts
break;
case 20:
// Fraktur font
break;
case 21:
// bold off and doubled underlined
break;
case 22:
currentAttributes.bold = false;
currentAttributes.faint = false;
break;
case 23:
currentAttributes.italic = false;
break;
case 24:
currentAttributes.underlined = false;
break;
case 25:
currentAttributes.blink = false;
break;
case 26:
// reserved
break;
case 27:
currentAttributes.inverse = false;
break;
case 28:
currentAttributes.invisible = false;
break;
case 29:
currentAttributes.strikeout = false;
break;
case 30:
..
case 37:
// set foreground color
/*
Color nc;
ubyte multiplier = currentAttributes.bold ? 255 : 127;
nc.r = cast(ubyte)((arg - 30) & 1) * multiplier;
nc.g = cast(ubyte)(((arg - 30) & 2)>>1) * multiplier;
nc.b = cast(ubyte)(((arg - 30) & 4)>>2) * multiplier;
nc.a = 255;
*/
currentAttributes.foregroundIndex = cast(ubyte)(arg - 30);
version(with_24_bit_color)
currentAttributes.foreground = palette[arg-30 + (currentAttributes.bold ? 8 : 0)];
break;
case 38:
// xterm 256 color set foreground color
auto args = getArgs()[argIdx + 1 .. $];
if(args.length > 3 && args[0] == 2) {
// set color to closest match in palette. but since we have full support, we'll just take it directly
auto fg = Color(args[1], args[2], args[3]);
version(with_24_bit_color)
currentAttributes.foreground = fg;
// and try to find a low default palette entry for maximum compatibility
// 0x8000 == approximation
currentAttributes.foregroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 16], fg);
} else if(args.length > 1 && args[0] == 5) {
// set to palette index
version(with_24_bit_color)
currentAttributes.foreground = palette[args[1]];
currentAttributes.foregroundIndex = cast(ushort) args[1];
}
break argsLoop;
case 39:
// default foreground color
auto dflt = defaultTextAttributes();
version(with_24_bit_color)
currentAttributes.foreground = dflt.foreground;
currentAttributes.foregroundIndex = dflt.foregroundIndex;
break;
case 40:
..
case 47:
// set background color
/*
Color nc;
nc.r = cast(ubyte)((arg - 40) & 1) * 255;
nc.g = cast(ubyte)(((arg - 40) & 2)>>1) * 255;
nc.b = cast(ubyte)(((arg - 40) & 4)>>2) * 255;
nc.a = 255;
*/
currentAttributes.backgroundIndex = cast(ubyte)(arg - 40);
//currentAttributes.background = nc;
version(with_24_bit_color)
currentAttributes.background = palette[arg-40];
break;
case 48:
// xterm 256 color set background color
auto args = getArgs()[argIdx + 1 .. $];
if(args.length > 3 && args[0] == 2) {
// set color to closest match in palette. but since we have full support, we'll just take it directly
auto bg = Color(args[1], args[2], args[3]);
version(with_24_bit_color)
currentAttributes.background = Color(args[1], args[2], args[3]);
// and try to find a low default palette entry for maximum compatibility
// 0x8000 == this is an approximation
currentAttributes.backgroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 8], bg);
} else if(args.length > 1 && args[0] == 5) {
// set to palette index
version(with_24_bit_color)
currentAttributes.background = palette[args[1]];
currentAttributes.backgroundIndex = cast(ushort) args[1];
}
break argsLoop;
case 49:
// default background color
auto dflt = defaultTextAttributes();
version(with_24_bit_color)
currentAttributes.background = dflt.background;
currentAttributes.backgroundIndex = dflt.backgroundIndex;
break;
case 51:
// framed
break;
case 52:
// encircled
break;
case 53:
// overlined
break;
case 54:
// not framed or encircled
break;
case 55:
// not overlined
break;
case 90: .. case 97:
// high intensity foreground color
break;
case 100: .. case 107:
// high intensity background color
break;
default:
unknownEscapeSequence(cast(string) esc);
}
break;
case 'J':
// erase in display
auto arg = getArgs(0)[0];
switch(arg) {
case 0:
TerminalCell plain;
plain.ch = ' ';
plain.attributes = currentAttributes;
// erase below
foreach(i; cursorY * screenWidth + cursorX .. screenWidth * screenHeight) {
if(alternateScreenActive)
alternateScreen[i] = plain;
else
normalScreen[i] = plain;
}
break;
case 1:
// erase above
unknownEscapeSequence("FIXME");
break;
case 2:
// erase all
cls();
break;
default: unknownEscapeSequence(cast(string) esc);
}
break;
case 'r':
if(esc[1] != '?') {
// set scrolling zone
// default should be full size of window
auto args = getArgs(1, screenHeight);
// FIXME: these are supposed to be per-buffer
scrollZoneTop = args[0] - 1;
scrollZoneBottom = args[1] - 1;
if(scrollZoneTop < 0)
scrollZoneTop = 0;
if(scrollZoneBottom > screenHeight)
scrollZoneBottom = screenHeight - 1;
} else {
// restore... something FIXME
}
break;
case 'h':
if(esc[1] != '?')
foreach(arg; getArgs())
switch(arg) {
case 4:
insertMode = true;
break;
case 34:
// no idea. vim inside screen sends it
break;
default: unknownEscapeSequence(cast(string) esc);
}
else
//import std.stdio; writeln("h magic ", cast(string) esc);
foreach(arg; getArgsBase(2, null)) {
if(arg > 65535) {
/* Extensions */
if(arg < 65536 + 65535) {
// activate hyperlink
hyperlinkFlipper = !hyperlinkFlipper;
hyperlinkActive = true;
hyperlinkNumber = arg - 65536;
}
} else
switch(arg) {
case 1:
// application cursor keys
applicationCursorKeys = true;
break;
case 3:
// 132 column mode
break;
case 4:
// smooth scroll
break;
case 5:
// reverse video
reverseVideo = true;
break;
case 6:
// origin mode
break;
case 7:
// wraparound mode
wraparoundMode = false;
// FIXME: wraparoundMode i think is supposed to be off by default but then bash doesn't work right so idk, this gives the best results
break;
case 9:
allMouseTrackingOff();
mouseButtonTracking = true;
break;
case 12:
// start blinking cursor
break;
case 1034:
// meta keys????
break;
case 1049:
// Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first.
alternateScreenActive = true;
scrollLock = false;
pushSavedCursor(cursorPosition);
cls();
notifyScrollbarRelevant(false, false);
break;
case 1000:
// send mouse X&Y on button press and release
allMouseTrackingOff();
mouseButtonTracking = true;
mouseButtonReleaseTracking = true;
break;
case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
break;
case 1002:
allMouseTrackingOff();
mouseButtonTracking = true;
mouseButtonReleaseTracking = true;
mouseButtonMotionTracking = true;
// use cell motion mouse tracking
break;
case 1003:
// ALL motion is sent
allMouseTrackingOff();
mouseButtonTracking = true;
mouseButtonReleaseTracking = true;
mouseMotionTracking = true;
break;
case 1004:
sendFocusEvents = true;
break;
case 1005:
utf8MouseMode = true;
// enable utf-8 mouse mode
/*
UTF-8 (1005)
This enables UTF-8 encoding for Cx and Cy under all tracking
modes, expanding the maximum encodable position from 223 to
2015. For positions less than 95, the resulting output is
identical under both modes. Under extended mouse mode, posi-
tions greater than 95 generate "extra" bytes which will con-
fuse applications which do not treat their input as a UTF-8
stream. Likewise, Cb will be UTF-8 encoded, to reduce confu-
sion with wheel mouse events.
Under normal mouse mode, positions outside (160,94) result in
byte pairs which can be interpreted as a single UTF-8 charac-
ter; applications which do treat their input as UTF-8 will
almost certainly be confused unless extended mouse mode is
active.
This scheme has the drawback that the encoded coordinates will
not pass through luit unchanged, e.g., for locales using non-
UTF-8 encoding.
*/
break;
case 1006:
/*
SGR (1006)
The normal mouse response is altered to use CSI < followed by
semicolon-separated encoded button value, the Cx and Cy ordi-
nates and a final character which is M for button press and m
for button release.
o The encoded button value in this case does not add 32 since
that was useful only in the X10 scheme for ensuring that the
byte containing the button value is a printable code.
o The modifiers are encoded in the same way.
o A different final character is used for button release to
resolve the X10 ambiguity regarding which button was
released.
The highlight tracking responses are also modified to an SGR-
like format, using the same SGR-style scheme and button-encod-
ings.
*/
break;
case 1014:
// ARSD extension: it is 1002 but selective, only
// on top row, row with cursor, or else if middle click/wheel.
//
// Quite specifically made for my getline function!
allMouseTrackingOff();
mouseButtonMotionTracking = true;
mouseButtonTracking = true;
mouseButtonReleaseTracking = true;
selectiveMouseTracking = true;
break;
case 1015:
/*
URXVT (1015)
The normal mouse response is altered to use CSI followed by
semicolon-separated encoded button value, the Cx and Cy ordi-
nates and final character M .
This uses the same button encoding as X10, but printing it as
a decimal integer rather than as a single byte.
However, CSI M can be mistaken for DL (delete lines), while
the highlight tracking CSI T can be mistaken for SD (scroll
down), and the Window manipulation controls. For these rea-
sons, the 1015 control is not recommended; it is not an
improvement over 1005.
*/
break;
case 1048:
pushSavedCursor(cursorPosition);
break;
case 2004:
bracketedPasteMode = true;
break;
case 3004:
bracketedHyperlinkMode = true;
break;
case 1047:
case 47:
alternateScreenActive = true;
scrollLock = false;
cls();
notifyScrollbarRelevant(false, false);
break;
case 25:
cursorShowing = true;
break;
/* Done */
default: unknownEscapeSequence(cast(string) esc);
}
}
break;
case 'p':
// it is asking a question... and tbh i don't care.
break;
case 'l':
//import std.stdio; writeln("l magic ", cast(string) esc);
if(esc[1] != '?')
foreach(arg; getArgs())
switch(arg) {
case 4:
insertMode = false;
break;
case 34:
// no idea. vim inside screen sends it
break;
case 1004:
sendFocusEvents = false;
break;
case 1005:
// turn off utf-8 mouse
utf8MouseMode = false;
break;
case 1006:
// turn off sgr mouse
break;
case 1015:
// turn off urxvt mouse
break;
default: unknownEscapeSequence(cast(string) esc);
}
else
foreach(arg; getArgsBase(2, null)) {
if(arg > 65535) {
/* Extensions */
if(arg < 65536 + 65535)
hyperlinkActive = false;
} else
switch(arg) {
case 1:
// normal cursor keys
applicationCursorKeys = false;
break;
case 3:
// 80 column mode
break;
case 4:
// smooth scroll
break;
case 5:
// normal video
reverseVideo = false;
break;
case 6:
// normal cursor mode
break;
case 7:
// wraparound mode
wraparoundMode = true;
break;
case 12:
// stop blinking cursor
break;
case 1034:
// meta keys????
break;
case 1049:
cursorPosition = popSavedCursor;
wraparoundMode = true;
returnToNormalScreen();
break;
case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
break;
case 9:
case 1000:
case 1002:
case 1003:
case 1014: // arsd extension
allMouseTrackingOff();
break;
case 1005:
case 1006:
// idk
break;
case 1048:
cursorPosition = popSavedCursor;
break;
case 2004:
bracketedPasteMode = false;
break;
case 3004:
bracketedHyperlinkMode = false;
break;
case 1047:
case 47:
returnToNormalScreen();
break;
case 25:
cursorShowing = false;
break;
default: unknownEscapeSequence(cast(string) esc);
}
}
break;
case 'X':
// erase characters
auto count = getArgs(1)[0];
TerminalCell plain;
plain.ch = ' ';
plain.attributes = currentAttributes;
foreach(cnt; 0 .. count) {
ASS[cursorY][cnt + cursorX] = plain;
}
break;
case 'S':
auto count = getArgs(1)[0];
// scroll up
scrollUp(count);
break;
case 'T':
auto count = getArgs(1)[0];
// scroll down
scrollDown(count);
break;
case 'P':
auto count = getArgs(1)[0];
// delete characters
foreach(cnt; 0 .. count) {
for(int i = cursorX; i < this.screenWidth-1; i++) {
if(ASS[cursorY][i].selected)
clearSelection();
ASS[cursorY][i] = ASS[cursorY][i + 1];
ASS[cursorY][i].invalidated = true;
}
if(ASS[cursorY][this.screenWidth - 1].selected)
clearSelection();
ASS[cursorY][this.screenWidth-1].ch = ' ';
ASS[cursorY][this.screenWidth-1].invalidated = true;
}
extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY);
break;
case '@':
// insert blank characters
auto count = getArgs(1)[0];
foreach(idx; 0 .. count) {
for(int i = this.screenWidth - 1; i > cursorX; i--) {
ASS[cursorY][i] = ASS[cursorY][i - 1];
ASS[cursorY][i].invalidated = true;
}
ASS[cursorY][cursorX].ch = ' ';
ASS[cursorY][cursorX].invalidated = true;
}
extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY);
break;
case 'c':
// send device attributes
// FIXME: what am i supposed to do here?
//sendToApplication("\033[>0;138;0c");
//sendToApplication("\033[?62;");
sendToApplication(terminalIdCode);
break;
default:
// [42\esc] seems to have gotten here once somehow
// also [24\esc]
unknownEscapeSequence("" ~ cast(string) esc);
}
} else {
unknownEscapeSequence(cast(string) esc);
}
}
}
}
// 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 + 0xF0000,
}
/* These match simpledisplay.d which match terminal.d, so you can just cast them */
enum MouseEventType : int {
motion = 0,
buttonPressed = 1,
buttonReleased = 2,
}
enum MouseButton : int {
// these names assume a right-handed mouse
left = 1,
right = 2,
middle = 4,
wheelUp = 8,
wheelDown = 16,
}
/*
mixin template ImageSupport() {
import arsd.png;
import arsd.bmp;
}
*/
/* helper functions that are generally useful but not necessarily required */
version(use_libssh2) {
import arsd.libssh2;
void startChild(alias masterFunc)(string host, short port, string username, string keyFile, string expectedFingerprint = null) {
int tries = 0;
try_again:
try {
import std.socket;
if(libssh2_init(0))
throw new Exception("libssh2_init");
scope(exit)
libssh2_exit();
auto socket = new Socket(AddressFamily.INET, SocketType.STREAM);
socket.connect(new InternetAddress(host, port));
scope(exit) socket.close();
auto session = libssh2_session_init_ex(null, null, null, null);
if(session is null) throw new Exception("init session");
scope(exit)
libssh2_session_disconnect_ex(session, 0, "normal", "EN");
libssh2_session_flag(session, LIBSSH2_FLAG_COMPRESS, 1);
if(libssh2_session_handshake(session, socket.handle))
throw new Exception("handshake");
auto fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
if(expectedFingerprint !is null && fingerprint[0 .. expectedFingerprint.length] != expectedFingerprint)
throw new Exception("fingerprint");
import std.string : toStringz;
if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, cast(int) username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
throw new Exception("auth");
auto channel = libssh2_channel_open_ex(session, "session".ptr, "session".length, LIBSSH2_CHANNEL_WINDOW_DEFAULT, LIBSSH2_CHANNEL_PACKET_DEFAULT, null, 0);
if(channel is null)
throw new Exception("channel open");
scope(exit)
libssh2_channel_free(channel);
// libssh2_channel_setenv_ex(channel, "ELVISBG".dup.ptr, "ELVISBG".length, "dark".ptr, "dark".length);
if(libssh2_channel_request_pty_ex(channel, "xterm", "xterm".length, null, 0, 80, 24, 0, 0))
throw new Exception("pty");
if(libssh2_channel_process_startup(channel, "shell".ptr, "shell".length, null, 0))
throw new Exception("process_startup");
libssh2_keepalive_config(session, 0, 60);
libssh2_session_set_blocking(session, 0);
masterFunc(socket, session, channel);
} catch(Exception e) {
if(e.msg == "handshake") {
tries++;
import core.thread;
Thread.sleep(200.msecs);
if(tries < 10)
goto try_again;
}
throw e;
}
}
} else
version(Posix) {
extern(C) static int forkpty(int* master, /*int* slave,*/ void* name, void* termp, void* winp);
pragma(lib, "util");
/// this is good
void startChild(alias masterFunc)(string program, string[] args) {
import core.sys.posix.termios;
import core.sys.posix.signal;
import core.sys.posix.sys.wait;
__gshared static int childrenAlive = 0;
extern(C) nothrow static @nogc
void childdead(int) {
childrenAlive--;
wait(null);
version(with_eventloop)
try {
import arsd.eventloop;
if(childrenAlive <= 0)
exit();
} catch(Exception e){}
}
signal(SIGCHLD, &childdead);
int master;
int pid = forkpty(&master, null, null, null);
if(pid == -1)
throw new Exception("forkpty");
if(pid == 0) {
import std.process;
environment["TERM"] = "xterm"; // we're closest to an xterm, so definitely want to pretend to be one to the child processes
environment["TERM_EXTENSIONS"] = "arsd"; // announce our extensions
import std.string;
if(environment["LANG"].indexOf("UTF-8") == -1)
environment["LANG"] = "en_US.UTF-8"; // tell them that utf8 rox (FIXME: what about non-US?)
import core.sys.posix.unistd;
import core.stdc.stdlib;
char** argv = cast(char**) malloc((char*).sizeof * (args.length + 1));
if(argv is null) throw new Exception("malloc");
foreach(i, arg; args) {
argv[i] = cast(char*) malloc(arg.length + 1);
if(argv[i] is null) throw new Exception("malloc");
argv[i][0 .. arg.length] = arg[];
argv[i][arg.length] = 0;
}
argv[args.length] = null;
termios info;
ubyte[128] hack; // jic that druntime definition is still wrong
tcgetattr(master, &info);
info.c_cc[VERASE] = '\b';
tcsetattr(master, TCSANOW, &info);
core.sys.posix.unistd.execv(argv[0], argv);
} else {
childrenAlive = 1;
masterFunc(master);
}
}
} else
version(Windows) {
import core.sys.windows.windows;
version(winpty) {
alias HPCON = HANDLE;
extern(Windows)
HRESULT function(HPCON, COORD) ResizePseudoConsole;
extern(Windows)
HRESULT function(COORD, HANDLE, HANDLE, DWORD, HPCON*) CreatePseudoConsole;
extern(Windows)
void function(HPCON) ClosePseudoConsole;
}
extern(Windows)
BOOL PeekNamedPipe(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD);
extern(Windows)
BOOL GetOverlappedResult(HANDLE,OVERLAPPED*,LPDWORD,BOOL);
extern(Windows)
private BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*);
extern(Windows)
BOOL PostMessageA(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
extern(Windows)
BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM);
extern(Windows)
BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, PVOID Context, ULONG dwMilliseconds, ULONG dwFlags);
extern(Windows)
BOOL SetHandleInformation(HANDLE, DWORD, DWORD);
extern(Windows)
HANDLE CreateNamedPipeA(
const(char)* lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstances,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTimeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
extern(Windows)
BOOL UnregisterWait(HANDLE);
struct STARTUPINFOEXA {
STARTUPINFOA StartupInfo;
void* lpAttributeList;
}
enum PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
enum EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
extern(Windows)
BOOL InitializeProcThreadAttributeList(void*, DWORD, DWORD, PSIZE_T);
extern(Windows)
BOOL UpdateProcThreadAttribute(void*, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T);
__gshared HANDLE waitHandle;
__gshared bool childDead;
extern(Windows)
void childCallback(void* tidp, bool) {
auto tid = cast(DWORD) tidp;
UnregisterWait(waitHandle);
PostThreadMessageA(tid, WM_QUIT, 0, 0);
childDead = true;
//stupidThreadAlive = false;
}
extern(Windows)
void SetLastError(DWORD);
/// this is good. best to call it with plink.exe so it can talk to unix
/// note that plink asks for the password out of band, so it won't actually work like that.
/// thus specify the password on the command line or better yet, use a private key file
/// e.g.
/// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\"");
void startChild(alias masterFunc)(string program, string commandLine) {
import core.sys.windows.windows;
import arsd.core : MyCreatePipeEx;
import std.conv;
SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
saAttr.bInheritHandle = true;
saAttr.lpSecurityDescriptor = null;
HANDLE inreadPipe;
HANDLE inwritePipe;
if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0)
throw new Exception("CreatePipe");
if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
throw new Exception("SetHandleInformation");
HANDLE outreadPipe;
HANDLE outwritePipe;
version(winpty)
auto flags = 0;
else
auto flags = FILE_FLAG_OVERLAPPED;
if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, flags, 0) == 0)
throw new Exception("CreatePipe");
if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
throw new Exception("SetHandleInformation");
version(winpty) {
auto lib = LoadLibrary("kernel32.dll");
if(lib is null) throw new Exception("holy wtf batman");
scope(exit) FreeLibrary(lib);
CreatePseudoConsole = cast(typeof(CreatePseudoConsole)) GetProcAddress(lib, "CreatePseudoConsole");
ClosePseudoConsole = cast(typeof(ClosePseudoConsole)) GetProcAddress(lib, "ClosePseudoConsole");
ResizePseudoConsole = cast(typeof(ResizePseudoConsole)) GetProcAddress(lib, "ResizePseudoConsole");
if(CreatePseudoConsole is null || ClosePseudoConsole is null || ResizePseudoConsole is null)
throw new Exception("Windows pseudo console not available on this version");
initPipeHack(outreadPipe);
HPCON hpc;
auto result = CreatePseudoConsole(
COORD(80, 24),
inreadPipe,
outwritePipe,
0, // flags
&hpc
);
assert(result == S_OK);
scope(exit)
ClosePseudoConsole(hpc);
}
STARTUPINFOEXA siex;
siex.StartupInfo.cb = siex.sizeof;
version(winpty) {
size_t size;
InitializeProcThreadAttributeList(null, 1, 0, &size);
ubyte[] wtf = new ubyte[](size);
siex.lpAttributeList = wtf.ptr;
InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size);
UpdateProcThreadAttribute(
siex.lpAttributeList,
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
hpc,
hpc.sizeof,
null,
null
);
} {//else {
siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
siex.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);//inreadPipe;
siex.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);//outwritePipe;
siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe;
}
PROCESS_INFORMATION pi;
import std.conv;
if(commandLine.length > 255)
throw new Exception("command line too long");
char[256] cmdLine;
cmdLine[0 .. commandLine.length] = commandLine[];
cmdLine[commandLine.length] = 0;
import std.string;
if(CreateProcessA(program is null ? null : toStringz(program), cmdLine.ptr, null, null, true, EXTENDED_STARTUPINFO_PRESENT /*0x08000000 /* CREATE_NO_WINDOW */, null /* environment */, null, cast(STARTUPINFOA*) &siex, &pi) == 0)
throw new Exception("CreateProcess " ~ to!string(GetLastError()));
if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0)
throw new Exception("RegisterWaitForSingleObject");
version(winpty)
masterFunc(hpc, inwritePipe, outreadPipe);
else
masterFunc(inwritePipe, outreadPipe);
//stupidThreadAlive = false;
//term.stupidThread.join();
/* // FIXME: we should close but only if we're legit done
// masterFunc typically runs an event loop but it might not.
CloseHandle(inwritePipe);
CloseHandle(outreadPipe);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
*/
}
}
/// Implementation of TerminalEmulator's abstract functions that forward them to output
mixin template ForwardVirtuals(alias writer) {
static import arsd.color;
protected override void changeCursorStyle(CursorStyle style) {
// FIXME: this should probably just import utility
final switch(style) {
case TerminalEmulator.CursorStyle.block:
writer("\033[2 q");
break;
case TerminalEmulator.CursorStyle.underline:
writer("\033[4 q");
break;
case TerminalEmulator.CursorStyle.bar:
writer("\033[6 q");
break;
}
}
protected override void changeWindowTitle(string t) {
import std.process;
if(t.length && environment["TERM"] != "linux")
writer("\033]0;"~t~"\007");
}
protected override void changeWindowIcon(arsd.color.IndexedImage t) {
if(t !is null) {
// forward it via our extension. xterm and such seems to ignore this so we should be ok just sending, except to Linux
import std.process;
if(environment["TERM"] != "linux")
writer("\033]5000;" ~ encodeSmallTextImage(t) ~ "\007");
}
}
protected override void changeIconTitle(string) {} // FIXME
protected override void changeTextAttributes(TextAttributes) {} // FIXME
protected override void soundBell() {
writer("\007");
}
protected override void demandAttention() {
import std.process;
if(environment["TERM"] != "linux")
writer("\033]5001;1\007"); // the 1 there means true but is currently ignored
}
protected override void copyToClipboard(string text) {
// this is xterm compatible, though xterm rarely implements it
import std.base64;
// idk why the cast is needed here
writer("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007");
}
protected override void pasteFromClipboard(void delegate(in char[]) dg) {
// this is a slight extension. xterm invented the string - it means request the primary selection -
// but it generally doesn't actually get a reply. so i'm using it to request the primary which will be
// sent as a pasted strong.
// (xterm prolly doesn't do it by default because it is potentially insecure, letting a naughty app steal your clipboard data, but meh, any X application can do that too and it is useful here for nesting.)
writer("\033]52;c;?\007");
}
protected override void copyToPrimary(string text) {
import std.base64;
writer("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007");
}
protected override void pasteFromPrimary(void delegate(in char[]) dg) {
writer("\033]52;p;?\007");
}
}
/// you can pass this as PtySupport's arguments when you just don't care
final void doNothing() {}
version(winpty) {
__gshared static HANDLE inputEvent;
__gshared static HANDLE magicEvent;
__gshared static ubyte[] helperBuffer;
__gshared static HANDLE helperThread;
static void initPipeHack(void* ptr) {
inputEvent = CreateEvent(null, false, false, null);
assert(inputEvent !is null);
magicEvent = CreateEvent(null, false, true, null);
assert(magicEvent !is null);
helperThread = CreateThread(
null,
0,
&actuallyRead,
ptr,
0,
null
);
assert(helperThread !is null);
}
extern(Windows) static
uint actuallyRead(void* ptr) {
ubyte[4096] buffer;
DWORD got;
while(true) {
// wait for the other thread to tell us they
// are done...
WaitForSingleObject(magicEvent, INFINITE);
auto ret = ReadFile(ptr, buffer.ptr, cast(DWORD) buffer.length, &got, null);
helperBuffer = buffer[0 .. got];
// tells the other thread it is allowed to read
// readyToReadPty
SetEvent(inputEvent);
}
assert(0);
}
}
/// You must implement a function called redraw() and initialize the members in your constructor
mixin template PtySupport(alias resizeHelper) {
// Initialize these!
final void redraw_() {
if(invalidateAll) {
extendInvalidatedRange(0, 0, this.screenWidth, this.screenHeight);
if(alternateScreenActive)
foreach(ref t; alternateScreen)
t.invalidated = true;
else
foreach(ref t; normalScreen)
t.invalidated = true;
invalidateAll = false;
}
redraw();
//soundBell();
}
version(use_libssh2) {
import arsd.libssh2;
LIBSSH2_CHANNEL* sshChannel;
} else version(Windows) {
import core.sys.windows.windows;
HANDLE stdin;
HANDLE stdout;
} else version(Posix) {
int master;
}
version(use_libssh2) { }
else version(Posix) {
int previousProcess = 0;
int activeProcess = 0;
int activeProcessWhenResized = 0;
bool resizedRecently;
/*
so, this isn't perfect, but it is meant to send the resize signal to an existing process
when it isn't in the front when you resize.
For example, open vim and resize. Then exit vim. We want bash to be updated.
But also don't want to do too many spurious signals.
It doesn't handle the case of bash -> vim -> :sh resize, then vim gets signal but
the outer bash won't see it. I guess I need some kind of process stack.
but it is okish.
*/
override void outputOccurred() {
import core.sys.posix.unistd;
auto pgrp = tcgetpgrp(master);
if(pgrp != -1) {
if(pgrp != activeProcess) {
auto previousProcessAtStartup = previousProcess;
previousProcess = activeProcess;
activeProcess = pgrp;
if(resizedRecently) {
if(activeProcess != activeProcessWhenResized) {
resizedRecently = false;
if(activeProcess == previousProcessAtStartup) {
//import std.stdio; writeln("informing new process ", activeProcess, " of size ", screenWidth, " x ", screenHeight);
import core.sys.posix.signal;
kill(-activeProcess, 28 /* 28 == SIGWINCH*/);
}
}
}
}
}
super.outputOccurred();
}
//return std.file.readText("/proc/" ~ to!string(pgrp) ~ "/cmdline");
}
override void resizeTerminal(int w, int h) {
version(Posix) {
activeProcessWhenResized = activeProcess;
resizedRecently = true;
}
resizeHelper();
super.resizeTerminal(w, h);
version(use_libssh2) {
libssh2_channel_request_pty_size_ex(sshChannel, w, h, 0, 0);
} else version(Posix) {
import core.sys.posix.sys.ioctl;
winsize win;
win.ws_col = cast(ushort) w;
win.ws_row = cast(ushort) h;
ioctl(master, TIOCSWINSZ, &win);
} else version(Windows) {
version(winpty) {
COORD coord;
coord.X = cast(ushort) w;
coord.Y = cast(ushort) h;
ResizePseudoConsole(hpc, coord);
} else {
sendToApplication([cast(ubyte) 254, cast(ubyte) w, cast(ubyte) h]);
}
} else static assert(0);
}
protected override void sendToApplication(scope const(void)[] data) {
version(use_libssh2) {
while(data.length) {
auto sent = libssh2_channel_write_ex(sshChannel, 0, data.ptr, data.length);
if(sent < 0)
throw new Exception("libssh2_channel_write_ex");
data = data[sent .. $];
}
} else version(Windows) {
import std.conv;
uint written;
if(WriteFile(stdin, data.ptr, cast(uint)data.length, &written, null) == 0)
throw new Exception("WriteFile " ~ to!string(GetLastError()));
} else version(Posix) {
import core.sys.posix.unistd;
int frozen;
while(data.length) {
enum MAX_SEND = 1024 * 20;
auto sent = write(master, data.ptr, data.length > MAX_SEND ? MAX_SEND : cast(int) data.length);
//import std.stdio; writeln("ROFL ", sent, " ", data.length);
import core.stdc.errno;
if(sent == -1 && errno == 11) {
import core.thread;
if(frozen == 50)
throw new Exception("write froze up");
frozen++;
Thread.sleep(10.msecs);
//import std.stdio; writeln("lol");
continue; // just try again
}
frozen = 0;
import std.conv;
if(sent < 0)
throw new Exception("write " ~ to!string(errno));
data = data[sent .. $];
}
} else static assert(0);
}
version(use_libssh2) {
int readyToRead(int fd) {
int count = 0; // if too much stuff comes at once, we still want to be responsive
while(true) {
ubyte[4096] buffer;
auto got = libssh2_channel_read_ex(sshChannel, 0, buffer.ptr, buffer.length);
if(got == LIBSSH2_ERROR_EAGAIN)
break; // got it all for now
if(got < 0)
throw new Exception("libssh2_channel_read_ex");
if(got == 0)
break; // NOT an error!
super.sendRawInput(buffer[0 .. got]);
count++;
if(count == 5) {
count = 0;
redraw_();
justRead();
}
}
if(libssh2_channel_eof(sshChannel)) {
libssh2_channel_close(sshChannel);
libssh2_channel_wait_closed(sshChannel);
return 1;
}
if(count != 0) {
redraw_();
justRead();
}
return 0;
}
} else version(winpty) {
void readyToReadPty() {
super.sendRawInput(helperBuffer);
SetEvent(magicEvent); // tell the other thread we have finished
redraw_();
justRead();
}
} else version(Windows) {
OVERLAPPED* overlapped;
bool overlappedBufferLocked;
ubyte[4096] overlappedBuffer;
extern(Windows)
static final void readyToReadWindows(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) {
assert(overlapped !is null);
typeof(this) w = cast(typeof(this)) overlapped.hEvent;
if(numberOfBytes) {
w.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]);
w.redraw_();
}
import std.conv;
if(ReadFileEx(w.stdout, w.overlappedBuffer.ptr, w.overlappedBuffer.length, overlapped, &readyToReadWindows) == 0) {
if(GetLastError() == 997)
{ } // there's pending i/o, let's just ignore for now and it should tell us later that it completed
else
throw new Exception("ReadFileEx " ~ to!string(GetLastError()));
} else {
}
w.justRead();
}
} else version(Posix) {
void readyToRead(int fd) {
import core.sys.posix.unistd;
ubyte[4096] buffer;
// the count is to limit how long we spend in this loop
// when it runs out, it goes back to the main event loop
// for a while (btw use level triggered events so the remaining
// data continues to get processed!) giving a chance to redraw
// and process user input periodically during insanely long and
// rapid output.
int cnt = 50; // the actual count is arbitrary, it just seems nice in my tests
version(arsd_te_conservative_draws)
cnt = 400;
// FIXME: if connected by ssh, up the count so we don't redraw as frequently.
// it'd save bandwidth
while(--cnt) {
auto len = read(fd, buffer.ptr, 4096);
if(len < 0) {
import core.stdc.errno;
if(errno == EAGAIN || errno == EWOULDBLOCK) {
break; // we got it all
} else {
//import std.conv;
//throw new Exception("read failed " ~ to!string(errno));
return;
}
}
if(len == 0) {
close(fd);
requestExit();
break;
}
auto data = buffer[0 .. len];
if(debugMode) {
import std.array; import std.stdio; writeln("GOT ", data, "\nOR ",
replace(cast(string) data, "\033", "\\")
.replace("\010", "^H")
.replace("\r", "^M")
.replace("\n", "^J")
);
}
super.sendRawInput(data);
}
outputOccurred();
redraw_();
// HACK: I don't even know why this works, but with this
// sleep in place, it gives X events from that socket a
// chance to be processed. It can add a few seconds to a huge
// output (like `find /usr`), but meh, that's worth it to me
// to have a chance to ctrl+c.
import core.thread;
Thread.sleep(dur!"msecs"(5));
justRead();
}
}
}
mixin template SdpyImageSupport() {
class NonCharacterData_Image : NonCharacterData {
Image data;
int imageOffsetX;
int imageOffsetY;
this(Image data, int x, int y) {
this.data = data;
this.imageOffsetX = x;
this.imageOffsetY = y;
}
}
version(TerminalDirectToEmulator)
class NonCharacterData_Widget : NonCharacterData {
this(void* data, size_t idx, int width, int height) {
this.window = cast(SimpleWindow) data;
this.idx = idx;
this.width = width;
this.height = height;
}
void position(int posx, int posy, int width, int height) {
if(posx == this.posx && posy == this.posy && width == this.pixelWidth && height == this.pixelHeight)
return;
this.posx = posx;
this.posy = posy;
this.pixelWidth = width;
this.pixelHeight = height;
window.moveResize(posx, posy, width, height);
import std.stdio; writeln(posx, " ", posy, " ", width, " ", height);
auto painter = this.window.draw;
painter.outlineColor = Color.red;
painter.fillColor = Color.green;
painter.drawRectangle(Point(0, 0), width, height);
}
SimpleWindow window;
size_t idx;
int width;
int height;
int posx;
int posy;
int pixelWidth;
int pixelHeight;
}
private struct CachedImage {
ulong hash;
BinaryDataTerminalRepresentation bui;
int timesSeen;
import core.time;
MonoTime lastUsed;
}
private CachedImage[] imageCache;
private CachedImage* findInCache(ulong hash) {
if(hash == 0)
return null;
/*
import std.stdio;
writeln("***");
foreach(cache; imageCache) {
writeln(cache.hash, " ", cache.timesSeen, " ", cache.lastUsed);
}
*/
foreach(ref i; imageCache)
if(i.hash == hash) {
import core.time;
i.lastUsed = MonoTime.currTime;
i.timesSeen++;
return &i;
}
return null;
}
private BinaryDataTerminalRepresentation addImageCache(ulong hash, BinaryDataTerminalRepresentation bui) {
import core.time;
if(imageCache.length == 0)
imageCache.length = 8;
auto now = MonoTime.currTime;
size_t oldestIndex;
MonoTime oldestTime = now;
size_t leastUsedIndex;
int leastUsedCount = int.max;
foreach(idx, ref cached; imageCache) {
if(cached.hash == 0) {
cached.hash = hash;
cached.bui = bui;
cached.timesSeen = 1;
cached.lastUsed = now;
return bui;
} else {
if(cached.timesSeen < leastUsedCount) {
leastUsedCount = cached.timesSeen;
leastUsedIndex = idx;
}
if(cached.lastUsed < oldestTime) {
oldestTime = cached.lastUsed;
oldestIndex = idx;
}
}
}
// need to overwrite one of the cached items, I'll just use the oldest one here
// but maybe that could be smarter later
imageCache[oldestIndex].hash = hash;
imageCache[oldestIndex].bui = bui;
imageCache[oldestIndex].timesSeen = 1;
imageCache[oldestIndex].lastUsed = now;
return bui;
}
// It has a cache of the 8 most recently used items right now so if there's a loop of 9 you get pwned
// but still the cache does an ok job at helping things while balancing out the big memory consumption it
// could do if just left to grow and grow. i hope.
protected override BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[] binaryData) {
version(none) {
//version(TerminalDirectToEmulator)
//if(binaryData.length == size_t.sizeof + 10) {
//if((cast(uint[]) binaryData[0 .. 4])[0] == 0xdeadbeef && (cast(uint[]) binaryData[$-4 .. $])[0] == 0xabcdef32) {
//auto widthInCharacterCells = binaryData[4];
//auto heightInCharacterCells = binaryData[5];
//auto pointer = (cast(void*[]) binaryData[6 .. $-4])[0];
auto widthInCharacterCells = 30;
auto heightInCharacterCells = 20;
SimpleWindow pwin;
foreach(k, v; SimpleWindow.nativeMapping) {
if(v.type == WindowTypes.normal)
pwin = v;
}
auto pointer = cast(void*) (new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin));
BinaryDataTerminalRepresentation bi;
bi.width = widthInCharacterCells;
bi.height = heightInCharacterCells;
bi.representation.length = bi.width * bi.height;
foreach(idx, ref cell; bi.representation) {
cell.nonCharacterData = new NonCharacterData_Widget(pointer, idx, widthInCharacterCells, heightInCharacterCells);
}
return bi;
//}
}
import std.digest.md;
ulong hash = * (cast(ulong*) md5Of(binaryData).ptr);
if(auto cached = findInCache(hash))
return cached.bui;
TrueColorImage mi;
if(binaryData.length > 8 && binaryData[1] == 'P' && binaryData[2] == 'N' && binaryData[3] == 'G') {
import arsd.png;
mi = imageFromPng(readPng(binaryData)).getAsTrueColorImage();
} else if(binaryData.length > 8 && binaryData[0] == 'B' && binaryData[1] == 'M') {
import arsd.bmp;
mi = readBmp(binaryData).getAsTrueColorImage();
} else if(binaryData.length > 2 && binaryData[0] == 0xff && binaryData[1] == 0xd8) {
import arsd.jpeg;
mi = readJpegFromMemory(binaryData).getAsTrueColorImage();
} else if(binaryData.length > 2 && binaryData[0] == '<') {
import arsd.svg;
NSVG* image = nsvgParse(cast(const(char)[]) binaryData);
if(image is null)
return BinaryDataTerminalRepresentation();
int w = cast(int) image.width + 1;
int h = cast(int) image.height + 1;
NSVGrasterizer rast = nsvgCreateRasterizer();
mi = new TrueColorImage(w, h);
rasterize(rast, image, 0, 0, 1, mi.imageData.bytes.ptr, w, h, w*4);
image.kill();
} else {
return BinaryDataTerminalRepresentation();
}
BinaryDataTerminalRepresentation bi;
bi.width = mi.width / fontWidth + ((mi.width%fontWidth) ? 1 : 0);
bi.height = mi.height / fontHeight + ((mi.height%fontHeight) ? 1 : 0);
bi.representation.length = bi.width * bi.height;
Image data = Image.fromMemoryImage(mi);
int ix, iy;
foreach(ref cell; bi.representation) {
/*
Image data = new Image(fontWidth, fontHeight);
foreach(y; 0 .. fontHeight) {
foreach(x; 0 .. fontWidth) {
if(x + ix >= mi.width || y + iy >= mi.height) {
data.putPixel(x, y, defaultTextAttributes.background);
continue;
}
data.putPixel(x, y, mi.imageData.colors[(iy + y) * mi.width + (ix + x)]);
}
}
*/
cell.nonCharacterData = new NonCharacterData_Image(data, ix, iy);
ix += fontWidth;
if(ix >= mi.width) {
ix = 0;
iy += fontHeight;
}
}
return addImageCache(hash, bi);
//return bi;
}
}
// this assumes you have imported arsd.simpledisplay and/or arsd.minigui in the mixin scope
mixin template SdpyDraw() {
// black bg, make the colors more visible
static Color contrastify(Color c) {
if(c == Color(0xcd, 0, 0))
return Color.fromHsl(0, 1.0, 0.75);
else if(c == Color(0, 0, 0xcd))
return Color.fromHsl(240, 1.0, 0.75);
else if(c == Color(229, 229, 229))
return Color(0x99, 0x99, 0x99);
else if(c == Color.black)
return Color(128, 128, 128);
else return c;
}
// white bg, make them more visible
static Color antiContrastify(Color c) {
if(c == Color(0xcd, 0xcd, 0))
return Color.fromHsl(60, 1.0, 0.25);
else if(c == Color(0, 0xcd, 0xcd))
return Color.fromHsl(180, 1.0, 0.25);
else if(c == Color(229, 229, 229))
return Color(0x99, 0x99, 0x99);
else if(c == Color.white)
return Color(128, 128, 128);
else return c;
}
struct SRectangle {
int left;
int top;
int right;
int bottom;
}
mixin SdpyImageSupport;
OperatingSystemFont font;
int fontWidth;
int fontHeight;
enum paddingLeft = 2;
enum paddingTop = 1;
void loadDefaultFont(int size = 14) {
static if(UsingSimpledisplayX11) {
font = new OperatingSystemFont("core:fixed", size, FontWeight.medium);
//font = new OperatingSystemFont("monospace", size, FontWeight.medium);
if(font.isNull) {
// didn't work, it is using a
// fallback, prolly fixed-13 is best
font = new OperatingSystemFont("core:fixed", 13, FontWeight.medium);
}
} else version(Windows) {
this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium);
if(!this.font.isNull && !this.font.isMonospace)
this.font.unload(); // non-monospace fonts are unusable here. This should never happen anyway though as Courier New comes with Windows
}
if(font.isNull) {
// no way to really tell... just guess so it doesn't crash but like eeek.
fontWidth = size / 2;
fontHeight = size;
} else {
fontWidth = font.averageWidth;
fontHeight = font.height;
}
}
bool lastDrawAlternativeScreen;
final SRectangle redrawPainter(T)(T painter, bool forceRedraw) {
SRectangle invalidated;
// FIXME: anything we can do to make this faster is good
// on both, the XImagePainter could use optimizations
// on both, drawing blocks would probably be good too - not just one cell at a time, find whole blocks of stuff
// on both it might also be good to keep scroll commands high level somehow. idk.
// FIXME on Windows it would definitely help a lot to do just one ExtTextOutW per line, if possible. the current code is brutally slow
// Or also see https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-polytextoutw
static if(is(T == WidgetPainter) || is(T == ScreenPainter)) {
if(font)
painter.setFont(font);
}
int posx = paddingLeft;
int posy = paddingTop;
char[512] bufferText;
bool hasBufferedInfo;
int bufferTextLength;
Color bufferForeground;
Color bufferBackground;
int bufferX = -1;
int bufferY = -1;
bool bufferReverse;
void flushBuffer() {
if(!hasBufferedInfo) {
return;
}
assert(posx - bufferX - 1 > 0);
painter.fillColor = bufferReverse ? bufferForeground : bufferBackground;
painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground;
painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight);
painter.fillColor = Color.transparent;
// Hack for contrast!
if(bufferBackground == Color.black && !bufferReverse) {
// brighter than normal in some cases so i can read it easily
painter.outlineColor = contrastify(bufferForeground);
} else if(bufferBackground == Color.white && !bufferReverse) {
// darker than normal so i can read it
painter.outlineColor = antiContrastify(bufferForeground);
} else if(bufferForeground == bufferBackground) {
// color on itself, I want it visible too
auto hsl = toHsl(bufferForeground, true);
if(hsl[0] == 240) {
// blue is a bit special, it generally looks darker
// so we want to get very bright or very dark
if(hsl[2] < 0.7)
hsl[2] = 0.9;
else
hsl[2] = 0.1;
} else {
if(hsl[2] < 0.5)
hsl[2] += 0.5;
else
hsl[2] -= 0.5;
}
painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]);
} else {
auto drawColor = bufferReverse ? bufferBackground : bufferForeground;
///+
// try to ensure legible contrast with any arbitrary combination
auto bgColor = bufferReverse ? bufferForeground : bufferBackground;
auto fghsl = toHsl(drawColor, true);
auto bghsl = toHsl(bgColor, true);
if(fghsl[2] > 0.5 && bghsl[2] > 0.5) {
// bright color on bright background
painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2);
} else if(fghsl[2] < 0.5 && bghsl[2] < 0.5) {
// dark color on dark background
if(fghsl[0] == 240 && bghsl[0] >= 60 && bghsl[0] <= 180)
// blue on green looks dark to the algorithm but isn't really
painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2);
else
painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.8);
} else {
// normal
painter.outlineColor = drawColor;
}
//+/
// normal
//painter.outlineColor = drawColor;
}
// FIXME: make sure this clips correctly
painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]);
// import std.stdio; writeln(bufferX, " ", bufferY);
hasBufferedInfo = false;
bufferReverse = false;
bufferTextLength = 0;
bufferX = -1;
bufferY = -1;
}
int x;
auto bfr = alternateScreenActive ? alternateScreen : normalScreen;
version(invalidator_2) {
if(invalidatedMax > bfr.length)
invalidatedMax = cast(int) bfr.length;
if(invalidatedMin > invalidatedMax)
invalidatedMin = invalidatedMax;
if(invalidatedMin >= 0)
bfr = bfr[invalidatedMin .. invalidatedMax];
posx += (invalidatedMin % screenWidth) * fontWidth;
posy += (invalidatedMin / screenWidth) * fontHeight;
//import std.stdio; writeln(invalidatedMin, " to ", invalidatedMax, " ", posx, "x", posy);
invalidated.left = posx;
invalidated.top = posy;
invalidated.right = posx;
invalidated.top = posy;
clearInvalidatedRange();
}
foreach(idx, ref cell; bfr) {
if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) {
flushBuffer();
goto skipDrawing;
}
cell.invalidated = false;
version(none) if(bufferX == -1) { // why was this ever here?
bufferX = posx;
bufferY = posy;
}
if(!cell.hasNonCharacterData) {
invalidated.left = posx < invalidated.left ? posx : invalidated.left;
invalidated.top = posy < invalidated.top ? posy : invalidated.top;
int xmax = posx + fontWidth;
int ymax = posy + fontHeight;
invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
// FIXME: this could be more efficient, simpledisplay could get better graphics context handling
{
bool reverse = (cell.attributes.inverse != reverseVideo);
if(cell.selected)
reverse = !reverse;
version(with_24_bit_color) {
auto fgc = cell.attributes.foreground;
auto bgc = cell.attributes.background;
if(!(cell.attributes.foregroundIndex & 0xff00)) {
// this refers to a specific palette entry, which may change, so we should use that
fgc = palette[cell.attributes.foregroundIndex];
}
if(!(cell.attributes.backgroundIndex & 0xff00)) {
// this refers to a specific palette entry, which may change, so we should use that
bgc = palette[cell.attributes.backgroundIndex];
}
} else {
auto fgc = cell.attributes.foregroundIndex == 256 ? defaultForeground : palette[cell.attributes.foregroundIndex & 0xff];
auto bgc = cell.attributes.backgroundIndex == 256 ? defaultBackground : palette[cell.attributes.backgroundIndex & 0xff];
}
if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse)
flushBuffer();
bufferReverse = reverse;
bufferBackground = bgc;
bufferForeground = fgc;
}
}
if(!cell.hasNonCharacterData) {
char[4] str;
import std.utf;
// now that it is buffered, we do want to draw it this way...
//if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing
try {
auto stride = encode(str, cell.ch);
if(bufferTextLength + stride > bufferText.length)
flushBuffer();
bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride];
bufferTextLength += stride;
if(bufferX == -1) {
bufferX = posx;
bufferY = posy;
}
hasBufferedInfo = true;
} catch(Exception e) {
// import std.stdio; writeln(cast(uint) cell.ch, " :: ", e.msg);
}
//}
} else if(cell.nonCharacterData !is null) {
//import std.stdio; writeln(cast(void*) cell.nonCharacterData);
if(auto ncdi = cast(NonCharacterData_Image) cell.nonCharacterData) {
flushBuffer();
painter.outlineColor = defaultBackground;
painter.fillColor = defaultBackground;
painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
painter.drawImage(Point(posx, posy), ncdi.data, Point(ncdi.imageOffsetX, ncdi.imageOffsetY), fontWidth, fontHeight);
}
version(TerminalDirectToEmulator)
if(auto wdi = cast(NonCharacterData_Widget) cell.nonCharacterData) {
flushBuffer();
if(wdi.idx == 0) {
wdi.position(posx, posy, fontWidth * wdi.width, fontHeight * wdi.height);
/*
painter.outlineColor = defaultBackground;
painter.fillColor = defaultBackground;
painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
*/
}
}
}
if(!cell.hasNonCharacterData)
if(cell.attributes.underlined) {
// the posx adjustment is because the buffer assumes it is going
// to be flushed after advancing, but here, we're doing it mid-character
// FIXME: we should just underline the whole thing consecutively, with the buffer
posx += fontWidth;
flushBuffer();
posx -= fontWidth;
painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1));
}
skipDrawing:
posx += fontWidth;
x++;
if(x == screenWidth) {
flushBuffer();
x = 0;
posy += fontHeight;
posx = paddingLeft;
}
}
flushBuffer();
if(cursorShowing) {
painter.fillColor = cursorColor;
painter.outlineColor = cursorColor;
painter.rasterOp = RasterOp.xor;
posx = cursorPosition.x * fontWidth + paddingLeft;
posy = cursorPosition.y * fontHeight + paddingTop;
int cursorWidth = fontWidth;
int cursorHeight = fontHeight;
final switch(cursorStyle) {
case CursorStyle.block:
painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight);
break;
case CursorStyle.underline:
painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2);
break;
case CursorStyle.bar:
painter.drawRectangle(Point(posx, posy), 2, cursorHeight);
break;
}
painter.rasterOp = RasterOp.normal;
painter.notifyCursorPosition(posx, posy, cursorWidth, cursorHeight);
// since the cursor draws over the cell, we need to make sure it is redrawn each time too
auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen);
if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) {
(*buffer)[cursorY * screenWidth + cursorX].invalidated = true;
}
extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY);
invalidated.left = posx < invalidated.left ? posx : invalidated.left;
invalidated.top = posy < invalidated.top ? posy : invalidated.top;
int xmax = posx + fontWidth;
int ymax = xmax + fontHeight;
invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
}
lastDrawAlternativeScreen = alternateScreenActive;
return invalidated;
}
}
string encodeSmallTextImage(IndexedImage ii) {
char encodeNumeric(int c) {
if(c < 10)
return cast(char)(c + '0');
if(c < 10 + 26)
return cast(char)(c - 10 + 'a');
assert(0);
}
string s;
s ~= encodeNumeric(ii.width);
s ~= encodeNumeric(ii.height);
foreach(entry; ii.palette)
s ~= entry.toRgbaHexString();
s ~= "Z";
ubyte rleByte;
int rleCount;
void rleCommit() {
if(rleByte >= 26)
assert(0); // too many colors for us to handle
if(rleCount == 0)
goto finish;
if(rleCount == 1) {
s ~= rleByte + 'a';
goto finish;
}
import std.conv;
s ~= to!string(rleCount);
s ~= rleByte + 'a';
finish:
rleByte = 0;
rleCount = 0;
}
foreach(b; ii.data) {
if(b == rleByte)
rleCount++;
else {
rleCommit();
rleByte = b;
rleCount = 1;
}
}
rleCommit();
return s;
}
IndexedImage readSmallTextImage(scope const(char)[] arg) {
auto origArg = arg;
int width;
int height;
int readNumeric(char c) {
if(c >= '0' && c <= '9')
return c - '0';
if(c >= 'a' && c <= 'z')
return c - 'a' + 10;
return 0;
}
if(arg.length > 2) {
width = readNumeric(arg[0]);
height = readNumeric(arg[1]);
arg = arg[2 .. $];
}
import std.conv;
assert(width == 16, to!string(width));
assert(height == 16, to!string(width));
Color[] palette;
ubyte[256] data;
int didx = 0;
bool readingPalette = true;
outer: while(arg.length) {
if(readingPalette) {
if(arg[0] == 'Z') {
readingPalette = false;
arg = arg[1 .. $];
continue;
}
if(arg.length < 8)
break;
foreach(a; arg[0..8]) {
// if not strict hex, forget it
if(!((a >= '0' && a <= '9') || (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z')))
break outer;
}
palette ~= Color.fromString(arg[0 .. 8]);
arg = arg[8 .. $];
} else {
char[3] rleChars;
int rlePos;
while(arg.length && arg[0] >= '0' && arg[0] <= '9') {
rleChars[rlePos] = arg[0];
arg = arg[1 .. $];
rlePos++;
if(rlePos >= rleChars.length)
break;
}
if(arg.length == 0)
break;
int rle;
if(rlePos == 0)
rle = 1;
else {
// 100
// rleChars[0] == '1'
foreach(c; rleChars[0 .. rlePos]) {
rle *= 10;
rle += c - '0';
}
}
foreach(i; 0 .. rle) {
if(arg[0] >= 'a' && arg[0] <= 'z')
data[didx] = cast(ubyte)(arg[0] - 'a');
didx++;
if(didx == data.length)
break outer;
}
arg = arg[1 .. $];
}
}
// width, height, palette, data is set up now
if(palette.length) {
auto ii = new IndexedImage(width, height);
ii.palette = palette;
ii.data = data.dup;
return ii;
}// else assert(0, origArg);
return null;
}
// workaround dmd bug fixed in next release
//static immutable Color[256] xtermPalette = [
immutable(Color)[] xtermPalette() {
// This is an approximation too for a few entries, but a very close one.
Color xtermPaletteIndexToColor(int paletteIdx) {
Color color;
color.a = 255;
if(paletteIdx < 16) {
if(paletteIdx == 7)
return Color(229, 229, 229); // real is 0xc0 but i think this is easier to see
else if(paletteIdx == 8)
return Color(0x80, 0x80, 0x80);
// real xterm uses 0x88 here, but I prefer 0xcd because it is easier for me to see
color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
} else if(paletteIdx < 232) {
// color ramp, 6x6x6 cube
color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55);
color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55);
color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55);
if(color.r == 55) color.r = 0;
if(color.g == 55) color.g = 0;
if(color.b == 55) color.b = 0;
} else {
// greyscale ramp, from 0x8 to 0xee
color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10);
color.g = color.r;
color.b = color.g;
}
return color;
}
static immutable(Color)[] ret;
if(ret.length == 256)
return ret;
ret.reserve(256);
foreach(i; 0 .. 256)
ret ~= xtermPaletteIndexToColor(i);
return ret;
}
static shared immutable dchar[dchar] lineDrawingCharacterSet;
shared static this() {
lineDrawingCharacterSet = [
'a' : ':',
'j' : '+',
'k' : '+',
'l' : '+',
'm' : '+',
'n' : '+',
'q' : '-',
't' : '+',
'u' : '+',
'v' : '+',
'w' : '+',
'x' : '|',
];
// this is what they SHOULD be but the font i use doesn't support all these
// the ascii fallback above looks pretty good anyway though.
version(none)
lineDrawingCharacterSet = [
'a' : '\u2592',
'j' : '\u2518',
'k' : '\u2510',
'l' : '\u250c',
'm' : '\u2514',
'n' : '\u253c',
'q' : '\u2500',
't' : '\u251c',
'u' : '\u2524',
'v' : '\u2534',
'w' : '\u252c',
'x' : '\u2502',
];
}
/+
Copyright: Adam D. Ruppe, 2013 - 2020
License: [http://www.boost.org/LICENSE_1_0.txt|Boost Software License 1.0]
Authors: Adam D. Ruppe
+/