LINUX.ORG.RU

ncurses. Текущее положение курсора.

 , ,


1

2

Решил поиграться с ncurses. Для начала решил написать программку, которая будет показывать текущее положение курсора мыши. И тут началось…

Для получения положения курсора настраивается мышь с помощью функции mousemask() с флагом REPORT_MOUSE_POSITION. Как оказалось, этого недостаточно. Поискал в интернетах, обнаружилось, что надо настроить терминал, чтобы он считывал положение мыши.

$ echo $TERM
xterm-256color

Выяснилось, что чтобы в xterm обнаружить движение, нужно передать следующую escape-последовательность "\033[?1003h\n".

Окей, программа работает, положение курсора указывается. Но после завершения программы, все движения мыши терминал выплёвывает в терминал. Чтобы отключить отслеживание курсора передаётся последовательность "\033[?1003l\n", добавил в конце программы. Всё супер. Но…

Программа может быть завершена с помощью сигналов, я добавил, чтобы обработчики сигналов также передавали последовательность для отключения мыши. Сделал. Всё супер. Но…

Также есть прекрасный сигнал SIGSTOP и на него никакой обработчик повесить нельзя. И всё, тупик. Если программу отправить в «фон», то у нас терминал весело плюётся данными о положении мыши.

Может кто подскажет как это грамотно сделать?

Код программы целиком:

#include <ncurses.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

enum { key_escape = 27 };

/*
 * Escape sequences for xterm mouse position detection.
 * TODO: find a better solution:
 */
const char report_mouse_start[]	= "\033[?1003h\n";
const char report_mouse_end[]	= "\033[?1003l\n";

static void at_exit()
{
	write(1, report_mouse_end, sizeof(report_mouse_end)-1);
	endwin();
}
static void sigexit_handler()
{
	at_exit();
	_exit(0);
}
static void set_signals()
{
	signal(SIGTERM, sigexit_handler);
	signal(SIGINT, sigexit_handler);
	signal(SIGQUIT, sigexit_handler);
}

static void init_curses()
{
	initscr();
	cbreak();
	keypad(stdscr, 1);
	noecho();
	curs_set(0);
	mousemask(REPORT_MOUSE_POSITION, NULL);
}

static void procmouse()
{
	MEVENT event;
	int rc;

	rc = getmouse(&event);
	if (rc == OK) {
		mvprintw(0, 0, "X:Y %03d:%03d", event.x, event.y);
	}
}

int main()
{
	int key;

	set_signals();
	atexit(at_exit);
	/* Init xterm mouse report: */
	write(1, report_mouse_start, sizeof(report_mouse_start)-1);
	init_curses();

	for (;;) {
		key = getch();
		if (key == KEY_MOUSE)
			procmouse();
		else if (key == key_escape)
			break;
	}

	return 0;
}



Последнее исправление: Jullyfish (всего исправлений: 2)

И тут началось…

… и монда ките бу малай, шундый тэмле булды бу эчпочмак, мин чэйни-чэйни, шулай тэмлэп-тэмлэп сот белэн эчэ-эчэ арып беттем, аннары кикереп усырып жибэрдем дэ йокларга яттым!

Bass ★★★★★
()
Последнее исправление: Bass (всего исправлений: 2)

Это касается всег программ, которые работают с мышкой. В некоторы дистрах shell может отправлять последовательность, чинящую терминал, но в остальном решением будет набрать в терминале reset после остановки или принудительного завершения

mittorn ★★★★★
()
Ответ на: комментарий от Jullyfish

https://man7.org/tlpi/code/online/dist/pgsjc/handling_SIGTSTP.c.html
здесь пример как обработать SIGTSTP и продолжить остановку процесса. Здесь ты можешь попытаться восстановить состояние терминала, а потом после SIGCONT установить его снова

mittorn ★★★★★
()
Последнее исправление: mittorn (всего исправлений: 1)

