Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 97 additions & 151 deletions source/dcell/parser.d
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Parser module for dcell contains the code for parsing terminfo escapes
* as they arrive on /dev/tty.
*
* Copyright: Copyright 2022 Garrett D'Amore
* Copyright: Copyright 2025 Garrett D'Amore
* Authors: Garrett D'Amore
* License:
* Distributed under the Boost Software License, Version 1.0.
Expand Down Expand Up @@ -354,7 +354,7 @@ class Parser
partial = false;

// we have to parse the paste bit first
if (parsePaste() || parseRune() || parseFnKey() || parseSgrMouse() || parseLegacyMouse())
if (parsePaste() || parseRune() || parseFnKey() || parseSgrMouse())
{
keyStart = now;
continue;
Expand Down Expand Up @@ -732,155 +732,101 @@ private:
return false;
}

bool parseLegacyMouse()
unittest
{
int x, y, btn;

enum State
{
start,
bracket,
end
}

State state;

for (int i = 0; i < buf.length; i++)
{
final switch (state)
{
case State.start:
switch (buf[i])
{
case '\x1b':
state = State.bracket;
break;
case '\x9b':
state = State.end;
break;
default:
return false;
}
break;
case State.bracket:
if (buf[i] != '[')
return false;
state++;
break;
case State.end:
if (buf[i] != 'M')
return false;
if (buf.length < i + 4)
break;
buf = buf[i + 1 .. $];
btn = int(buf[0]);
x = int(buf[1]) - 32 - 1;
y = int(buf[2]) - 32 - 1;
buf = buf[3 .. $];
evs ~= newMouseEvent(x, y, btn);
return true;
}
}
if (state > 0)
partial = true;
return false;
import core.thread;
import dcell.database;

// taken from xterm, but pared down
static immutable Termcap term = {
name: "test-term",
enterKeypad: "\x1b[?1h\x1b=",
exitKeypad: "\x1b[?1l\x1b>",
cursorBack1: "\x08",
cursorUp1: "\x1b[A",
keyBackspace: "\x08",
keyF1: "\x1bOP",
keyF2: "\x1bOQ",
keyF3: "\x1bOR",
keyInsert: "\x1b[2~",
keyDelete: "\x1b[3~",
keyHome: "\x1bOH",
keyEnd: "\x1bOF",
keyPgUp: "\x1b[5~",
keyPgDn: "\x1b[6~",
keyUp: "\x1bOA",
keyDown: "\x1bOB",
keyLeft: "\x1bOD",
keyRight: "\x1bOC",
keyBacktab: "\x1b[Z",
keyShfRight: "\x1b[1;2C",
mouse: "\x1b[M",
};
Database.put(&term);
auto tc = Database.get("test-term");
assert(tc !is null);
Parser p = new Parser(ParseKeys(tc));
assert(p.empty());
assert(p.parse("")); // no data, is fine
assert(p.parse("\x1bOC"));
auto ev = p.events();

assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.right);

// this tests that the timed pase parsing works -
// escape sequences are kept partially until we
// have a match or we have waited long enough.
assert(p.parse(['\x1b', 'O']) == false);
ev = p.events();
assert(ev.length == 0);
Thread.sleep(msecs(100));
assert(p.parse([]) == true);
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.rune);
assert(ev[0].key.mod == Modifiers.alt);

// lone escape
assert(p.parse(['\x1b']) == false);
ev = p.events();
assert(ev.length == 0);
Thread.sleep(msecs(100));
assert(p.parse([]) == true);
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.esc);
assert(ev[0].key.mod == Modifiers.none);

// try injecting paste events
assert(tc.enablePaste != "");
assert(p.parse(['\x1b', '[', '2', '0', '0', '~']));
assert(p.parse(['A']));
assert(p.parse(['\x1b', '[', '2', '0', '1', '~']));

ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.paste);
assert(ev[0].paste.content == "A");

// mouse events
assert(p.parse(['\x1b', '[', '<', '3', ';', '2', ';', '3', 'M']));
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.mouse);
assert(ev[0].mouse.pos.x == 1);
assert(ev[0].mouse.pos.y == 2);

// unicode
string b = [0xe2, 0x82, 0xac];
assert(p.parse(b));
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.rune);
assert(ev[0].key.ch == '€');
}
}

