ctlseqs/examples/sixdraw.c

395 lines
11 KiB
C

/**
* sixdraw.c - draw lines on your terminal
*
* Requires sixel graphics and 1016 mouse mode to run on your terminal.
* These features are not widely supported. To save yourself from trouble,
* use a latest version of XTerm or mintty.
*
* Before 1016 mode was introduced in XTerm patch #359, it is also possible
* to report mouse position in pixels using DEC locator (which is also rarely
* implemented). However, it is not used in this example.
*
* Copyright (C) 2020 CismonX <admin@cismon.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif // HAVE_CONFIG_H
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <ctlseqs.h>
#ifndef DEFAULT_TIMEOUT_MILLIS
# define DEFAULT_TIMEOUT_MILLIS 500
#endif // !DEFAULT_TIMEOUT_MILLIS
#define DECRQM_SET 1
#define DECRQM_RST 2
#define DECTCEM 25u
#define SGR_MOUSE_PIXELMODE 1016u
#define ALT_SCRBUF 1049u
#define SIXEL_SEQ_HEAD CTLSEQS_DCS "0;1q\"1;1;"
struct sixdraw_ctx {
struct termios termios;
union ctlseqs_value result[64];
char const *prog_name;
struct ctlseqs_matcher *matcher;
struct ctlseqs_reader *reader;
bool has_termios;
bool show_cursor;
bool normal_scrbuf;
bool sgr_pixelmode;
int in_fd;
int out_fd;
int timeout;
unsigned rows;
unsigned cols;
unsigned ch_width;
unsigned ch_height;
unsigned line_color;
};
static inline void
print_error(struct sixdraw_ctx const *ctx, char const *format, ...)
{
char msg[1024];
va_list args;
va_start(args, format);
vsnprintf(msg, 1024, format, args);
va_end(args);
dprintf(ctx->out_fd, "%s: [error] %s.\n", ctx->prog_name, msg);
}
static inline void
clear_signal(int signum)
{
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, signum);
sigwait(&sigset, &signum);
}
static bool
handle_signals(struct sixdraw_ctx *ctx)
{
sigset_t sigset;
sigpending(&sigset);
if (!sigismember(&sigset, SIGWINCH)) {
return false;
}
clear_signal(SIGWINCH);
return true;
}
static void
terminate(struct sixdraw_ctx *ctx)
{
ctlseqs_matcher_free(ctx->matcher);
ctlseqs_reader_free(ctx->reader);
// Restore cursor status.
if (ctx->show_cursor) {
dprintf(ctx->out_fd, CTLSEQS_DECSET("%u"), DECTCEM);
}
// Restore normal screen buffer.
if (ctx->normal_scrbuf) {
dprintf(ctx->out_fd, CTLSEQS_DECRST("%u"), ALT_SCRBUF);
}
// Restore original terminal modes.
if (ctx->has_termios) {
tcsetattr(ctx->in_fd, TCSANOW, &ctx->termios);
}
if (!ctx->sgr_pixelmode) {
dprintf(ctx->out_fd, CTLSEQS_DECRST("%u"), SGR_MOUSE_PIXELMODE);
}
}
static bool
get_winsize(struct sixdraw_ctx *ctx)
{
struct winsize ws = { 0 };
if (ioctl(ctx->in_fd, TIOCGWINSZ, &ws) != 0) {
print_error(ctx, "failed to get terminal window size");
return false;
}
if (ws.ws_xpixel == 0 || ws.ws_ypixel == 0) {
print_error(ctx, "failed to get terminal window size (in pixels)");
return false;
}
ctx->rows = ws.ws_row;
ctx->cols = ws.ws_col;
ctx->ch_width = ws.ws_xpixel / ws.ws_col;
ctx->ch_height = ws.ws_ypixel / ws.ws_row;
return true;
}
static bool
decrqm(struct sixdraw_ctx *ctx, unsigned mode_id, char const *name)
{
ssize_t retval;
union ctlseqs_value *result = ctx->result;
dprintf(ctx->out_fd, CTLSEQS_DECRQM("%u"), mode_id);
do {
retval = ctlseqs_read(ctx->reader, ctx->matcher, ctx->timeout);
} while (retval == CTLSEQS_PARTIAL);
if (retval != 1) {
print_error(ctx, "failed to get %s status", name);
return false;
}
if (result[0].num != mode_id || (result[1].num != DECRQM_SET && result[1].num != DECRQM_RST)) {
print_error(ctx, "%s status not recognizable", name);
return false;
}
return true;
}
static inline void
get_sixel_color(unsigned rgb888, unsigned *red, unsigned *green, unsigned *blue)
{
*red = ((rgb888 >> 16) & 0xFF) * 100 / 255;
*green = ((rgb888 >> 8) & 0xFF) * 100 / 255;
*blue = ((rgb888 >> 0) & 0xFF) * 100 / 255;
}
static inline char *
get_sixel_buffer()
{
static char sixel_seq_buffer[2048] = SIXEL_SEQ_HEAD;
return sixel_seq_buffer;
}
static void
print_sixel_dot(struct sixdraw_ctx *ctx, unsigned x, unsigned y)
{
char *sixel_seq = get_sixel_buffer();
size_t offset = sizeof(SIXEL_SEQ_HEAD) - 1;
offset += sprintf(sixel_seq + offset, "%d;%d", ctx->ch_width, ctx->ch_height);
// Select color
unsigned red, green, blue;
get_sixel_color(ctx->line_color, &red, &green, &blue);
offset += sprintf(sixel_seq + offset, "#0;2;%d;%d;%d#0", red, green, blue);
// Move cursor
unsigned row = y / ctx->ch_height + 1;
unsigned col = x / ctx->ch_width + 1;
dprintf(ctx->out_fd, CTLSEQS_CUP("%d", "%d"), row, col);
// Print dot
row = y % ctx->ch_height;
col = x % ctx->ch_width;
offset += sprintf(sixel_seq + offset, "%.*s", row / 6, "----------------");
offset += sprintf(sixel_seq + offset, "!%u?%u", col, (1 << row % 6) + 0x3F);
write(ctx->out_fd, sixel_seq, offset);
}
static bool
init(struct sixdraw_ctx *ctx, int argc, char **argv)
{
*ctx = (struct sixdraw_ctx) {
.prog_name = argc > 0 ? argv[0] : "sixdraw",
.in_fd = STDIN_FILENO,
.out_fd = STDOUT_FILENO,
.timeout = DEFAULT_TIMEOUT_MILLIS,
.line_color = 0x00FF00
};
// Process command line arguments.
int opt;
while (-1 != (opt = getopt(argc, argv, "t:"))) {
switch (opt) {
case 't':
ctx->timeout = atoi(optarg);
break;
case 'c':
ctx->line_color = strtoul(optarg, NULL, 16);
break;
case '?':
default:
return false;
}
}
// Initialize control sequence matcher.
ctx->matcher = ctlseqs_matcher_init();
if (ctx->matcher == NULL) {
print_error(ctx, "failed to initialize matcher");
return false;
}
char const *patterns[] = {
CTLSEQS_RESP_PRIMARY_DA(CTLSEQS_PH_NUMS),
CTLSEQS_RESP_DECRQM(CTLSEQS_PH_NUM, CTLSEQS_PH_NUM)
};
struct ctlseqs_matcher_options matcher_options = {
.patterns = patterns,
.npatterns = sizeof(patterns) / sizeof(char const *),
};
if (ctlseqs_matcher_config(ctx->matcher, &matcher_options) != 0) {
print_error(ctx, "failed to set matcher options");
return false;
}
// Initialize control sequence reader.
ctx->reader = ctlseqs_reader_init();
if (ctx->reader == NULL) {
print_error(ctx, "failed to initialize reader");
return false;
}
struct ctlseqs_reader_options reader_options = {
.result = ctx->result,
.fd = ctx->in_fd,
.maxlen = 4096,
};
if (ctlseqs_reader_config(ctx->reader, &reader_options) != CTLSEQS_OK) {
print_error(ctx, "failed to set reader options");
return false;
}
// Block SIGWINCH.
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, SIGWINCH);
sigprocmask(SIG_BLOCK, &sigset, NULL);
signal(SIGWINCH, SIG_DFL);
return true;
}
static bool
prepare(struct sixdraw_ctx *ctx)
{
// Check whether we're running on a terminal.
if (!isatty(ctx->in_fd) || !isatty(ctx->out_fd)) {
print_error(ctx, "this program can only run in a terminal");
return false;
}
// Set terminal to noncanonical mode.
if (tcgetattr(ctx->in_fd, &ctx->termios) != 0) {
print_error(ctx, "failed to get terminal attributes");
return false;
}
struct termios termios = ctx->termios;
termios.c_cc[VMIN] = 0;
termios.c_cc[VTIME] = 0;
termios.c_lflag &= ~(ICANON | ISIG | ECHO);
if (tcsetattr(ctx->in_fd, TCSANOW, &termios) != 0) {
print_error(ctx, "failed to set terminal attributes");
return false;
}
ctx->has_termios = true;
// Set STDIN flags to nonblocking.
int flags = fcntl(ctx->in_fd, F_GETFL);
if (flags == -1) {
print_error(ctx, "failed to get file status flags");
return false;
}
if (fcntl(ctx->in_fd, F_SETFL, flags | O_NONBLOCK) == -1) {
print_error(ctx, "failed to set file status flags");
return false;
}
// Get initial terminal window size.
if (!get_winsize(ctx)) {
return false;
}
// Check terminal support for sixel graphics and DEC locator.
dprintf(ctx->out_fd, CTLSEQS_PRIMARY_DA());
ssize_t retval;
do {
retval = ctlseqs_read(ctx->reader, ctx->matcher, ctx->timeout);
} while (retval == CTLSEQS_PARTIAL);
if (retval != 0) {
print_error(ctx, "failed to get terminal features");
return false;
}
bool has_sixel = false;
union ctlseqs_value *result = ctx->result;
for (size_t i = 1; i <= result[0].len; ++i) {
if (result[i].num == 4) {
has_sixel = true;
}
}
if (!has_sixel) {
print_error(ctx, "terminal does not support sixel graphics");
return false;
}
// Switch into SGR mouse pixel mode.
if (!decrqm(ctx, SGR_MOUSE_PIXELMODE, "SGR mouse pixel mode")) {
return false;
}
ctx->sgr_pixelmode = result[1].num == DECRQM_SET;
if (!ctx->sgr_pixelmode) {
dprintf(ctx->out_fd, CTLSEQS_DECSET("%u"), SGR_MOUSE_PIXELMODE);
}
// Hide cursor.
if (!decrqm(ctx, DECTCEM, "cursor")) {
return false;
}
ctx->show_cursor = result[1].num == DECRQM_SET;
if (ctx->show_cursor) {
dprintf(ctx->out_fd, CTLSEQS_DECRST("%u"), DECTCEM);
}
// Switch to alternate screen buffer.
if (!decrqm(ctx, ALT_SCRBUF, "screen buffer")) {
return false;
}
ctx->normal_scrbuf = result[1].num == DECRQM_RST;
if (ctx->normal_scrbuf) {
dprintf(ctx->out_fd, CTLSEQS_DECSET("%u"), ALT_SCRBUF);
}
return true;
}
int
main(int argc, char **argv)
{
int status = 0;
struct sixdraw_ctx ctx;
if (!init(&ctx, argc, argv) || !prepare(&ctx)) {
status = -1;
goto terminate;
}
terminate:
terminate(&ctx);
return status;
}