С sigstop, вижу, разобрались. Однако добавлю: если у тебя полноценная интерактивная прога, которая полностью контролирует свой экран, скорее всего переводит stdin в посимвольный noecho режим, да ещё и с мышью - то желательно заблочить все терминальные хоткеи (вообще, это делается через tcsetattr, но вероятно в ncurses, раз ты его используешь, должна быть встроенная к нему обёртка чтобы это всё сделать). То есть заблочишь и ctrl+z и ctrl+c и что там ещё и никакого SIGTSTP тебе не придёт, если только его вручную кто не отправит. Ну а вручную можно и SIGKILL отправить, терминал останется в испорченном виде неизбежно. Хотя можно на всякий случай всё-таки заглушку на SIGTSTP тоже поставить.

Почему именно блокировать: во время использования интерактивных прог пользователь склонен всякие ctrl+z рассматривать как хоткеи этой проги, которые придут к ней как есть и, если она знает их, как-то обработаются. Всякие же убирания в фон обычно получаются из-за случайных нажатий и весьма раздражают там, где блокировкой tty-хоткеев не озаботились.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

о есть заблочишь и ctrl+z и ctrl+c и что там ещё и никакого SIGTSTP тебе не придёт, если только его вручную кто не отправит.

В принципе, вариант. Вообще ncurses, позволяет всё это отключить функцией raw(), вместо cbreak().

Почему именно блокировать: во время использования интерактивных прог пользователь склонен всякие ctrl+z рассматривать как хоткеи этой проги, которые придут к ней как есть и, если она знает их, как-то обработаются. Всякие же убирания в фон обычно получаются из-за случайных нажатий и весьма раздражают там, где блокировкой tty-хоткеев не озаботились.

Сложно согласиться. У меня коллега тот же vim регулярно по ctrl+z прячет, делает что-то в терминали и обратно fg. Да и я когда консольными браузерами пользуюсь, тоже так делаю.

Jullyfish
() автор топика
Ответ на: комментарий от Jullyfish

Хм, не знал что кто-то шелловый job control для такого использует. По мне так он жутко неудобный и проще на другой терминал переключиться (в гуи - на другое окно, в консоли - alt-f2 итд). Ну раз так то не надо блокировать, придётся обрабатывать аккуратную постановку всего на паузу и назад.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)

В общем, всё пРоКлЯтО. Из man curs_initscr:

Curses implementations may provide for special handling of  the  SIGINT,  SIGQUIT,
and  SIGTSTP  signals  if  their  disposition  is  SIG_DFL  at the time initscr is
called...

Так что, если я добавляю обработчик SIGTSTP, ломается обработчик ncurses, а доступа к обработчику сигнала самого ncurses через API нет, чтобы я его сразу вызвал после своего обработчика сигнала.

Jullyfish
() автор топика
Ответ на: комментарий от Jullyfish

А ещё я обнаружил страшную вещь: у меня все handler’ы без int аргумента. Удивительно, что компилятор не ругнулся.

Язык Си говорит, что если функция имеет аид int foo(), то это означает, что она может принимать любые аргументы любых типов, а не что она не должна принимать никаких аргументов вообще.

akk ★★★★★
()
	write(1, report_mouse_end, sizeof(report_mouse_end)-1);

Один вызов write() может записать не всё, что хотелось, даже если ошибки ввода-вывода не случилось. Нужно дописать ещё в цикле.

static void at_exit()
{
	write(1, report_mouse_end, sizeof(report_mouse_end)-1);
	endwin();
}
static void sigexit_handler()
{
	at_exit();
	_exit(0);
}

Цитирую https://man.freebsd.org/cgi/man.cgi?query=endwin&sektion=3x&manpath=F...

Signal Handlers

Quoting from X/Open Curses Issue 7, section 3.1.1:

Curses implementations may provide for special handling of the SIGINT, SIGQUIT, and SIGTSTP signals if their disposition is SIG_DFL at the time initscr is called...

Any special handling for these signals may remain in effect for the life of the process or until the process changes the disposi- tion of the signal.

None of the Curses functions are required to be safe with respect to signals...

shdown
()
#include <ncurses.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

enum { key_escape = 27 };

const char report_mouse_start[]	= "\033[?1003h"; // Убрал \n, чтобы не дергало экран
const char report_mouse_end[]	= "\033[?1003l";

