#include "termdriver.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define strneq(a,b,n) (strncmp(a,b,n)==0)
struct XTermDriver {
TickitTermDriver driver;
int dcs_offset;
char dcs_buffer[16];
struct {
unsigned int altscreen:1;
unsigned int cursorvis:1;
unsigned int cursorblink:1;
unsigned int cursorshape:2;
unsigned int mouse:2;
unsigned int keypad:1;
} mode;
struct {
unsigned int cursorshape:1;
unsigned int slrm:1;
} cap;
struct {
unsigned int cursorvis:1;
unsigned int cursorblink:1;
unsigned int cursorshape:2;
unsigned int slrm:1;
} initialised;
};
static bool print(TickitTermDriver *ttd, const char *str, size_t len)
{
tickit_termdrv_write_str(ttd, str, len);
return true;
}
static bool goto_abs(TickitTermDriver *ttd, int line, int col)
{
if(line != -1 && col > 0)
tickit_termdrv_write_strf(ttd, "\e[%d;%dH", line+1, col+1);
else if(line != -1 && col == 0)
tickit_termdrv_write_strf(ttd, "\e[%dH", line+1);
else if(line != -1)
tickit_termdrv_write_strf(ttd, "\e[%dd", line+1);
else if(col > 0)
tickit_termdrv_write_strf(ttd, "\e[%dG", col+1);
else if(col != -1)
tickit_termdrv_write_str(ttd, "\e[G", 3);
return true;
}
static bool move_rel(TickitTermDriver *ttd, int downward, int rightward)
{
if(downward > 1)
tickit_termdrv_write_strf(ttd, "\e[%dB", downward);
else if(downward == 1)
tickit_termdrv_write_str(ttd, "\e[B", 3);
else if(downward == -1)
tickit_termdrv_write_str(ttd, "\e[A", 3);
else if(downward < -1)
tickit_termdrv_write_strf(ttd, "\e[%dA", -downward);
if(rightward > 1)
tickit_termdrv_write_strf(ttd, "\e[%dC", rightward);
else if(rightward == 1)
tickit_termdrv_write_str(ttd, "\e[C", 3);
else if(rightward == -1)
tickit_termdrv_write_str(ttd, "\e[D", 3);
else if(rightward < -1)
tickit_termdrv_write_strf(ttd, "\e[%dD", -rightward);
return true;
}
static bool scrollrect(TickitTermDriver *ttd, const TickitRect *rect, int downward, int rightward)
{
struct XTermDriver *xd = (struct XTermDriver *)ttd;
if(!downward && !rightward)
return true;
int term_cols;
tickit_term_get_size(ttd->tt, NULL, &term_cols);
int right = tickit_rect_right(rect);
/* Use DECSLRM only for 1 line of insert/delete, because any more and it's
* likely better to use the generic system below
*/
if(((xd->cap.slrm && rect->lines == 1) || (right == term_cols))
&& downward == 0) {
if(right < term_cols)
tickit_termdrv_write_strf(ttd, "\e[;%ds", right);
for(int line = rect->top; line < tickit_rect_bottom(rect); line++) {
goto_abs(ttd, line, rect->left);
if(rightward > 1)
tickit_termdrv_write_strf(ttd, "\e[%dP", rightward); /* DCH */
else if(rightward == 1)
tickit_termdrv_write_str(ttd, "\e[P", 3); /* DCH1 */
else if(rightward == -1)
tickit_termdrv_write_str(ttd, "\e[@", 3); /* ICH1 */
else if(rightward < -1)
tickit_termdrv_write_strf(ttd, "\e[%d@", -rightward); /* ICH */
}
if(right < term_cols)
tickit_termdrv_write_strf(ttd, "\e[s");
return true;
}
if(xd->cap.slrm ||
(rect->left == 0 && rect->cols == term_cols && rightward == 0)) {
tickit_termdrv_write_strf(ttd, "\e[%d;%dr", rect->top + 1, tickit_rect_bottom(rect));
if(rect->left > 0 || right < term_cols)
tickit_termdrv_write_strf(ttd, "\e[%d;%ds", rect->left + 1, right);
goto_abs(ttd, rect->top, rect->left);
if(downward > 1)
tickit_termdrv_write_strf(ttd, "\e[%dM", downward); /* DL */
else if(downward == 1)
tickit_termdrv_write_str(ttd, "\e[M", 3); /* DL1 */
else if(downward == -1)
tickit_termdrv_write_str(ttd, "\e[L", 3); /* IL1 */
else if(downward < -1)
tickit_termdrv_write_strf(ttd, "\e[%dL", -downward); /* IL */
if(rightward > 1)
tickit_termdrv_write_strf(ttd, "\e[%d'~", rightward); /* DECDC */
else if(rightward == 1)
tickit_termdrv_write_str(ttd, "\e['~", 4); /* DECDC1 */
else if(rightward == -1)
tickit_termdrv_write_str(ttd, "\e['}", 4); /* DECIC1 */
if(rightward < -1)
tickit_termdrv_write_strf(ttd, "\e[%d'}", -rightward); /* DECIC */
tickit_termdrv_write_str(ttd, "\e[r", 3);
if(rect->left > 0 || right < term_cols)
tickit_termdrv_write_str(ttd, "\e[s", 3);
return true;
}
return false;
}
static bool erasech(TickitTermDriver *ttd, int count, TickitMaybeBool moveend)
{
if(count < 1)
return true;
/* Only use ECH if we're not in reverse-video mode. xterm doesn't do rv+ECH
* properly
*/
if(!tickit_pen_get_bool_attr(tickit_termdrv_current_pen(ttd), TICKIT_PEN_REVERSE)) {
if(count == 1)
tickit_termdrv_write_str(ttd, "\e[X", 3);
else
tickit_termdrv_write_strf(ttd, "\e[%dX", count);
if(moveend == TICKIT_YES)
move_rel(ttd, 0, count);
}
else {
/* TODO: consider tickit_termdrv_write_chrfill(ttd, c, n)
*/
char *spaces = tickit_termdrv_get_tmpbuffer(ttd, 64);
memset(spaces, ' ', 64);
while(count > 64) {
tickit_termdrv_write_str(ttd, spaces, 64);
count -= 64;
}
tickit_termdrv_write_str(ttd, spaces, count);
if(moveend == TICKIT_NO)
move_rel(ttd, 0, -count);
}
return true;
}
static bool clear(TickitTermDriver *ttd)
{
tickit_termdrv_write_strf(ttd, "\e[2J", 4);
return true;
}
static struct SgrOnOff { int on, off; } sgr_onoff[] = {
{ 30, 39 }, /* fg */
{ 40, 49 }, /* bg */
{ 1, 22 }, /* bold */
{ 4, 24 }, /* under */
{ 3, 23 }, /* italic */
{ 7, 27 }, /* reverse */
{ 9, 29 }, /* strike */
{ 10, 10 }, /* altfont */
{ 5, 25 }, /* blink */
};
static bool chpen(TickitTermDriver *ttd, const TickitPen *delta, const TickitPen *final)
{
/* There can be at most 12 SGR parameters; 3 from each of 2 colours, and
* 6 single attributes
*/
int params[12];
int pindex = 0;
for(TickitPenAttr attr = 0; attr < TICKIT_N_PEN_ATTRS; attr++) {
if(!tickit_pen_has_attr(delta, attr))
continue;
struct SgrOnOff *onoff = &sgr_onoff[attr];
int val;
switch(attr) {
case TICKIT_PEN_FG:
case TICKIT_PEN_BG:
val = tickit_pen_get_colour_attr(delta, attr);
if(val < 0)
params[pindex++] = onoff->off;
else if(val < 8)
params[pindex++] = onoff->on + val;
else if(val < 16)
params[pindex++] = onoff->on+60 + val-8;
else {
params[pindex++] = (onoff->on+8) | 0x80000000;
params[pindex++] = 5 | 0x80000000;
params[pindex++] = val;
}
break;
case TICKIT_PEN_ALTFONT:
val = tickit_pen_get_int_attr(delta, attr);
if(val < 0 || val >= 10)
params[pindex++] = onoff->off;
else
params[pindex++] = onoff->on + val;
break;
case TICKIT_PEN_BOLD:
case TICKIT_PEN_UNDER:
case TICKIT_PEN_ITALIC:
case TICKIT_PEN_REVERSE:
case TICKIT_PEN_STRIKE:
case TICKIT_PEN_BLINK:
val = tickit_pen_get_bool_attr(delta, attr);
params[pindex++] = val ? onoff->on : onoff->off;
break;
case TICKIT_N_PEN_ATTRS:
break;
}
}
if(pindex == 0)
return true;
/* If we're going to clear all the attributes then empty SGR is neater */
if(!tickit_pen_is_nondefault(final))
pindex = 0;
/* Render params[] into a CSI string */
size_t len = 3; /* ESC [ ... m */
for(int i = 0; i < pindex; i++)
len += snprintf(NULL, 0, "%d", params[i]&0x7fffffff) + 1;
if(pindex > 0)
len--; /* Last one has no final separator */
char *buffer = tickit_termdrv_get_tmpbuffer(ttd, len + 1);
char *s = buffer;
s += sprintf(s, "\e[");
for(int i = 0; i < pindex-1; i++)
/* TODO: Work out what terminals support :s */
s += sprintf(s, "%d%c", params[i]&0x7fffffff, ';');
if(pindex > 0)
s += sprintf(s, "%d", params[pindex-1]&0x7fffffff);
sprintf(s, "m");
tickit_termdrv_write_str(ttd, buffer, len);
return true;
}
static bool getctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int *value)
{
struct XTermDriver *xd = (struct XTermDriver *)ttd;
switch(ctl) {
case TICKIT_TERMCTL_ALTSCREEN:
*value = xd->mode.altscreen;
return true;
case TICKIT_TERMCTL_CURSORVIS:
*value = xd->mode.cursorvis;
return true;
case TICKIT_TERMCTL_CURSORBLINK:
*value = xd->mode.cursorblink;
return true;
case TICKIT_TERMCTL_MOUSE:
*value = xd->mode.mouse;
return true;
case TICKIT_TERMCTL_CURSORSHAPE:
*value = xd->mode.cursorshape;
return true;
case TICKIT_TERMCTL_KEYPAD_APP:
*value = xd->mode.keypad;
return true;
case TICKIT_TERMCTL_COLORS:
*value = 256;
return true;
default:
return false;
}
}
static int mode_for_mouse(TickitTermMouseMode mode)
{
switch(mode) {
case TICKIT_TERM_MOUSEMODE_CLICK: return 1000;
case TICKIT_TERM_MOUSEMODE_DRAG: return 1002;
case TICKIT_TERM_MOUSEMODE_MOVE: return 1003;
case TICKIT_TERM_MOUSEMODE_OFF:
break;
}
return 0;
}
static bool setctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int value)
{
struct XTermDriver *xd = (struct XTermDriver *)ttd;
switch(ctl) {
case TICKIT_TERMCTL_ALTSCREEN:
if(!xd->mode.altscreen == !value)
return true;
tickit_termdrv_write_str(ttd, value ? "\e[?1049h" : "\e[?1049l", 0);
xd->mode.altscreen = !!value;
return true;
case TICKIT_TERMCTL_CURSORVIS:
if(!xd->mode.cursorvis == !value)
return true;
tickit_termdrv_write_str(ttd, value ? "\e[?25h" : "\e[?25l", 0);
xd->mode.cursorvis = !!value;
return true;
case TICKIT_TERMCTL_CURSORBLINK:
if(xd->initialised.cursorblink && !xd->mode.cursorblink == !value)
return true;
tickit_termdrv_write_str(ttd, value ? "\e[?12h" : "\e[?12l", 0);
xd->mode.cursorblink = !!value;
return true;
case TICKIT_TERMCTL_MOUSE:
if(xd->mode.mouse == value)
return true;
/* Modes 1000, 1002 and 1003 are mutually exclusive; enabling any one
* disables the other two
*/
if(!value)
tickit_termdrv_write_strf(ttd, "\e[?%dl\e[?1006l", mode_for_mouse(xd->mode.mouse));
else
tickit_termdrv_write_strf(ttd, "\e[?%dh\e[?1006h", mode_for_mouse(value));
xd->mode.mouse = value;
return true;
case TICKIT_TERMCTL_CURSORSHAPE:
if(xd->initialised.cursorshape && xd->mode.cursorshape == value)
return true;
if(xd->cap.cursorshape)
tickit_termdrv_write_strf(ttd, "\e[%d q", value * 2 + (xd->mode.cursorblink ? -1 : 0));
xd->mode.cursorshape = value;
return true;
case TICKIT_TERMCTL_KEYPAD_APP:
if(!xd->mode.keypad == !value)
return true;
tickit_termdrv_write_strf(ttd, value ? "\e=" : "\e>");
return true;
default:
return false;
}
}
static bool setctl_str(TickitTermDriver *ttd, TickitTermCtl ctl, const char *value)
{
switch(ctl) {
case TICKIT_TERMCTL_ICON_TEXT:
tickit_termdrv_write_strf(ttd, "\e]1;%s\e\\", value);
return true;
case TICKIT_TERMCTL_TITLE_TEXT:
tickit_termdrv_write_strf(ttd, "\e]2;%s\e\\", value);
return true;
case TICKIT_TERMCTL_ICONTITLE_TEXT:
tickit_termdrv_write_strf(ttd, "\e]0;%s\e\\", value);
return true;
default:
return false;
}
}
static void start(TickitTermDriver *ttd)
{
// Enable DECSLRM
tickit_termdrv_write_strf(ttd, "\e[?69h");
// Find out if DECSLRM is actually supported
tickit_termdrv_write_strf(ttd, "\e[?69$p");
// Also query the current cursor visibility, blink status, and shape
tickit_termdrv_write_strf(ttd, "\e[?25$p\e[?12$p\eP$q q\e\\");
/* Some terminals (e.g. xfce4-terminal) don't understand DECRQM and print
* the raw bytes directly as output, while still claiming to be TERM=xterm
* It doens't hurt at this point to clear the current line just in case.
*/
tickit_termdrv_write_strf(ttd, "\e[G\e[K");
}
static bool started(TickitTermDriver *ttd)
{
struct XTermDriver *xd = (struct XTermDriver *)ttd;
return xd->initialised.cursorvis &&
xd->initialised.cursorblink &&
xd->initialised.cursorshape &&
xd->initialised.slrm;
}
static void gotkey_modereport(struct XTermDriver *xd, int initial, int mode, int value)
{
if(initial == '?') // DEC mode
switch(mode) {
case 12: // Cursor blink
if(value == 1)
xd->mode.cursorblink = 1;
xd->initialised.cursorblink = 1;
break;
case 25: // DECTCEM == Cursor visibility
if(value == 1)
xd->mode.cursorvis = 1;
xd->initialised.cursorvis = 1;
break;
case 69: // DECVSSM
if(value == 1 || value == 2)
xd->cap.slrm = 1;
xd->initialised.slrm = 1;
break;
}
}
static void gotkey_decrqss(struct XTermDriver *xd, char status, char *args, size_t arglen)
{
if(strneq(args + arglen - 2, " q", 2)) { // DECSCUSR
int value;
if(status == '1' && sscanf(args, "%d", &value)) {
// value==1 or 2 => shape == 1, 3 or 4 => 2, etc..
int shape = (value+1) / 2;
xd->mode.cursorshape = shape;
xd->cap.cursorshape = 1;
}
xd->initialised.cursorshape = 1;
}
}
static int gotkey(TickitTermDriver *ttd, TermKey *tk, const TermKeyKey *key)
{
struct XTermDriver *xd = (struct XTermDriver *)ttd;
if(key->type == TERMKEY_TYPE_MODEREPORT) {
int initial, mode, value;
termkey_interpret_modereport(tk, key, &initial, &mode, &value);
gotkey_modereport(xd, initial, mode, value);
return 1;
}
// TODO: Long term we'll move libtermkey's code into terminal drivers and
// stop using it. Until then we'll have to have our own DCS parser
else if(key->type == TERMKEY_TYPE_UNICODE &&
key->modifiers == TERMKEY_KEYMOD_ALT &&
key->code.codepoint == 'P') {
xd->dcs_offset = 0;
return 1;
}
else if(xd->dcs_offset != -1 &&
key->type == TERMKEY_TYPE_UNICODE &&
key->modifiers == 0) {
if(xd->dcs_offset < sizeof xd->dcs_buffer)
xd->dcs_buffer[xd->dcs_offset++] = key->utf8[0]; // TODO: UTF-8 in DCS?
return 1;
}
else if(key->type == TERMKEY_TYPE_UNICODE &&
key->modifiers == TERMKEY_KEYMOD_ALT &&
key->code.codepoint == '\\') {
if(xd->dcs_offset == -1)
return 1;
size_t cmdlen = 1;
while(cmdlen < xd->dcs_offset &&
xd->dcs_buffer[cmdlen-1] < 0x40)
cmdlen++;
if(cmdlen == 3 && strneq(xd->dcs_buffer + 1, "$r", 2))
gotkey_decrqss(xd, xd->dcs_buffer[0], xd->dcs_buffer + cmdlen, xd->dcs_offset - cmdlen);
xd->dcs_offset = -1;
return 1;
}
return 0;
}
static void stop(TickitTermDriver *ttd)
{
struct XTermDriver *xd = (struct XTermDriver *)ttd;
if(xd->mode.mouse)
setctl_int(ttd, TICKIT_TERMCTL_MOUSE, TICKIT_TERM_MOUSEMODE_OFF);
if(!xd->mode.cursorvis)
setctl_int(ttd, TICKIT_TERMCTL_CURSORVIS, 1);
if(xd->mode.altscreen)
setctl_int(ttd, TICKIT_TERMCTL_ALTSCREEN, 0);
if(xd->mode.keypad)
setctl_int(ttd, TICKIT_TERMCTL_KEYPAD_APP, 0);
// Reset pen
tickit_termdrv_write_str(ttd, "\e[m", 3);
}
static void destroy(TickitTermDriver *ttd)
{
struct XTermDriver *xd = (struct XTermDriver *)ttd;
free(xd);
}
static TickitTermDriverVTable xterm_vtable = {
.destroy = destroy,
.start = start,
.started = started,
.stop = stop,
.print = print,
.goto_abs = goto_abs,
.move_rel = move_rel,
.scrollrect = scrollrect,
.erasech = erasech,
.clear = clear,
.chpen = chpen,
.getctl_int = getctl_int,
.setctl_int = setctl_int,
.setctl_str = setctl_str,
.gotkey = gotkey,
};
static TickitTermDriver *new(const char *termtype)
{
if(strncmp(termtype, "xterm", 5) != 0)
return NULL;
switch(termtype[5]) {
case 0: case '-':
break;
default:
return NULL;
}
struct XTermDriver *xd = malloc(sizeof(struct XTermDriver));
xd->driver.vtable = &xterm_vtable;
xd->dcs_offset = -1;
memset(&xd->mode, 0, sizeof xd->mode);
xd->mode.cursorvis = 1;
memset(&xd->cap, 0, sizeof xd->cap);
memset(&xd->initialised, 0, sizeof xd->initialised);
return (TickitTermDriver*)xd;
}
TickitTermDriverProbe tickit_termdrv_probe_xterm = {
.new = new,
};