unittest
{
import core.thread;
import dcell.database;

// taken from xterm, but pared down
static immutable Termcap term = {
name: "test-term",
enterKeypad: "\x1b[?1h\x1b=",
exitKeypad: "\x1b[?1l\x1b>",
cursorBack1: "\x08",
cursorUp1: "\x1b[A",
keyBackspace: "\x08",
keyF1: "\x1bOP",
keyF2: "\x1bOQ",
keyF3: "\x1bOR",
keyInsert: "\x1b[2~",
keyDelete: "\x1b[3~",
keyHome: "\x1bOH",
keyEnd: "\x1bOF",
keyPgUp: "\x1b[5~",
keyPgDn: "\x1b[6~",
keyUp: "\x1bOA",
keyDown: "\x1bOB",
keyLeft: "\x1bOD",
keyRight: "\x1bOC",
keyBacktab: "\x1b[Z",
keyShfRight: "\x1b[1;2C",
mouse: "\x1b[M",
};
Database.put(&term);
auto tc = Database.get("test-term");
assert(tc !is null);
Parser p = new Parser(ParseKeys(tc));
assert(p.empty());
assert(p.parse("")); // no data, is fine
assert(p.parse("\x1bOC"));
auto ev = p.events();

assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.right);

// this tests that the timed pase parsing works -
// escape sequences are kept partially until we
// have a match or we have waited long enough.
assert(p.parse(['\x1b', 'O']) == false);
ev = p.events();
assert(ev.length == 0);
Thread.sleep(msecs(100));
assert(p.parse([]) == true);
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.rune);
assert(ev[0].key.mod == Modifiers.alt);

// lone escape
assert(p.parse(['\x1b']) == false);
ev = p.events();
assert(ev.length == 0);
Thread.sleep(msecs(100));
assert(p.parse([]) == true);
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.esc);
assert(ev[0].key.mod == Modifiers.none);

// try injecting paste events
assert(tc.enablePaste != "");
assert(p.parse(['\x1b', '[', '2', '0', '0', '~']));
assert(p.parse(['A']));
assert(p.parse(['\x1b', '[', '2', '0', '1', '~']));

ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.paste);
assert(ev[0].paste.content == "A");

// mouse events
assert(p.parse(['\x1b', '[', '<', '3', ';', '2', ';', '3', 'M']));
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.mouse);
assert(ev[0].mouse.pos.x == 1);
assert(ev[0].mouse.pos.y == 2);

// unicode
string b = [0xe2, 0x82, 0xac];
assert(p.parse(b));
ev = p.events();
assert(ev.length == 1);
assert(ev[0].type == EventType.key);
assert(ev[0].key.key == Key.rune);
assert(ev[0].key.ch == '€');
}
29 changes: 4 additions & 25 deletions source/dcell/termcap.d
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Termcap module for dcell, contains the structure used to define terminal capabilities.
*
* Copyright: Copyright 2022 Garrett D'Amore
* Copyright: Copyright 2025 Garrett D'Amore
* Authors: Garrett D'Amore
* License:
* Distributed under the Boost Software License, Version 1.0.
Expand All @@ -10,7 +10,6 @@
*/
module dcell.termcap;

import core.thread;
import std.conv;
import std.algorithm;
import std.functional;
Expand Down Expand Up @@ -221,9 +220,9 @@ struct Termcap
* to an interactive terminal like /dev/tty or stdin, while
* interpreting embedded delay sequences of the form
* $<DELAY> (where DELAY is given in milliseconds, and must
* be a positive rational number of milliseconds). When these
* are encountered, the flush delegate is called (if not null),
* and the function sleeps for the indicated amount of time.
* be a positive rational number of milliseconds). However,
* we no longer need to delay as no terminal actually needs
* this (sorry ancient physical vt100 terminals), so we drop it.
*/
static void puts(R)(R output, string s, void delegate() flush = null)
if (isOutputRange!(R, ubyte))
Expand Down Expand Up @@ -282,15 +281,6 @@ struct Termcap
}
val = val[1 .. $];
}

if (valid)
{
if (flush !is null)
{
flush();
}
Thread.sleep(usecs(usec * mult));
}
}
}

Expand All @@ -305,10 +295,6 @@ struct Termcap

puts(ob, "AB$<1000>C");
puts(ob, "DEF$<100.5>\n");
auto end = Clock.currTime();
assert(end > now);
assert(now + seconds(1) <= end);
assert(now + seconds(2) > end);

assert(ob.toString() == "ABCDEF\n");
// negative tests -- we don't care what's in the file (UB), but it must not panic
Expand All @@ -328,9 +314,6 @@ struct Termcap
puts(ob, "AB$<100>C");
puts(ob, "DEF$<100.5>\n");
auto end = Clock.currTime();
assert(end > now);
assert(now + msecs(200) <= end);
assert(now + msecs(300) > end);

assert(ob.toString() == "ABCDEF\n");
}
Expand All @@ -350,12 +333,8 @@ struct Termcap
}

auto f = new Flusher();
auto now = Clock.currTime();
puts(f, "ABC$<100>DEF", &f.flush);
auto end = Clock.currTime();
assert(end > now);
assert(now + msecs(100) <= end);
assert(f.flushes == 1);
assert(f.toString() == "ABCDEF");
}

Expand Down
Loading