static void at_exit()
{
	write(1, report_mouse_end, sizeof(report_mouse_end)-1);
	endwin();
}

static void sigexit_handler(int sig)
{
	(void)sig;
	at_exit();
	_exit(0);
}

// Обработчик для Ctrl+Z
static void sigtstp_handler(int sig)
{
	// 1. Выключаем мышь и curses перед уходом в фон
	at_exit();

	// 2. Сбрасываем обработчик на дефолтный и "засыпаем"
	signal(SIGTSTP, SIG_DFL);
	raise(SIGTSTP);

	// --- Здесь программа стоит на паузе до команды 'fg' ---

	// 3. Когда вернулись (fg), восстанавливаем всё обратно
	signal(SIGTSTP, sigtstp_handler);
	write(1, report_mouse_start, sizeof(report_mouse_start)-1);
	refresh(); // ncurses восстановит экран
}

static void set_signals()
{
	signal(SIGTERM, sigexit_handler);
	signal(SIGINT, sigexit_handler);
	signal(SIGQUIT, sigexit_handler);
	signal(SIGTSTP, sigtstp_handler); // Перехватываем остановку
}

static void init_curses()
{
	initscr();
	cbreak();
	keypad(stdscr, 1);
	noecho();
	curs_set(0);
	mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
}

static void procmouse()
{
	MEVENT event;
	if (getmouse(&event) == OK) {
		mvprintw(0, 0, "X:Y %03d:%03d", event.x, event.y);
		clrtoeol();
		refresh();
	}
}

int main()
{
	int key;

	set_signals();
	atexit(at_exit);
	
	write(1, report_mouse_start, sizeof(report_mouse_start)-1);
	init_curses();

	while ((key = getch()) != key_escape) {
		if (key == KEY_MOUSE)
			procmouse();
	}

	return 0;
}

ИИ говорит что так всё должно работать, я проверить не могу, так как нет под рукой Linux…

anonymous
()
Ответ на: комментарий от lwo

Попробовал sigaction, то что надо:

struct sigaction curs_sigtstp;

static void sigtstp_handler(int sig)
{
	sigaction(sig, &curs_sigtstp, NULL);
	write(1, report_mouse_end, sizeof(report_mouse_end)-1);
	raise(sig);
	signal(SIGTSTP, sigtstp_handler);
	write(1, report_mouse_start, sizeof(report_mouse_start)-1);
}

/* Must be after init ncurses. */
static void set_signals()
{
	struct sigaction act = {0};

	act.sa_handler = sigtstp_handler;
	sigaction(SIGTSTP, &act, &curs_sigtstp);
}

Или так:

static void set_signals()
{
	sigaction(SIGTSTP, NULL, &curs_sigtstp);
	signal(SIGTSTP, sigtstp_handler);
}

Был какой-то баг с первым вариантом set_signal, но пока описывал прикол и повторно попробовал его воспроизвести – он исчез.

Соответственно, если делаем fg, всё чётко, если bg процесс продолжает быть в состоянии Stopped. У vim, например, такое же поведение.

А вот если сделать kill %N, то процесс продолжает отображаться в jobs как Stopped, то пока не сделать fg процесс висит. Опять же, у vim такое же поведение. Так что, мб, можно считать его удовлетворительным.

Jullyfish
() автор топика
static void sigtstp_handler(int sig)
{
	struct sigaction act = {0};

	/* Ставим обработчик curses и сохраняем текущий: */
	sigaction(sig, &curs_sigtstp, &act);
	/* Выключаем мышь: */
	write(1, report_mouse_end, sizeof(report_mouse_end)-1);
	/* Вызываем обработчик сигнала curses: */
	raise(sig);
	/* Ставим обратно наш обработчик: */
	sigaction(sig, &act, NULL);
	/* Включаем мышь: */
	write(1, report_mouse_start, sizeof(report_mouse_start)-1);
}

Вот в таком случае получается фигня, хотя кажется, что всё чётенько сделано.

Jullyfish
() автор топика