dlangide/src/dlangide/ui/terminal.d

857 lines
27 KiB
D

module dlangide.ui.terminal;
import dlangui.widgets.widget;
import dlangui.widgets.controls;
struct TerminalAttr {
ubyte bgColor = 7;
ubyte textColor = 0;
ubyte flags = 0;
}
struct TerminalChar {
TerminalAttr attr;
dchar ch = ' ';
}
__gshared static uint[16] TERMINAL_PALETTE = [
0x000000, // black
0xFF0000,
0x00FF00,
0xFFFF00,
0x0000FF,
0xFF00FF,
0x00FFFF,
0xFFFFFF, // white
0x808080,
0x800000,
0x008000,
0x808000,
0x000080,
0x800080,
0x008080,
0xC0C0C0,
];
uint attrToColor(ubyte v) {
if (v >= 16)
return 0;
return TERMINAL_PALETTE[v];
}
struct TerminalLine {
TerminalChar[] line;
bool overflowFlag;
bool eolFlag;
void clear() {
line.length = 0;
overflowFlag = false;
eolFlag = false;
}
void markLineOverflow() {}
void markLineEol() {}
void putCharAt(dchar ch, int x, TerminalAttr currentAttr) {
if (x >= line.length) {
TerminalChar d;
d.attr = currentAttr;
d.ch = ' ';
while (x >= line.length) {
line.assumeSafeAppend;
line ~= d;
}
}
line[x].attr = currentAttr;
line[x].ch = ch;
}
}
struct TerminalContent {
TerminalLine[] lines;
Rect rc;
FontRef font;
TerminalAttr currentAttr;
TerminalAttr defAttr;
int maxBufferLines = 3000;
int topLine;
int width; // width in chars
int height; // height in chars
int charw; // single char width
int charh; // single char height
int cursorx;
int cursory;
bool focused;
bool _lineWrap = true;
@property void lineWrap(bool v) {
_lineWrap = v;
}
void resetTerminal() {
for (int i = topLine; i < cast(int)lines.length; i++) {
lines[i] = TerminalLine.init;
}
cursorx = 0;
cursory = topLine;
}
@property int screenTopLine() {
int y = cast(int)lines.length - height;
if (y < 0)
y = 0;
return y;
}
void eraseScreen(int direction, bool forLine) {
if (forLine) {
for (int x = 0; x < width; x++) {
if ((direction == 1 && x <= cursorx) || (direction < 1 && x >= cursorx) || (direction == 2))
putCharAt(' ', x, cursory);
}
} else {
int screenTop = screenTopLine;
for (int y = 0; y < height; y++) {
int yy = screenTop + y;
if ((direction == 1 && yy <= cursory) || (direction < 1 && yy >= cursory) || (direction == 2)) {
for (int x = 0; x < width; x++) {
putCharAt(' ', x, yy);
}
}
}
if (direction == 2) {
cursorx = 0;
cursory = screenTop;
}
}
}
void moveCursorBy(int x, int y) {
if (x) {
cursorx += x;
if (cursorx < 0)
cursorx = 0;
if (cursorx > width)
cursorx = width;
} else if (y) {
int screenTop = screenTopLine;
cursory += y;
if (cursory < screenTop)
cursory = screenTop;
else if (cursory >= screenTop + height)
cursory = screenTop + height - 1;
}
}
void setAttributes(int[] attrs) {
foreach (attr; attrs) {
if (attr < 0)
continue;
if (attr >= 30 && attr <= 37) {
currentAttr.textColor = cast(ubyte)(attr - 30);
} else if (attr >= 40 && attr <= 47) {
currentAttr.bgColor = cast(ubyte)(attr - 40);
} else if (attr >= 0 && attr <= 10) {
switch(attr) {
case 0:
currentAttr = defAttr;
break;
case 1:
case 2:
case 4:
case 5:
case 7:
case 8:
default:
break;
}
}
}
}
void moveCursorTo(int x, int y) {
int screenTop = screenTopLine;
if (x < 0 || y < 0) {
cursorx = 0;
cursory = screenTop;
return;
}
if (x >= 1 && x <= width + 1 && y >= 1 && x <= height) {
cursorx = x - 1;
cursory = screenTop + y - 1;
}
}
void layout(FontRef font, Rect rc) {
this.rc = rc;
this.font = font;
this.charw = font.charWidth('0');
this.charh = font.height;
int w = rc.width / charw;
int h = rc.height / charh;
setViewSize(w, h);
}
void setViewSize(int w, int h) {
if (h < 2)
h = 2;
if (w < 16)
w = 16;
width = w;
height = h;
}
void draw(DrawBuf buf) {
Rect lineRect = rc;
dchar[] text;
text.length = 1;
text[0] = ' ';
int screenTopLine = cast(int)lines.length - height;
if (screenTopLine < 0)
screenTopLine = 0;
for (uint i = 0; i < height && i + topLine < lines.length; i++) {
lineRect.bottom = lineRect.top + charh;
TerminalLine * p = &lines[i + topLine];
// draw line in rect
for (int x = 0; x < width; x++) {
bool isCursorPos = x == cursorx && i + topLine == cursory;
TerminalChar ch = x < p.line.length ? p.line[x] : TerminalChar.init;
uint bgcolor = attrToColor(ch.attr.bgColor);
uint textcolor = attrToColor(ch.attr.textColor);
if (isCursorPos && focused) {
// invert
uint tmp = bgcolor;
bgcolor = textcolor;
textcolor = tmp;
}
Rect charrc = lineRect;
charrc.left = lineRect.left + x * charw;
charrc.right = charrc.left + charw;
charrc.bottom = charrc.top + charh;
buf.fillRect(charrc, bgcolor);
if (isCursorPos) {
buf.drawFrame(charrc, focused ? (textcolor | 0xC0000000) : (textcolor | 0x80000000), Rect(1,1,1,1));
}
if (ch.ch >= ' ') {
text[0] = ch.ch;
font.drawText(buf, charrc.left, charrc.top, text, textcolor);
}
}
lineRect.top = lineRect.bottom;
}
}
void clearExtraLines(ref int yy) {
int y = cast(int)lines.length;
if (y >= maxBufferLines) {
int delta = y - maxBufferLines;
for (uint i = 0; i + delta < maxBufferLines && i + delta < lines.length; i++) {
lines[i] = lines[i + delta];
}
lines.length = lines.length - delta;
yy -= delta;
topLine -= delta;
if (topLine < 0)
topLine = 0;
}
}
TerminalLine * getLine(ref int yy) {
if (yy < 0)
yy = 0;
while(yy >= cast(int)lines.length) {
lines ~= TerminalLine.init;
}
clearExtraLines(yy);
return &lines[yy];
}
void putCharAt(dchar ch, ref int x, ref int y) {
if (x < 0)
x = 0;
TerminalLine * line = getLine(y);
if (x >= width) {
line.markLineOverflow();
y++;
line = getLine(y);
x = 0;
}
line.putCharAt(ch, x, currentAttr);
ensureCursorIsVisible();
}
int tabSize = 8;
// supports printed characters and \r \n \t
void putChar(dchar ch) {
if (ch == '\a') {
// bell
return;
}
if (ch == '\b') {
// backspace
if (cursorx > 0) {
cursorx--;
putCharAt(' ', cursorx, cursory);
ensureCursorIsVisible();
}
return;
}
if (ch == '\r') {
cursorx = 0;
ensureCursorIsVisible();
return;
}
if (ch == '\n' || ch == '\f' || ch == '\v') {
TerminalLine * line = getLine(cursory);
line.markLineEol();
cursory++;
line = getLine(cursory);
cursorx = 0;
ensureCursorIsVisible();
return;
}
if (ch == '\t') {
int newx = (cursorx + tabSize) / tabSize * tabSize;
if (newx > width) {
TerminalLine * line = getLine(cursory);
line.markLineEol();
cursory++;
line = getLine(cursory);
cursorx = 0;
} else {
for (int x = cursorx; x < newx; x++) {
putCharAt(' ', cursorx, cursory);
cursorx++;
}
}
ensureCursorIsVisible();
return;
}
putCharAt(ch, cursorx, cursory);
cursorx++;
ensureCursorIsVisible();
}
void ensureCursorIsVisible() {
topLine = cast(int)lines.length - height;
if (topLine < 0)
topLine = 0;
if (cursory < topLine)
cursory = topLine;
}
void updateScrollBar(ScrollBar sb) {
sb.pageSize = height;
sb.maxValue = cast(int)lines.length;
sb.position = topLine;
}
void scrollTo(int y) {
if (y + height > lines.length)
y = cast(int)lines.length - height;
if (y < 0)
y = 0;
topLine = y;
}
}
class TerminalWidget : WidgetGroup, OnScrollHandler {
protected ScrollBar _verticalScrollBar;
protected TerminalContent _content;
protected TerminalDevice _device;
this() {
this(null);
}
this(string ID) {
super(ID);
styleId = "TERMINAL";
focusable = true;
_verticalScrollBar = new ScrollBar("VERTICAL_SCROLLBAR", Orientation.Vertical);
_verticalScrollBar.minValue = 0;
_verticalScrollBar.scrollEvent = this;
addChild(_verticalScrollBar);
_device = new TerminalDevice();
TerminalWidget _this = this;
if (_device.create()) {
_device.onBytesRead = delegate (char[] data) {
import dlangui.platforms.common.platform;
Window w = window;
if (w) {
//w.exe
w.executeInUiThread(delegate() {
if (w.isChild(_this))
write(cast(string)data);
});
}
};
}
}
~this() {
if (_device)
destroy(_device);
}
/// returns terminal/tty device (or named pipe for windows) name
@property string deviceName() { return _device ? _device.deviceName : null; }
void scrollTo(int y) {
_content.scrollTo(y);
}
/// handle scroll event
bool onScrollEvent(AbstractSlider source, ScrollEvent event) {
switch(event.action) {
/// space above indicator pressed
case ScrollAction.PageUp:
scrollTo(_content.topLine - (_content.height ? _content.height - 1 : 1));
break;
/// space below indicator pressed
case ScrollAction.PageDown:
scrollTo(_content.topLine + (_content.height ? _content.height - 1 : 1));
break;
/// up/left button pressed
case ScrollAction.LineUp:
scrollTo(_content.topLine - 1);
break;
/// down/right button pressed
case ScrollAction.LineDown:
scrollTo(_content.topLine + 1);
break;
/// slider pressed
case ScrollAction.SliderPressed:
break;
/// dragging in progress
case ScrollAction.SliderMoved:
scrollTo(event.position);
break;
/// dragging finished
case ScrollAction.SliderReleased:
break;
default:
break;
}
return true;
}
/**
Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
*/
override void measure(int parentWidth, int parentHeight) {
int w = (parentWidth == SIZE_UNSPECIFIED) ? font.charWidth('0') * 80 : parentWidth;
int h = (parentHeight == SIZE_UNSPECIFIED) ? font.height * 10 : parentHeight;
Rect rc = Rect(0, 0, w, h);
applyMargins(rc);
applyPadding(rc);
_verticalScrollBar.measure(rc.width, rc.height);
rc.right -= _verticalScrollBar.measuredWidth;
measuredContent(parentWidth, parentHeight, rc.width, rc.height);
}
/// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
override void layout(Rect rc) {
if (visibility == Visibility.Gone) {
return;
}
_pos = rc;
_needLayout = false;
applyMargins(rc);
applyPadding(rc);
Rect sbrc = rc;
sbrc.left = sbrc.right - _verticalScrollBar.measuredWidth;
_verticalScrollBar.layout(sbrc);
rc.right = sbrc.left;
_content.layout(font, rc);
if (outputChars.length) {
// push buffered text
write(""d);
_needLayout = false;
}
}
/// Draw widget at its position to buffer
override void onDraw(DrawBuf buf) {
if (visibility != Visibility.Visible)
return;
Rect rc = _pos;
applyMargins(rc);
auto saver = ClipRectSaver(buf, rc, alpha);
DrawableRef bg = backgroundDrawable;
if (!bg.isNull) {
bg.drawTo(buf, rc, state);
}
applyPadding(rc);
_verticalScrollBar.onDraw(buf);
_content.draw(buf);
}
private char[] outputBuffer;
// write utf 8
void write(string bytes) {
if (!bytes.length)
return;
import std.utf;
outputBuffer.assumeSafeAppend;
outputBuffer ~= bytes;
size_t index = 0;
dchar[] decoded;
decoded.assumeSafeAppend;
dchar ch = 0;
while (index < outputBuffer.length) {
size_t oldindex = index;
try {
ch = decode(outputBuffer, index);
decoded ~= ch;
} catch (UTFException e) {
if (index + 4 <= outputBuffer.length) {
// just append invalid character
ch = '?';
index++;
}
}
if (oldindex == index)
break;
}
if (index > 0) {
// move content
for (size_t i = 0; i + index < outputBuffer.length; i++)
outputBuffer[i] = outputBuffer[i + index];
outputBuffer.length = outputBuffer.length - index;
}
if (decoded.length)
write(cast(dstring)decoded);
}
static bool parseParam(dchar[] buf, ref int index, ref int value) {
if (index >= buf.length)
return false;
if (buf[index] < '0' || buf[index] > '9')
return false;
value = 0;
while (index < buf.length && buf[index] >= '0' && buf[index] <= '9') {
value = value * 10 + (buf[index] - '0');
index++;
}
return true;
}
void handleInput(dstring chars) {
import std.utf;
_device.write(chars.toUTF8);
}
private dchar[] outputChars;
// write utf32
void write(dstring chars) {
if (!chars.length && !outputChars.length)
return;
outputChars.assumeSafeAppend;
outputChars ~= chars;
if (!_content.width)
return;
uint i = 0;
for (; i < outputChars.length; i++) {
bool unfinished = false;
dchar ch = outputChars[i];
dchar ch2 = i + 1 < outputChars.length ? outputChars[i + 1] : 0;
dchar ch3 = i + 2 < outputChars.length ? outputChars[i + 2] : 0;
//dchar ch4 = i + 3 < outputChars.length ? outputChars[i + 3] : 0;
if (ch < ' ') {
// control character
if (ch == 27) {
if (ch2 == 0)
break; // unfinished ESC sequence
// ESC sequence
if (ch2 == '[') {
// ESC [
if (!ch3)
break; // unfinished
int param1 = -1;
int param2 = -1;
int[] extraParams;
int index = i + 2;
bool questionMark = false;
if (index < outputChars.length && outputChars[index] == '?') {
questionMark = true;
index++;
}
parseParam(outputChars, index, param1);
if (index < outputChars.length && outputChars[index] == ';') {
index++;
parseParam(outputChars, index, param2);
}
while (index < outputChars.length && outputChars[index] == ';') {
index++;
int n = -1;
parseParam(outputChars, index, n);
if (n >= 0)
extraParams ~= n;
}
if (index >= outputChars.length)
break; // unfinished sequence: not enough chars
int param1def1 = param1 >= 1 ? param1 : 1;
ch3 = outputChars[index];
i = index;
if (ch3 == 'm') {
// set attributes
_content.setAttributes([param1, param2]);
if (extraParams.length)
_content.setAttributes(extraParams);
}
// command is parsed completely, ch3 == command type char
// ESC[7h and ESC[7l -- enable/disable line wrap
if (param1 == '7' && (ch3 == 'h' || ch3 == 'l')) {
_content.lineWrap(ch3 == 'h');
continue;
}
if (ch3 == 'H' || ch3 == 'f') {
_content.moveCursorTo(param2, param1);
continue;
}
if (ch3 == 'A') { // cursor up
_content.moveCursorBy(0, -param1def1);
continue;
}
if (ch3 == 'B') { // cursor down
_content.moveCursorBy(0, param1def1);
continue;
}
if (ch3 == 'C') { // cursor forward
_content.moveCursorBy(param1def1, 0);
continue;
}
if (ch3 == 'D') { // cursor back
_content.moveCursorBy(-param1def1, 0);
continue;
}
if (ch3 == 'K' || ch3 == 'J') {
_content.eraseScreen(param1, ch3 == 'K');
continue;
}
} else switch(ch2) {
case 'c':
_content.resetTerminal();
i++;
break;
case '=': // Set alternate keypad mode
case '>': // Set numeric keypad mode
case 'N': // Set single shift 2
case 'O': // Set single shift 3
case 'H': // Set a tab at the current column
case '<': // Enter/exit ANSI mode (VT52)
i++;
// ignore
break;
case '(': // default font
case ')': // alternate font
i++;
i++;
// ignore
break;
default:
// unsupported
break;
}
if (unfinished)
break;
} else switch(ch) {
case '\a': // bell
case '\f': // form feed
case '\v': // vtab
case '\r': // cr
case '\n': // lf
case '\t': // tab
case '\b': // backspace
_content.putChar(ch);
break;
default:
break;
}
} else {
_content.putChar(ch);
}
}
if (i > 0) {
if (i == outputChars.length)
outputChars.length = 0;
else {
for (uint j = 0; j + i < outputChars.length; j++)
outputChars[j] = outputChars[j + i];
outputChars.length = outputChars.length - i;
}
}
_content.updateScrollBar(_verticalScrollBar);
}
/// override to handle focus changes
override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
if (focused)
_content.focused = true;
else {
_content.focused = false;
}
super.handleFocusChange(focused);
}
}
import core.thread;
interface TerminalInputHandler {
void onBytesReceived(char[] data);
}
class TerminalDevice : Thread {
Signal!TerminalInputHandler onBytesRead;
version (Windows) {
import win32.windows;
HANDLE hpipe;
} else {
int masterfd;
}
@property string deviceName() { return _name; }
private string _name;
private bool started;
private bool closed;
this() {
super(&threadProc);
}
~this() {
close();
}
void threadProc() {
started = true;
Log.d("TerminalDevice threadProc() enter");
version(Windows) {
while (!closed) {
Log.d("TerminalDevice -- Waiting for client");
if (ConnectNamedPipe(hpipe, null)) {
// accept client
Log.d("TerminalDevice client connected");
char[4096] buf;
for (;;) {
if (closed)
break;
DWORD bytesRead = 0;
// read data from client
if (ReadFile(hpipe, &buf, buf.length, &bytesRead, null)) {
if (closed)
break;
if (bytesRead && onBytesRead.assigned) {
onBytesRead(buf[0 .. bytesRead].dup);
}
} else {
break;
}
}
Log.d("TerminalDevice client disconnecting");
// disconnect client
DisconnectNamedPipe(hpipe);
}
}
} else {
// posix
}
Log.d("TerminalDevice threadProc() exit");
}
bool write(string msg) {
if (!msg.length)
return true;
if (closed || !started)
return false;
version (Windows) {
for (;;) {
DWORD bytesWritten = 0;
if (WriteFile(hpipe, cast(char*)msg.ptr, msg.length, &bytesWritten, null) != TRUE) {
return false;
}
if (bytesWritten < msg.length)
msg = msg[bytesWritten .. $];
else
break;
}
} else {
// linux/posix
}
return true;
}
void close() {
if (closed)
return;
closed = true;
if (!started)
return;
version (Windows) {
import std.string;
// ping terminal to handle closed flag
HANDLE h = CreateFileA(
_name.toStringz, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
null, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
null);
if (h != INVALID_HANDLE_VALUE) {
DWORD bytesWritten = 0;
WriteFile(h, "stop".ptr, 4, &bytesWritten, null);
CloseHandle(h);
}
if (hpipe && hpipe != INVALID_HANDLE_VALUE) {
CloseHandle(hpipe);
hpipe = null;
}
} else {
if (masterfd && masterfd != -1) {
import core.sys.posix.unistd: close_=close;
close_(masterfd);
masterfd = 0;
}
}
join(false);
_name = null;
}
bool create() {
import std.string;
version (Windows) {
import std.uuid;
_name = "\\\\.\\pipe\\dlangide-terminal-" ~ randomUUID().toString;
hpipe = CreateNamedPipeA(cast(const(char)*)_name.toStringz,
PIPE_ACCESS_DUPLEX,
cast(uint)PIPE_TYPE_BYTE,
1,
16384,
16384,
50,
null);
if (hpipe == INVALID_HANDLE_VALUE) {
Log.e("Failed to create named pipe for terminal, error=", GetLastError());
close();
return false;
}
} else {
const(char) * s = null;
{
import core.sys.posix.fcntl;
import core.sys.posix.stdio;
import core.sys.posix.stdlib;
import core.sys.posix.unistd;
masterfd = posix_openpt(O_RDWR | O_NOCTTY);
if (masterfd == -1) {
Log.e("posix_openpt failed - cannot open terminal");
close();
return false;
}
if (grantpt(masterfd) == -1 || unlockpt(masterfd) == -1) {
Log.e("grantpt / unlockpt failed - cannot open terminal");
close();
return false;
}
s = ptsname(masterfd);
if (!s) {
Log.e("ptsname failed - cannot open terminal");
close();
return false;
}
}
_name = fromStringz(s).dup;
}
Log.i("ptty device created: ", _name);
start();
return true;
}
}