284 lines
8.0 KiB
C
284 lines
8.0 KiB
C
/**
|
|
* sixdraw.c - draw lines on your terminal
|
|
*
|
|
* Requires sixel graphics and DEC locator support to run on your terminal.
|
|
* These features are not widely supported. To save yourself from trouble,
|
|
* use a latest version of XTerm or mlterm.
|
|
*
|
|
* 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 <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 SIXDRAW_DEFAULT_TIMEOUT_MILLIS
|
|
# define SIXDRAW_DEFAULT_TIMEOUT_MILLIS 500
|
|
#endif // !SIXDRAW_DEFAULT_TIMEOUT_MILLIS
|
|
|
|
struct sixdraw_ctx {
|
|
struct termios termios;
|
|
union ctlseqs_value buffer[10];
|
|
char const *prog_name;
|
|
struct ctlseqs_matcher *matcher;
|
|
struct ctlseqs_reader *reader;
|
|
bool has_termios;
|
|
bool show_cursor;
|
|
int in_fd;
|
|
int out_fd;
|
|
int timeout;
|
|
int rows;
|
|
int cols;
|
|
int ch_width;
|
|
int ch_height;
|
|
};
|
|
|
|
static inline void
|
|
sixdraw_print_error(struct sixdraw_ctx const *ctx, char const *msg)
|
|
{
|
|
dprintf(ctx->out_fd, "%s: [error] %s.\n", ctx->prog_name, msg);
|
|
}
|
|
|
|
static inline void
|
|
sixdraw_clear_signal(int signum)
|
|
{
|
|
sigset_t sigset;
|
|
sigemptyset(&sigset);
|
|
sigaddset(&sigset, signum);
|
|
sigwait(&sigset, &signum);
|
|
}
|
|
|
|
static bool
|
|
sixdraw_handle_signals(struct sixdraw_ctx *ctx)
|
|
{
|
|
sigset_t sigset;
|
|
sigpending(&sigset);
|
|
if (!sigismember(&sigset, SIGWINCH)) {
|
|
return false;
|
|
}
|
|
sixdraw_clear_signal(SIGWINCH);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
sixdraw_terminate(struct sixdraw_ctx *ctx)
|
|
{
|
|
ctlseqs_matcher_free(ctx->matcher);
|
|
ctlseqs_reader_free(ctx->reader);
|
|
|
|
// Restore original terminal modes
|
|
if (ctx->has_termios) {
|
|
tcsetattr(ctx->in_fd, TCSANOW, &ctx->termios);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
sixdraw_get_winsize(struct sixdraw_ctx *ctx)
|
|
{
|
|
struct winsize ws = { 0 };
|
|
if (ioctl(ctx->in_fd, TIOCGWINSZ, &ws) != 0) {
|
|
return false;
|
|
}
|
|
if (ws.ws_xpixel == 0 || ws.ws_ypixel == 0) {
|
|
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;
|
|
dprintf(ctx->out_fd, "%d %d \n%d %d %d %d\n", ws.ws_xpixel, ws.ws_ypixel, ctx->rows, ctx->cols, ctx->ch_width, ctx->ch_height);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
sixdraw_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 = SIXDRAW_DEFAULT_TIMEOUT_MILLIS,
|
|
};
|
|
|
|
// Process command line arguments
|
|
int opt;
|
|
while (-1 != (opt = getopt(argc, argv, "t:"))) {
|
|
switch (opt) {
|
|
case 't':
|
|
ctx->timeout = atoi(optarg);
|
|
break;
|
|
case '?':
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Initialize control sequence matcher
|
|
ctx->matcher = ctlseqs_matcher_init();
|
|
if (ctx->matcher == NULL) {
|
|
sixdraw_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) {
|
|
sixdraw_print_error(ctx, "failed to set matcher options");
|
|
return false;
|
|
}
|
|
|
|
// Initialize control sequence reader
|
|
ctx->reader = ctlseqs_reader_init();
|
|
if (ctx->reader == NULL) {
|
|
sixdraw_print_error(ctx, "failed to initialize reader");
|
|
return false;
|
|
}
|
|
struct ctlseqs_reader_options reader_options = {
|
|
.buffer = ctx->buffer,
|
|
.fd = ctx->in_fd,
|
|
.maxlen = 32,
|
|
};
|
|
if (ctlseqs_reader_config(ctx->reader, &reader_options) != CTLSEQS_OK) {
|
|
sixdraw_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
|
|
sixdraw_prepare(struct sixdraw_ctx *ctx)
|
|
{
|
|
// Check whether we're running on a terminal
|
|
if (!isatty(ctx->in_fd) || !isatty(ctx->out_fd)) {
|
|
sixdraw_print_error(ctx, "this program can only run in a terminal");
|
|
return false;
|
|
}
|
|
|
|
// Set terminal to noncanonical mode
|
|
struct termios termios;
|
|
if (tcgetattr(ctx->in_fd, &ctx->termios) != 0) {
|
|
sixdraw_print_error(ctx, "failed to get terminal attributes");
|
|
return false;
|
|
}
|
|
termios = ctx->termios;
|
|
termios.c_lflag &= ~(ICANON | ISIG | ECHO);
|
|
if (tcsetattr(ctx->in_fd, TCSANOW, &termios) != 0) {
|
|
sixdraw_print_error(ctx, "failed to get terminal attributes");
|
|
return false;
|
|
}
|
|
ctx->has_termios = true;
|
|
|
|
// Set STDIN flags to non-blocking
|
|
int flags = fcntl(ctx->in_fd, F_GETFL);
|
|
if (flags == -1) {
|
|
sixdraw_print_error(ctx, "failed to get file status flags");
|
|
return false;
|
|
}
|
|
if (fcntl(ctx->in_fd, F_SETFL, flags | O_NONBLOCK) == -1) {
|
|
sixdraw_print_error(ctx, "failed to set file status flags");
|
|
return false;
|
|
}
|
|
|
|
// Get initial terminal window size
|
|
if (!sixdraw_get_winsize(ctx)) {
|
|
sixdraw_print_error(ctx, "failed to get terminal window size");
|
|
return false;
|
|
}
|
|
return true;
|
|
|
|
// Check terminal support for sixel graphics and DEC locator
|
|
dprintf(ctx->out_fd, CTLSEQS_PRIMARY_DA());
|
|
ssize_t result = ctlseqs_read(ctx->reader, ctx->matcher, 500);
|
|
if (result != 0) {
|
|
sixdraw_print_error(ctx, "failed to get terminal attributes");
|
|
return false;
|
|
}
|
|
bool has_sixel = false;
|
|
bool has_dec_locator = false;
|
|
union ctlseqs_value *buffer = ctx->buffer;
|
|
for (size_t i = 1; i <= buffer[0].num; ++i) {
|
|
if (buffer[i].num == 4) {
|
|
has_sixel = true;
|
|
} else if (buffer[i].num == 29) {
|
|
has_dec_locator = true;
|
|
}
|
|
}
|
|
if (!has_sixel) {
|
|
sixdraw_print_error(ctx, "terminal does not support sixel graphics");
|
|
return false;
|
|
}
|
|
if (!has_dec_locator) {
|
|
sixdraw_print_error(ctx, "terminal does not support DEC locator mode");
|
|
return false;
|
|
}
|
|
|
|
// Get current cursor status
|
|
dprintf(ctx->out_fd, CTLSEQS_DECRQM("25"));
|
|
result = ctlseqs_read(ctx->reader, ctx->matcher, ctx->timeout);
|
|
if (result != 1) {
|
|
sixdraw_print_error(ctx, "failed to get cursor status");
|
|
return false;
|
|
}
|
|
if (buffer[0].num != 25 || (buffer[1].num != 1 && buffer[1].num != 2)) {
|
|
sixdraw_print_error(ctx, "cursor status not recognizable");
|
|
return false;
|
|
}
|
|
ctx->show_cursor = buffer[1].num == 1;
|
|
|
|
return true;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
int status = 0;
|
|
|
|
struct sixdraw_ctx ctx;
|
|
if (!sixdraw_init(&ctx, argc, argv) || !sixdraw_prepare(&ctx)) {
|
|
status = -1;
|
|
goto terminate;
|
|
}
|
|
|
|
terminate:
|
|
sixdraw_terminate(&ctx);
|
|
return status;
|
|
}
|